@design.estate/dees-catalog 9.8.0 → 11.0.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_bundle/bundle.js +5626 -5343
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_bundle/third-party-notices.md +783 -10
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/dist_ts_web/elements/00actionerror.d.ts +32 -0
- package/dist_ts_web/elements/00actionerror.js +45 -0
- package/dist_ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.d.ts +16 -0
- package/dist_ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.js +40 -3
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.d.ts +7 -0
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +17 -17
- package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.d.ts +14 -3
- package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.demo.js +23 -4
- package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.js +77 -5
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +62 -48
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +89 -51
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +528 -662
- package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.d.ts +45 -6
- package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.js +184 -27
- package/dist_ts_web/elements/00group-harness/interfaces.d.ts +55 -20
- package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.d.ts +23 -3
- package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.js +71 -11
- package/dist_ts_web/elements/00group-input/index.d.ts +0 -1
- package/dist_ts_web/elements/00group-input/index.js +1 -2
- package/dist_ts_web/elements/00group-overlay/dees-modal/dees-modal.d.ts +7 -0
- package/dist_ts_web/elements/00group-overlay/dees-modal/dees-modal.js +13 -3
- package/dist_ts_web/elements/index.d.ts +1 -2
- package/dist_ts_web/elements/index.js +2 -4
- package/dist_ts_web/services/DeesServiceLibLoader.d.ts +7 -0
- package/dist_ts_web/services/DeesServiceLibLoader.js +45 -22
- package/package.json +3 -4
- package/readme.md +105 -18
- package/scripts/check-third-party-notices.mjs +261 -0
- package/third-party-notices.md +783 -10
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00actionerror.ts +66 -0
- package/ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.ts +38 -2
- package/ts_web/elements/00group-dataview/dees-table/dees-table.ts +18 -16
- package/ts_web/elements/00group-feedback/dees-hint/dees-hint.demo.ts +22 -3
- package/ts_web/elements/00group-feedback/dees-hint/dees-hint.ts +71 -3
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts +99 -49
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +607 -752
- package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts +196 -24
- package/ts_web/elements/00group-harness/interfaces.ts +59 -19
- package/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +73 -13
- package/ts_web/elements/00group-input/index.ts +0 -1
- package/ts_web/elements/00group-overlay/dees-modal/dees-modal.ts +13 -2
- package/ts_web/elements/index.ts +1 -4
- package/ts_web/services/DeesServiceLibLoader.ts +150 -102
- package/.smartconfig.json +0 -86
- package/dist_ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.d.ts +0 -1
- package/dist_ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.js +0 -2
- package/dist_ts_web/elements/00group-input/dees-input-searchselect/index.d.ts +0 -1
- package/dist_ts_web/elements/00group-input/dees-input-searchselect/index.js +0 -2
- package/dist_ts_web/elements/dees-search/dees-search.d.ts +0 -1
- package/dist_ts_web/elements/dees-search/dees-search.js +0 -2
- package/dist_ts_web/elements/dees-search/index.d.ts +0 -1
- package/dist_ts_web/elements/dees-search/index.js +0 -2
- package/dist_ts_web/elements/dees-tooltip/dees-tooltip.d.ts +0 -1
- package/dist_ts_web/elements/dees-tooltip/dees-tooltip.js +0 -2
- package/dist_ts_web/elements/dees-tooltip/index.d.ts +0 -1
- package/dist_ts_web/elements/dees-tooltip/index.js +0 -2
- package/ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.ts +0 -0
- package/ts_web/elements/00group-input/dees-input-searchselect/index.ts +0 -1
- package/ts_web/elements/dees-search/dees-search.ts +0 -0
- package/ts_web/elements/dees-search/index.ts +0 -1
- package/ts_web/elements/dees-tooltip/dees-tooltip.ts +0 -1
- package/ts_web/elements/dees-tooltip/index.ts +0 -1
|
@@ -29,13 +29,46 @@ export interface ITerminalResizeDetail {
|
|
|
29
29
|
cols: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Detail of `terminal-error`: xterm could not be loaded, or the live terminal
|
|
34
|
+
* refused output. The element stays usable and keeps buffering.
|
|
35
|
+
*/
|
|
36
|
+
export interface ITerminalErrorDetail {
|
|
37
|
+
error: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Upper bound on output buffered while no terminal is live, in bytes.
|
|
42
|
+
* `Uint8Array` chunks count their `byteLength`, strings their UTF-16 memory
|
|
43
|
+
* (two bytes per code unit). Beyond the bound the oldest chunks are dropped,
|
|
44
|
+
* and the flush then emits RIS before the survivors so a truncated prefix is
|
|
45
|
+
* never rendered as if it were contiguous output.
|
|
46
|
+
*/
|
|
47
|
+
export const maxPendingWriteBytes = 8 * 1024 * 1024;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* RIS (ESC c), a hard reset carried *inside* the write stream: xterm parses it
|
|
51
|
+
* in order and routes it to the very same `Terminal.reset()` (InputHandler's
|
|
52
|
+
* ESC-c handler calls `fullReset()`, which fires `onRequestReset`, which
|
|
53
|
+
* `Terminal` handles with `reset()`), whereas calling `reset()` directly is
|
|
54
|
+
* documented as out of band with the pending write queue.
|
|
55
|
+
*/
|
|
56
|
+
const RIS = '\x1bc';
|
|
57
|
+
|
|
58
|
+
/** Memory cost of one buffered chunk, in bytes. */
|
|
59
|
+
const chunkByteLength = (chunkArg: string | Uint8Array): number =>
|
|
60
|
+
typeof chunkArg === 'string' ? chunkArg.length * 2 : chunkArg.byteLength;
|
|
61
|
+
|
|
32
62
|
/**
|
|
33
63
|
* Transport-agnostic xterm.js surface: the host pipes remote PTY output into
|
|
34
64
|
* `write()` and receives keystrokes via `terminal-input` — no process,
|
|
35
65
|
* runtime, or socket coupling. `terminal-resize` reports the grid size after
|
|
36
|
-
* every refit so the host can negotiate a PTY size.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
66
|
+
* every refit so the host can negotiate a PTY size. Output that arrives while
|
|
67
|
+
* no terminal is live is buffered up to `maxPendingWriteBytes` and replayed in
|
|
68
|
+
* order on the next connection, so a replay may start before the element is
|
|
69
|
+
* ready and survives being detached. xterm loads through DeesServiceLibLoader;
|
|
70
|
+
* CSP-locked hosts can pre-register bundled modules with
|
|
71
|
+
* `provideXtermModules()` to avoid the CDN.
|
|
39
72
|
*/
|
|
40
73
|
@customElement('dees-terminal-view')
|
|
41
74
|
export class DeesTerminalView extends DeesElement {
|
|
@@ -54,9 +87,37 @@ export class DeesTerminalView extends DeesElement {
|
|
|
54
87
|
private resizeObserver: ResizeObserver | undefined;
|
|
55
88
|
private inputDisposable: { dispose(): void } | undefined;
|
|
56
89
|
private lastReportedSize = '';
|
|
57
|
-
// Output that arrives before xterm
|
|
90
|
+
// Output that arrives before xterm is live is replayed in order, bounded by
|
|
91
|
+
// maxPendingWriteBytes and retained across a disconnect.
|
|
58
92
|
private pendingWrites: Array<string | Uint8Array> = [];
|
|
93
|
+
private pendingWriteBytes = 0;
|
|
94
|
+
private pendingWritesDropped = false;
|
|
59
95
|
private setupPromise: Promise<void> | undefined;
|
|
96
|
+
private readyPending = true;
|
|
97
|
+
private readyResolve: () => void = () => undefined;
|
|
98
|
+
private readyReject: (reasonArg: unknown) => void = () => undefined;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolves once the terminal of the current connection is live; a fresh
|
|
102
|
+
* promise is created for every connection. It rejects with the load error
|
|
103
|
+
* when xterm cannot be loaded (`terminal-error` carries the same error), and
|
|
104
|
+
* when the element is disconnected before the terminal came up.
|
|
105
|
+
*/
|
|
106
|
+
public ready: Promise<void>;
|
|
107
|
+
|
|
108
|
+
constructor() {
|
|
109
|
+
super();
|
|
110
|
+
this.ready = this.createReadyPromise();
|
|
111
|
+
// The house lifecycle: startup runs on every connection with an abort
|
|
112
|
+
// signal, garbage on every disconnection, so a reattached element is a
|
|
113
|
+
// live surface again instead of a permanently dead one.
|
|
114
|
+
this.registerStartupFunction(async (signalArg) => {
|
|
115
|
+
await this.ensureTerminal(signalArg);
|
|
116
|
+
});
|
|
117
|
+
this.registerGarbageFunction(() => {
|
|
118
|
+
this.teardownTerminal();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
60
121
|
|
|
61
122
|
public static styles = [
|
|
62
123
|
themeDefaultStyles,
|
|
@@ -209,43 +270,133 @@ export class DeesTerminalView extends DeesElement {
|
|
|
209
270
|
return html`<div class="terminalHost"></div>`;
|
|
210
271
|
}
|
|
211
272
|
|
|
212
|
-
public
|
|
213
|
-
|
|
214
|
-
|
|
273
|
+
public connectedCallback(): Promise<void> {
|
|
274
|
+
// A consumer awaiting readiness after reattaching must not be handed the
|
|
275
|
+
// settled promise of the previous connection.
|
|
276
|
+
if (!this.readyPending) this.ready = this.createReadyPromise();
|
|
277
|
+
return super.connectedCallback();
|
|
215
278
|
}
|
|
216
279
|
|
|
217
280
|
/** Feed remote output into the terminal; safe to call before setup finishes. */
|
|
218
281
|
public write(dataArg: string | Uint8Array): void {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.
|
|
282
|
+
// While detached the terminal is about to be disposed, so output belongs in
|
|
283
|
+
// the buffer rather than in a surface nobody will see again.
|
|
284
|
+
if (this.terminal && this.isConnected) {
|
|
285
|
+
this.writeToTerminal(this.terminal, dataArg);
|
|
286
|
+
return;
|
|
223
287
|
}
|
|
288
|
+
this.bufferWrite(dataArg);
|
|
224
289
|
}
|
|
225
290
|
|
|
226
291
|
/** Reset the screen and scrollback, e.g. before a full replay. */
|
|
227
292
|
public clear(): void {
|
|
228
|
-
|
|
229
|
-
this.
|
|
293
|
+
const terminal = this.terminal;
|
|
294
|
+
if (terminal && this.isConnected) {
|
|
295
|
+
this.writeToTerminal(terminal, RIS);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Without a terminal the reset is expressed as a buffered sentinel, so the
|
|
299
|
+
// flush of the next connection still performs it in the right order.
|
|
300
|
+
this.pendingWrites = [RIS];
|
|
301
|
+
this.pendingWriteBytes = chunkByteLength(RIS);
|
|
302
|
+
this.pendingWritesDropped = false;
|
|
230
303
|
}
|
|
231
304
|
|
|
232
305
|
public focus(): void {
|
|
233
306
|
this.terminal?.focus();
|
|
234
307
|
}
|
|
235
308
|
|
|
236
|
-
/** Current grid size, or undefined while
|
|
309
|
+
/** Current grid size, or undefined while no terminal is live. */
|
|
237
310
|
public get currentSize(): ITerminalResizeDetail | undefined {
|
|
238
311
|
if (!this.terminal) return undefined;
|
|
239
312
|
return { rows: this.terminal.rows, cols: this.terminal.cols };
|
|
240
313
|
}
|
|
241
314
|
|
|
242
|
-
private
|
|
315
|
+
private createReadyPromise(): Promise<void> {
|
|
316
|
+
this.readyPending = true;
|
|
317
|
+
const promise = new Promise<void>((resolveArg, rejectArg) => {
|
|
318
|
+
this.readyResolve = () => {
|
|
319
|
+
this.readyPending = false;
|
|
320
|
+
resolveArg();
|
|
321
|
+
};
|
|
322
|
+
this.readyReject = (reasonArg) => {
|
|
323
|
+
this.readyPending = false;
|
|
324
|
+
rejectArg(reasonArg);
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
// A readiness promise nobody awaits must never surface as an unhandled
|
|
328
|
+
// rejection; consumers that do await it still observe the reason.
|
|
329
|
+
promise.catch(() => undefined);
|
|
330
|
+
return promise;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private bufferWrite(chunkArg: string | Uint8Array): void {
|
|
334
|
+
this.pendingWrites.push(chunkArg);
|
|
335
|
+
this.pendingWriteBytes += chunkByteLength(chunkArg);
|
|
336
|
+
while (this.pendingWriteBytes > maxPendingWriteBytes && this.pendingWrites.length > 1) {
|
|
337
|
+
const droppedChunk = this.pendingWrites.shift()!;
|
|
338
|
+
this.pendingWriteBytes -= chunkByteLength(droppedChunk);
|
|
339
|
+
this.pendingWritesDropped = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private flushPendingWrites(terminalArg: Terminal): void {
|
|
344
|
+
const pending = this.pendingWrites;
|
|
345
|
+
const dropped = this.pendingWritesDropped;
|
|
346
|
+
this.pendingWrites = [];
|
|
347
|
+
this.pendingWriteBytes = 0;
|
|
348
|
+
this.pendingWritesDropped = false;
|
|
349
|
+
if (dropped && !this.writeToTerminal(terminalArg, RIS)) return;
|
|
350
|
+
for (const chunk of pending) {
|
|
351
|
+
if (!this.writeToTerminal(terminalArg, chunk)) return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** xterm throws at its own flow-control watermark; that has to reach the host. */
|
|
356
|
+
private writeToTerminal(terminalArg: Terminal, chunkArg: string | Uint8Array): boolean {
|
|
357
|
+
try {
|
|
358
|
+
terminalArg.write(chunkArg);
|
|
359
|
+
return true;
|
|
360
|
+
} catch (error) {
|
|
361
|
+
this.reportError(error);
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private reportError(errorArg: unknown): void {
|
|
367
|
+
if (this.readyPending) this.readyReject(errorArg);
|
|
368
|
+
const detail: ITerminalErrorDetail = { error: errorArg };
|
|
369
|
+
this.dispatchEvent(
|
|
370
|
+
new CustomEvent('terminal-error', { detail, bubbles: true, composed: true }),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private ensureTerminal(signalArg: AbortSignal): Promise<void> {
|
|
375
|
+
if (this.terminal) return Promise.resolve();
|
|
376
|
+
if (!this.setupPromise) this.setupPromise = this.setupTerminal(signalArg);
|
|
377
|
+
return this.setupPromise;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private async setupTerminal(signalArg: AbortSignal): Promise<void> {
|
|
381
|
+
try {
|
|
382
|
+
await this.createTerminal(signalArg);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
this.reportError(error);
|
|
385
|
+
} finally {
|
|
386
|
+
// Nothing came up: release the guard so the next connection retries.
|
|
387
|
+
if (!this.terminal) this.setupPromise = undefined;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private async createTerminal(signalArg: AbortSignal): Promise<void> {
|
|
392
|
+
await this.updateComplete;
|
|
243
393
|
const libLoader = DeesServiceLibLoader.getInstance();
|
|
244
394
|
const [xterm, fit] = await Promise.all([
|
|
245
395
|
libLoader.loadXterm(),
|
|
246
396
|
libLoader.loadXtermFitAddon(),
|
|
247
397
|
]);
|
|
248
|
-
|
|
398
|
+
// The load was awaited: the element may have been detached meanwhile.
|
|
399
|
+
if (signalArg.aborted || !this.isConnected) return;
|
|
249
400
|
const host = this.shadowRoot?.querySelector<HTMLElement>('.terminalHost');
|
|
250
401
|
if (!host) return;
|
|
251
402
|
const computed = getComputedStyle(this);
|
|
@@ -265,9 +416,21 @@ export class DeesTerminalView extends DeesElement {
|
|
|
265
416
|
cursor: themeColor('--dees-color-accent-primary', '#e6e6e6'),
|
|
266
417
|
},
|
|
267
418
|
});
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
419
|
+
let fitAddon: FitAddon;
|
|
420
|
+
try {
|
|
421
|
+
fitAddon = new fit.FitAddon();
|
|
422
|
+
terminal.loadAddon(fitAddon);
|
|
423
|
+
terminal.open(host);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
// The element does not own this instance yet, so teardown cannot reach
|
|
426
|
+
// it; a half-opened terminal would otherwise leak with its DOM attached.
|
|
427
|
+
try {
|
|
428
|
+
terminal.dispose();
|
|
429
|
+
} catch {
|
|
430
|
+
// Disposing a half-opened terminal must not mask the real failure.
|
|
431
|
+
}
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
271
434
|
this.terminal = terminal;
|
|
272
435
|
this.fitAddon = fitAddon;
|
|
273
436
|
this.inputDisposable = terminal.onData((dataArg) => {
|
|
@@ -277,11 +440,11 @@ export class DeesTerminalView extends DeesElement {
|
|
|
277
440
|
new CustomEvent('terminal-input', { detail, bubbles: true, composed: true }),
|
|
278
441
|
);
|
|
279
442
|
});
|
|
280
|
-
|
|
281
|
-
this.pendingWrites = [];
|
|
443
|
+
this.flushPendingWrites(terminal);
|
|
282
444
|
this.refit();
|
|
283
445
|
this.resizeObserver = new ResizeObserver(() => this.refit());
|
|
284
446
|
this.resizeObserver.observe(host);
|
|
447
|
+
this.readyResolve();
|
|
285
448
|
}
|
|
286
449
|
|
|
287
450
|
private refit(): void {
|
|
@@ -303,8 +466,7 @@ export class DeesTerminalView extends DeesElement {
|
|
|
303
466
|
);
|
|
304
467
|
}
|
|
305
468
|
|
|
306
|
-
|
|
307
|
-
await super.disconnectedCallback();
|
|
469
|
+
private teardownTerminal(): void {
|
|
308
470
|
this.resizeObserver?.disconnect();
|
|
309
471
|
this.resizeObserver = undefined;
|
|
310
472
|
this.inputDisposable?.dispose();
|
|
@@ -312,6 +474,16 @@ export class DeesTerminalView extends DeesElement {
|
|
|
312
474
|
this.terminal?.dispose();
|
|
313
475
|
this.terminal = undefined;
|
|
314
476
|
this.fitAddon = undefined;
|
|
315
|
-
|
|
477
|
+
// Instance state of the finished connection: a reattached element has to
|
|
478
|
+
// re-announce its grid and may retry a load that failed.
|
|
479
|
+
this.lastReportedSize = '';
|
|
480
|
+
this.setupPromise = undefined;
|
|
481
|
+
// Buffered output deliberately survives, so a replay that arrived while the
|
|
482
|
+
// element was detached still reaches the next connection.
|
|
483
|
+
if (this.readyPending) {
|
|
484
|
+
this.readyReject(
|
|
485
|
+
new Error('dees-terminal-view was disconnected before the terminal became ready'),
|
|
486
|
+
);
|
|
487
|
+
}
|
|
316
488
|
}
|
|
317
489
|
}
|
|
@@ -361,6 +361,23 @@ export interface IHarnessSessionMeta {
|
|
|
361
361
|
draggable?: boolean;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/** Discriminates the two kinds of rows the session list orders as peers. */
|
|
365
|
+
export type THarnessSessionListItemKind = 'session' | 'resource';
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Stable reference to one ordered row. Session ids and resource ids are
|
|
369
|
+
* separate namespaces, so ordering always carries the kind alongside the id.
|
|
370
|
+
*/
|
|
371
|
+
export interface IHarnessSessionListItemRef {
|
|
372
|
+
kind: THarnessSessionListItemKind;
|
|
373
|
+
id: string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Detail of `harness-session-select` and `harness-session-open`. */
|
|
377
|
+
export interface IHarnessSessionEventDetail {
|
|
378
|
+
session: IHarnessSessionMeta;
|
|
379
|
+
}
|
|
380
|
+
|
|
364
381
|
/** Host-owned metadata for a resource that can be associated with a harness session. */
|
|
365
382
|
export interface IHarnessResourceMeta<TState extends string = string> {
|
|
366
383
|
/** Stable identity used by selection and association records. */
|
|
@@ -375,41 +392,51 @@ export interface IHarnessResourceMeta<TState extends string = string> {
|
|
|
375
392
|
updatedAt?: number;
|
|
376
393
|
/** Optional host-defined display state. */
|
|
377
394
|
state?: TState;
|
|
395
|
+
/** Set false for pinned, non-sortable entries. Defaults to true. */
|
|
396
|
+
draggable?: boolean;
|
|
378
397
|
}
|
|
379
398
|
|
|
380
|
-
/**
|
|
399
|
+
/**
|
|
400
|
+
* A controlled zero-or-one association for one resource. The target is any
|
|
401
|
+
* other list item — a session or another resource such as a terminal.
|
|
402
|
+
*/
|
|
381
403
|
export interface IHarnessResourceAssociation {
|
|
382
404
|
resourceId: string;
|
|
383
|
-
|
|
405
|
+
target: IHarnessSessionListItemRef;
|
|
384
406
|
}
|
|
385
407
|
|
|
386
408
|
export interface IHarnessResourceEventDetail<TState extends string = string> {
|
|
387
409
|
resource: IHarnessResourceMeta<TState>;
|
|
388
|
-
|
|
410
|
+
/** The controlled association target, or null when the resource is unattached. */
|
|
411
|
+
target: IHarnessSessionListItemRef | null;
|
|
389
412
|
}
|
|
390
413
|
|
|
391
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Association is an explicit host action, never a drop: dragging a resource
|
|
416
|
+
* only moves it inside the ordered list.
|
|
417
|
+
*/
|
|
418
|
+
export type THarnessResourceAssociationInteractionSource = 'action' | 'keyboard';
|
|
392
419
|
|
|
393
420
|
export type THarnessResourceAssociationRequestDetail<TState extends string = string> =
|
|
394
421
|
| {
|
|
395
422
|
intent: 'attach';
|
|
396
423
|
resource: IHarnessResourceMeta<TState>;
|
|
397
|
-
|
|
398
|
-
|
|
424
|
+
fromTarget: null;
|
|
425
|
+
toTarget: IHarnessSessionListItemRef;
|
|
399
426
|
source: THarnessResourceAssociationInteractionSource;
|
|
400
427
|
}
|
|
401
428
|
| {
|
|
402
429
|
intent: 'detach';
|
|
403
430
|
resource: IHarnessResourceMeta<TState>;
|
|
404
|
-
|
|
405
|
-
|
|
431
|
+
fromTarget: IHarnessSessionListItemRef;
|
|
432
|
+
toTarget: null;
|
|
406
433
|
source: THarnessResourceAssociationInteractionSource;
|
|
407
434
|
}
|
|
408
435
|
| {
|
|
409
436
|
intent: 'reassign';
|
|
410
437
|
resource: IHarnessResourceMeta<TState>;
|
|
411
|
-
|
|
412
|
-
|
|
438
|
+
fromTarget: IHarnessSessionListItemRef;
|
|
439
|
+
toTarget: IHarnessSessionListItemRef;
|
|
413
440
|
source: THarnessResourceAssociationInteractionSource;
|
|
414
441
|
};
|
|
415
442
|
|
|
@@ -423,7 +450,7 @@ export interface IHarnessResourceContextDetail<TState extends string = string>
|
|
|
423
450
|
source: THarnessResourceContextSource;
|
|
424
451
|
originalEvent: MouseEvent | KeyboardEvent;
|
|
425
452
|
/** Routes a host menu choice through the component's validated controlled request event. */
|
|
426
|
-
requestAssociation: (
|
|
453
|
+
requestAssociation: (toTarget: IHarnessSessionListItemRef | null) => boolean;
|
|
427
454
|
/** Closes the external-menu disclosure state and restores focus to its invoking control. */
|
|
428
455
|
close: () => void;
|
|
429
456
|
}
|
|
@@ -438,22 +465,35 @@ export interface IHarnessSessionContextDetail {
|
|
|
438
465
|
originalEvent: MouseEvent;
|
|
439
466
|
}
|
|
440
467
|
|
|
441
|
-
/**
|
|
468
|
+
/**
|
|
469
|
+
* A user-defined group of list items; ordering of `itemIds` is the display
|
|
470
|
+
* order. Sessions and resources may be members in any interleaving.
|
|
471
|
+
*/
|
|
442
472
|
export interface IHarnessSessionGroup {
|
|
443
473
|
id: string;
|
|
444
474
|
name: string;
|
|
445
|
-
|
|
475
|
+
itemIds: IHarnessSessionListItemRef[];
|
|
446
476
|
}
|
|
447
477
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
478
|
+
export type THarnessItemMoveSource = 'pointer' | 'keyboard';
|
|
479
|
+
|
|
480
|
+
/** Detail of `harness-item-move`: one item landed in a new ordered position. */
|
|
481
|
+
export interface IHarnessItemMoveDetail {
|
|
482
|
+
item: IHarnessSessionListItemRef;
|
|
483
|
+
/** Group the item came from, or null for the ungrouped section. */
|
|
484
|
+
fromGroupId: string | null;
|
|
451
485
|
/** Target group, or null for the ungrouped section. */
|
|
486
|
+
toGroupId: string | null;
|
|
487
|
+
/** Stable target anchor in the dragged-item-excluded order; null means append. */
|
|
488
|
+
beforeItem: IHarnessSessionListItemRef | null;
|
|
489
|
+
source: THarnessItemMoveSource;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** The open insertion slot a drag currently targets. */
|
|
493
|
+
export interface IDropSlot {
|
|
452
494
|
groupId: string | null;
|
|
453
|
-
/**
|
|
495
|
+
/** Index within the section's sortable, dragged-item-excluded order. */
|
|
454
496
|
index: number;
|
|
455
|
-
/** Stable target anchor in the dragged-card-excluded order; absent means append. */
|
|
456
|
-
beforeSessionId?: string;
|
|
457
497
|
}
|
|
458
498
|
|
|
459
499
|
/** Detail of `harness-group-context`, fired on right-click of a group header. */
|
|
@@ -21,17 +21,44 @@ declare global {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Native input types this field maps to. Anything richer has its own element. */
|
|
25
|
+
export type TTextInputType = 'text' | 'number' | 'date' | 'email' | 'url' | 'tel' | 'password';
|
|
26
|
+
|
|
27
|
+
/** Keyboard hint per type; `inputMode` on the host still wins. */
|
|
28
|
+
const defaultInputModes: Record<TTextInputType, string> = {
|
|
29
|
+
text: 'text',
|
|
30
|
+
number: 'decimal',
|
|
31
|
+
date: 'text',
|
|
32
|
+
email: 'email',
|
|
33
|
+
url: 'url',
|
|
34
|
+
tel: 'tel',
|
|
35
|
+
password: 'text',
|
|
36
|
+
};
|
|
37
|
+
|
|
24
38
|
@customElement('dees-input-text')
|
|
25
39
|
export class DeesInputText extends DeesInputBase {
|
|
26
40
|
public static demo = demoFunc;
|
|
27
41
|
public static demoGroups = ['Input'];
|
|
28
42
|
|
|
29
43
|
// INSTANCE
|
|
44
|
+
/**
|
|
45
|
+
* The native input type. `number` keeps the field's value numeric, so a form
|
|
46
|
+
* collects a number rather than a numeric string; every other type keeps the
|
|
47
|
+
* value a string. `password` masks the value; `isPasswordBool` additionally
|
|
48
|
+
* renders the reveal button.
|
|
49
|
+
*/
|
|
50
|
+
@property({
|
|
51
|
+
type: String,
|
|
52
|
+
reflect: true,
|
|
53
|
+
})
|
|
54
|
+
accessor type: TTextInputType = 'text';
|
|
55
|
+
|
|
56
|
+
/** A string for every type except `number`, which holds a number or null while empty. */
|
|
30
57
|
@property({
|
|
31
58
|
type: String,
|
|
32
59
|
reflect: true,
|
|
33
60
|
})
|
|
34
|
-
accessor value: string = '';
|
|
61
|
+
accessor value: string | number | null = '';
|
|
35
62
|
|
|
36
63
|
@property({
|
|
37
64
|
type: Boolean,
|
|
@@ -315,8 +342,8 @@ export class DeesInputText extends DeesInputBase {
|
|
|
315
342
|
<div class="maincontainer">
|
|
316
343
|
<input
|
|
317
344
|
class=${this.vintegrated ? '' : 'field-control'}
|
|
318
|
-
type="${this.
|
|
319
|
-
.value=${this.
|
|
345
|
+
type="${this.nativeInputType}"
|
|
346
|
+
.value=${this.valueAsText}
|
|
320
347
|
@input="${this.updateValue}"
|
|
321
348
|
.disabled=${this.disabled}
|
|
322
349
|
?required=${this.required}
|
|
@@ -324,7 +351,7 @@ export class DeesInputText extends DeesInputBase {
|
|
|
324
351
|
aria-invalid=${this.validationState === 'invalid' ? 'true' : 'false'}
|
|
325
352
|
aria-describedby="field-description field-validation"
|
|
326
353
|
placeholder=${this.placeholder || (this.label ? '' : 'Enter text…')}
|
|
327
|
-
inputmode=${this.inputMode || 'text'}
|
|
354
|
+
inputmode=${this.inputMode || defaultInputModes[this.type] || 'text'}
|
|
328
355
|
/>
|
|
329
356
|
${this.isPasswordBool
|
|
330
357
|
? html`
|
|
@@ -352,6 +379,39 @@ export class DeesInputText extends DeesInputBase {
|
|
|
352
379
|
`;
|
|
353
380
|
}
|
|
354
381
|
|
|
382
|
+
/** The type handed to the native input; masking wins over the declared type. */
|
|
383
|
+
private get nativeInputType(): string {
|
|
384
|
+
if (this.isPasswordBool || this.type === 'password') {
|
|
385
|
+
return this.showPasswordBool ? 'text' : 'password';
|
|
386
|
+
}
|
|
387
|
+
return this.type;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** The value as text, for validators, the context menu and the native element. */
|
|
391
|
+
private get valueAsText(): string {
|
|
392
|
+
return this.value === null || this.value === undefined ? '' : String(this.value);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Keeps `value` in the shape the current type promises, whatever a host
|
|
397
|
+
* binds: a number (or null while empty) for `number`, a string otherwise.
|
|
398
|
+
*/
|
|
399
|
+
private normalizeValue(valueArg: string | number | null | undefined): string | number | null {
|
|
400
|
+
if (this.type !== 'number') {
|
|
401
|
+
return valueArg === null || valueArg === undefined ? '' : String(valueArg);
|
|
402
|
+
}
|
|
403
|
+
if (valueArg === null || valueArg === undefined || valueArg === '') return null;
|
|
404
|
+
const parsed = typeof valueArg === 'number' ? valueArg : Number(valueArg);
|
|
405
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
public willUpdate(changedProperties: Map<PropertyKey, unknown>): void {
|
|
409
|
+
super.willUpdate(changedProperties);
|
|
410
|
+
if (!changedProperties.has('value') && !changedProperties.has('type')) return;
|
|
411
|
+
const normalized = this.normalizeValue(this.value);
|
|
412
|
+
if (normalized !== this.value) this.value = normalized;
|
|
413
|
+
}
|
|
414
|
+
|
|
355
415
|
/**
|
|
356
416
|
* `autocomplete` is applied imperatively rather than bound in the template so that the
|
|
357
417
|
* default renders no attribute at all. Binding it would emit `autocomplete=""` on every
|
|
@@ -372,8 +432,8 @@ export class DeesInputText extends DeesInputBase {
|
|
|
372
432
|
}
|
|
373
433
|
|
|
374
434
|
firstUpdated() {
|
|
375
|
-
if (this.validationFunction && this.value) {
|
|
376
|
-
const result = this.validationFunction(this.
|
|
435
|
+
if (this.validationFunction && this.value !== null && this.value !== '') {
|
|
436
|
+
const result = this.validationFunction(this.valueAsText);
|
|
377
437
|
this.validationState = result.valid ? 'valid' : 'invalid';
|
|
378
438
|
this.validationText = result.message || '';
|
|
379
439
|
if (result.valid) {
|
|
@@ -387,10 +447,10 @@ export class DeesInputText extends DeesInputBase {
|
|
|
387
447
|
}
|
|
388
448
|
|
|
389
449
|
public async updateValue(eventArg: Event) {
|
|
390
|
-
const target
|
|
391
|
-
this.value = target.value;
|
|
450
|
+
const target = eventArg.target as HTMLInputElement;
|
|
451
|
+
this.value = this.normalizeValue(target.value);
|
|
392
452
|
if (this.validationFunction) {
|
|
393
|
-
const result = this.validationFunction(this.
|
|
453
|
+
const result = this.validationFunction(this.valueAsText);
|
|
394
454
|
this.validationState = result.valid ? 'valid' : 'invalid';
|
|
395
455
|
this.validationText = result.message || '';
|
|
396
456
|
if (result.valid) {
|
|
@@ -411,16 +471,16 @@ export class DeesInputText extends DeesInputBase {
|
|
|
411
471
|
public getContextMenuItems() {
|
|
412
472
|
return textInputContextMenuItems({
|
|
413
473
|
input: this.shadowRoot!.querySelector('input')!, disabled: this.disabled,
|
|
414
|
-
commit: value => { this.value = value; this.changeSubject.next(this); },
|
|
474
|
+
commit: value => { this.value = this.normalizeValue(value); this.changeSubject.next(this); },
|
|
415
475
|
});
|
|
416
476
|
}
|
|
417
477
|
|
|
418
|
-
public getValue(): string {
|
|
478
|
+
public getValue(): string | number | null {
|
|
419
479
|
return this.value;
|
|
420
480
|
}
|
|
421
481
|
|
|
422
|
-
public setValue(value: string): void {
|
|
423
|
-
this.value = value;
|
|
482
|
+
public setValue(value: string | number | null): void {
|
|
483
|
+
this.value = this.normalizeValue(value);
|
|
424
484
|
}
|
|
425
485
|
|
|
426
486
|
public async togglePasswordView() {
|
|
@@ -12,7 +12,6 @@ export * from './dees-input-phone/index.js';
|
|
|
12
12
|
export * from './dees-input-quantityselector/index.js';
|
|
13
13
|
export * from './dees-input-radiogroup/index.js';
|
|
14
14
|
export * from './dees-input-richtext/index.js';
|
|
15
|
-
export * from './dees-input-searchselect/index.js';
|
|
16
15
|
export * from './dees-input-tags/index.js';
|
|
17
16
|
export * from './dees-input-text/index.js';
|
|
18
17
|
export * from './dees-input-textarea/index.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from '../../00plugins.js';
|
|
2
2
|
import { zIndexRegistry } from '../../00zindex.js';
|
|
3
|
+
import { runAction } from '../../00actionerror.js';
|
|
3
4
|
|
|
4
5
|
import { demoFunc } from './dees-modal.demo.js';
|
|
5
6
|
import {
|
|
@@ -49,7 +50,7 @@ export class DeesModal extends DeesElement {
|
|
|
49
50
|
let modal: DeesModal;
|
|
50
51
|
const confirm = async () => {
|
|
51
52
|
if (modal.promptPending) return;
|
|
52
|
-
const value = input.value.trim();
|
|
53
|
+
const value = String(input.value ?? '').trim();
|
|
53
54
|
const error = value ? options.validate?.(value) : 'Enter a name.';
|
|
54
55
|
if (error) {
|
|
55
56
|
input.validationState = 'invalid'; input.validationText = error;
|
|
@@ -332,7 +333,7 @@ export class DeesModal extends DeesElement {
|
|
|
332
333
|
<dees-button class="modal-action" .text=${action.name}
|
|
333
334
|
.disabled=${this.promptPending}
|
|
334
335
|
.type=${index === this.menuOptions.length - 1 ? 'accent' : 'secondary'}
|
|
335
|
-
@clicked=${() =>
|
|
336
|
+
@clicked=${() => this.runMenuAction(action)}></dees-button>
|
|
336
337
|
`)}
|
|
337
338
|
</div>
|
|
338
339
|
` : ''}
|
|
@@ -385,4 +386,14 @@ export class DeesModal extends DeesElement {
|
|
|
385
386
|
private async handleHelp() {
|
|
386
387
|
if (this.onHelp) await this.onHelp();
|
|
387
388
|
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* A menu action is awaited so a rejected callback surfaces as a visible
|
|
392
|
+
* failure instead of an unhandled rejection. The error is announced on the
|
|
393
|
+
* modal through the cancelable `dees-action-error` event and, unless a
|
|
394
|
+
* listener cancels it, shown as a notification inside this dialog.
|
|
395
|
+
*/
|
|
396
|
+
private async runMenuAction(actionArg: plugins.tsclass.website.IMenuItem<DeesModal>) {
|
|
397
|
+
await runAction(this, { action: actionArg, source: 'modal-menu', ownerElement: this.dialog }, () => actionArg.action(this));
|
|
398
|
+
}
|
|
388
399
|
}
|
package/ts_web/elements/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from './00actionerror.js';
|
|
1
2
|
export * from './00zindex.js';
|
|
2
3
|
export * from './00theme.js';
|
|
3
4
|
|
|
@@ -17,7 +18,3 @@ export * from './00group-runtime/index.js';
|
|
|
17
18
|
export * from './00group-simple/index.js';
|
|
18
19
|
export * from './00group-utility/index.js';
|
|
19
20
|
export * from './00group-workspace/index.js';
|
|
20
|
-
|
|
21
|
-
// Standalone Components
|
|
22
|
-
export * from './dees-search/index.js';
|
|
23
|
-
export * from './dees-tooltip/index.js';
|