suneditor
Version:
Vanilla JavaScript based WYSIWYG web editor
599 lines (511 loc) • 20.7 kB
JavaScript
import { dom, unicode } from '../../../helper';
/**
* @typedef {Object} EffectContext_keydown
* @property {import('../ports').EventReducerPorts} ports - Ports for interacting with editor
* @property {import('../reducers/keydown.reducer').KeydownReducerCtx} ctx - Reducer context
*/
/**
* @typedef {(ctx: EffectContext_keydown, payload?: *) => *} Effect
*/
/** @type {Record<string, Effect>} */
export default {
/** [backspace and delete] */
/** @action delFormatRemoveAndMove */
'del.format.removeAndMove': ({ ports }, { container, formatEl }) => {
const rInfo = ports.html.remove();
if (rInfo.commonCon !== rInfo.container && formatEl.parentElement) {
if (formatEl.contains(container)) {
const focusNode = LineDelete_next(formatEl);
ports.selection.setRange(focusNode, focusNode.textContent.length, focusNode, focusNode.textContent.length);
} else {
const { focusNode, focusOffset } = LineDelete_prev(formatEl);
ports.selection.setRange(focusNode, focusOffset, focusNode, focusOffset);
}
}
},
/** [backspace] */
/** @action backspaceBrLineStrip — extract first line from brLine (PRE) */
'backspace.brline.strip': ({ ctx, ports }, { formatEl }) => {
const defaultTag = ctx.options.get('defaultLine');
const parent = formatEl.parentNode;
const newLine = dom.utils.createElement(defaultTag);
while (formatEl.firstChild && !dom.check.isBreak(formatEl.firstChild)) {
newLine.appendChild(formatEl.firstChild);
}
if (formatEl.firstChild && dom.check.isBreak(formatEl.firstChild)) {
formatEl.removeChild(formatEl.firstChild);
}
if (!newLine.firstChild) newLine.innerHTML = '<br>';
parent.insertBefore(newLine, formatEl);
if (!formatEl.firstChild || (!formatEl.textContent.trim() && !formatEl.querySelector('br'))) {
parent.removeChild(formatEl);
}
const focusNode = newLine.firstChild;
ports.selection.setRange(focusNode, 0, focusNode, 0);
},
/** @action backspaceFormatMaintain */
'backspace.format.maintain': ({ ctx }, { formatEl }) => {
if (formatEl.nodeName.toUpperCase() === ctx.options.get('defaultLine').toUpperCase()) {
formatEl.innerHTML = '<br>';
const attrs = formatEl.attributes;
while (attrs[0]) {
formatEl.removeAttribute(attrs[0].name);
}
} else {
formatEl.parentNode.replaceChild(dom.utils.createElement(ctx.options.get('defaultLine'), null, '<br>'), formatEl);
}
},
/** @action backspaceComponentSelect */
'backspace.component.select': ({ ports }, { selectionNode, range, fileComponentInfo }) => {
let currentZWS = null;
if (dom.check.isBreak(selectionNode)) dom.utils.removeItem(selectionNode);
else if (dom.check.isBreak((currentZWS = range.startContainer.childNodes?.[range.startOffset]))) dom.utils.removeItem(currentZWS);
if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.focusManager.blur();
},
/** @action backspaceComponentRemove */
'backspace.component.remove': ({ ports }, { isList, sel, formatEl, fileComponentInfo }) => {
if (isList) dom.utils.removeItem(sel);
if (formatEl.textContent.length === 0) dom.utils.removeItem(formatEl);
if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.focusManager.blur();
},
/** @action backspaceListMergePrev */
'backspace.list.mergePrev': ({ ports }, { prev, formatEl, rangeEl }) => {
let con = prev === rangeEl.parentNode ? rangeEl.previousSibling : prev.lastChild;
if (!con) {
con = dom.utils.createTextNode(unicode.zeroWidthSpace);
rangeEl.parentNode.insertBefore(con, rangeEl.parentNode.firstChild);
}
const offset = con.nodeType === 3 ? con.textContent.length : 1;
const children = formatEl.childNodes;
let after = con;
let child = children[0];
while ((child = children[0])) {
prev.insertBefore(child, after.nextSibling);
after = child;
}
dom.utils.removeItem(formatEl);
if (rangeEl.children.length === 0) dom.utils.removeItem(rangeEl);
ports.selection.setRange(con, offset, con, offset);
},
/** @action backspaceListRemoveNested */
'backspace.list.removeNested': ({ ports }, { range }) => {
ports.html.remove();
if (range.startContainer.nodeType === 3) {
ports.selection.setRange(range.startContainer, range.startContainer.textContent.length, range.startContainer, range.startContainer.textContent.length);
}
},
/** [delete] */
/** @action deleteComponentSelect */
'delete.component.select': ({ ports }, { formatEl, fileComponentInfo }) => {
if (dom.check.isListCell(formatEl)) {
const prev = fileComponentInfo.container.previousSibling;
if (dom.check.isZeroWidth(prev)) dom.utils.removeItem(prev);
} else if (dom.check.isZeroWidth(formatEl)) {
dom.utils.removeItem(formatEl);
}
if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.focusManager.blur();
},
/** @action deleteComponentSelectNext */
'delete.component.selectNext': ({ ports, ctx }, { formatEl, nextEl }) => {
if (dom.check.isZeroWidth(formatEl)) {
dom.utils.removeItem(formatEl);
// table component
if (dom.check.isTable(nextEl)) {
let cell = /** @type {HTMLElement} */ (dom.query.getEdgeChild(nextEl, dom.check.isTableCell, false));
cell = /** @type {HTMLElement} */ (cell.firstElementChild || cell);
ports.selection.setRange(cell, 0, cell, 0);
return;
}
}
// select file component
const fileComponentInfo = ports.component.get(nextEl);
if (fileComponentInfo) {
ctx.e.stopPropagation();
if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.focusManager.blur();
} else if (ports.component.is(nextEl)) {
ctx.e.stopPropagation();
dom.utils.removeItem(nextEl);
}
},
/** @action deleteListRemoveNested */
'delete.list.removeNested': ({ ports, ctx }, { range, formatEl, rangeEl }) => {
if (range.startContainer !== range.endContainer) ports.html.remove();
const next = /** @type {HTMLElement} */ (dom.utils.arrayFind(formatEl.children, dom.check.isList) || formatEl.nextElementSibling || rangeEl.parentElement.nextElementSibling);
if (next && (dom.check.isList(next) || dom.utils.arrayFind(next.children, dom.check.isList))) {
ctx.e.preventDefault();
let con, children;
if (dom.check.isList(next)) {
const child = next.firstElementChild;
children = child.childNodes;
con = children[0];
while (children[0]) {
formatEl.insertBefore(children[0], next);
}
dom.utils.removeItem(child);
} else {
con = next.firstChild;
children = next.childNodes;
while (children[0]) {
formatEl.appendChild(children[0]);
}
dom.utils.removeItem(next);
}
ports.selection.setRange(con, 0, con, 0);
ports.history.push(true);
}
},
/** [tab] */
/** @action tabFormatIndent */
'tab.format.indent': ({ ports, ctx }, { range, formatEl, shift }) => {
const selectedFormats = ports.format.getLines(null);
const cells = [];
const lines = [];
const firstCell = dom.check.isListCell(selectedFormats[0]),
lastCell = dom.check.isListCell(selectedFormats.at(-1));
let r = {
sc: range.startContainer,
so: range.startOffset,
ec: range.endContainer,
eo: range.endOffset,
};
for (let i = 0, len = selectedFormats.length, f; i < len; i++) {
f = selectedFormats[i];
if (dom.check.isListCell(f)) {
if (!f.previousElementSibling && !shift) {
continue;
} else {
cells.push(f);
}
} else {
lines.push(f);
}
}
// Nested list
if (cells.length > 0) {
r = ports.listFormat.applyNested(cells, shift);
}
// Lines tab
if (lines.length > 0) {
if (!shift) {
if (lines.length === 1) {
let tabSize = ctx.store.get('tabSize') + 1;
if (ctx.options.get('syncTabIndent')) {
const baseIndex = dom.query.findTextIndexOnLine(formatEl, range.startContainer, range.startOffset, (current) => ports.component.is(current));
const prevTabEndIndex = ports.format.isLine(formatEl.previousElementSibling) ? dom.query.findTabEndIndex(formatEl.previousElementSibling, baseIndex, 2) : 0;
if (prevTabEndIndex > baseIndex) {
tabSize = prevTabEndIndex - baseIndex;
}
}
const tabText = dom.utils.createTextNode(new Array(tabSize).join('\u00A0'));
if (!ports.html.insertNode(tabText, { afterNode: null, skipCharCount: false })) return false;
if (!firstCell) {
r.sc = tabText;
r.so = tabText.length;
}
if (!lastCell) {
r.ec = tabText;
r.eo = tabText.length;
}
} else {
const tabText = dom.utils.createTextNode(new Array(ctx.store.get('tabSize') + 1).join('\u00A0'));
const len = lines.length - 1;
for (let i = 0, child; i <= len; i++) {
child = lines[i].firstChild;
if (!child) continue;
if (dom.check.isBreak(child)) {
lines[i].insertBefore(tabText.cloneNode(false), child);
} else {
child.textContent = tabText.textContent + child.textContent;
}
}
const firstChild = dom.query.getEdgeChild(lines[0], 'text', false);
const endChild = dom.query.getEdgeChild(lines[len], 'text', true);
if (!firstCell && firstChild) {
r.sc = firstChild;
r.so = 0;
}
if (!lastCell && endChild) {
r.ec = endChild;
r.eo = endChild.textContent.length;
}
}
} else {
const len = lines.length - 1;
for (let i = 0, line; i <= len; i++) {
line = lines[i].childNodes;
for (let c = 0, cLen = line.length, child; c < cLen; c++) {
child = line[c];
if (!child) break;
if (dom.check.isZeroWidth(child)) continue;
if (/^\s{1,4}$/.test(child.textContent)) {
dom.utils.removeItem(child);
} else if (/^\s{1,4}/.test(child.textContent)) {
child.textContent = child.textContent.replace(/^\s{1,4}/, '');
}
break;
}
}
const firstChild = dom.query.getEdgeChild(lines[0], 'text', false);
const endChild = dom.query.getEdgeChild(lines[len], 'text', true);
if (!firstCell && firstChild) {
r.sc = firstChild;
r.so = 0;
}
if (!lastCell && endChild) {
r.ec = endChild;
r.eo = endChild.textContent.length;
}
}
}
ports.selection.setRange(r.sc, r.so, r.ec, r.eo);
},
/** [enter] */
/** @action enterScrollTo */
'enter.scrollTo': ({ ports }, { range }) => {
ports.enterScrollTo(range);
},
/** @action enterLineAddDefault */
'enter.line.addDefault': ({ ports, ctx }, { formatEl }) => {
const newFormat = ports.format.addLine(formatEl, ctx.options.get('defaultLine'));
const temp = newFormat.firstChild;
if (dom.check.isBreak(temp)) {
const zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
temp.parentNode.insertBefore(zeroWidth, temp);
ports.selection.setRange(zeroWidth, 1, zeroWidth, 1);
} else {
ports.selection.setRange(temp, 0, temp, 0);
}
},
/** @action enterListAddItem */
'enter.list.addItem': ({ ports }, { formatEl, selectionNode }) => {
const br = dom.utils.createElement('BR');
const newEl = dom.utils.createElement('LI', null, br);
formatEl.parentNode.insertBefore(newEl, formatEl.nextElementSibling);
newEl.appendChild(selectionNode.nextSibling);
ports.selection.setRange(br, 1, br, 1);
},
/** @action enterFormatExitEmpty */
'enter.format.exitEmpty': ({ ports, ctx }, { formatEl, rangeEl }) => {
let newEl = null;
if (dom.check.isListCell(rangeEl.parentElement)) {
const parentLi = formatEl.parentNode.parentElement;
rangeEl = parentLi.parentElement;
const newListCell = dom.utils.createElement('LI');
newListCell.innerHTML = '<br>';
dom.utils.copyTagAttributes(newListCell, formatEl, ctx.options.get('lineAttrReset'));
newEl = newListCell;
rangeEl.insertBefore(newEl, parentLi.nextElementSibling);
} else {
let newFormat;
if (dom.check.isTableCell(rangeEl.parentElement)) {
newFormat = 'DIV';
} else if (dom.check.isList(rangeEl.parentElement)) {
newFormat = 'LI';
} else if (ports.format.isLine(rangeEl.nextElementSibling)) {
newFormat = rangeEl.nextElementSibling.nodeName;
} else if (ports.format.isLine(rangeEl.previousElementSibling)) {
newFormat = rangeEl.previousElementSibling.nodeName;
} else {
newFormat = ctx.options.get('defaultLine');
}
newEl = dom.utils.createElement(newFormat);
const edge = ports.format.removeBlock(rangeEl, { selectedFormats: [formatEl], newBlockElement: null, shouldDelete: true, skipHistory: true });
edge.cc.insertBefore(newEl, edge.ec);
}
newEl.innerHTML = '<br>';
ports.nodeTransform.removeAllParents(formatEl, null, null);
ports.selection.setRange(newEl, 1, newEl, 1);
},
/** @action enterFormatCleanBrAndZWS */
'enter.format.cleanBrAndZWS': ({ ports }, { selectionNode, selectionFormat, brBlock, children, offset }) => {
if (selectionFormat) dom.utils.removeItem(children[offset - 1]);
else dom.utils.removeItem(selectionNode);
const brBlockNext = /** @type {HTMLElement} */ (brBlock).nextElementSibling;
const newEl = ports.format.addLine(brBlock, ports.format.isLine(brBlockNext) ? brBlockNext : null);
dom.utils.copyFormatAttributes(newEl, brBlock);
ports.selection.setRange(newEl, 1, newEl, 1);
},
/** @action enterFormatInsertBrHtml */
'enter.format.insertBrHtml': ({ ports }, { brBlock, range, wSelection, offset }) => {
ports.html.insert(range.collapsed && dom.check.isBreak(range.startContainer.childNodes[range.startOffset - 1]) ? '<br>' : '<br><br>', { selectInserted: false, skipCharCount: true, skipCleaning: true });
let focusNode = wSelection.focusNode;
const wOffset = wSelection.focusOffset;
if (brBlock === focusNode) {
focusNode = focusNode.childNodes[wOffset - offset > 1 ? wOffset - 1 : wOffset];
}
ports.selection.setRange(focusNode, 1, focusNode, 1);
ports.setOnShortcutKey(true);
},
/** @action enterFormatInsertBrNode */
'enter.format.insertBrNode': ({ ports }, { wSelection }) => {
const focusNext = wSelection.focusNode.nextSibling;
const br = dom.utils.createElement('BR');
ports.html.insertNode(br, { afterNode: null, skipCharCount: true });
const brPrev = br.previousSibling,
brNext = br.nextSibling;
if (!dom.check.isBreak(focusNext) && !dom.check.isBreak(brPrev) && (!brNext || dom.check.isZeroWidth(brNext))) {
br.parentNode.insertBefore(br.cloneNode(false), br);
ports.selection.setRange(br, 1, br, 1);
} else {
ports.selection.setRange(brNext, 0, brNext, 0);
}
ports.setOnShortcutKey(true);
},
/** @action enterFormatBreakAtEdge */
'enter.format.breakAtEdge': ({ ports, ctx }, { formatEl, selectionNode, formatStartEdge, formatEndEdge }) => {
const focusBR = dom.utils.createElement('BR');
const newFormat = dom.utils.createElement(formatEl.nodeName, null, focusBR);
dom.utils.copyTagAttributes(newFormat, formatEl, ctx.options.get('lineAttrReset'));
let child = focusBR;
let sNode = selectionNode;
do {
if (!dom.check.isBreak(sNode) && sNode.nodeType === 1) {
const f = /** @type {HTMLElement} */ (sNode.cloneNode(false));
f.appendChild(child);
child = f;
}
sNode = sNode.parentElement;
} while (formatEl !== sNode && formatEl.contains(sNode));
newFormat.appendChild(child);
formatEl.parentNode.insertBefore(newFormat, formatStartEdge && !formatEndEdge ? formatEl : formatEl.nextElementSibling);
if (formatEndEdge) {
ports.selection.setRange(focusBR, 1, focusBR, 1);
} else {
const firstEl = formatEl.firstChild || formatEl;
ports.selection.setRange(firstEl, 0, firstEl, 0);
}
},
/** @action enterFormatBreakWithSelection */
'enter.format.breakWithSelection': ({ ports, ctx }, { formatEl, range, formatStartEdge, formatEndEdge }) => {
const isMultiLine = ports.format.getLine(range.startContainer, null) !== ports.format.getLine(range.endContainer, null);
const newFormat = /** @type {HTMLElement} */ (formatEl.cloneNode(false));
newFormat.innerHTML = '<br>';
const commonCon = /** @type {HTMLElement} */ (range.commonAncestorContainer);
const rcon =
commonCon === range.startContainer && commonCon === range.endContainer && dom.check.isZeroWidth(commonCon)
? { container: commonCon, offset: range.endOffset, prevContainer: commonCon.previousElementSibling, commonCon: commonCon }
: ports.html.remove();
let newEl = ports.format.getLine(rcon.container, null);
let offset = 0;
if (!newEl) {
if (dom.check.isWysiwygFrame(rcon.container)) {
ports.enterPrevent(ctx.e);
ctx.fc.get('wysiwyg').appendChild(newFormat);
newEl = newFormat;
dom.utils.copyTagAttributes(newEl, formatEl, ctx.options.get('lineAttrReset'));
ports.selection.setRange(newEl, offset, newEl, offset);
}
return;
}
const innerRange = ports.format.getBlock(rcon.container);
newEl = newEl.contains(innerRange) ? dom.query.getEdgeChild(innerRange, (current) => Boolean(ports.format.getLine(current)), false) : newEl;
if (isMultiLine) {
if (formatEndEdge && !formatStartEdge) {
newEl.parentNode.insertBefore(newFormat, !rcon.prevContainer || rcon.container === rcon.prevContainer ? newEl.nextElementSibling : newEl);
newEl = newFormat;
offset = 0;
} else {
offset = rcon.offset;
if (formatStartEdge) {
const tempEl = newEl.parentNode.insertBefore(newFormat, newEl);
if (formatEndEdge) {
newEl = tempEl;
offset = 0;
}
}
}
} else {
if (formatEndEdge && formatStartEdge) {
newEl.parentNode.insertBefore(newFormat, rcon.prevContainer && rcon.container === rcon.prevContainer ? newEl.nextElementSibling : newEl);
newEl = newFormat;
offset = 0;
} else if (formatEndEdge) {
newEl = newEl.parentNode.insertBefore(newFormat, newEl.nextElementSibling);
newEl = newFormat;
offset = 0;
} else {
newEl = ports.nodeTransform.split(rcon.container, rcon.offset, dom.query.getNodeDepth(formatEl));
}
}
ports.enterPrevent(ctx.e);
dom.utils.copyTagAttributes(newEl, formatEl, ctx.options.get('lineAttrReset'));
ports.selection.setRange(newEl, offset, newEl, offset);
},
/** @action enterFormatBreakAtCursor */
'enter.format.breakAtCursor': ({ ports, ctx }, { formatEl, range }) => {
let newEl = null;
if (dom.check.isZeroWidth(formatEl)) {
newEl = ports.format.addLine(formatEl, formatEl.cloneNode(false));
} else {
newEl = ports.nodeTransform.split(range.endContainer, range.endOffset, dom.query.getNodeDepth(formatEl));
}
ports.enterPrevent(ctx.e);
dom.utils.copyTagAttributes(newEl, formatEl, ctx.options.get('lineAttrReset'));
ports.selection.setRange(newEl, 0, newEl, 0);
},
/** @action enterFigcaptionExitInList */
'enter.figcaption.exitInList': ({ ports }, { formatEl }) => {
const newEl = ports.format.addLine(formatEl, null);
ports.selection.setRange(newEl, 0, newEl, 0);
},
/** [keydown reducer] */
/** @action keydownInputInsertNbsp */
'keydown.input.insertNbsp': ({ ports }) => {
const nbsp = ports.html.insertNode(dom.utils.createTextNode('\u00a0'), { afterNode: null, skipCharCount: true });
if (nbsp) {
ports.selection.setRange(nbsp, nbsp.length, nbsp, nbsp.length);
}
},
/** @action keydownInputInsertZWS */
'keydown.input.insertZWS': ({ ports }) => {
const zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
ports.html.insertNode(zeroWidth, { afterNode: null, skipCharCount: true });
ports.selection.setRange(zeroWidth, 1, zeroWidth, 1);
},
};
/**
* @param {HTMLElement} formatEl - Format element
* @returns {Node}
*/
function LineDelete_next(formatEl) {
const focusNode = formatEl.lastChild;
const next = formatEl.nextElementSibling;
if (!next) return focusNode;
if (dom.check.isZeroWidth(next)) {
dom.utils.removeItem(next);
return focusNode;
}
const nextChild = next.childNodes;
while (nextChild[0]) {
formatEl.appendChild(nextChild[0]);
}
dom.utils.removeItem(next);
return focusNode;
}
/**
* @param {HTMLElement} formatEl - Format element
* @returns {{focusNode: Node, focusOffset: number}}
*/
function LineDelete_prev(formatEl) {
const formatChild = formatEl.childNodes;
const prev = formatEl.previousElementSibling;
let focusNode = formatChild[0];
let focusOffset = 0;
if (!prev) return { focusNode, focusOffset };
if (dom.check.isZeroWidth(prev)) {
dom.utils.removeItem(prev);
return { focusNode, focusOffset };
}
if (formatChild.length > 1 || formatChild[0]?.textContent.length > 0) {
while (formatChild[0]) {
prev.appendChild(formatChild[0]);
}
} else {
focusNode = prev.lastChild;
focusOffset = focusNode.textContent.length;
}
dom.utils.removeItem(formatEl);
return { focusNode, focusOffset };
}
// test export
export { LineDelete_next, LineDelete_prev };