jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
1,370 lines (1,173 loc) • 30.3 kB
text/typescript
/*!
* 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);
}
}
}
}