-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(utils): add new utilities (#226)
- Loading branch information
Showing
9 changed files
with
262 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
class SystemError extends Error { | ||
constructor(message: string, scope: string) { | ||
const completeMessage = `[TAPSI][${scope}]: ${message}`; | ||
|
||
super(completeMessage); | ||
|
||
this.name = "SystemError"; | ||
} | ||
} | ||
|
||
export default SystemError; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import type { ReactiveController, ReactiveControllerHost } from "lit"; | ||
import type { DirectiveResult } from "lit/async-directive"; | ||
import { live, type LiveDirective } from "lit/directives/live.js"; | ||
import kebabToCamel from "../kebab-to-camel"; | ||
import SystemError from "../SystemError"; | ||
|
||
class ControlledPropController< | ||
T, | ||
H extends ReactiveControllerHost = ReactiveControllerHost, | ||
> implements ReactiveController | ||
{ | ||
private _host: ReactiveControllerHost; | ||
|
||
private _isControlled: boolean = false; | ||
private _propKey: PropertyKey; | ||
private _controlBehaviorPropKey: PropertyKey; | ||
|
||
constructor(host: H, propKey: keyof H, controlBehaviorPropKey?: string) { | ||
host.addController(this); | ||
|
||
this._host = host; | ||
this._propKey = propKey; | ||
|
||
if (typeof controlBehaviorPropKey === "undefined") { | ||
let key: PropertyKey; | ||
|
||
if (typeof propKey === "symbol") { | ||
key = Symbol(kebabToCamel(`controlled-${propKey.description}`)); | ||
} else { | ||
key = kebabToCamel(`controlled-${String(propKey)}`); | ||
} | ||
|
||
this._controlBehaviorPropKey = key; | ||
} else this._controlBehaviorPropKey = controlBehaviorPropKey; | ||
} | ||
|
||
private _getPropDescriptor(propKey: PropertyKey): PropertyDescriptor { | ||
const proto = Object.getPrototypeOf(this._host) as object; | ||
const prop = Object.getOwnPropertyDescriptor(proto, propKey); | ||
|
||
if (!prop) { | ||
throw new SystemError( | ||
[ | ||
`The required member \`${String(propKey)}\` is not present in the prototype.`, | ||
"Please ensure it is included for correct functionality.", | ||
].join("\n"), | ||
`${proto.constructor.name}`, | ||
); | ||
} | ||
|
||
return prop; | ||
} | ||
|
||
private _getControlledProp(): PropertyDescriptor { | ||
return this._getPropDescriptor(this._propKey); | ||
} | ||
|
||
private _getBehaviorProp(): PropertyDescriptor { | ||
return this._getPropDescriptor(this._controlBehaviorPropKey); | ||
} | ||
|
||
private _setProp(newValue: T): void { | ||
const prop = this._getControlledProp(); | ||
|
||
prop.set?.call(this._host, newValue); | ||
} | ||
|
||
public get isControlled(): boolean { | ||
return this._isControlled; | ||
} | ||
|
||
public get value(): T { | ||
return this._getControlledProp().get?.call(this._host) as T; | ||
} | ||
|
||
public get liveInputBinding(): DirectiveResult<typeof LiveDirective> { | ||
return live(this.value); | ||
} | ||
|
||
public set value(newValue: T) { | ||
if (this._isControlled) { | ||
this._host.requestUpdate(); | ||
|
||
return; | ||
} | ||
|
||
this._setProp(newValue); | ||
} | ||
|
||
hostConnected(): void { | ||
const behaviorProp = this._getBehaviorProp(); | ||
|
||
this._isControlled = Boolean(behaviorProp.get?.call(this._host)); | ||
} | ||
|
||
hostDisconnected(): void {} | ||
} | ||
|
||
export default ControlledPropController; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as ControlledPropController } from "./controlled-prop"; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { isHTMLElement, isShadowRoot, isWindow } from "./is"; | ||
|
||
export const getWindow = (node: Node | Window): Window => { | ||
if (!node) return window; | ||
|
||
if (!isWindow(node)) { | ||
const ownerDocument = node.ownerDocument; | ||
|
||
return ownerDocument ? ownerDocument.defaultView || window : window; | ||
} | ||
|
||
return node; | ||
}; | ||
|
||
export const getDocumentElement = (node: Node | Window): HTMLElement => | ||
( | ||
(node instanceof Node ? node.ownerDocument : node.document) ?? | ||
window.document | ||
).documentElement; | ||
|
||
export const getNodeName = (node: Node | Window): string => | ||
isWindow(node) ? "" : node ? (node.nodeName || "").toLowerCase() : ""; | ||
|
||
export const getParentNode = (node: Node): Node => { | ||
if (getNodeName(node) === "html") return node; | ||
|
||
return ( | ||
// Step into the shadow DOM of the parent of a slotted node | ||
(<HTMLElement>node).assignedSlot || | ||
// DOM Element detected | ||
node.parentNode || | ||
// ShadowRoot detected | ||
(isShadowRoot(node) ? node.host : null) || | ||
// Fallback | ||
getDocumentElement(node) | ||
); | ||
}; | ||
|
||
export const getBoundingClientRect = ( | ||
element: Element, | ||
includeScale = false, | ||
) => { | ||
const clientRect = element.getBoundingClientRect(); | ||
|
||
let scaleX = 1; | ||
let scaleY = 1; | ||
|
||
if (includeScale && isHTMLElement(element)) { | ||
scaleX = | ||
element.offsetWidth > 0 | ||
? Math.round(clientRect.width) / element.offsetWidth || 1 | ||
: 1; | ||
scaleY = | ||
element.offsetHeight > 0 | ||
? Math.round(clientRect.height) / element.offsetHeight || 1 | ||
: 1; | ||
} | ||
|
||
return { | ||
width: clientRect.width / scaleX, | ||
height: clientRect.height / scaleY, | ||
top: clientRect.top / scaleY, | ||
right: clientRect.right / scaleX, | ||
bottom: clientRect.bottom / scaleY, | ||
left: clientRect.left / scaleX, | ||
x: clientRect.left / scaleX, | ||
y: clientRect.top / scaleY, | ||
}; | ||
}; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./get"; | ||
export * from "./is"; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
export const isWindow = <T extends { toString?: () => string }>( | ||
input: unknown, | ||
): input is Window => | ||
!input ? false : (input as T).toString?.() === "[object Window]"; | ||
|
||
export const isElement = (input: unknown): input is Element => | ||
input instanceof Element; | ||
|
||
export const isHTMLElement = (input: unknown): input is HTMLElement => | ||
input instanceof HTMLElement; | ||
|
||
export const isHTMLInputElement = (input: unknown): input is HTMLInputElement => | ||
input instanceof HTMLInputElement; | ||
|
||
export const isNode = (input: unknown): input is Node => input instanceof Node; | ||
|
||
export const isShadowRoot = (node: Node): node is ShadowRoot => | ||
node instanceof ShadowRoot || node instanceof ShadowRoot; | ||
|
||
export const contains = (parent: Element, child: Element): boolean => { | ||
if (parent.contains(child)) return true; | ||
|
||
const rootNode = child.getRootNode?.(); | ||
|
||
// Fallback to custom implementation with Shadow DOM support | ||
if (rootNode && isShadowRoot(rootNode)) { | ||
let next: Node = child; | ||
|
||
do { | ||
if (next && parent === next) return true; | ||
|
||
next = next.parentNode || (next as unknown as ShadowRoot).host; | ||
} while (next); | ||
} | ||
|
||
return false; | ||
}; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
export * from "./controllers"; | ||
export * from "./dom"; | ||
export * from "./events"; | ||
export * from "./mixins"; | ||
|
||
export { default as debounce } from "./debounce"; | ||
export { default as kebabToCamel } from "./kebab-to-camel"; | ||
export { default as logger } from "./logger"; | ||
export { default as SystemError } from "./SystemError"; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
const kebabToCamel = (kebabcase: string) => | ||
kebabcase.replace(/-./g, x => x.toUpperCase()?.[1] ?? ""); | ||
|
||
export default kebabToCamel; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/** | ||
* Returns value wrapped to the inclusive range of `min` and `max`. | ||
*/ | ||
export const wrap = (number: number, min: number, max: number): number => | ||
min + ((((number - min) % (max - min)) + (max - min)) % (max - min)); | ||
|
||
/** | ||
* Returns value clamped to the inclusive range of `min` and `max`. | ||
*/ | ||
export const clamp = (number: number, min: number, max: number): number => | ||
Math.max(Math.min(number, max), min); | ||
|
||
/** | ||
* Linear interpolate on the scale given by `a` to `b`, using `t` as the point on that scale. | ||
*/ | ||
export const lerp = (a: number, b: number, t: number) => a + t * (b - a); | ||
|
||
/** | ||
* Inverse Linar Interpolation, get the fraction between `a` and `b` on which `v` resides. | ||
*/ | ||
export const inLerp = (a: number, b: number, v: number) => (v - a) / (b - a); | ||
|
||
/** | ||
* Remap values from one linear scale to another. | ||
* | ||
* `oMin` and `oMax` are the scale on which the original value resides, | ||
* `rMin` and `rMax` are the scale to which it should be mapped. | ||
*/ | ||
export const remap = ( | ||
v: number, | ||
oMin: number, | ||
oMax: number, | ||
rMin: number, | ||
rMax: number, | ||
) => lerp(rMin, rMax, inLerp(oMin, oMax, v)); |