@stimulus-plumbers/controllers 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/stimulus-plumbers-controllers.es.js +246 -269
- package/dist/stimulus-plumbers-controllers.umd.js +1 -1
- package/package.json +1 -1
- package/src/controllers/calendar_month_controller.js +3 -1
- package/src/controllers/calendar_month_observer_controller.js +1 -1
- package/src/controllers/combobox_date_controller.js +1 -1
- package/src/controllers/combobox_dropdown_controller.js +24 -15
- package/src/controllers/input_combobox_controller.js +1 -1
- package/src/controllers/input_format_controller.js +1 -1
- package/src/controllers/modal_controller.js +2 -2
- package/src/index.js +8 -4
- package/src/plumbers/calendar.js +1 -1
- package/src/plumbers/content_loader.js +9 -63
- package/src/plumbers/dismisser.js +3 -51
- package/src/plumbers/flipper.js +12 -120
- package/src/plumbers/index.js +0 -1
- package/src/plumbers/plumber/config.js +6 -0
- package/src/plumbers/plumber/date.js +14 -0
- package/src/plumbers/plumber/geometry.js +36 -0
- package/src/plumbers/plumber/index.js +2 -1
- package/src/plumbers/plumber/window_observer.js +22 -0
- package/src/plumbers/shifter.js +8 -80
- package/src/plumbers/visibility.js +1 -1
- package/src/requestor.js +24 -0
- package/src/researcher.js +39 -0
- package/src/plumbers/combobox_dropdown.js +0 -60
- package/src/plumbers/plumber/support.js +0 -101
- /package/src/{aria.js → accessibility/aria.js} +0 -0
- /package/src/{focus.js → accessibility/focus.js} +0 -0
- /package/src/{keyboard.js → accessibility/keyboard.js} +0 -0
package/src/plumbers/flipper.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { defineRect, viewportRect, directionMap } from './plumber/
|
|
3
|
-
import { connectTriggerToTarget } from '../aria';
|
|
1
|
+
import WindowObserver from './plumber/window_observer';
|
|
2
|
+
import { defineRect, viewportRect, directionMap } from './plumber/geometry';
|
|
3
|
+
import { connectTriggerToTarget } from '../accessibility/aria';
|
|
4
4
|
|
|
5
5
|
const defaultOptions = {
|
|
6
6
|
anchor: null,
|
|
@@ -12,19 +12,7 @@ const defaultOptions = {
|
|
|
12
12
|
respectMotion: true,
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
export class Flipper extends
|
|
16
|
-
/**
|
|
17
|
-
* Creates a new Flipper plumber instance for smart positioning relative to an anchor.
|
|
18
|
-
* @param {Object} controller - Stimulus controller instance
|
|
19
|
-
* @param {Object} [options] - Configuration options
|
|
20
|
-
* @param {HTMLElement} [options.anchor] - Anchor element for positioning
|
|
21
|
-
* @param {string[]} [options.events=['click']] - Events triggering flip calculation
|
|
22
|
-
* @param {string} [options.placement='bottom'] - Initial placement direction ('top', 'bottom', 'left', 'right')
|
|
23
|
-
* @param {string} [options.alignment='start'] - Alignment ('start', 'center', 'end')
|
|
24
|
-
* @param {string} [options.onFlipped='flipped'] - Callback name when flipped
|
|
25
|
-
* @param {string} [options.ariaRole=null] - ARIA role to set on element
|
|
26
|
-
* @param {boolean} [options.respectMotion=true] - Respect prefers-reduced-motion preference
|
|
27
|
-
*/
|
|
15
|
+
export class Flipper extends WindowObserver {
|
|
28
16
|
constructor(controller, options = {}) {
|
|
29
17
|
super(controller, options);
|
|
30
18
|
|
|
@@ -43,22 +31,13 @@ export class Flipper extends Plumber {
|
|
|
43
31
|
this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
44
32
|
|
|
45
33
|
if (this.anchor && this.element) {
|
|
46
|
-
connectTriggerToTarget({
|
|
47
|
-
trigger: this.anchor,
|
|
48
|
-
target: this.element,
|
|
49
|
-
role: this.ariaRole,
|
|
50
|
-
});
|
|
34
|
+
connectTriggerToTarget({ trigger: this.anchor, target: this.element, role: this.ariaRole });
|
|
51
35
|
}
|
|
52
36
|
|
|
53
37
|
this.enhance();
|
|
54
|
-
this.observe();
|
|
38
|
+
this.observe(this.flip);
|
|
55
39
|
}
|
|
56
40
|
|
|
57
|
-
/**
|
|
58
|
-
* Attempts to place element in configured direction, flipping to opposite direction if needed.
|
|
59
|
-
* For example, if placement is 'bottom' but no space below anchor, flips to 'top'.
|
|
60
|
-
* @returns {Promise<void>}
|
|
61
|
-
*/
|
|
62
41
|
flip = async () => {
|
|
63
42
|
if (!this.visible) return;
|
|
64
43
|
|
|
@@ -68,7 +47,6 @@ export class Flipper extends Plumber {
|
|
|
68
47
|
|
|
69
48
|
const placement = this.flippedRect(this.anchor.getBoundingClientRect(), this.element.getBoundingClientRect());
|
|
70
49
|
|
|
71
|
-
// Disable transitions for users who prefer reduced motion
|
|
72
50
|
this.element.style.transition = this.respectMotion && this.prefersReducedMotion ? 'none' : '';
|
|
73
51
|
|
|
74
52
|
for (const [key, value] of Object.entries(placement)) {
|
|
@@ -79,12 +57,6 @@ export class Flipper extends Plumber {
|
|
|
79
57
|
this.dispatch('flipped', { detail: { placement } });
|
|
80
58
|
};
|
|
81
59
|
|
|
82
|
-
/**
|
|
83
|
-
* Determines the best position that fits within viewport boundaries.
|
|
84
|
-
* @param {DOMRect} anchorRect - Anchor element's bounding rect
|
|
85
|
-
* @param {DOMRect} referenceRect - Reference element's bounding rect
|
|
86
|
-
* @returns {Object} Position object with top and left styles
|
|
87
|
-
*/
|
|
88
60
|
flippedRect(anchorRect, referenceRect) {
|
|
89
61
|
const candidateRects = this.quadrumRect(anchorRect, viewportRect());
|
|
90
62
|
const candidates = [this.placement, directionMap[this.placement]];
|
|
@@ -92,7 +64,6 @@ export class Flipper extends Plumber {
|
|
|
92
64
|
while (!Object.keys(flipped).length && candidates.length > 0) {
|
|
93
65
|
const candidate = candidates.shift();
|
|
94
66
|
if (!this.biggerRectThan(candidateRects[candidate], referenceRect)) continue;
|
|
95
|
-
|
|
96
67
|
const placementRect = this.quadrumPlacement(anchorRect, candidate, referenceRect);
|
|
97
68
|
const alignmentRect = this.quadrumAlignment(anchorRect, candidate, placementRect);
|
|
98
69
|
flipped['top'] = `${alignmentRect['top'] + window.scrollY}px`;
|
|
@@ -105,32 +76,16 @@ export class Flipper extends Plumber {
|
|
|
105
76
|
return flipped;
|
|
106
77
|
}
|
|
107
78
|
|
|
108
|
-
/**
|
|
109
|
-
* Calculates available space in each direction around the inner rect within outer rect.
|
|
110
|
-
* @param {Object} inner - Inner rect object
|
|
111
|
-
* @param {Object} outer - Outer rect object
|
|
112
|
-
* @returns {Object} Rect objects for each direction (left, right, top, bottom)
|
|
113
|
-
*/
|
|
114
79
|
quadrumRect(inner, outer) {
|
|
115
80
|
return {
|
|
116
|
-
left: defineRect({
|
|
117
|
-
x: outer.x,
|
|
118
|
-
y: outer.y,
|
|
119
|
-
width: inner.x - outer.x,
|
|
120
|
-
height: outer.height,
|
|
121
|
-
}),
|
|
81
|
+
left: defineRect({ x: outer.x, y: outer.y, width: inner.x - outer.x, height: outer.height }),
|
|
122
82
|
right: defineRect({
|
|
123
83
|
x: inner.x + inner.width,
|
|
124
84
|
y: outer.y,
|
|
125
85
|
width: outer.width - (inner.x + inner.width),
|
|
126
86
|
height: outer.height,
|
|
127
87
|
}),
|
|
128
|
-
top: defineRect({
|
|
129
|
-
x: outer.x,
|
|
130
|
-
y: outer.y,
|
|
131
|
-
width: outer.width,
|
|
132
|
-
height: inner.y - outer.y,
|
|
133
|
-
}),
|
|
88
|
+
top: defineRect({ x: outer.x, y: outer.y, width: outer.width, height: inner.y - outer.y }),
|
|
134
89
|
bottom: defineRect({
|
|
135
90
|
x: outer.x,
|
|
136
91
|
y: inner.y + inner.height,
|
|
@@ -140,14 +95,6 @@ export class Flipper extends Plumber {
|
|
|
140
95
|
};
|
|
141
96
|
}
|
|
142
97
|
|
|
143
|
-
/**
|
|
144
|
-
* Calculates placement rect for reference element in given direction from anchor.
|
|
145
|
-
* @param {Object} anchor - Anchor rect object
|
|
146
|
-
* @param {string} direction - Direction ('top', 'bottom', 'left', 'right')
|
|
147
|
-
* @param {Object} reference - Reference rect object
|
|
148
|
-
* @returns {Object} Placed rect object
|
|
149
|
-
* @throws {string} If direction is invalid
|
|
150
|
-
*/
|
|
151
98
|
quadrumPlacement(anchor, direction, reference) {
|
|
152
99
|
switch (direction) {
|
|
153
100
|
case 'top':
|
|
@@ -183,14 +130,6 @@ export class Flipper extends Plumber {
|
|
|
183
130
|
}
|
|
184
131
|
}
|
|
185
132
|
|
|
186
|
-
/**
|
|
187
|
-
* Applies alignment adjustment to placed rect based on configuration.
|
|
188
|
-
* @param {Object} anchor - Anchor rect object
|
|
189
|
-
* @param {string} direction - Direction ('top', 'bottom', 'left', 'right')
|
|
190
|
-
* @param {Object} reference - Reference rect object
|
|
191
|
-
* @returns {Object} Aligned rect object
|
|
192
|
-
* @throws {string} If direction is invalid
|
|
193
|
-
*/
|
|
194
133
|
quadrumAlignment(anchor, direction, reference) {
|
|
195
134
|
switch (direction) {
|
|
196
135
|
case 'top':
|
|
@@ -198,75 +137,28 @@ export class Flipper extends Plumber {
|
|
|
198
137
|
let alignment = anchor.x;
|
|
199
138
|
if (this.alignment === 'center') alignment = anchor.x + anchor.width / 2 - reference.width / 2;
|
|
200
139
|
else if (this.alignment === 'end') alignment = anchor.x + anchor.width - reference.width;
|
|
201
|
-
return defineRect({
|
|
202
|
-
x: alignment,
|
|
203
|
-
y: reference.y,
|
|
204
|
-
width: reference.width,
|
|
205
|
-
height: reference.height,
|
|
206
|
-
});
|
|
140
|
+
return defineRect({ x: alignment, y: reference.y, width: reference.width, height: reference.height });
|
|
207
141
|
}
|
|
208
142
|
case 'left':
|
|
209
143
|
case 'right': {
|
|
210
144
|
let alignment = anchor.y;
|
|
211
145
|
if (this.alignment === 'center') alignment = anchor.y + anchor.height / 2 - reference.height / 2;
|
|
212
146
|
else if (this.alignment === 'end') alignment = anchor.y + anchor.height - reference.height;
|
|
213
|
-
return defineRect({
|
|
214
|
-
x: reference.x,
|
|
215
|
-
y: alignment,
|
|
216
|
-
width: reference.width,
|
|
217
|
-
height: reference.height,
|
|
218
|
-
});
|
|
147
|
+
return defineRect({ x: reference.x, y: alignment, width: reference.width, height: reference.height });
|
|
219
148
|
}
|
|
220
149
|
default:
|
|
221
150
|
throw `Unable align at the quadrum, ${direction}`;
|
|
222
151
|
}
|
|
223
152
|
}
|
|
224
153
|
|
|
225
|
-
/**
|
|
226
|
-
* Checks if the big rect can contain the small rect dimensions.
|
|
227
|
-
* @param {Object} big - Larger rect object
|
|
228
|
-
* @param {Object} small - Smaller rect object
|
|
229
|
-
* @returns {boolean} True if big rect can contain small rect
|
|
230
|
-
*/
|
|
231
154
|
biggerRectThan(big, small) {
|
|
232
155
|
return big.height >= small.height && big.width >= small.width;
|
|
233
156
|
}
|
|
234
157
|
|
|
235
|
-
/**
|
|
236
|
-
* Starts observing configured events for flipping.
|
|
237
|
-
*/
|
|
238
|
-
observe() {
|
|
239
|
-
this.events.forEach((event) => {
|
|
240
|
-
window.addEventListener(event, this.flip, true);
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Stops observing events for flipping.
|
|
246
|
-
*/
|
|
247
|
-
unobserve() {
|
|
248
|
-
this.events.forEach((event) => {
|
|
249
|
-
window.removeEventListener(event, this.flip, true);
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
158
|
enhance() {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
Object.assign(this.controller, {
|
|
257
|
-
disconnect: () => {
|
|
258
|
-
context.unobserve();
|
|
259
|
-
superDisconnect();
|
|
260
|
-
},
|
|
261
|
-
flip: context.flip.bind(context),
|
|
262
|
-
});
|
|
159
|
+
super.enhance();
|
|
160
|
+
this.controller.flip = this.flip;
|
|
263
161
|
}
|
|
264
162
|
}
|
|
265
163
|
|
|
266
|
-
/**
|
|
267
|
-
* Factory function to create and attach a Flipper plumber to a controller.
|
|
268
|
-
* @param {Object} controller - Stimulus controller instance
|
|
269
|
-
* @param {Object} [options] - Configuration options
|
|
270
|
-
* @returns {Flipper} Flipper plumber instance
|
|
271
|
-
*/
|
|
272
164
|
export const attachFlipper = (controller, options) => new Flipper(controller, options);
|
package/src/plumbers/index.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function isValidDate(value) {
|
|
2
|
+
return value instanceof Date && !isNaN(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function tryParseDate(...values) {
|
|
6
|
+
if (values.length === 0) throw 'Missing values to parse as date';
|
|
7
|
+
if (values.length === 1) {
|
|
8
|
+
const parsed = new Date(values[0]);
|
|
9
|
+
if (values[0] && isValidDate(parsed)) return parsed;
|
|
10
|
+
} else {
|
|
11
|
+
const parsed = new Date(...values);
|
|
12
|
+
if (isValidDate(parsed)) return parsed;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const directionMap = {
|
|
2
|
+
get top() {
|
|
3
|
+
return 'bottom';
|
|
4
|
+
},
|
|
5
|
+
get bottom() {
|
|
6
|
+
return 'top';
|
|
7
|
+
},
|
|
8
|
+
get left() {
|
|
9
|
+
return 'right';
|
|
10
|
+
},
|
|
11
|
+
get right() {
|
|
12
|
+
return 'left';
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function defineRect({ x, y, width, height }) {
|
|
17
|
+
return { x, y, width, height, left: x, right: x + width, top: y, bottom: y + height };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function viewportRect() {
|
|
21
|
+
return defineRect({
|
|
22
|
+
x: 0,
|
|
23
|
+
y: 0,
|
|
24
|
+
width: window.innerWidth || document.documentElement.clientWidth,
|
|
25
|
+
height: window.innerHeight || document.documentElement.clientHeight,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isWithinViewport(target) {
|
|
30
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
31
|
+
const outer = viewportRect();
|
|
32
|
+
const inner = target.getBoundingClientRect();
|
|
33
|
+
const vertical = inner.top <= outer.height && inner.top + inner.height > 0;
|
|
34
|
+
const horizontal = inner.left <= outer.width && inner.left + inner.width > 0;
|
|
35
|
+
return vertical && horizontal;
|
|
36
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Plumber from './index';
|
|
2
|
+
|
|
3
|
+
export default class WindowObserver extends Plumber {
|
|
4
|
+
observe(handler) {
|
|
5
|
+
this._handler = handler;
|
|
6
|
+
this.events.forEach((e) => window.addEventListener(e, handler, true));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
unobserve() {
|
|
10
|
+
if (!this._handler) return;
|
|
11
|
+
this.events.forEach((e) => window.removeEventListener(e, this._handler, true));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
enhance() {
|
|
15
|
+
const context = this;
|
|
16
|
+
const superDisconnect = this.controller.disconnect?.bind(this.controller) || (() => {});
|
|
17
|
+
this.controller.disconnect = () => {
|
|
18
|
+
context.unobserve();
|
|
19
|
+
superDisconnect();
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/plumbers/shifter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { viewportRect, directionMap, defineRect } from './plumber/
|
|
1
|
+
import WindowObserver from './plumber/window_observer';
|
|
2
|
+
import { viewportRect, directionMap, defineRect } from './plumber/geometry';
|
|
3
3
|
|
|
4
4
|
const defaultOptions = {
|
|
5
5
|
events: ['resize'],
|
|
@@ -8,16 +8,7 @@ const defaultOptions = {
|
|
|
8
8
|
respectMotion: true,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
export class Shifter extends
|
|
12
|
-
/**
|
|
13
|
-
* Creates a new Shifter plumber instance for viewport boundary shifting.
|
|
14
|
-
* @param {Object} controller - Stimulus controller instance
|
|
15
|
-
* @param {Object} [options] - Configuration options
|
|
16
|
-
* @param {string[]} [options.events=['resize']] - Events triggering shift calculation
|
|
17
|
-
* @param {string[]} [options.boundaries=['top','left','right']] - Boundaries to check (valid values: 'top', 'bottom', 'left', 'right')
|
|
18
|
-
* @param {string} [options.onShifted='shifted'] - Callback name when shifted
|
|
19
|
-
* @param {boolean} [options.respectMotion=true] - Respect prefers-reduced-motion preference
|
|
20
|
-
*/
|
|
11
|
+
export class Shifter extends WindowObserver {
|
|
21
12
|
constructor(controller, options = {}) {
|
|
22
13
|
super(controller, options);
|
|
23
14
|
|
|
@@ -29,13 +20,9 @@ export class Shifter extends Plumber {
|
|
|
29
20
|
this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
30
21
|
|
|
31
22
|
this.enhance();
|
|
32
|
-
this.observe();
|
|
23
|
+
this.observe(this.shift);
|
|
33
24
|
}
|
|
34
25
|
|
|
35
|
-
/**
|
|
36
|
-
* Calculates and applies transform to shift element within viewport boundaries.
|
|
37
|
-
* @returns {Promise<void>}
|
|
38
|
-
*/
|
|
39
26
|
shift = async () => {
|
|
40
27
|
if (!this.visible) return;
|
|
41
28
|
|
|
@@ -44,21 +31,13 @@ export class Shifter extends Plumber {
|
|
|
44
31
|
const translateX = overflow['left'] || overflow['right'] || 0;
|
|
45
32
|
const translateY = overflow['top'] || overflow['bottom'] || 0;
|
|
46
33
|
|
|
47
|
-
// Disable transitions for users who prefer reduced motion
|
|
48
34
|
this.element.style.transition = this.respectMotion && this.prefersReducedMotion ? 'none' : '';
|
|
49
|
-
|
|
50
35
|
this.element.style.transform = `translate(${translateX}px, ${translateY}px)`;
|
|
51
36
|
|
|
52
37
|
await this.awaitCallback(this.onShifted, overflow);
|
|
53
38
|
this.dispatch('shifted', { detail: overflow });
|
|
54
39
|
};
|
|
55
40
|
|
|
56
|
-
/**
|
|
57
|
-
* Calculates overflow distances for each boundary direction.
|
|
58
|
-
* @param {DOMRect} targetRect - Target element's bounding rect
|
|
59
|
-
* @param {Object} translations - Current transform translations
|
|
60
|
-
* @returns {Object} Overflow distances by direction
|
|
61
|
-
*/
|
|
62
41
|
overflowRect(targetRect, translations) {
|
|
63
42
|
const overflow = {};
|
|
64
43
|
const viewport = viewportRect();
|
|
@@ -81,14 +60,6 @@ export class Shifter extends Plumber {
|
|
|
81
60
|
return overflow;
|
|
82
61
|
}
|
|
83
62
|
|
|
84
|
-
/**
|
|
85
|
-
* Calculates distance from inner rect to outer boundary in given direction.
|
|
86
|
-
* @param {Object} inner - Inner rect object
|
|
87
|
-
* @param {string} direction - Direction ('top', 'bottom', 'left', 'right')
|
|
88
|
-
* @param {Object} outer - Outer rect object
|
|
89
|
-
* @returns {number} Distance to boundary (negative if overflowing)
|
|
90
|
-
* @throws {string} If direction is invalid
|
|
91
|
-
*/
|
|
92
63
|
directionDistance(inner, direction, outer) {
|
|
93
64
|
switch (direction) {
|
|
94
65
|
case 'top':
|
|
@@ -102,63 +73,20 @@ export class Shifter extends Plumber {
|
|
|
102
73
|
}
|
|
103
74
|
}
|
|
104
75
|
|
|
105
|
-
/**
|
|
106
|
-
* Extracts current translate values from element's transform style.
|
|
107
|
-
* @param {HTMLElement} target - Target element
|
|
108
|
-
* @returns {Object} Translation object with x and y values
|
|
109
|
-
*/
|
|
110
76
|
elementTranslations(target) {
|
|
111
77
|
const style = window.getComputedStyle(target);
|
|
112
78
|
const matrix = style['transform'] || style['webkitTransform'] || style['mozTransform'];
|
|
113
|
-
|
|
114
|
-
if (matrix === 'none' || typeof matrix === 'undefined') {
|
|
115
|
-
return { x: 0, y: 0 };
|
|
116
|
-
}
|
|
117
|
-
|
|
79
|
+
if (matrix === 'none' || typeof matrix === 'undefined') return { x: 0, y: 0 };
|
|
118
80
|
const matrixType = matrix.includes('3d') ? '3d' : '2d';
|
|
119
81
|
const matrixValues = matrix.match(/matrix.*\((.+)\)/)[1].split(', ');
|
|
120
|
-
|
|
121
|
-
if (matrixType === '2d') {
|
|
122
|
-
return { x: Number(matrixValues[4]), y: Number(matrixValues[5]) };
|
|
123
|
-
}
|
|
82
|
+
if (matrixType === '2d') return { x: Number(matrixValues[4]), y: Number(matrixValues[5]) };
|
|
124
83
|
return { x: 0, y: 0 };
|
|
125
84
|
}
|
|
126
85
|
|
|
127
|
-
/**
|
|
128
|
-
* Starts observing configured events for shifting.
|
|
129
|
-
*/
|
|
130
|
-
observe() {
|
|
131
|
-
this.events.forEach((event) => {
|
|
132
|
-
window.addEventListener(event, this.shift, true);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Stops observing events for shifting.
|
|
138
|
-
*/
|
|
139
|
-
unobserve() {
|
|
140
|
-
this.events.forEach((event) => {
|
|
141
|
-
window.removeEventListener(event, this.shift, true);
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
86
|
enhance() {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Object.assign(this.controller, {
|
|
149
|
-
disconnect: () => {
|
|
150
|
-
context.unobserve();
|
|
151
|
-
superDisconnect();
|
|
152
|
-
},
|
|
153
|
-
shift: context.shift.bind(context),
|
|
154
|
-
});
|
|
87
|
+
super.enhance();
|
|
88
|
+
this.controller.shift = this.shift;
|
|
155
89
|
}
|
|
156
90
|
}
|
|
157
91
|
|
|
158
|
-
/**
|
|
159
|
-
* Factory function to create and attach a Shifter plumber to a controller.
|
|
160
|
-
* @param {Object} controller - Stimulus controller instance
|
|
161
|
-
* @param {Object} [options] - Configuration options
|
|
162
|
-
* @returns {Shifter} Shifter plumber instance
|
|
163
|
-
*/
|
|
164
92
|
export const attachShifter = (controller, options) => new Shifter(controller, options);
|
package/src/requestor.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class Requestor {
|
|
2
|
+
constructor() {
|
|
3
|
+
this._abortController = null;
|
|
4
|
+
this._timer = null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
schedule(fn, delay) {
|
|
8
|
+
clearTimeout(this._timer);
|
|
9
|
+
this._timer = setTimeout(fn, delay);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async request(url, options = {}) {
|
|
13
|
+
this._abortController?.abort();
|
|
14
|
+
this._abortController = new AbortController();
|
|
15
|
+
const res = await fetch(url, { ...options, signal: this._abortController.signal });
|
|
16
|
+
if (!res.ok) throw new Error(`${res.status}`);
|
|
17
|
+
return res;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
cancel() {
|
|
21
|
+
clearTimeout(this._timer);
|
|
22
|
+
this._abortController?.abort();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function fuzzyMatcher(needle, haystack) {
|
|
2
|
+
let ni = 0;
|
|
3
|
+
for (let i = 0; i < haystack.length && ni < needle.length; i++) {
|
|
4
|
+
if (haystack[i] === needle[ni]) ni++;
|
|
5
|
+
}
|
|
6
|
+
return ni === needle.length;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function matchContains(needle, haystack) {
|
|
10
|
+
return haystack.includes(needle);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function matchPrefix(needle, haystack) {
|
|
14
|
+
return haystack.startsWith(needle);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getStrategy(strategy) {
|
|
18
|
+
if (strategy === 'contains') return matchContains;
|
|
19
|
+
if (strategy === 'prefix') return matchPrefix;
|
|
20
|
+
return fuzzyMatcher;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractDOMValue(el, field) {
|
|
24
|
+
if (field === 'textContent') return el.textContent?.trim().toLowerCase() ?? '';
|
|
25
|
+
return (el.getAttribute(field) ?? '').toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function filterOptions(listbox, query, options = {}) {
|
|
29
|
+
const { strategy = 'fuzzy', matcher, fields = ['textContent'] } = options;
|
|
30
|
+
const matchFn = typeof matcher === 'function' ? matcher : getStrategy(strategy);
|
|
31
|
+
const needle = query.toLowerCase();
|
|
32
|
+
let visible = 0;
|
|
33
|
+
listbox.querySelectorAll('[role="option"]').forEach((opt) => {
|
|
34
|
+
const match = fields.some((field) => matchFn(needle, extractDOMValue(opt, field)));
|
|
35
|
+
opt.hidden = !match;
|
|
36
|
+
if (match) visible++;
|
|
37
|
+
});
|
|
38
|
+
return visible;
|
|
39
|
+
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import Plumber from './plumber';
|
|
2
|
-
|
|
3
|
-
export class ComboboxDropdown extends Plumber {
|
|
4
|
-
constructor(controller, options = {}) {
|
|
5
|
-
super(controller, options);
|
|
6
|
-
this.debounceTimer = null;
|
|
7
|
-
this.abortController = null;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
fuzzyFilter(listbox, query) {
|
|
11
|
-
const needle = query.toLowerCase();
|
|
12
|
-
let visible = 0;
|
|
13
|
-
listbox.querySelectorAll('[role="option"]').forEach((opt) => {
|
|
14
|
-
const match = this.fuzzyMatch(needle, opt.textContent.trim().toLowerCase());
|
|
15
|
-
opt.hidden = !match;
|
|
16
|
-
if (match) visible++;
|
|
17
|
-
});
|
|
18
|
-
return visible;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
fuzzyMatch(needle, haystack) {
|
|
22
|
-
let ni = 0;
|
|
23
|
-
for (let i = 0; i < haystack.length && ni < needle.length; i++) {
|
|
24
|
-
if (haystack[i] === needle[ni]) ni++;
|
|
25
|
-
}
|
|
26
|
-
return ni === needle.length;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
scheduleFetch(query, delay, callback) {
|
|
30
|
-
clearTimeout(this.debounceTimer);
|
|
31
|
-
this.debounceTimer = setTimeout(() => this.fetch(query, callback), delay);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async fetch(query, { url, field, onLoading, onLoaded, onError }) {
|
|
35
|
-
this.abortController?.abort();
|
|
36
|
-
this.abortController = new AbortController();
|
|
37
|
-
onLoading?.(true);
|
|
38
|
-
const fetchUrl = new URL(url, window.location.href);
|
|
39
|
-
fetchUrl.searchParams.set(field, query);
|
|
40
|
-
try {
|
|
41
|
-
const res = await fetch(fetchUrl, {
|
|
42
|
-
signal: this.abortController.signal,
|
|
43
|
-
headers: { Accept: 'text/html', 'X-Requested-With': 'XMLHttpRequest' },
|
|
44
|
-
});
|
|
45
|
-
if (!res.ok) throw new Error(`${res.status}`);
|
|
46
|
-
onLoaded?.(await res.text());
|
|
47
|
-
} catch (err) {
|
|
48
|
-
if (err.name !== 'AbortError') onError?.(err);
|
|
49
|
-
} finally {
|
|
50
|
-
onLoading?.(false);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
cancel() {
|
|
55
|
-
clearTimeout(this.debounceTimer);
|
|
56
|
-
this.abortController?.abort();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export const initComboboxDropdown = (controller, options) => new ComboboxDropdown(controller, options);
|