@stackoverflow/stacks 1.3.0 → 1.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/dist/controllers/s-expandable-control.d.ts +1 -1
- package/dist/controllers/s-tooltip.d.ts +16 -1
- package/dist/css/stacks.css +715 -527
- package/dist/css/stacks.min.css +1 -1
- package/dist/js/stacks.js +153 -84
- package/dist/js/stacks.min.js +1 -1
- package/lib/css/atomic/misc.less +1 -1
- package/lib/css/atomic/typography.less +0 -6
- package/lib/css/components/activity-indicator.less +18 -17
- package/lib/css/components/avatars.less +50 -131
- package/lib/css/components/breadcrumbs.less +4 -4
- package/lib/css/components/buttons.less +8 -48
- package/lib/css/components/empty-states.less +15 -0
- package/lib/css/components/{collapsible.less → expandable.less} +0 -0
- package/lib/css/components/inputs.less +37 -101
- package/lib/css/components/labels.less +98 -0
- package/lib/css/components/notices.less +190 -163
- package/lib/css/components/post-summary.less +4 -4
- package/lib/css/components/progress-bars.less +1 -1
- package/lib/css/components/prose.less +4 -4
- package/lib/css/components/spinner.less +39 -1
- package/lib/css/components/tables.less +1 -5
- package/lib/css/components/uploader.less +70 -84
- package/lib/css/exports/constants-colors.less +14 -0
- package/lib/css/stacks-dynamic.less +0 -1
- package/lib/css/stacks-static.less +3 -2
- package/lib/ts/controllers/s-expandable-control.ts +23 -19
- package/lib/ts/controllers/s-modal.ts +16 -16
- package/lib/ts/controllers/s-navigation-tablist.ts +13 -13
- package/lib/ts/controllers/s-popover.ts +26 -18
- package/lib/ts/controllers/s-table.ts +31 -29
- package/lib/ts/controllers/s-tooltip.ts +62 -23
- package/lib/ts/stacks.ts +8 -4
- package/package.json +17 -17
- package/lib/css/components/banners.less +0 -80
- package/lib/css/components/blank-states.less +0 -26
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createPopper, Placement } from '@popperjs/core';
|
|
2
|
+
import type * as Popper from '@popperjs/core';
|
|
2
3
|
import * as Stacks from "../stacks";
|
|
3
4
|
|
|
4
5
|
type OutsideClickBehavior = "always" | "never" | "if-in-viewport" | "after-dismissal";
|
|
5
6
|
|
|
6
7
|
export abstract class BasePopoverController extends Stacks.StacksController {
|
|
7
|
-
|
|
8
|
-
private popper!: Popper;
|
|
8
|
+
private popper!: Popper.Instance;
|
|
9
9
|
|
|
10
10
|
protected popoverElement!: HTMLElement;
|
|
11
11
|
|
|
@@ -87,6 +87,8 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
87
87
|
this.hide();
|
|
88
88
|
if (this.popper) {
|
|
89
89
|
this.popper.destroy();
|
|
90
|
+
// eslint-disable-next-line
|
|
91
|
+
// @ts-ignore The operand of a 'delete' operator must be optional .ts(2790)
|
|
90
92
|
delete this.popper;
|
|
91
93
|
}
|
|
92
94
|
super.disconnect();
|
|
@@ -105,7 +107,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
105
107
|
show(dispatcher: Event|Element|null = null) {
|
|
106
108
|
if (this.isVisible) { return; }
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
const dispatcherElement = this.getDispatcher(dispatcher);
|
|
109
111
|
|
|
110
112
|
if (this.triggerEvent("show", {
|
|
111
113
|
dispatcher: dispatcherElement
|
|
@@ -115,7 +117,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
115
117
|
this.initializePopper();
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
this.popoverElement
|
|
120
|
+
this.popoverElement.classList.add("is-visible");
|
|
119
121
|
|
|
120
122
|
// ensure the popper has been positioned correctly
|
|
121
123
|
this.scheduleUpdate();
|
|
@@ -129,7 +131,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
129
131
|
hide(dispatcher: Event|Element|null = null) {
|
|
130
132
|
if (!this.isVisible) { return; }
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
const dispatcherElement = this.getDispatcher(dispatcher);
|
|
133
135
|
|
|
134
136
|
if (this.triggerEvent("hide", {
|
|
135
137
|
dispatcher: dispatcherElement
|
|
@@ -140,6 +142,8 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
140
142
|
if (this.popper) {
|
|
141
143
|
// completely destroy the popper on hide; this is in line with Popper.js's performance recommendations
|
|
142
144
|
this.popper.destroy();
|
|
145
|
+
// eslint-disable-next-line
|
|
146
|
+
// @ts-ignore The operand of a 'delete' operator must be optional .ts(2790)
|
|
143
147
|
delete this.popper;
|
|
144
148
|
}
|
|
145
149
|
|
|
@@ -182,7 +186,6 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
182
186
|
* Initializes the Popper for this instance
|
|
183
187
|
*/
|
|
184
188
|
private initializePopper() {
|
|
185
|
-
// @ts-ignore
|
|
186
189
|
this.popper = createPopper(this.referenceElement, this.popoverElement, {
|
|
187
190
|
placement: this.data.get("placement") as Placement || "bottom",
|
|
188
191
|
modifiers: [
|
|
@@ -206,7 +209,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
206
209
|
* Validates the popover settings and attempts to set necessary internal variables
|
|
207
210
|
*/
|
|
208
211
|
private validate() {
|
|
209
|
-
|
|
212
|
+
const referenceSelector = this.data.get("reference-selector");
|
|
210
213
|
|
|
211
214
|
this.referenceElement = <HTMLElement>this.element;
|
|
212
215
|
|
|
@@ -221,7 +224,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
221
224
|
|
|
222
225
|
const popoverId = this.referenceElement.getAttribute(this.popoverSelectorAttribute);
|
|
223
226
|
|
|
224
|
-
|
|
227
|
+
let popoverElement: HTMLElement | null = null;
|
|
225
228
|
|
|
226
229
|
// if the popover is named, attempt to fetch it (and throw an error if it doesn't exist)
|
|
227
230
|
if (popoverId) {
|
|
@@ -264,7 +267,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
264
267
|
*/
|
|
265
268
|
protected scheduleUpdate() {
|
|
266
269
|
if (this.popper && this.isVisible) {
|
|
267
|
-
this.popper.update();
|
|
270
|
+
void this.popper.update();
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
}
|
|
@@ -274,8 +277,8 @@ export class PopoverController extends BasePopoverController {
|
|
|
274
277
|
|
|
275
278
|
protected popoverSelectorAttribute = "aria-controls";
|
|
276
279
|
|
|
277
|
-
private boundHideOnOutsideClick!:
|
|
278
|
-
private boundHideOnEscapePress!:
|
|
280
|
+
private boundHideOnOutsideClick!: (event: MouseEvent) => void;
|
|
281
|
+
private boundHideOnEscapePress!: (event: KeyboardEvent) => void;
|
|
279
282
|
|
|
280
283
|
/**
|
|
281
284
|
* Toggles optional classes and accessibility attributes in addition to BasePopoverController.shown
|
|
@@ -332,7 +335,7 @@ export class PopoverController extends BasePopoverController {
|
|
|
332
335
|
const target = <Node>e.target;
|
|
333
336
|
// check if the document was clicked inside either the reference element or the popover itself
|
|
334
337
|
// note: .contains also returns true if the node itself matches the target element
|
|
335
|
-
if (this.shouldHideOnOutsideClick && !this.referenceElement.contains(target) && !this.popoverElement
|
|
338
|
+
if (this.shouldHideOnOutsideClick && !this.referenceElement.contains(target) && !this.popoverElement.contains(target) && document.body.contains(target)) {
|
|
336
339
|
this.hide(e);
|
|
337
340
|
}
|
|
338
341
|
};
|
|
@@ -349,7 +352,7 @@ export class PopoverController extends BasePopoverController {
|
|
|
349
352
|
|
|
350
353
|
// check if the target was inside the popover element and refocus the triggering element
|
|
351
354
|
// note: .contains also returns true if the node itself matches the target element
|
|
352
|
-
if (this.popoverElement
|
|
355
|
+
if (this.popoverElement.contains(<Node>e.target)) {
|
|
353
356
|
this.referenceElement.focus();
|
|
354
357
|
}
|
|
355
358
|
|
|
@@ -364,8 +367,10 @@ export class PopoverController extends BasePopoverController {
|
|
|
364
367
|
if (!this.data.has("toggle-class")) {
|
|
365
368
|
return;
|
|
366
369
|
}
|
|
367
|
-
|
|
368
|
-
this.data.get("toggle-class")
|
|
370
|
+
|
|
371
|
+
const toggleClass = this.data.get("toggle-class") || "";
|
|
372
|
+
const cl = this.referenceElement.classList;
|
|
373
|
+
toggleClass.split(/\s+/).forEach(function (cls: string) {
|
|
369
374
|
cl.toggle(cls, show);
|
|
370
375
|
});
|
|
371
376
|
}
|
|
@@ -375,7 +380,9 @@ export class PopoverController extends BasePopoverController {
|
|
|
375
380
|
* @param {boolean=} show - A boolean indicating whether this is being triggered by a show or hide.
|
|
376
381
|
*/
|
|
377
382
|
private toggleAccessibilityAttributes(show?: boolean) {
|
|
378
|
-
|
|
383
|
+
const expandedValue = show?.toString() || this.referenceElement.ariaExpanded || "false";
|
|
384
|
+
this.referenceElement.ariaExpanded = expandedValue;
|
|
385
|
+
this.referenceElement.setAttribute("aria-expanded", expandedValue);
|
|
379
386
|
}
|
|
380
387
|
}
|
|
381
388
|
|
|
@@ -453,6 +460,7 @@ export function attachPopover(element: Element, popover: Element | string, optio
|
|
|
453
460
|
}
|
|
454
461
|
|
|
455
462
|
if (typeof popover === 'string') {
|
|
463
|
+
// eslint-disable-next-line no-unsanitized/method
|
|
456
464
|
const elements = document.createRange().createContextualFragment(popover).children;
|
|
457
465
|
if (elements.length !== 1) {
|
|
458
466
|
throw "popover should contain a single element";
|
|
@@ -461,7 +469,7 @@ export function attachPopover(element: Element, popover: Element | string, optio
|
|
|
461
469
|
}
|
|
462
470
|
|
|
463
471
|
const existingId = referenceElement.getAttribute("aria-controls");
|
|
464
|
-
|
|
472
|
+
let popoverId = popover.id;
|
|
465
473
|
|
|
466
474
|
if (!popover.classList.contains('s-popover')) {
|
|
467
475
|
throw `popover should have the "s-popover" class but had class="${popover.className}"`;
|
|
@@ -557,7 +565,7 @@ function getPopover(element: Element): GetPopoverResult {
|
|
|
557
565
|
* @param include Whether to add the controllerName value
|
|
558
566
|
*/
|
|
559
567
|
function toggleController(el: Element, controllerName: string, include: boolean) {
|
|
560
|
-
|
|
568
|
+
const controllers = new Set(el.getAttribute('data-controller')?.split(/\s+/));
|
|
561
569
|
if (include) {
|
|
562
570
|
controllers.add(controllerName);
|
|
563
571
|
} else {
|
|
@@ -9,14 +9,15 @@ export class TableController extends Stacks.StacksController {
|
|
|
9
9
|
if (["asc", "desc", "none"].indexOf(direction) < 0) {
|
|
10
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
|
+
const controller = this;
|
|
13
14
|
this.columnTargets.forEach(function (target) {
|
|
14
|
-
|
|
15
|
+
const isCurrrent = target === headElem;
|
|
15
16
|
|
|
16
17
|
target.classList.toggle("is-sorted", isCurrrent && direction !== "none");
|
|
17
18
|
|
|
18
19
|
target.querySelectorAll(".js-sorting-indicator").forEach(function (icon) {
|
|
19
|
-
|
|
20
|
+
const visible = isCurrrent ? direction : "none";
|
|
20
21
|
icon.classList.toggle("d-none", !icon.classList.contains("js-sorting-indicator-" + visible));
|
|
21
22
|
});
|
|
22
23
|
|
|
@@ -29,16 +30,17 @@ export class TableController extends Stacks.StacksController {
|
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
sort(evt: Event) {
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
34
|
+
const controller = this;
|
|
35
|
+
const colHead = evt.currentTarget;
|
|
34
36
|
if (!(colHead instanceof HTMLTableCellElement)) {
|
|
35
37
|
throw "invalid event target";
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
const table = this.element as HTMLTableElement;
|
|
40
|
+
const tbody = table.tBodies[0];
|
|
39
41
|
|
|
40
42
|
// the column slot number of the clicked header
|
|
41
|
-
|
|
43
|
+
const colno = getCellSlot(colHead);
|
|
42
44
|
|
|
43
45
|
if (colno < 0) { // this shouldn't happen if the clicked element is actually a column head
|
|
44
46
|
return;
|
|
@@ -46,23 +48,23 @@ export class TableController extends Stacks.StacksController {
|
|
|
46
48
|
|
|
47
49
|
// an index of the <tbody>, so we can find out for each row which <td> element is
|
|
48
50
|
// in the same column slot as the header
|
|
49
|
-
|
|
51
|
+
const slotIndex = buildIndex(tbody);
|
|
50
52
|
|
|
51
53
|
// the default behavior when clicking a header is to sort by this column in ascending
|
|
52
54
|
// direction, *unless* it is already sorted that way
|
|
53
|
-
|
|
55
|
+
const direction = this.getElementData(colHead, "sort-direction") === "asc" ? -1 : 1;
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
const rows = Array.from(table.tBodies[0].rows);
|
|
56
58
|
|
|
57
59
|
// if this is still false after traversing the data, that means all values are integers (or empty)
|
|
58
60
|
// and thus we'll sort numerically.
|
|
59
|
-
|
|
61
|
+
let anyNonInt = false;
|
|
60
62
|
|
|
61
63
|
// data will be a list of tuples [value, rowNum], where value is what we're sorting by
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
const data: [string | number, number][] = [];
|
|
65
|
+
let firstBottomRow: HTMLTableRowElement;
|
|
64
66
|
rows.forEach(function (row, index) {
|
|
65
|
-
|
|
67
|
+
const force = controller.getElementData(row, "sort-to");
|
|
66
68
|
if (force === "top") {
|
|
67
69
|
return; // rows not added to the list will automatically end up at the top
|
|
68
70
|
} else if (force === "bottom") {
|
|
@@ -71,7 +73,7 @@ export class TableController extends Stacks.StacksController {
|
|
|
71
73
|
}
|
|
72
74
|
return;
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
const cell = slotIndex[index][colno];
|
|
75
77
|
if (!cell) {
|
|
76
78
|
data.push(["", index]);
|
|
77
79
|
return;
|
|
@@ -79,10 +81,10 @@ export class TableController extends Stacks.StacksController {
|
|
|
79
81
|
|
|
80
82
|
// unless the to-be-sorted-by value is explicitly provided on the element via this attribute,
|
|
81
83
|
// the value we're using is the cell's text, trimmed of any whitespace
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
const explicit = controller.getElementData(cell, "sort-val");
|
|
85
|
+
const d = typeof explicit === "string" ? explicit : cell.textContent!.trim();
|
|
84
86
|
|
|
85
|
-
if ((d !== "") && (parseInt(d, 10)
|
|
87
|
+
if ((d !== "") && (`${parseInt(d, 10)}` !== d)) {
|
|
86
88
|
anyNonInt = true;
|
|
87
89
|
}
|
|
88
90
|
data.push([d, index]);
|
|
@@ -114,7 +116,7 @@ export class TableController extends Stacks.StacksController {
|
|
|
114
116
|
|
|
115
117
|
// this is the actual reordering of the table rows
|
|
116
118
|
data.forEach(function (tup) {
|
|
117
|
-
|
|
119
|
+
const row = rows[tup[1]];
|
|
118
120
|
row.parentElement!.removeChild(row);
|
|
119
121
|
if (firstBottomRow) {
|
|
120
122
|
tbody.insertBefore(row, firstBottomRow);
|
|
@@ -165,29 +167,29 @@ function getCellSlot(cell: HTMLTableCellElement): number {
|
|
|
165
167
|
// If the second argument is given, it's a <td> or <th> that we're trying to find, and the algorithm
|
|
166
168
|
// stops as soon as it has found it and the function returns its slot number.
|
|
167
169
|
function buildIndexOrGetCellSlot(section: HTMLTableSectionElement, findCell?: HTMLTableCellElement) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const index = [];
|
|
171
|
+
let curRow = section.children[0];
|
|
170
172
|
|
|
171
173
|
// the elements of these two arrays are synchronized; the first array contains table cell elements,
|
|
172
174
|
// the second one contains a number that indicates for how many more rows this elements will
|
|
173
175
|
// exist (i.e. the value is initially one less than the cell's rowspan, and will be decreased for each row)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
const growing: HTMLTableCellElement[] = [];
|
|
177
|
+
const growingRowsLeft: number[] = [];
|
|
176
178
|
|
|
177
179
|
// continue while we have actual <tr>'s left *or* we still have rowspan'ed elements that aren't done
|
|
178
180
|
while (curRow || growingRowsLeft.some(function (e) { return e !== 0; })) {
|
|
179
|
-
|
|
181
|
+
const curIndexRow: HTMLTableCellElement[] = [];
|
|
180
182
|
index.push(curIndexRow);
|
|
181
183
|
|
|
182
|
-
|
|
184
|
+
let curSlot = 0;
|
|
183
185
|
if (curRow) {
|
|
184
|
-
for (
|
|
186
|
+
for (let curCellInd = 0; curCellInd < curRow.children.length; curCellInd++) {
|
|
185
187
|
while (growingRowsLeft[curSlot]) {
|
|
186
188
|
growingRowsLeft[curSlot]--;
|
|
187
189
|
curIndexRow[curSlot] = growing[curSlot];
|
|
188
190
|
curSlot++;
|
|
189
191
|
}
|
|
190
|
-
|
|
192
|
+
const cell = curRow.children[curCellInd];
|
|
191
193
|
if (!(cell instanceof HTMLTableCellElement)) {
|
|
192
194
|
throw "invalid table"
|
|
193
195
|
}
|
|
@@ -197,7 +199,7 @@ function buildIndexOrGetCellSlot(section: HTMLTableSectionElement, findCell?: HT
|
|
|
197
199
|
if (cell === findCell) {
|
|
198
200
|
return curSlot;
|
|
199
201
|
}
|
|
200
|
-
|
|
202
|
+
const nextFreeSlot = curSlot + cell.colSpan;
|
|
201
203
|
for (; curSlot < nextFreeSlot; curSlot++) {
|
|
202
204
|
growingRowsLeft[curSlot] = cell.rowSpan - 1; // if any of these is already growing, the table is broken -- no guarantees of anything
|
|
203
205
|
growing[curSlot] = cell;
|
|
@@ -10,10 +10,12 @@ export class TooltipController extends BasePopoverController {
|
|
|
10
10
|
|
|
11
11
|
protected popoverSelectorAttribute = "aria-describedby";
|
|
12
12
|
|
|
13
|
-
private boundScheduleShow!:
|
|
14
|
-
private boundHide!:
|
|
15
|
-
private boundHideIfWithin!:
|
|
16
|
-
private
|
|
13
|
+
private boundScheduleShow!: () => void;
|
|
14
|
+
private boundHide!: () => void;
|
|
15
|
+
private boundHideIfWithin!: () => void;
|
|
16
|
+
private boundHideOnEscapeKeyEvent!: () => void;
|
|
17
|
+
private boundClearActiveTimeout!: () => void;
|
|
18
|
+
private activeTimeout!: number;
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Binds mouseover and mouseout events in addition to BasePopoverController.connect
|
|
@@ -27,12 +29,14 @@ export class TooltipController extends BasePopoverController {
|
|
|
27
29
|
if (window.matchMedia("(hover: hover)").matches) {
|
|
28
30
|
this.bindMouseEvents();
|
|
29
31
|
}
|
|
32
|
+
this.bindKeyboardEvents();
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
36
|
* Unbinds mouse events in addition to BasePopoverController.disconnect
|
|
34
37
|
*/
|
|
35
38
|
disconnect() {
|
|
39
|
+
this.unbindKeyboardEvents();
|
|
36
40
|
this.unbindMouseEvents();
|
|
37
41
|
super.disconnect();
|
|
38
42
|
}
|
|
@@ -43,7 +47,7 @@ export class TooltipController extends BasePopoverController {
|
|
|
43
47
|
*/
|
|
44
48
|
show(dispatcher: Event|Element|null = null) {
|
|
45
49
|
// check and see if this controller coexists with a popover
|
|
46
|
-
|
|
50
|
+
const controller = Stacks.application.getControllerForElementAndIdentifier(this.element, "s-popover");
|
|
47
51
|
|
|
48
52
|
// if the controller exists and already has a visible popover, don't show the tooltip
|
|
49
53
|
if (controller && (<PopoverController>controller).isVisible) {
|
|
@@ -64,11 +68,16 @@ export class TooltipController extends BasePopoverController {
|
|
|
64
68
|
/**
|
|
65
69
|
* Cancels the scheduled tooltip popover display and hides it if already displayed
|
|
66
70
|
*/
|
|
67
|
-
|
|
71
|
+
scheduleHide(dispatcher: Event | Element | null = null) {
|
|
68
72
|
window.clearTimeout(this.activeTimeout);
|
|
69
|
-
this.activeTimeout =
|
|
73
|
+
this.activeTimeout = window.setTimeout(() => super.hide(dispatcher), 100);
|
|
74
|
+
}
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Cancels the activeTimeout
|
|
78
|
+
*/
|
|
79
|
+
clearActiveTimeout() {
|
|
80
|
+
clearTimeout(this.activeTimeout);
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
/**
|
|
@@ -76,13 +85,14 @@ export class TooltipController extends BasePopoverController {
|
|
|
76
85
|
*/
|
|
77
86
|
applyTitleAttributes() {
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
let content: Node;
|
|
80
89
|
|
|
81
|
-
|
|
90
|
+
const htmlTitle = this.data.get("html-title");
|
|
82
91
|
if (htmlTitle) {
|
|
92
|
+
// eslint-disable-next-line no-unsanitized/method
|
|
83
93
|
content = document.createRange().createContextualFragment(htmlTitle);
|
|
84
94
|
} else {
|
|
85
|
-
|
|
95
|
+
const plainTitle = this.element.getAttribute("title");
|
|
86
96
|
if (plainTitle) {
|
|
87
97
|
content = document.createTextNode(plainTitle);
|
|
88
98
|
} else {
|
|
@@ -93,21 +103,20 @@ export class TooltipController extends BasePopoverController {
|
|
|
93
103
|
this.data.delete("html-title");
|
|
94
104
|
this.element.removeAttribute("title");
|
|
95
105
|
|
|
96
|
-
|
|
106
|
+
let popoverId = this.element.getAttribute("aria-describedby");
|
|
97
107
|
if (!popoverId) {
|
|
98
108
|
popoverId = TooltipController.generateId();
|
|
99
109
|
this.element.setAttribute("aria-describedby", popoverId);
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
|
|
112
|
+
let popover = document.getElementById(popoverId);
|
|
103
113
|
if (!popover) {
|
|
104
114
|
popover = document.createElement("div");
|
|
105
115
|
popover.id = popoverId;
|
|
106
|
-
popover.className = "s-popover s-popover__tooltip
|
|
107
|
-
popover.setAttribute("aria-hidden", "true");
|
|
116
|
+
popover.className = "s-popover s-popover__tooltip";
|
|
108
117
|
popover.setAttribute("role", "tooltip");
|
|
109
118
|
|
|
110
|
-
|
|
119
|
+
const parentNode = this.element.parentNode;
|
|
111
120
|
if (parentNode) {
|
|
112
121
|
// insertBefore inserts at end if element.nextSibling is null.
|
|
113
122
|
parentNode.insertBefore(popover, this.element.nextSibling);
|
|
@@ -165,21 +174,49 @@ export class TooltipController extends BasePopoverController {
|
|
|
165
174
|
*/
|
|
166
175
|
private hideIfWithin(event: Event) {
|
|
167
176
|
if ((<Element>event.target!).contains(this.referenceElement)) {
|
|
168
|
-
this.
|
|
177
|
+
this.scheduleHide();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private hideOnEscapeKeyEvent(event: KeyboardEvent) {
|
|
182
|
+
if (event.key === "Escape") {
|
|
183
|
+
this.scheduleHide();
|
|
169
184
|
}
|
|
170
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Binds mouse events to show/hide on reference element hover
|
|
188
|
+
*/
|
|
189
|
+
private bindKeyboardEvents() {
|
|
190
|
+
this.boundScheduleShow = this.boundScheduleShow || this.scheduleShow.bind(this);
|
|
191
|
+
this.boundHide = this.boundHide || this.scheduleHide.bind(this);
|
|
192
|
+
this.boundHideOnEscapeKeyEvent = this.boundHideOnEscapeKeyEvent || this.hideOnEscapeKeyEvent.bind(this);
|
|
193
|
+
|
|
194
|
+
this.referenceElement.addEventListener("focus", this.boundScheduleShow);
|
|
195
|
+
this.referenceElement.addEventListener("blur", this.boundHide);
|
|
196
|
+
document.addEventListener("keyup", this.boundHideOnEscapeKeyEvent);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Unbinds all mouse events
|
|
200
|
+
*/
|
|
201
|
+
private unbindKeyboardEvents() {
|
|
202
|
+
this.referenceElement.removeEventListener("focus", this.boundScheduleShow);
|
|
203
|
+
this.referenceElement.removeEventListener("blur", this.boundHide);
|
|
204
|
+
document.removeEventListener("keyup", this.boundHideOnEscapeKeyEvent);
|
|
205
|
+
}
|
|
206
|
+
|
|
171
207
|
|
|
172
208
|
/**
|
|
173
209
|
* Binds mouse events to show/hide on reference element hover
|
|
174
210
|
*/
|
|
175
211
|
private bindMouseEvents() {
|
|
176
212
|
this.boundScheduleShow = this.boundScheduleShow || this.scheduleShow.bind(this);
|
|
177
|
-
this.boundHide = this.boundHide || this.
|
|
213
|
+
this.boundHide = this.boundHide || this.scheduleHide.bind(this);
|
|
214
|
+
this.boundClearActiveTimeout = this.boundClearActiveTimeout || this.clearActiveTimeout.bind(this);
|
|
178
215
|
|
|
179
216
|
this.referenceElement.addEventListener("mouseover", this.boundScheduleShow);
|
|
180
217
|
this.referenceElement.addEventListener("mouseout", this.boundHide);
|
|
181
|
-
this.
|
|
182
|
-
this.
|
|
218
|
+
this.popoverElement.addEventListener("mouseover", this.boundClearActiveTimeout);
|
|
219
|
+
this.popoverElement.addEventListener("mouseout", this.boundHide);
|
|
183
220
|
}
|
|
184
221
|
|
|
185
222
|
/**
|
|
@@ -190,6 +227,8 @@ export class TooltipController extends BasePopoverController {
|
|
|
190
227
|
this.referenceElement.removeEventListener("mouseout", this.boundHide);
|
|
191
228
|
this.referenceElement.removeEventListener("focus", this.boundScheduleShow);
|
|
192
229
|
this.referenceElement.removeEventListener("blur", this.boundHide);
|
|
230
|
+
this.popoverElement.removeEventListener("mouseover", this.boundClearActiveTimeout);
|
|
231
|
+
this.popoverElement.removeEventListener("mouseout", this.boundHide);
|
|
193
232
|
}
|
|
194
233
|
|
|
195
234
|
/**
|
|
@@ -231,16 +270,16 @@ export function setTooltipText(element: Element, text: string, options?: Tooltip
|
|
|
231
270
|
* @param options Options for rendering the tooltip.
|
|
232
271
|
*/
|
|
233
272
|
function applyOptionsAndTitleAttributes(element: Element, options?: TooltipOptions) {
|
|
234
|
-
|
|
235
273
|
if (options && options.placement) {
|
|
236
274
|
element.setAttribute("data-s-tooltip-placement", options.placement);
|
|
237
275
|
}
|
|
238
276
|
|
|
239
|
-
|
|
277
|
+
const controller = <TooltipController>Stacks.application.getControllerForElementAndIdentifier(element, "s-tooltip");
|
|
240
278
|
|
|
241
279
|
if (controller) {
|
|
242
280
|
controller.applyTitleAttributes();
|
|
243
281
|
} else {
|
|
244
|
-
|
|
282
|
+
const dataController = element.getAttribute("data-controller");
|
|
283
|
+
element.setAttribute("data-controller", `${dataController ? dataController : ""} s-tooltip`);
|
|
245
284
|
}
|
|
246
285
|
}
|
package/lib/ts/stacks.ts
CHANGED
|
@@ -9,7 +9,7 @@ export class StacksApplication extends Stimulus.Application {
|
|
|
9
9
|
const definitions = Array.isArray(head) ? head : [head, ...rest];
|
|
10
10
|
|
|
11
11
|
for (const definition of definitions) {
|
|
12
|
-
|
|
12
|
+
const hasPrefix = /^s-/.test(definition.identifier);
|
|
13
13
|
if (StacksApplication._initializing && !hasPrefix) {
|
|
14
14
|
throw "Stacks-created Stimulus controller names must start with \"s-\".";
|
|
15
15
|
}
|
|
@@ -23,6 +23,7 @@ export class StacksApplication extends Stimulus.Application {
|
|
|
23
23
|
|
|
24
24
|
static start(element?: Element, schema?: Stimulus.Schema): StacksApplication {
|
|
25
25
|
const application = new StacksApplication(element, schema);
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
26
27
|
application.start();
|
|
27
28
|
return application;
|
|
28
29
|
}
|
|
@@ -46,13 +47,14 @@ export class StacksController extends Stimulus.Controller {
|
|
|
46
47
|
};
|
|
47
48
|
protected triggerEvent<T>(eventName: string, detail?: T, optionalElement?: Element) {
|
|
48
49
|
const namespacedName = this.identifier + ":" + eventName;
|
|
49
|
-
|
|
50
|
+
let event : CustomEvent<T>;
|
|
50
51
|
try {
|
|
51
52
|
event = new CustomEvent(namespacedName, {bubbles: true, cancelable: true, detail: detail});
|
|
52
53
|
} catch (ex) {
|
|
53
54
|
// Internet Explorer
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
54
56
|
event = document.createEvent("CustomEvent");
|
|
55
|
-
event.initCustomEvent(namespacedName, true, true, detail
|
|
57
|
+
event.initCustomEvent(namespacedName, true, true, detail);
|
|
56
58
|
}
|
|
57
59
|
(optionalElement || this.element).dispatchEvent(event);
|
|
58
60
|
return event;
|
|
@@ -66,11 +68,13 @@ export interface ControllerDefinition {
|
|
|
66
68
|
targets?: string[];
|
|
67
69
|
}
|
|
68
70
|
export function createController(controllerDefinition: ControllerDefinition): typeof StacksController {
|
|
71
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
69
72
|
const Controller = controllerDefinition.hasOwnProperty("targets")
|
|
70
73
|
? class Controller extends StacksController { static targets = controllerDefinition.targets! }
|
|
71
74
|
: class Controller extends StacksController {};
|
|
72
75
|
|
|
73
|
-
for (
|
|
76
|
+
for (const prop in controllerDefinition) {
|
|
77
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
74
78
|
if (prop !== "targets" && controllerDefinition.hasOwnProperty(prop)) {
|
|
75
79
|
Object.defineProperty(
|
|
76
80
|
Controller.prototype,
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/StackExchange/Stacks.git"
|
|
7
7
|
},
|
|
8
|
-
"version": "1.3.
|
|
8
|
+
"version": "1.3.1",
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"lib"
|
|
@@ -30,27 +30,27 @@
|
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@popperjs/core": "^2.11.
|
|
33
|
+
"@popperjs/core": "^2.11.6",
|
|
34
34
|
"stimulus": "^2.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@11ty/eleventy": "^1.0.
|
|
37
|
+
"@11ty/eleventy": "^1.0.2",
|
|
38
38
|
"@highlightjs/cdn-assets": "^11.6.0",
|
|
39
|
-
"@stackoverflow/stacks-editor": "^0.
|
|
40
|
-
"@stackoverflow/stacks-icons": "^3.0.
|
|
41
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
42
|
-
"@typescript-eslint/parser": "^5.
|
|
43
|
-
"backstopjs": "^6.1.
|
|
39
|
+
"@stackoverflow/stacks-editor": "^0.7.0",
|
|
40
|
+
"@stackoverflow/stacks-icons": "^3.0.2",
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
|
42
|
+
"@typescript-eslint/parser": "^5.35.1",
|
|
43
|
+
"backstopjs": "^6.1.1",
|
|
44
44
|
"concurrently": "^7.3.0",
|
|
45
45
|
"css-loader": "^6.7.1",
|
|
46
|
-
"cssnano": "^5.1.
|
|
46
|
+
"cssnano": "^5.1.13",
|
|
47
47
|
"docsearch.js": "^2.6.3",
|
|
48
48
|
"eleventy-plugin-highlightjs": "^1.1.0",
|
|
49
49
|
"eleventy-plugin-nesting-toc": "^1.3.0",
|
|
50
|
-
"eslint": "^8.
|
|
50
|
+
"eslint": "^8.23.0",
|
|
51
51
|
"eslint-config-prettier": "^8.5.0",
|
|
52
52
|
"eslint-plugin-no-unsanitized": "^4.0.1",
|
|
53
|
-
"jquery": "^3.6.
|
|
53
|
+
"jquery": "^3.6.1",
|
|
54
54
|
"less-loader": "^11.0.0",
|
|
55
55
|
"list.js": "^2.3.1",
|
|
56
56
|
"markdown-it": "^13.0.1",
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"postcss-less": "^6.0.0",
|
|
59
59
|
"postcss-loader": "^7.0.1",
|
|
60
60
|
"prettier": "^2.7.1",
|
|
61
|
-
"stylelint": "^14.
|
|
62
|
-
"stylelint-config-recommended": "^
|
|
63
|
-
"stylelint-config-standard": "^
|
|
64
|
-
"terser-webpack-plugin": "^5.3.
|
|
61
|
+
"stylelint": "^14.11.0",
|
|
62
|
+
"stylelint-config-recommended": "^9.0.0",
|
|
63
|
+
"stylelint-config-standard": "^28.0.0",
|
|
64
|
+
"terser-webpack-plugin": "^5.3.5",
|
|
65
65
|
"ts-loader": "^9.3.1",
|
|
66
|
-
"typescript": "^4.
|
|
67
|
-
"webpack": "^5.
|
|
66
|
+
"typescript": "^4.8.2",
|
|
67
|
+
"webpack": "^5.74.0",
|
|
68
68
|
"webpack-cli": "^4.10.0",
|
|
69
69
|
"webpack-merge": "^5.8.0"
|
|
70
70
|
},
|