@oat-sa/tao-core-ui 3.22.0 → 3.24.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 (40) hide show
  1. package/dist/figure/FigureStateActive.js +64 -32
  2. package/dist/image/ImgStateActive/initHelper.js +64 -32
  3. package/dist/image/ImgStateActive.js +64 -32
  4. package/dist/mediaplayer/players/html5.js +44 -18
  5. package/dist/mediaplayer/utils/crossOrigin.js +64 -0
  6. package/dist/mediaplayer.js +7 -1
  7. package/dist/previewer.js +1 -1
  8. package/dist/resourcemgr/assetSearch.js +793 -0
  9. package/dist/resourcemgr/assetSearchContract.js +477 -0
  10. package/dist/resourcemgr/css/resourcemgr.css +1137 -97
  11. package/dist/resourcemgr/css/resourcemgr.css.map +1 -1
  12. package/dist/resourcemgr/fileBrowser.js +456 -79
  13. package/dist/resourcemgr/filePreview.js +40 -4
  14. package/dist/resourcemgr/fileSelector.js +362 -92
  15. package/dist/resourcemgr.js +309 -57
  16. package/dist/searchModal/advancedSearch.js +233 -31
  17. package/package.json +15 -6
  18. package/src/image/ImgStateActive/initHelper.js +69 -31
  19. package/src/mediaplayer/players/html5.js +17 -7
  20. package/src/mediaplayer/readme.md +17 -0
  21. package/src/mediaplayer/tpl/audio.tpl +1 -1
  22. package/src/mediaplayer/tpl/video.tpl +1 -1
  23. package/src/mediaplayer/utils/crossOrigin.js +63 -0
  24. package/src/mediaplayer.js +10 -0
  25. package/src/previewer.js +1 -1
  26. package/src/resourcemgr/assetSearch.js +847 -0
  27. package/src/resourcemgr/assetSearchContract.js +489 -0
  28. package/src/resourcemgr/css/resourcemgr.css +1137 -97
  29. package/src/resourcemgr/css/resourcemgr.css.map +1 -1
  30. package/src/resourcemgr/fileBrowser.js +470 -91
  31. package/src/resourcemgr/filePreview.js +46 -4
  32. package/src/resourcemgr/fileSelector.js +338 -46
  33. package/src/resourcemgr/scss/resourcemgr.scss +1351 -88
  34. package/src/resourcemgr/tpl/advanced-search.tpl +10 -0
  35. package/src/resourcemgr/tpl/fileSelect.tpl +25 -28
  36. package/src/resourcemgr/tpl/folder.tpl +4 -1
  37. package/src/resourcemgr/tpl/layout.tpl +113 -54
  38. package/src/resourcemgr/tpl/rootFolder.tpl +3 -1
  39. package/src/resourcemgr.js +289 -47
  40. package/src/searchModal/advancedSearch.js +242 -31
@@ -1,11 +1,11 @@
1
- define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/handlebars/helpers'], function ($$1, _, request, paginationComponent, Handlebars, Helpers0) { 'use strict';
1
+ define(['jquery', 'lodash', 'ui/pagination', 'handlebars', 'lib/handlebars/helpers', 'core/logger', 'ui/resourcemgr/assetSearchContract'], function ($$1, _, paginationComponent, Handlebars, Helpers0, loggerFactory, assetSearchContract) { 'use strict';
2
2
 
3
3
  $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1;
4
4
  _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
5
- request = request && Object.prototype.hasOwnProperty.call(request, 'default') ? request['default'] : request;
6
5
  paginationComponent = paginationComponent && Object.prototype.hasOwnProperty.call(paginationComponent, 'default') ? paginationComponent['default'] : paginationComponent;
7
6
  Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars;
8
7
  Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0;
8
+ loggerFactory = loggerFactory && Object.prototype.hasOwnProperty.call(loggerFactory, 'default') ? loggerFactory['default'] : loggerFactory;
9
9
 
10
10
  if (!Helpers0.__initialized) {
11
11
  Helpers0(Handlebars);
@@ -14,12 +14,15 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
14
14
  var Template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
15
15
  this.compilerInfo = [4,'>= 1.0.0'];
16
16
  helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
17
- var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
17
+ var buffer = "", stack1, self=this, functionType="function", escapeExpression=this.escapeExpression;
18
18
 
19
19
  function program1(depth0,data) {
20
20
 
21
21
  var buffer = "", stack1, helper;
22
- buffer += "\n <li class=\"root\">\n <a\n class=\"root-folder active\"\n href=\"#\"\n data-path=\"";
22
+ buffer += "\n <li class=\"root\">\n <a\n class=\"root-folder";
23
+ stack1 = helpers.unless.call(depth0, (depth0 && depth0.showToggle), {hash:{},inverse:self.noop,fn:self.program(2, program2, data),data:data});
24
+ if(stack1 || stack1 === 0) { buffer += stack1; }
25
+ buffer += "\"\n href=\"#\"\n data-path=\"";
23
26
  if (helper = helpers.path) { stack1 = helper.call(depth0, {hash:{},data:data}); }
24
27
  else { helper = (depth0 && depth0.path); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
25
28
  buffer += escapeExpression(stack1)
@@ -31,13 +34,27 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
31
34
  if (helper = helpers.childrenLimit) { stack1 = helper.call(depth0, {hash:{},data:data}); }
32
35
  else { helper = (depth0 && depth0.childrenLimit); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
33
36
  buffer += escapeExpression(stack1)
34
- + "\"\n data-level=\"0\"\n style=\"--tree-level: 0;\">\n ";
37
+ + "\"\n data-level=\"0\"\n style=\"--tree-level: 0;\">\n <span class=\"tree-toggle";
38
+ stack1 = helpers['if'].call(depth0, (depth0 && depth0.showToggle), {hash:{},inverse:self.noop,fn:self.program(4, program4, data),data:data});
39
+ if(stack1 || stack1 === 0) { buffer += stack1; }
40
+ buffer += "\" aria-hidden=\"true\"></span>\n <span class=\"tree-folder icon-folder\" aria-hidden=\"true\"></span>\n ";
35
41
  if (helper = helpers.label) { stack1 = helper.call(depth0, {hash:{},data:data}); }
36
42
  else { helper = (depth0 && depth0.label); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
37
43
  buffer += escapeExpression(stack1)
38
44
  + "\n </a>\n <ul></ul>\n </li>\n";
39
45
  return buffer;
40
46
  }
47
+ function program2(depth0,data) {
48
+
49
+
50
+ return " is-leaf";
51
+ }
52
+
53
+ function program4(depth0,data) {
54
+
55
+
56
+ return " icon-right";
57
+ }
41
58
 
42
59
  stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.read), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
43
60
  if(stack1 || stack1 === 0) { buffer += stack1; }
@@ -81,13 +98,30 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
81
98
  if (helper = helpers.level) { stack1 = helper.call(depth0, {hash:{},data:data}); }
82
99
  else { helper = (depth0 && depth0.level); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
83
100
  buffer += escapeExpression(stack1)
84
- + ";\"\n href=\"#\">\n ";
101
+ + ";\"\n href=\"#\"\n ";
102
+ stack1 = helpers.unless.call(depth0, (depth0 && depth0.showToggle), {hash:{},inverse:self.noop,fn:self.program(2, program2, data),data:data});
103
+ if(stack1 || stack1 === 0) { buffer += stack1; }
104
+ buffer += ">\n <span class=\"tree-toggle";
105
+ stack1 = helpers['if'].call(depth0, (depth0 && depth0.showToggle), {hash:{},inverse:self.noop,fn:self.program(4, program4, data),data:data});
106
+ if(stack1 || stack1 === 0) { buffer += stack1; }
107
+ buffer += "\" aria-hidden=\"true\"></span>\n <span class=\"tree-folder icon-folder\" aria-hidden=\"true\"></span>\n ";
85
108
  if (helper = helpers.label) { stack1 = helper.call(depth0, {hash:{},data:data}); }
86
109
  else { helper = (depth0 && depth0.label); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
87
110
  buffer += escapeExpression(stack1)
88
111
  + "\n </a>\n </li>\n";
89
112
  return buffer;
90
113
  }
114
+ function program2(depth0,data) {
115
+
116
+
117
+ return "class=\"is-leaf\"";
118
+ }
119
+
120
+ function program4(depth0,data) {
121
+
122
+
123
+ return " icon-right";
124
+ }
91
125
 
92
126
  stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.permissions)),stack1 == null || stack1 === false ? stack1 : stack1.read), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
93
127
  if(stack1 || stack1 === 0) { buffer += stack1; }
@@ -164,18 +198,25 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
164
198
  *
165
199
  * You should have received a copy of the GNU General Public License
166
200
  * along with this program; if not, write to the Free Software
167
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
201
+ * Foundation, Inc., 31 Milk St # 960789 Boston, MA 02196 USA.
168
202
  *
169
- * Copyright (c) 2018-2020 (original work) Open Assessment Technologies SA;
203
+ * Copyright (c) 2018-2026 (original work) Open Assessment Technologies SA;
170
204
  */
171
- const ns = 'resourcemgr';
205
+ const NS = 'resourcemgr';
206
+ const LOGGER = loggerFactory(`ui/${NS}`);
207
+ const DEFAULT_AJAX_TIMEOUT_MS = 10000;
172
208
  function fileBrowser (options) {
209
+ if (!options.browseUrl && options.url) {
210
+ options.browseUrl = options.url;
211
+ }
173
212
  const root = options.root || 'local';
174
213
  const rootPath = options.path || '/';
214
+ const initialPath = options.initialPath || rootPath;
175
215
  const $container = options.$target;
176
216
  const $fileBrowser = $$1('.file-browser .file-browser-wrapper', $container);
177
217
  const $divContainer = $$1(`.${root}`, $fileBrowser);
178
218
  const $folderContainer = $$1('.folders', $divContainer);
219
+ const $paginationContainer = $$1('.pagination-bottom', $container);
179
220
  const fileTree = {};
180
221
  // for pagination
181
222
  let selectedClass = {
@@ -184,25 +225,81 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
184
225
  total: 0,
185
226
  page: 1
186
227
  };
228
+ let searchMode = false;
229
+ let sort = Object.assign({}, assetSearchContract.DEFAULT_SORT);
230
+ const ajaxTimeoutMs = Number.isFinite(Number(options.ajaxTimeoutMs)) && Number(options.ajaxTimeoutMs) > 0 ? Number(options.ajaxTimeoutMs) : DEFAULT_AJAX_TIMEOUT_MS;
231
+ $container.on(`searchmode.${NS}`, function (e, enabled) {
232
+ searchMode = !!enabled;
233
+ });
234
+ $container.on(`sortchange.${NS}`, function (e, nextSort) {
235
+ sort = Object.assign({}, assetSearchContract.DEFAULT_SORT, nextSort || {});
236
+ selectedClass.page = 1;
237
+ invalidateFolderFiles(selectedClass.path);
238
+ if (searchMode || !isActiveBrowser()) {
239
+ return;
240
+ }
241
+ reloadSortedFolder();
242
+ });
243
+ $container.on(`searchclear.${NS}`, function (e, path) {
244
+ const targetPath = path || selectedClass.path;
245
+ selectedClass.page = 1;
246
+ invalidateFolderFiles(targetPath);
247
+ if (!isActiveBrowser()) {
248
+ return;
249
+ }
250
+ const subTree = getByExactPath(fileTree, targetPath) || getByPath(fileTree, targetPath) || fileTree;
251
+ getFolderContent(subTree, targetPath, function (content) {
252
+ if (content) {
253
+ selectFolder(content, targetPath);
254
+ }
255
+ });
256
+ });
257
+
258
+ // Reopen with resolved parent (AC3 edit/change): leave search, open folder again.
259
+ $container.on(`applycontext.${NS}`, function (e, ctx) {
260
+ const path = ctx && ctx.path || rootPath;
261
+ const pathIsTaomedia = path && String(path).indexOf('taomedia://') === 0;
262
+ if (pathIsTaomedia && root === 'local') {
263
+ return;
264
+ }
265
+ if (!pathIsTaomedia && root !== 'local') {
266
+ return;
267
+ }
268
+ $container.data('activeFileBrowserRoot', root);
269
+ if (searchMode) {
270
+ $container.trigger(`requestexitsearch.${NS}`);
271
+ }
272
+ selectedClass.page = 1;
273
+ openInitialPath(path);
274
+ });
187
275
 
188
276
  //load the content of the ROOT
189
277
  getFolderContent(fileTree, rootPath, function (content) {
278
+ if (!content) {
279
+ return;
280
+ }
190
281
  indexTree(content);
191
282
 
192
283
  //create the tree node for the ROOT folder by default once the initial content loaded
193
- $folderContainer.append(rootFolderTpl(content));
284
+ $folderContainer.append(rootFolderTpl(Object.assign({}, content, {
285
+ showToggle: hasNestedFolderChildren(content) !== false
286
+ })));
194
287
  const $rootNode = $$1('.root-folder', $folderContainer);
195
288
  //create an inner list and append found elements
196
289
  const $innerList = $$1('.root ul', $folderContainer);
197
- if (content.children) {
290
+ if (hasNestedFolderChildren(content)) {
198
291
  $rootNode.addClass('opened');
292
+ } else if (hasNestedFolderChildren(content) === false) {
293
+ setFolderToggleState($rootNode, false);
199
294
  }
200
295
  updateFolders(content, $innerList);
201
- if (content.permissions.read && !options.hasAlreadySelected) {
202
- $$1('.file-browser').find('li.active').removeClass('active');
203
- updateSelectedClass(content.path, content.total, content.childrenLimit);
204
- $container.trigger('folderselect.'.concat(ns), [content.label, getPage(content.children), content.path, content]);
205
- renderPagination();
296
+ if (content.permissions && content.permissions.read && !options.hasAlreadySelected) {
297
+ if (initialPath && initialPath !== rootPath) {
298
+ openInitialPath(initialPath);
299
+ } else {
300
+ selectFolder(content, content.path);
301
+ syncTreeActiveFolder(content.path || rootPath);
302
+ }
206
303
  if (root !== 'local') {
207
304
  options.hasAlreadySelected = true;
208
305
  }
@@ -213,9 +310,8 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
213
310
  $divContainer.off('click', '.folders a').on('click', '.folders a', function (e) {
214
311
  e.preventDefault();
215
312
  const $selected = $$1(this);
216
- const $folders = $$1('.folders li', $fileBrowser);
217
313
  const fullPath = $selected.data('path');
218
- const subTree = getByPath(fileTree, fullPath);
314
+ const subTree = getByExactPath(fileTree, fullPath);
219
315
 
220
316
  //get the folder content
221
317
  getFolderContent(subTree, fullPath, function (content) {
@@ -223,10 +319,12 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
223
319
  if (content) {
224
320
  //either create the inner list of the content is new or just show it
225
321
  let $innerList = $selected.siblings('ul');
226
- if (!$innerList.length && content.children && _.find(content.children, 'path') && !content.empty) {
322
+ const nested = hasNestedFolderChildren(content);
323
+ if (!$innerList.length && nested) {
227
324
  $innerList = $$1('<ul></ul>').insertAfter($selected);
228
325
  updateFolders(content, $innerList);
229
326
  $selected.addClass('opened');
327
+ setFolderToggleState($selected, true);
230
328
  } else if ($innerList.length) {
231
329
  if ($innerList.css('display') === 'none') {
232
330
  $innerList.show();
@@ -235,62 +333,185 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
235
333
  $innerList.hide();
236
334
  $selected.removeClass('opened');
237
335
  }
336
+ } else if (nested === false) {
337
+ // Leaf folder: keep alignment spacer, hide expand chevron.
338
+ setFolderToggleState($selected, false);
238
339
  }
239
-
240
- //toggle active element
241
- $folders.removeClass('active');
242
- $selected.parent('li').addClass('active');
340
+ syncTreeActiveFolder(fullPath, $selected);
243
341
 
244
342
  //internal event to set the file-selector content
245
- updateSelectedClass(fullPath, content.total, content.childrenLimit);
246
- $container.trigger(`folderselect.${ns}`, [content.label, getPage(content.children), content.path, content]);
247
- renderPagination();
343
+ selectFolder(content, fullPath);
248
344
  }
249
345
  });
250
346
  });
251
- $container.on(`filenew.${ns}`, function (e, file, path) {
252
- const subTree = getByPath(fileTree, path);
253
- if (subTree) {
254
- if (!subTree.children) {
255
- subTree.children = [];
347
+ $container.on(`filenew.${NS}`, function (e, file, path) {
348
+ const targetPath = path || selectedClass.path;
349
+ if (!targetPath || searchMode) {
350
+ return;
351
+ }
352
+ const activeRoot = $container.data('activeFileBrowserRoot');
353
+ if (activeRoot && activeRoot !== root) {
354
+ return;
355
+ }
356
+ const subTree = getByExactPath(fileTree, targetPath) || getByPath(fileTree, targetPath);
357
+ if (subTree && root === 'local' && file && file.name && _.find(subTree.children, {
358
+ name: file.name
359
+ })) {
360
+ return;
361
+ }
362
+ if (targetPath !== selectedClass.path) {
363
+ selectedClass.path = targetPath;
364
+ selectedClass.page = 1;
365
+ }
366
+ invalidateFolderFiles(targetPath);
367
+ reloadSortedFolder();
368
+ });
369
+ $container.on(`filedelete.${NS}`, function (e, path) {
370
+ if (searchMode) {
371
+ return;
372
+ }
373
+ const activeRoot = $container.data('activeFileBrowserRoot');
374
+ if (activeRoot && activeRoot !== root) {
375
+ return;
376
+ }
377
+ if (path) {
378
+ removeFromPath(fileTree, path);
379
+ }
380
+ invalidateFolderFiles(selectedClass.path);
381
+ reloadSortedFolder();
382
+ });
383
+
384
+ /**
385
+ * Open and select an initial folder path after the root tree is available.
386
+ * @param {String} path
387
+ */
388
+ function findFolderLink(path) {
389
+ return $folderContainer.find('a').filter(function () {
390
+ return $$1(this).data('path') === path;
391
+ });
392
+ }
393
+
394
+ /**
395
+ * Render directory children under an expanded folder anchor (replace / reopen).
396
+ * @param {String} parentPath
397
+ * @param {Object} parentContent
398
+ */
399
+ function mountFolderBranch(parentPath, parentContent) {
400
+ const $parentLink = findFolderLink(parentPath);
401
+ if (!$parentLink.length || !parentContent) {
402
+ return;
403
+ }
404
+ let $innerList = $parentLink.siblings('ul');
405
+ if (!$innerList.length) {
406
+ $innerList = $$1('<ul></ul>').insertAfter($parentLink);
407
+ }
408
+ updateFolders(parentContent, $innerList);
409
+ $parentLink.addClass('opened');
410
+ if ($innerList.css('display') === 'none') {
411
+ $innerList.show();
412
+ }
413
+ setFolderToggleState($parentLink, hasNestedFolderChildren(parentContent) !== false);
414
+ }
415
+ function markFolderActive(path) {
416
+ const $targetLink = findFolderLink(path);
417
+ if ($targetLink.length) {
418
+ $targetLink.parents('li').each(function () {
419
+ const $li = $$1(this);
420
+ const $anchor = $li.children('a');
421
+ const $list = $li.children('ul');
422
+ $anchor.addClass('opened');
423
+ if ($list.length) {
424
+ $list.show();
425
+ }
426
+ });
427
+ syncTreeActiveFolder(path, $targetLink);
428
+ } else if (path === rootPath) {
429
+ syncTreeActiveFolder(rootPath);
430
+ }
431
+ }
432
+ function openInitialPath(path) {
433
+ getFolderContent(fileTree, path, function (content) {
434
+ indexTree(fileTree);
435
+ if (!content) {
436
+ const rootContent = getByPath(fileTree, rootPath) || fileTree;
437
+ const fallbackPath = rootContent.path || rootPath;
438
+ selectFolder(rootContent, fallbackPath);
439
+ syncTreeActiveFolder(fallbackPath);
440
+ return;
441
+ }
442
+ function finishOpen() {
443
+ markFolderActive(path);
444
+ selectFolder(content, path);
445
+ }
446
+ if (findFolderLink(path).length) {
447
+ finishOpen();
448
+ return;
256
449
  }
257
- if (root !== 'local' || !_.find(subTree.children, {
258
- name: file.name
259
- })) {
260
- updatePermissions(file);
261
- const childrenFilesOnly = _.filter(subTree.children, function (child) {
262
- // Only file object has link property
263
- return Object.prototype.hasOwnProperty.call(child, 'link');
450
+ const parentFolderPath = content.parentFolderPath;
451
+ if (parentFolderPath && parentFolderPath !== path) {
452
+ getFolderContent(fileTree, parentFolderPath, function (parentContent) {
453
+ mountFolderBranch(parentFolderPath, parentContent);
454
+ finishOpen();
264
455
  });
265
- if (childrenFilesOnly.length === subTree.total) {
266
- // all children loaded new file can be pushed to the end of tree
267
- // if not all, new file will be loaded with next page
268
- subTree.children.push(file);
269
- }
270
- subTree.total = Number.isFinite(Number(subTree.total)) ? Number(subTree.total) + 1 : 1;
271
- if (selectedClass.path === path) {
272
- selectedClass.total = subTree.total;
273
- }
274
- $container.trigger(`folderselect.${ns}`, [subTree.label, getPage(subTree.children), path, subTree]);
275
- renderPagination();
456
+ return;
276
457
  }
458
+ finishOpen();
459
+ });
460
+ }
461
+
462
+ /**
463
+ * Mark the folder row that matches path as active (open folder icon for leaves).
464
+ * @param {String} path
465
+ * @param {jQuery} [$link]
466
+ */
467
+ function syncTreeActiveFolder(path, $link) {
468
+ $$1('.folders li', $fileBrowser).removeClass('active');
469
+ const $target = $link && $link.length ? $link : $folderContainer.find('a').filter(function () {
470
+ return $$1(this).data('path') === path;
471
+ });
472
+ if ($target.length) {
473
+ $target.parent('li').addClass('active');
277
474
  }
278
- });
279
- $container.on(`filedelete.${ns}`, function (e, path) {
280
- if (removeFromPath(fileTree, path)) {
281
- selectedClass.total--;
282
- loadPage();
475
+ }
476
+
477
+ /**
478
+ * Whether this media source currently owns the file table.
479
+ * @returns {Boolean}
480
+ */
481
+ function isActiveBrowser() {
482
+ return $container.data('activeFileBrowserRoot') === root;
483
+ }
484
+
485
+ /**
486
+ * Select a folder and publish its page of files to the selector.
487
+ * @param {Object} content
488
+ * @param {String} path
489
+ */
490
+ function selectFolder(content, path) {
491
+ if (!content) {
492
+ return;
283
493
  }
284
- });
494
+ $container.data('activeFileBrowserRoot', root);
495
+ if (searchMode) {
496
+ $container.trigger(`folderpath.${NS}`, [path, content.label]);
497
+ $container.trigger(`folderselect.${NS}`, [content.label, getPage(content.children || []), path, content]);
498
+ return;
499
+ }
500
+ updateSelectedClass(path, content.total, content.childrenLimit);
501
+ $container.trigger(`folderpath.${NS}`, [path, content.label]);
502
+ $container.trigger(`folderselect.${NS}`, [content.label, getPage(content.children || []), path, content]);
503
+ renderPagination();
504
+ }
505
+
285
506
  /**
286
507
  * Get files for page
287
508
  * @param {Array} children
288
509
  * @returns {Array} files for this page
289
510
  */
290
511
  function getPage(children) {
291
- const files = _.filter(children, function (item) {
512
+ const files = assetSearchContract.sortAssetItems(_.filter(children, function (item) {
292
513
  return !!item.uri;
293
- });
514
+ }), sort);
294
515
  if (selectedClass.childrenLimit) {
295
516
  return files.slice((selectedClass.page - 1) * selectedClass.childrenLimit, selectedClass.page * selectedClass.childrenLimit);
296
517
  }
@@ -302,6 +523,12 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
302
523
  * @param {String} path - the folder path (relative to the root)
303
524
  * @param {Function} cb - called back with the content in 1st parameter
304
525
  */
526
+ function markFolderEmptyAtPath(tree, path) {
527
+ const node = getByExactPath(tree, path);
528
+ if (node) {
529
+ node.empty = true;
530
+ }
531
+ }
305
532
  function getFolderContent(tree, path, cb) {
306
533
  let content = getByPath(tree, path);
307
534
  if (!content || !content.children && !content.empty) {
@@ -309,32 +536,47 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
309
536
  if (!tree.path) {
310
537
  tree = _.merge(tree, data);
311
538
  } else if (data.children) {
539
+ setToPath(tree, path, data);
312
540
  if (!_.find(data.children, 'path')) {
313
541
  // no subfolders inside folder
314
- tree.empty = true;
542
+ markFolderEmptyAtPath(tree, path);
315
543
  }
316
- setToPath(tree, path, data);
317
544
  } else {
318
- tree.empty = true;
545
+ setToPath(tree, path, data);
546
+ markFolderEmptyAtPath(tree, path);
319
547
  }
320
548
  cb(data);
549
+ }).catch(function () {
550
+ cb(null);
321
551
  });
322
552
  } else if (content.children) {
323
553
  const files = _.filter(content.children, function (item) {
324
554
  return !!item.uri;
325
555
  });
326
- // if files less then total and need toload this page
327
- if (files.length < selectedClass.total && files.length < selectedClass.page * selectedClass.childrenLimit) {
556
+ // Use folder total (not selectedClass): openInitialPath runs before selectFolder.
557
+ const expectedTotal = Number(content.total);
558
+ const pageSize = Number(content.childrenLimit) || selectedClass.childrenLimit || 10;
559
+ const page = selectedClass.page || 1;
560
+ // Missing/NaN total → unknown size; refetch so invalidated cache is not treated as complete.
561
+ if (!Number.isFinite(expectedTotal) || files.length < expectedTotal && files.length < page * pageSize) {
328
562
  loadContent(path).then(function (data) {
329
563
  const loadedFiles = _.filter(data.children, function (item) {
330
564
  return !!item.uri;
331
565
  });
566
+ const node = getByExactPath(tree, path);
567
+ if (node && !Number.isFinite(expectedTotal)) {
568
+ node.children = _.filter(node.children, function (item) {
569
+ return !item.uri;
570
+ });
571
+ }
332
572
  setToPath(tree, path, {
333
573
  children: loadedFiles,
334
574
  total: data.total,
335
575
  childrenLimit: data.childrenLimit
336
576
  });
337
- content = getByPath(tree, path);
577
+ content = getByExactPath(tree, path) || getByPath(tree, path);
578
+ cb(content);
579
+ }).catch(function () {
338
580
  cb(content);
339
581
  });
340
582
  } else {
@@ -384,6 +626,29 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
384
626
  return match;
385
627
  }
386
628
 
629
+ /**
630
+ * Get a subtree by exact path equality.
631
+ * @param {Object} tree
632
+ * @param {String} path
633
+ * @returns {Object|undefined}
634
+ */
635
+ function getByExactPath(tree, path) {
636
+ let match;
637
+ if (tree) {
638
+ if (tree.path === path) {
639
+ match = tree;
640
+ } else if (tree.children) {
641
+ _.forEach(tree.children, function (child) {
642
+ match = getByExactPath(child, path);
643
+ if (match) {
644
+ return false;
645
+ }
646
+ });
647
+ }
648
+ }
649
+ return match;
650
+ }
651
+
387
652
  /**
388
653
  * Merge data into at into the subtree
389
654
  * @param {Object} tree - the tree model
@@ -427,8 +692,11 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
427
692
  return child.path === path || child.name && tree.path + child.name === path || child.uri === path;
428
693
  });
429
694
  done = removed.length > 0;
430
- tree.total--;
431
- if (!done) {
695
+ if (done) {
696
+ if (Number.isFinite(Number(tree.total))) {
697
+ tree.total = Math.max(0, Number(tree.total) - removed.length);
698
+ }
699
+ } else {
432
700
  _.forEach(tree.children, function (child) {
433
701
  done = removeFromPath(child, path);
434
702
  if (done) {
@@ -440,31 +708,116 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
440
708
  return done;
441
709
  }
442
710
 
711
+ /**
712
+ * Drop cached file rows for a folder so the next load hits the service
713
+ * with the current sort. Nested folder nodes are kept.
714
+ * @param {String} path
715
+ */
716
+ function invalidateFolderFiles(path) {
717
+ const content = getByExactPath(fileTree, path);
718
+ if (content && Array.isArray(content.children)) {
719
+ content.children = content.children.filter(function (child) {
720
+ return child.path && !child.uri;
721
+ });
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Replace a folder node with a freshly loaded payload (files + folders).
727
+ * @param {String} path
728
+ * @param {Object} data
729
+ */
730
+ function replaceFolderContent(path, data) {
731
+ const content = getByExactPath(fileTree, path);
732
+ if (!content || !data) {
733
+ return;
734
+ }
735
+ if (Object.prototype.hasOwnProperty.call(data, 'children')) {
736
+ content.children = data.children;
737
+ } else {
738
+ content.children = [];
739
+ content.empty = true;
740
+ content.total = 0;
741
+ }
742
+ if (Object.prototype.hasOwnProperty.call(data, 'total')) {
743
+ content.total = data.total;
744
+ }
745
+ if (Object.prototype.hasOwnProperty.call(data, 'childrenLimit')) {
746
+ content.childrenLimit = data.childrenLimit;
747
+ }
748
+ if (data.label) {
749
+ content.label = data.label;
750
+ }
751
+ if (data.path) {
752
+ content.path = data.path;
753
+ }
754
+ }
755
+
443
756
  /**
444
757
  * Get the content of a folder
445
758
  * @param {String} path - the folder path
446
- * @returns {jQuery.Deferred} the defferred object to run done/complete/fail
759
+ * @returns {Promise} resolves with folder content
447
760
  */
448
761
  function loadContent(path) {
449
762
  const parameters = {};
450
- parameters[options.pathParam] = path;
451
- return request({
763
+ parameters[options.pathParam || 'path'] = path;
764
+ return Promise.resolve($$1.ajax({
452
765
  url: options.browseUrl,
453
766
  method: 'GET',
454
767
  dataType: 'json',
768
+ timeout: ajaxTimeoutMs,
455
769
  data: _.merge(parameters, options.params, {
456
- childrenOffset: (selectedClass.page - 1) * selectedClass.childrenLimit
457
- }),
458
- noToken: true
459
- }).then(response => response.data).then(response => {
460
- response = updatePermissions(response);
461
- if (response.children && response.children.length > 0) {
462
- response.children.map(responseChildren => updatePermissions(responseChildren));
770
+ // depth=2 so each rendered child folder includes its own dir children,
771
+ // allowing leaf folders to hide the expand chevron without an extra click.
772
+ depth: 2,
773
+ childrenOffset: (selectedClass.page - 1) * selectedClass.childrenLimit,
774
+ sortBy: sort.field,
775
+ sortDir: sort.direction
776
+ })
777
+ })).then(function (response) {
778
+ if (response && response.success === false) {
779
+ return Promise.reject(response);
463
780
  }
464
- return response;
781
+ let data = response && response.data ? response.data : response;
782
+ data = updatePermissions(data);
783
+ if (data.children && data.children.length > 0) {
784
+ data.children.map(responseChildren => updatePermissions(responseChildren));
785
+ }
786
+ return data;
465
787
  });
466
788
  }
467
789
 
790
+ /**
791
+ * Whether a folder node has nested directory children.
792
+ * @param {Object} node
793
+ * @returns {boolean|null} true/false when known; null when children were not loaded yet
794
+ */
795
+ function hasNestedFolderChildren(node) {
796
+ if (!node || node.empty === true) {
797
+ return false;
798
+ }
799
+ if (!Array.isArray(node.children)) {
800
+ return null;
801
+ }
802
+ return Boolean(_.find(node.children, 'path'));
803
+ }
804
+
805
+ /**
806
+ * Show or hide the expand chevron on a folder anchor (keep spacer width).
807
+ * @param {jQuery} $anchor
808
+ * @param {boolean} hasNested
809
+ */
810
+ function setFolderToggleState($anchor, hasNested) {
811
+ const $toggle = $anchor.children('.tree-toggle');
812
+ if (hasNested) {
813
+ $toggle.addClass('icon-right').removeClass('is-leaf');
814
+ $anchor.removeClass('is-leaf');
815
+ } else {
816
+ $toggle.removeClass('icon-right').addClass('is-leaf');
817
+ $anchor.removeClass('opened').addClass('is-leaf');
818
+ }
819
+ }
820
+
468
821
  /**
469
822
  * Update the HTML Tree
470
823
  * @param {Object} data - the tree data
@@ -476,7 +829,10 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
476
829
  if (typeof data.relPath === 'undefined') {
477
830
  data.relPath = data.path;
478
831
  }
832
+ // Unknown nested state (lazy depth) keeps the chevron until the folder is opened.
833
+ data.showToggle = hasNestedFolderChildren(data) !== false;
479
834
  $parent.append(folderTpl(data));
835
+ return;
480
836
  }
481
837
  if (data && data.children && _.isArray(data.children) && !data.empty) {
482
838
  _.forEach(data.children, function (child) {
@@ -505,7 +861,11 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
505
861
  * Render pagination
506
862
  */
507
863
  function renderPagination() {
508
- const $paginationContainer = $$1('.pagination-bottom', $container);
864
+ if (searchMode) {
865
+ // Drop browse pagination controls/handlers when search owns the table.
866
+ $paginationContainer.empty();
867
+ return;
868
+ }
509
869
  const total = Number(selectedClass.total);
510
870
  const childrenLimit = Number(selectedClass.childrenLimit);
511
871
  $paginationContainer.empty();
@@ -527,18 +887,35 @@ define(['jquery', 'lodash', 'core/request', 'ui/pagination', 'handlebars', 'lib/
527
887
  }).render($paginationContainer);
528
888
  }
529
889
  }
890
+ /**
891
+ * Re-fetch the current folder with the active sort and publish files.
892
+ */
893
+ function reloadSortedFolder() {
894
+ const path = selectedClass.path;
895
+ loadContent(path).then(function (data) {
896
+ if (!data) {
897
+ return;
898
+ }
899
+ replaceFolderContent(path, data);
900
+ const content = getByExactPath(fileTree, path) || data;
901
+ selectFolder(content, content.path || path);
902
+ }).catch(function (error) {
903
+ LOGGER.error(error);
904
+ });
905
+ }
906
+
530
907
  /**
531
908
  * Load page
532
909
  */
533
910
  function loadPage() {
534
- const subTree = getByPath(fileTree, selectedClass.path);
911
+ const subTree = getByPath(fileTree, selectedClass.path) || fileTree;
535
912
 
536
913
  //get the folder content
537
914
  getFolderContent(subTree, selectedClass.path, function (content) {
538
915
  indexTree(fileTree);
539
916
  if (content) {
540
917
  //internal event to set the file-selector content
541
- $container.trigger(`folderselect.${ns}`, [content.label, getPage(content.children), content.path]);
918
+ $container.trigger(`folderselect.${NS}`, [content.label, getPage(content.children), content.path, content]);
542
919
  }
543
920
  });
544
921
  }