@spaced-out/ui-design-system
Version:
Sense UI components library
239 lines (216 loc) • 5.72 kB
Flow
// @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<EventTarget> | Event;
export function stopEvent(event: MixedEvent) {
event.stopPropagation();
}
export function stopEventImmediately(event: SyntheticEvent<EventTarget>) {
event.nativeEvent.stopImmediatePropagation();
}
export function cancelEvent(event: MixedEvent) {
event.stopPropagation();
event.preventDefault();
}
export function requestPointerLock(element: Element): Promise<void> {
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;
};