From d4a0f7791a9b8f115fedc4571e0052ee11bee3f6 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Thu, 9 Nov 2017 22:44:10 +0000 Subject: [PATCH] Use ACE for Markdown syntax highlighting in editor --- ElectronClient/app/gui/MainScreen.jsx | 6 +- ElectronClient/app/gui/NoteText.jsx | 83 +++++++++++++++++++++++++-- ElectronClient/app/package-lock.json | 39 +++++++++++++ ElectronClient/app/package.json | 2 + 4 files changed, 122 insertions(+), 8 deletions(-) diff --git a/ElectronClient/app/gui/MainScreen.jsx b/ElectronClient/app/gui/MainScreen.jsx index 1c2f8cc84..1ea1efa4f 100644 --- a/ElectronClient/app/gui/MainScreen.jsx +++ b/ElectronClient/app/gui/MainScreen.jsx @@ -31,21 +31,21 @@ class MainScreenComponent extends React.Component { const rowHeight = style.height - theme.headerHeight; const sideBarStyle = { - width: layoutUtils.size(style.width * .2, 100, 300), + width: Math.floor(layoutUtils.size(style.width * .2, 100, 300)), height: rowHeight, display: 'inline-block', verticalAlign: 'top', }; const noteListStyle = { - width: layoutUtils.size(style.width * .2, 100, 300), + width: Math.floor(layoutUtils.size(style.width * .2, 100, 300)), height: rowHeight, display: 'inline-block', verticalAlign: 'top', }; const noteTextStyle = { - width: layoutUtils.size(style.width - sideBarStyle.width - noteListStyle.width, 0), + width: Math.floor(layoutUtils.size(style.width - sideBarStyle.width - noteListStyle.width, 0)), height: rowHeight, display: 'inline-block', verticalAlign: 'top', diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index 958e2234c..3b9b86f65 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -7,6 +7,9 @@ const MdToHtml = require('lib/MdToHtml'); const shared = require('lib/components/shared/note-screen-shared.js'); const { bridge } = require('electron').remote.require('./bridge'); const { themeStyle } = require('../theme.js'); +const AceEditor = require('react-ace').default; +require('brace/mode/markdown'); +require('brace/theme/chrome'); class NoteTextComponent extends React.Component { @@ -23,6 +26,7 @@ class NoteTextComponent extends React.Component { isLoading: true, webviewReady: false, scrollHeight: null, + editorScrollTop: 0, }; this.lastLoadedNoteId_ = null; @@ -30,6 +34,20 @@ class NoteTextComponent extends React.Component { this.webviewListeners_ = null; this.ignoreNextEditorScroll_ = false; this.scheduleSaveTimeout_ = null; + this.restoreScrollTop_ = null; + + // Complicated but reliable method to get editor content height + // https://github.com/ajaxorg/ace/issues/2046 + this.editorMaxScrollTop_ = 0; + this.onAfterEditorRender_ = () => { + const r = this.editor_.editor.renderer; + this.editorMaxScrollTop_ = Math.max(0, r.layerConfig.maxHeight - r.$size.scrollerHeight); + + if (this.restoreScrollTop_) { + this.editorSetScrollTop(this.restoreScrollTop_); + this.restoreScrollTop_ = null; + } + } } mdToHtml() { @@ -120,6 +138,12 @@ class NoteTextComponent extends React.Component { reg.logger().info('Got ipc-message: ' + msg, args); if (msg.indexOf('checkboxclick:') === 0) { + // Ugly hack because setting the body here will make the scrollbar + // go to some random position. So we save the scrollTop here and it + // will be restored after the editor ref has been reset, and the + // "afterRender" event has been called. + this.restoreScrollTop_ = this.editorScrollTop(); + const newBody = this.mdToHtml_.handleCheckboxClick(msg, this.state.note.body); this.saveOneProperty('body', newBody); } else if (msg.toLowerCase().indexOf('http') === 0) { @@ -143,11 +167,19 @@ class NoteTextComponent extends React.Component { } editorMaxScroll() { - return Math.max(0, this.editor_.scrollHeight - this.editor_.clientHeight); + return this.editorMaxScrollTop_; + } + + editorScrollTop() { + return this.editor_.editor.getSession().getScrollTop(); + } + + editorSetScrollTop(v) { + this.editor_.editor.getSession().setScrollTop(v); } setEditorPercentScroll(p) { - this.editor_.scrollTop = p * this.editorMaxScroll(); + this.editorSetScrollTop(p * this.editorMaxScroll()); } setViewerPercentScroll(p) { @@ -159,8 +191,9 @@ class NoteTextComponent extends React.Component { this.ignoreNextEditorScroll_ = false; return; } + const m = this.editorMaxScroll(); - this.setViewerPercentScroll(m ? this.editor_.scrollTop / m : 0); + this.setViewerPercentScroll(m ? this.editorScrollTop() / m : 0); } webview_domReady() { @@ -188,7 +221,17 @@ class NoteTextComponent extends React.Component { editor_ref(element) { if (this.editor_ === element) return; + + if (this.editor_) { + this.editorMaxScrollTop_ = 0; + this.editor_.editor.renderer.off('afterRender', this.onAfterEditorRender_); + } + this.editor_ = element; + + if (this.editor_) { + this.editor_.editor.renderer.on('afterRender', this.onAfterEditorRender_); + } } initWebview(wv) { @@ -220,6 +263,11 @@ class NoteTextComponent extends React.Component { this.webview_ = null; } + aceEditor_change(body) { + shared.noteComponent_change(this, 'body', body); + this.scheduleSave(); + } + render() { const style = this.props.style; const note = this.state.note; @@ -239,7 +287,7 @@ class NoteTextComponent extends React.Component { const editorStyle = { width: style.width - viewerStyle.width, height: style.height - paddingTop, - overflowY: 'scroll', + overflowY: 'hidden', float: 'left', verticalAlign: 'top', paddingTop: paddingTop + 'px', @@ -259,7 +307,32 @@ class NoteTextComponent extends React.Component { } const viewer = { this.webview_ref(elem); } } /> - const editor = + + const editorRootStyle = Object.assign({}, editorStyle); + delete editorRootStyle.width; + delete editorRootStyle.height; + delete editorRootStyle.fontSize; + + const editor = { this.editor_scroll(); }} + ref={(elem) => { this.editor_ref(elem); } } + onChange={(body) => { this.aceEditor_change(body) }} + + // Disable warning: "Automatically scrolling cursor into view after + // selection change this will be disabled in the next version set + // editor.$blockScrolling = Infinity to disable this message" + editorProps={{$blockScrolling: true}} + /> return (
diff --git a/ElectronClient/app/package-lock.json b/ElectronClient/app/package-lock.json index 9c0341fd4..bc0d885f9 100644 --- a/ElectronClient/app/package-lock.json +++ b/ElectronClient/app/package-lock.json @@ -780,6 +780,14 @@ } } }, + "brace": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/brace/-/brace-0.10.0.tgz", + "integrity": "sha1-7e9OubCSi6HuX3F//BV3SabdXXY=", + "requires": { + "w3c-blob": "0.0.1" + } + }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -2062,6 +2070,11 @@ "sntp": "2.1.0" } }, + "highlight.js": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", + "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=" + }, "hoek": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", @@ -2576,6 +2589,16 @@ "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.toarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", @@ -3471,6 +3494,17 @@ "prop-types": "15.6.0" } }, + "react-ace": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-5.5.0.tgz", + "integrity": "sha1-N9nCAR23IYTr8nq2/9inmBqAQUI=", + "requires": { + "brace": "0.10.0", + "lodash.get": "4.4.2", + "lodash.isequal": "4.5.0", + "prop-types": "15.6.0" + } + }, "react-dom": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.0.0.tgz", @@ -5055,6 +5089,11 @@ "extsprintf": "1.3.0" } }, + "w3c-blob": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/w3c-blob/-/w3c-blob-0.0.1.tgz", + "integrity": "sha1-sM01KhpQ9RVWNCD/1YYflQ8dhbg=" + }, "whatwg-fetch": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", diff --git a/ElectronClient/app/package.json b/ElectronClient/app/package.json index b9620718c..fd99774d3 100644 --- a/ElectronClient/app/package.json +++ b/ElectronClient/app/package.json @@ -28,6 +28,7 @@ "app-module-path": "^2.2.0", "electron-context-menu": "^0.9.1", "fs-extra": "^4.0.2", + "highlight.js": "^9.12.0", "html-entities": "^1.2.1", "lodash": "^4.17.4", "markdown-it": "^8.4.0", @@ -38,6 +39,7 @@ "promise": "^8.0.1", "query-string": "^5.0.1", "react": "^16.0.0", + "react-ace": "^5.5.0", "react-dom": "^16.0.0", "react-redux": "^5.0.6", "redux": "^3.7.2",