playbook_ui 16.2.0.pre.alpha.play284314650 → 16.2.0.pre.alpha.play284314659
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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_collapsible/index.js +16 -4
- data/app/pb_kits/playbook/pb_dialog/index.js +45 -5
- data/app/pb_kits/playbook/pb_dropdown/index.js +68 -13
- data/app/pb_kits/playbook/pb_dropdown/keyboard_accessibility.js +19 -3
- data/app/pb_kits/playbook/pb_enhanced_element/element_observer.ts +1 -1
- data/app/pb_kits/playbook/pb_enhanced_element/index.ts +2 -1
- data/app/pb_kits/playbook/pb_kit_registry/index.ts +180 -0
- data/app/pb_kits/playbook/pb_tooltip/index.js +60 -15
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +105 -3
- data/dist/chunks/{_pb_line_graph-BSLb5VXP.js → _pb_line_graph-BGY7jEks.js} +1 -1
- data/dist/chunks/{_typeahead-COVN8XN7.js → _typeahead-tG1K5JPP.js} +1 -1
- data/dist/chunks/{globalProps-DyTB8IdV.js → globalProps-CK2YuA9O.js} +1 -1
- data/dist/chunks/{lib-9wz3x5jl.js → lib-DspaUdlc.js} +1 -1
- data/dist/chunks/vendor.js +5 -5
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/lib/playbook/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f12b618cfe899125e5b51f0cb0615e124d3df5a1396892c3114d4c82ce26703f
|
|
4
|
+
data.tar.gz: 5dc0bba31972163cb2c9c8b7ee23f25346f104d948fe8d863c3f7761c2984bb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac99b0e45fa7fcec7af01e2d0d788a2052f69fc8bb2200b0bf7c8705ee0f94fecb253abd24cd987dc8d687c4b0e41dacd230794462040f811fa2cc2f6a94e5d3
|
|
7
|
+
data.tar.gz: 36c22534c654f7121e1b4e2652a70e39a06f89ab41d0795634db197a56b4b1490b13d62acfe3252d24ed250786ed43da7bdd943701b184eb6af3977835a446ec
|
|
@@ -11,9 +11,11 @@ export default class PbCollapsible extends PbEnhancedElement {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
connect() {
|
|
14
|
-
this.
|
|
14
|
+
this.clickHandler = () => {
|
|
15
15
|
this.toggleElement(this.target)
|
|
16
|
-
}
|
|
16
|
+
}
|
|
17
|
+
this.element.addEventListener('click', this.clickHandler)
|
|
18
|
+
|
|
17
19
|
// Check the initial state of the collapsible content and set the arrow accordingly
|
|
18
20
|
if (this.target.classList.contains('is-visible')) {
|
|
19
21
|
this.displayUpArrow()
|
|
@@ -21,9 +23,19 @@ export default class PbCollapsible extends PbEnhancedElement {
|
|
|
21
23
|
this.displayDownArrow()
|
|
22
24
|
}
|
|
23
25
|
// Listen for a custom event to toggle the collapsible
|
|
24
|
-
|
|
26
|
+
this.customEventHandler = () => {
|
|
25
27
|
this.toggleElement(this.target)
|
|
26
|
-
}
|
|
28
|
+
}
|
|
29
|
+
document.addEventListener(`${this.target.id}`, this.customEventHandler)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
disconnect() {
|
|
33
|
+
if (this.clickHandler) {
|
|
34
|
+
this.element.removeEventListener('click', this.clickHandler)
|
|
35
|
+
}
|
|
36
|
+
if (this.customEventHandler && this.target) {
|
|
37
|
+
document.removeEventListener(`${this.target.id}`, this.customEventHandler)
|
|
38
|
+
}
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
get target() {
|
|
@@ -8,8 +8,16 @@ export default class PbDialog extends PbEnhancedElement {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
connect() {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Store references to this instance's specific elements
|
|
12
|
+
this.dialogElement = this.element.querySelector(".pb_dialog_rails")
|
|
13
|
+
this.dialogId = this.dialogElement?.id
|
|
14
|
+
this.managedTriggers = new Set()
|
|
15
|
+
|
|
16
|
+
this.domContentLoadedHandler = () => this.setupDialog()
|
|
17
|
+
this.turboFrameLoadHandler = () => this.setupDialog()
|
|
18
|
+
|
|
19
|
+
window.addEventListener("DOMContentLoaded", this.domContentLoadedHandler)
|
|
20
|
+
window.addEventListener("turbo:frame-load", this.turboFrameLoadHandler)
|
|
13
21
|
|
|
14
22
|
// Code for custom_event_type setup (can take multiple events in a string separated by commas)
|
|
15
23
|
const customEventTypeString = this.element.dataset.customEventType
|
|
@@ -24,11 +32,38 @@ export default class PbDialog extends PbEnhancedElement {
|
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
disconnect() {
|
|
35
|
+
// Clean up window event listeners
|
|
36
|
+
if (this.domContentLoadedHandler) {
|
|
37
|
+
window.removeEventListener("DOMContentLoaded", this.domContentLoadedHandler)
|
|
38
|
+
}
|
|
39
|
+
if (this.turboFrameLoadHandler) {
|
|
40
|
+
window.removeEventListener("turbo:frame-load", this.turboFrameLoadHandler)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Clean up custom event listeners
|
|
27
44
|
if (this.customEventTypes && Array.isArray(this.customEventTypes)) {
|
|
28
45
|
this.customEventTypes.forEach(eventType => {
|
|
29
46
|
window.removeEventListener(eventType, this.handleCustomEvent)
|
|
30
47
|
})
|
|
31
48
|
}
|
|
49
|
+
|
|
50
|
+
// Clean up only the triggers that this instance managed
|
|
51
|
+
this.managedTriggers.forEach((trigger) => {
|
|
52
|
+
if (trigger._openDialogClickHandler) {
|
|
53
|
+
trigger.removeEventListener("click", trigger._openDialogClickHandler)
|
|
54
|
+
delete trigger._openDialogClickHandler
|
|
55
|
+
}
|
|
56
|
+
if (trigger._closeDialogClickHandler) {
|
|
57
|
+
trigger.removeEventListener("click", trigger._closeDialogClickHandler)
|
|
58
|
+
delete trigger._closeDialogClickHandler
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Clean up this dialog's outside click handler
|
|
63
|
+
if (this.dialogElement && this.dialogElement._outsideClickHandler) {
|
|
64
|
+
this.dialogElement.removeEventListener("mousedown", this.dialogElement._outsideClickHandler)
|
|
65
|
+
delete this.dialogElement._outsideClickHandler
|
|
66
|
+
}
|
|
32
67
|
}
|
|
33
68
|
|
|
34
69
|
handleCustomEvent = (event) => {
|
|
@@ -88,9 +123,12 @@ export default class PbDialog extends PbEnhancedElement {
|
|
|
88
123
|
}
|
|
89
124
|
|
|
90
125
|
setupDialog() {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
126
|
+
// Only set up triggers and dialogs that belong to this instance
|
|
127
|
+
if (!this.dialogId) return
|
|
128
|
+
|
|
129
|
+
const openTrigger = document.querySelectorAll(`[data-open-dialog="${this.dialogId}"]`);
|
|
130
|
+
const closeTrigger = document.querySelectorAll(`[data-close-dialog="${this.dialogId}"]`);
|
|
131
|
+
const dialogs = this.dialogElement ? [this.dialogElement] : []
|
|
94
132
|
|
|
95
133
|
const loadingButton = document.querySelector('[data-disable-with="Loading"]');
|
|
96
134
|
if (loadingButton && !loadingButton.dataset.listenerAttached) {
|
|
@@ -126,6 +164,7 @@ export default class PbDialog extends PbEnhancedElement {
|
|
|
126
164
|
};
|
|
127
165
|
|
|
128
166
|
open.addEventListener("click", open._openDialogClickHandler)
|
|
167
|
+
this.managedTriggers.add(open)
|
|
129
168
|
});
|
|
130
169
|
|
|
131
170
|
closeTrigger.forEach((close) => {
|
|
@@ -139,6 +178,7 @@ export default class PbDialog extends PbEnhancedElement {
|
|
|
139
178
|
};
|
|
140
179
|
|
|
141
180
|
close.addEventListener("click", close._closeDialogClickHandler)
|
|
181
|
+
this.managedTriggers.add(close)
|
|
142
182
|
});
|
|
143
183
|
|
|
144
184
|
// Close dialog box on outside click
|
|
@@ -54,14 +54,66 @@ export default class PbDropdown extends PbEnhancedElement {
|
|
|
54
54
|
this.isClearable = this.element.dataset.pbDropdownClearable !== "false";
|
|
55
55
|
if (this.clearBtn) {
|
|
56
56
|
this.clearBtn.style.display = "none";
|
|
57
|
-
this.
|
|
57
|
+
this.clearBtnHandler = (e) => {
|
|
58
58
|
e.stopPropagation();
|
|
59
59
|
this.clearSelection();
|
|
60
|
-
}
|
|
60
|
+
}
|
|
61
|
+
this.clearBtn.addEventListener("click", this.clearBtnHandler);
|
|
61
62
|
}
|
|
62
63
|
this.updateClearButton();
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
disconnect() {
|
|
67
|
+
// Clean up stored instance reference
|
|
68
|
+
if (this.element._pbDropdownInstance === this) {
|
|
69
|
+
delete this.element._pbDropdownInstance
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Clean up keyboard handler
|
|
73
|
+
if (this.keyboardHandler && typeof this.keyboardHandler.disconnect === 'function') {
|
|
74
|
+
this.keyboardHandler.disconnect()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Clean up custom trigger click listener
|
|
78
|
+
if (this.customTriggerClickHandler) {
|
|
79
|
+
const customTrigger = this.element.querySelector(CUSTOM_DISPLAY_SELECTOR) || this.element
|
|
80
|
+
customTrigger.removeEventListener('click', this.customTriggerClickHandler)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Clean up target click listener
|
|
84
|
+
if (this.handleOptionClickBound) {
|
|
85
|
+
this.target.removeEventListener('click', this.handleOptionClickBound)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Clean up document click listener
|
|
89
|
+
if (this.handleDocumentClickBound) {
|
|
90
|
+
document.removeEventListener('click', this.handleDocumentClickBound, true)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Clean up search bar listener
|
|
94
|
+
if (this.searchBar && this.searchBarHandler) {
|
|
95
|
+
this.searchBar.removeEventListener('input', this.searchBarHandler)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Clean up search input listeners
|
|
99
|
+
if (this.searchInput) {
|
|
100
|
+
if (this.searchInputFocusHandler) {
|
|
101
|
+
const trigger = this.element.querySelector(TRIGGER_SELECTOR)
|
|
102
|
+
if (trigger) {
|
|
103
|
+
trigger.removeEventListener('click', this.searchInputFocusHandler)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (this.searchInputHandler) {
|
|
107
|
+
this.searchInput.removeEventListener('input', this.searchInputHandler)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Clean up clear button listener
|
|
112
|
+
if (this.clearBtn && this.clearBtnHandler) {
|
|
113
|
+
this.clearBtn.removeEventListener('click', this.clearBtnHandler)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
65
117
|
updateClearButton() {
|
|
66
118
|
if (!this.clearBtn) return;
|
|
67
119
|
if (!this.isClearable) {
|
|
@@ -78,7 +130,7 @@ export default class PbDropdown extends PbEnhancedElement {
|
|
|
78
130
|
bindEventListeners() {
|
|
79
131
|
const customTrigger =
|
|
80
132
|
this.element.querySelector(CUSTOM_DISPLAY_SELECTOR) || this.element;
|
|
81
|
-
|
|
133
|
+
this.customTriggerClickHandler = (e) => {
|
|
82
134
|
const label = e.target.closest(LABEL_SELECTOR);
|
|
83
135
|
if (label && label.htmlFor) {
|
|
84
136
|
const trigger = this.element.querySelector(
|
|
@@ -89,12 +141,16 @@ export default class PbDropdown extends PbEnhancedElement {
|
|
|
89
141
|
}
|
|
90
142
|
}
|
|
91
143
|
this.toggleElement(this.target);
|
|
92
|
-
}
|
|
144
|
+
}
|
|
145
|
+
customTrigger.addEventListener("click", this.customTriggerClickHandler);
|
|
93
146
|
|
|
94
|
-
this.
|
|
147
|
+
this.handleOptionClickBound = this.handleOptionClick.bind(this)
|
|
148
|
+
this.target.addEventListener("click", this.handleOptionClickBound);
|
|
149
|
+
|
|
150
|
+
this.handleDocumentClickBound = this.handleDocumentClick.bind(this)
|
|
95
151
|
document.addEventListener(
|
|
96
152
|
"click",
|
|
97
|
-
this.
|
|
153
|
+
this.handleDocumentClickBound,
|
|
98
154
|
true,
|
|
99
155
|
);
|
|
100
156
|
}
|
|
@@ -103,9 +159,8 @@ export default class PbDropdown extends PbEnhancedElement {
|
|
|
103
159
|
this.searchBar = this.element.querySelector(SEARCH_BAR_SELECTOR);
|
|
104
160
|
if (!this.searchBar) return;
|
|
105
161
|
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
);
|
|
162
|
+
this.searchBarHandler = (e) => this.handleSearch(e.target.value)
|
|
163
|
+
this.searchBar.addEventListener('input', this.searchBarHandler);
|
|
109
164
|
}
|
|
110
165
|
|
|
111
166
|
bindSearchInput() {
|
|
@@ -113,14 +168,14 @@ export default class PbDropdown extends PbEnhancedElement {
|
|
|
113
168
|
if (!this.searchInput) return;
|
|
114
169
|
|
|
115
170
|
// Focus the input when anyone clicks the wrapper
|
|
171
|
+
this.searchInputFocusHandler = () => this.searchInput.focus()
|
|
116
172
|
this.element
|
|
117
173
|
.querySelector(TRIGGER_SELECTOR)
|
|
118
|
-
?.addEventListener(
|
|
174
|
+
?.addEventListener('click', this.searchInputFocusHandler);
|
|
119
175
|
|
|
120
176
|
// Live filter
|
|
121
|
-
this.
|
|
122
|
-
|
|
123
|
-
);
|
|
177
|
+
this.searchInputHandler = (e) => this.handleSearch(e.target.value)
|
|
178
|
+
this.searchInput.addEventListener('input', this.searchInputHandler);
|
|
124
179
|
}
|
|
125
180
|
|
|
126
181
|
adjustDropdownHeight() {
|
|
@@ -12,19 +12,35 @@ export class PbDropdownKeyboard {
|
|
|
12
12
|
this.searchInput = this.dropdownElement.querySelector(
|
|
13
13
|
SEARCH_INPUT_SELECTOR
|
|
14
14
|
);
|
|
15
|
+
// Store bound handlers for cleanup
|
|
16
|
+
this.handleKeyDownBound = this.handleKeyDown.bind(this);
|
|
17
|
+
this.handleSearchInputBound = () => this.openDropdownIfClosed();
|
|
15
18
|
this.init();
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
init() {
|
|
19
22
|
this.dropdownElement.addEventListener(
|
|
20
23
|
"keydown",
|
|
21
|
-
this.
|
|
24
|
+
this.handleKeyDownBound
|
|
22
25
|
);
|
|
23
26
|
if (this.searchInput) {
|
|
24
|
-
this.searchInput.addEventListener("input",
|
|
25
|
-
|
|
27
|
+
this.searchInput.addEventListener("input", this.handleSearchInputBound);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
disconnect() {
|
|
32
|
+
// Remove keydown listener
|
|
33
|
+
if (this.dropdownElement && this.handleKeyDownBound) {
|
|
34
|
+
this.dropdownElement.removeEventListener(
|
|
35
|
+
"keydown",
|
|
36
|
+
this.handleKeyDownBound
|
|
26
37
|
);
|
|
27
38
|
}
|
|
39
|
+
|
|
40
|
+
// Remove search input listener
|
|
41
|
+
if (this.searchInput && this.handleSearchInputBound) {
|
|
42
|
+
this.searchInput.removeEventListener("input", this.handleSearchInputBound);
|
|
43
|
+
}
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
getVisibleOptions() {
|
|
@@ -50,6 +50,7 @@ export default class PbEnhancedElement {
|
|
|
50
50
|
const enhansedElement = this.elements.get(element)
|
|
51
51
|
enhansedElement.disconnect()
|
|
52
52
|
this.elements.delete(element)
|
|
53
|
+
delete element._pbEnhanced
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
static start(): void {
|
|
@@ -57,7 +58,7 @@ export default class PbEnhancedElement {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
static stop(): void {
|
|
60
|
-
this.
|
|
61
|
+
this.observer.stop()
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
connect(): void {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// eslint-disable-next-line
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import PbEnhancedElement from "../pb_enhanced_element";
|
|
4
|
+
|
|
5
|
+
type KitClass = typeof PbEnhancedElement;
|
|
6
|
+
|
|
7
|
+
class PbKitRegistry {
|
|
8
|
+
private static instance: PbKitRegistry;
|
|
9
|
+
|
|
10
|
+
// array to avoid overwriting if multiple kits share a selector
|
|
11
|
+
private kits: Map<string, KitClass[]> = new Map();
|
|
12
|
+
|
|
13
|
+
private mutationObserver: MutationObserver | null = null;
|
|
14
|
+
private initialized = false;
|
|
15
|
+
|
|
16
|
+
// rAF batching to reduce jank during heavy DOM churn
|
|
17
|
+
private queued = false;
|
|
18
|
+
private pendingMutations: MutationRecord[] = [];
|
|
19
|
+
|
|
20
|
+
static getInstance(): PbKitRegistry {
|
|
21
|
+
if (!PbKitRegistry.instance) {
|
|
22
|
+
PbKitRegistry.instance = new PbKitRegistry();
|
|
23
|
+
}
|
|
24
|
+
return PbKitRegistry.instance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
register(kit: KitClass): void {
|
|
28
|
+
const selector = kit.selector;
|
|
29
|
+
if (!selector) {
|
|
30
|
+
console.warn("[PbKitRegistry] Kit missing selector:", kit.name);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const list = this.kits.get(selector) || [];
|
|
35
|
+
list.push(kit);
|
|
36
|
+
this.kits.set(selector, list);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
start(): void {
|
|
40
|
+
if (this.initialized) return;
|
|
41
|
+
this.initialized = true;
|
|
42
|
+
|
|
43
|
+
const target = document.documentElement || document;
|
|
44
|
+
|
|
45
|
+
// Single MutationObserver for ALL kits
|
|
46
|
+
// attributes OFF
|
|
47
|
+
this.mutationObserver = new MutationObserver((muts) => this.onMutations(muts));
|
|
48
|
+
|
|
49
|
+
this.mutationObserver.observe(target, {
|
|
50
|
+
childList: true,
|
|
51
|
+
subtree: true,
|
|
52
|
+
// attributes: false by omission
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Initial scan of document
|
|
56
|
+
this.scan(document);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
stop(): void {
|
|
60
|
+
if (!this.initialized) return;
|
|
61
|
+
|
|
62
|
+
this.mutationObserver?.disconnect();
|
|
63
|
+
this.mutationObserver = null;
|
|
64
|
+
|
|
65
|
+
// Disconnect all kit instances safely (snapshot keys first)
|
|
66
|
+
this.kits.forEach((kitsForSelector) => {
|
|
67
|
+
kitsForSelector.forEach((kit) => {
|
|
68
|
+
if (!kit.elements) return;
|
|
69
|
+
const els = Array.from(kit.elements.keys());
|
|
70
|
+
els.forEach((el) => kit.removeMatch(el));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.pendingMutations = [];
|
|
75
|
+
this.queued = false;
|
|
76
|
+
this.initialized = false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- Mutation batching ----
|
|
80
|
+
|
|
81
|
+
private onMutations(muts: MutationRecord[]): void {
|
|
82
|
+
this.pendingMutations.push(...muts);
|
|
83
|
+
|
|
84
|
+
if (this.queued) return;
|
|
85
|
+
this.queued = true;
|
|
86
|
+
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
this.queued = false;
|
|
89
|
+
const batch = this.pendingMutations;
|
|
90
|
+
this.pendingMutations = [];
|
|
91
|
+
this.processMutations(batch);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private processMutations(mutations: MutationRecord[]): void {
|
|
96
|
+
// We only care about added nodes here.
|
|
97
|
+
// Removals handled by cleanupDisconnected() (no selector queries on removed subtrees)
|
|
98
|
+
const addedRoots: Element[] = [];
|
|
99
|
+
|
|
100
|
+
for (const mutation of mutations) {
|
|
101
|
+
if (mutation.type !== "childList") continue;
|
|
102
|
+
|
|
103
|
+
mutation.addedNodes.forEach((node) => {
|
|
104
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
105
|
+
addedRoots.push(node as Element);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Enhance anything newly inserted
|
|
111
|
+
if (addedRoots.length) {
|
|
112
|
+
for (const root of addedRoots) {
|
|
113
|
+
this.scan(root);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle removals cheaply: only look at already-enhanced elements
|
|
118
|
+
this.cleanupDisconnected();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- Scanning / enhancing ----
|
|
122
|
+
|
|
123
|
+
private scan(root: ParentNode): void {
|
|
124
|
+
// For each selector, query within root once and attach all kits registered to it
|
|
125
|
+
this.kits.forEach((kitsForSelector, selector) => {
|
|
126
|
+
let matches: NodeListOf<Element>;
|
|
127
|
+
try {
|
|
128
|
+
// querySelectorAll doesn’t include root itself, so we handle that below too
|
|
129
|
+
matches = (root as any).querySelectorAll
|
|
130
|
+
? (root as any).querySelectorAll(selector)
|
|
131
|
+
: document.querySelectorAll(selector);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.debug(`[PbKitRegistry] Invalid selector "${selector}"`, error);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (matches && matches.length) {
|
|
138
|
+
matches.forEach((el) => {
|
|
139
|
+
kitsForSelector.forEach((kit) => kit.addMatch(el));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Include root itself if it matches
|
|
144
|
+
if ((root as any).matches?.(selector)) {
|
|
145
|
+
kitsForSelector.forEach((kit) => kit.addMatch(root as any));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// No selector queries on removals
|
|
151
|
+
// We just remove instances whose elements are no longer in the DOM
|
|
152
|
+
private cleanupDisconnected(): void {
|
|
153
|
+
this.kits.forEach((kitsForSelector) => {
|
|
154
|
+
kitsForSelector.forEach((kit) => {
|
|
155
|
+
if (!kit.elements) return;
|
|
156
|
+
|
|
157
|
+
// Snapshot keys to avoid mutate-while-iterating
|
|
158
|
+
const els = Array.from(kit.elements.keys());
|
|
159
|
+
for (const el of els) {
|
|
160
|
+
if (!el.isConnected) {
|
|
161
|
+
kit.removeMatch(el);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Debug helpers
|
|
169
|
+
getRegisteredSelectors(): string[] {
|
|
170
|
+
return Array.from(this.kits.keys());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Optional: manually rescan a subtree (useful if some page toggles data-* after insertion)
|
|
174
|
+
rescan(root: ParentNode = document): void {
|
|
175
|
+
if (!this.initialized) return;
|
|
176
|
+
this.scan(root);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default PbKitRegistry.getInstance();
|
|
@@ -11,11 +11,14 @@ export default class PbTooltip extends PbEnhancedElement {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
connect() {
|
|
14
|
+
this.triggerHandlers = new Map()
|
|
15
|
+
|
|
14
16
|
if (this.tooltipInteraction) {
|
|
15
|
-
|
|
17
|
+
this.mouseMoveHandler = (e) => {
|
|
16
18
|
this.lastMouseX = e.clientX
|
|
17
19
|
this.lastMouseY = e.clientY
|
|
18
|
-
}
|
|
20
|
+
}
|
|
21
|
+
document.addEventListener('mousemove', this.mouseMoveHandler)
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
this.triggerElements.forEach((trigger) => {
|
|
@@ -23,7 +26,7 @@ export default class PbTooltip extends PbEnhancedElement {
|
|
|
23
26
|
const interactionEnabled = this.tooltipInteraction
|
|
24
27
|
|
|
25
28
|
if (method === 'click') {
|
|
26
|
-
|
|
29
|
+
const clickHandler = (e) => {
|
|
27
30
|
if (this.useClickToOpen) {
|
|
28
31
|
e.preventDefault()
|
|
29
32
|
if (this.isTooltipVisible()) {
|
|
@@ -34,10 +37,12 @@ export default class PbTooltip extends PbEnhancedElement {
|
|
|
34
37
|
} else {
|
|
35
38
|
this.showTooltip(trigger)
|
|
36
39
|
}
|
|
37
|
-
}
|
|
40
|
+
}
|
|
41
|
+
trigger.addEventListener('click', clickHandler)
|
|
42
|
+
this.triggerHandlers.set(trigger, { click: clickHandler })
|
|
38
43
|
} else {
|
|
39
44
|
if (!this.useClickToOpen) {
|
|
40
|
-
|
|
45
|
+
const mouseenterHandler = () => {
|
|
41
46
|
clearSafeZoneListener(this)
|
|
42
47
|
clearTimeout(this.mouseleaveTimeout)
|
|
43
48
|
this.currentTrigger = trigger
|
|
@@ -48,9 +53,9 @@ export default class PbTooltip extends PbEnhancedElement {
|
|
|
48
53
|
this.checkCloseTooltip(trigger)
|
|
49
54
|
}
|
|
50
55
|
}, delayOpen)
|
|
51
|
-
}
|
|
56
|
+
}
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
const mouseleaveHandler = () => {
|
|
54
59
|
clearTimeout(this.mouseenterTimeout)
|
|
55
60
|
if (this.delayClose) {
|
|
56
61
|
const delayClose = parseInt(this.delayClose)
|
|
@@ -68,22 +73,62 @@ export default class PbTooltip extends PbEnhancedElement {
|
|
|
68
73
|
this.hideTooltip()
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
|
-
}
|
|
76
|
+
}
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
})
|
|
78
|
+
trigger.addEventListener('mouseenter', mouseenterHandler)
|
|
79
|
+
trigger.addEventListener('mouseleave', mouseleaveHandler)
|
|
80
|
+
this.triggerHandlers.set(trigger, { mouseenter: mouseenterHandler, mouseleave: mouseleaveHandler })
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
if (interactionEnabled) {
|
|
83
|
+
if (!this.tooltipMouseenterHandler) {
|
|
84
|
+
this.tooltipMouseenterHandler = () => {
|
|
85
|
+
clearSafeZoneListener(this)
|
|
86
|
+
}
|
|
87
|
+
this.tooltipMouseleaveHandler = () => {
|
|
88
|
+
this.attachSafeZoneListener()
|
|
89
|
+
}
|
|
90
|
+
this.tooltip.addEventListener('mouseenter', this.tooltipMouseenterHandler)
|
|
91
|
+
this.tooltip.addEventListener('mouseleave', this.tooltipMouseleaveHandler)
|
|
92
|
+
}
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
95
|
}
|
|
84
96
|
})
|
|
85
97
|
}
|
|
86
98
|
|
|
99
|
+
disconnect() {
|
|
100
|
+
// Clean up timers
|
|
101
|
+
clearTimeout(this.mouseenterTimeout)
|
|
102
|
+
clearTimeout(this.mouseleaveTimeout)
|
|
103
|
+
clearTimeout(this.autoHideTimeout)
|
|
104
|
+
clearSafeZoneListener(this)
|
|
105
|
+
|
|
106
|
+
// Clean up autoUpdate
|
|
107
|
+
if (this.cleanup) {
|
|
108
|
+
this.cleanup()
|
|
109
|
+
this.cleanup = null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Clean up document mousemove listener
|
|
113
|
+
if (this.mouseMoveHandler) {
|
|
114
|
+
document.removeEventListener('mousemove', this.mouseMoveHandler)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Clean up trigger element listeners
|
|
118
|
+
this.triggerHandlers.forEach((handlers, trigger) => {
|
|
119
|
+
Object.entries(handlers).forEach(([event, handler]) => {
|
|
120
|
+
trigger.removeEventListener(event, handler)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
this.triggerHandlers.clear()
|
|
124
|
+
|
|
125
|
+
// Clean up tooltip listeners
|
|
126
|
+
if (this.tooltipMouseenterHandler) {
|
|
127
|
+
this.tooltip.removeEventListener('mouseenter', this.tooltipMouseenterHandler)
|
|
128
|
+
this.tooltip.removeEventListener('mouseleave', this.tooltipMouseleaveHandler)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
87
132
|
isTooltipVisible() {
|
|
88
133
|
return this.tooltip && this.tooltip.classList.contains('show')
|
|
89
134
|
}
|