@oat-sa/tao-core-ui 3.23.0 → 3.24.1

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 +1230 -97
  8. package/dist/resourcemgr/css/resourcemgr.css.map +1 -1
  9. package/dist/resourcemgr/fileBrowser.js +815 -83
  10. package/dist/resourcemgr/filePreview.js +82 -7
  11. package/dist/resourcemgr/fileSelector.js +377 -92
  12. package/dist/resourcemgr.js +314 -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 +1230 -97
  20. package/src/resourcemgr/css/resourcemgr.css.map +1 -1
  21. package/src/resourcemgr/fileBrowser.js +872 -92
  22. package/src/resourcemgr/filePreview.js +89 -8
  23. package/src/resourcemgr/fileSelector.js +356 -46
  24. package/src/resourcemgr/scss/resourcemgr.scss +1450 -89
  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 +125 -54
  29. package/src/resourcemgr/tpl/rootFolder.tpl +3 -1
  30. package/src/resourcemgr.js +294 -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,16 +61,79 @@ 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
+
75
+ /**
76
+ * Row markup uses data-download for permission flag (boolean), not the URL.
77
+ * @param {*} file
78
+ * @returns {string}
79
+ */
80
+ function downloadHref(file) {
81
+ if (typeof file.downloadUrl === 'string' && file.downloadUrl) {
82
+ return file.downloadUrl;
83
+ }
84
+ if (typeof file.url === 'string' && file.url) {
85
+ const query = file.url.split('#')[0].split('?')[1] || '';
86
+ const hasSvgzSupport = query.split('&').some(param => param.split('=')[0] === 'svgzsupport');
87
+ return hasSvgzSupport ? file.url : `${file.url}&svgzsupport=true`;
88
+ }
89
+ if (typeof file.download === 'string' && file.download) {
90
+ return file.download;
91
+ }
92
+ return '';
93
+ }
94
+
95
+ /**
96
+ * @param {*} file
97
+ * @returns {string}
98
+ */
99
+ function downloadFilename(file) {
100
+ if (typeof file.display === 'string' && file.display) {
101
+ return file.display;
102
+ }
103
+ const path = file.file || '';
104
+ const idx = path.lastIndexOf('/');
105
+ return idx >= 0 ? path.slice(idx + 1) : path;
106
+ }
107
+
46
108
  function startPreview(file, preview, download, select) {
47
109
  if (preview) {
48
110
  $previewer.previewer(file);
49
- $propType.text(`${file.type} (${file.mime})`);
50
- $propSize.text(bytes.hrSize(file.size));
111
+ if (file.type || file.mime) {
112
+ $propType.text(
113
+ file.type && file.mime ? `${file.type} (${file.mime})` : metaOrDash(file.type || file.mime)
114
+ );
115
+ } else {
116
+ $propType.text(EMPTY_META);
117
+ }
118
+ $propSize.text(
119
+ file.size === null || typeof file.size === 'undefined' || file.size === ''
120
+ ? EMPTY_META
121
+ : bytes.hrSize(file.size)
122
+ );
123
+ } else {
124
+ $propType.text(EMPTY_META);
125
+ $propSize.text(EMPTY_META);
51
126
  }
52
- if(download) {
53
- $link.attr('href', file.download).attr('download', file.file);
54
- if ($link.hasClass('hidden')) {
55
- $link.removeClass('hidden');
127
+ if (download) {
128
+ const href = downloadHref(file);
129
+ if (href) {
130
+ $link.attr('href', href).attr('download', downloadFilename(file));
131
+ if ($link.hasClass('hidden')) {
132
+ $link.removeClass('hidden');
133
+ }
134
+ } else {
135
+ $link.attr('href', '#').removeAttr('download');
136
+ $link.addClass('hidden');
56
137
  }
57
138
  } else {
58
139
  $link.attr('href', '#').attr('download', '#');
@@ -65,8 +146,8 @@ export default function(options) {
65
146
 
66
147
  function stopPreview() {
67
148
  $previewer.previewer('update', { url: false });
68
- $propType.empty();
69
- $propSize.empty();
149
+ $propType.text(EMPTY_META);
150
+ $propSize.text(EMPTY_META);
70
151
  $('a', $propUrl).addClass('hidden');
71
152
  $selectButton.attr('disabled', 'disabled');
72
153
  }
@@ -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,23 @@ 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 $filesListLoading = $('.files-list-loading', $filesWrapper);
141
+ let $filesListLoadingStatus = $('.files-list-loading-status', $filesListLoading);
142
+ let $fileContainer = $('.files-list', $fileSelector);
74
143
  let $placeholder = $('.empty', $fileSelector);
75
144
  let $uploader = $('.file-upload-container', $fileSelector);
76
145
  let parentSelector = `#${$container.attr('id')} .file-selector`;
77
146
  let $pathTitle = $fileSelector.find('h1 > .title');
78
- let $browserTitle = $('.file-browser > h1', $container);
147
+ let $browserTitle = $('.file-browser > h1.resources-title', $container);
148
+ if (!$browserTitle.length) {
149
+ $browserTitle = $('.file-browser > h1', $container);
150
+ }
151
+ let searchMode = false;
152
+ let initialSelectionApplied = false;
153
+ let pendingContextSelection = null;
154
+ let sort = Object.assign({}, DEFAULT_SORT);
155
+ let lastFiles = [];
79
156
 
80
157
  //set up the uploader
81
158
  if (disableUpload) {
@@ -84,16 +161,35 @@ export default function (options) {
84
161
  } else {
85
162
  setUpUploader(root);
86
163
  }
164
+
165
+ $container.on(`searchmode.${ns}`, function (e, enabled) {
166
+ searchMode = !!enabled;
167
+ $fileSelector.toggleClass('search-mode', searchMode);
168
+ });
169
+
170
+ function applyFilesListLoading(loading) {
171
+ $filesWrapper.toggleClass('is-files-loading', !!loading);
172
+ $filesWrapper.attr('aria-busy', loading ? 'true' : 'false');
173
+ $filesListLoading.toggleClass('hidden', !loading).attr('aria-hidden', loading ? 'false' : 'true');
174
+ $filesListLoadingStatus.attr('aria-busy', loading ? 'true' : 'false');
175
+ }
176
+
177
+ $container.on(`filesloading.${ns}`, function (e, loading) {
178
+ if (searchMode) {
179
+ return;
180
+ }
181
+ applyFilesListLoading(loading);
182
+ });
183
+
87
184
  //update current folder
88
185
  $container.on(`folderselect.${ns}`, function (e, fullPath, data, activePath, content) {
89
186
  let files;
90
187
 
91
- data = data.map(function (dataItem) {
92
- if (Array.isArray(dataItem.permissions)) {
93
- updatePermissions(dataItem);
94
- }
95
- return dataItem;
96
- });
188
+ if (searchMode) {
189
+ return;
190
+ }
191
+
192
+ applyFilesListLoading(false);
97
193
 
98
194
  //update title
99
195
  if ($container[0].querySelector('.upload')) {
@@ -108,47 +204,245 @@ export default function (options) {
108
204
 
109
205
  //update content here
110
206
  if (_.isArray(data)) {
111
- files = _.filter(data, function (item) {
207
+ files = _.filter(data.map(ensurePermissions), function (item) {
112
208
  return !!item.uri;
113
209
  }).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;
210
+ return prepareFileForDisplay(file, fullPath, activePath);
211
+ });
212
+
213
+ updateFiles(files);
214
+ if (pendingContextSelection) {
215
+ const pending = pendingContextSelection;
216
+ initialSelectionApplied = false;
217
+ applyInitialSelection(pending, false);
218
+ if (initialSelectionApplied) {
219
+ pendingContextSelection = null;
119
220
  }
221
+ return;
222
+ }
223
+ applyInitialSelection();
224
+ }
225
+ });
120
226
 
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;
227
+ // Render scoped search results from the service as-is (no client MIME/auth filtering).
228
+ $container.on(`searchresults.${ns}`, function (e, result) {
229
+ const items = (result && result.items) || [];
230
+ const files = items
231
+ .filter(function (item) {
232
+ return item && !!item.uri;
233
+ })
234
+ .map(function (file) {
235
+ return prepareFileForDisplay(ensureSearchPermissions(file), result.path || '', result.path);
126
236
  });
127
237
 
128
- updateFiles(fullPath, files);
238
+ $pathTitle.text(__('Search results'));
239
+ updateFiles(files, {
240
+ emptyMessage: result && result.emptyMessage
241
+ });
129
242
 
130
- if (activePath) {
131
- $(`li[data-file="${activePath}"]`).trigger('click');
132
- }
243
+ if (!(result && result.error)) {
244
+ applyInitialSelection(result && result.initialSelection);
133
245
  }
134
246
  });
135
247
 
248
+ // Reopen resolve (AC3): defer preselect until folderselect loads the resolved parent folder.
249
+ $container.on(`applycontext.${ns}`, function (e, ctx) {
250
+ initialSelectionApplied = false;
251
+ options.initialSelection = ctx && ctx.selection ? ctx.selection : null;
252
+ options.currentAssetItem = ctx && ctx.currentAssetItem ? ctx.currentAssetItem : null;
253
+ pendingContextSelection = options.initialSelection || null;
254
+ $fileContainer.find('tr.active').removeClass('active');
255
+ });
256
+
136
257
  function injectTranscriptionMetadata(transcriptionUrl, metadataUri, resourceUri) {
137
258
  return `${transcriptionUrl}?metadataUri=${encodeURIComponent(metadataUri)}&resourceUri=${
138
259
  resourceUri.replace('taomedia://mediamanager/', '')
139
260
  }`;
140
261
  }
141
262
 
263
+ /**
264
+ * Normalize ACL flags unless they are already a boolean permission map.
265
+ * @param {Object} item
266
+ * @returns {Object}
267
+ */
268
+ function ensurePermissions(item) {
269
+ if (item.permissions && !Array.isArray(item.permissions) && typeof item.permissions === 'object') {
270
+ return item;
271
+ }
272
+ return updatePermissions(item);
273
+ }
274
+
275
+ /**
276
+ * Search results often omit ACL (indexed gateway). Missing/empty ACL must stay pickable
277
+ * (read/preview/download) without inventing write/delete/upload. Explicit ACL arrays and
278
+ * boolean maps are preserved / mapped via updatePermissions.
279
+ * @param {Object} item
280
+ * @returns {Object}
281
+ */
282
+ function ensureSearchPermissions(item) {
283
+ if (!item) {
284
+ return item;
285
+ }
286
+ if (item.permissions && !Array.isArray(item.permissions) && typeof item.permissions === 'object') {
287
+ return item;
288
+ }
289
+ if (!item.permissions || (Array.isArray(item.permissions) && item.permissions.length === 0)) {
290
+ item.permissions = {
291
+ read: true,
292
+ write: false,
293
+ preview: true,
294
+ download: true,
295
+ upload: false,
296
+ delete: false
297
+ };
298
+ return item;
299
+ }
300
+ return updatePermissions(item);
301
+ }
302
+
303
+ /**
304
+ * Map a service file record to the selector display model.
305
+ * @param {Object} file
306
+ * @param {String} fullPath
307
+ * @param {String} [folderPath]
308
+ * @returns {Object}
309
+ */
310
+ function prepareFileForDisplay(file, fullPath, folderPath) {
311
+ file.type = mimeType.getFileType(file);
312
+ if (typeof file.identifier === 'undefined') {
313
+ file.display = `${fullPath}/${file.name}`.replace('//', '/');
314
+ } else {
315
+ file.display = file.identifier + file.name;
316
+ }
317
+
318
+ file.viewUrl = `${options.downloadUrl}?${$.param(options.params)}&${
319
+ options.pathParam || 'path'
320
+ }=${encodeURIComponent(file.uri)}`;
321
+ file.downloadUrl = `${file.viewUrl}&svgzsupport=true`;
322
+ if (!file.location) {
323
+ file.location = folderPath || fullPath || '';
324
+ }
325
+ if (!file.updatedAt && file.updated_at) {
326
+ file.updatedAt = file.updated_at;
327
+ }
328
+ file.locationDisplay = formatLocationDisplay(file.location);
329
+ file.updatedAtDisplay = formatUpdatedAtUtc(file.updatedAt);
330
+ return file;
331
+ }
332
+
333
+ /**
334
+ * Sync sort classes and aria-sort on table headers.
335
+ */
336
+ function applySortHeader() {
337
+ $fileSelector.find('.files thead th[data-sort-by]').each(function () {
338
+ const $th = $(this);
339
+ const field = $th.attr('data-sort-by');
340
+ $th.removeClass('sorted sorted_asc sorted_desc');
341
+ if (field === sort.field) {
342
+ $th.addClass('sorted').addClass(sort.direction === 'desc' ? 'sorted_desc' : 'sorted_asc');
343
+ $th.attr('aria-sort', sort.direction === 'desc' ? 'descending' : 'ascending');
344
+ } else {
345
+ $th.attr('aria-sort', 'none');
346
+ }
347
+ });
348
+ }
349
+
350
+ /**
351
+ * Apply a column click to sort state and notify listeners.
352
+ * Skip optimistic re-sort of lastFiles (current page only) — that flashed the
353
+ * wrong order before browse/search refetch; updateFiles still sorts the response.
354
+ * @param {String} field
355
+ */
356
+ function changeSort(field) {
357
+ if (!field) {
358
+ return;
359
+ }
360
+ if (field === sort.field) {
361
+ sort = {
362
+ field,
363
+ direction: sort.direction === 'asc' ? 'desc' : 'asc'
364
+ };
365
+ } else {
366
+ sort = {
367
+ field,
368
+ direction: 'asc'
369
+ };
370
+ }
371
+ applySortHeader();
372
+ $container.trigger(`sortchange.${ns}`, [Object.assign({}, sort)]);
373
+ }
374
+
375
+ /**
376
+ * Preselect initialSelection once when the matching row is present.
377
+ * When resolve returned a currentAssetItem missing from the page, inject it.
378
+ * @param {String} [selection]
379
+ * @param {Boolean} [allowInject=true] - inject currentAssetItem when missing (skip during pending reopen)
380
+ */
381
+ function applyInitialSelection(selection, allowInject) {
382
+ const target = selection || options.initialSelection;
383
+ if (!target || initialSelectionApplied) {
384
+ return;
385
+ }
386
+ const mayInject = allowInject !== false;
387
+ let $item = $fileContainer.find('tr').filter(function () {
388
+ return $(this).attr('data-file') === String(target);
389
+ });
390
+ if (
391
+ !$item.length
392
+ && mayInject
393
+ && options.currentAssetItem
394
+ && String(options.currentAssetItem.uri) === String(target)
395
+ ) {
396
+ const injected = prepareFileForDisplay(
397
+ ensurePermissions(Object.assign({}, options.currentAssetItem)),
398
+ options.currentAssetItem.location || '',
399
+ options.currentAssetItem.location || ''
400
+ );
401
+ updateFiles([injected].concat(lastFiles || []));
402
+ $item = $fileContainer.find('tr').filter(function () {
403
+ return $(this).attr('data-file') === String(target);
404
+ });
405
+ }
406
+ if ($item.length) {
407
+ initialSelectionApplied = true;
408
+ $item.first().trigger('click');
409
+ }
410
+ }
411
+
412
+ applySortHeader();
413
+
414
+ $(parentSelector)
415
+ .off('click.resourcemgr', '.files thead th[data-sort-by]')
416
+ .on('click.resourcemgr', '.files thead th[data-sort-by]', function (e) {
417
+ e.preventDefault();
418
+ e.stopImmediatePropagation();
419
+ changeSort($(this).attr('data-sort-by'));
420
+ })
421
+ .off('keydown.resourcemgr', '.files thead th[data-sort-by]')
422
+ .on('keydown.resourcemgr', '.files thead th[data-sort-by]', function (e) {
423
+ if (e.key === 'Enter' || e.key === ' ') {
424
+ e.preventDefault();
425
+ e.stopImmediatePropagation();
426
+ changeSort($(this).attr('data-sort-by'));
427
+ }
428
+ });
429
+
142
430
  //listen for file activation
143
431
  $(parentSelector)
144
- .off('click', '.files li')
145
- .on('click', '.files li', function (e) {
432
+ .off('click', ROW_SELECTOR)
433
+ .on('click', ROW_SELECTOR, function (e) {
434
+ if ($(e.target).closest('.row-actions').length) {
435
+ return;
436
+ }
437
+ if ($(e.target).closest('a.select, a.download, a.delete').length) {
438
+ return;
439
+ }
146
440
  const clickedItem = e.target;
147
441
  if (clickedItem.hasAttribute('data-delete') || $(clickedItem).hasClass('icon-bin')) {
148
442
  return;
149
443
  }
150
444
  let $selected = $(this);
151
- let $files = $('.files > li', $fileSelector);
445
+ let $files = $(ROW_SELECTOR, $fileSelector);
152
446
  let data = _.clone($selected.data());
153
447
  if (
154
448
  options.resourceMetadataUrl
@@ -168,10 +462,10 @@ export default function (options) {
168
462
 
169
463
  //select a file
170
464
  $(parentSelector)
171
- .off('click', '.files li a.select')
172
- .on('click', '.files li a.select', function (e) {
465
+ .off('click', `${ROW_SELECTOR} a.select`)
466
+ .on('click', `${ROW_SELECTOR} a.select`, function (e) {
173
467
  e.preventDefault();
174
- let data = _.pick($(this).parents('li').data(), ['file', 'type', 'mime', 'size', 'alt']);
468
+ let data = _.pick($(this).closest('tr').data(), ['file', 'type', 'mime', 'size', 'alt']);
175
469
  if (context.mediaSources && context.mediaSources.length === 0 && data.file.indexOf('local/') > -1) {
176
470
  data.file = data.file.substring(6);
177
471
  }
@@ -180,8 +474,8 @@ export default function (options) {
180
474
 
181
475
  //delete a file
182
476
  $(parentSelector)
183
- .off('click', '.files li a.delete')
184
- .on('click', '.files li a.delete', function (e) {
477
+ .off('click', `${ROW_SELECTOR} a.delete`)
478
+ .on('click', `${ROW_SELECTOR} a.delete`, function (e) {
185
479
  // This function replaces ui/deleter and must follow the same logic.
186
480
  // The main difference is that it insert a confirmation dialog before deleting the file.
187
481
  e.preventDefault();
@@ -190,7 +484,7 @@ export default function (options) {
190
484
  return;
191
485
  }
192
486
 
193
- const $target = $elt.closest('li');
487
+ const $target = $elt.closest('tr');
194
488
  const path = $target.data('file');
195
489
  const hooks = [];
196
490
 
@@ -219,7 +513,7 @@ export default function (options) {
219
513
  params = {};
220
514
  if (e.namespace === 'deleter' && $target.length) {
221
515
  path = $target.data('file');
222
- params[options.pathParam] = path;
516
+ params[options.pathParam || 'path'] = path;
223
517
  $.getJSON(options.deleteUrl, _.merge(params, options.params), function (response) {
224
518
  if (response.deleted) {
225
519
  $container.trigger(`filedelete.${ns}`, [path]);
@@ -237,7 +531,9 @@ export default function (options) {
237
531
  $container.trigger(`filenew.${ns}`, [result, currentPath]);
238
532
  });
239
533
  $uploader.on('fail.uploader', function (e, file, err) {
240
- errors.push(__('Unable to upload file %s : %s', file.name, err.message));
534
+ errors.push(
535
+ __('Unable to upload file %s : %s', _.escape(file && file.name), _.escape(err && err.message))
536
+ );
241
537
  });
242
538
 
243
539
  $uploader.on('end.uploader', function () {
@@ -253,11 +549,11 @@ export default function (options) {
253
549
  $uploader.uploader({
254
550
  upload: true,
255
551
  multiple: true,
256
- uploadUrl: `${options.uploadUrl}?${$.param(options.params)}&${options.pathParam}=${currentPath}`,
552
+ uploadUrl: `${options.uploadUrl}?${$.param(options.params)}&${options.pathParam || 'path'}=${currentPath}`,
257
553
  fileSelect: function (files, done) {
258
554
  let givenLength = files.length;
259
555
  let fileNames = [];
260
- $fileContainer.find('li > .desc').each(function () {
556
+ $fileContainer.find('.desc').each(function () {
261
557
  fileNames.push($(this).text().toLowerCase());
262
558
  });
263
559
 
@@ -297,7 +593,7 @@ export default function (options) {
297
593
  let pathParam = `${currentPath}/${file.name}`;
298
594
  pathParam.replace('//', '/');
299
595
  $.getJSON(
300
- `${options.fileExistsUrl}?${$.param(options.params)}&${options.pathParam}=${pathParam}`,
596
+ `${options.fileExistsUrl}?${$.param(options.params)}&${options.pathParam || 'path'}=${pathParam}`,
301
597
  function (response) {
302
598
  if (response && response.exists === true) {
303
599
  //eslint-disable-next-line no-alert
@@ -324,7 +620,7 @@ export default function (options) {
324
620
  currentPath = uri;
325
621
  $uploader.uploader('options', {
326
622
  uploadUrl: `${options.uploadUrl}?${$.param(options.params)}&${
327
- options.pathParam
623
+ options.pathParam || 'path'
328
624
  }=${currentPath}&relPath=${currentPath}`
329
625
  });
330
626
  });
@@ -332,14 +628,18 @@ export default function (options) {
332
628
  function setUploadMode(enableUploadMode) {
333
629
  if (!enableUploadMode) {
334
630
  isUploadMode = false;
631
+ $fileSelector.removeClass('upload-mode');
335
632
  $uploader.hide();
633
+ $filesWrapper.show();
336
634
  $fileContainer.show();
337
635
  // Note: show() would display as inline, not inline-block!
338
636
  $switcher.filter('.upload').css({ display: 'inline-block' });
339
637
  $switcher.filter('.listing').hide();
340
- $browserTitle.text(__('Browse folders:'));
638
+ $browserTitle.text(__('Resources'));
341
639
  } else {
342
640
  isUploadMode = true;
641
+ $fileSelector.addClass('upload-mode');
642
+ $filesWrapper.hide();
343
643
  $fileContainer.hide();
344
644
  $placeholder.hide();
345
645
  $uploader.show();
@@ -363,17 +663,27 @@ export default function (options) {
363
663
  setUploadMode(isUploadMode);
364
664
  }
365
665
 
366
- function updateFiles(path, files) {
666
+ function updateFiles(files, updateOptions) {
667
+ const displayOptions = updateOptions || {};
668
+ const emptyLabel = displayOptions.emptyMessage || __('No files');
669
+ lastFiles = Array.isArray(files) ? files.slice() : [];
670
+ const sorted = sortAssetItems(lastFiles, sort);
367
671
  $fileContainer.empty();
368
- if (files.length) {
672
+ if (sorted.length) {
369
673
  $placeholder.hide();
674
+ $filesWrapper.show();
370
675
  $fileContainer.append(
371
676
  fileSelectTpl({
372
- files: files
677
+ files: sorted
373
678
  })
374
679
  );
375
- } else if ($fileContainer.css('display') !== 'none') {
376
- $placeholder.show();
680
+ } else if ($fileSelector.find('.asset-search-error:not([hidden])').length === 0) {
681
+ $filesWrapper.hide();
682
+ // Stylesheet defaults `.empty` to display:none; force visible for empty states.
683
+ $placeholder.text(emptyLabel).css('display', 'block');
684
+ } else {
685
+ $filesWrapper.hide();
686
+ $placeholder.hide();
377
687
  }
378
688
  }
379
689
  }