|
// sh.mts: javascript shorthand |
|
|
|
// #region array tools |
|
/** id function (returns first argument) */ |
|
export const id=<T,>(x: T): T=>x |
|
/** create an array filled with n copies of x */ |
|
export const af=<T,>(n: number, x: T): T[]=>Array(n).fill(x) // TODO: make this 'afl' or 'fil' (aa?) |
|
/** loop i from 0..n, calling f(i) */ |
|
export const ii=(n: number, f: (i: number)=>void)=>{for(let i=0;i<n;i++)f(i)} |
|
/** map f over [0..n] */ |
|
export const im=<T,>(n: number, f: (i: number)=>T): T[]=>af(n,0).map((_,i)=>f(i)) |
|
/** return the numbers [0..n] */ |
|
export const ia=(n: number): number[]=>im(n,id) |
|
/** extract the values of a for the given indices */ |
|
export const at=<T,>(a: T[], ixs: number[]): T[]=>ixs.map(i=>a[i]) |
|
/** index of each y in ys in xs */ |
|
export const io=<T,>(xs: readonly T[], ys: readonly T[]): number[]=>ys.map(y => xs.indexOf(y)) // Use readonly for safety and fix map fn |
|
/** Returns a[0] **/ |
|
export const a0=<T,>(a: T[]): T=>a[0] |
|
/** Returns a[1] **/ |
|
export const a1=<T,>(a: T[]): T=>a[1] |
|
|
|
/** Binary search: find lowest index i where xs[i] <= x, or -1 if none **/ |
|
export function bin(xs: number[], x: number): number { |
|
let a=0, z=xs.length-1, m, i |
|
if (!xs.length || x<xs[a]) return -1 |
|
if (x>=xs[z]) return z // Handle case where x is >= last element |
|
while (a<=z) { |
|
i = a + (m = (z-a)>>>1) |
|
if(!m || x === xs[i]) return i |
|
if(x < xs[i]) z=i; else a=i} |
|
console.warn("bin: failed to find index") // should never happen |
|
return a} |
|
|
|
// test suite for bin(xs,x) |
|
let actual = [1,3,5,7, 2,4,6, 0,8].map(x=>bin([1,3,5,7],x)) |
|
let expect = [0,1,2,3, 0,1,2, -1,3] |
|
for (let i=0;i<actual.length;i++) |
|
if(actual[i]!==expect[i]) console.warn(`${actual[i]}!==${expect[i]} at index ${i}`) |
|
|
|
/** Odometer: Generates a grid of coordinate pairs (x,y) for width x and height y. |
|
Ex: odom(2,3) => [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2]] */ |
|
export const odom=(x: number, y: number): [number, number][]=>{ |
|
let r:[number, number][]=[]; |
|
for(let j=0;j<y;j++) for(let i=0;i<x;i++) r.push([i,j]); return r } |
|
// #endregion |
|
|
|
// #region misc js |
|
/** Shorthand for Math functions (min, max, sin, tan, cos) **/ |
|
export const {min,max,sin,tan,cos}=Math |
|
/** Shortcut for random number generation **/ |
|
export const ran=Math.random |
|
/** Shortcut for floor function **/ |
|
export const flo=Math.floor |
|
/** Shortcut for ceil function **/ |
|
export const cei=Math.ceil |
|
/** Shortcut for round function **/ |
|
export const rou=Math.round |
|
/** Shortcut to parse integer **/ |
|
export const int=parseInt |
|
/** Shortcut to parse float **/ |
|
export const num=parseFloat |
|
/** Sorts array with an optional comparator **/ |
|
export const srt=<T,>(a: T[], f?: (a: T, b: T) => number): T[]=>a.toSorted(f) |
|
/** Returns length property of an object or string **/ |
|
export const len=(x: {length: number} | string): number=>x.length |
|
/** Reference to the document object **/ |
|
export const doc=document |
|
/** Checks if a value is undefined or null **/ |
|
export const und=(x: any): x is undefined | null =>x===undefined || x === null |
|
/** Shorthand to convert array-like objects to arrays **/ |
|
export const arf=Array.from // TODO: make this 'af' |
|
/** Object.assign shortcut **/ |
|
export const oas=Object.assign |
|
/** Creates an object from entries **/ |
|
export const ofe=Object.fromEntries |
|
/** Returns object's keys **/ |
|
export const oks=Object.keys |
|
/** Bound console.log shortcut **/ |
|
export const log=console.log.bind(console) |
|
/** Request animation frame helper **/ |
|
export const raf=(cb: FrameRequestCallback): number =>window.requestAnimationFrame(cb) |
|
/** Create a string from a Unicode code point **/ |
|
export const chr=String.fromCodePoint |
|
/** Returns code point(s) for a string **/ |
|
export const ord=(s: string): number | (number | undefined)[] | undefined => s.length === 1 ? s.codePointAt(0) : arf(s).map(c => s.codePointAt(0)) |
|
/** Fetch text and apply callback **/ |
|
export const ftx=(u: string | URL | Request, cb: (text: string)=>void): Promise<void>=>fetch(u).then(r=>r.text()).then(cb) |
|
/** Fetch JSON with optional method and data **/ |
|
export const fjs=<T=any,>(u: string | URL | Request, m?: string, d?: any): Promise<T>=>{ |
|
let f = (und(m)||m==='GET') ? fetch(u) |
|
: fetch(u,{method:m,headers:{'Content-type':'application/json'}, |
|
body:jss(d)}) |
|
return f.then(r=>r.json())} |
|
/** Shortcut to JSON.parse **/ |
|
export const jsp=JSON.parse |
|
/** Shortcut to JSON.stringify **/ |
|
export const jss=(x: any, y?: (key: string, value: any) => any | (number | string)[] | null): string =>JSON.stringify(x,y) |
|
/** Shorthand setTimeout wrapper **/ |
|
export const sto=(ms: number, cb: ()=>void): ReturnType<typeof setTimeout>=>setTimeout(cb,ms) |
|
|
|
// deb(f,ms) returns a debounced function that delays invoking f until after ms milliseconds have elapsed since the last time it was invoked. |
|
/** Creates a debounced version of a function **/ |
|
export function deb<T extends (...args: any[]) => any>(f: T, ms: number): (...args: Parameters<T>) => void { |
|
let timer: ReturnType<typeof setTimeout> | null = null; |
|
return function(this: ThisParameterType<T>, ...args: Parameters<T>) { |
|
if (timer) clearTimeout(timer); |
|
timer = setTimeout(() => { f.apply(this, args); }, ms)}} |
|
// #endregion |
|
// #region dom tools |
|
/** Create an HTMLElement with attributes and children **/ |
|
export const ce=<K extends keyof HTMLElementTagNameMap,>(t: K, a?: Record<string, string>, es?: (Node | string)[]): HTMLElementTagNameMap[K]=>app(ats(doc.createElement(t),a), ...(es||[])) |
|
/** Create an Element in a specific namespace with attributes and children **/ |
|
export const cen=(n: string | null, t: string, a?: Record<string, string>, es?: (Node | string)[]): Element =>app(ats(doc.createElementNS(n,t),a), ...(es||[])) |
|
/** Create an SVG element with attributes and children **/ |
|
export const svg=<K extends keyof SVGElementTagNameMap,>(t: K, a?: Record<string, string>, es?: (Node | string)[]): SVGElementTagNameMap[K]=>(es||=[],cen(svg.ns,t,a,es) as SVGElementTagNameMap[K]) |
|
svg.ns='http://www.w3.org/2000/svg' |
|
/** Create a new Animation instance **/ |
|
export const ani=(a: AnimationEffect | null, tl?: AnimationTimeline | null): Animation =>new Animation(a, tl) |
|
/** Create a KeyframeEffect instance **/ |
|
export const kfe=(e: Element | null, ks: Keyframe[] | PropertyIndexedKeyframes | null, o?: number | KeyframeEffectOptions)=>new KeyframeEffect(e,ks,o) |
|
/** Shortcut to create an Animation from an element and keyframes **/ |
|
export const akf=(e: Element | null, ks: Keyframe[] | PropertyIndexedKeyframes | null, o?: number | KeyframeEffectOptions)=>ani(kfe(e,ks,o)) |
|
/** Create an SVG element representing a drawing surface **/ |
|
export const S=(a?: Record<string, string>, es?: (Node | string)[]): SVGSVGElement => svg('svg',oas({ |
|
width:640,height:480,viewBox:'-320 -240 640 480'},a),es) |
|
/** Create an SVG circle element **/ |
|
export const C=(a?: Record<string, string>): SVGCircleElement =>svg('circle',oas({cx:0,cy:0,r:0},a)) |
|
/** Create an SVG rectangle element **/ |
|
export const R=(a?: Record<string, string>): SVGRectElement =>svg('rect',oas({x:-5,y:-5,width:10,height:10},a)) |
|
/** Create an SVG group element **/ |
|
export const G=(a?: Record<string, string>, es?: (Node | string)[]): SVGGElement =>svg('g',a,es) |
|
|
|
// -- dom shorthand |
|
/** Returns attribute value if v is undefined, otherwise sets attribute **/ |
|
export function att(e: Element, a: string): string | null |
|
export function att<T extends Element,>(e: T, a: string, v: string | number | boolean): T |
|
export function att<T extends Element,>(e: T, a: string, v?: string | number | boolean): T | string | null {return und(v)?e.getAttribute(a):(e.setAttribute(a,String(v)),e)} |
|
/** Sets multiple attributes from a key-value object **/ |
|
export function ats<T extends Element,>(e: T, kv?: Record<string, string | number | boolean>): T { |
|
if (kv) for (let k in kv) e.setAttribute(k,String(kv[k])); return e} |
|
/** Gets or sets inline styles on an element **/ |
|
export function sty(e: HTMLElement, a: keyof CSSStyleDeclaration): string | null |
|
export function sty<T extends HTMLElement,>(e: T, a: keyof CSSStyleDeclaration, v: string | null): T |
|
export function sty<T extends HTMLElement,>(e: T, a: keyof CSSStyleDeclaration, v?: string | null): T | string | null {return und(v) ? e.style[a as any] : (e.style[a as any]=v,e)} |
|
/** Checks if an element has a specific attribute **/ |
|
export const hat=(e: Element, a: string): boolean =>e.hasAttribute(a) |
|
/** Removes an attribute from an element **/ |
|
export const rat=(e: Element, a: string): void =>e.removeAttribute(a) |
|
/** Appends nodes or strings to an element or array **/ |
|
export function app<T extends Node,>(x: T, ...ys: (Node | string)[]): T |
|
export function app<T,>(x: T[], ...ys: T[]): T[] |
|
export function app(x: any, ...ys: any[]): any {return (x.append||x.push).bind(x)(...ys),x} |
|
/** Gets or sets the innerHTML of an element **/ |
|
export function iht(e: Element): string |
|
export function iht<T extends Element,>(e: T, h: string): T |
|
export function iht<T extends Element,>(e: T, h?: string): T | string {return und(h)?e.innerHTML:(e.innerHTML=h,e)} |
|
/** Gets or sets the innerText of an element **/ |
|
export function itx(e: HTMLElement): string |
|
export function itx<T extends HTMLElement,>(e: T, t: string): T |
|
export function itx<T extends HTMLElement,>(e: T, t?: string): T | string {return und(t)?e.innerText:(e.innerText=t,e)} |
|
/** Adds a class to an element **/ |
|
export const cla=<T extends Element,>(c: string, e: T): T=>(e.classList.add(c),e) |
|
/** Removes a class from an element **/ |
|
export const clr=<T extends Element,>(c: string, e: T): T=>(e.classList.remove(c),e) |
|
/** Checks if an element has a given class **/ |
|
export const hac=(e: Element, c: string): boolean =>e.classList.contains(c) |
|
/** Returns the bounding rectangle of an element **/ |
|
export const gbc=(e: Element): DOMRect =>e.getBoundingClientRect() |
|
/** Defines a custom element with a tag name **/ |
|
export const cus=(t: string, c: CustomElementConstructor): void => { |
|
try { |
|
customElements.define(t, c) |
|
} catch (e) { |
|
if (e instanceof DOMException && e.name === 'NotSupportedError' && |
|
e.message.includes('has already been used')) { |
|
console.warn(`Custom element "${t}" already defined, may be a duplicate import`) |
|
} else { |
|
throw e |
|
} |
|
} |
|
} |
|
|
|
/** Gets an element by id from document or fragment **/ |
|
export function eid(x: string): HTMLElement | null |
|
export function eid(x: DocumentFragment, y: string): HTMLElement | null |
|
export function eid(x: string | DocumentFragment, y?: string): HTMLElement | null { |
|
let [base, id]: [Document | DocumentFragment, string] = y===undefined ? [doc,x as string] : [x as DocumentFragment, y as string] |
|
return base instanceof Document ? base.getElementById(id) : base.querySelector(`#${CSS.escape(id)}`) |
|
} |
|
/** Proxy interface for shorthand element lookup **/ |
|
export interface Ei { |
|
(x: string): HTMLElement | null; |
|
(x: DocumentFragment, y: string): HTMLElement | null; |
|
[key: string]: HTMLElement | null; |
|
} |
|
/** Type for the proxied eid function **/ |
|
type EidFn = { |
|
(x: string): HTMLElement | null; |
|
(x: DocumentFragment, y: string): HTMLElement | null; |
|
} |
|
/** Proxied eid to allow property shorthand access **/ |
|
export const ei = new Proxy(eid as EidFn, { |
|
get: (targetEid, prop) => targetEid(typeof prop === 'symbol' ? prop.toString() : String(prop)), |
|
apply: (targetEid, thisArg, args: [string] | [DocumentFragment, string]) => { |
|
if (args.length === 1) return targetEid(args[0]) |
|
else return targetEid(args[0], args[1])}}) as Ei |
|
|
|
/** shorthand: y ? x.querySelector(y) : document.querySelector(x) **/ |
|
export function qs<T extends Element = HTMLElement>(x: string | ParentNode, y?: string): T | null { |
|
const [base, sel] = und(y) ? [doc, x as string] : [x as ParentNode, y as string] |
|
return base.querySelector<T>(sel)} |
|
|
|
/** Queries multiple elements matching selectors **/ |
|
export function qsa<T extends Element = HTMLElement>(x: string | ParentNode, y?: string): T[] { |
|
const [base, sel] = und(y) ? [doc,x as string] : [x as ParentNode,y as string] |
|
return Array.from(base.querySelectorAll<T>(sel))} |
|
|
|
/** Adds an event listener to a target **/ |
|
export function ael<K extends keyof WindowEventMap,>(target: Window, type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): Window |
|
export function ael<K extends keyof DocumentEventMap,>(target: Document, type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): Document |
|
export function ael<K extends keyof HTMLElementEventMap, T extends HTMLElement,>(target: T, type: K, listener: (this: T, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): T |
|
export function ael<T extends EventTarget,>(target: T, type: string, listener: Function, options?: boolean | AddEventListenerOptions): T |
|
export function ael(target: EventTarget | string, type: string | Function, listener?: Function | boolean | AddEventListenerOptions, options?: boolean | AddEventListenerOptions): EventTarget { |
|
let base: EventTarget = doc |
|
let eventType: string |
|
let callback: Function |
|
let eventOptions: boolean | AddEventListenerOptions | undefined |
|
if (typeof target === 'string') { |
|
eventType = target |
|
callback = type as Function |
|
eventOptions = listener as (boolean | AddEventListenerOptions | undefined)} |
|
else { |
|
base = target |
|
eventType = type as string |
|
callback = listener as Function |
|
eventOptions = options} |
|
if (typeof callback === 'function') { |
|
base.addEventListener(eventType, callback as EventListener, eventOptions)} |
|
else { |
|
console.warn('ael: Provided listener is not a function.', { target, type, listener, options })} |
|
return base} |
|
|
|
/** Adds a DOMContentLoaded listener **/ |
|
export const onl=(f: Function): Document =>ael(doc, 'DOMContentLoaded',f) |
|
/** Adds a click listener to an element **/ |
|
export const onc=<T extends EventTarget,>(e: T, f: Function): T =>ael(e,'click',f) |
|
/** Creates and returns a mutation observer **/ |
|
export function mob(cb: MutationCallback, e?: Node, c?: MutationObserverInit): MutationObserver { |
|
let res = new MutationObserver(cb) |
|
if (e) res.observe(e,c||{childList:true, subtree:true}) |
|
return res} |
|
// #endregion |
|
|
|
// #region glosh |
|
|
|
import * as sh from './sh.mjs' // Keep as .mjs if that's the compiled output expected elsewhere |
|
type ShModule = typeof sh; |
|
|
|
export function glosh(ctx0?: any, quiet?: boolean) { |
|
let ctx = ctx0 ?? globalThis as any // Use 'as any' for broad compatibility if needed |
|
if (!quiet) { |
|
log(`installing sh.* on ${ctx?.constructor?.name ?? ctx}:`) |
|
log(`[${oks(sh).join(' ')}]`)} |
|
(oks(sh) as Array<keyof ShModule>).forEach(k => { |
|
if (k in sh) { ctx[k] = sh[k]; } |
|
})} |
|
|
|
(globalThis as any).$sh=glosh // $sh() will make all global |
|
glosh(glosh,true) // register $sh.xxx globally, quietly |
|
// #endregion |