| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * `document.activeElement` will not enter shadow roots to find the element |
| * that has focus; use this method if you need to traverse through any shadow |
| * roots to find the actual, specific focused element. |
| */ |
| export function deepActiveElement(doc: Document): Element|null { |
| let activeElement: Element|null = doc.activeElement; |
| while (activeElement?.shadowRoot?.activeElement) { |
| activeElement = activeElement.shadowRoot.activeElement; |
| } |
| return activeElement; |
| } |
| |
| export function getEnclosingShadowRootForNode(node: Node): Node|null { |
| let parentNode = node.parentNodeOrShadowHost(); |
| while (parentNode) { |
| if (parentNode instanceof ShadowRoot) { |
| return parentNode; |
| } |
| parentNode = parentNode.parentNodeOrShadowHost(); |
| } |
| return null; |
| } |
| |
| export function rangeOfWord( |
| rootNode: Node, offset: number, stopCharacters: string, stayWithinNode: Node, direction?: string): Range { |
| let startNode; |
| let startOffset = 0; |
| let endNode; |
| let endOffset = 0; |
| |
| if (!stayWithinNode) { |
| stayWithinNode = rootNode; |
| } |
| |
| if (!direction || direction === 'backward' || direction === 'both') { |
| let node: Node|null = rootNode; |
| while (node) { |
| if (node === stayWithinNode) { |
| if (!startNode) { |
| startNode = stayWithinNode; |
| } |
| break; |
| } |
| |
| if (node.nodeType === Node.TEXT_NODE && node.nodeValue !== null) { |
| const start = (node === rootNode ? (offset - 1) : (node.nodeValue.length - 1)); |
| for (let i = start; i >= 0; --i) { |
| if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { |
| startNode = node; |
| startOffset = i + 1; |
| break; |
| } |
| } |
| } |
| |
| if (startNode) { |
| break; |
| } |
| |
| node = node.traversePreviousNode(stayWithinNode); |
| } |
| |
| if (!startNode) { |
| startNode = stayWithinNode; |
| startOffset = 0; |
| } |
| } else { |
| startNode = rootNode; |
| startOffset = offset; |
| } |
| |
| if (!direction || direction === 'forward' || direction === 'both') { |
| let node: (Node|null)|Node = rootNode; |
| while (node) { |
| if (node === stayWithinNode) { |
| if (!endNode) { |
| endNode = stayWithinNode; |
| } |
| break; |
| } |
| |
| if (node.nodeType === Node.TEXT_NODE && node.nodeValue !== null) { |
| const start = (node === rootNode ? offset : 0); |
| for (let i = start; i < node.nodeValue.length; ++i) { |
| if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { |
| endNode = node; |
| endOffset = i; |
| break; |
| } |
| } |
| } |
| |
| if (endNode) { |
| break; |
| } |
| |
| node = node.traverseNextNode(stayWithinNode); |
| } |
| |
| if (!endNode) { |
| endNode = stayWithinNode; |
| endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue?.length || 0 : |
| stayWithinNode.childNodes.length; |
| } |
| } else { |
| endNode = rootNode; |
| endOffset = offset; |
| } |
| |
| if (!rootNode.ownerDocument) { |
| throw new Error('No `ownerDocument` found for rootNode'); |
| } |
| const result = rootNode.ownerDocument.createRange(); |
| result.setStart(startNode, startOffset); |
| result.setEnd(endNode, endOffset); |
| |
| return result; |
| } |
| |
| /** |
| * Appends the list of `styles` as individual `<style>` elements to the |
| * given `node`. |
| * |
| * @param node the `Node` to append the `<style>` elements to. |
| * @param styles an optional list of styles to append to the `node`. |
| */ |
| export function appendStyle(node: Node, ...styles: CSSInJS[]): void { |
| for (const cssText of styles) { |
| const style = (node.ownerDocument ?? document).createElement('style'); |
| style.textContent = cssText; |
| node.appendChild(style); |
| } |
| } |