@selenite/commons
Version:
This typescript package provides a set of frequently used utilities, types and svelte actions for building projects with Typescript and Svelte.
278 lines (277 loc) • 8.66 kB
JavaScript
const _windowState = $state({ width: 1879, height: 961 });
let _isWindowStateSetup = false;
/**
* Reactive window state for use in svelte 5.
*
* At the moment, it exposes width and height.
*/
export class WindowState {
width = $derived(_windowState.width);
height = $derived(_windowState.height);
constructor() {
if (typeof window === 'undefined')
return;
if (_isWindowStateSetup)
return;
_isWindowStateSetup = true;
_windowState.width = window.innerWidth;
_windowState.height = window.innerHeight;
window.addEventListener('resize', () => {
_windowState.width = window.innerWidth;
_windowState.height = window.innerHeight;
});
}
}
/**
* Returns whether the current environment is a browser, based on window being defined.
*/
export function isBrowser() {
return typeof window !== 'undefined';
}
/**
* Constant indicating whether the current environment is a browser.
*/
export const browser = isBrowser();
export function posFromClient({ clientX: x, clientY: y }) {
return { x, y };
}
/**
* Definition of a rectangle..
*/
export class Rect {
x;
y;
width;
height;
constructor(x = 0, y = 0, width = 0, height = 0) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
get right() {
return this.x + this.width;
}
get bottom() {
return this.y + this.height;
}
}
/**
* Utils to compute intersection, union and area of bouding rectangles.
*/
// eslint-disable-next-line @typescript-eslint/no-namespace
(function (Rect) {
/**
* Returns the intersection of multiple bouding rectangles.
*/
function intersection(rect, ...rects) {
const res = new Rect();
res.x = rect.x;
res.y = rect.y;
res.width = rect.width;
res.height = rect.height;
for (const r of rects) {
const a = { x: Math.max(res.x, r.x), y: Math.max(res.y, r.y) };
const b = { x: Math.min(res.right, r.right), y: Math.min(res.bottom, r.bottom) };
res.x = a.x;
res.y = a.y;
res.width = Math.max(b.x - a.x, 0);
res.height = Math.max(b.y - a.y, 0);
}
return res;
}
Rect.intersection = intersection;
/**
* Return the union of multiple bounding rectangles.
*/
function union(rect, ...rects) {
const res = new Rect();
res.x = rect.x;
res.y = rect.y;
res.width = rect.width;
res.height = rect.height;
for (const r of rects) {
const a = { x: Math.min(res.x, r.x), y: Math.min(res.y, r.y) };
const b = { x: Math.max(res.right, r.right), y: Math.max(res.bottom, r.bottom) };
res.x = a.x;
res.y = a.y;
res.width = b.x - a.x;
res.height = b.y - a.y;
}
return res;
}
Rect.union = union;
/**
* Returns the area of a rectangle.
*/
function area(rect) {
return rect.width * rect.height;
}
Rect.area = area;
function pos(rect) {
return { x: rect.x, y: rect.y };
}
Rect.pos = pos;
})(Rect || (Rect = {}));
export function download(filename, data) {
const text = typeof data === 'string' ? data : JSON.stringify(data, undefined, 4);
const blob = new Blob([text], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
}
export function downloadJSON(name, data) {
const json = JSON.stringify(data, undefined, 4);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${name}on`;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
}
/**
* Returns the bounds of an element relative to the window.
* @param element
* @returns
*/
export function getBounds(element) {
if (!element)
return { top: 0, left: 0, right: 0, bottom: 0 };
const { x, y, width, height } = element.getBoundingClientRect();
const windowState = new WindowState();
return {
left: x,
top: y,
right: windowState.width - x - width,
bottom: windowState.height - y - height
};
}
/**
* Returns the union of the window bounds of multiple elements.
* @param bounds
* @returns
*/
export function getBoundsUnion(bounds) {
const res = { top: Infinity, left: Infinity, right: Infinity, bottom: Infinity };
for (const target of bounds) {
if (target === undefined)
continue;
const bound = target instanceof Element ? getBounds(target) : target;
res.top = Math.min(res.top, bound.top);
res.left = Math.min(res.left, bound.left);
res.right = Math.min(res.right, bound.right);
res.bottom = Math.min(res.bottom, bound.bottom);
}
return res;
}
/**
* Returns padded window bounds.
* @param bounds - bounds to pad
* @param padding - pixels to pad
* @returns padded window bounds
*/
export function padBounds(bounds, padding) {
return {
top: bounds.top - padding,
left: bounds.left - padding,
right: bounds.right - padding,
bottom: bounds.bottom - padding
};
}
export function getBoundsIntersection(...bounds) {
const res = { top: 0, left: 0, right: 0, bottom: 0 };
for (const target of bounds) {
if (target === undefined)
continue;
const bound = target instanceof HTMLElement ? getBounds(target) : target;
res.top = Math.max(res.top, bound.top);
res.left = Math.max(res.left, bound.left);
res.right = Math.max(res.right, bound.right);
res.bottom = Math.max(res.bottom, bound.bottom);
}
return res;
}
export function getClosestElement(target, elements) {
let closestElement = undefined;
let closestDistance = Infinity;
const tRect = target.getBoundingClientRect();
for (const e of elements) {
const eRect = e.getBoundingClientRect();
const distance = Math.sqrt(Math.pow(eRect.x - tRect.x, 2) + Math.pow(eRect.y - tRect.y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestElement = e;
}
}
return closestElement;
}
export function getClosestElementIndex(target, elements) {
let closestDistance = Infinity;
let closestIndex = -1;
const tRect = target.getBoundingClientRect();
for (const [i, e] of elements.entries()) {
const eRect = e.getBoundingClientRect();
const distance = Math.sqrt(Math.pow(eRect.x - tRect.x, 2) + Math.pow(eRect.y - tRect.y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestIndex = i;
}
}
return closestIndex;
}
export class PointerDownWatcher {
isPointerDown = $state(false);
pos = $state();
lastEvent = $state();
static #instance = undefined;
static get instance() {
if (!this.#instance)
this.#instance = new PointerDownWatcher();
return this.#instance;
}
constructor() {
if (typeof document === 'undefined')
return;
console.debug('Adding pointer down watcher.');
document.addEventListener('pointerdown', this.onpointerdown.bind(this), { capture: true });
document.addEventListener('pointerup', this.onpointerup.bind(this), { capture: true });
}
#subscribers = new Set();
subscribe(run) {
this.#subscribers.add(run);
run(this.isPointerDown);
return () => {
this.#subscribers.delete(run);
};
}
onpointerdown(e) {
this.isPointerDown = true;
this.lastEvent = e;
this.pos = posFromClient(e);
this.#subscribers.forEach((s) => s(true));
}
onpointerup() {
this.isPointerDown = false;
this.pos = undefined;
this.#subscribers.forEach((s) => s(false));
}
}
export function isOverflowing(el) {
const curOverflow = el.style.overflow;
if (!curOverflow || curOverflow === 'visible')
el.style.overflow = 'hidden';
const res = {
vertical: el.scrollHeight > el.clientHeight,
horizontal: el.scrollWidth > el.clientWidth
};
el.style.overflow = curOverflow;
return res;
}