@dodlhuat/basix 1.3.2 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +13 -7
  2. package/css/accordion.scss +0 -5
  3. package/css/badge.scss +1 -6
  4. package/css/bottom-sheet.scss +3 -8
  5. package/css/breadcrumb.scss +6 -15
  6. package/css/button.scss +2 -1
  7. package/css/calendar.scss +0 -54
  8. package/css/card.scss +0 -5
  9. package/css/carousel.scss +0 -3
  10. package/css/chart.scss +0 -25
  11. package/css/chat-bubbles.scss +0 -15
  12. package/css/checkbox.scss +3 -2
  13. package/css/chips.scss +3 -7
  14. package/css/code-viewer.scss +1 -5
  15. package/css/context-menu.scss +4 -6
  16. package/css/datepicker.scss +4 -7
  17. package/css/docs.scss +0 -4
  18. package/css/dropdown.scss +1 -1
  19. package/css/editor.scss +1 -23
  20. package/css/file-uploader.scss +2 -2
  21. package/css/flyout-menu.scss +66 -44
  22. package/css/form.scss +0 -28
  23. package/css/gallery.scss +2 -3
  24. package/css/group-picker.scss +5 -35
  25. package/css/icons.scss +0 -3
  26. package/css/lightbox.scss +2 -4
  27. package/css/mixins.scss +8 -0
  28. package/css/modal.scss +3 -3
  29. package/css/parameters.scss +6 -1
  30. package/css/popover.scss +3 -15
  31. package/css/progress.scss +0 -6
  32. package/css/push-menu.scss +3 -28
  33. package/css/radiobutton.scss +2 -1
  34. package/css/range-slider.scss +1 -7
  35. package/css/scrollbar.scss +2 -6
  36. package/css/sidebar-nav.scss +0 -12
  37. package/css/stepper.scss +0 -4
  38. package/css/style.css +63 -68
  39. package/css/style.css.map +1 -1
  40. package/css/style.min.css +1 -1
  41. package/css/style.min.css.map +1 -1
  42. package/css/style.scss +1 -1
  43. package/css/table.scss +0 -4
  44. package/css/tabs.scss +0 -2
  45. package/css/timeline.scss +1 -13
  46. package/css/timepicker.scss +6 -7
  47. package/css/toast.scss +1 -1
  48. package/css/tooltip.scss +1 -5
  49. package/css/tree.scss +1 -1
  50. package/css/typography.scss +3 -3
  51. package/css/virtual-dropdown.scss +3 -28
  52. package/js/bottom-sheet.d.ts +3 -1
  53. package/js/bottom-sheet.js +26 -27
  54. package/js/calendar.d.ts +7 -0
  55. package/js/calendar.js +14 -33
  56. package/js/carousel.d.ts +2 -0
  57. package/js/carousel.js +13 -5
  58. package/js/chart.d.ts +4 -0
  59. package/js/chart.js +13 -31
  60. package/js/code-viewer.d.ts +1 -0
  61. package/js/code-viewer.js +4 -0
  62. package/js/context-menu.d.ts +9 -2
  63. package/js/context-menu.js +17 -14
  64. package/js/datepicker.d.ts +4 -0
  65. package/js/datepicker.js +26 -11
  66. package/js/dropdown.d.ts +3 -3
  67. package/js/dropdown.js +6 -9
  68. package/js/editor.d.ts +1 -0
  69. package/js/editor.js +9 -3
  70. package/js/file-uploader.d.ts +4 -0
  71. package/js/file-uploader.js +52 -43
  72. package/js/flyout-menu.d.ts +5 -3
  73. package/js/flyout-menu.js +23 -46
  74. package/js/gallery.d.ts +3 -0
  75. package/js/gallery.js +22 -24
  76. package/js/group-picker.d.ts +5 -0
  77. package/js/group-picker.js +12 -17
  78. package/js/lightbox.d.ts +3 -0
  79. package/js/lightbox.js +12 -6
  80. package/js/modal.d.ts +3 -1
  81. package/js/modal.js +14 -11
  82. package/js/popover.d.ts +2 -0
  83. package/js/popover.js +26 -30
  84. package/js/position.d.ts +2 -0
  85. package/js/position.js +1 -5
  86. package/js/push-menu.d.ts +2 -0
  87. package/js/push-menu.js +22 -35
  88. package/js/range-slider.d.ts +1 -0
  89. package/js/range-slider.js +5 -3
  90. package/js/scroll.d.ts +2 -0
  91. package/js/scroll.js +1 -0
  92. package/js/scrollbar.d.ts +2 -0
  93. package/js/scrollbar.js +24 -36
  94. package/js/select.d.ts +1 -0
  95. package/js/select.js +5 -10
  96. package/js/sidebar-nav.d.ts +2 -0
  97. package/js/sidebar-nav.js +8 -0
  98. package/js/stepper.d.ts +2 -0
  99. package/js/stepper.js +7 -1
  100. package/js/table.d.ts +4 -0
  101. package/js/table.js +15 -22
  102. package/js/tabs.d.ts +2 -0
  103. package/js/tabs.js +6 -14
  104. package/js/theme.d.ts +1 -0
  105. package/js/theme.js +5 -13
  106. package/js/timepicker.d.ts +3 -0
  107. package/js/timepicker.js +81 -67
  108. package/js/toast.d.ts +3 -0
  109. package/js/toast.js +24 -15
  110. package/js/tooltip.d.ts +2 -0
  111. package/js/tooltip.js +21 -19
  112. package/js/tree.d.ts +3 -0
  113. package/js/tree.js +13 -0
  114. package/js/utils.d.ts +1 -3
  115. package/js/utils.js +0 -3
  116. package/js/virtual-dropdown.d.ts +3 -0
  117. package/js/virtual-dropdown.js +25 -0
  118. package/package.json +2 -2
@@ -1,9 +1,14 @@
1
+ /** Right-click context menu with keyboard navigation and nested submenu support. */
1
2
  class ContextMenu {
2
- constructor(selectorOrElement, items) {
3
- this.menuEl = null;
4
- this.currentTarget = null;
5
- this.abortController = new AbortController();
3
+ items;
4
+ targets;
5
+ menuEl = null;
6
+ currentTarget = null;
7
+ abortController = new AbortController();
8
+ spritePath;
9
+ constructor(selectorOrElement, items, options = {}) {
6
10
  this.items = items;
11
+ this.spritePath = options.spritePath ?? null;
7
12
  if (typeof selectorOrElement === 'string') {
8
13
  this.targets = Array.from(document.querySelectorAll(selectorOrElement));
9
14
  }
@@ -25,7 +30,6 @@ class ContextMenu {
25
30
  }, { signal });
26
31
  });
27
32
  document.addEventListener('click', () => this.close(), { signal });
28
- // Close on right-click outside the menu
29
33
  document.addEventListener('contextmenu', (e) => {
30
34
  if (this.menuEl && !this.menuEl.contains(e.target)) {
31
35
  this.close();
@@ -50,7 +54,6 @@ class ContextMenu {
50
54
  this.activateFocused();
51
55
  }
52
56
  }, { signal });
53
- // Close on scroll outside the menu
54
57
  window.addEventListener('scroll', (e) => {
55
58
  if (!this.menuEl?.contains(e.target))
56
59
  this.close();
@@ -61,14 +64,12 @@ class ContextMenu {
61
64
  this.close();
62
65
  this.menuEl = this.buildMenu(this.items);
63
66
  document.body.appendChild(this.menuEl);
64
- // Use offsetWidth/offsetHeight — unaffected by CSS transform
65
67
  const w = this.menuEl.offsetWidth;
66
68
  const h = this.menuEl.offsetHeight;
67
69
  const vw = window.innerWidth;
68
70
  const vh = window.innerHeight;
69
71
  const left = x + w > vw ? vw - w - 8 : x;
70
72
  const top = y + h > vh ? vh - h - 8 : y;
71
- // Set transform-origin to match the corner the menu opens from
72
73
  const originX = x + w > vw ? 'right' : 'left';
73
74
  const originY = y + h > vh ? 'bottom' : 'top';
74
75
  this.menuEl.style.left = `${left}px`;
@@ -82,7 +83,6 @@ class ContextMenu {
82
83
  const el = this.menuEl;
83
84
  this.menuEl = null;
84
85
  el.classList.remove('is-visible');
85
- // Wait for exit transition then remove from DOM
86
86
  el.addEventListener('transitionend', () => el.remove(), { once: true });
87
87
  setTimeout(() => el.isConnected && el.remove(), 200);
88
88
  }
@@ -116,11 +116,16 @@ class ContextMenu {
116
116
  li.classList.add('is-destructive');
117
117
  if (def.submenu)
118
118
  li.classList.add('has-submenu');
119
- // Always render icon slot — keeps label column aligned across all items
120
119
  const iconWrap = document.createElement('span');
121
120
  iconWrap.className = 'context-menu-icon';
122
- if (def.icon) {
123
- iconWrap.innerHTML = `<svg class="icon-svg"><use href="svg-icons/icons.svg#${def.icon}"/></svg>`;
121
+ if (def.icon && this.spritePath) {
122
+ const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
123
+ svgEl.setAttribute('aria-hidden', 'true');
124
+ svgEl.setAttribute('fill', 'currentColor');
125
+ const useEl = document.createElementNS('http://www.w3.org/2000/svg', 'use');
126
+ useEl.setAttribute('href', `${this.spritePath}#${def.icon}`);
127
+ svgEl.appendChild(useEl);
128
+ iconWrap.appendChild(svgEl);
124
129
  }
125
130
  li.appendChild(iconWrap);
126
131
  const label = document.createElement('span');
@@ -139,7 +144,6 @@ class ContextMenu {
139
144
  li.appendChild(chevron);
140
145
  const submenuEl = this.buildMenu(def.submenu);
141
146
  li.appendChild(submenuEl);
142
- // Determine flip synchronously from parent position — no rAF flash
143
147
  const shouldFlip = () => {
144
148
  const rect = li.getBoundingClientRect();
145
149
  return rect.right + submenuEl.offsetWidth > window.innerWidth;
@@ -179,7 +183,6 @@ class ContextMenu {
179
183
  return li;
180
184
  }
181
185
  closeAllSubmenus(menu) {
182
- // Only close direct-child submenus of this menu level
183
186
  Array.from(menu.children).forEach((child) => {
184
187
  child.classList.remove('is-active');
185
188
  });
@@ -1,7 +1,9 @@
1
+ /** Localised day and month names for the DatePicker. */
1
2
  interface DatePickerLocales {
2
3
  days: string[];
3
4
  months: string[];
4
5
  }
6
+ /** Configuration options for the DatePicker. */
5
7
  interface DatePickerOptions {
6
8
  mode?: 'single' | 'range';
7
9
  startDay?: number;
@@ -10,10 +12,12 @@ interface DatePickerOptions {
10
12
  format?: (date: Date) => string;
11
13
  onSelect?: (date: Date | DateRange) => void;
12
14
  }
15
+ /** A date range with optional start and end dates. */
13
16
  interface DateRange {
14
17
  start: Date | null;
15
18
  end: Date | null;
16
19
  }
20
+ /** Calendar-based date (or date-range) picker that attaches to an input element. */
17
21
  declare class DatePicker {
18
22
  private input;
19
23
  private options;
package/js/datepicker.js CHANGED
@@ -1,6 +1,23 @@
1
+ import { computePosition } from './position.js';
2
+ /** Calendar-based date (or date-range) picker that attaches to an input element. */
1
3
  class DatePicker {
4
+ input;
5
+ options;
6
+ currentDate;
7
+ selectedDate;
8
+ rangeStart;
9
+ rangeEnd;
10
+ viewYear;
11
+ viewMonth;
12
+ viewMode;
13
+ yearRangeStart;
14
+ selectedHours;
15
+ selectedMinutes;
16
+ calendar;
17
+ backdrop;
18
+ handleDocumentClick;
19
+ abortController = new AbortController();
2
20
  constructor(elementOrSelector, options = {}) {
3
- this.abortController = new AbortController();
4
21
  this.input = typeof elementOrSelector === 'string'
5
22
  ? document.querySelector(elementOrSelector)
6
23
  : elementOrSelector;
@@ -96,14 +113,14 @@ class DatePicker {
96
113
  this.backdrop.classList.remove('visible');
97
114
  document.body.style.overflow = '';
98
115
  if (this.input) {
99
- const rect = this.input.getBoundingClientRect();
100
- const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
101
- const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
102
- this.calendar.style.top = `${rect.bottom + scrollTop + 5}px`;
103
- this.calendar.style.left = `${rect.left + scrollLeft}px`;
104
- if (rect.left + 320 > window.innerWidth) {
105
- this.calendar.style.left = `${rect.right + scrollLeft - 320}px`;
106
- }
116
+ this.calendar.style.display = 'block';
117
+ this.calendar.style.visibility = 'hidden';
118
+ const calRect = this.calendar.getBoundingClientRect();
119
+ this.calendar.style.display = '';
120
+ this.calendar.style.visibility = '';
121
+ const { left, top } = computePosition(this.input.getBoundingClientRect(), calRect, { placement: 'bottom', align: 'start', offset: 5 });
122
+ this.calendar.style.top = `${top}px`;
123
+ this.calendar.style.left = `${left}px`;
107
124
  }
108
125
  setTimeout(() => {
109
126
  if (this.calendar.classList.contains('visible')) {
@@ -345,7 +362,6 @@ class DatePicker {
345
362
  wrapper.appendChild(label);
346
363
  const controls = document.createElement('div');
347
364
  controls.className = 'datepicker-time-controls';
348
- // Hours spinner
349
365
  const hoursSpinner = this.createSpinner(this.selectedHours, 0, 23, (value) => {
350
366
  this.selectedHours = value;
351
367
  this.applyTimeToSelection();
@@ -353,7 +369,6 @@ class DatePicker {
353
369
  const separator = document.createElement('span');
354
370
  separator.className = 'datepicker-time-separator';
355
371
  separator.textContent = ':';
356
- // Minutes spinner
357
372
  const minutesSpinner = this.createSpinner(this.selectedMinutes, 0, 59, (value) => {
358
373
  this.selectedMinutes = value;
359
374
  this.applyTimeToSelection();
package/js/dropdown.d.ts CHANGED
@@ -1,11 +1,14 @@
1
+ /** Configuration options for a Dropdown instance. */
1
2
  interface DropdownOptions {
2
3
  closeOnSelect?: boolean;
3
4
  allowMultipleOpen?: boolean;
4
5
  }
6
+ /** Event detail payload for the `dropdown-select` custom event. */
5
7
  interface DropdownSelectDetail {
6
8
  text: string;
7
9
  element: HTMLElement;
8
10
  }
11
+ /** Hierarchical dropdown menu with optional multi-open and close-on-select behaviour. */
9
12
  declare class Dropdown {
10
13
  private container;
11
14
  private trigger;
@@ -22,9 +25,6 @@ declare class Dropdown {
22
25
  private toggleSubmenu;
23
26
  private closeAllSubmenus;
24
27
  private handleSelection;
25
- /**
26
- * Cleanup method to remove event listeners
27
- */
28
28
  destroy(): void;
29
29
  }
30
30
  export { Dropdown, DropdownSelectDetail };
package/js/dropdown.js CHANGED
@@ -1,4 +1,10 @@
1
+ /** Hierarchical dropdown menu with optional multi-open and close-on-select behaviour. */
1
2
  class Dropdown {
3
+ container;
4
+ trigger;
5
+ menu;
6
+ options;
7
+ abortController;
2
8
  constructor(selector, options = {}) {
3
9
  const container = document.querySelector(selector);
4
10
  if (!container) {
@@ -26,18 +32,15 @@ class Dropdown {
26
32
  }
27
33
  attachEventListeners() {
28
34
  const { signal } = this.abortController;
29
- // Toggle main dropdown
30
35
  this.trigger.addEventListener('click', (e) => {
31
36
  e.stopPropagation();
32
37
  this.toggle();
33
38
  }, { signal });
34
- // Close when clicking outside
35
39
  document.addEventListener('click', (e) => {
36
40
  if (!this.container.contains(e.target)) {
37
41
  this.close();
38
42
  }
39
43
  }, { signal });
40
- // Handle item clicks using event delegation
41
44
  this.menu.addEventListener('click', (e) => {
42
45
  e.stopPropagation();
43
46
  const target = e.target;
@@ -78,7 +81,6 @@ class Dropdown {
78
81
  }
79
82
  toggleSubmenu(li) {
80
83
  const isOpening = !li.classList.contains('open');
81
- // Close siblings if not allowing multiple open menus
82
84
  if (isOpening && !this.options.allowMultipleOpen) {
83
85
  const parent = li.parentElement;
84
86
  if (parent) {
@@ -86,7 +88,6 @@ class Dropdown {
86
88
  siblings.forEach((sibling) => {
87
89
  if (sibling !== li && sibling.classList.contains('open')) {
88
90
  sibling.classList.remove('open');
89
- // Close deeply nested open items
90
91
  const deepOpenItems = sibling.querySelectorAll('.open');
91
92
  deepOpenItems.forEach((el) => el.classList.remove('open'));
92
93
  }
@@ -101,7 +102,6 @@ class Dropdown {
101
102
  }
102
103
  handleSelection(item) {
103
104
  const text = item.textContent?.trim() ?? '';
104
- // Dispatch custom event with proper typing
105
105
  const event = new CustomEvent('dropdown-select', {
106
106
  detail: {
107
107
  text,
@@ -111,9 +111,6 @@ class Dropdown {
111
111
  });
112
112
  this.container.dispatchEvent(event);
113
113
  }
114
- /**
115
- * Cleanup method to remove event listeners
116
- */
117
114
  destroy() {
118
115
  this.abortController.abort();
119
116
  this.close();
package/js/editor.d.ts CHANGED
@@ -3,6 +3,7 @@ interface EditorOptions {
3
3
  * without #code, #preview, or #sidePanel in the DOM. */
4
4
  simple?: boolean;
5
5
  }
6
+ /** Rich-text editor built on contenteditable with undo/redo and code/preview panels. */
6
7
  declare class Editor {
7
8
  private readonly editable;
8
9
  private readonly code;
package/js/editor.js CHANGED
@@ -1,9 +1,15 @@
1
1
  import { sanitizeHtml } from './utils.js';
2
+ /** Rich-text editor built on contenteditable with undo/redo and code/preview panels. */
2
3
  class Editor {
4
+ editable;
5
+ code;
6
+ preview;
7
+ sidePanel;
8
+ wordCount;
9
+ undoStack = [];
10
+ redoStack = [];
11
+ abortController = new AbortController();
3
12
  constructor(options = {}) {
4
- this.undoStack = [];
5
- this.redoStack = [];
6
- this.abortController = new AbortController();
7
13
  const editable = document.getElementById('editable');
8
14
  if (!editable) {
9
15
  throw new Error('Editor: #editable element not found');
@@ -1,17 +1,21 @@
1
+ /** Event detail payload for the `upload-completed` custom event. */
1
2
  interface UploadCompletedDetail {
2
3
  fileCount: number;
3
4
  files: File[];
4
5
  results: PromiseSettledResult<unknown>[];
5
6
  }
7
+ /** Event detail payload for the `file-validation-error` custom event. */
6
8
  interface FileValidationErrorDetail {
7
9
  file: File;
8
10
  reason: 'size' | 'type';
9
11
  }
12
+ /** Configuration options for the FileUploader. */
10
13
  interface FileUploaderConfig {
11
14
  uploadUrl?: string;
12
15
  maxFileSize?: number;
13
16
  allowedTypes?: string[];
14
17
  }
18
+ /** Drag-and-drop file uploader with progress tracking and XHR-based uploads. */
15
19
  declare class FileUploader {
16
20
  private container;
17
21
  private dropZone;
@@ -1,49 +1,17 @@
1
1
  import { escapeHtml } from './utils.js';
2
+ /** Drag-and-drop file uploader with progress tracking and XHR-based uploads. */
2
3
  class FileUploader {
4
+ container;
5
+ dropZone;
6
+ fileInput;
7
+ fileList;
8
+ uploadBtn;
9
+ files = new Map();
10
+ uploadUrl;
11
+ maxFileSize;
12
+ allowedTypes;
13
+ abortControllers = new Map();
3
14
  constructor(elementOrSelector, config = {}) {
4
- this.files = new Map();
5
- this.abortControllers = new Map();
6
- this.preventDefaults = (e) => {
7
- e.preventDefault();
8
- e.stopPropagation();
9
- };
10
- this.handleDragEnter = () => {
11
- this.dropZone.classList.add('drag-over');
12
- };
13
- this.handleDragLeave = () => {
14
- this.dropZone.classList.remove('drag-over');
15
- };
16
- this.handleDrop = (e) => {
17
- const droppedFiles = e.dataTransfer?.files;
18
- if (droppedFiles) {
19
- this.handleFiles(droppedFiles);
20
- }
21
- };
22
- this.handleDropZoneClick = () => {
23
- this.fileInput.click();
24
- };
25
- this.handleFileInputChange = (e) => {
26
- const target = e.target;
27
- if (target.files) {
28
- this.handleFiles(target.files);
29
- target.value = '';
30
- }
31
- };
32
- this.handleUploadClick = async () => {
33
- if (this.files.size === 0)
34
- return;
35
- this.uploadBtn.disabled = true;
36
- this.uploadBtn.textContent = 'Uploading...';
37
- const uploadPromises = Array.from(this.files.values()).map(({ file, element }) => this.uploadFile(file, element));
38
- const results = await Promise.allSettled(uploadPromises);
39
- this.uploadBtn.textContent = 'Upload Complete';
40
- setTimeout(() => {
41
- this.dispatchUploadCompletedEvent(results);
42
- this.fileList.innerHTML = '';
43
- this.files.clear();
44
- this.updateUploadButton();
45
- }, 1500);
46
- };
47
15
  const container = typeof elementOrSelector === 'string'
48
16
  ? document.querySelector(elementOrSelector)
49
17
  : elementOrSelector;
@@ -89,6 +57,47 @@ class FileUploader {
89
57
  this.fileInput.addEventListener('change', this.handleFileInputChange);
90
58
  this.uploadBtn.addEventListener('click', this.handleUploadClick);
91
59
  }
60
+ preventDefaults = (e) => {
61
+ e.preventDefault();
62
+ e.stopPropagation();
63
+ };
64
+ handleDragEnter = () => {
65
+ this.dropZone.classList.add('drag-over');
66
+ };
67
+ handleDragLeave = () => {
68
+ this.dropZone.classList.remove('drag-over');
69
+ };
70
+ handleDrop = (e) => {
71
+ const droppedFiles = e.dataTransfer?.files;
72
+ if (droppedFiles) {
73
+ this.handleFiles(droppedFiles);
74
+ }
75
+ };
76
+ handleDropZoneClick = () => {
77
+ this.fileInput.click();
78
+ };
79
+ handleFileInputChange = (e) => {
80
+ const target = e.target;
81
+ if (target.files) {
82
+ this.handleFiles(target.files);
83
+ target.value = '';
84
+ }
85
+ };
86
+ handleUploadClick = async () => {
87
+ if (this.files.size === 0)
88
+ return;
89
+ this.uploadBtn.disabled = true;
90
+ this.uploadBtn.textContent = 'Uploading...';
91
+ const uploadPromises = Array.from(this.files.values()).map(({ file, element }) => this.uploadFile(file, element));
92
+ const results = await Promise.allSettled(uploadPromises);
93
+ this.uploadBtn.textContent = 'Upload Complete';
94
+ setTimeout(() => {
95
+ this.dispatchUploadCompletedEvent(results);
96
+ this.fileList.innerHTML = '';
97
+ this.files.clear();
98
+ this.updateUploadButton();
99
+ }, 1500);
100
+ };
92
101
  handleFiles(fileList) {
93
102
  Array.from(fileList).forEach(file => {
94
103
  const key = this.fileKey(file);
@@ -1,3 +1,4 @@
1
+ /** Configuration options for the FlyoutMenu. */
1
2
  interface FlyoutMenuOptions {
2
3
  triggerSelector?: string;
3
4
  menuSelector?: string;
@@ -11,6 +12,7 @@ interface FlyoutMenuOptions {
11
12
  enableHeader?: boolean;
12
13
  enableFooter?: boolean;
13
14
  }
15
+ /** Off-canvas flyout navigation with nested submenu support. */
14
16
  declare class FlyoutMenu {
15
17
  private options;
16
18
  private menuTrigger;
@@ -19,7 +21,7 @@ declare class FlyoutMenu {
19
21
  private closeBtn;
20
22
  private submenuToggles;
21
23
  private menuLinks;
22
- private submenuHandlers;
24
+ private abortController;
23
25
  constructor(options?: FlyoutMenuOptions);
24
26
  private init;
25
27
  private hydrateMenu;
@@ -27,8 +29,8 @@ declare class FlyoutMenu {
27
29
  private renderHeader;
28
30
  private renderFooter;
29
31
  private bindEvents;
30
- private open;
31
- private close;
32
+ open: () => void;
33
+ close: () => void;
32
34
  private handleSubmenu;
33
35
  private handleKeydown;
34
36
  setDirection(direction: 'left' | 'right'): void;
package/js/flyout-menu.js CHANGED
@@ -1,9 +1,14 @@
1
+ /** Off-canvas flyout navigation with nested submenu support. */
1
2
  class FlyoutMenu {
3
+ options;
4
+ menuTrigger;
5
+ flyoutMenu;
6
+ flyoutOverlay;
7
+ closeBtn = null;
8
+ submenuToggles = null;
9
+ menuLinks = null;
10
+ abortController = new AbortController();
2
11
  constructor(options = {}) {
3
- this.closeBtn = null;
4
- this.submenuToggles = null;
5
- this.menuLinks = null;
6
- this.submenuHandlers = new Map();
7
12
  this.options = {
8
13
  triggerSelector: '.menu-trigger',
9
14
  menuSelector: '#flyoutMenu',
@@ -21,10 +26,6 @@ class FlyoutMenu {
21
26
  this.menuTrigger = document.querySelector(this.options.triggerSelector);
22
27
  this.flyoutMenu = document.querySelector(this.options.menuSelector);
23
28
  this.flyoutOverlay = document.querySelector(this.options.overlaySelector);
24
- this.open = this.open.bind(this);
25
- this.close = this.close.bind(this);
26
- this.handleSubmenu = this.handleSubmenu.bind(this);
27
- this.handleKeydown = this.handleKeydown.bind(this);
28
29
  this.init();
29
30
  }
30
31
  init() {
@@ -54,16 +55,13 @@ class FlyoutMenu {
54
55
  processListItems(ul) {
55
56
  const items = Array.from(ul.children);
56
57
  items.forEach((li, index) => {
57
- // Check if it has a nested UL
58
58
  const nestedUl = li.querySelector('ul');
59
59
  if (nestedUl) {
60
60
  li.classList.add('has-submenu');
61
61
  nestedUl.classList.add('submenu');
62
- // Get text content (excluding nested UL text)
63
62
  const textNode = Array.from(li.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() !== '');
64
63
  const text = textNode?.textContent?.trim() || 'Menu Item';
65
64
  textNode?.remove();
66
- // Create Toggle Button
67
65
  const button = document.createElement('button');
68
66
  button.className = 'submenu-toggle';
69
67
  button.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
@@ -74,11 +72,9 @@ class FlyoutMenu {
74
72
  </svg>
75
73
  `;
76
74
  li.insertBefore(button, nestedUl);
77
- // Recursively process nested UL
78
75
  this.processListItems(nestedUl);
79
76
  }
80
77
  else {
81
- // Leaf node - ensure it has a link
82
78
  const link = li.querySelector('a');
83
79
  if (link) {
84
80
  link.style.setProperty('--delay', `${(index + 1) * 0.1}s`);
@@ -113,36 +109,30 @@ class FlyoutMenu {
113
109
  this.flyoutMenu.append(footer);
114
110
  }
115
111
  bindEvents() {
116
- // Open
117
- this.menuTrigger?.addEventListener('click', this.open);
118
- // Close
119
- this.closeBtn?.addEventListener('click', this.close);
120
- this.flyoutOverlay?.addEventListener('click', this.close);
121
- // Submenus
112
+ const sig = { signal: this.abortController.signal };
113
+ this.menuTrigger?.addEventListener('click', this.open, sig);
114
+ this.closeBtn?.addEventListener('click', this.close, sig);
115
+ this.flyoutOverlay?.addEventListener('click', this.close, sig);
122
116
  this.submenuToggles?.forEach(toggle => {
123
- const handler = (e) => this.handleSubmenu(e, toggle);
124
- this.submenuHandlers.set(toggle, handler);
125
- toggle.addEventListener('click', handler);
117
+ toggle.addEventListener('click', (e) => this.handleSubmenu(e, toggle), sig);
126
118
  });
127
- // Close on Link Click
128
119
  this.menuLinks?.forEach(link => {
129
- link.addEventListener('click', this.close);
120
+ link.addEventListener('click', this.close, sig);
130
121
  });
131
- // Keyboard navigation
132
- document.addEventListener('keydown', this.handleKeydown);
122
+ document.addEventListener('keydown', this.handleKeydown, sig);
133
123
  }
134
- open() {
124
+ open = () => {
135
125
  this.flyoutMenu?.classList.add('is-open');
136
126
  this.flyoutOverlay?.classList.add('is-visible');
137
127
  document.body.style.overflow = 'hidden';
138
128
  this.menuTrigger?.setAttribute('aria-expanded', 'true');
139
- }
140
- close() {
129
+ };
130
+ close = () => {
141
131
  this.flyoutMenu?.classList.remove('is-open');
142
132
  this.flyoutOverlay?.classList.remove('is-visible');
143
133
  document.body.style.overflow = '';
144
134
  this.menuTrigger?.setAttribute('aria-expanded', 'false');
145
- }
135
+ };
146
136
  handleSubmenu(e, toggle) {
147
137
  e.preventDefault();
148
138
  e.stopPropagation();
@@ -151,7 +141,6 @@ class FlyoutMenu {
151
141
  const parentUl = parentLi?.parentElement;
152
142
  if (!parentUl || !parentLi)
153
143
  return;
154
- // Close other submenus at the same level
155
144
  const siblings = Array.from(parentUl.children);
156
145
  siblings.forEach(sibling => {
157
146
  if (sibling !== parentLi) {
@@ -166,11 +155,11 @@ class FlyoutMenu {
166
155
  toggle.classList.toggle('active');
167
156
  submenu?.classList.toggle('is-open');
168
157
  }
169
- handleKeydown(e) {
158
+ handleKeydown = (e) => {
170
159
  if (e.key === 'Escape' && this.flyoutMenu?.classList.contains('is-open')) {
171
160
  this.close();
172
161
  }
173
- }
162
+ };
174
163
  setDirection(direction) {
175
164
  if (!this.flyoutMenu)
176
165
  return;
@@ -182,19 +171,7 @@ class FlyoutMenu {
182
171
  this.options.direction = direction;
183
172
  }
184
173
  destroy() {
185
- this.menuTrigger?.removeEventListener('click', this.open);
186
- this.closeBtn?.removeEventListener('click', this.close);
187
- this.flyoutOverlay?.removeEventListener('click', this.close);
188
- this.submenuToggles?.forEach(toggle => {
189
- const handler = this.submenuHandlers.get(toggle);
190
- if (handler)
191
- toggle.removeEventListener('click', handler);
192
- });
193
- this.submenuHandlers.clear();
194
- this.menuLinks?.forEach(link => {
195
- link.removeEventListener('click', this.close);
196
- });
197
- document.removeEventListener('keydown', this.handleKeydown);
174
+ this.abortController.abort();
198
175
  document.body.style.overflow = '';
199
176
  }
200
177
  }
package/js/gallery.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ /** A single image record for MasonryGallery. */
1
2
  interface ImageData {
2
3
  src: string;
3
4
  title: string;
4
5
  desc: string;
5
6
  }
7
+ /** Configuration options for MasonryGallery. */
6
8
  interface MasonryGalleryOptions {
7
9
  fetchFunction: () => Promise<ImageData[]>;
8
10
  minColumnWidth?: number;
@@ -10,6 +12,7 @@ interface MasonryGalleryOptions {
10
12
  loaderSelector?: string;
11
13
  reload?: number;
12
14
  }
15
+ /** Infinite-scroll masonry gallery that distributes images across dynamically sized columns. */
13
16
  declare class MasonryGallery {
14
17
  private container;
15
18
  private readonly loader;