cledit refactoring

This commit is contained in:
benweet 2018-02-16 08:38:33 +00:00
parent efce97c16c
commit f0721c9405
18 changed files with 1049 additions and 1037 deletions

View File

@ -65,6 +65,7 @@ export default {
methods: { methods: {
computeText() { computeText() {
setTimeout(() => {
this.textSelection = false; this.textSelection = false;
let text = editorSvc.clEditor.getContent(); let text = editorSvc.clEditor.getContent();
const beforeText = text.slice(0, editorSvc.clEditor.selectionMgr.selectionEnd); const beforeText = text.slice(0, editorSvc.clEditor.selectionMgr.selectionEnd);
@ -80,8 +81,10 @@ export default {
this.textStats.forEach((stat) => { this.textStats.forEach((stat) => {
stat.value = (text.match(stat.regex) || []).length; stat.value = (text.match(stat.regex) || []).length;
}); });
}, 10);
}, },
computeHtml() { computeHtml() {
setTimeout(() => {
let text; let text;
if (editorSvc.previewSelectionRange) { if (editorSvc.previewSelectionRange) {
text = `${editorSvc.previewSelectionRange}`; text = `${editorSvc.previewSelectionRange}`;
@ -96,6 +99,7 @@ export default {
stat.value = (text.match(stat.regex) || []).length; stat.value = (text.match(stat.regex) || []).length;
}); });
} }
}, 10);
}, },
}, },
}; };

View File

@ -5,7 +5,7 @@
</div> </div>
<div class="side-bar__info" v-if="syncLocations.length"> <div class="side-bar__info" v-if="syncLocations.length">
<p><b>{{currentFileName}}</b> is already synchronized.</p> <p><b>{{currentFileName}}</b> is already synchronized.</p>
<menu-entry v-if="isSyncPossible" @click.native="requestSync"> <menu-entry @click.native="requestSync">
<icon-sync slot="icon"></icon-sync> <icon-sync slot="icon"></icon-sync>
<div>Synchronize now</div> <div>Synchronize now</div>
<span>Download / upload file changes.</span> <span>Download / upload file changes.</span>
@ -16,11 +16,6 @@
<span>Manage current file synchronized locations.</span> <span>Manage current file synchronized locations.</span>
</menu-entry> </menu-entry>
</div> </div>
<menu-entry v-else-if="isSyncPossible" @click.native="requestSync">
<icon-sync slot="icon"></icon-sync>
<div>Synchronize now</div>
<span>Download / upload file changes.</span>
</menu-entry>
<hr> <hr>
<div v-for="token in googleDriveTokens" :key="token.sub"> <div v-for="token in googleDriveTokens" :key="token.sub">
<menu-entry @click.native="openGoogleDrive(token)"> <menu-entry @click.native="openGoogleDrive(token)">
@ -118,10 +113,6 @@ export default {
currentFileName() { currentFileName() {
return this.$store.getters['file/current'].name; return this.$store.getters['file/current'].name;
}, },
isSyncPossible() {
return this.syncToken ||
this.$store.getters['syncLocation/current'].length;
},
googleDriveTokens() { googleDriveTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive); return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive);
}, },

View File

@ -104,7 +104,6 @@ export default {
&:focus, &:focus,
&:hover { &:hover {
opacity: 1; opacity: 1;
background-color: rgba(0, 0, 0, 0.1);
} }
} }
</style> </style>

View File

@ -1,415 +0,0 @@
var htmlSanitizer = require('./htmlSanitizer').default
var DiffMatchPatch = require('diff-match-patch')
var TurndownService = require('turndown/lib/turndown.browser.umd')
var turndownService = new TurndownService({
headingStyle: 'atx',
hr: '----------',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
})
function cledit(contentElt, scrollElt, windowParam) {
scrollElt = scrollElt || contentElt
var editor = {
$contentElt: contentElt,
$scrollElt: scrollElt,
$window: windowParam || window,
$keystrokes: [],
$markers: {}
}
editor.$document = editor.$window.document
cledit.Utils.createEventHooks(editor)
var debounce = cledit.Utils.debounce
editor.toggleEditable = function (isEditable) {
if (isEditable === undefined) {
isEditable = !contentElt.contentEditable
}
contentElt.contentEditable = isEditable
}
editor.toggleEditable(true)
function getTextContent() {
var textContent = contentElt.textContent.replace(/\r[\n\u0085]?|[\u2424\u2028\u0085]/g, '\n') // Markdown-it sanitization (Mac/DOS to Unix)
if (textContent.slice(-1) !== '\n') {
textContent += '\n'
}
return textContent
}
var lastTextContent = getTextContent()
var highlighter = new cledit.Highlighter(editor)
var sectionList
function parseSections(content, isInit) {
sectionList = highlighter.parseSections(content, isInit)
editor.$allElements = Array.prototype.slice.call(contentElt.querySelectorAll('.cledit-section *'))
return sectionList
}
// Used to detect editor changes
var watcher = new cledit.Watcher(editor, checkContentChange)
watcher.startWatching()
/* eslint-disable new-cap */
var diffMatchPatch = new DiffMatchPatch()
/* eslint-enable new-cap */
var selectionMgr = new cledit.SelectionMgr(editor)
function adjustCursorPosition(force) {
selectionMgr.saveSelectionState(true, true, force)
}
function replaceContent(selectionStart, selectionEnd, replacement) {
var min = Math.min(selectionStart, selectionEnd)
var max = Math.max(selectionStart, selectionEnd)
var range = selectionMgr.createRange(min, max)
var rangeText = '' + range
// Range can contain a br element, which is not taken into account in rangeText
if (rangeText.length === max - min && rangeText === replacement) {
return
}
range.deleteContents()
range.insertNode(editor.$document.createTextNode(replacement))
return range
}
var ignoreUndo = false
var noContentFix = false
function setContent(value, noUndo, maxStartOffset) {
var textContent = getTextContent()
maxStartOffset = maxStartOffset !== undefined && maxStartOffset < textContent.length ? maxStartOffset : textContent.length - 1
var startOffset = Math.min(
diffMatchPatch.diff_commonPrefix(textContent, value),
maxStartOffset
)
var endOffset = Math.min(
diffMatchPatch.diff_commonSuffix(textContent, value),
textContent.length - startOffset,
value.length - startOffset
)
var replacement = value.substring(startOffset, value.length - endOffset)
var range = replaceContent(startOffset, textContent.length - endOffset, replacement)
if (range) {
ignoreUndo = noUndo
noContentFix = true
}
return {
start: startOffset,
end: value.length - endOffset,
range: range
}
}
function replace(selectionStart, selectionEnd, replacement) {
undoMgr.setDefaultMode('single')
replaceContent(selectionStart, selectionEnd, replacement)
var endOffset = selectionStart + replacement.length
selectionMgr.setSelectionStartEnd(endOffset, endOffset)
selectionMgr.updateCursorCoordinates(true)
}
function replaceAll(search, replacement, startOffset = 0) {
undoMgr.setDefaultMode('single')
var text = getTextContent()
var subtext = getTextContent().slice(startOffset);
var value = subtext.replace(search, replacement)
if (value !== subtext) {
var offset = editor.setContent(text.slice(0, startOffset) + value);
selectionMgr.setSelectionStartEnd(offset.end, offset.end)
selectionMgr.updateCursorCoordinates(true)
}
}
function focus() {
selectionMgr.restoreSelection()
}
var undoMgr = new cledit.UndoMgr(editor)
function addMarker(marker) {
editor.$markers[marker.id] = marker
}
function removeMarker(marker) {
delete editor.$markers[marker.id]
}
var triggerSpellCheck = debounce(function () {
var selection = editor.$window.getSelection()
if (!selectionMgr.hasFocus() || highlighter.isComposing || selectionMgr.selectionStart !== selectionMgr.selectionEnd || !selection.modify) {
return
}
// Hack for Chrome to trigger the spell checker
if (selectionMgr.selectionStart) {
selection.modify('move', 'backward', 'character')
selection.modify('move', 'forward', 'character')
} else {
selection.modify('move', 'forward', 'character')
selection.modify('move', 'backward', 'character')
}
}, 10)
function checkContentChange(mutations) {
watcher.noWatch(function () {
var removedSections = []
var modifiedSections = []
function markModifiedSection(node) {
while (node && node !== contentElt) {
if (node.section) {
var array = node.parentNode ? modifiedSections : removedSections
return array.indexOf(node.section) === -1 && array.push(node.section)
}
node = node.parentNode
}
}
mutations.cl_each(function (mutation) {
markModifiedSection(mutation.target)
mutation.addedNodes.cl_each(markModifiedSection)
mutation.removedNodes.cl_each(markModifiedSection)
})
highlighter.fixContent(modifiedSections, removedSections, noContentFix)
noContentFix = false
})
var newTextContent = getTextContent()
var diffs = diffMatchPatch.diff_main(lastTextContent, newTextContent)
editor.$markers.cl_each(function (marker) {
marker.adjustOffset(diffs)
})
selectionMgr.saveSelectionState()
var sectionList = parseSections(newTextContent)
editor.$trigger('contentChanged', newTextContent, diffs, sectionList)
if (!ignoreUndo) {
undoMgr.addDiffs(lastTextContent, newTextContent, diffs)
undoMgr.setDefaultMode('typing')
undoMgr.saveState()
}
ignoreUndo = false
lastTextContent = newTextContent
triggerSpellCheck()
}
function setSelection(start, end) {
end = end === undefined ? start : end
selectionMgr.setSelectionStartEnd(start, end)
selectionMgr.updateCursorCoordinates()
}
function keydownHandler(handler) {
return function (evt) {
if (
evt.which !== 17 && // Ctrl
evt.which !== 91 && // Cmd
evt.which !== 18 && // Alt
evt.which !== 16 // Shift
) {
handler(evt)
}
}
}
function tryDestroy() {
if (!editor.$window.document.contains(contentElt)) {
watcher.stopWatching()
editor.$window.removeEventListener('keydown', windowKeydownListener)
editor.$window.removeEventListener('mousedown', windowMouseListener)
editor.$window.removeEventListener('mouseup', windowMouseListener)
editor.$window.removeEventListener('resize', windowResizeListener)
editor.$trigger('destroy')
return true
}
}
// In case of Ctrl/Cmd+A outside the editor element
function windowKeydownListener(evt) {
if (!tryDestroy()) {
keydownHandler(function () {
adjustCursorPosition()
})(evt)
}
}
editor.$window.addEventListener('keydown', windowKeydownListener, false)
// Mouseup can happen outside the editor element
function windowMouseListener() {
if (!tryDestroy()) {
selectionMgr.saveSelectionState(true, false)
}
}
editor.$window.addEventListener('mousedown', windowMouseListener)
editor.$window.addEventListener('mouseup', windowMouseListener)
// Resize provokes cursor coordinate changes
function windowResizeListener() {
if (!tryDestroy()) {
selectionMgr.updateCursorCoordinates()
}
}
editor.$window.addEventListener('resize', windowResizeListener)
// This can also provoke selection changes and does not fire mouseup event on Chrome/OSX
contentElt.addEventListener('contextmenu', selectionMgr.saveSelectionState.cl_bind(selectionMgr, true, false))
contentElt.addEventListener('keydown', keydownHandler(function (evt) {
selectionMgr.saveSelectionState()
adjustCursorPosition()
// Perform keystroke
var textContent = getTextContent()
var min = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd)
var max = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd)
var state = {
before: textContent.slice(0, min),
after: textContent.slice(max),
selection: textContent.slice(min, max),
isBackwardSelection: selectionMgr.selectionStart > selectionMgr.selectionEnd
}
editor.$keystrokes.cl_some(function (keystroke) {
if (keystroke.handler(evt, state, editor)) {
editor.setContent(state.before + state.selection + state.after, false, min)
min = state.before.length
max = min + state.selection.length
selectionMgr.setSelectionStartEnd(
state.isBackwardSelection ? max : min,
state.isBackwardSelection ? min : max
)
return true
}
})
}), false)
contentElt.addEventListener('compositionstart', function () {
highlighter.isComposing++
}, false)
contentElt.addEventListener('compositionend', function () {
setTimeout(function () {
highlighter.isComposing && highlighter.isComposing--
}, 0)
}, false)
contentElt.addEventListener('copy', function (evt) {
if (evt.clipboardData) {
evt.clipboardData.setData('text/plain', selectionMgr.getSelectedText());
evt.preventDefault();
}
})
contentElt.addEventListener('cut', function (evt) {
if (evt.clipboardData) {
evt.clipboardData.setData('text/plain', selectionMgr.getSelectedText());
evt.preventDefault();
replace(selectionMgr.selectionStart, selectionMgr.selectionEnd, '')
} else {
undoMgr.setCurrentMode('single')
}
adjustCursorPosition()
})
contentElt.addEventListener('paste', function (evt) {
undoMgr.setCurrentMode('single')
evt.preventDefault()
var data
var clipboardData = evt.clipboardData
if (clipboardData) {
data = clipboardData.getData('text/plain')
try {
var html = clipboardData.getData('text/html');
if (html && !clipboardData.getData('text/css')) {
var sanitizedHtml = htmlSanitizer.sanitizeHtml(html)
.replace(/&#160;/g, ' '); // Replace non-breaking spaces with classic spaces
if (sanitizedHtml) {
data = turndownService.turndown(sanitizedHtml);
}
}
} catch(e) {
// Ignore
}
} else {
clipboardData = editor.$window.clipboardData
data = clipboardData && clipboardData.getData('Text')
}
if (!data) {
return
}
replace(selectionMgr.selectionStart, selectionMgr.selectionEnd, data)
adjustCursorPosition()
}, false)
contentElt.addEventListener('focus', function () {
editor.$trigger('focus')
}, false)
contentElt.addEventListener('blur', function () {
editor.$trigger('blur')
}, false)
function addKeystroke(keystrokes) {
if (!Array.isArray(keystrokes)) {
keystrokes = [keystrokes]
}
editor.$keystrokes = editor.$keystrokes.concat(keystrokes).sort(function (keystroke1, keystroke2) {
return keystroke1.priority - keystroke2.priority
})
}
addKeystroke(cledit.defaultKeystrokes)
editor.selectionMgr = selectionMgr
editor.undoMgr = undoMgr
editor.highlighter = highlighter
editor.watcher = watcher
editor.adjustCursorPosition = adjustCursorPosition
editor.setContent = setContent
editor.replace = replace
editor.replaceAll = replaceAll
editor.getContent = getTextContent
editor.focus = focus
editor.setSelection = setSelection
editor.addKeystroke = addKeystroke
editor.addMarker = addMarker
editor.removeMarker = removeMarker
editor.init = function (options) {
options = ({
getCursorFocusRatio: function () {
return 0.1
},
sectionHighlighter: function (section) {
return section.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ')
},
sectionDelimiter: ''
}).cl_extend(options || {})
editor.options = options
if (options.content !== undefined) {
lastTextContent = options.content.toString()
if (lastTextContent.slice(-1) !== '\n') {
lastTextContent += '\n'
}
}
var sectionList = parseSections(lastTextContent, true)
editor.$trigger('contentChanged', lastTextContent, [0, lastTextContent], sectionList)
if (options.selectionStart !== undefined && options.selectionEnd !== undefined) {
editor.setSelection(options.selectionStart, options.selectionEnd)
} else {
selectionMgr.saveSelectionState()
}
undoMgr.init(options)
if (options.scrollTop !== undefined) {
scrollElt.scrollTop = options.scrollTop
}
}
return editor
}
module.exports = cledit

View File

@ -1,191 +0,0 @@
var cledit = require('./cleditCore')
var styleElts = []
function createStyleSheet(document) {
var styleElt = document.createElement('style')
styleElt.type = 'text/css'
styleElt.innerHTML = '.cledit-section * { display: inline; }'
document.head.appendChild(styleElt)
styleElts.push(styleElt)
}
function Highlighter(editor) {
var self = this
cledit.Utils.createEventHooks(this)
styleElts.cl_some(function (styleElt) {
return editor.$document.head.contains(styleElt)
}) || createStyleSheet(editor.$document)
var contentElt = editor.$contentElt
this.isComposing = 0
var sectionList = []
var insertBeforeSection
var useBr = cledit.Utils.isWebkit
var trailingNodeTag = 'div'
var hiddenLfInnerHtml = '<br><span class="hd-lf" style="display: none">\n</span>'
var lfHtml = '<span class="lf">' + (useBr ? hiddenLfInnerHtml : '\n') + '</span>'
this.fixContent = function (modifiedSections, removedSections, noContentFix) {
modifiedSections.cl_each(function (section) {
section.forceHighlighting = true
if (!noContentFix) {
if (useBr) {
section.elt.getElementsByClassName('hd-lf').cl_each(function (lfElt) {
lfElt.parentNode.removeChild(lfElt)
})
section.elt.getElementsByTagName('br').cl_each(function (brElt) {
brElt.parentNode.replaceChild(editor.$document.createTextNode('\n'), brElt)
})
}
if (section.elt.textContent.slice(-1) !== '\n') {
section.elt.appendChild(editor.$document.createTextNode('\n'))
}
}
})
}
this.addTrailingNode = function () {
this.trailingNode = editor.$document.createElement(trailingNodeTag)
contentElt.appendChild(this.trailingNode)
}
function Section(text) {
this.text = text.text === undefined ? text : text.text
this.data = text.data
}
Section.prototype.setElement = function (elt) {
this.elt = elt
elt.section = this
}
this.parseSections = function (content, isInit) {
if (this.isComposing) {
return sectionList
}
var newSectionList = editor.options.sectionParser ? editor.options.sectionParser(content) : [content]
newSectionList = newSectionList.cl_map(function (sectionText) {
return new Section(sectionText)
})
var modifiedSections = []
var sectionsToRemove = []
insertBeforeSection = undefined
if (isInit) {
// Render everything if isInit
sectionsToRemove = sectionList
sectionList = newSectionList
modifiedSections = newSectionList
} else {
// Find modified section starting from top
var leftIndex = sectionList.length
sectionList.cl_some(function (section, index) {
var newSection = newSectionList[index]
if (index >= newSectionList.length ||
section.forceHighlighting ||
// Check text modification
section.text !== newSection.text ||
// Check that section has not been detached or moved
section.elt.parentNode !== contentElt ||
// Check also the content since nodes can be injected in sections via copy/paste
section.elt.textContent !== newSection.text) {
leftIndex = index
return true
}
})
// Find modified section starting from bottom
var rightIndex = -sectionList.length
sectionList.slice().reverse().cl_some(function (section, index) {
var newSection = newSectionList[newSectionList.length - index - 1]
if (index >= newSectionList.length ||
section.forceHighlighting ||
// Check modified
section.text !== newSection.text ||
// Check that section has not been detached or moved
section.elt.parentNode !== contentElt ||
// Check also the content since nodes can be injected in sections via copy/paste
section.elt.textContent !== newSection.text) {
rightIndex = -index
return true
}
})
if (leftIndex - rightIndex > sectionList.length) {
// Prevent overlap
rightIndex = leftIndex - sectionList.length
}
var leftSections = sectionList.slice(0, leftIndex)
modifiedSections = newSectionList.slice(leftIndex, newSectionList.length + rightIndex)
var rightSections = sectionList.slice(sectionList.length + rightIndex, sectionList.length)
insertBeforeSection = rightSections[0]
sectionsToRemove = sectionList.slice(leftIndex, sectionList.length + rightIndex)
sectionList = leftSections.concat(modifiedSections).concat(rightSections)
}
var newSectionEltList = editor.$document.createDocumentFragment()
modifiedSections.cl_each(function (section) {
section.forceHighlighting = false
highlight(section)
newSectionEltList.appendChild(section.elt)
})
editor.watcher.noWatch(function () {
if (isInit) {
contentElt.innerHTML = ''
contentElt.appendChild(newSectionEltList)
return this.addTrailingNode()
}
// Remove outdated sections
sectionsToRemove.cl_each(function (section) {
// section may be already removed
section.elt.parentNode === contentElt && contentElt.removeChild(section.elt)
// To detect sections that come back with built-in undo
section.elt.section = undefined
})
if (insertBeforeSection !== undefined) {
contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt)
} else {
contentElt.appendChild(newSectionEltList)
}
// Remove unauthorized nodes (text nodes outside of sections or duplicated sections via copy/paste)
var childNode = contentElt.firstChild
while (childNode) {
var nextNode = childNode.nextSibling
if (!childNode.section) {
contentElt.removeChild(childNode)
}
childNode = nextNode
}
this.addTrailingNode()
self.$trigger('highlighted')
if (editor.selectionMgr.hasFocus()) {
editor.selectionMgr.restoreSelection()
editor.selectionMgr.updateCursorCoordinates()
}
}.cl_bind(this))
return sectionList
}
function highlight(section) {
var html = editor.options.sectionHighlighter(section).replace(/\n/g, lfHtml)
var sectionElt = editor.$document.createElement('div')
sectionElt.className = 'cledit-section'
sectionElt.innerHTML = html
section.setElement(sectionElt)
self.$trigger('sectionHighlighted', section)
}
}
cledit.Highlighter = Highlighter

View File

@ -1,183 +0,0 @@
var cledit = require('./cleditCore')
function Keystroke(handler, priority) {
this.handler = handler
this.priority = priority || 100
}
cledit.Keystroke = Keystroke
var clearNewline
var charTypes = Object.create(null)
// Word separators, as in Sublime Text
'./\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~?'.split('').cl_each(function (wordSeparator) {
charTypes[wordSeparator] = 'wordSeparator'
})
charTypes[' '] = 'space'
charTypes['\t'] = 'space'
charTypes['\n'] = 'newLine'
function getNextWordOffset(text, offset, isBackward) {
var previousType
while ((isBackward && offset > 0) || (!isBackward && offset < text.length)) {
var currentType = charTypes[isBackward ? text[offset - 1] : text[offset]] || 'word'
if (previousType && currentType !== previousType) {
if (previousType === 'word' || currentType === 'space' || previousType === 'newLine' || currentType === 'newLine') {
break
}
}
previousType = currentType
isBackward ? offset-- : offset++
}
return offset
}
cledit.defaultKeystrokes = [
new Keystroke(function (evt, state, editor) {
if ((!evt.ctrlKey && !evt.metaKey) || evt.altKey) {
return
}
var keyCode = evt.charCode || evt.keyCode
var keyCodeChar = String.fromCharCode(keyCode).toLowerCase()
var action
switch (keyCodeChar) {
case 'y':
action = 'redo'
break
case 'z':
action = evt.shiftKey ? 'redo' : 'undo'
break
}
if (action) {
evt.preventDefault()
setTimeout(function () {
editor.undoMgr[action]()
}, 10)
return true
}
}),
new Keystroke(function (evt, state) {
if (evt.which !== 9 /* tab */ || evt.metaKey || evt.ctrlKey) {
return
}
function strSplice(str, i, remove, add) {
remove = +remove || 0
add = add || ''
return str.slice(0, i) + add + str.slice(i + remove)
}
evt.preventDefault()
var isInverse = evt.shiftKey
var lf = state.before.lastIndexOf('\n') + 1
if (isInverse) {
if (/\s/.test(state.before.charAt(lf))) {
state.before = strSplice(state.before, lf, 1)
}
state.selection = state.selection.replace(/^[ \t]/gm, '')
} else {
if (state.selection) {
state.before = strSplice(state.before, lf, 0, '\t')
state.selection = state.selection.replace(/\n(?=[\s\S])/g, '\n\t')
} else {
state.before += '\t'
}
}
return true
}),
new Keystroke(function (evt, state, editor) {
if (evt.which !== 13 /* enter */) {
clearNewline = false
return
}
evt.preventDefault()
var lf = state.before.lastIndexOf('\n') + 1
if (clearNewline) {
state.before = state.before.substring(0, lf)
state.selection = ''
clearNewline = false
return true
}
clearNewline = false
var previousLine = state.before.slice(lf)
var indent = previousLine.match(/^\s*/)[0]
if (indent.length) {
clearNewline = true
}
editor.undoMgr.setCurrentMode('single')
state.before += '\n' + indent
state.selection = ''
return true
}),
new Keystroke(function (evt, state, editor) {
if (evt.which !== 8 /* backspace */ && evt.which !== 46 /* delete */) {
return
}
editor.undoMgr.setCurrentMode('delete')
if (!state.selection) {
var isJump = (cledit.Utils.isMac && evt.altKey) || (!cledit.Utils.isMac && evt.ctrlKey)
if (isJump) {
// Custom kill word behavior
var text = state.before + state.after
var offset = getNextWordOffset(text, state.before.length, evt.which === 8)
if (evt.which === 8) {
state.before = state.before.slice(0, offset)
} else {
state.after = state.after.slice(offset - text.length)
}
evt.preventDefault()
return true
} else if (evt.which === 8 && state.before.slice(-1) === '\n') {
// Special treatment for end of lines
state.before = state.before.slice(0, -1)
evt.preventDefault()
return true
} else if (evt.which === 46 && state.after.slice(0, 1) === '\n') {
state.after = state.after.slice(1)
evt.preventDefault()
return true
}
} else {
state.selection = ''
evt.preventDefault()
return true
}
}),
new Keystroke(function (evt, state, editor) {
if (evt.which !== 37 /* left arrow */ && evt.which !== 39 /* right arrow */) {
return
}
var isJump = (cledit.Utils.isMac && evt.altKey) || (!cledit.Utils.isMac && evt.ctrlKey)
if (!isJump) {
return
}
// Custom jump behavior
var textContent = editor.getContent()
var offset = getNextWordOffset(textContent, editor.selectionMgr.selectionEnd, evt.which === 37)
if (evt.shiftKey) {
// rebuild the state completely
var min = Math.min(editor.selectionMgr.selectionStart, offset)
var max = Math.max(editor.selectionMgr.selectionStart, offset)
state.before = textContent.slice(0, min)
state.after = textContent.slice(max)
state.selection = textContent.slice(min, max)
state.isBackwardSelection = editor.selectionMgr.selectionStart > offset
} else {
state.before = textContent.slice(0, offset)
state.after = textContent.slice(offset)
state.selection = ''
}
evt.preventDefault()
return true
})
]

View File

@ -1,44 +0,0 @@
var cledit = require('./cleditCore')
var DIFF_DELETE = -1
var DIFF_INSERT = 1
var DIFF_EQUAL = 0
var idCounter = 0
function Marker(offset, trailing) {
this.id = idCounter++
this.offset = offset
this.trailing = trailing
}
Marker.prototype.adjustOffset = function (diffs) {
var startOffset = 0
diffs.cl_each(function (diff) {
var diffType = diff[0]
var diffText = diff[1]
var diffOffset = diffText.length
switch (diffType) {
case DIFF_EQUAL:
startOffset += diffOffset
break
case DIFF_INSERT:
if (
this.trailing
? this.offset > startOffset
: this.offset >= startOffset
) {
this.offset += diffOffset
}
startOffset += diffOffset
break
case DIFF_DELETE:
if (this.offset > startOffset) {
this.offset -= Math.min(diffOffset, this.offset - startOffset)
}
break
}
}.cl_bind(this))
}
cledit.Marker = Marker

View File

@ -0,0 +1,426 @@
import DiffMatchPatch from 'diff-match-patch';
import TurndownService from 'turndown/lib/turndown.browser.umd';
import htmlSanitizer from '../../libs/htmlSanitizer';
const turndownService = new TurndownService({
headingStyle: 'atx',
hr: '----------',
bulletListMarker: '-',
codeBlockStyle: 'fenced',
});
function cledit(contentElt, scrollEltOpt) {
const scrollElt = scrollEltOpt || contentElt;
const editor = {
$contentElt: contentElt,
$scrollElt: scrollElt,
$keystrokes: [],
$markers: {},
};
cledit.Utils.createEventHooks(editor);
const debounce = cledit.Utils.debounce;
editor.toggleEditable = (isEditable) => {
contentElt.contentEditable = isEditable == null ? !contentElt.contentEditable : isEditable;
};
editor.toggleEditable(true);
function getTextContent() {
// Markdown-it sanitization (Mac/DOS to Unix)
let textContent = contentElt.textContent.replace(/\r[\n\u0085]?|[\u2424\u2028\u0085]/g, '\n');
if (textContent.slice(-1) !== '\n') {
textContent += '\n';
}
return textContent;
}
let lastTextContent = getTextContent();
const highlighter = new cledit.Highlighter(editor);
let sectionList;
function parseSections(content, isInit) {
sectionList = highlighter.parseSections(content, isInit);
editor.$allElements = Array.prototype.slice
.call(contentElt.querySelectorAll('.cledit-section *'));
return sectionList;
}
/* eslint-disable new-cap */
const diffMatchPatch = new DiffMatchPatch();
/* eslint-enable new-cap */
const selectionMgr = new cledit.SelectionMgr(editor);
function adjustCursorPosition(force) {
selectionMgr.saveSelectionState(true, true, force);
}
function replaceContent(selectionStart, selectionEnd, replacement) {
const min = Math.min(selectionStart, selectionEnd);
const max = Math.max(selectionStart, selectionEnd);
const range = selectionMgr.createRange(min, max);
const rangeText = `${range}`;
// Range can contain a br element, which is not taken into account in rangeText
if (rangeText.length === max - min && rangeText === replacement) {
return null;
}
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
return range;
}
let ignoreUndo = false;
let noContentFix = false;
function setContent(value, noUndo, maxStartOffsetOpt) {
const textContent = getTextContent();
const maxStartOffset = maxStartOffsetOpt != null && maxStartOffsetOpt < textContent.length
? maxStartOffsetOpt
: textContent.length - 1;
const startOffset = Math.min(
diffMatchPatch.diff_commonPrefix(textContent, value),
maxStartOffset,
);
const endOffset = Math.min(
diffMatchPatch.diff_commonSuffix(textContent, value),
textContent.length - startOffset,
value.length - startOffset,
);
const replacement = value.substring(startOffset, value.length - endOffset);
const range = replaceContent(startOffset, textContent.length - endOffset, replacement);
if (range) {
ignoreUndo = noUndo;
noContentFix = true;
}
return {
start: startOffset,
end: value.length - endOffset,
range,
};
}
const undoMgr = new cledit.UndoMgr(editor);
function replace(selectionStart, selectionEnd, replacement) {
undoMgr.setDefaultMode('single');
replaceContent(selectionStart, selectionEnd, replacement);
const endOffset = selectionStart + replacement.length;
selectionMgr.setSelectionStartEnd(endOffset, endOffset);
selectionMgr.updateCursorCoordinates(true);
}
function replaceAll(search, replacement, startOffset = 0) {
undoMgr.setDefaultMode('single');
const text = getTextContent();
const subtext = getTextContent().slice(startOffset);
const value = subtext.replace(search, replacement);
if (value !== subtext) {
const offset = editor.setContent(text.slice(0, startOffset) + value);
selectionMgr.setSelectionStartEnd(offset.end, offset.end);
selectionMgr.updateCursorCoordinates(true);
}
}
function focus() {
selectionMgr.restoreSelection();
}
function addMarker(marker) {
editor.$markers[marker.id] = marker;
}
function removeMarker(marker) {
delete editor.$markers[marker.id];
}
const triggerSpellCheck = debounce(() => {
// Hack for Chrome to trigger the spell checker
const selection = window.getSelection();
if (selectionMgr.hasFocus()
&& !highlighter.isComposing
&& selectionMgr.selectionStart === selectionMgr.selectionEnd
&& selection.modify
) {
if (selectionMgr.selectionStart) {
selection.modify('move', 'backward', 'character');
selection.modify('move', 'forward', 'character');
} else {
selection.modify('move', 'forward', 'character');
selection.modify('move', 'backward', 'character');
}
}
}, 10);
let watcher;
function checkContentChange(mutations) {
watcher.noWatch(() => {
const removedSections = [];
const modifiedSections = [];
function markModifiedSection(node) {
let currentNode = node;
while (currentNode && currentNode !== contentElt) {
if (currentNode.section) {
const array = currentNode.parentNode ? modifiedSections : removedSections;
if (array.indexOf(currentNode.section) === -1) {
array.push(currentNode.section);
}
return;
}
currentNode = currentNode.parentNode;
}
}
mutations.cl_each((mutation) => {
markModifiedSection(mutation.target);
mutation.addedNodes.cl_each(markModifiedSection);
mutation.removedNodes.cl_each(markModifiedSection);
});
highlighter.fixContent(modifiedSections, removedSections, noContentFix);
noContentFix = false;
});
const newTextContent = getTextContent();
const diffs = diffMatchPatch.diff_main(lastTextContent, newTextContent);
editor.$markers.cl_each((marker) => {
marker.adjustOffset(diffs);
});
selectionMgr.saveSelectionState();
const parsedSections = parseSections(newTextContent);
editor.$trigger('contentChanged', newTextContent, diffs, parsedSections);
if (!ignoreUndo) {
undoMgr.addDiffs(lastTextContent, newTextContent, diffs);
undoMgr.setDefaultMode('typing');
undoMgr.saveState();
}
ignoreUndo = false;
lastTextContent = newTextContent;
triggerSpellCheck();
}
// Detect editor changes
watcher = new cledit.Watcher(editor, checkContentChange);
watcher.startWatching();
function setSelection(start, end) {
selectionMgr.setSelectionStartEnd(start, end == null ? start : end);
selectionMgr.updateCursorCoordinates();
}
function keydownHandler(handler) {
return (evt) => {
if (
evt.which !== 17 && // Ctrl
evt.which !== 91 && // Cmd
evt.which !== 18 && // Alt
evt.which !== 16 // Shift
) {
handler(evt);
}
};
}
let windowKeydownListener;
let windowMouseListener;
let windowResizeListener;
function tryDestroy() {
if (document.contains(contentElt)) {
return false;
}
watcher.stopWatching();
window.removeEventListener('keydown', windowKeydownListener);
window.removeEventListener('mousedown', windowMouseListener);
window.removeEventListener('mouseup', windowMouseListener);
window.removeEventListener('resize', windowResizeListener);
editor.$trigger('destroy');
return true;
}
// In case of Ctrl/Cmd+A outside the editor element
windowKeydownListener = (evt) => {
if (!tryDestroy()) {
keydownHandler(() => {
adjustCursorPosition();
})(evt);
}
};
window.addEventListener('keydown', windowKeydownListener);
// Mouseup can happen outside the editor element
windowMouseListener = () => {
if (!tryDestroy()) {
selectionMgr.saveSelectionState(true, false);
}
};
window.addEventListener('mousedown', windowMouseListener);
window.addEventListener('mouseup', windowMouseListener);
// Resize provokes cursor coordinate changes
windowResizeListener = () => {
if (!tryDestroy()) {
selectionMgr.updateCursorCoordinates();
}
};
window.addEventListener('resize', windowResizeListener);
// Provokes selection changes and does not fire mouseup event on Chrome/OSX
contentElt.addEventListener('contextmenu',
selectionMgr.saveSelectionState.cl_bind(selectionMgr, true, false));
contentElt.addEventListener('keydown', keydownHandler((evt) => {
selectionMgr.saveSelectionState();
adjustCursorPosition();
// Perform keystroke
const textContent = getTextContent();
const min = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
const max = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
const state = {
before: textContent.slice(0, min),
after: textContent.slice(max),
selection: textContent.slice(min, max),
isBackwardSelection: selectionMgr.selectionStart > selectionMgr.selectionEnd,
};
editor.$keystrokes.cl_some((keystroke) => {
if (!keystroke.handler(evt, state, editor)) {
return false;
}
editor.setContent(state.before + state.selection + state.after, false, min);
const min1 = state.before.length;
const max1 = min + state.selection.length;
selectionMgr.setSelectionStartEnd(
state.isBackwardSelection ? max1 : min1,
state.isBackwardSelection ? min : max1,
);
return true;
});
}));
contentElt.addEventListener('compositionstart', () => {
highlighter.isComposing += 1;
});
contentElt.addEventListener('compositionend', () => {
setTimeout(() => {
if (highlighter.isComposing) {
highlighter.isComposing -= 1;
}
}, 1);
});
contentElt.addEventListener('copy', (evt) => {
if (evt.clipboardData) {
evt.clipboardData.setData('text/plain', selectionMgr.getSelectedText());
evt.preventDefault();
}
});
contentElt.addEventListener('cut', (evt) => {
if (evt.clipboardData) {
evt.clipboardData.setData('text/plain', selectionMgr.getSelectedText());
evt.preventDefault();
replace(selectionMgr.selectionStart, selectionMgr.selectionEnd, '');
} else {
undoMgr.setCurrentMode('single');
}
adjustCursorPosition();
});
contentElt.addEventListener('paste', (evt) => {
undoMgr.setCurrentMode('single');
evt.preventDefault();
let data;
let clipboardData = evt.clipboardData;
if (clipboardData) {
data = clipboardData.getData('text/plain');
try {
const html = clipboardData.getData('text/html');
if (html && !clipboardData.getData('text/css')) {
const sanitizedHtml = htmlSanitizer.sanitizeHtml(html)
.replace(/&#160;/g, ' '); // Replace non-breaking spaces with classic spaces
if (sanitizedHtml) {
data = turndownService.turndown(sanitizedHtml);
}
}
} catch (e) {
// Ignore
}
} else {
clipboardData = window.clipboardData;
data = clipboardData && clipboardData.getData('Text');
}
if (!data) {
return;
}
replace(selectionMgr.selectionStart, selectionMgr.selectionEnd, data);
adjustCursorPosition();
});
contentElt.addEventListener('focus', () => {
editor.$trigger('focus');
});
contentElt.addEventListener('blur', () => {
editor.$trigger('blur');
});
function addKeystroke(keystroke) {
const keystrokes = Array.isArray(keystroke) ? keystroke : [keystroke];
editor.$keystrokes = editor.$keystrokes.concat(keystrokes).sort(
(keystroke1, keystroke2) => keystroke1.priority - keystroke2.priority);
}
addKeystroke(cledit.defaultKeystrokes);
editor.selectionMgr = selectionMgr;
editor.undoMgr = undoMgr;
editor.highlighter = highlighter;
editor.watcher = watcher;
editor.adjustCursorPosition = adjustCursorPosition;
editor.setContent = setContent;
editor.replace = replace;
editor.replaceAll = replaceAll;
editor.getContent = getTextContent;
editor.focus = focus;
editor.setSelection = setSelection;
editor.addKeystroke = addKeystroke;
editor.addMarker = addMarker;
editor.removeMarker = removeMarker;
editor.init = (opts = {}) => {
const options = ({
getCursorFocusRatio() {
return 0.1;
},
sectionHighlighter(section) {
return section.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
},
sectionDelimiter: '',
}).cl_extend(opts);
editor.options = options;
if (options.content !== undefined) {
lastTextContent = options.content.toString();
if (lastTextContent.slice(-1) !== '\n') {
lastTextContent += '\n';
}
}
const parsedSections = parseSections(lastTextContent, true);
editor.$trigger('contentChanged', lastTextContent, [0, lastTextContent], parsedSections);
if (options.selectionStart !== undefined && options.selectionEnd !== undefined) {
editor.setSelection(options.selectionStart, options.selectionEnd);
} else {
selectionMgr.saveSelectionState();
}
undoMgr.init(options);
if (options.scrollTop !== undefined) {
scrollElt.scrollTop = options.scrollTop;
}
};
return editor;
}
module.exports = cledit;

View File

@ -0,0 +1,198 @@
import cledit from './cleditCore';
const styleElts = [];
function createStyleSheet(document) {
const styleElt = document.createElement('style');
styleElt.type = 'text/css';
styleElt.innerHTML = '.cledit-section * { display: inline; }';
document.head.appendChild(styleElt);
styleElts.push(styleElt);
}
function Highlighter(editor) {
const self = this;
cledit.Utils.createEventHooks(this);
if (!styleElts.cl_some(styleElt => document.head.contains(styleElt))) {
createStyleSheet(document);
}
const contentElt = editor.$contentElt;
this.isComposing = 0;
let sectionList = [];
let insertBeforeSection;
const useBr = cledit.Utils.isWebkit;
const trailingNodeTag = 'div';
const hiddenLfInnerHtml = '<br><span class="hd-lf" style="display: none">\n</span>';
const lfHtml = `<span class="lf">${useBr ? hiddenLfInnerHtml : '\n'}</span>`;
this.fixContent = (modifiedSections, removedSections, noContentFix) => {
modifiedSections.cl_each((section) => {
section.forceHighlighting = true;
if (!noContentFix) {
if (useBr) {
section.elt.getElementsByClassName('hd-lf').cl_each(
lfElt => lfElt.parentNode.removeChild(lfElt));
section.elt.getElementsByTagName('br').cl_each(
brElt => brElt.parentNode.replaceChild(document.createTextNode('\n'), brElt));
}
if (section.elt.textContent.slice(-1) !== '\n') {
section.elt.appendChild(document.createTextNode('\n'));
}
}
});
};
this.addTrailingNode = () => {
this.trailingNode = document.createElement(trailingNodeTag);
contentElt.appendChild(this.trailingNode);
};
class Section {
constructor(text) {
this.text = text.text === undefined ? text : text.text;
this.data = text.data;
}
setElement(elt) {
this.elt = elt;
elt.section = this;
}
}
this.parseSections = (content, isInit) => {
if (this.isComposing) {
return sectionList;
}
const newSectionList = (editor.options.sectionParser
? editor.options.sectionParser(content)
: [content])
.cl_map(sectionText => new Section(sectionText));
let modifiedSections = [];
let sectionsToRemove = [];
insertBeforeSection = undefined;
if (isInit) {
// Render everything if isInit
sectionsToRemove = sectionList;
sectionList = newSectionList;
modifiedSections = newSectionList;
} else {
// Find modified section starting from top
let leftIndex = sectionList.length;
sectionList.cl_some((section, index) => {
const newSection = newSectionList[index];
if (index >= newSectionList.length ||
section.forceHighlighting ||
// Check text modification
section.text !== newSection.text ||
// Check that section has not been detached or moved
section.elt.parentNode !== contentElt ||
// Check also the content since nodes can be injected in sections via copy/paste
section.elt.textContent !== newSection.text
) {
leftIndex = index;
return true;
}
return false;
});
// Find modified section starting from bottom
let rightIndex = -sectionList.length;
sectionList.slice().reverse().cl_some((section, index) => {
const newSection = newSectionList[newSectionList.length - index - 1];
if (index >= newSectionList.length ||
section.forceHighlighting ||
// Check modified
section.text !== newSection.text ||
// Check that section has not been detached or moved
section.elt.parentNode !== contentElt ||
// Check also the content since nodes can be injected in sections via copy/paste
section.elt.textContent !== newSection.text
) {
rightIndex = -index;
return true;
}
return false;
});
if (leftIndex - rightIndex > sectionList.length) {
// Prevent overlap
rightIndex = leftIndex - sectionList.length;
}
const leftSections = sectionList.slice(0, leftIndex);
modifiedSections = newSectionList.slice(leftIndex, newSectionList.length + rightIndex);
const rightSections = sectionList.slice(sectionList.length + rightIndex, sectionList.length);
insertBeforeSection = rightSections[0];
sectionsToRemove = sectionList.slice(leftIndex, sectionList.length + rightIndex);
sectionList = leftSections.concat(modifiedSections).concat(rightSections);
}
function highlight(section) {
const html = editor.options.sectionHighlighter(section).replace(/\n/g, lfHtml);
const sectionElt = document.createElement('div');
sectionElt.className = 'cledit-section';
sectionElt.innerHTML = html;
section.setElement(sectionElt);
self.$trigger('sectionHighlighted', section);
}
const newSectionEltList = document.createDocumentFragment();
modifiedSections.cl_each((section) => {
section.forceHighlighting = false;
highlight(section);
newSectionEltList.appendChild(section.elt);
});
editor.watcher.noWatch(() => {
if (isInit) {
contentElt.innerHTML = '';
contentElt.appendChild(newSectionEltList);
this.addTrailingNode();
return;
}
// Remove outdated sections
sectionsToRemove.cl_each((section) => {
// section may be already removed
if (section.elt.parentNode === contentElt) {
contentElt.removeChild(section.elt);
}
// To detect sections that come back with built-in undo
section.elt.section = undefined;
});
if (insertBeforeSection !== undefined) {
contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt);
} else {
contentElt.appendChild(newSectionEltList);
}
// Remove unauthorized nodes (text nodes outside of sections or
// duplicated sections via copy/paste)
let childNode = contentElt.firstChild;
while (childNode) {
const nextNode = childNode.nextSibling;
if (!childNode.section) {
contentElt.removeChild(childNode);
}
childNode = nextNode;
}
this.addTrailingNode();
self.$trigger('highlighted');
if (editor.selectionMgr.hasFocus()) {
editor.selectionMgr.restoreSelection();
editor.selectionMgr.updateCursorCoordinates();
}
});
return sectionList;
};
}
cledit.Highlighter = Highlighter;

View File

@ -0,0 +1,185 @@
import cledit from './cleditCore';
function Keystroke(handler, priority) {
this.handler = handler;
this.priority = priority || 100;
}
cledit.Keystroke = Keystroke;
let clearNewline;
const charTypes = Object.create(null);
// Word separators, as in Sublime Text
'./\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~?'.split('').cl_each((wordSeparator) => {
charTypes[wordSeparator] = 'wordSeparator';
});
charTypes[' '] = 'space';
charTypes['\t'] = 'space';
charTypes['\n'] = 'newLine';
function getNextWordOffset(text, offset, isBackward) {
let previousType;
let result = offset;
while ((isBackward && result > 0) || (!isBackward && result < text.length)) {
const currentType = charTypes[isBackward ? text[result - 1] : text[result]] || 'word';
if (previousType && currentType !== previousType) {
if (previousType === 'word' || currentType === 'space' || previousType === 'newLine' || currentType === 'newLine') {
break;
}
}
previousType = currentType;
if (isBackward) {
result -= 1;
} else {
result += 1;
}
}
return result;
}
cledit.defaultKeystrokes = [
new Keystroke((evt, state, editor) => {
if ((!evt.ctrlKey && !evt.metaKey) || evt.altKey) {
return false;
}
const keyCode = evt.charCode || evt.keyCode;
const keyCodeChar = String.fromCharCode(keyCode).toLowerCase();
let action;
switch (keyCodeChar) {
case 'y':
action = 'redo';
break;
case 'z':
action = evt.shiftKey ? 'redo' : 'undo';
break;
default:
}
if (action) {
evt.preventDefault();
setTimeout(() => editor.undoMgr[action](), 10);
return true;
}
return false;
}),
new Keystroke((evt, state) => {
if (evt.which !== 9 /* tab */ || evt.metaKey || evt.ctrlKey) {
return false;
}
const strSplice = (str, i, remove, add = '') =>
str.slice(0, i) + add + str.slice(i + (+remove || 0));
evt.preventDefault();
const isInverse = evt.shiftKey;
const lf = state.before.lastIndexOf('\n') + 1;
if (isInverse) {
if (/\s/.test(state.before.charAt(lf))) {
state.before = strSplice(state.before, lf, 1);
}
state.selection = state.selection.replace(/^[ \t]/gm, '');
} else if (state.selection) {
state.before = strSplice(state.before, lf, 0, '\t');
state.selection = state.selection.replace(/\n(?=[\s\S])/g, '\n\t');
} else {
state.before += '\t';
}
return true;
}),
new Keystroke((evt, state, editor) => {
if (evt.which !== 13 /* enter */) {
clearNewline = false;
return false;
}
evt.preventDefault();
const lf = state.before.lastIndexOf('\n') + 1;
if (clearNewline) {
state.before = state.before.substring(0, lf);
state.selection = '';
clearNewline = false;
return true;
}
clearNewline = false;
const previousLine = state.before.slice(lf);
const indent = previousLine.match(/^\s*/)[0];
if (indent.length) {
clearNewline = true;
}
editor.undoMgr.setCurrentMode('single');
state.before += `\n${indent}`;
state.selection = '';
return true;
}),
new Keystroke((evt, state, editor) => {
if (evt.which !== 8 /* backspace */ && evt.which !== 46 /* delete */) {
return false;
}
editor.undoMgr.setCurrentMode('delete');
if (!state.selection) {
const isJump = (cledit.Utils.isMac && evt.altKey) || (!cledit.Utils.isMac && evt.ctrlKey);
if (isJump) {
// Custom kill word behavior
const text = state.before + state.after;
const offset = getNextWordOffset(text, state.before.length, evt.which === 8);
if (evt.which === 8) {
state.before = state.before.slice(0, offset);
} else {
state.after = state.after.slice(offset - text.length);
}
evt.preventDefault();
return true;
} else if (evt.which === 8 && state.before.slice(-1) === '\n') {
// Special treatment for end of lines
state.before = state.before.slice(0, -1);
evt.preventDefault();
return true;
} else if (evt.which === 46 && state.after.slice(0, 1) === '\n') {
state.after = state.after.slice(1);
evt.preventDefault();
return true;
}
} else {
state.selection = '';
evt.preventDefault();
return true;
}
return false;
}),
new Keystroke((evt, state, editor) => {
if (evt.which !== 37 /* left arrow */ && evt.which !== 39 /* right arrow */) {
return false;
}
const isJump = (cledit.Utils.isMac && evt.altKey) || (!cledit.Utils.isMac && evt.ctrlKey);
if (!isJump) {
return false;
}
// Custom jump behavior
const textContent = editor.getContent();
const offset = getNextWordOffset(
textContent, editor.selectionMgr.selectionEnd, evt.which === 37);
if (evt.shiftKey) {
// rebuild the state completely
const min = Math.min(editor.selectionMgr.selectionStart, offset);
const max = Math.max(editor.selectionMgr.selectionStart, offset);
state.before = textContent.slice(0, min);
state.after = textContent.slice(max);
state.selection = textContent.slice(min, max);
state.isBackwardSelection = editor.selectionMgr.selectionStart > offset;
} else {
state.before = textContent.slice(0, offset);
state.after = textContent.slice(offset);
state.selection = '';
}
evt.preventDefault();
return true;
}),
];

View File

@ -0,0 +1,49 @@
import cledit from './cleditCore';
const DIFF_DELETE = -1;
const DIFF_INSERT = 1;
const DIFF_EQUAL = 0;
let idCounter = 0;
class Marker {
constructor(offset, trailing) {
this.id = idCounter;
idCounter += 1;
this.offset = offset;
this.trailing = trailing;
}
adjustOffset(diffs) {
let startOffset = 0;
diffs.cl_each((diff) => {
const diffType = diff[0];
const diffText = diff[1];
const diffOffset = diffText.length;
switch (diffType) {
case DIFF_EQUAL:
startOffset += diffOffset;
break;
case DIFF_INSERT:
if (
this.trailing
? this.offset > startOffset
: this.offset >= startOffset
) {
this.offset += diffOffset;
}
startOffset += diffOffset;
break;
case DIFF_DELETE:
if (this.offset > startOffset) {
this.offset -= Math.min(diffOffset, this.offset - startOffset);
}
break;
default:
}
});
}
}
cledit.Marker = Marker;

View File

@ -1,79 +1,83 @@
var cledit = require('./cleditCore') import cledit from './cleditCore';
function SelectionMgr(editor) { function SelectionMgr(editor) {
var debounce = cledit.Utils.debounce const debounce = cledit.Utils.debounce;
var contentElt = editor.$contentElt const contentElt = editor.$contentElt;
var scrollElt = editor.$scrollElt const scrollElt = editor.$scrollElt;
cledit.Utils.createEventHooks(this) cledit.Utils.createEventHooks(this);
var self = this const self = this;
var lastSelectionStart = 0 let lastSelectionStart = 0;
var lastSelectionEnd = 0 let lastSelectionEnd = 0;
this.selectionStart = 0 this.selectionStart = 0;
this.selectionEnd = 0 this.selectionEnd = 0;
this.cursorCoordinates = {} this.cursorCoordinates = {};
this.findContainer = function (offset) { this.findContainer = (offset) => {
var result = cledit.Utils.findContainer(contentElt, offset) const result = cledit.Utils.findContainer(contentElt, offset);
if (result.container.nodeValue === '\n') { if (result.container.nodeValue === '\n') {
var hdLfElt = result.container.parentNode const hdLfElt = result.container.parentNode;
if (hdLfElt.className === 'hd-lf' && hdLfElt.previousSibling && hdLfElt.previousSibling.tagName === 'BR') { if (hdLfElt.className === 'hd-lf' && hdLfElt.previousSibling && hdLfElt.previousSibling.tagName === 'BR') {
result.container = hdLfElt.parentNode result.container = hdLfElt.parentNode;
result.offsetInContainer = Array.prototype.indexOf.call(result.container.childNodes, result.offsetInContainer === 0 ? hdLfElt.previousSibling : hdLfElt) result.offsetInContainer = Array.prototype.indexOf.call(
result.container.childNodes,
result.offsetInContainer === 0 ? hdLfElt.previousSibling : hdLfElt,
);
} }
} }
return result return result;
} };
this.createRange = function (start, end) { this.createRange = function (start, end) {
var range = editor.$document.createRange() const range = document.createRange();
if (start === end) { const startContainer = isNaN(start) ? start : this.findContainer(start < 0 ? 0 : start);
end = start = isNaN(start) ? start : this.findContainer(start < 0 ? 0 : start) let endContainer = startContainer;
} else { if (start !== end) {
start = isNaN(start) ? start : this.findContainer(start < 0 ? 0 : start) endContainer = isNaN(end) ? end : this.findContainer(end < 0 ? 0 : end);
end = isNaN(end) ? end : this.findContainer(end < 0 ? 0 : end)
}
range.setStart(start.container, start.offsetInContainer)
range.setEnd(end.container, end.offsetInContainer)
return range
} }
range.setStart(startContainer.container, startContainer.offsetInContainer);
range.setEnd(endContainer.container, endContainer.offsetInContainer);
return range;
};
var adjustScroll let adjustScroll;
var debouncedUpdateCursorCoordinates = debounce(function () { const debouncedUpdateCursorCoordinates = debounce(() => {
var coordinates = this.getCoordinates(this.selectionEnd, this.selectionEndContainer, this.selectionEndOffset) const coordinates = this.getCoordinates(
this.selectionEnd, this.selectionEndContainer, this.selectionEndOffset);
if (this.cursorCoordinates.top !== coordinates.top || if (this.cursorCoordinates.top !== coordinates.top ||
this.cursorCoordinates.height !== coordinates.height || this.cursorCoordinates.height !== coordinates.height ||
this.cursorCoordinates.left !== coordinates.left this.cursorCoordinates.left !== coordinates.left
) { ) {
this.cursorCoordinates = coordinates this.cursorCoordinates = coordinates;
this.$trigger('cursorCoordinatesChanged', coordinates) this.$trigger('cursorCoordinatesChanged', coordinates);
} }
if (adjustScroll) { if (adjustScroll) {
var scrollEltHeight = scrollElt.clientHeight; let scrollEltHeight = scrollElt.clientHeight;
if (typeof adjustScroll === 'number') { if (typeof adjustScroll === 'number') {
scrollEltHeight -= adjustScroll scrollEltHeight -= adjustScroll;
} }
var adjustment = scrollEltHeight / 2 * editor.options.getCursorFocusRatio() const adjustment = (scrollEltHeight / 2) * editor.options.getCursorFocusRatio();
var cursorTop = this.cursorCoordinates.top + this.cursorCoordinates.height / 2 let cursorTop = this.cursorCoordinates.top + (this.cursorCoordinates.height / 2);
// Adjust cursorTop with contentElt position relative to scrollElt // Adjust cursorTop with contentElt position relative to scrollElt
cursorTop += contentElt.getBoundingClientRect().top - scrollElt.getBoundingClientRect().top + scrollElt.scrollTop; cursorTop += (contentElt.getBoundingClientRect().top - scrollElt.getBoundingClientRect().top)
var minScrollTop = cursorTop - adjustment + scrollElt.scrollTop;
var maxScrollTop = cursorTop + adjustment - scrollEltHeight const minScrollTop = cursorTop - adjustment;
const maxScrollTop = (cursorTop + adjustment) - scrollEltHeight;
if (scrollElt.scrollTop > minScrollTop) { if (scrollElt.scrollTop > minScrollTop) {
scrollElt.scrollTop = minScrollTop scrollElt.scrollTop = minScrollTop;
} else if (scrollElt.scrollTop < maxScrollTop) { } else if (scrollElt.scrollTop < maxScrollTop) {
scrollElt.scrollTop = maxScrollTop scrollElt.scrollTop = maxScrollTop;
} }
} }
adjustScroll = false adjustScroll = false;
}.cl_bind(this)) });
this.updateCursorCoordinates = function (adjustScrollParam) { this.updateCursorCoordinates = function (adjustScrollParam) {
adjustScroll = adjustScroll || adjustScrollParam adjustScroll = adjustScroll || adjustScrollParam;
debouncedUpdateCursorCoordinates() debouncedUpdateCursorCoordinates();
} };
var oldSelectionRange let oldSelectionRange;
function checkSelection(selectionRange) { function checkSelection(selectionRange) {
if (!oldSelectionRange || if (!oldSelectionRange ||
@ -82,170 +86,159 @@ function SelectionMgr(editor) {
oldSelectionRange.endContainer !== selectionRange.endContainer || oldSelectionRange.endContainer !== selectionRange.endContainer ||
oldSelectionRange.endOffset !== selectionRange.endOffset oldSelectionRange.endOffset !== selectionRange.endOffset
) { ) {
oldSelectionRange = selectionRange oldSelectionRange = selectionRange;
self.$trigger('selectionChanged', self.selectionStart, self.selectionEnd, selectionRange) self.$trigger('selectionChanged', self.selectionStart, self.selectionEnd, selectionRange);
return true return true;
} }
return false;
} }
this.hasFocus = function() { this.hasFocus = () => contentElt === document.activeElement;
return contentElt === editor.$document.activeElement;
}
this.restoreSelection = function () { this.restoreSelection = () => {
var min = Math.min(this.selectionStart, this.selectionEnd) const min = Math.min(this.selectionStart, this.selectionEnd);
var max = Math.max(this.selectionStart, this.selectionEnd) const max = Math.max(this.selectionStart, this.selectionEnd);
var selectionRange = this.createRange(min, max) const selectionRange = this.createRange(min, max);
if (editor.$document.contains(selectionRange.commonAncestorContainer)) { if (!document.contains(selectionRange.commonAncestorContainer)) {
var selection = editor.$window.getSelection() return null;
selection.removeAllRanges() }
var isBackward = this.selectionStart > this.selectionEnd const selection = window.getSelection();
if (selection.extend) { selection.removeAllRanges();
var beginRange = selectionRange.cloneRange() const isBackward = this.selectionStart > this.selectionEnd;
beginRange.collapse(!isBackward) if (isBackward && selection.extend) {
selection.addRange(beginRange) const beginRange = selectionRange.cloneRange();
if (isBackward) { beginRange.collapse(false);
selection.extend(selectionRange.startContainer, selectionRange.startOffset) selection.addRange(beginRange);
selection.extend(selectionRange.startContainer, selectionRange.startOffset);
} else { } else {
selection.extend(selectionRange.endContainer, selectionRange.endOffset) selection.addRange(selectionRange);
}
} else {
selection.addRange(selectionRange)
}
checkSelection(selectionRange)
return selectionRange
} }
checkSelection(selectionRange);
return selectionRange;
};
const saveLastSelection = debounce(() => {
lastSelectionStart = self.selectionStart;
lastSelectionEnd = self.selectionEnd;
}, 50);
function setSelection(start = self.selectionStart, end = this.selectionEnd) {
self.selectionStart = start < 0 ? 0 : start;
self.selectionEnd = end < 0 ? 0 : end;
saveLastSelection();
} }
var saveLastSelection = debounce(function () { this.setSelectionStartEnd = (start, end) => {
lastSelectionStart = self.selectionStart setSelection(start, end);
lastSelectionEnd = self.selectionEnd return this.hasFocus() && this.restoreSelection();
}, 50) };
function setSelection(start, end) { this.saveSelectionState = (() => {
if (start === undefined) {
start = self.selectionStart
}
if (start < 0) {
start = 0
}
if (end === undefined) {
end = this.selectionEnd
}
if (end < 0) {
end = 0
}
self.selectionStart = start
self.selectionEnd = end
saveLastSelection()
}
this.setSelectionStartEnd = function (start, end) {
setSelection(start, end)
return this.hasFocus() && this.restoreSelection()
}
this.saveSelectionState = (function () {
// Credit: https://github.com/timdown/rangy // Credit: https://github.com/timdown/rangy
function arrayContains(arr, val) { function arrayContains(arr, val) {
var i = arr.length let i = arr.length;
while (i--) { while (i) {
i -= 1;
if (arr[i] === val) { if (arr[i] === val) {
return true return true;
} }
} }
return false return false;
} }
function getClosestAncestorIn(node, ancestor, selfIsAncestor) { function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
var p let p;
var n = selfIsAncestor ? node : node.parentNode let n = selfIsAncestor ? node : node.parentNode;
while (n) { while (n) {
p = n.parentNode p = n.parentNode;
if (p === ancestor) { if (p === ancestor) {
return n return n;
} }
n = p n = p;
} }
return null return null;
} }
function getNodeIndex(node) { function getNodeIndex(node) {
var i = 0 let i = 0;
while ((node = node.previousSibling)) { let previousSibling = node.previousSibling;
++i while (previousSibling) {
i += 1;
previousSibling = node.previousSibling;
} }
return i return i;
} }
function getCommonAncestor(node1, node2) { function getCommonAncestor(node1, node2) {
var ancestors = [] const ancestors = [];
var n let n;
for (n = node1; n; n = n.parentNode) { for (n = node1; n; n = n.parentNode) {
ancestors.push(n) ancestors.push(n);
} }
for (n = node2; n; n = n.parentNode) { for (n = node2; n; n = n.parentNode) {
if (arrayContains(ancestors, n)) { if (arrayContains(ancestors, n)) {
return n return n;
} }
} }
return null return null;
} }
function comparePoints(nodeA, offsetA, nodeB, offsetB) { function comparePoints(nodeA, offsetA, nodeB, offsetB) {
// See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
var nodeC, root, childA, childB, n let n;
if (nodeA === nodeB) { if (nodeA === nodeB) {
// Case 1: nodes are the same // Case 1: nodes are the same
return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1 if (offsetA === offsetB) {
} else if ( return 0;
(nodeC = getClosestAncestorIn(nodeB, nodeA, true)) }
) { return offsetA < offsetB ? -1 : 1;
}
let nodeC = getClosestAncestorIn(nodeB, nodeA, true);
if (nodeC) {
// Case 2: node C (container B or an ancestor) is a child node of A // Case 2: node C (container B or an ancestor) is a child node of A
return offsetA <= getNodeIndex(nodeC) ? -1 : 1 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
} else if ( }
(nodeC = getClosestAncestorIn(nodeA, nodeB, true)) nodeC = getClosestAncestorIn(nodeA, nodeB, true);
) { if (nodeC) {
// Case 3: node C (container A or an ancestor) is a child node of B // Case 3: node C (container A or an ancestor) is a child node of B
return getNodeIndex(nodeC) < offsetB ? -1 : 1 return getNodeIndex(nodeC) < offsetB ? -1 : 1;
} else { }
root = getCommonAncestor(nodeA, nodeB) const root = getCommonAncestor(nodeA, nodeB);
if (!root) { if (!root) {
throw new Error('comparePoints error: nodes have no common ancestor') throw new Error('comparePoints error: nodes have no common ancestor');
} }
// Case 4: containers are siblings or descendants of siblings // Case 4: containers are siblings or descendants of siblings
childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true) const childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true) const childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
if (childA === childB) { if (childA === childB) {
// This shouldn't be possible // This shouldn't be possible
throw module.createError('comparePoints got to case 4 and childA and childB are the same!') throw module.createError('comparePoints got to case 4 and childA and childB are the same!');
} else { } else {
n = root.firstChild n = root.firstChild;
while (n) { while (n) {
if (n === childA) { if (n === childA) {
return -1 return -1;
} else if (n === childB) { } else if (n === childB) {
return 1 return 1;
}
n = n.nextSibling
} }
n = n.nextSibling;
} }
} }
} }
function save() { function save() {
var result let result;
if (self.hasFocus()) { if (self.hasFocus()) {
var selectionStart = self.selectionStart const selectionStart = self.selectionStart;
var selectionEnd = self.selectionEnd const selectionEnd = self.selectionEnd;
var selection = editor.$window.getSelection() const selection = window.getSelection();
if (selection.rangeCount > 0) { if (selection.rangeCount > 0) {
var selectionRange = selection.getRangeAt(0) const selectionRange = selection.getRangeAt(0);
var node = selectionRange.startContainer const node = selectionRange.startContainer;
if ((contentElt.compareDocumentPosition(node) & window.Node.DOCUMENT_POSITION_CONTAINED_BY) || contentElt === node) { if ((contentElt.compareDocumentPosition(node) & window.Node.DOCUMENT_POSITION_CONTAINED_BY) || contentElt === node) {
var offset = selectionRange.startOffset var offset = selectionRange.startOffset
if (node.firstChild && offset > 0) { if (node.firstChild && offset > 0) {

View File

@ -1,4 +1,4 @@
import './clunderscore'; import '../../libs/clunderscore';
import cledit from './cleditCore'; import cledit from './cleditCore';
import './cleditHighlighter'; import './cleditHighlighter';
import './cleditKeystroke'; import './cleditKeystroke';

View File

@ -393,7 +393,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
instantPreview = false; instantPreview = false;
}; };
const debouncedRefreshPreview = debounce(refreshPreview, 20); const debouncedRefreshPreview = debounce(refreshPreview, 50);
const onEditorChanged = const onEditorChanged =
(sectionList = this.sectionList, selectionRange = this.selectionRange) => { (sectionList = this.sectionList, selectionRange = this.selectionRange) => {

View File

@ -56,7 +56,7 @@ export default {
'layoutSettings', 'layoutSettings',
'tokens', 'tokens',
], ],
textMaxLength: 150000, textMaxLength: 250000,
sanitizeText(text) { sanitizeText(text) {
const result = `${text || ''}`.slice(0, this.textMaxLength); const result = `${text || ''}`.slice(0, this.textMaxLength);
// last char must be a `\n`. // last char must be a `\n`.