Skip to content

Commit bde8c0c

Browse files
TitanSnowdark-flames
authored andcommitted
Scroll sync (#15)
* scroll sync * move InjectLnParser into MarkdownPalettes.vue * add scroll animation for editor scroll sync * deal img become bigger after loaded * add scrollsync button on toolbar
1 parent 44b1130 commit bde8c0c

File tree

9 files changed

+316
-24
lines changed

9 files changed

+316
-24
lines changed
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { MarkdownParser } from './plugin/MarkdownParser'
1+
import MarkdownIt from 'markdown-it'
22

33
function contentParserFactory (parsers) {
4-
return content => {
5-
return MarkdownParser(content, parsers)
6-
}
4+
let converter = MarkdownIt()
5+
parsers.forEach(parser => {
6+
converter = converter.use(parser)
7+
})
8+
return content => converter.render(content)
79
}
810

911
export { contentParserFactory }

src/components/DefaultConfig.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export const defaultConfig = {
2727
mode: 'markdown',
2828
lineNumbers: true,
2929
lineWrapping: true
30-
}
30+
},
31+
scrollSync: true
3132
}
3233

3334
export function getConfig (config) {

src/components/InputArea.vue

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<script>
3636
import {codemirror} from 'vue-codemirror-lite'
3737
import 'codemirror/mode/markdown/markdown.js'
38+
import _ from 'lodash'
3839
3940
export default {
4041
name: 'input-area',
@@ -47,6 +48,9 @@ export default {
4748
},
4849
insertCode: {
4950
default: null
51+
},
52+
scrollSync: {
53+
type: Boolean
5054
}
5155
},
5256
computed: {
@@ -83,19 +87,104 @@ export default {
8387
},
8488
data: function () {
8589
return {
86-
code: ''
90+
code: '',
91+
scrollSynced: false,
92+
scrollAnimation: null
8793
}
8894
},
8995
components: {
9096
codemirror
9197
},
9298
mounted: function () {
9399
this.code = this.value
100+
101+
const debouncedEmitScrollSync = _.debounce(this.emitScrollSync, 50, { maxWait: 50 })
102+
const scrollSync = () => {
103+
if (this.scrollSynced) {
104+
this.scrollSynced = false
105+
} else {
106+
debouncedEmitScrollSync()
107+
if (this.scrollAnimation) {
108+
this.scrollAnimation.cancel()
109+
}
110+
}
111+
}
112+
this.editor.on('cursorActivity', scrollSync)
113+
this.editor.on('scroll', scrollSync)
94114
},
95115
methods: {
96116
updateCode (code) {
97117
this.$emit('input', code)
118+
},
119+
120+
emitScrollSync () {
121+
if (!this.scrollSync) return
122+
const cursorLine = this.editor.getCursor().line
123+
const scrollInfo = this.editor.getScrollInfo('local')
124+
const viewport = {
125+
from: this.editor.lineAtHeight(scrollInfo.top, 'local'),
126+
to: this.editor.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, 'local') + 1
127+
}
128+
const linesOffset = []
129+
for (let line = viewport.from; line < viewport.to; ++line) {
130+
const coords = this.editor.cursorCoords({ line, ch: 0 }, 'local')
131+
linesOffset[line] = {
132+
top: coords.top - scrollInfo.top,
133+
bottom: coords.bottom - scrollInfo.top
134+
}
135+
}
136+
const event = {
137+
cursorLine, scrollInfo, viewport, linesOffset
138+
}
139+
this.$emit('scroll-sync', event)
140+
},
141+
updateScrollSync ({ scrollInfo, linesOffset }) {
142+
if (!linesOffset.length) {
143+
return
144+
}
145+
const scrollMid = scrollInfo.height / 2
146+
let syncLine
147+
linesOffset.forEach(({ top }, line) => {
148+
if (typeof syncLine === 'undefined' || Math.abs(top - scrollMid) < Math.abs(linesOffset[syncLine].top - scrollMid)) {
149+
syncLine = line
150+
}
151+
})
152+
const scrollTop = this.editor.getScrollInfo().top
153+
const editorLineOffset = this.editor.heightAtLine(syncLine, 'local') - scrollTop
154+
if (this.scrollAnimation) {
155+
this.scrollAnimation.cancel()
156+
}
157+
let animationCancelled = false
158+
let animationSkipFrame = false
159+
const animationFrom = scrollTop
160+
const animationTo = scrollTop + editorLineOffset - linesOffset[syncLine].top
161+
const animationStartTime = Date.now()
162+
const animationDuration = 200
163+
const animationFrameCallback = () => {
164+
if (animationSkipFrame) {
165+
// skip frame so that user can scroll to interrupt animation
166+
requestAnimationFrame(animationFrameCallback)
167+
animationSkipFrame = false
168+
} else if (!animationCancelled) {
169+
const currentTime = Date.now()
170+
const precent = (currentTime - animationStartTime) / animationDuration
171+
this.scrollSynced = true
172+
if (precent >= 1) {
173+
this.editor.scrollTo(null, animationTo)
174+
} else {
175+
this.editor.scrollTo(null, (animationTo - animationFrom) * precent + animationFrom)
176+
requestAnimationFrame(animationFrameCallback)
177+
animationSkipFrame = true
178+
}
179+
}
180+
}
181+
animationFrameCallback()
182+
this.scrollAnimation = {
183+
cancel () {
184+
animationCancelled = true
185+
}
186+
}
98187
}
99188
}
100189
}
101-
</script>
190+
</script>

src/components/MarkdownPalettes.vue

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@
1313
ref="inputArea"
1414
@input="updateCode"
1515
@finish="insertCode = null"
16+
@scroll-sync="doScrollSync('editor', $event)"
1617
:insertCode="insertCode"
17-
:editorOption="editorConfig.editorOption"></input-area>
18+
:editorOption="editorConfig.editorOption"
19+
:scrollSync="scrollSync"></input-area>
1820
</div>
1921
<div id="mp-editor-preview-area" class="mp-editor-area mp-preview-area" :class="{
2022
'mp-editor-area': this.config.previewDisplay === 'normal',
2123
'mp-editor-area-hide': this.config.previewDisplay === 'hide'
2224
}">
23-
<preview-area v-model="code" :parser="contentParser" ref="previewArea"></preview-area>
25+
<preview-area v-model="code" :parser="contentParser" ref="previewArea"
26+
@scroll-sync="doScrollSync('preview', $event)"
27+
:scrollSync="scrollSync"></preview-area>
2428
</div>
2529
</div>
2630
<div id="mp-editor-dialog">
@@ -89,6 +93,7 @@ import EditorDialog from './Dialog.vue'
8993
9094
import { defaultConfig, getConfig } from './DefaultConfig'
9195
import { contentParserFactory } from './ContentParserFactory'
96+
import InjectLnParser from './plugins/InjectLnParser.js'
9297
9398
export default {
9499
name: 'markdown-palettes',
@@ -113,7 +118,8 @@ export default {
113118
editorConfig: config,
114119
editorHeight: '500px',
115120
fullScreen: config.fullScreen,
116-
contentParser: contentParserFactory(config.parsers)
121+
contentParser: contentParserFactory([...config.parsers, InjectLnParser]),
122+
scrollSync: config.scrollSync
117123
}
118124
},
119125
mounted () {
@@ -159,6 +165,16 @@ export default {
159165
this.fullScreen = false
160166
}
161167
}
168+
if (operation === 'scrollSync') {
169+
this.scrollSync = !this.scrollSync
170+
}
171+
},
172+
doScrollSync (emitter, info) {
173+
if (emitter === 'editor') {
174+
this.$refs.previewArea.updateScrollSync(info)
175+
} else if (emitter === 'preview') {
176+
this.$refs.inputArea.updateScrollSync(info)
177+
}
162178
}
163179
},
164180
watch: {
@@ -168,4 +184,4 @@ export default {
168184
}
169185
}
170186
}
171-
</script>
187+
</script>

0 commit comments

Comments
 (0)