snice 1.6.0 → 1.8.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 +5 -2
- package/package.json +1 -1
- package/src/controller.ts +3 -1
- package/src/element.ts +118 -19
- package/src/index.ts +5 -3
- package/src/router.ts +13 -118
- package/src/symbols.ts +7 -1
- package/src/transitions.ts +231 -0
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ Snice provides a clear separation of concerns through decorators:
|
|
|
37
37
|
- **`@query`** - Queries a single element from shadow DOM
|
|
38
38
|
- **`@queryAll`** - Queries multiple elements from shadow DOM
|
|
39
39
|
- **`@watch`** - Watches property changes and calls a method when they occur
|
|
40
|
+
- **`@ready`** - Runs a method after the element's shadow DOM is ready
|
|
41
|
+
- **`@dispose`** - Runs a method when the element is removed from the DOM
|
|
40
42
|
|
|
41
43
|
### Event Decorators
|
|
42
44
|
- **`@on`** - Listens for events on elements
|
|
@@ -559,8 +561,9 @@ class UserProfile extends HTMLElement {
|
|
|
559
561
|
const user = await (yield { userId: 123 });
|
|
560
562
|
return user;
|
|
561
563
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
+
|
|
565
|
+
@ready()
|
|
566
|
+
async load() {
|
|
564
567
|
const userData = await this.getUser();
|
|
565
568
|
this.displayUser(userData);
|
|
566
569
|
}
|
package/package.json
CHANGED
package/src/controller.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { setupEventHandlers, cleanupEventHandlers } from './events';
|
|
2
2
|
import { setupChannelHandlers, cleanupChannelHandlers } from './channel';
|
|
3
|
-
import { IS_CONTROLLER_CLASS, CONTROLLER_KEY, CONTROLLER_NAME_KEY, CONTROLLER_ID, CONTROLLER_OPERATIONS, NATIVE_CONTROLLER, IS_ELEMENT_CLASS, ROUTER_CONTEXT } from './symbols';
|
|
3
|
+
import { IS_CONTROLLER_CLASS, IS_CONTROLLER_INSTANCE, CONTROLLER_KEY, CONTROLLER_NAME_KEY, CONTROLLER_ID, CONTROLLER_OPERATIONS, NATIVE_CONTROLLER, IS_ELEMENT_CLASS, ROUTER_CONTEXT } from './symbols';
|
|
4
4
|
import { snice } from './global';
|
|
5
5
|
|
|
6
6
|
type Maybe<T> = T | null | undefined;
|
|
@@ -107,6 +107,8 @@ export async function attachController(element: HTMLElement, controllerName: str
|
|
|
107
107
|
const controllerId = snice.controllerIdCounter;
|
|
108
108
|
const scope = new ControllerScope();
|
|
109
109
|
|
|
110
|
+
// Mark this as a controller instance
|
|
111
|
+
(controllerInstance as any)[IS_CONTROLLER_INSTANCE] = true;
|
|
110
112
|
(controllerInstance as any)[CONTROLLER_ID] = controllerId;
|
|
111
113
|
controllerInstance.element = element;
|
|
112
114
|
|
package/src/element.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { attachController, detachController } from './controller';
|
|
2
2
|
import { setupEventHandlers, cleanupEventHandlers } from './events';
|
|
3
|
-
import { IS_ELEMENT_CLASS, READY_PROMISE, READY_RESOLVE, CONTROLLER, PROPERTIES, PROPERTY_VALUES, PROPERTIES_INITIALIZED, PROPERTY_WATCHERS, EXPLICITLY_SET_PROPERTIES, ROUTER_CONTEXT } from './symbols';
|
|
3
|
+
import { IS_ELEMENT_CLASS, IS_CONTROLLER_INSTANCE, READY_PROMISE, READY_RESOLVE, CONTROLLER, PROPERTIES, PROPERTY_VALUES, PROPERTIES_INITIALIZED, PROPERTY_WATCHERS, EXPLICITLY_SET_PROPERTIES, ROUTER_CONTEXT, READY_HANDLERS, DISPOSE_HANDLERS } from './symbols';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Applies core element functionality to a constructor
|
|
@@ -106,7 +106,7 @@ export function applyElementFunctionality(constructor: any) {
|
|
|
106
106
|
this[EXPLICITLY_SET_PROPERTIES].add(propName);
|
|
107
107
|
|
|
108
108
|
if (propOptions.type === Boolean) {
|
|
109
|
-
this[propName] = attrValue !== null;
|
|
109
|
+
this[propName] = attrValue !== null && attrValue !== 'false';
|
|
110
110
|
} else if (propOptions.type === Number) {
|
|
111
111
|
this[propName] = Number(attrValue);
|
|
112
112
|
} else {
|
|
@@ -193,6 +193,18 @@ export function applyElementFunctionality(constructor: any) {
|
|
|
193
193
|
}
|
|
194
194
|
// Setup @on event handlers - use element for host events, shadow root for delegated events
|
|
195
195
|
setupEventHandlers(this, this);
|
|
196
|
+
|
|
197
|
+
// Call @ready handlers after everything is set up
|
|
198
|
+
const readyHandlers = constructor[READY_HANDLERS];
|
|
199
|
+
if (readyHandlers) {
|
|
200
|
+
for (const handler of readyHandlers) {
|
|
201
|
+
try {
|
|
202
|
+
await handler.method.call(this);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error(`Error in @ready handler ${handler.methodName}:`, error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
196
208
|
} finally {
|
|
197
209
|
// Always mark element as ready, even if there were errors
|
|
198
210
|
if (this[READY_RESOLVE]) {
|
|
@@ -202,8 +214,20 @@ export function applyElementFunctionality(constructor: any) {
|
|
|
202
214
|
}
|
|
203
215
|
};
|
|
204
216
|
|
|
205
|
-
constructor.prototype.disconnectedCallback = function() {
|
|
206
|
-
// Call
|
|
217
|
+
constructor.prototype.disconnectedCallback = async function() {
|
|
218
|
+
// Call @dispose handlers
|
|
219
|
+
const disposeHandlers = constructor[DISPOSE_HANDLERS];
|
|
220
|
+
if (disposeHandlers) {
|
|
221
|
+
for (const handler of disposeHandlers) {
|
|
222
|
+
try {
|
|
223
|
+
await handler.method.call(this);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error(`Error in @dispose handler ${handler.methodName}:`, error);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Call original user-defined disconnectedCallback
|
|
207
231
|
if (originalDisconnectedCallback) {
|
|
208
232
|
originalDisconnectedCallback.call(this);
|
|
209
233
|
}
|
|
@@ -234,7 +258,7 @@ export function applyElementFunctionality(constructor: any) {
|
|
|
234
258
|
// Parse the new value based on type
|
|
235
259
|
let parsedValue: any;
|
|
236
260
|
if (propOptions.type === Boolean) {
|
|
237
|
-
parsedValue = newValue !== null;
|
|
261
|
+
parsedValue = newValue !== null && newValue !== 'false';
|
|
238
262
|
} else if (propOptions.type === Number) {
|
|
239
263
|
parsedValue = Number(newValue);
|
|
240
264
|
} else {
|
|
@@ -370,17 +394,34 @@ export function property(options?: PropertyOptions) {
|
|
|
370
394
|
};
|
|
371
395
|
}
|
|
372
396
|
|
|
373
|
-
export
|
|
397
|
+
export interface QueryOptions {
|
|
398
|
+
light?: boolean;
|
|
399
|
+
shadow?: boolean;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function query(selector: string, options: QueryOptions = {}) {
|
|
374
403
|
return function (target: any, propertyKey: string) {
|
|
404
|
+
// Default to shadow DOM only
|
|
405
|
+
const { light = false, shadow = true } = options;
|
|
406
|
+
|
|
375
407
|
Object.defineProperty(target, propertyKey, {
|
|
376
408
|
get() {
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
const root = this.element
|
|
380
|
-
|
|
381
|
-
|
|
409
|
+
// Check if this is a controller using the symbol
|
|
410
|
+
const isController = this[IS_CONTROLLER_INSTANCE] === true;
|
|
411
|
+
const root = isController && this.element ? this.element : this;
|
|
412
|
+
|
|
413
|
+
// Query in specified contexts
|
|
414
|
+
let result = null;
|
|
415
|
+
|
|
416
|
+
if (shadow && root.shadowRoot) {
|
|
417
|
+
result = root.shadowRoot.querySelector(selector);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!result && light) {
|
|
421
|
+
result = root.querySelector(selector);
|
|
382
422
|
}
|
|
383
|
-
|
|
423
|
+
|
|
424
|
+
return result || null;
|
|
384
425
|
},
|
|
385
426
|
enumerable: true,
|
|
386
427
|
configurable: true,
|
|
@@ -388,17 +429,32 @@ export function query(selector: string) {
|
|
|
388
429
|
};
|
|
389
430
|
}
|
|
390
431
|
|
|
391
|
-
export function queryAll(selector: string) {
|
|
432
|
+
export function queryAll(selector: string, options: QueryOptions = {}) {
|
|
392
433
|
return function (target: any, propertyKey: string) {
|
|
434
|
+
// Default to shadow DOM only
|
|
435
|
+
const { light = false, shadow = true } = options;
|
|
436
|
+
|
|
393
437
|
Object.defineProperty(target, propertyKey, {
|
|
394
438
|
get() {
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
const root = this.element
|
|
398
|
-
|
|
399
|
-
|
|
439
|
+
// Check if this is a controller using the symbol
|
|
440
|
+
const isController = this[IS_CONTROLLER_INSTANCE] === true;
|
|
441
|
+
const root = isController && this.element ? this.element : this;
|
|
442
|
+
|
|
443
|
+
// Query in specified contexts and combine results
|
|
444
|
+
const results: Element[] = [];
|
|
445
|
+
|
|
446
|
+
if (shadow && root.shadowRoot) {
|
|
447
|
+
const shadowResults = root.shadowRoot.querySelectorAll(selector);
|
|
448
|
+
results.push(...shadowResults);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (light) {
|
|
452
|
+
const lightResults = root.querySelectorAll(selector);
|
|
453
|
+
results.push(...lightResults);
|
|
400
454
|
}
|
|
401
|
-
|
|
455
|
+
|
|
456
|
+
// Return a static NodeList-like object
|
|
457
|
+
return results as any as NodeListOf<Element>;
|
|
402
458
|
},
|
|
403
459
|
enumerable: true,
|
|
404
460
|
configurable: true,
|
|
@@ -495,4 +551,47 @@ export function context() {
|
|
|
495
551
|
configurable: true
|
|
496
552
|
});
|
|
497
553
|
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Decorator for methods that should run when element is ready
|
|
558
|
+
* Runs after shadow DOM, controller attachment, and event setup
|
|
559
|
+
* Supports async methods
|
|
560
|
+
*/
|
|
561
|
+
export function ready() {
|
|
562
|
+
return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
|
|
563
|
+
const constructor = target.constructor;
|
|
564
|
+
|
|
565
|
+
if (!constructor[READY_HANDLERS]) {
|
|
566
|
+
constructor[READY_HANDLERS] = [];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
constructor[READY_HANDLERS].push({
|
|
570
|
+
methodName,
|
|
571
|
+
method: descriptor.value
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
return descriptor;
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Decorator for methods that should run when element is being disposed
|
|
580
|
+
* Used for cleanup tasks when element is removed from DOM
|
|
581
|
+
*/
|
|
582
|
+
export function dispose() {
|
|
583
|
+
return function (target: any, methodName: string, descriptor: PropertyDescriptor) {
|
|
584
|
+
const constructor = target.constructor;
|
|
585
|
+
|
|
586
|
+
if (!constructor[DISPOSE_HANDLERS]) {
|
|
587
|
+
constructor[DISPOSE_HANDLERS] = [];
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
constructor[DISPOSE_HANDLERS].push({
|
|
591
|
+
methodName,
|
|
592
|
+
method: descriptor.value
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
return descriptor;
|
|
596
|
+
};
|
|
498
597
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export { element, customElement, property, query, queryAll, watch, context, applyElementFunctionality } from './element';
|
|
1
|
+
export { element, customElement, property, query, queryAll, watch, context, applyElementFunctionality, ready, dispose } from './element';
|
|
2
2
|
export { Router } from './router';
|
|
3
3
|
export { controller, attachController, detachController, getController, useNativeElementControllers, cleanupNativeElementControllers } from './controller';
|
|
4
4
|
export { on, dispatch } from './events';
|
|
5
5
|
export { channel } from './channel';
|
|
6
|
-
export
|
|
7
|
-
export type {
|
|
6
|
+
export { IS_CONTROLLER_INSTANCE } from './symbols';
|
|
7
|
+
export type { Transition } from './transitions';
|
|
8
|
+
export type { PropertyOptions, PropertyConverter, QueryOptions } from './element';
|
|
9
|
+
export type { RouterOptions, PageOptions, Guard, RouteParams, RouterInstance } from './router';
|
|
8
10
|
export type { IController, ControllerClass } from './controller';
|
|
9
11
|
export type { DispatchOptions } from './events';
|
|
10
12
|
export type { ChannelOptions } from './channel';
|
package/src/router.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Route from 'route-parser';
|
|
2
2
|
import { applyElementFunctionality } from './element';
|
|
3
|
-
import { ROUTER_CONTEXT, CONTEXT_REQUEST_HANDLER } from './symbols';
|
|
3
|
+
import { ROUTER_CONTEXT, CONTEXT_REQUEST_HANDLER, PAGE_TRANSITION } from './symbols';
|
|
4
|
+
import { Transition, performTransition as performTransitionUtil } from './transitions';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Route parameters extracted from the URL
|
|
@@ -39,7 +40,7 @@ export interface RouterOptions {
|
|
|
39
40
|
/**
|
|
40
41
|
* Global transition configuration for all pages
|
|
41
42
|
*/
|
|
42
|
-
transition?:
|
|
43
|
+
transition?: Transition;
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Optional context object passed to guard functions
|
|
@@ -47,38 +48,6 @@ export interface RouterOptions {
|
|
|
47
48
|
context?: any;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
export interface PageTransition {
|
|
51
|
-
/**
|
|
52
|
-
* Name of the transition (for CSS class naming)
|
|
53
|
-
*/
|
|
54
|
-
name?: string;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Duration of the out transition in ms
|
|
58
|
-
*/
|
|
59
|
-
outDuration?: number;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Duration of the in transition in ms
|
|
63
|
-
*/
|
|
64
|
-
inDuration?: number;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* CSS classes or styles for the out transition
|
|
68
|
-
*/
|
|
69
|
-
out?: string;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* CSS classes or styles for the in transition
|
|
73
|
-
*/
|
|
74
|
-
in?: string;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Mode: 'sequential' (out then in) or 'simultaneous' (both at once)
|
|
78
|
-
*/
|
|
79
|
-
mode?: 'sequential' | 'simultaneous';
|
|
80
|
-
}
|
|
81
|
-
|
|
82
51
|
export interface PageOptions {
|
|
83
52
|
/**
|
|
84
53
|
* The tag name of the custom element.
|
|
@@ -96,7 +65,7 @@ export interface PageOptions {
|
|
|
96
65
|
/**
|
|
97
66
|
* Optional per-page transition override
|
|
98
67
|
*/
|
|
99
|
-
transition?:
|
|
68
|
+
transition?: Transition;
|
|
100
69
|
|
|
101
70
|
/**
|
|
102
71
|
* Guard functions that must pass for navigation to proceed.
|
|
@@ -112,7 +81,7 @@ export interface RouterInstance {
|
|
|
112
81
|
page: (pageOptions: PageOptions) => <C extends { new(...args: any[]): HTMLElement }>(constructor: C) => void;
|
|
113
82
|
initialize: () => void;
|
|
114
83
|
navigate: (path: string) => Promise<void>;
|
|
115
|
-
register: (route: string, tag: string, transition?:
|
|
84
|
+
register: (route: string, tag: string, transition?: Transition, guards?: Guard<any> | Guard<any>[]) => void;
|
|
116
85
|
context: any;
|
|
117
86
|
}
|
|
118
87
|
|
|
@@ -122,7 +91,7 @@ export interface RouterInstance {
|
|
|
122
91
|
* @returns An object containing the router's API methods.
|
|
123
92
|
*/
|
|
124
93
|
export function Router(options: RouterOptions): RouterInstance {
|
|
125
|
-
const routes: { route: Route, tag: string, transition?:
|
|
94
|
+
const routes: { route: Route, tag: string, transition?: Transition, guards?: Guard<any> | Guard<any>[] }[] = [];
|
|
126
95
|
let is_sorted = false;
|
|
127
96
|
|
|
128
97
|
let _404: string; // the 404 page
|
|
@@ -142,7 +111,7 @@ export function Router(options: RouterOptions): RouterInstance {
|
|
|
142
111
|
applyElementFunctionality(constructor);
|
|
143
112
|
|
|
144
113
|
// Store transition config on constructor for later use
|
|
145
|
-
(constructor as any)
|
|
114
|
+
(constructor as any)[PAGE_TRANSITION] = pageOptions.transition;
|
|
146
115
|
|
|
147
116
|
// Extend the connectedCallback to add router-specific functionality
|
|
148
117
|
const elementConnectedCallback = constructor.prototype.connectedCallback;
|
|
@@ -196,7 +165,7 @@ export function Router(options: RouterOptions): RouterInstance {
|
|
|
196
165
|
* @example
|
|
197
166
|
* register('/custom-route', 'custom-element');
|
|
198
167
|
*/
|
|
199
|
-
function register(route: string, tag: string, transition?:
|
|
168
|
+
function register(route: string, tag: string, transition?: Transition, guards?: Guard<any> | Guard<any>[]): void {
|
|
200
169
|
routes.push({ route: new Route(route), tag, transition, guards });
|
|
201
170
|
is_sorted = false;
|
|
202
171
|
|
|
@@ -277,7 +246,7 @@ export function Router(options: RouterOptions): RouterInstance {
|
|
|
277
246
|
}
|
|
278
247
|
|
|
279
248
|
let newPageElement: HTMLElement | null = null;
|
|
280
|
-
let transition:
|
|
249
|
+
let transition: Transition | undefined;
|
|
281
250
|
let guards: Guard<any> | Guard<any>[] | undefined;
|
|
282
251
|
|
|
283
252
|
// Home
|
|
@@ -318,7 +287,7 @@ export function Router(options: RouterOptions): RouterInstance {
|
|
|
318
287
|
// Store context on the page element
|
|
319
288
|
(newPageElement as any)[ROUTER_CONTEXT] = context;
|
|
320
289
|
const constructor = customElements.get(home);
|
|
321
|
-
transition = (constructor as any)?.
|
|
290
|
+
transition = (constructor as any)?.[PAGE_TRANSITION];
|
|
322
291
|
} else {
|
|
323
292
|
|
|
324
293
|
// Get the current route
|
|
@@ -372,7 +341,7 @@ export function Router(options: RouterOptions): RouterInstance {
|
|
|
372
341
|
// Store context on 404 page too
|
|
373
342
|
(newPageElement as any)[ROUTER_CONTEXT] = context;
|
|
374
343
|
const constructor = customElements.get(_404);
|
|
375
|
-
transition = (constructor as any)?.
|
|
344
|
+
transition = (constructor as any)?.[PAGE_TRANSITION];
|
|
376
345
|
} else {
|
|
377
346
|
// Provide a default 404 page
|
|
378
347
|
const div = document.createElement('div');
|
|
@@ -403,83 +372,9 @@ export function Router(options: RouterOptions): RouterInstance {
|
|
|
403
372
|
container: Element,
|
|
404
373
|
oldElement: HTMLElement,
|
|
405
374
|
newElement: HTMLElement,
|
|
406
|
-
transition:
|
|
375
|
+
transition: Transition
|
|
407
376
|
): Promise<void> {
|
|
408
|
-
|
|
409
|
-
const inDuration = transition.inDuration || 300;
|
|
410
|
-
const mode = transition.mode || 'sequential';
|
|
411
|
-
|
|
412
|
-
// Parse CSS properties from transition config
|
|
413
|
-
const parseStyles = (styleString: string): Record<string, string> => {
|
|
414
|
-
const styles: Record<string, string> = {};
|
|
415
|
-
styleString.split(';').forEach(rule => {
|
|
416
|
-
const [prop, value] = rule.split(':').map(s => s.trim());
|
|
417
|
-
if (prop && value) {
|
|
418
|
-
styles[prop] = value;
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
return styles;
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
// Default transitions
|
|
425
|
-
const outStyles = transition.out ? parseStyles(transition.out) : { opacity: '0' };
|
|
426
|
-
const inStartStyles = { opacity: '0' }; // Always start invisible
|
|
427
|
-
const inEndStyles = transition.in ? parseStyles(transition.in) : { opacity: '1' };
|
|
428
|
-
|
|
429
|
-
// Set container to relative positioning to allow absolute positioning of pages
|
|
430
|
-
const containerStyle = (container as HTMLElement).style;
|
|
431
|
-
const originalPosition = containerStyle.position;
|
|
432
|
-
containerStyle.position = 'relative';
|
|
433
|
-
|
|
434
|
-
// Style old element for transition
|
|
435
|
-
oldElement.style.position = 'absolute';
|
|
436
|
-
oldElement.style.top = '0';
|
|
437
|
-
oldElement.style.left = '0';
|
|
438
|
-
oldElement.style.width = '100%';
|
|
439
|
-
oldElement.style.transition = `all ${outDuration}ms ease-in-out`;
|
|
440
|
-
|
|
441
|
-
// Style new element with initial state
|
|
442
|
-
newElement.style.position = 'absolute';
|
|
443
|
-
newElement.style.top = '0';
|
|
444
|
-
newElement.style.left = '0';
|
|
445
|
-
newElement.style.width = '100%';
|
|
446
|
-
Object.assign(newElement.style, inStartStyles);
|
|
447
|
-
newElement.style.transition = `all ${inDuration}ms ease-in-out`;
|
|
448
|
-
|
|
449
|
-
// Add new element to container
|
|
450
|
-
container.appendChild(newElement);
|
|
451
|
-
|
|
452
|
-
// Force browser to calculate styles
|
|
453
|
-
void newElement.offsetHeight;
|
|
454
|
-
|
|
455
|
-
if (mode === 'simultaneous') {
|
|
456
|
-
// Start both transitions at once
|
|
457
|
-
Object.assign(oldElement.style, outStyles);
|
|
458
|
-
Object.assign(newElement.style, inEndStyles);
|
|
459
|
-
|
|
460
|
-
// Wait for both transitions to complete
|
|
461
|
-
await new Promise(resolve => setTimeout(resolve, Math.max(outDuration, inDuration)));
|
|
462
|
-
} else {
|
|
463
|
-
// Sequential: transition out old, then transition in new
|
|
464
|
-
Object.assign(oldElement.style, outStyles);
|
|
465
|
-
await new Promise(resolve => setTimeout(resolve, outDuration));
|
|
466
|
-
|
|
467
|
-
Object.assign(newElement.style, inEndStyles);
|
|
468
|
-
await new Promise(resolve => setTimeout(resolve, inDuration));
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Cleanup
|
|
472
|
-
oldElement.remove();
|
|
473
|
-
newElement.style.position = '';
|
|
474
|
-
newElement.style.top = '';
|
|
475
|
-
newElement.style.left = '';
|
|
476
|
-
newElement.style.width = '';
|
|
477
|
-
newElement.style.transition = '';
|
|
478
|
-
// Reset any transition styles
|
|
479
|
-
Object.keys({...inStartStyles, ...inEndStyles}).forEach(prop => {
|
|
480
|
-
newElement.style[prop as any] = '';
|
|
481
|
-
});
|
|
482
|
-
containerStyle.position = originalPosition;
|
|
377
|
+
return performTransitionUtil(container, oldElement, newElement, transition);
|
|
483
378
|
}
|
|
484
379
|
|
|
485
380
|
return {
|
package/src/symbols.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { getSymbol } from './global';
|
|
5
5
|
|
|
6
6
|
export const IS_CONTROLLER_CLASS = getSymbol('is-controller-class');
|
|
7
|
+
export const IS_CONTROLLER_INSTANCE = getSymbol('is-controller-instance');
|
|
7
8
|
export const IS_ELEMENT_CLASS = getSymbol('is-element-class');
|
|
8
9
|
export const CHANNEL_HANDLERS = getSymbol('channel-handlers');
|
|
9
10
|
|
|
@@ -34,4 +35,9 @@ export const EXPLICITLY_SET_PROPERTIES = getSymbol('explicitly-set-properties');
|
|
|
34
35
|
|
|
35
36
|
// Router context symbol
|
|
36
37
|
export const ROUTER_CONTEXT = getSymbol('router-context');
|
|
37
|
-
export const CONTEXT_REQUEST_HANDLER = getSymbol('context-request-handler');
|
|
38
|
+
export const CONTEXT_REQUEST_HANDLER = getSymbol('context-request-handler');
|
|
39
|
+
export const PAGE_TRANSITION = getSymbol('page-transition');
|
|
40
|
+
|
|
41
|
+
// Lifecycle symbols
|
|
42
|
+
export const READY_HANDLERS = getSymbol('ready-handlers');
|
|
43
|
+
export const DISPOSE_HANDLERS = getSymbol('dispose-handlers');
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic transition system for animating between elements
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface Transition {
|
|
6
|
+
/**
|
|
7
|
+
* Name of the transition (for identification)
|
|
8
|
+
*/
|
|
9
|
+
name?: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Duration of the out transition in ms
|
|
13
|
+
*/
|
|
14
|
+
outDuration?: number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Duration of the in transition in ms
|
|
18
|
+
*/
|
|
19
|
+
inDuration?: number;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* CSS properties for the out transition (as string)
|
|
23
|
+
* Example: 'opacity: 0; transform: scale(0.9)'
|
|
24
|
+
*/
|
|
25
|
+
out?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* CSS properties for the in transition (as string)
|
|
29
|
+
* Example: 'opacity: 1; transform: scale(1)'
|
|
30
|
+
*/
|
|
31
|
+
in?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Transition mode:
|
|
35
|
+
* - 'sequential': out transition completes before in transition starts
|
|
36
|
+
* - 'simultaneous': both transitions happen at the same time
|
|
37
|
+
*/
|
|
38
|
+
mode?: 'sequential' | 'simultaneous';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse CSS property string into an object
|
|
43
|
+
*/
|
|
44
|
+
function parseStyles(styleString: string): Record<string, string> {
|
|
45
|
+
const styles: Record<string, string> = {};
|
|
46
|
+
styleString.split(';').forEach(rule => {
|
|
47
|
+
const [prop, value] = rule.split(':').map(s => s.trim());
|
|
48
|
+
if (prop && value) {
|
|
49
|
+
styles[prop] = value;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return styles;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Perform a transition between two elements
|
|
57
|
+
*/
|
|
58
|
+
export async function performTransition(
|
|
59
|
+
container: Element,
|
|
60
|
+
oldElement: HTMLElement,
|
|
61
|
+
newElement: HTMLElement,
|
|
62
|
+
transition: Transition = {}
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
const outDuration = transition.outDuration || 300;
|
|
65
|
+
const inDuration = transition.inDuration || 300;
|
|
66
|
+
const mode = transition.mode || 'sequential';
|
|
67
|
+
|
|
68
|
+
// Default transitions
|
|
69
|
+
const outStyles = transition.out ? parseStyles(transition.out) : { opacity: '0' };
|
|
70
|
+
const inStartStyles = { opacity: '0' }; // Always start invisible
|
|
71
|
+
const inEndStyles = transition.in ? parseStyles(transition.in) : { opacity: '1' };
|
|
72
|
+
|
|
73
|
+
// Set container to relative positioning to allow absolute positioning
|
|
74
|
+
const containerStyle = (container as HTMLElement).style;
|
|
75
|
+
const originalPosition = containerStyle.position;
|
|
76
|
+
containerStyle.position = 'relative';
|
|
77
|
+
|
|
78
|
+
// Style old element for transition
|
|
79
|
+
oldElement.style.position = 'absolute';
|
|
80
|
+
oldElement.style.top = '0';
|
|
81
|
+
oldElement.style.left = '0';
|
|
82
|
+
oldElement.style.width = '100%';
|
|
83
|
+
oldElement.style.transition = `all ${outDuration}ms ease-in-out`;
|
|
84
|
+
|
|
85
|
+
// Style new element with initial state
|
|
86
|
+
newElement.style.position = 'absolute';
|
|
87
|
+
newElement.style.top = '0';
|
|
88
|
+
newElement.style.left = '0';
|
|
89
|
+
newElement.style.width = '100%';
|
|
90
|
+
Object.assign(newElement.style, inStartStyles);
|
|
91
|
+
newElement.style.transition = `all ${inDuration}ms ease-in-out`;
|
|
92
|
+
|
|
93
|
+
// Add new element to container
|
|
94
|
+
container.appendChild(newElement);
|
|
95
|
+
|
|
96
|
+
// Force browser to calculate styles
|
|
97
|
+
void newElement.offsetHeight;
|
|
98
|
+
|
|
99
|
+
if (mode === 'simultaneous') {
|
|
100
|
+
// Start both transitions at once
|
|
101
|
+
Object.assign(oldElement.style, outStyles);
|
|
102
|
+
Object.assign(newElement.style, inEndStyles);
|
|
103
|
+
|
|
104
|
+
// Wait for both transitions to complete
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, Math.max(outDuration, inDuration)));
|
|
106
|
+
} else {
|
|
107
|
+
// Sequential: transition out old, then transition in new
|
|
108
|
+
Object.assign(oldElement.style, outStyles);
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, outDuration));
|
|
110
|
+
|
|
111
|
+
Object.assign(newElement.style, inEndStyles);
|
|
112
|
+
await new Promise(resolve => setTimeout(resolve, inDuration));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Cleanup
|
|
116
|
+
oldElement.remove();
|
|
117
|
+
newElement.style.position = '';
|
|
118
|
+
newElement.style.top = '';
|
|
119
|
+
newElement.style.left = '';
|
|
120
|
+
newElement.style.width = '';
|
|
121
|
+
newElement.style.transition = '';
|
|
122
|
+
// Reset any transition styles
|
|
123
|
+
Object.keys({...inStartStyles, ...inEndStyles}).forEach(prop => {
|
|
124
|
+
newElement.style[prop as any] = '';
|
|
125
|
+
});
|
|
126
|
+
containerStyle.position = originalPosition;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Predefined transitions
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
// Fade transition
|
|
134
|
+
export const fadeTransition: Transition = {
|
|
135
|
+
name: 'fade',
|
|
136
|
+
outDuration: 200,
|
|
137
|
+
inDuration: 200,
|
|
138
|
+
out: 'opacity: 0',
|
|
139
|
+
in: 'opacity: 1',
|
|
140
|
+
mode: 'simultaneous'
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Slide transition (from left)
|
|
144
|
+
export const slideTransition: Transition = {
|
|
145
|
+
name: 'slide',
|
|
146
|
+
outDuration: 300,
|
|
147
|
+
inDuration: 300,
|
|
148
|
+
out: 'transform: translateX(-100%)',
|
|
149
|
+
in: 'transform: translateX(0)',
|
|
150
|
+
mode: 'sequential'
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Slide from right
|
|
154
|
+
export const slideRightTransition: Transition = {
|
|
155
|
+
name: 'slide-right',
|
|
156
|
+
outDuration: 300,
|
|
157
|
+
inDuration: 300,
|
|
158
|
+
out: 'transform: translateX(100%)',
|
|
159
|
+
in: 'transform: translateX(0)',
|
|
160
|
+
mode: 'sequential'
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Slide from top
|
|
164
|
+
export const slideUpTransition: Transition = {
|
|
165
|
+
name: 'slide-up',
|
|
166
|
+
outDuration: 300,
|
|
167
|
+
inDuration: 300,
|
|
168
|
+
out: 'transform: translateY(-100%)',
|
|
169
|
+
in: 'transform: translateY(0)',
|
|
170
|
+
mode: 'sequential'
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Slide from bottom
|
|
174
|
+
export const slideDownTransition: Transition = {
|
|
175
|
+
name: 'slide-down',
|
|
176
|
+
outDuration: 300,
|
|
177
|
+
inDuration: 300,
|
|
178
|
+
out: 'transform: translateY(100%)',
|
|
179
|
+
in: 'transform: translateY(0)',
|
|
180
|
+
mode: 'sequential'
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Scale transition
|
|
184
|
+
export const scaleTransition: Transition = {
|
|
185
|
+
name: 'scale',
|
|
186
|
+
outDuration: 250,
|
|
187
|
+
inDuration: 250,
|
|
188
|
+
out: 'transform: scale(0.9); opacity: 0',
|
|
189
|
+
in: 'transform: scale(1); opacity: 1',
|
|
190
|
+
mode: 'simultaneous'
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Rotate transition
|
|
194
|
+
export const rotateTransition: Transition = {
|
|
195
|
+
name: 'rotate',
|
|
196
|
+
outDuration: 400,
|
|
197
|
+
inDuration: 400,
|
|
198
|
+
out: 'transform: rotate(180deg) scale(0.5); opacity: 0',
|
|
199
|
+
in: 'transform: rotate(0) scale(1); opacity: 1',
|
|
200
|
+
mode: 'simultaneous'
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Flip transition
|
|
204
|
+
export const flipTransition: Transition = {
|
|
205
|
+
name: 'flip',
|
|
206
|
+
outDuration: 400,
|
|
207
|
+
inDuration: 400,
|
|
208
|
+
out: 'transform: rotateY(180deg); opacity: 0',
|
|
209
|
+
in: 'transform: rotateY(0); opacity: 1',
|
|
210
|
+
mode: 'sequential'
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Zoom transition
|
|
214
|
+
export const zoomTransition: Transition = {
|
|
215
|
+
name: 'zoom',
|
|
216
|
+
outDuration: 300,
|
|
217
|
+
inDuration: 300,
|
|
218
|
+
out: 'transform: scale(2); opacity: 0',
|
|
219
|
+
in: 'transform: scale(1); opacity: 1',
|
|
220
|
+
mode: 'simultaneous'
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// None transition (instant swap)
|
|
224
|
+
export const noneTransition: Transition = {
|
|
225
|
+
name: 'none',
|
|
226
|
+
outDuration: 0,
|
|
227
|
+
inDuration: 0,
|
|
228
|
+
out: '',
|
|
229
|
+
in: '',
|
|
230
|
+
mode: 'simultaneous'
|
|
231
|
+
};
|