Skip to content

Commit

Permalink
TextHighlight
Browse files Browse the repository at this point in the history
  • Loading branch information
romo-odoo committed Nov 15, 2024
1 parent 244a01d commit bcdbc2e
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 173 deletions.
128 changes: 128 additions & 0 deletions addons/website/static/src/interactions/text_highlights.js
Original file line number Diff line number Diff line change
@@ -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);
173 changes: 0 additions & 173 deletions addons/website/static/src/js/content/snippets.animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit bcdbc2e

Please sign in to comment.