sygnal 5.2.1 → 5.3.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/README.md +20 -1
- package/dist/astro/client.cjs.js +242 -12
- package/dist/astro/client.mjs +242 -12
- package/dist/index.cjs.js +437 -15
- package/dist/index.d.ts +29 -0
- package/dist/index.esm.js +435 -16
- package/dist/sygnal.min.js +1 -1
- package/package.json +1 -1
- package/src/component.ts +34 -4
- package/src/extra/command.ts +13 -2
- package/src/extra/devtools.ts +221 -7
- package/src/extra/dragDriver.ts +59 -5
- package/src/extra/eventDriver.ts +6 -2
- package/src/extra/pwa.ts +181 -0
- package/src/index.d.ts +29 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
package/src/component.ts
CHANGED
|
@@ -369,6 +369,9 @@ class Component {
|
|
|
369
369
|
this.sources.props$ = props$.map((val: any) => {
|
|
370
370
|
const { sygnalFactory, sygnalOptions, ...sanitizedProps }: any = val
|
|
371
371
|
this.currentProps = sanitizedProps
|
|
372
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
373
|
+
window.__SYGNAL_DEVTOOLS__.onPropsChanged(this._componentNumber, this.name, sanitizedProps)
|
|
374
|
+
}
|
|
372
375
|
return val
|
|
373
376
|
})
|
|
374
377
|
}
|
|
@@ -450,6 +453,9 @@ class Component {
|
|
|
450
453
|
}
|
|
451
454
|
|
|
452
455
|
dispose(): void {
|
|
456
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
457
|
+
window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name)
|
|
458
|
+
}
|
|
453
459
|
// Fire the DISPOSE built-in action so model handlers can run cleanup logic
|
|
454
460
|
const hasDispose = this.model && (this.model[DISPOSE_ACTION] || Object.keys(this.model).some(k => k.includes('|') && k.split('|')[0].trim() === DISPOSE_ACTION))
|
|
455
461
|
if (hasDispose && this.action$ && typeof this.action$.shamefullySendNext === 'function') {
|
|
@@ -870,6 +876,12 @@ class Component {
|
|
|
870
876
|
} else {
|
|
871
877
|
acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []))
|
|
872
878
|
}
|
|
879
|
+
// Stamp EVENTS sink emissions with emitter component info for devtools
|
|
880
|
+
if (name === 'EVENTS' && acc[name]) {
|
|
881
|
+
const _componentNumber = this._componentNumber
|
|
882
|
+
const _name = this.name
|
|
883
|
+
acc[name] = acc[name].map((ev: any) => ({...ev, __emitterId: _componentNumber, __emitterName: _name}))
|
|
884
|
+
}
|
|
873
885
|
return acc
|
|
874
886
|
}, {} as Record<string, any>)
|
|
875
887
|
|
|
@@ -1420,6 +1432,18 @@ class Component {
|
|
|
1420
1432
|
if (!isObj(sink$)) {
|
|
1421
1433
|
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`)
|
|
1422
1434
|
}
|
|
1435
|
+
|
|
1436
|
+
// Notify devtools of collection mount
|
|
1437
|
+
if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
|
|
1438
|
+
const itemName = typeof collectionOf === 'function'
|
|
1439
|
+
? (collectionOf.componentName || collectionOf.label || collectionOf.name || 'anonymous')
|
|
1440
|
+
: String(collectionOf)
|
|
1441
|
+
;(window as any).__SYGNAL_DEVTOOLS__.onCollectionMounted(
|
|
1442
|
+
this._componentNumber, this.name, itemName,
|
|
1443
|
+
typeof stateField === 'string' ? stateField : null
|
|
1444
|
+
)
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1423
1447
|
return sink$
|
|
1424
1448
|
}
|
|
1425
1449
|
|
|
@@ -1571,6 +1595,7 @@ class Component {
|
|
|
1571
1595
|
for (const key of Object.keys(props)) {
|
|
1572
1596
|
const val = props[key]
|
|
1573
1597
|
if (val && val.__sygnalCommand) {
|
|
1598
|
+
val._targetComponentName = componentName
|
|
1574
1599
|
sources.commands$ = makeCommandSource(val as Command)
|
|
1575
1600
|
break
|
|
1576
1601
|
}
|
|
@@ -1620,10 +1645,15 @@ class Component {
|
|
|
1620
1645
|
const wasReady = this._childReadyState[id]
|
|
1621
1646
|
this._childReadyState[id] = !!ready
|
|
1622
1647
|
// When READY state changes, trigger a re-render
|
|
1623
|
-
if (wasReady !== !!ready
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1648
|
+
if (wasReady !== !!ready) {
|
|
1649
|
+
if (this._readyChangedListener) {
|
|
1650
|
+
setTimeout(() => {
|
|
1651
|
+
this._readyChangedListener?.next(null)
|
|
1652
|
+
}, 0)
|
|
1653
|
+
}
|
|
1654
|
+
if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
|
|
1655
|
+
(window as any).__SYGNAL_DEVTOOLS__.onReadyChanged(this._componentNumber, this.name, id, !!ready)
|
|
1656
|
+
}
|
|
1627
1657
|
}
|
|
1628
1658
|
},
|
|
1629
1659
|
error: () => {},
|
package/src/extra/command.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface Command {
|
|
|
16
16
|
_stream: Stream<CommandMessage>;
|
|
17
17
|
/** @internal — marker for component.ts detection */
|
|
18
18
|
__sygnalCommand: true;
|
|
19
|
+
/** @internal — stamped by component.ts when wired to a child */
|
|
20
|
+
_targetComponentId?: number;
|
|
21
|
+
/** @internal — stamped by component.ts when wired to a child */
|
|
22
|
+
_targetComponentName?: string;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export function createCommand(): Command {
|
|
@@ -26,11 +30,18 @@ export function createCommand(): Command {
|
|
|
26
30
|
stop() { listener.next = () => {}; },
|
|
27
31
|
});
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
send: (type: string, data?: any) =>
|
|
33
|
+
const cmd: Command = {
|
|
34
|
+
send: (type: string, data?: any) => {
|
|
35
|
+
listener.next({ type, data });
|
|
36
|
+
if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
|
|
37
|
+
(window as any).__SYGNAL_DEVTOOLS__.onCommandSent(type, data, cmd._targetComponentId, cmd._targetComponentName);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
31
40
|
_stream,
|
|
32
41
|
__sygnalCommand: true as const,
|
|
33
42
|
};
|
|
43
|
+
|
|
44
|
+
return cmd;
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
export function makeCommandSource(cmd: Command): CommandSource {
|
package/src/extra/devtools.ts
CHANGED
|
@@ -26,6 +26,12 @@ interface StateHistoryEntry {
|
|
|
26
26
|
state: any;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
interface MviGraphData {
|
|
30
|
+
sources: string[];
|
|
31
|
+
actions: {name: string; sinks: string[]}[];
|
|
32
|
+
contextProvides: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
interface ComponentMeta {
|
|
30
36
|
id: number;
|
|
31
37
|
name: string;
|
|
@@ -39,6 +45,7 @@ interface ComponentMeta {
|
|
|
39
45
|
children: number[];
|
|
40
46
|
debug: boolean;
|
|
41
47
|
createdAt: number;
|
|
48
|
+
mviGraph: MviGraphData | null;
|
|
42
49
|
_instanceRef: WeakRef<any>;
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -94,6 +101,12 @@ class SygnalDevTools {
|
|
|
94
101
|
case 'TIME_TRAVEL':
|
|
95
102
|
this._timeTravel(msg.payload);
|
|
96
103
|
break;
|
|
104
|
+
case 'SNAPSHOT':
|
|
105
|
+
this._takeSnapshot();
|
|
106
|
+
break;
|
|
107
|
+
case 'RESTORE_SNAPSHOT':
|
|
108
|
+
this._restoreSnapshot(msg.payload);
|
|
109
|
+
break;
|
|
97
110
|
case 'GET_STATE':
|
|
98
111
|
this._sendComponentState(msg.payload.componentId);
|
|
99
112
|
break;
|
|
@@ -114,6 +127,7 @@ class SygnalDevTools {
|
|
|
114
127
|
children: [],
|
|
115
128
|
debug: instance._debug,
|
|
116
129
|
createdAt: Date.now(),
|
|
130
|
+
mviGraph: this._extractMviGraph(instance),
|
|
117
131
|
_instanceRef: new WeakRef(instance),
|
|
118
132
|
};
|
|
119
133
|
this._components.set(componentNumber, meta);
|
|
@@ -141,7 +155,6 @@ class SygnalDevTools {
|
|
|
141
155
|
componentId: componentNumber,
|
|
142
156
|
componentName: name,
|
|
143
157
|
state: entry.state,
|
|
144
|
-
historyIndex: this._stateHistory.length - 1,
|
|
145
158
|
});
|
|
146
159
|
}
|
|
147
160
|
|
|
@@ -179,6 +192,77 @@ class SygnalDevTools {
|
|
|
179
192
|
componentId: componentNumber,
|
|
180
193
|
componentName: name,
|
|
181
194
|
context: this._safeClone(context),
|
|
195
|
+
contextTrace: this._buildContextTrace(componentNumber, context),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
onPropsChanged(componentNumber: number, name: string, props: any): void {
|
|
200
|
+
if (!this.connected) return;
|
|
201
|
+
this._post('PROPS_CHANGED', {
|
|
202
|
+
componentId: componentNumber,
|
|
203
|
+
componentName: name,
|
|
204
|
+
props: this._safeClone(props),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
onBusEvent(event: {type: string; data?: any; __emitterId?: number; __emitterName?: string}): void {
|
|
209
|
+
if (!this.connected) return;
|
|
210
|
+
this._post('BUS_EVENT', {
|
|
211
|
+
type: event.type,
|
|
212
|
+
data: this._safeClone(event.data),
|
|
213
|
+
componentId: event.__emitterId ?? null,
|
|
214
|
+
componentName: event.__emitterName ?? null,
|
|
215
|
+
timestamp: Date.now(),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
onCommandSent(type: string, data: any, targetComponentId?: number, targetComponentName?: string): void {
|
|
220
|
+
if (!this.connected) return;
|
|
221
|
+
this._post('COMMAND_SENT', {
|
|
222
|
+
type,
|
|
223
|
+
data: this._safeClone(data),
|
|
224
|
+
targetComponentName: targetComponentName ?? null,
|
|
225
|
+
timestamp: Date.now(),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
onReadyChanged(parentId: number, parentName: string, childKey: string, ready: boolean): void {
|
|
230
|
+
if (!this.connected) return;
|
|
231
|
+
this._post('READY_CHANGED', {
|
|
232
|
+
parentId,
|
|
233
|
+
parentName,
|
|
234
|
+
childKey,
|
|
235
|
+
ready,
|
|
236
|
+
timestamp: Date.now(),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
onCollectionMounted(parentId: number, parentName: string, itemComponentName: string, stateField: string | null): void {
|
|
241
|
+
const meta = this._components.get(parentId);
|
|
242
|
+
if (meta) {
|
|
243
|
+
(meta as any).collection = {itemComponent: itemComponentName, stateField};
|
|
244
|
+
}
|
|
245
|
+
if (!this.connected) return;
|
|
246
|
+
this._post('COLLECTION_MOUNTED', {
|
|
247
|
+
parentId,
|
|
248
|
+
parentName,
|
|
249
|
+
itemComponent: itemComponentName,
|
|
250
|
+
stateField,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
onComponentDisposed(componentNumber: number, name: string): void {
|
|
255
|
+
const meta = this._components.get(componentNumber);
|
|
256
|
+
if (meta) {
|
|
257
|
+
(meta as any).disposed = true;
|
|
258
|
+
(meta as any).disposedAt = Date.now();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!this.connected) return;
|
|
262
|
+
this._post('COMPONENT_DISPOSED', {
|
|
263
|
+
componentId: componentNumber,
|
|
264
|
+
componentName: name,
|
|
265
|
+
timestamp: Date.now(),
|
|
182
266
|
});
|
|
183
267
|
}
|
|
184
268
|
|
|
@@ -209,18 +293,78 @@ class SygnalDevTools {
|
|
|
209
293
|
}
|
|
210
294
|
}
|
|
211
295
|
|
|
212
|
-
_timeTravel({
|
|
213
|
-
|
|
214
|
-
|
|
296
|
+
_timeTravel({componentId, componentName, state}: {componentId: number; componentName: string; state: any}): void {
|
|
297
|
+
if (componentId == null || !state) {
|
|
298
|
+
console.warn('[Sygnal DevTools] _timeTravel: missing componentId or state', {componentId, hasState: !!state});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
215
301
|
|
|
216
302
|
if (typeof window === 'undefined') return;
|
|
303
|
+
|
|
304
|
+
const newState = this._safeClone(state);
|
|
305
|
+
|
|
306
|
+
// Try per-component time-travel via the component's STATE sink (reducer stream)
|
|
307
|
+
const meta = this._components.get(componentId);
|
|
308
|
+
if (meta) {
|
|
309
|
+
const instance = meta._instanceRef?.deref();
|
|
310
|
+
if (!instance) {
|
|
311
|
+
console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
|
|
312
|
+
} else {
|
|
313
|
+
// sinks[stateSourceName] is the reducer stream — push a reducer that replaces state
|
|
314
|
+
const stateSinkName = instance.stateSourceName || 'STATE';
|
|
315
|
+
const stateSink = instance.sinks?.[stateSinkName];
|
|
316
|
+
if (stateSink?.shamefullySendNext) {
|
|
317
|
+
stateSink.shamefullySendNext(() => ({...newState}));
|
|
318
|
+
this._post('TIME_TRAVEL_APPLIED', {
|
|
319
|
+
componentId,
|
|
320
|
+
componentName,
|
|
321
|
+
state: newState,
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
console.warn(`[Sygnal DevTools] _timeTravel: component #${componentId} (${componentName}) has no STATE sink with shamefullySendNext. sinkName=${stateSinkName}, hasSinks=${!!instance.sinks}, sinkKeys=${instance.sinks ? Object.keys(instance.sinks).join(',') : 'none'}`);
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Fall back to root STATE sink for root-level components
|
|
217
332
|
const app = window.__SYGNAL_DEVTOOLS_APP__;
|
|
218
333
|
if (app?.sinks?.STATE?.shamefullySendNext) {
|
|
219
|
-
app.sinks.STATE.shamefullySendNext(() => ({...
|
|
334
|
+
app.sinks.STATE.shamefullySendNext(() => ({...newState}));
|
|
220
335
|
this._post('TIME_TRAVEL_APPLIED', {
|
|
221
|
-
|
|
222
|
-
|
|
336
|
+
componentId,
|
|
337
|
+
componentName,
|
|
338
|
+
state: newState,
|
|
223
339
|
});
|
|
340
|
+
} else {
|
|
341
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
_takeSnapshot(): void {
|
|
346
|
+
const entries: {componentId: number; componentName: string; state: any}[] = [];
|
|
347
|
+
for (const [id, meta] of this._components) {
|
|
348
|
+
if ((meta as any).disposed) continue;
|
|
349
|
+
const instance = meta._instanceRef?.deref();
|
|
350
|
+
if (instance?.currentState != null) {
|
|
351
|
+
entries.push({
|
|
352
|
+
componentId: id,
|
|
353
|
+
componentName: meta.name,
|
|
354
|
+
state: this._safeClone(instance.currentState),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
this._post('SNAPSHOT_TAKEN', {
|
|
359
|
+
entries,
|
|
360
|
+
timestamp: Date.now(),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
_restoreSnapshot(snapshot: {entries: {componentId: number; componentName: string; state: any}[]}): void {
|
|
365
|
+
if (!snapshot?.entries) return;
|
|
366
|
+
for (const entry of snapshot.entries) {
|
|
367
|
+
this._timeTravel(entry);
|
|
224
368
|
}
|
|
225
369
|
}
|
|
226
370
|
|
|
@@ -233,12 +377,39 @@ class SygnalDevTools {
|
|
|
233
377
|
componentId,
|
|
234
378
|
state: this._safeClone(instance.currentState),
|
|
235
379
|
context: this._safeClone(instance.currentContext),
|
|
380
|
+
contextTrace: this._buildContextTrace(componentId, instance.currentContext),
|
|
236
381
|
props: this._safeClone(instance.currentProps),
|
|
237
382
|
});
|
|
238
383
|
}
|
|
239
384
|
}
|
|
240
385
|
}
|
|
241
386
|
|
|
387
|
+
_buildContextTrace(componentId: number, context: any): {field: string; providerId: number; providerName: string}[] {
|
|
388
|
+
if (!context || typeof context !== 'object') return [];
|
|
389
|
+
const trace: {field: string; providerId: number; providerName: string}[] = [];
|
|
390
|
+
const fields = Object.keys(context);
|
|
391
|
+
|
|
392
|
+
for (const field of fields) {
|
|
393
|
+
// Walk up parent chain to find which component provides this field
|
|
394
|
+
let currentId: number | null = componentId;
|
|
395
|
+
let found = false;
|
|
396
|
+
while (currentId != null) {
|
|
397
|
+
const meta = this._components.get(currentId);
|
|
398
|
+
if (!meta) break;
|
|
399
|
+
if (meta.mviGraph?.contextProvides?.includes(field)) {
|
|
400
|
+
trace.push({field, providerId: meta.id, providerName: meta.name});
|
|
401
|
+
found = true;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
currentId = meta.parentId;
|
|
405
|
+
}
|
|
406
|
+
if (!found) {
|
|
407
|
+
trace.push({field, providerId: -1, providerName: 'unknown'});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return trace;
|
|
411
|
+
}
|
|
412
|
+
|
|
242
413
|
_sendFullTree(): void {
|
|
243
414
|
const tree: any[] = [];
|
|
244
415
|
for (const [, meta] of this._components) {
|
|
@@ -276,6 +447,49 @@ class SygnalDevTools {
|
|
|
276
447
|
}
|
|
277
448
|
}
|
|
278
449
|
|
|
450
|
+
_extractMviGraph(instance: any): MviGraphData | null {
|
|
451
|
+
if (!instance.model) return null;
|
|
452
|
+
try {
|
|
453
|
+
const sources = instance.sourceNames ? [...instance.sourceNames] : [];
|
|
454
|
+
const actions: {name: string; sinks: string[]}[] = [];
|
|
455
|
+
const model = instance.model;
|
|
456
|
+
|
|
457
|
+
for (const key of Object.keys(model)) {
|
|
458
|
+
let actionName = key;
|
|
459
|
+
let entry = model[key];
|
|
460
|
+
|
|
461
|
+
// Handle shorthand 'ACTION | SINK'
|
|
462
|
+
if (key.includes('|')) {
|
|
463
|
+
const parts = key.split('|').map((s: string) => s.trim());
|
|
464
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
465
|
+
actionName = parts[0];
|
|
466
|
+
actions.push({name: actionName, sinks: [parts[1]]});
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Function → targets STATE
|
|
472
|
+
if (typeof entry === 'function') {
|
|
473
|
+
actions.push({name: actionName, sinks: [instance.stateSourceName || 'STATE']});
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Object → keys are sink names
|
|
478
|
+
if (entry && typeof entry === 'object') {
|
|
479
|
+
actions.push({name: actionName, sinks: Object.keys(entry)});
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const contextProvides = instance.context && typeof instance.context === 'object'
|
|
485
|
+
? Object.keys(instance.context) : [];
|
|
486
|
+
|
|
487
|
+
return {sources, actions, contextProvides};
|
|
488
|
+
} catch (e) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
279
493
|
_serializeMeta(meta: ComponentMeta): Omit<ComponentMeta, '_instanceRef'> {
|
|
280
494
|
const {_instanceRef, ...rest} = meta;
|
|
281
495
|
return rest;
|
package/src/extra/dragDriver.ts
CHANGED
|
@@ -8,19 +8,72 @@ export interface DragConfig {
|
|
|
8
8
|
dragImage?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// ─── Enriched DND Event Streams ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface EnrichedDragStream<T = any> extends Stream<T> {
|
|
14
|
+
/** Extract a `data-*` attribute value from the payload's element/dataset.
|
|
15
|
+
* Optional transform fn: `.data('id', Number)` */
|
|
16
|
+
data(name: string): EnrichedDragStream<string | undefined>;
|
|
17
|
+
data<R>(name: string, fn: (val: string | undefined) => R): EnrichedDragStream<R>;
|
|
18
|
+
|
|
19
|
+
/** Extract the dragged element (dragstart) or dropZone element (drop) */
|
|
20
|
+
element(): EnrichedDragStream<Element | null>;
|
|
21
|
+
element<R>(fn: (el: Element | null) => R): EnrichedDragStream<R>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adds chainable convenience methods to a DND event stream,
|
|
26
|
+
* mirroring the DOM driver's `enrichEventStream` pattern.
|
|
27
|
+
*
|
|
28
|
+
* DND.dragstart('task').data('taskId')
|
|
29
|
+
* DND.dragstart('task').data('taskId', Number)
|
|
30
|
+
* DND.drop('lane').data('laneId')
|
|
31
|
+
* DND.dragstart('task').element()
|
|
32
|
+
*/
|
|
33
|
+
function enrichDragStream(stream$: any): any {
|
|
34
|
+
// .data(name, fn?) — extract dataset[name] from dragstart payload,
|
|
35
|
+
// or dropZone.dataset[name] from drop payload
|
|
36
|
+
stream$.data = function data(name: string, fn?: (val: any) => any): any {
|
|
37
|
+
const mapped = stream$.map((e: any) => {
|
|
38
|
+
// dragstart payload: { element, dataset }
|
|
39
|
+
// drop payload: { dropZone, insertBefore }
|
|
40
|
+
const val = e?.dataset?.[name]
|
|
41
|
+
?? (e?.dropZone as HTMLElement)?.dataset?.[name]
|
|
42
|
+
?? (e?.element as HTMLElement)?.dataset?.[name]
|
|
43
|
+
return fn ? fn(val) : val
|
|
44
|
+
});
|
|
45
|
+
return enrichDragStream(mapped);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// .element(fn?) — extract the primary element from the payload
|
|
49
|
+
stream$.element = function element(fn?: (el: any) => any): any {
|
|
50
|
+
const mapped = stream$.map((e: any) => {
|
|
51
|
+
const el = e?.element ?? e?.dropZone ?? null
|
|
52
|
+
return fn ? fn(el) : el
|
|
53
|
+
});
|
|
54
|
+
return enrichDragStream(mapped);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return stream$
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── DND Source Interfaces ───────────────────────────────────────────────────
|
|
61
|
+
|
|
11
62
|
export interface DragSourceEvents {
|
|
12
|
-
events(eventType: string):
|
|
63
|
+
events(eventType: string): EnrichedDragStream<any>;
|
|
13
64
|
}
|
|
14
65
|
|
|
15
66
|
export interface DragSource {
|
|
16
67
|
select(category: string): DragSourceEvents;
|
|
17
|
-
dragstart(category: string):
|
|
68
|
+
dragstart(category: string): EnrichedDragStream<any>;
|
|
18
69
|
dragend(category: string): Stream<any>;
|
|
19
|
-
drop(category: string):
|
|
70
|
+
drop(category: string): EnrichedDragStream<any>;
|
|
20
71
|
dragover(category: string): Stream<any>;
|
|
21
72
|
dispose(): void;
|
|
22
73
|
}
|
|
23
74
|
|
|
75
|
+
// ─── Driver Factory ──────────────────────────────────────────────────────────
|
|
76
|
+
|
|
24
77
|
export function makeDragDriver(): (sink$: Stream<any>) => DragSource {
|
|
25
78
|
return function dragDriver(sink$: Stream<any>): DragSource {
|
|
26
79
|
const categories = new Map<string, DragConfig>();
|
|
@@ -114,10 +167,10 @@ export function makeDragDriver(): (sink$: Stream<any>) => DragSource {
|
|
|
114
167
|
const source: DragSource = {
|
|
115
168
|
select(category: string): DragSourceEvents {
|
|
116
169
|
return {
|
|
117
|
-
events(eventType: string):
|
|
170
|
+
events(eventType: string): EnrichedDragStream<any> {
|
|
118
171
|
const busEventName = `${category}:${eventType}`;
|
|
119
172
|
let handler: ((e: Event) => void) | undefined;
|
|
120
|
-
|
|
173
|
+
const stream$ = xs.create({
|
|
121
174
|
start(listener) {
|
|
122
175
|
handler = ({detail}: any) => listener.next(detail);
|
|
123
176
|
bus.addEventListener(busEventName, handler);
|
|
@@ -126,6 +179,7 @@ export function makeDragDriver(): (sink$: Stream<any>) => DragSource {
|
|
|
126
179
|
if (handler) bus.removeEventListener(busEventName, handler);
|
|
127
180
|
},
|
|
128
181
|
});
|
|
182
|
+
return enrichDragStream(stream$);
|
|
129
183
|
},
|
|
130
184
|
};
|
|
131
185
|
},
|
package/src/extra/eventDriver.ts
CHANGED
|
@@ -14,8 +14,12 @@ export default function eventBusDriver(out$: Stream<BusEvent>): EventBusSource {
|
|
|
14
14
|
const events = new EventTarget();
|
|
15
15
|
|
|
16
16
|
out$.subscribe({
|
|
17
|
-
next: (event: BusEvent) =>
|
|
18
|
-
events.dispatchEvent(new CustomEvent('data', {detail: event}))
|
|
17
|
+
next: (event: BusEvent) => {
|
|
18
|
+
events.dispatchEvent(new CustomEvent('data', {detail: event}));
|
|
19
|
+
if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
|
|
20
|
+
(window as any).__SYGNAL_DEVTOOLS__.onBusEvent(event);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
19
23
|
error: (err: any) =>
|
|
20
24
|
console.error('[EVENTS driver] Error in sink stream:', err),
|
|
21
25
|
});
|