@praxisjs/composables 1.0.5 → 1.1.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 +82 -9
- package/dist/__tests__/browser.test.js +7 -0
- package/dist/__tests__/browser.test.js.map +1 -1
- package/dist/__tests__/dom.test.js +106 -22
- package/dist/__tests__/dom.test.js.map +1 -1
- package/dist/__tests__/list.test.d.ts +2 -0
- package/dist/__tests__/list.test.d.ts.map +1 -0
- package/dist/__tests__/list.test.js +156 -0
- package/dist/__tests__/list.test.js.map +1 -0
- package/dist/__tests__/utilities.test.js +10 -0
- package/dist/__tests__/utilities.test.js.map +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +23 -14
- package/dist/browser.js.map +1 -1
- package/dist/dom.d.ts +28 -6
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +69 -54
- package/dist/dom.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/list.d.ts +30 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +47 -0
- package/dist/list.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/browser.test.ts +8 -0
- package/src/__tests__/dom.test.ts +114 -23
- package/src/__tests__/list.test.ts +210 -0
- package/src/__tests__/utilities.test.ts +11 -0
- package/src/browser.ts +31 -15
- package/src/dom.ts +79 -51
- package/src/index.ts +1 -0
- package/src/list.ts +77 -0
package/src/browser.ts
CHANGED
|
@@ -16,7 +16,9 @@ export class MediaQuery extends Composable {
|
|
|
16
16
|
if (this._view) return this._view;
|
|
17
17
|
this._mql = window.matchMedia(this.query);
|
|
18
18
|
const matches = signal(this._mql.matches);
|
|
19
|
-
this._handler = (e) => {
|
|
19
|
+
this._handler = (e) => {
|
|
20
|
+
matches.set(e.matches);
|
|
21
|
+
};
|
|
20
22
|
this._mql.addEventListener("change", this._handler);
|
|
21
23
|
this._view = { matches };
|
|
22
24
|
return this._view;
|
|
@@ -42,7 +44,9 @@ export class ColorScheme extends Composable {
|
|
|
42
44
|
this._mql = window.matchMedia("(prefers-color-scheme: dark)");
|
|
43
45
|
const isDark = signal(this._mql.matches);
|
|
44
46
|
const isLight = computed(() => !isDark());
|
|
45
|
-
this._handler = (e) => {
|
|
47
|
+
this._handler = (e) => {
|
|
48
|
+
isDark.set(e.matches);
|
|
49
|
+
};
|
|
46
50
|
this._mql.addEventListener("change", this._handler);
|
|
47
51
|
this._view = { isDark, isLight };
|
|
48
52
|
return this._view;
|
|
@@ -59,15 +63,17 @@ export class Mouse extends Composable {
|
|
|
59
63
|
declare x: number;
|
|
60
64
|
declare y: number;
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
private _handler = (_e: MouseEvent) => {};
|
|
66
|
+
private _handler!: (e: MouseEvent) => void;
|
|
64
67
|
private _view?: Record<string, unknown>;
|
|
65
68
|
|
|
66
69
|
setup() {
|
|
67
70
|
if (this._view) return this._view;
|
|
68
71
|
const x = signal(0);
|
|
69
72
|
const y = signal(0);
|
|
70
|
-
this._handler = (e) => {
|
|
73
|
+
this._handler = (e) => {
|
|
74
|
+
x.set(e.clientX);
|
|
75
|
+
y.set(e.clientY);
|
|
76
|
+
};
|
|
71
77
|
window.addEventListener("mousemove", this._handler);
|
|
72
78
|
this._view = { x, y };
|
|
73
79
|
return this._view;
|
|
@@ -83,8 +89,7 @@ export class KeyCombo extends Composable {
|
|
|
83
89
|
|
|
84
90
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
85
91
|
private _keydownHandler = (_e: KeyboardEvent) => {};
|
|
86
|
-
|
|
87
|
-
private _keyupHandler = () => {};
|
|
92
|
+
private _keyupHandler!: () => void;
|
|
88
93
|
private _view?: Record<string, unknown>;
|
|
89
94
|
|
|
90
95
|
constructor(private readonly combo: string) {
|
|
@@ -93,7 +98,10 @@ export class KeyCombo extends Composable {
|
|
|
93
98
|
|
|
94
99
|
setup() {
|
|
95
100
|
if (this._view) return this._view;
|
|
96
|
-
const parts = this.combo
|
|
101
|
+
const parts = this.combo
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.split("+")
|
|
104
|
+
.map((p) => p.trim());
|
|
97
105
|
const pressed = signal(false);
|
|
98
106
|
|
|
99
107
|
const modifiers = ["ctrl", "shift", "alt", "meta"];
|
|
@@ -101,7 +109,9 @@ export class KeyCombo extends Composable {
|
|
|
101
109
|
|
|
102
110
|
// A combo with no non-modifier key is invalid and should never fire
|
|
103
111
|
if (!key) {
|
|
104
|
-
this._keyupHandler = () => {
|
|
112
|
+
this._keyupHandler = () => {
|
|
113
|
+
pressed.set(false);
|
|
114
|
+
};
|
|
105
115
|
window.addEventListener("keydown", this._keydownHandler);
|
|
106
116
|
window.addEventListener("keyup", this._keyupHandler);
|
|
107
117
|
this._view = { pressed };
|
|
@@ -117,7 +127,9 @@ export class KeyCombo extends Composable {
|
|
|
117
127
|
pressed.set(true);
|
|
118
128
|
}
|
|
119
129
|
};
|
|
120
|
-
this._keyupHandler = () => {
|
|
130
|
+
this._keyupHandler = () => {
|
|
131
|
+
pressed.set(false);
|
|
132
|
+
};
|
|
121
133
|
|
|
122
134
|
window.addEventListener("keydown", this._keydownHandler);
|
|
123
135
|
window.addEventListener("keyup", this._keyupHandler);
|
|
@@ -148,13 +160,17 @@ export class Idle extends Composable {
|
|
|
148
160
|
const reset = () => {
|
|
149
161
|
idle.set(false);
|
|
150
162
|
clearTimeout(this._timer);
|
|
151
|
-
this._timer = setTimeout(() => {
|
|
163
|
+
this._timer = setTimeout(() => {
|
|
164
|
+
idle.set(true);
|
|
165
|
+
}, this.timeout);
|
|
152
166
|
};
|
|
153
167
|
|
|
154
|
-
["mousemove", "keydown", "click", "scroll", "touchstart"].forEach(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
["mousemove", "keydown", "click", "scroll", "touchstart"].forEach(
|
|
169
|
+
(event) => {
|
|
170
|
+
window.addEventListener(event, reset, { passive: true });
|
|
171
|
+
this._listeners.push([event, reset]);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
158
174
|
|
|
159
175
|
reset();
|
|
160
176
|
this._view = { idle };
|
package/src/dom.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Composable } from "@praxisjs/core";
|
|
2
|
-
import { signal
|
|
2
|
+
import { signal } from "@praxisjs/core/internal";
|
|
3
3
|
|
|
4
4
|
export class WindowSize extends Composable {
|
|
5
5
|
declare width: number;
|
|
6
6
|
declare height: number;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
private _handler = () => {};
|
|
8
|
+
private _handler!: () => void;
|
|
10
9
|
private _view?: Record<string, unknown>;
|
|
11
10
|
|
|
12
11
|
setup() {
|
|
@@ -31,31 +30,48 @@ export class ScrollPosition extends Composable {
|
|
|
31
30
|
declare x: number;
|
|
32
31
|
declare y: number;
|
|
33
32
|
|
|
34
|
-
private readonly
|
|
35
|
-
|
|
36
|
-
private _handler
|
|
37
|
-
private
|
|
33
|
+
private readonly _ref?: { current: HTMLElement | null };
|
|
34
|
+
private readonly _staticTarget?: HTMLElement | Window;
|
|
35
|
+
private _handler!: () => void;
|
|
36
|
+
private _x!: ReturnType<typeof signal<number>>;
|
|
37
|
+
private _y!: ReturnType<typeof signal<number>>;
|
|
38
|
+
private _mounted = false;
|
|
38
39
|
|
|
39
|
-
constructor(target: HTMLElement | Window = window) {
|
|
40
|
+
constructor(target: HTMLElement | Window | { current: HTMLElement | null } = window) {
|
|
40
41
|
super();
|
|
41
|
-
|
|
42
|
+
if (typeof target === "object" && "current" in target) {
|
|
43
|
+
this._ref = target as { current: HTMLElement | null };
|
|
44
|
+
} else {
|
|
45
|
+
this._staticTarget = target;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private get _target(): HTMLElement | Window {
|
|
50
|
+
return this._ref ? (this._ref.current ?? window) : (this._staticTarget ?? window);
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
setup() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
this._x = signal(0);
|
|
55
|
+
this._y = signal(0);
|
|
56
|
+
return { x: this._x, y: this._y };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onMount() {
|
|
60
|
+
if (this._mounted) return;
|
|
61
|
+
this._mounted = true;
|
|
48
62
|
const t = this._target;
|
|
49
63
|
this._handler = () => {
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
this._x.set(t === window ? window.scrollX : (t as HTMLElement).scrollLeft);
|
|
65
|
+
this._y.set(t === window ? window.scrollY : (t as HTMLElement).scrollTop);
|
|
52
66
|
};
|
|
53
67
|
t.addEventListener("scroll", this._handler);
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
// set initial values
|
|
69
|
+
this._x.set(t === window ? window.scrollX : (t as HTMLElement).scrollLeft);
|
|
70
|
+
this._y.set(t === window ? window.scrollY : (t as HTMLElement).scrollTop);
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
onUnmount() {
|
|
74
|
+
this._mounted = false;
|
|
59
75
|
this._target.removeEventListener("scroll", this._handler);
|
|
60
76
|
}
|
|
61
77
|
}
|
|
@@ -65,6 +81,8 @@ export class ElementSize extends Composable {
|
|
|
65
81
|
declare height: number;
|
|
66
82
|
|
|
67
83
|
private _observer?: ResizeObserver;
|
|
84
|
+
private _width!: ReturnType<typeof signal<number>>;
|
|
85
|
+
private _height!: ReturnType<typeof signal<number>>;
|
|
68
86
|
private _view?: Record<string, unknown>;
|
|
69
87
|
|
|
70
88
|
constructor(private readonly ref: { current: HTMLElement | null }) {
|
|
@@ -73,23 +91,24 @@ export class ElementSize extends Composable {
|
|
|
73
91
|
|
|
74
92
|
setup() {
|
|
75
93
|
if (this._view) return this._view;
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
this._width = signal(0);
|
|
95
|
+
this._height = signal(0);
|
|
78
96
|
this._observer = new ResizeObserver(([entry]) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
});
|
|
82
|
-
effect(() => {
|
|
83
|
-
if (this.ref.current) {
|
|
84
|
-
this._observer?.observe(this.ref.current);
|
|
85
|
-
width.set(this.ref.current.offsetWidth);
|
|
86
|
-
height.set(this.ref.current.offsetHeight);
|
|
87
|
-
}
|
|
97
|
+
this._width.set(entry.contentRect.width);
|
|
98
|
+
this._height.set(entry.contentRect.height);
|
|
88
99
|
});
|
|
89
|
-
this._view = { width, height };
|
|
100
|
+
this._view = { width: this._width, height: this._height };
|
|
90
101
|
return this._view;
|
|
91
102
|
}
|
|
92
103
|
|
|
104
|
+
onMount() {
|
|
105
|
+
const el = this.ref.current;
|
|
106
|
+
if (!el) return;
|
|
107
|
+
this._observer?.observe(el);
|
|
108
|
+
this._width.set(el.offsetWidth);
|
|
109
|
+
this._height.set(el.offsetHeight);
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
onUnmount() {
|
|
94
113
|
this._observer?.disconnect();
|
|
95
114
|
}
|
|
@@ -99,6 +118,7 @@ export class Intersection extends Composable {
|
|
|
99
118
|
declare visible: boolean;
|
|
100
119
|
|
|
101
120
|
private _observer?: IntersectionObserver;
|
|
121
|
+
private _visible!: ReturnType<typeof signal<boolean>>;
|
|
102
122
|
private _view?: Record<string, unknown>;
|
|
103
123
|
|
|
104
124
|
constructor(
|
|
@@ -110,17 +130,19 @@ export class Intersection extends Composable {
|
|
|
110
130
|
|
|
111
131
|
setup() {
|
|
112
132
|
if (this._view) return this._view;
|
|
113
|
-
|
|
133
|
+
this._visible = signal(false);
|
|
114
134
|
this._observer = new IntersectionObserver(([entry]) => {
|
|
115
|
-
|
|
135
|
+
this._visible.set(entry.isIntersecting);
|
|
116
136
|
}, this.options);
|
|
117
|
-
|
|
118
|
-
if (this.ref.current) this._observer?.observe(this.ref.current);
|
|
119
|
-
});
|
|
120
|
-
this._view = { visible };
|
|
137
|
+
this._view = { visible: this._visible };
|
|
121
138
|
return this._view;
|
|
122
139
|
}
|
|
123
140
|
|
|
141
|
+
onMount() {
|
|
142
|
+
const el = this.ref.current;
|
|
143
|
+
if (el) this._observer?.observe(el);
|
|
144
|
+
}
|
|
145
|
+
|
|
124
146
|
onUnmount() {
|
|
125
147
|
this._observer?.disconnect();
|
|
126
148
|
}
|
|
@@ -129,28 +151,34 @@ export class Intersection extends Composable {
|
|
|
129
151
|
export class Focus extends Composable {
|
|
130
152
|
declare focused: boolean;
|
|
131
153
|
|
|
132
|
-
private
|
|
154
|
+
private _focused!: ReturnType<typeof signal<boolean>>;
|
|
155
|
+
private readonly _onFocus = () => { this._focused.set(true); };
|
|
156
|
+
private readonly _onBlur = () => { this._focused.set(false); };
|
|
157
|
+
private _mounted = false;
|
|
133
158
|
|
|
134
159
|
constructor(private readonly ref: { current: HTMLElement | null }) {
|
|
135
160
|
super();
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
setup() {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
this.
|
|
154
|
-
|
|
164
|
+
this._focused = signal(false);
|
|
165
|
+
return { focused: this._focused };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
onMount() {
|
|
169
|
+
if (this._mounted) return;
|
|
170
|
+
this._mounted = true;
|
|
171
|
+
const el = this.ref.current;
|
|
172
|
+
if (!el) return;
|
|
173
|
+
el.addEventListener("focus", this._onFocus);
|
|
174
|
+
el.addEventListener("blur", this._onBlur);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
onUnmount() {
|
|
178
|
+
this._mounted = false;
|
|
179
|
+
const el = this.ref.current;
|
|
180
|
+
if (!el) return;
|
|
181
|
+
el.removeEventListener("focus", this._onFocus);
|
|
182
|
+
el.removeEventListener("blur", this._onBlur);
|
|
155
183
|
}
|
|
156
184
|
}
|
package/src/index.ts
CHANGED
package/src/list.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Composable } from "@praxisjs/core";
|
|
2
|
+
import { computed, signal } from "@praxisjs/core/internal";
|
|
3
|
+
|
|
4
|
+
export interface VirtualItem<T> {
|
|
5
|
+
item: T;
|
|
6
|
+
index: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class VirtualList<T = unknown> extends Composable {
|
|
10
|
+
declare visibleItems: Array<VirtualItem<T>>;
|
|
11
|
+
declare totalHeight: number;
|
|
12
|
+
declare offsetTop: number;
|
|
13
|
+
declare offsetBottom: number;
|
|
14
|
+
|
|
15
|
+
private readonly _scrollTop = signal(0);
|
|
16
|
+
private readonly _viewHeight = signal(600);
|
|
17
|
+
private _scrollCleanup?: () => void;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly containerRef: { current: HTMLElement | null },
|
|
21
|
+
private readonly getItems: () => T[],
|
|
22
|
+
private readonly itemHeight: number,
|
|
23
|
+
private readonly buffer = 3,
|
|
24
|
+
) {
|
|
25
|
+
super();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setup() {
|
|
29
|
+
const { _scrollTop, _viewHeight, itemHeight, buffer, getItems } = this;
|
|
30
|
+
|
|
31
|
+
const items = computed(getItems);
|
|
32
|
+
const total = computed(() => items().length);
|
|
33
|
+
|
|
34
|
+
const startIdx = computed(() =>
|
|
35
|
+
Math.max(0, Math.floor(_scrollTop() / itemHeight) - buffer),
|
|
36
|
+
);
|
|
37
|
+
const endIdx = computed(() =>
|
|
38
|
+
Math.min(
|
|
39
|
+
total() - 1,
|
|
40
|
+
Math.ceil((_scrollTop() + _viewHeight()) / itemHeight) + buffer,
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const visibleItems = computed<Array<VirtualItem<T>>>(() => {
|
|
45
|
+
const arr = items();
|
|
46
|
+
const result: Array<VirtualItem<T>> = [];
|
|
47
|
+
for (let i = startIdx(); i <= endIdx(); i++) {
|
|
48
|
+
result.push({ item: arr[i], index: i });
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const totalHeight = computed(() => total() * itemHeight);
|
|
54
|
+
const offsetTop = computed(() => startIdx() * itemHeight);
|
|
55
|
+
const offsetBottom = computed(() =>
|
|
56
|
+
Math.max(0, total() - 1 - endIdx()) * itemHeight,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return { visibleItems, totalHeight, offsetTop, offsetBottom };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onMount() {
|
|
63
|
+
const el = this.containerRef.current;
|
|
64
|
+
if (!el) return;
|
|
65
|
+
|
|
66
|
+
const h = el.clientHeight;
|
|
67
|
+
if (h > 0) this._viewHeight.set(h);
|
|
68
|
+
|
|
69
|
+
const handler = () => { this._scrollTop.set(el.scrollTop); };
|
|
70
|
+
el.addEventListener("scroll", handler);
|
|
71
|
+
this._scrollCleanup = () => { el.removeEventListener("scroll", handler); };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onUnmount() {
|
|
75
|
+
this._scrollCleanup?.();
|
|
76
|
+
}
|
|
77
|
+
}
|