diff --git a/docs/dev/code/renderer/editor.md b/docs/dev/code/renderer/editor.md index 36743589..5c8265d9 100644 --- a/docs/dev/code/renderer/editor.md +++ b/docs/dev/code/renderer/editor.md @@ -82,6 +82,7 @@ interface IDocumentState index: number }, cursor: any, + firstViewportVisibleItem: any, wordCount: { paragraph: number, word: number, diff --git a/src/muya/lib/contentState/index.js b/src/muya/lib/contentState/index.js index 76731b66..f04e6f13 100644 --- a/src/muya/lib/contentState/index.js +++ b/src/muya/lib/contentState/index.js @@ -81,6 +81,7 @@ class ContentState { this._selectedImage = null this.dropAnchor = null this.prevCursor = null + this.firstViewportVisibleItem = null this.historyTimer = null this.history = new History(this) this.turndownConfig = Object.assign({}, DEFAULT_TURNDOWN_CONFIG, { bulletListMarker }) @@ -345,6 +346,43 @@ class ContentState { return this.cursor } + getBlockKeyByIndex (index) { + let arr = index.split('.') + const travel = blocks => { + let pos = arr.shift() + let num = parseInt(pos) + if (num !== pos || Number.isInteger(num) === false) { return null } + pos = num + if (blocks.length <= pos) { return null } + let block = blocks[pos] + if (arr.length === 0) { return block.key } else { return travel(block.children) } + } + return travel(this.blocks) + } + + getBlockIndex (key) { + if (!key) return null + let result = null + const travel = blocks => { + for (let index = 0; index < blocks.length; index++) { + const block = blocks[index] + if (block.key === key) { + result = `${index}` + return true + } + if (block.children.length) { + if (travel(block.children)) { + result = `${index}.${result}` + return true + } + } + } + return false + } + travel(this.blocks) + return result + } + getBlock (key) { if (!key) return null let result = null diff --git a/src/renderer/components/editorWithTabs/editor.vue b/src/renderer/components/editorWithTabs/editor.vue index 96701c28..c6ecc768 100644 --- a/src/renderer/components/editorWithTabs/editor.vue +++ b/src/renderer/components/editorWithTabs/editor.vue @@ -111,6 +111,7 @@ import '@/assets/themes/codemirror/one-dark.css' // import 'view-image/lib/imgViewer.css' import CloseIcon from '@/assets/icons/close.svg' +// Minus 67 here is the Y offset of the container itself, before we didn't include that in the scroll calculations, as we do now this keeps the actual Y offset the same const STANDAR_Y = 320 export default { @@ -501,8 +502,20 @@ export default { }, currentFile: function (value, oldValue) { + if (this.sourceCode === false && oldValue && oldValue !== value) { // Cannot use the changed event above as it happens after we have switched to the new file + let firstViewportVisibleItem = this.getFirstElementInViewport() + if (firstViewportVisibleItem) { oldValue.firstViewportVisibleItem = 'M' + this.editor.contentState.getBlockIndex(firstViewportVisibleItem.id) } else { oldValue.firstViewportVisibleItem = 'Z' }// undefining if already set + } + if (value && value !== oldValue) { - this.scrollToCursor(0) + if (this.sourceCode === false && value.firstViewportVisibleItem && value.firstViewportVisibleItem.startsWith('M')) { + let indexStr = value.firstViewportVisibleItem.substring(1) + this.$nextTick(() => { + let elemId = this.editor.contentState.getBlockKeyByIndex(indexStr) + if (!elemId) { return } + this.scrollToElement('#' + elemId, 15, true) + }) + } else { this.scrollToCursor(0) } // Hide float tools if needed. this.editor && this.editor.hideAllFloatTools() } @@ -690,7 +703,6 @@ export default { // // this.setImageViewerVisible(true) // }) - this.editor.on('selectionChange', changes => { const { y } = changes.cursorCoords if (this.typewriter) { @@ -1065,14 +1077,51 @@ export default { return this.scrollToElement(`#${slug}`) }, - scrollToElement (selector) { + getFirstElementInViewport () { + let node = this.editor.container + if (node.childNodes.length === 0) { return null } + let offsetY = node.scrollTop + node = node.childNodes[0]// this gets us to the editors primary div + if (offsetY === 0) { + if (node.childNodes.length === 0) { return null } + return node.childNodes[0] + } + + const nodeStack = [] + + while (node) { + // Only iterate over elements and text nodes + if (node.nodeType > 3) { + node = nodeStack.pop() + continue + } + if (node.offsetTop >= offsetY) { return node } + + if (node.nodeType === 1) { + // this is an element + // add all its children to the stack + let i = node.childNodes.length - 1 + while (i >= 0) { + nodeStack.push(node.childNodes[i]) + i -= 1 + } + } + + node = nodeStack.pop() + } + }, + + scrollToElement (selector, duration = 300, dontAddStandardHeadroom = false) { // Scroll to search highlight word const { container } = this.editor const anchor = document.querySelector(selector) if (anchor) { - const { y } = anchor.getBoundingClientRect() - const DURATION = 300 - animatedScrollTo(container, container.scrollTop + y - STANDAR_Y, DURATION) + const DURATION = duration + const anchorY = anchor.getBoundingClientRect().y + const containerY = container.getBoundingClientRect().y + + const add = dontAddStandardHeadroom ? 0 : STANDAR_Y + animatedScrollTo(container, container.scrollTop + anchorY - containerY - add, DURATION) } }, @@ -1229,7 +1278,7 @@ export default { if (cursor) { editor.setMarkdown(markdown, cursor, true) } else { - editor.setMarkdown(markdown) + editor.setMarkdown(markdown, null, true) } } }, diff --git a/src/renderer/components/editorWithTabs/sourceCode.vue b/src/renderer/components/editorWithTabs/sourceCode.vue index 6b8ebe8a..565e0243 100644 --- a/src/renderer/components/editorWithTabs/sourceCode.vue +++ b/src/renderer/components/editorWithTabs/sourceCode.vue @@ -10,10 +10,13 @@ import codeMirror, { setMode, setCursorAtLastLine, setTextDirection } from '../../codeMirror' import { wordCount as getWordCount } from 'muya/lib/utils' import { mapState } from 'vuex' -import { adjustCursor } from '../../util' +import { adjustCursor, animatedScrollTo } from '../../util' import bus from '../../bus' import { oneDarkThemes, railscastsThemes } from '@/config' +// Same as editor.vue +const STANDAR_Y = 320 + export default { props: { markdown: String, @@ -43,6 +46,18 @@ export default { }, watch: { + currentTab: function (value, oldValue) { + if (!this.sourceCode) { return } + // Don't need to worry about setting the line on the oldValue already did in prepareTabSwitch + if (value && value !== oldValue) { + if (value.firstViewportVisibleItem && value.firstViewportVisibleItem.startsWith('S')) { + let line = value.firstViewportVisibleItem.substring(1) + this.$nextTick(() => { + this.scrollToLineNumberInViewport(line, 15, true) + }) + } + } + }, textDirection: function (value, oldValue) { const { editor } = this if (value !== oldValue && editor) { @@ -237,15 +252,28 @@ export default { }, // Commit changes from old tab. Problem: tab was already switched, so commit changes with old tab id. prepareTabSwitch () { + let firstViewportVisibleItem = 'S' + this.getFirstLineNumberInViewport() + if (this.sourceCode === false) { // this shouldn't happen + firstViewportVisibleItem = undefined + } + if (this.commitTimer) clearTimeout(this.commitTimer) if (this.tabId) { const { editor } = this const { cursor, markdown } = this.getMarkdownAndCursor(editor) - this.$store.dispatch('LISTEN_FOR_CONTENT_CHANGE', { id: this.tabId, markdown, cursor }) + this.$store.dispatch('LISTEN_FOR_CONTENT_CHANGE', { id: this.tabId, markdown, cursor, firstViewportVisibleItem }) this.tabId = null // invalidate tab id } }, - + scrollToLineNumberInViewport (line, duration = 300, dontAddStandardHeadroom = false) { + let pos = this.editor.charCoords({ line: line, ch: 0 }, 'local').top + const DURATION = duration + if (!dontAddStandardHeadroom) { pos -= STANDAR_Y } + animatedScrollTo(this.$el, pos, DURATION) + }, + getFirstLineNumberInViewport () { + return this.editor.coordsChar({ left: 0, top: this.$el.scrollTop }, 'local').line + }, handleSelectAll () { if (!this.sourceCode) { return diff --git a/src/renderer/components/editorWithTabs/sourceCode.vue.orig b/src/renderer/components/editorWithTabs/sourceCode.vue.orig new file mode 100644 index 00000000..2e766a44 --- /dev/null +++ b/src/renderer/components/editorWithTabs/sourceCode.vue.orig @@ -0,0 +1,389 @@ + + + + + diff --git a/src/renderer/store/editor.js b/src/renderer/store/editor.js index 6726503f..1f599abe 100644 --- a/src/renderer/store/editor.js +++ b/src/renderer/store/editor.js @@ -247,6 +247,11 @@ const mutations = { state.currentFile.history = history } }, + SET_FIRST_VIS_ITEM (state, firstViewportVisibleItem) { + if (hasKeys(state.currentFile)) { + state.currentFile.firstViewportVisibleItem = firstViewportVisibleItem + } + }, CLOSE_TABS (state, tabIdList) { if (!tabIdList || tabIdList.length === 0) return @@ -922,7 +927,7 @@ const actions = { // Content change from realtime preview editor and source code editor // WORKAROUND: id is "muya" if changes come from muya and not source code editor! So we don't have to apply the workaround. - LISTEN_FOR_CONTENT_CHANGE ({ commit, dispatch, state, rootState }, { id, markdown, wordCount, cursor, history, toc }) { + LISTEN_FOR_CONTENT_CHANGE ({ commit, dispatch, state, rootState }, { id, markdown, wordCount, cursor, history, toc, firstViewportVisibleItem }) { const { autoSave } = rootState.preferences const { id: currentId, @@ -952,6 +957,10 @@ const actions = { if (history) { tab.history = history } + // Set Line or First Visible Item Index + if (firstViewportVisibleItem) { + tab.firstViewportVisibleItem = firstViewportVisibleItem + } break } } @@ -978,6 +987,9 @@ const actions = { if (history) { commit('SET_HISTORY', history) } + if (firstViewportVisibleItem) { + commit('SET_FIRST_VIS_ITEM', firstViewportVisibleItem) + } // Set toc if (toc && !equal(toc, listToc)) { commit('SET_TOC', toc)