Yuki Takei 8 лет назад
Родитель
Сommit
cc5f4208eb

+ 19 - 20
resource/js/components/PageEditor.js

@@ -9,6 +9,7 @@ import GrowiRenderer from '../util/GrowiRenderer';
 import { EditorOptions, PreviewOptions } from './PageEditor/OptionsSelector';
 import { EditorOptions, PreviewOptions } from './PageEditor/OptionsSelector';
 import Editor from './PageEditor/Editor';
 import Editor from './PageEditor/Editor';
 import Preview from './PageEditor/Preview';
 import Preview from './PageEditor/Preview';
+import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
 
 
 export default class PageEditor extends React.Component {
 export default class PageEditor extends React.Component {
 
 
@@ -38,14 +39,14 @@ export default class PageEditor extends React.Component {
     this.onSave = this.onSave.bind(this);
     this.onSave = this.onSave.bind(this);
     this.onUpload = this.onUpload.bind(this);
     this.onUpload = this.onUpload.bind(this);
     this.onEditorScroll = this.onEditorScroll.bind(this);
     this.onEditorScroll = this.onEditorScroll.bind(this);
-    this.getMaxScrollTop = this.getMaxScrollTop.bind(this);
-    this.getScrollTop = this.getScrollTop.bind(this);
+    this.onEditorScrollCursorIntoView = this.onEditorScrollCursorIntoView.bind(this);
     this.saveDraft = this.saveDraft.bind(this);
     this.saveDraft = this.saveDraft.bind(this);
     this.clearDraft = this.clearDraft.bind(this);
     this.clearDraft = this.clearDraft.bind(this);
     this.pageSavedHandler = this.pageSavedHandler.bind(this);
     this.pageSavedHandler = this.pageSavedHandler.bind(this);
     this.apiErrorHandler = this.apiErrorHandler.bind(this);
     this.apiErrorHandler = this.apiErrorHandler.bind(this);
 
 
     // create throttled function
     // create throttled function
+    this.scrollPreviewByLineWithThrottle = throttle(30, this.scrollPreviewByLine);
     this.renderWithDebounce = debounce(50, throttle(100, this.renderPreview));
     this.renderWithDebounce = debounce(50, throttle(100, this.renderPreview));
     this.saveDraftWithDebounce = debounce(300, this.saveDraft);
     this.saveDraftWithDebounce = debounce(300, this.saveDraft);
   }
   }
@@ -181,30 +182,27 @@ export default class PageEditor extends React.Component {
   /**
   /**
    * the scroll event handler from codemirror
    * the scroll event handler from codemirror
    * @param {any} data {left, top, width, height, clientWidth, clientHeight} object that represents the current scroll position, the size of the scrollable area, and the size of the visible area (minus scrollbars).
    * @param {any} data {left, top, width, height, clientWidth, clientHeight} object that represents the current scroll position, the size of the scrollable area, and the size of the visible area (minus scrollbars).
-   *    see https://codemirror.net/doc/manual.html#events
+   *                    And data.line is also available that is added by Editor component
+   * @see https://codemirror.net/doc/manual.html#events
    */
    */
   onEditorScroll(data) {
   onEditorScroll(data) {
-    const rate = data.top / (data.height - data.clientHeight)
-    const top = this.getScrollTop(this.previewElement, rate);
+    console.log('onEditorScroll');
+    this.scrollPreviewByLineWithThrottle(data.line);
+  }
 
 
-    this.previewElement.scrollTop = top;
+  onEditorScrollCursorIntoView(line) {
+    console.log('onEditorScrollCursorIntoView');
+    this.scrollPreviewByLineWithThrottle(line);
   }
   }
+
   /**
   /**
-   * transplanted from crowi-form.js -- 2018.01.21 Yuki Takei
-   * @param {*} dom
-   */
-  getMaxScrollTop(dom) {
-    var rect = dom.getBoundingClientRect();
-    return dom.scrollHeight - rect.height;
-  };
-  /**
-   * transplanted from crowi-form.js -- 2018.01.21 Yuki Takei
-   * @param {*} dom
+   * scroll Preview by the specified line
+   * @param {number} line
    */
    */
-  getScrollTop(dom, rate) {
-    var maxScrollTop = this.getMaxScrollTop(dom);
-    var top = maxScrollTop * rate;
-    return top;
+  scrollPreviewByLine(line) {
+    if (this.previewElement != null) {
+      scrollSyncHelper.scrollToRevealSourceLine(this.previewElement, line);
+    }
   };
   };
 
 
   /*
   /*
@@ -305,6 +303,7 @@ export default class PageEditor extends React.Component {
               isUploadable={this.state.isUploadable}
               isUploadable={this.state.isUploadable}
               isUploadableFile={this.state.isUploadableFile}
               isUploadableFile={this.state.isUploadableFile}
               onScroll={this.onEditorScroll}
               onScroll={this.onEditorScroll}
+              onScrollCursorIntoView={this.onEditorScrollCursorIntoView}
               onChange={this.onMarkdownChanged}
               onChange={this.onMarkdownChanged}
               onSave={this.onSave}
               onSave={this.onSave}
               onUpload={this.onUpload}
               onUpload={this.onUpload}

+ 13 - 0
resource/js/components/PageEditor/Editor.js

@@ -57,6 +57,7 @@ export default class Editor extends React.Component {
     this.forceToFocus = this.forceToFocus.bind(this);
     this.forceToFocus = this.forceToFocus.bind(this);
     this.dispatchSave = this.dispatchSave.bind(this);
     this.dispatchSave = this.dispatchSave.bind(this);
 
 
+    this.onScrollCursorIntoView = this.onScrollCursorIntoView.bind(this);
     this.onPaste = this.onPaste.bind(this);
     this.onPaste = this.onPaste.bind(this);
 
 
     this.onDragEnterForCM = this.onDragEnterForCM.bind(this);
     this.onDragEnterForCM = this.onDragEnterForCM.bind(this);
@@ -143,6 +144,13 @@ export default class Editor extends React.Component {
     }
     }
   }
   }
 
 
+  onScrollCursorIntoView(editor, event) {
+    if (this.props.onScrollCursorIntoView != null) {
+      const line = editor.getCursor().line;
+      this.props.onScrollCursorIntoView(line);
+    }
+  }
+
   /**
   /**
    * CodeMirror paste event handler
    * CodeMirror paste event handler
    * see: https://codemirror.net/doc/manual.html#events
    * see: https://codemirror.net/doc/manual.html#events
@@ -296,6 +304,7 @@ export default class Editor extends React.Component {
             editorDidMount={(editor) => {
             editorDidMount={(editor) => {
               // add event handlers
               // add event handlers
               editor.on('paste', this.onPaste);
               editor.on('paste', this.onPaste);
+              editor.on('scrollCursorIntoView', this.onScrollCursorIntoView);
             }}
             }}
             value={this.state.value}
             value={this.state.value}
             options={{
             options={{
@@ -327,6 +336,9 @@ export default class Editor extends React.Component {
             }}
             }}
             onScroll={(editor, data) => {
             onScroll={(editor, data) => {
               if (this.props.onScroll != null) {
               if (this.props.onScroll != null) {
+                // add line data
+                const line = editor.lineAtHeight(data.top, 'local');
+                data.line = line;
                 this.props.onScroll(data);
                 this.props.onScroll(data);
               }
               }
             }}
             }}
@@ -363,6 +375,7 @@ Editor.propTypes = {
   isUploadableFile: PropTypes.bool,
   isUploadableFile: PropTypes.bool,
   onChange: PropTypes.func,
   onChange: PropTypes.func,
   onScroll: PropTypes.func,
   onScroll: PropTypes.func,
+  onScrollCursorIntoView: PropTypes.func,
   onSave: PropTypes.func,
   onSave: PropTypes.func,
   onUpload: PropTypes.func,
   onUpload: PropTypes.func,
 };
 };

+ 90 - 0
resource/js/components/PageEditor/ScrollSyncHelper.js

@@ -0,0 +1,90 @@
+/**
+ * This class is copied from Microsoft/vscode repository
+ * @see https://github.com/Microsoft/vscode/blob/0532a3429a18688a0c086a4212e7e5b4888b2a48/extensions/markdown/media/main.js
+ */
+class ScrollSyncHelper {
+
+  /**
+	 * @typedef {{ element: Element, line: number }} CodeLineElement
+	 */
+
+  constructor() {
+  }
+
+  getCodeLineElements(parentElement) {
+    /** @type {CodeLineElement[]} */
+    let elements;
+    if (!elements) {
+      elements = Array.prototype.map.call(
+        parentElement.getElementsByClassName('code-line'),
+        element => {
+          const line = +element.getAttribute('data-line');
+          return { element, line }
+        })
+        .filter(x => !isNaN(x.line));
+    }
+    return elements;
+  }
+
+  /**
+	 * Find the html elements that map to a specific target line in the editor.
+	 *
+	 * If an exact match, returns a single element. If the line is between elements,
+	 * returns the element prior to and the element after the given line.
+	 *
+   * @param {Element} parentElement
+	 * @param {number} targetLine
+	 *
+	 * @returns {{ previous: CodeLineElement, next?: CodeLineElement }}
+	 */
+	getElementsForSourceLine(parentElement, targetLine) {
+		const lines = this.getCodeLineElements(parentElement);
+		let previous = lines[0] || null;
+		for (const entry of lines) {
+			if (entry.line === targetLine) {
+				return { previous: entry, next: null };
+			} else if (entry.line > targetLine) {
+				return { previous, next: entry };
+			}
+			previous = entry;
+		}
+		return { previous };
+  }
+
+  getSourceRevealAddedOffset(element) {
+    // get paddingTop
+    const style = window.getComputedStyle(element, null);
+    const paddingTop = +(style.paddingTop.replace('px', ''));
+
+		return -(paddingTop + element.clientHeight * 1 / 10);
+  }
+
+  /**
+	 * Attempt to reveal the element for a source line in the editor.
+	 *
+   * @param {Element} element
+	 * @param {number} line
+	 */
+	scrollToRevealSourceLine(element, line) {
+		const { previous, next } = this.getElementsForSourceLine(element, line);
+		// marker.update(previous && previous.element);
+		if (previous) {
+			let scrollTo = 0;
+			if (next) {
+				// Between two elements. Go to percentage offset between them.
+				const betweenProgress = (line - previous.line) / (next.line - previous.line);
+				const elementOffset = next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top;
+				scrollTo = previous.element.getBoundingClientRect().top + betweenProgress * elementOffset;
+			} else {
+				scrollTo = previous.element.getBoundingClientRect().top;
+      }
+      element.scroll(0, element.scrollTop + scrollTo + this.getSourceRevealAddedOffset(element));
+		}
+  }
+
+}
+
+// singleton pattern
+const instance = new ScrollSyncHelper();
+Object.freeze(instance);
+export default instance;