@praxisjs/composables 1.0.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/CHANGELOG.md +17 -0
- package/dist/__tests__/browser.test.js +139 -0
- package/dist/__tests__/browser.test.js.map +1 -1
- package/dist/__tests__/dom.test.js +120 -0
- package/dist/__tests__/dom.test.js.map +1 -1
- package/dist/__tests__/utilities.test.js +43 -0
- package/dist/__tests__/utilities.test.js.map +1 -1
- package/dist/browser.d.ts +10 -17
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +33 -8
- package/dist/browser.js.map +1 -1
- package/dist/dom.d.ts +10 -18
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +20 -5
- package/dist/dom.js.map +1 -1
- package/dist/utilities.d.ts +4 -0
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +16 -1
- package/dist/utilities.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/browser.test.ts +160 -0
- package/src/__tests__/dom.test.ts +144 -0
- package/src/__tests__/utilities.test.ts +62 -0
- package/src/browser.ts +35 -8
- package/src/dom.ts +21 -5
- package/src/utilities.ts +19 -1
|
@@ -189,6 +189,82 @@ describe("Intersection", () => {
|
|
|
189
189
|
});
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
// ── ElementSize (additional) ──────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
describe("ElementSize (additional)", () => {
|
|
195
|
+
it("ref.current changes to a new element: old observer is disconnected and new one started", () => {
|
|
196
|
+
const disconnectFn = vi.fn();
|
|
197
|
+
const observeFn = vi.fn();
|
|
198
|
+
|
|
199
|
+
vi.stubGlobal("ResizeObserver", class {
|
|
200
|
+
observe = observeFn;
|
|
201
|
+
disconnect = disconnectFn;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const el1 = document.createElement("div");
|
|
205
|
+
const ref = { current: el1 as HTMLElement | null };
|
|
206
|
+
const es = new ElementSize(ref);
|
|
207
|
+
es.setup();
|
|
208
|
+
|
|
209
|
+
// Disconnect old observer and create new one for el2
|
|
210
|
+
es.onUnmount();
|
|
211
|
+
expect(disconnectFn).toHaveBeenCalled();
|
|
212
|
+
|
|
213
|
+
const el2 = document.createElement("div");
|
|
214
|
+
ref.current = el2;
|
|
215
|
+
const es2 = new ElementSize(ref);
|
|
216
|
+
es2.setup();
|
|
217
|
+
expect(observeFn).toHaveBeenCalledWith(el2);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ── Intersection (additional) ─────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
describe("Intersection (additional)", () => {
|
|
224
|
+
let observerCallback: IntersectionObserverCallback;
|
|
225
|
+
|
|
226
|
+
beforeEach(() => {
|
|
227
|
+
vi.stubGlobal("IntersectionObserver", class {
|
|
228
|
+
constructor(cb: IntersectionObserverCallback) { observerCallback = cb; }
|
|
229
|
+
observe = vi.fn();
|
|
230
|
+
disconnect = vi.fn();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("visibility toggles false after being true", () => {
|
|
235
|
+
const el = document.createElement("div");
|
|
236
|
+
const ref = { current: el };
|
|
237
|
+
const int = new Intersection(ref);
|
|
238
|
+
const { visible } = int.setup() as { visible: () => boolean };
|
|
239
|
+
|
|
240
|
+
observerCallback(
|
|
241
|
+
[{ isIntersecting: true } as IntersectionObserverEntry],
|
|
242
|
+
{} as IntersectionObserver,
|
|
243
|
+
);
|
|
244
|
+
expect(visible()).toBe(true);
|
|
245
|
+
|
|
246
|
+
observerCallback(
|
|
247
|
+
[{ isIntersecting: false } as IntersectionObserverEntry],
|
|
248
|
+
{} as IntersectionObserver,
|
|
249
|
+
);
|
|
250
|
+
expect(visible()).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── ScrollPosition (additional) ───────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
describe("ScrollPosition (additional)", () => {
|
|
257
|
+
it("setup() called twice registers only one scroll listener", () => {
|
|
258
|
+
const el = document.createElement("div");
|
|
259
|
+
const add = vi.spyOn(el, "addEventListener");
|
|
260
|
+
const sp = new ScrollPosition(el);
|
|
261
|
+
sp.setup();
|
|
262
|
+
sp.setup();
|
|
263
|
+
const scrollCalls = add.mock.calls.filter((c) => c[0] === "scroll").length;
|
|
264
|
+
expect(scrollCalls).toBe(1);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
192
268
|
// ── Focus ─────────────────────────────────────────────────────────────────────
|
|
193
269
|
|
|
194
270
|
describe("Focus", () => {
|
|
@@ -258,4 +334,72 @@ describe("Focus", () => {
|
|
|
258
334
|
document.body.removeChild(el1);
|
|
259
335
|
document.body.removeChild(el2);
|
|
260
336
|
});
|
|
337
|
+
|
|
338
|
+
it("setup() called twice registers only one listener pair", () => {
|
|
339
|
+
const el = document.createElement("input");
|
|
340
|
+
document.body.appendChild(el);
|
|
341
|
+
const add = vi.spyOn(el, "addEventListener");
|
|
342
|
+
const ref = { current: el };
|
|
343
|
+
const focus = new Focus(ref);
|
|
344
|
+
focus.setup();
|
|
345
|
+
focus.setup();
|
|
346
|
+
const focusCalls = add.mock.calls.filter((c) => c[0] === "focus").length;
|
|
347
|
+
expect(focusCalls).toBe(1);
|
|
348
|
+
document.body.removeChild(el);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ── WindowSize (additional) ───────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
describe("WindowSize (additional)", () => {
|
|
355
|
+
it("setup() called twice returns the same view object", () => {
|
|
356
|
+
const ws = new WindowSize();
|
|
357
|
+
const view1 = ws.setup();
|
|
358
|
+
const view2 = ws.setup();
|
|
359
|
+
expect(view1).toBe(view2);
|
|
360
|
+
ws.onUnmount();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("setup() called twice registers only one resize listener", () => {
|
|
364
|
+
const add = vi.spyOn(window, "addEventListener");
|
|
365
|
+
const ws = new WindowSize();
|
|
366
|
+
ws.setup();
|
|
367
|
+
ws.setup();
|
|
368
|
+
const resizeCalls = add.mock.calls.filter((c) => c[0] === "resize").length;
|
|
369
|
+
expect(resizeCalls).toBe(1);
|
|
370
|
+
ws.onUnmount();
|
|
371
|
+
vi.restoreAllMocks();
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// ── ElementSize (cache) ───────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
describe("ElementSize (cache)", () => {
|
|
378
|
+
it("setup() called twice returns the same view object", () => {
|
|
379
|
+
const el = document.createElement("div");
|
|
380
|
+
document.body.appendChild(el);
|
|
381
|
+
const ref = { current: el };
|
|
382
|
+
const es = new ElementSize(ref);
|
|
383
|
+
const view1 = es.setup();
|
|
384
|
+
const view2 = es.setup();
|
|
385
|
+
expect(view1).toBe(view2);
|
|
386
|
+
es.onUnmount();
|
|
387
|
+
document.body.removeChild(el);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// ── Intersection (cache) ──────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
describe("Intersection (cache)", () => {
|
|
394
|
+
it("setup() called twice returns the same view object", () => {
|
|
395
|
+
const el = document.createElement("div");
|
|
396
|
+
document.body.appendChild(el);
|
|
397
|
+
const ref = { current: el };
|
|
398
|
+
const inter = new Intersection(ref);
|
|
399
|
+
const view1 = inter.setup();
|
|
400
|
+
const view2 = inter.setup();
|
|
401
|
+
expect(view1).toBe(view2);
|
|
402
|
+
inter.onUnmount();
|
|
403
|
+
document.body.removeChild(el);
|
|
404
|
+
});
|
|
261
405
|
});
|
|
@@ -56,6 +56,17 @@ describe("Clipboard", () => {
|
|
|
56
56
|
expect(warn).toHaveBeenCalled();
|
|
57
57
|
warn.mockRestore();
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
it("setTimeout is cleared on onUnmount", async () => {
|
|
61
|
+
vi.useFakeTimers();
|
|
62
|
+
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
|
|
63
|
+
const cb = new Clipboard(1000);
|
|
64
|
+
const { copy } = cb.setup() as { copy: (t: string) => Promise<void> };
|
|
65
|
+
await copy("hello");
|
|
66
|
+
cb.onUnmount();
|
|
67
|
+
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
68
|
+
vi.useRealTimers();
|
|
69
|
+
});
|
|
59
70
|
});
|
|
60
71
|
|
|
61
72
|
// ── Geolocation ───────────────────────────────────────────────────────────────
|
|
@@ -111,6 +122,42 @@ describe("Geolocation", () => {
|
|
|
111
122
|
expect(loading()).toBe(false);
|
|
112
123
|
expect(error()).toBe(mockError);
|
|
113
124
|
});
|
|
125
|
+
|
|
126
|
+
it("success callback after unmount does not update state", () => {
|
|
127
|
+
let capturedSuccess: PositionCallback | undefined;
|
|
128
|
+
mockGeolocation((success) => { capturedSuccess = success; });
|
|
129
|
+
|
|
130
|
+
const geo = new Geolocation();
|
|
131
|
+
const { lat, lng, loading } = geo.setup() as {
|
|
132
|
+
lat: () => number | null;
|
|
133
|
+
lng: () => number | null;
|
|
134
|
+
loading: () => boolean;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
geo.onUnmount();
|
|
138
|
+
capturedSuccess!({ coords: { latitude: 99, longitude: 99 } } as GeolocationPosition);
|
|
139
|
+
|
|
140
|
+
expect(lat()).toBeNull();
|
|
141
|
+
expect(lng()).toBeNull();
|
|
142
|
+
expect(loading()).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("error callback after unmount does not update state", () => {
|
|
146
|
+
let capturedError: PositionErrorCallback | undefined;
|
|
147
|
+
mockGeolocation((_success, error) => { capturedError = error ?? undefined; });
|
|
148
|
+
|
|
149
|
+
const geo = new Geolocation();
|
|
150
|
+
const { error, loading } = geo.setup() as {
|
|
151
|
+
error: () => GeolocationPositionError | null;
|
|
152
|
+
loading: () => boolean;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
geo.onUnmount();
|
|
156
|
+
capturedError!({ code: 1, message: "denied" } as GeolocationPositionError);
|
|
157
|
+
|
|
158
|
+
expect(error()).toBeNull();
|
|
159
|
+
expect(loading()).toBe(true);
|
|
160
|
+
});
|
|
114
161
|
});
|
|
115
162
|
|
|
116
163
|
// ── TimeAgo ───────────────────────────────────────────────────────────────────
|
|
@@ -165,6 +212,13 @@ describe("TimeAgo", () => {
|
|
|
165
212
|
ta.onUnmount();
|
|
166
213
|
expect(clearInterval).toHaveBeenCalled();
|
|
167
214
|
});
|
|
215
|
+
|
|
216
|
+
it("future date produces a positive relative time string (e.g. 'in 30 seconds')", () => {
|
|
217
|
+
// A future timestamp produces a positive diff; Intl.RelativeTimeFormat formats it as "in X"
|
|
218
|
+
const ta = new TimeAgo(() => now + 30_000, "en");
|
|
219
|
+
const { value } = ta.setup() as { value: () => string };
|
|
220
|
+
expect(value()).toContain("in 30 seconds");
|
|
221
|
+
});
|
|
168
222
|
});
|
|
169
223
|
|
|
170
224
|
// ── Pagination ────────────────────────────────────────────────────────────────
|
|
@@ -251,4 +305,12 @@ describe("Pagination", () => {
|
|
|
251
305
|
const { totalPages } = p.setup() as { totalPages: () => number };
|
|
252
306
|
expect(totalPages()).toBe(3);
|
|
253
307
|
});
|
|
308
|
+
|
|
309
|
+
it("throws when pageSize is 0", () => {
|
|
310
|
+
expect(() => new Pagination({ total: 10, pageSize: 0 })).toThrow("pageSize must be greater than 0");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("throws when pageSize is negative", () => {
|
|
314
|
+
expect(() => new Pagination({ total: 10, pageSize: -1 })).toThrow("pageSize must be greater than 0");
|
|
315
|
+
});
|
|
254
316
|
});
|
package/src/browser.ts
CHANGED
|
@@ -6,17 +6,20 @@ export class MediaQuery extends Composable {
|
|
|
6
6
|
|
|
7
7
|
private _mql?: MediaQueryList;
|
|
8
8
|
private _handler?: (e: MediaQueryListEvent) => void;
|
|
9
|
+
private _view?: Record<string, unknown>;
|
|
9
10
|
|
|
10
11
|
constructor(private readonly query: string) {
|
|
11
12
|
super();
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
setup() {
|
|
16
|
+
if (this._view) return this._view;
|
|
15
17
|
this._mql = window.matchMedia(this.query);
|
|
16
18
|
const matches = signal(this._mql.matches);
|
|
17
19
|
this._handler = (e) => { matches.set(e.matches); };
|
|
18
20
|
this._mql.addEventListener("change", this._handler);
|
|
19
|
-
|
|
21
|
+
this._view = { matches };
|
|
22
|
+
return this._view;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
onUnmount() {
|
|
@@ -32,14 +35,17 @@ export class ColorScheme extends Composable {
|
|
|
32
35
|
|
|
33
36
|
private _mql?: MediaQueryList;
|
|
34
37
|
private _handler?: (e: MediaQueryListEvent) => void;
|
|
38
|
+
private _view?: Record<string, unknown>;
|
|
35
39
|
|
|
36
40
|
setup() {
|
|
41
|
+
if (this._view) return this._view;
|
|
37
42
|
this._mql = window.matchMedia("(prefers-color-scheme: dark)");
|
|
38
43
|
const isDark = signal(this._mql.matches);
|
|
39
44
|
const isLight = computed(() => !isDark());
|
|
40
45
|
this._handler = (e) => { isDark.set(e.matches); };
|
|
41
46
|
this._mql.addEventListener("change", this._handler);
|
|
42
|
-
|
|
47
|
+
this._view = { isDark, isLight };
|
|
48
|
+
return this._view;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
onUnmount() {
|
|
@@ -55,13 +61,16 @@ export class Mouse extends Composable {
|
|
|
55
61
|
|
|
56
62
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
57
63
|
private _handler = (_e: MouseEvent) => {};
|
|
64
|
+
private _view?: Record<string, unknown>;
|
|
58
65
|
|
|
59
66
|
setup() {
|
|
67
|
+
if (this._view) return this._view;
|
|
60
68
|
const x = signal(0);
|
|
61
69
|
const y = signal(0);
|
|
62
70
|
this._handler = (e) => { x.set(e.clientX); y.set(e.clientY); };
|
|
63
71
|
window.addEventListener("mousemove", this._handler);
|
|
64
|
-
|
|
72
|
+
this._view = { x, y };
|
|
73
|
+
return this._view;
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
onUnmount() {
|
|
@@ -76,21 +85,35 @@ export class KeyCombo extends Composable {
|
|
|
76
85
|
private _keydownHandler = (_e: KeyboardEvent) => {};
|
|
77
86
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
78
87
|
private _keyupHandler = () => {};
|
|
88
|
+
private _view?: Record<string, unknown>;
|
|
79
89
|
|
|
80
90
|
constructor(private readonly combo: string) {
|
|
81
91
|
super();
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
setup() {
|
|
85
|
-
|
|
95
|
+
if (this._view) return this._view;
|
|
96
|
+
const parts = this.combo.toLowerCase().split("+").map((p) => p.trim());
|
|
86
97
|
const pressed = signal(false);
|
|
87
98
|
|
|
99
|
+
const modifiers = ["ctrl", "shift", "alt", "meta"];
|
|
100
|
+
const key = parts.find((p) => !modifiers.includes(p));
|
|
101
|
+
|
|
102
|
+
// A combo with no non-modifier key is invalid and should never fire
|
|
103
|
+
if (!key) {
|
|
104
|
+
this._keyupHandler = () => { pressed.set(false); };
|
|
105
|
+
window.addEventListener("keydown", this._keydownHandler);
|
|
106
|
+
window.addEventListener("keyup", this._keyupHandler);
|
|
107
|
+
this._view = { pressed };
|
|
108
|
+
return this._view;
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
this._keydownHandler = (e) => {
|
|
89
112
|
const ctrl = parts.includes("ctrl") ? e.ctrlKey : true;
|
|
90
113
|
const shift = parts.includes("shift") ? e.shiftKey : true;
|
|
91
114
|
const alt = parts.includes("alt") ? e.altKey : true;
|
|
92
|
-
const
|
|
93
|
-
if (ctrl && shift && alt &&
|
|
115
|
+
const meta = parts.includes("meta") ? e.metaKey : true;
|
|
116
|
+
if (ctrl && shift && alt && meta && e.key.toLowerCase() === key) {
|
|
94
117
|
pressed.set(true);
|
|
95
118
|
}
|
|
96
119
|
};
|
|
@@ -98,7 +121,8 @@ export class KeyCombo extends Composable {
|
|
|
98
121
|
|
|
99
122
|
window.addEventListener("keydown", this._keydownHandler);
|
|
100
123
|
window.addEventListener("keyup", this._keyupHandler);
|
|
101
|
-
|
|
124
|
+
this._view = { pressed };
|
|
125
|
+
return this._view;
|
|
102
126
|
}
|
|
103
127
|
|
|
104
128
|
onUnmount() {
|
|
@@ -112,12 +136,14 @@ export class Idle extends Composable {
|
|
|
112
136
|
|
|
113
137
|
private _listeners: Array<[string, () => void]> = [];
|
|
114
138
|
private _timer?: ReturnType<typeof setTimeout>;
|
|
139
|
+
private _view?: Record<string, unknown>;
|
|
115
140
|
|
|
116
141
|
constructor(private readonly timeout = 60_000) {
|
|
117
142
|
super();
|
|
118
143
|
}
|
|
119
144
|
|
|
120
145
|
setup() {
|
|
146
|
+
if (this._view) return this._view;
|
|
121
147
|
const idle = signal(false);
|
|
122
148
|
const reset = () => {
|
|
123
149
|
idle.set(false);
|
|
@@ -131,7 +157,8 @@ export class Idle extends Composable {
|
|
|
131
157
|
});
|
|
132
158
|
|
|
133
159
|
reset();
|
|
134
|
-
|
|
160
|
+
this._view = { idle };
|
|
161
|
+
return this._view;
|
|
135
162
|
}
|
|
136
163
|
|
|
137
164
|
onUnmount() {
|
package/src/dom.ts
CHANGED
|
@@ -7,8 +7,10 @@ export class WindowSize extends Composable {
|
|
|
7
7
|
|
|
8
8
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
9
9
|
private _handler = () => {};
|
|
10
|
+
private _view?: Record<string, unknown>;
|
|
10
11
|
|
|
11
12
|
setup() {
|
|
13
|
+
if (this._view) return this._view;
|
|
12
14
|
const width = signal(window.innerWidth);
|
|
13
15
|
const height = signal(window.innerHeight);
|
|
14
16
|
this._handler = () => {
|
|
@@ -16,7 +18,8 @@ export class WindowSize extends Composable {
|
|
|
16
18
|
height.set(window.innerHeight);
|
|
17
19
|
};
|
|
18
20
|
window.addEventListener("resize", this._handler);
|
|
19
|
-
|
|
21
|
+
this._view = { width, height };
|
|
22
|
+
return this._view;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
onUnmount() {
|
|
@@ -31,6 +34,7 @@ export class ScrollPosition extends Composable {
|
|
|
31
34
|
private readonly _target: HTMLElement | Window;
|
|
32
35
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
33
36
|
private _handler = () => {};
|
|
37
|
+
private _view?: Record<string, unknown>;
|
|
34
38
|
|
|
35
39
|
constructor(target: HTMLElement | Window = window) {
|
|
36
40
|
super();
|
|
@@ -38,6 +42,7 @@ export class ScrollPosition extends Composable {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
setup() {
|
|
45
|
+
if (this._view) return this._view;
|
|
41
46
|
const x = signal(0);
|
|
42
47
|
const y = signal(0);
|
|
43
48
|
const t = this._target;
|
|
@@ -46,7 +51,8 @@ export class ScrollPosition extends Composable {
|
|
|
46
51
|
y.set(t === window ? window.scrollY : (t as HTMLElement).scrollTop);
|
|
47
52
|
};
|
|
48
53
|
t.addEventListener("scroll", this._handler);
|
|
49
|
-
|
|
54
|
+
this._view = { x, y };
|
|
55
|
+
return this._view;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
onUnmount() {
|
|
@@ -59,12 +65,14 @@ export class ElementSize extends Composable {
|
|
|
59
65
|
declare height: number;
|
|
60
66
|
|
|
61
67
|
private _observer?: ResizeObserver;
|
|
68
|
+
private _view?: Record<string, unknown>;
|
|
62
69
|
|
|
63
70
|
constructor(private readonly ref: { current: HTMLElement | null }) {
|
|
64
71
|
super();
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
setup() {
|
|
75
|
+
if (this._view) return this._view;
|
|
68
76
|
const width = signal(0);
|
|
69
77
|
const height = signal(0);
|
|
70
78
|
this._observer = new ResizeObserver(([entry]) => {
|
|
@@ -78,7 +86,8 @@ export class ElementSize extends Composable {
|
|
|
78
86
|
height.set(this.ref.current.offsetHeight);
|
|
79
87
|
}
|
|
80
88
|
});
|
|
81
|
-
|
|
89
|
+
this._view = { width, height };
|
|
90
|
+
return this._view;
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
onUnmount() {
|
|
@@ -90,6 +99,7 @@ export class Intersection extends Composable {
|
|
|
90
99
|
declare visible: boolean;
|
|
91
100
|
|
|
92
101
|
private _observer?: IntersectionObserver;
|
|
102
|
+
private _view?: Record<string, unknown>;
|
|
93
103
|
|
|
94
104
|
constructor(
|
|
95
105
|
private readonly ref: { current: HTMLElement | null },
|
|
@@ -99,6 +109,7 @@ export class Intersection extends Composable {
|
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
setup() {
|
|
112
|
+
if (this._view) return this._view;
|
|
102
113
|
const visible = signal(false);
|
|
103
114
|
this._observer = new IntersectionObserver(([entry]) => {
|
|
104
115
|
visible.set(entry.isIntersecting);
|
|
@@ -106,7 +117,8 @@ export class Intersection extends Composable {
|
|
|
106
117
|
effect(() => {
|
|
107
118
|
if (this.ref.current) this._observer?.observe(this.ref.current);
|
|
108
119
|
});
|
|
109
|
-
|
|
120
|
+
this._view = { visible };
|
|
121
|
+
return this._view;
|
|
110
122
|
}
|
|
111
123
|
|
|
112
124
|
onUnmount() {
|
|
@@ -117,11 +129,14 @@ export class Intersection extends Composable {
|
|
|
117
129
|
export class Focus extends Composable {
|
|
118
130
|
declare focused: boolean;
|
|
119
131
|
|
|
132
|
+
private _view?: Record<string, unknown>;
|
|
133
|
+
|
|
120
134
|
constructor(private readonly ref: { current: HTMLElement | null }) {
|
|
121
135
|
super();
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
setup() {
|
|
139
|
+
if (this._view) return this._view;
|
|
125
140
|
const focused = signal(false);
|
|
126
141
|
effect(() => {
|
|
127
142
|
const el = this.ref.current;
|
|
@@ -135,6 +150,7 @@ export class Focus extends Composable {
|
|
|
135
150
|
el.removeEventListener("blur", onBlur);
|
|
136
151
|
};
|
|
137
152
|
});
|
|
138
|
-
|
|
153
|
+
this._view = { focused };
|
|
154
|
+
return this._view;
|
|
139
155
|
}
|
|
140
156
|
}
|
package/src/utilities.ts
CHANGED
|
@@ -13,6 +13,8 @@ export class Clipboard extends Composable {
|
|
|
13
13
|
declare content: string;
|
|
14
14
|
declare copy: (text: string) => Promise<void>;
|
|
15
15
|
|
|
16
|
+
private _resetTimer?: ReturnType<typeof setTimeout>;
|
|
17
|
+
|
|
16
18
|
constructor(private readonly resetDelay = 2000) {
|
|
17
19
|
super();
|
|
18
20
|
}
|
|
@@ -27,7 +29,8 @@ export class Clipboard extends Composable {
|
|
|
27
29
|
await navigator.clipboard.writeText(text);
|
|
28
30
|
content.set(text);
|
|
29
31
|
copied.set(true);
|
|
30
|
-
|
|
32
|
+
clearTimeout(this._resetTimer);
|
|
33
|
+
this._resetTimer = setTimeout(() => { copied.set(false); }, resetDelay);
|
|
31
34
|
} catch {
|
|
32
35
|
console.warn("[Clipboard] Falha ao copiar");
|
|
33
36
|
}
|
|
@@ -35,6 +38,10 @@ export class Clipboard extends Composable {
|
|
|
35
38
|
|
|
36
39
|
return { copied, content, copy };
|
|
37
40
|
}
|
|
41
|
+
|
|
42
|
+
onUnmount() {
|
|
43
|
+
clearTimeout(this._resetTimer);
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
export class Geolocation extends Composable {
|
|
@@ -43,6 +50,8 @@ export class Geolocation extends Composable {
|
|
|
43
50
|
declare error: GeolocationPositionError | null;
|
|
44
51
|
declare loading: boolean;
|
|
45
52
|
|
|
53
|
+
private _active = true;
|
|
54
|
+
|
|
46
55
|
constructor(private readonly options?: PositionOptions) {
|
|
47
56
|
super();
|
|
48
57
|
}
|
|
@@ -55,11 +64,13 @@ export class Geolocation extends Composable {
|
|
|
55
64
|
|
|
56
65
|
navigator.geolocation.getCurrentPosition(
|
|
57
66
|
(pos) => {
|
|
67
|
+
if (!this._active) return;
|
|
58
68
|
lat.set(pos.coords.latitude);
|
|
59
69
|
lng.set(pos.coords.longitude);
|
|
60
70
|
loading.set(false);
|
|
61
71
|
},
|
|
62
72
|
(err) => {
|
|
73
|
+
if (!this._active) return;
|
|
63
74
|
error.set(err);
|
|
64
75
|
loading.set(false);
|
|
65
76
|
},
|
|
@@ -68,6 +79,10 @@ export class Geolocation extends Composable {
|
|
|
68
79
|
|
|
69
80
|
return { lat, lng, error, loading };
|
|
70
81
|
}
|
|
82
|
+
|
|
83
|
+
onUnmount() {
|
|
84
|
+
this._active = false;
|
|
85
|
+
}
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
export class TimeAgo extends Composable {
|
|
@@ -127,6 +142,9 @@ export class Pagination extends Composable {
|
|
|
127
142
|
|
|
128
143
|
constructor(private readonly options: PaginationOptions) {
|
|
129
144
|
super();
|
|
145
|
+
if (options.pageSize <= 0) {
|
|
146
|
+
throw new Error("pageSize must be greater than 0");
|
|
147
|
+
}
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
setup() {
|