@stackoverflow/stacks 0.74.0 → 1.0.0-beta.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.
Files changed (89) hide show
  1. package/dist/controllers/index.d.ts +7 -0
  2. package/dist/controllers/s-expandable-control.d.ts +17 -0
  3. package/dist/controllers/s-modal.d.ts +97 -0
  4. package/dist/controllers/s-navigation-tablist.d.ts +36 -0
  5. package/dist/controllers/s-popover.d.ts +155 -0
  6. package/dist/controllers/s-table.d.ts +8 -0
  7. package/dist/controllers/s-tooltip.d.ts +82 -0
  8. package/dist/controllers/s-uploader.d.ts +48 -0
  9. package/dist/css/stacks.css +4633 -2703
  10. package/dist/css/stacks.min.css +1 -1
  11. package/dist/index.d.ts +3 -0
  12. package/dist/js/stacks.js +6026 -5403
  13. package/dist/js/stacks.min.js +1 -1
  14. package/dist/stacks.d.ts +21 -0
  15. package/lib/css/atomic/{_stacks-borders.less → borders.less} +30 -30
  16. package/lib/css/atomic/{_stacks-colors.less → colors.less} +0 -0
  17. package/lib/css/atomic/{_stacks-flex.less → flex.less} +14 -13
  18. package/lib/css/atomic/{_stacks-grid.less → grid.less} +12 -11
  19. package/lib/css/atomic/{_stacks-misc.less → misc.less} +23 -22
  20. package/lib/css/atomic/spacing.less +314 -0
  21. package/lib/css/atomic/{_stacks-typography.less → typography.less} +29 -29
  22. package/lib/css/atomic/width-height.less +194 -0
  23. package/lib/css/base/{_stacks-body.less → body.less} +3 -5
  24. package/lib/css/base/{_stacks-configuration-static.less → configuration-static.less} +3 -1
  25. package/lib/css/base/{_stacks-icons.less → icons.less} +0 -0
  26. package/lib/css/base/{_stacks-internals.less → internals.less} +0 -10
  27. package/lib/css/base/{_stacks-reset-meyer.less → reset-meyer.less} +0 -0
  28. package/lib/css/base/{_stacks-reset-normalize.less → reset-normalize.less} +0 -0
  29. package/lib/css/base/{_stacks-reset.less → reset.less} +2 -2
  30. package/lib/css/components/{_stacks-activity-indicator.less → activity-indicator.less} +9 -9
  31. package/lib/css/components/{_stacks-avatars.less → avatars.less} +40 -40
  32. package/lib/css/components/{_stacks-badges.less → badges.less} +11 -11
  33. package/lib/css/components/{_stacks-banners.less → banners.less} +5 -6
  34. package/lib/css/components/{_stacks-blank-states.less → blank-states.less} +2 -2
  35. package/lib/css/components/{_stacks-breadcrumbs.less → breadcrumbs.less} +7 -7
  36. package/lib/css/components/{_stacks-button-groups.less → button-groups.less} +2 -2
  37. package/lib/css/components/{_stacks-buttons.less → buttons.less} +77 -72
  38. package/lib/css/components/{_stacks-cards.less → cards.less} +2 -2
  39. package/lib/css/components/{_stacks-code-blocks.less → code-blocks.less} +8 -8
  40. package/lib/css/components/{_stacks-collapsible.less → collapsible.less} +0 -0
  41. package/lib/css/components/{_stacks-inputs.less → inputs.less} +41 -41
  42. package/lib/css/components/{_stacks-link-previews.less → link-previews.less} +17 -17
  43. package/lib/css/components/{_stacks-links.less → links.less} +4 -4
  44. package/lib/css/components/{_stacks-menu.less → menu.less} +5 -5
  45. package/lib/css/components/{_stacks-modals.less → modals.less} +20 -20
  46. package/lib/css/components/{_stacks-navigation.less → navigation.less} +12 -12
  47. package/lib/css/components/{_stacks-notices.less → notices.less} +12 -12
  48. package/lib/css/components/{_stacks-page-titles.less → page-titles.less} +9 -9
  49. package/lib/css/components/{_stacks-pagination.less → pagination.less} +8 -8
  50. package/lib/css/components/{_stacks-popovers.less → popovers.less} +17 -17
  51. package/lib/css/components/{_stacks-post-summary.less → post-summary.less} +155 -60
  52. package/lib/css/components/{_stacks-progress-bars.less → progress-bars.less} +29 -30
  53. package/lib/css/components/{_stacks-prose.less → prose.less} +31 -31
  54. package/lib/css/components/{_stacks-spinner.less → spinner.less} +14 -14
  55. package/lib/css/components/{_stacks-tables.less → tables.less} +10 -10
  56. package/lib/css/components/{_stacks-tags.less → tags.less} +33 -41
  57. package/lib/css/components/{_stacks-toggle-switches.less → toggle-switches.less} +5 -5
  58. package/lib/css/components/{_stacks-topbar.less → topbar.less} +27 -40
  59. package/lib/css/components/{_stacks-uploader.less → uploader.less} +18 -18
  60. package/lib/css/components/{_stacks-user-cards.less → user-cards.less} +14 -14
  61. package/lib/css/components/{_stacks-widget-dynamic.less → widget-dynamic.less} +1 -1
  62. package/lib/css/components/{_stacks-widget-static.less → widget-static.less} +39 -38
  63. package/lib/css/exports/{_stacks-constants-colors.less → constants-colors.less} +21 -29
  64. package/lib/css/exports/constants-helpers.less +108 -0
  65. package/lib/css/exports/constants-type.less +153 -0
  66. package/lib/css/exports/{_stacks-exports.less → exports.less} +4 -4
  67. package/lib/css/exports/{_stacks-mixins.less → mixins.less} +18 -1
  68. package/lib/css/stacks-dynamic.less +12 -13
  69. package/lib/css/stacks-static.less +38 -38
  70. package/lib/ts/controllers/index.ts +8 -0
  71. package/lib/ts/controllers/s-expandable-control.ts +163 -164
  72. package/lib/ts/controllers/s-modal.ts +259 -261
  73. package/lib/ts/controllers/s-navigation-tablist.ts +96 -97
  74. package/lib/ts/controllers/s-popover.ts +438 -440
  75. package/lib/ts/controllers/s-table.ts +203 -203
  76. package/lib/ts/controllers/s-tooltip.ts +195 -196
  77. package/lib/ts/controllers/s-uploader.ts +162 -164
  78. package/lib/ts/index.ts +20 -0
  79. package/lib/ts/stacks.ts +73 -68
  80. package/lib/tsconfig.json +8 -6
  81. package/package.json +43 -27
  82. package/dist/css/stacks-flexgrid-shim.min.css +0 -1
  83. package/lib/css/atomic/_stacks-spacing.less +0 -162
  84. package/lib/css/atomic/_stacks-width-height.less +0 -189
  85. package/lib/css/base/_stacks-configuration-dynamic.less +0 -106
  86. package/lib/css/exports/_stacks-constants-helpers.less +0 -139
  87. package/lib/css/exports/_stacks-constants-type.less +0 -91
  88. package/lib/ts/finalize.ts +0 -1
  89. package/lib/ts/stimulus.d.ts +0 -4
@@ -1,549 +1,547 @@
1
- namespace Stacks {
1
+ import { createPopper, Placement } from '@popperjs/core';
2
+ import * as Stacks from "../stacks";
2
3
 
3
- type OutsideClickBehavior = "always" | "never" | "if-in-viewport" | "after-dismissal";
4
+ type OutsideClickBehavior = "always" | "never" | "if-in-viewport" | "after-dismissal";
4
5
 
5
- export abstract class BasePopoverController extends StacksController {
6
- // @ts-ignore
7
- private popper!: Popper;
6
+ export abstract class BasePopoverController extends Stacks.StacksController {
7
+ // @ts-ignore
8
+ private popper!: Popper;
8
9
 
9
- protected popoverElement!: HTMLElement;
10
+ protected popoverElement!: HTMLElement;
10
11
 
11
- protected referenceElement!: HTMLElement;
12
+ protected referenceElement!: HTMLElement;
12
13
 
13
- /**
14
- * An attribute containing the ID of the popover element to render, e.g. aria-controls or aria-describedby.
15
- */
16
- protected abstract popoverSelectorAttribute: string;
14
+ /**
15
+ * An attribute containing the ID of the popover element to render, e.g. aria-controls or aria-describedby.
16
+ */
17
+ protected abstract popoverSelectorAttribute: string;
17
18
 
18
- /**
19
- * Binds events to the document on element show
20
- */
21
- protected abstract bindDocumentEvents(): void;
19
+ /**
20
+ * Binds events to the document on element show
21
+ */
22
+ protected abstract bindDocumentEvents(): void;
22
23
 
23
- /**
24
- * Unbinds events on the document on element hide
25
- */
26
- protected abstract unbindDocumentEvents(): void;
24
+ /**
25
+ * Unbinds events on the document on element hide
26
+ */
27
+ protected abstract unbindDocumentEvents(): void;
27
28
 
28
- /**
29
- * Returns true if the if the popover is currently visible.
30
- */
31
- get isVisible() {
32
- const popoverElement = this.popoverElement;
33
- return popoverElement ? popoverElement.classList.contains("is-visible") : false;
34
- }
29
+ /**
30
+ * Returns true if the if the popover is currently visible.
31
+ */
32
+ get isVisible() {
33
+ const popoverElement = this.popoverElement;
34
+ return popoverElement ? popoverElement.classList.contains("is-visible") : false;
35
+ }
35
36
 
36
- /**
37
- * Gets whether the element is visible in the browser's viewport.
38
- */
39
- get isInViewport() {
40
- const element = this.popoverElement;
41
- if (!this.isVisible || !element) { return false; }
37
+ /**
38
+ * Gets whether the element is visible in the browser's viewport.
39
+ */
40
+ get isInViewport() {
41
+ const element = this.popoverElement;
42
+ if (!this.isVisible || !element) { return false; }
42
43
 
43
- // From https://stackoverflow.com/a/5354536. Theoretically, this could be calculated using Popper's detectOverflow function,
44
- // but it's unclear how to access that with our current configuration.
44
+ // From https://stackoverflow.com/a/5354536. Theoretically, this could be calculated using Popper's detectOverflow function,
45
+ // but it's unclear how to access that with our current configuration.
45
46
 
46
- const rect = element.getBoundingClientRect();
47
- const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
48
- const viewWidth = Math.max(document.documentElement.clientWidth, window.innerWidth);
47
+ const rect = element.getBoundingClientRect();
48
+ const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
49
+ const viewWidth = Math.max(document.documentElement.clientWidth, window.innerWidth);
49
50
 
50
- return rect.bottom > 0 && rect.top < viewHeight && rect.right > 0 && rect.left < viewWidth;
51
- }
51
+ return rect.bottom > 0 && rect.top < viewHeight && rect.right > 0 && rect.left < viewWidth;
52
+ }
52
53
 
53
- protected get shouldHideOnOutsideClick() {
54
- const hideBehavior = <OutsideClickBehavior>this.data.get("hide-on-outside-click");
55
- switch (hideBehavior) {
56
- case "after-dismissal":
57
- case "never":
58
- return false;
59
- case "if-in-viewport":
60
- return this.isInViewport;
61
- default:
62
- return true;
63
- }
54
+ protected get shouldHideOnOutsideClick() {
55
+ const hideBehavior = <OutsideClickBehavior>this.data.get("hide-on-outside-click");
56
+ switch (hideBehavior) {
57
+ case "after-dismissal":
58
+ case "never":
59
+ return false;
60
+ case "if-in-viewport":
61
+ return this.isInViewport;
62
+ default:
63
+ return true;
64
64
  }
65
+ }
65
66
 
66
- /**
67
- * Initializes and validates controller variables
68
- */
69
- connect() {
70
- super.connect();
71
- this.validate();
72
- if (this.isVisible) {
73
- // just call initialize here, not show. This keeps already visible popovers from adding/firing document events
74
- this.initializePopper();
75
- } else if (this.data.get("auto-show") === "true") {
76
- this.show(null);
77
- }
78
-
79
- this.data.delete("auto-show");
67
+ /**
68
+ * Initializes and validates controller variables
69
+ */
70
+ connect() {
71
+ super.connect();
72
+ this.validate();
73
+ if (this.isVisible) {
74
+ // just call initialize here, not show. This keeps already visible popovers from adding/firing document events
75
+ this.initializePopper();
76
+ } else if (this.data.get("auto-show") === "true") {
77
+ this.show(null);
80
78
  }
81
79
 
82
- /**
83
- * Cleans up popper.js elements and disconnects all added event listeners
84
- */
85
- disconnect() {
86
- this.hide();
87
- if (this.popper) {
88
- this.popper.destroy();
89
- delete this.popper;
90
- }
91
- super.disconnect();
92
- }
80
+ this.data.delete("auto-show");
81
+ }
93
82
 
94
- /**
95
- * Toggles the visibility of the popover
96
- */
97
- toggle(dispatcher: Event|Element|null = null) {
98
- this.isVisible ? this.hide(dispatcher) : this.show(dispatcher);
83
+ /**
84
+ * Cleans up popper.js elements and disconnects all added event listeners
85
+ */
86
+ disconnect() {
87
+ this.hide();
88
+ if (this.popper) {
89
+ this.popper.destroy();
90
+ delete this.popper;
99
91
  }
92
+ super.disconnect();
93
+ }
100
94
 
101
- /**
102
- * Shows the popover if not already visible
103
- */
104
- show(dispatcher: Event|Element|null = null) {
105
- if (this.isVisible) { return; }
106
-
107
- let dispatcherElement = this.getDispatcher(dispatcher);
108
-
109
- if (this.triggerEvent("show", {
110
- dispatcher: dispatcherElement
111
- }).defaultPrevented) { return; }
95
+ /**
96
+ * Toggles the visibility of the popover
97
+ */
98
+ toggle(dispatcher: Event|Element|null = null) {
99
+ this.isVisible ? this.hide(dispatcher) : this.show(dispatcher);
100
+ }
112
101
 
113
- if (!this.popper) {
114
- this.initializePopper();
115
- }
102
+ /**
103
+ * Shows the popover if not already visible
104
+ */
105
+ show(dispatcher: Event|Element|null = null) {
106
+ if (this.isVisible) { return; }
116
107
 
117
- this.popoverElement!.classList.add("is-visible");
108
+ let dispatcherElement = this.getDispatcher(dispatcher);
118
109
 
119
- // ensure the popper has been positioned correctly
120
- this.scheduleUpdate();
110
+ if (this.triggerEvent("show", {
111
+ dispatcher: dispatcherElement
112
+ }).defaultPrevented) { return; }
121
113
 
122
- this.shown(dispatcherElement);
114
+ if (!this.popper) {
115
+ this.initializePopper();
123
116
  }
124
117
 
125
- /**
126
- * Hides the popover if not already hidden
127
- */
128
- hide(dispatcher: Event|Element|null = null) {
129
- if (!this.isVisible) { return; }
118
+ this.popoverElement!.classList.add("is-visible");
130
119
 
131
- let dispatcherElement = this.getDispatcher(dispatcher);
120
+ // ensure the popper has been positioned correctly
121
+ this.scheduleUpdate();
132
122
 
133
- if (this.triggerEvent("hide", {
134
- dispatcher: dispatcherElement
135
- }).defaultPrevented) { return; }
136
-
137
- this.popoverElement.classList.remove("is-visible");
123
+ this.shown(dispatcherElement);
124
+ }
138
125
 
139
- if (this.popper) {
140
- // completely destroy the popper on hide; this is in line with Popper.js's performance recommendations
141
- this.popper.destroy();
142
- delete this.popper;
143
- }
126
+ /**
127
+ * Hides the popover if not already hidden
128
+ */
129
+ hide(dispatcher: Event|Element|null = null) {
130
+ if (!this.isVisible) { return; }
144
131
 
145
- // on first interaction, hide-on-outside-click with value "after-dismissal" reverts to the default behavior
146
- if (<OutsideClickBehavior>this.data.get("hide-on-outside-click") === "after-dismissal") {
147
- this.data.delete("hide-on-outside-click");
148
- }
132
+ let dispatcherElement = this.getDispatcher(dispatcher);
149
133
 
150
- this.hidden(dispatcherElement);
151
- }
134
+ if (this.triggerEvent("hide", {
135
+ dispatcher: dispatcherElement
136
+ }).defaultPrevented) { return; }
152
137
 
153
- /**
154
- * Binds document events for this popover and fires the shown event
155
- */
156
- protected shown(dispatcher: Element|null = null) {
157
- this.bindDocumentEvents();
158
- this.triggerEvent("shown", {
159
- dispatcher: dispatcher
160
- });
161
- }
138
+ this.popoverElement.classList.remove("is-visible");
162
139
 
163
- /**
164
- * Unbinds document events for this popover and fires the hidden event
165
- */
166
- protected hidden(dispatcher: Element|null = null) {
167
- this.unbindDocumentEvents();
168
- this.triggerEvent("hidden", {
169
- dispatcher: dispatcher
170
- });
140
+ if (this.popper) {
141
+ // completely destroy the popper on hide; this is in line with Popper.js's performance recommendations
142
+ this.popper.destroy();
143
+ delete this.popper;
171
144
  }
172
145
 
173
- /**
174
- * Generates the popover if not found during initialization
175
- */
176
- protected generatePopover(): HTMLElement | null {
177
- return null;
146
+ // on first interaction, hide-on-outside-click with value "after-dismissal" reverts to the default behavior
147
+ if (<OutsideClickBehavior>this.data.get("hide-on-outside-click") === "after-dismissal") {
148
+ this.data.delete("hide-on-outside-click");
178
149
  }
179
150
 
180
- /**
181
- * Initializes the Popper for this instance
182
- */
183
- private initializePopper() {
184
- // @ts-ignore
185
- this.popper = Popper.createPopper(this.referenceElement, this.popoverElement, {
186
- placement: this.data.get("placement") || "bottom",
187
- modifiers: [
188
- {
189
- name: "offset",
190
- options: {
191
- offset: [0, 10], // The entire popover should be 10px away from the element
192
- }
193
- },
194
- {
195
- name: "arrow",
196
- options: {
197
- element: ".s-popover--arrow"
198
- },
199
- },
200
- ]
201
- });
202
- }
151
+ this.hidden(dispatcherElement);
152
+ }
203
153
 
204
- /**
205
- * Validates the popover settings and attempts to set necessary internal variables
206
- */
207
- private validate() {
208
- var referenceSelector = this.data.get("reference-selector");
154
+ /**
155
+ * Binds document events for this popover and fires the shown event
156
+ */
157
+ protected shown(dispatcher: Element|null = null) {
158
+ this.bindDocumentEvents();
159
+ this.triggerEvent("shown", {
160
+ dispatcher: dispatcher
161
+ });
162
+ }
209
163
 
210
- this.referenceElement = <HTMLElement>this.element;
164
+ /**
165
+ * Unbinds document events for this popover and fires the hidden event
166
+ */
167
+ protected hidden(dispatcher: Element|null = null) {
168
+ this.unbindDocumentEvents();
169
+ this.triggerEvent("hidden", {
170
+ dispatcher: dispatcher
171
+ });
172
+ }
211
173
 
212
- // if there is an alternative reference selector and that element exists, use it (and throw if it isn't found)
213
- if (referenceSelector) {
214
- this.referenceElement = <HTMLElement>this.element.querySelector(referenceSelector);
174
+ /**
175
+ * Generates the popover if not found during initialization
176
+ */
177
+ protected generatePopover(): HTMLElement | null {
178
+ return null;
179
+ }
215
180
 
216
- if (!this.referenceElement) {
217
- throw "Unable to find element by reference selector: " + referenceSelector;
218
- }
219
- }
181
+ /**
182
+ * Initializes the Popper for this instance
183
+ */
184
+ private initializePopper() {
185
+ // @ts-ignore
186
+ this.popper = createPopper(this.referenceElement, this.popoverElement, {
187
+ placement: this.data.get("placement") as Placement || "bottom",
188
+ modifiers: [
189
+ {
190
+ name: "offset",
191
+ options: {
192
+ offset: [0, 10], // The entire popover should be 10px away from the element
193
+ }
194
+ },
195
+ {
196
+ name: "arrow",
197
+ options: {
198
+ element: ".s-popover--arrow"
199
+ },
200
+ },
201
+ ]
202
+ });
203
+ }
220
204
 
221
- const popoverId = this.referenceElement.getAttribute(this.popoverSelectorAttribute);
205
+ /**
206
+ * Validates the popover settings and attempts to set necessary internal variables
207
+ */
208
+ private validate() {
209
+ var referenceSelector = this.data.get("reference-selector");
222
210
 
223
- var popoverElement = null;
211
+ this.referenceElement = <HTMLElement>this.element;
224
212
 
225
- // if the popover is named, attempt to fetch it (and throw an error if it doesn't exist)
226
- if (popoverId) {
227
- popoverElement = document.getElementById(popoverId);
213
+ // if there is an alternative reference selector and that element exists, use it (and throw if it isn't found)
214
+ if (referenceSelector) {
215
+ this.referenceElement = <HTMLElement>this.element.querySelector(referenceSelector);
228
216
 
229
- if (!popoverElement){
230
- throw `[${this.popoverSelectorAttribute}="{POPOVER_ID}"] required`;
231
- }
232
- }
233
- // if the popover isn't named, attempt to generate it
234
- else {
235
- popoverElement = this.generatePopover();
217
+ if (!this.referenceElement) {
218
+ throw "Unable to find element by reference selector: " + referenceSelector;
236
219
  }
220
+ }
237
221
 
238
- if (!popoverElement) {
239
- throw "unable to find or generate popover element";
240
- }
222
+ const popoverId = this.referenceElement.getAttribute(this.popoverSelectorAttribute);
241
223
 
242
- this.popoverElement = popoverElement;
243
- }
224
+ var popoverElement = null;
244
225
 
245
- /**
246
- * Determines the correct dispatching element from a potential input
247
- * @param dispatcher The event or element to get the dispatcher from
248
- */
249
- protected getDispatcher(dispatcher: Event|Element|null = null) : Element {
250
- if (dispatcher instanceof Event) {
251
- return <Element>dispatcher.target;
252
- }
253
- else if (dispatcher instanceof Element) {
254
- return dispatcher;
255
- }
256
- else {
257
- return this.element;
258
- }
259
- }
226
+ // if the popover is named, attempt to fetch it (and throw an error if it doesn't exist)
227
+ if (popoverId) {
228
+ popoverElement = document.getElementById(popoverId);
260
229
 
261
- /**
262
- * Schedules the popover to update on the next animation frame if visible
263
- */
264
- protected scheduleUpdate() {
265
- if (this.popper && this.isVisible) {
266
- this.popper.update();
230
+ if (!popoverElement){
231
+ throw `[${this.popoverSelectorAttribute}="{POPOVER_ID}"] required`;
267
232
  }
268
233
  }
269
- }
270
-
271
- export class PopoverController extends BasePopoverController {
272
- static targets = [];
273
-
274
- protected popoverSelectorAttribute = "aria-controls";
275
-
276
- private boundHideOnOutsideClick!: any;
277
- private boundHideOnEscapePress!: any;
278
-
279
- /**
280
- * Toggles optional classes in addition to BasePopoverController.shown
281
- */
282
- protected shown(dispatcher: Element|null = null) {
283
- this.toggleOptionalClasses(true);
284
- super.shown(dispatcher);
234
+ // if the popover isn't named, attempt to generate it
235
+ else {
236
+ popoverElement = this.generatePopover();
285
237
  }
286
238
 
287
- /**
288
- * Toggles optional classes in addition to BasePopoverController.hidden
289
- */
290
- protected hidden(dispatcher: Element|null = null) {
291
- this.toggleOptionalClasses(false);
292
- super.hidden(dispatcher);
239
+ if (!popoverElement) {
240
+ throw "unable to find or generate popover element";
293
241
  }
294
242
 
295
- /**
296
- * Binds global events to the document for hiding popovers on user interaction
297
- */
298
- protected bindDocumentEvents() {
299
- this.boundHideOnOutsideClick = this.boundHideOnOutsideClick || this.hideOnOutsideClick.bind(this);
300
- this.boundHideOnEscapePress = this.boundHideOnEscapePress || this.hideOnEscapePress.bind(this);
243
+ this.popoverElement = popoverElement;
244
+ }
301
245
 
302
- document.addEventListener("mousedown", this.boundHideOnOutsideClick);
303
- document.addEventListener("keyup", this.boundHideOnEscapePress);
246
+ /**
247
+ * Determines the correct dispatching element from a potential input
248
+ * @param dispatcher The event or element to get the dispatcher from
249
+ */
250
+ protected getDispatcher(dispatcher: Event|Element|null = null) : Element {
251
+ if (dispatcher instanceof Event) {
252
+ return <Element>dispatcher.target;
304
253
  }
254
+ else if (dispatcher instanceof Element) {
255
+ return dispatcher;
256
+ }
257
+ else {
258
+ return this.element;
259
+ }
260
+ }
305
261
 
306
- /**
307
- * Unbinds global events to the document for hiding popovers on user interaction
308
- */
309
- protected unbindDocumentEvents() {
310
- document.removeEventListener("mousedown", this.boundHideOnOutsideClick);
311
- document.removeEventListener("keyup", this.boundHideOnEscapePress);
262
+ /**
263
+ * Schedules the popover to update on the next animation frame if visible
264
+ */
265
+ protected scheduleUpdate() {
266
+ if (this.popper && this.isVisible) {
267
+ this.popper.update();
312
268
  }
269
+ }
270
+ }
313
271
 
314
- /**
315
- * Forces the popover to hide if a user clicks outside of it or its reference element
316
- * @param {Event} e - The document click event
317
- */
318
- private hideOnOutsideClick(e: MouseEvent) {
319
- const target = <Node>e.target;
320
- // check if the document was clicked inside either the reference element or the popover itself
321
- // note: .contains also returns true if the node itself matches the target element
322
- if (this.shouldHideOnOutsideClick && !this.referenceElement.contains(target) && !this.popoverElement!.contains(target) && document.body.contains(target)) {
323
- this.hide(e);
324
- }
325
- };
326
-
327
- /**
328
- * Forces the popover to hide if the user presses escape while it, one of its childen, or the reference element are focused
329
- * @param {Event} e - The document keyup event
330
- */
331
- private hideOnEscapePress(e: KeyboardEvent) {
332
- // if the ESC key (27) wasn't pressed or if no popovers are showing, return
333
- if (e.which !== 27 || !this.isVisible) {
334
- return;
335
- }
272
+ export class PopoverController extends BasePopoverController {
273
+ static targets = [];
336
274
 
337
- // check if the target was inside the popover element and refocus the triggering element
338
- // note: .contains also returns true if the node itself matches the target element
339
- if (this.popoverElement!.contains(<Node>e.target)) {
340
- this.referenceElement.focus();
341
- }
275
+ protected popoverSelectorAttribute = "aria-controls";
342
276
 
343
- this.hide(e);
344
- };
345
-
346
- /**
347
- * Toggles all classes on the originating element based on the `class-toggle` data
348
- * @param {boolean=} show - A boolean indicating whether this is being triggered by a show or hide.
349
- */
350
- private toggleOptionalClasses(show?: boolean) {
351
- if (!this.data.has("toggle-class")) {
352
- return;
353
- }
354
- var cl = this.referenceElement.classList;
355
- this.data.get("toggle-class")!.split(/\s+/).forEach(function (cls: string) {
356
- cl.toggle(cls, show);
357
- });
358
- }
359
- }
277
+ private boundHideOnOutsideClick!: any;
278
+ private boundHideOnEscapePress!: any;
360
279
 
361
280
  /**
362
- * Helper to manually show an s-popover element via external JS
363
- * @param element the element the `data-controller="s-popover"` attribute is on
281
+ * Toggles optional classes in addition to BasePopoverController.shown
364
282
  */
365
- export function showPopover(element: HTMLElement) {
366
- const { isPopover, controller } = getPopover(element);
367
- if (controller) {
368
- controller.show();
369
- } else if (isPopover) {
370
- element.setAttribute("data-s-popover-auto-show", "true");
371
- } else {
372
- throw `element does not have data-controller="s-popover"`;
373
- }
283
+ protected shown(dispatcher: Element|null = null) {
284
+ this.toggleOptionalClasses(true);
285
+ super.shown(dispatcher);
374
286
  }
375
287
 
376
288
  /**
377
- * Helper to manually hide an s-popover element via external JS
378
- * @param element the element the `data-controller="s-popover"` attribute is on
289
+ * Toggles optional classes in addition to BasePopoverController.hidden
379
290
  */
380
- export function hidePopover(element: Element) {
381
- const { isPopover, controller, popover } = getPopover(element);
382
-
383
- if (controller) {
384
- controller.hide();
385
- } else if (isPopover) {
386
- element.removeAttribute("data-s-popover-auto-show");
387
- if (popover) {
388
- popover.classList.remove("is-visible");
389
- }
390
- } else {
391
- throw `element does not have data-controller="s-popover"`;
392
- }
291
+ protected hidden(dispatcher: Element|null = null) {
292
+ this.toggleOptionalClasses(false);
293
+ super.hidden(dispatcher);
393
294
  }
394
295
 
395
296
  /**
396
- * Options to use when attaching a popover via `Stacks.attachPopover`.
397
- * @see Stacks.attachPopover
297
+ * Binds global events to the document for hiding popovers on user interaction
398
298
  */
399
- export interface PopoverOptions {
400
- /**
401
- * When true, the `click->s-popover#toggle` action will be attached to the controller element or reference element.
402
- */
403
- toggleOnClick?: boolean;
404
- /**
405
- * When set, `data-s-popover-placement` will be set to this value on the controller element.
406
- */
407
- placement?: string;
408
-
409
- /**
410
- * When true, the popover will appear immediately when the controller connects.
411
- */
412
- autoShow?: boolean;
299
+ protected bindDocumentEvents() {
300
+ this.boundHideOnOutsideClick = this.boundHideOnOutsideClick || this.hideOnOutsideClick.bind(this);
301
+ this.boundHideOnEscapePress = this.boundHideOnEscapePress || this.hideOnEscapePress.bind(this);
302
+
303
+ document.addEventListener("mousedown", this.boundHideOnOutsideClick);
304
+ document.addEventListener("keyup", this.boundHideOnEscapePress);
413
305
  }
414
306
 
415
307
  /**
416
- * Attaches a popover to an element and performs additional configuration.
417
- * @param element the element that will receive the `data-controller="s-popover"` attribute.
418
- * @param popover an element with the `.s-popover` class or HTML string containing a single element with the `.s-popover` class.
419
- * If the popover does not have a parent element, it will be inserted as a immediately after the reference element.
420
- * @param options an optional collection of options to use when configuring the popover.
308
+ * Unbinds global events to the document for hiding popovers on user interaction
421
309
  */
422
- export function attachPopover(element: Element, popover: Element | string, options?: PopoverOptions)
423
- {
424
- const { referenceElement, popover: existingPopover } = getPopover(element);
310
+ protected unbindDocumentEvents() {
311
+ document.removeEventListener("mousedown", this.boundHideOnOutsideClick);
312
+ document.removeEventListener("keyup", this.boundHideOnEscapePress);
313
+ }
425
314
 
426
- if (existingPopover) {
427
- throw `element already has popover with id="${existingPopover.id}"`
315
+ /**
316
+ * Forces the popover to hide if a user clicks outside of it or its reference element
317
+ * @param {Event} e - The document click event
318
+ */
319
+ private hideOnOutsideClick(e: MouseEvent) {
320
+ const target = <Node>e.target;
321
+ // check if the document was clicked inside either the reference element or the popover itself
322
+ // note: .contains also returns true if the node itself matches the target element
323
+ if (this.shouldHideOnOutsideClick && !this.referenceElement.contains(target) && !this.popoverElement!.contains(target) && document.body.contains(target)) {
324
+ this.hide(e);
428
325
  }
326
+ };
429
327
 
430
- if (!referenceElement) {
431
- throw `element has invalid data-s-popover-reference-selector attribute`
328
+ /**
329
+ * Forces the popover to hide if the user presses escape while it, one of its childen, or the reference element are focused
330
+ * @param {Event} e - The document keyup event
331
+ */
332
+ private hideOnEscapePress(e: KeyboardEvent) {
333
+ // if the ESC key (27) wasn't pressed or if no popovers are showing, return
334
+ if (e.which !== 27 || !this.isVisible) {
335
+ return;
432
336
  }
433
337
 
434
- if (typeof popover === 'string') {
435
- const elements = document.createRange().createContextualFragment(popover).children;
436
- if (elements.length !== 1) {
437
- throw "popover should contain a single element";
438
- }
439
- popover = elements[0];
338
+ // check if the target was inside the popover element and refocus the triggering element
339
+ // note: .contains also returns true if the node itself matches the target element
340
+ if (this.popoverElement!.contains(<Node>e.target)) {
341
+ this.referenceElement.focus();
440
342
  }
441
343
 
442
- const existingId = referenceElement.getAttribute("aria-controls");
443
- var popoverId = popover.id;
344
+ this.hide(e);
345
+ };
444
346
 
445
- if (!popover.classList.contains('s-popover')) {
446
- throw `popover should have the "s-popover" class but had class="${popover.className}"`;
447
- }
347
+ /**
348
+ * Toggles all classes on the originating element based on the `class-toggle` data
349
+ * @param {boolean=} show - A boolean indicating whether this is being triggered by a show or hide.
350
+ */
351
+ private toggleOptionalClasses(show?: boolean) {
352
+ if (!this.data.has("toggle-class")) {
353
+ return;
354
+ }
355
+ var cl = this.referenceElement.classList;
356
+ this.data.get("toggle-class")!.split(/\s+/).forEach(function (cls: string) {
357
+ cl.toggle(cls, show);
358
+ });
359
+ }
360
+ }
448
361
 
449
- if (existingId && existingId !== popoverId) {
450
- throw `element has aria-controls="${existingId}" but popover has id="${popoverId}"`;
451
- }
362
+ /**
363
+ * Helper to manually show an s-popover element via external JS
364
+ * @param element the element the `data-controller="s-popover"` attribute is on
365
+ */
366
+ export function showPopover(element: HTMLElement) {
367
+ const { isPopover, controller } = getPopover(element);
368
+ if (controller) {
369
+ controller.show();
370
+ } else if (isPopover) {
371
+ element.setAttribute("data-s-popover-auto-show", "true");
372
+ } else {
373
+ throw `element does not have data-controller="s-popover"`;
374
+ }
375
+ }
452
376
 
453
- if (!popoverId) {
454
- popoverId = "--stacks-s-popover-" + Math.random().toString(36).substring(2, 10);
455
- popover.id = popoverId;
456
- }
377
+ /**
378
+ * Helper to manually hide an s-popover element via external JS
379
+ * @param element the element the `data-controller="s-popover"` attribute is on
380
+ */
381
+ export function hidePopover(element: Element) {
382
+ const { isPopover, controller, popover } = getPopover(element);
383
+
384
+ if (controller) {
385
+ controller.hide();
386
+ } else if (isPopover) {
387
+ element.removeAttribute("data-s-popover-auto-show");
388
+ if (popover) {
389
+ popover.classList.remove("is-visible");
390
+ }
391
+ } else {
392
+ throw `element does not have data-controller="s-popover"`;
393
+ }
394
+ }
457
395
 
458
- if (!existingId) {
459
- referenceElement.setAttribute("aria-controls", popoverId);
460
- }
396
+ /**
397
+ * Options to use when attaching a popover via `Stacks.attachPopover`.
398
+ * @see Stacks.attachPopover
399
+ */
400
+ export interface PopoverOptions {
401
+ /**
402
+ * When true, the `click->s-popover#toggle` action will be attached to the controller element or reference element.
403
+ */
404
+ toggleOnClick?: boolean;
405
+ /**
406
+ * When set, `data-s-popover-placement` will be set to this value on the controller element.
407
+ */
408
+ placement?: string;
461
409
 
462
- if (!popover.parentElement && element.parentElement) {
463
- referenceElement.insertAdjacentElement("afterend", popover);
464
- }
410
+ /**
411
+ * When true, the popover will appear immediately when the controller connects.
412
+ */
413
+ autoShow?: boolean;
414
+ }
465
415
 
466
- toggleController(element, "s-popover", true);
416
+ /**
417
+ * Attaches a popover to an element and performs additional configuration.
418
+ * @param element the element that will receive the `data-controller="s-popover"` attribute.
419
+ * @param popover an element with the `.s-popover` class or HTML string containing a single element with the `.s-popover` class.
420
+ * If the popover does not have a parent element, it will be inserted as a immediately after the reference element.
421
+ * @param options an optional collection of options to use when configuring the popover.
422
+ */
423
+ export function attachPopover(element: Element, popover: Element | string, options?: PopoverOptions)
424
+ {
425
+ const { referenceElement, popover: existingPopover } = getPopover(element);
426
+
427
+ if (existingPopover) {
428
+ throw `element already has popover with id="${existingPopover.id}"`
429
+ }
467
430
 
468
- if (options) {
469
- if (options.toggleOnClick) {
470
- referenceElement.setAttribute("data-action", "click->s-popover#toggle");
471
- }
472
- if (options.placement) {
473
- element.setAttribute("data-s-popover-placement", options.placement);
474
- }
475
- if (options.autoShow) {
476
- element.setAttribute("data-s-popover-auto-show", "true");
477
- }
431
+ if (!referenceElement) {
432
+ throw `element has invalid data-s-popover-reference-selector attribute`
433
+ }
434
+
435
+ if (typeof popover === 'string') {
436
+ const elements = document.createRange().createContextualFragment(popover).children;
437
+ if (elements.length !== 1) {
438
+ throw "popover should contain a single element";
478
439
  }
440
+ popover = elements[0];
479
441
  }
480
442
 
481
- /**
482
- * Removes the popover controller from an element and removes the popover from the DOM.
483
- * @param element the element that has the `data-controller="s-popover"` attribute.
484
- * @returns The popover that was attached to the element.
485
- */
486
- export function detachPopover(element: Element) {
487
- const { isPopover, controller, referenceElement, popover } = getPopover(element);
443
+ const existingId = referenceElement.getAttribute("aria-controls");
444
+ var popoverId = popover.id;
488
445
 
489
- // Hide the popover so its events fire.
490
- controller?.hide();
446
+ if (!popover.classList.contains('s-popover')) {
447
+ throw `popover should have the "s-popover" class but had class="${popover.className}"`;
448
+ }
491
449
 
492
- // Remove the popover if it exists
493
- popover?.remove();
450
+ if (existingId && existingId !== popoverId) {
451
+ throw `element has aria-controls="${existingId}" but popover has id="${popoverId}"`;
452
+ }
494
453
 
495
- // Remove the popover controller and the aria-controls attributes.
496
- if (isPopover) {
497
- toggleController(element, "s-popover", false);
498
- if (referenceElement) {
499
- referenceElement.removeAttribute("aria-controls");
500
- }
501
- }
454
+ if (!popoverId) {
455
+ popoverId = "--stacks-s-popover-" + Math.random().toString(36).substring(2, 10);
456
+ popover.id = popoverId;
457
+ }
502
458
 
503
- return popover;
459
+ if (!existingId) {
460
+ referenceElement.setAttribute("aria-controls", popoverId);
504
461
  }
505
462
 
506
- interface GetPopoverResult {
507
- /** indicates whether or not the element has s-popover in its `data-controller` class */
508
- isPopover: boolean,
509
- /** element's existing `PopoverController` or null it it has not been configured yet */
510
- controller: PopoverController | null,
511
- /** popover's reference element as would live in `referenceSelector` or null if invalid */
512
- referenceElement: Element | null,
513
- /** popover currently associated with the controller, or null if one does not exist in the DOM */
514
- popover: HTMLElement | null
463
+ if (!popover.parentElement && element.parentElement) {
464
+ referenceElement.insertAdjacentElement("afterend", popover);
515
465
  }
516
466
 
517
- /**
518
- * Gets the current state of an element that may be or is intended to be an s-popover controller
519
- * so it can be configured either directly or via the DOM.
520
- * @param element An element that may have `data-controller="s-popover"`.
521
- */
522
- function getPopover(element: Element): GetPopoverResult {
523
- const isPopover = element.getAttribute("data-controller")?.includes("s-popover") || false;
524
- const controller = Stacks.application.getControllerForElementAndIdentifier(element, "s-popover") as PopoverController;
525
- const referenceSelector = element.getAttribute("data-s-popover-reference-selector");
526
- const referenceElement = referenceSelector ? element.querySelector(referenceSelector) : element;
527
- const popoverId = referenceElement ? referenceElement.getAttribute("aria-controls") : null;
528
- const popover = popoverId ? document.getElementById(popoverId) : null;
529
- return { isPopover, controller, referenceElement, popover };
467
+ toggleController(element, "s-popover", true);
468
+
469
+ if (options) {
470
+ if (options.toggleOnClick) {
471
+ referenceElement.setAttribute("data-action", "click->s-popover#toggle");
472
+ }
473
+ if (options.placement) {
474
+ element.setAttribute("data-s-popover-placement", options.placement);
475
+ }
476
+ if (options.autoShow) {
477
+ element.setAttribute("data-s-popover-auto-show", "true");
478
+ }
530
479
  }
480
+ }
531
481
 
532
- /**
533
- * Adds or removes the controller from an element's [data-controller] attribute without altering existing entries
534
- * @param el The element to alter
535
- * @param controllerName The name of the controller to add/remove
536
- * @param include Whether to add the controllerName value
537
- */
538
- function toggleController(el: Element, controllerName: string, include: boolean) {
539
- var controllers = new Set(el.getAttribute('data-controller')?.split(/\s+/));
540
- if (include) {
541
- controllers.add(controllerName);
542
- } else {
543
- controllers.delete(controllerName);
482
+ /**
483
+ * Removes the popover controller from an element and removes the popover from the DOM.
484
+ * @param element the element that has the `data-controller="s-popover"` attribute.
485
+ * @returns The popover that was attached to the element.
486
+ */
487
+ export function detachPopover(element: Element) {
488
+ const { isPopover, controller, referenceElement, popover } = getPopover(element);
489
+
490
+ // Hide the popover so its events fire.
491
+ controller?.hide();
492
+
493
+ // Remove the popover if it exists
494
+ popover?.remove();
495
+
496
+ // Remove the popover controller and the aria-controls attributes.
497
+ if (isPopover) {
498
+ toggleController(element, "s-popover", false);
499
+ if (referenceElement) {
500
+ referenceElement.removeAttribute("aria-controls");
544
501
  }
545
- el.setAttribute('data-controller', Array.from(controllers).join(' '))
546
502
  }
503
+
504
+ return popover;
505
+ }
506
+
507
+ interface GetPopoverResult {
508
+ /** indicates whether or not the element has s-popover in its `data-controller` class */
509
+ isPopover: boolean,
510
+ /** element's existing `PopoverController` or null it it has not been configured yet */
511
+ controller: PopoverController | null,
512
+ /** popover's reference element as would live in `referenceSelector` or null if invalid */
513
+ referenceElement: Element | null,
514
+ /** popover currently associated with the controller, or null if one does not exist in the DOM */
515
+ popover: HTMLElement | null
547
516
  }
548
517
 
549
- Stacks.application.register("s-popover", Stacks.PopoverController);
518
+ /**
519
+ * Gets the current state of an element that may be or is intended to be an s-popover controller
520
+ * so it can be configured either directly or via the DOM.
521
+ * @param element An element that may have `data-controller="s-popover"`.
522
+ */
523
+ function getPopover(element: Element): GetPopoverResult {
524
+ const isPopover = element.getAttribute("data-controller")?.includes("s-popover") || false;
525
+ const controller = Stacks.application.getControllerForElementAndIdentifier(element, "s-popover") as PopoverController;
526
+ const referenceSelector = element.getAttribute("data-s-popover-reference-selector");
527
+ const referenceElement = referenceSelector ? element.querySelector(referenceSelector) : element;
528
+ const popoverId = referenceElement ? referenceElement.getAttribute("aria-controls") : null;
529
+ const popover = popoverId ? document.getElementById(popoverId) : null;
530
+ return { isPopover, controller, referenceElement, popover };
531
+ }
532
+
533
+ /**
534
+ * Adds or removes the controller from an element's [data-controller] attribute without altering existing entries
535
+ * @param el The element to alter
536
+ * @param controllerName The name of the controller to add/remove
537
+ * @param include Whether to add the controllerName value
538
+ */
539
+ function toggleController(el: Element, controllerName: string, include: boolean) {
540
+ var controllers = new Set(el.getAttribute('data-controller')?.split(/\s+/));
541
+ if (include) {
542
+ controllers.add(controllerName);
543
+ } else {
544
+ controllers.delete(controllerName);
545
+ }
546
+ el.setAttribute('data-controller', Array.from(controllers).join(' '))
547
+ }