blacklight-spotlight 4.7.1 → 5.0.0.pre.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) 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 +35 -87
  137. data/app/assets/stylesheets/spotlight/#_accessibility.scss# +0 -12
  138. data/app/javascript/spotlight/admin/checkbox_submit.js +0 -75
  139. data/app/javascript/spotlight/admin/exhibit_tag_autocomplete.js +0 -39
  140. data/app/javascript/spotlight/user/report_a_problem.js +0 -30
  141. data/app/views/spotlight/browse/_tophat.html.erb +0 -1
  142. data/app/views/spotlight/catalog/_tophat_default.html.erb +0 -1
  143. data/app/views/spotlight/home_pages/_tophat.html.erb +0 -2
  144. data/app/views/spotlight/pages/_tophat.html.erb +0 -1
  145. data/lib/generators/spotlight/templates/spotlight.js +0 -1
  146. data/lib/generators/spotlight/templates/spotlight.scss +0 -5
  147. data/spec/support/features/capybara_default_max_wait_metadata_helper.rb +0 -20
  148. data/vendor/assets/javascripts/bootstrap-tagsinput.js +0 -530
  149. data/vendor/assets/javascripts/jquery.serializejson.js +0 -234
  150. data/vendor/assets/javascripts/nestable.js +0 -645
  151. data/vendor/assets/javascripts/sir-trevor.js +0 -23508
  152. data/vendor/assets/javascripts/typeahead.bundle.min.js +0 -7
  153. 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
  }