[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:
Nicolas Bayet
2022-09-20 13:48:38 +02:00
parent 3645e25f36
commit 3accb27494
10 changed files with 643 additions and 493 deletions
@@ -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