@stackoverflow/stacks 1.6.2 → 1.6.4
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 +22 -0
- package/dist/controllers/index.d.ts +7 -7
- package/dist/controllers/s-expandable-control.d.ts +1 -1
- package/dist/css/stacks.css +17 -28
- package/dist/css/stacks.min.css +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/js/stacks.js +174 -112
- package/dist/js/stacks.min.js +1 -1
- package/dist/stacks.d.ts +1 -1
- package/lib/css/atomic/gap.less +1 -1
- package/lib/css/components/buttons.less +3 -1
- package/lib/css/components/cards.less +1 -1
- package/lib/css/components/expandable.less +1 -1
- package/lib/css/components/inputs.less +4 -18
- package/lib/css/components/labels.less +6 -1
- package/lib/css/components/pagination.less +1 -1
- package/lib/css/components/popovers.less +6 -7
- package/lib/ts/controllers/index.ts +14 -7
- package/lib/ts/controllers/s-expandable-control.ts +79 -34
- package/lib/ts/controllers/s-modal.ts +116 -58
- package/lib/ts/controllers/s-navigation-tablist.ts +30 -20
- package/lib/ts/controllers/s-popover.ts +149 -73
- package/lib/ts/controllers/s-table.ts +69 -28
- package/lib/ts/controllers/s-tooltip.ts +87 -29
- package/lib/ts/controllers/s-uploader.ts +58 -39
- package/lib/ts/index.ts +11 -3
- package/lib/ts/stacks.ts +40 -19
- package/lib/tsconfig.json +1 -1
- package/package.json +8 -6
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { createPopper, Placement } from
|
|
2
|
-
import type * as Popper from
|
|
1
|
+
import { createPopper, Placement } from "@popperjs/core";
|
|
2
|
+
import type * as Popper from "@popperjs/core";
|
|
3
3
|
import * as Stacks from "../stacks";
|
|
4
4
|
|
|
5
|
-
type OutsideClickBehavior =
|
|
5
|
+
type OutsideClickBehavior =
|
|
6
|
+
| "always"
|
|
7
|
+
| "never"
|
|
8
|
+
| "if-in-viewport"
|
|
9
|
+
| "after-dismissal";
|
|
6
10
|
|
|
7
11
|
export abstract class BasePopoverController extends Stacks.StacksController {
|
|
8
12
|
private popper!: Popper.Instance;
|
|
@@ -31,7 +35,9 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
31
35
|
*/
|
|
32
36
|
get isVisible() {
|
|
33
37
|
const popoverElement = this.popoverElement;
|
|
34
|
-
return popoverElement
|
|
38
|
+
return popoverElement
|
|
39
|
+
? popoverElement.classList.contains("is-visible")
|
|
40
|
+
: false;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
@@ -39,28 +45,43 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
39
45
|
*/
|
|
40
46
|
get isInViewport() {
|
|
41
47
|
const element = this.popoverElement;
|
|
42
|
-
if (!this.isVisible || !element) {
|
|
48
|
+
if (!this.isVisible || !element) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
43
51
|
|
|
44
52
|
// From https://stackoverflow.com/a/5354536. Theoretically, this could be calculated using Popper's detectOverflow function,
|
|
45
53
|
// but it's unclear how to access that with our current configuration.
|
|
46
54
|
|
|
47
55
|
const rect = element.getBoundingClientRect();
|
|
48
|
-
const viewHeight = Math.max(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
const viewHeight = Math.max(
|
|
57
|
+
document.documentElement.clientHeight,
|
|
58
|
+
window.innerHeight
|
|
59
|
+
);
|
|
60
|
+
const viewWidth = Math.max(
|
|
61
|
+
document.documentElement.clientWidth,
|
|
62
|
+
window.innerWidth
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
rect.bottom > 0 &&
|
|
67
|
+
rect.top < viewHeight &&
|
|
68
|
+
rect.right > 0 &&
|
|
69
|
+
rect.left < viewWidth
|
|
70
|
+
);
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
protected get shouldHideOnOutsideClick() {
|
|
55
|
-
const hideBehavior = <OutsideClickBehavior>
|
|
74
|
+
const hideBehavior = <OutsideClickBehavior>(
|
|
75
|
+
this.data.get("hide-on-outside-click")
|
|
76
|
+
);
|
|
56
77
|
switch (hideBehavior) {
|
|
57
78
|
case "after-dismissal":
|
|
58
79
|
case "never":
|
|
59
80
|
return false;
|
|
60
81
|
case "if-in-viewport":
|
|
61
|
-
|
|
82
|
+
return this.isInViewport;
|
|
62
83
|
default:
|
|
63
|
-
|
|
84
|
+
return true;
|
|
64
85
|
}
|
|
65
86
|
}
|
|
66
87
|
|
|
@@ -97,21 +118,27 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
97
118
|
/**
|
|
98
119
|
* Toggles the visibility of the popover
|
|
99
120
|
*/
|
|
100
|
-
toggle(dispatcher: Event|Element|null = null) {
|
|
121
|
+
toggle(dispatcher: Event | Element | null = null) {
|
|
101
122
|
this.isVisible ? this.hide(dispatcher) : this.show(dispatcher);
|
|
102
123
|
}
|
|
103
124
|
|
|
104
125
|
/**
|
|
105
126
|
* Shows the popover if not already visible
|
|
106
127
|
*/
|
|
107
|
-
show(dispatcher: Event|Element|null = null) {
|
|
108
|
-
if (this.isVisible) {
|
|
128
|
+
show(dispatcher: Event | Element | null = null) {
|
|
129
|
+
if (this.isVisible) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
109
132
|
|
|
110
133
|
const dispatcherElement = this.getDispatcher(dispatcher);
|
|
111
134
|
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
if (
|
|
136
|
+
this.triggerEvent("show", {
|
|
137
|
+
dispatcher: dispatcherElement,
|
|
138
|
+
}).defaultPrevented
|
|
139
|
+
) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
115
142
|
|
|
116
143
|
if (!this.popper) {
|
|
117
144
|
this.initializePopper();
|
|
@@ -128,14 +155,20 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
128
155
|
/**
|
|
129
156
|
* Hides the popover if not already hidden
|
|
130
157
|
*/
|
|
131
|
-
hide(dispatcher: Event|Element|null = null) {
|
|
132
|
-
if (!this.isVisible) {
|
|
158
|
+
hide(dispatcher: Event | Element | null = null) {
|
|
159
|
+
if (!this.isVisible) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
133
162
|
|
|
134
163
|
const dispatcherElement = this.getDispatcher(dispatcher);
|
|
135
164
|
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
165
|
+
if (
|
|
166
|
+
this.triggerEvent("hide", {
|
|
167
|
+
dispatcher: dispatcherElement,
|
|
168
|
+
}).defaultPrevented
|
|
169
|
+
) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
139
172
|
|
|
140
173
|
this.popoverElement.classList.remove("is-visible");
|
|
141
174
|
|
|
@@ -148,7 +181,10 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
148
181
|
}
|
|
149
182
|
|
|
150
183
|
// on first interaction, hide-on-outside-click with value "after-dismissal" reverts to the default behavior
|
|
151
|
-
if (
|
|
184
|
+
if (
|
|
185
|
+
<OutsideClickBehavior>this.data.get("hide-on-outside-click") ===
|
|
186
|
+
"after-dismissal"
|
|
187
|
+
) {
|
|
152
188
|
this.data.delete("hide-on-outside-click");
|
|
153
189
|
}
|
|
154
190
|
|
|
@@ -158,20 +194,20 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
158
194
|
/**
|
|
159
195
|
* Binds document events for this popover and fires the shown event
|
|
160
196
|
*/
|
|
161
|
-
protected shown(dispatcher: Element|null = null) {
|
|
197
|
+
protected shown(dispatcher: Element | null = null) {
|
|
162
198
|
this.bindDocumentEvents();
|
|
163
199
|
this.triggerEvent("shown", {
|
|
164
|
-
dispatcher: dispatcher
|
|
200
|
+
dispatcher: dispatcher,
|
|
165
201
|
});
|
|
166
202
|
}
|
|
167
203
|
|
|
168
204
|
/**
|
|
169
205
|
* Unbinds document events for this popover and fires the hidden event
|
|
170
206
|
*/
|
|
171
|
-
protected hidden(dispatcher: Element|null = null) {
|
|
207
|
+
protected hidden(dispatcher: Element | null = null) {
|
|
172
208
|
this.unbindDocumentEvents();
|
|
173
209
|
this.triggerEvent("hidden", {
|
|
174
|
-
dispatcher: dispatcher
|
|
210
|
+
dispatcher: dispatcher,
|
|
175
211
|
});
|
|
176
212
|
}
|
|
177
213
|
|
|
@@ -187,21 +223,21 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
187
223
|
*/
|
|
188
224
|
private initializePopper() {
|
|
189
225
|
this.popper = createPopper(this.referenceElement, this.popoverElement, {
|
|
190
|
-
placement: this.data.get("placement") as Placement || "bottom",
|
|
226
|
+
placement: (this.data.get("placement") as Placement) || "bottom",
|
|
191
227
|
modifiers: [
|
|
192
228
|
{
|
|
193
229
|
name: "offset",
|
|
194
230
|
options: {
|
|
195
231
|
offset: [0, 10], // The entire popover should be 10px away from the element
|
|
196
|
-
}
|
|
232
|
+
},
|
|
197
233
|
},
|
|
198
234
|
{
|
|
199
235
|
name: "arrow",
|
|
200
236
|
options: {
|
|
201
|
-
element: ".s-popover--arrow"
|
|
237
|
+
element: ".s-popover--arrow",
|
|
202
238
|
},
|
|
203
239
|
},
|
|
204
|
-
]
|
|
240
|
+
],
|
|
205
241
|
});
|
|
206
242
|
}
|
|
207
243
|
|
|
@@ -215,14 +251,21 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
215
251
|
|
|
216
252
|
// if there is an alternative reference selector and that element exists, use it (and throw if it isn't found)
|
|
217
253
|
if (referenceSelector) {
|
|
218
|
-
this.referenceElement = <HTMLElement>
|
|
254
|
+
this.referenceElement = <HTMLElement>(
|
|
255
|
+
this.element.querySelector(referenceSelector)
|
|
256
|
+
);
|
|
219
257
|
|
|
220
258
|
if (!this.referenceElement) {
|
|
221
|
-
throw
|
|
259
|
+
throw (
|
|
260
|
+
"Unable to find element by reference selector: " +
|
|
261
|
+
referenceSelector
|
|
262
|
+
);
|
|
222
263
|
}
|
|
223
264
|
}
|
|
224
265
|
|
|
225
|
-
const popoverId = this.referenceElement.getAttribute(
|
|
266
|
+
const popoverId = this.referenceElement.getAttribute(
|
|
267
|
+
this.popoverSelectorAttribute
|
|
268
|
+
);
|
|
226
269
|
|
|
227
270
|
let popoverElement: HTMLElement | null = null;
|
|
228
271
|
|
|
@@ -230,7 +273,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
230
273
|
if (popoverId) {
|
|
231
274
|
popoverElement = document.getElementById(popoverId);
|
|
232
275
|
|
|
233
|
-
if (!popoverElement){
|
|
276
|
+
if (!popoverElement) {
|
|
234
277
|
throw `[${this.popoverSelectorAttribute}="{POPOVER_ID}"] required`;
|
|
235
278
|
}
|
|
236
279
|
}
|
|
@@ -250,14 +293,14 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
250
293
|
* Determines the correct dispatching element from a potential input
|
|
251
294
|
* @param dispatcher The event or element to get the dispatcher from
|
|
252
295
|
*/
|
|
253
|
-
protected getDispatcher(
|
|
296
|
+
protected getDispatcher(
|
|
297
|
+
dispatcher: Event | Element | null = null
|
|
298
|
+
): Element {
|
|
254
299
|
if (dispatcher instanceof Event) {
|
|
255
300
|
return <Element>dispatcher.target;
|
|
256
|
-
}
|
|
257
|
-
else if (dispatcher instanceof Element) {
|
|
301
|
+
} else if (dispatcher instanceof Element) {
|
|
258
302
|
return dispatcher;
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
303
|
+
} else {
|
|
261
304
|
return this.element;
|
|
262
305
|
}
|
|
263
306
|
}
|
|
@@ -278,12 +321,12 @@ export class PopoverController extends BasePopoverController {
|
|
|
278
321
|
protected popoverSelectorAttribute = "aria-controls";
|
|
279
322
|
|
|
280
323
|
private boundHideOnOutsideClick!: (event: MouseEvent) => void;
|
|
281
|
-
private boundHideOnEscapePress!:
|
|
324
|
+
private boundHideOnEscapePress!: (event: KeyboardEvent) => void;
|
|
282
325
|
|
|
283
326
|
/**
|
|
284
327
|
* Toggles optional classes and accessibility attributes in addition to BasePopoverController.shown
|
|
285
328
|
*/
|
|
286
|
-
protected override shown(dispatcher: Element|null = null) {
|
|
329
|
+
protected override shown(dispatcher: Element | null = null) {
|
|
287
330
|
this.toggleOptionalClasses(true);
|
|
288
331
|
this.toggleAccessibilityAttributes(true);
|
|
289
332
|
super.shown(dispatcher);
|
|
@@ -292,13 +335,12 @@ export class PopoverController extends BasePopoverController {
|
|
|
292
335
|
/**
|
|
293
336
|
* Toggles optional classes and accessibility attributes in addition to BasePopoverController.hidden
|
|
294
337
|
*/
|
|
295
|
-
protected override hidden(dispatcher: Element|null = null) {
|
|
338
|
+
protected override hidden(dispatcher: Element | null = null) {
|
|
296
339
|
this.toggleOptionalClasses(false);
|
|
297
340
|
this.toggleAccessibilityAttributes(false);
|
|
298
341
|
super.hidden(dispatcher);
|
|
299
342
|
}
|
|
300
343
|
|
|
301
|
-
|
|
302
344
|
/**
|
|
303
345
|
* Initializes accessibility attributes in addition to BasePopoverController.connect
|
|
304
346
|
*/
|
|
@@ -312,8 +354,10 @@ export class PopoverController extends BasePopoverController {
|
|
|
312
354
|
* Binds global events to the document for hiding popovers on user interaction
|
|
313
355
|
*/
|
|
314
356
|
protected bindDocumentEvents() {
|
|
315
|
-
this.boundHideOnOutsideClick =
|
|
316
|
-
|
|
357
|
+
this.boundHideOnOutsideClick =
|
|
358
|
+
this.boundHideOnOutsideClick || this.hideOnOutsideClick.bind(this);
|
|
359
|
+
this.boundHideOnEscapePress =
|
|
360
|
+
this.boundHideOnEscapePress || this.hideOnEscapePress.bind(this);
|
|
317
361
|
|
|
318
362
|
document.addEventListener("mousedown", this.boundHideOnOutsideClick);
|
|
319
363
|
document.addEventListener("keyup", this.boundHideOnEscapePress);
|
|
@@ -335,10 +379,15 @@ export class PopoverController extends BasePopoverController {
|
|
|
335
379
|
const target = <Node>e.target;
|
|
336
380
|
// check if the document was clicked inside either the reference element or the popover itself
|
|
337
381
|
// note: .contains also returns true if the node itself matches the target element
|
|
338
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
this.shouldHideOnOutsideClick &&
|
|
384
|
+
!this.referenceElement.contains(target) &&
|
|
385
|
+
!this.popoverElement.contains(target) &&
|
|
386
|
+
document.body.contains(target)
|
|
387
|
+
) {
|
|
339
388
|
this.hide(e);
|
|
340
389
|
}
|
|
341
|
-
}
|
|
390
|
+
}
|
|
342
391
|
|
|
343
392
|
/**
|
|
344
393
|
* Forces the popover to hide if the user presses escape while it, one of its childen, or the reference element are focused
|
|
@@ -357,7 +406,7 @@ export class PopoverController extends BasePopoverController {
|
|
|
357
406
|
}
|
|
358
407
|
|
|
359
408
|
this.hide(e);
|
|
360
|
-
}
|
|
409
|
+
}
|
|
361
410
|
|
|
362
411
|
/**
|
|
363
412
|
* Toggles all classes on the originating element based on the `class-toggle` data
|
|
@@ -380,7 +429,8 @@ export class PopoverController extends BasePopoverController {
|
|
|
380
429
|
* @param {boolean=} show - A boolean indicating whether this is being triggered by a show or hide.
|
|
381
430
|
*/
|
|
382
431
|
private toggleAccessibilityAttributes(show?: boolean) {
|
|
383
|
-
const expandedValue =
|
|
432
|
+
const expandedValue =
|
|
433
|
+
show?.toString() || this.referenceElement.ariaExpanded || "false";
|
|
384
434
|
this.referenceElement.ariaExpanded = expandedValue;
|
|
385
435
|
this.referenceElement.setAttribute("aria-expanded", expandedValue);
|
|
386
436
|
}
|
|
@@ -447,21 +497,26 @@ export interface PopoverOptions {
|
|
|
447
497
|
* If the popover does not have a parent element, it will be inserted as a immediately after the reference element.
|
|
448
498
|
* @param options an optional collection of options to use when configuring the popover.
|
|
449
499
|
*/
|
|
450
|
-
export function attachPopover(
|
|
451
|
-
|
|
500
|
+
export function attachPopover(
|
|
501
|
+
element: Element,
|
|
502
|
+
popover: Element | string,
|
|
503
|
+
options?: PopoverOptions
|
|
504
|
+
) {
|
|
452
505
|
const { referenceElement, popover: existingPopover } = getPopover(element);
|
|
453
506
|
|
|
454
507
|
if (existingPopover) {
|
|
455
|
-
throw `element already has popover with id="${existingPopover.id}"
|
|
508
|
+
throw `element already has popover with id="${existingPopover.id}"`;
|
|
456
509
|
}
|
|
457
510
|
|
|
458
511
|
if (!referenceElement) {
|
|
459
|
-
throw `element has invalid data-s-popover-reference-selector attribute
|
|
512
|
+
throw `element has invalid data-s-popover-reference-selector attribute`;
|
|
460
513
|
}
|
|
461
514
|
|
|
462
|
-
if (typeof popover ===
|
|
515
|
+
if (typeof popover === "string") {
|
|
463
516
|
// eslint-disable-next-line no-unsanitized/method
|
|
464
|
-
const elements = document
|
|
517
|
+
const elements = document
|
|
518
|
+
.createRange()
|
|
519
|
+
.createContextualFragment(popover).children;
|
|
465
520
|
if (elements.length !== 1) {
|
|
466
521
|
throw "popover should contain a single element";
|
|
467
522
|
}
|
|
@@ -471,7 +526,7 @@ export function attachPopover(element: Element, popover: Element | string, optio
|
|
|
471
526
|
const existingId = referenceElement.getAttribute("aria-controls");
|
|
472
527
|
let popoverId = popover.id;
|
|
473
528
|
|
|
474
|
-
if (!popover.classList.contains(
|
|
529
|
+
if (!popover.classList.contains("s-popover")) {
|
|
475
530
|
throw `popover should have the "s-popover" class but had class="${popover.className}"`;
|
|
476
531
|
}
|
|
477
532
|
|
|
@@ -480,7 +535,8 @@ export function attachPopover(element: Element, popover: Element | string, optio
|
|
|
480
535
|
}
|
|
481
536
|
|
|
482
537
|
if (!popoverId) {
|
|
483
|
-
popoverId =
|
|
538
|
+
popoverId =
|
|
539
|
+
"--stacks-s-popover-" + Math.random().toString(36).substring(2, 10);
|
|
484
540
|
popover.id = popoverId;
|
|
485
541
|
}
|
|
486
542
|
|
|
@@ -496,7 +552,10 @@ export function attachPopover(element: Element, popover: Element | string, optio
|
|
|
496
552
|
|
|
497
553
|
if (options) {
|
|
498
554
|
if (options.toggleOnClick) {
|
|
499
|
-
referenceElement.setAttribute(
|
|
555
|
+
referenceElement.setAttribute(
|
|
556
|
+
"data-action",
|
|
557
|
+
"click->s-popover#toggle"
|
|
558
|
+
);
|
|
500
559
|
}
|
|
501
560
|
if (options.placement) {
|
|
502
561
|
element.setAttribute("data-s-popover-placement", options.placement);
|
|
@@ -513,7 +572,8 @@ export function attachPopover(element: Element, popover: Element | string, optio
|
|
|
513
572
|
* @returns The popover that was attached to the element.
|
|
514
573
|
*/
|
|
515
574
|
export function detachPopover(element: Element) {
|
|
516
|
-
const { isPopover, controller, referenceElement, popover } =
|
|
575
|
+
const { isPopover, controller, referenceElement, popover } =
|
|
576
|
+
getPopover(element);
|
|
517
577
|
|
|
518
578
|
// Hide the popover so its events fire.
|
|
519
579
|
controller?.hide();
|
|
@@ -534,13 +594,13 @@ export function detachPopover(element: Element) {
|
|
|
534
594
|
|
|
535
595
|
interface GetPopoverResult {
|
|
536
596
|
/** indicates whether or not the element has s-popover in its `data-controller` class */
|
|
537
|
-
isPopover: boolean
|
|
597
|
+
isPopover: boolean;
|
|
538
598
|
/** element's existing `PopoverController` or null it it has not been configured yet */
|
|
539
|
-
controller: PopoverController | null
|
|
599
|
+
controller: PopoverController | null;
|
|
540
600
|
/** popover's reference element as would live in `referenceSelector` or null if invalid */
|
|
541
|
-
referenceElement: Element | null
|
|
601
|
+
referenceElement: Element | null;
|
|
542
602
|
/** popover currently associated with the controller, or null if one does not exist in the DOM */
|
|
543
|
-
popover: HTMLElement | null
|
|
603
|
+
popover: HTMLElement | null;
|
|
544
604
|
}
|
|
545
605
|
|
|
546
606
|
/**
|
|
@@ -549,11 +609,21 @@ interface GetPopoverResult {
|
|
|
549
609
|
* @param element An element that may have `data-controller="s-popover"`.
|
|
550
610
|
*/
|
|
551
611
|
function getPopover(element: Element): GetPopoverResult {
|
|
552
|
-
const isPopover =
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
612
|
+
const isPopover =
|
|
613
|
+
element.getAttribute("data-controller")?.includes("s-popover") || false;
|
|
614
|
+
const controller = Stacks.application.getControllerForElementAndIdentifier(
|
|
615
|
+
element,
|
|
616
|
+
"s-popover"
|
|
617
|
+
) as PopoverController;
|
|
618
|
+
const referenceSelector = element.getAttribute(
|
|
619
|
+
"data-s-popover-reference-selector"
|
|
620
|
+
);
|
|
621
|
+
const referenceElement = referenceSelector
|
|
622
|
+
? element.querySelector(referenceSelector)
|
|
623
|
+
: element;
|
|
624
|
+
const popoverId = referenceElement
|
|
625
|
+
? referenceElement.getAttribute("aria-controls")
|
|
626
|
+
: null;
|
|
557
627
|
const popover = popoverId ? document.getElementById(popoverId) : null;
|
|
558
628
|
return { isPopover, controller, referenceElement, popover };
|
|
559
629
|
}
|
|
@@ -564,12 +634,18 @@ function getPopover(element: Element): GetPopoverResult {
|
|
|
564
634
|
* @param controllerName The name of the controller to add/remove
|
|
565
635
|
* @param include Whether to add the controllerName value
|
|
566
636
|
*/
|
|
567
|
-
function toggleController(
|
|
568
|
-
|
|
637
|
+
function toggleController(
|
|
638
|
+
el: Element,
|
|
639
|
+
controllerName: string,
|
|
640
|
+
include: boolean
|
|
641
|
+
) {
|
|
642
|
+
const controllers = new Set(
|
|
643
|
+
el.getAttribute("data-controller")?.split(/\s+/)
|
|
644
|
+
);
|
|
569
645
|
if (include) {
|
|
570
646
|
controllers.add(controllerName);
|
|
571
647
|
} else {
|
|
572
648
|
controllers.delete(controllerName);
|
|
573
649
|
}
|
|
574
|
-
el.setAttribute(
|
|
650
|
+
el.setAttribute("data-controller", Array.from(controllers).join(" "));
|
|
575
651
|
}
|
|
@@ -7,19 +7,29 @@ export class TableController extends Stacks.StacksController {
|
|
|
7
7
|
|
|
8
8
|
setCurrentSort(headElem: Element, direction: "asc" | "desc" | "none") {
|
|
9
9
|
if (["asc", "desc", "none"].indexOf(direction) < 0) {
|
|
10
|
-
throw "direction must be one of asc, desc, or none"
|
|
10
|
+
throw "direction must be one of asc, desc, or none";
|
|
11
11
|
}
|
|
12
12
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
13
13
|
const controller = this;
|
|
14
14
|
this.columnTargets.forEach(function (target) {
|
|
15
15
|
const isCurrrent = target === headElem;
|
|
16
16
|
|
|
17
|
-
target.classList.toggle(
|
|
17
|
+
target.classList.toggle(
|
|
18
|
+
"is-sorted",
|
|
19
|
+
isCurrrent && direction !== "none"
|
|
20
|
+
);
|
|
18
21
|
|
|
19
|
-
target
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
target
|
|
23
|
+
.querySelectorAll(".js-sorting-indicator")
|
|
24
|
+
.forEach(function (icon) {
|
|
25
|
+
const visible = isCurrrent ? direction : "none";
|
|
26
|
+
icon.classList.toggle(
|
|
27
|
+
"d-none",
|
|
28
|
+
!icon.classList.contains(
|
|
29
|
+
"js-sorting-indicator-" + visible
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
});
|
|
23
33
|
|
|
24
34
|
if (!isCurrrent || direction === "none") {
|
|
25
35
|
controller.removeElementData(target, "sort-direction");
|
|
@@ -27,7 +37,7 @@ export class TableController extends Stacks.StacksController {
|
|
|
27
37
|
controller.setElementData(target, "sort-direction", direction);
|
|
28
38
|
}
|
|
29
39
|
});
|
|
30
|
-
}
|
|
40
|
+
}
|
|
31
41
|
|
|
32
42
|
sort(evt: Event) {
|
|
33
43
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
@@ -42,7 +52,8 @@ export class TableController extends Stacks.StacksController {
|
|
|
42
52
|
// the column slot number of the clicked header
|
|
43
53
|
const colno = getCellSlot(colHead);
|
|
44
54
|
|
|
45
|
-
if (colno < 0) {
|
|
55
|
+
if (colno < 0) {
|
|
56
|
+
// this shouldn't happen if the clicked element is actually a column head
|
|
46
57
|
return;
|
|
47
58
|
}
|
|
48
59
|
|
|
@@ -52,7 +63,8 @@ export class TableController extends Stacks.StacksController {
|
|
|
52
63
|
|
|
53
64
|
// the default behavior when clicking a header is to sort by this column in ascending
|
|
54
65
|
// direction, *unless* it is already sorted that way
|
|
55
|
-
const direction =
|
|
66
|
+
const direction =
|
|
67
|
+
this.getElementData(colHead, "sort-direction") === "asc" ? -1 : 1;
|
|
56
68
|
|
|
57
69
|
const rows = Array.from(table.tBodies[0].rows);
|
|
58
70
|
|
|
@@ -82,9 +94,12 @@ export class TableController extends Stacks.StacksController {
|
|
|
82
94
|
// unless the to-be-sorted-by value is explicitly provided on the element via this attribute,
|
|
83
95
|
// the value we're using is the cell's text, trimmed of any whitespace
|
|
84
96
|
const explicit = controller.getElementData(cell, "sort-val");
|
|
85
|
-
const d =
|
|
97
|
+
const d =
|
|
98
|
+
typeof explicit === "string"
|
|
99
|
+
? explicit
|
|
100
|
+
: cell.textContent?.trim() ?? "";
|
|
86
101
|
|
|
87
|
-
if (
|
|
102
|
+
if (d !== "" && `${parseInt(d, 10)}` !== d) {
|
|
88
103
|
anyNonInt = true;
|
|
89
104
|
}
|
|
90
105
|
data.push([d, index]);
|
|
@@ -94,7 +109,10 @@ export class TableController extends Stacks.StacksController {
|
|
|
94
109
|
// having the lowest possible value (i.e. sorted to the top if ascending, bottom if descending)
|
|
95
110
|
if (!anyNonInt) {
|
|
96
111
|
data.forEach(function (tuple) {
|
|
97
|
-
tuple[0] =
|
|
112
|
+
tuple[0] =
|
|
113
|
+
tuple[0] === ""
|
|
114
|
+
? Number.MIN_VALUE
|
|
115
|
+
: parseInt(tuple[0] as string, 10);
|
|
98
116
|
});
|
|
99
117
|
}
|
|
100
118
|
|
|
@@ -117,7 +135,7 @@ export class TableController extends Stacks.StacksController {
|
|
|
117
135
|
// this is the actual reordering of the table rows
|
|
118
136
|
data.forEach(function (tup) {
|
|
119
137
|
const row = rows[tup[1]];
|
|
120
|
-
row.parentElement
|
|
138
|
+
row.parentElement?.removeChild(row);
|
|
121
139
|
if (firstBottomRow) {
|
|
122
140
|
tbody.insertBefore(row, firstBottomRow);
|
|
123
141
|
} else {
|
|
@@ -129,26 +147,35 @@ export class TableController extends Stacks.StacksController {
|
|
|
129
147
|
// will cause sorting in descending direction
|
|
130
148
|
this.setCurrentSort(colHead, direction === 1 ? "asc" : "desc");
|
|
131
149
|
}
|
|
132
|
-
|
|
133
150
|
}
|
|
134
151
|
|
|
135
|
-
function buildIndex(
|
|
152
|
+
function buildIndex(
|
|
153
|
+
section: HTMLTableSectionElement
|
|
154
|
+
): HTMLTableCellElement[][] {
|
|
136
155
|
const result = buildIndexOrGetCellSlot(section);
|
|
137
156
|
if (!(result instanceof Array)) {
|
|
138
|
-
throw "shouldn't happen"
|
|
157
|
+
throw "shouldn't happen";
|
|
139
158
|
}
|
|
140
159
|
return result;
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
function getCellSlot(cell: HTMLTableCellElement): number {
|
|
144
|
-
if (
|
|
145
|
-
|
|
163
|
+
if (
|
|
164
|
+
!(
|
|
165
|
+
cell.parentElement &&
|
|
166
|
+
cell.parentElement.parentElement instanceof HTMLTableSectionElement
|
|
167
|
+
)
|
|
168
|
+
) {
|
|
169
|
+
throw "invalid table";
|
|
146
170
|
}
|
|
147
|
-
const result = buildIndexOrGetCellSlot(
|
|
171
|
+
const result = buildIndexOrGetCellSlot(
|
|
172
|
+
cell.parentElement.parentElement,
|
|
173
|
+
cell
|
|
174
|
+
);
|
|
148
175
|
if (typeof result !== "number") {
|
|
149
|
-
throw "shouldn't happen"
|
|
176
|
+
throw "shouldn't happen";
|
|
150
177
|
}
|
|
151
|
-
return result
|
|
178
|
+
return result;
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
// Just because a <td> is the 4th *child* of its <tr> doesn't mean it belongs to the 4th *column*
|
|
@@ -166,9 +193,12 @@ function getCellSlot(cell: HTMLTableCellElement): number {
|
|
|
166
193
|
//
|
|
167
194
|
// If the second argument is given, it's a <td> or <th> that we're trying to find, and the algorithm
|
|
168
195
|
// stops as soon as it has found it and the function returns its slot number.
|
|
169
|
-
function buildIndexOrGetCellSlot(
|
|
196
|
+
function buildIndexOrGetCellSlot(
|
|
197
|
+
section: HTMLTableSectionElement,
|
|
198
|
+
findCell?: HTMLTableCellElement
|
|
199
|
+
) {
|
|
170
200
|
const index = [];
|
|
171
|
-
let curRow = section.children[0];
|
|
201
|
+
let curRow: Element | null = section.children[0];
|
|
172
202
|
|
|
173
203
|
// the elements of these two arrays are synchronized; the first array contains table cell elements,
|
|
174
204
|
// the second one contains a number that indicates for how many more rows this elements will
|
|
@@ -177,13 +207,22 @@ function buildIndexOrGetCellSlot(section: HTMLTableSectionElement, findCell?: HT
|
|
|
177
207
|
const growingRowsLeft: number[] = [];
|
|
178
208
|
|
|
179
209
|
// continue while we have actual <tr>'s left *or* we still have rowspan'ed elements that aren't done
|
|
180
|
-
while (
|
|
210
|
+
while (
|
|
211
|
+
curRow ||
|
|
212
|
+
growingRowsLeft.some(function (e) {
|
|
213
|
+
return e !== 0;
|
|
214
|
+
})
|
|
215
|
+
) {
|
|
181
216
|
const curIndexRow: HTMLTableCellElement[] = [];
|
|
182
217
|
index.push(curIndexRow);
|
|
183
218
|
|
|
184
219
|
let curSlot = 0;
|
|
185
220
|
if (curRow) {
|
|
186
|
-
for (
|
|
221
|
+
for (
|
|
222
|
+
let curCellInd = 0;
|
|
223
|
+
curCellInd < curRow.children.length;
|
|
224
|
+
curCellInd++
|
|
225
|
+
) {
|
|
187
226
|
while (growingRowsLeft[curSlot]) {
|
|
188
227
|
growingRowsLeft[curSlot]--;
|
|
189
228
|
curIndexRow[curSlot] = growing[curSlot];
|
|
@@ -191,7 +230,7 @@ function buildIndexOrGetCellSlot(section: HTMLTableSectionElement, findCell?: HT
|
|
|
191
230
|
}
|
|
192
231
|
const cell = curRow.children[curCellInd];
|
|
193
232
|
if (!(cell instanceof HTMLTableCellElement)) {
|
|
194
|
-
throw "invalid table"
|
|
233
|
+
throw "invalid table";
|
|
195
234
|
}
|
|
196
235
|
if (getComputedStyle(cell).display === "none") {
|
|
197
236
|
continue;
|
|
@@ -215,8 +254,10 @@ function buildIndexOrGetCellSlot(section: HTMLTableSectionElement, findCell?: HT
|
|
|
215
254
|
curSlot++;
|
|
216
255
|
}
|
|
217
256
|
if (curRow) {
|
|
218
|
-
curRow = curRow.nextElementSibling
|
|
257
|
+
curRow = curRow.nextElementSibling;
|
|
219
258
|
}
|
|
220
259
|
}
|
|
221
|
-
return findCell
|
|
260
|
+
return findCell
|
|
261
|
+
? -1
|
|
262
|
+
: index; /* if findCell was given but we end up here, that means it isn't in this section */
|
|
222
263
|
}
|