import {MsgSafeNode} from './msgsafe' // From saka-key lib/dom.js, under Apachev2 /** * Given a DOM element, returns true if you can edit it with key presses or * if the element is of a type that should handle its own keypresses * (e.g. role=application for google docs/sheets) * TODO: work on case sensitivity * consider all the possible cases * @param {HTMLElement} element * @returns {boolean} */ export function isTextEditable (element: MsgSafeNode) { if (element) { switch (element.nodeName) { case 'INPUT': return isEditableHTMLInput(element) case 'TEXTAREA': case 'OBJECT': return true } switch (true) { case element.contentEditable.toUpperCase() === 'TRUE': case element.role === 'application': return true } } return false } /** * Returns whether the passed HTML input element is editable * @param {HTMLInputElement} element */ function isEditableHTMLInput (element: MsgSafeNode) { if (element.disabled || element.readonly) return false switch (element.type) { case undefined: case 'text': case 'search': case 'email': case 'url': case 'number': case 'password': case 'date': case 'tel': return true } return false } /** * Dispatch a mouse event to the target element * based on cVim's implementation * @param {HTMLElement} element * @param {'hover' | 'unhover' | 'click'} type * @param {{ ctrlKey, shiftKey, altKey, metaKey }} modifierKeys */ export function mouseEvent (element: Element, type: 'hover'|'unhover'|'click', modifierKeys = {}) { let events switch (type) { case 'hover': events = ['mouseover', 'mouseenter', 'mousemove'] break case 'unhover': events = ['mousemove', 'mouseout', 'mouseleave'] break case 'click': events = ['mouseover', 'mousedown', 'mouseup', 'click'] break } events.forEach(type => { const event = new MouseEvent(type, { bubbles: true, cancelable: true, view: window, detail: 1, // usually the click count ...modifierKeys }) element.dispatchEvent(event) }) } /** Iterable of elements that match xpath. Adapted from stackoverflow */ export function* elementsByXPath(xpath, parent?) { let query = document.evaluate(xpath, parent || document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (let i=0, length=query.snapshotLength; i= innerHeight - 4: case clientRect.left < 0: case clientRect.left >= innerWidth - 4: case clientRect.width < 3: case clientRect.height < 3: case computedStyle.visibility !== 'visible': case computedStyle.display === 'none': return false } return true /* // Eliminate elements hidden by another overlapping element. */ /* // To do that, get topmost element at some offset from upper-left corner of clientRect */ /* // and check whether it is the element itself or one of its descendants. */ /* // The offset is needed to account for coordinates truncation and elements with rounded borders. */ /* // */ /* // Coordinates truncation occcurs when using zoom. In that case, clientRect coords should be float, */ /* // but we get integers instead. That makes so that elementFromPoint(clientRect.left, clientRect.top) */ /* // sometimes returns an element different from the one clientRect was obtained from. */ /* // So we introduce an offset to make sure elementFromPoint hits the right element. */ /* // */ /* // For elements with a rounded topleft border, the upper left corner lies outside the element. */ /* // Then, we need an offset to get to the point nearest to the upper left corner, but within border. */ /* const coordTruncationOffset = 2 // A value of 1 has been observed not to be enough, */ /* // so we heuristically choose 2, which seems to work well. */ /* // We know a value of 2 is still safe (lies within the element) because, */ /* // from the code above, widht & height are >= 3. */ /* const radius = parseFloat(computedStyle.borderTopLeftRadius) */ /* const roundedBorderOffset = Math.ceil(radius * (1 - Math.sin(Math.PI / 4))) */ /* const offset = Math.max(coordTruncationOffset, roundedBorderOffset) */ /* if (offset >= clientRect.width || offset >= clientRect.height) { */ /* return false */ /* } */ /* let el: Node = document.elementFromPoint( */ /* clientRect.left + offset, */ /* clientRect.top + offset */ /* ) */ /* while (el && el !== element) { */ /* el = el.parentNode */ /* } */ /* if (!el) { */ /* return false */ /* } */ /* return true */ } /** Get all elements that match the given selector * * @param selector `the CSS selector to choose elements with * @param filter filter to use to further chose items, or null for all */ export function getElemsBySelector(selector: string, filter: ElementFilter) { let elems = Array.from(document.querySelectorAll(selector)) return filter ? elems.filter(filter) : elems } /** Get the nth input element on a page * * @param nth the element index, can be negative to start at the end * @param filter filter to use to further chose items, or null for all */ export function getNthElement(selectors: string, nth: number, filter: ElementFilter) { let inputs = getElemsBySelector(selectors, filter) if (inputs.length) { let index = Number(nth).clamp(-inputs.length, inputs.length - 1) .mod(inputs.length) return inputs[index] } return null } /** Comparison function by offsetWidth/Height, used for sorting elements by their * area on the page */ export function compareElementArea(a: HTMLElement, b: HTMLElement): number { const aArea = a.offsetWidth * a.offsetHeight const bArea = b.offsetWidth * b.offsetHeight return aArea - bArea }