diff options
Diffstat (limited to 'public/js/lib/editor')
-rw-r--r-- | public/js/lib/editor/index.js | 482 | ||||
-rw-r--r-- | public/js/lib/editor/ui-elements.js | 86 | ||||
-rw-r--r-- | public/js/lib/editor/utils.js | 48 |
3 files changed, 616 insertions, 0 deletions
diff --git a/public/js/lib/editor/index.js b/public/js/lib/editor/index.js new file mode 100644 index 00000000..a97fd976 --- /dev/null +++ b/public/js/lib/editor/index.js @@ -0,0 +1,482 @@ +import * as utils from './utils' + +/* config section */ +const isMac = CodeMirror.keyMap.default === CodeMirror.keyMap.macDefault +const defaultEditorMode = 'gfm' +const viewportMargin = 20 + +const jumpToAddressBarKeymapName = isMac ? 'Cmd-L' : 'Ctrl-L' + +export default class Editor { + constructor () { + this.editor = null + + this.defaultExtraKeys = { + F10: function (cm) { + cm.setOption('fullScreen', !cm.getOption('fullScreen')) + }, + Esc: function (cm) { + if (cm.getOption('keyMap').substr(0, 3) === 'vim') { + return CodeMirror.Pass + } else if (cm.getOption('fullScreen')) { + cm.setOption('fullScreen', false) + } + }, + 'Cmd-S': function () { + return false + }, + 'Ctrl-S': function () { + return false + }, + Enter: 'newlineAndIndentContinueMarkdownList', + Tab: function (cm) { + var tab = '\t' + + // contruct x length spaces + var spaces = Array(parseInt(cm.getOption('indentUnit')) + 1).join(' ') + + // auto indent whole line when in list or blockquote + var cursor = cm.getCursor() + var line = cm.getLine(cursor.line) + + // this regex match the following patterns + // 1. blockquote starts with "> " or ">>" + // 2. unorder list starts with *+- + // 3. order list starts with "1." or "1)" + var regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/ + + var match + var multiple = cm.getSelection().split('\n').length > 1 || + cm.getSelections().length > 1 + + if (multiple) { + cm.execCommand('defaultTab') + } else if ((match = regex.exec(line)) !== null) { + var ch = match[1].length + var pos = { + line: cursor.line, + ch: ch + } + if (cm.getOption('indentWithTabs')) { + cm.replaceRange(tab, pos, pos, '+input') + } else { + cm.replaceRange(spaces, pos, pos, '+input') + } + } else { + if (cm.getOption('indentWithTabs')) { + cm.execCommand('defaultTab') + } else { + cm.replaceSelection(spaces) + } + } + }, + 'Cmd-Left': 'goLineLeftSmart', + 'Cmd-Right': 'goLineRight', + 'Ctrl-C': function (cm) { + if (!isMac && cm.getOption('keyMap').substr(0, 3) === 'vim') { + document.execCommand('copy') + } else { + return CodeMirror.Pass + } + }, + 'Ctrl-*': cm => { + utils.wrapTextWith(this.editor, cm, '*') + }, + 'Shift-Ctrl-8': cm => { + utils.wrapTextWith(this.editor, cm, '*') + }, + 'Ctrl-_': cm => { + utils.wrapTextWith(this.editor, cm, '_') + }, + 'Shift-Ctrl--': cm => { + utils.wrapTextWith(this.editor, cm, '_') + }, + 'Ctrl-~': cm => { + utils.wrapTextWith(this.editor, cm, '~') + }, + 'Shift-Ctrl-`': cm => { + utils.wrapTextWith(this.editor, cm, '~') + }, + 'Ctrl-^': cm => { + utils.wrapTextWith(this.editor, cm, '^') + }, + 'Shift-Ctrl-6': cm => { + utils.wrapTextWith(this.editor, cm, '^') + }, + 'Ctrl-+': cm => { + utils.wrapTextWith(this.editor, cm, '+') + }, + 'Shift-Ctrl-=': cm => { + utils.wrapTextWith(this.editor, cm, '+') + }, + 'Ctrl-=': cm => { + utils.wrapTextWith(this.editor, cm, '=') + }, + 'Shift-Ctrl-Backspace': cm => { + utils.wrapTextWith(this.editor, cm, 'Backspace') + } + } + + this.jumpToAddressBarKeymapValue = null + } + + getStatusBarTemplate (callback) { + $.get(window.serverurl + '/views/statusbar.html', template => { + this.statusBarTemplate = template + if (callback) callback() + }) + } + + addStatusBar () { + if (!this.statusBarTemplate) { + this.getStatusBarTemplate(this.addStatusBar) + return + } + this.statusBar = $(this.statusBarTemplate) + this.statusCursor = this.statusBar.find('.status-cursor') + 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() + } + + setIndent () { + var cookieIndentType = Cookies.get('indent_type') + var cookieTabSize = parseInt(Cookies.get('tab_size')) + var cookieSpaceUnits = parseInt(Cookies.get('space_units')) + if (cookieIndentType) { + if (cookieIndentType === 'tab') { + this.editor.setOption('indentWithTabs', true) + if (cookieTabSize) { + this.editor.setOption('indentUnit', cookieTabSize) + } + } else if (cookieIndentType === 'space') { + this.editor.setOption('indentWithTabs', false) + if (cookieSpaceUnits) { + this.editor.setOption('indentUnit', cookieSpaceUnits) + } + } + } + if (cookieTabSize) { + this.editor.setOption('tabSize', cookieTabSize) + } + + var type = this.statusIndicators.find('.indent-type') + var widthLabel = this.statusIndicators.find('.indent-width-label') + var widthInput = this.statusIndicators.find('.indent-width-input') + + const setType = () => { + if (this.editor.getOption('indentWithTabs')) { + Cookies.set('indent_type', 'tab', { + expires: 365 + }) + type.text('Tab Size:') + } else { + Cookies.set('indent_type', 'space', { + expires: 365 + }) + type.text('Spaces:') + } + } + setType() + + const setUnit = () => { + var unit = this.editor.getOption('indentUnit') + if (this.editor.getOption('indentWithTabs')) { + Cookies.set('tab_size', unit, { + expires: 365 + }) + } else { + Cookies.set('space_units', unit, { + expires: 365 + }) + } + widthLabel.text(unit) + } + setUnit() + + type.click(() => { + if (this.editor.getOption('indentWithTabs')) { + this.editor.setOption('indentWithTabs', false) + cookieSpaceUnits = parseInt(Cookies.get('space_units')) + if (cookieSpaceUnits) { + this.editor.setOption('indentUnit', cookieSpaceUnits) + } + } else { + this.editor.setOption('indentWithTabs', true) + cookieTabSize = parseInt(Cookies.get('tab_size')) + if (cookieTabSize) { + this.editor.setOption('indentUnit', cookieTabSize) + this.editor.setOption('tabSize', cookieTabSize) + } + } + setType() + setUnit() + }) + widthLabel.click(() => { + if (widthLabel.is(':visible')) { + widthLabel.addClass('hidden') + widthInput.removeClass('hidden') + widthInput.val(this.editor.getOption('indentUnit')) + widthInput.select() + } else { + widthLabel.removeClass('hidden') + widthInput.addClass('hidden') + } + }) + widthInput.on('change', () => { + var val = parseInt(widthInput.val()) + if (!val) val = this.editor.getOption('indentUnit') + if (val < 1) val = 1 + else if (val > 10) val = 10 + + if (this.editor.getOption('indentWithTabs')) { + this.editor.setOption('tabSize', val) + } + this.editor.setOption('indentUnit', val) + setUnit() + }) + widthInput.on('blur', function () { + widthLabel.removeClass('hidden') + widthInput.addClass('hidden') + }) + } + + setKeymap () { + var cookieKeymap = Cookies.get('keymap') + if (cookieKeymap) { + this.editor.setOption('keyMap', cookieKeymap) + } + + var label = this.statusIndicators.find('.ui-keymap-label') + var sublime = this.statusIndicators.find('.ui-keymap-sublime') + var emacs = this.statusIndicators.find('.ui-keymap-emacs') + var vim = this.statusIndicators.find('.ui-keymap-vim') + + const setKeymapLabel = () => { + var keymap = this.editor.getOption('keyMap') + Cookies.set('keymap', keymap, { + expires: 365 + }) + label.text(keymap) + this.restoreOverrideEditorKeymap() + this.setOverrideBrowserKeymap() + } + setKeymapLabel() + + sublime.click(() => { + this.editor.setOption('keyMap', 'sublime') + setKeymapLabel() + }) + emacs.click(() => { + this.editor.setOption('keyMap', 'emacs') + setKeymapLabel() + }) + vim.click(() => { + this.editor.setOption('keyMap', 'vim') + setKeymapLabel() + }) + } + + setTheme () { + var cookieTheme = Cookies.get('theme') + if (cookieTheme) { + this.editor.setOption('theme', cookieTheme) + } + + var themeToggle = this.statusTheme.find('.ui-theme-toggle') + + const checkTheme = () => { + var theme = this.editor.getOption('theme') + if (theme === 'one-dark') { + themeToggle.removeClass('active') + } else { + themeToggle.addClass('active') + } + } + + themeToggle.click(() => { + var theme = this.editor.getOption('theme') + if (theme === 'one-dark') { + theme = 'default' + } else { + theme = 'one-dark' + } + this.editor.setOption('theme', theme) + Cookies.set('theme', theme, { + expires: 365 + }) + + checkTheme() + }) + + checkTheme() + } + + setSpellcheck () { + var cookieSpellcheck = Cookies.get('spellcheck') + if (cookieSpellcheck) { + var mode = null + if (cookieSpellcheck === 'true' || cookieSpellcheck === true) { + mode = 'spell-checker' + } else { + mode = defaultEditorMode + } + if (mode && mode !== this.editor.getOption('mode')) { + this.editor.setOption('mode', mode) + } + } + + var spellcheckToggle = this.statusSpellcheck.find('.ui-spellcheck-toggle') + + const checkSpellcheck = () => { + var mode = this.editor.getOption('mode') + if (mode === defaultEditorMode) { + spellcheckToggle.removeClass('active') + } else { + spellcheckToggle.addClass('active') + } + } + + spellcheckToggle.click(() => { + var mode = this.editor.getOption('mode') + if (mode === defaultEditorMode) { + mode = 'spell-checker' + } else { + mode = defaultEditorMode + } + if (mode && mode !== this.editor.getOption('mode')) { + this.editor.setOption('mode', mode) + } + Cookies.set('spellcheck', mode === 'spell-checker', { + expires: 365 + }) + + checkSpellcheck() + }) + + checkSpellcheck() + + // workaround spellcheck might not activate beacuse the ajax loading + if (window.num_loaded < 2) { + var spellcheckTimer = setInterval( + () => { + if (window.num_loaded >= 2) { + if (this.editor.getOption('mode') === 'spell-checker') { + this.editor.setOption('mode', 'spell-checker') + } + clearInterval(spellcheckTimer) + } + }, + 100, + ) + } + } + + resetEditorKeymapToBrowserKeymap () { + var keymap = this.editor.getOption('keyMap') + if (!this.jumpToAddressBarKeymapValue) { + this.jumpToAddressBarKeymapValue = CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] + delete CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] + } + } + + restoreOverrideEditorKeymap () { + var keymap = this.editor.getOption('keyMap') + if (this.jumpToAddressBarKeymapValue) { + CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] = this.jumpToAddressBarKeymapValue + this.jumpToAddressBarKeymapValue = null + } + } + setOverrideBrowserKeymap () { + var overrideBrowserKeymap = $( + '.ui-preferences-override-browser-keymap label > input[type="checkbox"]', + ) + if (overrideBrowserKeymap.is(':checked')) { + Cookies.set('preferences-override-browser-keymap', true, { + expires: 365 + }) + this.restoreOverrideEditorKeymap() + } else { + Cookies.remove('preferences-override-browser-keymap') + this.resetEditorKeymapToBrowserKeymap() + } + } + + setPreferences () { + var overrideBrowserKeymap = $( + '.ui-preferences-override-browser-keymap label > input[type="checkbox"]', + ) + var cookieOverrideBrowserKeymap = Cookies.get( + 'preferences-override-browser-keymap', + ) + if (cookieOverrideBrowserKeymap && cookieOverrideBrowserKeymap === 'true') { + overrideBrowserKeymap.prop('checked', true) + } else { + overrideBrowserKeymap.prop('checked', false) + } + this.setOverrideBrowserKeymap() + + overrideBrowserKeymap.change(() => { + this.setOverrideBrowserKeymap() + }) + } + + init (textit) { + this.editor = CodeMirror.fromTextArea(textit, { + mode: defaultEditorMode, + backdrop: defaultEditorMode, + keyMap: 'sublime', + viewportMargin: viewportMargin, + styleActiveLine: true, + lineNumbers: true, + lineWrapping: true, + showCursorWhenSelecting: true, + highlightSelectionMatches: true, + indentUnit: 4, + continueComments: 'Enter', + theme: 'one-dark', + inputStyle: 'textarea', + matchBrackets: true, + autoCloseBrackets: true, + matchTags: { + bothTags: true + }, + autoCloseTags: true, + foldGutter: true, + gutters: [ + 'CodeMirror-linenumbers', + 'authorship-gutters', + 'CodeMirror-foldgutter' + ], + extraKeys: this.defaultExtraKeys, + flattenSpans: true, + addModeClass: true, + readOnly: true, + autoRefresh: true, + otherCursors: true, + 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 + } + + getEditor () { + return this.editor + } +} diff --git a/public/js/lib/editor/ui-elements.js b/public/js/lib/editor/ui-elements.js new file mode 100644 index 00000000..0d330d77 --- /dev/null +++ b/public/js/lib/editor/ui-elements.js @@ -0,0 +1,86 @@ +/* + * Global UI elements references + */ + +export const getUIElements = () => ({ + spinner: $('.ui-spinner'), + content: $('.ui-content'), + toolbar: { + shortStatus: $('.ui-short-status'), + status: $('.ui-status'), + new: $('.ui-new'), + publish: $('.ui-publish'), + extra: { + revision: $('.ui-extra-revision'), + slide: $('.ui-extra-slide') + }, + download: { + markdown: $('.ui-download-markdown'), + html: $('.ui-download-html'), + rawhtml: $('.ui-download-raw-html'), + pdf: $('.ui-download-pdf-beta') + }, + export: { + dropbox: $('.ui-save-dropbox'), + googleDrive: $('.ui-save-google-drive'), + gist: $('.ui-save-gist'), + snippet: $('.ui-save-snippet') + }, + import: { + dropbox: $('.ui-import-dropbox'), + googleDrive: $('.ui-import-google-drive'), + gist: $('.ui-import-gist'), + snippet: $('.ui-import-snippet'), + clipboard: $('.ui-import-clipboard') + }, + mode: $('.ui-mode'), + edit: $('.ui-edit'), + view: $('.ui-view'), + both: $('.ui-both'), + uploadImage: $('.ui-upload-image') + }, + infobar: { + lastchange: $('.ui-lastchange'), + lastchangeuser: $('.ui-lastchangeuser'), + nolastchangeuser: $('.ui-no-lastchangeuser'), + permission: { + permission: $('.ui-permission'), + label: $('.ui-permission-label'), + freely: $('.ui-permission-freely'), + editable: $('.ui-permission-editable'), + locked: $('.ui-permission-locked'), + private: $('.ui-permission-private'), + limited: $('.ui-permission-limited'), + protected: $('.ui-permission-protected') + }, + delete: $('.ui-delete-note') + }, + toc: { + toc: $('.ui-toc'), + affix: $('.ui-affix-toc'), + label: $('.ui-toc-label'), + dropdown: $('.ui-toc-dropdown') + }, + area: { + edit: $('.ui-edit-area'), + view: $('.ui-view-area'), + codemirror: $('.ui-edit-area .CodeMirror'), + codemirrorScroll: $('.ui-edit-area .CodeMirror .CodeMirror-scroll'), + codemirrorSizer: $('.ui-edit-area .CodeMirror .CodeMirror-sizer'), + codemirrorSizerInner: $( + '.ui-edit-area .CodeMirror .CodeMirror-sizer > div', + ), + markdown: $('.ui-view-area .markdown-body'), + resize: { + handle: $('.ui-resizable-handle'), + syncToggle: $('.ui-sync-toggle') + } + }, + modal: { + snippetImportProjects: $('#snippetImportModalProjects'), + snippetImportSnippets: $('#snippetImportModalSnippets'), + revision: $('#revisionModal') + } +}) + +export default getUIElements diff --git a/public/js/lib/editor/utils.js b/public/js/lib/editor/utils.js new file mode 100644 index 00000000..3702a166 --- /dev/null +++ b/public/js/lib/editor/utils.js @@ -0,0 +1,48 @@ +const wrapSymbols = ['*', '_', '~', '^', '+', '='] +export function wrapTextWith (editor, cm, symbol) { + if (!cm.getSelection()) { + return CodeMirror.Pass + } else { + var ranges = cm.listSelections() + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i] + if (!range.empty()) { + const from = range.from() + const to = range.to() + + if (symbol !== 'Backspace') { + cm.replaceRange(symbol, to, to, '+input') + cm.replaceRange(symbol, from, from, '+input') + // workaround selection range not correct after add symbol + var _ranges = cm.listSelections() + var anchorIndex = editor.indexFromPos(_ranges[i].anchor) + var headIndex = editor.indexFromPos(_ranges[i].head) + if (anchorIndex > headIndex) { + _ranges[i].anchor.ch-- + } else { + _ranges[i].head.ch-- + } + cm.setSelections(_ranges) + } else { + var preEndPos = { + line: to.line, + ch: to.ch + 1 + } + var preText = cm.getRange(to, preEndPos) + var preIndex = wrapSymbols.indexOf(preText) + var postEndPos = { + line: from.line, + ch: from.ch - 1 + } + var postText = cm.getRange(postEndPos, from) + var postIndex = wrapSymbols.indexOf(postText) + // check if surround symbol are list in array and matched + if (preIndex > -1 && postIndex > -1 && preIndex === postIndex) { + cm.replaceRange('', to, preEndPos, '+delete') + cm.replaceRange('', postEndPos, from, '+delete') + } + } + } + } + } +} |