hedgedoc/public/js/lib/editor/index.js
David Mehren faa10da86b 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-07-10 18:40:56 +08:00

651 lines
21 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')
var makeDiagramUMLSequenc = $('#makeDiagramUMLSequenc')
var makeDiagramFlow = $('#makeDiagramFlow')
var makeDiagramGraphviz = $('#makeDiagramGraphviz')
var makeDiagramMermaidFlowchart = $('#makeDiagramMermaidFlowchart')
var makeDiagramMermaidSequence = $('#makeDiagramMermaidSequence')
var makeDiagramMermaidClass = $('#makeDiagramMermaidClass')
var makeDiagramMermaidState = $('#makeDiagramMermaidState')
var makeDiagramMermaidGantt = $('#makeDiagramMermaidGantt')
var makeDiagramMermaidPie = $('#makeDiagramMermaidPie')
var makeDiagramAbcMusic = $('#makeDiagramAbcMusic')
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)
})
makeDiagramUMLSequenc.click(() => {
utils.insertText(this.editor, '```sequence\nAlice->Bob: Hello Bob, how are you?\nNote right of Bob: Bob thinks\nBob-->Alice: I am good thanks!\nNote left of Alice: Alice responds\nAlice->Bob: Where have you been?\n```\n')
})
makeDiagramFlow.click(() => {
utils.insertText(this.editor, '```flow\nst=>start: Start\ne=>end: End\nop=>operation: My Operation\nop2=>operation: lalala\ncond=>condition: Yes or No?\n\nst->op->op2->cond\ncond(yes)->e\ncond(no)->op2\n```\n')
})
makeDiagramGraphviz.click(() => {
utils.insertText(this.editor, '```graphviz\ndigraph hierarchy {\nnodesep=1.0 // Increases the separation between nodes\n\nnode [color=Red,fontname=Courier,shape=box] // All nodes will this shape and colour\nedge [color=Blue, style=dashed] // All the lines look like this\n\nHeadteacher->{Deputy1 Deputy2 BusinessManager}\nDeputy1->{Teacher1 Teacher2}\nBusinessManager->ITManager\n{rank=same;ITManager Teacher1 Teacher2} // Put them on the same level\n}\n```\n')
})
makeDiagramMermaidFlowchart.click(() => {
utils.insertText(this.editor, '```mermaid\ngraph TB\nc1-->a2\nsubgraph one\na1-->a2\nend\nsubgraph two\nb1-->b2\nend\nc1-->c2\n```\n')
})
makeDiagramMermaidSequence.click(() => {
utils.insertText(this.editor, '```mermaid\nsequenceDiagram\nAlice->>John: Hello John, how are you?\nJohn-->>Alice: Great!\n```')
})
makeDiagramMermaidClass.click(() => {
utils.insertText(this.editor, '```mermaid\nclassDiagram\nAnimal <|-- Duck\nAnimal <|-- Fish\nAnimal <|-- Zebra\nAnimal : +int age\nAnimal : +String gender\nAnimal: +isMammal()\nAnimal: +mate()\nclass Duck{\n+String beakColor\n+swim()\n+quack()\n}\nclass Fish{\n-int sizeInFeet\n-canEat()\n}\nclass Zebra{\n+bool is_wild\n+run()\n}\n```')
})
makeDiagramMermaidState.click(() => {
utils.insertText(this.editor, '```mermaid\nstateDiagram\n[*] --> Still\nStill --> [*]\n\nStill --> Moving\nMoving --> Still\nMoving --> Crash\nCrash --> [*]\n```')
})
makeDiagramMermaidGantt.click(() => {
utils.insertText(this.editor, '```mermaid\ngantt\ntitle A Gantt Diagram\n\nsection Section\nA task: a1, 2014-01-01, 30d\nAnother task: after a1, 20d\n\nsection Another\nTask in sec: 2014-01-12, 12d\nAnother task: 24d\n```\n')
})
makeDiagramMermaidPie.click(() => {
utils.insertText(this.editor, '```mermaid\npie title Pets adopted by volunteers\n"Dogs" : 386\n"Cats" : 85\n"Rats" : 15\n```')
})
makeDiagramAbcMusic.click(() => {
utils.insertText(this.editor, '```abc\nX:1\nT:Speed the Plough\nM:4/4\nC:Trad.\nK:G\n|:GABc dedB|dedB dedB|c2ec B2dB|c2A2 A2BA|\nGABc dedB|dedB dedB|c2ec B2dB|A2F2 G4:|\n|:g2gf gdBd|g2f2 e2d2|c2ec B2dB|c2A2 A2df|\ng2gf g2Bd|g2f2 e2d2|c2ec B2dB|A2F2 G4:|\n```\n')
})
}
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
}
}