Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 9x 9x 9x 9x 1x 1x 1x 11x 11x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 31x 31x 31x 31x 31x 55x 55x 55x 55x 55x 433x 190x 190x 243x 243x 433x 7x 7x 55x 55x 31x 31x 31x 31x 31x 1x 1x 1x 1x 1x 1x 1x 1x 243x 243x 243x 243x 243x 243x 236x 236x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 12x 12x 12x 10x 12x 2x 2x 2x 2x 2x 2x 12x 12x 1x 1x 1x 12x 1x 1x 1x 1x 1x 1x 11x 11x 11x 11x 11x 11x 11x 11x 11x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | import { getLanguageFromElement } from '../utils/algorithms/get-lang-from-element.util.js' import { IterableWeakMap, IterableWeakSet } from '../utils/algorithms/iterable-weak-struct.js' /** @type {IterableWeakMap<Node, ObserveInformation>} */ const rootNodes = new IterableWeakMap() const data = Symbol('ElementLangObserverData') /** @type {WeakMap<Element, {currentLang: string, observers: Set<ElementLangObserver>}>} */ const observingElementsInfo = new WeakMap() export const domRootLangDispatchListener = { /** * @param {Element | Document} target - root target * @param {EventListenerOrEventListenerObject} callback - callback * @param {boolean | AddEventListenerOptions} options - listener options * @returns {{removeListener: () => void}} event listener */ onDispatchOnRoot: (target, callback, options) => { target.addEventListener(rootEventName, callback, options) return { removeListener: () => target.removeEventListener(rootEventName, callback, options), } }, } export const rootEventName = 'lang-change-dispatched' export class ElementLangObserver { /** * @param {ElementLangObserverHandler} callback - handler callback */ constructor (callback) { this[data] = { callback, } } /** @param {Element} element - element target to observe */ observe (element) { observeLangFromElement(element, this) } /** @param {Element} element - element target to stop observing */ unobserve (element) { unobserveLangFromElement(element, this) } } /** * @type {MutationObserverInit} */ const mutationProperties = Object.freeze({ attributes: true, attributeFilter: ['lang'], subtree: true, }) /** * callback of MutationObserver that detect language changes * @param {MutationRecord[]} records - mutation records */ function langMutationObserverCallback (records) { const triggeredNodes = new Set() const validatedNodes = new Set() const rootNodesToTrigger = new Set() for (const record of records) { const recordTarget = record.target const rootNode = recordTarget.getRootNode() rootNodesToTrigger.add(rootNode) const observingElements = rootNodes.get(rootNode)?.observingElements observingElements && observingElements.forEach((node) => { if (validatedNodes.has(node)) { return } validatedNodes.add(node) const result = handleLangMutationOnElement(node, recordTarget) if (result === changeTriggered) { triggeredNodes.add(node) } }) } const event = new CustomEvent(rootEventName, { detail: { triggeredNodes: Array.from(triggeredNodes) } }) for (const node of rootNodesToTrigger) { node.dispatchEvent(event) } } const changeTriggered = Object.freeze({ changeTriggered: true }) const changeNotTriggered = Object.freeze({ changeTriggered: false }) /** * @param {Element} element - target element * @param {Node} causingElement - the element that caused the locale change, most likely by having the `lang` attribute changed * @returns {Readonly<{ changeTriggered: boolean }>} result object if change has triggered or not */ function handleLangMutationOnElement (element, causingElement) { const info = observingElementsInfo.get(element) if (!info) { return changeNotTriggered } const oldLang = info.currentLang const newLang = getLanguageFromElement(element) if (newLang === oldLang) { return changeNotTriggered } info.currentLang = newLang info.observers.forEach(observer => { observer[data].callback([{ target: element, causingElement, previousLanguage: oldLang, language: newLang, }]) }) return changeTriggered } /** * Creates an observer to handle `lang` change on DOM root * @param {Node} targetNode - root node be it the `<html>` element or the `ShadowRoot` * @returns {MutationObserver} created mutation observe observing `targetNode` */ function createObserver (targetNode) { const observer = new MutationObserver(langMutationObserverCallback) observer.observe(targetNode, mutationProperties) return observer } /** * Traverse DOM roots, creating an observer if not defined to listen for `lang` attribute change * @param {Node} rootNode - traversing root node * @param {Element} element - element to trigger when `rootNode` detects a language change */ function traverseRootNode (rootNode, element) { const observeInformation = rootNodes.get(rootNode) if (observeInformation) { observeInformation.observingElements.add(element) } else { rootNodes.set(rootNode, { observer: createObserver(rootNode), observingElements: new IterableWeakSet([element]), targetNode: new WeakRef(rootNode), }) } if (rootNode instanceof ShadowRoot) { const host = rootNode.host traverseRootNode(host.getRootNode(), element) } } /** * @param {Element} element - target element * @param {ElementLangObserver} observer - observer to observe */ export function observeLangFromElement (element, observer) { const oldVal = observingElementsInfo.get(element) if (oldVal) { oldVal.observers.add(observer) return } observingElementsInfo.set(element, { currentLang: getLanguageFromElement(element), observers: new Set([observer]), }) const rootNode = element.getRootNode() traverseRootNode(rootNode, element) } /** * @param {Element} element - target element * @param {ElementLangObserver} observer - observing observer */ export function unobserveLangFromElement (element, observer) { const observers = observingElementsInfo.get(element)?.observers if (!observers) { return } observers.delete(observer) if (observers.size > 0) { return } observingElementsInfo.delete(element) for (const [rootNode, info] of rootNodes.entries()) { const { observingElements, observer } = info observingElements.delete(element) if (observingElements.size <= 0) { observer.disconnect() rootNodes.delete(rootNode) } } } /** * @typedef {object} ObserveInformation * @property {IterableWeakSet<Element>} observingElements - the elements that will react when `targetNode` detects a language change * @property {MutationObserver} observer - mutationObserver applied to `targetNode` * @property {WeakRef<Node>} targetNode - rootNode of the current DOM (<html> or ShadowRoot) */ /** * @callback ElementLangObserverHandler * @param {ElementLangObserverRecord[]} records * @returns {void} */ /** * @typedef {object} ElementLangObserverRecord * @property {Element} target - observer target * @property {Node} causingElement - element that changed language * @property {string} previousLanguage - previous lang value * @property {string} language - new lang value */ |