| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview DOM observer for annotation changes. |
| */ |
| |
| import {isDecorationNode} from '//ios/web/annotations/resources/text_decoration.js'; |
| import {isValidNode} from '//ios/web/annotations/resources/text_dom_utils.js'; |
| |
| // Consumer of decoration `Node` removed callback. |
| export interface TextDecorationNodeRemovedConsumer { |
| (node: Node): void; |
| } |
| |
| // Interface for an `IntersectionObserver` that should count `observe` and |
| // `unobserve` call and actually `unobserve` only when the count reaches 0. |
| // The `TextDomObserver` will call it for every text node and doesn't |
| // care about keeping track of observations state. |
| export interface CountedIntersectionObserver { |
| observe(node: Node): void; |
| unobserve(node: Node): void; |
| } |
| |
| // Class for a DOM `MutationObserver` that handles passing on to a |
| // `CountedIntersectionObserver` the text nodes that should be observed |
| // (or unobserved) for viewport intersection. |
| export class TextDomObserver { |
| constructor( |
| public root: Element, |
| private intersectionObserver: CountedIntersectionObserver, |
| private decorationNodeRemoved: TextDecorationNodeRemovedConsumer) {} |
| |
| // Mutation observer for handling added and removed nodes and text mutation. |
| private mutationCallback = (mutationList: MutationRecord[]) => { |
| for (const mutation of mutationList) { |
| if (mutation.type === 'childList') { |
| // Avoid observing again if this is triggered by decorating. |
| for (const node of mutation.addedNodes) { |
| if (!isDecorationNode(node)) { |
| this.observeNodes(node); |
| } |
| } |
| for (const node of mutation.removedNodes) { |
| this.unobserveNodes(node); |
| // This wasn't removed by the decorator, there's corruption. |
| if (isDecorationNode(node) && node.nodeName === 'CHROME_ANNOTATION') { |
| this.decorationNodeRemoved(node); |
| } |
| } |
| } else if (mutation.type === 'characterData') { |
| // Since it was probably handled and unobserved, let's observe it |
| // again with its new value. The IntersectionObserver will trigger |
| // right away if the `mutation.target`'s parent is visible. |
| this.observeNodes(mutation.target); |
| } |
| } |
| }; |
| |
| private mutationObserver = new MutationObserver(this.mutationCallback); |
| |
| // Starts at given `node` and traverses all of its descendants, registering |
| // text nodes with the IntersectionObserver. |
| private observeNodes(node: Node): void { |
| // Observe only nodes with text. |
| if (node.nodeType === Node.TEXT_NODE) { |
| this.intersectionObserver.observe(node); |
| } |
| if (node instanceof Element && isValidNode(node)) { |
| if (node.shadowRoot && node.shadowRoot !== node as Node) { |
| this.observeNodes(node.shadowRoot); |
| } else if (node.hasChildNodes()) { |
| for (const childNode of node.childNodes) { |
| this.observeNodes(childNode); |
| } |
| } |
| } |
| } |
| |
| // Starts at given `node` and traverses all of its descendants, unregistering |
| // text nodes with the IntersectionObserver. |
| private unobserveNodes(node: Node): void { |
| // Only nodes with text are observed. |
| if (node.nodeType === Node.TEXT_NODE) { |
| this.intersectionObserver.unobserve(node); |
| } |
| if (node instanceof Element && isValidNode(node)) { |
| if (node.shadowRoot && node.shadowRoot !== node as Node) { |
| this.unobserveNodes(node.shadowRoot); |
| } else if (node.hasChildNodes()) { |
| for (const childNode of node.childNodes) { |
| this.unobserveNodes(childNode); |
| } |
| } |
| } |
| } |
| |
| // Starts the DOM observer. Scans the tree under `root` that is already loaded |
| // then observes for further mutations. |
| start(): void { |
| this.observeNodes(this.root); |
| // Only monitor DOM `element` changes and text nodes mutation. |
| this.mutationObserver.observe(this.root, { |
| attributes: false, |
| childList: true, |
| characterData: true, |
| subtree: true, |
| attributeOldValue: false, |
| characterDataOldValue: false, |
| }); |
| } |
| |
| // Stops the DOM observer. Also cleans `intersectionObserver` under `root`. |
| stop(): void { |
| this.unobserveNodes(this.root); |
| this.mutationObserver.disconnect(); |
| } |
| |
| // Force updating before next js main thread cycle. |
| updateForTesting(): void { |
| this.mutationCallback(this.mutationObserver.takeRecords()); |
| } |
| } |