From db06a51299e0888b07062cefd780d514d09ebd37 Mon Sep 17 00:00:00 2001 From: Yukai Huang Date: Sun, 9 Apr 2017 20:05:48 +0800 Subject: Load statusbar template by string-loader --- public/js/lib/editor/index.js | 54 ++++++++++++++----------------------- public/js/lib/editor/statusbar.html | 41 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 public/js/lib/editor/statusbar.html (limited to 'public/js/lib') diff --git a/public/js/lib/editor/index.js b/public/js/lib/editor/index.js index c807a17f..8cab486c 100644 --- a/public/js/lib/editor/index.js +++ b/public/js/lib/editor/index.js @@ -1,5 +1,6 @@ import * as utils from './utils' import config from './config' +import statusBarTemplate from './statusbar.html' /* config section */ const isMac = CodeMirror.keyMap.default === CodeMirror.keyMap.macDefault @@ -132,40 +133,27 @@ export default class Editor { }) } - getStatusBarTemplate () { - return new Promise((resolve, reject) => { - $.get(window.serverurl + '/views/statusbar.html').done(template => { - this.statusBarTemplate = template - resolve() - }).fail(reject) - }) - } - addStatusBar () { - if (!this.statusBarTemplate) { - this.getStatusBarTemplate.then(this.addStatusBar) - } else { - this.statusBar = $(this.statusBarTemplate) - this.statusCursor = this.statusBar.find('.status-cursor > .status-line-column') - this.statusSelection = this.statusBar.find('.status-cursor > .status-selection') - this.statusFile = this.statusBar.find('.status-file') - this.statusIndicators = this.statusBar.find('.status-indicators') - this.statusIndent = this.statusBar.find('.status-indent') - this.statusKeymap = this.statusBar.find('.status-keymap') - this.statusLength = this.statusBar.find('.status-length') - this.statusTheme = this.statusBar.find('.status-theme') - this.statusSpellcheck = this.statusBar.find('.status-spellcheck') - this.statusPreferences = this.statusBar.find('.status-preferences') - this.statusPanel = this.editor.addPanel(this.statusBar[0], { - position: 'bottom' - }) + this.statusBar = $(statusBarTemplate) + this.statusCursor = this.statusBar.find('.status-cursor > .status-line-column') + this.statusSelection = this.statusBar.find('.status-cursor > .status-selection') + this.statusFile = this.statusBar.find('.status-file') + this.statusIndicators = this.statusBar.find('.status-indicators') + this.statusIndent = this.statusBar.find('.status-indent') + this.statusKeymap = this.statusBar.find('.status-keymap') + this.statusLength = this.statusBar.find('.status-length') + this.statusTheme = this.statusBar.find('.status-theme') + this.statusSpellcheck = this.statusBar.find('.status-spellcheck') + this.statusPreferences = this.statusBar.find('.status-preferences') + this.statusPanel = this.editor.addPanel(this.statusBar[0], { + position: 'bottom' + }) - this.setIndent() - this.setKeymap() - this.setTheme() - this.setSpellcheck() - this.setPreferences() - } + this.setIndent() + this.setKeymap() + this.setTheme() + this.setSpellcheck() + this.setPreferences() } updateStatusBar () { @@ -508,8 +496,6 @@ export default class Editor { placeholder: "← Start by entering a title here\n===\nVisit /features if you don't know what to do.\nHappy hacking :)" }) - this.getStatusBarTemplate() - return this.editor } diff --git a/public/js/lib/editor/statusbar.html b/public/js/lib/editor/statusbar.html new file mode 100644 index 00000000..24cbf6c2 --- /dev/null +++ b/public/js/lib/editor/statusbar.html @@ -0,0 +1,41 @@ +
+
+
+ + +
+
+
+
+
+ + +
+
Spaces:
+
4
+ +
+
+ +
+
+ +
+
+
-- cgit v1.2.3 From c6c11c54ef62ed5c87dc7eb8139805a2889cbcc8 Mon Sep 17 00:00:00 2001 From: Yukai Huang Date: Sun, 9 Apr 2017 21:14:23 +0800 Subject: Expose internal editor config variable --- public/js/lib/editor/index.js | 1 + 1 file changed, 1 insertion(+) (limited to 'public/js/lib') diff --git a/public/js/lib/editor/index.js b/public/js/lib/editor/index.js index 8cab486c..2991998b 100644 --- a/public/js/lib/editor/index.js +++ b/public/js/lib/editor/index.js @@ -119,6 +119,7 @@ export default class Editor { } } this.eventListeners = {} + this.config = config } on (event, cb) { -- cgit v1.2.3 From 68ccee20b3709b9a0499d659c74ac0a298a9ffeb Mon Sep 17 00:00:00 2001 From: Yukai Huang Date: Tue, 11 Apr 2017 11:48:59 +0800 Subject: Extract modeType --- public/js/lib/editor/modeType.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 public/js/lib/editor/modeType.js (limited to 'public/js/lib') diff --git a/public/js/lib/editor/modeType.js b/public/js/lib/editor/modeType.js new file mode 100644 index 00000000..f3212105 --- /dev/null +++ b/public/js/lib/editor/modeType.js @@ -0,0 +1,11 @@ +export default { + edit: { + name: 'edit' + }, + view: { + name: 'view' + }, + both: { + name: 'both' + } +} -- cgit v1.2.3 From 0e9afde5fa87d7039d4cebd43e5613541c56849a Mon Sep 17 00:00:00 2001 From: Yukai Huang Date: Tue, 11 Apr 2017 21:33:54 +0800 Subject: Move syncsroll under lib --- public/js/lib/syncscroll.js | 368 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 public/js/lib/syncscroll.js (limited to 'public/js/lib') diff --git a/public/js/lib/syncscroll.js b/public/js/lib/syncscroll.js new file mode 100644 index 00000000..6b07e79c --- /dev/null +++ b/public/js/lib/syncscroll.js @@ -0,0 +1,368 @@ +/* eslint-env browser, jquery */ +/* global _ */ +// Inject line numbers for sync scroll. + +import markdownitContainer from 'markdown-it-container' + +import { md } from '../extra' +import modeType from '../lib/editor/modeType' + +function addPart (tokens, idx) { + if (tokens[idx].map && tokens[idx].level === 0) { + const startline = tokens[idx].map[0] + 1 + const endline = tokens[idx].map[1] + tokens[idx].attrJoin('class', 'part') + tokens[idx].attrJoin('data-startline', startline) + tokens[idx].attrJoin('data-endline', endline) + } +} + +md.renderer.rules.blockquote_open = function (tokens, idx, options, env, self) { + tokens[idx].attrJoin('class', 'raw') + addPart(tokens, idx) + return self.renderToken(...arguments) +} +md.renderer.rules.table_open = function (tokens, idx, options, env, self) { + addPart(tokens, idx) + return self.renderToken(...arguments) +} +md.renderer.rules.bullet_list_open = function (tokens, idx, options, env, self) { + addPart(tokens, idx) + return self.renderToken(...arguments) +} +md.renderer.rules.list_item_open = function (tokens, idx, options, env, self) { + tokens[idx].attrJoin('class', 'raw') + if (tokens[idx].map) { + const startline = tokens[idx].map[0] + 1 + const endline = tokens[idx].map[1] + tokens[idx].attrJoin('data-startline', startline) + tokens[idx].attrJoin('data-endline', endline) + } + return self.renderToken(...arguments) +} +md.renderer.rules.ordered_list_open = function (tokens, idx, options, env, self) { + addPart(tokens, idx) + return self.renderToken(...arguments) +} +md.renderer.rules.link_open = function (tokens, idx, options, env, self) { + addPart(tokens, idx) + return self.renderToken(...arguments) +} +md.renderer.rules.paragraph_open = function (tokens, idx, options, env, self) { + addPart(tokens, idx) + return self.renderToken(...arguments) +} +md.renderer.rules.heading_open = function (tokens, idx, options, env, self) { + tokens[idx].attrJoin('class', 'raw') + addPart(tokens, idx) + return self.renderToken(...arguments) +} +md.renderer.rules.fence = (tokens, idx, options, env, self) => { + const token = tokens[idx] + const info = token.info ? md.utils.unescapeAll(token.info).trim() : '' + let langName = '' + let highlighted + + if (info) { + langName = info.split(/\s+/g)[0] + if (/!$/.test(info)) token.attrJoin('class', 'wrap') + token.attrJoin('class', options.langPrefix + langName.replace(/=$|=\d+$|=\+$|!$|=!/, '')) + token.attrJoin('class', 'hljs') + token.attrJoin('class', 'raw') + } + + if (options.highlight) { + highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content) + } else { + highlighted = md.utils.escapeHtml(token.content) + } + + if (highlighted.indexOf('${highlighted}\n` + } + + return `
${highlighted}
\n` +} +md.renderer.rules.code_block = (tokens, idx, options, env, self) => { + if (tokens[idx].map && tokens[idx].level === 0) { + const startline = tokens[idx].map[0] + 1 + const endline = tokens[idx].map[1] + return `
${md.utils.escapeHtml(tokens[idx].content)}
\n` + } + return `
${md.utils.escapeHtml(tokens[idx].content)}
\n` +} +function renderContainer (tokens, idx, options, env, self) { + tokens[idx].attrJoin('role', 'alert') + tokens[idx].attrJoin('class', 'alert') + tokens[idx].attrJoin('class', `alert-${tokens[idx].info.trim()}`) + addPart(tokens, idx) + return self.renderToken(...arguments) +} + +md.use(markdownitContainer, 'success', { render: renderContainer }) +md.use(markdownitContainer, 'info', { render: renderContainer }) +md.use(markdownitContainer, 'warning', { render: renderContainer }) +md.use(markdownitContainer, 'danger', { render: renderContainer }) + +// FIXME: expose syncscroll to window +window.syncscroll = true + +window.preventSyncScrollToEdit = false +window.preventSyncScrollToView = false + +const editScrollThrottle = 5 +const viewScrollThrottle = 5 +const buildMapThrottle = 100 + +let viewScrolling = false +let editScrolling = false + +let editArea = null +let viewArea = null +let markdownArea = null + +export function setupSyncAreas (edit, view, markdown) { + editArea = edit + viewArea = view + markdownArea = markdown + editArea.on('scroll', _.throttle(syncScrollToView, editScrollThrottle)) + viewArea.on('scroll', _.throttle(syncScrollToEdit, viewScrollThrottle)) +} + +let scrollMap, lineHeightMap, viewTop, viewBottom + +export function clearMap () { + scrollMap = null + lineHeightMap = null + viewTop = null + viewBottom = null +} +window.viewAjaxCallback = clearMap + +const buildMap = _.throttle(buildMapInner, buildMapThrottle) + +// Build offsets for each line (lines can be wrapped) +// That's a bit dirty to process each line everytime, but ok for demo. +// Optimizations are required only for big texts. +function buildMapInner (callback) { + if (!viewArea || !markdownArea) return + let i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount, acc, _scrollMap + + offset = viewArea.scrollTop() - viewArea.offset().top + _scrollMap = [] + nonEmptyList = [] + _lineHeightMap = [] + viewTop = 0 + viewBottom = viewArea[0].scrollHeight - viewArea.height() + + acc = 0 + const lines = window.editor.getValue().split('\n') + const lineHeight = window.editor.defaultTextHeight() + for (i = 0; i < lines.length; i++) { + const str = lines[i] + + _lineHeightMap.push(acc) + + if (str.length === 0) { + acc++ + continue + } + + const h = window.editor.heightAtLine(i + 1) - window.editor.heightAtLine(i) + acc += Math.round(h / lineHeight) + } + _lineHeightMap.push(acc) + linesCount = acc + + for (i = 0; i < linesCount; i++) { + _scrollMap.push(-1) + } + + nonEmptyList.push(0) + // make the first line go top + _scrollMap[0] = viewTop + + const parts = markdownArea.find('.part').toArray() + for (i = 0; i < parts.length; i++) { + const $el = $(parts[i]) + let t = $el.attr('data-startline') - 1 + if (t === '') { + return + } + t = _lineHeightMap[t] + if (t !== 0 && t !== nonEmptyList[nonEmptyList.length - 1]) { + nonEmptyList.push(t) + } + _scrollMap[t] = Math.round($el.offset().top + offset - 10) + } + + nonEmptyList.push(linesCount) + _scrollMap[linesCount] = viewArea[0].scrollHeight + + pos = 0 + for (i = 1; i < linesCount; i++) { + if (_scrollMap[i] !== -1) { + pos++ + continue + } + + a = nonEmptyList[pos] + b = nonEmptyList[pos + 1] + _scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a)) + } + + _scrollMap[0] = 0 + + scrollMap = _scrollMap + lineHeightMap = _lineHeightMap + + if (window.loaded && callback) callback() +} + +// sync view scroll progress to edit +let viewScrollingTimer = null + +export function syncScrollToEdit (event, preventAnimate) { + if (window.currentMode !== modeType.both || !window.syncscroll || !editArea) return + if (window.preventSyncScrollToEdit) { + if (typeof window.preventSyncScrollToEdit === 'number') { + window.preventSyncScrollToEdit-- + } else { + window.preventSyncScrollToEdit = false + } + return + } + if (!scrollMap || !lineHeightMap) { + buildMap(() => { + syncScrollToEdit(event, preventAnimate) + }) + return + } + if (editScrolling) return + + const scrollTop = viewArea[0].scrollTop + let lineIndex = 0 + for (let i = 0, l = scrollMap.length; i < l; i++) { + if (scrollMap[i] > scrollTop) { + break + } else { + lineIndex = i + } + } + let lineNo = 0 + let lineDiff = 0 + for (let i = 0, l = lineHeightMap.length; i < l; i++) { + if (lineHeightMap[i] > lineIndex) { + break + } else { + lineNo = lineHeightMap[i] + lineDiff = lineHeightMap[i + 1] - lineNo + } + } + + let posTo = 0 + let topDiffPercent = 0 + let posToNextDiff = 0 + const scrollInfo = window.editor.getScrollInfo() + const textHeight = window.editor.defaultTextHeight() + const preLastLineHeight = scrollInfo.height - scrollInfo.clientHeight - textHeight + const preLastLineNo = Math.round(preLastLineHeight / textHeight) + const preLastLinePos = scrollMap[preLastLineNo] + + if (scrollInfo.height > scrollInfo.clientHeight && scrollTop >= preLastLinePos) { + posTo = preLastLineHeight + topDiffPercent = (scrollTop - preLastLinePos) / (viewBottom - preLastLinePos) + posToNextDiff = textHeight * topDiffPercent + posTo += Math.ceil(posToNextDiff) + } else { + posTo = lineNo * textHeight + topDiffPercent = (scrollTop - scrollMap[lineNo]) / (scrollMap[lineNo + lineDiff] - scrollMap[lineNo]) + posToNextDiff = textHeight * lineDiff * topDiffPercent + posTo += Math.ceil(posToNextDiff) + } + + if (preventAnimate) { + editArea.scrollTop(posTo) + } else { + const posDiff = Math.abs(scrollInfo.top - posTo) + var duration = posDiff / 50 + duration = duration >= 100 ? duration : 100 + editArea.stop(true, true).animate({ + scrollTop: posTo + }, duration, 'linear') + } + + viewScrolling = true + clearTimeout(viewScrollingTimer) + viewScrollingTimer = setTimeout(viewScrollingTimeoutInner, duration * 1.5) +} + +function viewScrollingTimeoutInner () { + viewScrolling = false +} + +// sync edit scroll progress to view +let editScrollingTimer = null + +export function syncScrollToView (event, preventAnimate) { + if (window.currentMode !== modeType.both || !window.syncscroll || !viewArea) return + if (window.preventSyncScrollToView) { + if (typeof preventSyncScrollToView === 'number') { + window.preventSyncScrollToView-- + } else { + window.preventSyncScrollToView = false + } + return + } + if (!scrollMap || !lineHeightMap) { + buildMap(() => { + syncScrollToView(event, preventAnimate) + }) + return + } + if (viewScrolling) return + + let lineNo, posTo + let topDiffPercent, posToNextDiff + const scrollInfo = window.editor.getScrollInfo() + const textHeight = window.editor.defaultTextHeight() + lineNo = Math.floor(scrollInfo.top / textHeight) + // if reach the last line, will start lerp to the bottom + const diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight) + if (scrollInfo.height > scrollInfo.clientHeight && diffToBottom > 0) { + topDiffPercent = diffToBottom / textHeight + posTo = scrollMap[lineNo + 1] + posToNextDiff = (viewBottom - posTo) * topDiffPercent + posTo += Math.floor(posToNextDiff) + } else { + topDiffPercent = (scrollInfo.top % textHeight) / textHeight + posTo = scrollMap[lineNo] + posToNextDiff = (scrollMap[lineNo + 1] - posTo) * topDiffPercent + posTo += Math.floor(posToNextDiff) + } + + if (preventAnimate) { + viewArea.scrollTop(posTo) + } else { + const posDiff = Math.abs(viewArea.scrollTop() - posTo) + var duration = posDiff / 50 + duration = duration >= 100 ? duration : 100 + viewArea.stop(true, true).animate({ + scrollTop: posTo + }, duration, 'linear') + } + + editScrolling = true + clearTimeout(editScrollingTimer) + editScrollingTimer = setTimeout(editScrollingTimeoutInner, duration * 1.5) +} + +function editScrollingTimeoutInner () { + editScrolling = false +} -- cgit v1.2.3 From 4839838d0cdfca29a9e87fcf966ec026ee99a14f Mon Sep 17 00:00:00 2001 From: Yukai Huang Date: Wed, 12 Apr 2017 09:21:13 +0800 Subject: Manage syncscroll / currentMode in appState --- public/js/lib/appState.js | 8 ++++++++ public/js/lib/editor/modeType.js | 11 ----------- public/js/lib/modeType.js | 11 +++++++++++ public/js/lib/syncscroll.js | 31 +++++++++++++++++-------------- 4 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 public/js/lib/appState.js delete mode 100644 public/js/lib/editor/modeType.js create mode 100644 public/js/lib/modeType.js (limited to 'public/js/lib') diff --git a/public/js/lib/appState.js b/public/js/lib/appState.js new file mode 100644 index 00000000..fb8030e1 --- /dev/null +++ b/public/js/lib/appState.js @@ -0,0 +1,8 @@ +import modeType from './modeType' + +let state = { + syncscroll: true, + currentMode: modeType.view +} + +export default state diff --git a/public/js/lib/editor/modeType.js b/public/js/lib/editor/modeType.js deleted file mode 100644 index f3212105..00000000 --- a/public/js/lib/editor/modeType.js +++ /dev/null @@ -1,11 +0,0 @@ -export default { - edit: { - name: 'edit' - }, - view: { - name: 'view' - }, - both: { - name: 'both' - } -} diff --git a/public/js/lib/modeType.js b/public/js/lib/modeType.js new file mode 100644 index 00000000..f3212105 --- /dev/null +++ b/public/js/lib/modeType.js @@ -0,0 +1,11 @@ +export default { + edit: { + name: 'edit' + }, + view: { + name: 'view' + }, + both: { + name: 'both' + } +} diff --git a/public/js/lib/syncscroll.js b/public/js/lib/syncscroll.js index 6b07e79c..cee317ea 100644 --- a/public/js/lib/syncscroll.js +++ b/public/js/lib/syncscroll.js @@ -5,7 +5,8 @@ import markdownitContainer from 'markdown-it-container' import { md } from '../extra' -import modeType from '../lib/editor/modeType' +import modeType from './modeType' +import appState from './appState' function addPart (tokens, idx) { if (tokens[idx].map && tokens[idx].level === 0) { @@ -110,9 +111,6 @@ md.use(markdownitContainer, 'info', { render: renderContainer }) md.use(markdownitContainer, 'warning', { render: renderContainer }) md.use(markdownitContainer, 'danger', { render: renderContainer }) -// FIXME: expose syncscroll to window -window.syncscroll = true - window.preventSyncScrollToEdit = false window.preventSyncScrollToView = false @@ -127,10 +125,15 @@ let editArea = null let viewArea = null let markdownArea = null -export function setupSyncAreas (edit, view, markdown) { +let editor + +export function setupSyncAreas (edit, view, markdown, _editor) { editArea = edit viewArea = view markdownArea = markdown + + editor = _editor + editArea.on('scroll', _.throttle(syncScrollToView, editScrollThrottle)) viewArea.on('scroll', _.throttle(syncScrollToEdit, viewScrollThrottle)) } @@ -162,8 +165,8 @@ function buildMapInner (callback) { viewBottom = viewArea[0].scrollHeight - viewArea.height() acc = 0 - const lines = window.editor.getValue().split('\n') - const lineHeight = window.editor.defaultTextHeight() + const lines = editor.getValue().split('\n') + const lineHeight = editor.defaultTextHeight() for (i = 0; i < lines.length; i++) { const str = lines[i] @@ -174,7 +177,7 @@ function buildMapInner (callback) { continue } - const h = window.editor.heightAtLine(i + 1) - window.editor.heightAtLine(i) + const h = editor.heightAtLine(i + 1) - editor.heightAtLine(i) acc += Math.round(h / lineHeight) } _lineHeightMap.push(acc) @@ -229,7 +232,7 @@ function buildMapInner (callback) { let viewScrollingTimer = null export function syncScrollToEdit (event, preventAnimate) { - if (window.currentMode !== modeType.both || !window.syncscroll || !editArea) return + if (appState.currentMode !== modeType.both || !appState.syncscroll || !editArea) return if (window.preventSyncScrollToEdit) { if (typeof window.preventSyncScrollToEdit === 'number') { window.preventSyncScrollToEdit-- @@ -269,8 +272,8 @@ export function syncScrollToEdit (event, preventAnimate) { let posTo = 0 let topDiffPercent = 0 let posToNextDiff = 0 - const scrollInfo = window.editor.getScrollInfo() - const textHeight = window.editor.defaultTextHeight() + const scrollInfo = editor.getScrollInfo() + const textHeight = editor.defaultTextHeight() const preLastLineHeight = scrollInfo.height - scrollInfo.clientHeight - textHeight const preLastLineNo = Math.round(preLastLineHeight / textHeight) const preLastLinePos = scrollMap[preLastLineNo] @@ -311,7 +314,7 @@ function viewScrollingTimeoutInner () { let editScrollingTimer = null export function syncScrollToView (event, preventAnimate) { - if (window.currentMode !== modeType.both || !window.syncscroll || !viewArea) return + if (appState.currentMode !== modeType.both || !appState.syncscroll || !viewArea) return if (window.preventSyncScrollToView) { if (typeof preventSyncScrollToView === 'number') { window.preventSyncScrollToView-- @@ -330,8 +333,8 @@ export function syncScrollToView (event, preventAnimate) { let lineNo, posTo let topDiffPercent, posToNextDiff - const scrollInfo = window.editor.getScrollInfo() - const textHeight = window.editor.defaultTextHeight() + const scrollInfo = editor.getScrollInfo() + const textHeight = editor.defaultTextHeight() lineNo = Math.floor(scrollInfo.top / textHeight) // if reach the last line, will start lerp to the bottom const diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight) -- cgit v1.2.3