hedgedoc/public/js/lib/editor/index.js
David Mehren e77e7b165a
Set all cookies with sameSite: strict
Modern browsers do not support (or will stop supporting) sameSite: none (or no sameSite attribute) without the Secure flag. As we don't want everyone to be able to make requests with our cookies anyway, this commit sets sameSite to strict. See https://developer.mozilla.org/de/docs/Web/HTTP/Headers/Set-Cookie/SameSite

Signed-off-by: David Mehren <dmehren1@gmail.com>
2020-06-08 15:27:31 +02:00

610 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) {
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',
'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 () {
var inlineAttach = inlineAttachment.editors.codemirror4.attach(this.editor)
this.toolBar = $(toolBarTemplate)
this.toolbarPanel = this.editor.addPanel(this.toolBar[0], {
position: 'top'
})
var makeBold = $('#makeBold')
var makeItalic = $('#makeItalic')
var makeStrike = $('#makeStrike')
var makeHeader = $('#makeHeader')
var makeCode = $('#makeCode')
var makeQuote = $('#makeQuote')
var makeGenericList = $('#makeGenericList')
var makeOrderedList = $('#makeOrderedList')
var makeCheckList = $('#makeCheckList')
var makeLink = $('#makeLink')
var makeImage = $('#makeImage')
var makeTable = $('#makeTable')
var makeLine = $('#makeLine')
var makeComment = $('#makeComment')
var 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) {
var 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
var cursor = this.editor.getCursor()
var cursorText = 'Line ' + (cursor.line + 1) + ', Columns ' + (cursor.ch + 1)
this.statusCursor.text(cursorText)
var fileText = ' — ' + editor.lineCount() + ' Lines'
this.statusFile.text(fileText)
var 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 () {
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,
sameSite: 'strict'
})
type.text('Tab Size:')
} else {
Cookies.set('indent_type', 'space', {
expires: 365,
sameSite: 'strict'
})
type.text('Spaces:')
}
}
setType()
const setUnit = () => {
var unit = this.editor.getOption('indentUnit')
if (this.editor.getOption('indentWithTabs')) {
Cookies.set('tab_size', unit, {
expires: 365,
sameSite: 'strict'
})
} else {
Cookies.set('space_units', unit, {
expires: 365,
sameSite: 'strict'
})
}
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,
sameSite: 'strict'
})
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,
sameSite: 'strict'
})
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,
sameSite: 'strict'
})
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,
sameSite: 'strict'
})
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 :)"
})
return this.editor
}
getEditor () {
return this.editor
}
}