@liwe3/webcomponents 1.0.14 → 1.1.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/AITextEditor.d.ts +173 -0
- package/dist/AITextEditor.d.ts.map +1 -0
- package/dist/ChunkUploader.d.ts +103 -0
- package/dist/ChunkUploader.d.ts.map +1 -0
- package/dist/ChunkUploader.js +614 -0
- package/dist/ChunkUploader.js.map +1 -0
- package/dist/ContainerBox.d.ts +112 -0
- package/dist/ContainerBox.d.ts.map +1 -0
- package/dist/ContainerBox.js +359 -0
- package/dist/ContainerBox.js.map +1 -0
- package/dist/DateSelector.d.ts +103 -0
- package/dist/DateSelector.d.ts.map +1 -0
- package/dist/Drawer.d.ts +63 -0
- package/dist/Drawer.d.ts.map +1 -0
- package/dist/Drawer.js +340 -0
- package/dist/Drawer.js.map +1 -0
- package/dist/ImageView.d.ts +42 -0
- package/dist/ImageView.d.ts.map +1 -0
- package/dist/ImageView.js +209 -0
- package/dist/ImageView.js.map +1 -0
- package/dist/PopoverMenu.d.ts +103 -0
- package/dist/PopoverMenu.d.ts.map +1 -0
- package/dist/SmartSelect.d.ts +99 -0
- package/dist/SmartSelect.d.ts.map +1 -0
- package/dist/Toast.d.ts +127 -0
- package/dist/Toast.d.ts.map +1 -0
- package/dist/Toast.js +79 -49
- package/dist/Toast.js.map +1 -1
- package/dist/TreeView.d.ts +84 -0
- package/dist/TreeView.d.ts.map +1 -0
- package/dist/TreeView.js +478 -0
- package/dist/TreeView.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -12
- package/dist/index.js.map +1 -1
- package/package.json +28 -3
- package/src/ChunkUploader.ts +921 -0
- package/src/ContainerBox.ts +570 -0
- package/src/Drawer.ts +435 -0
- package/src/ImageView.ts +265 -0
- package/src/Toast.ts +96 -32
- package/src/TreeView.ts +673 -0
- package/src/index.ts +45 -6
package/src/TreeView.ts
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TreeView Web Component
|
|
3
|
+
* A customizable tree view with infinite depth, checkboxes, and folder icons
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type TreeNode = {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
children?: TreeNode[];
|
|
10
|
+
icon?: string;
|
|
11
|
+
customIcon?: string;
|
|
12
|
+
selected?: boolean;
|
|
13
|
+
expanded?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class TreeViewElement extends HTMLElement {
|
|
17
|
+
declare shadowRoot: ShadowRoot;
|
|
18
|
+
private treeData: TreeNode[] = [];
|
|
19
|
+
private selectedIds: Set<string> = new Set();
|
|
20
|
+
private expandedIds: Set<string> = new Set();
|
|
21
|
+
private indentWidth: number = 20;
|
|
22
|
+
private showBorder = true;
|
|
23
|
+
|
|
24
|
+
constructor () {
|
|
25
|
+
super();
|
|
26
|
+
this.attachShadow( { mode: 'open' } );
|
|
27
|
+
this.render();
|
|
28
|
+
this.bindEvents();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static get observedAttributes (): string[] {
|
|
32
|
+
return [ 'data', 'indent-width', 'selected-ids', 'show-border' ];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
attributeChangedCallback ( name: string, oldValue: string | null, newValue: string | null ): void {
|
|
36
|
+
if ( oldValue !== newValue ) {
|
|
37
|
+
if ( name === 'indent-width' ) {
|
|
38
|
+
this.indentWidth = parseInt( newValue || '20', 10 );
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if ( name === 'show-border' ) {
|
|
42
|
+
this.showBorder = newValue !== 'false';
|
|
43
|
+
}
|
|
44
|
+
this.render();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get data (): TreeNode[] {
|
|
49
|
+
const dataAttr = this.getAttribute( 'data' );
|
|
50
|
+
if ( dataAttr ) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse( dataAttr );
|
|
53
|
+
} catch ( e ) {
|
|
54
|
+
console.error( 'Invalid data format:', e );
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return this.treeData;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
set data ( value: TreeNode[] ) {
|
|
62
|
+
this.treeData = value;
|
|
63
|
+
this.initializeExpandedState( value );
|
|
64
|
+
this.setAttribute( 'data', JSON.stringify( value ) );
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get selectedNodeIds (): string[] {
|
|
68
|
+
return Array.from( this.selectedIds );
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
set selectedNodeIds ( ids: string[] ) {
|
|
72
|
+
this.selectedIds = new Set( ids );
|
|
73
|
+
this.render();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get showBorderEnabled (): boolean {
|
|
77
|
+
return this.showBorder;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
set showBorderEnabled ( value: boolean ) {
|
|
81
|
+
this.showBorder = value;
|
|
82
|
+
this.setAttribute( 'show-border', value.toString() );
|
|
83
|
+
this.render();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Initialize expanded state from the data
|
|
88
|
+
*/
|
|
89
|
+
private initializeExpandedState ( nodes: TreeNode[] ): void {
|
|
90
|
+
const traverse = ( node: TreeNode ) => {
|
|
91
|
+
if ( node.expanded ) {
|
|
92
|
+
this.expandedIds.add( node.id );
|
|
93
|
+
}
|
|
94
|
+
if ( node.children ) {
|
|
95
|
+
node.children.forEach( traverse );
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
nodes.forEach( traverse );
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Toggle node expansion
|
|
104
|
+
*/
|
|
105
|
+
toggleExpansion ( nodeId: string ): void {
|
|
106
|
+
// Update DOM directly instead of full re-render
|
|
107
|
+
const nodeElement = this.shadowRoot.querySelector( `.tree-node[data-node-id="${ nodeId }"]` ) as HTMLElement;
|
|
108
|
+
if ( !nodeElement ) return;
|
|
109
|
+
|
|
110
|
+
// Find the direct child .node-children element (not nested ones)
|
|
111
|
+
const childrenContainer = Array.from( nodeElement.children ).find(
|
|
112
|
+
el => el.classList.contains( 'node-children' )
|
|
113
|
+
) as HTMLElement | undefined;
|
|
114
|
+
|
|
115
|
+
// Determine current state from DOM, not from expandedIds
|
|
116
|
+
const hasChildrenInDom = !!childrenContainer;
|
|
117
|
+
|
|
118
|
+
if ( hasChildrenInDom ) {
|
|
119
|
+
// Currently expanded - collapse it
|
|
120
|
+
childrenContainer.remove();
|
|
121
|
+
this.expandedIds.delete( nodeId );
|
|
122
|
+
|
|
123
|
+
// Toggle expand icon rotation
|
|
124
|
+
const expandIcon = nodeElement.querySelector( '.expand-icon' ) as HTMLElement;
|
|
125
|
+
if ( expandIcon ) {
|
|
126
|
+
expandIcon.classList.remove( 'expanded' );
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.dispatchEvent( new CustomEvent( 'toggle', {
|
|
130
|
+
detail: { nodeId, expanded: false }
|
|
131
|
+
} ) );
|
|
132
|
+
} else {
|
|
133
|
+
// Currently collapsed - expand it
|
|
134
|
+
const node = this.findNode( nodeId );
|
|
135
|
+
if ( node && node.children ) {
|
|
136
|
+
const depth = parseInt( nodeElement.dataset.depth || '0', 10 );
|
|
137
|
+
const childrenHtml = `
|
|
138
|
+
<div class="node-children">
|
|
139
|
+
${ node.children.map( child => this.renderNode( child, depth + 1 ) ).join( '' ) }
|
|
140
|
+
</div>
|
|
141
|
+
`;
|
|
142
|
+
nodeElement.insertAdjacentHTML( 'beforeend', childrenHtml );
|
|
143
|
+
this.expandedIds.add( nodeId );
|
|
144
|
+
|
|
145
|
+
// Toggle expand icon rotation
|
|
146
|
+
const expandIcon = nodeElement.querySelector( '.expand-icon' ) as HTMLElement;
|
|
147
|
+
if ( expandIcon ) {
|
|
148
|
+
expandIcon.classList.add( 'expanded' );
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.dispatchEvent( new CustomEvent( 'toggle', {
|
|
152
|
+
detail: { nodeId, expanded: true }
|
|
153
|
+
} ) );
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Toggle node selection
|
|
160
|
+
*/
|
|
161
|
+
toggleSelection ( nodeId: string ): void {
|
|
162
|
+
const isSelected = this.selectedIds.has( nodeId );
|
|
163
|
+
|
|
164
|
+
if ( isSelected ) {
|
|
165
|
+
this.selectedIds.delete( nodeId );
|
|
166
|
+
} else {
|
|
167
|
+
this.selectedIds.add( nodeId );
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Update checkbox state directly instead of full re-render
|
|
171
|
+
const checkbox = this.shadowRoot.querySelector( `.node-checkbox[data-node-id="${ nodeId }"]` ) as HTMLInputElement;
|
|
172
|
+
if ( checkbox ) {
|
|
173
|
+
checkbox.checked = !isSelected;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.dispatchEvent( new CustomEvent( 'selectionchange', {
|
|
177
|
+
detail: { selectedIds: this.selectedNodeIds }
|
|
178
|
+
} ) );
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Select all nodes recursively
|
|
183
|
+
*/
|
|
184
|
+
selectAll (): void {
|
|
185
|
+
const traverse = ( node: TreeNode ) => {
|
|
186
|
+
this.selectedIds.add( node.id );
|
|
187
|
+
if ( node.children ) {
|
|
188
|
+
node.children.forEach( traverse );
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
this.data.forEach( traverse );
|
|
193
|
+
this.render();
|
|
194
|
+
this.dispatchEvent( new CustomEvent( 'selectionchange', {
|
|
195
|
+
detail: { selectedIds: this.selectedNodeIds }
|
|
196
|
+
} ) );
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Deselect all nodes
|
|
201
|
+
*/
|
|
202
|
+
deselectAll (): void {
|
|
203
|
+
this.selectedIds.clear();
|
|
204
|
+
this.render();
|
|
205
|
+
this.dispatchEvent( new CustomEvent( 'selectionchange', {
|
|
206
|
+
detail: { selectedIds: this.selectedNodeIds }
|
|
207
|
+
} ) );
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Expand all nodes
|
|
212
|
+
*/
|
|
213
|
+
expandAll (): void {
|
|
214
|
+
const traverse = ( node: TreeNode ) => {
|
|
215
|
+
if ( node.children && node.children.length > 0 ) {
|
|
216
|
+
this.expandedIds.add( node.id );
|
|
217
|
+
node.children.forEach( traverse );
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
this.data.forEach( traverse );
|
|
222
|
+
this.render();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Collapse all nodes
|
|
227
|
+
*/
|
|
228
|
+
collapseAll (): void {
|
|
229
|
+
this.expandedIds.clear();
|
|
230
|
+
this.render();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get node by ID
|
|
235
|
+
*/
|
|
236
|
+
private findNode ( nodeId: string, nodes: TreeNode[] = this.data ): TreeNode | null {
|
|
237
|
+
for ( const node of nodes ) {
|
|
238
|
+
if ( node.id === nodeId ) {
|
|
239
|
+
return node;
|
|
240
|
+
}
|
|
241
|
+
if ( node.children ) {
|
|
242
|
+
const found = this.findNode( nodeId, node.children );
|
|
243
|
+
if ( found ) return found;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get the default folder icon
|
|
251
|
+
*/
|
|
252
|
+
private getDefaultIcon ( node: TreeNode, isExpanded: boolean ): string {
|
|
253
|
+
if ( node.children && node.children.length > 0 ) {
|
|
254
|
+
return isExpanded ? '📂' : '📁';
|
|
255
|
+
}
|
|
256
|
+
return '📄';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Render a single tree node
|
|
261
|
+
*/
|
|
262
|
+
private renderNode ( node: TreeNode, depth: number = 0 ): string {
|
|
263
|
+
const isExpanded = this.expandedIds.has( node.id );
|
|
264
|
+
const isSelected = this.selectedIds.has( node.id );
|
|
265
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
266
|
+
const paddingLeft = depth * this.indentWidth;
|
|
267
|
+
|
|
268
|
+
// Determine icon to display
|
|
269
|
+
let iconHtml = '';
|
|
270
|
+
if ( node.customIcon ) {
|
|
271
|
+
iconHtml = `<span class="node-icon custom-icon">${ node.customIcon }</span>`;
|
|
272
|
+
} else if ( node.icon !== undefined && node.icon !== '' ) {
|
|
273
|
+
iconHtml = `<span class="node-icon">${ node.icon }</span>`;
|
|
274
|
+
} else if ( node.icon !== '' ) {
|
|
275
|
+
iconHtml = `<span class="node-icon">${ this.getDefaultIcon( node, isExpanded ) }</span>`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Render children if expanded
|
|
279
|
+
let childrenHtml = '';
|
|
280
|
+
if ( hasChildren && isExpanded ) {
|
|
281
|
+
childrenHtml = `<div class="node-children">${ node.children!.map( child => this.renderNode( child, depth + 1 ) ).join( '' ) }</div>`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const nodeHtml = `
|
|
285
|
+
<div class="tree-node" data-node-id="${ node.id }" data-depth="${ depth }">
|
|
286
|
+
<div class="node-content" style="padding-left: ${ paddingLeft }px">
|
|
287
|
+
<div class="node-controls">
|
|
288
|
+
${ hasChildren
|
|
289
|
+
? `<button class="expand-toggle" data-node-id="${ node.id }" aria-label="${ isExpanded ? 'Collapse' : 'Expand' }">
|
|
290
|
+
<svg class="expand-icon ${ isExpanded ? 'expanded' : '' }" viewBox="0 0 24 24" width="16" height="16">
|
|
291
|
+
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
|
|
292
|
+
</svg>
|
|
293
|
+
</button>`
|
|
294
|
+
: '<span class="expand-spacer"></span>'
|
|
295
|
+
}
|
|
296
|
+
<label class="checkbox-wrapper">
|
|
297
|
+
<input
|
|
298
|
+
type="checkbox"
|
|
299
|
+
class="node-checkbox"
|
|
300
|
+
data-node-id="${ node.id }"
|
|
301
|
+
${ isSelected ? 'checked' : '' }
|
|
302
|
+
>
|
|
303
|
+
<span class="checkbox-custom"></span>
|
|
304
|
+
</label>
|
|
305
|
+
</div>
|
|
306
|
+
${ iconHtml }
|
|
307
|
+
<span class="node-label" data-node-id="${ node.id }">${ node.label }</span>
|
|
308
|
+
</div>
|
|
309
|
+
${ childrenHtml }
|
|
310
|
+
</div>
|
|
311
|
+
`;
|
|
312
|
+
|
|
313
|
+
return nodeHtml;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Bind event listeners
|
|
318
|
+
*/
|
|
319
|
+
private bindEvents (): void {
|
|
320
|
+
// Use event delegation for all interactive elements
|
|
321
|
+
this.shadowRoot.addEventListener( 'click', ( e ) => {
|
|
322
|
+
const target = e.target as HTMLElement;
|
|
323
|
+
|
|
324
|
+
// Handle expand/collapse toggle button
|
|
325
|
+
if ( target.closest( '.expand-toggle' ) ) {
|
|
326
|
+
const button = target.closest( '.expand-toggle' ) as HTMLElement;
|
|
327
|
+
const nodeId = button.dataset.nodeId;
|
|
328
|
+
if ( nodeId ) {
|
|
329
|
+
this.toggleExpansion( nodeId );
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
} );
|
|
334
|
+
|
|
335
|
+
// Handle checkbox changes
|
|
336
|
+
this.shadowRoot.addEventListener( 'change', ( e ) => {
|
|
337
|
+
const target = e.target as HTMLInputElement;
|
|
338
|
+
if ( target.classList.contains( 'node-checkbox' ) ) {
|
|
339
|
+
const nodeId = target.dataset.nodeId;
|
|
340
|
+
if ( nodeId ) {
|
|
341
|
+
// Toggle current node selection
|
|
342
|
+
this.toggleSelection( nodeId );
|
|
343
|
+
|
|
344
|
+
// If this node is a folder, cascade selection state to all descendants
|
|
345
|
+
const node = this.findNode( nodeId );
|
|
346
|
+
if ( node && node.children && node.children.length > 0 ) {
|
|
347
|
+
const checked = !!target.checked;
|
|
348
|
+
|
|
349
|
+
const applyToDescendants = ( n: TreeNode ) => {
|
|
350
|
+
if ( n.children ) n.children.forEach( applyToDescendants );
|
|
351
|
+
// Skip the root node itself because it's already handled above
|
|
352
|
+
if ( n.id !== nodeId ) {
|
|
353
|
+
if ( checked ) this.selectedIds.add( n.id );
|
|
354
|
+
else this.selectedIds.delete( n.id );
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
applyToDescendants( node );
|
|
359
|
+
|
|
360
|
+
// Update any rendered descendant checkboxes in the DOM
|
|
361
|
+
const rootEl = this.shadowRoot.querySelector( `.tree-node[data-node-id="${ nodeId }"]` );
|
|
362
|
+
if ( rootEl ) {
|
|
363
|
+
rootEl.querySelectorAll<HTMLInputElement>( '.node-checkbox' ).forEach( ( cb ) => {
|
|
364
|
+
cb.checked = checked;
|
|
365
|
+
} );
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.dispatchEvent( new CustomEvent( 'selectionchange', {
|
|
369
|
+
detail: { selectedIds: this.selectedNodeIds }
|
|
370
|
+
} ) );
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} );
|
|
375
|
+
|
|
376
|
+
// Handle double-click events
|
|
377
|
+
this.shadowRoot.addEventListener( 'dblclick', ( e ) => {
|
|
378
|
+
const target = e.target as HTMLElement;
|
|
379
|
+
|
|
380
|
+
// Ignore double-clicks that originate from the checkbox area
|
|
381
|
+
if ( target.closest( '.checkbox-wrapper' ) || target.classList.contains( 'node-checkbox' ) ) {
|
|
382
|
+
e.stopPropagation();
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Check if clicking on a node label or content
|
|
388
|
+
if ( target.closest( '.node-label' ) || target.closest( '.node-content' ) ) {
|
|
389
|
+
const nodeContent = target.closest( '.node-content' ) as HTMLElement;
|
|
390
|
+
if ( !nodeContent ) return;
|
|
391
|
+
|
|
392
|
+
const nodeElement = nodeContent.closest( '.tree-node' ) as HTMLElement;
|
|
393
|
+
if ( !nodeElement ) return;
|
|
394
|
+
|
|
395
|
+
const nodeId = nodeElement.dataset.nodeId;
|
|
396
|
+
if ( !nodeId ) return;
|
|
397
|
+
|
|
398
|
+
const node = this.findNode( nodeId );
|
|
399
|
+
if ( !node ) return;
|
|
400
|
+
|
|
401
|
+
// If it's a folder (has children), toggle expansion
|
|
402
|
+
if ( node.children && node.children.length > 0 ) {
|
|
403
|
+
this.toggleExpansion( nodeId );
|
|
404
|
+
} else {
|
|
405
|
+
// If it's an item (no children), fire itemselected event
|
|
406
|
+
this.dispatchEvent( new CustomEvent( 'itemselected', {
|
|
407
|
+
detail: { node, nodeId }
|
|
408
|
+
} ) );
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} );
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Render the component
|
|
416
|
+
*/
|
|
417
|
+
private render (): void {
|
|
418
|
+
const containerClass = this.showBorder ? 'tree-container bordered' : 'tree-container borderless';
|
|
419
|
+
|
|
420
|
+
this.shadowRoot.innerHTML = `
|
|
421
|
+
<style>
|
|
422
|
+
:host {
|
|
423
|
+
display: block;
|
|
424
|
+
font-family: var(--tree-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
425
|
+
font-size: var(--tree-font-size, 14px);
|
|
426
|
+
color: var(--tree-text-color, #333);
|
|
427
|
+
background: var(--tree-background, transparent);
|
|
428
|
+
user-select: none;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.tree-container {
|
|
432
|
+
overflow: auto;
|
|
433
|
+
padding: var(--tree-padding, 8px);
|
|
434
|
+
border-radius: var(--tree-border-radius, 10px);
|
|
435
|
+
background: var(--tree-container-background, transparent);
|
|
436
|
+
border: none;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.tree-container.bordered {
|
|
440
|
+
border: var(--tree-border, 1px solid rgba(15, 23, 42, 0.12));
|
|
441
|
+
box-shadow: var(--tree-border-shadow, none);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.tree-container.borderless {
|
|
445
|
+
border: none;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.tree-node {
|
|
449
|
+
position: relative;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.node-content {
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: 6px;
|
|
456
|
+
padding: 4px 8px;
|
|
457
|
+
border-radius: var(--tree-node-border-radius, 6px);
|
|
458
|
+
transition: background-color 0.15s ease;
|
|
459
|
+
cursor: pointer;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.node-content:hover {
|
|
463
|
+
background-color: var(--tree-hover-background, rgba(0, 123, 255, 0.08));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.node-controls {
|
|
467
|
+
display: flex;
|
|
468
|
+
align-items: center;
|
|
469
|
+
gap: 4px;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.expand-toggle {
|
|
473
|
+
display: flex;
|
|
474
|
+
align-items: center;
|
|
475
|
+
justify-content: center;
|
|
476
|
+
width: 20px;
|
|
477
|
+
height: 20px;
|
|
478
|
+
padding: 0;
|
|
479
|
+
border: none;
|
|
480
|
+
background: transparent;
|
|
481
|
+
cursor: pointer;
|
|
482
|
+
border-radius: 4px;
|
|
483
|
+
transition: background-color 0.15s ease;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.expand-toggle:hover {
|
|
487
|
+
background-color: var(--tree-expand-hover-background, rgba(0, 0, 0, 0.1));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.expand-toggle:focus {
|
|
491
|
+
outline: 2px solid var(--tree-focus-color, #007bff);
|
|
492
|
+
outline-offset: 2px;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.expand-icon {
|
|
496
|
+
fill: var(--tree-icon-color, #666);
|
|
497
|
+
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.expand-icon.expanded {
|
|
501
|
+
transform: rotate(90deg);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.expand-spacer {
|
|
505
|
+
width: 20px;
|
|
506
|
+
height: 20px;
|
|
507
|
+
display: inline-block;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.checkbox-wrapper {
|
|
511
|
+
display: flex;
|
|
512
|
+
align-items: center;
|
|
513
|
+
cursor: pointer;
|
|
514
|
+
margin: 0;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.node-checkbox {
|
|
518
|
+
position: absolute;
|
|
519
|
+
opacity: 0;
|
|
520
|
+
width: 0;
|
|
521
|
+
height: 0;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.checkbox-custom {
|
|
525
|
+
position: relative;
|
|
526
|
+
width: 18px;
|
|
527
|
+
height: 18px;
|
|
528
|
+
border: 2px solid var(--tree-checkbox-border, #ccc);
|
|
529
|
+
border-radius: var(--tree-checkbox-border-radius, 4px);
|
|
530
|
+
background: var(--tree-checkbox-background, white);
|
|
531
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.checkbox-custom::after {
|
|
535
|
+
content: '';
|
|
536
|
+
position: absolute;
|
|
537
|
+
display: none;
|
|
538
|
+
left: 5px;
|
|
539
|
+
top: 2px;
|
|
540
|
+
width: 4px;
|
|
541
|
+
height: 8px;
|
|
542
|
+
border: solid white;
|
|
543
|
+
border-width: 0 2px 2px 0;
|
|
544
|
+
transform: rotate(45deg);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.node-checkbox:checked + .checkbox-custom {
|
|
548
|
+
background: var(--tree-checkbox-checked-background, #007bff);
|
|
549
|
+
border-color: var(--tree-checkbox-checked-border, #007bff);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.node-checkbox:checked + .checkbox-custom::after {
|
|
553
|
+
display: block;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.checkbox-wrapper:hover .checkbox-custom {
|
|
557
|
+
border-color: var(--tree-checkbox-hover-border, #999);
|
|
558
|
+
box-shadow: 0 0 0 3px var(--tree-checkbox-hover-shadow, rgba(0, 123, 255, 0.1));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.node-checkbox:focus + .checkbox-custom {
|
|
562
|
+
outline: 2px solid var(--tree-focus-color, #007bff);
|
|
563
|
+
outline-offset: 2px;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.node-icon {
|
|
567
|
+
display: inline-flex;
|
|
568
|
+
align-items: center;
|
|
569
|
+
justify-content: center;
|
|
570
|
+
font-size: 18px;
|
|
571
|
+
min-width: 20px;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.node-icon.custom-icon {
|
|
575
|
+
font-size: var(--tree-custom-icon-size, 20px);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.node-label {
|
|
579
|
+
flex: 1;
|
|
580
|
+
padding: 2px 4px;
|
|
581
|
+
color: var(--tree-label-color, #333);
|
|
582
|
+
font-weight: var(--tree-label-font-weight, 400);
|
|
583
|
+
line-height: 1.4;
|
|
584
|
+
cursor: pointer;
|
|
585
|
+
transition: color 0.15s ease;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.node-label:hover {
|
|
589
|
+
color: var(--tree-label-hover-color, #007bff);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.node-children {
|
|
593
|
+
position: relative;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.node-children::before {
|
|
597
|
+
content: '';
|
|
598
|
+
position: absolute;
|
|
599
|
+
left: calc(var(--tree-indent-width, 20px) / 2 + 8px);
|
|
600
|
+
top: 0;
|
|
601
|
+
bottom: 0;
|
|
602
|
+
width: 1px;
|
|
603
|
+
background: var(--tree-guide-line-color, rgba(0, 0, 0, 0.1));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* Animations */
|
|
607
|
+
@keyframes slideDown {
|
|
608
|
+
from {
|
|
609
|
+
opacity: 0;
|
|
610
|
+
transform: translateY(-4px);
|
|
611
|
+
}
|
|
612
|
+
to {
|
|
613
|
+
opacity: 1;
|
|
614
|
+
transform: translateY(0);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.node-children {
|
|
619
|
+
animation: slideDown 0.2s ease-out;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/* Empty state */
|
|
623
|
+
.tree-empty {
|
|
624
|
+
padding: 32px;
|
|
625
|
+
text-align: center;
|
|
626
|
+
color: var(--tree-empty-color, #999);
|
|
627
|
+
font-style: italic;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* Scrollbar styling */
|
|
631
|
+
.tree-container::-webkit-scrollbar {
|
|
632
|
+
width: 8px;
|
|
633
|
+
height: 8px;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.tree-container::-webkit-scrollbar-track {
|
|
637
|
+
background: var(--tree-scrollbar-track, #f1f1f1);
|
|
638
|
+
border-radius: 4px;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.tree-container::-webkit-scrollbar-thumb {
|
|
642
|
+
background: var(--tree-scrollbar-thumb, #888);
|
|
643
|
+
border-radius: 4px;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.tree-container::-webkit-scrollbar-thumb:hover {
|
|
647
|
+
background: var(--tree-scrollbar-thumb-hover, #555);
|
|
648
|
+
}
|
|
649
|
+
</style>
|
|
650
|
+
|
|
651
|
+
<div class="${ containerClass }">
|
|
652
|
+
${ this.data.length > 0
|
|
653
|
+
? this.data.map( node => this.renderNode( node ) ).join( '' )
|
|
654
|
+
: '<div class="tree-empty">No items to display</div>'
|
|
655
|
+
}
|
|
656
|
+
</div>
|
|
657
|
+
`;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Conditionally defines the custom element if in a browser environment.
|
|
663
|
+
*/
|
|
664
|
+
const defineTreeView = ( tagName: string = 'liwe3-tree-view' ): void => {
|
|
665
|
+
if ( typeof window !== 'undefined' && !window.customElements.get( tagName ) ) {
|
|
666
|
+
customElements.define( tagName, TreeViewElement );
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// Auto-register with default tag name
|
|
671
|
+
defineTreeView();
|
|
672
|
+
|
|
673
|
+
export { defineTreeView };
|