Last active
May 14, 2025 20:29
-
-
Save tangentstorm/4f271600fc20404150e05c373109551d to your computer and use it in GitHub Desktop.
sh.mts: javascript shorthand (mostly for web/dom stuff)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
example usage
// terse animation
app(iht(doc.body,''),
ce('input',{id:'scrub',type:'range',min:0,max:100,value:0}),
ce('br'),
S({style:'background:gold'},[
R({fill:'red',y:-50}),
...im(7,i=>C({cx:50*i-150,cy:0,r:10,id:'c'+i}))]))
a=akf(ei.c1,[0,100,0,-100,0].map(x=>({cy:x})),
{duration:1000,iterations:2,fill:'forwards'})
ei.scrub.oninput=e=>
a.currentTime=a.effect.getTiming().duration*ei.scrub.value/100
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
see also ngn's https://github.com/Dyalog/ride/blob/master/docs/coding-style.txt
and https://codeberg.org/ngn/k/src/branch/master/w/k.js