@qorejs/qore 0.6.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/dist/index.js +1193 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
- package/src/component.ts +26 -0
- package/src/error.ts +144 -0
- package/src/index.ts +95 -0
- package/src/render.ts +286 -0
- package/src/signal.ts +213 -0
- package/src/ssr.ts +341 -0
- package/src/stream.ts +432 -0
- package/src/utils.ts +116 -0
- package/src/virtual-list.ts +405 -0
package/src/stream.ts
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qore Stream - AI Streaming & Server-Side Streaming Support
|
|
3
|
+
* Minimal API for AI responses and SSR streaming
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { signal, effect, computed } from './signal';
|
|
7
|
+
import { VNode, Component } from './render';
|
|
8
|
+
|
|
9
|
+
export interface StreamWriter {
|
|
10
|
+
(chunk: string): void;
|
|
11
|
+
clear(): void;
|
|
12
|
+
done(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StreamOptions {
|
|
16
|
+
container: HTMLElement;
|
|
17
|
+
parseMarkdown?: boolean;
|
|
18
|
+
onComplete?: () => void;
|
|
19
|
+
onError?: (error: Error) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* AI streaming response (client-side)
|
|
24
|
+
*/
|
|
25
|
+
export function stream(
|
|
26
|
+
fn: (write: StreamWriter) => Promise<void>,
|
|
27
|
+
options: StreamOptions
|
|
28
|
+
): { abort: () => void } {
|
|
29
|
+
const { container, parseMarkdown = false, onComplete, onError } = options;
|
|
30
|
+
|
|
31
|
+
let aborted = false;
|
|
32
|
+
let content = '';
|
|
33
|
+
|
|
34
|
+
container.innerHTML = '';
|
|
35
|
+
const output = document.createElement('div');
|
|
36
|
+
output.className = 'stream-output';
|
|
37
|
+
container.appendChild(output);
|
|
38
|
+
|
|
39
|
+
const update = () => {
|
|
40
|
+
output.innerHTML = parseMarkdown ? doParseMarkdown(content) : content;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const write: StreamWriter = (chunk: string) => {
|
|
44
|
+
if (aborted) return;
|
|
45
|
+
content += chunk;
|
|
46
|
+
update();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
write.clear = () => { content = ''; update(); };
|
|
50
|
+
write.done = () => { if (!aborted) onComplete?.(); };
|
|
51
|
+
|
|
52
|
+
Promise.resolve().then(() => fn(write))
|
|
53
|
+
.catch((err: Error) => { if (!aborted) onError?.(err); });
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
abort: () => { aborted = true; }
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Simple Markdown parser
|
|
62
|
+
*/
|
|
63
|
+
function doParseMarkdown(text: string): string {
|
|
64
|
+
return text
|
|
65
|
+
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
|
66
|
+
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
|
67
|
+
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
|
68
|
+
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
|
|
69
|
+
.replace(/\*(.*)\*/gim, '<em>$1</em>')
|
|
70
|
+
.replace(/`([^`]+)`/gim, '<code>$1</code>')
|
|
71
|
+
.replace(/\n/gim, '<br>');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Typewriter effect
|
|
76
|
+
*/
|
|
77
|
+
export function streamText(
|
|
78
|
+
text: string,
|
|
79
|
+
options: { container: HTMLElement; speed?: number; onComplete?: () => void }
|
|
80
|
+
): { abort: () => void } {
|
|
81
|
+
const { container, speed = 30, onComplete } = options;
|
|
82
|
+
|
|
83
|
+
return stream(async (write) => {
|
|
84
|
+
for (let i = 0; i < text.length; i++) {
|
|
85
|
+
await new Promise(r => setTimeout(r, speed));
|
|
86
|
+
write(text[i]);
|
|
87
|
+
}
|
|
88
|
+
write.done();
|
|
89
|
+
}, { container, onComplete });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============== Server-Side Streaming ==============
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Server-side stream renderer
|
|
96
|
+
* Support chunked HTML fragment output
|
|
97
|
+
*/
|
|
98
|
+
export class StreamRenderer {
|
|
99
|
+
private chunks: string[] = [];
|
|
100
|
+
private callbacks: ((chunk: string) => void)[] = [];
|
|
101
|
+
private resolved = false;
|
|
102
|
+
private error: Error | null = null;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Write an HTML chunk
|
|
106
|
+
*/
|
|
107
|
+
write(chunk: string): void {
|
|
108
|
+
this.chunks.push(chunk);
|
|
109
|
+
this.callbacks.forEach(cb => cb(chunk));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Complete stream rendering
|
|
114
|
+
*/
|
|
115
|
+
end(): void {
|
|
116
|
+
this.resolved = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Throw error
|
|
121
|
+
*/
|
|
122
|
+
fail(err: Error): void {
|
|
123
|
+
this.error = err;
|
|
124
|
+
this.resolved = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Subscribe to stream output
|
|
129
|
+
*/
|
|
130
|
+
subscribe(callback: (chunk: string) => void): () => void {
|
|
131
|
+
this.callbacks.push(callback);
|
|
132
|
+
// Immediately send existing chunks
|
|
133
|
+
this.chunks.forEach(chunk => callback(chunk));
|
|
134
|
+
|
|
135
|
+
return () => {
|
|
136
|
+
const idx = this.callbacks.indexOf(callback);
|
|
137
|
+
if (idx !== -1) this.callbacks.splice(idx, 1);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get complete HTML
|
|
143
|
+
*/
|
|
144
|
+
getHTML(): string {
|
|
145
|
+
return this.chunks.join('');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Async iterator - for use with for await...of
|
|
150
|
+
*/
|
|
151
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<string, void, unknown> {
|
|
152
|
+
let index = 0;
|
|
153
|
+
|
|
154
|
+
while (index < this.chunks.length || !this.resolved) {
|
|
155
|
+
if (index < this.chunks.length) {
|
|
156
|
+
yield this.chunks[index++];
|
|
157
|
+
} else {
|
|
158
|
+
await new Promise(resolve => {
|
|
159
|
+
const check = () => {
|
|
160
|
+
if (index < this.chunks.length || this.resolved) {
|
|
161
|
+
resolve(undefined);
|
|
162
|
+
} else {
|
|
163
|
+
setTimeout(check, 10);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
check();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (this.error) {
|
|
172
|
+
throw this.error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Stream HTML fragment generator
|
|
179
|
+
*/
|
|
180
|
+
export function createStreamHTML(): {
|
|
181
|
+
renderer: StreamRenderer;
|
|
182
|
+
html: () => string;
|
|
183
|
+
stream: () => AsyncGenerator<string>;
|
|
184
|
+
} {
|
|
185
|
+
const renderer = new StreamRenderer();
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
renderer,
|
|
189
|
+
html: () => renderer.getHTML(),
|
|
190
|
+
stream: () => renderer[Symbol.asyncIterator]()
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ============== Suspense & Lazy Loading ==============
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Suspense state
|
|
198
|
+
*/
|
|
199
|
+
export type SuspenseState = 'pending' | 'resolved' | 'error';
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Suspense component props
|
|
203
|
+
*/
|
|
204
|
+
export interface SuspenseProps {
|
|
205
|
+
fallback: VNode;
|
|
206
|
+
children: () => VNode;
|
|
207
|
+
onError?: (error: Error) => void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Suspense boundary component
|
|
212
|
+
* Wraps asynchronously loaded components - each instance has independent state
|
|
213
|
+
*/
|
|
214
|
+
export function Suspense({ fallback, children, onError }: SuspenseProps): Component {
|
|
215
|
+
// State sinks to component instance level, avoiding global singleton issues
|
|
216
|
+
const state = signal<SuspenseState>('pending');
|
|
217
|
+
const errorSig = signal<Error | null>(null);
|
|
218
|
+
|
|
219
|
+
return () => {
|
|
220
|
+
const s = state();
|
|
221
|
+
const err = errorSig();
|
|
222
|
+
|
|
223
|
+
if (s === 'error') {
|
|
224
|
+
onError?.(err!);
|
|
225
|
+
return fallback;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (s === 'pending') {
|
|
229
|
+
return fallback;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return children();
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create Suspense component with state
|
|
238
|
+
* Allow external control of loading state
|
|
239
|
+
*/
|
|
240
|
+
export function createSuspense({ fallback, children, onError }: SuspenseProps): {
|
|
241
|
+
component: Component;
|
|
242
|
+
setState: (state: SuspenseState, error?: Error) => void;
|
|
243
|
+
getState: () => SuspenseState;
|
|
244
|
+
} {
|
|
245
|
+
const state = signal<SuspenseState>('pending');
|
|
246
|
+
const errorSig = signal<Error | null>(null);
|
|
247
|
+
|
|
248
|
+
const component: Component = () => {
|
|
249
|
+
const s = state();
|
|
250
|
+
const err = errorSig();
|
|
251
|
+
|
|
252
|
+
if (s === 'error') {
|
|
253
|
+
onError?.(err!);
|
|
254
|
+
return fallback;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (s === 'pending') {
|
|
258
|
+
return fallback;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return children();
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
component,
|
|
266
|
+
setState: (newState: SuspenseState, error?: Error) => {
|
|
267
|
+
state(newState);
|
|
268
|
+
if (error) errorSig(error);
|
|
269
|
+
},
|
|
270
|
+
getState: () => state()
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* lazy() - lazy load component
|
|
276
|
+
* Returns a wrapped component that displays Suspense fallback on first render
|
|
277
|
+
*/
|
|
278
|
+
export function lazy<T extends Component>(
|
|
279
|
+
importFn: () => Promise<{ default: T }>
|
|
280
|
+
): () => { load: () => Promise<T>; component: T | null } {
|
|
281
|
+
let loadedComponent: T | null = null;
|
|
282
|
+
let loadPromise: Promise<T> | null = null;
|
|
283
|
+
|
|
284
|
+
const load = async (): Promise<T> => {
|
|
285
|
+
if (loadedComponent) return loadedComponent;
|
|
286
|
+
if (loadPromise) return loadPromise;
|
|
287
|
+
|
|
288
|
+
loadPromise = importFn().then(mod => {
|
|
289
|
+
loadedComponent = mod.default;
|
|
290
|
+
return loadedComponent;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return loadPromise;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return () => {
|
|
297
|
+
return {
|
|
298
|
+
load,
|
|
299
|
+
component: loadedComponent
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Async component wrapper
|
|
306
|
+
*/
|
|
307
|
+
export function asyncComponent<T extends Component>(
|
|
308
|
+
importFn: () => Promise<{ default: T }>,
|
|
309
|
+
fallback: VNode
|
|
310
|
+
): Component {
|
|
311
|
+
const lazyFactory = lazy(importFn);
|
|
312
|
+
const state = signal<SuspenseState>('pending');
|
|
313
|
+
const component = signal<T | null>(null);
|
|
314
|
+
|
|
315
|
+
// 触发加载
|
|
316
|
+
lazyFactory().load()
|
|
317
|
+
.then(comp => {
|
|
318
|
+
component(comp);
|
|
319
|
+
state('resolved');
|
|
320
|
+
})
|
|
321
|
+
.catch(err => {
|
|
322
|
+
console.error('Async component load failed:', err);
|
|
323
|
+
state('error');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return () => {
|
|
327
|
+
if (state() === 'pending') {
|
|
328
|
+
return fallback;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (state() === 'error') {
|
|
332
|
+
return fallback;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const comp = component();
|
|
336
|
+
return comp ? comp() : fallback;
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ============== Incremental DOM Updates ==============
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Incremental update chunk
|
|
344
|
+
*/
|
|
345
|
+
export interface IncrementalUpdate {
|
|
346
|
+
id: string;
|
|
347
|
+
html: string;
|
|
348
|
+
type: 'replace' | 'append' | 'prepend' | 'remove';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Create incremental update message
|
|
353
|
+
*/
|
|
354
|
+
export function createUpdate(id: string, html: string, type: IncrementalUpdate['type'] = 'replace'): IncrementalUpdate {
|
|
355
|
+
return { id, html, type };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Apply incremental update to DOM
|
|
360
|
+
*/
|
|
361
|
+
export function applyUpdate(container: HTMLElement, update: IncrementalUpdate): void {
|
|
362
|
+
const { id, html, type } = update;
|
|
363
|
+
const element = container.querySelector(`[data-stream-id="${id}"]`);
|
|
364
|
+
|
|
365
|
+
switch (type) {
|
|
366
|
+
case 'replace':
|
|
367
|
+
if (element) {
|
|
368
|
+
element.outerHTML = html;
|
|
369
|
+
} else {
|
|
370
|
+
const temp = document.createElement('div');
|
|
371
|
+
temp.innerHTML = html;
|
|
372
|
+
const newEl = temp.firstElementChild;
|
|
373
|
+
if (newEl) {
|
|
374
|
+
newEl.setAttribute('data-stream-id', id);
|
|
375
|
+
container.appendChild(newEl);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
|
|
380
|
+
case 'append':
|
|
381
|
+
if (element) {
|
|
382
|
+
element.insertAdjacentHTML('beforeend', html);
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
|
|
386
|
+
case 'prepend':
|
|
387
|
+
if (element) {
|
|
388
|
+
element.insertAdjacentHTML('afterbegin', html);
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
|
|
392
|
+
case 'remove':
|
|
393
|
+
if (element) {
|
|
394
|
+
element.remove();
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Stream render to target element
|
|
402
|
+
* Support server-push incremental updates
|
|
403
|
+
*/
|
|
404
|
+
export function renderToStream(
|
|
405
|
+
container: HTMLElement,
|
|
406
|
+
stream: AsyncGenerator<string, void, unknown>
|
|
407
|
+
): { abort: () => void } {
|
|
408
|
+
let aborted = false;
|
|
409
|
+
|
|
410
|
+
(async () => {
|
|
411
|
+
try {
|
|
412
|
+
for await (const chunk of stream) {
|
|
413
|
+
if (aborted) break;
|
|
414
|
+
|
|
415
|
+
// Parse incremental update
|
|
416
|
+
try {
|
|
417
|
+
const update: IncrementalUpdate = JSON.parse(chunk);
|
|
418
|
+
applyUpdate(container, update);
|
|
419
|
+
} catch {
|
|
420
|
+
// If not JSON, directly append HTML
|
|
421
|
+
container.insertAdjacentHTML('beforeend', chunk);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.error('Stream rendering error:', err);
|
|
426
|
+
}
|
|
427
|
+
})();
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
abort: () => { aborted = true; }
|
|
431
|
+
};
|
|
432
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qore Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function debounce<T extends (...args: any[]) => any>(
|
|
6
|
+
fn: T,
|
|
7
|
+
delay: number
|
|
8
|
+
): (...args: Parameters<T>) => void {
|
|
9
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
10
|
+
return (...args) => {
|
|
11
|
+
if (timer) clearTimeout(timer);
|
|
12
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function throttle<T extends (...args: any[]) => any>(
|
|
17
|
+
fn: T,
|
|
18
|
+
interval: number
|
|
19
|
+
): (...args: Parameters<T>) => void {
|
|
20
|
+
let last = 0;
|
|
21
|
+
return (...args) => {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
if (now - last >= interval) {
|
|
24
|
+
last = now;
|
|
25
|
+
fn(...args);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Class name utility - conditional classes
|
|
32
|
+
*/
|
|
33
|
+
export function cx(...classes: (string | false | null | undefined | Record<string, boolean>)[]): string {
|
|
34
|
+
const result: string[] = [];
|
|
35
|
+
|
|
36
|
+
for (const cls of classes) {
|
|
37
|
+
if (!cls) continue;
|
|
38
|
+
|
|
39
|
+
if (typeof cls === 'string') {
|
|
40
|
+
result.push(cls);
|
|
41
|
+
} else if (typeof cls === 'object') {
|
|
42
|
+
for (const [key, value] of Object.entries(cls)) {
|
|
43
|
+
if (value) result.push(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result.join(' ');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Style merging utility - merge multiple style objects
|
|
53
|
+
*/
|
|
54
|
+
export function style(...styles: (Record<string, string | number> | false | null | undefined)[]): Record<string, string | number> {
|
|
55
|
+
const result: Record<string, string | number> = {};
|
|
56
|
+
|
|
57
|
+
for (const s of styles) {
|
|
58
|
+
if (!s) continue;
|
|
59
|
+
Object.assign(result, s);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Event listener utility with cleanup
|
|
67
|
+
*/
|
|
68
|
+
export function on<K extends keyof HTMLElementEventMap>(
|
|
69
|
+
el: HTMLElement,
|
|
70
|
+
type: K,
|
|
71
|
+
handler: (e: HTMLElementEventMap[K]) => void,
|
|
72
|
+
options?: boolean | AddEventListenerOptions
|
|
73
|
+
): () => void {
|
|
74
|
+
el.addEventListener(type, handler as EventListener, options);
|
|
75
|
+
return () => el.removeEventListener(type, handler as EventListener, options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Event handler creators for common events
|
|
80
|
+
*/
|
|
81
|
+
export const onEvent = {
|
|
82
|
+
click: (handler: (e: MouseEvent) => void) => ({ onClick: handler }),
|
|
83
|
+
change: (handler: (e: Event) => void) => ({ onChange: handler }),
|
|
84
|
+
input: (handler: (e: Event) => void) => ({ onInput: handler }),
|
|
85
|
+
submit: (handler: (e: SubmitEvent) => void) => ({ onSubmit: handler }),
|
|
86
|
+
keydown: (handler: (e: KeyboardEvent) => void) => ({ onKeyDown: handler }),
|
|
87
|
+
keyup: (handler: (e: KeyboardEvent) => void) => ({ onKeyUp: handler }),
|
|
88
|
+
focus: (handler: (e: FocusEvent) => void) => ({ onFocus: handler }),
|
|
89
|
+
blur: (handler: (e: FocusEvent) => void) => ({ onBlur: handler }),
|
|
90
|
+
mouseenter: (handler: (e: MouseEvent) => void) => ({ onMouseEnter: handler }),
|
|
91
|
+
mouseleave: (handler: (e: MouseEvent) => void) => ({ onMouseLeave: handler }),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create event handler with prevent default
|
|
96
|
+
*/
|
|
97
|
+
export function preventDefault<T extends Event>(handler: (e: T) => void): (e: T) => void {
|
|
98
|
+
return (e: T) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
handler(e);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create event handler with stop propagation
|
|
106
|
+
*/
|
|
107
|
+
export function stopPropagation<T extends Event>(handler: (e: T) => void): (e: T) => void {
|
|
108
|
+
return (e: T) => {
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
handler(e);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function sleep(ms: number): Promise<void> {
|
|
115
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
116
|
+
}
|