vanilla-jui 1.4.8 → 1.5.2
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 +10 -4
- package/dist/index.d.ts +402 -977
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -3
- package/src/components/accordion.ts +467 -349
- package/src/components/flow.ts +779 -1133
- package/src/components/form.ts +666 -509
- package/src/components/menu.ts +199 -326
- package/src/components/modal.ts +510 -747
- package/src/components/offcanvas.ts +281 -361
- package/src/components/pagination.ts +207 -374
- package/src/components/swiper.ts +688 -1168
- package/src/components/tabs.ts +468 -788
- package/src/components/toc.ts +191 -244
- package/src/core/component.ts +332 -0
- package/src/core/motion.ts +298 -0
- package/src/core/presence.ts +233 -0
- package/src/core/scheduler.ts +91 -0
- package/src/core/view.ts +31 -0
- package/src/css/accordion.css +94 -3
- package/src/css/color.css +4 -5
- package/src/css/color_p3.css +6 -5
- package/src/css/content.css +18 -24
- package/src/css/drop.css +4 -1
- package/src/css/form.css +8 -0
- package/src/css/gradient.css +64 -131
- package/src/css/layout.css +9 -23
- package/src/css/menu.css +10 -5
- package/src/css/modal.css +2 -1
- package/src/css/offcanvas.css +1 -17
- package/src/css/swiper.css +2 -1
- package/src/css/tabs.css +8 -17
- package/src/css/themes/default/accordion.css +0 -48
- package/src/css/themes/default/swiper.css +1 -1
- package/src/css/themes/default/tabs.css +4 -6
- package/src/css/toast.css +1 -17
- package/src/css/tokens.css +18 -35
- package/src/css/tooltip.css +2 -1
- package/src/index.ts +8 -3
- package/src/locales/index.ts +3 -0
- package/src/primitives/drop.ts +253 -262
- package/src/primitives/loading.ts +22 -4
- package/src/primitives/parabola.ts +123 -164
- package/src/primitives/sticky.ts +297 -0
- package/src/primitives/theme.ts +164 -222
- package/src/primitives/toast.ts +309 -284
- package/src/primitives/tooltip.ts +65 -66
- package/src/utilities/browser.ts +38 -0
- package/src/utilities/dom.ts +0 -54
- package/src/utilities/refs.ts +61 -0
- package/src/utilities/state.ts +73 -0
- package/src/utilities/timer.ts +11 -31
- package/src/validation/validator.ts +289 -389
- package/src/components/sticky.ts +0 -297
- package/src/core/Component.ts +0 -377
- package/src/utilities/scheduler.ts +0 -156
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createDeepStore,
|
|
3
|
+
createEffect,
|
|
4
|
+
flushSync,
|
|
5
|
+
insert,
|
|
6
|
+
jsx,
|
|
7
|
+
untrack,
|
|
8
|
+
} from 'vanilla-signal';
|
|
2
9
|
|
|
3
|
-
import Component, {
|
|
4
|
-
type ComponentDOM,
|
|
5
|
-
type ComponentRuntime,
|
|
6
|
-
} from '../core/Component.ts';
|
|
7
|
-
import { createLoading } from '../primitives/loading.ts';
|
|
8
10
|
import {
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
} from '../
|
|
11
|
+
type FunctionalComponent,
|
|
12
|
+
defineComponent,
|
|
13
|
+
} from '../core/component.ts';
|
|
14
|
+
import { createLoading } from '../primitives/loading.ts';
|
|
15
|
+
import { type RenderableContent } from '../utilities/dom.ts';
|
|
12
16
|
import { randomId } from '../utilities/id.ts';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
17
|
+
import { createEventManager } from '../utilities/events.ts';
|
|
18
|
+
import { createMotionGroup, createTransition } from '../core/motion.ts';
|
|
19
|
+
import { createPresence } from '../core/presence.ts';
|
|
20
|
+
import { createElementRef } from '../utilities/refs.ts';
|
|
15
21
|
import {
|
|
16
22
|
type ResolveSchema,
|
|
17
23
|
resolveProps,
|
|
@@ -73,38 +79,35 @@ interface ResolvedOffcanvasProps extends Record<string, unknown> {
|
|
|
73
79
|
|
|
74
80
|
interface OffcanvasState extends Record<string, unknown> {
|
|
75
81
|
content: OffcanvasContent;
|
|
82
|
+
resolvedContent: RenderableContent<Offcanvas>;
|
|
76
83
|
visible: boolean;
|
|
77
84
|
loading: boolean;
|
|
78
85
|
}
|
|
79
86
|
|
|
80
|
-
interface OffcanvasDOM extends ComponentDOM {
|
|
81
|
-
root: HTMLElement | null;
|
|
82
|
-
overlay: HTMLElement | null;
|
|
83
|
-
content: HTMLElement | null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
87
|
interface OffcanvasCache {
|
|
87
88
|
content: RenderableContent<Offcanvas>;
|
|
88
89
|
hasContent: boolean;
|
|
89
90
|
updatedAt: number;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
interface
|
|
93
|
-
built: boolean;
|
|
93
|
+
interface OffcanvasRuntimeExtras {
|
|
94
94
|
cache: OffcanvasCache;
|
|
95
95
|
contentLoadId: number;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
interface OffcanvasCleanupExtras {
|
|
99
|
-
state?: (() => void) | null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
98
|
const DEFAULT_CLASS_NAMES: OffcanvasClassNames = {
|
|
103
99
|
root: 'j-offcanvas',
|
|
104
100
|
overlay: 'j-offcanvas-overlay',
|
|
105
101
|
content: 'offcanvas-content',
|
|
106
102
|
};
|
|
107
103
|
|
|
104
|
+
const OFFCANVAS_HIDDEN_TRANSFORM: Record<OffcanvasDirection, string> = {
|
|
105
|
+
top: 'translateY(-100%)',
|
|
106
|
+
right: 'translateX(100%)',
|
|
107
|
+
bottom: 'translateY(100%)',
|
|
108
|
+
left: 'translateX(-100%)',
|
|
109
|
+
};
|
|
110
|
+
|
|
108
111
|
const OFFCANVAS_PROPS_SCHEMA = {
|
|
109
112
|
content: {
|
|
110
113
|
default: '',
|
|
@@ -154,6 +157,9 @@ const OFFCANVAS_PROPS_SCHEMA = {
|
|
|
154
157
|
|
|
155
158
|
const OFFCANVAS_STATE_SCHEMA = {
|
|
156
159
|
content: OFFCANVAS_PROPS_SCHEMA.content,
|
|
160
|
+
resolvedContent: { type: 'renderable' },
|
|
161
|
+
visible: { type: 'boolean' },
|
|
162
|
+
loading: { type: 'boolean' },
|
|
157
163
|
};
|
|
158
164
|
|
|
159
165
|
function normalizeTtl(ttl: number): number {
|
|
@@ -182,381 +188,295 @@ function normalizeProps(input: OffcanvasProps): ResolvedOffcanvasProps {
|
|
|
182
188
|
};
|
|
183
189
|
}
|
|
184
190
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
> {
|
|
190
|
-
declare runtime: OffcanvasRuntime;
|
|
191
|
-
declare state: OffcanvasState;
|
|
192
|
-
declare cleanup: Component['cleanup'] & OffcanvasCleanupExtras;
|
|
193
|
-
|
|
194
|
-
constructor(input: OffcanvasProps = {}) {
|
|
195
|
-
const props = normalizeProps(input);
|
|
196
|
-
super(props);
|
|
197
|
-
|
|
198
|
-
this.dom.overlay = null;
|
|
199
|
-
this.dom.content = null;
|
|
200
|
-
|
|
201
|
-
this.cleanup.state = null;
|
|
202
|
-
|
|
203
|
-
this.runtime.built = false;
|
|
204
|
-
this.runtime.cache = {
|
|
205
|
-
content: null,
|
|
206
|
-
hasContent: false,
|
|
207
|
-
updatedAt: 0,
|
|
208
|
-
};
|
|
209
|
-
this.runtime.contentLoadId = 0;
|
|
210
|
-
|
|
211
|
-
this.state = createDeepStore({
|
|
212
|
-
content: props.content,
|
|
213
|
-
visible: false,
|
|
214
|
-
loading: false,
|
|
215
|
-
}) as OffcanvasState;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
build(): this {
|
|
219
|
-
if (this.runtime.destroyed) {
|
|
220
|
-
throw new Error('Offcanvas.build: instance has been destroyed.');
|
|
221
|
-
}
|
|
222
|
-
if (this.runtime.built) return this;
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
this.dom.root = this.buildRoot();
|
|
226
|
-
this.runtime.built = true;
|
|
227
|
-
this.bindState();
|
|
228
|
-
} catch (error) {
|
|
229
|
-
this.destroy();
|
|
230
|
-
throw error;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
this.emit('init', this.props);
|
|
234
|
-
return this;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private assertBuilt(method: string): void {
|
|
238
|
-
if (this.runtime.destroyed) {
|
|
239
|
-
throw new Error(`Offcanvas.${method}: instance has been destroyed.`);
|
|
240
|
-
}
|
|
241
|
-
if (!this.runtime.built) {
|
|
242
|
-
throw new Error(`Offcanvas.${method}: call build() first.`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private buildRoot(): HTMLElement {
|
|
247
|
-
if (this.props.overlay) {
|
|
248
|
-
this.dom.overlay = this.buildOverlay();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return this.buildPanel();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
private buildOverlay(): HTMLElement {
|
|
255
|
-
const filterStyle = this.props.filter
|
|
256
|
-
? { backdropFilter: 'blur(2px)' }
|
|
257
|
-
: {};
|
|
258
|
-
return jsx('div', {
|
|
259
|
-
className: this.props.className.overlay,
|
|
260
|
-
hidden: true,
|
|
261
|
-
'data-offcanvas-overlay': this.props.id,
|
|
262
|
-
style: {
|
|
263
|
-
...filterStyle,
|
|
264
|
-
},
|
|
265
|
-
}) as HTMLElement;
|
|
266
|
-
}
|
|
191
|
+
interface OffcanvasActions {
|
|
192
|
+
show(): Promise<void>;
|
|
193
|
+
hide(): Promise<void>;
|
|
194
|
+
}
|
|
267
195
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
className: this.props.className.content,
|
|
271
|
-
'data-offcanvas-content': this.props.id,
|
|
272
|
-
}) as HTMLElement;
|
|
273
|
-
this.dom.content = content;
|
|
196
|
+
let offcanvasScrollLockCount = 0;
|
|
197
|
+
let offcanvasBodyOverflow = '';
|
|
274
198
|
|
|
275
|
-
|
|
276
|
-
|
|
199
|
+
export type Offcanvas = FunctionalComponent<
|
|
200
|
+
ResolvedOffcanvasProps,
|
|
201
|
+
OffcanvasState,
|
|
202
|
+
HTMLElement,
|
|
203
|
+
OffcanvasActions
|
|
204
|
+
>;
|
|
205
|
+
|
|
206
|
+
function validateStatePatch(patch: Partial<OffcanvasState>): void {
|
|
207
|
+
validateParam('state', patch, { type: 'plainObject' }, 'Offcanvas.setState');
|
|
208
|
+
for (const key of Object.keys(patch)) {
|
|
209
|
+
if (!Object.hasOwn(OFFCANVAS_STATE_SCHEMA, key)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Offcanvas.setState: "${key}" is not a supported state key.`
|
|
212
|
+
);
|
|
277
213
|
}
|
|
278
|
-
|
|
279
|
-
return jsx('div', {
|
|
280
|
-
className: this.props.className.root,
|
|
281
|
-
id: this.props.id,
|
|
282
|
-
role: 'dialog',
|
|
283
|
-
'aria-modal': this.props.overlay ? 'true' : 'false',
|
|
284
|
-
'aria-expanded': 'false',
|
|
285
|
-
'data-offcanvas': 'root',
|
|
286
|
-
'data-direction': this.props.direction,
|
|
287
|
-
'data-animate': this.props.animate,
|
|
288
|
-
children: content,
|
|
289
|
-
}) as HTMLElement;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private bindState(): void {
|
|
293
|
-
if (this.cleanup.state) return;
|
|
294
|
-
|
|
295
|
-
this.cleanup.state = createStateSync(
|
|
296
|
-
() => this.state.content,
|
|
297
|
-
(content) => this.syncContent(content)
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private async syncContent(content: OffcanvasContent): Promise<void> {
|
|
302
|
-
if (!this.runtime.built) return;
|
|
214
|
+
const stateKey = key as keyof typeof OFFCANVAS_STATE_SCHEMA;
|
|
303
215
|
validateParam(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
OFFCANVAS_STATE_SCHEMA
|
|
307
|
-
'Offcanvas.
|
|
216
|
+
key,
|
|
217
|
+
patch[key as keyof OffcanvasState],
|
|
218
|
+
OFFCANVAS_STATE_SCHEMA[stateKey],
|
|
219
|
+
'Offcanvas.setState'
|
|
308
220
|
);
|
|
309
|
-
this.runtime.cache.hasContent = false;
|
|
310
|
-
this.runtime.cache.content = null;
|
|
311
|
-
this.runtime.cache.updatedAt = 0;
|
|
312
|
-
|
|
313
|
-
if (typeof content === 'function') {
|
|
314
|
-
if (this.state.visible) await this.loadContent();
|
|
315
|
-
else this.clearContent();
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
this.runtime.contentLoadId += 1;
|
|
320
|
-
flushSync(() => {
|
|
321
|
-
this.state.loading = false;
|
|
322
|
-
});
|
|
323
|
-
this.renderContent(content);
|
|
324
221
|
}
|
|
222
|
+
}
|
|
325
223
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
224
|
+
export function createOffcanvas(input: OffcanvasProps = {}): Offcanvas {
|
|
225
|
+
const props = normalizeProps(input);
|
|
226
|
+
const state = createDeepStore({
|
|
227
|
+
content: props.content,
|
|
228
|
+
resolvedContent: typeof props.content === 'function' ? null : props.content,
|
|
229
|
+
visible: false,
|
|
230
|
+
loading: false,
|
|
231
|
+
}) as OffcanvasState;
|
|
232
|
+
const runtime: OffcanvasRuntimeExtras = {
|
|
233
|
+
cache: { content: null, hasContent: false, updatedAt: 0 },
|
|
234
|
+
contentLoadId: 0,
|
|
235
|
+
};
|
|
236
|
+
const events = createEventManager();
|
|
237
|
+
const overlayRef = createElementRef<HTMLElement>();
|
|
238
|
+
let offcanvas!: Offcanvas;
|
|
239
|
+
let scrollLocked = false;
|
|
240
|
+
|
|
241
|
+
const lockScroll = (): void => {
|
|
242
|
+
if (!props.bodyOverflow || scrollLocked) return;
|
|
243
|
+
if (offcanvasScrollLockCount === 0) {
|
|
244
|
+
offcanvasBodyOverflow = document.body.style.overflow;
|
|
245
|
+
document.body.style.overflow = 'hidden';
|
|
246
|
+
}
|
|
247
|
+
offcanvasScrollLockCount += 1;
|
|
248
|
+
scrollLocked = true;
|
|
249
|
+
};
|
|
250
|
+
const unlockScroll = (): void => {
|
|
251
|
+
if (!scrollLocked) return;
|
|
252
|
+
offcanvasScrollLockCount = Math.max(0, offcanvasScrollLockCount - 1);
|
|
253
|
+
if (offcanvasScrollLockCount === 0) {
|
|
254
|
+
document.body.style.overflow = offcanvasBodyOverflow;
|
|
255
|
+
offcanvasBodyOverflow = '';
|
|
256
|
+
}
|
|
257
|
+
scrollLocked = false;
|
|
258
|
+
};
|
|
332
259
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
260
|
+
const panelMotion = createTransition(() => offcanvas.element, {
|
|
261
|
+
keyframes: [
|
|
262
|
+
{ transform: OFFCANVAS_HIDDEN_TRANSFORM[props.direction] },
|
|
263
|
+
{ transform: 'translate(0, 0)' },
|
|
264
|
+
],
|
|
265
|
+
options: { duration: 300, easing: 'ease' },
|
|
266
|
+
});
|
|
267
|
+
const motion = createMotionGroup(
|
|
268
|
+
...(props.animate === 'slide' ? [panelMotion] : []),
|
|
269
|
+
createTransition(() => overlayRef.current, {
|
|
270
|
+
keyframes: [{ opacity: 0 }, { opacity: 1 }],
|
|
271
|
+
options: { duration: 300, easing: 'ease' },
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const presence = createPresence({
|
|
276
|
+
elements: () => [overlayRef.current, offcanvas.element],
|
|
277
|
+
mount: () => {
|
|
278
|
+
const overlay = overlayRef.current;
|
|
279
|
+
if (overlay && !overlay.isConnected) insert(document.body, overlay);
|
|
280
|
+
offcanvas.mount(document.body);
|
|
281
|
+
lockScroll();
|
|
282
|
+
},
|
|
283
|
+
activate: () => {
|
|
284
|
+
state.visible = true;
|
|
285
|
+
},
|
|
286
|
+
deactivate: () => {
|
|
287
|
+
state.visible = false;
|
|
288
|
+
},
|
|
289
|
+
motion,
|
|
290
|
+
unmount: () => {
|
|
291
|
+
overlayRef.current?.remove();
|
|
292
|
+
offcanvas.unmount();
|
|
293
|
+
unlockScroll();
|
|
294
|
+
},
|
|
295
|
+
});
|
|
336
296
|
|
|
337
|
-
|
|
338
|
-
|
|
297
|
+
const clearCache = (): void => {
|
|
298
|
+
runtime.cache = { content: null, hasContent: false, updatedAt: 0 };
|
|
299
|
+
};
|
|
339
300
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
301
|
+
const isCacheValid = (): boolean => {
|
|
302
|
+
if (!props.cache || !runtime.cache.hasContent) return false;
|
|
303
|
+
const ttl = normalizeTtl(props.ttl);
|
|
304
|
+
return !ttl || Date.now() - runtime.cache.updatedAt <= ttl;
|
|
305
|
+
};
|
|
343
306
|
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
const content = this.state.content;
|
|
307
|
+
const loadContent = async (): Promise<void> => {
|
|
308
|
+
const content = state.content;
|
|
347
309
|
if (typeof content !== 'function') {
|
|
348
|
-
|
|
310
|
+
runtime.contentLoadId += 1;
|
|
349
311
|
flushSync(() => {
|
|
350
|
-
|
|
312
|
+
state.resolvedContent = content;
|
|
313
|
+
state.loading = false;
|
|
351
314
|
});
|
|
352
315
|
return;
|
|
353
316
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
this.runtime.contentLoadId += 1;
|
|
357
|
-
this.renderContent(this.runtime.cache.content);
|
|
317
|
+
if (isCacheValid()) {
|
|
318
|
+
runtime.contentLoadId += 1;
|
|
358
319
|
flushSync(() => {
|
|
359
|
-
|
|
320
|
+
state.resolvedContent = runtime.cache.content;
|
|
321
|
+
state.loading = false;
|
|
360
322
|
});
|
|
361
323
|
return;
|
|
362
324
|
}
|
|
363
325
|
|
|
364
|
-
const loadId = ++
|
|
326
|
+
const loadId = ++runtime.contentLoadId;
|
|
365
327
|
flushSync(() => {
|
|
366
|
-
|
|
328
|
+
state.loading = true;
|
|
329
|
+
state.resolvedContent = null;
|
|
367
330
|
});
|
|
368
|
-
|
|
369
|
-
if (this.dom.content) {
|
|
370
|
-
this.clearContent();
|
|
371
|
-
this.dom.content.appendChild(createLoading());
|
|
372
|
-
}
|
|
373
|
-
|
|
374
331
|
try {
|
|
375
|
-
const result = await Promise.resolve(content(
|
|
376
|
-
|
|
377
|
-
if (this.runtime.destroyed || loadId !== this.runtime.contentLoadId) {
|
|
332
|
+
const result = await Promise.resolve(content(offcanvas));
|
|
333
|
+
if (offcanvas.runtime.destroyed || loadId !== runtime.contentLoadId)
|
|
378
334
|
return;
|
|
335
|
+
if (props.cache) {
|
|
336
|
+
runtime.cache = {
|
|
337
|
+
content: result,
|
|
338
|
+
hasContent: true,
|
|
339
|
+
updatedAt: Date.now(),
|
|
340
|
+
};
|
|
379
341
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
this.runtime.cache.hasContent = true;
|
|
384
|
-
this.runtime.cache.updatedAt = Date.now();
|
|
385
|
-
}
|
|
386
|
-
this.renderContent(result);
|
|
342
|
+
flushSync(() => {
|
|
343
|
+
state.resolvedContent = result;
|
|
344
|
+
});
|
|
387
345
|
} finally {
|
|
388
|
-
if (!
|
|
346
|
+
if (!offcanvas.runtime.destroyed && loadId === runtime.contentLoadId) {
|
|
389
347
|
flushSync(() => {
|
|
390
|
-
|
|
348
|
+
state.loading = false;
|
|
391
349
|
});
|
|
392
350
|
}
|
|
393
351
|
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
private bindEvents(): void {
|
|
397
|
-
this.unbindEvents();
|
|
398
|
-
const { bgClose, escClose } = this.props;
|
|
399
|
-
|
|
400
|
-
if (this.dom.overlay && bgClose) {
|
|
401
|
-
this.cleanup.events.on('overlay', this.dom.overlay, 'click', () => {
|
|
402
|
-
void this.hide();
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
if (escClose) {
|
|
406
|
-
this.cleanup.events.on('esc', document, 'keydown', (event) => {
|
|
407
|
-
if (
|
|
408
|
-
event instanceof KeyboardEvent &&
|
|
409
|
-
event.key === 'Escape' &&
|
|
410
|
-
this.state.visible
|
|
411
|
-
) {
|
|
412
|
-
void this.hide();
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
if (this.dom.root) {
|
|
417
|
-
this.cleanup.events.on('close', this.dom.root, 'click', (event) => {
|
|
418
|
-
if (!(event.target instanceof Element)) return;
|
|
419
|
-
const action = event.target.closest<HTMLElement>('[data-action]');
|
|
420
|
-
const name = action?.dataset.action;
|
|
421
|
-
if (name === 'close' || name === 'cancel') void this.hide();
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
private unbindEvents(): void {
|
|
427
|
-
this.cleanup.events.clear();
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
private showPanel(): void {
|
|
431
|
-
const { id } = this.props;
|
|
432
|
-
timer.cancel(`oc-hide-${id}`);
|
|
433
|
-
|
|
434
|
-
if (this.dom.overlay && !this.dom.overlay.isConnected) {
|
|
435
|
-
this.dom.overlay.hidden = false;
|
|
436
|
-
document.body.appendChild(this.dom.overlay);
|
|
437
|
-
}
|
|
438
|
-
if (this.dom.root && !this.dom.root.isConnected) {
|
|
439
|
-
document.body.appendChild(this.dom.root);
|
|
440
|
-
}
|
|
441
|
-
if (this.props.bodyOverflow) {
|
|
442
|
-
document.body.style.overflow = 'hidden';
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
timer.start(`oc-show-${id}`, 10, () => {
|
|
446
|
-
this.dom.root?.setAttribute('aria-expanded', 'true');
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
private hidePanel(): void {
|
|
451
|
-
const { id } = this.props;
|
|
452
|
-
timer.cancel(`oc-show-${id}`);
|
|
453
|
-
timer.cancel(`oc-shown-${id}`);
|
|
454
|
-
|
|
455
|
-
this.dom.root?.setAttribute('aria-expanded', 'false');
|
|
352
|
+
};
|
|
456
353
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
document.body.style.overflow = '';
|
|
354
|
+
const actions: OffcanvasActions = {
|
|
355
|
+
async show() {
|
|
356
|
+
if (!offcanvas.runtime.built) {
|
|
357
|
+
throw new Error('Offcanvas.show: call build() first.');
|
|
462
358
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
async show(): Promise<void> {
|
|
467
|
-
this.assertBuilt('show');
|
|
468
|
-
if (this.state.visible) return;
|
|
469
|
-
|
|
470
|
-
const { onShow, onShown, id } = this.props;
|
|
471
|
-
if (onShow) await Promise.resolve(onShow(this));
|
|
472
|
-
|
|
473
|
-
this.showPanel();
|
|
474
|
-
this.bindEvents();
|
|
475
|
-
|
|
476
|
-
flushSync(() => {
|
|
477
|
-
this.state.visible = true;
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
await this.loadContent();
|
|
481
|
-
|
|
482
|
-
timer.start(`oc-shown-${id}`, 300, () => {
|
|
483
|
-
if (this.runtime.destroyed) return;
|
|
484
|
-
void onShown?.(this);
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
async hide(): Promise<void> {
|
|
489
|
-
this.assertBuilt('hide');
|
|
490
|
-
if (!this.state.visible) return;
|
|
491
|
-
|
|
492
|
-
const { onHide, onHidden } = this.props;
|
|
493
|
-
if (onHide) await Promise.resolve(onHide(this));
|
|
494
|
-
|
|
495
|
-
this.runtime.contentLoadId += 1;
|
|
496
|
-
flushSync(() => {
|
|
497
|
-
this.state.loading = false;
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
this.unbindEvents();
|
|
501
|
-
this.hidePanel();
|
|
502
|
-
|
|
503
|
-
flushSync(() => {
|
|
504
|
-
this.state.visible = false;
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
void onHidden?.(this);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
protected onDestroy(): void {
|
|
511
|
-
const { id } = this.props;
|
|
512
|
-
|
|
513
|
-
timer.cancel(`oc-show-${id}`);
|
|
514
|
-
timer.cancel(`oc-hide-${id}`);
|
|
515
|
-
timer.cancel(`oc-shown-${id}`);
|
|
359
|
+
if (state.visible) return;
|
|
360
|
+
await Promise.resolve(props.onShow?.(offcanvas));
|
|
516
361
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
362
|
+
const entering = presence.enter();
|
|
363
|
+
const loading = loadContent();
|
|
364
|
+
if ((await entering) && !offcanvas.runtime.destroyed) {
|
|
365
|
+
void props.onShown?.(offcanvas);
|
|
366
|
+
}
|
|
367
|
+
await loading;
|
|
368
|
+
},
|
|
369
|
+
async hide() {
|
|
370
|
+
if (!offcanvas.runtime.built) {
|
|
371
|
+
throw new Error('Offcanvas.hide: call build() first.');
|
|
372
|
+
}
|
|
373
|
+
if (!state.visible) return;
|
|
374
|
+
const onHideResult = props.onHide?.(offcanvas);
|
|
375
|
+
if (onHideResult) await onHideResult;
|
|
530
376
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
{
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
377
|
+
runtime.contentLoadId += 1;
|
|
378
|
+
flushSync(() => {
|
|
379
|
+
state.loading = false;
|
|
380
|
+
});
|
|
381
|
+
if (await presence.leave()) {
|
|
382
|
+
void props.onHidden?.(offcanvas);
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
};
|
|
540
386
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
387
|
+
offcanvas = defineComponent({
|
|
388
|
+
name: 'Offcanvas',
|
|
389
|
+
props,
|
|
390
|
+
state,
|
|
391
|
+
actions,
|
|
392
|
+
validateStatePatch,
|
|
393
|
+
view(context) {
|
|
394
|
+
const overlay = props.overlay
|
|
395
|
+
? (jsx('div', {
|
|
396
|
+
ref: overlayRef.set,
|
|
397
|
+
className: props.className.overlay,
|
|
398
|
+
'data-offcanvas-overlay': props.id,
|
|
399
|
+
'data-mount': () => (state.visible ? 'true' : 'false'),
|
|
400
|
+
style: props.filter ? { backdropFilter: 'blur(2px)' } : {},
|
|
401
|
+
onClick: () => {
|
|
402
|
+
if (props.bgClose) void offcanvas.hide();
|
|
403
|
+
},
|
|
404
|
+
}) as HTMLElement)
|
|
405
|
+
: null;
|
|
406
|
+
if (overlay) context.own(() => overlay.remove());
|
|
407
|
+
|
|
408
|
+
createEffect(() => {
|
|
409
|
+
const content = state.content;
|
|
410
|
+
validateParam(
|
|
411
|
+
'content',
|
|
412
|
+
content,
|
|
413
|
+
OFFCANVAS_STATE_SCHEMA.content,
|
|
414
|
+
'Offcanvas.state'
|
|
545
415
|
);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
416
|
+
clearCache();
|
|
417
|
+
if (typeof content === 'function') {
|
|
418
|
+
state.resolvedContent = null;
|
|
419
|
+
if (untrack(() => state.visible)) void loadContent();
|
|
420
|
+
} else {
|
|
421
|
+
runtime.contentLoadId += 1;
|
|
422
|
+
state.resolvedContent = content;
|
|
423
|
+
state.loading = false;
|
|
424
|
+
}
|
|
425
|
+
});
|
|
557
426
|
|
|
558
|
-
|
|
427
|
+
return jsx('div', {
|
|
428
|
+
className: props.className.root,
|
|
429
|
+
id: props.id,
|
|
430
|
+
role: 'dialog',
|
|
431
|
+
'aria-modal': props.overlay ? 'true' : 'false',
|
|
432
|
+
'aria-expanded': () => String(state.visible),
|
|
433
|
+
'data-mount': () => (state.visible ? 'true' : 'false'),
|
|
434
|
+
'data-offcanvas': 'root',
|
|
435
|
+
'data-direction': props.direction,
|
|
436
|
+
'data-animate': props.animate,
|
|
437
|
+
onClick: (event: Event) => {
|
|
438
|
+
if (!(event.target instanceof Element)) return;
|
|
439
|
+
const action = event.target.closest<HTMLElement>('[data-action]');
|
|
440
|
+
if (
|
|
441
|
+
action?.dataset.action === 'close' ||
|
|
442
|
+
action?.dataset.action === 'cancel'
|
|
443
|
+
) {
|
|
444
|
+
void offcanvas.hide();
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
children: jsx('div', {
|
|
448
|
+
className: props.className.content,
|
|
449
|
+
'data-offcanvas-content': props.id,
|
|
450
|
+
children: () =>
|
|
451
|
+
state.loading
|
|
452
|
+
? createLoading()
|
|
453
|
+
: typeof state.resolvedContent === 'function'
|
|
454
|
+
? state.resolvedContent(offcanvas)
|
|
455
|
+
: state.resolvedContent,
|
|
456
|
+
}),
|
|
457
|
+
}) as HTMLElement;
|
|
458
|
+
},
|
|
459
|
+
onBuild(context) {
|
|
460
|
+
if (props.escClose) {
|
|
461
|
+
events.on('esc', document, 'keydown', (event) => {
|
|
462
|
+
if (
|
|
463
|
+
event instanceof KeyboardEvent &&
|
|
464
|
+
event.key === 'Escape' &&
|
|
465
|
+
state.visible
|
|
466
|
+
) {
|
|
467
|
+
void offcanvas.hide();
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
context.own(() => events.clear());
|
|
472
|
+
},
|
|
473
|
+
onDestroy() {
|
|
474
|
+
presence.cancel();
|
|
475
|
+
runtime.contentLoadId += 1;
|
|
476
|
+
overlayRef.current?.remove();
|
|
477
|
+
unlockScroll();
|
|
478
|
+
},
|
|
479
|
+
});
|
|
559
480
|
|
|
560
|
-
|
|
561
|
-
return new OffcanvasComponent(input);
|
|
481
|
+
return offcanvas;
|
|
562
482
|
}
|