From 618d5bc3ca8dd354f86d34e8f49cbf9a8b526c6f Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Mon, 11 Apr 2022 14:40:02 +0000 Subject: [PATCH] [FIX] web_editor: provide proper support for mixed language directions This introduces a "direction" option on the editor to set the text direction of the editor (passed by Odoo based on the localization), and a Powerbox command to switch said direction on a given block. This allows users to mix several text directions within the same text, eg. when quoting Hebrew in an English text. task-2814004 X-original-commit: 693df325588ccaf6385c9f610709f687ccdf61a8 Part-of: odoo/odoo#90247 --- .../static/lib/odoo-editor/src/OdooEditor.js | 13 ++++++- .../lib/odoo-editor/src/commands/commands.js | 35 +++++++++++++++++-- .../static/lib/odoo-editor/src/utils/utils.js | 22 ++++++++++-- .../static/src/js/wysiwyg/wysiwyg.js | 2 ++ 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js index 5e49780d208..a3eb46ded77 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js @@ -208,6 +208,7 @@ export class OdooEditor extends EventTarget { }, isHintBlacklisted: () => false, filterMutationRecords: (records) => records, + direction: 'ltr', _t: string => string, allowCommandVideo: true, }, @@ -271,6 +272,7 @@ export class OdooEditor extends EventTarget { this._idToNodeMap.set(1, editable); this.editable = this.options.toSanitize ? sanitize(editable) : editable; this.editable.classList.add("odoo-editor-editable"); + this.editable.setAttribute('dir', this.options.direction); // Set contenteditable before clone as FF updates the content at this point. this._activateContenteditable(); @@ -1839,6 +1841,15 @@ export class OdooEditor extends EventTarget { this.commandbarTablePicker.show(); }, }, + { + groupName: 'Basic blocks', + title: this.options._t('Switch direction'), + description: this.options._t('Switch the text\'s direction.'), + fontawesome: 'fa-exchange', + callback: () => { + this.execCommand('switchDirection'); + }, + }, { groupName: 'Widgets', title: '3 Stars', @@ -1969,7 +1980,7 @@ export class OdooEditor extends EventTarget { const selectionStartStyle = getComputedStyle(closestStartContainer); // queryCommandState does not take stylesheets into account - for (const format of ['bold', 'italic', 'underline', 'strikeThrough']) { + for (const format of ['bold', 'italic', 'underline', 'strikeThrough', 'switchDirection']) { const formatButton = this.toolbar.querySelector(`#${format.toLowerCase()}`); if (formatButton) { formatButton.classList.toggle('active', isSelectionFormat(this.editable, format)); diff --git a/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js b/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js index a8a279c218c..eca2cc4c0da 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js +++ b/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js @@ -273,6 +273,7 @@ function hasColor(element, mode) { * 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) { getDeepRange(editor.editable, { splitText: true, select: true }); @@ -312,6 +313,7 @@ export function applyInlineStyle(editor, applyStyle, style, shouldApply=true) { } 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. @@ -344,6 +346,7 @@ export function applyInlineStyle(editor, applyStyle, style, shouldApply=true) { newParent.appendChild(textNode); } applyStyle(textNode.parentElement); + changedElements.push(textNode.parentElement) } if (selectedTextNodes[0] && selectedTextNodes[0].textContent === '\u200B') { setSelection(selectedTextNodes[0], 0); @@ -356,6 +359,7 @@ export function applyInlineStyle(editor, applyStyle, style, shouldApply=true) { setSelection(lastNode, lastNode.length, firstNode, 0); } } + return changedElements; } const styles = { bold: { @@ -377,7 +381,10 @@ const styles = { is: editable => isSelectionFormat(editable, 'strikeThrough'), name: 'textDecorationLine', value: 'line-through', - } + }, + switchDirection: { + is: editable => isSelectionFormat(editable, 'switchDirection'), + }, } export function toggleFormat(editor, format) { const selection = editor.document.getSelection(); @@ -398,6 +405,7 @@ export function toggleFormat(editor, format) { const selectedTextNodes = getSelectedNodes(editor.editable) .filter(n => n.nodeType === Node.TEXT_NODE && n.nodeValue.trim().length); const isAlreadyFormatted = style.is(editor.editable); + let changedElements = []; if (isAlreadyFormatted && style.name === 'textDecorationLine') { const decoratedPairs = new Set(selectedTextNodes.map(n => [closestElement(n, `[style*="text-decoration-line: ${style.value}"]`), n])); for (const [closestDecorated, textNode] of decoratedPairs) { @@ -407,6 +415,7 @@ export function toggleFormat(editor, format) { if (!decorationToRemove.style.cssText) { for (const child of decorationToRemove.childNodes) { decorationToRemove.before(child); + changedElements.push(child); } decorationToRemove.remove(); } @@ -429,8 +438,18 @@ export function toggleFormat(editor, format) { } 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 { - applyInlineStyle(editor, el => { + changedElements = applyInlineStyle(editor, el => { if (isAlreadyFormatted) { const block = closestBlock(el); el.style[style.name] = style.is(block) ? 'normal' : getComputedStyle(block)[style.name]; @@ -464,6 +483,7 @@ export function toggleFormat(editor, format) { } }, format, !isAlreadyFormatted); } + return changedElements; } function addColumn(editor, beforeOrAfter) { getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding td. @@ -562,6 +582,17 @@ export const editorCommands = { italic: editor => toggleFormat(editor, 'italic'), underline: editor => toggleFormat(editor, 'underline'), strikeThrough: editor => toggleFormat(editor, 'strikeThrough'), + switchDirection: editor => { + const changedElements = toggleFormat(editor, 'switchDirection'); + for (const element of changedElements) { + const style = getComputedStyle(element); + if (style.direction === 'ltr' && style.textAlign === 'right') { + element.style.setProperty('text-align', 'left'); + } else if (style.direction === 'rtl' && style.textAlign === 'left') { + element.style.setProperty('text-align', 'right'); + } + } + }, removeFormat: editor => { editor.document.execCommand('removeFormat'); for (const node of getTraversedNodes(editor.editable)) { diff --git a/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js b/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js index 61ec4c9372e..1e1db7fd575 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js +++ b/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js @@ -908,11 +908,27 @@ export function isStrikeThrough(node) { } return false; } +/** + * Return true if the given node appears in a different direction than that of + * the editable ('ltr' or 'rtl'). + * + * Note: The direction of the editable is set on its "dir" attribute, to the + * value of the "direction" option on instantiation of the editor. + * + * @param {Node} node + * @param {Element} editable + * @returns {boolean} + */ + export function isDirectionSwitched(node, editable) { + 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 @@ -920,16 +936,16 @@ export const isFormat = { * node in it appears as that format. * * @param {Element} editable - * @param {String} format 'bold'|'italic'|'underline'|'strikeThrought' + * @param {String} format 'bold'|'italic'|'underline'|'strikeThrough'|'switchDirection' * @returns {boolean} */ export function isSelectionFormat(editable, format) { const selectedText = getSelectedNodes(editable) .filter(n => n.nodeType === Node.TEXT_NODE && n.nodeValue.trim().length); if (selectedText.length) { - return selectedText.every(n => isFormat[format](n.parentElement)); + return selectedText.every(n => isFormat[format](n.parentElement, editable)) } else { - return isFormat[format](closestElement(editable.ownerDocument.getSelection().anchorNode)); + return isFormat[format](closestElement(editable.ownerDocument.getSelection().anchorNode), editable); } } diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index b906c16261f..0044ef5345f 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -12,6 +12,7 @@ const {ColorpickerWidget} = require('web.Colorpicker'); const concurrency = require('web.concurrency'); const { device } = require('web.config'); const weContext = require('web_editor.context'); +const { localization } = require('@web/core/l10n/localization'); const OdooEditorLib = require('@web_editor/../lib/odoo-editor/src/OdooEditor'); const snippetsEditor = require('web_editor.snippet.editor'); const Toolbar = require('web_editor.toolbar'); @@ -164,6 +165,7 @@ const Wysiwyg = Widget.extend({ }, commands: commands, plugins: options.editorPlugins, + direction: localization.direction || 'ltr', }, editorCollaborationOptions)); this.odooEditor.addEventListener('contentChanged', function () {