1// Copyright (C) 2023 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {Vector2D} from './geom'; 16 17export type CSSCursor = 18 | 'alias' 19 | 'all-scroll' 20 | 'auto' 21 | 'cell' 22 | 'context-menu' 23 | 'col-resize' 24 | 'copy' 25 | 'crosshair' 26 | 'default' 27 | 'e-resize' 28 | 'ew-resize' 29 | 'grab' 30 | 'grabbing' 31 | 'help' 32 | 'move' 33 | 'n-resize' 34 | 'ne-resize' 35 | 'nesw-resize' 36 | 'ns-resize' 37 | 'nw-resize' 38 | 'nwse-resize' 39 | 'no-drop' 40 | 'none' 41 | 'not-allowed' 42 | 'pointer' 43 | 'progress' 44 | 'row-resize' 45 | 's-resize' 46 | 'se-resize' 47 | 'sw-resize' 48 | 'text' 49 | 'vertical-text' 50 | 'w-resize' 51 | 'wait' 52 | 'zoom-in' 53 | 'zoom-out'; 54 55// Check whether a DOM element contains another, or whether they're the same 56export function isOrContains(container: Element, target: Element): boolean { 57 return container === target || container.contains(target); 58} 59 60// Find a DOM element with a given "ref" attribute 61export function findRef(root: Element, ref: string): Element | null { 62 const query = `[ref=${ref}]`; 63 if (root.matches(query)) { 64 return root; 65 } else { 66 return root.querySelector(query); 67 } 68} 69 70// Safely cast an Element to an HTMLElement. 71// Throws if the element is not an HTMLElement. 72export function toHTMLElement(el: Element): HTMLElement { 73 if (!(el instanceof HTMLElement)) { 74 throw new Error('Element is not an HTMLElement'); 75 } 76 return el as HTMLElement; 77} 78 79// Return true if EventTarget is or is inside an editable element. 80// Editable elements incluce: <input type="text">, <textarea>, or elements with 81// the |contenteditable| attribute set. 82export function elementIsEditable(target: EventTarget | null): boolean { 83 if (target === null) { 84 return false; 85 } 86 87 if (!(target instanceof Element)) { 88 return false; 89 } 90 91 const editable = target.closest('input, textarea, [contenteditable=true]'); 92 93 if (editable === null) { 94 return false; 95 } 96 97 if (editable instanceof HTMLInputElement) { 98 if (['radio', 'checkbox', 'button'].includes(editable.type)) { 99 return false; 100 } 101 } 102 103 return true; 104} 105 106// Returns the mouse pointer's position relative to |e.currentTarget| for a 107// given |MouseEvent|. 108// Similar to |offsetX|, |offsetY| but for |currentTarget| rather than |target|. 109// If the event has no currentTarget or it is not an element, offsetX & offsetY 110// are returned instead. 111export function currentTargetOffset(e: MouseEvent): Vector2D { 112 if (e.currentTarget === e.target) { 113 return new Vector2D({x: e.offsetX, y: e.offsetY}); 114 } 115 116 if (e.currentTarget && e.currentTarget instanceof Element) { 117 const rect = e.currentTarget.getBoundingClientRect(); 118 const offsetX = e.clientX - rect.left; 119 const offsetY = e.clientY - rect.top; 120 return new Vector2D({x: offsetX, y: offsetY}); 121 } 122 123 return new Vector2D({x: e.offsetX, y: e.offsetY}); 124} 125 126// Adds an event listener to a DOM element, returning a disposable to remove it. 127export function bindEventListener<K extends keyof HTMLElementEventMap>( 128 element: EventTarget, 129 event: K, 130 handler: (event: HTMLElementEventMap[K]) => void, 131): Disposable { 132 element.addEventListener(event, handler as EventListener); 133 return { 134 [Symbol.dispose]() { 135 element.removeEventListener(event, handler as EventListener); 136 }, 137 }; 138} 139