mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-12-01 14:32:03 -05:00
e77e7b165a
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>
610 lines
18 KiB
JavaScript
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
|
|
}
|
|
}
|