lexgui 8.2.5 → 8.3.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.
Files changed (35) hide show
  1. package/build/components/NodeTree.d.ts +51 -51
  2. package/build/components/Tabs.d.ts +1 -0
  3. package/build/core/Namespace.js +1 -1
  4. package/build/core/Namespace.js.map +1 -1
  5. package/build/extensions/AssetView.d.ts +138 -138
  6. package/build/extensions/AssetView.js +1433 -1433
  7. package/build/extensions/CodeEditor.d.ts +466 -363
  8. package/build/extensions/CodeEditor.js +3768 -4638
  9. package/build/extensions/CodeEditor.js.map +1 -1
  10. package/build/extensions/DocMaker.d.ts +28 -28
  11. package/build/extensions/DocMaker.js +363 -363
  12. package/build/extensions/Timeline.d.ts +2 -2
  13. package/build/extensions/Timeline.js +28 -15
  14. package/build/extensions/Timeline.js.map +1 -1
  15. package/build/extensions/VideoEditor.d.ts +1 -1
  16. package/build/extensions/VideoEditor.js +15 -7
  17. package/build/extensions/VideoEditor.js.map +1 -1
  18. package/build/extensions/index.js +1 -1
  19. package/build/lexgui.all.js +6169 -6960
  20. package/build/lexgui.all.js.map +1 -1
  21. package/build/lexgui.all.min.js +1 -1
  22. package/build/lexgui.all.module.js +6169 -6961
  23. package/build/lexgui.all.module.js.map +1 -1
  24. package/build/lexgui.all.module.min.js +1 -1
  25. package/build/lexgui.css +7534 -7459
  26. package/build/lexgui.js +4475 -205
  27. package/build/lexgui.js.map +1 -1
  28. package/build/lexgui.min.css +1 -1
  29. package/build/lexgui.min.js +1 -1
  30. package/build/lexgui.module.js +4475 -205
  31. package/build/lexgui.module.js.map +1 -1
  32. package/build/lexgui.module.min.js +1 -1
  33. package/changelog.md +31 -1
  34. package/examples/code-editor.html +88 -16
  35. package/package.json +1 -1
@@ -1,1433 +1,1433 @@
1
- // This is a generated file. Do not edit.
2
- import { LX } from '../core/Namespace.js';
3
-
4
- // AssetView.ts @jxarco
5
- if (!LX) {
6
- throw ('Missing LX namespace!');
7
- }
8
- LX.extensions.push('AssetView');
9
- const Area = LX.Area;
10
- LX.Panel;
11
- LX.NodeTree;
12
- LX.Tree;
13
- /**
14
- * @class AssetView
15
- * @description Asset container with Tree for file system
16
- */
17
- class AssetView {
18
- static LAYOUT_GRID = 0;
19
- static LAYOUT_COMPACT = 1;
20
- static LAYOUT_LIST = 2;
21
- static CONTENT_SORT_ASC = 0;
22
- static CONTENT_SORT_DESC = 1;
23
- root;
24
- area = null;
25
- content; // "!" to avoid TS strict property initialization error
26
- leftPanel = null;
27
- toolsPanel;
28
- contentPanel;
29
- previewPanel;
30
- tree = null;
31
- prevData = [];
32
- nextData = [];
33
- data = [];
34
- currentData = [];
35
- currentFolder = undefined;
36
- rootItem;
37
- path = [];
38
- rootPath = '';
39
- selectedItems = [];
40
- allowedTypes;
41
- searchValue = '';
42
- filter = 'None';
43
- gridScale = 1.0;
44
- // Options
45
- layout = AssetView.LAYOUT_GRID;
46
- sortMode = AssetView.CONTENT_SORT_ASC;
47
- skipBrowser = false;
48
- skipPreview = false;
49
- useNativeTitle = false;
50
- onlyFolders = true;
51
- allowMultipleSelection = true;
52
- allowItemCheck = false;
53
- previewActions = [];
54
- contextMenu = [];
55
- itemContextMenuOptions = null;
56
- _assetsPerPage = 24;
57
- get assetsPerPage() {
58
- return this._assetsPerPage;
59
- }
60
- set assetsPerPage(v) {
61
- this._setAssetsPerPage(v);
62
- }
63
- _callbacks = {};
64
- _lastSortBy = '';
65
- _paginator;
66
- _scriptCodeDialog;
67
- _moveItemDialog;
68
- _movingItem;
69
- constructor(options = {}) {
70
- this.rootPath = 'https://raw.githubusercontent.com/jxarco/lexgui.js/master/';
71
- this.layout = options.layout ?? this.layout;
72
- this.sortMode = options.sortMode ?? this.sortMode;
73
- if (options.rootPath) {
74
- if (options.rootPath.constructor !== String) {
75
- console.warn(`Asset Root Path must be a String (now is a ${options.rootPath.constructor.name})`);
76
- }
77
- else {
78
- this.rootPath = options.rootPath;
79
- }
80
- }
81
- let div = document.createElement('div');
82
- div.className = 'lexassetbrowser';
83
- this.root = div;
84
- let area = new Area({ width: '100%', height: '100%' });
85
- div.appendChild(area.root);
86
- let left, right, contentArea = area;
87
- this.skipBrowser = options.skipBrowser ?? this.skipBrowser;
88
- this.skipPreview = options.skipPreview ?? this.skipPreview;
89
- this.useNativeTitle = options.useNativeTitle ?? this.useNativeTitle;
90
- this.onlyFolders = options.onlyFolders ?? this.onlyFolders;
91
- this.allowMultipleSelection = options.allowMultipleSelection ?? this.allowMultipleSelection;
92
- this.allowItemCheck = options.allowItemCheck ?? this.allowItemCheck;
93
- this.previewActions = options.previewActions ?? [];
94
- this.itemContextMenuOptions = options.itemContextMenuOptions;
95
- this.gridScale = options.gridScale ?? this.gridScale;
96
- if (this.gridScale !== 1.0) {
97
- const r = document.querySelector(':root');
98
- r.style.setProperty('--av-grid-scale', this.gridScale);
99
- }
100
- // Append temporarily to the dom
101
- document.body.appendChild(this.root);
102
- if (!this.skipBrowser) {
103
- [left, right] = area.split({ type: 'horizontal', sizes: ['15%', '85%'] });
104
- contentArea = right;
105
- left.setLimitBox(210, 0);
106
- right.setLimitBox(512, 0);
107
- }
108
- if (!this.skipPreview) {
109
- [contentArea, right] = contentArea.split({ type: 'horizontal', sizes: ['80%', '20%'] });
110
- }
111
- this.allowedTypes = {
112
- 'None': {},
113
- 'Image': { color: 'yellow-500' },
114
- 'JSON': { color: 'sky-200' },
115
- 'Video': { color: 'indigo-400' },
116
- ...(options.allowedTypes ?? {})
117
- };
118
- this.path = ['@'];
119
- this.rootItem = { id: '/', children: this.data, type: 'folder', metadata: { uid: LX.guidGenerator() } };
120
- this.currentFolder = this.rootItem;
121
- this._processData(this.data);
122
- this.currentData = this.data;
123
- if (!this.skipBrowser) {
124
- this._createTreePanel(left);
125
- }
126
- this._createContentPanel(contentArea);
127
- // Create resource preview panel
128
- if (!this.skipPreview) {
129
- this.previewPanel = right.addPanel({ className: 'lexassetcontentpanel', style: { overflow: 'scroll' } });
130
- }
131
- // Clean up
132
- document.body.removeChild(this.root);
133
- }
134
- /**
135
- * @method on
136
- * @description Stores an event callback for the desired action
137
- */
138
- on(eventName, callback) {
139
- this._callbacks[eventName] = callback;
140
- }
141
- /**
142
- * @method load
143
- * @description Loads and processes the input data
144
- */
145
- load(data) {
146
- this.prevData.length = 0;
147
- this.nextData.length = 0;
148
- this.data = data;
149
- // Update root children
150
- this.rootItem.children = this.data;
151
- this._processData(this.data);
152
- this.currentData = this.data;
153
- this.path = ['@'];
154
- if (!this.skipBrowser) {
155
- this.tree.refresh({ id: '/', children: this.data, type: 'folder', metadata: { uid: LX.guidGenerator() } });
156
- }
157
- this._refreshContent();
158
- }
159
- /**
160
- * @method addItem
161
- * @description Creates an item DOM element
162
- */
163
- addItem(item, childIndex, updateTree = true) {
164
- const isListLayout = this.layout == AssetView.LAYOUT_LIST;
165
- const isGridLayout = this.layout == AssetView.LAYOUT_GRID; // default
166
- const type = item.type.charAt(0).toUpperCase() + item.type.slice(1);
167
- const extension = LX.getExtension(item.id);
168
- const isFolder = type === 'Folder';
169
- const that = this;
170
- let itemEl = document.createElement('li');
171
- itemEl.className = 'lexassetitem ' + item.type.toLowerCase();
172
- itemEl.tabIndex = -1;
173
- LX.insertChildAtIndex(this.content, itemEl, childIndex);
174
- const typeColor = this.allowedTypes[type]?.color;
175
- if (typeColor) {
176
- // Add type tag
177
- LX.makeElement('span', `rounded-full w-2 h-2 z-100 flex absolute ml-2 mt-2 bg-${typeColor}`, '', itemEl);
178
- }
179
- const metadata = item.metadata;
180
- if (!metadata.uid) {
181
- metadata.uid = LX.guidGenerator();
182
- }
183
- if (metadata.lastModified && !metadata.lastModifiedDate) {
184
- metadata.lastModifiedDate = this._lastModifiedToStringDate(metadata.lastModified);
185
- }
186
- if (!this.useNativeTitle) {
187
- let desc = document.createElement('span');
188
- desc.className = 'lexitemdesc';
189
- desc.id = LX.getSupportedDOMName(`floatingTitle_${metadata.uid}`);
190
- desc.innerHTML = `File: ${item.id}<br>Type: ${type}`;
191
- LX.insertChildAtIndex(this.content, desc, childIndex !== undefined ? childIndex + 1 : undefined);
192
- itemEl.addEventListener('mousemove', (e) => {
193
- if (!isGridLayout) {
194
- return;
195
- }
196
- const target = e.target;
197
- const dialog = itemEl.closest('dialog');
198
- const rect = itemEl.getBoundingClientRect();
199
- const targetRect = target.getBoundingClientRect();
200
- let localOffsetX = rect.x + e.offsetX;
201
- let localOffsetY = rect.y + e.offsetY;
202
- if (dialog) {
203
- const dialogRect = dialog.getBoundingClientRect();
204
- localOffsetX -= dialogRect.x;
205
- localOffsetY -= dialogRect.y;
206
- }
207
- if (target.classList.contains('lexassettitle')) {
208
- localOffsetY += targetRect.y - rect.y;
209
- }
210
- desc.style.left = localOffsetX + 'px';
211
- desc.style.top = (localOffsetY - 36) + 'px';
212
- });
213
- }
214
- else {
215
- itemEl.title = type + ': ' + item.id;
216
- }
217
- if (this.allowItemCheck) {
218
- let checkbox = document.createElement('input');
219
- checkbox.type = 'checkbox';
220
- checkbox.className = 'lexcheckbox';
221
- checkbox.checked = metadata.selected;
222
- checkbox.addEventListener('change', (e) => {
223
- metadata.selected = !metadata.selected;
224
- const onCheck = that._callbacks['check'];
225
- if (onCheck !== undefined) {
226
- const event = {
227
- type: 'check',
228
- items: [item],
229
- userInitiated: true
230
- };
231
- onCheck(event);
232
- // event.multiple = !!e.shiftKey;
233
- }
234
- e.stopPropagation();
235
- e.stopImmediatePropagation();
236
- });
237
- itemEl.appendChild(checkbox);
238
- }
239
- // Asset title
240
- LX.makeElement('span', 'lexassettitle absolute w-full h-8 bottom-0 text-sm bg-card text-card-foreground cursor-pointer text-center content-center block px-3 py-0.5 truncate z-1 pointer-events-none', item.id, itemEl);
241
- if (!this.skipPreview) {
242
- if (item.type === 'video') {
243
- const itemVideo = LX.makeElement('video', 'absolute left-0 top-0 w-full border-none pointer-events-none', '', itemEl);
244
- itemVideo.setAttribute('disablePictureInPicture', false);
245
- itemVideo.setAttribute('disableRemotePlayback', false);
246
- itemVideo.setAttribute('loop', true);
247
- itemVideo.setAttribute('async', true);
248
- itemVideo.style.transition = 'opacity 0.2s ease-out';
249
- itemVideo.style.opacity = metadata.preview ? '0' : '1';
250
- itemVideo.src = item.src;
251
- itemVideo.volume = metadata.volume ?? 0.4;
252
- }
253
- let preview = null;
254
- const previewSrc = metadata.preview ?? item.src;
255
- const hasImage = previewSrc && ((() => {
256
- const ext = LX.getExtension(previewSrc.split('?')[0].split('#')[0]); // get final source without url parameters/anchors
257
- return ext ? ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'avif'].includes(ext.toLowerCase()) : false;
258
- })()
259
- || previewSrc.startsWith('data:image/'));
260
- if (hasImage || isFolder || !isGridLayout) {
261
- const defaultPreviewPath = `${this.rootPath}images/file.png`;
262
- const defaultFolderPath = `${this.rootPath}images/folder.png`;
263
- preview = document.createElement('img');
264
- let realSrc = metadata.unknownExtension
265
- ? defaultPreviewPath
266
- : (isFolder ? defaultFolderPath : previewSrc);
267
- preview.src = isGridLayout || isFolder ? realSrc : defaultPreviewPath;
268
- preview.setAttribute('draggable', 'false');
269
- preview.className = 'pointer-events-none';
270
- itemEl.appendChild(preview);
271
- }
272
- else {
273
- preview = document.createElement('svg');
274
- preview.className = 'asset-file-preview';
275
- itemEl.appendChild(preview);
276
- let textEl = document.createElement('text');
277
- textEl.innerText = (!extension || extension == item.id)
278
- ? item.type.toUpperCase()
279
- : (`${extension.toUpperCase()}`); // If no extension, e.g. Clip, use the type...
280
- preview.appendChild(textEl);
281
- var newLength = textEl.innerText.length;
282
- var charsPerLine = 2.5;
283
- var newEmSize = charsPerLine / newLength;
284
- var textBaseSize = 64;
285
- if (newEmSize < 1) {
286
- var newFontSize = newEmSize * textBaseSize;
287
- textEl.style.fontSize = newFontSize + 'px';
288
- preview.style.paddingTop = `calc(50% - ${(textEl.offsetHeight * 0.5 + 10)}px)`;
289
- }
290
- }
291
- }
292
- // Add item type info
293
- let itemInfoHtml = type;
294
- if (isListLayout) {
295
- if (metadata.bytesize)
296
- itemInfoHtml += ` | ${LX.formatBytes(metadata.bytesize)}`;
297
- if (metadata.lastModifiedDate)
298
- itemInfoHtml += ` | ${metadata.lastModifiedDate}`;
299
- }
300
- LX.makeContainer(['auto', 'auto'], 'lexassetinfo', itemInfoHtml, itemEl);
301
- itemEl.addEventListener('click', function (e) {
302
- e.stopImmediatePropagation();
303
- e.stopPropagation();
304
- const isDoubleClick = e.detail == LX.MOUSE_DOUBLE_CLICK;
305
- if (!isDoubleClick) {
306
- if (!e.shiftKey || !that.allowMultipleSelection) {
307
- that.content.querySelectorAll('.lexassetitem').forEach((i) => i.classList.remove('selected'));
308
- that.selectedItems.length = 0;
309
- }
310
- this.classList.add('selected');
311
- that.selectedItems.push(item);
312
- if (!that.skipPreview) {
313
- that._previewAsset(item);
314
- }
315
- }
316
- else if (isFolder) {
317
- that._enterFolder(item);
318
- return;
319
- }
320
- const onSelect = that._callbacks['select'];
321
- const onDblClick = that._callbacks['dblClick'];
322
- if (isDoubleClick && onDblClick !== undefined) {
323
- const event = {
324
- type: 'dbl_click',
325
- items: [item],
326
- userInitiated: true
327
- };
328
- onDblClick(event);
329
- }
330
- else if (!isDoubleClick && onSelect !== undefined) {
331
- const event = {
332
- type: 'select',
333
- items: [item],
334
- userInitiated: true
335
- };
336
- onSelect(event);
337
- }
338
- });
339
- itemEl.addEventListener('contextmenu', function (e) {
340
- e.preventDefault();
341
- e.stopImmediatePropagation();
342
- e.stopPropagation();
343
- const multipleSelection = that.selectedItems.length > 1;
344
- const options = [
345
- {
346
- name: multipleSelection ? (`${that.selectedItems.length} selected`) : item.id,
347
- icon: LX.makeIcon('CircleSmall', { svgClass: `fill-current text-${typeColor}` }),
348
- className: 'text-sm',
349
- disabled: true
350
- },
351
- null
352
- ];
353
- // By now, allow with none/single selected items
354
- if (!multipleSelection) {
355
- options.push({ name: 'Rename', icon: 'TextCursor', callback: that._renameItemPopover.bind(that, item) });
356
- }
357
- // By now, allow with none/single selected items
358
- if (!isFolder && !multipleSelection) {
359
- options.push({ name: 'Clone', icon: 'Copy', callback: that._requestCloneItem.bind(that, item) });
360
- }
361
- // By now, allow with none/single selected items
362
- if (!multipleSelection) {
363
- options.push({ name: 'Move', icon: 'FolderInput', callback: () => that._moveItem(item) });
364
- }
365
- // By now, allow with none/single selected items
366
- if (!multipleSelection && type == 'Script' && LX.has('CodeEditor')) {
367
- options.push({ name: 'Open in Editor', icon: 'Code', callback: that._openScriptInEditor.bind(that, item) });
368
- }
369
- if (that.itemContextMenuOptions) {
370
- if (options.length > 2)
371
- options.push(null);
372
- for (let o of that.itemContextMenuOptions) {
373
- if (!o.name || !o.callback)
374
- continue;
375
- options.push({ name: o.name, icon: o.icon,
376
- callback: o.callback?.bind(that, multipleSelection ? that.selectedItems : [item]) });
377
- }
378
- }
379
- options.push(null, { name: 'Delete', icon: 'Trash2', className: 'destructive',
380
- callback: that._requestDeleteItem.bind(that, multipleSelection ? that.selectedItems : [item]) });
381
- LX.addClass(that.contentPanel.root, 'pointer-events-none');
382
- LX.addDropdownMenu(e.target, options, { side: 'right', align: 'start', event: e, onBlur: () => {
383
- LX.removeClass(that.contentPanel.root, 'pointer-events-none');
384
- } });
385
- });
386
- const onDrop = function (src, target) {
387
- const targetType = target.type.charAt(0).toUpperCase() + target.type.slice(1);
388
- if (!(targetType === 'Folder') || (src.metadata.uid == target.metadata.uid)) {
389
- console.error('[AssetView Error] Cannot drop: Target item is not a folder or target is the dragged element!');
390
- return;
391
- }
392
- // Animate dragged element
393
- const draggedEl = src.domEl;
394
- if (draggedEl) {
395
- draggedEl.classList.add('moving-to-folder');
396
- // When animation ends, finalize move
397
- draggedEl.addEventListener('animationend', () => {
398
- draggedEl.classList.remove('moving-to-folder');
399
- that._requestMoveItemToFolder(src, target);
400
- }, { once: true });
401
- }
402
- };
403
- itemEl.addEventListener('dragstart', (e) => {
404
- window.__av_item_dragged = item;
405
- var img = new Image();
406
- img.src = '';
407
- if (e.dataTransfer) {
408
- e.dataTransfer.setDragImage(img, 0, 0);
409
- e.dataTransfer.effectAllowed = 'move';
410
- }
411
- const domName = LX.getSupportedDOMName(`floatingTitle_${metadata.uid}`);
412
- const desc = that.content.querySelector(`#${domName}`);
413
- if (desc)
414
- desc.style.display = 'none';
415
- }, false);
416
- itemEl.addEventListener('dragend', (e) => {
417
- e.preventDefault(); // Prevent default action (open as link for some elements)
418
- let dragged = window.__av_item_dragged;
419
- if (dragged && dragged._nodeTarget) { // We dropped into a NodeTree element
420
- onDrop(dragged, dragged._nodeTarget);
421
- }
422
- delete window.__av_item_dragged;
423
- }, false);
424
- itemEl.addEventListener('dragenter', (e) => {
425
- e.preventDefault(); // Prevent default action (open as link for some elements)
426
- let dragged = window.__av_item_dragged;
427
- if (!dragged || !isFolder || (dragged.metadata.uid == metadata.uid))
428
- return;
429
- LX.addClass(item.domEl, 'animate-pulse');
430
- });
431
- itemEl.addEventListener('dragleave', (e) => {
432
- e.preventDefault(); // Prevent default action (open as link for some elements)
433
- let dragged = window.__av_item_dragged;
434
- if (!dragged) {
435
- return;
436
- }
437
- LX.removeClass(item.domEl, 'animate-pulse');
438
- });
439
- itemEl.addEventListener('drop', (e) => {
440
- e.preventDefault(); // Prevent default action (open as link for some elements)
441
- let dragged = window.__av_item_dragged;
442
- if (dragged)
443
- onDrop(dragged, item);
444
- });
445
- itemEl.addEventListener('mouseenter', (e) => {
446
- if (!that.useNativeTitle && isGridLayout) {
447
- const domName = LX.getSupportedDOMName(`floatingTitle_${metadata.uid}`);
448
- const desc = that.content.querySelector(`#${domName}`);
449
- if (desc)
450
- desc.style.display = 'unset';
451
- }
452
- if (item.type !== 'video')
453
- return;
454
- e.preventDefault();
455
- const video = itemEl.querySelector('video');
456
- video.style.opacity = '1';
457
- video.play();
458
- });
459
- itemEl.addEventListener('mouseleave', (e) => {
460
- if (!that.useNativeTitle && isGridLayout) {
461
- setTimeout(() => {
462
- const domName = LX.getSupportedDOMName(`floatingTitle_${metadata.uid}`);
463
- const desc = that.content.querySelector(`#${domName}`);
464
- if (desc)
465
- desc.style.display = 'none';
466
- }, 100);
467
- }
468
- if (item.type !== 'video')
469
- return;
470
- e.preventDefault();
471
- const video = itemEl.querySelector('video');
472
- video.pause();
473
- video.currentTime = 0;
474
- if (metadata.preview) {
475
- video.style.opacity = '0';
476
- }
477
- });
478
- if (!this.skipBrowser && updateTree) {
479
- this.tree.refresh();
480
- }
481
- return itemEl;
482
- }
483
- /**
484
- * @method clear
485
- * @description Creates all AssetView container panels
486
- */
487
- clear() {
488
- if (this.previewPanel) {
489
- this.previewPanel.clear();
490
- }
491
- if (this.leftPanel) {
492
- this.leftPanel.clear();
493
- }
494
- if (this.toolsPanel) {
495
- this.toolsPanel.clear();
496
- }
497
- }
498
- _processData(data, parent) {
499
- // Processing an item
500
- if (data.constructor !== Array) {
501
- data.parent = parent;
502
- data.dir = parent?.children;
503
- data.children = data.children ?? [];
504
- data.metadata = data.metadata || {};
505
- }
506
- // Get the new parent
507
- const newParent = parent ? data : this.rootItem;
508
- for (let item of newParent.children) {
509
- this._processData(item, newParent);
510
- }
511
- }
512
- _updatePath() {
513
- this.path.length = 0;
514
- if (this.currentFolder && this.currentFolder.parent) {
515
- this.path.push(this.currentFolder.id);
516
- const _pushParentsId = (i) => {
517
- if (!i)
518
- return;
519
- this.path.push(i.parent ? i.id : '@');
520
- _pushParentsId(i.parent);
521
- };
522
- _pushParentsId(this.currentFolder.parent);
523
- }
524
- else {
525
- this.path.push('@');
526
- }
527
- LX.emitSignal('@on_folder_change', this.path.reverse().join('/'));
528
- }
529
- _createNavigationBar(panel) {
530
- panel.sameLine(4, 'justify-center');
531
- panel.addButton(null, 'GoBackButton', () => {
532
- if (!this.prevData.length || !this.currentFolder)
533
- return;
534
- this.nextData.push(this.currentFolder);
535
- this._enterFolder(this.prevData.pop(), false);
536
- }, { buttonClass: 'ghost', title: 'Go Back', tooltip: true, icon: 'ArrowLeft' });
537
- panel.addButton(null, 'GoForwardButton', () => {
538
- if (!this.nextData.length || !this.currentFolder)
539
- return;
540
- this._enterFolder(this.nextData.pop());
541
- }, { buttonClass: 'ghost', title: 'Go Forward', tooltip: true, icon: 'ArrowRight' });
542
- panel.addButton(null, 'GoUpButton', () => {
543
- const parentFolder = this.currentFolder?.parent;
544
- if (parentFolder)
545
- this._enterFolder(parentFolder);
546
- }, { buttonClass: 'ghost', title: 'Go Upper Folder', tooltip: true, icon: 'ArrowUp' });
547
- panel.addButton(null, 'RefreshButton', () => {
548
- this._refreshContent(undefined, undefined, true);
549
- }, { buttonClass: 'ghost', title: 'Refresh', tooltip: true, icon: 'Refresh' });
550
- }
551
- _createTreePanel(area) {
552
- if (this.leftPanel) {
553
- this.leftPanel.clear();
554
- }
555
- else {
556
- this.leftPanel = area.addPanel({ className: 'lexassetbrowserpanel' });
557
- }
558
- this._createNavigationBar(this.leftPanel);
559
- const treeData = { id: '/', children: this.data };
560
- const tree = this.leftPanel.addTree('Content Browser', treeData, {
561
- filter: false,
562
- onlyFolders: this.onlyFolders
563
- });
564
- this._subscribeTreeEvents(tree);
565
- this.tree = tree.innerTree;
566
- }
567
- _subscribeTreeEvents(tree) {
568
- // If some of these events we don't have to call "resolve" since the AV itself
569
- // will update the data and refresh when necessary
570
- tree.on('select', (event, resolve) => {
571
- if (event.items.length > 1) { // Do nothing if multiple selection
572
- return;
573
- }
574
- const node = event.items[0];
575
- if (!node.parent) {
576
- if (this.currentFolder) {
577
- this.prevData.push(this.currentFolder);
578
- }
579
- this.currentFolder = undefined;
580
- this.currentData = this.data;
581
- this._refreshContent();
582
- this._updatePath();
583
- }
584
- else {
585
- this._enterFolder(node.type === 'folder' ? node : node.parent);
586
- this._previewAsset(node);
587
- if (node.type !== 'folder') {
588
- this.content.querySelectorAll('.lexassetitem').forEach((i) => i.classList.remove('selected'));
589
- const dom = node.domEl;
590
- dom?.classList.add('selected');
591
- }
592
- this.selectedItems = [node];
593
- }
594
- });
595
- tree.on('beforeMove', (event, resolve) => {
596
- const onBeforeNodeDragged = this._callbacks['beforeNodeDragged'];
597
- const onNodeDragged = this._callbacks['nodeDragged'];
598
- const node = event.items[0];
599
- const value = event.to;
600
- const av_resolve = (...args) => {
601
- if (node.parent) {
602
- const idx = node.parent.children.indexOf(node);
603
- node.parent.children.splice(idx, 1);
604
- }
605
- if (!value.children) {
606
- value.children = [];
607
- }
608
- value.children.push(node);
609
- node.parent = value;
610
- node.dir = value.children;
611
- // Resolve Tree move event
612
- resolve(...args);
613
- // Fire AV drag event, and not catch the onMove Tree vent
614
- const av_event = {
615
- type: 'node-drag',
616
- items: [node],
617
- to: value,
618
- userInitiated: true
619
- };
620
- if (onNodeDragged)
621
- onNodeDragged(av_event, ...args);
622
- this._refreshContent();
623
- };
624
- if (onBeforeNodeDragged) {
625
- const av_event = {
626
- type: 'node-drag',
627
- items: [node],
628
- to: value,
629
- userInitiated: true
630
- };
631
- onBeforeNodeDragged(av_event, av_resolve);
632
- }
633
- else {
634
- av_resolve();
635
- }
636
- });
637
- tree.on('beforeDelete', (event, resolve) => {
638
- const node = event.items[0];
639
- this._requestDeleteItem(node);
640
- });
641
- tree.on('beforeRename', (event, resolve) => {
642
- const node = event.items[0];
643
- this._requestRenameItem(node, event.newName, true);
644
- });
645
- }
646
- _setContentLayout(layoutMode) {
647
- this.layout = layoutMode;
648
- this.toolsPanel.refresh();
649
- this._refreshContent();
650
- }
651
- _createContentPanel(area) {
652
- const that = this;
653
- area.root.classList.add('flex', 'flex-col');
654
- if (this.toolsPanel) {
655
- this.contentPanel.clear();
656
- }
657
- else {
658
- this.toolsPanel = area.addPanel({ className: 'flex-auto', height: 'auto' });
659
- this.contentPanel = area.addPanel({
660
- className: 'lexassetcontentpanel flex flex-col flex-auto-fill content-center overflow-hidden'
661
- });
662
- this._paginator = new LX.Pagination({
663
- className: 'ml-auto',
664
- pages: Math.max(Math.ceil(this.data.length / this.assetsPerPage), 1),
665
- onChange: () => this._refreshContent()
666
- });
667
- this.contentPanel.root.addEventListener('wheel', (e) => {
668
- if (!e.ctrlKey)
669
- return;
670
- e.preventDefault();
671
- this.gridScale *= e.deltaY < 0 ? 1.05 : 0.95;
672
- this.gridScale = LX.clamp(this.gridScale, 0.5, 2.0);
673
- const r = document.querySelector(':root');
674
- r.style.setProperty('--av-grid-scale', this.gridScale);
675
- });
676
- }
677
- const _onSort = (value, event) => {
678
- LX.addDropdownMenu(event.target, [
679
- { name: 'Name', icon: 'ALargeSmall', callback: () => this._sortData('id') },
680
- { name: 'Type', icon: 'Type', callback: () => this._sortData('type') },
681
- null,
682
- { name: 'Ascending', icon: 'SortAsc', callback: () => this._sortData(undefined, AssetView.CONTENT_SORT_ASC) },
683
- { name: 'Descending', icon: 'SortDesc', callback: () => this._sortData(undefined, AssetView.CONTENT_SORT_DESC) }
684
- ], { side: 'bottom', align: 'start' });
685
- };
686
- const _onChangeView = (value, event) => {
687
- LX.addDropdownMenu(event.target, [
688
- { name: 'Grid', icon: 'LayoutGrid', callback: () => this._setContentLayout(AssetView.LAYOUT_GRID) },
689
- { name: 'Compact', icon: 'LayoutList', callback: () => this._setContentLayout(AssetView.LAYOUT_COMPACT) },
690
- { name: 'List', icon: 'List', callback: () => this._setContentLayout(AssetView.LAYOUT_LIST) }
691
- ], { side: 'bottom', align: 'start' });
692
- };
693
- this.toolsPanel.refresh = () => {
694
- this.toolsPanel.clear();
695
- const typeEntries = Object.keys(this.allowedTypes);
696
- // Put it in the content panel if no browser
697
- if (this.skipBrowser) {
698
- this._createNavigationBar(this.toolsPanel);
699
- }
700
- this.toolsPanel.sameLine();
701
- const sortButton = this.toolsPanel.addButton(null, '', _onSort.bind(this), { title: 'Sort', tooltip: true,
702
- icon: (this.sortMode === AssetView.CONTENT_SORT_ASC) ? 'SortAsc' : 'SortDesc' });
703
- this.toolsPanel.addButton(null, '', _onChangeView.bind(this), { title: 'View', tooltip: true,
704
- icon: (this.layout === AssetView.LAYOUT_GRID) ? 'LayoutGrid' : 'LayoutList' });
705
- this.toolsPanel.addSelect(null, typeEntries, this.filter ?? typeEntries[0], (v) => {
706
- this._refreshContent(undefined, v);
707
- }, { overflowContainer: null });
708
- this.toolsPanel.addText(null, this.searchValue ?? '', (v) => this._refreshContent(v), {
709
- className: 'flex flex-auto-fill',
710
- placeholder: 'Search assets..'
711
- });
712
- this.toolsPanel.endLine();
713
- if (this._paginator) {
714
- const inlineContainer = sortButton.root.parentElement;
715
- inlineContainer.appendChild(this._paginator.root);
716
- }
717
- };
718
- // Start content panel
719
- this.content = document.createElement('ul');
720
- this.content.className = 'lexassetscontent';
721
- this.contentPanel.attach(this.content);
722
- if (!this.skipBrowser) {
723
- this.contentPanel.addText(null, this.path.join('/'), null, {
724
- inputClass: 'bg-none text-muted-foreground text-sm text-end',
725
- disabled: true,
726
- signal: '@on_folder_change'
727
- });
728
- }
729
- this.content.addEventListener('dragenter', function (e) {
730
- e.preventDefault();
731
- this.classList.add('dragging');
732
- });
733
- this.content.addEventListener('dragleave', function (e) {
734
- e.preventDefault();
735
- this.classList.remove('dragging');
736
- });
737
- this.content.addEventListener('drop', (e) => {
738
- e.preventDefault();
739
- this._processDrop(e);
740
- });
741
- this.content.addEventListener('click', function () {
742
- this.querySelectorAll('.lexassetitem').forEach((i) => i.classList.remove('selected'));
743
- that.selectedItems.length = 0;
744
- });
745
- this.content.addEventListener('contextmenu', function (e) {
746
- e.preventDefault();
747
- const options = [
748
- {
749
- name: 'New Folder',
750
- icon: LX.makeIcon('FolderPlus'),
751
- callback: () => {
752
- that._requestCreateFolder();
753
- }
754
- }
755
- ];
756
- LX.addClass(that.contentPanel.root, 'pointer-events-none');
757
- LX.addDropdownMenu(e.target, options, { side: 'right', align: 'start', event: e, onBlur: () => {
758
- LX.removeClass(that.contentPanel.root, 'pointer-events-none');
759
- } });
760
- });
761
- this._refreshContent();
762
- // After content to update the size of the content based on the toolbar
763
- LX.doAsync(() => this.toolsPanel.refresh(), 100);
764
- }
765
- _makeNameFilterFn(searchValue) {
766
- const q = searchValue.trim();
767
- if (q.includes('*') || q.includes('?')) {
768
- const regex = LX.wildcardToRegExp(q);
769
- return (name) => regex.test(name);
770
- }
771
- // default case, only check include
772
- return (name) => name.toLowerCase().includes(q.toLowerCase());
773
- }
774
- _refreshContent(searchValue, filter, userInitiated = false) {
775
- const onBeforeRefreshContent = this._callbacks['beforeRefreshContent'];
776
- const onRefreshContent = this._callbacks['refreshContent'];
777
- const resolve = (...args) => {
778
- const isCompactLayout = this.layout == AssetView.LAYOUT_COMPACT;
779
- const isListLayout = this.layout == AssetView.LAYOUT_LIST;
780
- this.filter = filter ?? (this.filter ?? 'None');
781
- this.searchValue = searchValue ?? (this.searchValue ?? '');
782
- this.content.innerHTML = '';
783
- this.content.className = `lexassetscontent${isCompactLayout ? ' compact' : (isListLayout ? ' list' : '')}`;
784
- if (!this.currentData.length) {
785
- return;
786
- }
787
- const fr = new FileReader();
788
- const nameFilterFn = this._makeNameFilterFn(this.searchValue);
789
- const filteredData = this.currentData.filter((_i) => {
790
- const typeMatch = this.filter !== 'None' ? _i.type.toLowerCase() === this.filter.toLowerCase() : true;
791
- const nameMatch = nameFilterFn(_i.id);
792
- return typeMatch && nameMatch;
793
- });
794
- this._paginator?.setPages(Math.max(Math.ceil(filteredData.length / this.assetsPerPage), 1));
795
- // Show all data if using filters
796
- const start = this._paginator ? (this._paginator.page - 1) * this.assetsPerPage : 0;
797
- const end = this._paginator ? Math.min(start + this.assetsPerPage, filteredData.length) : filteredData.length;
798
- for (let i = start; i < end; ++i) {
799
- let item = filteredData[i];
800
- if (item.path) {
801
- LX.request({ url: item.path, dataType: 'blob', success: (f) => {
802
- item.metadata.bytesize = f.size;
803
- fr.readAsDataURL(f);
804
- fr.onload = (e) => {
805
- const target = e.currentTarget;
806
- item.src = target.result; // This is a base64 string...
807
- item.metadata.path = item.path;
808
- delete item.path;
809
- this._refreshContent(searchValue, filter);
810
- };
811
- } });
812
- }
813
- else {
814
- item.domEl = this.addItem(item, undefined, false);
815
- }
816
- }
817
- const event = {
818
- type: 'refresh-content',
819
- search: [this.searchValue, this.filter],
820
- items: filteredData.slice(start, end),
821
- userInitiated
822
- };
823
- if (onRefreshContent)
824
- onRefreshContent(event, ...args);
825
- };
826
- if (onBeforeRefreshContent) {
827
- const event = {
828
- type: 'refresh-content',
829
- search: [this.searchValue, this.filter],
830
- userInitiated
831
- };
832
- onBeforeRefreshContent(event, resolve);
833
- }
834
- else {
835
- resolve();
836
- }
837
- }
838
- _previewAsset(file) {
839
- if (this.skipPreview) {
840
- return;
841
- }
842
- const is_base_64 = file.src && file.src.includes('data:image/');
843
- file.metadata = file.metadata ?? {};
844
- this.previewPanel.clear();
845
- this.previewPanel.branch('Asset');
846
- if (file.type == 'image' || file.src) {
847
- const hasImage = ['png', 'jpg'].indexOf(LX.getExtension(file.src)) > -1 || is_base_64;
848
- if (hasImage) {
849
- this.previewPanel.addImage(null, file.src, { style: { width: '100%' } });
850
- }
851
- }
852
- if (file.metadata.lastModified && !file.metadata.lastModifiedDate) {
853
- file.metadata.lastModifiedDate = this._lastModifiedToStringDate(file.metadata.lastModified);
854
- }
855
- const options = { disabled: true };
856
- this.previewPanel.addText('Filename', file.id, null, options);
857
- if (file.metadata.lastModifiedDate) {
858
- this.previewPanel.addText('Last Modified', file.metadata.lastModifiedDate, null, options);
859
- }
860
- if (file.metadata.path || file.src) {
861
- this.previewPanel.addText('URL', file.metadata.path ? file.metadata.path : file.src, null, options);
862
- }
863
- this.previewPanel.addText('Path', this.path.join('/'), null, options);
864
- this.previewPanel.addText('Type', file.type, null, options);
865
- if (file.metadata.bytesize) {
866
- this.previewPanel.addText('Size', LX.formatBytes(file.metadata.bytesize), null, options);
867
- }
868
- if (file.type == 'folder') {
869
- this.previewPanel.addText('Files', file.children ? file.children.length.toString() : '0', null, options);
870
- }
871
- this.previewPanel.addSeparator();
872
- const previewActions = [...this.previewActions];
873
- if (!previewActions.length && file.type !== 'folder') {
874
- // By default
875
- previewActions.push({
876
- name: 'Download',
877
- callback: () => LX.downloadURL(file.src, file.id)
878
- });
879
- }
880
- for (let action of previewActions) {
881
- if (action.type && action.type !== file.type || action.path && action.path !== this.path.join('/')) {
882
- continue;
883
- }
884
- this.previewPanel.addButton(null, action.name, action.callback.bind(this, file));
885
- }
886
- this.previewPanel.merge();
887
- }
888
- _processDrop(e) {
889
- if (!e.dataTransfer || !e.dataTransfer.files || e.dataTransfer.files.length == 0) {
890
- return;
891
- }
892
- const fr = new FileReader();
893
- const num_files = e.dataTransfer.files.length;
894
- for (let i = 0; i < e.dataTransfer.files.length; ++i) {
895
- const file = e.dataTransfer.files[i];
896
- const result = this.currentData.find((e) => e.id === file.name);
897
- if (result)
898
- continue;
899
- fr.readAsDataURL(file);
900
- fr.onload = (e) => {
901
- let ext = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase();
902
- let type = null;
903
- switch (ext) {
904
- case 'png':
905
- case 'jpg':
906
- type = 'image';
907
- break;
908
- case 'js':
909
- case 'css':
910
- type = 'script';
911
- break;
912
- case 'json':
913
- type = 'json';
914
- break;
915
- case 'obj':
916
- type = 'mesh';
917
- break;
918
- default:
919
- type = ext;
920
- break;
921
- }
922
- let item = {
923
- id: file.name,
924
- src: e.currentTarget.result,
925
- type,
926
- children: [],
927
- metadata: {
928
- extension: ext,
929
- lastModified: file.lastModified,
930
- lastModifiedDate: this._lastModifiedToStringDate(file.lastModified),
931
- unknownExtension: type == ext
932
- }
933
- };
934
- this.currentData.push(item);
935
- if (i == (num_files - 1)) {
936
- this._refreshContent();
937
- this.tree?.refresh(); // Refresh if tree exists
938
- }
939
- };
940
- }
941
- }
942
- _sortData(sortBy, sortMode) {
943
- sortBy = sortBy ?? (this._lastSortBy ?? 'id');
944
- sortMode = sortMode ?? this.sortMode;
945
- const sortDesc = sortMode === AssetView.CONTENT_SORT_DESC;
946
- this.currentData = this.currentData.sort((a, b) => {
947
- var r = sortDesc ? b[sortBy].localeCompare(a[sortBy]) : a[sortBy].localeCompare(b[sortBy]);
948
- if (r == 0)
949
- r = sortDesc ? b['id'].localeCompare(a['id']) : a['id'].localeCompare(b['id']);
950
- return r;
951
- });
952
- this._lastSortBy = sortBy;
953
- this.sortMode = sortMode;
954
- this.toolsPanel.refresh();
955
- this._refreshContent();
956
- }
957
- async _enterFolder(folderItem, storeCurrent = true) {
958
- if (!folderItem) {
959
- return;
960
- }
961
- const child = this.currentData[0];
962
- const sameFolder = child?.parent?.metadata?.uid === folderItem.metadata?.uid;
963
- if (storeCurrent) {
964
- this.prevData.push(this.currentFolder ?? {
965
- id: '/',
966
- children: this.data,
967
- type: 'root',
968
- metadata: {}
969
- });
970
- }
971
- let mustRefresh = !sameFolder;
972
- const onEnterFolder = this._callbacks['enterFolder'];
973
- if (onEnterFolder !== undefined) {
974
- const event = {
975
- type: 'enter_folder',
976
- to: folderItem,
977
- userInitiated: true
978
- };
979
- const r = await onEnterFolder(event);
980
- mustRefresh = mustRefresh || r;
981
- }
982
- // Update this after the event since the user might have added or modified the data
983
- this.currentFolder = folderItem;
984
- this.currentData = this.currentFolder?.children ?? [];
985
- if (mustRefresh) {
986
- this._processData(this.data);
987
- this._refreshContent();
988
- // Get path to avoid same id issues
989
- let path = `${this.currentFolder.id}/`;
990
- let parent = this.currentFolder.parent;
991
- while (parent && parent.id !== '/') {
992
- path += `${parent.id}/`;
993
- parent = parent.parent;
994
- }
995
- const parentsPath = path.split('/').filter(Boolean).reverse();
996
- this.tree?.select(undefined, parentsPath);
997
- }
998
- this._updatePath();
999
- }
1000
- _removeItemFromParent(item) {
1001
- const oldParent = item.parent;
1002
- if (oldParent) {
1003
- const idx = oldParent.children?.indexOf(item) ?? -1;
1004
- if (idx < 0) {
1005
- return false;
1006
- }
1007
- oldParent.children?.splice(idx, 1);
1008
- }
1009
- else {
1010
- const oldDir = item.dir;
1011
- if (oldDir) {
1012
- const idx = oldDir.indexOf(item);
1013
- if (idx < 0) {
1014
- return false;
1015
- }
1016
- oldDir.splice(idx, 1);
1017
- }
1018
- }
1019
- return true;
1020
- }
1021
- _requestDeleteItem(items) {
1022
- const onBeforeDelete = this._callbacks['beforeDelete'];
1023
- const onDelete = this._callbacks['delete'];
1024
- const resolve = (...args) => {
1025
- items.forEach((item) => this._deleteItem(item));
1026
- const event = {
1027
- type: 'delete',
1028
- items,
1029
- userInitiated: true
1030
- };
1031
- if (onDelete)
1032
- onDelete(event, ...args);
1033
- };
1034
- if (onBeforeDelete) {
1035
- const event = {
1036
- type: 'delete',
1037
- items,
1038
- userInitiated: true
1039
- };
1040
- onBeforeDelete(event, resolve);
1041
- }
1042
- else {
1043
- resolve();
1044
- }
1045
- }
1046
- _deleteItem(item) {
1047
- const ok = this._removeItemFromParent(item);
1048
- if (!ok) {
1049
- console.error('[AssetView Error] Cannot delete. Item not found.');
1050
- return;
1051
- }
1052
- this._refreshContent(this.searchValue, this.filter);
1053
- this.tree?.refresh();
1054
- this.previewPanel?.clear();
1055
- }
1056
- _requestMoveItemToFolder(item, folder) {
1057
- const onBeforeMove = this._callbacks['beforeMove'];
1058
- const onMove = this._callbacks['move'];
1059
- const resolve = (...args) => {
1060
- this._moveItemToFolder(item, folder);
1061
- const event = {
1062
- type: 'move',
1063
- items: [item],
1064
- from: item.parent,
1065
- to: folder,
1066
- userInitiated: true
1067
- };
1068
- if (onMove)
1069
- onMove(event, ...args);
1070
- };
1071
- if (onBeforeMove) {
1072
- const event = {
1073
- type: 'move',
1074
- items: [item],
1075
- from: item.parent,
1076
- to: folder,
1077
- userInitiated: true
1078
- };
1079
- onBeforeMove(event, resolve);
1080
- }
1081
- else {
1082
- resolve();
1083
- }
1084
- }
1085
- _moveItemToFolder(item, folder) {
1086
- const ok = this._removeItemFromParent(item);
1087
- if (!ok) {
1088
- console.error('[AssetView Error] Cannot move. Item not found.');
1089
- return;
1090
- }
1091
- folder.children = folder.children ?? [];
1092
- folder.children.push(item);
1093
- item.parent = folder;
1094
- item.dir = folder.children;
1095
- this._refreshContent();
1096
- this.tree?.refresh();
1097
- this._moveItemDialog?.destroy();
1098
- this._movingItem = undefined;
1099
- this.previewPanel?.clear();
1100
- }
1101
- _moveItem(item, defaultFolder) {
1102
- if (this._moveItemDialog) {
1103
- this._moveItemDialog.destroy();
1104
- }
1105
- this._movingItem = item;
1106
- let targetFolder = null;
1107
- let bcContainer;
1108
- const _openFolder = function (p, container, updateBc = true) {
1109
- container.innerHTML = '';
1110
- targetFolder = p;
1111
- for (let pi of (targetFolder.children ?? targetFolder)) {
1112
- const row = LX.makeContainer(['100%', 'auto'], 'flex flex-row px-1 items-center', '', container);
1113
- const isFolder = pi.type === 'folder';
1114
- const rowItem = LX.makeContainer(['100%', 'auto'], `move-item flex flex-row gap-1 py-1 px-3 cursor-pointer items-center ${isFolder ? 'text-foreground font-medium' : 'text-muted-foreground'} rounded-2xl ${isFolder ? 'hover:bg-accent' : 'hover:bg-muted'}`, `${isFolder ? LX.makeIcon('FolderOpen', { svgClass: '' }).innerHTML : ''}${pi.id}`, row);
1115
- if (isFolder) {
1116
- rowItem.addEventListener('click', () => {
1117
- container.querySelectorAll('.move-item').forEach((el) => LX.removeClass(el, 'bg-primary text-primary-foreground'));
1118
- LX.addClass(rowItem, 'bg-primary text-primary-foreground');
1119
- targetFolder = pi;
1120
- });
1121
- const fPathButton = new LX.Button(null, 'FPathButton', () => {
1122
- _openFolder(pi, container);
1123
- }, { icon: 'ChevronRight', className: 'ml-auto h-8', buttonClass: 'ghost' });
1124
- row.appendChild(fPathButton.root);
1125
- }
1126
- }
1127
- if (!updateBc) {
1128
- return;
1129
- }
1130
- const path = [];
1131
- if (targetFolder && targetFolder.parent) {
1132
- path.push(targetFolder.id);
1133
- const _pushParentsId = (i) => {
1134
- if (!i)
1135
- return;
1136
- path.push(i.parent ? i.id : '@');
1137
- _pushParentsId(i.parent);
1138
- };
1139
- _pushParentsId(targetFolder.parent);
1140
- }
1141
- else {
1142
- path.push('@');
1143
- }
1144
- bcContainer.innerHTML = '';
1145
- bcContainer.appendChild(LX.makeBreadcrumb(path.reverse().map((p) => {
1146
- return { name: p };
1147
- }), {
1148
- maxItems: 4,
1149
- separatorIcon: 'ChevronRight'
1150
- }));
1151
- };
1152
- this._moveItemDialog = new LX.Dialog(`Moving: ${item.id}`, (p) => {
1153
- const area = new LX.Area({ className: 'flex flex-col rounded-lg' });
1154
- p.attach(area);
1155
- const content = LX.makeContainer(['auto', '100%'], 'flex flex-auto-fill flex-col overflow-scroll py-2 gap-1', ``);
1156
- {
1157
- const headerPanel = area.addPanel({ className: 'p-2 border-b-color flex flex-auto-keep', height: 'auto' });
1158
- headerPanel.sameLine(2, 'w-full');
1159
- headerPanel.addButton(null, 'BackButton', () => {
1160
- if (targetFolder && targetFolder.parent)
1161
- _openFolder(targetFolder.parent, content);
1162
- }, { icon: 'ArrowLeft', title: 'Back', tooltip: true, className: 'flex-auto-keep', buttonClass: 'ghost' });
1163
- bcContainer = LX.makeElement('div');
1164
- headerPanel.addContent('ITEM_MOVE_PATH', bcContainer, { signal: '@item_move_path', className: 'flex-auto-fill' });
1165
- }
1166
- area.attach(content);
1167
- _openFolder(defaultFolder ?? this.data, content);
1168
- {
1169
- const footerPanel = area.addPanel({ className: 'p-2 border-t-color flex flex-auto-keep justify-between', height: 'auto' });
1170
- footerPanel.addButton(null, 'NewFolderButton', () => {
1171
- this._requestCreateFolder(targetFolder);
1172
- }, { width: 'auto', icon: 'FolderPlus', title: 'Create Folder', tooltip: true, className: 'ml-2', buttonClass: 'ghost' });
1173
- footerPanel.sameLine(2, 'mr-2');
1174
- footerPanel.addButton(null, 'Cancel', () => {
1175
- this._moveItemDialog.close();
1176
- }, { buttonClass: 'ghost text-destructive' });
1177
- footerPanel.addButton(null, 'Move', () => {
1178
- this._requestMoveItemToFolder(item, targetFolder);
1179
- }, { className: '', buttonClass: 'primary' });
1180
- }
1181
- }, { modal: true, size: ['616px', '500px'], closable: true, onBeforeClose: () => {
1182
- delete this._moveItemDialog;
1183
- } });
1184
- }
1185
- _requestCloneItem(item) {
1186
- if (item.type === 'folder') {
1187
- console.error('[AssetView Error] Cannot clone a folder.');
1188
- return;
1189
- }
1190
- const dir = item.dir ?? [];
1191
- const idx = dir.indexOf(item);
1192
- if (idx < 0) {
1193
- console.error('[AssetView Error] Cannot clone. Item not found.');
1194
- return false;
1195
- }
1196
- const onBeforeClone = this._callbacks['beforeClone'];
1197
- const onClone = this._callbacks['clone'];
1198
- const resolve = (...args) => {
1199
- const clonedItem = this._cloneItem(item);
1200
- const event = {
1201
- type: 'clone',
1202
- items: [item],
1203
- result: [clonedItem],
1204
- userInitiated: true
1205
- };
1206
- if (onClone)
1207
- onClone(event, ...args);
1208
- };
1209
- if (onBeforeClone) {
1210
- const event = {
1211
- type: 'clone',
1212
- items: [item],
1213
- userInitiated: true
1214
- };
1215
- onBeforeClone(event, resolve);
1216
- }
1217
- else {
1218
- resolve();
1219
- }
1220
- }
1221
- _cloneItem(item) {
1222
- const parent = item.parent;
1223
- const dir = item.dir ?? [];
1224
- const idx = dir.indexOf(item);
1225
- delete item.domEl;
1226
- delete item.dir;
1227
- delete item.parent;
1228
- const newItem = LX.deepCopy(item);
1229
- newItem.id = this._getClonedName(item.id, dir);
1230
- newItem.dir = item.dir = dir;
1231
- newItem.parent = item.parent = parent;
1232
- newItem.metadata.uid = LX.guidGenerator(); // generate new uid
1233
- dir.splice(idx + 1, 0, newItem);
1234
- this._refreshContent(this.searchValue, this.filter);
1235
- return newItem;
1236
- }
1237
- _getClonedName(originalName, siblings) {
1238
- const dotIndex = originalName.lastIndexOf('.');
1239
- let base = originalName;
1240
- let ext = '';
1241
- if (dotIndex > 0) {
1242
- base = originalName.substring(0, dotIndex);
1243
- ext = originalName.substring(dotIndex); // includes the dot
1244
- }
1245
- // core name without (N)
1246
- const match = base.match(/^(.*)\s\((\d+)\)$/);
1247
- if (match) {
1248
- base = match[1];
1249
- }
1250
- let maxN = -1;
1251
- for (const s of siblings) {
1252
- if (!s.id)
1253
- continue;
1254
- let sBase = s.id;
1255
- let sExt = '';
1256
- const sDot = sBase.lastIndexOf('.');
1257
- if (sDot > 0) {
1258
- sExt = sBase.substring(sDot);
1259
- sBase = sBase.substring(0, sDot);
1260
- }
1261
- // Only compare same extension and same base!
1262
- if (sExt !== ext)
1263
- continue;
1264
- const m = sBase.match(new RegExp('^' + LX.escapeRegExp(base) + '\\s\\((\\d+)\\)$'));
1265
- if (m) {
1266
- const num = parseInt(m[1]);
1267
- if (num > maxN)
1268
- maxN = num;
1269
- }
1270
- else if (sBase === base) {
1271
- // Base name exists without number
1272
- maxN = Math.max(maxN, 0);
1273
- }
1274
- }
1275
- return maxN === -1 ? originalName : `${base} (${maxN + 1})${ext}`;
1276
- }
1277
- _requestRenameItem(item, newName, treeEvent = false) {
1278
- const onBeforeRename = this._callbacks['beforeRename'];
1279
- const onRename = this._callbacks['rename'];
1280
- const oldName = item.id;
1281
- const resolve = (...args) => {
1282
- this._renameItem(item, newName, treeEvent ? item.dir : this.currentData);
1283
- const event = {
1284
- type: 'rename',
1285
- items: [item],
1286
- oldName,
1287
- newName,
1288
- userInitiated: true
1289
- };
1290
- if (onRename)
1291
- onRename(event, ...args);
1292
- };
1293
- if (onBeforeRename) {
1294
- const event = {
1295
- type: 'rename',
1296
- items: [item],
1297
- oldName,
1298
- newName,
1299
- userInitiated: true
1300
- };
1301
- onBeforeRename(event, resolve);
1302
- }
1303
- else {
1304
- resolve();
1305
- }
1306
- }
1307
- _renameItem(item, newName, data) {
1308
- data = data ?? this.currentData;
1309
- const idx = data.indexOf(item);
1310
- if (idx < 0) {
1311
- return;
1312
- }
1313
- // It could be a Tree event, so maybe the elements is not created yet
1314
- if (item.domEl) {
1315
- const wasSelected = LX.hasClass(item.domEl, 'selected');
1316
- const hoverTitleDomName = LX.getSupportedDOMName(`floatingTitle_${item.metadata.uid}`);
1317
- const hoverTitle = this.content.querySelector(`#${hoverTitleDomName}`);
1318
- if (hoverTitle)
1319
- hoverTitle.remove();
1320
- item.domEl?.remove();
1321
- // Update new name
1322
- item.id = newName;
1323
- item.domEl = this.addItem(item, idx * 2);
1324
- if (wasSelected) {
1325
- this._previewAsset(item);
1326
- }
1327
- }
1328
- else {
1329
- item.id = newName;
1330
- }
1331
- this.tree?.refresh();
1332
- this._processData(this.data);
1333
- }
1334
- _renameItemPopover(item) {
1335
- const idx = this.currentData.indexOf(item);
1336
- if (idx < 0) {
1337
- return;
1338
- }
1339
- const onRename = (value) => {
1340
- p.destroy();
1341
- this._requestRenameItem(item, value);
1342
- };
1343
- let newName = item.id;
1344
- const panel = new LX.Panel();
1345
- panel.addText(null, item.id, (v, e) => {
1346
- newName = v;
1347
- if (e.constructor === KeyboardEvent)
1348
- onRename(v);
1349
- });
1350
- panel.addButton(null, 'Save', () => {
1351
- onRename(newName);
1352
- }, { buttonClass: 'primary' });
1353
- const p = new LX.Popover(item.domEl, [panel], { align: 'center', side: 'bottom', sideOffset: -128 });
1354
- }
1355
- _requestCreateFolder(folder) {
1356
- folder = folder ?? this.currentFolder;
1357
- if (!folder) {
1358
- return;
1359
- }
1360
- const onBeforeCreateFolder = this._callbacks['beforeCreateFolder'];
1361
- const onCreateFolder = this._callbacks['createFolder'];
1362
- const resolve = (...args) => {
1363
- const newFolder = this._createFolder(folder, ...args);
1364
- const event = {
1365
- type: 'create-folder',
1366
- result: [newFolder],
1367
- where: folder,
1368
- userInitiated: true
1369
- };
1370
- if (onCreateFolder)
1371
- onCreateFolder(event, ...args);
1372
- };
1373
- if (onBeforeCreateFolder) {
1374
- const event = {
1375
- type: 'create-folder',
1376
- where: folder,
1377
- userInitiated: true
1378
- };
1379
- onBeforeCreateFolder(event, resolve);
1380
- }
1381
- else {
1382
- resolve();
1383
- }
1384
- }
1385
- _createFolder(folder, newFolderName) {
1386
- folder = folder ?? this.currentFolder;
1387
- if (!folder) {
1388
- throw ('_createFolder: Something went wrong!');
1389
- }
1390
- const dir = folder.children ?? folder;
1391
- const newFolder = {
1392
- id: this._getClonedName(newFolderName ?? 'New Folder', dir),
1393
- type: 'folder',
1394
- children: [],
1395
- parent: this.currentFolder,
1396
- metadata: {}
1397
- };
1398
- dir.push(newFolder);
1399
- this._refreshContent();
1400
- this.tree?.refresh();
1401
- if (this._moveItemDialog && this._movingItem) {
1402
- this._moveItem(this._movingItem, folder);
1403
- }
1404
- return newFolder;
1405
- }
1406
- _openScriptInEditor(script) {
1407
- if (this._scriptCodeDialog) {
1408
- this._scriptCodeDialog.destroy();
1409
- }
1410
- this._scriptCodeDialog = new LX.Dialog(null, (p) => {
1411
- const area = new LX.Area({ className: 'rounded-lg' });
1412
- p.attach(area);
1413
- new LX.CodeEditor(area, {
1414
- allowAddScripts: false,
1415
- files: [script.src]
1416
- });
1417
- }, { size: ['50%', '600px'], closable: true, onBeforeClose: () => {
1418
- delete this._scriptCodeDialog;
1419
- } });
1420
- }
1421
- _setAssetsPerPage(n) {
1422
- this._assetsPerPage = n;
1423
- this._refreshContent();
1424
- }
1425
- _lastModifiedToStringDate(lm) {
1426
- const d = new Date(lm).toLocaleString();
1427
- return d.substring(0, d.indexOf(','));
1428
- }
1429
- }
1430
- LX.AssetView = AssetView;
1431
-
1432
- export { AssetView };
1433
- //# sourceMappingURL=AssetView.js.map
1
+ // This is a generated file. Do not edit.
2
+ import { LX } from '../core/Namespace.js';
3
+
4
+ // AssetView.ts @jxarco
5
+ if (!LX) {
6
+ throw ('Missing LX namespace!');
7
+ }
8
+ LX.extensions.push('AssetView');
9
+ const Area = LX.Area;
10
+ LX.Panel;
11
+ LX.NodeTree;
12
+ LX.Tree;
13
+ /**
14
+ * @class AssetView
15
+ * @description Asset container with Tree for file system
16
+ */
17
+ class AssetView {
18
+ static LAYOUT_GRID = 0;
19
+ static LAYOUT_COMPACT = 1;
20
+ static LAYOUT_LIST = 2;
21
+ static CONTENT_SORT_ASC = 0;
22
+ static CONTENT_SORT_DESC = 1;
23
+ root;
24
+ area = null;
25
+ content; // "!" to avoid TS strict property initialization error
26
+ leftPanel = null;
27
+ toolsPanel;
28
+ contentPanel;
29
+ previewPanel;
30
+ tree = null;
31
+ prevData = [];
32
+ nextData = [];
33
+ data = [];
34
+ currentData = [];
35
+ currentFolder = undefined;
36
+ rootItem;
37
+ path = [];
38
+ rootPath = '';
39
+ selectedItems = [];
40
+ allowedTypes;
41
+ searchValue = '';
42
+ filter = 'None';
43
+ gridScale = 1.0;
44
+ // Options
45
+ layout = AssetView.LAYOUT_GRID;
46
+ sortMode = AssetView.CONTENT_SORT_ASC;
47
+ skipBrowser = false;
48
+ skipPreview = false;
49
+ useNativeTitle = false;
50
+ onlyFolders = true;
51
+ allowMultipleSelection = true;
52
+ allowItemCheck = false;
53
+ previewActions = [];
54
+ contextMenu = [];
55
+ itemContextMenuOptions = null;
56
+ _assetsPerPage = 24;
57
+ get assetsPerPage() {
58
+ return this._assetsPerPage;
59
+ }
60
+ set assetsPerPage(v) {
61
+ this._setAssetsPerPage(v);
62
+ }
63
+ _callbacks = {};
64
+ _lastSortBy = '';
65
+ _paginator;
66
+ _scriptCodeDialog;
67
+ _moveItemDialog;
68
+ _movingItem;
69
+ constructor(options = {}) {
70
+ this.rootPath = 'https://raw.githubusercontent.com/jxarco/lexgui.js/master/';
71
+ this.layout = options.layout ?? this.layout;
72
+ this.sortMode = options.sortMode ?? this.sortMode;
73
+ if (options.rootPath) {
74
+ if (options.rootPath.constructor !== String) {
75
+ console.warn(`Asset Root Path must be a String (now is a ${options.rootPath.constructor.name})`);
76
+ }
77
+ else {
78
+ this.rootPath = options.rootPath;
79
+ }
80
+ }
81
+ let div = document.createElement('div');
82
+ div.className = 'lexassetbrowser';
83
+ this.root = div;
84
+ let area = new Area({ width: '100%', height: '100%' });
85
+ div.appendChild(area.root);
86
+ let left, right, contentArea = area;
87
+ this.skipBrowser = options.skipBrowser ?? this.skipBrowser;
88
+ this.skipPreview = options.skipPreview ?? this.skipPreview;
89
+ this.useNativeTitle = options.useNativeTitle ?? this.useNativeTitle;
90
+ this.onlyFolders = options.onlyFolders ?? this.onlyFolders;
91
+ this.allowMultipleSelection = options.allowMultipleSelection ?? this.allowMultipleSelection;
92
+ this.allowItemCheck = options.allowItemCheck ?? this.allowItemCheck;
93
+ this.previewActions = options.previewActions ?? [];
94
+ this.itemContextMenuOptions = options.itemContextMenuOptions;
95
+ this.gridScale = options.gridScale ?? this.gridScale;
96
+ if (this.gridScale !== 1.0) {
97
+ const r = document.querySelector(':root');
98
+ r.style.setProperty('--av-grid-scale', this.gridScale);
99
+ }
100
+ // Append temporarily to the dom
101
+ document.body.appendChild(this.root);
102
+ if (!this.skipBrowser) {
103
+ [left, right] = area.split({ type: 'horizontal', sizes: ['15%', '85%'] });
104
+ contentArea = right;
105
+ left.setLimitBox(210, 0);
106
+ right.setLimitBox(512, 0);
107
+ }
108
+ if (!this.skipPreview) {
109
+ [contentArea, right] = contentArea.split({ type: 'horizontal', sizes: ['80%', '20%'] });
110
+ }
111
+ this.allowedTypes = {
112
+ 'None': {},
113
+ 'Image': { color: 'yellow-500' },
114
+ 'JSON': { color: 'sky-200' },
115
+ 'Video': { color: 'indigo-400' },
116
+ ...(options.allowedTypes ?? {})
117
+ };
118
+ this.path = ['@'];
119
+ this.rootItem = { id: '/', children: this.data, type: 'folder', metadata: { uid: LX.guidGenerator() } };
120
+ this.currentFolder = this.rootItem;
121
+ this._processData(this.data);
122
+ this.currentData = this.data;
123
+ if (!this.skipBrowser) {
124
+ this._createTreePanel(left);
125
+ }
126
+ this._createContentPanel(contentArea);
127
+ // Create resource preview panel
128
+ if (!this.skipPreview) {
129
+ this.previewPanel = right.addPanel({ className: 'lexassetcontentpanel', style: { overflow: 'scroll' } });
130
+ }
131
+ // Clean up
132
+ document.body.removeChild(this.root);
133
+ }
134
+ /**
135
+ * @method on
136
+ * @description Stores an event callback for the desired action
137
+ */
138
+ on(eventName, callback) {
139
+ this._callbacks[eventName] = callback;
140
+ }
141
+ /**
142
+ * @method load
143
+ * @description Loads and processes the input data
144
+ */
145
+ load(data) {
146
+ this.prevData.length = 0;
147
+ this.nextData.length = 0;
148
+ this.data = data;
149
+ // Update root children
150
+ this.rootItem.children = this.data;
151
+ this._processData(this.data);
152
+ this.currentData = this.data;
153
+ this.path = ['@'];
154
+ if (!this.skipBrowser) {
155
+ this.tree.refresh({ id: '/', children: this.data, type: 'folder', metadata: { uid: LX.guidGenerator() } });
156
+ }
157
+ this._refreshContent();
158
+ }
159
+ /**
160
+ * @method addItem
161
+ * @description Creates an item DOM element
162
+ */
163
+ addItem(item, childIndex, updateTree = true) {
164
+ const isListLayout = this.layout == AssetView.LAYOUT_LIST;
165
+ const isGridLayout = this.layout == AssetView.LAYOUT_GRID; // default
166
+ const type = item.type.charAt(0).toUpperCase() + item.type.slice(1);
167
+ const extension = LX.getExtension(item.id);
168
+ const isFolder = type === 'Folder';
169
+ const that = this;
170
+ let itemEl = document.createElement('li');
171
+ itemEl.className = 'lexassetitem ' + item.type.toLowerCase();
172
+ itemEl.tabIndex = -1;
173
+ LX.insertChildAtIndex(this.content, itemEl, childIndex);
174
+ const typeColor = this.allowedTypes[type]?.color;
175
+ if (typeColor) {
176
+ // Add type tag
177
+ LX.makeElement('span', `rounded-full w-2 h-2 z-100 flex absolute ml-2 mt-2 bg-${typeColor}`, '', itemEl);
178
+ }
179
+ const metadata = item.metadata;
180
+ if (!metadata.uid) {
181
+ metadata.uid = LX.guidGenerator();
182
+ }
183
+ if (metadata.lastModified && !metadata.lastModifiedDate) {
184
+ metadata.lastModifiedDate = this._lastModifiedToStringDate(metadata.lastModified);
185
+ }
186
+ if (!this.useNativeTitle) {
187
+ let desc = document.createElement('span');
188
+ desc.className = 'lexitemdesc';
189
+ desc.id = LX.getSupportedDOMName(`floatingTitle_${metadata.uid}`);
190
+ desc.innerHTML = `File: ${item.id}<br>Type: ${type}`;
191
+ LX.insertChildAtIndex(this.content, desc, childIndex !== undefined ? childIndex + 1 : undefined);
192
+ itemEl.addEventListener('mousemove', (e) => {
193
+ if (!isGridLayout) {
194
+ return;
195
+ }
196
+ const target = e.target;
197
+ const dialog = itemEl.closest('dialog');
198
+ const rect = itemEl.getBoundingClientRect();
199
+ const targetRect = target.getBoundingClientRect();
200
+ let localOffsetX = rect.x + e.offsetX;
201
+ let localOffsetY = rect.y + e.offsetY;
202
+ if (dialog) {
203
+ const dialogRect = dialog.getBoundingClientRect();
204
+ localOffsetX -= dialogRect.x;
205
+ localOffsetY -= dialogRect.y;
206
+ }
207
+ if (target.classList.contains('lexassettitle')) {
208
+ localOffsetY += targetRect.y - rect.y;
209
+ }
210
+ desc.style.left = localOffsetX + 'px';
211
+ desc.style.top = (localOffsetY - 36) + 'px';
212
+ });
213
+ }
214
+ else {
215
+ itemEl.title = type + ': ' + item.id;
216
+ }
217
+ if (this.allowItemCheck) {
218
+ let checkbox = document.createElement('input');
219
+ checkbox.type = 'checkbox';
220
+ checkbox.className = 'lexcheckbox';
221
+ checkbox.checked = metadata.selected;
222
+ checkbox.addEventListener('change', (e) => {
223
+ metadata.selected = !metadata.selected;
224
+ const onCheck = that._callbacks['check'];
225
+ if (onCheck !== undefined) {
226
+ const event = {
227
+ type: 'check',
228
+ items: [item],
229
+ userInitiated: true
230
+ };
231
+ onCheck(event);
232
+ // event.multiple = !!e.shiftKey;
233
+ }
234
+ e.stopPropagation();
235
+ e.stopImmediatePropagation();
236
+ });
237
+ itemEl.appendChild(checkbox);
238
+ }
239
+ // Asset title
240
+ LX.makeElement('span', 'lexassettitle absolute w-full h-8 bottom-0 text-sm bg-card text-card-foreground cursor-pointer text-center content-center block px-3 py-0.5 truncate z-1 pointer-events-none', item.id, itemEl);
241
+ if (!this.skipPreview) {
242
+ if (item.type === 'video') {
243
+ const itemVideo = LX.makeElement('video', 'absolute left-0 top-0 w-full border-none pointer-events-none', '', itemEl);
244
+ itemVideo.setAttribute('disablePictureInPicture', false);
245
+ itemVideo.setAttribute('disableRemotePlayback', false);
246
+ itemVideo.setAttribute('loop', true);
247
+ itemVideo.setAttribute('async', true);
248
+ itemVideo.style.transition = 'opacity 0.2s ease-out';
249
+ itemVideo.style.opacity = metadata.preview ? '0' : '1';
250
+ itemVideo.src = item.src;
251
+ itemVideo.volume = metadata.volume ?? 0.4;
252
+ }
253
+ let preview = null;
254
+ const previewSrc = metadata.preview ?? item.src;
255
+ const hasImage = previewSrc && ((() => {
256
+ const ext = LX.getExtension(previewSrc.split('?')[0].split('#')[0]); // get final source without url parameters/anchors
257
+ return ext ? ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'avif'].includes(ext.toLowerCase()) : false;
258
+ })()
259
+ || previewSrc.startsWith('data:image/'));
260
+ if (hasImage || isFolder || !isGridLayout) {
261
+ const defaultPreviewPath = `${this.rootPath}images/file.png`;
262
+ const defaultFolderPath = `${this.rootPath}images/folder.png`;
263
+ preview = document.createElement('img');
264
+ let realSrc = metadata.unknownExtension
265
+ ? defaultPreviewPath
266
+ : (isFolder ? defaultFolderPath : previewSrc);
267
+ preview.src = isGridLayout || isFolder ? realSrc : defaultPreviewPath;
268
+ preview.setAttribute('draggable', 'false');
269
+ preview.className = 'pointer-events-none';
270
+ itemEl.appendChild(preview);
271
+ }
272
+ else {
273
+ preview = document.createElement('svg');
274
+ preview.className = 'asset-file-preview';
275
+ itemEl.appendChild(preview);
276
+ let textEl = document.createElement('text');
277
+ textEl.innerText = (!extension || extension == item.id)
278
+ ? item.type.toUpperCase()
279
+ : (`${extension.toUpperCase()}`); // If no extension, e.g. Clip, use the type...
280
+ preview.appendChild(textEl);
281
+ var newLength = textEl.innerText.length;
282
+ var charsPerLine = 2.5;
283
+ var newEmSize = charsPerLine / newLength;
284
+ var textBaseSize = 64;
285
+ if (newEmSize < 1) {
286
+ var newFontSize = newEmSize * textBaseSize;
287
+ textEl.style.fontSize = newFontSize + 'px';
288
+ preview.style.paddingTop = `calc(50% - ${(textEl.offsetHeight * 0.5 + 10)}px)`;
289
+ }
290
+ }
291
+ }
292
+ // Add item type info
293
+ let itemInfoHtml = type;
294
+ if (isListLayout) {
295
+ if (metadata.bytesize)
296
+ itemInfoHtml += ` | ${LX.formatBytes(metadata.bytesize)}`;
297
+ if (metadata.lastModifiedDate)
298
+ itemInfoHtml += ` | ${metadata.lastModifiedDate}`;
299
+ }
300
+ LX.makeContainer(['auto', 'auto'], 'lexassetinfo', itemInfoHtml, itemEl);
301
+ itemEl.addEventListener('click', function (e) {
302
+ e.stopImmediatePropagation();
303
+ e.stopPropagation();
304
+ const isDoubleClick = e.detail == LX.MOUSE_DOUBLE_CLICK;
305
+ if (!isDoubleClick) {
306
+ if (!e.shiftKey || !that.allowMultipleSelection) {
307
+ that.content.querySelectorAll('.lexassetitem').forEach((i) => i.classList.remove('selected'));
308
+ that.selectedItems.length = 0;
309
+ }
310
+ this.classList.add('selected');
311
+ that.selectedItems.push(item);
312
+ if (!that.skipPreview) {
313
+ that._previewAsset(item);
314
+ }
315
+ }
316
+ else if (isFolder) {
317
+ that._enterFolder(item);
318
+ return;
319
+ }
320
+ const onSelect = that._callbacks['select'];
321
+ const onDblClick = that._callbacks['dblClick'];
322
+ if (isDoubleClick && onDblClick !== undefined) {
323
+ const event = {
324
+ type: 'dbl_click',
325
+ items: [item],
326
+ userInitiated: true
327
+ };
328
+ onDblClick(event);
329
+ }
330
+ else if (!isDoubleClick && onSelect !== undefined) {
331
+ const event = {
332
+ type: 'select',
333
+ items: [item],
334
+ userInitiated: true
335
+ };
336
+ onSelect(event);
337
+ }
338
+ });
339
+ itemEl.addEventListener('contextmenu', function (e) {
340
+ e.preventDefault();
341
+ e.stopImmediatePropagation();
342
+ e.stopPropagation();
343
+ const multipleSelection = that.selectedItems.length > 1;
344
+ const options = [
345
+ {
346
+ name: multipleSelection ? (`${that.selectedItems.length} selected`) : item.id,
347
+ icon: LX.makeIcon('CircleSmall', { svgClass: `fill-current text-${typeColor}` }),
348
+ className: 'text-sm',
349
+ disabled: true
350
+ },
351
+ null
352
+ ];
353
+ // By now, allow with none/single selected items
354
+ if (!multipleSelection) {
355
+ options.push({ name: 'Rename', icon: 'TextCursor', callback: that._renameItemPopover.bind(that, item) });
356
+ }
357
+ // By now, allow with none/single selected items
358
+ if (!isFolder && !multipleSelection) {
359
+ options.push({ name: 'Clone', icon: 'Copy', callback: that._requestCloneItem.bind(that, item) });
360
+ }
361
+ // By now, allow with none/single selected items
362
+ if (!multipleSelection) {
363
+ options.push({ name: 'Move', icon: 'FolderInput', callback: () => that._moveItem(item) });
364
+ }
365
+ // By now, allow with none/single selected items
366
+ if (!multipleSelection && type == 'Script' && LX.has('CodeEditor')) {
367
+ options.push({ name: 'Open in Editor', icon: 'Code', callback: that._openScriptInEditor.bind(that, item) });
368
+ }
369
+ if (that.itemContextMenuOptions) {
370
+ if (options.length > 2)
371
+ options.push(null);
372
+ for (let o of that.itemContextMenuOptions) {
373
+ if (!o.name || !o.callback)
374
+ continue;
375
+ options.push({ name: o.name, icon: o.icon,
376
+ callback: o.callback?.bind(that, multipleSelection ? that.selectedItems : [item]) });
377
+ }
378
+ }
379
+ options.push(null, { name: 'Delete', icon: 'Trash2', className: 'destructive',
380
+ callback: that._requestDeleteItem.bind(that, multipleSelection ? that.selectedItems : [item]) });
381
+ LX.addClass(that.contentPanel.root, 'pointer-events-none');
382
+ LX.addDropdownMenu(e.target, options, { side: 'right', align: 'start', event: e, onBlur: () => {
383
+ LX.removeClass(that.contentPanel.root, 'pointer-events-none');
384
+ } });
385
+ });
386
+ const onDrop = function (src, target) {
387
+ const targetType = target.type.charAt(0).toUpperCase() + target.type.slice(1);
388
+ if (!(targetType === 'Folder') || (src.metadata.uid == target.metadata.uid)) {
389
+ console.error('[AssetView Error] Cannot drop: Target item is not a folder or target is the dragged element!');
390
+ return;
391
+ }
392
+ // Animate dragged element
393
+ const draggedEl = src.domEl;
394
+ if (draggedEl) {
395
+ draggedEl.classList.add('moving-to-folder');
396
+ // When animation ends, finalize move
397
+ draggedEl.addEventListener('animationend', () => {
398
+ draggedEl.classList.remove('moving-to-folder');
399
+ that._requestMoveItemToFolder(src, target);
400
+ }, { once: true });
401
+ }
402
+ };
403
+ itemEl.addEventListener('dragstart', (e) => {
404
+ window.__av_item_dragged = item;
405
+ var img = new Image();
406
+ img.src = '';
407
+ if (e.dataTransfer) {
408
+ e.dataTransfer.setDragImage(img, 0, 0);
409
+ e.dataTransfer.effectAllowed = 'move';
410
+ }
411
+ const domName = LX.getSupportedDOMName(`floatingTitle_${metadata.uid}`);
412
+ const desc = that.content.querySelector(`#${domName}`);
413
+ if (desc)
414
+ desc.style.display = 'none';
415
+ }, false);
416
+ itemEl.addEventListener('dragend', (e) => {
417
+ e.preventDefault(); // Prevent default action (open as link for some elements)
418
+ let dragged = window.__av_item_dragged;
419
+ if (dragged && dragged._nodeTarget) { // We dropped into a NodeTree element
420
+ onDrop(dragged, dragged._nodeTarget);
421
+ }
422
+ delete window.__av_item_dragged;
423
+ }, false);
424
+ itemEl.addEventListener('dragenter', (e) => {
425
+ e.preventDefault(); // Prevent default action (open as link for some elements)
426
+ let dragged = window.__av_item_dragged;
427
+ if (!dragged || !isFolder || (dragged.metadata.uid == metadata.uid))
428
+ return;
429
+ LX.addClass(item.domEl, 'animate-pulse');
430
+ });
431
+ itemEl.addEventListener('dragleave', (e) => {
432
+ e.preventDefault(); // Prevent default action (open as link for some elements)
433
+ let dragged = window.__av_item_dragged;
434
+ if (!dragged) {
435
+ return;
436
+ }
437
+ LX.removeClass(item.domEl, 'animate-pulse');
438
+ });
439
+ itemEl.addEventListener('drop', (e) => {
440
+ e.preventDefault(); // Prevent default action (open as link for some elements)
441
+ let dragged = window.__av_item_dragged;
442
+ if (dragged)
443
+ onDrop(dragged, item);
444
+ });
445
+ itemEl.addEventListener('mouseenter', (e) => {
446
+ if (!that.useNativeTitle && isGridLayout) {
447
+ const domName = LX.getSupportedDOMName(`floatingTitle_${metadata.uid}`);
448
+ const desc = that.content.querySelector(`#${domName}`);
449
+ if (desc)
450
+ desc.style.display = 'unset';
451
+ }
452
+ if (item.type !== 'video')
453
+ return;
454
+ e.preventDefault();
455
+ const video = itemEl.querySelector('video');
456
+ video.style.opacity = '1';
457
+ video.play();
458
+ });
459
+ itemEl.addEventListener('mouseleave', (e) => {
460
+ if (!that.useNativeTitle && isGridLayout) {
461
+ setTimeout(() => {
462
+ const domName = LX.getSupportedDOMName(`floatingTitle_${metadata.uid}`);
463
+ const desc = that.content.querySelector(`#${domName}`);
464
+ if (desc)
465
+ desc.style.display = 'none';
466
+ }, 100);
467
+ }
468
+ if (item.type !== 'video')
469
+ return;
470
+ e.preventDefault();
471
+ const video = itemEl.querySelector('video');
472
+ video.pause();
473
+ video.currentTime = 0;
474
+ if (metadata.preview) {
475
+ video.style.opacity = '0';
476
+ }
477
+ });
478
+ if (!this.skipBrowser && updateTree) {
479
+ this.tree.refresh();
480
+ }
481
+ return itemEl;
482
+ }
483
+ /**
484
+ * @method clear
485
+ * @description Creates all AssetView container panels
486
+ */
487
+ clear() {
488
+ if (this.previewPanel) {
489
+ this.previewPanel.clear();
490
+ }
491
+ if (this.leftPanel) {
492
+ this.leftPanel.clear();
493
+ }
494
+ if (this.toolsPanel) {
495
+ this.toolsPanel.clear();
496
+ }
497
+ }
498
+ _processData(data, parent) {
499
+ // Processing an item
500
+ if (data.constructor !== Array) {
501
+ data.parent = parent;
502
+ data.dir = parent?.children;
503
+ data.children = data.children ?? [];
504
+ data.metadata = data.metadata || {};
505
+ }
506
+ // Get the new parent
507
+ const newParent = parent ? data : this.rootItem;
508
+ for (let item of newParent.children) {
509
+ this._processData(item, newParent);
510
+ }
511
+ }
512
+ _updatePath() {
513
+ this.path.length = 0;
514
+ if (this.currentFolder && this.currentFolder.parent) {
515
+ this.path.push(this.currentFolder.id);
516
+ const _pushParentsId = (i) => {
517
+ if (!i)
518
+ return;
519
+ this.path.push(i.parent ? i.id : '@');
520
+ _pushParentsId(i.parent);
521
+ };
522
+ _pushParentsId(this.currentFolder.parent);
523
+ }
524
+ else {
525
+ this.path.push('@');
526
+ }
527
+ LX.emitSignal('@on_folder_change', this.path.reverse().join('/'));
528
+ }
529
+ _createNavigationBar(panel) {
530
+ panel.sameLine(4, 'justify-center');
531
+ panel.addButton(null, 'GoBackButton', () => {
532
+ if (!this.prevData.length || !this.currentFolder)
533
+ return;
534
+ this.nextData.push(this.currentFolder);
535
+ this._enterFolder(this.prevData.pop(), false);
536
+ }, { buttonClass: 'ghost', title: 'Go Back', tooltip: true, icon: 'ArrowLeft' });
537
+ panel.addButton(null, 'GoForwardButton', () => {
538
+ if (!this.nextData.length || !this.currentFolder)
539
+ return;
540
+ this._enterFolder(this.nextData.pop());
541
+ }, { buttonClass: 'ghost', title: 'Go Forward', tooltip: true, icon: 'ArrowRight' });
542
+ panel.addButton(null, 'GoUpButton', () => {
543
+ const parentFolder = this.currentFolder?.parent;
544
+ if (parentFolder)
545
+ this._enterFolder(parentFolder);
546
+ }, { buttonClass: 'ghost', title: 'Go Upper Folder', tooltip: true, icon: 'ArrowUp' });
547
+ panel.addButton(null, 'RefreshButton', () => {
548
+ this._refreshContent(undefined, undefined, true);
549
+ }, { buttonClass: 'ghost', title: 'Refresh', tooltip: true, icon: 'Refresh' });
550
+ }
551
+ _createTreePanel(area) {
552
+ if (this.leftPanel) {
553
+ this.leftPanel.clear();
554
+ }
555
+ else {
556
+ this.leftPanel = area.addPanel({ className: 'lexassetbrowserpanel' });
557
+ }
558
+ this._createNavigationBar(this.leftPanel);
559
+ const treeData = { id: '/', children: this.data };
560
+ const tree = this.leftPanel.addTree('Content Browser', treeData, {
561
+ filter: false,
562
+ onlyFolders: this.onlyFolders
563
+ });
564
+ this._subscribeTreeEvents(tree);
565
+ this.tree = tree.innerTree;
566
+ }
567
+ _subscribeTreeEvents(tree) {
568
+ // If some of these events we don't have to call "resolve" since the AV itself
569
+ // will update the data and refresh when necessary
570
+ tree.on('select', (event, resolve) => {
571
+ if (event.items.length > 1) { // Do nothing if multiple selection
572
+ return;
573
+ }
574
+ const node = event.items[0];
575
+ if (!node.parent) {
576
+ if (this.currentFolder) {
577
+ this.prevData.push(this.currentFolder);
578
+ }
579
+ this.currentFolder = undefined;
580
+ this.currentData = this.data;
581
+ this._refreshContent();
582
+ this._updatePath();
583
+ }
584
+ else {
585
+ this._enterFolder(node.type === 'folder' ? node : node.parent);
586
+ this._previewAsset(node);
587
+ if (node.type !== 'folder') {
588
+ this.content.querySelectorAll('.lexassetitem').forEach((i) => i.classList.remove('selected'));
589
+ const dom = node.domEl;
590
+ dom?.classList.add('selected');
591
+ }
592
+ this.selectedItems = [node];
593
+ }
594
+ });
595
+ tree.on('beforeMove', (event, resolve) => {
596
+ const onBeforeNodeDragged = this._callbacks['beforeNodeDragged'];
597
+ const onNodeDragged = this._callbacks['nodeDragged'];
598
+ const node = event.items[0];
599
+ const value = event.to;
600
+ const av_resolve = (...args) => {
601
+ if (node.parent) {
602
+ const idx = node.parent.children.indexOf(node);
603
+ node.parent.children.splice(idx, 1);
604
+ }
605
+ if (!value.children) {
606
+ value.children = [];
607
+ }
608
+ value.children.push(node);
609
+ node.parent = value;
610
+ node.dir = value.children;
611
+ // Resolve Tree move event
612
+ resolve(...args);
613
+ // Fire AV drag event, and not catch the onMove Tree vent
614
+ const av_event = {
615
+ type: 'node-drag',
616
+ items: [node],
617
+ to: value,
618
+ userInitiated: true
619
+ };
620
+ if (onNodeDragged)
621
+ onNodeDragged(av_event, ...args);
622
+ this._refreshContent();
623
+ };
624
+ if (onBeforeNodeDragged) {
625
+ const av_event = {
626
+ type: 'node-drag',
627
+ items: [node],
628
+ to: value,
629
+ userInitiated: true
630
+ };
631
+ onBeforeNodeDragged(av_event, av_resolve);
632
+ }
633
+ else {
634
+ av_resolve();
635
+ }
636
+ });
637
+ tree.on('beforeDelete', (event, resolve) => {
638
+ const node = event.items[0];
639
+ this._requestDeleteItem(node);
640
+ });
641
+ tree.on('beforeRename', (event, resolve) => {
642
+ const node = event.items[0];
643
+ this._requestRenameItem(node, event.newName, true);
644
+ });
645
+ }
646
+ _setContentLayout(layoutMode) {
647
+ this.layout = layoutMode;
648
+ this.toolsPanel.refresh();
649
+ this._refreshContent();
650
+ }
651
+ _createContentPanel(area) {
652
+ const that = this;
653
+ area.root.classList.add('flex', 'flex-col');
654
+ if (this.toolsPanel) {
655
+ this.contentPanel.clear();
656
+ }
657
+ else {
658
+ this.toolsPanel = area.addPanel({ className: 'flex-auto', height: 'auto' });
659
+ this.contentPanel = area.addPanel({
660
+ className: 'lexassetcontentpanel flex flex-col flex-auto-fill content-center overflow-hidden'
661
+ });
662
+ this._paginator = new LX.Pagination({
663
+ className: 'ml-auto',
664
+ pages: Math.max(Math.ceil(this.data.length / this.assetsPerPage), 1),
665
+ onChange: () => this._refreshContent()
666
+ });
667
+ this.contentPanel.root.addEventListener('wheel', (e) => {
668
+ if (!e.ctrlKey)
669
+ return;
670
+ e.preventDefault();
671
+ this.gridScale *= e.deltaY < 0 ? 1.05 : 0.95;
672
+ this.gridScale = LX.clamp(this.gridScale, 0.5, 2.0);
673
+ const r = document.querySelector(':root');
674
+ r.style.setProperty('--av-grid-scale', this.gridScale);
675
+ });
676
+ }
677
+ const _onSort = (value, event) => {
678
+ LX.addDropdownMenu(event.target, [
679
+ { name: 'Name', icon: 'ALargeSmall', callback: () => this._sortData('id') },
680
+ { name: 'Type', icon: 'Type', callback: () => this._sortData('type') },
681
+ null,
682
+ { name: 'Ascending', icon: 'SortAsc', callback: () => this._sortData(undefined, AssetView.CONTENT_SORT_ASC) },
683
+ { name: 'Descending', icon: 'SortDesc', callback: () => this._sortData(undefined, AssetView.CONTENT_SORT_DESC) }
684
+ ], { side: 'bottom', align: 'start' });
685
+ };
686
+ const _onChangeView = (value, event) => {
687
+ LX.addDropdownMenu(event.target, [
688
+ { name: 'Grid', icon: 'LayoutGrid', callback: () => this._setContentLayout(AssetView.LAYOUT_GRID) },
689
+ { name: 'Compact', icon: 'LayoutList', callback: () => this._setContentLayout(AssetView.LAYOUT_COMPACT) },
690
+ { name: 'List', icon: 'List', callback: () => this._setContentLayout(AssetView.LAYOUT_LIST) }
691
+ ], { side: 'bottom', align: 'start' });
692
+ };
693
+ this.toolsPanel.refresh = () => {
694
+ this.toolsPanel.clear();
695
+ const typeEntries = Object.keys(this.allowedTypes);
696
+ // Put it in the content panel if no browser
697
+ if (this.skipBrowser) {
698
+ this._createNavigationBar(this.toolsPanel);
699
+ }
700
+ this.toolsPanel.sameLine();
701
+ const sortButton = this.toolsPanel.addButton(null, '', _onSort.bind(this), { title: 'Sort', tooltip: true,
702
+ icon: (this.sortMode === AssetView.CONTENT_SORT_ASC) ? 'SortAsc' : 'SortDesc' });
703
+ this.toolsPanel.addButton(null, '', _onChangeView.bind(this), { title: 'View', tooltip: true,
704
+ icon: (this.layout === AssetView.LAYOUT_GRID) ? 'LayoutGrid' : 'LayoutList' });
705
+ this.toolsPanel.addSelect(null, typeEntries, this.filter ?? typeEntries[0], (v) => {
706
+ this._refreshContent(undefined, v);
707
+ }, { overflowContainer: null });
708
+ this.toolsPanel.addText(null, this.searchValue ?? '', (v) => this._refreshContent(v), {
709
+ className: 'flex flex-auto-fill',
710
+ placeholder: 'Search assets..'
711
+ });
712
+ this.toolsPanel.endLine();
713
+ if (this._paginator) {
714
+ const inlineContainer = sortButton.root.parentElement;
715
+ inlineContainer.appendChild(this._paginator.root);
716
+ }
717
+ };
718
+ // Start content panel
719
+ this.content = document.createElement('ul');
720
+ this.content.className = 'lexassetscontent';
721
+ this.contentPanel.attach(this.content);
722
+ if (!this.skipBrowser) {
723
+ this.contentPanel.addText(null, this.path.join('/'), null, {
724
+ inputClass: 'bg-none text-muted-foreground text-sm text-end',
725
+ disabled: true,
726
+ signal: '@on_folder_change'
727
+ });
728
+ }
729
+ this.content.addEventListener('dragenter', function (e) {
730
+ e.preventDefault();
731
+ this.classList.add('dragging');
732
+ });
733
+ this.content.addEventListener('dragleave', function (e) {
734
+ e.preventDefault();
735
+ this.classList.remove('dragging');
736
+ });
737
+ this.content.addEventListener('drop', (e) => {
738
+ e.preventDefault();
739
+ this._processDrop(e);
740
+ });
741
+ this.content.addEventListener('click', function () {
742
+ this.querySelectorAll('.lexassetitem').forEach((i) => i.classList.remove('selected'));
743
+ that.selectedItems.length = 0;
744
+ });
745
+ this.content.addEventListener('contextmenu', function (e) {
746
+ e.preventDefault();
747
+ const options = [
748
+ {
749
+ name: 'New Folder',
750
+ icon: LX.makeIcon('FolderPlus'),
751
+ callback: () => {
752
+ that._requestCreateFolder();
753
+ }
754
+ }
755
+ ];
756
+ LX.addClass(that.contentPanel.root, 'pointer-events-none');
757
+ LX.addDropdownMenu(e.target, options, { side: 'right', align: 'start', event: e, onBlur: () => {
758
+ LX.removeClass(that.contentPanel.root, 'pointer-events-none');
759
+ } });
760
+ });
761
+ this._refreshContent();
762
+ // After content to update the size of the content based on the toolbar
763
+ LX.doAsync(() => this.toolsPanel.refresh(), 100);
764
+ }
765
+ _makeNameFilterFn(searchValue) {
766
+ const q = searchValue.trim();
767
+ if (q.includes('*') || q.includes('?')) {
768
+ const regex = LX.wildcardToRegExp(q);
769
+ return (name) => regex.test(name);
770
+ }
771
+ // default case, only check include
772
+ return (name) => name.toLowerCase().includes(q.toLowerCase());
773
+ }
774
+ _refreshContent(searchValue, filter, userInitiated = false) {
775
+ const onBeforeRefreshContent = this._callbacks['beforeRefreshContent'];
776
+ const onRefreshContent = this._callbacks['refreshContent'];
777
+ const resolve = (...args) => {
778
+ const isCompactLayout = this.layout == AssetView.LAYOUT_COMPACT;
779
+ const isListLayout = this.layout == AssetView.LAYOUT_LIST;
780
+ this.filter = filter ?? (this.filter ?? 'None');
781
+ this.searchValue = searchValue ?? (this.searchValue ?? '');
782
+ this.content.innerHTML = '';
783
+ this.content.className = `lexassetscontent${isCompactLayout ? ' compact' : (isListLayout ? ' list' : '')}`;
784
+ if (!this.currentData.length) {
785
+ return;
786
+ }
787
+ const fr = new FileReader();
788
+ const nameFilterFn = this._makeNameFilterFn(this.searchValue);
789
+ const filteredData = this.currentData.filter((_i) => {
790
+ const typeMatch = this.filter !== 'None' ? _i.type.toLowerCase() === this.filter.toLowerCase() : true;
791
+ const nameMatch = nameFilterFn(_i.id);
792
+ return typeMatch && nameMatch;
793
+ });
794
+ this._paginator?.setPages(Math.max(Math.ceil(filteredData.length / this.assetsPerPage), 1));
795
+ // Show all data if using filters
796
+ const start = this._paginator ? (this._paginator.page - 1) * this.assetsPerPage : 0;
797
+ const end = this._paginator ? Math.min(start + this.assetsPerPage, filteredData.length) : filteredData.length;
798
+ for (let i = start; i < end; ++i) {
799
+ let item = filteredData[i];
800
+ if (item.path) {
801
+ LX.request({ url: item.path, dataType: 'blob', success: (f) => {
802
+ item.metadata.bytesize = f.size;
803
+ fr.readAsDataURL(f);
804
+ fr.onload = (e) => {
805
+ const target = e.currentTarget;
806
+ item.src = target.result; // This is a base64 string...
807
+ item.metadata.path = item.path;
808
+ delete item.path;
809
+ this._refreshContent(searchValue, filter);
810
+ };
811
+ } });
812
+ }
813
+ else {
814
+ item.domEl = this.addItem(item, undefined, false);
815
+ }
816
+ }
817
+ const event = {
818
+ type: 'refresh-content',
819
+ search: [this.searchValue, this.filter],
820
+ items: filteredData.slice(start, end),
821
+ userInitiated
822
+ };
823
+ if (onRefreshContent)
824
+ onRefreshContent(event, ...args);
825
+ };
826
+ if (onBeforeRefreshContent) {
827
+ const event = {
828
+ type: 'refresh-content',
829
+ search: [this.searchValue, this.filter],
830
+ userInitiated
831
+ };
832
+ onBeforeRefreshContent(event, resolve);
833
+ }
834
+ else {
835
+ resolve();
836
+ }
837
+ }
838
+ _previewAsset(file) {
839
+ if (this.skipPreview) {
840
+ return;
841
+ }
842
+ const is_base_64 = file.src && file.src.includes('data:image/');
843
+ file.metadata = file.metadata ?? {};
844
+ this.previewPanel.clear();
845
+ this.previewPanel.branch('Asset');
846
+ if (file.type == 'image' || file.src) {
847
+ const hasImage = ['png', 'jpg'].indexOf(LX.getExtension(file.src)) > -1 || is_base_64;
848
+ if (hasImage) {
849
+ this.previewPanel.addImage(null, file.src, { style: { width: '100%' } });
850
+ }
851
+ }
852
+ if (file.metadata.lastModified && !file.metadata.lastModifiedDate) {
853
+ file.metadata.lastModifiedDate = this._lastModifiedToStringDate(file.metadata.lastModified);
854
+ }
855
+ const options = { disabled: true };
856
+ this.previewPanel.addText('Filename', file.id, null, options);
857
+ if (file.metadata.lastModifiedDate) {
858
+ this.previewPanel.addText('Last Modified', file.metadata.lastModifiedDate, null, options);
859
+ }
860
+ if (file.metadata.path || file.src) {
861
+ this.previewPanel.addText('URL', file.metadata.path ? file.metadata.path : file.src, null, options);
862
+ }
863
+ this.previewPanel.addText('Path', this.path.join('/'), null, options);
864
+ this.previewPanel.addText('Type', file.type, null, options);
865
+ if (file.metadata.bytesize) {
866
+ this.previewPanel.addText('Size', LX.formatBytes(file.metadata.bytesize), null, options);
867
+ }
868
+ if (file.type == 'folder') {
869
+ this.previewPanel.addText('Files', file.children ? file.children.length.toString() : '0', null, options);
870
+ }
871
+ this.previewPanel.addSeparator();
872
+ const previewActions = [...this.previewActions];
873
+ if (!previewActions.length && file.type !== 'folder') {
874
+ // By default
875
+ previewActions.push({
876
+ name: 'Download',
877
+ callback: () => LX.downloadURL(file.src, file.id)
878
+ });
879
+ }
880
+ for (let action of previewActions) {
881
+ if (action.type && action.type !== file.type || action.path && action.path !== this.path.join('/')) {
882
+ continue;
883
+ }
884
+ this.previewPanel.addButton(null, action.name, action.callback.bind(this, file));
885
+ }
886
+ this.previewPanel.merge();
887
+ }
888
+ _processDrop(e) {
889
+ if (!e.dataTransfer || !e.dataTransfer.files || e.dataTransfer.files.length == 0) {
890
+ return;
891
+ }
892
+ const fr = new FileReader();
893
+ const num_files = e.dataTransfer.files.length;
894
+ for (let i = 0; i < e.dataTransfer.files.length; ++i) {
895
+ const file = e.dataTransfer.files[i];
896
+ const result = this.currentData.find((e) => e.id === file.name);
897
+ if (result)
898
+ continue;
899
+ fr.readAsDataURL(file);
900
+ fr.onload = (e) => {
901
+ let ext = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase();
902
+ let type = null;
903
+ switch (ext) {
904
+ case 'png':
905
+ case 'jpg':
906
+ type = 'image';
907
+ break;
908
+ case 'js':
909
+ case 'css':
910
+ type = 'script';
911
+ break;
912
+ case 'json':
913
+ type = 'json';
914
+ break;
915
+ case 'obj':
916
+ type = 'mesh';
917
+ break;
918
+ default:
919
+ type = ext;
920
+ break;
921
+ }
922
+ let item = {
923
+ id: file.name,
924
+ src: e.currentTarget.result,
925
+ type,
926
+ children: [],
927
+ metadata: {
928
+ extension: ext,
929
+ lastModified: file.lastModified,
930
+ lastModifiedDate: this._lastModifiedToStringDate(file.lastModified),
931
+ unknownExtension: type == ext
932
+ }
933
+ };
934
+ this.currentData.push(item);
935
+ if (i == (num_files - 1)) {
936
+ this._refreshContent();
937
+ this.tree?.refresh(); // Refresh if tree exists
938
+ }
939
+ };
940
+ }
941
+ }
942
+ _sortData(sortBy, sortMode) {
943
+ sortBy = sortBy ?? (this._lastSortBy ?? 'id');
944
+ sortMode = sortMode ?? this.sortMode;
945
+ const sortDesc = sortMode === AssetView.CONTENT_SORT_DESC;
946
+ this.currentData = this.currentData.sort((a, b) => {
947
+ var r = sortDesc ? b[sortBy].localeCompare(a[sortBy]) : a[sortBy].localeCompare(b[sortBy]);
948
+ if (r == 0)
949
+ r = sortDesc ? b['id'].localeCompare(a['id']) : a['id'].localeCompare(b['id']);
950
+ return r;
951
+ });
952
+ this._lastSortBy = sortBy;
953
+ this.sortMode = sortMode;
954
+ this.toolsPanel.refresh();
955
+ this._refreshContent();
956
+ }
957
+ async _enterFolder(folderItem, storeCurrent = true) {
958
+ if (!folderItem) {
959
+ return;
960
+ }
961
+ const child = this.currentData[0];
962
+ const sameFolder = child?.parent?.metadata?.uid === folderItem.metadata?.uid;
963
+ if (storeCurrent) {
964
+ this.prevData.push(this.currentFolder ?? {
965
+ id: '/',
966
+ children: this.data,
967
+ type: 'root',
968
+ metadata: {}
969
+ });
970
+ }
971
+ let mustRefresh = !sameFolder;
972
+ const onEnterFolder = this._callbacks['enterFolder'];
973
+ if (onEnterFolder !== undefined) {
974
+ const event = {
975
+ type: 'enter_folder',
976
+ to: folderItem,
977
+ userInitiated: true
978
+ };
979
+ const r = await onEnterFolder(event);
980
+ mustRefresh = mustRefresh || r;
981
+ }
982
+ // Update this after the event since the user might have added or modified the data
983
+ this.currentFolder = folderItem;
984
+ this.currentData = this.currentFolder?.children ?? [];
985
+ if (mustRefresh) {
986
+ this._processData(this.data);
987
+ this._refreshContent();
988
+ // Get path to avoid same id issues
989
+ let path = `${this.currentFolder.id}/`;
990
+ let parent = this.currentFolder.parent;
991
+ while (parent && parent.id !== '/') {
992
+ path += `${parent.id}/`;
993
+ parent = parent.parent;
994
+ }
995
+ const parentsPath = path.split('/').filter(Boolean).reverse();
996
+ this.tree?.select(undefined, parentsPath);
997
+ }
998
+ this._updatePath();
999
+ }
1000
+ _removeItemFromParent(item) {
1001
+ const oldParent = item.parent;
1002
+ if (oldParent) {
1003
+ const idx = oldParent.children?.indexOf(item) ?? -1;
1004
+ if (idx < 0) {
1005
+ return false;
1006
+ }
1007
+ oldParent.children?.splice(idx, 1);
1008
+ }
1009
+ else {
1010
+ const oldDir = item.dir;
1011
+ if (oldDir) {
1012
+ const idx = oldDir.indexOf(item);
1013
+ if (idx < 0) {
1014
+ return false;
1015
+ }
1016
+ oldDir.splice(idx, 1);
1017
+ }
1018
+ }
1019
+ return true;
1020
+ }
1021
+ _requestDeleteItem(items) {
1022
+ const onBeforeDelete = this._callbacks['beforeDelete'];
1023
+ const onDelete = this._callbacks['delete'];
1024
+ const resolve = (...args) => {
1025
+ items.forEach((item) => this._deleteItem(item));
1026
+ const event = {
1027
+ type: 'delete',
1028
+ items,
1029
+ userInitiated: true
1030
+ };
1031
+ if (onDelete)
1032
+ onDelete(event, ...args);
1033
+ };
1034
+ if (onBeforeDelete) {
1035
+ const event = {
1036
+ type: 'delete',
1037
+ items,
1038
+ userInitiated: true
1039
+ };
1040
+ onBeforeDelete(event, resolve);
1041
+ }
1042
+ else {
1043
+ resolve();
1044
+ }
1045
+ }
1046
+ _deleteItem(item) {
1047
+ const ok = this._removeItemFromParent(item);
1048
+ if (!ok) {
1049
+ console.error('[AssetView Error] Cannot delete. Item not found.');
1050
+ return;
1051
+ }
1052
+ this._refreshContent(this.searchValue, this.filter);
1053
+ this.tree?.refresh();
1054
+ this.previewPanel?.clear();
1055
+ }
1056
+ _requestMoveItemToFolder(item, folder) {
1057
+ const onBeforeMove = this._callbacks['beforeMove'];
1058
+ const onMove = this._callbacks['move'];
1059
+ const resolve = (...args) => {
1060
+ this._moveItemToFolder(item, folder);
1061
+ const event = {
1062
+ type: 'move',
1063
+ items: [item],
1064
+ from: item.parent,
1065
+ to: folder,
1066
+ userInitiated: true
1067
+ };
1068
+ if (onMove)
1069
+ onMove(event, ...args);
1070
+ };
1071
+ if (onBeforeMove) {
1072
+ const event = {
1073
+ type: 'move',
1074
+ items: [item],
1075
+ from: item.parent,
1076
+ to: folder,
1077
+ userInitiated: true
1078
+ };
1079
+ onBeforeMove(event, resolve);
1080
+ }
1081
+ else {
1082
+ resolve();
1083
+ }
1084
+ }
1085
+ _moveItemToFolder(item, folder) {
1086
+ const ok = this._removeItemFromParent(item);
1087
+ if (!ok) {
1088
+ console.error('[AssetView Error] Cannot move. Item not found.');
1089
+ return;
1090
+ }
1091
+ folder.children = folder.children ?? [];
1092
+ folder.children.push(item);
1093
+ item.parent = folder;
1094
+ item.dir = folder.children;
1095
+ this._refreshContent();
1096
+ this.tree?.refresh();
1097
+ this._moveItemDialog?.destroy();
1098
+ this._movingItem = undefined;
1099
+ this.previewPanel?.clear();
1100
+ }
1101
+ _moveItem(item, defaultFolder) {
1102
+ if (this._moveItemDialog) {
1103
+ this._moveItemDialog.destroy();
1104
+ }
1105
+ this._movingItem = item;
1106
+ let targetFolder = null;
1107
+ let bcContainer;
1108
+ const _openFolder = function (p, container, updateBc = true) {
1109
+ container.innerHTML = '';
1110
+ targetFolder = p;
1111
+ for (let pi of (targetFolder.children ?? targetFolder)) {
1112
+ const row = LX.makeContainer(['100%', 'auto'], 'flex flex-row px-1 items-center', '', container);
1113
+ const isFolder = pi.type === 'folder';
1114
+ const rowItem = LX.makeContainer(['100%', 'auto'], `move-item flex flex-row gap-1 py-1 px-3 cursor-pointer items-center ${isFolder ? 'text-foreground font-medium' : 'text-muted-foreground'} rounded-2xl ${isFolder ? 'hover:bg-accent' : 'hover:bg-muted'}`, `${isFolder ? LX.makeIcon('FolderOpen', { svgClass: '' }).innerHTML : ''}${pi.id}`, row);
1115
+ if (isFolder) {
1116
+ rowItem.addEventListener('click', () => {
1117
+ container.querySelectorAll('.move-item').forEach((el) => LX.removeClass(el, 'bg-primary text-primary-foreground'));
1118
+ LX.addClass(rowItem, 'bg-primary text-primary-foreground');
1119
+ targetFolder = pi;
1120
+ });
1121
+ const fPathButton = new LX.Button(null, 'FPathButton', () => {
1122
+ _openFolder(pi, container);
1123
+ }, { icon: 'ChevronRight', className: 'ml-auto h-8', buttonClass: 'ghost' });
1124
+ row.appendChild(fPathButton.root);
1125
+ }
1126
+ }
1127
+ if (!updateBc) {
1128
+ return;
1129
+ }
1130
+ const path = [];
1131
+ if (targetFolder && targetFolder.parent) {
1132
+ path.push(targetFolder.id);
1133
+ const _pushParentsId = (i) => {
1134
+ if (!i)
1135
+ return;
1136
+ path.push(i.parent ? i.id : '@');
1137
+ _pushParentsId(i.parent);
1138
+ };
1139
+ _pushParentsId(targetFolder.parent);
1140
+ }
1141
+ else {
1142
+ path.push('@');
1143
+ }
1144
+ bcContainer.innerHTML = '';
1145
+ bcContainer.appendChild(LX.makeBreadcrumb(path.reverse().map((p) => {
1146
+ return { name: p };
1147
+ }), {
1148
+ maxItems: 4,
1149
+ separatorIcon: 'ChevronRight'
1150
+ }));
1151
+ };
1152
+ this._moveItemDialog = new LX.Dialog(`Moving: ${item.id}`, (p) => {
1153
+ const area = new LX.Area({ className: 'flex flex-col rounded-lg' });
1154
+ p.attach(area);
1155
+ const content = LX.makeContainer(['auto', '100%'], 'flex flex-auto-fill flex-col overflow-scroll py-2 gap-1', ``);
1156
+ {
1157
+ const headerPanel = area.addPanel({ className: 'p-2 border-b-color flex flex-auto-keep', height: 'auto' });
1158
+ headerPanel.sameLine(2, 'w-full');
1159
+ headerPanel.addButton(null, 'BackButton', () => {
1160
+ if (targetFolder && targetFolder.parent)
1161
+ _openFolder(targetFolder.parent, content);
1162
+ }, { icon: 'ArrowLeft', title: 'Back', tooltip: true, className: 'flex-auto-keep', buttonClass: 'ghost' });
1163
+ bcContainer = LX.makeElement('div');
1164
+ headerPanel.addContent('ITEM_MOVE_PATH', bcContainer, { signal: '@item_move_path', className: 'flex-auto-fill' });
1165
+ }
1166
+ area.attach(content);
1167
+ _openFolder(defaultFolder ?? this.data, content);
1168
+ {
1169
+ const footerPanel = area.addPanel({ className: 'p-2 border-t-color flex flex-auto-keep justify-between', height: 'auto' });
1170
+ footerPanel.addButton(null, 'NewFolderButton', () => {
1171
+ this._requestCreateFolder(targetFolder);
1172
+ }, { width: 'auto', icon: 'FolderPlus', title: 'Create Folder', tooltip: true, className: 'ml-2', buttonClass: 'ghost' });
1173
+ footerPanel.sameLine(2, 'mr-2');
1174
+ footerPanel.addButton(null, 'Cancel', () => {
1175
+ this._moveItemDialog.close();
1176
+ }, { buttonClass: 'ghost text-destructive' });
1177
+ footerPanel.addButton(null, 'Move', () => {
1178
+ this._requestMoveItemToFolder(item, targetFolder);
1179
+ }, { className: '', buttonClass: 'primary' });
1180
+ }
1181
+ }, { modal: true, size: ['616px', '500px'], closable: true, onBeforeClose: () => {
1182
+ delete this._moveItemDialog;
1183
+ } });
1184
+ }
1185
+ _requestCloneItem(item) {
1186
+ if (item.type === 'folder') {
1187
+ console.error('[AssetView Error] Cannot clone a folder.');
1188
+ return;
1189
+ }
1190
+ const dir = item.dir ?? [];
1191
+ const idx = dir.indexOf(item);
1192
+ if (idx < 0) {
1193
+ console.error('[AssetView Error] Cannot clone. Item not found.');
1194
+ return false;
1195
+ }
1196
+ const onBeforeClone = this._callbacks['beforeClone'];
1197
+ const onClone = this._callbacks['clone'];
1198
+ const resolve = (...args) => {
1199
+ const clonedItem = this._cloneItem(item);
1200
+ const event = {
1201
+ type: 'clone',
1202
+ items: [item],
1203
+ result: [clonedItem],
1204
+ userInitiated: true
1205
+ };
1206
+ if (onClone)
1207
+ onClone(event, ...args);
1208
+ };
1209
+ if (onBeforeClone) {
1210
+ const event = {
1211
+ type: 'clone',
1212
+ items: [item],
1213
+ userInitiated: true
1214
+ };
1215
+ onBeforeClone(event, resolve);
1216
+ }
1217
+ else {
1218
+ resolve();
1219
+ }
1220
+ }
1221
+ _cloneItem(item) {
1222
+ const parent = item.parent;
1223
+ const dir = item.dir ?? [];
1224
+ const idx = dir.indexOf(item);
1225
+ delete item.domEl;
1226
+ delete item.dir;
1227
+ delete item.parent;
1228
+ const newItem = LX.deepCopy(item);
1229
+ newItem.id = this._getClonedName(item.id, dir);
1230
+ newItem.dir = item.dir = dir;
1231
+ newItem.parent = item.parent = parent;
1232
+ newItem.metadata.uid = LX.guidGenerator(); // generate new uid
1233
+ dir.splice(idx + 1, 0, newItem);
1234
+ this._refreshContent(this.searchValue, this.filter);
1235
+ return newItem;
1236
+ }
1237
+ _getClonedName(originalName, siblings) {
1238
+ const dotIndex = originalName.lastIndexOf('.');
1239
+ let base = originalName;
1240
+ let ext = '';
1241
+ if (dotIndex > 0) {
1242
+ base = originalName.substring(0, dotIndex);
1243
+ ext = originalName.substring(dotIndex); // includes the dot
1244
+ }
1245
+ // core name without (N)
1246
+ const match = base.match(/^(.*)\s\((\d+)\)$/);
1247
+ if (match) {
1248
+ base = match[1];
1249
+ }
1250
+ let maxN = -1;
1251
+ for (const s of siblings) {
1252
+ if (!s.id)
1253
+ continue;
1254
+ let sBase = s.id;
1255
+ let sExt = '';
1256
+ const sDot = sBase.lastIndexOf('.');
1257
+ if (sDot > 0) {
1258
+ sExt = sBase.substring(sDot);
1259
+ sBase = sBase.substring(0, sDot);
1260
+ }
1261
+ // Only compare same extension and same base!
1262
+ if (sExt !== ext)
1263
+ continue;
1264
+ const m = sBase.match(new RegExp('^' + LX.escapeRegExp(base) + '\\s\\((\\d+)\\)$'));
1265
+ if (m) {
1266
+ const num = parseInt(m[1]);
1267
+ if (num > maxN)
1268
+ maxN = num;
1269
+ }
1270
+ else if (sBase === base) {
1271
+ // Base name exists without number
1272
+ maxN = Math.max(maxN, 0);
1273
+ }
1274
+ }
1275
+ return maxN === -1 ? originalName : `${base} (${maxN + 1})${ext}`;
1276
+ }
1277
+ _requestRenameItem(item, newName, treeEvent = false) {
1278
+ const onBeforeRename = this._callbacks['beforeRename'];
1279
+ const onRename = this._callbacks['rename'];
1280
+ const oldName = item.id;
1281
+ const resolve = (...args) => {
1282
+ this._renameItem(item, newName, treeEvent ? item.dir : this.currentData);
1283
+ const event = {
1284
+ type: 'rename',
1285
+ items: [item],
1286
+ oldName,
1287
+ newName,
1288
+ userInitiated: true
1289
+ };
1290
+ if (onRename)
1291
+ onRename(event, ...args);
1292
+ };
1293
+ if (onBeforeRename) {
1294
+ const event = {
1295
+ type: 'rename',
1296
+ items: [item],
1297
+ oldName,
1298
+ newName,
1299
+ userInitiated: true
1300
+ };
1301
+ onBeforeRename(event, resolve);
1302
+ }
1303
+ else {
1304
+ resolve();
1305
+ }
1306
+ }
1307
+ _renameItem(item, newName, data) {
1308
+ data = data ?? this.currentData;
1309
+ const idx = data.indexOf(item);
1310
+ if (idx < 0) {
1311
+ return;
1312
+ }
1313
+ // It could be a Tree event, so maybe the elements is not created yet
1314
+ if (item.domEl) {
1315
+ const wasSelected = LX.hasClass(item.domEl, 'selected');
1316
+ const hoverTitleDomName = LX.getSupportedDOMName(`floatingTitle_${item.metadata.uid}`);
1317
+ const hoverTitle = this.content.querySelector(`#${hoverTitleDomName}`);
1318
+ if (hoverTitle)
1319
+ hoverTitle.remove();
1320
+ item.domEl?.remove();
1321
+ // Update new name
1322
+ item.id = newName;
1323
+ item.domEl = this.addItem(item, idx * 2);
1324
+ if (wasSelected) {
1325
+ this._previewAsset(item);
1326
+ }
1327
+ }
1328
+ else {
1329
+ item.id = newName;
1330
+ }
1331
+ this.tree?.refresh();
1332
+ this._processData(this.data);
1333
+ }
1334
+ _renameItemPopover(item) {
1335
+ const idx = this.currentData.indexOf(item);
1336
+ if (idx < 0) {
1337
+ return;
1338
+ }
1339
+ const onRename = (value) => {
1340
+ p.destroy();
1341
+ this._requestRenameItem(item, value);
1342
+ };
1343
+ let newName = item.id;
1344
+ const panel = new LX.Panel();
1345
+ panel.addText(null, item.id, (v, e) => {
1346
+ newName = v;
1347
+ if (e.constructor === KeyboardEvent)
1348
+ onRename(v);
1349
+ });
1350
+ panel.addButton(null, 'Save', () => {
1351
+ onRename(newName);
1352
+ }, { buttonClass: 'primary' });
1353
+ const p = new LX.Popover(item.domEl, [panel], { align: 'center', side: 'bottom', sideOffset: -128 });
1354
+ }
1355
+ _requestCreateFolder(folder) {
1356
+ folder = folder ?? this.currentFolder;
1357
+ if (!folder) {
1358
+ return;
1359
+ }
1360
+ const onBeforeCreateFolder = this._callbacks['beforeCreateFolder'];
1361
+ const onCreateFolder = this._callbacks['createFolder'];
1362
+ const resolve = (...args) => {
1363
+ const newFolder = this._createFolder(folder, ...args);
1364
+ const event = {
1365
+ type: 'create-folder',
1366
+ result: [newFolder],
1367
+ where: folder,
1368
+ userInitiated: true
1369
+ };
1370
+ if (onCreateFolder)
1371
+ onCreateFolder(event, ...args);
1372
+ };
1373
+ if (onBeforeCreateFolder) {
1374
+ const event = {
1375
+ type: 'create-folder',
1376
+ where: folder,
1377
+ userInitiated: true
1378
+ };
1379
+ onBeforeCreateFolder(event, resolve);
1380
+ }
1381
+ else {
1382
+ resolve();
1383
+ }
1384
+ }
1385
+ _createFolder(folder, newFolderName) {
1386
+ folder = folder ?? this.currentFolder;
1387
+ if (!folder) {
1388
+ throw ('_createFolder: Something went wrong!');
1389
+ }
1390
+ const dir = folder.children ?? folder;
1391
+ const newFolder = {
1392
+ id: this._getClonedName(newFolderName ?? 'New Folder', dir),
1393
+ type: 'folder',
1394
+ children: [],
1395
+ parent: this.currentFolder,
1396
+ metadata: {}
1397
+ };
1398
+ dir.push(newFolder);
1399
+ this._refreshContent();
1400
+ this.tree?.refresh();
1401
+ if (this._moveItemDialog && this._movingItem) {
1402
+ this._moveItem(this._movingItem, folder);
1403
+ }
1404
+ return newFolder;
1405
+ }
1406
+ _openScriptInEditor(script) {
1407
+ if (this._scriptCodeDialog) {
1408
+ this._scriptCodeDialog.destroy();
1409
+ }
1410
+ this._scriptCodeDialog = new LX.Dialog(null, (p) => {
1411
+ const area = new LX.Area({ className: 'rounded-lg' });
1412
+ p.attach(area);
1413
+ new LX.CodeEditor(area, {
1414
+ allowAddScripts: false,
1415
+ files: [script.src]
1416
+ });
1417
+ }, { size: ['50%', '600px'], closable: true, onBeforeClose: () => {
1418
+ delete this._scriptCodeDialog;
1419
+ } });
1420
+ }
1421
+ _setAssetsPerPage(n) {
1422
+ this._assetsPerPage = n;
1423
+ this._refreshContent();
1424
+ }
1425
+ _lastModifiedToStringDate(lm) {
1426
+ const d = new Date(lm).toLocaleString();
1427
+ return d.substring(0, d.indexOf(','));
1428
+ }
1429
+ }
1430
+ LX.AssetView = AssetView;
1431
+
1432
+ export { AssetView };
1433
+ //# sourceMappingURL=AssetView.js.map