@c2n/tree 0.0.9

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/tree.js ADDED
@@ -0,0 +1,607 @@
1
+ import { a as __decorate, i as TreeItem } from "./tree-item-C6XnvUA1.js";
2
+ import { LitElement, html, nothing, unsafeCSS } from "lit";
3
+ import { property, query } from "lit/decorators.js";
4
+ import { repeat } from "lit/directives/repeat.js";
5
+ import { customElement } from "@c2n/core/element-helper.js";
6
+ import { isServer } from "lit-html/is-server.js";
7
+ import { arrayPropertyConverter, jsonPropertyConverter } from "@c2n/core/lit-helper.js";
8
+ //#region src/tree.scss?inline
9
+ var tree_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: block;\n box-sizing: border-box;\n background: var(--c2-tree--background,#ffffff);\n font-size: var(--c2-tree--font-size,14px);\n font-family: var(--c2-tree--font-family);\n border-top: var(--c2-tree--border-top);\n border-right: var(--c2-tree--border-right);\n border-bottom: var(--c2-tree--border-bottom);\n border-left: var(--c2-tree--border-left);\n border-top-left-radius: var(--c2-tree--border-top-left-radius,8px);\n border-top-right-radius: var(--c2-tree--border-top-right-radius,8px);\n border-bottom-left-radius: var(--c2-tree--border-bottom-left-radius,8px);\n border-bottom-right-radius: var(--c2-tree--border-bottom-right-radius,8px);\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host([disabled]) {\n opacity: var(--c2-tree__disabled--opacity,0.38);\n pointer-events: none;\n}\n\n.c2-tree {\n box-sizing: border-box;\n padding-top: var(--c2-tree--padding-top,4px);\n padding-right: var(--c2-tree--padding-right,0px);\n padding-bottom: var(--c2-tree--padding-bottom,4px);\n padding-left: var(--c2-tree--padding-left,0px);\n max-height: var(--c2-tree--max-height);\n overflow: auto;\n}\n\n.empty {\n color: var(--c2-tree__empty--color,#71717a);\n padding: var(--c2-tree__empty--padding,18px);\n text-align: center;\n}";
10
+ //#endregion
11
+ //#region src/tree.ts
12
+ var TYPEAHEAD_WINDOW = 600;
13
+ /** Caps the type-ahead scan so a keystroke cannot stall on a very large tree. */
14
+ var TYPEAHEAD_SCAN_LIMIT = 2e3;
15
+ var Tree = class Tree extends LitElement {
16
+ constructor(..._args) {
17
+ super(..._args);
18
+ this.items = [];
19
+ this.value = [];
20
+ this.expandedItems = [];
21
+ this.selection = "single";
22
+ this.checkboxSelection = false;
23
+ this.selectionPropagation = "both";
24
+ this.childrenOutline = false;
25
+ this.disabled = false;
26
+ this.expandOnClick = false;
27
+ this.#loaded = /* @__PURE__ */ new Set();
28
+ this.#typeahead = "";
29
+ this.#typeaheadAt = 0;
30
+ this.#syncing = false;
31
+ this.#syncQueued = false;
32
+ this.#handleItemChange = (event) => {
33
+ event.stopPropagation();
34
+ if (this.#syncing || this.#syncQueued) return;
35
+ this.#syncQueued = true;
36
+ queueMicrotask(() => {
37
+ this.#syncQueued = false;
38
+ this.#syncItems();
39
+ });
40
+ };
41
+ this.#handleClick = (event) => {
42
+ if (this.disabled) return;
43
+ const item = this.#itemFromEvent(event);
44
+ if (!item) return;
45
+ this.dispatchEvent(new CustomEvent("item-click", {
46
+ detail: { node: item.node },
47
+ bubbles: true,
48
+ composed: true
49
+ }));
50
+ if (item.disabled) return;
51
+ this.#focus(item);
52
+ if (this.expandOnClick && item.isBranch) this.#setExpanded(item, !item.expanded);
53
+ if (this.checkboxSelection) return;
54
+ this.#applySelection(item, {
55
+ toggle: event.ctrlKey || event.metaKey,
56
+ range: event.shiftKey
57
+ });
58
+ };
59
+ this.#handleToggle = (event) => {
60
+ event.stopPropagation();
61
+ if (this.disabled) return;
62
+ const item = this.#itemFromEvent(event);
63
+ if (item) this.#setExpanded(item, !item.expanded);
64
+ };
65
+ this.#handleCheck = (event) => {
66
+ event.stopPropagation();
67
+ if (this.disabled) return;
68
+ const item = this.#itemFromEvent(event);
69
+ if (item) this.#applyCheck(item, event.detail);
70
+ };
71
+ this.#handleKeydown = (event) => {
72
+ if (this.disabled) return;
73
+ const item = this.#itemFromEvent(event) ?? this.#focused;
74
+ if (!item) return;
75
+ const visible = this.visibleItems.filter((row) => !row.disabled);
76
+ const index = visible.indexOf(item);
77
+ switch (event.key) {
78
+ case "ArrowDown": {
79
+ event.preventDefault();
80
+ const next = visible[Math.min(index + 1, visible.length - 1)];
81
+ this.#focus(next);
82
+ if (next && event.shiftKey && this.#multiple) this.#applySelection(next, { range: true });
83
+ return;
84
+ }
85
+ case "ArrowUp": {
86
+ event.preventDefault();
87
+ const previous = visible[Math.max(index - 1, 0)];
88
+ this.#focus(previous);
89
+ if (previous && event.shiftKey && this.#multiple) this.#applySelection(previous, { range: true });
90
+ return;
91
+ }
92
+ case "ArrowRight":
93
+ event.preventDefault();
94
+ if (item.isBranch && !item.expanded) this.#setExpanded(item, true);
95
+ else this.#focus(item.childItems.find((child) => !child.disabled));
96
+ return;
97
+ case "ArrowLeft":
98
+ event.preventDefault();
99
+ if (item.expanded) this.#setExpanded(item, false);
100
+ else this.#focus(this.#ancestorsOf(item).pop());
101
+ return;
102
+ case "Home":
103
+ event.preventDefault();
104
+ this.#focus(visible[0]);
105
+ return;
106
+ case "End":
107
+ event.preventDefault();
108
+ this.#focus(visible[visible.length - 1]);
109
+ return;
110
+ case "Enter":
111
+ event.preventDefault();
112
+ if (this.expandOnClick && item.isBranch) this.#setExpanded(item, !item.expanded);
113
+ if (item.href && !item.disabled) item.renderRoot.querySelector("a")?.click();
114
+ else this.#applySelection(item);
115
+ return;
116
+ case " ":
117
+ event.preventDefault();
118
+ if (this.expandOnClick && item.isBranch) this.#setExpanded(item, !item.expanded);
119
+ if (this.checkboxSelection) this.#applyCheck(item, !item.selected);
120
+ else this.#applySelection(item, { toggle: this.#multiple });
121
+ return;
122
+ case "*": {
123
+ event.preventDefault();
124
+ const branches = (this.#ancestorsOf(item).pop()?.childItems ?? this.rootItems).filter((row) => row.isBranch && !row.disabled).map((row) => row.value);
125
+ this.expandedItems = [.../* @__PURE__ */ new Set([...this.expandedItems, ...branches])];
126
+ return;
127
+ }
128
+ case "a":
129
+ case "A": if (event.ctrlKey || event.metaKey) {
130
+ event.preventDefault();
131
+ this.selectAll();
132
+ return;
133
+ }
134
+ }
135
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) this.#typeaheadTo(event.key, visible, index);
136
+ };
137
+ }
138
+ static {
139
+ this.styles = unsafeCSS(tree_default);
140
+ }
141
+ /** The row the roving `tabindex` currently rests on. */
142
+ #focused;
143
+ /** Anchor for Shift-extended range selection. */
144
+ #anchor;
145
+ /** Branches whose children have already been requested, so a second expand does not refetch. */
146
+ #loaded;
147
+ #typeahead;
148
+ #typeaheadAt;
149
+ #syncing;
150
+ #syncQueued;
151
+ /** Every row in the tree, in document order, expanded or not. */
152
+ get allItems() {
153
+ const result = [];
154
+ const walk = (items) => {
155
+ for (const item of items) {
156
+ result.push(item);
157
+ walk(item.childItems);
158
+ }
159
+ };
160
+ walk(this.rootItems);
161
+ return result;
162
+ }
163
+ /**
164
+ * The rows a user can actually see and move to — every row whose ancestors are all expanded, skipping any row
165
+ * `hidden` takes off the page. A filtered-out row must not stay in the keyboard order, or Arrow Down walks
166
+ * onto something nobody can see.
167
+ */
168
+ get visibleItems() {
169
+ const result = [];
170
+ const walk = (items) => {
171
+ for (const item of items) {
172
+ if (item.hidden) continue;
173
+ result.push(item);
174
+ if (item.expanded) walk(item.childItems);
175
+ }
176
+ };
177
+ walk(this.rootItems);
178
+ return result;
179
+ }
180
+ /**
181
+ * The root rows, from whichever mode produced them. Data-driven rows live in this shadow root and slotted
182
+ * rows in the light DOM, so both sources are read; below the root the two modes are identical and
183
+ * `TreeItem.childItems` serves both.
184
+ */
185
+ get rootItems() {
186
+ if (isServer) return [];
187
+ const slotted = this.rootSlot?.assignedElements({ flatten: true }) ?? [];
188
+ return [...this.container ? [...this.container.children] : [], ...slotted].flatMap((element) => unwrap(element));
189
+ }
190
+ /** The rows that are currently selected, in tree order. */
191
+ getSelectedNodes() {
192
+ return this.allItems.filter((item) => this.value.includes(item.value)).map((item) => item.node);
193
+ }
194
+ /** Expands a row. Unknown or disabled values are ignored. */
195
+ expand(value) {
196
+ const item = this.#itemOf(value);
197
+ if (item) this.#setExpanded(item, true);
198
+ }
199
+ /** Collapses a row. Unknown values are ignored. */
200
+ collapse(value) {
201
+ const item = this.#itemOf(value);
202
+ if (item) this.#setExpanded(item, false);
203
+ }
204
+ /** Expands a collapsed row, or collapses an expanded one. */
205
+ toggle(value) {
206
+ const item = this.#itemOf(value);
207
+ if (item) this.#setExpanded(item, !item.expanded);
208
+ }
209
+ /** Expands every branch already present in the tree. Branches that load on demand are left alone. */
210
+ expandAll() {
211
+ this.expandedItems = this.allItems.filter((item) => item.childItems.length > 0).map((item) => item.value);
212
+ }
213
+ /** Collapses every branch. */
214
+ collapseAll() {
215
+ this.expandedItems = [];
216
+ }
217
+ /** Selects every enabled row. Only meaningful while several rows may be selected. */
218
+ selectAll() {
219
+ if (!this.#multiple) return;
220
+ this.#commitSelection(this.allItems.filter((item) => !item.disabled).map((item) => item.value));
221
+ }
222
+ /** Clears the selection. */
223
+ clearSelection() {
224
+ this.#commitSelection([]);
225
+ }
226
+ /** Moves focus to a row, expanding its ancestors if needed. */
227
+ focusItem(value) {
228
+ const item = this.#itemOf(value);
229
+ if (!item) return;
230
+ for (const ancestor of this.#ancestorsOf(item)) if (!this.expandedItems.includes(ancestor.value)) this.expandedItems = [...this.expandedItems, ancestor.value];
231
+ this.updateComplete.then(() => this.#focus(item));
232
+ }
233
+ /** Drops the cached "already loaded" mark so the next expand runs `loadChildren` again. */
234
+ reload(value) {
235
+ if (value === void 0) this.#loaded.clear();
236
+ else this.#loaded.delete(value);
237
+ }
238
+ get #multiple() {
239
+ return this.selection === "multiple" || this.checkboxSelection;
240
+ }
241
+ #itemOf(value) {
242
+ return this.allItems.find((item) => item.value === value);
243
+ }
244
+ #ancestorsOf(item) {
245
+ if (isServer) return [];
246
+ const result = [];
247
+ let parent = item.parentElement;
248
+ while (parent instanceof TreeItem) {
249
+ result.unshift(parent);
250
+ parent = parent.parentElement;
251
+ }
252
+ return result;
253
+ }
254
+ connectedCallback() {
255
+ super.connectedCallback();
256
+ this.setAttribute("role", "tree");
257
+ }
258
+ willUpdate(changed) {
259
+ for (const key of ["value", "expandedItems"]) if (changed.has(key) && typeof this[key] === "string") this[key] = arrayPropertyConverter.fromAttribute(this[key]);
260
+ if (changed.has("items") && !Array.isArray(this.items)) this.items = [];
261
+ }
262
+ updated(changed) {
263
+ this.setAttribute("aria-multiselectable", String(this.#multiple));
264
+ if (this.disabled) this.setAttribute("aria-disabled", "true");
265
+ else this.removeAttribute("aria-disabled");
266
+ if (changed.has("items")) this.#loaded.clear();
267
+ this.#syncItems();
268
+ }
269
+ /**
270
+ * Rows announce themselves on connect, disconnect and identity changes. A declarative tree fires one per
271
+ * row while it parses, so the resync is coalesced into a microtask — resyncing per event would be quadratic
272
+ * on first paint.
273
+ */
274
+ #handleItemChange;
275
+ /**
276
+ * Writes the tree's state onto every row: depth, expansion, selection, the derived indeterminate state and
277
+ * the roving `tabindex`. Rows never derive any of this themselves, which keeps one ordered walk authoritative
278
+ * for rendering, ARIA and keyboard movement alike.
279
+ */
280
+ #syncItems() {
281
+ if (this.#syncing) return;
282
+ this.#syncing = true;
283
+ try {
284
+ const selected = new Set(this.value);
285
+ const expanded = new Set(this.expandedItems);
286
+ const partial = this.checkboxSelection ? this.#derivePartial(selected) : void 0;
287
+ const walk = (items, level) => {
288
+ items.forEach((item, index) => {
289
+ item.level = level;
290
+ item.setSize = items.length;
291
+ item.posInSet = index + 1;
292
+ item.expanded = expanded.has(item.value);
293
+ item.selected = selected.has(item.value);
294
+ item.indeterminate = partial?.has(item.value) ?? false;
295
+ item.checkboxSelection = this.checkboxSelection;
296
+ item.childrenOutline = this.childrenOutline;
297
+ item.selectable = this.selection !== "none" || this.checkboxSelection;
298
+ walk(item.childItems, level + 1);
299
+ });
300
+ };
301
+ walk(this.rootItems, 0);
302
+ const focusable = this.visibleItems.filter((item) => !item.disabled);
303
+ if (!this.#focused || !focusable.includes(this.#focused)) this.#focused = focusable.find((item) => selected.has(item.value)) ?? focusable[0];
304
+ for (const item of this.allItems) item.tabIndex = item === this.#focused && !this.disabled ? 0 : -1;
305
+ } finally {
306
+ this.#syncing = false;
307
+ }
308
+ }
309
+ /**
310
+ * Values of the branches only *some* of whose descendants are selected.
311
+ *
312
+ * Derived on every sync rather than stored, which also repairs the native input clearing its own
313
+ * indeterminate flag when clicked. Disabled rows are left out of the tally entirely, so a branch whose only
314
+ * unselected descendants are disabled reads as fully selected instead of being stuck half-ticked.
315
+ */
316
+ #derivePartial(selected) {
317
+ const partial = /* @__PURE__ */ new Set();
318
+ const walk = (item) => {
319
+ const children = item.childItems;
320
+ if (children.length === 0) return item.disabled ? {
321
+ total: 0,
322
+ selected: 0
323
+ } : {
324
+ total: 1,
325
+ selected: selected.has(item.value) ? 1 : 0
326
+ };
327
+ let total = 0;
328
+ let hit = 0;
329
+ for (const child of children) {
330
+ const counts = walk(child);
331
+ total += counts.total;
332
+ hit += counts.selected;
333
+ }
334
+ if (hit > 0 && hit < total) partial.add(item.value);
335
+ return {
336
+ total,
337
+ selected: hit
338
+ };
339
+ };
340
+ for (const root of this.rootItems) walk(root);
341
+ return partial;
342
+ }
343
+ /** Every descendant of a row, expanded or not. */
344
+ #descendantsOf(item) {
345
+ const result = [];
346
+ const walk = (items) => {
347
+ for (const child of items) {
348
+ result.push(child);
349
+ walk(child.childItems);
350
+ }
351
+ };
352
+ walk(item.childItems);
353
+ return result;
354
+ }
355
+ async #setExpanded(item, expanded) {
356
+ if (item.disabled || !item.isBranch) return;
357
+ if (expanded === this.expandedItems.includes(item.value)) return;
358
+ if (expanded) {
359
+ const event = new CustomEvent("item-expand", {
360
+ detail: {
361
+ node: item.node,
362
+ loading: item.hasChildren && !this.#loaded.has(item.value)
363
+ },
364
+ bubbles: true,
365
+ composed: true,
366
+ cancelable: true
367
+ });
368
+ this.dispatchEvent(event);
369
+ if (event.defaultPrevented) return;
370
+ await this.#loadIfNeeded(item);
371
+ }
372
+ this.expandedItems = expanded ? [...this.expandedItems, item.value] : this.expandedItems.filter((value) => value !== item.value);
373
+ this.dispatchEvent(new CustomEvent("expansion-change", {
374
+ detail: {
375
+ expandedItems: this.expandedItems,
376
+ node: item.node,
377
+ expanded
378
+ },
379
+ bubbles: false,
380
+ composed: true
381
+ }));
382
+ }
383
+ /** Runs `loadChildren` once per branch, showing a spinner on the row while it is in flight. */
384
+ async #loadIfNeeded(item) {
385
+ if (!this.loadChildren || !item.hasChildren || this.#loaded.has(item.value)) return;
386
+ if (item.childItems.length > 0) return;
387
+ this.#loaded.add(item.value);
388
+ item.loading = true;
389
+ try {
390
+ const children = await this.loadChildren({
391
+ node: item.node,
392
+ level: item.level,
393
+ expanded: true,
394
+ selected: item.selected
395
+ });
396
+ if (children?.length) this.items = replaceChildren(this.items, item.value, children);
397
+ } catch (error) {
398
+ this.#loaded.delete(item.value);
399
+ this.dispatchEvent(new CustomEvent("item-load-error", {
400
+ detail: {
401
+ node: item.node,
402
+ error
403
+ },
404
+ bubbles: true,
405
+ composed: true
406
+ }));
407
+ } finally {
408
+ item.loading = false;
409
+ }
410
+ }
411
+ #commitSelection(values) {
412
+ this.value = values;
413
+ this.#syncItems();
414
+ this.dispatchEvent(new CustomEvent("selection-change", {
415
+ detail: {
416
+ value: this.value,
417
+ nodes: this.getSelectedNodes()
418
+ },
419
+ bubbles: false,
420
+ composed: true
421
+ }));
422
+ }
423
+ /** Applies a click or key press to the selection, honouring the range and toggle modifiers. */
424
+ #applySelection(item, options = {}) {
425
+ if (this.selection === "none" && !this.checkboxSelection) return;
426
+ if (item.disabled) return;
427
+ if (!this.#multiple) {
428
+ this.#anchor = item;
429
+ this.#commitSelection(this.value.includes(item.value) ? [] : [item.value]);
430
+ return;
431
+ }
432
+ if (options.range && this.#anchor) {
433
+ const visible = this.visibleItems;
434
+ const from = visible.indexOf(this.#anchor);
435
+ const to = visible.indexOf(item);
436
+ if (from !== -1 && to !== -1) {
437
+ const [start, end] = from < to ? [from, to] : [to, from];
438
+ const range = visible.slice(start, end + 1).filter((row) => !row.disabled);
439
+ this.#commitSelection([.../* @__PURE__ */ new Set([...this.value, ...range.map((row) => row.value)])]);
440
+ return;
441
+ }
442
+ }
443
+ this.#anchor = item;
444
+ if (options.toggle) {
445
+ const next = this.value.includes(item.value) ? this.value.filter((value) => value !== item.value) : [...this.value, item.value];
446
+ this.#commitSelection(next);
447
+ return;
448
+ }
449
+ this.#commitSelection([item.value]);
450
+ }
451
+ /**
452
+ * Ticks or clears a checkbox and carries the change to the row's relatives.
453
+ *
454
+ * A disabled row is never added or removed by a relative: it would otherwise end up in `value` with no way
455
+ * for the user to click it back out.
456
+ */
457
+ #applyCheck(item, checked) {
458
+ if (item.disabled) return;
459
+ const next = new Set(this.value);
460
+ const propagateDown = this.selectionPropagation === "descendants" || this.selectionPropagation === "both";
461
+ const propagateUp = this.selectionPropagation === "parents" || this.selectionPropagation === "both";
462
+ const affected = [item, ...propagateDown ? this.#descendantsOf(item) : []];
463
+ for (const row of affected) {
464
+ if (row.disabled) continue;
465
+ if (checked) next.add(row.value);
466
+ else next.delete(row.value);
467
+ }
468
+ if (propagateUp) for (const ancestor of this.#ancestorsOf(item).reverse()) {
469
+ const descendants = this.#descendantsOf(ancestor).filter((row) => !row.disabled);
470
+ if (descendants.length > 0 && descendants.every((row) => next.has(row.value))) next.add(ancestor.value);
471
+ else next.delete(ancestor.value);
472
+ }
473
+ this.#commitSelection(this.allItems.filter((row) => next.has(row.value)).map((row) => row.value));
474
+ }
475
+ #focus(item) {
476
+ if (!item) return;
477
+ this.#focused = item;
478
+ for (const row of this.allItems) row.tabIndex = row === item ? 0 : -1;
479
+ item.focus();
480
+ }
481
+ #itemFromEvent(event) {
482
+ const target = event.target;
483
+ return target instanceof Element ? target.closest("c2-tree-item") ?? void 0 : void 0;
484
+ }
485
+ #handleClick;
486
+ #handleToggle;
487
+ #handleCheck;
488
+ #handleKeydown;
489
+ #typeaheadTo(key, visible, index) {
490
+ const now = Date.now();
491
+ this.#typeahead = now - this.#typeaheadAt > TYPEAHEAD_WINDOW ? key : this.#typeahead + key;
492
+ this.#typeaheadAt = now;
493
+ const needle = this.#typeahead.toLowerCase();
494
+ const scan = Math.min(visible.length, TYPEAHEAD_SCAN_LIMIT);
495
+ for (let step = 1; step <= scan; step += 1) {
496
+ const candidate = visible[(index + step) % visible.length];
497
+ if (candidate?.resolvedLabel.toLowerCase().startsWith(needle)) {
498
+ this.#focus(candidate);
499
+ return;
500
+ }
501
+ }
502
+ }
503
+ /** Renders one node and, when it is expanded, its children as its own light children — the shape a
504
+ * declaratively authored tree already has, so one walk serves both modes below the root. */
505
+ #renderNode(node, level) {
506
+ const context = {
507
+ node,
508
+ level,
509
+ expanded: this.expandedItems.includes(node.value),
510
+ selected: this.value.includes(node.value)
511
+ };
512
+ return html`<c2-tree-item
513
+ .value=${node.value}
514
+ .label=${node.label ?? ""}
515
+ .data=${node.data}
516
+ ?disabled=${node.disabled ?? false}
517
+ ?has-children=${node.hasChildren ?? false}
518
+ >
519
+ ${this.renderItem ? html`<span slot="label">${this.renderItem(context)}</span>` : html`${this.renderIcon ? html`<span slot="icon">${this.renderIcon(context)}</span>` : nothing}${this.renderLabel ? html`<span slot="label">${this.renderLabel(context)}</span>` : nothing}${this.renderActions ? html`<span slot="actions">${this.renderActions(context)}</span>` : nothing}`}
520
+ ${repeat(node.children ?? [], (child) => child.value, (child) => this.#renderNode(child, level + 1))}
521
+ </c2-tree-item>`;
522
+ }
523
+ render() {
524
+ const empty = this.items.length === 0 && this.childElementCount === 0;
525
+ return html`
526
+ <div
527
+ class="c2-tree"
528
+ part="tree"
529
+ @click=${this.#handleClick}
530
+ @keydown=${this.#handleKeydown}
531
+ @c2-tree-item-toggle=${this.#handleToggle}
532
+ @c2-tree-item-check=${this.#handleCheck}
533
+ @c2-tree-item-change=${this.#handleItemChange}
534
+ >
535
+ ${repeat(this.items, (node) => node.value, (node) => this.#renderNode(node, 0))}
536
+ <slot @slotchange=${this.#handleItemChange}></slot>
537
+ ${empty ? html`<div class="empty"><slot name="empty">No items</slot></div>` : nothing}
538
+ </div>
539
+ `;
540
+ }
541
+ };
542
+ __decorate([property({ converter: jsonPropertyConverter })], Tree.prototype, "items", void 0);
543
+ __decorate([property({
544
+ converter: arrayPropertyConverter,
545
+ reflect: true
546
+ })], Tree.prototype, "value", void 0);
547
+ __decorate([property({
548
+ converter: arrayPropertyConverter,
549
+ reflect: true,
550
+ attribute: "expanded-items"
551
+ })], Tree.prototype, "expandedItems", void 0);
552
+ __decorate([property({ type: String })], Tree.prototype, "selection", void 0);
553
+ __decorate([property({
554
+ type: Boolean,
555
+ attribute: "checkbox-selection"
556
+ })], Tree.prototype, "checkboxSelection", void 0);
557
+ __decorate([property({
558
+ type: String,
559
+ attribute: "selection-propagation"
560
+ })], Tree.prototype, "selectionPropagation", void 0);
561
+ __decorate([property({
562
+ type: Boolean,
563
+ reflect: true,
564
+ attribute: "children-outline"
565
+ })], Tree.prototype, "childrenOutline", void 0);
566
+ __decorate([property({
567
+ type: Boolean,
568
+ reflect: true
569
+ })], Tree.prototype, "disabled", void 0);
570
+ __decorate([property({
571
+ type: Boolean,
572
+ attribute: "expand-on-click"
573
+ })], Tree.prototype, "expandOnClick", void 0);
574
+ __decorate([property({ attribute: false })], Tree.prototype, "loadChildren", void 0);
575
+ __decorate([property({ attribute: false })], Tree.prototype, "renderItem", void 0);
576
+ __decorate([property({ attribute: false })], Tree.prototype, "renderLabel", void 0);
577
+ __decorate([property({ attribute: false })], Tree.prototype, "renderIcon", void 0);
578
+ __decorate([property({ attribute: false })], Tree.prototype, "renderActions", void 0);
579
+ __decorate([query(".c2-tree")], Tree.prototype, "container", void 0);
580
+ __decorate([query("slot:not([name])")], Tree.prototype, "rootSlot", void 0);
581
+ Tree = __decorate([customElement("c2-tree")], Tree);
582
+ /**
583
+ * Unwraps the single wrapper element an Astro island puts around a row in the docs site, so the walk sees the
584
+ * row either way. Returns nothing for anything that is not a row.
585
+ */
586
+ function unwrap(element) {
587
+ if (element instanceof TreeItem) return [element];
588
+ const child = element.firstElementChild;
589
+ return child instanceof TreeItem ? [child] : [];
590
+ }
591
+ /** Rebuilds only the path down to `value`, leaving every untouched branch identical. */
592
+ function replaceChildren(items, value, children) {
593
+ return items.map((node) => {
594
+ if (node.value === value) return {
595
+ ...node,
596
+ children
597
+ };
598
+ if (!node.children) return node;
599
+ const next = replaceChildren(node.children, value, children);
600
+ return next.every((child, index) => child === node.children[index]) ? node : {
601
+ ...node,
602
+ children: next
603
+ };
604
+ });
605
+ }
606
+ //#endregion
607
+ export { Tree };
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@c2n/tree",
3
+ "version": "0.0.9",
4
+ "type": "module",
5
+ "main": "dist/tree.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./types/src/tree.d.ts",
9
+ "default": "./dist/tree.js"
10
+ },
11
+ "./tree-item.js": {
12
+ "types": "./types/src/tree-item.d.ts",
13
+ "default": "./dist/tree-item.js"
14
+ },
15
+ "./tree-types.js": {
16
+ "types": "./types/src/tree-types.d.ts",
17
+ "default": "./dist/tree-types.js"
18
+ },
19
+ "./react": {
20
+ "types": "./react.d.ts",
21
+ "default": "./react.js"
22
+ },
23
+ "./vue": {
24
+ "types": "./vue.d.ts",
25
+ "default": "./vue.js"
26
+ },
27
+ "./custom-elements.json": "./custom-elements.json"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "types",
32
+ "react.d.ts",
33
+ "react.js",
34
+ "vue.d.ts",
35
+ "vue.js"
36
+ ],
37
+ "keywords": [
38
+ "tree",
39
+ "web component",
40
+ "lit"
41
+ ],
42
+ "license": "MIT",
43
+ "author": "code2nguyen@gmail.com",
44
+ "publishConfig": {
45
+ "registry": "https://registry.npmjs.org",
46
+ "access": "public"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/code2nguyen/web-components.git"
51
+ },
52
+ "scripts": {
53
+ "dev": "vite",
54
+ "build": "wireit",
55
+ "build:only": "vite build",
56
+ "type-check": "wireit"
57
+ },
58
+ "wireit": {
59
+ "type-check": {
60
+ "dependencies": [
61
+ "../../core:build",
62
+ "../checkbox:build",
63
+ "../spinner:build"
64
+ ],
65
+ "command": "tsc -p tsconfig.lib.json --composite false"
66
+ },
67
+ "build": {
68
+ "dependencies": [
69
+ "type-check"
70
+ ],
71
+ "command": "vite build"
72
+ }
73
+ },
74
+ "dependencies": {
75
+ "@c2n/checkbox": "0.0.9",
76
+ "@c2n/core": "0.0.9",
77
+ "@c2n/spinner": "0.0.9",
78
+ "lit": "3.3.3"
79
+ },
80
+ "devDependencies": {
81
+ "@c2n/config": "*"
82
+ },
83
+ "customElements": "custom-elements.json",
84
+ "gitHead": "e4768cce5e0fbd5bc3165cd493a1e8d57cf0fff4"
85
+ }