// @flow strict export type Placement = 'top' | 'left' | 'right' | 'bottom'; export type ArrowPosition = 'start' | 'center' | 'end'; export type Justify = ArrowPosition; export type PositionedPlacement = { placement: Placement, x: number, y: number, }; export function createElement( document: Document, type: string, props: {[string]: mixed, ...}, ): HTMLElement { const el = document.createElement(type); return Object.assign(el, props); } export function pageHeight(): number { const {body, documentElement} = window.document; return Math.max( body.scrollHeight, body.clientHeight, documentElement.scrollHeight, documentElement.clientHeight, ); } export function getFixedAnchorPosition( element: HTMLElement, placement: Placement = 'top', pad?: number = 0, justify?: Justify = 'center', ): PositionedPlacement { const rect = element.getBoundingClientRect(); const docX = rect.left; const docY = rect.top; let x, y; switch (placement) { case 'bottom': y = docY + rect.height + pad; switch (justify) { case 'start': x = docX; break; case 'center': x = docX + rect.width / 2; break; case 'end': default: x = docX + rect.width; break; } break; case 'left': x = docX - pad; switch (justify) { case 'start': y = docY; break; case 'center': y = docY + rect.height / 2; break; case 'end': default: y = docY + rect.height; break; } break; case 'right': x = docX + rect.width + pad; y = docY + rect.height / 2; break; default: y = docY - pad; switch (justify) { case 'start': x = docX; break; case 'center': x = docX + rect.width / 2; break; case 'end': default: x = docX + rect.width; break; } break; } return {x, y, placement}; } export function getAnchorPosition( element: HTMLElement, placement?: Placement = 'top', pad?: number = 0, justify?: Justify = 'center', ): PositionedPlacement { const position = getFixedAnchorPosition(element, placement, pad, justify); const documentStyle = window.document.documentElement.style; return { ...position, x: position.x + window.pageXOffset - pxToNumber(documentStyle.paddingLeft), y: position.y + window.pageYOffset - pxToNumber(documentStyle.paddingTop), }; } export function pxToNumber(px: string): number { return parseFloat(px.replace('px', '') || '0'); } // TODO (kyle): add more handler types? type Handlers = $Shape<{ click: (MouseEvent) => mixed, mousedown: (MouseEvent) => mixed, mouseup: (MouseEvent) => mixed, pointerdown: (PointerEvent) => mixed, pointerup: (PointerEvent) => mixed, pointercancel: (PointerEvent) => mixed, [string]: (Event) => mixed, }>; export function listen( target: EventTarget, handlers: Handlers, options?: EventListenerOptionsOrUseCapture, hook: 'addEventListener' | 'removeEventListener' = 'addEventListener', ) { for (const eventName in handlers) { // $FlowFixMe indexing valid EventTarget properties target[hook](eventName, handlers[eventName], options); } } export function forget( target: EventTarget, events: Handlers, options: EventListenerOptionsOrUseCapture, ): void { return listen(target, events, options, 'removeEventListener'); } type MixedEvent = SyntheticEvent | Event; export function stopEvent(event: MixedEvent) { event.stopPropagation(); } export function stopEventImmediately(event: SyntheticEvent) { event.nativeEvent.stopImmediatePropagation(); } export function cancelEvent(event: MixedEvent) { event.stopPropagation(); event.preventDefault(); } export function requestPointerLock(element: Element): Promise { return new Promise((resolve, reject) => { const handleChange = () => { // $FlowIssue if (document.pointerLockElement === element) { resolve(); removeHandlers(); } }; const handleError = () => { reject(); removeHandlers(); }; const removeHandlers = () => { // $FlowFixMe forget(document, { pointerlockchange: handleChange, pointerlockerror: handleError, }); }; // $FlowFixMe listen(document, { pointerlockchange: handleChange, pointerlockerror: handleError, }); element.requestPointerLock(); }); } export function checkDateInputSupport(): boolean { const input = window.document.createElement('input'); input.setAttribute('type', 'date'); const notADateValue = 'not-a-date'; input.setAttribute('value', notADateValue); return input.value !== notADateValue; } //reference: https://stackoverflow.com/a/10199306 export const getListPasteHandler = ({ listItemSeparatorRegex = /[\,\n]/, handleValue, }: { listItemSeparatorRegex?: RegExp, handleValue?: (string[]) => mixed, }): ((ClipboardEvent) => mixed) => { const handlePaste = (event: ClipboardEvent) => { const value = event.clipboardData?.getData('text'); if (!value || !value.length) { return; } //do nothing if the copied string fails the regex test if (!listItemSeparatorRegex.test(value)) { return; } event.preventDefault(); const parsedValues = value .split(listItemSeparatorRegex) .reduce((acc, val) => { const newVal = val.trim(); if ( // value exists !!newVal.length && // value not already in queue !acc.includes(newVal) ) { acc.push(newVal); } return acc; }, []); handleValue?.(parsedValues); }; return handlePaste; };