UNPKG

jodit

Version:

Jodit is awesome and usefully wysiwyg editor with filebrowser

1,370 lines (1,173 loc) 30.3 kB
/*! * Jodit Editor (https://xdsoft.net/jodit/) * Licensed under GNU General Public License version 2 or later or a commercial license or MIT; * For GPL see LICENSE-GPL.txt in the project root for license information. * For MIT see LICENSE-MIT.txt in the project root for license information. * For commercial licenses see https://xdsoft.net/jodit/commercial/ * Copyright (c) 2013-2019 Valeriy Chupurnov. All rights reserved. https://xdsoft.net */ import * as consts from '../constants'; import { INVISIBLE_SPACE, INVISIBLE_SPACE_REG_EXP_END, INVISIBLE_SPACE_REG_EXP_START } from '../constants'; import { HTMLTagNames, IDictionary, IJodit, markerInfo } from '../types'; import { Dom } from './Dom'; import { css } from './helpers/css'; import { normalizeNode, normilizeCSSValue } from './helpers/normalize'; import { $$ } from './helpers/selector'; import { isPlainObject } from './helpers/checker'; import { each } from './helpers/each'; import { trim } from './helpers/string'; type WindowSelection = Selection | null; export class Select { constructor(readonly jodit: IJodit) { } /** * Throw Error exception if parameter is not Node * @param node */ private errorNode(node: unknown) { if (!Dom.isNode(node, this.win)) { throw new Error('Parameter node must be instance of Node'); } } /** * Return current work place - for Jodit is Editor */ get area(): HTMLElement { return this.jodit.editor; } /** * Editor Window - it can be different for iframe mode */ get win(): Window { return this.jodit.editorWindow; } /** * Current jodit editor doc */ get doc(): Document { return this.jodit.editorDocument; } /** * Return current selection object */ get sel(): WindowSelection { return this.win.getSelection(); } /** * Return first selected range or create new */ get range(): Range { const sel = this.sel; return sel && sel.rangeCount ? sel.getRangeAt(0) : this.createRange(); } /** * Return current selection object */ createRange(): Range { return this.doc.createRange(); } /** * Remove all selected content */ remove() { const sel = this.sel, current: false | Node = this.current(); if (sel && current) { for (let i = 0; i < sel.rangeCount; i += 1) { sel.getRangeAt(i).deleteContents(); sel.getRangeAt(i).collapse(true); } } } /** * Insert the cursor toWYSIWYG any point x, y * * @method insertAtPoint * @param {int} x Coordinate by horizontal * @param {int} y Coordinate by vertical * @return boolean Something went wrong */ insertCursorAtPoint(x: number, y: number): boolean { this.removeMarkers(); try { let rng: Range = this.createRange(); if ((this.doc as any).caretPositionFromPoint) { const caret: CaretPosition = (this .doc as any).caretPositionFromPoint(x, y); rng.setStart(caret.offsetNode, caret.offset); } else if (this.doc.caretRangeFromPoint) { const caret: Range = this.doc.caretRangeFromPoint(x, y); rng.setStart(caret.startContainer, caret.startOffset); } if (rng) { rng.collapse(true); const sel = this.sel; if (sel) { sel.removeAllRanges(); sel.addRange(rng); } } else if ( typeof (this.doc as any).body.createTextRange !== 'undefined' ) { const range: any = (this.doc as any).body.createTextRange(); range.moveToPoint(x, y); const endRange: any = range.duplicate(); endRange.moveToPoint(x, y); range.setEndPoint('EndToEnd', endRange); range.select(); } return true; } catch { } return false; } /** * Define element is selection helper * @param elm */ isMarker = (elm: Node): boolean => Dom.isNode(elm, this.win) && elm.nodeType === Node.ELEMENT_NODE && elm.nodeName === 'SPAN' && (elm as Element).hasAttribute('data-' + consts.MARKER_CLASS); /** * Remove all markers */ removeMarkers() { $$('span[data-' + consts.MARKER_CLASS + ']', this.area).forEach( Dom.safeRemove ); } /** * Create marker element * * @param atStart * @param range */ marker(atStart = false, range?: Range): HTMLSpanElement { let newRange: Range | null = null; if (range) { newRange = range.cloneRange(); newRange.collapse(atStart); } const marker: HTMLSpanElement = this.jodit.create.inside.span(); marker.id = consts.MARKER_CLASS + '_' + +new Date() + '_' + ('' + Math.random()).slice(2); marker.style.lineHeight = '0'; marker.style.display = 'none'; marker.setAttribute( 'data-' + consts.MARKER_CLASS, atStart ? 'start' : 'end' ); marker.appendChild( this.jodit.create.inside.text(consts.INVISIBLE_SPACE) ); if (newRange) { if ( Dom.isOrContains( this.area, atStart ? newRange.startContainer : newRange.endContainer ) ) { newRange.insertNode(marker); } } return marker; } /** * Restores user selections using marker invisible elements in the DOM. * * @param {markerInfo[]|null} selectionInfo */ restore(selectionInfo: markerInfo[] | null = []) { if (Array.isArray(selectionInfo)) { const sel = this.sel; sel && sel.removeAllRanges(); selectionInfo.forEach((selection: markerInfo) => { const range = this.createRange(), end = this.area.querySelector( '#' + selection.endId ) as HTMLElement, start = this.area.querySelector( '#' + selection.startId ) as HTMLElement; if (!start) { return; } if (selection.collapsed || !end) { const previousNode: Node | null = start.previousSibling; if ( previousNode && previousNode.nodeType === Node.TEXT_NODE ) { 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); } sel && sel.addRange(range); }); } } /** * Saves selections using marker invisible elements in the DOM. * * @return markerInfo[] */ save(): markerInfo[] { const sel = this.sel; if (!sel || !sel.rangeCount) { return []; } const info: markerInfo[] = [], length: number = sel.rangeCount, ranges: Range[] = []; let i: number, start: HTMLSpanElement, end: HTMLSpanElement; for (i = 0; i < length; i += 1) { ranges[i] = sel.getRangeAt(i); if (ranges[i].collapsed) { start = this.marker(true, ranges[i]); info[i] = { startId: start.id, collapsed: true, startMarker: start.outerHTML }; } else { start = this.marker(true, ranges[i]); end = this.marker(false, ranges[i]); info[i] = { startId: start.id, endId: end.id, collapsed: false, startMarker: start.outerHTML, endMarker: end.outerHTML }; } } sel.removeAllRanges(); for (i = length - 1; i >= 0; --i) { const startElm: HTMLElement | null = this.doc.getElementById( info[i].startId ); if (startElm) { if (info[i].collapsed) { ranges[i].setStartAfter(startElm); ranges[i].collapse(true); } else { ranges[i].setStartBefore(startElm); if (info[i].endId) { const endElm: HTMLElement | null = this.doc.getElementById( info[i].endId as string ); if (endElm) { ranges[i].setEndAfter(endElm); } } } } try { sel.addRange(ranges[i].cloneRange()); } catch { } } return info; } /** * Set focus in editor */ focus = (): boolean => { if (!this.isFocused()) { if (this.jodit.iframe) { if (this.doc.readyState == 'complete') { this.jodit.iframe.focus(); } } this.win.focus(); this.area.focus(); const sel = this.sel, range = this.createRange(); if (sel && (!sel.rangeCount || !this.current())) { range.setStart(this.area, 0); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } return true; } return false; }; /** * Checks whether the current selection is something or just set the cursor is * * @return boolean true Selection does't have content */ isCollapsed(): boolean { const sel = this.sel; for (let r: number = 0; sel && r < sel.rangeCount; r += 1) { if (!sel.getRangeAt(r).collapsed) { return false; } } return true; } /** * Checks whether the editor currently in focus * * @return boolean */ isFocused(): boolean { return ( this.doc.hasFocus && this.doc.hasFocus() && this.area === this.doc.activeElement ); } /** * Returns the current element under the cursor inside editor * * @return false|Node The element under the cursor or false if undefined or not in editor */ current(checkChild: boolean = true): false | Node { if (this.jodit.getRealMode() === consts.MODE_WYSIWYG) { const sel = this.sel; if (sel && sel.rangeCount > 0) { const range = sel.getRangeAt(0); let node: Node | null = range.startContainer, rightMode: boolean = false; const child = (nd: Node): Node | null => rightMode ? nd.lastChild : nd.firstChild; if (node.nodeType !== Node.TEXT_NODE) { node = range.startContainer.childNodes[range.startOffset]; if (!node) { node = range.startContainer.childNodes[ range.startOffset - 1 ]; rightMode = true; } if ( node && sel.isCollapsed && node.nodeType !== Node.TEXT_NODE ) { // test Current method - Cursor in the left of some SPAN if ( !rightMode && node.previousSibling && node.previousSibling.nodeType === Node.TEXT_NODE ) { node = node.previousSibling; } else if (checkChild) { let current: Node | null = child(node); while (current) { if ( current && current.nodeType === Node.TEXT_NODE ) { node = current; break; } current = child(current); } } } if ( node && !sel.isCollapsed && node.nodeType !== Node.TEXT_NODE ) { let leftChild: Node | null = node, rightChild: Node | null = node; do { leftChild = leftChild.firstChild; rightChild = rightChild.lastChild; } while ( leftChild && rightChild && leftChild.nodeType !== Node.TEXT_NODE ); if ( leftChild === rightChild && leftChild && leftChild.nodeType === Node.TEXT_NODE ) { node = leftChild; } } } // check - cursor inside editor if (node && Dom.isOrContains(this.area, node)) { return node; } } } return false; } /** * Insert element in editor * * @param {Node} node * @param {Boolean} [insertCursorAfter=true] After insert, cursor will move after element * @param {Boolean} [fireChange=true] After insert, editor fire change event. You can prevent this behavior */ insertNode( node: Node, insertCursorAfter = true, fireChange: boolean = true ) { this.errorNode(node); this.focus(); const sel = this.sel; if (!this.isCollapsed()) { this.jodit.execCommand('Delete'); } if (sel && sel.rangeCount) { const range = sel.getRangeAt(0); if (Dom.isOrContains(this.area, range.commonAncestorContainer)) { range.deleteContents(); range.insertNode(node); } else { this.area.appendChild(node); } } else { this.area.appendChild(node); } if (insertCursorAfter) { this.setCursorAfter(node); } if (fireChange && this.jodit.events) { this.jodit.events.fire('synchro'); } if (this.jodit.events) { this.jodit.events.fire('afterInsertNode', node); } } /** * Inserts in the current cursor position some HTML snippet * * @param {string} html HTML The text toWYSIWYG be inserted into the document * @example * ```javascript * parent.selection.insertHTML('<img src="image.png"/>'); * ``` */ insertHTML(html: number | string | Node) { if (html === '') { return; } const node = this.jodit.create.inside.div(), fragment = this.jodit.create.inside.fragment(); let lastChild: Node | null, lastEditorElement: Node | null; if (!this.isFocused() && this.jodit.isEditorMode()) { this.focus(); } if (!Dom.isNode(html, this.win)) { node.innerHTML = html.toString(); } else { node.appendChild(html); } if ( !this.jodit.isEditorMode() && this.jodit.events.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, false); if (lastChild) { this.setCursorAfter(lastChild); } else { this.setCursorIn(fragment); } lastEditorElement = this.area.lastChild; while ( lastEditorElement && lastEditorElement.nodeType === Node.TEXT_NODE && lastEditorElement.previousSibling && lastEditorElement.nodeValue && /^\s*$/.test(lastEditorElement.nodeValue) ) { lastEditorElement = lastEditorElement.previousSibling; } if (lastChild) { if ( lastEditorElement && lastChild === lastEditorElement && lastChild.nodeType === Node.ELEMENT_NODE ) { this.area.appendChild(this.jodit.create.inside.element('br')); } this.setCursorAfter(lastChild); } } /** * Insert image in editor * * @param {string|HTMLImageElement} url URL for image, or HTMLImageElement * @param {string} [styles] If specified, it will be applied <code>$(image).css(styles)</code> * @param { number | string | null } defaultWidth * * @fired afterInsertImage */ insertImage( url: string | HTMLImageElement, styles: IDictionary<string> | null, defaultWidth: number | string | null ) { const image: HTMLImageElement = typeof url === 'string' ? this.jodit.create.inside.element('img') : url; if (typeof url === 'string') { image.setAttribute('src', url); } if (defaultWidth !== null) { let dw: string = defaultWidth.toString(); if ( dw && 'auto' !== dw && String(dw).indexOf('px') < 0 && String(dw).indexOf('%') < 0 ) { dw += 'px'; } css(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); }; image.addEventListener('load', onload); if (image.complete) { onload(); } const result = this.insertNode(image); /** * Triggered after image was inserted {@link Selection~insertImage|insertImage}. This method can executed from * {@link FileBrowser|FileBrowser} or {@link Uploader|Uploader} * @event afterInsertImage * @param {HTMLImageElement} image * @example * ```javascript * var editor = new Jodit("#redactor"); * editor.events.on('afterInsertImage', function (image) { * image.className = 'bloghead4'; * }); * ``` */ this.jodit.events.fire('afterInsertImage', image); return result; } eachSelection = (callback: (current: Node) => void) => { const sel = this.sel; if (sel && sel.rangeCount) { const range = sel.getRangeAt(0); const nodes: Node[] = [], startOffset: number = range.startOffset, length: number = this.area.childNodes.length, start: Node = range.startContainer === this.area ? this.area.childNodes[ startOffset < length ? startOffset : length - 1 ] : range.startContainer, end: Node = range.endContainer === this.area ? this.area.childNodes[range.endOffset - 1] : range.endContainer; Dom.find( start, (node: Node | null) => { if ( node && node !== this.area && !Dom.isEmptyTextNode(node) && !this.isMarker(node as HTMLElement) ) { nodes.push(node); } // checks parentElement as well because partial selections are not equal to entire element return node === end || (node && node.contains(end)); }, this.area, true, 'nextSibling', false ); const forEvery = (current: Node): void => { if (current.nodeName.match(/^(UL|OL)$/)) { return Array.from(current.childNodes).forEach(forEvery); } if (current.nodeName === 'LI') { if (current.firstChild) { current = current.firstChild; } else { const currentB = this.jodit.create.inside.text( INVISIBLE_SPACE ); current.appendChild(currentB); current = currentB; } } callback(current); }; if (nodes.length === 0 && Dom.isEmptyTextNode(start)) { nodes.push(start); } nodes.forEach(forEvery); } }; /** * Set cursor after the node * * @param {Node} node * @return {Node} fake invisible textnode. After insert it can be removed */ setCursorAfter( node: Node | HTMLElement | HTMLTableElement | HTMLTableCellElement ): Text | false { this.errorNode(node); if ( !Dom.up( node, (elm: Node | null) => elm === this.area || (elm && elm.parentNode === this.area), this.area ) ) { throw new Error('Node element must be in editor'); } const range = this.createRange(); let fakeNode: Text | false = false; if (node.nodeType !== Node.TEXT_NODE) { fakeNode = this.doc.createTextNode(consts.INVISIBLE_SPACE); range.setStartAfter(node); range.insertNode(fakeNode); range.selectNode(fakeNode); } else { range.setEnd( node, node.nodeValue !== null ? node.nodeValue.length : 0 ); } range.collapse(false); this.selectRange(range); return fakeNode; } /** * Checks if the cursor is at the end(start) block * * @param {boolean} start=false true - check whether the cursor is at the start block * @param {HTMLElement} parentBlock - Find in this * * @return {boolean | null} true - the cursor is at the end(start) block, null - cursor somewhere outside */ cursorInTheEdge(start: boolean, parentBlock: HTMLElement): boolean | null { const sel = this.sel, range: Range | null = sel && sel.rangeCount ? sel.getRangeAt(0) : null; if (!range) { return null; } const container = start ? range.startContainer : range.endContainer, sibling = (node: Node): Node | false => { return start ? Dom.prev(node, elm => !!elm, parentBlock) : Dom.next(node, elm => !!elm, parentBlock); }, checkSiblings = (next: Node | false): false | void => { while (next) { next = sibling(next); if ( next && !Dom.isEmptyTextNode(next) && next.nodeName !== 'BR' ) { return false; } } }; if (container.nodeType === Node.TEXT_NODE) { const value: string = container.nodeValue || ''; if ( start && range.startOffset > value.length - value.replace(INVISIBLE_SPACE_REG_EXP_START, '').length ) { return false; } if ( !start && range.startOffset < value.replace(INVISIBLE_SPACE_REG_EXP_END, '').length ) { return false; } if (checkSiblings(container) === false) { return false; } } const current: Node | false = this.current(false); if (!current || !Dom.isOrContains(parentBlock, current, true)) { return null; } if (!start && range.startContainer.childNodes[range.startOffset]) { if (current && !Dom.isEmptyTextNode(current)) { return false; } } return checkSiblings(current) !== false; } /** * Set cursor before the node * * @param {Node} node * @return {Text} fake invisible textnode. After insert it can be removed */ setCursorBefore( node: Node | HTMLElement | HTMLTableElement | HTMLTableCellElement ): Text | false { this.errorNode(node); if ( !Dom.up( node, (elm: Node | null) => elm === this.area || (elm && elm.parentNode === this.area), this.area ) ) { throw new Error('Node element must be in editor'); } const range = this.createRange(); let fakeNode: Text | false = false; if (node.nodeType !== Node.TEXT_NODE) { fakeNode = this.doc.createTextNode(consts.INVISIBLE_SPACE); range.setStartBefore(node); range.collapse(true); range.insertNode(fakeNode); range.selectNode(fakeNode); } else { range.setStart( node, node.nodeValue !== null ? node.nodeValue.length : 0 ); } range.collapse(true); this.selectRange(range); return fakeNode; } /** * Set cursor in the node * * @param {Node} node * @param {boolean} [inStart=false] set cursor in start of element */ setCursorIn(node: Node, inStart: boolean = false) { this.errorNode(node); if ( !Dom.up( node, (elm: Node | null) => elm === this.area || (elm && elm.parentNode === this.area), this.area ) ) { throw new Error('Node element must be in editor'); } const range = this.createRange(); let start: Node | null = node, last: Node = node; do { if (start.nodeType === Node.TEXT_NODE) { break; } last = start; start = inStart ? start.firstChild : start.lastChild; } while (start); if (!start) { const fakeNode: Text = this.doc.createTextNode( consts.INVISIBLE_SPACE ); if (!/^(img|br|input)$/i.test(last.nodeName)) { last.appendChild(fakeNode); last = fakeNode; } else { start = last; } } range.selectNodeContents(start || last); range.collapse(inStart); this.selectRange(range); return last; } /** * Set range selection * * @param range * * @fires changeSelection */ selectRange(range: Range) { const sel = this.sel; if (sel) { sel.removeAllRanges(); sel.addRange(range); } /** * Fired after change selection * * @event changeSelection */ this.jodit.events.fire('changeSelection'); } /** * Select node * * @param {Node} node * @param {boolean} [inward=false] select all inside */ select( node: Node | HTMLElement | HTMLTableElement | HTMLTableCellElement, inward = false ) { this.errorNode(node); if ( !Dom.up( node, (elm: Node | null) => elm === this.area || (elm && elm.parentNode === this.area), this.area ) ) { throw new Error('Node element must be in editor'); } const range = this.createRange(); range[inward ? 'selectNodeContents' : 'selectNode'](node); this.selectRange(range); } /** * Return current selected HTML */ getHTML(): string { const sel = this.sel; if (sel && sel.rangeCount > 0) { const range = sel.getRangeAt(0); const clonedSelection = range.cloneContents(); const div = this.jodit.create.inside.div(); div.appendChild(clonedSelection); return div.innerHTML; } return ''; } /** * Apply some css rules for all selections. It method wraps selections in nodeName tag. * * @param {object} cssRules * @param {string} nodeName * @param {object} options */ applyCSS( cssRules: IDictionary<string | number | undefined>, nodeName: HTMLTagNames = 'span', options?: | ((jodit: IJodit, elm: HTMLElement) => boolean) | IDictionary<string | string[]> | IDictionary<(editor: IJodit, elm: HTMLElement) => boolean> ) { const WRAP = 1, UNWRAP = 0, defaultTag = 'SPAN', FONT = 'FONT'; let mode: number; const findNextCondition = (elm: Node | null): boolean => elm !== null && !Dom.isEmptyTextNode(elm) && !this.isMarker(elm as HTMLElement); const checkCssRulesFor = (elm: HTMLElement): boolean => { return ( elm.nodeName !== FONT && elm.nodeType === Node.ELEMENT_NODE && ((isPlainObject(options) && each( options as IDictionary<string[]>, (cssPropertyKey, cssPropertyValues) => { const value = css( elm, cssPropertyKey, undefined, true ); return ( value !== null && value !== '' && cssPropertyValues.indexOf( value.toString().toLowerCase() ) !== -1 ); } )) || (typeof options === 'function' && options(this.jodit, elm))) ); }; const isSuitElement = (elm: Node | null): boolean | null => { if (!elm) { return false; } const reg: RegExp = new RegExp('^' + elm.nodeName + '$', 'i'); return ( (reg.test(nodeName) || !!(options && checkCssRulesFor(elm as HTMLElement))) && findNextCondition(elm) ); }; const toggleStyles = (elm: HTMLElement) => { if (isSuitElement(elm)) { // toggle CSS rules if (elm.nodeName === defaultTag && cssRules) { // TODO need check == and === Object.keys(cssRules).forEach((rule: string) => { if ( mode === UNWRAP || css(elm, rule) === normilizeCSSValue(rule, cssRules[ rule ] as string) ) { css(elm, rule, ''); if (mode === undefined) { mode = UNWRAP; } } else { css(elm, rule, cssRules[rule]); if (mode === undefined) { mode = WRAP; } } }); } if ( !Dom.isBlock(elm, this.win) && (!elm.getAttribute('style') || elm.nodeName !== defaultTag) ) { // toggle `<strong>test</strong>` toWYSIWYG `test`, and // `<span style="">test</span>` toWYSIWYG `test` Dom.unwrap(elm); if (mode === undefined) { mode = UNWRAP; } } } }; if (!this.isCollapsed()) { const selInfo: markerInfo[] = this.save(); normalizeNode(this.area.firstChild); // FF fix for test "commandsTest - Exec command "bold" // for some text that contains a few STRONG elements, should unwrap all of these" // fix issue https://github.com/xdan/jodit/issues/65 $$('*[style*=font-size]', this.area).forEach((elm: HTMLElement) => { elm.style && elm.style.fontSize && elm.setAttribute( 'data-font-size', elm.style.fontSize.toString() ); }); this.doc.execCommand('fontsize', false, '7'); $$('*[data-font-size]', this.area).forEach((elm: HTMLElement) => { if (elm.style && elm.getAttribute('data-font-size')) { elm.style.fontSize = elm.getAttribute('data-font-size'); elm.removeAttribute('data-font-size'); } }); $$('font[size="7"]', this.area).forEach((font: HTMLElement) => { if ( !Dom.next( font, findNextCondition, font.parentNode as HTMLElement ) && !Dom.prev( font, findNextCondition, font.parentNode as HTMLElement ) && isSuitElement(font.parentNode as HTMLElement) && font.parentNode !== this.area && (!Dom.isBlock(font.parentNode, this.win) || consts.IS_BLOCK.test(nodeName)) ) { toggleStyles(font.parentNode as HTMLElement); } else if ( font.firstChild && !Dom.next( font.firstChild, findNextCondition, font as HTMLElement ) && !Dom.prev( font.firstChild, findNextCondition, font as HTMLElement ) && isSuitElement(font.firstChild as HTMLElement) ) { toggleStyles(font.firstChild as HTMLElement); } else if (Dom.closest(font, isSuitElement, this.area)) { const leftRange = this.createRange(), wrapper = Dom.closest( font, isSuitElement, this.area ) as HTMLElement; leftRange.setStartBefore(wrapper); leftRange.setEndBefore(font); const leftFragment: DocumentFragment = leftRange.extractContents(); if ( (!leftFragment.textContent || !trim(leftFragment.textContent).length) && leftFragment.firstChild ) { Dom.unwrap(leftFragment.firstChild); } if (wrapper.parentNode) { wrapper.parentNode.insertBefore(leftFragment, wrapper); } leftRange.setStartAfter(font); leftRange.setEndAfter(wrapper); const rightFragment = leftRange.extractContents(); // case then marker can be inside fragnment if ( (!rightFragment.textContent || !trim(rightFragment.textContent).length) && rightFragment.firstChild ) { Dom.unwrap(rightFragment.firstChild); } Dom.after(wrapper, rightFragment); toggleStyles(wrapper); } else { // unwrap all suit elements inside const needUnwrap: Node[] = []; let firstElementSuit: boolean | undefined; if (font.firstChild) { Dom.find( font.firstChild, (elm: Node | null) => { if (elm && isSuitElement(elm as HTMLElement)) { if (firstElementSuit === undefined) { firstElementSuit = true; } needUnwrap.push(elm); } else { if (firstElementSuit === undefined) { firstElementSuit = false; } } return false; }, font, true ); } needUnwrap.forEach(Dom.unwrap); if (!firstElementSuit) { if (mode === undefined) { mode = WRAP; } if (mode === WRAP) { css( Dom.replace( font, nodeName, false, false, this.doc ), cssRules && nodeName.toUpperCase() === defaultTag ? cssRules : {} ); } } } if (font.parentNode) { Dom.unwrap(font); } }); this.restore(selInfo); } else { let clearStyle: boolean = false; if ( this.current() && Dom.closest(this.current() as Node, nodeName, this.area) ) { clearStyle = true; const closest: Node = Dom.closest( this.current() as Node, nodeName, this.area ) as Node; if (closest) { this.setCursorAfter(closest); } } if (nodeName.toUpperCase() === defaultTag || !clearStyle) { const node: Node = this.jodit.create.inside.element(nodeName); node.appendChild( this.jodit.create.inside.text(consts.INVISIBLE_SPACE) ); this.insertNode(node, false, false); if (nodeName.toUpperCase() === defaultTag && cssRules) { css(node as HTMLElement, cssRules); } this.setCursorIn(node); } } } }