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 250b2578..c33e6cb9 100644
--- a/src/muya/lib/contentState/index.js
+++ b/src/muya/lib/contentState/index.js
@@ -83,6 +83,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 })
@@ -347,6 +348,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 0b407039..ccdde598 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 {
@@ -437,8 +438,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()
}
@@ -630,7 +643,6 @@ export default {
//
// this.setImageViewerVisible(true)
// })
-
this.editor.on('selectionChange', changes => {
const { y } = changes.cursorCoords
if (this.typewriter) {
@@ -908,14 +920,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)
}
},
@@ -1072,7 +1121,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 687a505d..c45d6217 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) {
@@ -244,15 +259,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 3b1c3ee6..cd3db027 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
@@ -896,7 +901,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,
@@ -926,6 +931,10 @@ const actions = {
if (history) {
tab.history = history
}
+ // Set Line or First Visible Item Index
+ if (firstViewportVisibleItem) {
+ tab.firstViewportVisibleItem = firstViewportVisibleItem
+ }
break
}
}
@@ -952,6 +961,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)