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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cb000fca3484843059f2931860f4a6b637eb926c4d1342fad44896c5dd59f0e
4
- data.tar.gz: 22905440ea6c5d23f981a0d9176a02ac355fe6f96b2955398f1b9c128fb80c78
3
+ metadata.gz: f12b618cfe899125e5b51f0cb0615e124d3df5a1396892c3114d4c82ce26703f
4
+ data.tar.gz: 5dc0bba31972163cb2c9c8b7ee23f25346f104d948fe8d863c3f7761c2984bb1
5
5
  SHA512:
6
- metadata.gz: c9aca337d39326590c916cd62015fdc02dea42c9c7454e60e3505ae71169a19827f07c46d1ed29582cddd255442bcb20ddf759b944761d523201d7e267327456
7
- data.tar.gz: 8ba322dfe0e345300ec7efab9fa19c4660754b94768e1f5e7c14a8d79efa3298d338343eb501d5836a3712fd37c0ea64e9947e0e52347eae9cc7f4d7cce1c73c
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.element.addEventListener('click', () => {
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
- document.addEventListener(`${this.target.id}`, () => {
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
- window.addEventListener("DOMContentLoaded", () => this.setupDialog())
12
- window.addEventListener("turbo:frame-load", () => this.setupDialog())
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
- const openTrigger = document.querySelectorAll("[data-open-dialog]");
92
- const closeTrigger = document.querySelectorAll("[data-close-dialog]");
93
- const dialogs = document.querySelectorAll(".pb_dialog_rails")
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.clearBtn.addEventListener("click", (e) => {
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
- customTrigger.addEventListener("click", (e) => {
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.target.addEventListener("click", this.handleOptionClick.bind(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.handleDocumentClick.bind(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.searchBar.addEventListener("input", (e) =>
107
- this.handleSearch(e.target.value),
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("click", () => this.searchInput.focus());
174
+ ?.addEventListener('click', this.searchInputFocusHandler);
119
175
 
120
176
  // Live filter
121
- this.searchInput.addEventListener("input", (e) =>
122
- this.handleSearch(e.target.value),
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.handleKeyDown.bind(this)
24
+ this.handleKeyDownBound
22
25
  );
23
26
  if (this.searchInput) {
24
- this.searchInput.addEventListener("input", () =>
25
- this.openDropdownIfClosed()
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() {
@@ -23,7 +23,7 @@ export default class ElementObserver {
23
23
  }
24
24
 
25
25
  stop(): void {
26
- this.mutationObserverdisconnect()
26
+ this.mutationObserver.disconnect()
27
27
  }
28
28
 
29
29
  catchup(): void {
@@ -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.mutationObserver.stop()
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
- document.addEventListener('mousemove', (e) => {
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
- trigger.addEventListener('click', (e) => {
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
- trigger.addEventListener('mouseenter', () => {
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
- trigger.addEventListener('mouseleave', () => {
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
- if (interactionEnabled) {
74
- this.tooltip.addEventListener('mouseenter', () => {
75
- clearSafeZoneListener(this)
76
- })
78
+ trigger.addEventListener('mouseenter', mouseenterHandler)
79
+ trigger.addEventListener('mouseleave', mouseleaveHandler)
80
+ this.triggerHandlers.set(trigger, { mouseenter: mouseenterHandler, mouseleave: mouseleaveHandler })
77
81
 
78
- this.tooltip.addEventListener('mouseleave', () => {
79
- this.attachSafeZoneListener()
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
  }