@dodlhuat/basix 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +651 -482
- package/css/badge.scss +104 -0
- package/css/bottom-sheet.scss +192 -0
- package/css/breadcrumb.scss +158 -0
- package/css/context-menu.scss +182 -0
- package/css/editor.scss +628 -461
- package/css/form.scss +139 -0
- package/css/stepper.scss +212 -0
- package/css/style.css +1495 -70
- package/css/style.css.map +1 -1
- package/css/style.min.css +1 -1
- package/css/style.scss +7 -1
- package/css/typography.scss +194 -161
- package/js/bottom-sheet.js +173 -0
- package/js/bottom-sheet.ts +222 -0
- package/js/carousel.js +26 -13
- package/js/context-menu.js +212 -0
- package/js/context-menu.ts +252 -0
- package/js/editor.js +46 -32
- package/js/editor.ts +56 -37
- package/js/gallery.js +11 -10
- package/js/index.js +472 -374
- package/js/index.ts +116 -2
- package/js/push-menu.js +113 -113
- package/js/stepper.js +80 -0
- package/js/stepper.ts +104 -0
- package/js/timepicker.js +21 -8
- package/package.json +3 -2
- package/fonts/Outfit-VariableFont_wght.woff +0 -0
- package/fonts/material-icons.woff2 +0 -0
- package/icons/activity-outline.svg +0 -1
- package/icons/alert-circle-outline.svg +0 -1
- package/icons/alert-triangle-outline.svg +0 -1
- package/icons/archive-outline.svg +0 -1
- package/icons/arrow-back-outline.svg +0 -1
- package/icons/arrow-circle-down-outline.svg +0 -1
- package/icons/arrow-circle-left-outline.svg +0 -1
- package/icons/arrow-circle-right-outline.svg +0 -1
- package/icons/arrow-circle-up-outline.svg +0 -1
- package/icons/arrow-down-outline.svg +0 -1
- package/icons/arrow-downward-outline.svg +0 -1
- package/icons/arrow-forward-outline.svg +0 -1
- package/icons/arrow-ios-back-outline.svg +0 -1
- package/icons/arrow-ios-downward-outline.svg +0 -1
- package/icons/arrow-ios-forward-outline.svg +0 -1
- package/icons/arrow-ios-upward-outline.svg +0 -1
- package/icons/arrow-left-outline.svg +0 -1
- package/icons/arrow-right-outline.svg +0 -1
- package/icons/arrow-up-outline.svg +0 -1
- package/icons/arrow-upward-outline.svg +0 -1
- package/icons/arrowhead-down-outline.svg +0 -1
- package/icons/arrowhead-left-outline.svg +0 -1
- package/icons/arrowhead-right-outline.svg +0 -1
- package/icons/arrowhead-up-outline.svg +0 -1
- package/icons/at-outline.svg +0 -1
- package/icons/attach-2-outline.svg +0 -1
- package/icons/attach-outline.svg +0 -1
- package/icons/award-outline.svg +0 -1
- package/icons/backspace-outline.svg +0 -1
- package/icons/bar-chart-2-outline.svg +0 -1
- package/icons/bar-chart-outline.svg +0 -1
- package/icons/battery-outline.svg +0 -1
- package/icons/behance-outline.svg +0 -1
- package/icons/bell-off-outline.svg +0 -1
- package/icons/bell-outline.svg +0 -1
- package/icons/bluetooth-outline.svg +0 -1
- package/icons/book-open-outline.svg +0 -1
- package/icons/book-outline.svg +0 -1
- package/icons/bookmark-outline.svg +0 -1
- package/icons/briefcase-outline.svg +0 -1
- package/icons/browser-outline.svg +0 -1
- package/icons/brush-outline.svg +0 -1
- package/icons/bulb-outline.svg +0 -1
- package/icons/calendar-outline.svg +0 -1
- package/icons/camera-outline.svg +0 -1
- package/icons/car-outline.svg +0 -1
- package/icons/cast-outline.svg +0 -1
- package/icons/charging-outline.svg +0 -1
- package/icons/checkmark-circle-2-outline.svg +0 -1
- package/icons/checkmark-circle-outline.svg +0 -1
- package/icons/checkmark-outline.svg +0 -1
- package/icons/checkmark-square-2-outline.svg +0 -1
- package/icons/checkmark-square-outline.svg +0 -1
- package/icons/chevron-down-outline.svg +0 -1
- package/icons/chevron-left-outline.svg +0 -1
- package/icons/chevron-right-outline.svg +0 -1
- package/icons/chevron-up-outline.svg +0 -1
- package/icons/clipboard-outline.svg +0 -1
- package/icons/clock-outline.svg +0 -1
- package/icons/close-circle-outline.svg +0 -1
- package/icons/close-outline.svg +0 -1
- package/icons/close-square-outline.svg +0 -1
- package/icons/cloud-download-outline.svg +0 -1
- package/icons/cloud-upload-outline.svg +0 -1
- package/icons/code-download-outline.svg +0 -1
- package/icons/code-outline.svg +0 -1
- package/icons/collapse-outline.svg +0 -1
- package/icons/color-palette-outline.svg +0 -1
- package/icons/color-picker-outline.svg +0 -1
- package/icons/compass-outline.svg +0 -1
- package/icons/copy-outline.svg +0 -1
- package/icons/corner-down-left-outline.svg +0 -1
- package/icons/corner-down-right-outline.svg +0 -1
- package/icons/corner-left-down-outline.svg +0 -1
- package/icons/corner-left-up-outline.svg +0 -1
- package/icons/corner-right-down-outline.svg +0 -1
- package/icons/corner-right-up-outline.svg +0 -1
- package/icons/corner-up-left-outline.svg +0 -1
- package/icons/corner-up-right-outline.svg +0 -1
- package/icons/credit-card-outline.svg +0 -1
- package/icons/crop-outline.svg +0 -1
- package/icons/cube-outline.svg +0 -1
- package/icons/diagonal-arrow-left-down-outline.svg +0 -1
- package/icons/diagonal-arrow-left-up-outline.svg +0 -1
- package/icons/diagonal-arrow-right-down-outline.svg +0 -1
- package/icons/diagonal-arrow-right-up-outline.svg +0 -1
- package/icons/done-all-outline.svg +0 -1
- package/icons/download-outline.svg +0 -1
- package/icons/droplet-off-outline.svg +0 -1
- package/icons/droplet-outline.svg +0 -1
- package/icons/edit-2-outline.svg +0 -1
- package/icons/edit-outline.svg +0 -1
- package/icons/email-outline.svg +0 -1
- package/icons/expand-outline.svg +0 -1
- package/icons/external-link-outline.svg +0 -1
- package/icons/eye-off-2-outline.svg +0 -1
- package/icons/eye-off-outline.svg +0 -1
- package/icons/eye-outline.svg +0 -1
- package/icons/facebook-outline.svg +0 -1
- package/icons/file-add-outline.svg +0 -1
- package/icons/file-outline.svg +0 -1
- package/icons/file-remove-outline.svg +0 -1
- package/icons/file-text-outline.svg +0 -1
- package/icons/film-outline.svg +0 -1
- package/icons/flag-outline.svg +0 -1
- package/icons/flash-off-outline.svg +0 -1
- package/icons/flash-outline.svg +0 -1
- package/icons/flip-2-outline.svg +0 -1
- package/icons/flip-outline.svg +0 -1
- package/icons/folder-add-outline.svg +0 -1
- package/icons/folder-outline.svg +0 -1
- package/icons/folder-remove-outline.svg +0 -1
- package/icons/funnel-outline.svg +0 -1
- package/icons/gift-outline.svg +0 -1
- package/icons/github-outline.svg +0 -1
- package/icons/globe-2-outline.svg +0 -1
- package/icons/globe-outline.svg +0 -1
- package/icons/google-outline.svg +0 -1
- package/icons/grid-outline.svg +0 -1
- package/icons/hard-drive-outline.svg +0 -1
- package/icons/hash-outline.svg +0 -1
- package/icons/headphones-outline.svg +0 -1
- package/icons/heart-outline.svg +0 -1
- package/icons/home-outline.svg +0 -1
- package/icons/image-outline.svg +0 -1
- package/icons/inbox-outline.svg +0 -1
- package/icons/info-outline.svg +0 -1
- package/icons/keypad-outline.svg +0 -1
- package/icons/layers-outline.svg +0 -1
- package/icons/layout-outline.svg +0 -1
- package/icons/link-2-outline.svg +0 -1
- package/icons/link-outline.svg +0 -1
- package/icons/linkedin-outline.svg +0 -1
- package/icons/list-outline.svg +0 -1
- package/icons/loader-outline.svg +0 -1
- package/icons/lock-outline.svg +0 -1
- package/icons/log-in-outline.svg +0 -1
- package/icons/log-out-outline.svg +0 -1
- package/icons/map-outline.svg +0 -1
- package/icons/maximize-outline.svg +0 -1
- package/icons/menu-2-outline.svg +0 -1
- package/icons/menu-arrow-outline.svg +0 -1
- package/icons/menu-outline.svg +0 -1
- package/icons/message-circle-outline.svg +0 -1
- package/icons/message-square-outline.svg +0 -1
- package/icons/mic-off-outline.svg +0 -1
- package/icons/mic-outline.svg +0 -1
- package/icons/minimize-outline.svg +0 -1
- package/icons/minus-circle-outline.svg +0 -1
- package/icons/minus-outline.svg +0 -1
- package/icons/minus-square-outline.svg +0 -1
- package/icons/monitor-outline.svg +0 -1
- package/icons/moon-outline.svg +0 -1
- package/icons/more-horizontal-outline.svg +0 -1
- package/icons/more-vertical-outline.svg +0 -1
- package/icons/move-outline.svg +0 -1
- package/icons/music-outline.svg +0 -1
- package/icons/navigation-2-outline.svg +0 -1
- package/icons/navigation-outline.svg +0 -1
- package/icons/npm-outline.svg +0 -1
- package/icons/options-2-outline.svg +0 -1
- package/icons/options-outline.svg +0 -1
- package/icons/pantone-outline.svg +0 -1
- package/icons/paper-plane-outline.svg +0 -1
- package/icons/pause-circle-outline.svg +0 -1
- package/icons/people-outline.svg +0 -1
- package/icons/percent-outline.svg +0 -1
- package/icons/person-add-outline.svg +0 -1
- package/icons/person-delete-outline.svg +0 -1
- package/icons/person-done-outline.svg +0 -1
- package/icons/person-outline.svg +0 -1
- package/icons/person-remove-outline.svg +0 -1
- package/icons/phone-call-outline.svg +0 -1
- package/icons/phone-missed-outline.svg +0 -1
- package/icons/phone-off-outline.svg +0 -1
- package/icons/phone-outline.svg +0 -1
- package/icons/pie-chart-outline.svg +0 -1
- package/icons/pin-outline.svg +0 -1
- package/icons/play-circle-outline.svg +0 -1
- package/icons/plus-circle-outline.svg +0 -1
- package/icons/plus-outline.svg +0 -1
- package/icons/plus-square-outline.svg +0 -1
- package/icons/power-outline.svg +0 -1
- package/icons/pricetags-outline.svg +0 -1
- package/icons/printer-outline.svg +0 -1
- package/icons/question-mark-circle-outline.svg +0 -1
- package/icons/question-mark-outline.svg +0 -1
- package/icons/radio-button-off-outline.svg +0 -1
- package/icons/radio-button-on-outline.svg +0 -1
- package/icons/radio-outline.svg +0 -1
- package/icons/recording-outline.svg +0 -1
- package/icons/refresh-outline.svg +0 -1
- package/icons/repeat-outline.svg +0 -1
- package/icons/rewind-left-outline.svg +0 -1
- package/icons/rewind-right-outline.svg +0 -1
- package/icons/save-outline.svg +0 -1
- package/icons/scissors-outline.svg +0 -1
- package/icons/search-outline.svg +0 -1
- package/icons/settings-2-outline.svg +0 -1
- package/icons/settings-outline.svg +0 -1
- package/icons/shake-outline.svg +0 -1
- package/icons/share-outline.svg +0 -1
- package/icons/shield-off-outline.svg +0 -1
- package/icons/shield-outline.svg +0 -1
- package/icons/shopping-bag-outline.svg +0 -1
- package/icons/shopping-cart-outline.svg +0 -1
- package/icons/shuffle-2-outline.svg +0 -1
- package/icons/shuffle-outline.svg +0 -1
- package/icons/skip-back-outline.svg +0 -1
- package/icons/skip-forward-outline.svg +0 -1
- package/icons/slash-outline.svg +0 -1
- package/icons/smartphone-outline.svg +0 -1
- package/icons/smiling-face-outline.svg +0 -1
- package/icons/speaker-outline.svg +0 -1
- package/icons/square-outline.svg +0 -1
- package/icons/star-outline.svg +0 -1
- package/icons/stop-circle-outline.svg +0 -1
- package/icons/sun-outline.svg +0 -1
- package/icons/swap-outline.svg +0 -1
- package/icons/sync-outline.svg +0 -1
- package/icons/text-outline.svg +0 -1
- package/icons/thermometer-minus-outline.svg +0 -1
- package/icons/thermometer-outline.svg +0 -1
- package/icons/thermometer-plus-outline.svg +0 -1
- package/icons/toggle-left-outline.svg +0 -1
- package/icons/toggle-right-outline.svg +0 -1
- package/icons/trash-2-outline.svg +0 -1
- package/icons/trash-outline.svg +0 -1
- package/icons/trending-down-outline.svg +0 -1
- package/icons/trending-up-outline.svg +0 -1
- package/icons/tv-outline.svg +0 -1
- package/icons/twitter-outline.svg +0 -1
- package/icons/umbrella-outline.svg +0 -1
- package/icons/undo-outline.svg +0 -1
- package/icons/unlock-outline.svg +0 -1
- package/icons/upload-outline.svg +0 -1
- package/icons/video-off-outline.svg +0 -1
- package/icons/video-outline.svg +0 -1
- package/icons/volume-down-outline.svg +0 -1
- package/icons/volume-mute-outline.svg +0 -1
- package/icons/volume-off-outline.svg +0 -1
- package/icons/volume-up-outline.svg +0 -1
- package/icons/wifi-off-outline.svg +0 -1
- package/icons/wifi-outline.svg +0 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
interface ContextMenuItemDef {
|
|
2
|
+
label: string;
|
|
3
|
+
icon?: string;
|
|
4
|
+
shortcut?: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
destructive?: boolean;
|
|
7
|
+
action?: (target: HTMLElement) => void;
|
|
8
|
+
submenu?: ContextMenuInput[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type ContextMenuInput = ContextMenuItemDef | 'separator' | { group: string };
|
|
12
|
+
|
|
13
|
+
class ContextMenu {
|
|
14
|
+
private items: ContextMenuInput[];
|
|
15
|
+
private targets: HTMLElement[];
|
|
16
|
+
private menuEl: HTMLElement | null = null;
|
|
17
|
+
private currentTarget: HTMLElement | null = null;
|
|
18
|
+
private abortController = new AbortController();
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
selectorOrElement: string | HTMLElement | HTMLElement[],
|
|
22
|
+
items: ContextMenuInput[]
|
|
23
|
+
) {
|
|
24
|
+
this.items = items;
|
|
25
|
+
|
|
26
|
+
if (typeof selectorOrElement === 'string') {
|
|
27
|
+
this.targets = Array.from(document.querySelectorAll<HTMLElement>(selectorOrElement));
|
|
28
|
+
} else if (Array.isArray(selectorOrElement)) {
|
|
29
|
+
this.targets = selectorOrElement;
|
|
30
|
+
} else {
|
|
31
|
+
this.targets = [selectorOrElement];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.init();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private init(): void {
|
|
38
|
+
const { signal } = this.abortController;
|
|
39
|
+
|
|
40
|
+
this.targets.forEach((target) => {
|
|
41
|
+
target.addEventListener('contextmenu', (e: MouseEvent) => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
this.currentTarget = target;
|
|
44
|
+
this.open(e.clientX, e.clientY);
|
|
45
|
+
}, { signal });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
document.addEventListener('click', () => this.close(), { signal });
|
|
49
|
+
|
|
50
|
+
// Close on right-click outside the menu
|
|
51
|
+
document.addEventListener('contextmenu', (e: MouseEvent) => {
|
|
52
|
+
if (this.menuEl && !this.menuEl.contains(e.target as Node)) {
|
|
53
|
+
this.close();
|
|
54
|
+
}
|
|
55
|
+
}, { signal, capture: true });
|
|
56
|
+
|
|
57
|
+
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
58
|
+
if (!this.menuEl) return;
|
|
59
|
+
if (e.key === 'Escape') { this.close(); }
|
|
60
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); this.moveFocus(1); }
|
|
61
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); this.moveFocus(-1); }
|
|
62
|
+
if (e.key === 'Enter') { e.preventDefault(); this.activateFocused(); }
|
|
63
|
+
}, { signal });
|
|
64
|
+
|
|
65
|
+
// Close on scroll outside the menu
|
|
66
|
+
window.addEventListener('scroll', (e: Event) => {
|
|
67
|
+
if (!this.menuEl?.contains(e.target as Node)) this.close();
|
|
68
|
+
}, { signal, capture: true });
|
|
69
|
+
|
|
70
|
+
window.addEventListener('resize', () => this.close(), { signal });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private open(x: number, y: number): void {
|
|
74
|
+
this.close();
|
|
75
|
+
|
|
76
|
+
this.menuEl = this.buildMenu(this.items);
|
|
77
|
+
document.body.appendChild(this.menuEl);
|
|
78
|
+
|
|
79
|
+
// Use offsetWidth/offsetHeight — unaffected by CSS transform
|
|
80
|
+
const w = this.menuEl.offsetWidth;
|
|
81
|
+
const h = this.menuEl.offsetHeight;
|
|
82
|
+
const vw = window.innerWidth;
|
|
83
|
+
const vh = window.innerHeight;
|
|
84
|
+
|
|
85
|
+
const left = x + w > vw ? vw - w - 8 : x;
|
|
86
|
+
const top = y + h > vh ? vh - h - 8 : y;
|
|
87
|
+
|
|
88
|
+
// Set transform-origin to match the corner the menu opens from
|
|
89
|
+
const originX = x + w > vw ? 'right' : 'left';
|
|
90
|
+
const originY = y + h > vh ? 'bottom' : 'top';
|
|
91
|
+
|
|
92
|
+
this.menuEl.style.left = `${left}px`;
|
|
93
|
+
this.menuEl.style.top = `${top}px`;
|
|
94
|
+
this.menuEl.style.transformOrigin = `${originY} ${originX}`;
|
|
95
|
+
|
|
96
|
+
requestAnimationFrame(() => this.menuEl?.classList.add('is-visible'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private close(): void {
|
|
100
|
+
if (!this.menuEl) return;
|
|
101
|
+
const el = this.menuEl;
|
|
102
|
+
this.menuEl = null;
|
|
103
|
+
|
|
104
|
+
el.classList.remove('is-visible');
|
|
105
|
+
|
|
106
|
+
// Wait for exit transition then remove from DOM
|
|
107
|
+
el.addEventListener('transitionend', () => el.remove(), { once: true });
|
|
108
|
+
setTimeout(() => el.isConnected && el.remove(), 200);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private buildMenu(items: ContextMenuInput[]): HTMLElement {
|
|
112
|
+
const ul = document.createElement('ul');
|
|
113
|
+
ul.className = 'context-menu';
|
|
114
|
+
|
|
115
|
+
for (const item of items) {
|
|
116
|
+
if (item === 'separator') {
|
|
117
|
+
const li = document.createElement('li');
|
|
118
|
+
li.className = 'context-menu-separator';
|
|
119
|
+
ul.appendChild(li);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if ('group' in item) {
|
|
124
|
+
const li = document.createElement('li');
|
|
125
|
+
li.className = 'context-menu-group-label';
|
|
126
|
+
li.textContent = item.group;
|
|
127
|
+
ul.appendChild(li);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ul.appendChild(this.buildItem(item));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return ul;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private buildItem(def: ContextMenuItemDef): HTMLElement {
|
|
138
|
+
const li = document.createElement('li');
|
|
139
|
+
li.className = 'context-menu-item';
|
|
140
|
+
|
|
141
|
+
if (def.disabled) li.classList.add('is-disabled');
|
|
142
|
+
if (def.destructive) li.classList.add('is-destructive');
|
|
143
|
+
if (def.submenu) li.classList.add('has-submenu');
|
|
144
|
+
|
|
145
|
+
// Always render icon slot — keeps label column aligned across all items
|
|
146
|
+
const iconWrap = document.createElement('span');
|
|
147
|
+
iconWrap.className = 'context-menu-icon';
|
|
148
|
+
if (def.icon) {
|
|
149
|
+
iconWrap.innerHTML = `<svg class="icon-svg"><use href="svg-icons/icons.svg#${def.icon}"/></svg>`;
|
|
150
|
+
}
|
|
151
|
+
li.appendChild(iconWrap);
|
|
152
|
+
|
|
153
|
+
const label = document.createElement('span');
|
|
154
|
+
label.className = 'context-menu-label';
|
|
155
|
+
label.textContent = def.label;
|
|
156
|
+
li.appendChild(label);
|
|
157
|
+
|
|
158
|
+
if (def.shortcut) {
|
|
159
|
+
const sc = document.createElement('span');
|
|
160
|
+
sc.className = 'context-menu-shortcut';
|
|
161
|
+
sc.textContent = def.shortcut;
|
|
162
|
+
li.appendChild(sc);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (def.submenu) {
|
|
166
|
+
const chevron = document.createElement('span');
|
|
167
|
+
chevron.className = 'context-menu-chevron';
|
|
168
|
+
li.appendChild(chevron);
|
|
169
|
+
|
|
170
|
+
const submenuEl = this.buildMenu(def.submenu);
|
|
171
|
+
li.appendChild(submenuEl);
|
|
172
|
+
|
|
173
|
+
// Determine flip synchronously from parent position — no rAF flash
|
|
174
|
+
const shouldFlip = (): boolean => {
|
|
175
|
+
const rect = li.getBoundingClientRect();
|
|
176
|
+
return rect.right + submenuEl.offsetWidth > window.innerWidth;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Delay timer prevents the submenu closing when mouse travels from
|
|
180
|
+
// item → submenu (mouseleave fires before mouseenter on the submenu)
|
|
181
|
+
let closeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
182
|
+
|
|
183
|
+
const openSub = () => {
|
|
184
|
+
if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; }
|
|
185
|
+
this.closeAllSubmenus(li.closest<HTMLElement>('.context-menu')!);
|
|
186
|
+
li.classList.toggle('submenu-flip', shouldFlip());
|
|
187
|
+
li.classList.add('is-active');
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const closeSub = () => {
|
|
191
|
+
closeTimer = setTimeout(() => li.classList.remove('is-active'), 120);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
li.addEventListener('mouseenter', openSub);
|
|
195
|
+
li.addEventListener('mouseleave', closeSub);
|
|
196
|
+
submenuEl.addEventListener('mouseenter', () => {
|
|
197
|
+
if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; }
|
|
198
|
+
});
|
|
199
|
+
submenuEl.addEventListener('mouseleave', closeSub);
|
|
200
|
+
} else if (!def.disabled) {
|
|
201
|
+
li.addEventListener('click', (e: MouseEvent) => {
|
|
202
|
+
e.stopPropagation();
|
|
203
|
+
def.action?.(this.currentTarget!);
|
|
204
|
+
this.close();
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return li;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private closeAllSubmenus(menu: HTMLElement): void {
|
|
212
|
+
// Only close direct-child submenus of this menu level
|
|
213
|
+
Array.from(menu.children).forEach((child) => {
|
|
214
|
+
child.classList.remove('is-active');
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private getFocusableItems(): HTMLElement[] {
|
|
219
|
+
if (!this.menuEl) return [];
|
|
220
|
+
return Array.from(
|
|
221
|
+
this.menuEl.children
|
|
222
|
+
).filter(
|
|
223
|
+
(el): el is HTMLElement =>
|
|
224
|
+
el.classList.contains('context-menu-item') &&
|
|
225
|
+
!el.classList.contains('is-disabled')
|
|
226
|
+
) as HTMLElement[];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private moveFocus(direction: 1 | -1): void {
|
|
230
|
+
const items = this.getFocusableItems();
|
|
231
|
+
if (!items.length) return;
|
|
232
|
+
|
|
233
|
+
const currentIndex = items.findIndex((el) => el.classList.contains('is-focused'));
|
|
234
|
+
const nextIndex = (currentIndex + direction + items.length) % items.length;
|
|
235
|
+
|
|
236
|
+
items[currentIndex]?.classList.remove('is-focused');
|
|
237
|
+
items[nextIndex].classList.add('is-focused');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private activateFocused(): void {
|
|
241
|
+
this.menuEl
|
|
242
|
+
?.querySelector<HTMLElement>('.context-menu-item.is-focused')
|
|
243
|
+
?.click();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public destroy(): void {
|
|
247
|
+
this.close();
|
|
248
|
+
this.abortController.abort();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export { ContextMenu, type ContextMenuInput, type ContextMenuItemDef };
|
package/js/editor.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
class Editor {
|
|
2
|
-
constructor() {
|
|
2
|
+
constructor(options = {}) {
|
|
3
3
|
this.undoStack = [];
|
|
4
4
|
this.redoStack = [];
|
|
5
5
|
const editable = document.getElementById('editable');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const sidePanel = document.getElementById('sidePanel');
|
|
9
|
-
const wordCount = document.getElementById('wordCount');
|
|
10
|
-
if (!editable || !code || !preview || !sidePanel) {
|
|
11
|
-
throw new Error('Editor: Required elements not found');
|
|
6
|
+
if (!editable) {
|
|
7
|
+
throw new Error('Editor: #editable element not found');
|
|
12
8
|
}
|
|
13
9
|
this.editable = editable;
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
this.wordCount = document.getElementById('wordCount');
|
|
11
|
+
if (options.simple) {
|
|
12
|
+
this.code = null;
|
|
13
|
+
this.preview = null;
|
|
14
|
+
this.sidePanel = document.getElementById('sidePanel');
|
|
15
|
+
this.sidePanel?.classList.add('hidden');
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const code = document.getElementById('code');
|
|
19
|
+
const preview = document.getElementById('preview');
|
|
20
|
+
const sidePanel = document.getElementById('sidePanel');
|
|
21
|
+
if (!code || !preview || !sidePanel) {
|
|
22
|
+
throw new Error('Editor: #code, #preview and #sidePanel are required unless simple: true');
|
|
23
|
+
}
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.preview = preview;
|
|
26
|
+
this.sidePanel = sidePanel;
|
|
27
|
+
this.sidePanel.classList.add('hidden');
|
|
28
|
+
}
|
|
18
29
|
this.bindToolbar();
|
|
19
30
|
this.bindActions();
|
|
20
31
|
this.bindKeyboard();
|
|
@@ -22,8 +33,6 @@ class Editor {
|
|
|
22
33
|
this.bindTabs();
|
|
23
34
|
this.syncViews();
|
|
24
35
|
this.saveState();
|
|
25
|
-
// Start with side panel hidden
|
|
26
|
-
this.sidePanel.classList.add('hidden');
|
|
27
36
|
}
|
|
28
37
|
bindToolbar() {
|
|
29
38
|
document.querySelectorAll('[data-cmd]').forEach(btn => {
|
|
@@ -69,26 +78,29 @@ class Editor {
|
|
|
69
78
|
document.getElementById('undoBtn')?.addEventListener('click', () => this.undo());
|
|
70
79
|
document.getElementById('redoBtn')?.addEventListener('click', () => this.redo());
|
|
71
80
|
document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
|
|
72
|
-
this.sidePanel
|
|
81
|
+
this.sidePanel?.classList.toggle('hidden');
|
|
73
82
|
this.syncViews();
|
|
74
83
|
});
|
|
75
84
|
// Code action buttons — matched by position within .code-actions
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
if (this.code) {
|
|
86
|
+
const code = this.code;
|
|
87
|
+
const codeActions = document.querySelectorAll('.code-actions button');
|
|
88
|
+
codeActions[0]?.addEventListener('click', () => {
|
|
89
|
+
this.editable.innerHTML = this.sanitizeHTML(code.value);
|
|
90
|
+
this.onContentChange();
|
|
91
|
+
});
|
|
92
|
+
codeActions[1]?.addEventListener('click', () => {
|
|
93
|
+
code.value = this.sanitizeHTML(code.value);
|
|
94
|
+
this.editable.innerHTML = code.value;
|
|
95
|
+
this.onContentChange();
|
|
96
|
+
});
|
|
97
|
+
codeActions[2]?.addEventListener('click', () => {
|
|
98
|
+
code.value = code.value
|
|
99
|
+
.replace(/\n/g, '')
|
|
100
|
+
.replace(/>\s+</g, '><')
|
|
101
|
+
.trim();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
92
104
|
const saveBtn = document.getElementById('saveBtn');
|
|
93
105
|
saveBtn?.addEventListener('click', () => this.downloadHTML());
|
|
94
106
|
document.getElementById('clearBtn')?.addEventListener('click', () => {
|
|
@@ -163,8 +175,10 @@ class Editor {
|
|
|
163
175
|
this.syncViews();
|
|
164
176
|
}
|
|
165
177
|
syncViews() {
|
|
166
|
-
this.code
|
|
167
|
-
|
|
178
|
+
if (this.code)
|
|
179
|
+
this.code.value = this.editable.innerHTML.trim();
|
|
180
|
+
if (this.preview)
|
|
181
|
+
this.preview.innerHTML = this.editable.innerHTML;
|
|
168
182
|
this.updateWordCount();
|
|
169
183
|
}
|
|
170
184
|
updateWordCount() {
|
package/js/editor.ts
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
|
+
interface EditorOptions {
|
|
2
|
+
/** Hides the entire side panel (code/preview) permanently. Safe to use
|
|
3
|
+
* without #code, #preview, or #sidePanel in the DOM. */
|
|
4
|
+
simple?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
class Editor {
|
|
2
8
|
private readonly editable: HTMLElement;
|
|
3
|
-
private readonly code: HTMLTextAreaElement;
|
|
4
|
-
private readonly preview: HTMLElement;
|
|
5
|
-
private readonly sidePanel: HTMLElement;
|
|
9
|
+
private readonly code: HTMLTextAreaElement | null;
|
|
10
|
+
private readonly preview: HTMLElement | null;
|
|
11
|
+
private readonly sidePanel: HTMLElement | null;
|
|
6
12
|
private readonly wordCount: HTMLElement | null;
|
|
7
13
|
private undoStack: string[] = [];
|
|
8
14
|
private redoStack: string[] = [];
|
|
9
15
|
|
|
10
|
-
constructor() {
|
|
16
|
+
constructor(options: EditorOptions = {}) {
|
|
11
17
|
const editable = document.getElementById('editable');
|
|
12
|
-
const code = document.getElementById('code') as HTMLTextAreaElement;
|
|
13
|
-
const preview = document.getElementById('preview');
|
|
14
|
-
const sidePanel = document.getElementById('sidePanel');
|
|
15
|
-
const wordCount = document.getElementById('wordCount');
|
|
16
18
|
|
|
17
|
-
if (!editable
|
|
18
|
-
throw new Error('Editor:
|
|
19
|
+
if (!editable) {
|
|
20
|
+
throw new Error('Editor: #editable element not found');
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
this.editable
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
this.editable = editable;
|
|
24
|
+
this.wordCount = document.getElementById('wordCount');
|
|
25
|
+
|
|
26
|
+
if (options.simple) {
|
|
27
|
+
this.code = null;
|
|
28
|
+
this.preview = null;
|
|
29
|
+
this.sidePanel = document.getElementById('sidePanel');
|
|
30
|
+
this.sidePanel?.classList.add('hidden');
|
|
31
|
+
} else {
|
|
32
|
+
const code = document.getElementById('code') as HTMLTextAreaElement;
|
|
33
|
+
const preview = document.getElementById('preview');
|
|
34
|
+
const sidePanel = document.getElementById('sidePanel');
|
|
35
|
+
|
|
36
|
+
if (!code || !preview || !sidePanel) {
|
|
37
|
+
throw new Error('Editor: #code, #preview and #sidePanel are required unless simple: true');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.preview = preview;
|
|
42
|
+
this.sidePanel = sidePanel;
|
|
43
|
+
this.sidePanel.classList.add('hidden');
|
|
44
|
+
}
|
|
26
45
|
|
|
27
46
|
this.bindToolbar();
|
|
28
47
|
this.bindActions();
|
|
@@ -31,9 +50,6 @@ class Editor {
|
|
|
31
50
|
this.bindTabs();
|
|
32
51
|
this.syncViews();
|
|
33
52
|
this.saveState();
|
|
34
|
-
|
|
35
|
-
// Start with side panel hidden
|
|
36
|
-
this.sidePanel.classList.add('hidden');
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
private bindToolbar(): void {
|
|
@@ -82,27 +98,30 @@ class Editor {
|
|
|
82
98
|
document.getElementById('redoBtn')?.addEventListener('click', () => this.redo());
|
|
83
99
|
|
|
84
100
|
document.getElementById('toggleCodeBtn')?.addEventListener('click', () => {
|
|
85
|
-
this.sidePanel
|
|
101
|
+
this.sidePanel?.classList.toggle('hidden');
|
|
86
102
|
this.syncViews();
|
|
87
103
|
});
|
|
88
104
|
|
|
89
105
|
// Code action buttons — matched by position within .code-actions
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
if (this.code) {
|
|
107
|
+
const code = this.code;
|
|
108
|
+
const codeActions = document.querySelectorAll<HTMLButtonElement>('.code-actions button');
|
|
109
|
+
codeActions[0]?.addEventListener('click', () => {
|
|
110
|
+
this.editable.innerHTML = this.sanitizeHTML(code.value);
|
|
111
|
+
this.onContentChange();
|
|
112
|
+
});
|
|
113
|
+
codeActions[1]?.addEventListener('click', () => {
|
|
114
|
+
code.value = this.sanitizeHTML(code.value);
|
|
115
|
+
this.editable.innerHTML = code.value;
|
|
116
|
+
this.onContentChange();
|
|
117
|
+
});
|
|
118
|
+
codeActions[2]?.addEventListener('click', () => {
|
|
119
|
+
code.value = code.value
|
|
120
|
+
.replace(/\n/g, '')
|
|
121
|
+
.replace(/>\s+</g, '><')
|
|
122
|
+
.trim();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
106
125
|
|
|
107
126
|
const saveBtn = document.getElementById('saveBtn');
|
|
108
127
|
saveBtn?.addEventListener('click', () => this.downloadHTML());
|
|
@@ -171,8 +190,8 @@ class Editor {
|
|
|
171
190
|
}
|
|
172
191
|
|
|
173
192
|
private syncViews(): void {
|
|
174
|
-
this.code.value
|
|
175
|
-
this.preview.innerHTML
|
|
193
|
+
if (this.code) this.code.value = this.editable.innerHTML.trim();
|
|
194
|
+
if (this.preview) this.preview.innerHTML = this.editable.innerHTML;
|
|
176
195
|
this.updateWordCount();
|
|
177
196
|
}
|
|
178
197
|
|
package/js/gallery.js
CHANGED
|
@@ -13,6 +13,9 @@ class MasonryGallery {
|
|
|
13
13
|
this.loadMoreImages();
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
|
+
this.fetchMockImages = () => {
|
|
17
|
+
throw Error("Method not implemented.");
|
|
18
|
+
};
|
|
16
19
|
const container = document.getElementById(containerId);
|
|
17
20
|
if (!container) {
|
|
18
21
|
throw new Error(`Container with id "${containerId}" not found`);
|
|
@@ -85,7 +88,8 @@ class MasonryGallery {
|
|
|
85
88
|
});
|
|
86
89
|
}
|
|
87
90
|
async loadMoreImages(isAutoFill = false) {
|
|
88
|
-
if (!isAutoFill)
|
|
91
|
+
if (!isAutoFill)
|
|
92
|
+
this.reloaded++;
|
|
89
93
|
if (this.options.reload > 0 && this.reloaded > this.options.reload) {
|
|
90
94
|
console.warn("Maximum reload limit reached.");
|
|
91
95
|
return;
|
|
@@ -119,9 +123,6 @@ class MasonryGallery {
|
|
|
119
123
|
this.loader.classList.toggle("hidden", !show);
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
|
-
fetchMockImages = () => {
|
|
123
|
-
throw Error("Method not implemented.");
|
|
124
|
-
};
|
|
125
126
|
renderImages(imageDataList) {
|
|
126
127
|
imageDataList.forEach((data) => {
|
|
127
128
|
const item = this.createCard(data);
|
|
@@ -142,12 +143,12 @@ class MasonryGallery {
|
|
|
142
143
|
createCard(data) {
|
|
143
144
|
const div = document.createElement("div");
|
|
144
145
|
div.className = "masonry-item";
|
|
145
|
-
div.innerHTML = `
|
|
146
|
-
<img src="${this.escapeHtml(data.src)}" alt="${this.escapeHtml(data.title)}" loading="lazy">
|
|
147
|
-
<div class="masonry-item-info">
|
|
148
|
-
<h3 class="masonry-item-title">${this.escapeHtml(data.title)}</h3>
|
|
149
|
-
<p class="masonry-item-desc">${this.escapeHtml(data.desc)}</p>
|
|
150
|
-
</div>
|
|
146
|
+
div.innerHTML = `
|
|
147
|
+
<img src="${this.escapeHtml(data.src)}" alt="${this.escapeHtml(data.title)}" loading="lazy">
|
|
148
|
+
<div class="masonry-item-info">
|
|
149
|
+
<h3 class="masonry-item-title">${this.escapeHtml(data.title)}</h3>
|
|
150
|
+
<p class="masonry-item-desc">${this.escapeHtml(data.desc)}</p>
|
|
151
|
+
</div>
|
|
151
152
|
`;
|
|
152
153
|
return div;
|
|
153
154
|
}
|