Kaynağa Gözat

Merge remote-tracking branch 'origin/master' into feat/integrate-with-hackmd

# Conflicts:
#	resource/styles/scss/_on-edit.scss
Yuki Takei 7 yıl önce
ebeveyn
işleme
65452ad367

+ 1 - 0
CHANGES.md

@@ -8,6 +8,7 @@ CHANGES
 ## 3.1.14-RC
 
 * Improvement: Show help for header search box
+* Improvement: Add Markdown Cheatsheet to Editor component
 * Fix: Couldn't delete page completely from search result page
 * Fix: Tabs of trash page are broken
 

+ 209 - 12
resource/js/components/PageEditor/CodeMirrorEditor.js

@@ -1,6 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import Modal from 'react-bootstrap/es/Modal';
+
 import AbstractEditor from './AbstractEditor';
 
 import urljoin from 'url-join';
@@ -19,6 +21,7 @@ window.CodeMirror = require('codemirror');
 
 
 import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
+require('codemirror/addon/display/placeholder');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/matchtags');
 require('codemirror/addon/edit/closetag');
@@ -58,6 +61,9 @@ export default class CodeMirrorEditor extends AbstractEditor {
       isGfmMode: this.props.isGfmMode,
       isEnabledEmojiAutoComplete: false,
       isLoadingKeymap: false,
+      isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value.length === 0,
+      isCheatsheetModalButtonShown: this.props.isGfmMode && this.props.value.length > 0,
+      isCheatsheetModalShown: false,
       additionalClassSet: new Set(),
     };
 
@@ -77,8 +83,12 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.scrollCursorIntoViewHandler = this.scrollCursorIntoViewHandler.bind(this);
     this.pasteHandler = this.pasteHandler.bind(this);
     this.cursorHandler = this.cursorHandler.bind(this);
+    this.changeHandler = this.changeHandler.bind(this);
+
+    this.updateCheatsheetStates = this.updateCheatsheetStates.bind(this);
 
     this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
+    this.renderCheatsheetModalButton = this.renderCheatsheetModalButton.bind(this);
   }
 
   init() {
@@ -152,13 +162,13 @@ export default class CodeMirrorEditor extends AbstractEditor {
    */
   setGfmMode(bool) {
     // update state
-    const additionalClassSet = this.state.additionalClassSet;
     this.setState({
       isGfmMode: bool,
       isEnabledEmojiAutoComplete: bool,
-      additionalClassSet,
     });
 
+    this.updateCheatsheetStates(bool, null);
+
     // update CodeMirror option
     const mode = bool ? 'gfm' : undefined;
     this.getCodeMirror().setOption('mode', mode);
@@ -412,6 +422,19 @@ export default class CodeMirrorEditor extends AbstractEditor {
     }
   }
 
+  changeHandler(editor, data, value) {
+    if (this.props.onChange != null) {
+      this.props.onChange(value);
+    }
+
+    this.updateCheatsheetStates(null, value);
+
+    // Emoji AutoComplete
+    if (this.state.isEnabledEmojiAutoComplete) {
+      this.emojiAutoCompleteHelper.showHint(editor);
+    }
+  }
+
   /**
    * CodeMirror paste event handler
    * see: https://codemirror.net/doc/manual.html#events
@@ -431,7 +454,26 @@ export default class CodeMirrorEditor extends AbstractEditor {
     }
   }
 
+  /**
+   * update states which related to cheatsheet
+   * @param {boolean} isGfmMode (use state.isGfmMode if null is set)
+   * @param {string} value (get value from codemirror if null is set)
+   */
+  updateCheatsheetStates(isGfmMode, value) {
+    if (isGfmMode == null) {
+      isGfmMode = this.state.isGfmMode;
+    }
+    if (value == null) {
+      value = this.getCodeMirror().getDoc().getValue();
+    }
+    // update isSimpleCheatsheetShown, isCheatsheetModalButtonShown
+    const isSimpleCheatsheetShown = isGfmMode && value.length === 0;
+    const isCheatsheetModalButtonShown = isGfmMode && value.length > 0;
+    this.setState({ isSimpleCheatsheetShown, isCheatsheetModalButtonShown });
+  }
+
   renderLoadingKeymapOverlay() {
+    // centering
     const style = {
       top: 0,
       right: 0,
@@ -448,6 +490,161 @@ export default class CodeMirrorEditor extends AbstractEditor {
       : '';
   }
 
+  renderSimpleCheatsheet() {
+    return (
+      <div className="panel panel-default gfm-cheatsheet mb-0">
+        <div className="panel-heading"><i className="icon-fw icon-question"/>Markdown Help</div>
+        <div className="panel-body small p-b-0">
+          <div className="row">
+            <div className="col-xs-6">
+              <p>
+                # 見出し1<br />
+                ## 見出し2
+              </p>
+              <p><i>*斜体*</i>&nbsp;&nbsp;<b>**強調**</b></p>
+              <p>
+                [リンク](http://..)<br />
+                [/ページ名/子ページ名]
+              </p>
+              <p>
+                ```javascript:index.js<br />
+                writeCode();<br />
+                ```
+              </p>
+            </div>
+            <div className="col-xs-6">
+              <p>
+                - リスト 1<br />
+                &nbsp;&nbsp;&nbsp;&nbsp;- リスト 1_1<br />
+                - リスト 2<br />
+                1. 番号付きリスト 1
+                1. 番号付きリスト 2
+              </p>
+              <hr />
+              <p>行末にスペース2つ[ ][ ]<br />で改行</p>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderCheatsheetModalBody() {
+    return (
+      <div className="row small">
+        <div className="col-sm-6">
+          <h4>Header</h4>
+          <ul className="hljs">
+            <li><code># </code>見出し1</li>
+            <li><code>## </code>見出し2</li>
+            <li><code>### </code>見出し3</li>
+          </ul>
+          <h4>Block</h4>
+          <p className="mb-1"><code>[空白行]</code>を挟むことで段落になります</p>
+          <ul className="hljs">
+            <li>text</li>
+            <li></li>
+            <li>text</li>
+          </ul>
+          <h4>Line breaks</h4>
+          <p className="mb-1">段落中、<code>[space][space]</code>(スペース2つ) で改行されます</p>
+          <ul className="hljs">
+            <li>text<code> </code><code> </code></li>
+            <li>text</li>
+          </ul>
+          <h4>Typography</h4>
+          <ul className="hljs">
+            <li><i>*イタリック*</i></li>
+            <li><b>**ボールド**</b></li>
+            <li><i><b>***イタリックボールド***</b></i></li>
+            <li>~~取り消し線~~ => <s>striked text</s></li>
+          </ul>
+          <h4>Link</h4>
+          <ul className="hljs">
+            <li>[Google](https://www.google.co.jp/)</li>
+            <li>[/Page1/ChildPage1]</li>
+          </ul>
+          <h4>コードハイライト</h4>
+          <ul className="hljs">
+            <li>```javascript:index.js</li>
+            <li>writeCode();</li>
+            <li>```</li>
+          </ul>
+        </div>
+        <div className="col-sm-6">
+          <h4>リスト</h4>
+          <ul className="hljs">
+            <li>- リスト 1</li>
+            <li>&nbsp;&nbsp;- リスト 1_1</li>
+            <li>- リスト 2</li>
+          </ul>
+          <ul className="hljs">
+            <li>1. 番号付きリスト 1</li>
+            <li>1. 番号付きリスト 2</li>
+          </ul>
+          <ul className="hljs">
+            <li>- [ ] タスク(チェックなし)</li>
+            <li>- [x] タスク(チェック付き)</li>
+          </ul>
+          <h4>引用</h4>
+          <ul className="hljs">
+            <li>> 複数行の引用文を</li>
+            <li>> 書くことができます</li>
+          </ul>
+          <ul className="hljs">
+            <li>>> 多重引用</li>
+            <li>>>> 多重引用</li>
+            <li>>>>> 多重引用</li>
+          </ul>
+          <h4>Table</h4>
+          <ul className="hljs text-center">
+            <li>|&nbsp;&nbsp;&nbsp;左寄せ&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;中央寄せ&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;右寄せ&nbsp;&nbsp;&nbsp;|</li>
+            <li>|:-----------|:----------:|-----------:|</li>
+            <li>|column 1&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;column 2&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;column 3|</li>
+            <li>|column 1&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;column 2&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;column 3|</li>
+          </ul>
+          <h4>Images</h4>
+          <p className="mb-1"><code> ![Alt文字列](URL)</code> で<span className="text-info">&lt;img&gt;</span>タグを挿入できます</p>
+          <ul className="hljs">
+            <li>![ex](https://example.com/images/a.png)</li>
+          </ul>
+
+          <hr />
+          <a href="/Sandbox" className="btn btn-info btn-block" target="_blank">
+            <i className="icon-share-alt"/> Sandbox を開く
+          </a>
+        </div>
+      </div>
+    );
+  }
+
+  renderCheatsheetModalButton() {
+    const showCheatsheetModal = () => {
+      this.setState({isCheatsheetModalShown: true});
+    };
+
+    const hideCheatsheetModal = () => {
+      this.setState({isCheatsheetModalShown: false});
+    };
+
+    return (
+      <React.Fragment>
+        <Modal className="modal-gfm-cheatsheet" show={this.state.isCheatsheetModalShown} onHide={() => { hideCheatsheetModal() }}>
+          <Modal.Header closeButton>
+            <Modal.Title><i className="icon-fw icon-question"/>Markdown Help</Modal.Title>
+          </Modal.Header>
+          <Modal.Body className="pt-1">
+            { this.renderCheatsheetModalBody() }
+          </Modal.Body>
+        </Modal>
+
+        <a className="gfm-cheatsheet-modal-link text-muted small" onClick={() => { showCheatsheetModal() }}>
+          <i className="icon-question" /> Markdown
+        </a>
+      </React.Fragment>
+    );
+  }
+
   render() {
     const mode = this.state.isGfmMode ? 'gfm' : undefined;
     const defaultEditorOptions = {
@@ -457,6 +654,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
     const editorOptions = Object.assign(defaultEditorOptions, this.props.editorOptions || {});
 
+    const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plane Text..';
+
     return <React.Fragment>
       <ReactCodeMirror
         ref="cm"
@@ -478,6 +677,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
           lineWrapping: true,
           autoRefresh: {force: true},   // force option is enabled by autorefresh.ext.js -- Yuki Takei
           autoCloseTags: true,
+          placeholder: placeholder,
           matchBrackets: true,
           matchTags: {bothTags: true},
           // folding
@@ -506,16 +706,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
             this.props.onScroll(data);
           }
         }}
-        onChange={(editor, data, value) => {
-          if (this.props.onChange != null) {
-            this.props.onChange(value);
-          }
-
-          // Emoji AutoComplete
-          if (this.state.isEnabledEmojiAutoComplete) {
-            this.emojiAutoCompleteHelper.showHint(editor);
-          }
-        }}
+        onChange={this.changeHandler}
         onDragEnter={(editor, event) => {
           if (this.props.onDragEnter != null) {
             this.props.onDragEnter(event);
@@ -524,6 +715,12 @@ export default class CodeMirrorEditor extends AbstractEditor {
       />
 
       { this.renderLoadingKeymapOverlay() }
+
+      <div className="overlay overlay-gfm-cheatsheet mt-1 p-3 pt-3">
+        { this.state.isSimpleCheatsheetShown && this.renderSimpleCheatsheet() }
+        { this.state.isCheatsheetModalButtonShown && this.renderCheatsheetModalButton() }
+      </div>
+
     </React.Fragment>;
   }
 

+ 7 - 0
resource/styles/scss/_comment_growi.scss

@@ -93,7 +93,14 @@
     display: block;
   }
 
+  // display cheatsheet for comment form only
   .comment-form {
+    .editor-cheatsheet {
+        display: none;
+    }
+
+
+
     position: relative;
     margin-top: 2em;
     // user icon

+ 48 - 6
resource/styles/scss/_editor-overlay.scss

@@ -1,9 +1,8 @@
 @mixin overlay-processing-style($additionalSelector, $contentFontSize: inherit, $contentPadding: inherit) {
   .overlay.#{$additionalSelector} {
-    background: rgba(255,255,255,0.5);
-
+    background: rgba(255, 255, 255, 0.5);
     .overlay-content {
-      background: rgba(200,200,200,0.5);
+      background: rgba(200, 200, 200, 0.5);
       color: #444;
       font-size: $contentFontSize;
       padding: $contentPadding;
@@ -18,20 +17,63 @@
     display: flex;
     justify-content: center;
     align-items: center;
-
     position: absolute;
-    z-index: 7;  // forward than .CodeMirror-vscrollbar
+    z-index: 7; // forward than .CodeMirror-vscrollbar
     top: 0;
     right: 0;
     bottom: 0;
     left: 0;
-
     .overlay-content {
       padding: 0.5em;
+      right: 0;
+      bottom: 0;
     }
   }
 
   // loading keymap
   @include overlay-processing-style(overlay-loading-keymap, 2.5em, 0.3em);
 
+  // cheat sheat
+  .overlay.overlay-gfm-cheatsheet {
+    justify-content: flex-end;
+    align-items: flex-end;
+
+    pointer-events: none;
+
+    .panel.gfm-cheatsheet {
+      opacity: 0.6;
+      box-shadow: unset;
+      .panel-body {
+        color: $text-muted;
+        font-family: monospace;
+        min-width: 30em;
+      }
+      ul > li {
+        list-style: none;
+      }
+    }
+
+    a.gfm-cheatsheet-modal-link {
+      pointer-events: all;
+      cursor: pointer;
+
+      opacity: 0.6;
+      color: $text-muted;
+
+      &:hover, &:focus {
+        opacity: 1;
+      }
+    }
+
+    // hide on smartphone
+    @media (max-width: $screen-xs) {
+      display: none;
+    }
+  }
+}
+
+.modal-gfm-cheatsheet .modal-body {
+  .hljs {
+    font-family: monospace;
+  }
 }

+ 16 - 2
resource/styles/scss/_on-edit.scss

@@ -91,7 +91,7 @@ body.on-edit {
       #page-editor {
         // right(preview)
         &,
-        .row,
+        &>.row,
         .page-editor-preview-container,
         .page-editor-preview-body {
           min-height: calc(100vh - #{$header-plus-footer});   // for IE11
@@ -106,6 +106,10 @@ body.on-edit {
           .textarea-editor {
             height: calc(100vh - #{$editor-margin});
           }
+
+          @media (min-width: $screen-md) {
+            padding-right: 0;
+          }
         }
       }
 
@@ -185,7 +189,6 @@ body.on-edit {
     *****************/
     .page-editor-editor-container {
       border-right: 1px solid transparent;
-      padding-right: 0;
       // override CodeMirror styles
       .CodeMirror {
         .cm-matchhighlight {
@@ -247,6 +250,17 @@ body.on-edit {
       }
     }
 
+    #page-grant-selector {
+      .btn-group {
+        min-width: 150px;
+      }
+    }
+
+    #page-grant-selector {
+      .btn-group {
+        min-width: 150px;
+      }
+    }
   } // .builtin-editor .tab-pane#edit