mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-29 21:34:32 -05:00
136d895d15
Signed-off-by: Philip Molares <philip.molares@udo.edu>
611 lines
18 KiB
JavaScript
611 lines
18 KiB
JavaScript
import * as utils from './utils'
|
|
import config from './config'
|
|
import statusBarTemplate from './statusbar.html'
|
|
import toolBarTemplate from './toolbar.html'
|
|
|
|
import '../../../css/ui/toolbar.css'
|
|
|
|
/* 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('fullScreen') && !(cm.getOption('keyMap').substr(0, 3) === 'vim')) {
|
|
cm.setOption('fullScreen', false)
|
|
} else {
|
|
return CodeMirror.Pass
|
|
}
|
|
},
|
|
'Cmd-S': function () {
|
|
return false
|
|
},
|
|
'Ctrl-S': function () {
|
|
return false
|
|
},
|
|
Enter: 'newlineAndIndentContinueMarkdownList',
|
|
Tab: function (cm) {
|
|
const tab = '\t'
|
|
|
|
// contruct x length spaces
|
|
const spaces = Array(parseInt(cm.getOption('indentUnit')) + 1).join(' ')
|
|
|
|
// auto indent whole line when in list or blockquote
|
|
const cursor = cm.getCursor()
|
|
const 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)"
|
|
const regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/
|
|
|
|
let match
|
|
const multiple = cm.getSelection().split('\n').length > 1 ||
|
|
cm.getSelections().length > 1
|
|
|
|
if (multiple) {
|
|
cm.execCommand('defaultTab')
|
|
} else if ((match = regex.exec(line)) !== null) {
|
|
const ch = match[1].length
|
|
const 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',
|
|
Home: 'goLineLeftSmart',
|
|
End: '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.eventListeners = {}
|
|
this.config = config
|
|
}
|
|
|
|
on (event, cb) {
|
|
if (!this.eventListeners[event]) {
|
|
this.eventListeners[event] = [cb]
|
|
} else {
|
|
this.eventListeners[event].push(cb)
|
|
}
|
|
|
|
this.editor.on(event, (...args) => {
|
|
this.eventListeners[event].forEach(cb => cb.bind(this)(...args))
|
|
})
|
|
}
|
|
|
|
addToolBar () {
|
|
const inlineAttach = inlineAttachment.editors.codemirror4.attach(this.editor)
|
|
this.toolBar = $(toolBarTemplate)
|
|
this.toolbarPanel = this.editor.addPanel(this.toolBar[0], {
|
|
position: 'top'
|
|
})
|
|
|
|
const makeBold = $('#makeBold')
|
|
const makeItalic = $('#makeItalic')
|
|
const makeStrike = $('#makeStrike')
|
|
const makeHeader = $('#makeHeader')
|
|
const makeCode = $('#makeCode')
|
|
const makeQuote = $('#makeQuote')
|
|
const makeGenericList = $('#makeGenericList')
|
|
const makeOrderedList = $('#makeOrderedList')
|
|
const makeCheckList = $('#makeCheckList')
|
|
const makeLink = $('#makeLink')
|
|
const makeImage = $('#makeImage')
|
|
const makeTable = $('#makeTable')
|
|
const makeLine = $('#makeLine')
|
|
const makeComment = $('#makeComment')
|
|
const uploadImage = $('#uploadImage')
|
|
|
|
makeBold.click(() => {
|
|
utils.wrapTextWith(this.editor, this.editor, '**')
|
|
this.editor.focus()
|
|
})
|
|
|
|
makeItalic.click(() => {
|
|
utils.wrapTextWith(this.editor, this.editor, '*')
|
|
this.editor.focus()
|
|
})
|
|
|
|
makeStrike.click(() => {
|
|
utils.wrapTextWith(this.editor, this.editor, '~~')
|
|
this.editor.focus()
|
|
})
|
|
|
|
makeHeader.click(() => {
|
|
utils.insertHeader(this.editor)
|
|
})
|
|
|
|
makeCode.click(() => {
|
|
utils.wrapTextWith(this.editor, this.editor, '```')
|
|
this.editor.focus()
|
|
})
|
|
|
|
makeQuote.click(() => {
|
|
utils.insertOnStartOfLines(this.editor, '> ')
|
|
})
|
|
|
|
makeGenericList.click(() => {
|
|
utils.insertOnStartOfLines(this.editor, '* ')
|
|
})
|
|
|
|
makeOrderedList.click(() => {
|
|
utils.insertOnStartOfLines(this.editor, '1. ')
|
|
})
|
|
|
|
makeCheckList.click(() => {
|
|
utils.insertOnStartOfLines(this.editor, '- [ ] ')
|
|
})
|
|
|
|
makeLink.click(() => {
|
|
utils.insertLink(this.editor, false)
|
|
})
|
|
|
|
makeImage.click(() => {
|
|
utils.insertLink(this.editor, true)
|
|
})
|
|
|
|
makeTable.click(() => {
|
|
utils.insertText(this.editor, '\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n')
|
|
})
|
|
|
|
makeLine.click(() => {
|
|
utils.insertText(this.editor, '\n----\n')
|
|
})
|
|
|
|
makeComment.click(() => {
|
|
utils.insertText(this.editor, '> []')
|
|
})
|
|
|
|
uploadImage.bind('change', function (e) {
|
|
const files = e.target.files || e.dataTransfer.files
|
|
e.dataTransfer = {}
|
|
e.dataTransfer.files = files
|
|
inlineAttach.onDrop(e)
|
|
})
|
|
}
|
|
|
|
addStatusBar () {
|
|
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()
|
|
}
|
|
|
|
updateStatusBar () {
|
|
if (!this.statusBar) return
|
|
|
|
const cursor = this.editor.getCursor()
|
|
const cursorText = 'Line ' + (cursor.line + 1) + ', Columns ' + (cursor.ch + 1)
|
|
this.statusCursor.text(cursorText)
|
|
const fileText = ' — ' + editor.lineCount() + ' Lines'
|
|
this.statusFile.text(fileText)
|
|
const docLength = editor.getValue().length
|
|
this.statusLength.text('Length ' + docLength)
|
|
if (docLength > (config.docmaxlength * 0.95)) {
|
|
this.statusLength.css('color', 'red')
|
|
this.statusLength.attr('title', 'You have almost reached the limit for this document.')
|
|
} else if (docLength > (config.docmaxlength * 0.8)) {
|
|
this.statusLength.css('color', 'orange')
|
|
this.statusLength.attr('title', 'This document is nearly full, consider splitting it or creating a new one.')
|
|
} else {
|
|
this.statusLength.css('color', 'white')
|
|
this.statusLength.attr('title', 'You can write up to ' + config.docmaxlength + ' characters in this document.')
|
|
}
|
|
}
|
|
|
|
setIndent () {
|
|
const cookieIndentType = Cookies.get('indent_type')
|
|
let cookieTabSize = parseInt(Cookies.get('tab_size'))
|
|
let 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)
|
|
}
|
|
|
|
const type = this.statusIndicators.find('.indent-type')
|
|
const widthLabel = this.statusIndicators.find('.indent-width-label')
|
|
const widthInput = this.statusIndicators.find('.indent-width-input')
|
|
|
|
const setType = () => {
|
|
if (this.editor.getOption('indentWithTabs')) {
|
|
Cookies.set('indent_type', 'tab', {
|
|
expires: 365,
|
|
sameSite: window.cookiePolicy
|
|
})
|
|
type.text('Tab Size:')
|
|
} else {
|
|
Cookies.set('indent_type', 'space', {
|
|
expires: 365,
|
|
sameSite: window.cookiePolicy
|
|
})
|
|
type.text('Spaces:')
|
|
}
|
|
}
|
|
setType()
|
|
|
|
const setUnit = () => {
|
|
const unit = this.editor.getOption('indentUnit')
|
|
if (this.editor.getOption('indentWithTabs')) {
|
|
Cookies.set('tab_size', unit, {
|
|
expires: 365,
|
|
sameSite: window.cookiePolicy
|
|
})
|
|
} else {
|
|
Cookies.set('space_units', unit, {
|
|
expires: 365,
|
|
sameSite: window.cookiePolicy
|
|
})
|
|
}
|
|
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', () => {
|
|
let 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 () {
|
|
const cookieKeymap = Cookies.get('keymap')
|
|
if (cookieKeymap) {
|
|
this.editor.setOption('keyMap', cookieKeymap)
|
|
}
|
|
|
|
const label = this.statusIndicators.find('.ui-keymap-label')
|
|
const sublime = this.statusIndicators.find('.ui-keymap-sublime')
|
|
const emacs = this.statusIndicators.find('.ui-keymap-emacs')
|
|
const vim = this.statusIndicators.find('.ui-keymap-vim')
|
|
|
|
const setKeymapLabel = () => {
|
|
const keymap = this.editor.getOption('keyMap')
|
|
Cookies.set('keymap', keymap, {
|
|
expires: 365,
|
|
sameSite: window.cookiePolicy
|
|
})
|
|
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 () {
|
|
const cookieTheme = Cookies.get('theme')
|
|
if (cookieTheme) {
|
|
this.editor.setOption('theme', cookieTheme)
|
|
}
|
|
|
|
const themeToggle = this.statusTheme.find('.ui-theme-toggle')
|
|
|
|
const checkTheme = () => {
|
|
const theme = this.editor.getOption('theme')
|
|
if (theme === 'one-dark') {
|
|
themeToggle.removeClass('active')
|
|
} else {
|
|
themeToggle.addClass('active')
|
|
}
|
|
}
|
|
|
|
themeToggle.click(() => {
|
|
let 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,
|
|
sameSite: window.cookiePolicy
|
|
})
|
|
|
|
checkTheme()
|
|
})
|
|
|
|
checkTheme()
|
|
}
|
|
|
|
setSpellcheck () {
|
|
const cookieSpellcheck = Cookies.get('spellcheck')
|
|
if (cookieSpellcheck) {
|
|
let 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)
|
|
}
|
|
}
|
|
|
|
const spellcheckToggle = this.statusSpellcheck.find('.ui-spellcheck-toggle')
|
|
|
|
const checkSpellcheck = () => {
|
|
const mode = this.editor.getOption('mode')
|
|
if (mode === defaultEditorMode) {
|
|
spellcheckToggle.removeClass('active')
|
|
} else {
|
|
spellcheckToggle.addClass('active')
|
|
}
|
|
}
|
|
|
|
spellcheckToggle.click(() => {
|
|
let 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,
|
|
sameSite: window.cookiePolicy
|
|
})
|
|
|
|
checkSpellcheck()
|
|
})
|
|
|
|
checkSpellcheck()
|
|
|
|
// workaround spellcheck might not activate beacuse the ajax loading
|
|
if (window.num_loaded < 2) {
|
|
const spellcheckTimer = setInterval(
|
|
() => {
|
|
if (window.num_loaded >= 2) {
|
|
if (this.editor.getOption('mode') === 'spell-checker') {
|
|
this.editor.setOption('mode', 'spell-checker')
|
|
}
|
|
clearInterval(spellcheckTimer)
|
|
}
|
|
},
|
|
100
|
|
)
|
|
}
|
|
}
|
|
|
|
resetEditorKeymapToBrowserKeymap () {
|
|
const keymap = this.editor.getOption('keyMap')
|
|
if (!this.jumpToAddressBarKeymapValue) {
|
|
this.jumpToAddressBarKeymapValue = CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName]
|
|
delete CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName]
|
|
}
|
|
}
|
|
|
|
restoreOverrideEditorKeymap () {
|
|
const keymap = this.editor.getOption('keyMap')
|
|
if (this.jumpToAddressBarKeymapValue) {
|
|
CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] = this.jumpToAddressBarKeymapValue
|
|
this.jumpToAddressBarKeymapValue = null
|
|
}
|
|
}
|
|
|
|
setOverrideBrowserKeymap () {
|
|
const overrideBrowserKeymap = $(
|
|
'.ui-preferences-override-browser-keymap label > input[type="checkbox"]'
|
|
)
|
|
if (overrideBrowserKeymap.is(':checked')) {
|
|
Cookies.set('preferences-override-browser-keymap', true, {
|
|
expires: 365,
|
|
sameSite: window.cookiePolicy
|
|
})
|
|
this.restoreOverrideEditorKeymap()
|
|
} else {
|
|
Cookies.remove('preferences-override-browser-keymap')
|
|
this.resetEditorKeymapToBrowserKeymap()
|
|
}
|
|
}
|
|
|
|
setPreferences () {
|
|
const overrideBrowserKeymap = $(
|
|
'.ui-preferences-override-browser-keymap label > input[type="checkbox"]'
|
|
)
|
|
const 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 :)"
|
|
})
|
|
|
|
return this.editor
|
|
}
|
|
|
|
getEditor () {
|
|
return this.editor
|
|
}
|
|
}
|