cledit refactoring
This commit is contained in:
parent
efce97c16c
commit
f0721c9405
@ -65,6 +65,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
computeText() {
|
||||
setTimeout(() => {
|
||||
this.textSelection = false;
|
||||
let text = editorSvc.clEditor.getContent();
|
||||
const beforeText = text.slice(0, editorSvc.clEditor.selectionMgr.selectionEnd);
|
||||
@ -80,8 +81,10 @@ export default {
|
||||
this.textStats.forEach((stat) => {
|
||||
stat.value = (text.match(stat.regex) || []).length;
|
||||
});
|
||||
}, 10);
|
||||
},
|
||||
computeHtml() {
|
||||
setTimeout(() => {
|
||||
let text;
|
||||
if (editorSvc.previewSelectionRange) {
|
||||
text = `${editorSvc.previewSelectionRange}`;
|
||||
@ -96,6 +99,7 @@ export default {
|
||||
stat.value = (text.match(stat.regex) || []).length;
|
||||
});
|
||||
}
|
||||
}, 10);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -5,7 +5,7 @@
|
||||
</div>
|
||||
<div class="side-bar__info" v-if="syncLocations.length">
|
||||
<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>
|
||||
<div>Synchronize now</div>
|
||||
<span>Download / upload file changes.</span>
|
||||
@ -16,11 +16,6 @@
|
||||
<span>Manage current file synchronized locations.</span>
|
||||
</menu-entry>
|
||||
</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>
|
||||
<div v-for="token in googleDriveTokens" :key="token.sub">
|
||||
<menu-entry @click.native="openGoogleDrive(token)">
|
||||
@ -118,10 +113,6 @@ export default {
|
||||
currentFileName() {
|
||||
return this.$store.getters['file/current'].name;
|
||||
},
|
||||
isSyncPossible() {
|
||||
return this.syncToken ||
|
||||
this.$store.getters['syncLocation/current'].length;
|
||||
},
|
||||
googleDriveTokens() {
|
||||
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive);
|
||||
},
|
||||
|
@ -104,7 +104,6 @@ export default {
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -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(/ /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, '&').replace(/</g, '<').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
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
]
|
@ -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
|
426
src/services/cledit/cleditCore.js
Normal file
426
src/services/cledit/cleditCore.js
Normal 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(/ /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, '&').replace(/</g, '<').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;
|
198
src/services/cledit/cleditHighlighter.js
Normal file
198
src/services/cledit/cleditHighlighter.js
Normal 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;
|
||||
|
185
src/services/cledit/cleditKeystroke.js
Normal file
185
src/services/cledit/cleditKeystroke.js
Normal 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;
|
||||
}),
|
||||
];
|
49
src/services/cledit/cleditMarker.js
Normal file
49
src/services/cledit/cleditMarker.js
Normal 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;
|
@ -1,79 +1,83 @@
|
||||
var cledit = require('./cleditCore')
|
||||
import cledit from './cleditCore';
|
||||
|
||||
function SelectionMgr(editor) {
|
||||
var debounce = cledit.Utils.debounce
|
||||
var contentElt = editor.$contentElt
|
||||
var scrollElt = editor.$scrollElt
|
||||
cledit.Utils.createEventHooks(this)
|
||||
const debounce = cledit.Utils.debounce;
|
||||
const contentElt = editor.$contentElt;
|
||||
const scrollElt = editor.$scrollElt;
|
||||
cledit.Utils.createEventHooks(this);
|
||||
|
||||
var self = this
|
||||
var lastSelectionStart = 0
|
||||
var lastSelectionEnd = 0
|
||||
this.selectionStart = 0
|
||||
this.selectionEnd = 0
|
||||
this.cursorCoordinates = {}
|
||||
const self = this;
|
||||
let lastSelectionStart = 0;
|
||||
let lastSelectionEnd = 0;
|
||||
this.selectionStart = 0;
|
||||
this.selectionEnd = 0;
|
||||
this.cursorCoordinates = {};
|
||||
|
||||
this.findContainer = function (offset) {
|
||||
var result = cledit.Utils.findContainer(contentElt, offset)
|
||||
this.findContainer = (offset) => {
|
||||
const result = cledit.Utils.findContainer(contentElt, offset);
|
||||
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') {
|
||||
result.container = hdLfElt.parentNode
|
||||
result.offsetInContainer = Array.prototype.indexOf.call(result.container.childNodes, result.offsetInContainer === 0 ? hdLfElt.previousSibling : hdLfElt)
|
||||
result.container = hdLfElt.parentNode;
|
||||
result.offsetInContainer = Array.prototype.indexOf.call(
|
||||
result.container.childNodes,
|
||||
result.offsetInContainer === 0 ? hdLfElt.previousSibling : hdLfElt,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
this.createRange = function (start, end) {
|
||||
var range = editor.$document.createRange()
|
||||
if (start === end) {
|
||||
end = start = isNaN(start) ? start : this.findContainer(start < 0 ? 0 : start)
|
||||
} else {
|
||||
start = isNaN(start) ? start : this.findContainer(start < 0 ? 0 : start)
|
||||
end = isNaN(end) ? end : this.findContainer(end < 0 ? 0 : end)
|
||||
}
|
||||
range.setStart(start.container, start.offsetInContainer)
|
||||
range.setEnd(end.container, end.offsetInContainer)
|
||||
return range
|
||||
const range = document.createRange();
|
||||
const startContainer = isNaN(start) ? start : this.findContainer(start < 0 ? 0 : start);
|
||||
let endContainer = startContainer;
|
||||
if (start !== end) {
|
||||
endContainer = isNaN(end) ? end : this.findContainer(end < 0 ? 0 : end);
|
||||
}
|
||||
range.setStart(startContainer.container, startContainer.offsetInContainer);
|
||||
range.setEnd(endContainer.container, endContainer.offsetInContainer);
|
||||
return range;
|
||||
};
|
||||
|
||||
var adjustScroll
|
||||
var debouncedUpdateCursorCoordinates = debounce(function () {
|
||||
var coordinates = this.getCoordinates(this.selectionEnd, this.selectionEndContainer, this.selectionEndOffset)
|
||||
let adjustScroll;
|
||||
const debouncedUpdateCursorCoordinates = debounce(() => {
|
||||
const coordinates = this.getCoordinates(
|
||||
this.selectionEnd, this.selectionEndContainer, this.selectionEndOffset);
|
||||
if (this.cursorCoordinates.top !== coordinates.top ||
|
||||
this.cursorCoordinates.height !== coordinates.height ||
|
||||
this.cursorCoordinates.left !== coordinates.left
|
||||
) {
|
||||
this.cursorCoordinates = coordinates
|
||||
this.$trigger('cursorCoordinatesChanged', coordinates)
|
||||
this.cursorCoordinates = coordinates;
|
||||
this.$trigger('cursorCoordinatesChanged', coordinates);
|
||||
}
|
||||
if (adjustScroll) {
|
||||
var scrollEltHeight = scrollElt.clientHeight;
|
||||
let scrollEltHeight = scrollElt.clientHeight;
|
||||
if (typeof adjustScroll === 'number') {
|
||||
scrollEltHeight -= adjustScroll
|
||||
scrollEltHeight -= adjustScroll;
|
||||
}
|
||||
var adjustment = scrollEltHeight / 2 * editor.options.getCursorFocusRatio()
|
||||
var cursorTop = this.cursorCoordinates.top + this.cursorCoordinates.height / 2
|
||||
const adjustment = (scrollEltHeight / 2) * editor.options.getCursorFocusRatio();
|
||||
let cursorTop = this.cursorCoordinates.top + (this.cursorCoordinates.height / 2);
|
||||
// Adjust cursorTop with contentElt position relative to scrollElt
|
||||
cursorTop += contentElt.getBoundingClientRect().top - scrollElt.getBoundingClientRect().top + scrollElt.scrollTop;
|
||||
var minScrollTop = cursorTop - adjustment
|
||||
var maxScrollTop = cursorTop + adjustment - scrollEltHeight
|
||||
cursorTop += (contentElt.getBoundingClientRect().top - scrollElt.getBoundingClientRect().top)
|
||||
+ scrollElt.scrollTop;
|
||||
const minScrollTop = cursorTop - adjustment;
|
||||
const maxScrollTop = (cursorTop + adjustment) - scrollEltHeight;
|
||||
if (scrollElt.scrollTop > minScrollTop) {
|
||||
scrollElt.scrollTop = minScrollTop
|
||||
scrollElt.scrollTop = minScrollTop;
|
||||
} else if (scrollElt.scrollTop < maxScrollTop) {
|
||||
scrollElt.scrollTop = maxScrollTop
|
||||
scrollElt.scrollTop = maxScrollTop;
|
||||
}
|
||||
}
|
||||
adjustScroll = false
|
||||
}.cl_bind(this))
|
||||
adjustScroll = false;
|
||||
});
|
||||
|
||||
this.updateCursorCoordinates = function (adjustScrollParam) {
|
||||
adjustScroll = adjustScroll || adjustScrollParam
|
||||
debouncedUpdateCursorCoordinates()
|
||||
}
|
||||
adjustScroll = adjustScroll || adjustScrollParam;
|
||||
debouncedUpdateCursorCoordinates();
|
||||
};
|
||||
|
||||
var oldSelectionRange
|
||||
let oldSelectionRange;
|
||||
|
||||
function checkSelection(selectionRange) {
|
||||
if (!oldSelectionRange ||
|
||||
@ -82,170 +86,159 @@ function SelectionMgr(editor) {
|
||||
oldSelectionRange.endContainer !== selectionRange.endContainer ||
|
||||
oldSelectionRange.endOffset !== selectionRange.endOffset
|
||||
) {
|
||||
oldSelectionRange = selectionRange
|
||||
self.$trigger('selectionChanged', self.selectionStart, self.selectionEnd, selectionRange)
|
||||
return true
|
||||
oldSelectionRange = selectionRange;
|
||||
self.$trigger('selectionChanged', self.selectionStart, self.selectionEnd, selectionRange);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
this.hasFocus = function() {
|
||||
return contentElt === editor.$document.activeElement;
|
||||
}
|
||||
this.hasFocus = () => contentElt === document.activeElement;
|
||||
|
||||
this.restoreSelection = function () {
|
||||
var min = Math.min(this.selectionStart, this.selectionEnd)
|
||||
var max = Math.max(this.selectionStart, this.selectionEnd)
|
||||
var selectionRange = this.createRange(min, max)
|
||||
if (editor.$document.contains(selectionRange.commonAncestorContainer)) {
|
||||
var selection = editor.$window.getSelection()
|
||||
selection.removeAllRanges()
|
||||
var isBackward = this.selectionStart > this.selectionEnd
|
||||
if (selection.extend) {
|
||||
var beginRange = selectionRange.cloneRange()
|
||||
beginRange.collapse(!isBackward)
|
||||
selection.addRange(beginRange)
|
||||
if (isBackward) {
|
||||
selection.extend(selectionRange.startContainer, selectionRange.startOffset)
|
||||
this.restoreSelection = () => {
|
||||
const min = Math.min(this.selectionStart, this.selectionEnd);
|
||||
const max = Math.max(this.selectionStart, this.selectionEnd);
|
||||
const selectionRange = this.createRange(min, max);
|
||||
if (!document.contains(selectionRange.commonAncestorContainer)) {
|
||||
return null;
|
||||
}
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
const isBackward = this.selectionStart > this.selectionEnd;
|
||||
if (isBackward && selection.extend) {
|
||||
const beginRange = selectionRange.cloneRange();
|
||||
beginRange.collapse(false);
|
||||
selection.addRange(beginRange);
|
||||
selection.extend(selectionRange.startContainer, selectionRange.startOffset);
|
||||
} else {
|
||||
selection.extend(selectionRange.endContainer, selectionRange.endOffset)
|
||||
}
|
||||
} else {
|
||||
selection.addRange(selectionRange)
|
||||
}
|
||||
checkSelection(selectionRange)
|
||||
return selectionRange
|
||||
selection.addRange(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 () {
|
||||
lastSelectionStart = self.selectionStart
|
||||
lastSelectionEnd = self.selectionEnd
|
||||
}, 50)
|
||||
this.setSelectionStartEnd = (start, end) => {
|
||||
setSelection(start, end);
|
||||
return this.hasFocus() && this.restoreSelection();
|
||||
};
|
||||
|
||||
function setSelection(start, end) {
|
||||
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 () {
|
||||
this.saveSelectionState = (() => {
|
||||
// Credit: https://github.com/timdown/rangy
|
||||
function arrayContains(arr, val) {
|
||||
var i = arr.length
|
||||
while (i--) {
|
||||
let i = arr.length;
|
||||
while (i) {
|
||||
i -= 1;
|
||||
if (arr[i] === val) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
|
||||
var p
|
||||
var n = selfIsAncestor ? node : node.parentNode
|
||||
let p;
|
||||
let n = selfIsAncestor ? node : node.parentNode;
|
||||
while (n) {
|
||||
p = n.parentNode
|
||||
p = n.parentNode;
|
||||
if (p === ancestor) {
|
||||
return n
|
||||
return n;
|
||||
}
|
||||
n = p
|
||||
n = p;
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
function getNodeIndex(node) {
|
||||
var i = 0
|
||||
while ((node = node.previousSibling)) {
|
||||
++i
|
||||
let i = 0;
|
||||
let previousSibling = node.previousSibling;
|
||||
while (previousSibling) {
|
||||
i += 1;
|
||||
previousSibling = node.previousSibling;
|
||||
}
|
||||
return i
|
||||
return i;
|
||||
}
|
||||
|
||||
function getCommonAncestor(node1, node2) {
|
||||
var ancestors = []
|
||||
var n
|
||||
const ancestors = [];
|
||||
let n;
|
||||
for (n = node1; n; n = n.parentNode) {
|
||||
ancestors.push(n)
|
||||
ancestors.push(n);
|
||||
}
|
||||
|
||||
for (n = node2; n; n = n.parentNode) {
|
||||
if (arrayContains(ancestors, n)) {
|
||||
return n
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
function comparePoints(nodeA, offsetA, nodeB, offsetB) {
|
||||
// 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) {
|
||||
// Case 1: nodes are the same
|
||||
return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1
|
||||
} else if (
|
||||
(nodeC = getClosestAncestorIn(nodeB, nodeA, true))
|
||||
) {
|
||||
if (offsetA === offsetB) {
|
||||
return 0;
|
||||
}
|
||||
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
|
||||
return offsetA <= getNodeIndex(nodeC) ? -1 : 1
|
||||
} else if (
|
||||
(nodeC = getClosestAncestorIn(nodeA, nodeB, true))
|
||||
) {
|
||||
return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
|
||||
}
|
||||
nodeC = getClosestAncestorIn(nodeA, nodeB, true);
|
||||
if (nodeC) {
|
||||
// Case 3: node C (container A or an ancestor) is a child node of B
|
||||
return getNodeIndex(nodeC) < offsetB ? -1 : 1
|
||||
} else {
|
||||
root = getCommonAncestor(nodeA, nodeB)
|
||||
return getNodeIndex(nodeC) < offsetB ? -1 : 1;
|
||||
}
|
||||
const root = getCommonAncestor(nodeA, nodeB);
|
||||
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
|
||||
childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true)
|
||||
childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true)
|
||||
const childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
|
||||
const childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
|
||||
|
||||
if (childA === childB) {
|
||||
// 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 {
|
||||
n = root.firstChild
|
||||
n = root.firstChild;
|
||||
while (n) {
|
||||
if (n === childA) {
|
||||
return -1
|
||||
return -1;
|
||||
} else if (n === childB) {
|
||||
return 1
|
||||
}
|
||||
n = n.nextSibling
|
||||
return 1;
|
||||
}
|
||||
n = n.nextSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
var result
|
||||
let result;
|
||||
if (self.hasFocus()) {
|
||||
var selectionStart = self.selectionStart
|
||||
var selectionEnd = self.selectionEnd
|
||||
var selection = editor.$window.getSelection()
|
||||
const selectionStart = self.selectionStart;
|
||||
const selectionEnd = self.selectionEnd;
|
||||
const selection = window.getSelection();
|
||||
if (selection.rangeCount > 0) {
|
||||
var selectionRange = selection.getRangeAt(0)
|
||||
var node = selectionRange.startContainer
|
||||
const selectionRange = selection.getRangeAt(0);
|
||||
const node = selectionRange.startContainer;
|
||||
if ((contentElt.compareDocumentPosition(node) & window.Node.DOCUMENT_POSITION_CONTAINED_BY) || contentElt === node) {
|
||||
var offset = selectionRange.startOffset
|
||||
if (node.firstChild && offset > 0) {
|
@ -1,4 +1,4 @@
|
||||
import './clunderscore';
|
||||
import '../../libs/clunderscore';
|
||||
import cledit from './cleditCore';
|
||||
import './cleditHighlighter';
|
||||
import './cleditKeystroke';
|
@ -393,7 +393,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
||||
instantPreview = false;
|
||||
};
|
||||
|
||||
const debouncedRefreshPreview = debounce(refreshPreview, 20);
|
||||
const debouncedRefreshPreview = debounce(refreshPreview, 50);
|
||||
|
||||
const onEditorChanged =
|
||||
(sectionList = this.sectionList, selectionRange = this.selectionRange) => {
|
||||
|
@ -56,7 +56,7 @@ export default {
|
||||
'layoutSettings',
|
||||
'tokens',
|
||||
],
|
||||
textMaxLength: 150000,
|
||||
textMaxLength: 250000,
|
||||
sanitizeText(text) {
|
||||
const result = `${text || ''}`.slice(0, this.textMaxLength);
|
||||
// last char must be a `\n`.
|
||||
|
Loading…
Reference in New Issue
Block a user