diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index 51deb545d2..d6038117ff 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -117,6 +117,13 @@ class NoteTextComponent extends React.Component { const note = noteId ? await Note.load(noteId) : null; if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading + // If the note hasn't been changed, exit now + if (this.state.note && note) { + let diff = Note.diffObjects(this.state.note, note); + delete diff.type_; + if (!Object.getOwnPropertyNames(diff).length) return; + } + // If we are loading nothing (noteId == null), make sure to // set webviewReady to false too because the webview component // is going to be removed in render(). @@ -128,8 +135,8 @@ class NoteTextComponent extends React.Component { // and then (in the renderer callback) to the value we actually need. The first // operation helps clear the scroll position cache. See: // https://github.com/ajaxorg/ace/issues/2195 - this.editorSetScrollTop(1); - this.restoreScrollTop_ = 0; + this.editorSetScrollTop(1); + this.restoreScrollTop_ = 0; this.setState({ note: note, diff --git a/ElectronClient/app/gui/note-viewer/index.html b/ElectronClient/app/gui/note-viewer/index.html index 2f1a5969f0..753a92ca72 100644 --- a/ElectronClient/app/gui/note-viewer/index.html +++ b/ElectronClient/app/gui/note-viewer/index.html @@ -71,6 +71,33 @@ function applyHljs(codeElements) { // / Handle dynamically loading HLJS when a code element is present // ---------------------------------------------------------------------- +// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because +// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while +// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed +// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore +// it at any time knowing that it's not going to be changed because the content height has changed. +// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during +// one second after the content has been updated. +// +// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result +// of programmatically changing scrollTop. We only want to respond to events initiated by the user. + +let percentScroll_ = 0; +let checkScrollIID_ = null; + +function setPercentScroll(percent) { + percentScroll_ = percent; + contentElement.scrollTop = percentScroll_ * maxScrollTop(); +} + +function percentScroll() { + return percentScroll_; +} + +function restorePercentScroll() { + setPercentScroll(percentScroll_); +} + ipcRenderer.on('setHtml', (event, html) => { contentElement.innerHTML = html; @@ -88,12 +115,37 @@ ipcRenderer.on('setHtml', (event, html) => { ul.style.listStyleType = 'none'; ul.style.paddingLeft = 0; } + + let previousContentHeight = contentElement.scrollHeight; + let startTime = Date.now(); + ignoreNextScrollEvent = true; + restorePercentScroll(); + + if (!checkScrollIID_) { + checkScrollIID_ = setInterval(() => { + const h = contentElement.scrollHeight; + if (h !== previousContentHeight) { + previousContentHeight = h; + ignoreNextScrollEvent = true; + restorePercentScroll(); + } + if (Date.now() - startTime >= 1000) { + clearInterval(checkScrollIID_); + checkScrollIID_ = null; + } + }, 1); + } }); -let ignoreNextScroll = false; +let ignoreNextScrollEvent = false; ipcRenderer.on('setPercentScroll', (event, percent) => { - ignoreNextScroll = true; - contentElement.scrollTop = percent * maxScrollTop(); + if (checkScrollIID_) { + clearInterval(checkScrollIID_); + checkScrollIID_ = null; + } + + ignoreNextScrollEvent = true; + setPercentScroll(percent); }); function maxScrollTop() { @@ -101,12 +153,14 @@ function maxScrollTop() { } contentElement.addEventListener('scroll', function(e) { - if (ignoreNextScroll) { - ignoreNextScroll = false; + if (ignoreNextScrollEvent) { + ignoreNextScrollEvent = false; return; } const m = maxScrollTop(); - ipcRenderer.sendToHost('percentScroll', m ? contentElement.scrollTop / m : 0); + const percent = m ? contentElement.scrollTop / m : 0; + setPercentScroll(percent); + ipcRenderer.sendToHost('percentScroll', percent); }); // Disable drag and drop otherwise it's possible to drop a URL