diff --git a/addons/website/static/src/interactions/text_highlights.js b/addons/website/static/src/interactions/text_highlights.js new file mode 100644 index 0000000000000..7a0ddba540b44 --- /dev/null +++ b/addons/website/static/src/interactions/text_highlights.js @@ -0,0 +1,128 @@ +import { Interaction } from "@website/core/interaction"; +import { registry } from "@web/core/registry"; +import { + applyTextHighlight, + removeTextHighlight, + switchTextHighlight, +} from "@website/js/text_processing"; + +class TextHighlight extends Interaction { + + static selector = '#wrapwrap' + + setup() { + this.observerLock = new Map(); + this.resizeObserver = new window.ResizeObserver(entries => { + if (this.isDestroyed) { + return; + } + window.requestAnimationFrame(() => { + const textHighlightEls = new Set(); + entries.forEach(entry => { + const target = entry.target; + if (this.observerLock.get(target)) { + return this.observerLock.set(target, false); + } + const topTextEl = target.closest(".o_text_highlight"); + for (const el of topTextEl ? [topTextEl] + : target.querySelectorAll(":scope .o_text_highlight") + ) { + textHighlightEls.add(el); + } + }); + textHighlightEls.forEach(textHighlightEl => { + for (const textHighlightItemEl of this.getTextHighlightItems(textHighlightEl)) { + this.resizeObserver.unobserve(textHighlightItemEl); + } + switchTextHighlight(textHighlightEl); + }); + }); + }); + this.el.addEventListener("text_highlight_added", this.onTextHighlightAdded.bind(this)); + this.el.addEventListener("text_highlight_remove", this.onTextHighlightRemoved.bind(this)); + for (const textEl of this.el.querySelectorAll(".o_text_highlight")) { + applyTextHighlight(textEl); + } + } + + destroy() { + this.el.removeEventListener("text_highlight_added"); + this.el.removeEventListener("text_highlight_remove"); + for (const textHighlightEl of this.el.querySelectorAll(".o_text_highlight")) { + removeTextHighlight(textHighlightEl); + } + } + + /** + * @param {HTMLElement} el + */ + closestToObserve(el) { + if (el === this.el || !el) { + return null; + } + if (window.getComputedStyle(el).display !== "inline") { + return el; + } + return this.closestToObserve(el.parentElement); + } + + /** + * @param {HTMLElement} el + */ + getTextHighlightItems(el = this.el) { + return el.querySelectorAll(":scope .o_text_highlight_item"); + } + + /** + * @param {HTMLElement} topTextEl + */ + getObservedEls(topTextEl) { + const closestToObserve = this.closestToObserve(topTextEl); + return [ + ...(closestToObserve ? [closestToObserve] : []), + ...this.getTextHighlightItems(topTextEl), + ] + } + + /** + * @param {HTMLElement} topTextEl + * be observed. + */ + observeTextHighlightResize(topTextEl) { + // The `ResizeObserver` cannot detect the width change on highlight + // units (`.o_text_highlight_item`) as long as the width of the entire + // `.o_text_highlight` element remains the same, so we need to observe + // each one of them and do the adjustment only once for the whole text. + for (const highlightItemEl of this.getObservedEls(topTextEl)) { + this.resizeObserver.observe(highlightItemEl); + } + } + + /** + * @param {HTMLElement} topTextEl + */ + lockTextHighlightObserver(topTextEl) { + for (const targetEl of this.getObservedEls(topTextEl)) { + this.observerLock.set(targetEl, true); + } + } + + /** + * @param {Event} ev + */ + onTextHighlightAdded(ev) { + this.lockTextHighlightObserver(ev.target); + this.observeTextHighlightResize(ev.target); + } + + /** + * @param {Event} ev + */ + onTextHighlightRemoved(ev) { + for (const highlightItemEl of this.getTextHighlightItems(ev.target)) { + this.observerLock.delete(highlightItemEl); + } + } +} + +registry.category("website.active_elements").add("website.text_highlight", TextHighlight); diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index d749b58c0820a..6af6ca7817928 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -12,14 +12,8 @@ import publicWidget from "@web/legacy/js/public/public_widget"; import { renderToElement } from "@web/core/utils/render"; import { hasTouch } from "@web/core/browser/feature_detection"; import { SIZES, utils as uiUtils } from "@web/core/ui/ui_service"; -import { - applyTextHighlight, - removeTextHighlight, - switchTextHighlight, -} from "@website/js/text_processing"; import { touching } from "@web/core/utils/ui"; import { ObservingCookieWidgetMixin } from "@website/snippets/observing_cookie_mixin"; -import { scrollTo } from "@web_editor/js/common/scrolling"; // Initialize fallbacks for the use of requestAnimationFrame, // cancelAnimationFrame and performance.now() @@ -1784,173 +1778,6 @@ registry.ImageShapeHoverEffet = publicWidget.Widget.extend({ }, }); -registry.TextHighlight = publicWidget.Widget.extend({ - selector: '#wrapwrap', - disabledInEditableMode: false, - - /** - * @override - */ - async start() { - // We need to adapt the text highlights on resize (E.g. custom fonts - // loading, layout option changes, window resized...), mainly to take in - // consideration the rendered line breaks in text nodes... But after - // every adjustment, the `ResizeObserver` will unfortunately immediately - // notify a size change once new highlight items are observed leading to - // an infinite loop. To avoid that, we use a lock map (`observerLock`) - // to block the callback on this first notification for observed items. - this.observerLock = new Map(); - this.resizeObserver = new window.ResizeObserver(entries => { - // Some options, like the popup, trigger a resize after a delay - // before the page is saved. This causes the highlights to be added - // back to the DOM after the "TextHighlight" widget has been - // destroyed. This is why the following line is needed. - if (this.isDestroyed()) { - return; - } - window.requestAnimationFrame(() => { - const textHighlightEls = new Set(); - entries.forEach(entry => { - const target = entry.target; - if (this.observerLock.get(target)) { - // Unlock the target, the next resize will trigger a - // highlight adaptation. - return this.observerLock.set(target, false); - } - const topTextEl = target.closest(".o_text_highlight"); - for (const el of topTextEl - ? [topTextEl] - : target.querySelectorAll(":scope .o_text_highlight")) { - textHighlightEls.add(el); - } - }); - textHighlightEls.forEach(textHighlightEl => { - for (const textHighlightItemEl of this._getHighlightItems(textHighlightEl)) { - // Unobserve the highlight lines (they will be replaced - // by new ones after the update). - this.resizeObserver.unobserve(textHighlightItemEl); - } - // Adapt the highlight (new items are automatically locked - // and observed). - switchTextHighlight(textHighlightEl); - }); - }); - }); - - this.el.addEventListener("text_highlight_added", this._onTextHighlightAdded.bind(this)); - this.el.addEventListener("text_highlight_remove", this._onTextHighlightRemove.bind(this)); - // Text highlights are saved with a single wrapper that contains all - // information to build the effects, So we need to make the adaptation - // here to show the SVGs. - for (const textEl of this.el.querySelectorAll(".o_text_highlight")) { - applyTextHighlight(textEl); - } - return this._super(...arguments); - }, - /** - * @override - */ - destroy() { - // We only save the highlight information on the main text wrapper, - // the full structure will be restored on page load. - for (const textHighlightEl of this.el.querySelectorAll(".o_text_highlight")) { - removeTextHighlight(textHighlightEl); - } - this._super(...arguments); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * The `resizeObserver` ignores an element if it has an inline display. - * We need to target the closest non-inline parent. - * - * @private - * @param {HTMLElement} el - */ - _closestToObserve(el) { - if (el === this.el || !el) { - return null; - } - if (window.getComputedStyle(el).display !== "inline") { - return el; - } - return this._closestToObserve(el.parentElement); - }, - /** - * Returns a list of text highlight items (lines) in the provided element. - * - * @private - * @param {HTMLElement} el - */ - _getHighlightItems(el = this.el) { - return el.querySelectorAll(":scope .o_text_highlight_item"); - }, - /** - * Returns a list of highlight elements to observe. - * - * @private - * @param {HTMLElement} topTextEl - */ - _getObservedEls(topTextEl) { - const closestToObserve = this._closestToObserve(topTextEl); - return [ - ...(closestToObserve ? [closestToObserve] : []), - ...this._getHighlightItems(topTextEl), - ]; - }, - /** - * @private - * @param {HTMLElement} topTextEl the element where the "resize" should - * be observed. - */ - _observeHighlightResize(topTextEl) { - // The `ResizeObserver` cannot detect the width change on highlight - // units (`.o_text_highlight_item`) as long as the width of the entire - // `.o_text_highlight` element remains the same, so we need to observe - // each one of them and do the adjustment only once for the whole text. - for (const highlightItemEl of this._getObservedEls(topTextEl)) { - this.resizeObserver.observe(highlightItemEl); - } - }, - /** - * Used to prevent the first callback triggered by `ResizeObserver` on new - * observed items. - * - * @private - * @param {HTMLElement} topTextEl the container of observed items. - */ - _lockHighlightObserver(topTextEl) { - for (const targetEl of this._getObservedEls(topTextEl)) { - this.observerLock.set(targetEl, true); - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onTextHighlightAdded({ target }) { - this._lockHighlightObserver(target); - this._observeHighlightResize(target); - }, - /** - * @private - */ - _onTextHighlightRemove({ target }) { - // We don't need to track the removed text highlight items after - // highlight adaptations. - for (const highlightItemEl of this._getHighlightItems(target)) { - this.observerLock.delete(highlightItemEl); - } - }, -}); - export default { Widget: publicWidget.Widget, Animation: Animation,