@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,4 +1,4 @@
1
- define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/searchModal/css/advancedSearch.css', 'ui/component', 'ui/modal', 'ui/datatable', 'select2', 'core/dataProvider/request'], function ($$1, _, Handlebars, Helpers0, advancedSearch_css, component, modal, datatable, select2, request) { 'use strict';
1
+ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/searchModal/css/advancedSearch.css', 'ui/component', 'ui/modal', 'ui/datatable', 'select2', 'core/dataProvider/request', 'i18n'], function ($$1, _, Handlebars, Helpers0, advancedSearch_css, component, modal, datatable, select2, request, __) { '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'] : _;
@@ -6,6 +6,7 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
6
6
  Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0;
7
7
  component = component && Object.prototype.hasOwnProperty.call(component, 'default') ? component['default'] : component;
8
8
  request = request && Object.prototype.hasOwnProperty.call(request, 'default') ? request['default'] : request;
9
+ __ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __;
9
10
 
10
11
  if (!Helpers0.__initialized) {
11
12
  Helpers0(Handlebars);
@@ -410,6 +411,7 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
410
411
  let $addCriteriaInput = null;
411
412
  let $criteriaSelect = null;
412
413
  let $advancedCriteriaContainer = null;
414
+ let $appliedFiltersSummary = null;
413
415
  let criteriaState = null;
414
416
  let criteriaMapping = {};
415
417
  const criteriaTypes = {
@@ -422,7 +424,6 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
422
424
  not: 'LOGIC_NOT'
423
425
  };
424
426
  let isAdvancedSearchStatusEnabled;
425
- let isCriteriaListUpdated = false;
426
427
 
427
428
  // Creates new component
428
429
  const instance = component({
@@ -442,16 +443,20 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
442
443
  if (!isAdvancedSearchStatusEnabled) {
443
444
  return Promise.resolve();
444
445
  }
445
- const $criteriaIcon = $$1('.add-criteria-container a span').eq(0);
446
- $criteriaIcon.toggleClass('icon-add').toggleClass('icon-loop');
446
+ const $criteriaIcon = $addCriteria ? $$1('a span', $addCriteria).eq(0) : $$1();
447
+ const idleIconClass = $criteriaIcon.hasClass('icon-plus') ? 'icon-plus' : 'icon-add';
448
+ $criteriaIcon.removeClass('icon-add icon-plus').addClass('icon-loop');
447
449
  return request(route).then(response => {
448
450
  criteriaMapping = response.criteriaMapping || {};
449
451
  const classTree = response.classDefinition ? response.classDefinition : response;
450
452
  const criteria = formatCriteria(classTree);
451
453
  updateCriteria(criteria);
452
- isCriteriaListUpdated = true;
453
- $criteriaIcon.toggleClass('icon-add').toggleClass('icon-loop');
454
- }).catch(e => instance.trigger('error', e));
454
+ $criteriaIcon.removeClass('icon-loop').addClass(idleIconClass);
455
+ }).catch(e => {
456
+ $criteriaIcon.removeClass('icon-loop').addClass(idleIconClass);
457
+ instance.trigger('error', e);
458
+ throw e;
459
+ });
455
460
  },
456
461
  /**
457
462
  * Access to component state
@@ -467,10 +472,19 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
467
472
  clear() {
468
473
  $advancedCriteriaContainer.removeClass(['scrollable', 'scroll-separator-top', 'scroll-separator-bottom']);
469
474
  $advancedCriteriaContainer.empty();
475
+ updateAppliedFiltersSummary(0);
476
+ // Rebuild selectable options: adding a criterion removes its <option>,
477
+ // so Clear must restore them (searchModal recovers via class re-select + updateCriteria).
478
+ if ($criteriaSelect && $criteriaSelect.length) {
479
+ $criteriaSelect.find('option:not(:first-child)').remove();
480
+ }
470
481
  _.forEach(criteriaState, criterion => {
471
482
  criterion.rendered = false;
472
483
  criterion.value = null;
473
484
  criterion.logic = null;
485
+ if ($criteriaSelect && $criteriaSelect.length) {
486
+ $criteriaSelect.append(createCriteriaOption(criterion));
487
+ }
474
488
  });
475
489
  },
476
490
  /**
@@ -503,7 +517,7 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
503
517
  });
504
518
  return query;
505
519
  }
506
- }).setTemplate(advancedSearchTpl).on('render', () => {
520
+ }).setTemplate(config.layoutTemplate || advancedSearchTpl).on('render', () => {
507
521
  initUiSelectors();
508
522
  initAddCriteriaSelector().then(() => {
509
523
  initCriteriaState();
@@ -521,6 +535,13 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
521
535
  $addCriteriaInput = $$1('.add-criteria-container a', $container);
522
536
  $criteriaSelect = $$1('.add-criteria-container select', $container);
523
537
  $advancedCriteriaContainer = $$1('.advanced-criteria-container', $container);
538
+ $appliedFiltersSummary = $$1('.applied-filters-summary', $container);
539
+ if (config.collapsibleCriteria && $appliedFiltersSummary.length) {
540
+ $appliedFiltersSummary.off('click.advancedSearch').on('click.advancedSearch', function (e) {
541
+ e.preventDefault();
542
+ togglePreviousFilters();
543
+ });
544
+ }
524
545
  $advancedCriteriaContainer.on('scroll', _.throttle(animateScroll, 100));
525
546
  }
526
547
 
@@ -580,12 +601,23 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
580
601
  });
581
602
 
582
603
  // open dropdown when user clicks on add criteria input
583
- $addCriteriaInput.on('click', () => {
584
- if (isCriteriaListUpdated) {
585
- $criteriaSelect.select2('open');
586
- // if dropdown is opened above addCriteria input, top property is slightly decreased to avoid overlapping with addCriteria icon
587
- if ($$1('.criteria-dropdown-select2').hasClass('select2-drop-above')) {
588
- $$1('.criteria-dropdown-select2').css('top', $$1('.criteria-dropdown-select2').css('top').split('px')[0] - 10 + 'px');
604
+ $addCriteriaInput.on('click', function (e) {
605
+ e.preventDefault();
606
+ e.stopPropagation();
607
+ const hasOptions = $criteriaSelect.find('option').filter(function () {
608
+ return Boolean(this.value);
609
+ }).length > 0;
610
+ // Never open an empty select2 (mask-only / no results). Wait for ClassMetadata options.
611
+ if (!hasOptions) {
612
+ return;
613
+ }
614
+ $criteriaSelect.select2('open');
615
+ // if dropdown is opened above addCriteria input, top property is slightly decreased to avoid overlapping with addCriteria icon
616
+ const $dropdown = $$1('.criteria-dropdown-select2');
617
+ if ($dropdown.hasClass('select2-drop-above')) {
618
+ const top = parseFloat($dropdown.css('top'));
619
+ if (!Number.isNaN(top)) {
620
+ $dropdown.css('top', `${top - 10}px`);
589
621
  }
590
622
  }
591
623
  });
@@ -646,9 +678,12 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
646
678
  // render new criterion
647
679
  const criterion = criteriaState[criterionToAdd];
648
680
  const $criterionContainer = renderCriterion(criterion);
681
+ if (config.collapsibleCriteria) {
682
+ wrapCriterionAsSpoiler($criterionContainer, criterion);
683
+ }
649
684
 
650
685
  // set logic to remove criterion
651
- $$1('.icon-result-nok', $criterionContainer).on('click', {
686
+ $criterionContainer.find('.icon-result-nok, .filter-spoiler-delete').on('click', {
652
687
  criterion
653
688
  }, removeCriterion);
654
689
 
@@ -660,6 +695,99 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
660
695
  $advancedCriteriaContainer.addClass('scrollable');
661
696
  }
662
697
  criterion.rendered = true;
698
+ if (config.collapsibleCriteria) {
699
+ reorganizeFilterLayout();
700
+ }
701
+ }
702
+
703
+ /**
704
+ * @returns {jQuery}
705
+ */
706
+ function getAllRenderedFilters() {
707
+ return $advancedCriteriaContainer.children('.filter-container').not('.invalid-criteria-warning-container');
708
+ }
709
+
710
+ /**
711
+ * @param {jQuery} $filter
712
+ * @param {boolean} open
713
+ */
714
+ function setFilterSpoilerOpen($filter, open) {
715
+ const $spoiler = $filter.find('.filter-spoiler').first();
716
+ if (!$spoiler.length) {
717
+ return;
718
+ }
719
+ $spoiler.toggleClass('is-open', open);
720
+ $filter.find('.filter-spoiler-toggle').attr('aria-expanded', open);
721
+ }
722
+
723
+ /**
724
+ * @param {number} appliedCount
725
+ * @returns {string}
726
+ */
727
+ function getAppliedFiltersSummaryText(appliedCount) {
728
+ if (appliedCount === 1) {
729
+ return __('1 filter applied');
730
+ }
731
+ return __('%s filters applied').replace('%s', String(appliedCount));
732
+ }
733
+
734
+ /**
735
+ * Keeps each filter as its own spoiler card: newest expanded, previous collapsed (headers stay visible).
736
+ * With collapsibleCriteria, newest is prepended (index 0); otherwise newest is last.
737
+ */
738
+ function reorganizeFilterLayout() {
739
+ const $filters = getAllRenderedFilters();
740
+ const count = $filters.length;
741
+ updateAppliedFiltersSummary(count);
742
+ if (count === 0) {
743
+ return;
744
+ }
745
+ const openIndex = config.collapsibleCriteria ? 0 : count - 1;
746
+ $filters.each(function (index) {
747
+ setFilterSpoilerOpen($$1(this), index === openIndex);
748
+ });
749
+ if ($appliedFiltersSummary && $appliedFiltersSummary.length) {
750
+ $appliedFiltersSummary.attr('aria-expanded', 'false');
751
+ }
752
+ }
753
+
754
+ /**
755
+ * Updates the "N filters applied" summary next to Add filter.
756
+ * @param {number} appliedCount
757
+ */
758
+ function updateAppliedFiltersSummary(appliedCount) {
759
+ if ($appliedFiltersSummary && $appliedFiltersSummary.length) {
760
+ if (appliedCount > 0) {
761
+ $appliedFiltersSummary.text(getAppliedFiltersSummaryText(appliedCount)).removeClass('hidden').prop('hidden', false);
762
+ } else {
763
+ $appliedFiltersSummary.addClass('hidden').prop('hidden', true).text('').attr('aria-expanded', 'false');
764
+ }
765
+ }
766
+ instance.trigger('criteriachange', {
767
+ count: appliedCount
768
+ });
769
+ }
770
+
771
+ /**
772
+ * Toggles expand/collapse for all filters except the newest one.
773
+ * With collapsibleCriteria (prepend), newest is first; otherwise newest is last.
774
+ */
775
+ function togglePreviousFilters() {
776
+ const $filters = getAllRenderedFilters();
777
+ if ($filters.length <= 1) {
778
+ return;
779
+ }
780
+ const $previous = config.collapsibleCriteria ? $filters.slice(1) : $filters.slice(0, -1);
781
+ const anyOpen = $previous.filter(function () {
782
+ return $$1(this).find('.filter-spoiler').first().hasClass('is-open');
783
+ }).length > 0;
784
+ const open = !anyOpen;
785
+ $previous.each(function () {
786
+ setFilterSpoilerOpen($$1(this), open);
787
+ });
788
+ if ($appliedFiltersSummary && $appliedFiltersSummary.length) {
789
+ $appliedFiltersSummary.attr('aria-expanded', open);
790
+ }
663
791
  }
664
792
 
665
793
  /**
@@ -677,9 +805,15 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
677
805
  } else {
678
806
  templateToUse = listCheckboxCriterionTpl;
679
807
  }
680
- $advancedCriteriaContainer.append(templateToUse({
681
- criterion
682
- }));
808
+ if (config.collapsibleCriteria) {
809
+ $advancedCriteriaContainer.prepend(templateToUse({
810
+ criterion
811
+ }));
812
+ } else {
813
+ $advancedCriteriaContainer.append(templateToUse({
814
+ criterion
815
+ }));
816
+ }
683
817
  const $criterionContainer = $$1(`.${criterion.id}-filter`, $container);
684
818
  const valueMapping = criteriaMapping[criterion.type];
685
819
 
@@ -760,15 +894,17 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
760
894
  * @param {object} $criterionContainer - rendered criterion
761
895
  */
762
896
  function bindCriterionValue(criterion, $criterionContainer) {
897
+ if (criterion.type === criteriaTypes.text) {
898
+ // Bind synchronously so typing / Search right after Add filter is not lost
899
+ // (getInitialCriterionLabel resolves on a microtask).
900
+ $$1('input', $criterionContainer).val(criterion.value);
901
+ $$1('input', $criterionContainer).on('input change', function () {
902
+ criterion.value = $$1(this).val() || null;
903
+ });
904
+ return;
905
+ }
763
906
  getInitialCriterionLabel(criterion).then(initialCriterion => {
764
- if (criterion.type === criteriaTypes.text) {
765
- // set initial value
766
- $$1('input', $criterionContainer).val(criterion.value);
767
- // set event to bind input value to critariaState
768
- $$1('input', $criterionContainer).on('change', function () {
769
- criterion.value = $$1(this).val() || null;
770
- });
771
- } else if (criterion.type === criteriaTypes.list && criterion.uri) {
907
+ if (criterion.type === criteriaTypes.list && criterion.uri) {
772
908
  // set initial value
773
909
  if (criterion.value) {
774
910
  $$1(`input[name=${criterion.id}-select]`, $criterionContainer).select2('data', initialCriterion);
@@ -798,6 +934,58 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
798
934
  });
799
935
  }
800
936
 
937
+ /**
938
+ * Wraps a rendered criterion in a collapsible spoiler (Resource Manager UX).
939
+ * @param {jQuery} $criterionContainer
940
+ * @param {object} criterion
941
+ */
942
+ function wrapCriterionAsSpoiler($criterionContainer, criterion) {
943
+ let $deleteBtn = $criterionContainer.find('.icon-result-nok').first();
944
+ if (!$deleteBtn.length) {
945
+ $deleteBtn = $$1('<button>', {
946
+ type: 'button',
947
+ class: 'filter-spoiler-delete icon-bin',
948
+ 'aria-label': __('Remove criteria')
949
+ });
950
+ } else {
951
+ $deleteBtn.attr('type', 'button').removeClass('icon-result-nok').addClass('filter-spoiler-delete icon-bin').attr('aria-label', __('Remove criteria'));
952
+ }
953
+ const $title = $$1('<span>', {
954
+ class: 'filter-spoiler-title',
955
+ text: criterion.label
956
+ });
957
+ const $toggle = $$1('<button>', {
958
+ type: 'button',
959
+ class: 'filter-spoiler-toggle',
960
+ 'aria-expanded': 'true'
961
+ }).append($title);
962
+ const $header = $$1('<div>', {
963
+ class: 'filter-spoiler-header'
964
+ }).append($toggle, $deleteBtn);
965
+ const $body = $$1('<div>', {
966
+ class: 'filter-spoiler-body'
967
+ });
968
+ $criterionContainer.children().appendTo($body);
969
+ const $spoiler = $$1('<div>', {
970
+ class: 'filter-spoiler is-open'
971
+ }).append($header, $body);
972
+ $criterionContainer.empty().append($spoiler);
973
+ $criterionContainer.addClass('filter-spoiler-wrapper');
974
+ $toggle.on('click', function (e) {
975
+ e.preventDefault();
976
+ const $spoilerEl = $criterionContainer.find('.filter-spoiler').first();
977
+ const willOpen = !$spoilerEl.hasClass('is-open');
978
+ if (willOpen && config.collapsibleCriteria) {
979
+ getAllRenderedFilters().each(function () {
980
+ if (this !== $criterionContainer.get(0)) {
981
+ setFilterSpoilerOpen($$1(this), false);
982
+ }
983
+ });
984
+ }
985
+ setFilterSpoilerOpen($criterionContainer, willOpen);
986
+ });
987
+ }
988
+
801
989
  /**
802
990
  * Removes a criterion from advanced criteria container when user clicks on the criterion close icon.
803
991
  * It also adds the option element to criteria select so removed criterion can be rendered again
@@ -809,7 +997,7 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
809
997
  const criterionKey = getCriterionStateId(criterion);
810
998
 
811
999
  // remove criterion and append new criterion to select options
812
- $$1(this).parent().remove();
1000
+ $$1(this).closest('.filter-container').remove();
813
1001
  $criteriaSelect.append(newOption);
814
1002
 
815
1003
  // reset criterion values on criteriaState
@@ -820,6 +1008,9 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
820
1008
  if ($advancedCriteriaContainer.get(0).scrollHeight <= $advancedCriteriaContainer.outerHeight()) {
821
1009
  $advancedCriteriaContainer.removeClass('scrollable');
822
1010
  }
1011
+ if (config.collapsibleCriteria) {
1012
+ reorganizeFilterLayout();
1013
+ }
823
1014
  }
824
1015
 
825
1016
  /**
@@ -832,15 +1023,23 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
832
1023
  function formatCriteria(classTree) {
833
1024
  let criteria = [];
834
1025
  _.forEach(classTree, classInstance => {
835
- criteria.push(...classInstance.metadata);
1026
+ const metadata = classInstance && classInstance.metadata;
1027
+ if (Array.isArray(metadata) && metadata.length) {
1028
+ criteria.push(...metadata);
1029
+ }
836
1030
  });
837
1031
 
838
1032
  // extends each criterion with an id that can be use as a valid css class
839
- _.forEach(criteria, criterion => {
1033
+ criteria = criteria.filter(criterion => {
1034
+ const id = String(criterion && criterion.propertyUri || '').replace(/^[^a-zA-Z]*|[^a-zA-Z0-9]*/g, '');
1035
+ if (!id) {
1036
+ return false;
1037
+ }
840
1038
  criterion.label = getCriterionLabel(criterion);
841
- criterion.id = criterion.propertyUri.replace(/^[^a-zA-Z]*|[^a-zA-Z0-9]*/g, '');
1039
+ criterion.id = id;
1040
+ return true;
842
1041
  });
843
- return criteria;
1042
+ return _.uniqBy(criteria, 'propertyUri');
844
1043
  }
845
1044
 
846
1045
  /**
@@ -856,6 +1055,9 @@ define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'css!ui/sear
856
1055
  const invalidCriteria = deleteDeprecatedCriteria(criteria);
857
1056
  extendCriteria(criteria);
858
1057
  renderWarningMessage(invalidCriteria);
1058
+ if (config.collapsibleCriteria) {
1059
+ reorganizeFilterLayout();
1060
+ }
859
1061
  }
860
1062
 
861
1063
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oat-sa/tao-core-ui",
3
- "version": "3.23.0",
3
+ "version": "3.24.0",
4
4
  "displayName": "TAO Core UI",
5
5
  "description": "UI libraries of TAO",
6
6
  "scripts": {
@@ -20,7 +20,8 @@
20
20
  "lint:src": "eslint -c .eslintrc.js src",
21
21
  "lint:test": "eslint -c test/.eslintrc.js test",
22
22
  "lint:report": "eslint -c .eslintrc.js --output-file eslint_report.json --format json src | true",
23
- "prepare": "npm run build:all"
23
+ "prepare": "npm run build:all",
24
+ "audit:critical": "npm audit --audit-level=critical"
24
25
  },
25
26
  "keywords": [
26
27
  "tao",
@@ -43,9 +44,9 @@
43
44
  },
44
45
  "license": "GPL-2.0",
45
46
  "devDependencies": {
46
- "@babel/core": "^7.19.3",
47
- "@babel/eslint-parser": "^7.19.1",
48
- "@babel/preset-env": "^7.19.4",
47
+ "@babel/core": "^7.28.5",
48
+ "@babel/eslint-parser": "^7.28.5",
49
+ "@babel/preset-env": "^7.28.5",
49
50
  "@csstools/postcss-sass": "^5.0.1",
50
51
  "@oat-sa/browserslist-config-tao": "^1.0.1",
51
52
  "@oat-sa/eslint-config-tao": "^2.0.0",
@@ -60,7 +61,7 @@
60
61
  "autoprefixer": "^10.4.14",
61
62
  "cross-env": "^7.0.2",
62
63
  "devbridge-autocomplete": "^1.2.20",
63
- "dompurify": "^2.4.0",
64
+ "dompurify": "^2.5.9",
64
65
  "eslint": "^8.39.0",
65
66
  "flatpickr": "4.5.7",
66
67
  "fs-extra": "^11.1.1",
@@ -95,6 +96,14 @@
95
96
  "text": "git+https://github.com/requirejs/text.git#2.0.16",
96
97
  "tooltip.js": "^1.3.3"
97
98
  },
99
+ "overrides": {
100
+ "shell-quote": "1.10.0",
101
+ "@babel/traverse": "^7.28.5",
102
+ "istanbul-lib-instrument": "^6.0.3",
103
+ "rollup-plugin-handlebars-plus": {
104
+ "handlebars": "~4.7.9"
105
+ }
106
+ },
98
107
  "browserslist": [
99
108
  "extends @oat-sa/browserslist-config-tao"
100
109
  ],
@@ -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) 2021 (original work) Open Assessment Technologies SA ;
16
+ * Copyright (c) 2021-2026 (original work) Open Assessment Technologies SA ;
17
17
  *
18
18
  */
19
19
  import $ from 'jquery';
@@ -38,11 +38,30 @@ export const initAdvanced = function (widget) {
38
38
  export const initUpload = function (widget) {
39
39
  const $form = widget.$form;
40
40
  const options = widget.options;
41
- const { img } = getImage(widget);
41
+ const { img, $img } = getImage(widget);
42
42
  const $uploadTrigger = $form.find('[data-role="upload-trigger"]');
43
43
  const $src = $form.find('input[name=src]');
44
44
  const $alt = $form.find('input[name=alt]');
45
45
 
46
+ const commitSrc = function (srcValue) {
47
+ $src.val(srcValue);
48
+ // Keep QTI model + preview in sync immediately: the form src callback is
49
+ // throttled (1s), so a late trailing call must not leave a rejected pick.
50
+ if (img) {
51
+ img.attr('src', srcValue);
52
+ }
53
+ if ($img && $img.length) {
54
+ $img.attr('src', srcValue ? widget.getAssetManager().resolve(srcValue) : '');
55
+ $img.trigger('contentChange.qti-widget').change();
56
+ }
57
+ _.defer(function () {
58
+ if (img) {
59
+ img.attr('off-media-editor', 1);
60
+ }
61
+ $src.trigger('change');
62
+ });
63
+ };
64
+
46
65
  const _openResourceMgr = function () {
47
66
  $uploadTrigger.resourcemgr({
48
67
  title: __(
@@ -51,10 +70,15 @@ export const initUpload = function (widget) {
51
70
  appendContainer: options.mediaManager.appendContainer,
52
71
  mediaSourcesUrl: options.mediaManager.mediaSourcesUrl,
53
72
  browseUrl: options.mediaManager.browseUrl,
73
+ searchUrl: options.mediaManager.searchUrl,
54
74
  uploadUrl: options.mediaManager.uploadUrl,
55
75
  deleteUrl: options.mediaManager.deleteUrl,
56
76
  downloadUrl: options.mediaManager.downloadUrl,
57
77
  fileExistsUrl: options.mediaManager.fileExistsUrl,
78
+ rootClassUri: options.mediaManager.rootClassUri,
79
+ classMappingUrl: options.mediaManager.classMappingUrl,
80
+ statusUrl: options.mediaManager.statusUrl,
81
+ maxListSize: options.mediaManager.maxListSize,
58
82
  params: {
59
83
  uri: options.uri,
60
84
  lang: options.lang,
@@ -69,44 +93,58 @@ export const initUpload = function (widget) {
69
93
  pathParam: 'path',
70
94
  path: options.mediaManager.path,
71
95
  root: options.mediaManager.root,
96
+ currentAsset: $src.val() || null,
72
97
  select: function (e, files) {
73
98
  let file, alt;
74
99
  let confirmBox, cancel, save;
75
- if (files && files.length) {
76
- file = files[0].file;
77
- alt = files[0].alt;
78
- $src.val(file);
79
- if ($.trim($alt.val()) === '') {
80
- if (alt === '') {
81
- alt = extractLabel(file);
82
- }
100
+ if (!(files && files.length)) {
101
+ return;
102
+ }
103
+
104
+ file = files[0].file;
105
+ alt = files[0].alt;
106
+
107
+ // Apply the new file immediately; alt confirm only decides alt text.
108
+ commitSrc(file);
109
+
110
+ if ($.trim($alt.val()) === '') {
111
+ if (alt === '') {
112
+ alt = extractLabel(file);
113
+ }
114
+ if (img) {
83
115
  img.attr('alt', alt);
84
- $alt.val(alt).trigger('change');
85
- } else {
86
- confirmBox = $('.change-alt-modal-feedback', $form);
87
- cancel = confirmBox.find('.cancel');
88
- save = confirmBox.find('.save');
116
+ }
117
+ $alt.val(alt).trigger('change');
118
+ return;
119
+ }
89
120
 
90
- $('.alt-text', confirmBox).html(`"${$alt.val()}"<br>${__('with')}<br>"${alt}" ?`);
121
+ // Existing alt: confirm replace. Cancel / overlay / Esc keeps the new image;
122
+ // only alt text stays unchanged unless the user confirms.
123
+ confirmBox = $('.change-alt-modal-feedback', $form);
124
+ cancel = confirmBox.find('.cancel');
125
+ save = confirmBox.find('.save');
91
126
 
92
- confirmBox.modal({ width: 500 });
127
+ const $altText = $('.alt-text', confirmBox).empty();
128
+ $altText
129
+ .append(document.createTextNode(`"${$alt.val()}"`))
130
+ .append($('<br>'))
131
+ .append(document.createTextNode(__('with')))
132
+ .append($('<br>'))
133
+ .append(document.createTextNode(`"${alt}" ?`));
93
134
 
94
- save.off('click').on('click', function () {
95
- img.attr('alt', alt);
96
- $alt.val(alt).trigger('change');
97
- confirmBox.modal('close');
98
- });
135
+ confirmBox.modal({ width: 500 });
99
136
 
100
- cancel.off('click').on('click', function () {
101
- confirmBox.modal('close');
102
- });
137
+ save.off('click').on('click', function () {
138
+ if (img) {
139
+ img.attr('alt', alt);
103
140
  }
141
+ $alt.val(alt).trigger('change');
142
+ confirmBox.modal('close');
143
+ });
104
144
 
105
- _.defer(function () {
106
- img.attr('off-media-editor', 1);
107
- $src.trigger('change');
108
- });
109
- }
145
+ cancel.off('click').on('click', function () {
146
+ confirmBox.modal('close');
147
+ });
110
148
  },
111
149
  open: function () {
112
150
  // hide tooltip if displayed
package/src/previewer.js CHANGED
@@ -227,7 +227,7 @@ const previewer = {
227
227
  // stop video, free the socket and remove player interface on video deletion
228
228
  // stop video and free the socket on all other cases when video is selected or temporary hidden or modal window is closed
229
229
  $controls = $(
230
- '.actions a:nth-child(1), .actions a:nth-child(3), .icon-close, .upload-switcher, .select-action, .files li>span',
230
+ '.actions a:nth-child(1), .actions a:nth-child(3), .icon-close, .upload-switcher, .select-action, .files-list tr, .files .desc',
231
231
  '#mediaManager'
232
232
  );
233
233
  $controls.off('mousedown.mediaelement').on('mousedown.mediaelement', function(event) {