cledit refactoring
This commit is contained in:
parent
efce97c16c
commit
f0721c9405
@ -65,37 +65,41 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
computeText() {
|
computeText() {
|
||||||
this.textSelection = false;
|
setTimeout(() => {
|
||||||
let text = editorSvc.clEditor.getContent();
|
this.textSelection = false;
|
||||||
const beforeText = text.slice(0, editorSvc.clEditor.selectionMgr.selectionEnd);
|
let text = editorSvc.clEditor.getContent();
|
||||||
const beforeLines = beforeText.split('\n');
|
const beforeText = text.slice(0, editorSvc.clEditor.selectionMgr.selectionEnd);
|
||||||
this.line = beforeLines.length;
|
const beforeLines = beforeText.split('\n');
|
||||||
this.column = beforeLines.pop().length;
|
this.line = beforeLines.length;
|
||||||
|
this.column = beforeLines.pop().length;
|
||||||
|
|
||||||
const selectedText = editorSvc.clEditor.selectionMgr.getSelectedText();
|
const selectedText = editorSvc.clEditor.selectionMgr.getSelectedText();
|
||||||
if (selectedText) {
|
if (selectedText) {
|
||||||
this.textSelection = true;
|
this.textSelection = true;
|
||||||
text = selectedText;
|
text = selectedText;
|
||||||
}
|
}
|
||||||
this.textStats.forEach((stat) => {
|
this.textStats.forEach((stat) => {
|
||||||
stat.value = (text.match(stat.regex) || []).length;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
computeHtml() {
|
|
||||||
let text;
|
|
||||||
if (editorSvc.previewSelectionRange) {
|
|
||||||
text = `${editorSvc.previewSelectionRange}`;
|
|
||||||
}
|
|
||||||
this.htmlSelection = true;
|
|
||||||
if (!text) {
|
|
||||||
this.htmlSelection = false;
|
|
||||||
text = editorSvc.previewText;
|
|
||||||
}
|
|
||||||
if (text != null) {
|
|
||||||
this.htmlStats.forEach((stat) => {
|
|
||||||
stat.value = (text.match(stat.regex) || []).length;
|
stat.value = (text.match(stat.regex) || []).length;
|
||||||
});
|
});
|
||||||
}
|
}, 10);
|
||||||
|
},
|
||||||
|
computeHtml() {
|
||||||
|
setTimeout(() => {
|
||||||
|
let text;
|
||||||
|
if (editorSvc.previewSelectionRange) {
|
||||||
|
text = `${editorSvc.previewSelectionRange}`;
|
||||||
|
}
|
||||||
|
this.htmlSelection = true;
|
||||||
|
if (!text) {
|
||||||
|
this.htmlSelection = false;
|
||||||
|
text = editorSvc.previewText;
|
||||||
|
}
|
||||||
|
if (text != null) {
|
||||||
|
this.htmlStats.forEach((stat) => {
|
||||||
|
stat.value = (text.match(stat.regex) || []).length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
|
@ -104,7 +104,6 @@ export default {
|
|||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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) {
|
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.setStart(startContainer.container, startContainer.offsetInContainer);
|
||||||
range.setEnd(end.container, end.offsetInContainer)
|
range.setEnd(endContainer.container, endContainer.offsetInContainer);
|
||||||
return range
|
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 = () => {
|
||||||
|
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.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.restoreSelection = function () {
|
this.setSelectionStartEnd = (start, end) => {
|
||||||
var min = Math.min(this.selectionStart, this.selectionEnd)
|
setSelection(start, end);
|
||||||
var max = Math.max(this.selectionStart, this.selectionEnd)
|
return this.hasFocus() && this.restoreSelection();
|
||||||
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)
|
|
||||||
} else {
|
|
||||||
selection.extend(selectionRange.endContainer, selectionRange.endOffset)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selection.addRange(selectionRange)
|
|
||||||
}
|
|
||||||
checkSelection(selectionRange)
|
|
||||||
return selectionRange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var saveLastSelection = debounce(function () {
|
this.saveSelectionState = (() => {
|
||||||
lastSelectionStart = self.selectionStart
|
|
||||||
lastSelectionEnd = self.selectionEnd
|
|
||||||
}, 50)
|
|
||||||
|
|
||||||
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 () {
|
|
||||||
// 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))
|
|
||||||
) {
|
|
||||||
// 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))
|
|
||||||
) {
|
|
||||||
// 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)
|
|
||||||
if (!root) {
|
|
||||||
throw new Error('comparePoints error: nodes have no common ancestor')
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const root = getCommonAncestor(nodeA, nodeB);
|
||||||
|
if (!root) {
|
||||||
|
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) {
|
@ -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';
|
@ -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) => {
|
||||||
|
@ -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`.
|
||||||
|
Loading…
Reference in New Issue
Block a user