grainjs 0.1.0 → 1.0.2
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/README.md +54 -9
- package/dist/cjs/index.d.ts +6 -2
- package/dist/cjs/index.js +24 -17
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/PriorityQueue.d.ts +1 -1
- package/dist/cjs/lib/PriorityQueue.js +1 -0
- package/dist/cjs/lib/PriorityQueue.js.map +1 -1
- package/dist/cjs/lib/_computed_queue.d.ts +18 -0
- package/dist/cjs/lib/_computed_queue.js +6 -1
- package/dist/cjs/lib/_computed_queue.js.map +1 -1
- package/dist/cjs/lib/binding.d.ts +16 -10
- package/dist/cjs/lib/binding.js +22 -27
- package/dist/cjs/lib/binding.js.map +1 -1
- package/dist/cjs/lib/browserGlobals.d.ts +4 -1
- package/dist/cjs/lib/browserGlobals.js +2 -0
- package/dist/cjs/lib/browserGlobals.js.map +1 -1
- package/dist/cjs/lib/computed.d.ts +11 -7
- package/dist/cjs/lib/computed.js +16 -0
- package/dist/cjs/lib/computed.js.map +1 -1
- package/dist/cjs/lib/dispose.d.ts +106 -14
- package/dist/cjs/lib/dispose.js +76 -11
- package/dist/cjs/lib/dispose.js.map +1 -1
- package/dist/cjs/lib/dom.d.ts +21 -17
- package/dist/cjs/lib/dom.js +33 -26
- package/dist/cjs/lib/dom.js.map +1 -1
- package/dist/cjs/lib/domComponent.d.ts +71 -0
- package/dist/cjs/lib/domComponent.js +15 -0
- package/dist/cjs/lib/domComponent.js.map +1 -0
- package/dist/cjs/lib/domComputed.d.ts +89 -0
- package/dist/cjs/lib/domComputed.js +92 -0
- package/dist/cjs/lib/domComputed.js.map +1 -0
- package/dist/cjs/lib/{_domDispose.d.ts → domDispose.d.ts} +12 -2
- package/dist/cjs/lib/{_domDispose.js → domDispose.js} +21 -8
- package/dist/cjs/lib/domDispose.js.map +1 -0
- package/dist/cjs/lib/{_domForEach.d.ts → domForEach.d.ts} +2 -2
- package/dist/cjs/lib/domForEach.js +72 -0
- package/dist/cjs/lib/domForEach.js.map +1 -0
- package/dist/cjs/lib/{_domImpl.d.ts → domImpl.d.ts} +15 -12
- package/dist/cjs/lib/{_domImpl.js → domImpl.js} +23 -6
- package/dist/cjs/lib/domImpl.js.map +1 -0
- package/dist/cjs/lib/{_domMethods.d.ts → domMethods.d.ts} +27 -62
- package/dist/cjs/lib/{_domMethods.js → domMethods.js} +21 -76
- package/dist/cjs/lib/domMethods.js.map +1 -0
- package/dist/cjs/lib/domevent.d.ts +32 -21
- package/dist/cjs/lib/domevent.js +33 -12
- package/dist/cjs/lib/domevent.js.map +1 -1
- package/dist/cjs/lib/emit.d.ts +25 -2
- package/dist/cjs/lib/emit.js +3 -1
- package/dist/cjs/lib/emit.js.map +1 -1
- package/dist/cjs/lib/kowrap.d.ts +45 -3
- package/dist/cjs/lib/kowrap.js +93 -10
- package/dist/cjs/lib/kowrap.js.map +1 -1
- package/dist/cjs/lib/obsArray.d.ts +8 -8
- package/dist/cjs/lib/obsArray.js +1 -0
- package/dist/cjs/lib/obsArray.js.map +1 -1
- package/dist/cjs/lib/observable.d.ts +6 -1
- package/dist/cjs/lib/observable.js +11 -2
- package/dist/cjs/lib/observable.js.map +1 -1
- package/dist/cjs/lib/pureComputed.d.ts +3 -3
- package/dist/cjs/lib/pureComputed.js +2 -1
- package/dist/cjs/lib/pureComputed.js.map +1 -1
- package/dist/cjs/lib/styled.d.ts +76 -11
- package/dist/cjs/lib/styled.js +55 -23
- package/dist/cjs/lib/styled.js.map +1 -1
- package/dist/cjs/lib/subscribe.d.ts +15 -6
- package/dist/cjs/lib/subscribe.js +6 -2
- package/dist/cjs/lib/subscribe.js.map +1 -1
- package/dist/cjs/lib/util.js +1 -0
- package/dist/cjs/lib/util.js.map +1 -1
- package/dist/cjs/lib/widgets/input.d.ts +2 -2
- package/dist/cjs/lib/widgets/input.js +2 -2
- package/dist/cjs/lib/widgets/input.js.map +1 -1
- package/dist/cjs/lib/widgets/select.d.ts +1 -1
- package/dist/cjs/lib/widgets/select.js +1 -0
- package/dist/cjs/lib/widgets/select.js.map +1 -1
- package/dist/esm/index.js +6 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/PriorityQueue.js.map +1 -1
- package/dist/esm/lib/_computed_queue.js +5 -1
- package/dist/esm/lib/_computed_queue.js.map +1 -1
- package/dist/esm/lib/binding.js +20 -27
- package/dist/esm/lib/binding.js.map +1 -1
- package/dist/esm/lib/browserGlobals.js +1 -0
- package/dist/esm/lib/browserGlobals.js.map +1 -1
- package/dist/esm/lib/computed.js +15 -0
- package/dist/esm/lib/computed.js.map +1 -1
- package/dist/esm/lib/dispose.js +74 -11
- package/dist/esm/lib/dispose.js.map +1 -1
- package/dist/esm/lib/dom.js +21 -17
- package/dist/esm/lib/dom.js.map +1 -1
- package/dist/esm/lib/domComponent.js +11 -0
- package/dist/esm/lib/domComponent.js.map +1 -0
- package/dist/esm/lib/domComputed.js +84 -0
- package/dist/esm/lib/domComputed.js.map +1 -0
- package/dist/esm/lib/{_domDispose.js → domDispose.js} +19 -8
- package/dist/esm/lib/domDispose.js.map +1 -0
- package/dist/esm/lib/domForEach.js +68 -0
- package/dist/esm/lib/domForEach.js.map +1 -0
- package/dist/esm/lib/{_domImpl.js → domImpl.js} +20 -4
- package/dist/esm/lib/domImpl.js.map +1 -0
- package/dist/esm/lib/{_domMethods.js → domMethods.js} +8 -63
- package/dist/esm/lib/domMethods.js.map +1 -0
- package/dist/esm/lib/domevent.js +30 -11
- package/dist/esm/lib/domevent.js.map +1 -1
- package/dist/esm/lib/emit.js +2 -1
- package/dist/esm/lib/emit.js.map +1 -1
- package/dist/esm/lib/kowrap.js +90 -10
- package/dist/esm/lib/kowrap.js.map +1 -1
- package/dist/esm/lib/obsArray.js.map +1 -1
- package/dist/esm/lib/observable.js +9 -1
- package/dist/esm/lib/observable.js.map +1 -1
- package/dist/esm/lib/pureComputed.js +1 -1
- package/dist/esm/lib/pureComputed.js.map +1 -1
- package/dist/esm/lib/styled.js +52 -22
- package/dist/esm/lib/styled.js.map +1 -1
- package/dist/esm/lib/subscribe.js +5 -2
- package/dist/esm/lib/subscribe.js.map +1 -1
- package/dist/esm/lib/util.js.map +1 -1
- package/dist/esm/lib/widgets/input.js +1 -2
- package/dist/esm/lib/widgets/input.js.map +1 -1
- package/dist/esm/lib/widgets/select.js.map +1 -1
- package/dist/grain-full.debug.js +1627 -1222
- package/dist/grain-full.min.js +1 -1
- package/dist/grain-full.min.js.map +1 -1
- package/index.ts +6 -2
- package/lib/_computed_queue.ts +7 -1
- package/lib/binding.ts +33 -28
- package/lib/browserGlobals.ts +3 -1
- package/lib/computed.ts +37 -7
- package/lib/dispose.ts +81 -33
- package/lib/dom.ts +24 -18
- package/lib/domComponent.ts +89 -0
- package/lib/domComputed.ts +146 -0
- package/lib/{_domDispose.ts → domDispose.ts} +26 -8
- package/lib/{_domForEach.ts → domForEach.ts} +12 -11
- package/lib/{_domImpl.ts → domImpl.ts} +36 -30
- package/lib/{_domMethods.ts → domMethods.ts} +33 -103
- package/lib/domevent.ts +59 -22
- package/lib/emit.ts +2 -1
- package/lib/kowrap.ts +109 -11
- package/lib/obsArray.ts +2 -2
- package/lib/observable.ts +10 -2
- package/lib/pureComputed.ts +7 -6
- package/lib/styled.ts +65 -39
- package/lib/subscribe.ts +24 -8
- package/lib/widgets/input.ts +9 -7
- package/lib/widgets/select.ts +3 -3
- package/package.json +41 -42
- package/dist/cjs/lib/_domComponent.d.ts +0 -84
- package/dist/cjs/lib/_domComponent.js +0 -160
- package/dist/cjs/lib/_domComponent.js.map +0 -1
- package/dist/cjs/lib/_domDispose.js.map +0 -1
- package/dist/cjs/lib/_domForEach.js +0 -71
- package/dist/cjs/lib/_domForEach.js.map +0 -1
- package/dist/cjs/lib/_domImpl.js.map +0 -1
- package/dist/cjs/lib/_domMethods.js.map +0 -1
- package/dist/esm/lib/_domComponent.js +0 -155
- package/dist/esm/lib/_domComponent.js.map +0 -1
- package/dist/esm/lib/_domDispose.js.map +0 -1
- package/dist/esm/lib/_domForEach.js +0 -68
- package/dist/esm/lib/_domForEach.js.map +0 -1
- package/dist/esm/lib/_domImpl.js.map +0 -1
- package/dist/esm/lib/_domMethods.js.map +0 -1
- package/lib/_domComponent.ts +0 -167
package/lib/domevent.ts
CHANGED
|
@@ -39,10 +39,14 @@
|
|
|
39
39
|
* let lis = domevent.onElem(elem, 'mouseup', e => { lis.dispose(); other_work(); });
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
import {DomElementMethod} from './_domImpl';
|
|
43
42
|
import {IDisposable} from './dispose';
|
|
43
|
+
import {DomElementMethod, DomMethod} from './domImpl';
|
|
44
44
|
|
|
45
|
-
export type
|
|
45
|
+
export type EventName = keyof HTMLElementEventMap;
|
|
46
|
+
export type EventType<E extends EventName|string> = E extends EventName ? HTMLElementEventMap[E] : Event;
|
|
47
|
+
|
|
48
|
+
export type EventCB<E extends Event = Event, T extends EventTarget = EventTarget> =
|
|
49
|
+
(this: void, event: E, elem: T) => void;
|
|
46
50
|
|
|
47
51
|
function _findMatch(inner: Element, outer: Element, selector: string): Element|null {
|
|
48
52
|
for (let el: Element|null = inner; el && el !== outer; el = el.parentElement) {
|
|
@@ -53,18 +57,18 @@ function _findMatch(inner: Element, outer: Element, selector: string): Element|n
|
|
|
53
57
|
return null;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
class DomEventListener implements EventListenerObject, IDisposable {
|
|
57
|
-
constructor(protected elem:
|
|
60
|
+
class DomEventListener<E extends Event, T extends EventTarget> implements EventListenerObject, IDisposable {
|
|
61
|
+
constructor(protected elem: T,
|
|
58
62
|
protected eventType: string,
|
|
59
|
-
protected callback: EventCB,
|
|
63
|
+
protected callback: EventCB<E, T>,
|
|
60
64
|
protected useCapture: boolean,
|
|
61
65
|
protected selector?: string) {
|
|
62
66
|
this.elem.addEventListener(this.eventType, this, this.useCapture);
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
public handleEvent(event:
|
|
69
|
+
public handleEvent(event: E) {
|
|
66
70
|
const cb = this.callback;
|
|
67
|
-
cb(event, this.elem
|
|
71
|
+
cb(event, this.elem);
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
public dispose() {
|
|
@@ -72,8 +76,8 @@ class DomEventListener implements EventListenerObject, IDisposable {
|
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
class DomEventMatchListener extends DomEventListener {
|
|
76
|
-
public handleEvent(event:
|
|
79
|
+
class DomEventMatchListener<E extends Event> extends DomEventListener<E, EventTarget> {
|
|
80
|
+
public handleEvent(event: E) {
|
|
77
81
|
const elem = _findMatch(event.target as Element, this.elem as Element, this.selector!);
|
|
78
82
|
if (elem) {
|
|
79
83
|
const cb = this.callback;
|
|
@@ -92,11 +96,13 @@ class DomEventMatchListener extends DomEventListener {
|
|
|
92
96
|
* rarely be useful (e.g. JQuery doesn't even offer it as an option).
|
|
93
97
|
* @returns {Object} Listener object whose .dispose() method will remove the event listener.
|
|
94
98
|
*/
|
|
95
|
-
export function onElem
|
|
96
|
-
|
|
99
|
+
export function onElem<E extends EventName|string, T extends EventTarget>(
|
|
100
|
+
elem: T, eventType: E, callback: EventCB<EventType<E>, T>, {useCapture = false} = {}): IDisposable {
|
|
97
101
|
return new DomEventListener(elem, eventType, callback, useCapture);
|
|
98
102
|
}
|
|
99
|
-
|
|
103
|
+
|
|
104
|
+
export function on<E extends EventName|string, T extends EventTarget>(
|
|
105
|
+
eventType: E, callback: EventCB<EventType<E>, T>, {useCapture = false} = {}): DomMethod<T> {
|
|
100
106
|
// tslint:disable-next-line:no-unused-expression
|
|
101
107
|
return (elem) => { new DomEventListener(elem, eventType, callback, useCapture); };
|
|
102
108
|
}
|
|
@@ -126,27 +132,58 @@ export function onMatch(selector: string, eventType: string, callback: EventCB,
|
|
|
126
132
|
return (elem) => { new DomEventMatchListener(elem, eventType, callback, useCapture, selector); };
|
|
127
133
|
}
|
|
128
134
|
|
|
135
|
+
export type KeyEventType = 'keypress' | 'keyup' | 'keydown';
|
|
136
|
+
|
|
137
|
+
export interface IKeyHandlers<T extends HTMLElement = HTMLElement> {
|
|
138
|
+
[key: string]: (this: void, ev: KeyboardEvent, elem: T) => void;
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
/**
|
|
130
|
-
* Listen to key
|
|
131
|
-
* `elem` argument, and may be used as an argument to dom().
|
|
132
|
-
*
|
|
142
|
+
* Listen to key events (typically 'keydown' or 'keypress'), with specified per-key callbacks.
|
|
133
143
|
* Key names are listed at https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
|
|
134
144
|
*
|
|
145
|
+
* Methods onKeyPress() and onKeyDown() are intended to be used as arguments to dom().
|
|
146
|
+
*
|
|
147
|
+
* By default, handled events are stopped from bubbling with stopPropagation() and
|
|
148
|
+
* preventDefault(). If, however, you register a key with a "$" suffix (i.e. "Enter$" instead of
|
|
149
|
+
* "Enter"), then the event is allowed to bubble normally.
|
|
150
|
+
*
|
|
151
|
+
* When this handler is set on an element, we automatically ensure that tabindex attribute is set,
|
|
152
|
+
* to allow this element to receive keyboard events.
|
|
153
|
+
*
|
|
135
154
|
* For example:
|
|
136
155
|
*
|
|
137
156
|
* dom('input', ...
|
|
138
|
-
* dom.
|
|
157
|
+
* dom.onKeyDown({
|
|
139
158
|
* Enter: (e, elem) => console.log("Enter pressed"),
|
|
140
159
|
* Escape: (e, elem) => console.log("Escape pressed"),
|
|
160
|
+
* Delete$: (e, elem) => console.log("Delete pressed, will bubble"),
|
|
141
161
|
* })
|
|
142
162
|
* )
|
|
143
163
|
*/
|
|
144
|
-
export function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
164
|
+
export function onKeyElem<T extends HTMLElement>(
|
|
165
|
+
elem: T, evType: KeyEventType, keyHandlers: IKeyHandlers<T>,
|
|
166
|
+
): IDisposable {
|
|
167
|
+
if (!(elem.tabIndex >= 0)) { // If tabIndex property is undefined or -1,
|
|
168
|
+
elem.setAttribute('tabindex', '-1'); // Set tabIndex attribute to make the element focusable.
|
|
169
|
+
}
|
|
170
|
+
return onElem(elem, evType, (ev, _elem) => {
|
|
171
|
+
const plainHandler = keyHandlers[ev.key];
|
|
172
|
+
const handler = plainHandler || keyHandlers[ev.key + '$'];
|
|
173
|
+
if (handler) {
|
|
174
|
+
if (plainHandler!) {
|
|
175
|
+
ev.stopPropagation();
|
|
176
|
+
ev.preventDefault();
|
|
177
|
+
}
|
|
178
|
+
handler(ev, _elem);
|
|
179
|
+
}
|
|
148
180
|
});
|
|
149
181
|
}
|
|
150
|
-
|
|
151
|
-
|
|
182
|
+
|
|
183
|
+
export function onKeyPress<T extends HTMLElement>(keyHandlers: IKeyHandlers<T>): DomMethod<T> {
|
|
184
|
+
return (elem) => { onKeyElem(elem, 'keypress', keyHandlers); };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function onKeyDown<T extends HTMLElement>(keyHandlers: IKeyHandlers<T>): DomMethod<T> {
|
|
188
|
+
return (elem) => { onKeyElem(elem, 'keydown', keyHandlers); };
|
|
152
189
|
}
|
package/lib/emit.ts
CHANGED
|
@@ -116,7 +116,8 @@ export class Emitter extends LLink {
|
|
|
116
116
|
* Sets the single callback that would get called when a listener is added or removed.
|
|
117
117
|
* @param {Function} changeCB(hasListeners): Function to call after a listener is added or
|
|
118
118
|
* removed. It's called with a boolean indicating whether this Emitter has any listeners.
|
|
119
|
-
* Pass in `null` to unset the callback.
|
|
119
|
+
* Pass in `null` to unset the callback. Note that it can be called multiple times in a row
|
|
120
|
+
* with hasListeners `true`.
|
|
120
121
|
*/
|
|
121
122
|
public setChangeCB(changeCB: ChangeCB, optContext?: any): void {
|
|
122
123
|
this._changeCB = changeCB || _noop;
|
package/lib/kowrap.ts
CHANGED
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
* knockout as a dependency of grainjs.
|
|
20
20
|
*
|
|
21
21
|
* In both cases, calling fromKo/toKo twice on the same observable will return the same wrapper,
|
|
22
|
-
* and subscriptions and disposal are appropriately set up to make usage seamless.
|
|
22
|
+
* and subscriptions and disposal are appropriately set up to make usage seamless. In particular,
|
|
23
|
+
* the returned wrapper should not be disposed; it's tied to the lifetime of the wrapped object.
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
|
-
import {
|
|
26
|
+
import {domDisposeHooks} from './domDispose';
|
|
27
|
+
import {bundleChanges, Observable} from './observable';
|
|
26
28
|
|
|
27
29
|
// Implementation note. Both wrappers are implemented in the same way.
|
|
28
30
|
//
|
|
@@ -31,31 +33,69 @@ import {observable, Observable} from './observable';
|
|
|
31
33
|
// be garbage-collected once it has no listeners AND the underlying observable is disposed or
|
|
32
34
|
// unreferenced.
|
|
33
35
|
|
|
34
|
-
export interface IKnockoutObservable<T> {
|
|
36
|
+
export interface IKnockoutObservable<T> extends IKnockoutReadObservable<T> {
|
|
35
37
|
(val: T): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface IKnockoutReadObservable<T> {
|
|
41
|
+
(): T;
|
|
36
42
|
peek(): T;
|
|
37
43
|
subscribe(callback: (newValue: T) => void, target?: any, event?: "change"): any;
|
|
44
|
+
getSubscriptionsCount(): number;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
// Inference from Knockout observable gets very tricky because ko.Observable includes the function
|
|
48
|
+
// signature `(val: T) => any` from which type `any` gets inferred. We can infer the correct type
|
|
49
|
+
// with this helper.
|
|
50
|
+
export type InferKoType<KObs extends IKnockoutReadObservable<any>> =
|
|
51
|
+
KObs extends {peek(): infer T} ? T : never;
|
|
52
|
+
|
|
40
53
|
const fromKoWrappers: WeakMap<IKnockoutObservable<any>, Observable<any>> = new WeakMap();
|
|
41
54
|
const toKoWrappers: WeakMap<Observable<any>, IKnockoutObservable<any>> = new WeakMap();
|
|
42
55
|
|
|
43
56
|
/**
|
|
44
57
|
* Returns a Grain.js observable which mirrors a Knockout observable.
|
|
58
|
+
*
|
|
59
|
+
* Do not dispose this wrapper, as it is shared by all code using koObs, and its lifetime is tied
|
|
60
|
+
* to the lifetime of koObs. If unused, it consumes minimal resources, and should get garbage
|
|
61
|
+
* collected along with koObs.
|
|
62
|
+
*/
|
|
63
|
+
export function fromKo<KObs extends IKnockoutObservable<any>>(koObs: KObs): Observable<InferKoType<KObs>> {
|
|
64
|
+
return fromKoWrappers.get(koObs) || fromKoWrappers.set(koObs, new KoWrapObs(koObs)).get(koObs)!;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* An Observable that wraps a Knockout observable, created via fromKo(). It keeps minimal overhead
|
|
69
|
+
* when unused by only subscribing to the wrapped observable while it itself has subscriptions.
|
|
70
|
+
*
|
|
71
|
+
* This way, when unused, the only reference is from the wrapper to the wrapped object. KoWrapObs
|
|
72
|
+
* should not be disposed; its lifetime is tied to that of the wrapped object.
|
|
45
73
|
*/
|
|
46
|
-
export
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
export class KoWrapObs<T> extends Observable<T> {
|
|
75
|
+
private _koSub: any = null;
|
|
76
|
+
|
|
77
|
+
constructor(private _koObs: IKnockoutObservable<T>) {
|
|
78
|
+
super(_koObs.peek());
|
|
79
|
+
this.setListenerChangeCB((hasListeners) => {
|
|
80
|
+
if (!hasListeners) {
|
|
81
|
+
this._koSub.dispose();
|
|
82
|
+
this._koSub = null;
|
|
83
|
+
} else if (!this._koSub) {
|
|
84
|
+
// TODO this is a little hack, really, BaseObservable should expose a way to set the value
|
|
85
|
+
// directly by derived classes, i.e. a protected setter.
|
|
86
|
+
(this as any)._value = this._koObs.peek();
|
|
87
|
+
this._koSub = this._koObs.subscribe((val) => this.setAndTrigger(val));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
50
90
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return newObs;
|
|
91
|
+
public get(): T { return this._koObs.peek(); }
|
|
92
|
+
public set(value: T): void { bundleChanges(() => this._koObs(value)); }
|
|
93
|
+
public dispose(): void { throw new Error("KoWrapObs should not be disposed"); }
|
|
55
94
|
}
|
|
56
95
|
|
|
57
96
|
export interface IKnockoutModule {
|
|
58
97
|
observable<T>(value: T): IKnockoutObservable<T>;
|
|
98
|
+
cleanNode(node: Node): void;
|
|
59
99
|
}
|
|
60
100
|
|
|
61
101
|
/**
|
|
@@ -71,3 +111,61 @@ export function toKo<T>(knockout: IKnockoutModule, grainObs: Observable<T>): IKn
|
|
|
71
111
|
grainObs.addListener((val) => newKoObs(val));
|
|
72
112
|
return newKoObs;
|
|
73
113
|
}
|
|
114
|
+
|
|
115
|
+
// Marker for when knockout-disposal integration has already been setup.
|
|
116
|
+
let koDisposalIsSetup = false;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Set up integration between grainjs and knockout disposal. Knockout does cleanup using
|
|
120
|
+
* ko.removeNode / ko.cleanNode (it also takes care of JQuery cleanup if needed). GrainJS does
|
|
121
|
+
* cleanup using dom.domDispose(). By default these don't know about each other.
|
|
122
|
+
*
|
|
123
|
+
* If you mix the two libraries, however, disposing an element may need to trigger disposers
|
|
124
|
+
* registered by either library.
|
|
125
|
+
*
|
|
126
|
+
* This method ensures that this happens.
|
|
127
|
+
*
|
|
128
|
+
* Note: grainjs disposes text nodes too, but nothing relies on it. When disposal is triggered via
|
|
129
|
+
* knockout, we are forced to rely on knockout's node traversal which ignores text nodes.
|
|
130
|
+
*/
|
|
131
|
+
export function setupKoDisposal(ko: IKnockoutModule) {
|
|
132
|
+
// Ensure we don't do the setup more than once, or things will get called multiple times.
|
|
133
|
+
if (koDisposalIsSetup) { return; }
|
|
134
|
+
koDisposalIsSetup = true;
|
|
135
|
+
|
|
136
|
+
const koDomNodeDisposal = (ko as any).utils.domNodeDisposal;
|
|
137
|
+
|
|
138
|
+
// Knockout by default has an external-data-cleanup func set to cleanup JQuery. Whatever it is
|
|
139
|
+
// set to, we will continue calling it, and also will call grainjs domDisposeNode.
|
|
140
|
+
const origKoCleanExternalData = koDomNodeDisposal.cleanExternalData;
|
|
141
|
+
|
|
142
|
+
// The original function called by grainjs to clean nodes recursively. We'll override it.
|
|
143
|
+
const origGrainDisposeRecursive = domDisposeHooks.disposeRecursive;
|
|
144
|
+
|
|
145
|
+
// New function called by knockout to do extra cleanup. Now calls grainjs single-node cleanup.
|
|
146
|
+
// (In knockout, we can only override single-node cleanup.)
|
|
147
|
+
function newKoCleanExternalData(node: Node) {
|
|
148
|
+
origKoCleanExternalData(node);
|
|
149
|
+
domDisposeHooks.disposeNode(node);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Function called by grainjs to clean nodes recursively. We override the recursive cleanup
|
|
153
|
+
// function to call the recursive knockout cleanup (letting knockout do the dom traversal it
|
|
154
|
+
// normally does).
|
|
155
|
+
function newGrainDisposeRecursive(node: Node) {
|
|
156
|
+
origGrainDisposeRecursive(node);
|
|
157
|
+
|
|
158
|
+
// While doing knockout cleanup, do NOT have it call grainjs cleanup too, as that would cause
|
|
159
|
+
// multiple unnecessary traversals of DOM.
|
|
160
|
+
koDomNodeDisposal.cleanExternalData = origKoCleanExternalData;
|
|
161
|
+
try {
|
|
162
|
+
ko.cleanNode(node);
|
|
163
|
+
} finally {
|
|
164
|
+
koDomNodeDisposal.cleanExternalData = newKoCleanExternalData;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Use knockout and grainjs hooks to actually set the new cleanup functions.
|
|
169
|
+
koDomNodeDisposal.cleanExternalData = newKoCleanExternalData;
|
|
170
|
+
domDisposeHooks.disposeRecursive = newGrainDisposeRecursive;
|
|
171
|
+
}
|
package/lib/obsArray.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* ownership of those disposables that are added to it as array elements.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import {IDisposable,
|
|
34
|
+
import {IDisposable, IDisposableOwnerT, setDisposeOwner} from './dispose';
|
|
35
35
|
import {Listener} from './emit';
|
|
36
36
|
import {BaseObservable, Observable} from './observable';
|
|
37
37
|
import {subscribe, Subscription} from './subscribe';
|
|
@@ -291,7 +291,7 @@ export function computedArray<T, U>(
|
|
|
291
291
|
* The returned observable has an additional .setLive(bool) method. While set to false, the
|
|
292
292
|
* observable will not be adjusted as the array changes, except to keep it valid.
|
|
293
293
|
*/
|
|
294
|
-
export function makeLiveIndex<T>(owner:
|
|
294
|
+
export function makeLiveIndex<T>(owner: IDisposableOwnerT<LiveIndex>|null, obsArr: ObsArray<T>,
|
|
295
295
|
initialIndex: number = 0): LiveIndex {
|
|
296
296
|
return setDisposeOwner(owner, new LiveIndex(obsArr, initialIndex));
|
|
297
297
|
}
|
package/lib/observable.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import {compute, DepItem} from './_computed_queue';
|
|
25
|
-
import {IDisposable, IDisposableOwnerT} from './dispose';
|
|
25
|
+
import {IDisposable, IDisposableOwnerT, setDisposeOwner} from './dispose';
|
|
26
26
|
import {Emitter, Listener} from './emit';
|
|
27
27
|
|
|
28
28
|
export {bundleChanges} from './_computed_queue';
|
|
@@ -90,7 +90,8 @@ export class BaseObservable<T> {
|
|
|
90
90
|
* previously-set such callback.
|
|
91
91
|
* @param {Function} changeCB(hasListeners): Function to call after a listener is added or
|
|
92
92
|
* removed. It's called with a boolean indicating whether this observable has any listeners.
|
|
93
|
-
* Pass in `null` to unset the callback.
|
|
93
|
+
* Pass in `null` to unset the callback. Note that it can be called multiple times in a row
|
|
94
|
+
* with hasListeners `true`.
|
|
94
95
|
*/
|
|
95
96
|
public setListenerChangeCB(changeCB: (hasListeners: boolean) => void, optContext?: any): void {
|
|
96
97
|
this._onChange.setChangeCB(changeCB, optContext);
|
|
@@ -143,6 +144,13 @@ export class Observable<T> extends BaseObservable<T> implements IDisposableOwner
|
|
|
143
144
|
return obs;
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Creates a new Observable with the given initial value, and owned by owner.
|
|
149
|
+
*/
|
|
150
|
+
public static create<T>(owner: IDisposableOwnerT<Observable<T>>|null, value: T): Observable<T> {
|
|
151
|
+
return setDisposeOwner(owner, new Observable<T>(value));
|
|
152
|
+
}
|
|
153
|
+
|
|
146
154
|
private _owned?: T & IDisposable = undefined;
|
|
147
155
|
|
|
148
156
|
/**
|
package/lib/pureComputed.ts
CHANGED
|
@@ -11,15 +11,16 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {DepItem} from './_computed_queue';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import {IKnockoutReadObservable} from './kowrap';
|
|
15
|
+
import {BaseObservable, Observable} from './observable';
|
|
16
|
+
import {ISubscribable, ISubscribableObs, Subscription, UseCB} from './subscribe';
|
|
16
17
|
|
|
17
18
|
function _noWrite(): never {
|
|
18
19
|
throw new Error("Can't write to non-writable pureComputed");
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function _useFunc<T>(obs:
|
|
22
|
-
return obs.get();
|
|
22
|
+
function _useFunc<T>(obs: BaseObservable<T>|IKnockoutReadObservable<T>): T {
|
|
23
|
+
return ('get' in obs) ? obs.get() : obs.peek();
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
// Constant empty array, which we use to avoid allocating new read-only empty arrays.
|
|
@@ -29,7 +30,7 @@ export class PureComputed<T> extends Observable<T> {
|
|
|
29
30
|
private _callback: (use: UseCB, ...args: any[]) => T;
|
|
30
31
|
private _write: (value: T) => void;
|
|
31
32
|
private _sub: Subscription|null;
|
|
32
|
-
private readonly _dependencies: ReadonlyArray<
|
|
33
|
+
private readonly _dependencies: ReadonlyArray<ISubscribableObs>;
|
|
33
34
|
private _inCall: boolean;
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -57,7 +58,7 @@ export class PureComputed<T> extends Observable<T> {
|
|
|
57
58
|
// _inCall member prevents infinite recursion.
|
|
58
59
|
this._inCall = true;
|
|
59
60
|
try {
|
|
60
|
-
const readArgs: any[] = [_useFunc];
|
|
61
|
+
const readArgs: [UseCB, ...any[]] = [_useFunc];
|
|
61
62
|
// Note that this attempts to optimize for speed.
|
|
62
63
|
for (let i = 0, len = this._dependencies.length; i < len; i++) {
|
|
63
64
|
readArgs[i + 1] = this._dependencies[i].get();
|
package/lib/styled.ts
CHANGED
|
@@ -54,32 +54,38 @@
|
|
|
54
54
|
* myButton(myButton.cls('-small'), 'Test')
|
|
55
55
|
*
|
|
56
56
|
* creates a button with both the myButton style above, and the style specified under "&-small".
|
|
57
|
+
*
|
|
58
|
+
* Animations with @keyframes may be created with a unique name by using the keyframes() helper:
|
|
59
|
+
*
|
|
60
|
+
* const rotate360 = keyframes(`
|
|
61
|
+
* from { transform: rotate(0deg); }
|
|
62
|
+
* to { transform: rotate(360deg); }
|
|
63
|
+
* `);
|
|
64
|
+
*
|
|
65
|
+
* const Rotate = styled('div', `
|
|
66
|
+
* display: inline-block;
|
|
67
|
+
* animation: ${rotate360} 2s linear infinite;
|
|
68
|
+
* `);
|
|
57
69
|
*/
|
|
58
70
|
|
|
59
71
|
// Use the browser globals in a way that allows replacing them with mocks in tests.
|
|
60
72
|
import {G} from './browserGlobals';
|
|
61
|
-
import {dom,
|
|
62
|
-
|
|
63
|
-
export type DomCreateFunc0<R> = (...args: DomElementArg[]) => R;
|
|
64
|
-
export type DomCreateFunc1<R, T> = (a: T, ...args: DomElementArg[]) => R;
|
|
65
|
-
export type DomCreateFunc2<R, T, U> = (a: T, b: U, ...args: DomElementArg[]) => R;
|
|
66
|
-
export type DomCreateFunc3<R, T, U, W> = (a: T, b: U, c: W, ...args: DomElementArg[]) => R;
|
|
73
|
+
import {dom, IDomArgs, TagElem, TagName} from './domImpl';
|
|
74
|
+
import {cls, clsPrefix} from './domMethods';
|
|
67
75
|
|
|
68
76
|
// The value returned by styled() matches the input (first argument), and also implements IClsName
|
|
69
77
|
// interface.
|
|
70
78
|
export interface IClsName {
|
|
71
79
|
className: string; // Name of the generated class.
|
|
72
|
-
cls: typeof
|
|
80
|
+
cls: typeof cls; // Helper like dom.cls(), but which prefixes classes by className.
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
export type DomCreateFunc<R, Args extends IDomArgs<R> = IDomArgs<R>> = (...args: Args) => R;
|
|
84
|
+
|
|
75
85
|
// See module documentation for details.
|
|
76
|
-
export function styled<
|
|
77
|
-
export function styled<
|
|
78
|
-
|
|
79
|
-
export function styled<R, T, U>(
|
|
80
|
-
creator: DomCreateFunc2<R, T, U>, styles: string): DomCreateFunc2<R, T, U> & IClsName;
|
|
81
|
-
export function styled<R, T, U, W>(
|
|
82
|
-
creator: DomCreateFunc3<R, T, U, W>, styles: string): DomCreateFunc3<R, T, U, W> & IClsName;
|
|
86
|
+
export function styled<Tag extends TagName>(tag: Tag, styles: string): DomCreateFunc<TagElem<Tag>> & IClsName;
|
|
87
|
+
export function styled<Args extends any[], R extends Element>(
|
|
88
|
+
creator: (...args: Args) => R, styles: string): typeof creator & IClsName;
|
|
83
89
|
export function styled(creator: any, styles: string): IClsName {
|
|
84
90
|
// Note that we intentionally minimize the work done when styled() is called; it's better to do
|
|
85
91
|
// any needed work on first use. That's when we will actually build the css rules.
|
|
@@ -88,45 +94,54 @@ export function styled(creator: any, styles: string): IClsName {
|
|
|
88
94
|
// Creator function reflects the input, with only the addition of style.use() at the end. Note
|
|
89
95
|
// that it needs to be at the end because creator() might take special initial arguments.
|
|
90
96
|
const newCreator = (typeof creator === 'string') ?
|
|
91
|
-
(...args:
|
|
92
|
-
(...args: any[]) => creator(...args
|
|
97
|
+
(...args: any[]) => style.addToElem(dom(creator, ...args)) :
|
|
98
|
+
(...args: any[]) => style.addToElem(creator(...args));
|
|
93
99
|
return Object.assign(newCreator, {
|
|
94
100
|
className: style.className,
|
|
95
|
-
cls:
|
|
101
|
+
cls: clsPrefix.bind(null, style.className),
|
|
96
102
|
});
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
// Keyframes produces simply a string with the generated name. Note that these does not support
|
|
106
|
+
// nesting or ampersand (&) handling, since these would be difficult and are entirely unneeded.
|
|
107
|
+
export function keyframes(styles: string): string {
|
|
108
|
+
return (new KeyframePiece(styles)).className;
|
|
109
|
+
}
|
|
110
|
+
|
|
99
111
|
function createCssRules(className: string, styles: string) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
// The first time we encounter a nested section, we know which are the "main" rules, and can
|
|
113
|
+
// wrap them appropriately.
|
|
114
|
+
const nestedStart = styles.search(/[^;]*\{/);
|
|
115
|
+
const mainRules = nestedStart < 0 ? styles : styles.slice(0, nestedStart);
|
|
116
|
+
const nestedRules = nestedStart < 0 ? "" : styles.slice(nestedStart);
|
|
117
|
+
|
|
118
|
+
// At the end, replace all occurrences of & with ".className".
|
|
119
|
+
return `& {${mainRules}\n}\n${nestedRules}`.replace(/&/g, className);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Used by getNextStyleNum when running without a global window object (e.g. in tests).
|
|
123
|
+
const _global = {};
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
// Keep the counter for next class attached to the global window object rather than be a library
|
|
126
|
+
// global. This way if by some chance multiple instance of grainjs are loaded into the page, it
|
|
127
|
+
// still works without overwriting class names (which would be extremely confusing).
|
|
128
|
+
function getNextStyleNum() {
|
|
129
|
+
const g: any = G.window || _global;
|
|
130
|
+
return g._grainNextStyleNum = (g._grainNextStyleNum || 0) + 1;
|
|
112
131
|
}
|
|
113
132
|
|
|
114
133
|
class StylePiece {
|
|
115
|
-
// Index of next auto-generated css class name.
|
|
116
|
-
private static _next: number = 1;
|
|
117
|
-
|
|
118
134
|
// Set of all StylePieces created but not yet mounted.
|
|
119
135
|
private static _unmounted = new Set<StylePiece>();
|
|
120
136
|
|
|
121
|
-
// Generate a new css class name.
|
|
122
|
-
private static _nextClassName() { return `_grain${
|
|
137
|
+
// Generate a new css class name. The suffix ensures that names like "&2" can't cause a conflict.
|
|
138
|
+
private static _nextClassName() { return `_grain${getNextStyleNum()}_`; }
|
|
123
139
|
|
|
124
140
|
// Mount all unmounted StylePieces, and clear the _unmounted map.
|
|
125
141
|
private static _mountAll(): void {
|
|
126
|
-
const sheet = Array.from(this._unmounted, (p) =>
|
|
127
|
-
.join('\n\n');
|
|
142
|
+
const sheet: string = Array.from(this._unmounted, (p) => p._createRules()).join("\n\n");
|
|
128
143
|
|
|
129
|
-
G.document.head
|
|
144
|
+
G.document.head!.appendChild(dom('style', sheet));
|
|
130
145
|
for (const piece of this._unmounted) {
|
|
131
146
|
piece._mounted = true;
|
|
132
147
|
}
|
|
@@ -136,13 +151,24 @@ class StylePiece {
|
|
|
136
151
|
public readonly className: string;
|
|
137
152
|
private _mounted: boolean = false;
|
|
138
153
|
|
|
139
|
-
constructor(
|
|
154
|
+
constructor(protected _styles: string) {
|
|
140
155
|
this.className = StylePiece._nextClassName();
|
|
141
156
|
StylePiece._unmounted.add(this);
|
|
142
157
|
}
|
|
143
158
|
|
|
144
|
-
public
|
|
159
|
+
public addToElem<T extends Element>(elem: T): T {
|
|
145
160
|
if (!this._mounted) { StylePiece._mountAll(); }
|
|
146
|
-
|
|
161
|
+
elem.classList.add(this.className);
|
|
162
|
+
return elem;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected _createRules(): string {
|
|
166
|
+
return createCssRules('.' + this.className, this._styles);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
class KeyframePiece extends StylePiece {
|
|
171
|
+
protected _createRules(): string {
|
|
172
|
+
return `@keyframes ${this.className} {${this._styles}}`;
|
|
147
173
|
}
|
|
148
174
|
}
|
package/lib/subscribe.ts
CHANGED
|
@@ -19,17 +19,31 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import {DepItem} from './_computed_queue';
|
|
22
|
+
import {IDisposableOwner} from './dispose';
|
|
22
23
|
import {Listener} from './emit';
|
|
24
|
+
import {fromKo, IKnockoutReadObservable} from './kowrap';
|
|
23
25
|
import {BaseObservable as Obs} from './observable';
|
|
24
26
|
|
|
25
|
-
export interface
|
|
27
|
+
export interface ISubscribableObs {
|
|
26
28
|
_getDepItem(): DepItem|null;
|
|
27
29
|
addListener(callback: (val: any, prev: any) => void, optContext?: object): Listener;
|
|
28
30
|
get(): any;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
export type ISubscribable = ISubscribableObs | IKnockoutReadObservable<any>;
|
|
34
|
+
|
|
35
|
+
// Type inference from the simpler Obs<T>|IKnockoutReadObservable<T> does not always produce
|
|
36
|
+
// correct T for ko.Observable. The formula below is a workaround. See also InferKoType in kowrap.
|
|
37
|
+
export type InferUseType<TObs extends Obs<any>|IKnockoutReadObservable<any>> =
|
|
38
|
+
TObs extends Obs<infer T> ? T :
|
|
39
|
+
TObs extends {peek(): infer U} ? U : never;
|
|
40
|
+
|
|
31
41
|
// The generic type for the use() function that callbacks get.
|
|
32
|
-
export type UseCB = <
|
|
42
|
+
export type UseCB = <TObs extends Obs<any>|IKnockoutReadObservable<any>>(obs: TObs) => InferUseType<TObs>;
|
|
43
|
+
|
|
44
|
+
export interface UseCBOwner extends UseCB { // tslint:disable-line:interface-name
|
|
45
|
+
owner: IDisposableOwner;
|
|
46
|
+
}
|
|
33
47
|
|
|
34
48
|
interface IListenerWithInUse extends Listener {
|
|
35
49
|
_inUse: boolean;
|
|
@@ -40,9 +54,9 @@ const emptyArray: ReadonlyArray<any> = [];
|
|
|
40
54
|
|
|
41
55
|
export class Subscription {
|
|
42
56
|
private readonly _depItem: DepItem;
|
|
43
|
-
private readonly _dependencies: ReadonlyArray<
|
|
57
|
+
private readonly _dependencies: ReadonlyArray<ISubscribableObs>;
|
|
44
58
|
private readonly _depListeners: ReadonlyArray<Listener>;
|
|
45
|
-
private _dynDeps: Map<
|
|
59
|
+
private _dynDeps: Map<ISubscribableObs, IListenerWithInUse>;
|
|
46
60
|
private _callback: (use: UseCB, ...args: any[]) => void;
|
|
47
61
|
private _useFunc: UseCB;
|
|
48
62
|
|
|
@@ -59,7 +73,7 @@ export class Subscription {
|
|
|
59
73
|
this._callback = callback;
|
|
60
74
|
this._useFunc = this._useDependency.bind(this);
|
|
61
75
|
if (owner) {
|
|
62
|
-
(this._useFunc as
|
|
76
|
+
(this._useFunc as UseCBOwner).owner = owner;
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
this._evaluate();
|
|
@@ -85,7 +99,8 @@ export class Subscription {
|
|
|
85
99
|
* subscription to `obs` if one doesn't yet exist.
|
|
86
100
|
* @param {Observable} obs: The observable being used as a dependency.
|
|
87
101
|
*/
|
|
88
|
-
private _useDependency(
|
|
102
|
+
private _useDependency(_obs: ISubscribable) {
|
|
103
|
+
const obs = ('_getDepItem' in _obs) ? _obs : fromKo(_obs);
|
|
89
104
|
let listener = this._dynDeps.get(obs);
|
|
90
105
|
if (!listener) {
|
|
91
106
|
listener = this._subscribeTo(obs) as IListenerWithInUse;
|
|
@@ -105,7 +120,7 @@ export class Subscription {
|
|
|
105
120
|
if (this._callback === null) { return; } // Means this Subscription has been disposed.
|
|
106
121
|
try {
|
|
107
122
|
// Note that this is faster than using .map().
|
|
108
|
-
const readArgs = [this._useFunc];
|
|
123
|
+
const readArgs: [UseCB, ...any[]] = [this._useFunc];
|
|
109
124
|
for (let i = 0, len = this._dependencies.length; i < len; i++) {
|
|
110
125
|
readArgs[i + 1] = this._dependencies[i].get();
|
|
111
126
|
this._depItem.useDep(this._dependencies[i]._getDepItem());
|
|
@@ -130,7 +145,8 @@ export class Subscription {
|
|
|
130
145
|
* @param {Observable} obs: The observable to subscribe to.
|
|
131
146
|
* @returns {Listener} Listener object.
|
|
132
147
|
*/
|
|
133
|
-
private _subscribeTo(
|
|
148
|
+
private _subscribeTo(_obs: ISubscribable) {
|
|
149
|
+
const obs = ('_getDepItem' in _obs) ? _obs : fromKo(_obs);
|
|
134
150
|
return obs.addListener(this._enqueue, this);
|
|
135
151
|
}
|
|
136
152
|
|