@whykusanagi/corrupted-theme 0.1.5 → 0.1.7

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.
@@ -4,13 +4,25 @@
4
4
  *
5
5
  * Provides helper functions for:
6
6
  * - Accordion/Collapse
7
+ * - Modal
8
+ * - Dropdown
9
+ * - Tabs
7
10
  * - Toast Notifications
8
- * - Auto-initialization
11
+ * - Auto-initialization via data-ct-* attributes
9
12
  *
10
13
  * @module components
11
- * @version 1.0.0
14
+ * @version 2.0.0
12
15
  */
13
16
 
17
+ import { EventTracker } from '../core/event-tracker.js';
18
+
19
+ /**
20
+ * Shared tracker for listeners created during auto-initialization.
21
+ * Cleaned up by destroyComponents().
22
+ * @private
23
+ */
24
+ const _initTracker = new EventTracker();
25
+
14
26
  // ========== ACCORDION / COLLAPSE ==========
15
27
 
16
28
  /**
@@ -28,7 +40,7 @@ export function initAccordions() {
28
40
  const header = item.querySelector('.accordion-header');
29
41
  if (!header) return;
30
42
 
31
- header.addEventListener('click', () => {
43
+ _initTracker.add(header, 'click', () => {
32
44
  const wasActive = item.classList.contains('active');
33
45
 
34
46
  // Close all items in this accordion (unless it's already active)
@@ -137,32 +149,38 @@ class ToastManager {
137
149
  const toast = document.createElement('div');
138
150
  toast.className = `toast ${type}`;
139
151
 
140
- // Create toast content
141
- const content = `
142
- ${title ? `
143
- <div class="toast-header">
144
- <span>${title}</span>
145
- </div>
146
- ` : ''}
147
- <div class="toast-body">${message}</div>
148
- <button class="toast-close" aria-label="Close">×</button>
149
- `;
150
-
151
- toast.innerHTML = content;
152
-
153
- // Add close handler
154
- const closeBtn = toast.querySelector('.toast-close');
152
+ // Create toast content using safe DOM methods
153
+ if (title) {
154
+ const header = document.createElement('div');
155
+ header.className = 'toast-header';
156
+ const titleSpan = document.createElement('span');
157
+ titleSpan.textContent = title;
158
+ header.appendChild(titleSpan);
159
+ toast.appendChild(header);
160
+ }
161
+
162
+ const body = document.createElement('div');
163
+ body.className = 'toast-body';
164
+ body.textContent = message;
165
+ toast.appendChild(body);
166
+
167
+ const closeBtn = document.createElement('button');
168
+ closeBtn.className = 'toast-close';
169
+ closeBtn.setAttribute('aria-label', 'Close');
170
+ closeBtn.textContent = '\u00d7';
155
171
  closeBtn.addEventListener('click', () => {
156
172
  this.dismiss(toast, onClose);
157
173
  });
174
+ toast.appendChild(closeBtn);
158
175
 
159
176
  // Add to container
160
177
  container.appendChild(toast);
161
178
  this.toasts.push(toast);
162
179
 
163
- // Auto-dismiss
180
+ // Auto-dismiss (tracked for cleanup on manual dismiss)
164
181
  if (duration > 0) {
165
- setTimeout(() => {
182
+ toast._autoDismissId = setTimeout(() => {
183
+ toast._autoDismissId = null;
166
184
  this.dismiss(toast, onClose);
167
185
  }, duration);
168
186
  }
@@ -178,6 +196,12 @@ class ToastManager {
178
196
  dismiss(toast, onClose = null) {
179
197
  if (!toast || !document.contains(toast)) return;
180
198
 
199
+ // Clear auto-dismiss timer if dismissing early
200
+ if (toast._autoDismissId) {
201
+ clearTimeout(toast._autoDismissId);
202
+ toast._autoDismissId = null;
203
+ }
204
+
181
205
  toast.classList.add('hiding');
182
206
 
183
207
  setTimeout(() => {
@@ -201,7 +225,19 @@ class ToastManager {
201
225
  * Dismiss all toasts
202
226
  */
203
227
  dismissAll() {
204
- this.toasts.forEach(toast => this.dismiss(toast));
228
+ [...this.toasts].forEach(toast => this.dismiss(toast));
229
+ }
230
+
231
+ /**
232
+ * Tear down the toast system: dismiss all toasts and remove container.
233
+ */
234
+ destroy() {
235
+ this.dismissAll();
236
+ if (this.container && this.container.parentNode) {
237
+ this.container.parentNode.removeChild(this.container);
238
+ }
239
+ this.container = null;
240
+ this.toasts = [];
205
241
  }
206
242
 
207
243
  // Convenience methods for different toast types
@@ -247,16 +283,305 @@ export const toast = {
247
283
  dismissAll: () => toastManager.dismissAll()
248
284
  };
249
285
 
286
+ // ========== MODAL ==========
287
+
288
+ /**
289
+ * Modal manager with lifecycle management
290
+ *
291
+ * Usage:
292
+ * ```html
293
+ * <button data-ct-toggle="modal" data-ct-target="#my-modal">Open</button>
294
+ * <div class="modal-overlay" id="my-modal">
295
+ * <div class="modal">
296
+ * <div class="modal-header">
297
+ * <h3 class="modal-title">Title</h3>
298
+ * <button class="modal-close">&times;</button>
299
+ * </div>
300
+ * <div class="modal-body">Content</div>
301
+ * </div>
302
+ * </div>
303
+ * ```
304
+ */
305
+ class ModalManager {
306
+ constructor() {
307
+ this._events = new EventTracker();
308
+ this._initialized = new Set();
309
+ }
310
+
311
+ /**
312
+ * Initialize a modal overlay
313
+ * @param {string|HTMLElement} selector - Modal overlay selector or element
314
+ */
315
+ init(selector) {
316
+ const overlay = typeof selector === 'string'
317
+ ? document.querySelector(selector) : selector;
318
+ if (!overlay || this._initialized.has(overlay)) return;
319
+
320
+ const closeBtn = overlay.querySelector('.modal-close');
321
+ if (closeBtn) {
322
+ this._events.add(closeBtn, 'click', () => this.close(overlay));
323
+ }
324
+
325
+ // Close on overlay click (outside modal content)
326
+ this._events.add(overlay, 'click', (e) => {
327
+ if (e.target === overlay) this.close(overlay);
328
+ });
329
+
330
+ this._initialized.add(overlay);
331
+ }
332
+
333
+ /**
334
+ * Open a modal
335
+ * @param {string|HTMLElement} selector - Modal overlay selector or element
336
+ */
337
+ open(selector) {
338
+ const overlay = typeof selector === 'string'
339
+ ? document.querySelector(selector) : selector;
340
+ if (!overlay) return;
341
+
342
+ overlay.classList.add('active');
343
+ document.body.style.overflow = 'hidden';
344
+
345
+ overlay.dispatchEvent(new CustomEvent('modal:open', { bubbles: true }));
346
+ }
347
+
348
+ /**
349
+ * Close a modal
350
+ * @param {string|HTMLElement} selector - Modal overlay selector or element
351
+ */
352
+ close(selector) {
353
+ const overlay = typeof selector === 'string'
354
+ ? document.querySelector(selector) : selector;
355
+ if (!overlay) return;
356
+
357
+ overlay.classList.remove('active');
358
+ document.body.style.overflow = '';
359
+
360
+ overlay.dispatchEvent(new CustomEvent('modal:close', { bubbles: true }));
361
+ }
362
+
363
+ /**
364
+ * Tear down all tracked listeners and state
365
+ */
366
+ destroy() {
367
+ this._events.removeAll();
368
+ this._initialized.clear();
369
+ }
370
+ }
371
+
372
+ const modalManager = new ModalManager();
373
+
374
+ /**
375
+ * Open a modal by selector
376
+ * @param {string|HTMLElement} selector
377
+ */
378
+ export function openModal(selector) {
379
+ modalManager.open(selector);
380
+ }
381
+
382
+ /**
383
+ * Close a modal by selector
384
+ * @param {string|HTMLElement} selector
385
+ */
386
+ export function closeModal(selector) {
387
+ modalManager.close(selector);
388
+ }
389
+
390
+ // ========== DROPDOWN ==========
391
+
392
+ /**
393
+ * Dropdown manager with click-outside-to-close
394
+ *
395
+ * Usage:
396
+ * ```html
397
+ * <div class="dropdown">
398
+ * <button class="dropdown-toggle" data-ct-toggle="dropdown">Menu</button>
399
+ * <div class="dropdown-menu">
400
+ * <a class="dropdown-item" href="#">Item 1</a>
401
+ * </div>
402
+ * </div>
403
+ * ```
404
+ */
405
+ class DropdownManager {
406
+ constructor() {
407
+ this._events = new EventTracker();
408
+ this._initialized = false;
409
+ this._outsideClickBound = false;
410
+ }
411
+
412
+ /**
413
+ * Initialize dropdown toggle behavior
414
+ * @param {HTMLElement} toggle - The dropdown-toggle element
415
+ */
416
+ init(toggle) {
417
+ const menu = toggle.parentElement?.querySelector('.dropdown-menu');
418
+ if (!menu) return;
419
+
420
+ this._events.add(toggle, 'click', (e) => {
421
+ e.stopPropagation();
422
+ const isOpen = menu.classList.contains('active');
423
+
424
+ // Close all other dropdowns first
425
+ this.closeAll();
426
+
427
+ if (!isOpen) {
428
+ menu.classList.add('active');
429
+ toggle.classList.add('active');
430
+ }
431
+ });
432
+
433
+ // Setup global click-outside handler once
434
+ if (!this._outsideClickBound) {
435
+ this._events.add(document, 'click', () => this.closeAll());
436
+ this._outsideClickBound = true;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Close all open dropdowns
442
+ */
443
+ closeAll() {
444
+ document.querySelectorAll('.dropdown-menu.active').forEach(menu => {
445
+ menu.classList.remove('active');
446
+ });
447
+ document.querySelectorAll('.dropdown-toggle.active').forEach(toggle => {
448
+ toggle.classList.remove('active');
449
+ });
450
+ }
451
+
452
+ /**
453
+ * Tear down all tracked listeners
454
+ */
455
+ destroy() {
456
+ this.closeAll();
457
+ this._events.removeAll();
458
+ this._outsideClickBound = false;
459
+ }
460
+ }
461
+
462
+ const dropdownManager = new DropdownManager();
463
+
464
+ // ========== TABS ==========
465
+
466
+ /**
467
+ * Tab manager
468
+ *
469
+ * Usage:
470
+ * ```html
471
+ * <div class="tabs">
472
+ * <button class="tab active" data-ct-target="#panel-1">Tab 1</button>
473
+ * <button class="tab" data-ct-target="#panel-2">Tab 2</button>
474
+ * </div>
475
+ * <div class="tab-content active" id="panel-1">Panel 1</div>
476
+ * <div class="tab-content" id="panel-2">Panel 2</div>
477
+ * ```
478
+ */
479
+ class TabManager {
480
+ constructor() {
481
+ this._events = new EventTracker();
482
+ }
483
+
484
+ /**
485
+ * Initialize a tab container
486
+ * @param {HTMLElement} tabsContainer - The .tabs element
487
+ */
488
+ init(tabsContainer) {
489
+ const tabs = tabsContainer.querySelectorAll('.tab[data-ct-target]');
490
+
491
+ tabs.forEach(tab => {
492
+ this._events.add(tab, 'click', () => {
493
+ // Deactivate all sibling tabs
494
+ tabsContainer.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
495
+
496
+ // Hide all associated panels
497
+ tabs.forEach(t => {
498
+ const panel = document.querySelector(t.dataset.ctTarget);
499
+ if (panel) panel.classList.remove('active');
500
+ });
501
+
502
+ // Activate clicked tab and its panel
503
+ tab.classList.add('active');
504
+ const panel = document.querySelector(tab.dataset.ctTarget);
505
+ if (panel) panel.classList.add('active');
506
+ });
507
+ });
508
+ }
509
+
510
+ /**
511
+ * Tear down all tracked listeners
512
+ */
513
+ destroy() {
514
+ this._events.removeAll();
515
+ }
516
+ }
517
+
518
+ const tabManager = new TabManager();
519
+
250
520
  // ========== AUTO-INITIALIZATION ==========
251
521
 
252
522
  /**
253
- * Initialize all components on page load
523
+ * Initialize all components on page load.
524
+ * Scans for data-ct-* attributes and wires up behavior.
525
+ * All listeners are tracked via _initTracker or manager EventTrackers.
254
526
  */
255
527
  function initComponents() {
256
- // Initialize accordions
528
+ // Accordions
257
529
  if (document.querySelector('.accordion')) {
258
530
  initAccordions();
259
531
  }
532
+
533
+ // Modals — init overlays and wire triggers
534
+ document.querySelectorAll('.modal-overlay').forEach(overlay => {
535
+ modalManager.init(overlay);
536
+ });
537
+
538
+ document.querySelectorAll('[data-ct-toggle="modal"]').forEach(trigger => {
539
+ const targetSel = trigger.dataset.ctTarget;
540
+ if (!targetSel) return;
541
+ _initTracker.add(trigger, 'click', () => modalManager.open(targetSel));
542
+ });
543
+
544
+ // Escape key closes active modals
545
+ if (document.querySelector('.modal-overlay')) {
546
+ _initTracker.add(document, 'keydown', (e) => {
547
+ if (e.key === 'Escape') {
548
+ document.querySelectorAll('.modal-overlay.active').forEach(overlay => {
549
+ modalManager.close(overlay);
550
+ });
551
+ }
552
+ });
553
+ }
554
+
555
+ // Dropdowns
556
+ document.querySelectorAll('[data-ct-toggle="dropdown"]').forEach(toggle => {
557
+ dropdownManager.init(toggle);
558
+ });
559
+
560
+ // Tabs
561
+ document.querySelectorAll('.tabs').forEach(tabsContainer => {
562
+ if (tabsContainer.querySelector('.tab[data-ct-target]')) {
563
+ tabManager.init(tabsContainer);
564
+ }
565
+ });
566
+
567
+ // Collapse triggers
568
+ document.querySelectorAll('[data-ct-toggle="collapse"]').forEach(trigger => {
569
+ const targetSel = trigger.dataset.ctTarget;
570
+ if (!targetSel) return;
571
+ _initTracker.add(trigger, 'click', () => toggleCollapse(targetSel));
572
+ });
573
+ }
574
+
575
+ /**
576
+ * Destroy all component managers and clean up listeners.
577
+ * Removes all tracked listeners from auto-initialization and managers.
578
+ */
579
+ export function destroyComponents() {
580
+ _initTracker.removeAll();
581
+ modalManager.destroy();
582
+ dropdownManager.destroy();
583
+ tabManager.destroy();
584
+ toastManager.destroy();
260
585
  }
261
586
 
262
587
  // Auto-initialize on DOM ready
@@ -271,13 +596,20 @@ if (typeof window !== 'undefined') {
271
596
  // ========== EXPORTS ==========
272
597
 
273
598
  export default {
274
- // Accordion
599
+ // Accordion / Collapse
275
600
  initAccordions,
276
601
  toggleCollapse,
277
602
  showCollapse,
278
603
  hideCollapse,
279
604
 
605
+ // Modal
606
+ openModal,
607
+ closeModal,
608
+
280
609
  // Toast
281
610
  showToast,
282
- toast
611
+ toast,
612
+
613
+ // Lifecycle
614
+ destroyComponents
283
615
  };
@@ -46,6 +46,9 @@ class CorruptedText {
46
46
  this.currentVariantIndex = 0;
47
47
  this.isAnimating = false;
48
48
  this.animationFrame = null;
49
+ this._startDelayId = null;
50
+ this._animateTimeoutId = null;
51
+ this._corruptTimeoutId = null;
49
52
 
50
53
  this.init();
51
54
  }
@@ -61,7 +64,7 @@ class CorruptedText {
61
64
 
62
65
  // Start animation after delay
63
66
  if (this.options.startDelay > 0) {
64
- setTimeout(() => this.start(), this.options.startDelay);
67
+ this._startDelayId = setTimeout(() => this.start(), this.options.startDelay);
65
68
  } else {
66
69
  this.start();
67
70
  }
@@ -75,9 +78,22 @@ class CorruptedText {
75
78
 
76
79
  stop() {
77
80
  this.isAnimating = false;
78
- if (this.animationFrame) {
79
- cancelAnimationFrame(this.animationFrame);
81
+ if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
82
+ if (this._startDelayId) clearTimeout(this._startDelayId);
83
+ if (this._animateTimeoutId) clearTimeout(this._animateTimeoutId);
84
+ if (this._corruptTimeoutId) clearTimeout(this._corruptTimeoutId);
85
+ this.animationFrame = null;
86
+ this._startDelayId = null;
87
+ this._animateTimeoutId = null;
88
+ this._corruptTimeoutId = null;
89
+ }
90
+
91
+ destroy() {
92
+ this.stop();
93
+ if (this.element && this.element.corruptedTextInstance === this) {
94
+ delete this.element.corruptedTextInstance;
80
95
  }
96
+ this.element = null;
81
97
  }
82
98
 
83
99
  animate() {
@@ -99,7 +115,7 @@ class CorruptedText {
99
115
  }
100
116
 
101
117
  // Continue animation
102
- setTimeout(() => {
118
+ this._animateTimeoutId = setTimeout(() => {
103
119
  if (this.isAnimating) {
104
120
  this.animate();
105
121
  }
@@ -147,7 +163,7 @@ class CorruptedText {
147
163
  step++;
148
164
 
149
165
  this.animationFrame = requestAnimationFrame(() => {
150
- setTimeout(corrupt, this.options.cycleDelay);
166
+ this._corruptTimeoutId = setTimeout(corrupt, this.options.cycleDelay);
151
167
  });
152
168
  };
153
169
 
@@ -31,6 +31,34 @@
31
31
  localStorage.setItem("corruptionLoadingLastPlayed", Date.now().toString());
32
32
  }
33
33
 
34
+ // Inline timer tracking (IIFE can't import ES modules)
35
+ const _timers = { _t: new Set(), _i: new Set() };
36
+ function _setTimeout(fn, delay) {
37
+ const id = setTimeout(() => { _timers._t.delete(id); fn(); }, delay);
38
+ _timers._t.add(id);
39
+ return id;
40
+ }
41
+ function _setInterval(fn, delay) {
42
+ const id = setInterval(fn, delay);
43
+ _timers._i.add(id);
44
+ return id;
45
+ }
46
+ function _clearAllTimers() {
47
+ _timers._t.forEach(id => clearTimeout(id));
48
+ _timers._i.forEach(id => clearInterval(id));
49
+ _timers._t.clear();
50
+ _timers._i.clear();
51
+ }
52
+
53
+ // Cancel loading screen early
54
+ function cancelLoading() {
55
+ _clearAllTimers();
56
+ const screen = document.getElementById('corruption-loading');
57
+ if (screen) screen.remove();
58
+ const styles = document.getElementById('corruption-loading-styles');
59
+ if (styles) styles.remove();
60
+ }
61
+
34
62
  // Main function to show loading screen
35
63
  function showCorruptionLoading(options = {}) {
36
64
  const config = {
@@ -51,7 +79,7 @@
51
79
  // Inject styles
52
80
  const style = document.createElement("style");
53
81
  style.id = "corruption-loading-styles";
54
- style.innerHTML = `
82
+ style.textContent = `
55
83
  @keyframes flicker {
56
84
  0%, 100% { opacity: 1; }
57
85
  50% { opacity: 0.4; }
@@ -235,6 +263,7 @@
235
263
  const loadingScreen = document.createElement("div");
236
264
  loadingScreen.id = "corruption-loading-screen";
237
265
 
266
+ // Static HTML only — no interpolated variables, safe from XSS
238
267
  loadingScreen.innerHTML = `
239
268
  <div class="corrupt-stream"></div>
240
269
  <div class="crt-overlay"></div>
@@ -306,8 +335,8 @@
306
335
 
307
336
  target.innerHTML = '';
308
337
  let i = 0;
309
- const interval = setInterval(() => {
310
- if (i >= text.length) return clearInterval(interval);
338
+ const interval = _setInterval(() => {
339
+ if (i >= text.length) { clearInterval(interval); _timers._i.delete(interval); return; }
311
340
  const span = document.createElement('span');
312
341
  span.className = 'glyph';
313
342
  span.textContent = text[i];
@@ -349,16 +378,16 @@
349
378
  });
350
379
 
351
380
  // Animate progress text
352
- setTimeout(() => {
381
+ _setTimeout(() => {
353
382
  [...progressText.children].forEach((span, idx) => {
354
- setTimeout(() => {
383
+ _setTimeout(() => {
355
384
  span.textContent = phrase.replace(/\s/g, '').charAt(idx);
356
385
  }, 500 * (idx + 1));
357
386
  });
358
387
  }, 3600);
359
388
 
360
389
  // Type grow text
361
- setTimeout(() => {
390
+ _setTimeout(() => {
362
391
  const growText = document.getElementById("grow-text");
363
392
  if (growText) {
364
393
  typeGlyphText("grow-text", "Initializing corruption protocols...", 80);
@@ -366,9 +395,10 @@
366
395
  }, 2000);
367
396
 
368
397
  // Remove loading screen
369
- setTimeout(() => {
398
+ _setTimeout(() => {
370
399
  loadingScreen.style.opacity = "0";
371
- setTimeout(() => {
400
+ _setTimeout(() => {
401
+ _clearAllTimers();
372
402
  loadingScreen.remove();
373
403
  const styles = document.getElementById("corruption-loading-styles");
374
404
  if (styles) styles.remove();
@@ -394,12 +424,12 @@
394
424
 
395
425
  // Export for manual use
396
426
  window.showCorruptionLoading = showCorruptionLoading;
397
- window.CorruptionLoading = { show: showCorruptionLoading };
427
+ window.CorruptionLoading = { show: showCorruptionLoading, cancel: cancelLoading };
398
428
  }
399
429
 
400
430
  // Export for modules
401
431
  if (typeof module !== 'undefined' && module.exports) {
402
- module.exports = { showCorruptionLoading };
432
+ module.exports = { showCorruptionLoading, cancelLoading };
403
433
  }
404
434
  })();
405
435
 
@@ -134,6 +134,8 @@ let state = {
134
134
  config: null,
135
135
  countdownInterval: null,
136
136
  popupInterval: null,
137
+ popupInitTimeout: null,
138
+ popupDurationTimeout: null,
137
139
  isCompleted: false
138
140
  };
139
141
 
@@ -314,7 +316,7 @@ function renderWidget(config) {
314
316
  if (config.popup?.message) {
315
317
  popup = document.createElement('div');
316
318
  popup.className = 'countdown-popup';
317
- popup.innerHTML = config.popup.message;
319
+ popup.textContent = config.popup.message;
318
320
 
319
321
  if (config.popup.colors) {
320
322
  if (config.popup.colors.bg) popup.style.background = config.popup.colors.bg;
@@ -460,7 +462,8 @@ function startPopup(popupConfig, popupElement) {
460
462
  const duration = popupConfig.duration || 5000;
461
463
 
462
464
  // Show popup initially after a delay
463
- setTimeout(() => {
465
+ state.popupInitTimeout = setTimeout(() => {
466
+ state.popupInitTimeout = null;
464
467
  showPopup(popupElement, duration);
465
468
  }, 2000);
466
469
 
@@ -478,8 +481,14 @@ function startPopup(popupConfig, popupElement) {
478
481
  */
479
482
  function showPopup(popup, duration) {
480
483
  popup.classList.add('active');
481
-
482
- setTimeout(() => {
484
+
485
+ // Clear previous duration timeout if popup is re-shown before it hides
486
+ if (state.popupDurationTimeout) {
487
+ clearTimeout(state.popupDurationTimeout);
488
+ }
489
+
490
+ state.popupDurationTimeout = setTimeout(() => {
491
+ state.popupDurationTimeout = null;
483
492
  popup.classList.remove('active');
484
493
  }, duration);
485
494
  }
@@ -584,12 +593,22 @@ export function destroyCountdown() {
584
593
  clearInterval(state.countdownInterval);
585
594
  state.countdownInterval = null;
586
595
  }
587
-
596
+
588
597
  if (state.popupInterval) {
589
598
  clearInterval(state.popupInterval);
590
599
  state.popupInterval = null;
591
600
  }
592
-
601
+
602
+ if (state.popupInitTimeout) {
603
+ clearTimeout(state.popupInitTimeout);
604
+ state.popupInitTimeout = null;
605
+ }
606
+
607
+ if (state.popupDurationTimeout) {
608
+ clearTimeout(state.popupDurationTimeout);
609
+ state.popupDurationTimeout = null;
610
+ }
611
+
593
612
  state.config = null;
594
613
  state.isCompleted = false;
595
614