blacklight-spotlight 4.7.0 → 5.0.0.pre.alpha1

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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -12
  3. data/Rakefile +8 -1
  4. data/app/assets/javascripts/spotlight/application.js +0 -1
  5. data/app/assets/javascripts/spotlight/spotlight.esm.js +3620 -3847
  6. data/app/assets/javascripts/spotlight/spotlight.esm.js.map +1 -1
  7. data/app/assets/javascripts/spotlight/spotlight.js +3620 -3852
  8. data/app/assets/javascripts/spotlight/spotlight.js.map +1 -1
  9. data/app/assets/stylesheets/spotlight/_accessibility.scss +0 -9
  10. data/app/assets/stylesheets/spotlight/_autocomplete.scss +49 -0
  11. data/app/assets/stylesheets/spotlight/_blacklight_configuration.scss +0 -1
  12. data/app/assets/stylesheets/spotlight/_blacklight_overrides.scss +1 -6
  13. data/app/assets/stylesheets/spotlight/_browse.scss +2 -2
  14. data/app/assets/stylesheets/spotlight/_catalog.scss +40 -41
  15. data/app/assets/stylesheets/spotlight/_curation.scss +1 -1
  16. data/app/assets/stylesheets/spotlight/_exhibit_admin.scss +7 -0
  17. data/app/assets/stylesheets/spotlight/_exhibits_index.scss +8 -5
  18. data/app/assets/stylesheets/spotlight/_featured_browse_categories_block.scss +3 -3
  19. data/app/assets/stylesheets/spotlight/_header.scss +13 -0
  20. data/app/assets/stylesheets/spotlight/_mixins.scss +3 -4
  21. data/app/assets/stylesheets/spotlight/_nestable.scss +2 -12
  22. data/app/assets/stylesheets/spotlight/_pages.scss +11 -9
  23. data/app/assets/stylesheets/spotlight/_report_a_problem.scss +1 -3
  24. data/app/assets/stylesheets/spotlight/_sir-trevor_overrides.scss +2 -2
  25. data/app/assets/stylesheets/spotlight/_spotlight.scss +2 -1
  26. data/app/assets/stylesheets/spotlight/_tag_selector.scss +34 -0
  27. data/app/assets/stylesheets/spotlight/_variables.scss +0 -8
  28. data/app/components/spotlight/analytics/dashboard_component.html.erb +3 -3
  29. data/app/components/spotlight/breadcrumbs_component.html.erb +13 -19
  30. data/app/components/spotlight/bulk_action_component.rb +1 -1
  31. data/app/components/spotlight/document_component.rb +1 -1
  32. data/app/components/spotlight/save_search_component.rb +1 -1
  33. data/app/components/spotlight/select_image_component.html.erb +17 -0
  34. data/app/components/spotlight/select_image_component.rb +24 -0
  35. data/app/components/spotlight/skip_link_component.rb +16 -0
  36. data/app/components/spotlight/tag_selector_component.html.erb +40 -0
  37. data/app/components/spotlight/tag_selector_component.rb +41 -0
  38. data/app/components/spotlight/tag_selector_component.yml +6 -0
  39. data/app/components/spotlight/title_component.html.erb +8 -0
  40. data/app/components/spotlight/title_component.rb +22 -0
  41. data/app/controllers/spotlight/accessibility_controller.rb +2 -2
  42. data/app/controllers/spotlight/catalog_controller.rb +7 -2
  43. data/app/controllers/spotlight/contact_email_controller.rb +8 -2
  44. data/app/controllers/spotlight/languages_controller.rb +9 -4
  45. data/app/helpers/spotlight/application_helper.rb +7 -0
  46. data/app/helpers/spotlight/crop_helper.rb +4 -0
  47. data/app/helpers/spotlight/meta_helper.rb +59 -36
  48. data/app/javascript/spotlight/admin/blacklight_configuration.js +1 -1
  49. data/app/javascript/spotlight/admin/block_mixins/autocompleteable.js +70 -34
  50. data/app/javascript/spotlight/admin/blocks/block.js +1 -0
  51. data/app/javascript/spotlight/admin/blocks/browse_block.js +8 -12
  52. data/app/javascript/spotlight/admin/blocks/browse_group_categories_block.js +14 -18
  53. data/app/javascript/spotlight/admin/blocks/pages_block.js +6 -10
  54. data/app/javascript/spotlight/admin/blocks/resources_block.js +33 -15
  55. data/app/javascript/spotlight/admin/blocks/solr_documents_base_block.js +11 -6
  56. data/app/javascript/spotlight/admin/blocks/solr_documents_embed_block.js +1 -0
  57. data/app/javascript/spotlight/admin/blocks/uploaded_items_block.js +4 -3
  58. data/app/javascript/spotlight/admin/copy_email_addresses.js +2 -0
  59. data/app/javascript/spotlight/admin/crop.js +45 -17
  60. data/app/javascript/spotlight/admin/croppable.js +8 -1
  61. data/app/javascript/spotlight/admin/croppable_modal.js +68 -0
  62. data/app/javascript/spotlight/admin/exhibits.js +15 -10
  63. data/app/javascript/spotlight/admin/form_observer.js +1 -1
  64. data/app/javascript/spotlight/admin/index.js +0 -10
  65. data/app/javascript/spotlight/admin/locks.js +15 -5
  66. data/app/javascript/spotlight/admin/pages.js +1 -1
  67. data/app/javascript/spotlight/admin/search_typeahead.js +62 -55
  68. data/app/javascript/spotlight/admin/spotlight_nestable.js +173 -50
  69. data/app/javascript/spotlight/admin/visibility_toggle.js +1 -11
  70. data/app/javascript/spotlight/controllers/index.js +8 -0
  71. data/app/javascript/spotlight/controllers/tag_selector_controller.js +203 -0
  72. data/app/javascript/spotlight/core.js +4 -6
  73. data/app/javascript/spotlight/index.js +2 -0
  74. data/app/javascript/spotlight/user/browse_group_categories.js +2 -0
  75. data/app/javascript/spotlight/user/carousel.js +3 -1
  76. data/app/javascript/spotlight/user/index.js +0 -2
  77. data/app/models/sir_trevor_rails/block.rb +5 -4
  78. data/app/models/sir_trevor_rails/blocks/solr_documents_block.rb +1 -1
  79. data/app/models/sir_trevor_rails/blocks/solr_documents_embed_block.rb +1 -1
  80. data/app/models/sir_trevor_rails/blocks/uploaded_items_block.rb +1 -1
  81. data/app/models/spotlight/page_configurations.rb +1 -1
  82. data/app/views/catalog/_add_tags.html.erb +2 -2
  83. data/app/views/catalog/_change_visibility.html.erb +1 -1
  84. data/app/views/catalog/_remove_tags.html.erb +2 -2
  85. data/app/views/layouts/spotlight/base.html.erb +24 -13
  86. data/app/views/layouts/spotlight/spotlight.html.erb +6 -6
  87. data/app/views/shared/_masthead.html.erb +4 -31
  88. data/app/views/shared/_site_sidebar.html.erb +1 -1
  89. data/app/views/shared/_user_util_links.html.erb +3 -1
  90. data/app/views/spotlight/accessibility/alt_text.html.erb +2 -2
  91. data/app/views/spotlight/admin_users/index.html.erb +3 -3
  92. data/app/views/spotlight/appearances/edit.html.erb +1 -1
  93. data/app/views/spotlight/browse/_search_box.html.erb +8 -8
  94. data/app/views/spotlight/browse/show.html.erb +1 -1
  95. data/app/views/spotlight/bulk_updates/_download.html.erb +1 -1
  96. data/app/views/spotlight/bulk_updates/_upload.html.erb +1 -1
  97. data/app/views/spotlight/catalog/_admin_header.html.erb +1 -1
  98. data/app/views/spotlight/catalog/_edit_default.html.erb +2 -1
  99. data/app/views/spotlight/catalog/select_image.html.erb +1 -0
  100. data/app/views/spotlight/contacts/_form.html.erb +1 -1
  101. data/app/views/spotlight/exhibits/_contact.html.erb +5 -6
  102. data/app/views/spotlight/exhibits/_delete.html.erb +1 -1
  103. data/app/views/spotlight/exhibits/_languages.html.erb +3 -2
  104. data/app/views/spotlight/featured_images/_form.html.erb +6 -2
  105. data/app/views/spotlight/featured_images/_upload_form.html.erb +1 -1
  106. data/app/views/spotlight/metadata_configurations/_metadata_field.html.erb +1 -1
  107. data/app/views/spotlight/metadata_configurations/edit.html.erb +6 -6
  108. data/app/views/spotlight/pages/show.html.erb +1 -1
  109. data/app/views/spotlight/resources/csv_upload/_form.html.erb +1 -1
  110. data/app/views/spotlight/resources/upload/_form.html.erb +1 -1
  111. data/app/views/spotlight/roles/index.html.erb +1 -1
  112. data/app/views/spotlight/searches/_form.html.erb +1 -1
  113. data/app/views/spotlight/shared/_dd3_item.html.erb +1 -1
  114. data/app/views/spotlight/sir_trevor/blocks/_browse_group_categories_block.html.erb +1 -1
  115. data/app/views/spotlight/sir_trevor/blocks/_solr_documents_block.html.erb +1 -1
  116. data/app/views/spotlight/sir_trevor/blocks/_solr_documents_carousel_block.html.erb +1 -1
  117. data/app/views/spotlight/sir_trevor/blocks/_uploaded_items_block.html.erb +1 -1
  118. data/app/views/spotlight/tags/index.html.erb +2 -3
  119. data/app/views/spotlight/translations/_import.html.erb +2 -2
  120. data/config/importmap.rb +5 -0
  121. data/config/locales/spotlight.en.yml +2 -0
  122. data/config/routes.rb +5 -3
  123. data/lib/generators/spotlight/assets/generator_common_utilities.rb +36 -0
  124. data/lib/generators/spotlight/assets/importmap_generator.rb +87 -0
  125. data/lib/generators/spotlight/assets/propshaft_generator.rb +96 -0
  126. data/lib/generators/spotlight/assets_generator.rb +22 -0
  127. data/lib/generators/spotlight/install_generator.rb +8 -36
  128. data/lib/generators/spotlight/scaffold_resource_generator.rb +1 -1
  129. data/lib/generators/spotlight/templates/assets/spotlight.scss +6 -0
  130. data/lib/generators/spotlight/templates/javascript/jquery-shim.js +1 -0
  131. data/lib/spotlight/engine.rb +7 -6
  132. data/lib/spotlight/version.rb +1 -1
  133. data/spec/support/features/capybara_wait_metadata_helper.rb +13 -0
  134. data/spec/support/features/test_features_helpers.rb +16 -30
  135. data/vendor/assets/javascripts/tiny-slider.js +3 -0
  136. metadata +36 -87
  137. data/app/javascript/spotlight/admin/checkbox_submit.js +0 -75
  138. data/app/javascript/spotlight/admin/exhibit_tag_autocomplete.js +0 -39
  139. data/app/javascript/spotlight/user/report_a_problem.js +0 -30
  140. data/app/views/spotlight/browse/_tophat.html.erb +0 -1
  141. data/app/views/spotlight/catalog/_tophat_default.html.erb +0 -1
  142. data/app/views/spotlight/home_pages/_tophat.html.erb +0 -2
  143. data/app/views/spotlight/pages/_tophat.html.erb +0 -1
  144. data/lib/generators/spotlight/templates/spotlight.js +0 -1
  145. data/lib/generators/spotlight/templates/spotlight.scss +0 -5
  146. data/spec/support/features/capybara_default_max_wait_metadata_helper.rb +0 -20
  147. data/vendor/assets/javascripts/bootstrap-tagsinput.js +0 -530
  148. data/vendor/assets/javascripts/jquery.serializejson.js +0 -234
  149. data/vendor/assets/javascripts/nestable.js +0 -645
  150. data/vendor/assets/javascripts/sir-trevor.js +0 -23508
  151. data/vendor/assets/javascripts/typeahead.bundle.min.js +0 -7
  152. data/vendor/assets/stylesheets/bootstrap-tagsinput.css +0 -46
@@ -1,66 +1,73 @@
1
1
  import { addImageSelector } from 'spotlight/admin/add_image_selector'
2
2
 
3
- (function($){
4
- $.fn.spotlightSearchTypeAhead = function( options ) {
5
- $.each(this, function(){
6
- addAutocompleteBehavior($(this));
7
- });
3
+ const docStore = new Map();
8
4
 
9
- function addAutocompleteBehavior( typeAheadInput, _ ) {
10
- var settings = $.extend({
11
- displayKey: 'title',
12
- minLength: 0,
13
- highlight: (typeAheadInput.data('autocomplete-highlight') || true),
14
- hint: (typeAheadInput.data('autocomplete-hint') || false),
15
- autoselect: (typeAheadInput.data('autocomplete-autoselect') || true)
16
- }, options);
17
- typeAheadInput.typeahead(settings, {
18
- displayKey: settings.displayKey,
19
- source: settings.bloodhound.ttAdapter(),
20
- templates: {
21
- suggestion: settings.template
22
- }
23
- })
24
- }
25
- return this;
26
- }
27
- })( jQuery );
5
+ function highlight(value, query) {
6
+ if (query.trim() === '') return value;
7
+ const queryValue = query.trim();
8
+ return queryValue ? value.replace(new RegExp(queryValue, 'gi'), '<strong>$&</strong>') : value;
9
+ }
28
10
 
29
- function itemsBloodhound() {
30
- var results = new Bloodhound({
31
- datumTokenizer: function(d) {
32
- return Bloodhound.tokenizers.whitespace(d.title);
33
- },
34
- queryTokenizer: Bloodhound.tokenizers.whitespace,
35
- limit: 100,
36
- remote: {
37
- url: $('form[data-autocomplete-exhibit-catalog-path]').data('autocomplete-exhibit-catalog-path').replace("%25QUERY", "%QUERY"),
38
- filter: function(response) {
39
- return $.map(response['docs'], function(doc) {
40
- return doc;
41
- })
42
- }
43
- }
44
- });
45
- results.initialize();
46
- return results;
47
- };
11
+ function templateFunc(obj, query) {
12
+ const thumbnail = obj.thumbnail ? `<div class="document-thumbnail"><img class="img-thumbnail" src="${obj.thumbnail}" /></div>` : '';
13
+ const privateClass = obj.private ? ' blacklight-private' : '';
14
+ const title = highlight(obj.title, query);
15
+ const description = obj.description ? `<small>&nbsp;&nbsp;${highlight(obj.description, query)}</small>` : '';
16
+ return `<div class="autocomplete-item${privateClass}">${thumbnail}
17
+ <span class="autocomplete-title">${title}</span><br/>${description}
18
+ </div>`;
19
+ }
20
+
21
+ function autoCompleteElementTemplate(obj, query) {
22
+ return `<li role="option" data-autocomplete-value="${obj.id}">${templateFunc(obj, query)}</li>`;
23
+ }
24
+
25
+ function getAutoCompleteElementDataMap(autoCompleteElement) {
26
+ if (!docStore.has(autoCompleteElement.id)) {
27
+ docStore.set(autoCompleteElement.id, new Map());
28
+ }
29
+ return docStore.get(autoCompleteElement.id);
30
+ }
48
31
 
49
- function templateFunc(obj) {
50
- const thumbnail = obj.thumbnail ? `<div class="document-thumbnail"><img class="img-thumbnail" src="${obj.thumbnail}" /></div>` : ''
51
- return $(`<div class="autocomplete-item${obj.private ? ' blacklight-private' : ''}">${thumbnail}
52
- <span class="autocomplete-title">${obj.title}</span><br/><small>&nbsp;&nbsp;${obj.description}</small></div>`)
32
+ async function fetchResult(url) {
33
+ const result = await fetchAutocompleteJSON(url);
34
+ const docs = result.docs || [];
35
+ const query = this.querySelector('input').value || '';
36
+ const autoCompleteElementDataMap = getAutoCompleteElementDataMap(this);
37
+ return docs.map(doc => {
38
+ autoCompleteElementDataMap.set(doc.id, doc);
39
+ return autoCompleteElementTemplate(doc, query);
40
+ }).join('');
53
41
  }
54
42
 
55
43
  export function addAutocompletetoFeaturedImage(){
56
- if($('[data-featured-image-typeahead]').length > 0) {
57
- $('[data-featured-image-typeahead]').spotlightSearchTypeAhead({bloodhound: itemsBloodhound(), template: templateFunc}).on('click', function() {
58
- $(this).select();
59
- }).on('typeahead:selected typeahead:autocompleted', function(e, data) {
60
- var panel = $($(this).data('target-panel'));
61
- addImageSelector($(this), panel, data.iiif_manifest, true);
62
- $($(this).data('id-field')).val(data['global_id']);
63
- $(this).attr('type', 'text');
44
+ const autocompletePath = $('form[data-autocomplete-exhibit-catalog-path]').data('autocomplete-exhibit-catalog-path');
45
+ const featuredImageTypeaheads = $('[data-featured-image-typeahead]');
46
+ if (featuredImageTypeaheads.length === 0) return;
47
+
48
+ $.each(featuredImageTypeaheads, function(index, autoCompleteInput) {
49
+ const autoCompleteElement = autoCompleteInput.closest('auto-complete');
50
+
51
+ autoCompleteElement.setAttribute('src', autocompletePath);
52
+ autoCompleteElement.fetchResult = fetchResult;
53
+ autoCompleteElement.addEventListener('auto-complete-change', e => {
54
+ const data = getAutoCompleteElementDataMap(autoCompleteElement).get(e.relatedTarget.value);
55
+ if (!data) return;
56
+
57
+ const inputElement = $(e.relatedTarget);
58
+ const panel = document.querySelector(e.relatedTarget.dataset.targetPanel);
59
+ e.relatedTarget.value = data.title;
60
+ addImageSelector(inputElement, $(panel), data.iiif_manifest, true);
61
+ $(inputElement.data('id-field')).val(data['global_id']);
62
+ inputElement.attr('type', 'text');
64
63
  });
64
+ });
65
+ }
66
+
67
+ export async function fetchAutocompleteJSON(url) {
68
+ const res = await(fetch(url.toString()));
69
+ if (!res.ok) {
70
+ throw new Error(await res.text());
65
71
  }
72
+ return await res.json();
66
73
  }
@@ -1,68 +1,191 @@
1
+ import Sortable from 'sortablejs';
2
+
1
3
  const Module = (function() {
2
- var nestableSelector = '[data-behavior="nestable"]';
3
- return {
4
- init: function(selector){
5
-
6
- $(selector || nestableSelector).each(function(){
7
- // Because the Rails helper will not maintain the case that Nestable
8
- // expects, we just need to do this manual conversion. :(
9
- var data = $(this).data();
10
- data.expandBtnHTML = data.expandBtnHtml;
11
- data.collapseBtnHTML = data.collapseBtnHtml;
12
- $(this).nestable(data);
13
- updateWeightsAndRelationships($(this));
14
- });
4
+ const nestableContainerSelector = '[data-behavior="nestable"]';
5
+ const sortableOptions = {
6
+ animation: 150,
7
+ draggable: '.dd-item',
8
+ handle: '.dd-handle',
9
+ fallbackOnBody: true,
10
+ swapThreshold: 0.65,
11
+ emptyInsertThreshold: 15,
12
+ onStart: onStartHandler,
13
+ onEnd: onEndHandler,
14
+ onMove: onMoveHandler,
15
+ }
16
+ const draggableClass = 'dd-item';
17
+ const nestedSortableClass = 'dd-list';
18
+ const nestedSortableSelector = '.dd-list';
19
+ const nestedSortableNodeName = 'ol';
20
+ const findNode = (id, container) => container.querySelector(`[data-id="${id}"]`);
21
+ const setWeight = (node, weight) => weightField(node).value = weight;
22
+ const setParent = (node, parentId) => parentPageField(node).value = parentId;
23
+ const weightField = node => findProperty(node, "weight");
24
+ const parentPageField = node => findProperty(node, "parent_page");
25
+ const findProperty = (node, property) => node.querySelector(`input[data-property="${property}"]`);
26
+ let nestedId = 0;
27
+
28
+ return {
29
+ init: function(nestedContainers) {
30
+ if (nestedContainers === undefined) {
31
+ nestedContainers = document.querySelectorAll(nestableContainerSelector);
15
32
  }
16
- };
17
- function updateWeightsAndRelationships(nestedList){
18
- nestedList.on('change', function(event){
19
- var container = $(event.currentTarget);
20
- var data = $(this).nestable('serialize');
21
- var weight = 0;
22
- for(var i in data){
23
- var parent_id = data[i]['id'];
24
- const parent_node = findNode(parent_id, container);
25
- setWeight(parent_node, weight++);
26
- if(data[i]['children']){
27
- var children = data[i]['children'];
28
- for(var child in children){
29
- var id = children[child]['id']
30
- var child_node = findNode(id, container);
31
- setWeight(child_node, weight++);
32
- setParent(child_node, parent_id);
33
- }
34
- } else {
35
- setParent(parent_node, "");
36
- }
37
- }
38
- });
39
33
 
34
+ // nestedContainers could be a jQuery selector result, normalize to an array.
35
+ const containersToInit = Array.from(nestedContainers);
36
+ containersToInit.forEach((container) => {
37
+ // Sir Trevor listens for drag and drop events and will error on Sortable events.
38
+ // Don't let them bubble past the Sortable wrapper.
39
+ container.addEventListener('drop', stopPropagationHandler);
40
+
41
+ const nestedSortables = [
42
+ ...(container.matches(nestedSortableSelector) ? [container] : []),
43
+ ...Array.from(container.querySelectorAll(nestedSortableSelector))
44
+ ];
45
+ const group = `nested-${nestedId++}`;
46
+
47
+ nestedSortables.forEach(sortable => {
48
+ new Sortable(sortable, { ...sortableOptions, group: group });
49
+ });
50
+ });
40
51
  }
41
- function findNode(id, container) {
42
- return container.find("[data-id="+id+"]");
52
+ };
53
+
54
+ function stopPropagationHandler(evt) {
55
+ evt.stopPropagation();
56
+ }
57
+
58
+ function onStartHandler(evt) {
59
+ makeEmptyChildSortablesForEligibleParents(getNestableContainer(evt.item), getMaxNestingLevelSetting(evt.item));
60
+ }
61
+
62
+ function onEndHandler(evt) {
63
+ const nestableContainer = getNestableContainer(evt.item);
64
+ removeEmptySortables(nestableContainer);
65
+ updateWeightsAndRelationships(nestableContainer);
66
+ }
67
+
68
+ function onMoveHandler(evt) {
69
+ // The usage of data-max-depth is one off of the standard notion of depth (# edges to root)
70
+ // E.g., data-max-depth=2 allows for one level of nesting.
71
+ // evt.dragged is a draggable in a Sortable (e.g., a dd-item)
72
+ // evt.to is the Sortable to insert into (e.g., a dd-list)
73
+ const maxAllowedDepth = getMaxNestingLevelSetting(evt.to) - 1;
74
+ const newDepth = getSortableDepth(evt.to) + getHeight(evt.dragged);
75
+
76
+ // Be careful here. Returning true is different than returning nothing in SortableJS.
77
+ if (newDepth > maxAllowedDepth) {
78
+ return false;
43
79
  }
80
+ }
81
+
82
+ // Get the depth of the sortable element from the root container
83
+ function getSortableDepth(sortableElement) {
84
+ const originatingGroup = Sortable.get(sortableElement).options.group.name;
85
+ let depth = 0;
86
+ let parentSortableElement = sortableElement;
44
87
 
45
- function setWeight(node, weight) {
46
- weight_field(node).val(weight);
88
+ while ((parentSortableElement = parentSortableElement.parentElement.closest(nestedSortableSelector))) {
89
+ const parentSortable = Sortable.get(parentSortableElement);
90
+ if (parentSortable?.options.group.name === originatingGroup) {
91
+ depth++;
92
+ }
47
93
  }
48
94
 
49
- function setParent(node, parent_id) {
50
- parent_page_field(node).val(parent_id);
95
+ return depth;
96
+ }
97
+
98
+ // Find the max child depth in the tree, starting from the draggableElement
99
+ function findMaxDepth(draggableElement) {
100
+ const childSortableElement = draggableElement.querySelector(nestedSortableSelector);
101
+ if (!childSortableElement) {
102
+ return 1;
51
103
  }
52
104
 
53
- /* find the input element with data-property="weight" that is nested under the given node */
54
- function weight_field(node) {
55
- return find_property(node, "weight");
105
+ const children = childSortableElement.querySelectorAll(`.${draggableClass}`);
106
+ const childDepths = Array.from(children).map(findMaxDepth);
107
+ return 1 + Math.max(0, ...childDepths);
108
+ }
109
+
110
+ function getHeight(draggableElement) {
111
+ return findMaxDepth(draggableElement) - 1;
112
+ }
113
+
114
+ function getNestableContainer(element) {
115
+ return element.closest(nestableContainerSelector);
116
+ }
117
+
118
+ function getMaxNestingLevelSetting(element) {
119
+ return getNestableContainer(element).getAttribute('data-max-depth') || 1;
120
+ }
121
+
122
+ // Create empty child sortables for all potential parents as appropriate for the given nesting level
123
+ function makeEmptyChildSortablesForEligibleParents(container, nestingLevel) {
124
+ if (nestingLevel <= 1) {
125
+ return;
56
126
  }
57
127
 
58
- /* find the input element with data-property="parent_page" that is nested under the given node */
59
- function parent_page_field(node){
60
- return find_property(node, "parent_page");
128
+ const sortableElement = container.querySelector(nestedSortableSelector);
129
+ const sortable = Sortable.get(sortableElement);
130
+ if (!sortable) {
131
+ return;
61
132
  }
62
133
 
63
- function find_property(node, property) {
64
- return node.find("input[data-property=" + property + "]");
134
+ const group = sortable.options.group.name;
135
+ const draggableElements = Array.from(sortableElement.children)
136
+ .filter(child => child.classList.contains(draggableClass));
137
+
138
+ draggableElements.forEach(draggableElement => {
139
+ if (!draggableElement.querySelector(nestedSortableSelector)) {
140
+ const emptySortableElement = document.createElement(nestedSortableNodeName);
141
+ emptySortableElement.className = nestedSortableClass;
142
+ draggableElement.appendChild(emptySortableElement);
143
+ new Sortable(emptySortableElement, { ...sortableOptions, group: group });
144
+ }
145
+ makeEmptyChildSortablesForEligibleParents(draggableElement, nestingLevel - 1);
146
+ });
147
+ }
148
+
149
+ // Remove any empty sortables within the container. They could be empty lists, which are invalid for accessibility.
150
+ function removeEmptySortables(container) {
151
+ const sortableElements = container.querySelectorAll(nestedSortableSelector);
152
+ sortableElements.forEach(sortableElement => {
153
+ if (sortableElement.innerHTML.trim() === '') {
154
+ const sortable = Sortable.get(sortableElement);
155
+ if (sortable) {
156
+ sortable.destroy();
157
+ sortableElement.remove();
158
+ }
159
+ }
160
+ });
161
+ }
162
+
163
+ // Traverse all sortables within a container and update the weight and parent_page inputs
164
+ function updateWeightsAndRelationships(container) {
165
+ const sortableElement = container.matches(nestedSortableSelector) ? container : container.querySelector(nestedSortableSelector);
166
+ const nestingLevelSetting = getMaxNestingLevelSetting(sortableElement);
167
+ const sortable = Sortable.get(sortableElement);
168
+ const stack = [{nodes: sortable.toArray(), parentId: ''}];
169
+ let weight = 0;
170
+
171
+ while (stack.length > 0) {
172
+ const {nodes, parentId} = stack.pop();
173
+
174
+ nodes.forEach((nodeId) => {
175
+ const node = findNode(nodeId, container);
176
+ setWeight(node, weight++);
177
+
178
+ if (nestingLevelSetting > 1) {
179
+ setParent(node, parentId);
180
+ const children = node.querySelector(nestedSortableSelector);
181
+ if (children) {
182
+ const sortableElement = Sortable.get(children);
183
+ stack.push({nodes: sortableElement.toArray(), parentId: nodeId});
184
+ }
185
+ }
186
+ });
65
187
  }
188
+ }
66
189
  })();
67
190
 
68
191
  export default Module
@@ -1,23 +1,13 @@
1
- // Visibility toggle for items in an exhibit, based on Blacklight's bookmark toggle
2
- // See: https://github.com/projectblacklight/blacklight/blob/main/app/javascript/blacklight/bookmark_toggle.js
3
-
4
- import CheckboxSubmit from 'spotlight/admin/checkbox_submit'
5
-
1
+ // Blacklight's BookmarkToggle is doing the real work, this only adds/removes the "blacklight-private" class.
6
2
  const VisibilityToggle = (e) => {
7
3
  if (e.target.matches('[data-checkboxsubmit-target="checkbox"]')) {
8
4
  const form = e.target.closest('form')
9
5
  if (form) {
10
- if (!Blacklight.BookmarkToggle) new CheckboxSubmit(form).clicked(e)
11
-
12
6
  // Add/remove the "private" label to the document row when visibility is toggled
13
7
  const docRow = form.closest('tr')
14
8
  if (docRow) docRow.classList.toggle('blacklight-private')
15
9
  }
16
10
  }
17
11
  }
18
-
19
- VisibilityToggle.selector = 'form.visibility-toggle'
20
-
21
12
  document.addEventListener('click', VisibilityToggle)
22
-
23
13
  export default VisibilityToggle
@@ -0,0 +1,8 @@
1
+ import TagSelectorController from 'spotlight/controllers/tag_selector_controller'
2
+
3
+ export default class {
4
+ connect() {
5
+ if (typeof Stimulus === "undefined") return
6
+ Stimulus.register('tag-selector', TagSelectorController)
7
+ }
8
+ }
@@ -0,0 +1,203 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+
5
+ static targets = [
6
+ 'addNewTagWrapper',
7
+ 'dropdownContent',
8
+ 'initialTags',
9
+ 'newTag',
10
+ 'searchResultTags',
11
+ 'selectedTags',
12
+ 'tagControlWrapper',
13
+ 'tagSearch',
14
+ 'tagsField',
15
+ 'tagSearchDropdown',
16
+ 'tagSearchInputWrapper'
17
+ ]
18
+
19
+ static values = {
20
+ tags: Array,
21
+ translations: Object
22
+ }
23
+
24
+ tagDropdown (event) {
25
+ this.dropdownContentTarget.classList.toggle('d-none')
26
+ }
27
+
28
+ clickOutside (event) {
29
+ const isShown = !this.dropdownContentTarget.classList.contains('d-none')
30
+ const inSelected = event.target.classList.contains('pill-close')
31
+ const inContainer = this.tagControlWrapperTarget.contains(event.target)
32
+ if (!inContainer && !inSelected && isShown) {
33
+ this.tagDropdown(event)
34
+ }
35
+ }
36
+
37
+ handleKeydown (event) {
38
+ if (event.key === 'Enter') {
39
+ event.preventDefault()
40
+ const hidden = this.dropdownContentTarget.classList.contains('d-none')
41
+ if (hidden) return;
42
+
43
+ const tagElementToAdd = this.dropdownContentTarget.querySelector('.active')?.firstElementChild
44
+ if (tagElementToAdd) tagElementToAdd.click()
45
+ }
46
+
47
+ if (event.key === ',') {
48
+ event.preventDefault()
49
+ if (this.tagSearchTarget.value.length === 0) return
50
+
51
+ if (!this.addNewTagWrapperTarget.classList.contains('d-none')) {
52
+ this.addNewTagWrapperTarget.click()
53
+ this.tagSearchTarget.focus()
54
+ return
55
+ }
56
+
57
+ const exactMatch = this.dropdownContentTarget.querySelector('.active')?.firstElementChild
58
+ if (exactMatch?.checked === false) {
59
+ exactMatch.click()
60
+ this.resetSearch()
61
+ }
62
+ this.tagSearchTarget.focus()
63
+ }
64
+ }
65
+
66
+ addNewTag (event) {
67
+ if (this.addNewTagWrapperTarget.classList.contains('d-none') || this.newTagTarget.dataset.tag.length === 0) {
68
+ return
69
+ }
70
+
71
+ this.tagsValue = this.tagsValue.concat([this.newTagTarget.dataset.tag])
72
+ this.resetSearch()
73
+ }
74
+
75
+ resetSearch() {
76
+ this.tagSearchTarget.value = ''
77
+ this.newTagTarget.innerHTML = ''
78
+ this.newTagTarget.dataset.tag = ''
79
+ this.newTagTarget.disabled = true
80
+ this.addNewTagWrapperTarget.classList.add('d-none')
81
+ this.searchResultTagsTargets.forEach(target => this.showElement(target.parentElement))
82
+ }
83
+
84
+ tagUpdate (event) {
85
+ const target = event.target ? event.target : event
86
+ if (target.checked) {
87
+ this.tagsValue = this.tagsValue.concat([target.dataset.tag])
88
+ } else {
89
+ this.tagsValue = this.tagsValue.filter(tag => tag !== target.dataset.tag)
90
+ }
91
+ }
92
+
93
+ updateSearchResultsPlaceholder(event) {
94
+ const placeholderElement = this.dropdownContentTarget.querySelector('.no-results')
95
+ if (!placeholderElement) return
96
+
97
+ const hasVisibleTags = this.dropdownContentTarget.querySelector('label:not(.d-none):not(.no-results)')
98
+ placeholderElement.classList.toggle('d-none', hasVisibleTags)
99
+ }
100
+
101
+ tagCreate(event) {
102
+ event.preventDefault()
103
+ const newTagCheckbox = document.createElement('label')
104
+ newTagCheckbox.innerHTML = `<input type="checkbox" checked data-action="click->${this.identifier}#tagUpdate" data-tag-selector-target="searchResultTags" data-tag="${this.newTagTarget.dataset.tag}"> ${this.newTagTarget.dataset.tag}`
105
+ const existingTags = Array.from(this.dropdownContentTarget.querySelectorAll('label:not(#add-new-tag-wrapper)'))
106
+ const insertPosition = existingTags.findIndex(tag => tag.textContent.trim().localeCompare(this.newTagTarget.dataset.tag) > 0)
107
+ if (insertPosition === -1) {
108
+ this.addNewTagWrapperTarget.insertAdjacentElement('beforebegin', newTagCheckbox)
109
+ } else {
110
+ existingTags[insertPosition].insertAdjacentElement('beforebegin', newTagCheckbox)
111
+ }
112
+
113
+ this.tagsValue = this.tagsValue.concat([this.newTagTarget.dataset.tag])
114
+ this.tagSearchTarget.value = ''
115
+ this.tagSearchTarget.dispatchEvent(new Event('input'))
116
+ }
117
+
118
+
119
+ tagsValueChanged() {
120
+ const isEmpty = this.tagsValue.length === 0
121
+
122
+ this.selectedTagsTarget.classList.toggle('d-none', isEmpty)
123
+ this.tagSearchInputWrapperTarget.classList.toggle('rounded', isEmpty)
124
+ this.tagSearchInputWrapperTarget.classList.toggle('rounded-bottom', !isEmpty)
125
+
126
+ if (!isEmpty) {
127
+ this.selectedTagsTarget.innerHTML = `<ul class="list-unstyled border rounded-top mb-0 p-1 px-2">${this.renderTagPills()}</ul>`
128
+ }
129
+
130
+ const newValue = this.tagsValue.join(', ')
131
+ if (this.tagsFieldTarget.value !== newValue) {
132
+ this.tagsFieldTarget.value = newValue
133
+ }
134
+ }
135
+
136
+ normalizeTag (tag) {
137
+ const normalizeRegex = /[^\w\s]/gi
138
+ return tag.replace(normalizeRegex, '').toLowerCase().trim()
139
+ }
140
+
141
+ showElement (element) {
142
+ element.classList.add('d-block')
143
+ element.classList.remove('d-none')
144
+ }
145
+
146
+ hideElement (element) {
147
+ element.classList.remove('d-block')
148
+ element.classList.add('d-none')
149
+ }
150
+
151
+ search(event) {
152
+ const searchTerm = this.normalizeTag(event.target.value)
153
+ this.dropdownContentTarget.classList.remove('d-none')
154
+
155
+ const exactMatch = this.searchResultTagsTargets.some(target => {
156
+ const compareTerm = this.normalizeTag(target.dataset.tag)
157
+ const isMatch = compareTerm.includes(searchTerm)
158
+ target.parentElement.classList.remove('active')
159
+ this[isMatch ? 'showElement' : 'hideElement'](target.parentElement)
160
+ return compareTerm === searchTerm
161
+ })
162
+
163
+ this[searchTerm.length > 0 && !exactMatch ? 'showElement' : 'hideElement'](this.addNewTagWrapperTarget)
164
+ this.addNewTagWrapperTarget.classList.remove('active')
165
+ this.dropdownContentTarget.querySelector('label:not(.d-none)')?.classList.add('active')
166
+ }
167
+
168
+ updateTagToAdd (event) {
169
+ const tagAlreadyAdded = this.tagsValue.some(tag =>
170
+ this.normalizeTag(tag) === this.normalizeTag(event.target.value)
171
+ )
172
+ this.newTagTarget.dataset.tag = event.target.value.trim()
173
+ this.newTagTarget.nextSibling.textContent = ` ${this.translationsValue.add_new_tag}: ${event.target.value}`
174
+ this.newTagTarget.disabled = !this.newTagTarget.dataset.tag.length || tagAlreadyAdded
175
+ }
176
+
177
+ deselect (event) {
178
+ event.preventDefault()
179
+
180
+ const clickedTag = event.target.closest('button').dataset.tag
181
+ const target = this.searchResultTagsTargets.find((tag) => tag.dataset.tag === clickedTag)
182
+ target ? target.click() : this.tagsValue = this.tagsValue.filter(tag => tag !== clickedTag)
183
+ }
184
+
185
+ renderTagPills () {
186
+ return this.tagsValue.map((tag) => {
187
+ return `
188
+ <li class="d-inline-flex gap-2 align-items-center my-2">
189
+ <span class="bg-light badge rounded-pill border selected-item d-inline-flex align-items-center text-dark">
190
+ <span class="selected-item-label d-inline-flex">${tag}</span>
191
+ <button
192
+ type="button"
193
+ data-action="${this.identifier}#deselect"
194
+ data-tag="${tag}"
195
+ class="btn-close close ms-1 ml-1"
196
+ aria-label="${this.translationsValue.remove} ${tag}"
197
+ ><span aria-hidden="true" class="visually-hidden">&times;</span></button>
198
+ </span>
199
+ </li>
200
+ `
201
+ }).join('')
202
+ }
203
+ }
@@ -1,3 +1,5 @@
1
+ import SirTrevor from "sir-trevor"
2
+
1
3
  const Spotlight = function() {
2
4
  var buffer = [];
3
5
  return {
@@ -6,6 +8,7 @@ const Spotlight = function() {
6
8
  },
7
9
 
8
10
  activate: function() {
11
+ this.sirTrevorIcon = window.sirTrevorIcon;
9
12
  for(var i = 0; i < buffer.length; i++) {
10
13
  buffer[i].call();
11
14
  }
@@ -23,10 +26,5 @@ const Spotlight = function() {
23
26
 
24
27
  // This allows us to configure Spotlight in app/views/layouts/base.html.erb
25
28
  window.Spotlight = Spotlight
26
-
29
+ window.SirTrevor = SirTrevor
27
30
  export default Spotlight
28
-
29
- Blacklight.onLoad(function() {
30
- Spotlight.activate();
31
- });
32
-
@@ -1,8 +1,10 @@
1
1
  import UserIndex from 'spotlight/user'
2
2
  import AdminIndex from 'spotlight/admin'
3
3
  import Core from 'spotlight/core'
4
+ import SpotlightControllers from 'spotlight/controllers'
4
5
 
5
6
  Core.onLoad(() => {
7
+ new SpotlightControllers().connect()
6
8
  new UserIndex().connect()
7
9
  new AdminIndex().connect()
8
10
  })
@@ -1,3 +1,5 @@
1
+ import tns from 'tiny-slider'
2
+
1
3
  export default class {
2
4
  connect() {
3
5
  var $container, slider;
@@ -1,5 +1,7 @@
1
1
  export default class {
2
2
  connect() {
3
- $('.carousel').carousel();
3
+ if ($.fn.carousel) {
4
+ $('.carousel').carousel();
5
+ }
4
6
  }
5
7
  }