snice 1.14.0 → 1.14.1
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/dist/index.cjs +1 -1
- package/dist/index.cjs.min +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.iife.js +1 -1
- package/dist/index.iife.min.js +1 -1
- package/dist/symbols.cjs +1 -1
- package/dist/symbols.esm.js +1 -1
- package/dist/transitions.cjs +1 -1
- package/dist/transitions.esm.js +1 -1
- package/package.json +3 -3
- package/src/controller.ts +0 -347
- package/src/element.ts +0 -908
- package/src/events.ts +0 -349
- package/src/global.ts +0 -31
- package/src/index.ts +0 -14
- package/src/observe.ts +0 -414
- package/src/request-response.ts +0 -336
- package/src/router.ts +0 -552
- package/src/symbols.ts +0 -54
- package/src/transitions.ts +0 -264
package/dist/index.cjs
CHANGED
package/dist/index.cjs.min
CHANGED
package/dist/index.esm.js
CHANGED
package/dist/index.esm.min.js
CHANGED
package/dist/index.iife.js
CHANGED
package/dist/index.iife.min.js
CHANGED
package/dist/symbols.cjs
CHANGED
package/dist/symbols.esm.js
CHANGED
package/dist/transitions.cjs
CHANGED
package/dist/transitions.esm.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snice",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Imperative TypeScript framework for building vanilla web components with decorators, routing, and controllers. No virtual DOM, no build complexity.",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
"import": "./dist/transitions.esm.js",
|
|
24
24
|
"require": "./dist/transitions.cjs",
|
|
25
25
|
"types": "./dist/types/transitions.d.ts"
|
|
26
|
-
}
|
|
26
|
+
},
|
|
27
|
+
"./components/*": "./components/*"
|
|
27
28
|
},
|
|
28
29
|
"bin": {
|
|
29
30
|
"snice": "./bin/snice.js"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
32
33
|
"dist",
|
|
33
|
-
"src",
|
|
34
34
|
"components",
|
|
35
35
|
"bin",
|
|
36
36
|
"README.md",
|
package/src/controller.ts
DELETED
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import { setupEventHandlers, cleanupEventHandlers } from './events';
|
|
2
|
-
import { setupObservers, cleanupObservers } from './observe';
|
|
3
|
-
import { setupResponseHandlers, cleanupResponseHandlers } from './request-response';
|
|
4
|
-
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';
|
|
5
|
-
import { snice } from './global';
|
|
6
|
-
|
|
7
|
-
type Maybe<T> = T | null | undefined;
|
|
8
|
-
|
|
9
|
-
export interface IController<T extends HTMLElement = HTMLElement> {
|
|
10
|
-
element: Maybe<T>;
|
|
11
|
-
attach(element: T): void | Promise<void>;
|
|
12
|
-
detach(element: T): void | Promise<void>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type ControllerClass<T extends HTMLElement = HTMLElement> = new() => IController<T>;
|
|
16
|
-
|
|
17
|
-
// Controller-scoped cleanup registry
|
|
18
|
-
class ControllerScope {
|
|
19
|
-
private cleanupFns: Map<string, Function> = new Map();
|
|
20
|
-
private pendingOperations: Set<Promise<void>> = new Set();
|
|
21
|
-
|
|
22
|
-
register(key: string, cleanup: Function): void {
|
|
23
|
-
this.cleanupFns.set(key, cleanup);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
unregister(key: string): void {
|
|
27
|
-
this.cleanupFns.delete(key);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async cleanup(): Promise<void> {
|
|
31
|
-
// Wait for all pending operations
|
|
32
|
-
await Promise.all(this.pendingOperations);
|
|
33
|
-
|
|
34
|
-
// Run all cleanup functions
|
|
35
|
-
for (const cleanup of this.cleanupFns.values()) {
|
|
36
|
-
try {
|
|
37
|
-
await cleanup();
|
|
38
|
-
} catch (error) {
|
|
39
|
-
console.error('Error during cleanup:', error);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
this.cleanupFns.clear();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async runOperation<T>(operation: () => Promise<T>): Promise<T> {
|
|
46
|
-
const promise = operation();
|
|
47
|
-
const voidPromise = promise.then(() => {}, () => {});
|
|
48
|
-
this.pendingOperations.add(voidPromise);
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const result = await promise;
|
|
52
|
-
this.pendingOperations.delete(voidPromise);
|
|
53
|
-
return result;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
this.pendingOperations.delete(voidPromise);
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Decorator to register a controller class with a name
|
|
63
|
-
* @param name The name to register the controller under
|
|
64
|
-
*/
|
|
65
|
-
export function controller(name: string) {
|
|
66
|
-
return function <T extends ControllerClass>(constructor: T) {
|
|
67
|
-
snice.controllerRegistry.set(name, constructor);
|
|
68
|
-
// Mark as controller class for channel decorator detection
|
|
69
|
-
(constructor.prototype as any)[IS_CONTROLLER_CLASS] = true;
|
|
70
|
-
return constructor;
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Attaches a controller to an element
|
|
76
|
-
* @param element The element to attach the controller to
|
|
77
|
-
* @param controllerName The name of the controller to attach
|
|
78
|
-
*/
|
|
79
|
-
export async function attachController(element: HTMLElement, controllerName: string): Promise<void> {
|
|
80
|
-
const existingController = (element as any)[CONTROLLER_KEY] as IController | undefined;
|
|
81
|
-
const existingName = (element as any)[CONTROLLER_NAME_KEY] as string | undefined;
|
|
82
|
-
|
|
83
|
-
// For native elements, check if this is actually the desired controller
|
|
84
|
-
const nativeController = (element as any)[NATIVE_CONTROLLER];
|
|
85
|
-
if (nativeController !== undefined && nativeController !== controllerName) {
|
|
86
|
-
// This attachment is outdated, skip it
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (existingName === controllerName && existingController) {
|
|
91
|
-
// Already attached and controller exists
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// If there's an existing controller, detach it first
|
|
96
|
-
if (existingController) {
|
|
97
|
-
await detachController(element);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const ControllerClass = snice.controllerRegistry.get(controllerName);
|
|
101
|
-
if (!ControllerClass) {
|
|
102
|
-
throw new Error(`Controller "${controllerName}" not found in registry`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Create controller instance with unique ID and scope
|
|
106
|
-
const controllerInstance = new ControllerClass();
|
|
107
|
-
snice.controllerIdCounter += 1;
|
|
108
|
-
const controllerId = snice.controllerIdCounter;
|
|
109
|
-
const scope = new ControllerScope();
|
|
110
|
-
|
|
111
|
-
// Mark this as a controller instance
|
|
112
|
-
(controllerInstance as any)[IS_CONTROLLER_INSTANCE] = true;
|
|
113
|
-
(controllerInstance as any)[CONTROLLER_ID] = controllerId;
|
|
114
|
-
controllerInstance.element = element;
|
|
115
|
-
|
|
116
|
-
// Pass router context from element to controller if it exists
|
|
117
|
-
const routerContext = (element as any)[ROUTER_CONTEXT];
|
|
118
|
-
if (routerContext !== undefined) {
|
|
119
|
-
(controllerInstance as any)[ROUTER_CONTEXT] = routerContext;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Store references
|
|
123
|
-
(element as any)[CONTROLLER_KEY] = controllerInstance;
|
|
124
|
-
(element as any)[CONTROLLER_NAME_KEY] = controllerName;
|
|
125
|
-
(element as any)[CONTROLLER_OPERATIONS] = scope;
|
|
126
|
-
|
|
127
|
-
// Wait for element to be ready (required)
|
|
128
|
-
await (element as any).ready;
|
|
129
|
-
|
|
130
|
-
// Run attach in the controller's scope
|
|
131
|
-
await scope.runOperation(async () => {
|
|
132
|
-
await controllerInstance.attach(element);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Setup @on event handlers for controller
|
|
136
|
-
setupEventHandlers(controllerInstance, element);
|
|
137
|
-
|
|
138
|
-
// Setup @observe observers for controller
|
|
139
|
-
setupObservers(controllerInstance, element);
|
|
140
|
-
|
|
141
|
-
// Setup @channel handlers for controller
|
|
142
|
-
setupResponseHandlers(controllerInstance, element);
|
|
143
|
-
|
|
144
|
-
element.dispatchEvent(new CustomEvent('@snice/controller-attached', {
|
|
145
|
-
detail: { name: controllerName, controller: controllerInstance }
|
|
146
|
-
}));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Detaches a controller from an element
|
|
151
|
-
* @param element The element to detach the controller from
|
|
152
|
-
*/
|
|
153
|
-
export async function detachController(element: HTMLElement): Promise<void> {
|
|
154
|
-
const controllerInstance = (element as any)[CONTROLLER_KEY] as IController | undefined;
|
|
155
|
-
const controllerName = (element as any)[CONTROLLER_NAME_KEY] as string | undefined;
|
|
156
|
-
const scope = (element as any)[CONTROLLER_OPERATIONS] as ControllerScope | undefined;
|
|
157
|
-
|
|
158
|
-
if (!controllerInstance) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Run detach in the controller's scope
|
|
163
|
-
if (scope) {
|
|
164
|
-
await scope.runOperation(async () => {
|
|
165
|
-
await controllerInstance.detach(element);
|
|
166
|
-
});
|
|
167
|
-
} else {
|
|
168
|
-
await controllerInstance.detach(element);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
controllerInstance.element = null;
|
|
172
|
-
|
|
173
|
-
// Cleanup @on event handlers for controller
|
|
174
|
-
cleanupEventHandlers(controllerInstance);
|
|
175
|
-
|
|
176
|
-
// Cleanup @observe observers for controller
|
|
177
|
-
cleanupObservers(controllerInstance);
|
|
178
|
-
|
|
179
|
-
// Cleanup @channel handlers for controller
|
|
180
|
-
cleanupResponseHandlers(controllerInstance);
|
|
181
|
-
|
|
182
|
-
// Cleanup the controller scope
|
|
183
|
-
if (scope) {
|
|
184
|
-
await scope.cleanup();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Clean up router context reference
|
|
188
|
-
delete (controllerInstance as any)[ROUTER_CONTEXT];
|
|
189
|
-
|
|
190
|
-
delete (element as any)[CONTROLLER_KEY];
|
|
191
|
-
delete (element as any)[CONTROLLER_NAME_KEY];
|
|
192
|
-
delete (element as any)[CONTROLLER_OPERATIONS];
|
|
193
|
-
|
|
194
|
-
element.dispatchEvent(new CustomEvent('@snice/controller-detached', {
|
|
195
|
-
detail: { name: controllerName, controller: controllerInstance }
|
|
196
|
-
}));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Gets the controller instance attached to an element
|
|
201
|
-
* @param element The element to get the controller from
|
|
202
|
-
* @returns The controller instance or undefined
|
|
203
|
-
*/
|
|
204
|
-
export function getController<T extends IController = IController>(element: HTMLElement): T | undefined {
|
|
205
|
-
return (element as any)[CONTROLLER_KEY] as T | undefined;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Gets the controller scope for an element
|
|
210
|
-
* @param element The element to get the scope from
|
|
211
|
-
* @returns The controller scope or undefined
|
|
212
|
-
*/
|
|
213
|
-
export function getControllerScope(element: HTMLElement): ControllerScope | undefined {
|
|
214
|
-
return (element as any)[CONTROLLER_OPERATIONS] as ControllerScope | undefined;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Enable controller support for native HTML elements
|
|
219
|
-
* This sets up a MutationObserver to watch for controller attributes
|
|
220
|
-
* on non-custom elements (elements without hyphens in their tag names)
|
|
221
|
-
*/
|
|
222
|
-
export function useNativeElementControllers() {
|
|
223
|
-
// Return if already initialized
|
|
224
|
-
if ((globalThis as any).sniceNativeControllersInitialized) {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
(globalThis as any).sniceNativeControllersInitialized = true;
|
|
228
|
-
|
|
229
|
-
// Process elements that already have controller attribute
|
|
230
|
-
function processElement(element: Element) {
|
|
231
|
-
if (!(element instanceof HTMLElement)) return;
|
|
232
|
-
|
|
233
|
-
// Skip custom elements (they handle controllers themselves)
|
|
234
|
-
if (element.tagName.includes('-')) return;
|
|
235
|
-
|
|
236
|
-
// Skip elements that are @element decorated (they have their own controller handling)
|
|
237
|
-
if ((element as any)[IS_ELEMENT_CLASS]) return;
|
|
238
|
-
|
|
239
|
-
const controllerName = element.getAttribute('controller');
|
|
240
|
-
const currentControllerName = (element as any)[NATIVE_CONTROLLER];
|
|
241
|
-
|
|
242
|
-
if (controllerName && controllerName !== currentControllerName) {
|
|
243
|
-
// Controller added or changed
|
|
244
|
-
(element as any)[NATIVE_CONTROLLER] = controllerName;
|
|
245
|
-
|
|
246
|
-
// For non-custom elements, we need to add the ready promise
|
|
247
|
-
if (!(element as any).ready) {
|
|
248
|
-
(element as any).ready = Promise.resolve();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Detach old controller if exists (don't await - let it run async)
|
|
252
|
-
if (currentControllerName) {
|
|
253
|
-
detachController(element as HTMLElement).catch(error => {
|
|
254
|
-
console.error(`Failed to detach old controller from native element:`, error);
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Attach the new controller
|
|
259
|
-
attachController(element as HTMLElement, controllerName).catch(error => {
|
|
260
|
-
console.error(`Failed to attach controller "${controllerName}" to native element:`, error);
|
|
261
|
-
});
|
|
262
|
-
} else if (!controllerName && currentControllerName) {
|
|
263
|
-
// Controller was removed
|
|
264
|
-
delete (element as any)[NATIVE_CONTROLLER];
|
|
265
|
-
// Clear the controller name immediately to allow re-attachment
|
|
266
|
-
delete (element as any)[CONTROLLER_NAME_KEY];
|
|
267
|
-
detachController(element as HTMLElement).catch(error => {
|
|
268
|
-
console.error(`Failed to detach controller from native element:`, error);
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Set up MutationObserver to watch for controller attributes
|
|
274
|
-
const observer = new MutationObserver((mutations) => {
|
|
275
|
-
for (const mutation of mutations) {
|
|
276
|
-
if (mutation.type === 'attributes' && mutation.attributeName === 'controller') {
|
|
277
|
-
processElement(mutation.target as Element);
|
|
278
|
-
} else if (mutation.type === 'childList') {
|
|
279
|
-
// Process added nodes
|
|
280
|
-
mutation.addedNodes.forEach(node => {
|
|
281
|
-
if (node instanceof HTMLElement) {
|
|
282
|
-
// Process the node itself
|
|
283
|
-
processElement(node);
|
|
284
|
-
// Process all descendants with controller attribute
|
|
285
|
-
node.querySelectorAll('[controller]:not([class*="-"])').forEach(processElement);
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// Start observing when DOM is ready
|
|
293
|
-
if (document.readyState === 'loading') {
|
|
294
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
295
|
-
// Process existing elements (excluding custom elements)
|
|
296
|
-
document.querySelectorAll('[controller]:not([class*="-"])').forEach(processElement);
|
|
297
|
-
|
|
298
|
-
// Start observing
|
|
299
|
-
observer.observe(document.body, {
|
|
300
|
-
attributes: true,
|
|
301
|
-
attributeFilter: ['controller'],
|
|
302
|
-
childList: true,
|
|
303
|
-
subtree: true
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
} else {
|
|
307
|
-
// DOM already loaded
|
|
308
|
-
document.querySelectorAll('[controller]:not([class*="-"])').forEach(processElement);
|
|
309
|
-
|
|
310
|
-
observer.observe(document.body, {
|
|
311
|
-
attributes: true,
|
|
312
|
-
attributeFilter: ['controller'],
|
|
313
|
-
childList: true,
|
|
314
|
-
subtree: true
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Store observer reference for cleanup if needed
|
|
319
|
-
(globalThis as any).sniceNativeControllerObserver = observer;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Stop watching for native element controllers
|
|
324
|
-
*/
|
|
325
|
-
export function cleanupNativeElementControllers() {
|
|
326
|
-
const observer = (globalThis as any).sniceNativeControllerObserver;
|
|
327
|
-
if (observer) {
|
|
328
|
-
observer.disconnect();
|
|
329
|
-
delete (globalThis as any).sniceNativeControllerObserver;
|
|
330
|
-
delete (globalThis as any).sniceNativeControllersInitialized;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Registers a cleanup function for the current controller
|
|
336
|
-
* @param controller The controller instance
|
|
337
|
-
* @param key A unique key for this cleanup function
|
|
338
|
-
* @param cleanup The cleanup function to register
|
|
339
|
-
*/
|
|
340
|
-
export function registerControllerCleanup(controller: IController, key: string, cleanup: Function): void {
|
|
341
|
-
if (!controller.element) return;
|
|
342
|
-
|
|
343
|
-
const scope = getControllerScope(controller.element as HTMLElement);
|
|
344
|
-
if (scope) {
|
|
345
|
-
scope.register(key, cleanup);
|
|
346
|
-
}
|
|
347
|
-
}
|