Просмотр исходного кода

WIP: sync scroll to editor from preview

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

+ 30 - 7
resource/js/components/PageEditor.js

@@ -40,6 +40,7 @@ export default class PageEditor extends React.Component {
     this.onUpload = this.onUpload.bind(this);
     this.onEditorScroll = this.onEditorScroll.bind(this);
     this.onEditorScrollCursorIntoView = this.onEditorScrollCursorIntoView.bind(this);
+    this.onPreviewScroll = this.onPreviewScroll.bind(this);
     this.saveDraft = this.saveDraft.bind(this);
     this.clearDraft = this.clearDraft.bind(this);
     this.pageSavedHandler = this.pageSavedHandler.bind(this);
@@ -49,8 +50,9 @@ export default class PageEditor extends React.Component {
     this.lastScrolledDateWithCursor = null;
 
     // create throttled function
-    this.scrollPreviewByEditorScrollWithThrottle = throttle(20, this.scrollPreviewByEditorScroll);
+    this.scrollPreviewByEditorLineWithThrottle = throttle(20, this.scrollPreviewByEditorLine);
     this.scrollPreviewByCursorMovingWithThrottle = throttle(20, this.scrollPreviewByCursorMoving);
+    this.scrollEditorByPreviewScrollWithThrottle = throttle(50, this.scrollEditorByPreviewScroll);
     this.renderWithDebounce = debounce(50, throttle(100, this.renderPreview));
     this.saveDraftWithDebounce = debounce(800, this.saveDraft);
   }
@@ -72,7 +74,7 @@ export default class PageEditor extends React.Component {
    */
   setCaretLine(line) {
     this.refs.editor.setCaretLine(line);
-    this.scrollPreviewByLine(line);
+    this.scrollPreviewByEditorLine(line);
   }
 
   /**
@@ -198,7 +200,7 @@ export default class PageEditor extends React.Component {
       return;
     }
 
-    this.scrollPreviewByEditorScrollWithThrottle(data.line);
+    this.scrollPreviewByEditorLineWithThrottle(data.line);
   }
 
   onEditorScrollCursorIntoView(line) {
@@ -207,22 +209,42 @@ export default class PageEditor extends React.Component {
   }
 
   /**
-   * scroll Preview by the specified line
+   * scroll Preview element by scroll event
    * @param {number} line
    */
-  scrollPreviewByEditorScroll(line) {
+  scrollPreviewByEditorLine(line) {
     if (this.previewElement == null) {
       return;
     }
-    scrollSyncHelper.scrollToRevealSourceLine(this.previewElement, line);
+    scrollSyncHelper.scrollPreview(this.previewElement, line);
   };
+
+  /**
+   * scroll Preview element by cursor moving
+   * @param {number} line
+   */
   scrollPreviewByCursorMoving(line) {
     if (this.previewElement == null) {
       return;
     }
-    scrollSyncHelper.scrollToRevealOverflowingSourceLine(this.previewElement, line);
+    scrollSyncHelper.scrollPreviewToRevealOverflowing(this.previewElement, line);
   };
 
+  /**
+   *
+   * @param {*} event
+   */
+  onPreviewScroll(offset) {
+    this.scrollEditorByPreviewScrollWithThrottle(offset);
+  }
+
+  scrollEditorByPreviewScroll(offset) {
+    if (this.previewElement == null) {
+      return;
+    }
+    scrollSyncHelper.scrollEditor(this.refs.editor, this.previewElement, offset);
+  }
+
   /*
    * methods for draft
    */
@@ -332,6 +354,7 @@ export default class PageEditor extends React.Component {
               isMathJaxEnabled={this.state.isMathJaxEnabled}
               renderMathJaxOnInit={false}
               previewOptions={this.state.previewOptions}
+              onScroll={this.onPreviewScroll}
           />
         </div>
       </div>

+ 15 - 1
resource/js/components/PageEditor/Editor.js

@@ -54,6 +54,7 @@ export default class Editor extends React.Component {
 
     this.getCodeMirror = this.getCodeMirror.bind(this);
     this.setCaretLine = this.setCaretLine.bind(this);
+    this.setScrollTopByLine = this.setScrollTopByLine.bind(this);
     this.forceToFocus = this.forceToFocus.bind(this);
     this.dispatchSave = this.dispatchSave.bind(this);
 
@@ -97,16 +98,29 @@ export default class Editor extends React.Component {
    */
   setCaretLine(line) {
     const editor = this.getCodeMirror();
+    const linePosition = Math.max(0, line);
 
     // scroll to the bottom for a moment
     const lastLine = editor.getDoc().lastLine();
     editor.scrollIntoView(lastLine);
 
-    const linePosition = Math.max(0, line);
     editor.scrollIntoView(linePosition);
     editor.setCursor({line: linePosition});   // leave 'ch' field as null/undefined to indicate the end of line
   }
 
+  /**
+   * scroll
+   * @param {number} line
+   */
+  setScrollTopByLine(line) {
+    console.log(line);
+
+    const editor = this.getCodeMirror();
+    const scrollInfo = editor.getScrollInfo();
+
+    editor.scrollIntoView(line);
+  }
+
   /**
    * remove overlay and set isUploading to false
    */

+ 103 - 19
resource/js/components/PageEditor/ScrollSyncHelper.js

@@ -9,6 +9,8 @@ class ScrollSyncHelper {
 	 */
 
   constructor() {
+    this.isSyncScrollToPreviewFired = false;
+    this.isSyncScrollToEditorFired = false;
   }
 
   getCodeLineElements(parentElement) {
@@ -51,6 +53,58 @@ class ScrollSyncHelper {
 		return { previous };
   }
 
+  /**
+	 * Find the html elements that are at a specific pixel offset on the page.
+	 *
+   * @param {Element} parentElement
+	 * @param {number} offset
+   *
+	 * @returns {{ previous: CodeLineElement, next?: CodeLineElement }}
+	 */
+	getLineElementsAtPageOffset(parentElement, offset) {
+		const lines = this.getCodeLineElements(parentElement);
+
+    const position = offset - parentElement.scrollTop + this.getParentElementOffset(parentElement);
+
+		let lo = -1;
+		let hi = lines.length - 1;
+		while (lo + 1 < hi) {
+			const mid = Math.floor((lo + hi) / 2);
+      const bounds = lines[mid].element.getBoundingClientRect();
+			if (bounds.top + bounds.height >= position) {
+				hi = mid;
+			} else {
+				lo = mid;
+			}
+    }
+
+		const hiElement = lines[hi];
+		if (hi >= 1 && hiElement.element.getBoundingClientRect().top > position) {
+			const loElement = lines[lo];
+			const bounds = loElement.element.getBoundingClientRect();
+			const previous = { element: loElement.element, line: loElement.line + (position - bounds.top) / (bounds.height) };
+			const next = { element: hiElement.element, line: hiElement.line, fractional: 0 };
+			return { previous, next };
+		}
+
+		const bounds = hiElement.element.getBoundingClientRect();
+    const previous = { element: hiElement.element, line: hiElement.line + (position - bounds.top) / (bounds.height) };
+		return { previous };
+  }
+
+  getEditorLineNumberForPageOffset(parentElement, offset) {
+    const { previous, next } = this.getLineElementsAtPageOffset(parentElement, offset);
+		if (previous) {
+			if (next) {
+        const betweenProgress = (offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top) / (next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top);
+				return previous.line + betweenProgress * (next.line - previous.line);
+			} else {
+				return previous.line;
+			}
+		}
+		return null;
+  }
+
   getParentElementOffset(parentElement) {
     // get paddingTop
     const style = window.getComputedStyle(parentElement, null);
@@ -60,13 +114,13 @@ class ScrollSyncHelper {
   }
 
   /**
-	 * Attempt to reveal the element for a source line in the editor.
+	 * Attempt to scroll preview element for a source line in the editor.
 	 *
-   * @param {Element} element
+   * @param {Element} previewElement
 	 * @param {number} line
 	 */
-	scrollToRevealSourceLine(element, line) {
-		const { previous, next } = this.getElementsForSourceLine(element, line);
+	scrollPreview(previewElement, line) {
+		const { previous, next } = this.getElementsForSourceLine(previewElement, line);
 		// marker.update(previous && previous.element);
 		if (previous) {
 			let scrollTo = 0;
@@ -79,42 +133,72 @@ class ScrollSyncHelper {
 				scrollTo = previous.element.getBoundingClientRect().top;
       }
 
-      scrollTo -= this.getParentElementOffset(element);
+      scrollTo -= this.getParentElementOffset(previewElement);
+
+      // turn on the flag
+      this.isSyncScrollToPreviewFired = true;
 
-      element.scroll(0, element.scrollTop + scrollTo);
+      previewElement.scroll(0, previewElement.scrollTop + scrollTo);
 		}
   }
 
   /**
-	 * Attempt to reveal the element that is overflowing from parent element.
+	 * Attempt to reveal the element that is overflowing from previewElement.
 	 *
-   * @param {Element} element
+   * @param {Element} previewElement
 	 * @param {number} line
 	 */
-	scrollToRevealOverflowingSourceLine(element, line) {
-		const { previous, next } = this.getElementsForSourceLine(element, line);
+  scrollPreviewToRevealOverflowing(previewElement, line) {
+    // turn off the flag
+    if (this.isSyncScrollToEditorFired) {
+      this.isSyncScrollToEditorFired = false;
+      return;
+    }
+
+		const { previous, next } = this.getElementsForSourceLine(previewElement, line);
 		// marker.update(previous && previous.element);
 		if (previous) {
-      const parentElementOffset = this.getParentElementOffset(element);
+      const parentElementOffset = this.getParentElementOffset(previewElement);
       const prevElmTop = previous.element.getBoundingClientRect().top - parentElementOffset;
       const prevElmBottom = previous.element.getBoundingClientRect().bottom - parentElementOffset;
 
+      let scrollTo = null;
       if (prevElmTop < 0) {
-        // set the top of 'previous.element' to the top of 'element'
-        const scrollTo = element.scrollTop + prevElmTop;
-        element.scroll(0, scrollTo);
+        // set the top of 'previous.element' to the top of 'previewElement'
+        scrollTo = previewElement.scrollTop + prevElmTop;
       }
-      if (prevElmBottom > element.clientHeight) {
-        // set the bottom of 'previous.element' to the bottom of 'element'
-        const scrollTo = element.scrollTop + prevElmBottom - element.clientHeight + 20;
-        element.scroll(0, scrollTo);
+      else if (prevElmBottom > previewElement.clientHeight) {
+        // set the bottom of 'previous.element' to the bottom of 'previewElement'
+        scrollTo = previewElement.scrollTop + prevElmBottom - previewElement.clientHeight + 20;
       }
+
+      if (scrollTo == null) {
+        return;
+      }
+
+      // turn on the flag
+      this.isSyncScrollToPreviewFired = true;
+
+      previewElement.scroll(0, scrollTo);
 		}
   }
 
+  scrollEditor(editor, previewElement, offset) {
+    // turn off the flag
+    if (this.isSyncScrollToPreviewFired) {
+      this.isSyncScrollToPreviewFired = false;
+      return;
+    }
+
+    let line = this.getEditorLineNumberForPageOffset(previewElement, offset);
+    line = Math.floor(line);
+
+    // turn on flag
+    this.isSyncScrollToEditorFired = true;
+    editor.setScrollTopByLine(line);
+  }
 }
 
 // singleton pattern
 const instance = new ScrollSyncHelper();
-Object.freeze(instance);
 export default instance;