UNPKG

jodit

Version:

Jodit is an awesome and useful wysiwyg editor with filebrowser

1,141 lines (1,140 loc) 40.3 kB
/*! * Jodit Editor (https://xdsoft.net/jodit/) * Released under MIT see LICENSE.txt in the project root for license information. * Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net */ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import * as consts from "../constants.js"; import { INSEPARABLE_TAGS, INVISIBLE_SPACE, IS_PROD } from "../constants.js"; import { autobind } from "../decorators/index.js"; import { Dom } from "../dom/dom.js"; import { $$, attr, css, error, getScrollParent, scrollIntoViewIfNeeded, size, toArray } from "../helpers/index.js"; import { isFunction, isMarker, isString } from "../helpers/checker/index.js"; import { assert } from "../helpers/utils/assert.js"; import { moveTheNodeAlongTheEdgeOutward } from "./helpers/move-the-node-along-the-edge-outward.js"; import "./interface.js"; import { CommitStyle } from "./style/commit-style.js"; import { cursorInTheEdgeOfString, findCorrectCurrentNode } from "./helpers/index.js"; export class Selection { constructor(jodit) { this.jodit = jodit; jodit.e.on('removeMarkers', () => { this.removeMarkers(); }); } /** * Short alias for this.jodit */ get j() { return this.jodit; } /** * Throw Error exception if parameter is not Node */ errorNode(node) { if (!Dom.isNode(node)) { throw error('Parameter node must be instance of Node'); } } /** * Return current work place - for Jodit is Editor */ get area() { return this.j.editor; } /** * Editor Window - it can be different for iframe mode */ get win() { return this.j.ew; } /** * Current jodit editor doc */ get doc() { return this.j.ed; } /** * Return current selection object */ get sel() { if (this.j.o.shadowRoot && isFunction(this.j.o.shadowRoot.getSelection)) { return this.j.o.shadowRoot.getSelection(); } return this.win.getSelection(); } /** * Return first selected range or create new */ get range() { const sel = this.sel; return sel && sel.rangeCount ? sel.getRangeAt(0) : this.createRange(); } /** * Checks if the selected text is currently inside the editor */ get isInsideArea() { const { sel } = this; const range = (sel === null || sel === void 0 ? void 0 : sel.rangeCount) ? sel.getRangeAt(0) : null; return !(!range || !Dom.isOrContains(this.area, range.startContainer)); } /** * Return current selection object * @param select - Immediately add in selection */ createRange(select = false) { const range = this.doc.createRange(); if (select) { this.selectRange(range); } return range; } /** * Remove all selected content */ remove() { const sel = this.sel, current = this.current(); if (sel && current) { for (let i = 0; i < sel.rangeCount; i += 1) { sel.getRangeAt(i).deleteContents(); sel.getRangeAt(i).collapse(true); } } } /** * Clear all selection */ clear() { var _a, _b; if ((_a = this.sel) === null || _a === void 0 ? void 0 : _a.rangeCount) { (_b = this.sel) === null || _b === void 0 ? void 0 : _b.removeAllRanges(); } } /** * Remove node element from editor */ removeNode(node) { if (!Dom.isOrContains(this.j.editor, node, true)) { throw error("Selection.removeNode can remove only editor's children"); } Dom.safeRemove(node); this.j.e.fire('afterRemoveNode', node); } /** * Insert the cursor to any point x, y * * @param x - Coordinate by horizontal * @param y - Coordinate by vertical * @returns false - Something went wrong */ insertCursorAtPoint(x, y) { this.removeMarkers(); try { const rng = this.createRange(); (() => { if (this.doc.caretPositionFromPoint) { const caret = this.doc.caretPositionFromPoint(x, y); if (caret) { rng.setStart(caret.offsetNode, caret.offset); return; } } if (this.doc.caretRangeFromPoint) { const caret = this.doc.caretRangeFromPoint(x, y); assert(caret, 'Incorrect caretRangeFromPoint behaviour'); rng.setStart(caret.startContainer, caret.startOffset); } })(); rng.collapse(true); this.selectRange(rng); return true; } catch (_a) { } return false; } /** * Check if editor has selection markers */ get hasMarkers() { return Boolean(this.markers.length); } /** * Check if editor has selection markers */ get markers() { return $$('span[data-' + consts.MARKER_CLASS + ']', this.area); } /** * Remove all markers */ removeMarkers() { Dom.safeRemove.apply(null, this.markers); } /** * Create marker element */ marker(atStart = false, range) { let newRange = null; if (range) { newRange = range.cloneRange(); newRange.collapse(atStart); } const marker = this.j.createInside.span(); marker.id = consts.MARKER_CLASS + '_' + Number(new Date()) + '_' + String(Math.random()).slice(2); marker.style.lineHeight = '0'; marker.style.display = 'none'; Dom.markTemporary(marker); attr(marker, 'data-' + consts.MARKER_CLASS, atStart ? 'start' : 'end'); marker.appendChild(this.j.createInside.text(consts.INVISIBLE_SPACE)); if (newRange) { if (Dom.isOrContains(this.area, atStart ? newRange.startContainer : newRange.endContainer)) { // Here need do unsafe inserting // Deny Dom.safeInsertNode(newRange, marker); // Apply style -> Test Style module -> Base apply -> For selection <p><strong>|test|</strong></p> apply style {"element":"em","style":{"fontStyle":"italic"}} newRange.insertNode(marker); } } return marker; } /** * Restores user selections using marker invisible elements in the DOM. */ restore() { let range = false; const markAttr = (start) => `span[data-${consts.MARKER_CLASS}=${start ? 'start' : 'end'}]`; const start = this.area.querySelector(markAttr(true)), end = this.area.querySelector(markAttr(false)); if (!start) { return; } range = this.createRange(); if (!end) { const previousNode = start.previousSibling; if (Dom.isText(previousNode)) { range.setStart(previousNode, previousNode.nodeValue ? previousNode.nodeValue.length : 0); } else { range.setStartBefore(start); } Dom.safeRemove(start); range.collapse(true); } else { range.setStartAfter(start); Dom.safeRemove(start); range.setEndBefore(end); Dom.safeRemove(end); } if (range) { this.selectRange(range); } } fakes() { const sel = this.sel; if (!sel || !sel.rangeCount) { return []; } const range = sel.getRangeAt(0); assert(range, 'Range is null'); const left = range.cloneRange(); left.collapse(true); const fakeLeft = this.j.createInside.fake(); Dom.safeInsertNode(left, fakeLeft); range.setStartBefore(fakeLeft); const result = [fakeLeft]; if (!range.collapsed) { const right = range.cloneRange(); right.collapse(false); const fakeRight = this.j.createInside.fake(); Dom.safeInsertNode(right, fakeRight); range.setEndAfter(fakeRight); result.push(fakeRight); } this.selectRange(range); return result; } restoreFakes(fakes) { var _a, _b, _c, _d; const nodes = fakes.filter(n => n.isConnected); if (!nodes.length) { return; } const [fakeLeft, fakeRight] = nodes; const range = this.createRange(); range.setStartAfter(fakeLeft); if (fakeRight) { range.setEndBefore(fakeRight); } this.selectRange(range); if (((_a = fakeLeft.parentNode) === null || _a === void 0 ? void 0 : _a.firstChild) !== ((_b = fakeLeft.parentNode) === null || _b === void 0 ? void 0 : _b.lastChild)) { Dom.safeRemove(fakeLeft); } if (((_c = fakeRight === null || fakeRight === void 0 ? void 0 : fakeRight.parentNode) === null || _c === void 0 ? void 0 : _c.firstChild) !== ((_d = fakeRight === null || fakeRight === void 0 ? void 0 : fakeRight.parentNode) === null || _d === void 0 ? void 0 : _d.lastChild)) { Dom.safeRemove(fakeRight); } } /** * Saves selections using marker invisible elements in the DOM. * @param silent - Do not change current range */ save(silent = false) { if (this.hasMarkers) { return []; } const sel = this.sel; if (!sel || !sel.rangeCount) { return []; } const info = [], length = sel.rangeCount, ranges = []; for (let i = 0; i < length; i += 1) { ranges[i] = sel.getRangeAt(i); if (ranges[i].collapsed) { const start = this.marker(true, ranges[i]); info[i] = { startId: start.id, collapsed: true, startMarker: start.outerHTML }; } else { const start = this.marker(true, ranges[i]); const end = this.marker(false, ranges[i]); info[i] = { startId: start.id, endId: end.id, collapsed: false, startMarker: start.outerHTML, endMarker: end.outerHTML }; } } if (!silent) { sel.removeAllRanges(); for (let i = length - 1; i >= 0; --i) { const startElm = this.doc.getElementById(info[i].startId); if (!startElm) { continue; } if (info[i].collapsed) { ranges[i].setStartAfter(startElm); ranges[i].collapse(true); } else { ranges[i].setStartBefore(startElm); if (info[i].endId) { const endElm = this.doc.getElementById(info[i].endId); if (endElm) { ranges[i].setEndAfter(endElm); } } } try { sel.addRange(ranges[i].cloneRange()); } catch (_a) { } } } return info; } /** * Set focus in editor */ focus(options = { preventScroll: true }) { var _a, _b; if (!this.isFocused()) { const scrollParent = getScrollParent(this.j.container), scrollTop = scrollParent === null || scrollParent === void 0 ? void 0 : scrollParent.scrollTop; if (this.j.iframe) { if (this.doc.readyState === 'complete') { this.j.iframe.focus(options); } } this.win.focus(); this.area.focus(options); if (scrollTop && (scrollParent === null || scrollParent === void 0 ? void 0 : scrollParent.scrollTo)) { scrollParent.scrollTo(0, scrollTop); } const sel = this.sel, range = (sel === null || sel === void 0 ? void 0 : sel.rangeCount) ? sel === null || sel === void 0 ? void 0 : sel.getRangeAt(0) : null; if (!range || !Dom.isOrContains(this.area, range.startContainer)) { const range = this.createRange(); range.setStart(this.area, 0); range.collapse(true); this.selectRange(range, false); } if (!this.j.editorIsActive) { (_b = (_a = this.j) === null || _a === void 0 ? void 0 : _a.events) === null || _b === void 0 ? void 0 : _b.fire('focus'); } return true; } return false; } /** * Checks whether the current selection is something or just set the cursor is * @returns true Selection does't have content */ isCollapsed() { const sel = this.sel; for (let r = 0; sel && r < sel.rangeCount; r += 1) { if (!sel.getRangeAt(r).collapsed) { return false; } } return true; } /** * Checks whether the editor currently in focus */ isFocused() { return (this.doc.hasFocus && this.doc.hasFocus() && this.area === this.doc.activeElement); } /** * Returns the current element under the cursor inside editor */ current(checkChild = true) { if (this.j.getRealMode() !== consts.MODE_WYSIWYG) { return null; } const sel = this.sel; if (!sel || sel.rangeCount === 0) { return null; } const range = sel.getRangeAt(0); let node = range.startContainer; let rightMode = false; const child = (nd) => rightMode ? nd.lastChild : nd.firstChild; if (Dom.isTag(node, 'br') && sel.isCollapsed) { return node; } if (!Dom.isText(node)) { const ret = findCorrectCurrentNode(node, range, rightMode, sel.isCollapsed, checkChild, child); node = ret.node; rightMode = ret.rightMode; } // check - cursor inside editor if (node && Dom.isOrContains(this.area, node)) { return node; } return null; } /** * Insert element in editor * * @param node - Node for insert * @param insertCursorAfter - After insert, cursor will move after element * @param fireChange - After insert, editor fire change event. You can prevent this behavior */ insertNode(node, insertCursorAfter = true, fireChange = true) { this.errorNode(node); const child = Dom.isFragment(node) ? node.lastChild : node; this.j.e.fire('safeHTML', node); if (!this.isFocused() && this.j.isEditorMode()) { this.focus(); this.restore(); } const sel = this.sel; this.j.history.snapshot.transaction(() => { if (!this.isCollapsed()) { this.j.execCommand('Delete'); } this.j.e.fire('beforeInsertNode', node); if (sel && sel.rangeCount) { const range = sel.getRangeAt(0); if (Dom.isOrContains(this.area, range.commonAncestorContainer)) { Dom.safeInsertNode(range, node); } else { this.area.appendChild(node); } } else { this.area.appendChild(node); } const setCursor = (node) => { if (Dom.isBlock(node)) { const child = node.lastChild; if (child) { return setCursor(child); } } this.setCursorAfter(node); }; if (insertCursorAfter) { if (Dom.isFragment(node)) { child && setCursor(child); } else { setCursor(node); } } if (this.j.o.scrollToPastedContent) { scrollIntoViewIfNeeded(child !== null && child !== void 0 ? child : node, this.j.editor, this.doc); } }); if (fireChange && this.j.events) { this.j.__imdSynchronizeValues(); } if (this.j.events) { this.j.e.fire('afterInsertNode', Dom.isFragment(node) ? child : node); } } /** * Inserts in the current cursor position some HTML snippet * * @param html - HTML The text to be inserted into the document * @param insertCursorAfter - After insert, cursor will move after element * @example * ```javascript * parent.s.insertHTML('<img src="image.png"/>'); * ``` */ insertHTML(html, insertCursorAfter = true) { if (html === '') { return; } const node = this.j.createInside.div(); const fragment = this.j.createInside.fragment(); let lastChild; if (!this.isFocused() && this.j.isEditorMode()) { this.focus(); this.restore(); } if (!Dom.isNode(html)) { node.innerHTML = html.toString(); } else { node.appendChild(html); } if (!this.j.isEditorMode() && this.j.e.fire('insertHTML', node.innerHTML) === false) { return; } lastChild = node.lastChild; if (!lastChild) { return; } while (node.firstChild) { lastChild = node.firstChild; fragment.appendChild(node.firstChild); } this.insertNode(fragment, insertCursorAfter, false); // There is no need to use synchronizeValues because you need to apply the changes immediately this.j.__imdSynchronizeValues(); } /** * Insert image in editor * * @param url - URL for image, or HTMLImageElement * @param styles - If specified, it will be applied <code>$(image).css(styles)</code> * @param defaultWidth - If specified, it will be applied <code>css('width', defaultWidth)</code> */ insertImage(url, styles = null, defaultWidth = null) { const image = isString(url) ? this.j.createInside.element('img') : url; if (isString(url)) { image.setAttribute('src', url); } if (defaultWidth != null) { let dw = defaultWidth.toString(); if (dw && 'auto' !== dw && String(dw).indexOf('px') < 0 && String(dw).indexOf('%') < 0) { dw += 'px'; } attr(image, 'width', dw); } if (styles && typeof styles === 'object') { css(image, styles); } const onload = () => { if (image.naturalHeight < image.offsetHeight || image.naturalWidth < image.offsetWidth) { image.style.width = ''; image.style.height = ''; } image.removeEventListener('load', onload); }; this.j.e.on(image, 'load', onload); if (image.complete) { onload(); } this.insertNode(image); /** * Triggered after image was inserted [[Select.insertImage]]. This method can executed from * [[FileBrowser]] or [[Uploader]] * @example * ```javascript * const editor = Jodit.make("#redactor"); * editor.e.on('afterInsertImage', function (image) { * image.className = 'bloghead4'; * }); * ``` */ this.j.e.fire('afterInsertImage', image); } /** * Call callback for all selection node */ // eslint-disable-next-line complexity eachSelection(callback) { var _a; const sel = this.sel; if (!sel || !sel.rangeCount) { return; } const range = sel.getRangeAt(0); let root = range.commonAncestorContainer; if (!Dom.isHTMLElement(root)) { root = root.parentElement; } const nodes = []; const startOffset = range.startOffset; const length = root.childNodes.length; const elementOffset = startOffset < length ? startOffset : length - 1; let start = range.startContainer === this.area ? root.childNodes[elementOffset] : range.startContainer; let end = range.endContainer === this.area ? root.childNodes[range.endOffset - 1] : range.endContainer; if (Dom.isText(start) && start === range.startContainer && range.startOffset === ((_a = start.nodeValue) === null || _a === void 0 ? void 0 : _a.length) && start.nextSibling) { start = start.nextSibling; } if (Dom.isText(end) && end === range.endContainer && range.endOffset === 0 && end.previousSibling) { end = end.previousSibling; } const checkElm = (node) => { if (node && node !== root && !Dom.isEmptyTextNode(node) && !isMarker(node)) { nodes.push(node); } }; checkElm(start); if (start !== end && Dom.isOrContains(root, start, true)) { Dom.find(start, node => { checkElm(node); // checks parentElement as well because partial selections are not equal to entire element return (node === end || (node && node.contains && node.contains(end))); }, root, true, false); } const forEvery = (current) => { if (!Dom.isOrContains(this.j.editor, current, true)) { return; } if (current.nodeName.match(/^(UL|OL)$/)) { return toArray(current.childNodes).forEach(forEvery); } if (Dom.isTag(current, 'li')) { if (current.firstChild) { current = current.firstChild; } else { const currentB = this.j.createInside.text(INVISIBLE_SPACE); current.appendChild(currentB); current = currentB; } } callback(current); }; if (nodes.length === 0) { if (Dom.isEmptyTextNode(start)) { nodes.push(start); } if (start.firstChild) { nodes.push(start.firstChild); } } nodes.forEach(forEvery); } /** * Checks if the cursor is at the end(start) block * * @param start - true - check whether the cursor is at the start block * @param parentBlock - Find in this * @param fake - Node for cursor position * * @returns true - the cursor is at the end(start) block, null - cursor somewhere outside */ cursorInTheEdge(start, parentBlock, fake = null) { var _a; const end = !start, range = (_a = this.sel) === null || _a === void 0 ? void 0 : _a.getRangeAt(0); fake !== null && fake !== void 0 ? fake : (fake = this.current(false)); if (!range || !fake || !Dom.isOrContains(parentBlock, fake, true)) { return null; } const container = start ? range.startContainer : range.endContainer; const offset = start ? range.startOffset : range.endOffset; const isSignificant = (elm) => Boolean(elm && !Dom.isTag(elm, 'br') && !Dom.isEmptyTextNode(elm) && !Dom.isTemporary(elm) && !(Dom.isElement(elm) && this.j.e.fire('isInvisibleForCursor', elm) === true)); // check right offset if (Dom.isText(container)) { if (cursorInTheEdgeOfString(container, offset, start, end)) { return false; } } else { const children = toArray(container.childNodes); if (end) { if (children.slice(offset).some(isSignificant)) { return false; } } else { if (children.slice(0, offset).some(isSignificant)) { return false; } } } let next = fake; while (next && next !== parentBlock) { const nextOne = Dom.sibling(next, start); if (!nextOne) { next = next.parentNode; continue; } next = nextOne; if (next && isSignificant(next)) { return false; } } return true; } /** * Wrapper for cursorInTheEdge */ cursorOnTheLeft(parentBlock, fake) { return this.cursorInTheEdge(true, parentBlock, fake); } /** * Wrapper for cursorInTheEdge */ cursorOnTheRight(parentBlock, fake) { return this.cursorInTheEdge(false, parentBlock, fake); } /** * Set cursor after the node * @returns fake invisible textnode. After insert it can be removed */ setCursorAfter(node) { return this.setCursorNearWith(node, false); } /** * Set cursor before the node * @returns fake invisible textnode. After insert it can be removed */ setCursorBefore(node) { return this.setCursorNearWith(node, true); } /** * Add fake node for new cursor position */ setCursorNearWith(node, inStart) { var _a, _b; this.errorNode(node); if (!Dom.up(node, (elm) => elm === this.area || (elm && elm.parentNode === this.area), this.area)) { throw error('Node element must be in editor'); } const range = this.createRange(); let fakeNode = null; if (!Dom.isText(node)) { fakeNode = this.j.createInside.fake(); inStart ? range.setStartBefore(node) : range.setEndAfter(node); range.collapse(inStart); Dom.safeInsertNode(range, fakeNode); range.selectNode(fakeNode); } else { if (inStart) { range.setStart(node, 0); } else { range.setEnd(node, (_b = (_a = node.nodeValue) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0); } } range.collapse(inStart); this.selectRange(range); return fakeNode; } /** * Set cursor in the node * @param node - Node element * @param inStart - set cursor in start of element */ setCursorIn(node, inStart = false) { this.errorNode(node); if (!Dom.up(node, (elm) => elm === this.area || (elm && elm.parentNode === this.area), this.area)) { throw error('Node element must be in editor'); } const range = this.createRange(); let start = node, last = node; do { if (Dom.isText(start) || Dom.isTag(start, INSEPARABLE_TAGS)) { break; } last = start; start = inStart ? start.firstChild : start.lastChild; } while (start); if (!start) { const fakeNode = this.j.createInside.text(consts.INVISIBLE_SPACE); if (!Dom.isTag(last, INSEPARABLE_TAGS)) { last.appendChild(fakeNode); last = fakeNode; } else { start = last; } } const workElm = start || last; if (!Dom.isTag(workElm, INSEPARABLE_TAGS)) { range.selectNodeContents(workElm); range.collapse(inStart); } else { inStart || Dom.isTag(workElm, 'br') ? range.setStartBefore(workElm) : range.setEndAfter(workElm); range.collapse(inStart); } this.selectRange(range); return last; } /** * Set range selection */ selectRange(range, focus = true) { const sel = this.sel; if (focus && !this.isFocused()) { this.focus(); } if (sel) { sel.removeAllRanges(); sel.addRange(range); } /** * Fired after change selection */ this.j.e.fire('changeSelection'); return this; } /** * Select node * @param node - Node element * @param inward - select all inside */ select(node, inward = false) { this.errorNode(node); if (!Dom.up(node, (elm) => elm === this.area || (elm && elm.parentNode === this.area), this.area)) { throw error('Node element must be in editor'); } const range = this.createRange(); range[inward ? 'selectNodeContents' : 'selectNode'](node); return this.selectRange(range); } /** * Return current selected HTML * @example * ```javascript * const editor = Jodit.make(); * console.log(editor.s.html); // html * console.log(Jodit.modules.Helpers.stripTags(editor.s.html)); // plain text * ``` */ get html() { const sel = this.sel; if (sel && sel.rangeCount > 0) { const range = sel.getRangeAt(0); const clonedSelection = range.cloneContents(); const div = this.j.createInside.div(); div.appendChild(clonedSelection); return div.innerHTML; } return ''; } /** * Wrap all selected fragments inside Tag or apply some callback */ *wrapInTagGen(fakes) { if (this.isCollapsed()) { const font = this.jodit.createInside.element('font', INVISIBLE_SPACE); this.insertNode(font, false, false); if (fakes && fakes[0]) { font.appendChild(fakes[0]); } yield font; Dom.unwrap(font); return; } // fix issue https://github.com/xdan/jodit/issues/65 $$('*[style*=font-size]', this.area).forEach(elm => { attr(elm, 'data-font-size', elm.style.fontSize.toString()); elm.style.removeProperty('font-size'); }); this.j.nativeExecCommand('fontsize', false, '7'); $$('*[data-font-size]', this.area).forEach(elm => { const fontSize = attr(elm, 'data-font-size'); if (fontSize) { elm.style.fontSize = fontSize; attr(elm, 'data-font-size', null); } }); const elms = $$('font[size="7"]', this.area); for (const font of elms) { const { firstChild, lastChild } = font; if (firstChild && firstChild === lastChild && isMarker(firstChild)) { Dom.unwrap(font); continue; } if (firstChild && isMarker(firstChild)) { Dom.before(font, firstChild); } if (lastChild && isMarker(lastChild)) { Dom.after(font, lastChild); } yield font; Dom.unwrap(font); } return; } /** * Wrap all selected fragments inside Tag or apply some callback */ wrapInTag(tagOrCallback) { const result = []; for (const font of this.wrapInTagGen()) { try { if (font.firstChild && font.firstChild === font.lastChild && isMarker(font.firstChild)) { continue; } if (isFunction(tagOrCallback)) { tagOrCallback(font); } else { result.push(Dom.replace(font, tagOrCallback, this.j.createInside)); } } finally { const pn = font.parentNode; if (pn) { Dom.unwrap(font); if (Dom.isEmpty(pn)) { Dom.unwrap(pn); } } } } return result; } /** * Apply some css rules for all selections. It method wraps selections in nodeName tag. * @example * ```js * const editor = Jodit.make('#editor'); * editor.value = 'test'; * editor.execCommand('selectall'); * * editor.s.commitStyle({ * style: {color: 'red'} * }) // will wrap `text` in `span` and add style `color:red` * editor.s.commitStyle({ * style: {color: 'red'} * }) // will remove `color:red` from `span` * ``` */ commitStyle(options) { assert(size(options) > 0, 'Need to pass at least one option'); const styleElm = new CommitStyle(options); styleElm.apply(this.j); } /** * Split selection on two parts: left and right */ splitSelection(currentBox, edge) { if (!this.isCollapsed()) { return null; } const leftRange = this.createRange(); const range = this.range; leftRange.setStartBefore(currentBox); const cursorOnTheRight = this.cursorOnTheRight(currentBox, edge); const cursorOnTheLeft = this.cursorOnTheLeft(currentBox, edge); const br = this.j.createInside.element('br'), prevFake = this.j.createInside.fake(), nextFake = prevFake.cloneNode(); try { if (cursorOnTheRight || cursorOnTheLeft) { if (edge) { Dom.before(edge, br); } else { Dom.safeInsertNode(range, br); } const clearBR = (start, getNext) => { let next = getNext(start); while (next) { const nextSib = getNext(next); if (next && (Dom.isTag(next, 'br') || Dom.isEmptyTextNode(next))) { Dom.safeRemove(next); } else { break; } next = nextSib; } }; clearBR(br, (n) => n.nextSibling); clearBR(br, (n) => n.previousSibling); Dom.after(br, nextFake); Dom.before(br, prevFake); if (cursorOnTheRight) { leftRange.setEndBefore(br); range.setEndBefore(br); } else { leftRange.setEndAfter(br); range.setEndAfter(br); } } else { leftRange.setEnd(range.startContainer, range.startOffset); } const fragment = leftRange.extractContents(); const clearEmpties = (node) => Dom.each(node, node => Dom.isEmptyTextNode(node) && Dom.safeRemove(node)); assert(currentBox.parentNode, 'Splitting fails'); try { clearEmpties(fragment); clearEmpties(currentBox); currentBox.parentNode.insertBefore(fragment, currentBox); if (!edge && cursorOnTheRight && (br === null || br === void 0 ? void 0 : br.parentNode)) { const range = this.createRange(); range.setStartBefore(br); this.selectRange(range); } } catch (e) { if (!IS_PROD) { throw e; } } // After splitting some part can be empty const fillFakeParent = (fake) => { var _a, _b, _c; if (((_a = fake === null || fake === void 0 ? void 0 : fake.parentNode) === null || _a === void 0 ? void 0 : _a.firstChild) === ((_b = fake === null || fake === void 0 ? void 0 : fake.parentNode) === null || _b === void 0 ? void 0 : _b.lastChild)) { (_c = fake === null || fake === void 0 ? void 0 : fake.parentNode) === null || _c === void 0 ? void 0 : _c.appendChild(br.cloneNode()); } }; fillFakeParent(prevFake); fillFakeParent(nextFake); } finally { Dom.safeRemove(prevFake); Dom.safeRemove(nextFake); } return currentBox.previousElementSibling; } expandSelection() { if (this.isCollapsed()) { return this; } const { range } = this; const c = range.cloneRange(); if (!Dom.isOrContains(this.j.editor, range.commonAncestorContainer, true)) { return this; } const moveMaxEdgeFake = (start) => { const fake = this.j.createInside.fake(); const r = range.cloneRange(); r.collapse(start); Dom.safeInsertNode(r, fake); moveTheNodeAlongTheEdgeOutward(fake, start, this.j.editor); return fake; }; const leftFake = moveMaxEdgeFake(true); const rightFake = moveMaxEdgeFake(false); c.setStartAfter(leftFake); c.setEndBefore(rightFake); const leftBox = Dom.findSibling(leftFake, false); const rightBox = Dom.findSibling(rightFake, true); if (leftBox !== rightBox) { const rightInsideLeft = Dom.isElement(leftBox) && Dom.isOrContains(leftBox, rightFake); const leftInsideRight = !rightInsideLeft && Dom.isElement(rightBox) && Dom.isOrContains(rightBox, leftFake); if (rightInsideLeft || leftInsideRight) { let child = (rightInsideLeft ? leftBox : rightBox), container = child; while (Dom.isElement(child)) { child = rightInsideLeft ? child.firstElementChild : child.lastElementChild; if (child) { const isInside = rightInsideLeft ? Dom.isOrContains(child, rightFake) : Dom.isOrContains(child, leftFake); if (isInside) { container = child; } } } if (rightInsideLeft) { c.setStart(container, 0); } else { c.setEnd(container, container.childNodes.length); } } } this.selectRange(c); Dom.safeRemove(leftFake, rightFake); if (this.isCollapsed()) { throw error('Selection is collapsed'); } return this; } } __decorate([ autobind ], Selection.prototype, "createRange", null); __decorate([ autobind ], Selection.prototype, "focus", null); __decorate([ autobind ], Selection.prototype, "setCursorAfter", null); __decorate([ autobind ], Selection.prototype, "setCursorBefore", null); __decorate([ autobind ], Selection.prototype, "setCursorIn", null);