Skip to content

Instantly share code, notes, and snippets.

@tangentstorm
Last active May 14, 2025 20:29
Show Gist options
  • Save tangentstorm/4f271600fc20404150e05c373109551d to your computer and use it in GitHub Desktop.
Save tangentstorm/4f271600fc20404150e05c373109551d to your computer and use it in GitHub Desktop.
sh.mts: javascript shorthand (mostly for web/dom stuff)
// 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
@tangentstorm
Copy link
Author

tangentstorm commented Mar 21, 2024

@tangentstorm
Copy link
Author

example usage

image

// 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