Merge pull request #387 from hackmdio/cm-refactor

Extract CodeMirror instance
This commit is contained in:
Max Wu 2017-03-14 23:11:56 +08:00 committed by GitHub
commit f6bd238b0f
6 changed files with 683 additions and 563 deletions

View file

@ -3,7 +3,7 @@ root = true
# Tab indentation # Tab indentation
[*] [*]
indent_style = space indent_style = space
indent_size = 4 indent_size = 2
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true

View file

@ -169,6 +169,27 @@
"webpack-parallel-uglify-plugin": "^0.2.0" "webpack-parallel-uglify-plugin": "^0.2.0"
}, },
"standard": { "standard": {
"globals": [
"$",
"CodeMirror",
"Cookies",
"moment", "editor",
"ui",
"Spinner",
"modeType",
"Idle",
"serverurl",
"key",
"gapi",
"Dropbox",
"FilePicker",
"ot",
"MediaUploader",
"hex2rgb",
"num_loaded",
"Visibility",
"inlineAttachment"
],
"ignore": [ "ignore": [
"lib/ot", "lib/ot",
"public/vendor" "public/vendor"

View file

@ -78,141 +78,13 @@ import {
var renderer = require('./render') var renderer = require('./render')
var preventXSS = renderer.preventXSS var preventXSS = renderer.preventXSS
import Editor from './lib/editor'
import getUIElements from './lib/editor/ui-elements'
var defaultTextHeight = 20 var defaultTextHeight = 20
var viewportMargin = 20 var viewportMargin = 20
var mac = CodeMirror.keyMap['default'] === CodeMirror.keyMap.macDefault
var defaultEditorMode = 'gfm' var defaultEditorMode = 'gfm'
var 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'
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)
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 (!mac && cm.getOption('keyMap').substr(0, 3) === 'vim') document.execCommand('copy')
else return CodeMirror.Pass
},
'Ctrl-*': function (cm) {
wrapTextWith(cm, '*')
},
'Shift-Ctrl-8': function (cm) {
wrapTextWith(cm, '*')
},
'Ctrl-_': function (cm) {
wrapTextWith(cm, '_')
},
'Shift-Ctrl--': function (cm) {
wrapTextWith(cm, '_')
},
'Ctrl-~': function (cm) {
wrapTextWith(cm, '~')
},
'Shift-Ctrl-`': function (cm) {
wrapTextWith(cm, '~')
},
'Ctrl-^': function (cm) {
wrapTextWith(cm, '^')
},
'Shift-Ctrl-6': function (cm) {
wrapTextWith(cm, '^')
},
'Ctrl-+': function (cm) {
wrapTextWith(cm, '+')
},
'Shift-Ctrl-=': function (cm) {
wrapTextWith(cm, '+')
},
'Ctrl-=': function (cm) {
wrapTextWith(cm, '=')
},
'Shift-Ctrl-Backspace': function (cm) {
wrapTextWith(cm, 'Backspace')
}
}
var wrapSymbols = ['*', '_', '~', '^', '+', '=']
function wrapTextWith (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()) {
var from = range.from()
var 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 = window.editor.indexFromPos(_ranges[i].anchor)
var headIndex = window.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')
}
}
}
}
}
}
var idleTime = 300000 // 5 mins var idleTime = 300000 // 5 mins
var updateViewDebounce = 100 var updateViewDebounce = 100
@ -435,343 +307,23 @@ window.fileTypes = {
// editor settings // editor settings
var textit = document.getElementById('textit') var textit = document.getElementById('textit')
if (!textit) throw new Error('There was no textit area!') if (!textit) {
window.editor = CodeMirror.fromTextArea(textit, { throw new Error('There was no textit area!')
mode: defaultEditorMode, }
backdrop: defaultEditorMode,
keyMap: 'sublime', const editorInstance = new Editor()
viewportMargin: viewportMargin, var editor = editorInstance.init(textit)
styleActiveLine: true,
lineNumbers: true, // TODO: global referncing in jquery-textcomplete patch
lineWrapping: true, window.editor = editor
showCursorWhenSelecting: true,
highlightSelectionMatches: true, var inlineAttach = inlineAttachment.editors.codemirror4.attach(editor)
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: 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 :)"
})
var inlineAttach = window.inlineAttachment.editors.codemirror4.attach(editor)
defaultTextHeight = parseInt($('.CodeMirror').css('line-height')) defaultTextHeight = parseInt($('.CodeMirror').css('line-height'))
var statusBarTemplate = null
var statusBar = null
var statusCursor = null
var statusFile = null
var statusIndicators = null
var statusLength = null
var statusTheme = null
var statusSpellcheck = null
function getStatusBarTemplate (callback) {
$.get(serverurl + '/views/statusbar.html', function (template) {
statusBarTemplate = template
if (callback) callback()
})
}
getStatusBarTemplate()
function addStatusBar () {
if (!statusBarTemplate) {
getStatusBarTemplate(addStatusBar)
return
}
statusBar = $(statusBarTemplate)
statusCursor = statusBar.find('.status-cursor')
statusFile = statusBar.find('.status-file')
statusIndicators = statusBar.find('.status-indicators')
statusBar.find('.status-indent')
statusBar.find('.status-keymap')
statusLength = statusBar.find('.status-length')
statusTheme = statusBar.find('.status-theme')
statusSpellcheck = statusBar.find('.status-spellcheck')
statusBar.find('.status-preferences')
editor.addPanel(statusBar[0], {
position: 'bottom'
})
setIndent()
setKeymap()
setTheme()
setSpellcheck()
setPreferences()
}
function 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') {
editor.setOption('indentWithTabs', true)
if (cookieTabSize) { editor.setOption('indentUnit', cookieTabSize) }
} else if (cookieIndentType === 'space') {
editor.setOption('indentWithTabs', false)
if (cookieSpaceUnits) { editor.setOption('indentUnit', cookieSpaceUnits) }
}
}
if (cookieTabSize) { editor.setOption('tabSize', cookieTabSize) }
var type = statusIndicators.find('.indent-type')
var widthLabel = statusIndicators.find('.indent-width-label')
var widthInput = statusIndicators.find('.indent-width-input')
function setType () {
if (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()
function setUnit () {
var unit = editor.getOption('indentUnit')
if (editor.getOption('indentWithTabs')) {
Cookies.set('tab_size', unit, {
expires: 365
})
} else {
Cookies.set('space_units', unit, {
expires: 365
})
}
widthLabel.text(unit)
}
setUnit()
type.click(function () {
if (editor.getOption('indentWithTabs')) {
editor.setOption('indentWithTabs', false)
cookieSpaceUnits = parseInt(Cookies.get('space_units'))
if (cookieSpaceUnits) { editor.setOption('indentUnit', cookieSpaceUnits) }
} else {
editor.setOption('indentWithTabs', true)
cookieTabSize = parseInt(Cookies.get('tab_size'))
if (cookieTabSize) {
editor.setOption('indentUnit', cookieTabSize)
editor.setOption('tabSize', cookieTabSize)
}
}
setType()
setUnit()
})
widthLabel.click(function () {
if (widthLabel.is(':visible')) {
widthLabel.addClass('hidden')
widthInput.removeClass('hidden')
widthInput.val(editor.getOption('indentUnit'))
widthInput.select()
} else {
widthLabel.removeClass('hidden')
widthInput.addClass('hidden')
}
})
widthInput.on('change', function () {
var val = parseInt(widthInput.val())
if (!val) val = editor.getOption('indentUnit')
if (val < 1) val = 1
else if (val > 10) val = 10
if (editor.getOption('indentWithTabs')) {
editor.setOption('tabSize', val)
}
editor.setOption('indentUnit', val)
setUnit()
})
widthInput.on('blur', function () {
widthLabel.removeClass('hidden')
widthInput.addClass('hidden')
})
}
function setKeymap () {
var cookieKeymap = Cookies.get('keymap')
if (cookieKeymap) { editor.setOption('keyMap', cookieKeymap) }
var label = statusIndicators.find('.ui-keymap-label')
var sublime = statusIndicators.find('.ui-keymap-sublime')
var emacs = statusIndicators.find('.ui-keymap-emacs')
var vim = statusIndicators.find('.ui-keymap-vim')
function setKeymapLabel () {
var keymap = editor.getOption('keyMap')
Cookies.set('keymap', keymap, {
expires: 365
})
label.text(keymap)
restoreOverrideEditorKeymap()
setOverrideBrowserKeymap()
}
setKeymapLabel()
sublime.click(function () {
editor.setOption('keyMap', 'sublime')
setKeymapLabel()
})
emacs.click(function () {
editor.setOption('keyMap', 'emacs')
setKeymapLabel()
})
vim.click(function () {
editor.setOption('keyMap', 'vim')
setKeymapLabel()
})
}
function setTheme () {
var cookieTheme = Cookies.get('theme')
if (cookieTheme) {
editor.setOption('theme', cookieTheme)
}
var themeToggle = statusTheme.find('.ui-theme-toggle')
themeToggle.click(function () {
var theme = editor.getOption('theme')
if (theme === 'one-dark') {
theme = 'default'
} else {
theme = 'one-dark'
}
editor.setOption('theme', theme)
Cookies.set('theme', theme, {
expires: 365
})
checkTheme()
})
function checkTheme () {
var theme = editor.getOption('theme')
if (theme === 'one-dark') {
themeToggle.removeClass('active')
} else {
themeToggle.addClass('active')
}
}
checkTheme()
}
function 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 !== editor.getOption('mode')) {
editor.setOption('mode', mode)
}
}
var spellcheckToggle = statusSpellcheck.find('.ui-spellcheck-toggle')
spellcheckToggle.click(function () {
var mode = editor.getOption('mode')
if (mode === defaultEditorMode) {
mode = 'spell-checker'
} else {
mode = defaultEditorMode
}
if (mode && mode !== editor.getOption('mode')) {
editor.setOption('mode', mode)
}
Cookies.set('spellcheck', (mode === 'spell-checker'), {
expires: 365
})
checkSpellcheck()
})
function checkSpellcheck () {
var mode = editor.getOption('mode')
if (mode === defaultEditorMode) {
spellcheckToggle.removeClass('active')
} else {
spellcheckToggle.addClass('active')
}
}
checkSpellcheck()
// workaround spellcheck might not activate beacuse the ajax loading
/* eslint-disable camelcase */
if (num_loaded < 2) {
var spellcheckTimer = setInterval(function () {
if (num_loaded >= 2) {
if (editor.getOption('mode') === 'spell-checker') { editor.setOption('mode', 'spell-checker') }
clearInterval(spellcheckTimer)
}
}, 100)
}
/* eslint-endable camelcase */
}
var jumpToAddressBarKeymapName = mac ? 'Cmd-L' : 'Ctrl-L'
var jumpToAddressBarKeymapValue = null
function resetEditorKeymapToBrowserKeymap () {
var keymap = editor.getOption('keyMap')
if (!jumpToAddressBarKeymapValue) {
jumpToAddressBarKeymapValue = CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName]
delete CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName]
}
}
function restoreOverrideEditorKeymap () {
var keymap = editor.getOption('keyMap')
if (jumpToAddressBarKeymapValue) {
CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] = jumpToAddressBarKeymapValue
jumpToAddressBarKeymapValue = null
}
}
function 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
})
restoreOverrideEditorKeymap()
} else {
Cookies.remove('preferences-override-browser-keymap')
resetEditorKeymapToBrowserKeymap()
}
}
function 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)
}
setOverrideBrowserKeymap()
overrideBrowserKeymap.change(function () {
setOverrideBrowserKeymap()
})
}
var selection = null var selection = null
function updateStatusBar () { function updateStatusBar () {
if (!statusBar) return if (!editorInstance.statusBar) return
var cursor = editor.getCursor() var cursor = editor.getCursor()
var cursorText = 'Line ' + (cursor.line + 1) + ', Columns ' + (cursor.ch + 1) var cursorText = 'Line ' + (cursor.line + 1) + ', Columns ' + (cursor.ch + 1)
if (selection) { if (selection) {
@ -788,105 +340,34 @@ function updateStatusBar () {
lines-- lines--
} }
selectionText += lines + ' lines' selectionText += lines + ' lines'
} else if (selectionCharCount > 0) { selectionText += selectionCharCount + ' columns' } } else if (selectionCharCount > 0) {
if (start.line !== end.line || selectionCharCount > 0) { cursorText += selectionText } selectionText += selectionCharCount + ' columns'
} }
statusCursor.text(cursorText) if (start.line !== end.line || selectionCharCount > 0) {
cursorText += selectionText
}
}
editorInstance.statusCursor.text(cursorText)
var fileText = ' — ' + editor.lineCount() + ' Lines' var fileText = ' — ' + editor.lineCount() + ' Lines'
statusFile.text(fileText) editorInstance.statusFile.text(fileText)
var docLength = editor.getValue().length var docLength = editor.getValue().length
statusLength.text('Length ' + docLength) editorInstance.statusLength.text('Length ' + docLength)
if (docLength > (docmaxlength * 0.95)) { if (docLength > (docmaxlength * 0.95)) {
statusLength.css('color', 'red') editorInstance.statusLength.css('color', 'red')
statusLength.attr('title', 'Your almost reach note max length limit.') editorInstance.statusLength.attr('title', 'Your almost reach note max length limit.')
} else if (docLength > (docmaxlength * 0.8)) { } else if (docLength > (docmaxlength * 0.8)) {
statusLength.css('color', 'orange') editorInstance.statusLength.css('color', 'orange')
statusLength.attr('title', 'You nearly fill the note, consider to make more pieces.') editorInstance.statusLength.attr('title', 'You nearly fill the note, consider to make more pieces.')
} else { } else {
statusLength.css('color', 'white') editorInstance.statusLength.css('color', 'white')
statusLength.attr('title', 'You could write up to ' + docmaxlength + ' characters in this note.') editorInstance.statusLength.attr('title', 'You could write up to ' + docmaxlength + ' characters in this note.')
} }
} }
// ui vars // initalize ui reference
window.ui = { // TODO: fix ui exporting
spinner: $('.ui-spinner'), const ui = getUIElements()
content: $('.ui-content'), window.ui = ui
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')
}
}
// page actions // page actions
var opts = { var opts = {
@ -1135,7 +616,7 @@ var lastEditorWidth = 0
var previousFocusOnEditor = null var previousFocusOnEditor = null
function checkEditorStyle () { function checkEditorStyle () {
var desireHeight = statusBar ? (ui.area.edit.height() - statusBar.outerHeight()) : ui.area.edit.height() var desireHeight = editorInstance.statusBar ? (ui.area.edit.height() - editorInstance.statusBar.outerHeight()) : ui.area.edit.height()
// set editor height and min height based on scrollbar style and mode // set editor height and min height based on scrollbar style and mode
var scrollbarStyle = editor.getOption('scrollbarStyle') var scrollbarStyle = editor.getOption('scrollbarStyle')
if (scrollbarStyle === 'overlay' || window.currentMode === modeType.both) { if (scrollbarStyle === 'overlay' || window.currentMode === modeType.both) {
@ -1370,8 +851,8 @@ function changeMode (type) {
if (window.currentMode === modeType.edit || window.currentMode === modeType.both) { if (window.currentMode === modeType.edit || window.currentMode === modeType.both) {
ui.toolbar.uploadImage.fadeIn() ui.toolbar.uploadImage.fadeIn()
// add and update status bar // add and update status bar
if (!statusBar) { if (!editorInstance.statusBar) {
addStatusBar() editorInstance.addStatusBar()
updateStatusBar() updateStatusBar()
} }
// work around foldGutter might not init properly // work around foldGutter might not init properly
@ -1388,7 +869,11 @@ function changeMode (type) {
} }
// check resizable editor style // check resizable editor style
if (window.currentMode === modeType.both) { if (window.currentMode === modeType.both) {
if (lastEditorWidth > 0) { ui.area.edit.css('width', lastEditorWidth + 'px') } else { ui.area.edit.css('width', '') } if (lastEditorWidth > 0) {
ui.area.edit.css('width', lastEditorWidth + 'px')
} else {
ui.area.edit.css('width', '')
}
ui.area.resize.handle.show() ui.area.resize.handle.show()
} else { } else {
ui.area.edit.css('width', '') ui.area.edit.css('width', '')
@ -3924,6 +3409,6 @@ $(editor.getInputField())
}, },
'textComplete:hide': function (e) { 'textComplete:hide': function (e) {
$(this).data('autocompleting', false) $(this).data('autocompleting', false)
editor.setOption('extraKeys', defaultExtraKeys) editor.setOption('extraKeys', editorInstance.defaultExtraKeys)
} }
}) })

View file

@ -0,0 +1,480 @@
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.jumpToAddressBarKeymapValue = 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')
}
}
}
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
}
}

View file

@ -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

View file

@ -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')
}
}
}
}
}
}