UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

351 lines (322 loc) 11.1 kB
import { dom } from '../../../helper'; import { cleanRemovedTags, hardDelete, isUneditableNode, setDefaultLine, isRtlBidiMismatch, } from '../effects/ruleHelpers'; import { A } from '../actions'; /** * @typedef {import('../actions').Action[]} EventActions * @typedef {import('../ports').EventReducerPorts} EventPorts * @typedef {import('../reducers/keydown.reducer').KeydownReducerCtx} EventKeydownCtx */ /** * @this {void} * @description Backspace key down rule * @param {EventActions} actions - Action list * @param {EventPorts} ports - Ports for interacting with editor * @param {EventKeydownCtx} ctx - Context object * @returns {boolean} Return `false` to stop the processing */ export function reduceBackspaceDown(actions, ports, ctx) { const { format, component } = ports; const { fc, options, range, selectionNode } = ctx; let { formatEl } = ctx; const selectRange = !range.collapsed || range.startContainer !== range.endContainer; // RTL bidi guard: if offset 0 is actually at the visual end due to LTR text in RTL line, skip front-edge handling const bidiNotFront = options.get('_rtl') && !selectRange && range.startOffset === 0 && isRtlBidiMismatch(range, formatEl, 'front', fc.get('_wd')); actions.push(A.componentDeselect()); actions.push(A.cacheStyleNode()); if (selectRange && hardDelete(ports)) { actions.push(A.preventStop()); actions.push(A.caretScrollTo(range)); return true; } if ( !format.isNormalLine(formatEl) && !format.isBrLine(formatEl) && !fc.get('wysiwyg').firstElementChild && !component.is(selectionNode) && setDefaultLine(ports, options.get('defaultLine')) !== null ) { actions.push(A.preventStop()); return false; } // line delete if ( format.isLine(formatEl) && selectRange && dom.check.isEdgePoint(range.startContainer, range.startOffset, 'front') && (!range.startContainer.previousSibling || dom.check.isZeroWidth(range.startContainer.previousSibling)) && format.getLine(range.startContainer) !== format.getLine(range.endContainer) && (format.isLine(formatEl.previousElementSibling) || dom.check.isListCell(formatEl)) ) { actions.push(A.preventStop()); actions.push(A.delFormatRemoveAndMove(range.startContainer, formatEl)); actions.push(A.historyPush(true)); actions.push(A.caretScrollTo(range)); return false; } // closure, default if ( !selectRange && !bidiNotFront && !formatEl.previousElementSibling && range.startOffset === 0 && !selectionNode.previousSibling && !dom.check.isListCell(formatEl) && format.isLine(formatEl) && (!format.isBrLine(formatEl) || format.isClosureBrLine(formatEl) || dom.check.isWysiwygFrame(formatEl.parentNode)) ) { // closure range if (format.isClosureBlock(formatEl.parentNode)) { actions.push(A.preventStop()); return false; } // brLine (pre): strip tag to default line(s) if (format.isBrLine(formatEl) && dom.check.isWysiwygFrame(formatEl.parentNode)) { actions.push(A.preventStop()); actions.push(A.backspaceBrLineStrip(formatEl)); actions.push(A.historyPush(true)); return false; } // maintain default format if ( dom.check.isWysiwygFrame(formatEl.parentNode) && formatEl.childNodes.length <= 1 && (!formatEl.firstChild || dom.check.isZeroWidth(formatEl.textContent)) ) { actions.push(A.preventStop()); actions.push(A.backspaceFormatMaintain(formatEl)); } actions.push(A.editorNativeFocus()); actions.push(A.caretScrollTo(range)); return false; } // clean remove tag const startCon = range.startContainer; if ( formatEl && !bidiNotFront && !formatEl.previousElementSibling && range.startOffset === 0 && startCon.nodeType === 3 && dom.check.isZeroWidth(startCon) ) { if (cleanRemovedTags(ports, startCon, formatEl) === true) return true; } // line component if (!selectRange && formatEl && (range.startOffset === 0 || selectionNode === formatEl)) { const sel = selectionNode === formatEl ? isUneditableNode(ports, range, true) : dom.check.isElement(selectionNode.previousSibling) ? selectionNode.previousSibling : dom.check.isEdgePoint(range.startContainer, range.startOffset) ? dom.query.getPreviousDeepestNode(range.startContainer) : null; if (component.is(sel)) { const fileComponentInfo = component.get(sel); if (fileComponentInfo) { actions.push(A.preventStop()); actions.push(A.backspaceComponentSelect(selectionNode, range, fileComponentInfo)); return true; } } } // tag[contenteditable='false'] if (isUneditableNode(ports, range, true)) { actions.push(A.preventStop()); return true; } // format attributes if (!selectRange && !bidiNotFront && format.isEdgeLine(range.startContainer, range.startOffset, 'front')) { if (format.isLine(formatEl.previousElementSibling)) { actions.push(A.cacheFormatAttrsTemp(formatEl.previousElementSibling.attributes)); } } // nested list formatEl = format.getLine(range.startContainer, null); const rangeEl = format.getBlock(formatEl, null); const commonCon = range.commonAncestorContainer; if (rangeEl && formatEl && !dom.check.isTableCell(rangeEl) && !/^FIGCAPTION$/i.test(rangeEl.nodeName)) { if ( dom.check.isListCell(formatEl) && dom.check.isList(rangeEl) && (dom.check.isListCell(rangeEl.parentElement) || formatEl.previousElementSibling) && (selectionNode === formatEl || (selectionNode.nodeType === 3 && (!selectionNode.previousSibling || dom.check.isList(selectionNode.previousSibling)))) && (format.getLine(range.startContainer, null) !== format.getLine(range.endContainer, null) ? rangeEl.contains(range.startContainer) : range.startOffset === 0 && range.collapsed && !bidiNotFront) ) { if (range.startContainer !== range.endContainer) { actions.push(A.prevent()); actions.push(A.backspaceListRemoveNested(range)); actions.push(A.historyPush(true)); } else { let prev = formatEl.previousElementSibling || rangeEl.parentElement; if (dom.check.isListCell(prev)) { actions.push(A.prevent()); let prevLast = prev; if ( !prev.contains(formatEl) && dom.check.isListCell(prevLast) && dom.check.isList(prevLast.lastElementChild) ) { prevLast = /** @type {HTMLLIElement} */ (prevLast.lastElementChild.lastElementChild); while (dom.check.isListCell(prevLast) && dom.check.isList(prevLast.lastElementChild)) { prevLast = /** @type {HTMLLIElement} */ ( prevLast.lastElementChild && prevLast.lastElementChild.lastElementChild ); } prev = prevLast; } actions.push(A.backspaceListMergePrev(prev, formatEl, rangeEl)); actions.push(A.historyPush(true)); } } actions.push(A.caretScrollTo(range)); return true; } // detach range if (!selectRange && range.startOffset === 0 && !bidiNotFront) { let detach = true; let comm = commonCon; while (comm && comm !== rangeEl && !dom.check.isWysiwygFrame(comm)) { if (comm.previousSibling) { if ( comm.previousSibling.nodeType === 1 || !dom.check.isZeroWidth(comm.previousSibling.textContent.trim()) ) { detach = false; break; } } comm = comm.parentNode; } if (detach && rangeEl.parentNode) { actions.push(A.prevent()); actions.push( A.formatRemoveBlock( rangeEl, dom.check.isListCell(formatEl) ? [formatEl] : null, null, false, false, ), ); actions.push(A.historyPush(true)); actions.push(A.caretScrollTo(range)); return true; } } } // empty line adjacent to component (offset-independent — handles RTL caret at offset 1) if (!selectRange && formatEl && dom.check.isEmptyLine(formatEl) && component.is(formatEl.previousElementSibling)) { const fileComponentInfo = component.get(formatEl.previousElementSibling); if (fileComponentInfo) { actions.push(A.preventStop()); actions.push(A.backspaceComponentRemove(false, formatEl.firstChild, formatEl, fileComponentInfo)); actions.push(A.caretScrollTo(range)); return true; } } // component if ( !selectRange && formatEl && (range.startOffset === 0 || (selectionNode === formatEl ? formatEl.childNodes[range.startOffset] : false)) ) { const isList = dom.check.isListCell(formatEl); const sel = selectionNode === formatEl ? formatEl.childNodes[range.startOffset] : selectionNode; const prev = (isList ? sel : formatEl).previousSibling; // select file component const ignoreZWS = isList || ((commonCon.nodeType === 3 || dom.check.isBreak(commonCon)) && !commonCon.previousSibling && range.startOffset === 0); if ( sel && (isList || !sel.previousSibling) && ((commonCon && component.is(commonCon.previousSibling)) || (ignoreZWS && component.is(prev))) ) { const fileComponentInfo = component.get(prev); if (fileComponentInfo) { actions.push(A.preventStop()); actions.push(A.backspaceComponentRemove(isList, sel, formatEl, fileComponentInfo)); } else if (component.is(prev)) { actions.push(A.preventStop()); actions.push(A.domUtilsRemoveItem(prev)); } actions.push(A.caretScrollTo(range)); return true; } // delete nonEditable if (sel && dom.check.isNonEditable(sel.previousSibling)) { actions.push(A.preventStop()); actions.push(A.domUtilsRemoveItem(sel.previousSibling)); actions.push(A.caretScrollTo(range)); return true; } } // empty row inside a brLine (PRE): Enter leaves the caret on a bare <br> that begins an empty row if ( !selectRange && format.isBrLine(formatEl) && dom.check.isBreak(range.startContainer) && range.startOffset === 0 ) { let rowStartBr = range.startContainer.previousSibling; if (rowStartBr?.nodeType === 3 && dom.check.isZeroWidth(rowStartBr)) rowStartBr = rowStartBr.previousSibling; if (dom.check.isBreak(rowStartBr)) { actions.push(A.preventStop()); actions.push(A.backspaceBrLineRowMerge(range.startContainer, rowStartBr)); actions.push(A.historyPush(true)); actions.push(A.caretScrollTo(range)); return false; } } // soft line break (Shift+Enter) if ( !selectRange && range.startContainer.nodeType === 3 && dom.check.isZeroWidth(range.startContainer) && range.startOffset <= 1 && dom.check.isBreak(range.startContainer.previousSibling) ) { actions.push(A.preventStop()); actions.push(A.backspaceSoftBreakMerge(range.startContainer)); actions.push(A.historyPush(true)); actions.push(A.caretScrollTo(range)); return false; } // empty line: merge into the previous line const emptyLinePrev = formatEl?.previousElementSibling; if ( !selectRange && formatEl && format.isNormalLine(formatEl) && !dom.check.isListCell(formatEl) && dom.check.isEmptyLine(formatEl) && (format.isNormalLine(emptyLinePrev) || format.isBrLine(emptyLinePrev)) ) { actions.push(A.preventStop()); actions.push(A.backspaceEmptyLineMergePrev(formatEl, emptyLinePrev)); actions.push(A.historyPush(true)); actions.push(A.caretScrollTo(range)); return false; } actions.push(A.caretScrollTo(range)); return true; }