@thomas-siegfried/tapout 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1206 -0
- package/dist/applyBindings.d.ts +10 -0
- package/dist/applyBindings.d.ts.map +1 -0
- package/dist/applyBindings.js +261 -0
- package/dist/applyBindings.js.map +1 -0
- package/dist/arrayToDomMapping.d.ts +14 -0
- package/dist/arrayToDomMapping.d.ts.map +1 -0
- package/dist/arrayToDomMapping.js +219 -0
- package/dist/arrayToDomMapping.js.map +1 -0
- package/dist/attributeInterpolationMarkup.d.ts +6 -0
- package/dist/attributeInterpolationMarkup.d.ts.map +1 -0
- package/dist/attributeInterpolationMarkup.js +70 -0
- package/dist/attributeInterpolationMarkup.js.map +1 -0
- package/dist/bindingContext.d.ts +38 -0
- package/dist/bindingContext.d.ts.map +1 -0
- package/dist/bindingContext.js +138 -0
- package/dist/bindingContext.js.map +1 -0
- package/dist/bindingEvent.d.ts +33 -0
- package/dist/bindingEvent.d.ts.map +1 -0
- package/dist/bindingEvent.js +144 -0
- package/dist/bindingEvent.js.map +1 -0
- package/dist/bindingProvider.d.ts +38 -0
- package/dist/bindingProvider.d.ts.map +1 -0
- package/dist/bindingProvider.js +145 -0
- package/dist/bindingProvider.js.map +1 -0
- package/dist/bindings.d.ts +2 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +862 -0
- package/dist/bindings.js.map +1 -0
- package/dist/compareArrays.d.ts +13 -0
- package/dist/compareArrays.d.ts.map +1 -0
- package/dist/compareArrays.js +91 -0
- package/dist/compareArrays.js.map +1 -0
- package/dist/componentBinding.d.ts +4 -0
- package/dist/componentBinding.d.ts.map +1 -0
- package/dist/componentBinding.js +240 -0
- package/dist/componentBinding.js.map +1 -0
- package/dist/componentDecorator.d.ts +22 -0
- package/dist/componentDecorator.d.ts.map +1 -0
- package/dist/componentDecorator.js +32 -0
- package/dist/componentDecorator.js.map +1 -0
- package/dist/components.d.ts +34 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +256 -0
- package/dist/components.js.map +1 -0
- package/dist/computed.d.ts +54 -0
- package/dist/computed.d.ts.map +1 -0
- package/dist/computed.js +376 -0
- package/dist/computed.js.map +1 -0
- package/dist/configure.d.ts +4 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +48 -0
- package/dist/configure.js.map +1 -0
- package/dist/controlFlowBindings.d.ts +2 -0
- package/dist/controlFlowBindings.d.ts.map +1 -0
- package/dist/controlFlowBindings.js +151 -0
- package/dist/controlFlowBindings.js.map +1 -0
- package/dist/core.d.ts +29 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +27 -0
- package/dist/core.js.map +1 -0
- package/dist/decorators.d.ts +40 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +276 -0
- package/dist/decorators.js.map +1 -0
- package/dist/dependencyDetection.d.ts +21 -0
- package/dist/dependencyDetection.d.ts.map +1 -0
- package/dist/dependencyDetection.js +53 -0
- package/dist/dependencyDetection.js.map +1 -0
- package/dist/disposable.d.ts +12 -0
- package/dist/disposable.d.ts.map +1 -0
- package/dist/disposable.js +29 -0
- package/dist/disposable.js.map +1 -0
- package/dist/domData.d.ts +6 -0
- package/dist/domData.d.ts.map +1 -0
- package/dist/domData.js +40 -0
- package/dist/domData.js.map +1 -0
- package/dist/domNodeDisposal.d.ts +7 -0
- package/dist/domNodeDisposal.d.ts.map +1 -0
- package/dist/domNodeDisposal.js +78 -0
- package/dist/domNodeDisposal.js.map +1 -0
- package/dist/effects.d.ts +13 -0
- package/dist/effects.d.ts.map +1 -0
- package/dist/effects.js +24 -0
- package/dist/effects.js.map +1 -0
- package/dist/event.d.ts +38 -0
- package/dist/event.d.ts.map +1 -0
- package/dist/event.js +138 -0
- package/dist/event.js.map +1 -0
- package/dist/expressionRewriting.d.ts +22 -0
- package/dist/expressionRewriting.d.ts.map +1 -0
- package/dist/expressionRewriting.js +169 -0
- package/dist/expressionRewriting.js.map +1 -0
- package/dist/extenders.d.ts +17 -0
- package/dist/extenders.d.ts.map +1 -0
- package/dist/extenders.js +75 -0
- package/dist/extenders.js.map +1 -0
- package/dist/filters.d.ts +5 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +81 -0
- package/dist/filters.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolationMarkup.d.ts +6 -0
- package/dist/interpolationMarkup.d.ts.map +1 -0
- package/dist/interpolationMarkup.js +110 -0
- package/dist/interpolationMarkup.js.map +1 -0
- package/dist/memoization.d.ts +7 -0
- package/dist/memoization.d.ts.map +1 -0
- package/dist/memoization.js +59 -0
- package/dist/memoization.js.map +1 -0
- package/dist/namespacedBindings.d.ts +7 -0
- package/dist/namespacedBindings.d.ts.map +1 -0
- package/dist/namespacedBindings.js +81 -0
- package/dist/namespacedBindings.js.map +1 -0
- package/dist/observable.d.ts +13 -0
- package/dist/observable.d.ts.map +1 -0
- package/dist/observable.js +39 -0
- package/dist/observable.js.map +1 -0
- package/dist/observableArray.d.ts +58 -0
- package/dist/observableArray.d.ts.map +1 -0
- package/dist/observableArray.js +328 -0
- package/dist/observableArray.js.map +1 -0
- package/dist/options.d.ts +14 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +12 -0
- package/dist/options.js.map +1 -0
- package/dist/selectExtensions.d.ts +4 -0
- package/dist/selectExtensions.d.ts.map +1 -0
- package/dist/selectExtensions.js +67 -0
- package/dist/selectExtensions.js.map +1 -0
- package/dist/slotBinding.d.ts +2 -0
- package/dist/slotBinding.d.ts.map +1 -0
- package/dist/slotBinding.js +63 -0
- package/dist/slotBinding.js.map +1 -0
- package/dist/subscribable.d.ts +57 -0
- package/dist/subscribable.d.ts.map +1 -0
- package/dist/subscribable.js +211 -0
- package/dist/subscribable.js.map +1 -0
- package/dist/tasks.d.ts +8 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +68 -0
- package/dist/tasks.js.map +1 -0
- package/dist/templateEngine.d.ts +37 -0
- package/dist/templateEngine.d.ts.map +1 -0
- package/dist/templateEngine.js +60 -0
- package/dist/templateEngine.js.map +1 -0
- package/dist/templateRendering.d.ts +8 -0
- package/dist/templateRendering.d.ts.map +1 -0
- package/dist/templateRendering.js +322 -0
- package/dist/templateRendering.js.map +1 -0
- package/dist/templateRewriting.d.ts +7 -0
- package/dist/templateRewriting.d.ts.map +1 -0
- package/dist/templateRewriting.js +63 -0
- package/dist/templateRewriting.js.map +1 -0
- package/dist/templateSources.d.ts +19 -0
- package/dist/templateSources.d.ts.map +1 -0
- package/dist/templateSources.js +110 -0
- package/dist/templateSources.js.map +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +100 -0
- package/dist/utils.js.map +1 -0
- package/dist/utilsDom.d.ts +8 -0
- package/dist/utilsDom.d.ts.map +1 -0
- package/dist/utilsDom.js +81 -0
- package/dist/utilsDom.js.map +1 -0
- package/dist/virtualElements.d.ts +12 -0
- package/dist/virtualElements.d.ts.map +1 -0
- package/dist/virtualElements.js +141 -0
- package/dist/virtualElements.js.map +1 -0
- package/dist/wireParams.d.ts +6 -0
- package/dist/wireParams.d.ts.map +1 -0
- package/dist/wireParams.js +39 -0
- package/dist/wireParams.js.map +1 -0
- package/package.json +55 -0
- package/src/applyBindings.ts +372 -0
- package/src/arrayToDomMapping.ts +300 -0
- package/src/attributeInterpolationMarkup.ts +91 -0
- package/src/bindingContext.ts +207 -0
- package/src/bindingEvent.ts +198 -0
- package/src/bindingProvider.ts +260 -0
- package/src/bindings.ts +963 -0
- package/src/compareArrays.ts +122 -0
- package/src/componentBinding.ts +318 -0
- package/src/componentDecorator.ts +62 -0
- package/src/components.ts +367 -0
- package/src/computed.ts +439 -0
- package/src/configure.ts +52 -0
- package/src/controlFlowBindings.ts +206 -0
- package/src/core.ts +60 -0
- package/src/decorators.ts +407 -0
- package/src/dependencyDetection.ts +76 -0
- package/src/disposable.ts +36 -0
- package/src/domData.ts +42 -0
- package/src/domNodeDisposal.ts +94 -0
- package/src/effects.ts +29 -0
- package/src/event.ts +173 -0
- package/src/expressionRewriting.ts +219 -0
- package/src/extenders.ts +102 -0
- package/src/filters.ts +91 -0
- package/src/index.ts +150 -0
- package/src/interpolationMarkup.ts +130 -0
- package/src/memoization.ts +71 -0
- package/src/namespacedBindings.ts +132 -0
- package/src/observable.ts +48 -0
- package/src/observableArray.ts +397 -0
- package/src/options.ts +25 -0
- package/src/selectExtensions.ts +68 -0
- package/src/slotBinding.ts +84 -0
- package/src/subscribable.ts +266 -0
- package/src/tasks.ts +79 -0
- package/src/templateEngine.ts +108 -0
- package/src/templateRendering.ts +399 -0
- package/src/templateRewriting.ts +94 -0
- package/src/templateSources.ts +134 -0
- package/src/utils.ts +123 -0
- package/src/utilsDom.ts +87 -0
- package/src/virtualElements.ts +153 -0
- package/src/wireParams.ts +49 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { domDataGet, domDataSet, domDataClear, domDataNextKey } from './domData.js';
|
|
2
|
+
import { ignore } from './dependencyDetection.js';
|
|
3
|
+
|
|
4
|
+
type DisposeCallback = (node: Node) => void;
|
|
5
|
+
|
|
6
|
+
const disposeCallbacksKey = domDataNextKey();
|
|
7
|
+
|
|
8
|
+
const CLEANABLE: Record<number, boolean> = { 1: true, 8: true, 9: true };
|
|
9
|
+
const CLEANABLE_WITH_DESCENDANTS: Record<number, boolean> = { 1: true, 9: true };
|
|
10
|
+
|
|
11
|
+
function getCallbacks(node: Node, create: true): DisposeCallback[];
|
|
12
|
+
function getCallbacks(node: Node, create: false): DisposeCallback[] | undefined;
|
|
13
|
+
function getCallbacks(node: Node, create: boolean): DisposeCallback[] | undefined {
|
|
14
|
+
let callbacks = domDataGet(node, disposeCallbacksKey) as DisposeCallback[] | undefined;
|
|
15
|
+
if (!callbacks && create) {
|
|
16
|
+
callbacks = [];
|
|
17
|
+
domDataSet(node, disposeCallbacksKey, callbacks);
|
|
18
|
+
}
|
|
19
|
+
return callbacks;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function destroyCallbacks(node: Node): void {
|
|
23
|
+
domDataSet(node, disposeCallbacksKey, undefined);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function cleanSingleNode(node: Node): void {
|
|
27
|
+
const callbacks = getCallbacks(node, false);
|
|
28
|
+
if (callbacks) {
|
|
29
|
+
const snapshot = callbacks.slice(0);
|
|
30
|
+
for (let i = 0; i < snapshot.length; i++) {
|
|
31
|
+
snapshot[i](node);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
domDataClear(node);
|
|
36
|
+
|
|
37
|
+
if (CLEANABLE_WITH_DESCENDANTS[node.nodeType]) {
|
|
38
|
+
cleanNodesInList(node.childNodes, true);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function cleanNodesInList(nodeList: ArrayLike<Node>, onlyComments?: boolean): void {
|
|
43
|
+
const cleaned: Node[] = [];
|
|
44
|
+
let lastCleaned: Node;
|
|
45
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
46
|
+
if (!onlyComments || nodeList[i].nodeType === 8) {
|
|
47
|
+
cleanSingleNode(cleaned[cleaned.length] = lastCleaned = nodeList[i]);
|
|
48
|
+
if (nodeList[i] !== lastCleaned) {
|
|
49
|
+
while (i-- && cleaned.indexOf(nodeList[i]) === -1) { /* adjust index */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function addDisposeCallback(node: Node, callback: DisposeCallback): void {
|
|
56
|
+
if (typeof callback !== 'function') {
|
|
57
|
+
throw new Error('Callback must be a function');
|
|
58
|
+
}
|
|
59
|
+
getCallbacks(node, true).push(callback);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function removeDisposeCallback(node: Node, callback: DisposeCallback): void {
|
|
63
|
+
const callbacks = getCallbacks(node, false);
|
|
64
|
+
if (callbacks) {
|
|
65
|
+
const index = callbacks.indexOf(callback);
|
|
66
|
+
if (index >= 0) {
|
|
67
|
+
callbacks.splice(index, 1);
|
|
68
|
+
if (callbacks.length === 0) {
|
|
69
|
+
destroyCallbacks(node);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function cleanNode(node: Node): Node {
|
|
76
|
+
ignore(() => {
|
|
77
|
+
if (CLEANABLE[node.nodeType]) {
|
|
78
|
+
cleanSingleNode(node);
|
|
79
|
+
|
|
80
|
+
if (CLEANABLE_WITH_DESCENDANTS[node.nodeType]) {
|
|
81
|
+
const descendants = (node as Element).getElementsByTagName('*');
|
|
82
|
+
cleanNodesInList(descendants);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return node;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function removeNode(node: Node): void {
|
|
90
|
+
cleanNode(node);
|
|
91
|
+
if (node.parentNode) {
|
|
92
|
+
node.parentNode.removeChild(node);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/effects.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PureComputed } from './computed.js';
|
|
2
|
+
|
|
3
|
+
export interface EffectHandle {
|
|
4
|
+
dispose(): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Track reactive dependencies in `fn` and call `act` whenever they change.
|
|
9
|
+
* Returns a handle to stop the effect.
|
|
10
|
+
*/
|
|
11
|
+
export function observe<T>(fn: () => T, act: (val: T) => void): EffectHandle {
|
|
12
|
+
const computed = new PureComputed(fn);
|
|
13
|
+
computed.extend({ notify: 'always' });
|
|
14
|
+
const subscription = computed.subscribe(act);
|
|
15
|
+
return {
|
|
16
|
+
dispose() {
|
|
17
|
+
subscription.dispose();
|
|
18
|
+
computed.dispose();
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Same as `observe`, but also runs `act` immediately with the current value.
|
|
25
|
+
*/
|
|
26
|
+
export function effect<T>(fn: () => T, act: (val: T) => void): EffectHandle {
|
|
27
|
+
act(fn());
|
|
28
|
+
return observe(fn, act);
|
|
29
|
+
}
|
package/src/event.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { addDisposeCallback } from './domNodeDisposal.js';
|
|
2
|
+
|
|
3
|
+
export type EventCallback<T> = (value: T) => void;
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
export type Constructor<T> = abstract new (...args: any[]) => T;
|
|
7
|
+
|
|
8
|
+
interface SubscriptionEntry<T> {
|
|
9
|
+
callback: EventCallback<T>;
|
|
10
|
+
disposed: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class EventSubscription {
|
|
14
|
+
private _disposeCallback: (() => void) | null;
|
|
15
|
+
private _isDisposed = false;
|
|
16
|
+
|
|
17
|
+
constructor(disposeCallback: () => void) {
|
|
18
|
+
this._disposeCallback = disposeCallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
dispose(): void {
|
|
22
|
+
if (!this._isDisposed) {
|
|
23
|
+
this._isDisposed = true;
|
|
24
|
+
this._disposeCallback!();
|
|
25
|
+
this._disposeCallback = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get closed(): boolean {
|
|
30
|
+
return this._isDisposed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
disposeWhenNodeIsRemoved(node: Node): void {
|
|
34
|
+
addDisposeCallback(node, () => this.dispose());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class EventSubscribable<T = unknown> {
|
|
39
|
+
#entries: SubscriptionEntry<T>[] = [];
|
|
40
|
+
#isDisposed = false;
|
|
41
|
+
|
|
42
|
+
subscribe(callback: EventCallback<T>): EventSubscription {
|
|
43
|
+
if (this.#isDisposed) {
|
|
44
|
+
throw new Error('EventSubscribable is disposed');
|
|
45
|
+
}
|
|
46
|
+
const entry: SubscriptionEntry<T> = { callback, disposed: false };
|
|
47
|
+
this.#entries.push(entry);
|
|
48
|
+
return new EventSubscription(() => {
|
|
49
|
+
entry.disposed = true;
|
|
50
|
+
const idx = this.#entries.indexOf(entry);
|
|
51
|
+
if (idx !== -1) {
|
|
52
|
+
this.#entries.splice(idx, 1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
on<U extends T>(type: Constructor<U>): EventSubscribable<U> {
|
|
58
|
+
return new FilteredEventSubscribable<T, U>(this, type);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get subscriberCount(): number {
|
|
62
|
+
return this.#entries.length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @internal */
|
|
66
|
+
_emit(value: T): void {
|
|
67
|
+
if (this.#isDisposed) return;
|
|
68
|
+
const snapshot = this.#entries.slice();
|
|
69
|
+
for (let i = 0; i < snapshot.length; i++) {
|
|
70
|
+
if (!snapshot[i].disposed) {
|
|
71
|
+
snapshot[i].callback(value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @internal */
|
|
77
|
+
_dispose(): void {
|
|
78
|
+
this.#isDisposed = true;
|
|
79
|
+
this.#entries.length = 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class FilteredEventSubscribable<T, U extends T> extends EventSubscribable<U> {
|
|
84
|
+
private _parent: EventSubscribable<T>;
|
|
85
|
+
private _type: Constructor<U>;
|
|
86
|
+
|
|
87
|
+
constructor(parent: EventSubscribable<T>, type: Constructor<U>) {
|
|
88
|
+
super();
|
|
89
|
+
this._parent = parent;
|
|
90
|
+
this._type = type;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override subscribe(callback: EventCallback<U>): EventSubscription {
|
|
94
|
+
return this._parent.subscribe((value: T) => {
|
|
95
|
+
if (value instanceof this._type) {
|
|
96
|
+
callback(value as U);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override on<V extends U>(type: Constructor<V>): EventSubscribable<V> {
|
|
102
|
+
return new FilteredEventSubscribable<T, V>(this._parent, type);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class Event<T = unknown> {
|
|
107
|
+
private _subscribable: EventSubscribable<T>;
|
|
108
|
+
private _isDisposed = false;
|
|
109
|
+
|
|
110
|
+
constructor() {
|
|
111
|
+
this._subscribable = new EventSubscribable<T>();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get subscribable(): EventSubscribable<T> {
|
|
115
|
+
return this._subscribable;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
emit(value: T): void {
|
|
119
|
+
if (this._isDisposed) {
|
|
120
|
+
throw new Error('Event is disposed');
|
|
121
|
+
}
|
|
122
|
+
this._subscribable._emit(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
dispose(): void {
|
|
126
|
+
if (!this._isDisposed) {
|
|
127
|
+
this._isDisposed = true;
|
|
128
|
+
this._subscribable._dispose();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get isDisposed(): boolean {
|
|
133
|
+
return this._isDisposed;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class AggregateEvent<T = unknown> extends Event<T> {
|
|
138
|
+
private _pipedSubscriptions: EventSubscription[] = [];
|
|
139
|
+
|
|
140
|
+
pipe(...sources: EventSubscribable<T>[]): EventSubscription[] {
|
|
141
|
+
const newSubs: EventSubscription[] = [];
|
|
142
|
+
for (const source of sources) {
|
|
143
|
+
const sub = source.subscribe((value: T) => {
|
|
144
|
+
this.emit(value);
|
|
145
|
+
});
|
|
146
|
+
this._pipedSubscriptions.push(sub);
|
|
147
|
+
newSubs.push(sub);
|
|
148
|
+
}
|
|
149
|
+
return newSubs;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
override dispose(): void {
|
|
153
|
+
for (const sub of this._pipedSubscriptions) {
|
|
154
|
+
if (!sub.closed) {
|
|
155
|
+
sub.dispose();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
this._pipedSubscriptions.length = 0;
|
|
159
|
+
super.dispose();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function isEvent(value: unknown): value is Event {
|
|
164
|
+
return value instanceof Event;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function isEventSubscribable(value: unknown): value is EventSubscribable {
|
|
168
|
+
return value instanceof EventSubscribable;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function isAggregateEvent(value: unknown): value is AggregateEvent {
|
|
172
|
+
return value instanceof AggregateEvent;
|
|
173
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Observable } from './observable.js';
|
|
2
|
+
import { Computed } from './computed.js';
|
|
3
|
+
import { isSubscribable } from './subscribable.js';
|
|
4
|
+
|
|
5
|
+
// ---- Types ----
|
|
6
|
+
|
|
7
|
+
export interface KeyValuePair {
|
|
8
|
+
key?: string;
|
|
9
|
+
value?: string;
|
|
10
|
+
unknown?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PreProcessOptions {
|
|
14
|
+
valueAccessors?: boolean;
|
|
15
|
+
bindingParams?: boolean;
|
|
16
|
+
getBindingHandler?: (key: string) => { preprocess?(val: string, key: string, addBinding: (key: string, val: string) => void): string | void } | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AllBindingsAccessor {
|
|
20
|
+
get(key: string): unknown;
|
|
21
|
+
has(key: string): boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---- Two-way binding registry ----
|
|
25
|
+
|
|
26
|
+
export const twoWayBindings: Record<string, boolean | string> = {};
|
|
27
|
+
|
|
28
|
+
// ---- Token regex ----
|
|
29
|
+
|
|
30
|
+
const javaScriptReservedWords = ['true', 'false', 'null', 'undefined'];
|
|
31
|
+
|
|
32
|
+
// Matches something assignable: an isolated identifier or something ending with a property accessor.
|
|
33
|
+
const javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;
|
|
34
|
+
|
|
35
|
+
const specials = ',"\'`{}()/:[\\]';
|
|
36
|
+
const bindingToken = RegExp([
|
|
37
|
+
'"(?:\\\\.|[^"])*"', // double-quoted string
|
|
38
|
+
"'(?:\\\\.|[^'])*'", // single-quoted string
|
|
39
|
+
'`(?:\\\\.|[^`])*`', // template literal
|
|
40
|
+
'/\\*(?:[^*]|\\*+[^*/])*\\*+/', // block comment
|
|
41
|
+
'//.*\n', // line comment
|
|
42
|
+
'/(?:\\\\.|[^/])+/\\w*', // regex literal
|
|
43
|
+
'[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']', // multi-char token
|
|
44
|
+
'[^\\s]', // single char
|
|
45
|
+
].join('|'), 'g');
|
|
46
|
+
|
|
47
|
+
const divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/;
|
|
48
|
+
const keywordRegexLookBehind: Record<string, number> = { 'in': 1, 'return': 1, 'typeof': 1 };
|
|
49
|
+
|
|
50
|
+
// ---- Core functions ----
|
|
51
|
+
|
|
52
|
+
function getWriteableValue(expression: string): string | false {
|
|
53
|
+
if (javaScriptReservedWords.indexOf(expression) >= 0) return false;
|
|
54
|
+
const match = expression.match(javaScriptAssignmentTarget);
|
|
55
|
+
if (match === null) return false;
|
|
56
|
+
return match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function parseObjectLiteral(objectLiteralString: string): KeyValuePair[] {
|
|
60
|
+
let str = objectLiteralString.trim();
|
|
61
|
+
|
|
62
|
+
// Strip surrounding braces
|
|
63
|
+
if (str.charCodeAt(0) === 123 /* { */) str = str.slice(1, -1);
|
|
64
|
+
|
|
65
|
+
// Append newline + comma so the last pair terminates uniformly
|
|
66
|
+
str += '\n,';
|
|
67
|
+
|
|
68
|
+
const toks = str.match(bindingToken);
|
|
69
|
+
const result: KeyValuePair[] = [];
|
|
70
|
+
|
|
71
|
+
if (!toks || toks.length <= 1) return result;
|
|
72
|
+
|
|
73
|
+
let key: string | undefined;
|
|
74
|
+
let values: string[] = [];
|
|
75
|
+
let depth = 0;
|
|
76
|
+
|
|
77
|
+
for (let i = 0, tok: string; (tok = toks[i]); ++i) {
|
|
78
|
+
const c = tok.charCodeAt(0);
|
|
79
|
+
|
|
80
|
+
if (c === 44 /* , */) {
|
|
81
|
+
if (depth <= 0) {
|
|
82
|
+
result.push(
|
|
83
|
+
(key && values.length)
|
|
84
|
+
? { key, value: values.join('') }
|
|
85
|
+
: { unknown: key || values.join('') },
|
|
86
|
+
);
|
|
87
|
+
key = undefined;
|
|
88
|
+
depth = 0;
|
|
89
|
+
values = [];
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
} else if (c === 58 /* : */) {
|
|
93
|
+
if (!depth && !key && values.length === 1) {
|
|
94
|
+
key = values.pop()!;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
} else if (c === 47 /* / */ && tok.length > 1 && (tok.charCodeAt(1) === 47 || tok.charCodeAt(1) === 42)) {
|
|
98
|
+
// Line or block comment — skip
|
|
99
|
+
continue;
|
|
100
|
+
} else if (c === 47 /* / */ && i && tok.length > 1) {
|
|
101
|
+
// Disambiguate regex vs division
|
|
102
|
+
const prevMatch = toks[i - 1].match(divisionLookBehind);
|
|
103
|
+
if (prevMatch && !keywordRegexLookBehind[prevMatch[0]]) {
|
|
104
|
+
str = str.substr(str.indexOf(tok) + 1);
|
|
105
|
+
const newToks = str.match(bindingToken);
|
|
106
|
+
if (newToks) {
|
|
107
|
+
toks.length = 0;
|
|
108
|
+
toks.push(...newToks);
|
|
109
|
+
}
|
|
110
|
+
i = -1;
|
|
111
|
+
tok = '/';
|
|
112
|
+
}
|
|
113
|
+
} else if (c === 40 || c === 123 || c === 91) {
|
|
114
|
+
// ( { [
|
|
115
|
+
++depth;
|
|
116
|
+
} else if (c === 41 || c === 125 || c === 93) {
|
|
117
|
+
// ) } ]
|
|
118
|
+
--depth;
|
|
119
|
+
} else if (!key && !values.length && (c === 34 || c === 39)) {
|
|
120
|
+
// Quoted key — strip quotes
|
|
121
|
+
tok = tok.slice(1, -1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
values.push(tok);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (depth > 0) {
|
|
128
|
+
throw new Error('Unbalanced parentheses, braces, or brackets');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function preProcessBindings(
|
|
135
|
+
bindingsStringOrKeyValueArray: string | KeyValuePair[],
|
|
136
|
+
options?: PreProcessOptions,
|
|
137
|
+
): string {
|
|
138
|
+
options = options || {};
|
|
139
|
+
|
|
140
|
+
const resultStrings: string[] = [];
|
|
141
|
+
const propertyAccessorResultStrings: string[] = [];
|
|
142
|
+
const makeValueAccessors = options.valueAccessors;
|
|
143
|
+
const bindingParams = options.bindingParams;
|
|
144
|
+
const getBindingHandler = options.getBindingHandler;
|
|
145
|
+
|
|
146
|
+
const keyValueArray = typeof bindingsStringOrKeyValueArray === 'string'
|
|
147
|
+
? parseObjectLiteral(bindingsStringOrKeyValueArray)
|
|
148
|
+
: bindingsStringOrKeyValueArray;
|
|
149
|
+
|
|
150
|
+
function processKeyValue(key: string, val: string) {
|
|
151
|
+
let writableVal: string | false;
|
|
152
|
+
|
|
153
|
+
function callPreprocessHook(
|
|
154
|
+
obj: { preprocess?(val: string, key: string, addBinding: (key: string, val: string) => void): string | void } | undefined,
|
|
155
|
+
): boolean {
|
|
156
|
+
if (obj && obj.preprocess) {
|
|
157
|
+
const result = obj.preprocess(val, key, processKeyValue);
|
|
158
|
+
if (result !== undefined) val = result;
|
|
159
|
+
return result !== undefined;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!bindingParams) {
|
|
165
|
+
if (getBindingHandler && !callPreprocessHook(getBindingHandler(key))) return;
|
|
166
|
+
|
|
167
|
+
if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) {
|
|
168
|
+
const writeKey = typeof twoWayBindings[key] === 'string' ? twoWayBindings[key] as string : key;
|
|
169
|
+
propertyAccessorResultStrings.push("'" + writeKey + "':function(_z){" + writableVal + '=_z}');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (makeValueAccessors) {
|
|
174
|
+
val = 'function(){return ' + val + ' }';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
resultStrings.push("'" + key + "':" + val);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const kv of keyValueArray) {
|
|
181
|
+
processKeyValue(kv.key || kv.unknown || '', kv.value || '');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (propertyAccessorResultStrings.length) {
|
|
185
|
+
processKeyValue('_tap_property_writers', '{' + propertyAccessorResultStrings.join(',') + ' }');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return resultStrings.join(',');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function keyValueArrayContainsKey(keyValueArray: KeyValuePair[], key: string): boolean {
|
|
192
|
+
for (const kv of keyValueArray) {
|
|
193
|
+
if (kv.key === key) return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function writeValueToProperty(
|
|
199
|
+
property: unknown,
|
|
200
|
+
allBindings: AllBindingsAccessor,
|
|
201
|
+
key: string,
|
|
202
|
+
value: unknown,
|
|
203
|
+
checkIfDifferent?: boolean,
|
|
204
|
+
): void {
|
|
205
|
+
if (!property || !isSubscribable(property)) {
|
|
206
|
+
const propWriters = allBindings.get('_tap_property_writers') as Record<string, (v: unknown) => void> | undefined;
|
|
207
|
+
if (propWriters && propWriters[key]) {
|
|
208
|
+
propWriters[key](value);
|
|
209
|
+
}
|
|
210
|
+
} else if (isWritable(property) && (!checkIfDifferent || property.peek() !== value)) {
|
|
211
|
+
property.set(value as never);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isWritable(value: unknown): value is Observable<unknown> | Computed<unknown> {
|
|
216
|
+
if (value instanceof Observable) return true;
|
|
217
|
+
if (value instanceof Computed) return value.hasWriteFunction;
|
|
218
|
+
return false;
|
|
219
|
+
}
|
package/src/extenders.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Subscribable } from './subscribable.js';
|
|
2
|
+
import { valuesArePrimitiveAndEqual } from './subscribable.js';
|
|
3
|
+
import { schedule, cancel } from './tasks.js';
|
|
4
|
+
|
|
5
|
+
export type ExtenderHandler = (target: Subscribable, value: unknown) => Subscribable | void;
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
8
|
+
export interface ExtenderMap {
|
|
9
|
+
notify: 'always';
|
|
10
|
+
rateLimit: number | RateLimitOptions;
|
|
11
|
+
deferred: true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ExtenderOptions = Partial<ExtenderMap> & Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
const registry: Record<string, ExtenderHandler> = {};
|
|
17
|
+
|
|
18
|
+
export function registerExtender(name: string, handler: ExtenderHandler): void {
|
|
19
|
+
registry[name] = handler;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getExtenderHandler(name: string): ExtenderHandler | undefined {
|
|
23
|
+
return registry[name];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function throttle(callback: () => void, timeout: number): () => void {
|
|
27
|
+
let timeoutInstance: ReturnType<typeof setTimeout> | undefined;
|
|
28
|
+
return function () {
|
|
29
|
+
if (!timeoutInstance) {
|
|
30
|
+
timeoutInstance = setTimeout(function () {
|
|
31
|
+
timeoutInstance = undefined;
|
|
32
|
+
callback();
|
|
33
|
+
}, timeout);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function debounce(callback: () => void, timeout: number): () => void {
|
|
39
|
+
let timeoutInstance: ReturnType<typeof setTimeout> | undefined;
|
|
40
|
+
return function () {
|
|
41
|
+
clearTimeout(timeoutInstance);
|
|
42
|
+
timeoutInstance = setTimeout(callback, timeout);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RateLimitOptions {
|
|
47
|
+
timeout: number;
|
|
48
|
+
method?: 'notifyWhenChangesStop' | ((callback: () => void, timeout: number) => () => void);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
registerExtender('notify', (target, notifyWhen) => {
|
|
52
|
+
target.equalityComparer = notifyWhen === 'always'
|
|
53
|
+
? undefined
|
|
54
|
+
: valuesArePrimitiveAndEqual;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
registerExtender('rateLimit', (target, options) => {
|
|
58
|
+
let timeout: number;
|
|
59
|
+
let method: RateLimitOptions['method'] | undefined;
|
|
60
|
+
|
|
61
|
+
if (typeof options === 'number') {
|
|
62
|
+
timeout = options;
|
|
63
|
+
} else {
|
|
64
|
+
const opts = options as RateLimitOptions;
|
|
65
|
+
timeout = opts.timeout;
|
|
66
|
+
method = opts.method;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
target._deferUpdates = false;
|
|
70
|
+
|
|
71
|
+
const limitFunction = typeof method === 'function'
|
|
72
|
+
? method
|
|
73
|
+
: method === 'notifyWhenChangesStop' ? debounce : throttle;
|
|
74
|
+
|
|
75
|
+
target.limit((callback) => limitFunction(callback, timeout));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
registerExtender('deferred', (target, options) => {
|
|
79
|
+
if (options !== true) {
|
|
80
|
+
throw new Error("The 'deferred' extender only accepts the value 'true', because it is not supported to turn deferral off once enabled.");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!target._deferUpdates) {
|
|
84
|
+
target._deferUpdates = true;
|
|
85
|
+
target.limit((callback) => {
|
|
86
|
+
let handle: number;
|
|
87
|
+
let ignoreUpdates = false;
|
|
88
|
+
return () => {
|
|
89
|
+
if (!ignoreUpdates) {
|
|
90
|
+
cancel(handle);
|
|
91
|
+
handle = schedule(callback);
|
|
92
|
+
try {
|
|
93
|
+
ignoreUpdates = true;
|
|
94
|
+
target.notifySubscribers(undefined as never, 'dirty');
|
|
95
|
+
} finally {
|
|
96
|
+
ignoreUpdates = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
package/src/filters.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { addBindingPreprocessor, setFiltersRegistry } from './bindingProvider.js';
|
|
2
|
+
import type { PreprocessFn } from './bindingProvider.js';
|
|
3
|
+
import { unwrapObservable, toJSON } from './utils.js';
|
|
4
|
+
|
|
5
|
+
// ---- Filter Registry ----
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
8
|
+
export const filters: Record<string, Function> = {};
|
|
9
|
+
|
|
10
|
+
// Register the filters object so binding expressions can access it as $filters
|
|
11
|
+
setFiltersRegistry(filters);
|
|
12
|
+
|
|
13
|
+
// ---- Filter Preprocessor ----
|
|
14
|
+
|
|
15
|
+
// Tokenizer regex: matches quoted strings, || (logical OR — not a pipe), single | and :
|
|
16
|
+
// as individual tokens, and any non-whitespace runs between delimiters.
|
|
17
|
+
const TOKEN_REGEX = /"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|\|\||[|:]|[^\s|:"'][^|:"']*[^\s|:"']|[^\s|:"']/g;
|
|
18
|
+
|
|
19
|
+
// Transforms `expression | filter1 | filter2:arg1:arg2` into
|
|
20
|
+
// `$filters['filter2']($filters['filter1'](expression),arg1,arg2)`
|
|
21
|
+
export const filterPreprocessor: PreprocessFn = function (input: string): string | void {
|
|
22
|
+
if (input.indexOf('|') === -1) return input;
|
|
23
|
+
|
|
24
|
+
const tokens = input.match(TOKEN_REGEX);
|
|
25
|
+
if (tokens && tokens.length > 1) {
|
|
26
|
+
tokens.push('|');
|
|
27
|
+
let result = tokens[0];
|
|
28
|
+
let lastToken: string | undefined;
|
|
29
|
+
let inFilters = false;
|
|
30
|
+
let nextIsFilter = false;
|
|
31
|
+
|
|
32
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
33
|
+
const token = tokens[i];
|
|
34
|
+
if (token === '|') {
|
|
35
|
+
if (inFilters) {
|
|
36
|
+
if (lastToken === ':') result += 'undefined';
|
|
37
|
+
result += ')';
|
|
38
|
+
}
|
|
39
|
+
nextIsFilter = true;
|
|
40
|
+
inFilters = true;
|
|
41
|
+
} else {
|
|
42
|
+
if (nextIsFilter) {
|
|
43
|
+
result = "$filters['" + token + "'](" + result;
|
|
44
|
+
} else if (inFilters && token === ':') {
|
|
45
|
+
if (lastToken === ':') result += 'undefined';
|
|
46
|
+
result += ',';
|
|
47
|
+
} else {
|
|
48
|
+
result += token;
|
|
49
|
+
}
|
|
50
|
+
nextIsFilter = false;
|
|
51
|
+
}
|
|
52
|
+
lastToken = token;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return input;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Enable the filter preprocessor for a specific binding handler
|
|
62
|
+
export function enableTextFilter(bindingKeyOrHandler: string): void {
|
|
63
|
+
addBindingPreprocessor(bindingKeyOrHandler, filterPreprocessor);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- Built-in Filters ----
|
|
67
|
+
|
|
68
|
+
filters['uppercase'] = function (value: unknown): string {
|
|
69
|
+
return String(unwrapObservable(value) ?? '').toUpperCase();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
filters['lowercase'] = function (value: unknown): string {
|
|
73
|
+
return String(unwrapObservable(value) ?? '').toLowerCase();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
filters['default'] = function (value: unknown, defaultValue: unknown): unknown {
|
|
77
|
+
const unwrapped = unwrapObservable(value);
|
|
78
|
+
if (typeof unwrapped === 'function') return unwrapped;
|
|
79
|
+
if (typeof unwrapped === 'string') return unwrapped.trim() === '' ? defaultValue : unwrapped;
|
|
80
|
+
if (unwrapped == null) return defaultValue;
|
|
81
|
+
if (Array.isArray(unwrapped) && unwrapped.length === 0) return defaultValue;
|
|
82
|
+
return unwrapped;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
filters['json'] = function (
|
|
86
|
+
rootObject: unknown,
|
|
87
|
+
space?: string | number,
|
|
88
|
+
replacer?: (key: string, value: unknown) => unknown,
|
|
89
|
+
): string {
|
|
90
|
+
return toJSON(rootObject, replacer, space);
|
|
91
|
+
};
|