Skip to content

Commit

Permalink
Merge pull request #3 from MindfulMinun/next
Browse files Browse the repository at this point in the history
1.2.0
  • Loading branch information
MindfulMinun committed Feb 16, 2023
2 parents d40e3a8 + 8d0e185 commit e566edf
Show file tree
Hide file tree
Showing 18 changed files with 1,487 additions and 269 deletions.
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 1.2.0

- core
- async.ts
- wait: Promise that resolves after a timeout
- EventStream: Moved EventStream from iterable.ts to async.ts
- pinkyPromise: Moved pinkyPromise from iterable.ts to async.ts
- iterable.ts
- Fixed range not working with a reversed range
- Deprecated choose and shuffle, use the methods on the Random class in rng.ts instead!
- EventStream: Moved EventStream from iterable.ts to async.ts
- pinkyPromise: Moqved pinkyPromise from iterable.ts to async.ts
- rng.ts
- Added RNG.chooseWithWeights
- string.ts
- Sorted stuff from helpers.ts into here
- tools
- structures.ts
- Common computer science data structures, such as a stack and queue
- graph
- Graph implementation, as well as a few common graph theory algorithms
- math/primes.ts
- Functions to to help use with prime numbers

# 1.1.0

- Add limit and limitAsync by @MindfulMinun in #1
- Add rng.ts by @MindfulMinun in #2

# 1.0.0 (Initial release)
145 changes: 145 additions & 0 deletions core/async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Promise that resolves after timeout, optionally with a value to resolve with.
*
* @example
* // Play hide and seek, count to 100 (seconds)
* // Both methods are identical
* wait(100e3).then(() => "Ready or not, here I come!").then(seek)
* wait(100e3, "Ready or not, here I come!").then(seek)
* @since 2020-06-23
*/
export function wait(timeout: number): Promise<void>
export function wait<T>(timeout: number, resolveWith: T): Promise<T>
export function wait<T>(timeout: number, resolveWith?: T): Promise<T | void> {
return new Promise(resolve => setTimeout(() => resolve(resolveWith), timeout))
}

/**
* Helper class to handle events and turn them into async iterables
*
* ```ts
* const stream = new EventStream<MouseEvent>()
*
* document.body.addEventListener('click', ev => stream.emit(ev))
* wait(10e3).then(() => stream.end())
*
* for await (const event of stream) {
* alert("Clicked!")
* }
* ```
* @since 2021-08-07
*/
export class EventStream<T> implements AsyncIterable<T> {
#done: boolean
#events: T[]
#resolve: () => void
#promise!: Promise<void>

constructor() {
this.#done = false
this.#events = []
this.#resolve = () => { }
this.#defer()
}

#defer() {
this.#promise = new Promise(r => this.#resolve = r)
}

async*[Symbol.asyncIterator]() {
while (!this.#done) {
await this.#promise
yield* this.#events
this.#events = []
}
}

/**
* Dispatches an event. For-await-of listeners of this class instance will recieve this event.
* Note that once an event is emitted, it cannot be cancelled.
* @since 2021-08-07
*/
emit(event: T) {
this.#events.push(event)
this.#resolve()
this.#defer()
}

/**
* Waits for a specific event to be emitted according to a predicate.
*
* @example
* const stream = new EventStream<MouseEvent>()
* // Some events happen...
* const click = stream.once(ev => ev.type === 'click')
* @since 2021-12-22
*/
once(predicate: (value: T) => boolean) {
return once(this, predicate)
}

/**
* Stops the iterator. This terminates for-await-of loops listening for events,
* and any newly dispatched events will not be sent to the iterator.
* @since 2021-08-07
*/
end() {
this.#done = true
this.#resolve()
}
}

/**
* Wait for any async iterable to yield a specific value according to a predicate.
*
* @example
* const stream = new EventStream<MouseEvent>()
* // Some events happen...
* const click = once(stream, ev => ev.type === 'click')
* @since 2021-12-22
*/
export async function once<T>(source: AsyncIterable<T>, predicate: (value: T) => boolean) {
for await (const value of source) {
if (predicate(value)) return value
}
}



/**
* A "destructured" Promise, useful for waiting for callbacks.
* @example
* // When awaited, this function returns a promise that resolves to
* // an *OPEN* WebSocket
* () => {
* const sock = new WebSocket('wss://wss.example.com')
* const [p, res, rej] = pinkyPromise<typeof sock>()
* sock.onerror = err => rej(err)
* sock.onopen = () => res(sock)
* return p
* }
*
* @author MindfulMinun
* @since 2022-06-05
*/
export function pinkyPromise(): [Promise<void>, () => void, (reason?: unknown) => void]
export function pinkyPromise<T>(): [Promise<T>, (value: T) => void, (reason?: unknown) => void]
export function pinkyPromise<T>(): [
Promise<T | void>,
(value?: T | PromiseLike<T>) => void,
(reason?: unknown) => void
] {
let resolve: (value?: T | PromiseLike<T>) => void
let reject: (error: unknown) => void

// This works because callbacks to the Promise constructor are called synchronously.
const p = new Promise<T | void>((yay, nay) => {
resolve = value => yay(value)
reject = reason => nay(reason)
})

// HACK: This works because callbacks to the Promise constructor are called synchronously.
// In other words, they're immediately invoked, so they're available immediately after
// the assignment to `p`
return [p, resolve!, reject!]
}
90 changes: 80 additions & 10 deletions dom.ts → core/dom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// <reference lib="dom" />
import { templateNoop, HTML_ESCAPES, xss } from "./helpers.ts"
import { templateNoop, HTML_ESCAPES, xss } from "./string.ts"

export { HTML_ESCAPES }

Expand All @@ -17,6 +17,7 @@ export { HTML_ESCAPES }
* ${html`<em>Look ma, nested HTML!</em>`}
* ${`<img src="/bogus/path" onerror="alert('xss')">`}
* </strong>`
*
* // DocumentFragment
* // └─ strong
* // ├─ em
Expand All @@ -27,8 +28,8 @@ export { HTML_ESCAPES }
* @since 2020-06-23
*/
export function html(content: string): DocumentFragment
export function html(strings: TemplateStringsArray, ...exprs: any[]): DocumentFragment
export function html(strings: TemplateStringsArray | string, ...exprs: any[]): DocumentFragment {
export function html(strings: TemplateStringsArray, ...exprs: unknown[]): DocumentFragment
export function html(strings: TemplateStringsArray | string, ...exprs: unknown[]): DocumentFragment {
const temp = document.createElement("template")
let out = ""

Expand All @@ -45,7 +46,7 @@ export function html(strings: TemplateStringsArray | string, ...exprs: any[]): D
continue
}
// Otherwise, just join the expressions together, escaping dynamic content
out += strings[i] + (exprs[i] != null ? xss("" + exprs[i]) : "")
out += strings[i] + (typeof exprs[i] != 'undefined' ? xss("" + exprs[i]) : "")
}
temp.innerHTML = out
const clone = temp.content.cloneNode(true) as DocumentFragment
Expand All @@ -55,7 +56,7 @@ export function html(strings: TemplateStringsArray | string, ...exprs: any[]): D
if (!(node instanceof HTMLElement)) return
if (!node.parentNode) return
const index = parseInt(node.getAttribute("data-replaceindex") || "", 10)
node.parentNode.replaceChild(exprs[index], node);
node.parentNode.replaceChild(exprs[index] as Node, node);
})

return clone
Expand All @@ -67,18 +68,87 @@ export function html(strings: TemplateStringsArray | string, ...exprs: any[]): D
* @since 2022-06-04
*/
export function textNode(templ: string): Text
export function textNode(templ: TemplateStringsArray, ...values: any[]): Text
export function textNode(templ: string | TemplateStringsArray, ...values: any[]): Text {
export function textNode(templ: TemplateStringsArray, ...values: unknown[]): Text
export function textNode(templ: string | TemplateStringsArray, ...values: unknown[]): Text {
const string = templateNoop(templ, ...values)
return document.createTextNode(string)
}

/**
* FLIP is a mnemonic device for effective JavaScript animations: First, Last, Invert, Play.
* This helper class makes it easy to perform FLIP animations.
*
* https://aerotwist.com/blog/flip-your-animations/
*
* @author MindfulMinun
* @since 2022-12-31
*/
class _FlipAnimator {
el: Element
#first: DOMRect | null = null

constructor(el: Element) {
this.el = el
this.#first = null
}

/**
* This method immediately performs a FLIP animation.
* The caller must pass in a callback that describes the transition between the first and last states.
*/
async flip(cb: (this: Element, el: Element, rect: DOMRect) => Promise<void> | void, options: KeyframeAnimationOptions = {}) {
// Get the current state of the element
this.recordFirstState()

// Call the callback. The callback should change the element's DOMRect.
await cb.call(this.el, this.el, this.#first!)

// Animate the element from the first state to the last state
return this.animateFromFirst(options)
}

/**
* Captures the initial DOMRect of the element.
* The caller may also pass in a DOMRect to use instead of querying the DOM.
*/
recordFirstState(rect: DOMRect = this.el.getBoundingClientRect()) {
this.#first = rect
return this
}

/**
* Using the captured first state, this method will animate the element
* from the first state to the last state.
* Then this method will clear the DOMRects.
*/
animateFromFirst(options: KeyframeAnimationOptions = {}) {
if (!this.#first) throw new Error('No first state recorded.')
const last = this.el.getBoundingClientRect()

const deltaX = this.#first.left - last.left
const deltaY = this.#first.top - last.top
const scaleX = this.#first.width / last.width
const scaleY = this.#first.height / last.height

this.#first = null

return this.el.animate([
{ transform: `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})` },
{ transform: 'none' }
], {
duration: 300,
easing: 'ease-in-out',
...options
})
}
}

/**
* Walks the DOM recursively, calling the callback for each node.
* @deprecated
* @param {Node} root The root of the DOM walk
* @param {number} [filters] A bitmask of what nodes to show. Defaults to `NodeFilter.SHOW_ELEMENT`.
* @param {(node: Node, root: Node, walker: TreeWalker) => boolean} callback If this callback returns true, stop tree traversal.
* @param root - The root of the DOM walk
* @param callback - If this callback returns true, stop tree traversal.
* @param filters - A bitmask of what nodes to show. Defaults to `NodeFilter.SHOW_ELEMENT`.
* @author MindfulMinun
* @since 2020-06-29
*/
Expand Down
Loading

0 comments on commit e566edf

Please sign in to comment.