[FIX] web_editor: apply format with tag
Before this commit The editor used only span with a style tag in order to format text. This was considered to be unclean code. After this commit - use style tag (strong, em, s, u) rather than span with a style tag - use span with style tag if a container is already styled - remove redundant style/tags task-2850566 closes odoo/odoo#99812 Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
@@ -3426,9 +3426,6 @@ export class OdooEditor extends EventTarget {
|
||||
}
|
||||
hint.removeAttribute('placeholder');
|
||||
}
|
||||
|
||||
sanitize(element);
|
||||
|
||||
this._pluginCall('cleanForSave', [element]);
|
||||
// Clean the remaining ZeroWidthspaces added by the `fillEmpty` function
|
||||
// ( contain "data-oe-zws-empty-inline" attr)
|
||||
@@ -3449,6 +3446,8 @@ export class OdooEditor extends EventTarget {
|
||||
emptyElement.removeAttribute('data-oe-zws-empty-inline');
|
||||
}
|
||||
}
|
||||
sanitize(element);
|
||||
|
||||
// Remove contenteditable=false on elements
|
||||
for (const el of element.querySelectorAll('[contenteditable="false"]')) {
|
||||
if (!el.hasAttribute('data-oe-keep-contenteditable')) {
|
||||
|
||||
@@ -9,21 +9,18 @@ import {
|
||||
closestPath,
|
||||
DIRECTIONS,
|
||||
findNode,
|
||||
getCursorDirection,
|
||||
getCursors,
|
||||
getDeepRange,
|
||||
getInSelection,
|
||||
getListMode,
|
||||
getNormalizedCursorPosition,
|
||||
getSelectedNodes,
|
||||
getTraversedNodes,
|
||||
insertAndSelectZws,
|
||||
insertText,
|
||||
isBlock,
|
||||
isFormat,
|
||||
isSelectionFormat,
|
||||
isColorGradient,
|
||||
isContentTextNode,
|
||||
isSelectionFormat,
|
||||
isShrunkBlock,
|
||||
isVisible,
|
||||
isVisibleStr,
|
||||
@@ -46,6 +43,7 @@ import {
|
||||
pxToFloat,
|
||||
getRowIndex,
|
||||
parseHTML,
|
||||
formatSelection,
|
||||
} from '../utils/utils.js';
|
||||
|
||||
const TEXT_CLASSES_REGEX = /\btext-[^\s]*\b/g;
|
||||
@@ -133,250 +131,6 @@ function hasColor(element, mode) {
|
||||
getComputedStyle(element)[mode] !== getComputedStyle(parent)[mode])
|
||||
);
|
||||
}
|
||||
/**
|
||||
* This function abstracts the difficulty of applying a inline style to a
|
||||
* selection. TODO: This implementations potentially adds one span per text
|
||||
* node, in an ideal world it would wrap all concerned nodes in one span
|
||||
* whenever possible.
|
||||
* @param {Element => void} applyStyle Callback that receives an element to
|
||||
* which the wanted style should be applied
|
||||
* @param {string | [string, string]} [style] the format type to toggle or an
|
||||
* array with the style property name and the value to apply to it
|
||||
* @param {boolean} [shouldApply=true] set to false to undo a style rather than
|
||||
* apply it.
|
||||
* @returns {Element[]} the elements on which the style was changed.
|
||||
*/
|
||||
export function applyInlineStyle(editor, applyStyle, style, shouldApply=true) {
|
||||
let selectedTextNodes, direction, startContainer, startOffset, endContainer, endOffset;
|
||||
if (editor.editable.querySelector('.o_selected_td')) {
|
||||
const selectedNodes = getSelectedNodes(editor.editable);
|
||||
selectedTextNodes = selectedNodes.filter(node => node.nodeType === Node.TEXT_NODE);
|
||||
direction = DIRECTIONS.RIGHT;
|
||||
[startContainer, startOffset] = [selectedNodes[0], 0];
|
||||
[endContainer, endOffset] = [selectedNodes[selectedNodes.length - 1], selectedNodes[selectedNodes.length - 1].childNodes.length];
|
||||
} else {
|
||||
getDeepRange(editor.editable, { splitText: true, select: true });
|
||||
const sel = editor.document.getSelection();
|
||||
const { startContainer, startOffset, endContainer, endOffset } = sel.getRangeAt(0);
|
||||
const { anchorNode, anchorOffset, focusNode, focusOffset } = sel;
|
||||
direction = getCursorDirection(anchorNode, anchorOffset, focusNode, focusOffset);
|
||||
const [
|
||||
normalizedStartContainer,
|
||||
normalizedStartOffset
|
||||
] = getNormalizedCursorPosition(startContainer, startOffset);
|
||||
const [
|
||||
normalizedEndContainer,
|
||||
normalizedEndOffset
|
||||
] = getNormalizedCursorPosition(endContainer, endOffset);
|
||||
selectedTextNodes = getSelectedNodes(editor.editable).filter(node => {
|
||||
const atLeastOneCharFromNodeInSelection = !(
|
||||
(node === normalizedEndContainer && normalizedEndOffset === 0) ||
|
||||
(node === normalizedStartContainer && normalizedStartOffset === node.textContent.length)
|
||||
);
|
||||
return isContentTextNode(node) && atLeastOneCharFromNodeInSelection;
|
||||
});
|
||||
}
|
||||
const textNodesToFormat = selectedTextNodes.filter(node => {
|
||||
let isApplied;
|
||||
if (Array.isArray(style) && style[style[0]]) {
|
||||
let ancestor = node;
|
||||
while (ancestor) {
|
||||
if (ancestor.style[style[0]]) {
|
||||
isApplied = ancestor.style[style[0]] === style[1];
|
||||
break;
|
||||
} else {
|
||||
ancestor = ancestor.parentElement;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isApplied = isFormat[style] && isFormat[style](node);
|
||||
}
|
||||
return shouldApply ? !isApplied : isApplied;
|
||||
});
|
||||
const changedElements = [];
|
||||
for (const textNode of textNodesToFormat) {
|
||||
// If text node ends after the end of the selection, split it and
|
||||
// keep the part that is inside.
|
||||
if (endContainer === textNode && endOffset < textNode.textContent.length) {
|
||||
// No reassignement needed, entirely dependent on the
|
||||
// splitTextNode implementation.
|
||||
splitTextNode(textNode, endOffset, DIRECTIONS.LEFT);
|
||||
}
|
||||
// If text node starts before the beginning of the selection, split it
|
||||
// and keep the part that is inside as textNode.
|
||||
if (startContainer === textNode && startOffset > 0) {
|
||||
// No reassignement needed, entirely dependent on the
|
||||
// splitTextNode implementation.
|
||||
splitTextNode(textNode, startOffset, DIRECTIONS.RIGHT);
|
||||
}
|
||||
// If the parent is not inline or is not completely in the
|
||||
// selection, wrap text node in inline node. Also skips <a> tags to
|
||||
// work with native `removeFormat` command
|
||||
const siblings = [...textNode.parentElement.childNodes];
|
||||
if (
|
||||
isBlock(textNode.parentElement) ||
|
||||
!(
|
||||
selectedTextNodes.includes(siblings[0]) &&
|
||||
selectedTextNodes.includes(siblings[siblings.length - 1])
|
||||
) ||
|
||||
textNode.parentElement.tagName === 'A'
|
||||
) {
|
||||
const newParent = document.createElement('span');
|
||||
textNode.before(newParent);
|
||||
// Group selected text node siblings:
|
||||
const newChildren = [textNode];
|
||||
let currentTextNode = textNode.nextSibling;
|
||||
while (currentTextNode && textNodesToFormat.includes(currentTextNode)) {
|
||||
newChildren.push(currentTextNode);
|
||||
textNodesToFormat.splice(textNodesToFormat.indexOf(currentTextNode), 1);
|
||||
currentTextNode = currentTextNode.nextSibling;
|
||||
}
|
||||
newParent.append(...newChildren);
|
||||
}
|
||||
applyStyle(textNode.parentElement);
|
||||
changedElements.push(textNode.parentElement);
|
||||
}
|
||||
if (selectedTextNodes[0] && selectedTextNodes[0].textContent === '\u200B') {
|
||||
setSelection(selectedTextNodes[0], 0);
|
||||
} else if (selectedTextNodes.length) {
|
||||
const firstNode = selectedTextNodes[0];
|
||||
const lastNode = selectedTextNodes[selectedTextNodes.length - 1];
|
||||
if (direction === DIRECTIONS.RIGHT) {
|
||||
setSelection(firstNode, 0, lastNode, lastNode.length);
|
||||
} else {
|
||||
setSelection(lastNode, lastNode.length, firstNode, 0);
|
||||
}
|
||||
}
|
||||
return changedElements;
|
||||
}
|
||||
const styles = {
|
||||
bold: {
|
||||
is: editable => isSelectionFormat(editable, 'bold'),
|
||||
name: 'fontWeight',
|
||||
value: 'bolder',
|
||||
},
|
||||
italic: {
|
||||
is: editable => isSelectionFormat(editable, 'italic'),
|
||||
name: 'fontStyle',
|
||||
value: 'italic',
|
||||
},
|
||||
underline: {
|
||||
is: editable => isSelectionFormat(editable, 'underline'),
|
||||
name: 'textDecorationLine',
|
||||
value: 'underline',
|
||||
},
|
||||
strikeThrough: {
|
||||
is: editable => isSelectionFormat(editable, 'strikeThrough'),
|
||||
name: 'textDecorationLine',
|
||||
value: 'line-through',
|
||||
},
|
||||
switchDirection: {
|
||||
is: editable => isSelectionFormat(editable, 'switchDirection'),
|
||||
},
|
||||
};
|
||||
|
||||
export function toggleFormat(editor, format) {
|
||||
const selectedTableCells = editor.editable.querySelectorAll('.o_selected_td');
|
||||
const selection = editor.document.getSelection();
|
||||
if (!selection.rangeCount && !selectedTableCells.length) return;
|
||||
const wasCollapsed = selection.getRangeAt(0).collapsed && !selectedTableCells.length;
|
||||
let zws;
|
||||
if (wasCollapsed) {
|
||||
if (selection.anchorNode.nodeType === Node.TEXT_NODE && selection.anchorNode.textContent === '\u200b') {
|
||||
zws = selection.anchorNode;
|
||||
selection.getRangeAt(0).selectNode(zws);
|
||||
} else {
|
||||
zws = insertAndSelectZws(selection);
|
||||
}
|
||||
}
|
||||
getDeepRange(editor.editable, { splitText: true, select: true, correctTripleClick: true });
|
||||
const {anchorNode, anchorOffset, focusNode, focusOffset} = editor.document.getSelection();
|
||||
const style = styles[format];
|
||||
const selectedTextNodes = getSelectedNodes(editor.editable)
|
||||
.filter(n => n.nodeType === Node.TEXT_NODE && n.nodeValue.length);
|
||||
const isAlreadyFormatted = style.is(editor.editable);
|
||||
let changedElements = [];
|
||||
if (isAlreadyFormatted && style.name === 'textDecorationLine') {
|
||||
for (const textNode of selectedTextNodes) {
|
||||
const closestDecorated = closestElement(textNode, `[style*="text-decoration-line: ${style.value}"]`);
|
||||
if (closestDecorated) {
|
||||
const splitResult = splitAroundUntil(textNode, closestDecorated);
|
||||
const decorationToRemove = splitResult[0] || splitResult[1] || closestDecorated;
|
||||
decorationToRemove.style.removeProperty('text-decoration-line');
|
||||
if (!decorationToRemove.style.cssText) {
|
||||
for (const child of decorationToRemove.childNodes) {
|
||||
decorationToRemove.before(child);
|
||||
changedElements.push(child);
|
||||
}
|
||||
decorationToRemove.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wasCollapsed) {
|
||||
const siblings = [...zws.parentElement.childNodes];
|
||||
if (
|
||||
selectedTextNodes.includes(siblings[0]) &&
|
||||
selectedTextNodes.includes(siblings[siblings.length - 1])
|
||||
) {
|
||||
zws.parentElement.setAttribute('data-oe-zws-empty-inline', '');
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
span.setAttribute('data-oe-zws-empty-inline', '');
|
||||
zws.before(span);
|
||||
span.append(zws);
|
||||
|
||||
}
|
||||
setSelection(zws, 1);
|
||||
} else {
|
||||
setSelection(anchorNode, anchorOffset, focusNode, focusOffset);
|
||||
}
|
||||
} else if (format === 'switchDirection') {
|
||||
const defaultDirection = editor.options.direction;
|
||||
for (const block of new Set(selectedTextNodes.map(textNode => closestBlock(textNode)))) {
|
||||
if (isAlreadyFormatted) {
|
||||
block.removeAttribute('dir');
|
||||
} else {
|
||||
block.setAttribute('dir', defaultDirection === 'ltr' ? 'rtl' : 'ltr');
|
||||
}
|
||||
changedElements.push(block);
|
||||
}
|
||||
} else {
|
||||
changedElements = applyInlineStyle(editor, el => {
|
||||
if (isAlreadyFormatted) {
|
||||
const block = closestBlock(el);
|
||||
el.style[style.name] = style.is(block) ? 'normal' : getComputedStyle(block)[style.name];
|
||||
} else if (style.name === 'textDecorationLine' && el.style[style.name]) {
|
||||
// The <span> (el) has a text decoration and we want to set
|
||||
// another. We don't want to replace the old with the new, we
|
||||
// want to add a new one (eg it was underlined, we want it also
|
||||
// strikeThrough).
|
||||
const newChild = document.createElement('span');
|
||||
const children = [...el.childNodes];
|
||||
el.prepend(newChild);
|
||||
newChild.append(...children);
|
||||
newChild.style[style.name] = style.value;
|
||||
} else {
|
||||
el.style[style.name] = style.value;
|
||||
}
|
||||
if (zws) {
|
||||
const siblings = [...zws.parentElement.childNodes];
|
||||
if (
|
||||
selectedTextNodes.includes(siblings[0]) &&
|
||||
selectedTextNodes.includes(siblings[siblings.length - 1])
|
||||
) {
|
||||
zws.parentElement.setAttribute('data-oe-zws-empty-inline', '');
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
span.setAttribute('data-oe-zws-empty-inline', '');
|
||||
zws.before(span);
|
||||
span.append(zws);
|
||||
|
||||
}
|
||||
}
|
||||
}, format, !isAlreadyFormatted);
|
||||
}
|
||||
return changedElements;
|
||||
}
|
||||
|
||||
// This is a whitelist of the commands that are implemented by the
|
||||
// editor itself rather than the node prototypes. It might be
|
||||
@@ -586,12 +340,29 @@ export const editorCommands = {
|
||||
|
||||
// Formats
|
||||
// -------------------------------------------------------------------------
|
||||
bold: editor => toggleFormat(editor, 'bold'),
|
||||
italic: editor => toggleFormat(editor, 'italic'),
|
||||
underline: editor => toggleFormat(editor, 'underline'),
|
||||
strikeThrough: editor => toggleFormat(editor, 'strikeThrough'),
|
||||
bold: editor => formatSelection(editor, 'bold'),
|
||||
italic: editor => formatSelection(editor, 'italic'),
|
||||
underline: editor => formatSelection(editor, 'underline'),
|
||||
strikeThrough: editor => formatSelection(editor, 'strikeThrough'),
|
||||
setFontSize: (editor, size) => formatSelection(editor, 'fontSize', {applyStyle: true, formatProps: {size}}),
|
||||
switchDirection: editor => {
|
||||
const changedElements = toggleFormat(editor, 'switchDirection');
|
||||
getDeepRange(editor.editable, { splitText: true, select: true, correctTripleClick: true });
|
||||
const selection = editor.document.getSelection();
|
||||
const selectedTextNodes = [selection.anchorNode, ...getSelectedNodes(editor.editable), selection.focusNode]
|
||||
.filter(n => n.nodeType === Node.TEXT_NODE && n.nodeValue.trim().length);
|
||||
|
||||
const changedElements = [];
|
||||
const defaultDirection = editor.options.direction;
|
||||
const shouldApplyStyle = !isSelectionFormat(editor.editable, 'switchDirection');
|
||||
for (const block of new Set(selectedTextNodes.map(textNode => closestBlock(textNode)))) {
|
||||
if (!shouldApplyStyle) {
|
||||
block.removeAttribute('dir');
|
||||
} else {
|
||||
block.setAttribute('dir', defaultDirection === 'ltr' ? 'rtl' : 'ltr');
|
||||
}
|
||||
changedElements.push(block);
|
||||
}
|
||||
|
||||
for (const element of changedElements) {
|
||||
const style = getComputedStyle(element);
|
||||
if (style.direction === 'ltr' && style.textAlign === 'right') {
|
||||
@@ -614,19 +385,6 @@ export const editorCommands = {
|
||||
justifyRight: editor => align(editor, 'right'),
|
||||
justifyCenter: editor => align(editor, 'center'),
|
||||
justifyFull: editor => align(editor, 'justify'),
|
||||
/**
|
||||
* @param {string} size A valid css size string
|
||||
*/
|
||||
setFontSize: (editor, size) => {
|
||||
const selection = editor.document.getSelection();
|
||||
if (!selection.rangeCount) return;
|
||||
if (selection.getRangeAt(0).collapsed) {
|
||||
insertAndSelectZws(selection);
|
||||
}
|
||||
applyInlineStyle(editor, element => {
|
||||
element.style.fontSize = size;
|
||||
}, ['fontSize', size]);
|
||||
},
|
||||
|
||||
// Link
|
||||
createLink: (editor, link, content) => {
|
||||
@@ -762,8 +520,7 @@ export const editorCommands = {
|
||||
// Partially selected <font>: split it.
|
||||
const selectedChildren = children.filter(child => selectedNodes.includes(child));
|
||||
if (selectedChildren.length) {
|
||||
const splitResult = splitAroundUntil(selectedChildren, font);
|
||||
font = splitResult[0] || splitResult[1] || font;
|
||||
font = splitAroundUntil(selectedChildren, font);
|
||||
} else {
|
||||
font = [];
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ class Sanitize {
|
||||
if (
|
||||
node.nodeType === Node.TEXT_NODE &&
|
||||
node.textContent.includes('\u200B') &&
|
||||
node.parentElement.hasAttribute('oe-zws-empty-inline') &&
|
||||
node.parentElement.hasAttribute('data-oe-zws-empty-inline') &&
|
||||
(
|
||||
node.textContent.length > 1 ||
|
||||
// There can be multiple ajacent text nodes, in which case
|
||||
|
||||
@@ -870,6 +870,248 @@ export function preserveCursor(document) {
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Format utils
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const formatsSpecs = {
|
||||
italic: {
|
||||
tagName: 'em',
|
||||
isFormatted: isItalic,
|
||||
isTag: (node) => ['EM', 'I'].includes(node.tagName),
|
||||
hasStyle: (node) => Boolean(node.style && node.style['font-style']),
|
||||
addStyle: (node) => node.style['font-style'] = 'italic',
|
||||
addNeutralStyle: (node) => node.style['font-style'] = 'normal',
|
||||
removeStyle: (node) => removeStyle(node, 'font-style'),
|
||||
},
|
||||
bold: {
|
||||
tagName: 'strong',
|
||||
isFormatted: isBold,
|
||||
isTag: (node) => ['STRONG', 'B'].includes(node.tagName),
|
||||
hasStyle: (node) => Boolean(node.style && node.style['font-weight']),
|
||||
addStyle: (node) => node.style['font-weight'] = 'bolder',
|
||||
addNeutralStyle: (node) => {
|
||||
node.style['font-weight'] = 'normal'
|
||||
},
|
||||
removeStyle: (node) => removeStyle(node, 'font-weight'),
|
||||
},
|
||||
underline: {
|
||||
tagName: 'u',
|
||||
isFormatted: isUnderline,
|
||||
isTag: (node) => node.tagName === 'U',
|
||||
hasStyle: (node) => node.style && node.style['text-decoration-line'].includes('underline'),
|
||||
addStyle: (node) => node.style['text-decoration-line'] += ' underline',
|
||||
removeStyle: (node) => removeStyle(node, 'text-decoration-line', 'underline'),
|
||||
},
|
||||
strikeThrough: {
|
||||
tagName: 's',
|
||||
isFormatted: isStrikeThrough,
|
||||
isTag: (node) => node.tagName === 'S',
|
||||
hasStyle: (node) => node.style && node.style['text-decoration-line'].includes('line-through'),
|
||||
addStyle: (node) => node.style['text-decoration-line'] += ' line-through',
|
||||
removeStyle: (node) => removeStyle(node, 'text-decoration-line', 'line-through'),
|
||||
},
|
||||
fontSize: {
|
||||
isFormatted: isFontSize,
|
||||
hasStyle: (node, props) => node.style && node.style['font-size'] === props.size,
|
||||
addStyle: (node, props) => node.style['font-size'] = props.size,
|
||||
removeStyle: (node) => removeStyle(node, 'font-size'),
|
||||
},
|
||||
switchDirection: {
|
||||
isFormatted: isDirectionSwitched,
|
||||
}
|
||||
}
|
||||
|
||||
const removeStyle = (node, styleName, item) => {
|
||||
if (item) {
|
||||
const newStyle = node.style[styleName].split(' ').filter(x => x !== item).join(' ');
|
||||
node.style[styleName] = newStyle || null;
|
||||
} else {
|
||||
node.style[styleName] = null;
|
||||
}
|
||||
if (node.getAttribute('style') === '') {
|
||||
node.removeAttribute('style');
|
||||
}
|
||||
};
|
||||
const getOrCreateSpan = (node, ancestors) => {
|
||||
const span = ancestors.find((element) => element.tagName === 'SPAN' && element.isConnected);
|
||||
if (span) {
|
||||
return span;
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
node.after(span);
|
||||
span.append(node);
|
||||
return span;
|
||||
}
|
||||
}
|
||||
const removeFormat = (node, formatSpec) => {
|
||||
node = closestElement(node);
|
||||
if (formatSpec.hasStyle(node)) {
|
||||
formatSpec.removeStyle(node);
|
||||
if (['SPAN', 'FONT'].includes(node.tagName) && !node.getAttributeNames().length) {
|
||||
return unwrapContents(node);
|
||||
}
|
||||
}
|
||||
|
||||
if (formatSpec.isTag(node)) {
|
||||
const attributesNames = node.getAttributeNames().filter((name)=> {
|
||||
return name !== 'data-oe-zws-empty-inline';
|
||||
});
|
||||
if (attributesNames.length) {
|
||||
// Change tag name
|
||||
const newNode = document.createElement('span');
|
||||
while (node.firstChild) {
|
||||
newNode.appendChild(node.firstChild);
|
||||
}
|
||||
for (let index = node.attributes.length - 1; index >= 0; --index) {
|
||||
newNode.attributes.setNamedItem(node.attributes[index].cloneNode());
|
||||
}
|
||||
node.parentNode.replaceChild(newNode, node);
|
||||
} else {
|
||||
unwrapContents(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const formatSelection = (editor, formatName, {applyStyle, formatProps} = {}) => {
|
||||
const selection = editor.document.getSelection();
|
||||
let direction
|
||||
let wasCollapsed;
|
||||
if (editor.editable.querySelector('.o_selected_td')) {
|
||||
direction = DIRECTIONS.RIGHT;
|
||||
} else {
|
||||
if (!selection.rangeCount) return;
|
||||
wasCollapsed = selection.getRangeAt(0).collapsed;
|
||||
|
||||
direction = getCursorDirection(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset);
|
||||
}
|
||||
getDeepRange(editor.editable, { splitText: true, select: true, correctTripleClick: true });
|
||||
|
||||
if (typeof applyStyle === 'undefined') {
|
||||
applyStyle = !isSelectionFormat(editor.editable, formatName);
|
||||
}
|
||||
|
||||
let zws;
|
||||
if (wasCollapsed) {
|
||||
if (selection.anchorNode.nodeType === Node.TEXT_NODE && selection.anchorNode.textContent === '\u200b') {
|
||||
zws = selection.anchorNode;
|
||||
selection.getRangeAt(0).selectNode(zws);
|
||||
} else {
|
||||
zws = insertAndSelectZws(selection);
|
||||
}
|
||||
getDeepRange(editor.editable, { splitText: true, select: true, correctTripleClick: true });
|
||||
}
|
||||
|
||||
const selectedTextNodes = getSelectedNodes(editor.editable)
|
||||
.filter(n => n.nodeType === Node.TEXT_NODE);
|
||||
|
||||
const formatSpec = formatsSpecs[formatName];
|
||||
for (const selectedTextNode of selectedTextNodes) {
|
||||
// Compute inline ancestors until finding one which is a block or has a class.
|
||||
const inlineAncestors = [];
|
||||
let node = selectedTextNode;
|
||||
while ((node = node.parentElement) && (!isBlock(node) && !(node.classList && node.classList.length))) {
|
||||
inlineAncestors.unshift(node);
|
||||
}
|
||||
const blockOrInlineClass = node;
|
||||
|
||||
const firstBlockOrClassHasFormat = formatSpec.isFormatted(blockOrInlineClass, formatProps);
|
||||
|
||||
let previousAncestorHasFormat = firstBlockOrClassHasFormat;
|
||||
let lastAncestorInlineFormat;
|
||||
|
||||
for (const ancestor of inlineAncestors) {
|
||||
const ancestorIsTag = formatSpec.isTag && formatSpec.isTag(ancestor);
|
||||
const ancestorHasStyle = formatSpec.hasStyle && formatSpec.hasStyle(ancestor, formatProps);
|
||||
const ancestorTagIsFormated = ancestorIsTag || ancestorHasStyle;
|
||||
const isUselessZwsSpan =
|
||||
ancestor.tagName === 'SPAN' &&
|
||||
ancestor.hasAttribute('data-oe-zws-empty-inline') &&
|
||||
ancestor.getAttributeNames().length === 1;
|
||||
const ancestorIsFormatted = formatSpec.isFormatted(ancestor, formatProps);
|
||||
|
||||
if (ancestorTagIsFormated && previousAncestorHasFormat !== ancestorIsFormatted) {
|
||||
if (lastAncestorInlineFormat) {
|
||||
// Remove format on two node that switch the format to flatten the DOM.
|
||||
const newLastAncestorInlineFormat = splitAroundUntil(ancestor, lastAncestorInlineFormat);
|
||||
removeFormat(newLastAncestorInlineFormat, formatSpec);
|
||||
removeFormat(ancestor, formatSpec);
|
||||
lastAncestorInlineFormat = undefined;
|
||||
} else {
|
||||
lastAncestorInlineFormat = ancestor;
|
||||
}
|
||||
// if there is two consecutive format that are the same, remove one.
|
||||
} else if (
|
||||
(ancestorIsTag || ancestorHasStyle) &&
|
||||
(
|
||||
previousAncestorHasFormat ||
|
||||
(!previousAncestorHasFormat && !ancestorIsFormatted)
|
||||
)
|
||||
) {
|
||||
removeFormat(ancestor, formatSpec);
|
||||
} else if (isUselessZwsSpan) {
|
||||
unwrapContents(ancestor);
|
||||
}
|
||||
previousAncestorHasFormat = ancestor.parentElement && formatSpec.isFormatted(ancestor, formatProps);
|
||||
}
|
||||
|
||||
if (selectedTextNode.nodeType === Node.TEXT_NODE) {
|
||||
const isLeafNodeFormated = formatSpec.isFormatted(selectedTextNode, formatProps);
|
||||
if (isLeafNodeFormated !== applyStyle) {
|
||||
if (lastAncestorInlineFormat) {
|
||||
const splitNode = splitAroundUntil(selectedTextNode, lastAncestorInlineFormat);
|
||||
removeFormat(splitNode, formatSpec);
|
||||
} else {
|
||||
if (firstBlockOrClassHasFormat && !applyStyle) {
|
||||
formatSpec.addNeutralStyle(getOrCreateSpan(selectedTextNode, inlineAncestors));
|
||||
} else if (!firstBlockOrClassHasFormat && applyStyle) {
|
||||
const tag = formatSpec.tagName && document.createElement(formatSpec.tagName);
|
||||
if (tag) {
|
||||
selectedTextNode.after(tag);
|
||||
tag.append(selectedTextNode);
|
||||
|
||||
if (!formatSpec.isFormatted(tag, formatProps)) {
|
||||
tag.after(selectedTextNode);
|
||||
tag.remove();
|
||||
formatSpec.addStyle(getOrCreateSpan(selectedTextNode, inlineAncestors), formatProps);
|
||||
}
|
||||
} else {
|
||||
formatSpec.addStyle(getOrCreateSpan(selectedTextNode, inlineAncestors), formatProps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (zws) {
|
||||
const siblings = [...zws.parentElement.childNodes];
|
||||
if (
|
||||
selectedTextNodes.includes(siblings[0]) &&
|
||||
selectedTextNodes.includes(siblings[siblings.length - 1])
|
||||
) {
|
||||
zws.parentElement.setAttribute('data-oe-zws-empty-inline', '');
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
span.setAttribute('data-oe-zws-empty-inline', '');
|
||||
zws.before(span);
|
||||
span.append(zws);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTextNodes[0] && selectedTextNodes[0].textContent === '\u200B') {
|
||||
setSelection(selectedTextNodes[0], 0);
|
||||
} else if (selectedTextNodes.length) {
|
||||
const firstNode = selectedTextNodes[0];
|
||||
const lastNode = selectedTextNodes[selectedTextNodes.length - 1];
|
||||
if (direction === DIRECTIONS.RIGHT) {
|
||||
setSelection(firstNode, 0, lastNode, lastNode.length, false);
|
||||
} else {
|
||||
setSelection(lastNode, lastNode.length, firstNode, 0, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// DOM Info utils
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -1000,7 +1242,7 @@ export function isItalic(node) {
|
||||
export function isUnderline(node) {
|
||||
let parent = closestElement(node);
|
||||
while (parent) {
|
||||
if (getComputedStyle(parent).textDecorationLine === 'underline') {
|
||||
if (getComputedStyle(parent).textDecorationLine.includes('underline')) {
|
||||
return true;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
@@ -1016,13 +1258,26 @@ export function isUnderline(node) {
|
||||
export function isStrikeThrough(node) {
|
||||
let parent = closestElement(node);
|
||||
while (parent) {
|
||||
if (getComputedStyle(parent).textDecorationLine === 'line-through') {
|
||||
if (getComputedStyle(parent).textDecorationLine.includes('line-through')) {
|
||||
return true;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Return true if the given node font-size is equal to `props.size`.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Node} props.node A node to compare the font-size against.
|
||||
* @param {String} props.size The font-size value of the node that will be
|
||||
* checked against.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isFontSize(node, props) {
|
||||
const element = closestElement(node);
|
||||
return getComputedStyle(element)['font-size'] === props.size;
|
||||
}
|
||||
/**
|
||||
* Return true if the given node appears in a different direction than that of
|
||||
* the editable ('ltr' or 'rtl').
|
||||
@@ -1038,13 +1293,6 @@ export function isStrikeThrough(node) {
|
||||
const defaultDirection = editable.getAttribute('dir');
|
||||
return getComputedStyle(closestElement(node)).direction !== defaultDirection;
|
||||
}
|
||||
export const isFormat = {
|
||||
bold: isBold,
|
||||
italic: isItalic,
|
||||
underline: isUnderline,
|
||||
strikeThrough: isStrikeThrough,
|
||||
switchDirection: isDirectionSwitched,
|
||||
};
|
||||
/**
|
||||
* Return true if the current selection on the editable appears as the given
|
||||
* format. The selection is considered to appear as that format if every text
|
||||
@@ -1055,13 +1303,12 @@ export const isFormat = {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSelectionFormat(editable, format) {
|
||||
const selectedText = getSelectedNodes(editable)
|
||||
const selectedNodes = getSelectedNodes(editable)
|
||||
.filter(n => n.nodeType === Node.TEXT_NODE && n.nodeValue.trim().length);
|
||||
if (selectedText.length) {
|
||||
return selectedText.every(n => isFormat[format](n.parentElement, editable))
|
||||
} else {
|
||||
return isFormat[format](closestElement(editable.ownerDocument.getSelection().anchorNode), editable);
|
||||
}
|
||||
const isFormatted = formatsSpecs[format].isFormatted;
|
||||
selectedNodes.push(closestElement(editable.ownerDocument.getSelection().anchorNode));
|
||||
selectedNodes.push(closestElement(editable.ownerDocument.getSelection().focusNode));
|
||||
return selectedNodes.every(n => isFormatted(n, editable));
|
||||
}
|
||||
|
||||
export function isUnbreakable(node) {
|
||||
@@ -1575,8 +1822,8 @@ export function splitElement(element, offset) {
|
||||
/**
|
||||
* Split around the given elements, until a given ancestor (included). Elements
|
||||
* will be removed in the process so caution is advised in dealing with their
|
||||
* references. Returns a tuple containing the new elements on both sides of the
|
||||
* split.
|
||||
* references. Returns the new split root element that is a clone of
|
||||
* limitAncestor or the original limitAncestor if no split occured.
|
||||
*
|
||||
* @see splitElement
|
||||
* @param {Node[] | Node} elements
|
||||
@@ -1585,29 +1832,34 @@ export function splitElement(element, offset) {
|
||||
*/
|
||||
export function splitAroundUntil(elements, limitAncestor) {
|
||||
elements = Array.isArray(elements) ? elements : [elements];
|
||||
let after = elements[elements.length - 1].nextSibling;
|
||||
let newUntil = limitAncestor;
|
||||
const firstNode = elements[0];
|
||||
const lastNode = elements[elements.length - 1];
|
||||
if ([firstNode, lastNode].includes(limitAncestor)) {
|
||||
return limitAncestor;
|
||||
}
|
||||
let before = firstNode.previousSibling;
|
||||
let after = lastNode.nextSibling;
|
||||
let beforeSplit, afterSplit;
|
||||
if (!before && !after && elements[0] !== limitAncestor) {
|
||||
return splitAroundUntil(elements[0].parentElement, limitAncestor);
|
||||
}
|
||||
// Split up ancestors up to font
|
||||
while (after && after.parentElement !== limitAncestor) {
|
||||
afterSplit = splitElement(after.parentElement, childNodeIndex(after))[0];
|
||||
newUntil = afterSplit;
|
||||
after = newUntil.nextSibling;
|
||||
after = afterSplit.nextSibling;
|
||||
}
|
||||
if (after) {
|
||||
afterSplit = splitElement(limitAncestor, childNodeIndex(after))[0];
|
||||
limitAncestor = afterSplit;
|
||||
}
|
||||
let before = elements[0].previousSibling;
|
||||
while (before && before.parentElement !== limitAncestor) {
|
||||
beforeSplit = splitElement(before.parentElement, childNodeIndex(before) + 1)[1];
|
||||
newUntil = beforeSplit;
|
||||
before = newUntil.previousSibling;
|
||||
before = beforeSplit.previousSibling;
|
||||
}
|
||||
if (before) {
|
||||
beforeSplit = splitElement(limitAncestor, childNodeIndex(before) + 1)[1];
|
||||
}
|
||||
return [beforeSplit, afterSplit];
|
||||
return beforeSplit || afterSplit || limitAncestor;
|
||||
}
|
||||
|
||||
export function insertText(sel, content) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import './spec/editor.test.js';
|
||||
import './spec/copyPaste.test.js';
|
||||
import './spec/list.test.js';
|
||||
import './spec/link.test.js';
|
||||
import './spec/fontSize.test.js';
|
||||
import './spec/format.test.js';
|
||||
import './spec/insertHTML.test.js';
|
||||
import './spec/fontAwesome.test.js';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { applyInlineStyle } from '../../src/commands/commands.js';
|
||||
import { OdooEditor } from '../../src/OdooEditor.js';
|
||||
import { getTraversedNodes } from '../../src/utils/utils.js';
|
||||
import {
|
||||
@@ -3062,18 +3061,18 @@ X[]
|
||||
contentAfter: '<p>ab <span>x[]\u200B</span> cd</p>',
|
||||
});
|
||||
});
|
||||
it('should insert a char into a oe-zws-empty-inline span removing the zws and oe-zws-empty-inline', async () => {
|
||||
it('should insert a char into a data-oe-zws-empty-inline span removing the zws and data-oe-zws-empty-inline', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>ab<span oe-zws-empty-inline="">[]\u200B</span>cd</p>',
|
||||
contentBefore: '<p>ab<span data-oe-zws-empty-inline="">[]\u200B</span>cd</p>',
|
||||
stepFunction: async editor => {
|
||||
await insertText(editor, 'x');
|
||||
},
|
||||
contentAfter: '<p>ab<span>x[]</span>cd</p>',
|
||||
});
|
||||
});
|
||||
it('should insert a char into a oe-zws-empty-inline span surrounded by space without removing the zws and oe-zws-empty-inline', async () => {
|
||||
it('should insert a char into a data-oe-zws-empty-inline span surrounded by space without removing the zws and data-oe-zws-empty-inline', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>ab<span oe-zws-empty-inline="">[]\u200B</span>cd</p>',
|
||||
contentBefore: '<p>ab<span data-oe-zws-empty-inline="">[]\u200B</span>cd</p>',
|
||||
stepFunction: async editor => {
|
||||
await insertText(editor, 'x');
|
||||
},
|
||||
@@ -3653,12 +3652,8 @@ X[]
|
||||
stepFunction: async editor => {
|
||||
// simulate preview
|
||||
editor.historyPauseSteps();
|
||||
try {
|
||||
applyInlineStyle(editor, (el) => el.style.color = 'lime');
|
||||
// a[bcd]e with bcd in lime
|
||||
} finally {
|
||||
editor.historyUnpauseSteps();
|
||||
}
|
||||
editor.execCommand('bold');
|
||||
editor.historyUnpauseSteps();
|
||||
// simulate preview's reset
|
||||
editor.historyRevertCurrentStep(); // back to initial state
|
||||
},
|
||||
@@ -4262,11 +4257,11 @@ X[]
|
||||
'<td>ef</td>' +
|
||||
'</tr></tbody></table>',
|
||||
stepFunction: async editor => editor.execCommand('bold'),
|
||||
contentAfterEdit: '<p>a<span style="font-weight: bolder;">[bc</span></p>' +
|
||||
contentAfterEdit: '<p>a<strong>[bc</strong></p>' +
|
||||
'<table class="o_selected_table"><tbody><tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef]</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef]</strong></td>' +
|
||||
'</tr></tbody></table>',
|
||||
});
|
||||
});
|
||||
@@ -4279,11 +4274,11 @@ X[]
|
||||
'</tr></tbody></table><p>a]bc</p>',
|
||||
stepFunction: async editor => editor.execCommand('bold'),
|
||||
contentAfterEdit: '<table class="o_selected_table"><tbody><tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">[ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef</span></td>' +
|
||||
'<td class="o_selected_td"><strong>[ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef</strong></td>' +
|
||||
'</tr></tbody></table>' +
|
||||
'<p><span style="font-weight: bolder;">a]</span>bc</p>',
|
||||
'<p><strong>a]</strong>bc</p>',
|
||||
});
|
||||
});
|
||||
it('should apply bold to some characters, a table and some more characters', async () => {
|
||||
@@ -4296,13 +4291,13 @@ X[]
|
||||
'</tr></tbody></table>' +
|
||||
'<p>a]bc</p>',
|
||||
stepFunction: async editor => editor.execCommand('bold'),
|
||||
contentAfterEdit: '<p>a<span style="font-weight: bolder;">[bc</span></p>' +
|
||||
contentAfterEdit: '<p>a<strong>[bc</strong></p>' +
|
||||
'<table class="o_selected_table"><tbody><tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef</strong></td>' +
|
||||
'</tr></tbody></table>' +
|
||||
'<p><span style="font-weight: bolder;">a]</span>bc</p>',
|
||||
'<p><strong>a]</strong>bc</p>',
|
||||
});
|
||||
});
|
||||
it('should apply bold to some characters, a table, some more characters and another table', async () => {
|
||||
@@ -4320,17 +4315,17 @@ X[]
|
||||
'<td>ef</td>' +
|
||||
'</tr></tbody></table>',
|
||||
stepFunction: async editor => editor.execCommand('bold'),
|
||||
contentAfterEdit: '<p>a<span style="font-weight: bolder;">[bc</span></p>' +
|
||||
contentAfterEdit: '<p>a<strong>[bc</strong></p>' +
|
||||
'<table class="o_selected_table"><tbody><tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef</strong></td>' +
|
||||
'</tr></tbody></table>' +
|
||||
'<p><span style="font-weight: bolder;">abc</span></p>' +
|
||||
'<p><strong>abc</strong></p>' +
|
||||
'<table class="o_selected_table"><tbody><tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef]</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef]</strong></td>' +
|
||||
'</tr></tbody></table>',
|
||||
});
|
||||
});
|
||||
@@ -4350,19 +4345,19 @@ X[]
|
||||
'</tr></tbody></table>' +
|
||||
'<p>a]bc</p>',
|
||||
stepFunction: async editor => editor.execCommand('bold'),
|
||||
contentAfterEdit: '<p>a<span style="font-weight: bolder;">[bc</span></p>' +
|
||||
contentAfterEdit: '<p>a<strong>[bc</strong></p>' +
|
||||
'<table class="o_selected_table"><tbody><tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef</strong></td>' +
|
||||
'</tr></tbody></table>' +
|
||||
'<p><span style="font-weight: bolder;">abc</span></p>' +
|
||||
'<p><strong>abc</strong></p>' +
|
||||
'<table class="o_selected_table"><tbody><tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef</strong></td>' +
|
||||
'</tr></tbody></table>' +
|
||||
'<p><span style="font-weight: bolder;">a]</span>bc</p>',
|
||||
'<p><strong>a]</strong>bc</p>',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4591,8 +4586,8 @@ X[]
|
||||
'</tr></tbody></table>',
|
||||
stepFunction: async editor => editor.execCommand('bold'),
|
||||
contentAfterEdit: '<table class="o_selected_table"><tbody><tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">[ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd]</span></td>' +
|
||||
'<td class="o_selected_td"><strong>[ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd]</strong></td>' +
|
||||
'<td>ef</td>' +
|
||||
'</tr></tbody></table>',
|
||||
});
|
||||
@@ -4606,9 +4601,9 @@ X[]
|
||||
'</tr><tr><td>ab</td><td>cd</td><td>ef</td></tr></tbody></table>',
|
||||
stepFunction: async editor => editor.execCommand('bold'),
|
||||
contentAfterEdit: '<table class="o_selected_table"><tbody><tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">[ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef]</span></td>' +
|
||||
'<td class="o_selected_td"><strong>[ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef]</strong></td>' +
|
||||
'</tr><tr><td>ab</td><td>cd</td><td>ef</td></tr></tbody></table>',
|
||||
});
|
||||
});
|
||||
@@ -4634,17 +4629,17 @@ X[]
|
||||
stepFunction: async editor => editor.execCommand('bold'),
|
||||
contentAfterEdit: '<table class="o_selected_table"><tbody>' +
|
||||
'<tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">[ab</span></td>' +
|
||||
'<td class="o_selected_td"><strong>[ab</strong></td>' +
|
||||
'<td>cd</td>' +
|
||||
'<td>ef</td>' +
|
||||
'</tr>' +
|
||||
'<tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab</strong></td>' +
|
||||
'<td>cd</td>' +
|
||||
'<td>ef</td>' +
|
||||
'</tr>' +
|
||||
'<tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab]</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab]</strong></td>' +
|
||||
'<td>cd</td>' +
|
||||
'<td>ef</td>' +
|
||||
'</tr>' +
|
||||
@@ -4673,13 +4668,13 @@ X[]
|
||||
stepFunction: async editor => editor.execCommand('bold'),
|
||||
contentAfterEdit: '<table class="o_selected_table"><tbody>' +
|
||||
'<tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">[ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><strong>[ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td>ef</td>' +
|
||||
'</tr>' +
|
||||
'<tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd]</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd]</strong></td>' +
|
||||
'<td>ef</td>' +
|
||||
'</tr>' +
|
||||
'<tr>' +
|
||||
@@ -4712,19 +4707,19 @@ X[]
|
||||
stepFunction: async editor => editor.execCommand('bold'),
|
||||
contentAfterEdit: '<table class="o_selected_table"><tbody>' +
|
||||
'<tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">[ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef</span></td>' +
|
||||
'<td class="o_selected_td"><strong>[ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef</strong></td>' +
|
||||
'</tr>' +
|
||||
'<tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef</strong></td>' +
|
||||
'</tr>' +
|
||||
'<tr>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ab</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">cd</span></td>' +
|
||||
'<td class="o_selected_td"><span style="font-weight: bolder;">ef]</span></td>' +
|
||||
'<td class="o_selected_td"><strong>ab</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>cd</strong></td>' +
|
||||
'<td class="o_selected_td"><strong>ef]</strong></td>' +
|
||||
'</tr>' +
|
||||
'</tbody></table>',
|
||||
});
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { BasicEditor, testEditor } from '../utils.js';
|
||||
|
||||
const setFontSize = size => {
|
||||
return async editor => {
|
||||
await editor.execCommand('setFontSize', size);
|
||||
};
|
||||
};
|
||||
|
||||
describe('FontSize', () => {
|
||||
describe('setFontSize', () => {
|
||||
it('should change the font size of a few characters', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>ab[cde]fg</p>',
|
||||
stepFunction: setFontSize('10px'),
|
||||
contentAfter: '<p>ab<span style="font-size: 10px;">[cde]</span>fg</p>',
|
||||
});
|
||||
});
|
||||
it('should change the font size of a whole heading after a triple click', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<h1>[ab</h1><p>]cd</p>',
|
||||
stepFunction: setFontSize('36px'),
|
||||
contentAfter: '<h1><span style="font-size: 36px;">[ab]</span></h1><p>cd</p>',
|
||||
});
|
||||
});
|
||||
it('should get ready to type with a different font size', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>ab[]cd</p>',
|
||||
stepFunction: setFontSize('36px'),
|
||||
contentAfter: '<p>ab<span style="font-size: 36px;">[]\u200B</span>cd</p>',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isSelectionFormat } from '../../src/utils/utils.js';
|
||||
import { BasicEditor, testEditor, setTestSelection, Direction } from '../utils.js';
|
||||
import { applyInlineStyle } from '../../src/commands/commands.js';
|
||||
|
||||
const bold = async editor => {
|
||||
await editor.execCommand('bold');
|
||||
@@ -13,177 +13,273 @@ const underline = async editor => {
|
||||
const strikeThrough = async editor => {
|
||||
await editor.execCommand('strikeThrough');
|
||||
};
|
||||
const setFontSize = size => editor => editor.execCommand('setFontSize', size);
|
||||
|
||||
const switchDirection = editor => editor.execCommand('switchDirection');
|
||||
|
||||
describe('Format', () => {
|
||||
const b = (content, zws) => `<span${zws === 'first' ? ' data-oe-zws-empty-inline=""' : ''} style="font-weight: bolder;"${zws === 'last' ? ' data-oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
const notB = (content, weight, zws) => `<span${zws === 'first' ? ' data-oe-zws-empty-inline=""' : ''} style="font-weight: ${weight || 'normal'};"${zws === 'last' ? ' data-oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
const getZwsTag = (tagName, {style} = {}) => {
|
||||
const styleAttr = style ? ` style="${style}"` : '';
|
||||
return (content, zws) => {
|
||||
const zwsFirstAttr = zws === 'first' ? ' data-oe-zws-empty-inline=""' : '';
|
||||
const zwsLastAttr = zws === 'last' ? ' data-oe-zws-empty-inline=""' : '';
|
||||
return `<${tagName}${zwsFirstAttr}${styleAttr}${zwsLastAttr}>${content}</${tagName}>`
|
||||
};
|
||||
}
|
||||
|
||||
const span = getZwsTag('span');
|
||||
|
||||
const strong = getZwsTag('strong');
|
||||
const notStrong = getZwsTag('span', {style: 'font-weight: normal;'});
|
||||
const spanBold = getZwsTag('span', {style: 'font-weight: bolder;'});
|
||||
const b = getZwsTag('b');
|
||||
const repeatWithBoldTags = async (cb) => {
|
||||
for (const fn of [spanBold, b, strong]) {
|
||||
await cb(fn);
|
||||
}
|
||||
}
|
||||
|
||||
const em = getZwsTag('em');
|
||||
|
||||
const u = getZwsTag('u');
|
||||
|
||||
const s = getZwsTag('s');
|
||||
|
||||
|
||||
describe('bold', () => {
|
||||
it('should make a few characters bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>ab[cde]fg</p>',
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>ab${b(`[cde]`)}fg</p>`,
|
||||
contentAfter: `<p>ab${strong(`[cde]`)}fg</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a few characters not bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${b(`ab[cde]fg`)}</p>`,
|
||||
contentBefore: `<p>${strong(`ab[cde]fg`)}</p>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>${b(`ab${notB(`[cde]`)}fg`)}</p>`,
|
||||
contentAfter: `<p>${strong(`ab`)}[cde]${strong(`fg`)}</p>`,
|
||||
});
|
||||
});
|
||||
it('should make two paragraphs bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>[abc</p><p>def]</p>',
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>${b(`[abc`)}</p><p>${b(`def]`)}</p>`,
|
||||
contentAfter: `<p>${strong(`[abc`)}</p><p>${strong(`def]`)}</p>`,
|
||||
});
|
||||
});
|
||||
it('should make two paragraphs not bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${b(`[abc`)}</p><p>${b(`def]`)}</p>`,
|
||||
contentBefore: `<p>${strong(`[abc`)}</p><p>${strong(`def]`)}</p>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>${notB(`[abc`)}</p><p>${notB(`def]`, 400)}</p>`,
|
||||
contentAfter: `<p>[abc</p><p>def]</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a whole heading bold after a triple click', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<h1>${notB(`[ab`)}</h1><p>]cd</p>`,
|
||||
contentBefore: `<h1>${notStrong(`[ab`)}</h1><p>]cd</p>`,
|
||||
stepFunction: bold,
|
||||
// TODO: ideally should restore regular h1 without span instead.
|
||||
contentAfter: `<h1>${b(`[ab]`)}</h1><p>cd</p>`,
|
||||
contentAfter: `<h1>[ab]</h1><p>cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a whole heading not bold after a triple click (heading is considered bold)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<h1>[ab</h1><p>]cd</p>',
|
||||
stepFunction: bold,
|
||||
contentAfter: `<h1>${notB(`[ab]`)}</h1><p>cd</p>`,
|
||||
contentAfter: `<h1>${notStrong(`[ab]`)}</h1><p>cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a selection starting with bold text fully bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${b(`[ab`)}</p><p>c]d</p>`,
|
||||
contentBefore: `<p>${strong(`[ab`)}</p><p>c]d</p>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>${b(`[ab`)}</p><p>${b(`c]`)}d</p>`,
|
||||
contentAfter: `<p>${strong(`[ab`)}</p><p>${strong(`c]`)}d</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a selection with bold text in the middle fully bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>[a${b(`b`)}</p><p>${b(`c`)}d]e</p>`,
|
||||
contentBefore: `<p>[a${strong(`b`)}</p><p>${strong(`c`)}d]e</p>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>${b(`[ab`)}</p><p>${b(`cd]`)}e</p>`,
|
||||
contentAfter: `<p>${strong(`[ab`)}</p><p>${strong(`cd]`)}e</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a selection ending with bold text fully bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<h1>${notB(`[ab`)}</h1><p>${b(`c]d`)}</p>`,
|
||||
contentBefore: `<h1>${notStrong(`[ab`)}</h1><p>${strong(`c]d`)}</p>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<h1>${b(`[ab`)}</h1><p>${b(`c]d`)}</p>`,
|
||||
contentAfter: `<h1>[ab</h1><p>${strong(`c]d`)}</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to type in bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>ab[]cd</p>',
|
||||
stepFunction: bold,
|
||||
contentAfterEdit: `<p>ab${b(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${strong(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to type in not bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${b(`ab[]cd`)}</p>`,
|
||||
contentBefore: `<p>${strong(`ab[]cd`)}</p>`,
|
||||
stepFunction: bold,
|
||||
contentAfterEdit: `<p>${b(`ab${notB(`[]\u200B`, undefined, 'first')}cd`)}</p>`,
|
||||
contentAfter: `<p>${b(`ab[]cd`)}</p>`,
|
||||
contentAfterEdit: `<p>${strong(`ab`)}${span(`[]\u200B`, 'first')}${strong(`cd`)}</p>`,
|
||||
contentAfter: `<p>${strong(`ab[]cd`)}</p>`,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should remove a bold tag that was redondant while performing the command', async () => {
|
||||
await repeatWithBoldTags(async (tag) => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>a${tag(`b${tag(`[c]`)}d`)}e</p>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>a${tag('b')}[c]${tag('d')}e</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should remove a bold tag that was redondant with different tags while performing the command', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>a${spanBold(`b${strong(`c${b(`[d]`)}e`)}f`)}g</p>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>a${spanBold(`bc`)}[d]${spanBold(`ef`)}g</p>`,
|
||||
});
|
||||
});
|
||||
|
||||
describe('inside container or inline with class already bold', () => {
|
||||
it('should force the font-weight to normal with an inline with class', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
styleContent: `.boldClass { font-weight: bold; }`,
|
||||
contentBefore: `<div>a<span class="boldClass">[b]</span>c</div>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<div>a<span class="boldClass"><span style="font-weight: normal;">[b]</span></span>c</div>`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should force the font-weight to normal', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<h1>a[b]c</h1>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<h1>a<span style="font-weight: normal;">[b]</span>c</h1>`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should force the font-weight to normal while removing redundant tag', async () => {
|
||||
await repeatWithBoldTags(async (tag) => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<h1>a${tag('[b]')}c</h1>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<h1>a<span style="font-weight: normal;">[b]</span>c</h1>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('inside container font-weight: 500 and strong being strong-weight: 500', () => {
|
||||
it('should remove the redundant strong style and add span with a bolder font-weight', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
styleContent: `h1, strong {font-weight: 500;}`,
|
||||
contentBefore: `<h1>a${strong(`[b]`)}c</h1>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<h1>a<span style="font-weight: bolder;">[b]</span>c</h1>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
const i = (content, zws) => `<span${zws === 'first' ? ' data-oe-zws-empty-inline=""' : ''} style="font-style: italic;"${zws === 'last' ? ' data-oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
const notI = (content, zws) => `<span${zws === 'first' ? ' data-oe-zws-empty-inline=""' : ''} style="font-style: normal;"${zws === 'last' ? ' data-oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
describe('bold and italic inside container already bold', () => {
|
||||
it('should remove the redundant strong style and add span with a bolder font-weight', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
styleContent: `h1 {font-weight: bold;}`,
|
||||
contentBefore: `<h1>a[b]c</h1>`,
|
||||
stepFunction: (editor) => {
|
||||
editor.execCommand('italic');
|
||||
editor.execCommand('bold');
|
||||
editor.execCommand('italic');
|
||||
},
|
||||
contentAfter: `<h1>a<span style="font-weight: normal;">[b]</span>c</h1>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('italic', () => {
|
||||
it('should make a few characters italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[cde]fg</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfter: `<p>ab${i(`[cde]`)}fg</p>`,
|
||||
contentAfter: `<p>ab${em(`[cde]`)}fg</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a few characters not italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${i(`ab[cde]fg`)}</p>`,
|
||||
contentBefore: `<p>${em(`ab[cde]fg`)}</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfter: `<p>${i(`ab${notI(`[cde]`)}fg`)}</p>`,
|
||||
contentAfter: `<p>${em(`ab`)}[cde]${em(`fg`)}</p>`,
|
||||
});
|
||||
});
|
||||
it('should make two paragraphs italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>[abc</p><p>def]</p>',
|
||||
stepFunction: italic,
|
||||
contentAfter: `<p>${i(`[abc`)}</p><p>${i(`def]`)}</p>`,
|
||||
contentAfter: `<p>${em(`[abc`)}</p><p>${em(`def]`)}</p>`,
|
||||
});
|
||||
});
|
||||
it('should make two paragraphs not italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${i(`[abc`)}</p><p>${i(`def]`)}</p>`,
|
||||
contentBefore: `<p>${em(`[abc`)}</p><p>${em(`def]`)}</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfter: `<p>${notI(`[abc`)}</p><p>${notI(`def]`)}</p>`,
|
||||
contentAfter: `<p>[abc</p><p>def]</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a whole heading italic after a triple click', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<h1>[ab</h1><p>]cd</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfter: `<h1>${i(`[ab]`)}</h1><p>cd</p>`,
|
||||
contentAfter: `<h1>${em(`[ab]`)}</h1><p>cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a whole heading not italic after a triple click', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<h1>${i(`[ab`)}</h1><p>]cd</p>`,
|
||||
contentBefore: `<h1>${em(`[ab`)}</h1><p>]cd</p>`,
|
||||
stepFunction: italic,
|
||||
// TODO: ideally should restore regular h1 without span instead.
|
||||
contentAfter: `<h1>${notI(`[ab]`)}</h1><p>cd</p>`,
|
||||
contentAfter: `<h1>[ab]</h1><p>cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a selection starting with italic text fully italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${i(`[ab`)}</p><p>c]d</p>`,
|
||||
contentBefore: `<p>${em(`[ab`)}</p><p>c]d</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfter: `<p>${i(`[ab`)}</p><p>${i(`c]`)}d</p>`,
|
||||
contentAfter: `<p>${em(`[ab`)}</p><p>${em(`c]`)}d</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a selection with italic text in the middle fully italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>[a${i(`b`)}</p><p>${i(`c`)}d]e</p>`,
|
||||
contentBefore: `<p>[a${em(`b`)}</p><p>${em(`c`)}d]e</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfter: `<p>${i(`[ab`)}</p><p>${i(`cd]`)}e</p>`,
|
||||
contentAfter: `<p>${em(`[ab`)}</p><p>${em(`cd]`)}e</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a selection ending with italic text fully italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>[ab</p><p>${i(`c]d`)}</p>`,
|
||||
contentBefore: `<p>[ab</p><p>${em(`c]d`)}</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfter: `<p>${i(`[ab`)}</p><p>${i(`c]d`)}</p>`,
|
||||
contentAfter: `<p>${em(`[ab`)}</p><p>${em(`c]d`)}</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to type in italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfterEdit: `<p>ab${i(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${em(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to type in not italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${i(`ab[]cd`)}</p>`,
|
||||
contentBefore: `<p>${em(`ab[]cd`)}</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfterEdit: `<p>${i(`ab${notI(`[]\u200B`, 'first')}cd`)}</p>`,
|
||||
contentAfter: `<p>${i(`ab[]cd`)}</p>`,
|
||||
contentAfterEdit: `<p>${em(`ab`)}${span(`[]\u200B`, 'first')}${em(`cd`)}</p>`,
|
||||
contentAfter: `<p>${em(`ab[]cd`)}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
const u = (content, zws) => `<span${zws === 'first' ? ' data-oe-zws-empty-inline=""' : ''} style="text-decoration-line: underline;"${zws === 'last' ? ' data-oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
describe('underline', () => {
|
||||
it('should make a few characters underline', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
@@ -196,7 +292,7 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${u(`ab[cde]fg`)}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>${u(`ab[`)}cde]${u(`fg`)}</p>`,
|
||||
contentAfter: `<p>${u(`ab`)}[cde]${u(`fg`)}</p>`,
|
||||
});
|
||||
});
|
||||
it('should make two paragraphs underline', async () => {
|
||||
@@ -260,12 +356,11 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${u(`ab[]cd`)}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>${u(`ab`)}<span data-oe-zws-empty-inline="">\u200B[]</span>${u(`cd`)}</p>`,
|
||||
contentAfter: `<p>${u(`ab`)}[]${u(`cd`)}</p>`,
|
||||
contentAfterEdit: `<p>${u(`ab`)}${span(`[]\u200B`, 'first')}${u(`cd`)}</p>`,
|
||||
contentAfter: `<p>${u(`ab[]cd`)}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
const s = (content, zws) => `<span${zws === 'first' ? ' data-oe-zws-empty-inline=""' : ''} style="text-decoration-line: line-through;"${zws === 'last' ? ' data-oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
describe('strikeThrough', () => {
|
||||
it('should make a few characters strikeThrough', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
@@ -278,7 +373,7 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${s(`ab[cde]fg`)}</p>`,
|
||||
stepFunction: strikeThrough,
|
||||
contentAfter: `<p>${s(`ab[`)}cde]${s(`fg`)}</p>`,
|
||||
contentAfter: `<p>${s(`ab`)}[cde]${s(`fg`)}</p>`,
|
||||
});
|
||||
});
|
||||
it('should make a few characters strikeThrough then remove style inside', async () => {
|
||||
@@ -286,7 +381,7 @@ describe('Format', () => {
|
||||
contentBefore: `<p>ab[c d]ef</p>`,
|
||||
stepFunction: async editor => {
|
||||
await strikeThrough(editor);
|
||||
const styleSpan = editor.editable.querySelector('span[style="text-decoration-line: line-through;"]').childNodes[0];
|
||||
const styleSpan = editor.editable.querySelector('s').childNodes[0];
|
||||
const selection = {
|
||||
anchorNode: styleSpan,
|
||||
anchorOffset: 1,
|
||||
@@ -297,7 +392,7 @@ describe('Format', () => {
|
||||
await setTestSelection(selection);
|
||||
await strikeThrough(editor);
|
||||
},
|
||||
contentAfter: `<p>ab${s(`c[`)} ]${s(`d`)}ef</p>`,
|
||||
contentAfter: `<p>ab${s(`c`)}[ ]${s(`d`)}ef</p>`,
|
||||
});
|
||||
});
|
||||
it('should make strikeThrough then more then remove', async () => {
|
||||
@@ -398,18 +493,19 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${s(`ab[]cd`)}</p>`,
|
||||
stepFunction: strikeThrough,
|
||||
contentAfterEdit: `<p>${s(`ab`)}<span data-oe-zws-empty-inline="">\u200B[]</span>${s(`cd`)}</p>`,
|
||||
contentAfter: `<p>${s(`ab`)}[]${s(`cd`)}</p>`,
|
||||
contentAfterEdit: `<p>${s(`ab`)}${span(`[]\u200B`, 'first')}${s(`cd`)}</p>`,
|
||||
contentAfter: `<p>${s(`ab[]cd`)}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('underline + strikeThrough', () => {
|
||||
it('should get ready to write in strikeThrough without underline (underline was first)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(s(`cd[]ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`\u200b[]`, 'last')}${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}[]${u(s(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`[]\u200b`, 'last')}${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd[]ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should restore underline after removing it (collapsed, strikeThrough)', async () => {
|
||||
@@ -417,22 +513,22 @@ describe('Format', () => {
|
||||
contentBefore: `<p>ab${u(s(`cd`))}${s(`\u200b[]`, 'first')}${u(s(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(u(`[]\u200b`, 'first'), 'first')}${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}[]${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd[]ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline after restoring it after removing it (collapsed, strikeThrough)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(s(`cd`))}${s(u(`[]\u200b`, 'first'))}${u(s(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`\u200b[]`, 'last')}${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}[]${u(s(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`[]\u200b`, 'last')}${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd[]ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline after restoring it and writing after removing it (collapsed, strikeThrough)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(s(`cd`))}${s(u(`ghi[]`))}${u(s(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(u(`ghi`) + `<span data-oe-zws-empty-inline="">\u200b[]</span>`)}${u(s(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`${u(`ghi`)}${span(`[]\u200b`, 'first')}`)}${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}${s(u(`ghi`) + `[]`)}${u(s(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
@@ -452,9 +548,15 @@ describe('Format', () => {
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`A${u(`B`)}${uselessSpan(`C[]`)}${uselessU}`)}${u(s(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove only underline decoration on a span', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p><span style="text-decoration: underline line-through;">[a]</span></p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p><span style="text-decoration: line-through;">[a]</span></p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('underline + italic', () => {
|
||||
const iAndU = (content, zws) => `<span${zws === 'first' ? ' data-oe-zws-empty-inline=""' : ''} style="font-style: italic; text-decoration-line: underline;"${zws === 'last' ? ' data-oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
it('should get ready to write in italic and underline', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
@@ -462,11 +564,11 @@ describe('Format', () => {
|
||||
await editor.execCommand('italic');
|
||||
await editor.execCommand('underline');
|
||||
},
|
||||
contentAfterEdit: `<p>ab${iAndU(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${em(u(`[]\u200B`, 'first'), 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to write in italic, after changing one\'s mind about underline', async () => {
|
||||
it('should get ready to write in italic, after changing one\'s mind about underline (two consecutive at the end)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
stepFunction: async editor => {
|
||||
@@ -474,9 +576,11 @@ describe('Format', () => {
|
||||
await editor.execCommand('underline');
|
||||
await editor.execCommand('underline');
|
||||
},
|
||||
contentAfterEdit: `<p>ab${i(`\u200B[]`, 'first')}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${em(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to write in italic, after changing one\'s mind about underline (separated by italic)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
stepFunction: async editor => {
|
||||
@@ -484,9 +588,11 @@ describe('Format', () => {
|
||||
await editor.execCommand('italic');
|
||||
await editor.execCommand('underline');
|
||||
},
|
||||
contentAfterEdit: `<p>ab${i(`\u200B[]`, 'first')}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${em(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to write in italic, after changing one\'s mind about underline (two consecutive at the beginning)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
stepFunction: async editor => {
|
||||
@@ -494,47 +600,47 @@ describe('Format', () => {
|
||||
await editor.execCommand('underline');
|
||||
await editor.execCommand('italic');
|
||||
},
|
||||
contentAfterEdit: `<p>ab${i(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${em(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to write in italic without underline (underline was first)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(i(`cd[]ef`))}</p>`,
|
||||
contentBefore: `<p>ab${u(em(`cd[]ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>ab${u(i(`cd`))}${i(`\u200b[]`, 'last')}${u(i(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}[]${u(i(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(em(`cd`))}${em(`[]\u200b`, 'last')}${u(em(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(em(`cd[]ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should restore underline after removing it (collapsed, italic)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(i(`cd`))}${i(`\u200b[]`)}${u(i(`ef`))}</p>`,
|
||||
contentBefore: `<p>ab${u(em(`cd`))}${em(`[]\u200b`)}${u(em(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>ab${u(i(`cd`))}${iAndU(`[]\u200b`, 'last')}${u(i(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}[]${u(i(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(em(`cd`))}${em(u(`[]\u200b`, 'last'))}${u(em(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(em(`cd`))}${em(`[]`)}${u(em(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline after restoring it after removing it (collapsed, italic)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(i(`cd`))}${i(u(`[]\u200b`))}${u(i(`ef`))}</p>`,
|
||||
contentBefore: `<p>ab${u(em(`cd`))}${em(u(`[]\u200b`))}${u(em(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>ab${u(i(`cd`))}${i(`\u200b[]`, 'last')}${u(i(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}[]${u(i(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(em(`cd`))}${em(`[]\u200b`, 'last')}${u(em(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(em(`cd[]ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline after restoring it and writing after removing it (collapsed, italic)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(i(`cd`))}${i(u(`ghi[]`))}${u(i(`ef`))}</p>`,
|
||||
contentBefore: `<p>ab${u(em(`cd`))}${em(u(`ghi[]`))}${u(em(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>ab${u(i(`cd`))}${i(u(`ghi`) + `<span data-oe-zws-empty-inline="">\u200b[]</span>`)}${u(i(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}${i(u(`ghi`) + `[]`)}${u(i(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(em(`cd`))}${em(u(`ghi`) + `<span data-oe-zws-empty-inline="">[]\u200b</span>`)}${u(em(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(em(`cd`))}${em(u(`ghi`) + `[]`)}${u(em(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline, write, restore underline, write, remove underline again, write (collapsed, italic)', async () => {
|
||||
const uselessSpan = content => `<span>${content}</span>`;
|
||||
const uselessU = u(''); // TODO: clean
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(i(`cd[]ef`))}</p>`,
|
||||
contentBefore: `<p>ab${u(em(`cd[]ef`))}</p>`,
|
||||
stepFunction: async editor => {
|
||||
await editor.execCommand('underline');
|
||||
await editor.execCommand('insert', 'A');
|
||||
@@ -543,18 +649,84 @@ describe('Format', () => {
|
||||
await editor.execCommand('underline');
|
||||
await editor.execCommand('insert', 'C');
|
||||
},
|
||||
contentAfter: `<p>ab${u(i(`cd`))}${i(`A${u(`B`)}${uselessSpan(`C[]`)}${uselessU}`)}${u(i(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(em(`cd`))}${em(`A${u(`B`)}${uselessSpan(`C[]`)}${uselessU}`)}${u(em(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyInlineStyle', () => {
|
||||
it('should apply style to selection only', async () => {
|
||||
describe('setFontSize', () => {
|
||||
it('should change the font size of a few characters', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>ab[cde]fg</p>',
|
||||
stepFunction: setFontSize('10px'),
|
||||
contentAfter: '<p>ab<span style="font-size: 10px;">[cde]</span>fg</p>',
|
||||
});
|
||||
});
|
||||
it('should change the font size of a whole heading after a triple click', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<h1>[ab</h1><p>]cd</p>',
|
||||
stepFunction: setFontSize('36px'),
|
||||
contentAfter: '<h1><span style="font-size: 36px;">[ab]</span></h1><p>cd</p>',
|
||||
});
|
||||
});
|
||||
it('should get ready to type with a different font size', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>ab[]cd</p>',
|
||||
stepFunction: setFontSize('36px'),
|
||||
contentAfterEdit: `<p>ab<span data-oe-zws-empty-inline="" style="font-size: 36px;">[]\u200B</span>cd</p>`,
|
||||
contentAfter: '<p>ab[]cd</p>',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should add style to a span parent of an inline', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>a<span>[b<span>c]d</span>e</span>f</p>',
|
||||
stepFunction: editor => applyInlineStyle(editor, (el) => el.style.color = 'tomato'),
|
||||
contentAfter: '<p>a<span><span style="color: tomato;">[b</span><span><span style="color: tomato;">c]</span>d</span>e</span>f</p>',
|
||||
contentBefore: `<p>a<span style="background-color: black;">${strong(`[bc]`)}</span>d</p>`,
|
||||
stepFunction: setFontSize('10px'),
|
||||
contentAfter: `<p>a<span style="background-color: black; font-size: 10px;">${strong(`[bc]`)}</span>d</p>`,
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSelectionFormat', () => {
|
||||
it('return false for isSelectionFormat when partially selecting 2 text node, the anchor is formated and focus is not formated', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${strong(`a[b`)}</p><p>c]d</p>`,
|
||||
stepFunction: (editor) => {
|
||||
window.chai.expect(isSelectionFormat(editor.editable, 'bold')).to.be.equal(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
it('return false for isSelectionFormat when partially selecting 2 text node, the anchor is not formated and focus is formated', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${strong(`a]b`)}</p><p>c[d</p>`,
|
||||
stepFunction: (editor) => {
|
||||
window.chai.expect(isSelectionFormat(editor.editable, 'bold')).to.be.equal(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
it('return false for isSelectionFormat when selecting 3 text node, the anchor and focus not formated and the text node in between formated', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>a[b</p><p>${strong(`c`)}</p><p>d]e</p>`,
|
||||
stepFunction: (editor) => {
|
||||
window.chai.expect(isSelectionFormat(editor.editable, 'bold')).to.be.equal(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('switchDirection', () => {
|
||||
it('should switch direction on a collapsed range', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>a[]b</p>`,
|
||||
stepFunction: switchDirection,
|
||||
contentAfter: `<p dir="rtl">a[]b</p>`,
|
||||
});
|
||||
});
|
||||
it('should switch direction on an uncollapsed range', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>a[b]c</p>`,
|
||||
stepFunction: switchDirection,
|
||||
contentAfter: `<p dir="rtl">a[b]c</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1549,13 +1549,10 @@ describe('Utils', () => {
|
||||
splitTextNode(cd, 1);
|
||||
const d = cd;
|
||||
const result = splitAroundUntil(d, p.childNodes[1]);
|
||||
window.chai.expect(result.every(node => node.tagName === 'FONT')).to.be.ok;
|
||||
window.chai.expect(result.tagName === 'FONT').to.be.ok;
|
||||
window.chai.expect(p.outerHTML).to.eql(
|
||||
'<p>a<font>b<span>c</span></font><font><span>d</span></font><font><span>e</span>f</font>g</p>'
|
||||
);
|
||||
window.chai.expect(result[0]).to.eql(p.childNodes[2]);
|
||||
// "before" split happened so "after" split is not in the DOM anymore:
|
||||
window.chai.expect(result[1].parentElement).to.eql(null);
|
||||
});
|
||||
it('should split a slice of text from its inline ancestry', () => {
|
||||
const [p] = insertTestHtml('<p>a<font>b<span>cdefg</span>h</font>i</p>');
|
||||
@@ -1570,21 +1567,27 @@ describe('Utils', () => {
|
||||
splitTextNode(cd, 1);
|
||||
const d = cd;
|
||||
const result = splitAroundUntil([d, d.nextSibling.nextSibling], p.childNodes[1]);
|
||||
window.chai.expect(result.every(node => node.tagName === 'FONT')).to.be.ok;
|
||||
window.chai.expect(result.tagName === 'FONT').to.be.ok;
|
||||
window.chai.expect(p.outerHTML).to.eql(
|
||||
'<p>a<font>b<span>c</span></font><font><span>def</span></font><font><span>g</span>h</font>i</p>'
|
||||
);
|
||||
window.chai.expect(result[0]).to.eql(p.childNodes[2]);
|
||||
// "before" split happened so "after" split is not in the DOM anymore:
|
||||
window.chai.expect(result[1].parentElement).to.eql(null);
|
||||
});
|
||||
it('should split from a textNode that has no siblings', () => {
|
||||
const [p] = insertTestHtml('<p>a<font>b<span>cde</span>f</font>g</p>');
|
||||
const font = p.querySelector('font');
|
||||
const cde = p.querySelector('span').firstChild;
|
||||
const result = splitAroundUntil(cde, font);
|
||||
window.chai.expect(result.tagName === 'FONT' && result !== font).to.be.ok;
|
||||
window.chai.expect(p.outerHTML).to.eql('<p>a<font>b</font><font><span>cde</span></font><font>f</font>g</p>');
|
||||
});
|
||||
it('should not do anything (nothing to split)', () => {
|
||||
const [p] = insertTestHtml('<p>a<font>b<span>cde</span>f</font>g</p>');
|
||||
const cde = p.childNodes[1].childNodes[1].firstChild;
|
||||
const result = splitAroundUntil(cde, p.childNodes[1]);
|
||||
window.chai.expect(result.every(node => node === undefined)).to.be.ok;
|
||||
window.chai.expect(p.outerHTML).to.eql('<p>a<font>b<span>cde</span>f</font>g</p>');
|
||||
const [p] = insertTestHtml('<p>a<font><span>bcd</span></font>e</p>');
|
||||
const bcd = p.querySelector('span').firstChild;
|
||||
const result = splitAroundUntil(bcd, p.childNodes[1]);
|
||||
window.chai.expect(result === p.childNodes[1]).to.be.ok;
|
||||
window.chai.expect(p.outerHTML).to.eql('<p>a<font><span>bcd</span></font>e</p>');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('CleanUp Html', () => {
|
||||
|
||||
@@ -277,6 +277,12 @@ export async function testEditor(Editor = OdooEditor, spec, options = {}) {
|
||||
const testNode = document.createElement('div');
|
||||
document.querySelector('#editor-test-container').innerHTML = '';
|
||||
document.querySelector('#editor-test-container').appendChild(testNode);
|
||||
let styleTag;
|
||||
if (spec.styleContent) {
|
||||
styleTag = document.createElement('style');
|
||||
styleTag.textContent = spec.styleContent;
|
||||
document.querySelector('#editor-test-container').appendChild(styleTag);
|
||||
}
|
||||
|
||||
// Add the content to edit and remove the "[]" markers *before* initializing
|
||||
// the editor as otherwise those would genererate mutations the editor would
|
||||
|
||||
Reference in New Issue
Block a user