@oat-sa/tao-core-ui 3.23.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 (31) 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/previewer.js +1 -1
  5. package/dist/resourcemgr/assetSearch.js +793 -0
  6. package/dist/resourcemgr/assetSearchContract.js +477 -0
  7. package/dist/resourcemgr/css/resourcemgr.css +1137 -97
  8. package/dist/resourcemgr/css/resourcemgr.css.map +1 -1
  9. package/dist/resourcemgr/fileBrowser.js +456 -79
  10. package/dist/resourcemgr/filePreview.js +40 -4
  11. package/dist/resourcemgr/fileSelector.js +362 -92
  12. package/dist/resourcemgr.js +309 -57
  13. package/dist/searchModal/advancedSearch.js +233 -31
  14. package/package.json +15 -6
  15. package/src/image/ImgStateActive/initHelper.js +69 -31
  16. package/src/previewer.js +1 -1
  17. package/src/resourcemgr/assetSearch.js +847 -0
  18. package/src/resourcemgr/assetSearchContract.js +489 -0
  19. package/src/resourcemgr/css/resourcemgr.css +1137 -97
  20. package/src/resourcemgr/css/resourcemgr.css.map +1 -1
  21. package/src/resourcemgr/fileBrowser.js +470 -91
  22. package/src/resourcemgr/filePreview.js +46 -4
  23. package/src/resourcemgr/fileSelector.js +338 -46
  24. package/src/resourcemgr/scss/resourcemgr.scss +1351 -88
  25. package/src/resourcemgr/tpl/advanced-search.tpl +10 -0
  26. package/src/resourcemgr/tpl/fileSelect.tpl +25 -28
  27. package/src/resourcemgr/tpl/folder.tpl +4 -1
  28. package/src/resourcemgr/tpl/layout.tpl +113 -54
  29. package/src/resourcemgr/tpl/rootFolder.tpl +3 -1
  30. package/src/resourcemgr.js +289 -47
  31. package/src/searchModal/advancedSearch.js +242 -31
@@ -1,3 +1,20 @@
1
+ /**
2
+ * This program is free software; you can redistribute it and/or
3
+ * modify it under the terms of the GNU General Public License
4
+ * as published by the Free Software Foundation; under version 2
5
+ * of the License (non-upgradable).
6
+ *
7
+ * This program is distributed in the hope that it will be useful,
8
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ * GNU General Public License for more details.
11
+ *
12
+ * You should have received a copy of the GNU General Public License
13
+ * along with this program; if not, write to the Free Software
14
+ * Foundation, Inc., 31 Milk St # 960789 Boston, MA 02196 USA.
15
+ *
16
+ * Copyright (c) 2018-2026 (original work) Open Assessment Technologies SA;
17
+ */
1
18
  import $ from 'jquery';
2
19
  import _ from 'lodash';
3
20
  import bytes from 'util/bytes';
@@ -5,6 +22,7 @@ import context from 'context';
5
22
  import 'ui/previewer';
6
23
 
7
24
  const ns = 'resourcemgr';
25
+ const EMPTY_META = '-';
8
26
 
9
27
  export default function(options) {
10
28
  const $container = options.$target;
@@ -43,11 +61,35 @@ export default function(options) {
43
61
  $container.trigger(`select.${ns}`, [[data]]);
44
62
  });
45
63
 
64
+ /**
65
+ * @param {*} value
66
+ * @returns {string}
67
+ */
68
+ function metaOrDash(value) {
69
+ if (value === null || typeof value === 'undefined' || value === '') {
70
+ return EMPTY_META;
71
+ }
72
+ return String(value);
73
+ }
74
+
46
75
  function startPreview(file, preview, download, select) {
47
76
  if (preview) {
48
77
  $previewer.previewer(file);
49
- $propType.text(`${file.type} (${file.mime})`);
50
- $propSize.text(bytes.hrSize(file.size));
78
+ if (file.type || file.mime) {
79
+ $propType.text(
80
+ file.type && file.mime ? `${file.type} (${file.mime})` : metaOrDash(file.type || file.mime)
81
+ );
82
+ } else {
83
+ $propType.text(EMPTY_META);
84
+ }
85
+ $propSize.text(
86
+ file.size === null || typeof file.size === 'undefined' || file.size === ''
87
+ ? EMPTY_META
88
+ : bytes.hrSize(file.size)
89
+ );
90
+ } else {
91
+ $propType.text(EMPTY_META);
92
+ $propSize.text(EMPTY_META);
51
93
  }
52
94
  if(download) {
53
95
  $link.attr('href', file.download).attr('download', file.file);
@@ -65,8 +107,8 @@ export default function(options) {
65
107
 
66
108
  function stopPreview() {
67
109
  $previewer.previewer('update', { url: false });
68
- $propType.empty();
69
- $propSize.empty();
110
+ $propType.text(EMPTY_META);
111
+ $propSize.text(EMPTY_META);
70
112
  $('a', $propUrl).addClass('hidden');
71
113
  $selectButton.attr('disabled', 'disabled');
72
114
  }
@@ -11,9 +11,9 @@
11
11
  *
12
12
  * You should have received a copy of the GNU General Public License
13
13
  * along with this program; if not, write to the Free Software
14
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
14
+ * Foundation, Inc., 31 Milk St # 960789 Boston, MA 02196 USA.
15
15
  *
16
- * Copyright (c) 2015-2024 (original work) Open Assessment Technologies SA;
16
+ * Copyright (c) 2015-2026 (original work) Open Assessment Technologies SA;
17
17
  *
18
18
  */
19
19
 
@@ -32,9 +32,11 @@ import context from 'context';
32
32
  import 'ui/uploader';
33
33
  import updatePermissions from './util/updatePermissions';
34
34
  import loggerFactory from 'core/logger';
35
+ import { DEFAULT_SORT, sortAssetItems } from 'ui/resourcemgr/assetSearchContract';
35
36
 
36
37
  const ns = 'resourcemgr';
37
38
  const logger = loggerFactory(`ui/${ns}`);
39
+ const ROW_SELECTOR = '.files-list tr';
38
40
 
39
41
  function shortenPath(path) {
40
42
  let tokens = path.replace(/\/$/, '').split('/');
@@ -47,6 +49,70 @@ function shortenPath(path) {
47
49
  return title.join('/');
48
50
  }
49
51
 
52
+ /**
53
+ * Normalize a catalog folder path for the Location column.
54
+ * Strips URI schemes and media-root labels (Assets/Media); keeps directory path only.
55
+ * @param {String} location
56
+ * @returns {String}
57
+ */
58
+ function formatLocationDisplay(location) {
59
+ if (location === null || typeof location === 'undefined' || location === '') {
60
+ return '';
61
+ }
62
+ let path = String(location).trim().replace(/\\/g, '/');
63
+ if (!path) {
64
+ return '';
65
+ }
66
+
67
+ // Drop URI schemes (taomedia://…, https://…, asset://…)
68
+ path = path.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
69
+ // Residual host/path from http(s) after scheme strip
70
+ if (path.indexOf('/') > 0 && /^[^/]+\.[^/]+\//.test(path)) {
71
+ path = path.slice(path.indexOf('/'));
72
+ }
73
+
74
+ const segments = path.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
75
+ if (!segments.length) {
76
+ return '/';
77
+ }
78
+
79
+ // Directory path only: drop trailing filename-like segment
80
+ const last = segments[segments.length - 1];
81
+ if (segments.length > 1 && /\.[a-z0-9]{1,8}$/i.test(last)) {
82
+ segments.pop();
83
+ }
84
+
85
+ // Strip media-tree root label prefix (Assets, Media, …)
86
+ const rootLabels = ['assets', 'media', 'mediamanager'];
87
+ if (segments.length && rootLabels.indexOf(segments[0].toLowerCase()) !== -1) {
88
+ segments.shift();
89
+ }
90
+
91
+ return segments.length ? `/${segments.join('/')}` : '/';
92
+ }
93
+
94
+ /**
95
+ * Format updatedAt as a fixed UTC date-time (YYYY-MM-DD HH:mm).
96
+ * @param {String} value
97
+ * @returns {String}
98
+ */
99
+ function formatUpdatedAtUtc(value) {
100
+ if (!value) {
101
+ return '';
102
+ }
103
+ const date = new Date(value);
104
+ if (Number.isNaN(date.getTime()) || date.getTime() <= 0) {
105
+ return '—';
106
+ }
107
+ const pad = function (n) {
108
+ return String(n).padStart(2, '0');
109
+ };
110
+ return (
111
+ `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ` +
112
+ `${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`
113
+ );
114
+ }
115
+
50
116
  function isTextLarger($element, text) {
51
117
  let $dummy = $element
52
118
  .clone()
@@ -70,12 +136,21 @@ export default function (options) {
70
136
  let disableUpload = options.disableUpload || false;
71
137
  let $container = options.$target;
72
138
  let $fileSelector = $('.file-selector', $container);
73
- let $fileContainer = $('.files', $fileSelector);
139
+ let $filesWrapper = $('.files-wrapper', $fileSelector);
140
+ let $fileContainer = $('.files-list', $fileSelector);
74
141
  let $placeholder = $('.empty', $fileSelector);
75
142
  let $uploader = $('.file-upload-container', $fileSelector);
76
143
  let parentSelector = `#${$container.attr('id')} .file-selector`;
77
144
  let $pathTitle = $fileSelector.find('h1 > .title');
78
- let $browserTitle = $('.file-browser > h1', $container);
145
+ let $browserTitle = $('.file-browser > h1.resources-title', $container);
146
+ if (!$browserTitle.length) {
147
+ $browserTitle = $('.file-browser > h1', $container);
148
+ }
149
+ let searchMode = false;
150
+ let initialSelectionApplied = false;
151
+ let pendingContextSelection = null;
152
+ let sort = Object.assign({}, DEFAULT_SORT);
153
+ let lastFiles = [];
79
154
 
80
155
  //set up the uploader
81
156
  if (disableUpload) {
@@ -84,16 +159,19 @@ export default function (options) {
84
159
  } else {
85
160
  setUpUploader(root);
86
161
  }
162
+
163
+ $container.on(`searchmode.${ns}`, function (e, enabled) {
164
+ searchMode = !!enabled;
165
+ $fileSelector.toggleClass('search-mode', searchMode);
166
+ });
167
+
87
168
  //update current folder
88
169
  $container.on(`folderselect.${ns}`, function (e, fullPath, data, activePath, content) {
89
170
  let files;
90
171
 
91
- data = data.map(function (dataItem) {
92
- if (Array.isArray(dataItem.permissions)) {
93
- updatePermissions(dataItem);
94
- }
95
- return dataItem;
96
- });
172
+ if (searchMode) {
173
+ return;
174
+ }
97
175
 
98
176
  //update title
99
177
  if ($container[0].querySelector('.upload')) {
@@ -108,47 +186,245 @@ export default function (options) {
108
186
 
109
187
  //update content here
110
188
  if (_.isArray(data)) {
111
- files = _.filter(data, function (item) {
189
+ files = _.filter(data.map(ensurePermissions), function (item) {
112
190
  return !!item.uri;
113
191
  }).map(function (file) {
114
- file.type = mimeType.getFileType(file);
115
- if (typeof file.identifier === 'undefined') {
116
- file.display = `${fullPath}/${file.name}`.replace('//', '/');
117
- } else {
118
- file.display = file.identifier + file.name;
192
+ return prepareFileForDisplay(file, fullPath, activePath);
193
+ });
194
+
195
+ updateFiles(files);
196
+ if (pendingContextSelection) {
197
+ const pending = pendingContextSelection;
198
+ initialSelectionApplied = false;
199
+ applyInitialSelection(pending, false);
200
+ if (initialSelectionApplied) {
201
+ pendingContextSelection = null;
119
202
  }
203
+ return;
204
+ }
205
+ applyInitialSelection();
206
+ }
207
+ });
120
208
 
121
- file.viewUrl = `${options.downloadUrl}?${$.param(options.params)}&${
122
- options.pathParam
123
- }=${encodeURIComponent(file.uri)}`;
124
- file.downloadUrl = `${file.viewUrl}&svgzsupport=true`;
125
- return file;
209
+ // Render scoped search results from the service as-is (no client MIME/auth filtering).
210
+ $container.on(`searchresults.${ns}`, function (e, result) {
211
+ const items = (result && result.items) || [];
212
+ const files = items
213
+ .filter(function (item) {
214
+ return item && !!item.uri;
215
+ })
216
+ .map(function (file) {
217
+ return prepareFileForDisplay(ensureSearchPermissions(file), result.path || '', result.path);
126
218
  });
127
219
 
128
- updateFiles(fullPath, files);
220
+ $pathTitle.text(__('Search results'));
221
+ updateFiles(files, {
222
+ emptyMessage: result && result.emptyMessage
223
+ });
129
224
 
130
- if (activePath) {
131
- $(`li[data-file="${activePath}"]`).trigger('click');
132
- }
225
+ if (!(result && result.error)) {
226
+ applyInitialSelection(result && result.initialSelection);
133
227
  }
134
228
  });
135
229
 
230
+ // Reopen resolve (AC3): defer preselect until folderselect loads the resolved parent folder.
231
+ $container.on(`applycontext.${ns}`, function (e, ctx) {
232
+ initialSelectionApplied = false;
233
+ options.initialSelection = ctx && ctx.selection ? ctx.selection : null;
234
+ options.currentAssetItem = ctx && ctx.currentAssetItem ? ctx.currentAssetItem : null;
235
+ pendingContextSelection = options.initialSelection || null;
236
+ $fileContainer.find('tr.active').removeClass('active');
237
+ });
238
+
136
239
  function injectTranscriptionMetadata(transcriptionUrl, metadataUri, resourceUri) {
137
240
  return `${transcriptionUrl}?metadataUri=${encodeURIComponent(metadataUri)}&resourceUri=${
138
241
  resourceUri.replace('taomedia://mediamanager/', '')
139
242
  }`;
140
243
  }
141
244
 
245
+ /**
246
+ * Normalize ACL flags unless they are already a boolean permission map.
247
+ * @param {Object} item
248
+ * @returns {Object}
249
+ */
250
+ function ensurePermissions(item) {
251
+ if (item.permissions && !Array.isArray(item.permissions) && typeof item.permissions === 'object') {
252
+ return item;
253
+ }
254
+ return updatePermissions(item);
255
+ }
256
+
257
+ /**
258
+ * Search results often omit ACL (indexed gateway). Missing/empty ACL must stay pickable
259
+ * (read/preview/download) without inventing write/delete/upload. Explicit ACL arrays and
260
+ * boolean maps are preserved / mapped via updatePermissions.
261
+ * @param {Object} item
262
+ * @returns {Object}
263
+ */
264
+ function ensureSearchPermissions(item) {
265
+ if (!item) {
266
+ return item;
267
+ }
268
+ if (item.permissions && !Array.isArray(item.permissions) && typeof item.permissions === 'object') {
269
+ return item;
270
+ }
271
+ if (!item.permissions || (Array.isArray(item.permissions) && item.permissions.length === 0)) {
272
+ item.permissions = {
273
+ read: true,
274
+ write: false,
275
+ preview: true,
276
+ download: true,
277
+ upload: false,
278
+ delete: false
279
+ };
280
+ return item;
281
+ }
282
+ return updatePermissions(item);
283
+ }
284
+
285
+ /**
286
+ * Map a service file record to the selector display model.
287
+ * @param {Object} file
288
+ * @param {String} fullPath
289
+ * @param {String} [folderPath]
290
+ * @returns {Object}
291
+ */
292
+ function prepareFileForDisplay(file, fullPath, folderPath) {
293
+ file.type = mimeType.getFileType(file);
294
+ if (typeof file.identifier === 'undefined') {
295
+ file.display = `${fullPath}/${file.name}`.replace('//', '/');
296
+ } else {
297
+ file.display = file.identifier + file.name;
298
+ }
299
+
300
+ file.viewUrl = `${options.downloadUrl}?${$.param(options.params)}&${
301
+ options.pathParam || 'path'
302
+ }=${encodeURIComponent(file.uri)}`;
303
+ file.downloadUrl = `${file.viewUrl}&svgzsupport=true`;
304
+ if (!file.location) {
305
+ file.location = folderPath || fullPath || '';
306
+ }
307
+ if (!file.updatedAt && file.updated_at) {
308
+ file.updatedAt = file.updated_at;
309
+ }
310
+ file.locationDisplay = formatLocationDisplay(file.location);
311
+ file.updatedAtDisplay = formatUpdatedAtUtc(file.updatedAt);
312
+ return file;
313
+ }
314
+
315
+ /**
316
+ * Sync sort classes and aria-sort on table headers.
317
+ */
318
+ function applySortHeader() {
319
+ $fileSelector.find('.files thead th[data-sort-by]').each(function () {
320
+ const $th = $(this);
321
+ const field = $th.attr('data-sort-by');
322
+ $th.removeClass('sorted sorted_asc sorted_desc');
323
+ if (field === sort.field) {
324
+ $th.addClass('sorted').addClass(sort.direction === 'desc' ? 'sorted_desc' : 'sorted_asc');
325
+ $th.attr('aria-sort', sort.direction === 'desc' ? 'descending' : 'ascending');
326
+ } else {
327
+ $th.attr('aria-sort', 'none');
328
+ }
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Apply a column click to sort state and notify listeners.
334
+ * Skip optimistic re-sort of lastFiles (current page only) — that flashed the
335
+ * wrong order before browse/search refetch; updateFiles still sorts the response.
336
+ * @param {String} field
337
+ */
338
+ function changeSort(field) {
339
+ if (!field) {
340
+ return;
341
+ }
342
+ if (field === sort.field) {
343
+ sort = {
344
+ field,
345
+ direction: sort.direction === 'asc' ? 'desc' : 'asc'
346
+ };
347
+ } else {
348
+ sort = {
349
+ field,
350
+ direction: 'asc'
351
+ };
352
+ }
353
+ applySortHeader();
354
+ $container.trigger(`sortchange.${ns}`, [Object.assign({}, sort)]);
355
+ }
356
+
357
+ /**
358
+ * Preselect initialSelection once when the matching row is present.
359
+ * When resolve returned a currentAssetItem missing from the page, inject it.
360
+ * @param {String} [selection]
361
+ * @param {Boolean} [allowInject=true] - inject currentAssetItem when missing (skip during pending reopen)
362
+ */
363
+ function applyInitialSelection(selection, allowInject) {
364
+ const target = selection || options.initialSelection;
365
+ if (!target || initialSelectionApplied) {
366
+ return;
367
+ }
368
+ const mayInject = allowInject !== false;
369
+ let $item = $fileContainer.find('tr').filter(function () {
370
+ return $(this).attr('data-file') === String(target);
371
+ });
372
+ if (
373
+ !$item.length
374
+ && mayInject
375
+ && options.currentAssetItem
376
+ && String(options.currentAssetItem.uri) === String(target)
377
+ ) {
378
+ const injected = prepareFileForDisplay(
379
+ ensurePermissions(Object.assign({}, options.currentAssetItem)),
380
+ options.currentAssetItem.location || '',
381
+ options.currentAssetItem.location || ''
382
+ );
383
+ updateFiles([injected].concat(lastFiles || []));
384
+ $item = $fileContainer.find('tr').filter(function () {
385
+ return $(this).attr('data-file') === String(target);
386
+ });
387
+ }
388
+ if ($item.length) {
389
+ initialSelectionApplied = true;
390
+ $item.first().trigger('click');
391
+ }
392
+ }
393
+
394
+ applySortHeader();
395
+
396
+ $(parentSelector)
397
+ .off('click.resourcemgr', '.files thead th[data-sort-by]')
398
+ .on('click.resourcemgr', '.files thead th[data-sort-by]', function (e) {
399
+ e.preventDefault();
400
+ e.stopImmediatePropagation();
401
+ changeSort($(this).attr('data-sort-by'));
402
+ })
403
+ .off('keydown.resourcemgr', '.files thead th[data-sort-by]')
404
+ .on('keydown.resourcemgr', '.files thead th[data-sort-by]', function (e) {
405
+ if (e.key === 'Enter' || e.key === ' ') {
406
+ e.preventDefault();
407
+ e.stopImmediatePropagation();
408
+ changeSort($(this).attr('data-sort-by'));
409
+ }
410
+ });
411
+
142
412
  //listen for file activation
143
413
  $(parentSelector)
144
- .off('click', '.files li')
145
- .on('click', '.files li', function (e) {
414
+ .off('click', ROW_SELECTOR)
415
+ .on('click', ROW_SELECTOR, function (e) {
416
+ if ($(e.target).closest('.row-actions').length) {
417
+ return;
418
+ }
419
+ if ($(e.target).closest('a.select, a.download, a.delete').length) {
420
+ return;
421
+ }
146
422
  const clickedItem = e.target;
147
423
  if (clickedItem.hasAttribute('data-delete') || $(clickedItem).hasClass('icon-bin')) {
148
424
  return;
149
425
  }
150
426
  let $selected = $(this);
151
- let $files = $('.files > li', $fileSelector);
427
+ let $files = $(ROW_SELECTOR, $fileSelector);
152
428
  let data = _.clone($selected.data());
153
429
  if (
154
430
  options.resourceMetadataUrl
@@ -168,10 +444,10 @@ export default function (options) {
168
444
 
169
445
  //select a file
170
446
  $(parentSelector)
171
- .off('click', '.files li a.select')
172
- .on('click', '.files li a.select', function (e) {
447
+ .off('click', `${ROW_SELECTOR} a.select`)
448
+ .on('click', `${ROW_SELECTOR} a.select`, function (e) {
173
449
  e.preventDefault();
174
- let data = _.pick($(this).parents('li').data(), ['file', 'type', 'mime', 'size', 'alt']);
450
+ let data = _.pick($(this).closest('tr').data(), ['file', 'type', 'mime', 'size', 'alt']);
175
451
  if (context.mediaSources && context.mediaSources.length === 0 && data.file.indexOf('local/') > -1) {
176
452
  data.file = data.file.substring(6);
177
453
  }
@@ -180,8 +456,8 @@ export default function (options) {
180
456
 
181
457
  //delete a file
182
458
  $(parentSelector)
183
- .off('click', '.files li a.delete')
184
- .on('click', '.files li a.delete', function (e) {
459
+ .off('click', `${ROW_SELECTOR} a.delete`)
460
+ .on('click', `${ROW_SELECTOR} a.delete`, function (e) {
185
461
  // This function replaces ui/deleter and must follow the same logic.
186
462
  // The main difference is that it insert a confirmation dialog before deleting the file.
187
463
  e.preventDefault();
@@ -190,7 +466,7 @@ export default function (options) {
190
466
  return;
191
467
  }
192
468
 
193
- const $target = $elt.closest('li');
469
+ const $target = $elt.closest('tr');
194
470
  const path = $target.data('file');
195
471
  const hooks = [];
196
472
 
@@ -219,7 +495,7 @@ export default function (options) {
219
495
  params = {};
220
496
  if (e.namespace === 'deleter' && $target.length) {
221
497
  path = $target.data('file');
222
- params[options.pathParam] = path;
498
+ params[options.pathParam || 'path'] = path;
223
499
  $.getJSON(options.deleteUrl, _.merge(params, options.params), function (response) {
224
500
  if (response.deleted) {
225
501
  $container.trigger(`filedelete.${ns}`, [path]);
@@ -237,7 +513,9 @@ export default function (options) {
237
513
  $container.trigger(`filenew.${ns}`, [result, currentPath]);
238
514
  });
239
515
  $uploader.on('fail.uploader', function (e, file, err) {
240
- errors.push(__('Unable to upload file %s : %s', file.name, err.message));
516
+ errors.push(
517
+ __('Unable to upload file %s : %s', _.escape(file && file.name), _.escape(err && err.message))
518
+ );
241
519
  });
242
520
 
243
521
  $uploader.on('end.uploader', function () {
@@ -253,11 +531,11 @@ export default function (options) {
253
531
  $uploader.uploader({
254
532
  upload: true,
255
533
  multiple: true,
256
- uploadUrl: `${options.uploadUrl}?${$.param(options.params)}&${options.pathParam}=${currentPath}`,
534
+ uploadUrl: `${options.uploadUrl}?${$.param(options.params)}&${options.pathParam || 'path'}=${currentPath}`,
257
535
  fileSelect: function (files, done) {
258
536
  let givenLength = files.length;
259
537
  let fileNames = [];
260
- $fileContainer.find('li > .desc').each(function () {
538
+ $fileContainer.find('.desc').each(function () {
261
539
  fileNames.push($(this).text().toLowerCase());
262
540
  });
263
541
 
@@ -297,7 +575,7 @@ export default function (options) {
297
575
  let pathParam = `${currentPath}/${file.name}`;
298
576
  pathParam.replace('//', '/');
299
577
  $.getJSON(
300
- `${options.fileExistsUrl}?${$.param(options.params)}&${options.pathParam}=${pathParam}`,
578
+ `${options.fileExistsUrl}?${$.param(options.params)}&${options.pathParam || 'path'}=${pathParam}`,
301
579
  function (response) {
302
580
  if (response && response.exists === true) {
303
581
  //eslint-disable-next-line no-alert
@@ -324,7 +602,7 @@ export default function (options) {
324
602
  currentPath = uri;
325
603
  $uploader.uploader('options', {
326
604
  uploadUrl: `${options.uploadUrl}?${$.param(options.params)}&${
327
- options.pathParam
605
+ options.pathParam || 'path'
328
606
  }=${currentPath}&relPath=${currentPath}`
329
607
  });
330
608
  });
@@ -332,14 +610,18 @@ export default function (options) {
332
610
  function setUploadMode(enableUploadMode) {
333
611
  if (!enableUploadMode) {
334
612
  isUploadMode = false;
613
+ $fileSelector.removeClass('upload-mode');
335
614
  $uploader.hide();
615
+ $filesWrapper.show();
336
616
  $fileContainer.show();
337
617
  // Note: show() would display as inline, not inline-block!
338
618
  $switcher.filter('.upload').css({ display: 'inline-block' });
339
619
  $switcher.filter('.listing').hide();
340
- $browserTitle.text(__('Browse folders:'));
620
+ $browserTitle.text(__('Resources'));
341
621
  } else {
342
622
  isUploadMode = true;
623
+ $fileSelector.addClass('upload-mode');
624
+ $filesWrapper.hide();
343
625
  $fileContainer.hide();
344
626
  $placeholder.hide();
345
627
  $uploader.show();
@@ -363,17 +645,27 @@ export default function (options) {
363
645
  setUploadMode(isUploadMode);
364
646
  }
365
647
 
366
- function updateFiles(path, files) {
648
+ function updateFiles(files, updateOptions) {
649
+ const displayOptions = updateOptions || {};
650
+ const emptyLabel = displayOptions.emptyMessage || __('No files');
651
+ lastFiles = Array.isArray(files) ? files.slice() : [];
652
+ const sorted = sortAssetItems(lastFiles, sort);
367
653
  $fileContainer.empty();
368
- if (files.length) {
654
+ if (sorted.length) {
369
655
  $placeholder.hide();
656
+ $filesWrapper.show();
370
657
  $fileContainer.append(
371
658
  fileSelectTpl({
372
- files: files
659
+ files: sorted
373
660
  })
374
661
  );
375
- } else if ($fileContainer.css('display') !== 'none') {
376
- $placeholder.show();
662
+ } else if ($fileSelector.find('.asset-search-error:not([hidden])').length === 0) {
663
+ $filesWrapper.hide();
664
+ // Stylesheet defaults `.empty` to display:none; force visible for empty states.
665
+ $placeholder.text(emptyLabel).css('display', 'block');
666
+ } else {
667
+ $filesWrapper.hide();
668
+ $placeholder.hide();
377
669
  }
378
670
  }
379
671
  }