@praxisjs/composables 0.1.5 → 1.0.0
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 +47 -0
- package/dist/__tests__/browser.test.js +151 -82
- package/dist/__tests__/browser.test.js.map +1 -1
- package/dist/__tests__/dom.test.js +168 -124
- package/dist/__tests__/dom.test.js.map +1 -1
- package/dist/__tests__/utilities.test.js +155 -193
- package/dist/__tests__/utilities.test.js.map +1 -1
- package/dist/browser.d.ts +55 -12
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +105 -49
- package/dist/browser.js.map +1 -1
- package/dist/dom.d.ts +60 -25
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +108 -74
- package/dist/dom.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/utilities.d.ts +68 -26
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +102 -94
- package/dist/utilities.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/browser.test.ts +172 -92
- package/src/__tests__/dom.test.ts +184 -135
- package/src/__tests__/utilities.test.ts +185 -213
- package/src/browser.ts +134 -53
- package/src/dom.ts +129 -82
- package/src/index.ts +11 -18
- package/src/utilities.ts +144 -111
|
@@ -1,282 +1,254 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
-
|
|
4
3
|
import { signal } from "@praxisjs/core/internal";
|
|
5
4
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
describe("usePagination", () => {
|
|
9
|
-
it("starts on page 1 by default", () => {
|
|
10
|
-
const p = usePagination({ total: 100, pageSize: 10 });
|
|
11
|
-
expect(p.page()).toBe(1);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("respects initial page option", () => {
|
|
15
|
-
const p = usePagination({ total: 100, pageSize: 10, initial: 3 });
|
|
16
|
-
expect(p.page()).toBe(3);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("computes total pages correctly", () => {
|
|
20
|
-
expect(usePagination({ total: 100, pageSize: 10 }).totalPages()).toBe(10);
|
|
21
|
-
expect(usePagination({ total: 101, pageSize: 10 }).totalPages()).toBe(11);
|
|
22
|
-
expect(usePagination({ total: 5, pageSize: 10 }).totalPages()).toBe(1);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("computes offset", () => {
|
|
26
|
-
const p = usePagination({ total: 100, pageSize: 10 });
|
|
27
|
-
expect(p.offset()).toBe(0);
|
|
28
|
-
p.goTo(3);
|
|
29
|
-
expect(p.offset()).toBe(20);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("next() advances the page", () => {
|
|
33
|
-
const p = usePagination({ total: 30, pageSize: 10 });
|
|
34
|
-
p.next();
|
|
35
|
-
expect(p.page()).toBe(2);
|
|
36
|
-
});
|
|
5
|
+
import { Clipboard, Geolocation, TimeAgo, Pagination } from "../utilities";
|
|
37
6
|
|
|
38
|
-
|
|
39
|
-
const p = usePagination({ total: 10, pageSize: 10 });
|
|
40
|
-
p.next();
|
|
41
|
-
expect(p.page()).toBe(1);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("prev() goes back", () => {
|
|
45
|
-
const p = usePagination({ total: 30, pageSize: 10, initial: 3 });
|
|
46
|
-
p.prev();
|
|
47
|
-
expect(p.page()).toBe(2);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("prev() does nothing on first page", () => {
|
|
51
|
-
const p = usePagination({ total: 30, pageSize: 10 });
|
|
52
|
-
p.prev();
|
|
53
|
-
expect(p.page()).toBe(1);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("goTo() clamps to valid range", () => {
|
|
57
|
-
const p = usePagination({ total: 30, pageSize: 10 });
|
|
58
|
-
p.goTo(0);
|
|
59
|
-
expect(p.page()).toBe(1);
|
|
60
|
-
p.goTo(99);
|
|
61
|
-
expect(p.page()).toBe(3);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("first() jumps to page 1", () => {
|
|
65
|
-
const p = usePagination({ total: 50, pageSize: 10, initial: 5 });
|
|
66
|
-
p.first();
|
|
67
|
-
expect(p.page()).toBe(1);
|
|
68
|
-
});
|
|
7
|
+
// ── Clipboard ─────────────────────────────────────────────────────────────────
|
|
69
8
|
|
|
70
|
-
|
|
71
|
-
const p = usePagination({ total: 50, pageSize: 10 });
|
|
72
|
-
p.last();
|
|
73
|
-
expect(p.page()).toBe(5);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("hasNext is false on last page", () => {
|
|
77
|
-
const p = usePagination({ total: 10, pageSize: 10 });
|
|
78
|
-
expect(p.hasNext()).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("hasPrev is false on first page", () => {
|
|
82
|
-
const p = usePagination({ total: 30, pageSize: 10 });
|
|
83
|
-
expect(p.hasPrev()).toBe(false);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("pages() returns array of page numbers", () => {
|
|
87
|
-
const p = usePagination({ total: 30, pageSize: 10 });
|
|
88
|
-
expect(p.pages()).toEqual([1, 2, 3]);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("pageSize() reflects the configured size", () => {
|
|
92
|
-
const p = usePagination({ total: 100, pageSize: 25 });
|
|
93
|
-
expect(p.pageSize()).toBe(25);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("accepts a factory function for options", () => {
|
|
97
|
-
const p = usePagination(() => ({ total: 50, pageSize: 5 }));
|
|
98
|
-
expect(p.totalPages()).toBe(10);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// ── useClipboard ──────────────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
describe("useClipboard", () => {
|
|
9
|
+
describe("Clipboard", () => {
|
|
105
10
|
beforeEach(() => {
|
|
106
|
-
vi.useFakeTimers();
|
|
107
11
|
Object.defineProperty(navigator, "clipboard", {
|
|
108
12
|
value: { writeText: vi.fn().mockResolvedValue(undefined) },
|
|
109
13
|
configurable: true,
|
|
110
|
-
writable: true,
|
|
111
14
|
});
|
|
112
15
|
});
|
|
113
16
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("copied starts as false", () => {
|
|
120
|
-
const { copied } = useClipboard();
|
|
17
|
+
it("starts with copied=false and empty content", () => {
|
|
18
|
+
const cb = new Clipboard();
|
|
19
|
+
const { copied, content } = cb.setup() as { copied: () => boolean; content: () => string };
|
|
121
20
|
expect(copied()).toBe(false);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("content starts as empty string", () => {
|
|
125
|
-
const { content } = useClipboard();
|
|
126
21
|
expect(content()).toBe("");
|
|
127
22
|
});
|
|
128
23
|
|
|
129
|
-
it("sets copied
|
|
130
|
-
const
|
|
24
|
+
it("sets copied=true and content after copy", async () => {
|
|
25
|
+
const cb = new Clipboard();
|
|
26
|
+
const { copy, copied, content } = cb.setup() as {
|
|
27
|
+
copy: (t: string) => Promise<void>;
|
|
28
|
+
copied: () => boolean;
|
|
29
|
+
content: () => string;
|
|
30
|
+
};
|
|
131
31
|
await copy("hello");
|
|
132
32
|
expect(copied()).toBe(true);
|
|
133
33
|
expect(content()).toBe("hello");
|
|
134
34
|
});
|
|
135
35
|
|
|
136
|
-
it("resets copied
|
|
137
|
-
|
|
36
|
+
it("resets copied after resetDelay", async () => {
|
|
37
|
+
vi.useFakeTimers();
|
|
38
|
+
const cb = new Clipboard(1000);
|
|
39
|
+
const { copy, copied } = cb.setup() as {
|
|
40
|
+
copy: (t: string) => Promise<void>;
|
|
41
|
+
copied: () => boolean;
|
|
42
|
+
};
|
|
138
43
|
await copy("test");
|
|
139
44
|
expect(copied()).toBe(true);
|
|
140
45
|
vi.advanceTimersByTime(1000);
|
|
141
46
|
expect(copied()).toBe(false);
|
|
47
|
+
vi.useRealTimers();
|
|
142
48
|
});
|
|
143
49
|
|
|
144
|
-
it("warns
|
|
145
|
-
(navigator.clipboard as
|
|
146
|
-
vi.fn().mockRejectedValue(new Error("denied"));
|
|
50
|
+
it("warns on clipboard failure", async () => {
|
|
51
|
+
(navigator.clipboard.writeText as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("denied"));
|
|
147
52
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
148
|
-
const
|
|
53
|
+
const cb = new Clipboard();
|
|
54
|
+
const { copy } = cb.setup() as { copy: (t: string) => Promise<void> };
|
|
149
55
|
await copy("fail");
|
|
150
|
-
expect(copied()).toBe(false);
|
|
151
56
|
expect(warn).toHaveBeenCalled();
|
|
152
57
|
warn.mockRestore();
|
|
153
58
|
});
|
|
154
59
|
});
|
|
155
60
|
|
|
156
|
-
// ──
|
|
157
|
-
|
|
158
|
-
describe("useGeolocation", () => {
|
|
159
|
-
it("starts with loading=true and lat/lng null", () => {
|
|
160
|
-
const successCb: PositionCallback[] = [];
|
|
161
|
-
Object.defineProperty(navigator, "geolocation", {
|
|
162
|
-
value: {
|
|
163
|
-
getCurrentPosition: (success: PositionCallback) => {
|
|
164
|
-
successCb.push(success);
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
configurable: true,
|
|
168
|
-
});
|
|
61
|
+
// ── Geolocation ───────────────────────────────────────────────────────────────
|
|
169
62
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
63
|
+
describe("Geolocation", () => {
|
|
64
|
+
function mockGeolocation(impl: (success: PositionCallback, error?: PositionErrorCallback | null) => void) {
|
|
65
|
+
vi.stubGlobal("navigator", {
|
|
66
|
+
...navigator,
|
|
67
|
+
geolocation: { getCurrentPosition: impl },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
afterEach(() => { vi.unstubAllGlobals(); });
|
|
72
|
+
|
|
73
|
+
it("starts in loading state", () => {
|
|
74
|
+
mockGeolocation(() => {});
|
|
75
|
+
const geo = new Geolocation();
|
|
76
|
+
const { loading, lat, lng, error } = geo.setup() as {
|
|
77
|
+
loading: () => boolean;
|
|
78
|
+
lat: () => number | null;
|
|
79
|
+
lng: () => number | null;
|
|
80
|
+
error: () => GeolocationPositionError | null;
|
|
81
|
+
};
|
|
82
|
+
expect(loading()).toBe(true);
|
|
83
|
+
expect(lat()).toBeNull();
|
|
84
|
+
expect(lng()).toBeNull();
|
|
85
|
+
expect(error()).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("sets coordinates on success", () => {
|
|
89
|
+
mockGeolocation((success) => {
|
|
90
|
+
success({ coords: { latitude: 51.5, longitude: -0.1 } } as GeolocationPosition);
|
|
91
|
+
});
|
|
92
|
+
const geo = new Geolocation();
|
|
93
|
+
const { loading, lat, lng } = geo.setup() as {
|
|
94
|
+
loading: () => boolean;
|
|
95
|
+
lat: () => number | null;
|
|
96
|
+
lng: () => number | null;
|
|
97
|
+
};
|
|
98
|
+
expect(loading()).toBe(false);
|
|
99
|
+
expect(lat()).toBe(51.5);
|
|
100
|
+
expect(lng()).toBe(-0.1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("sets error on failure", () => {
|
|
104
|
+
const mockError = { code: 1, message: "denied" } as GeolocationPositionError;
|
|
105
|
+
mockGeolocation((_s, error) => { error!(mockError); });
|
|
106
|
+
const geo = new Geolocation();
|
|
107
|
+
const { loading, error } = geo.setup() as {
|
|
108
|
+
loading: () => boolean;
|
|
109
|
+
error: () => GeolocationPositionError | null;
|
|
110
|
+
};
|
|
111
|
+
expect(loading()).toBe(false);
|
|
112
|
+
expect(error()).toBe(mockError);
|
|
174
113
|
});
|
|
114
|
+
});
|
|
175
115
|
|
|
176
|
-
|
|
177
|
-
let successCb: PositionCallback | null = null;
|
|
178
|
-
Object.defineProperty(navigator, "geolocation", {
|
|
179
|
-
value: {
|
|
180
|
-
getCurrentPosition: (success: PositionCallback) => {
|
|
181
|
-
successCb = success;
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
configurable: true,
|
|
185
|
-
});
|
|
116
|
+
// ── TimeAgo ───────────────────────────────────────────────────────────────────
|
|
186
117
|
|
|
187
|
-
|
|
118
|
+
describe("TimeAgo", () => {
|
|
119
|
+
beforeEach(() => { vi.useFakeTimers(); });
|
|
120
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
188
121
|
|
|
189
|
-
|
|
190
|
-
coords: { latitude: 10.5, longitude: 20.5 } as GeolocationCoordinates,
|
|
191
|
-
} as GeolocationPosition);
|
|
122
|
+
const now = Date.now();
|
|
192
123
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
124
|
+
it("formats seconds ago", () => {
|
|
125
|
+
const ta = new TimeAgo(() => now - 30_000, "en");
|
|
126
|
+
const { value } = ta.setup() as { value: () => string };
|
|
127
|
+
expect(value()).toContain("30 seconds ago");
|
|
196
128
|
});
|
|
197
129
|
|
|
198
|
-
it("
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
130
|
+
it("formats minutes ago", () => {
|
|
131
|
+
const ta = new TimeAgo(() => now - 5 * 60_000, "en");
|
|
132
|
+
const { value } = ta.setup() as { value: () => string };
|
|
133
|
+
expect(value()).toContain("5 minutes ago");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("formats hours ago", () => {
|
|
137
|
+
const ta = new TimeAgo(() => now - 2 * 3_600_000, "en");
|
|
138
|
+
const { value } = ta.setup() as { value: () => string };
|
|
139
|
+
expect(value()).toContain("2 hours ago");
|
|
140
|
+
});
|
|
208
141
|
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
142
|
+
it("formats days ago", () => {
|
|
143
|
+
const ta = new TimeAgo(() => now - 3 * 86_400_000, "en");
|
|
144
|
+
const { value } = ta.setup() as { value: () => string };
|
|
145
|
+
expect(value()).toContain("3 days ago");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("formats months ago", () => {
|
|
149
|
+
const ta = new TimeAgo(() => now - 60 * 86_400_000, "en");
|
|
150
|
+
const { value } = ta.setup() as { value: () => string };
|
|
151
|
+
expect(value()).toContain("2 months ago");
|
|
152
|
+
});
|
|
212
153
|
|
|
213
|
-
|
|
214
|
-
|
|
154
|
+
it("accepts a signal as source", () => {
|
|
155
|
+
const src = signal(now - 30_000);
|
|
156
|
+
const ta = new TimeAgo(src, "en");
|
|
157
|
+
const { value } = ta.setup() as { value: () => string };
|
|
158
|
+
expect(value()).toContain("30 seconds ago");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("clears interval on unmount", () => {
|
|
162
|
+
const clearInterval = vi.spyOn(globalThis, "clearInterval");
|
|
163
|
+
const ta = new TimeAgo(() => now, "en");
|
|
164
|
+
ta.setup();
|
|
165
|
+
ta.onUnmount();
|
|
166
|
+
expect(clearInterval).toHaveBeenCalled();
|
|
215
167
|
});
|
|
216
168
|
});
|
|
217
169
|
|
|
218
|
-
// ──
|
|
170
|
+
// ── Pagination ────────────────────────────────────────────────────────────────
|
|
219
171
|
|
|
220
|
-
describe("
|
|
221
|
-
|
|
222
|
-
|
|
172
|
+
describe("Pagination", () => {
|
|
173
|
+
it("initialises with defaults", () => {
|
|
174
|
+
const p = new Pagination({ total: 100, pageSize: 10 });
|
|
175
|
+
const s = p.setup() as {
|
|
176
|
+
page: () => number; totalPages: () => number; offset: () => number;
|
|
177
|
+
hasNext: () => boolean; hasPrev: () => boolean; pageSize: () => number;
|
|
178
|
+
};
|
|
179
|
+
expect(s.page()).toBe(1);
|
|
180
|
+
expect(s.totalPages()).toBe(10);
|
|
181
|
+
expect(s.offset()).toBe(0);
|
|
182
|
+
expect(s.hasNext()).toBe(true);
|
|
183
|
+
expect(s.hasPrev()).toBe(false);
|
|
184
|
+
expect(s.pageSize()).toBe(10);
|
|
223
185
|
});
|
|
224
186
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
187
|
+
it("respects initial page", () => {
|
|
188
|
+
const p = new Pagination({ total: 100, pageSize: 10, initial: 3 });
|
|
189
|
+
const { page, offset } = p.setup() as { page: () => number; offset: () => number };
|
|
190
|
+
expect(page()).toBe(3);
|
|
191
|
+
expect(offset()).toBe(20);
|
|
228
192
|
});
|
|
229
193
|
|
|
230
|
-
it("
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
194
|
+
it("next() advances page", () => {
|
|
195
|
+
const p = new Pagination({ total: 30, pageSize: 10 });
|
|
196
|
+
const { page, next } = p.setup() as { page: () => number; next: () => void };
|
|
197
|
+
(next as () => void)();
|
|
198
|
+
expect(page()).toBe(2);
|
|
234
199
|
});
|
|
235
200
|
|
|
236
|
-
it("
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
|
|
201
|
+
it("prev() goes back a page", () => {
|
|
202
|
+
const p = new Pagination({ total: 30, pageSize: 10, initial: 3 });
|
|
203
|
+
const { page, prev } = p.setup() as { page: () => number; prev: () => void };
|
|
204
|
+
(prev as () => void)();
|
|
205
|
+
expect(page()).toBe(2);
|
|
240
206
|
});
|
|
241
207
|
|
|
242
|
-
it("
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
208
|
+
it("next() does not go past last page", () => {
|
|
209
|
+
const p = new Pagination({ total: 10, pageSize: 10 });
|
|
210
|
+
const { page, next } = p.setup() as { page: () => number; next: () => void };
|
|
211
|
+
(next as () => void)();
|
|
212
|
+
expect(page()).toBe(1);
|
|
246
213
|
});
|
|
247
214
|
|
|
248
|
-
it("
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
215
|
+
it("prev() does not go before first page", () => {
|
|
216
|
+
const p = new Pagination({ total: 30, pageSize: 10 });
|
|
217
|
+
const { page, prev } = p.setup() as { page: () => number; prev: () => void };
|
|
218
|
+
(prev as () => void)();
|
|
219
|
+
expect(page()).toBe(1);
|
|
252
220
|
});
|
|
253
221
|
|
|
254
|
-
it("
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
222
|
+
it("goTo() jumps to a page", () => {
|
|
223
|
+
const p = new Pagination({ total: 50, pageSize: 10 });
|
|
224
|
+
const { page, goTo } = p.setup() as { page: () => number; goTo: (n: number) => void };
|
|
225
|
+
(goTo as (n: number) => void)(4);
|
|
226
|
+
expect(page()).toBe(4);
|
|
258
227
|
});
|
|
259
228
|
|
|
260
|
-
it("
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
|
|
229
|
+
it("first() jumps to page 1", () => {
|
|
230
|
+
const p = new Pagination({ total: 50, pageSize: 10, initial: 5 });
|
|
231
|
+
const { page, first } = p.setup() as { page: () => number; first: () => void };
|
|
232
|
+
(first as () => void)();
|
|
233
|
+
expect(page()).toBe(1);
|
|
264
234
|
});
|
|
265
235
|
|
|
266
|
-
it("
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
236
|
+
it("last() jumps to last page", () => {
|
|
237
|
+
const p = new Pagination({ total: 50, pageSize: 10 });
|
|
238
|
+
const { page, last } = p.setup() as { page: () => number; last: () => void };
|
|
239
|
+
(last as () => void)();
|
|
240
|
+
expect(page()).toBe(5);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("pages() returns array of page numbers", () => {
|
|
244
|
+
const p = new Pagination({ total: 30, pageSize: 10 });
|
|
245
|
+
const { pages } = p.setup() as { pages: () => number[] };
|
|
246
|
+
expect((pages as () => number[])()).toEqual([1, 2, 3]);
|
|
270
247
|
});
|
|
271
248
|
|
|
272
|
-
it("
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
vi.advanceTimersByTime(60_000);
|
|
277
|
-
// After ticking, the computed re-evaluates (may or may not change the string)
|
|
278
|
-
expect(typeof t()).toBe("string");
|
|
279
|
-
// The interval ran — just verify no errors
|
|
280
|
-
expect(before).toBeTruthy();
|
|
249
|
+
it("clamps totalPages for fractional divisions", () => {
|
|
250
|
+
const p = new Pagination({ total: 25, pageSize: 10 });
|
|
251
|
+
const { totalPages } = p.setup() as { totalPages: () => number };
|
|
252
|
+
expect(totalPages()).toBe(3);
|
|
281
253
|
});
|
|
282
254
|
});
|
package/src/browser.ts
CHANGED
|
@@ -1,63 +1,144 @@
|
|
|
1
|
+
import { Composable } from "@praxisjs/core";
|
|
1
2
|
import { signal, computed } from "@praxisjs/core/internal";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
|
|
4
|
+
export class MediaQuery extends Composable {
|
|
5
|
+
declare matches: boolean;
|
|
6
|
+
|
|
7
|
+
private _mql?: MediaQueryList;
|
|
8
|
+
private _handler?: (e: MediaQueryListEvent) => void;
|
|
9
|
+
|
|
10
|
+
constructor(private readonly query: string) {
|
|
11
|
+
super();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setup() {
|
|
15
|
+
this._mql = window.matchMedia(this.query);
|
|
16
|
+
const matches = signal(this._mql.matches);
|
|
17
|
+
this._handler = (e) => { matches.set(e.matches); };
|
|
18
|
+
this._mql.addEventListener("change", this._handler);
|
|
19
|
+
return { matches };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onUnmount() {
|
|
23
|
+
if (this._mql && this._handler) {
|
|
24
|
+
this._mql.removeEventListener("change", this._handler);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
11
27
|
}
|
|
12
28
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
export class ColorScheme extends Composable {
|
|
30
|
+
declare isDark: boolean;
|
|
31
|
+
declare isLight: boolean;
|
|
32
|
+
|
|
33
|
+
private _mql?: MediaQueryList;
|
|
34
|
+
private _handler?: (e: MediaQueryListEvent) => void;
|
|
35
|
+
|
|
36
|
+
setup() {
|
|
37
|
+
this._mql = window.matchMedia("(prefers-color-scheme: dark)");
|
|
38
|
+
const isDark = signal(this._mql.matches);
|
|
39
|
+
const isLight = computed(() => !isDark());
|
|
40
|
+
this._handler = (e) => { isDark.set(e.matches); };
|
|
41
|
+
this._mql.addEventListener("change", this._handler);
|
|
42
|
+
return { isDark, isLight };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onUnmount() {
|
|
46
|
+
if (this._mql && this._handler) {
|
|
47
|
+
this._mql.removeEventListener("change", this._handler);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
17
50
|
}
|
|
18
51
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
export class Mouse extends Composable {
|
|
53
|
+
declare x: number;
|
|
54
|
+
declare y: number;
|
|
55
|
+
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
57
|
+
private _handler = (_e: MouseEvent) => {};
|
|
58
|
+
|
|
59
|
+
setup() {
|
|
60
|
+
const x = signal(0);
|
|
61
|
+
const y = signal(0);
|
|
62
|
+
this._handler = (e) => { x.set(e.clientX); y.set(e.clientY); };
|
|
63
|
+
window.addEventListener("mousemove", this._handler);
|
|
64
|
+
return { x, y };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onUnmount() {
|
|
68
|
+
window.removeEventListener("mousemove", this._handler);
|
|
69
|
+
}
|
|
27
70
|
}
|
|
28
71
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
pressed
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
export class KeyCombo extends Composable {
|
|
73
|
+
declare pressed: boolean;
|
|
74
|
+
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
76
|
+
private _keydownHandler = (_e: KeyboardEvent) => {};
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
78
|
+
private _keyupHandler = () => {};
|
|
79
|
+
|
|
80
|
+
constructor(private readonly combo: string) {
|
|
81
|
+
super();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setup() {
|
|
85
|
+
const parts = this.combo.toLowerCase().split("+");
|
|
86
|
+
const pressed = signal(false);
|
|
87
|
+
|
|
88
|
+
this._keydownHandler = (e) => {
|
|
89
|
+
const ctrl = parts.includes("ctrl") ? e.ctrlKey : true;
|
|
90
|
+
const shift = parts.includes("shift") ? e.shiftKey : true;
|
|
91
|
+
const alt = parts.includes("alt") ? e.altKey : true;
|
|
92
|
+
const key = parts.find((p) => !["ctrl", "shift", "alt", "meta"].includes(p));
|
|
93
|
+
if (ctrl && shift && alt && (!key || e.key.toLowerCase() === key)) {
|
|
94
|
+
pressed.set(true);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
this._keyupHandler = () => { pressed.set(false); };
|
|
98
|
+
|
|
99
|
+
window.addEventListener("keydown", this._keydownHandler);
|
|
100
|
+
window.addEventListener("keyup", this._keyupHandler);
|
|
101
|
+
return { pressed };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
onUnmount() {
|
|
105
|
+
window.removeEventListener("keydown", this._keydownHandler);
|
|
106
|
+
window.removeEventListener("keyup", this._keyupHandler);
|
|
107
|
+
}
|
|
46
108
|
}
|
|
47
109
|
|
|
48
|
-
export
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
110
|
+
export class Idle extends Composable {
|
|
111
|
+
declare idle: boolean;
|
|
112
|
+
|
|
113
|
+
private _listeners: Array<[string, () => void]> = [];
|
|
114
|
+
private _timer?: ReturnType<typeof setTimeout>;
|
|
115
|
+
|
|
116
|
+
constructor(private readonly timeout = 60_000) {
|
|
117
|
+
super();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setup() {
|
|
121
|
+
const idle = signal(false);
|
|
122
|
+
const reset = () => {
|
|
123
|
+
idle.set(false);
|
|
124
|
+
clearTimeout(this._timer);
|
|
125
|
+
this._timer = setTimeout(() => { idle.set(true); }, this.timeout);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
["mousemove", "keydown", "click", "scroll", "touchstart"].forEach((event) => {
|
|
129
|
+
window.addEventListener(event, reset, { passive: true });
|
|
130
|
+
this._listeners.push([event, reset]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
reset();
|
|
134
|
+
return { idle };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
onUnmount() {
|
|
138
|
+
clearTimeout(this._timer);
|
|
139
|
+
this._listeners.forEach(([event, handler]) => {
|
|
140
|
+
window.removeEventListener(event, handler);
|
|
141
|
+
});
|
|
142
|
+
this._listeners = [];
|
|
143
|
+
}
|
|
63
144
|
}
|