diff --git a/.rubocop.yml b/.rubocop.yml index ad70526151..52001c4ea0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -229,6 +229,7 @@ RSpec/DescribeClass: Exclude: - 'spec/lib/tasks/blacklight_task_spec.rb' - 'spec/features/**/*' + - 'spec/requests/**/*' - 'spec/views/**/*' RSpec/ExcessiveDocstringSpacing: # new in 2.5 Enabled: true diff --git a/app/assets/javascripts/blacklight/blacklight.js b/app/assets/javascripts/blacklight/blacklight.js index 6e3d0368ec..f029b34ece 100644 --- a/app/assets/javascripts/blacklight/blacklight.js +++ b/app/assets/javascripts/blacklight/blacklight.js @@ -1,12 +1,8 @@ (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('typeahead.js/dist/bloodhound.js')) : - typeof define === 'function' && define.amd ? define(['typeahead.js/dist/bloodhound.js'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Blacklight = factory(global.Bloodhound)); -})(this, (function (Bloodhound) { 'use strict'; - - const _interopDefaultLegacy = e => e && typeof e === 'object' && 'default' in e ? e : { default: e }; - - const Bloodhound__default = /*#__PURE__*/_interopDefaultLegacy(Bloodhound); + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Blacklight = factory()); +})(this, (function () { 'use strict'; const Blacklight = function() { var buffer = new Array; @@ -61,41 +57,6 @@ elem.classList.add('js'); }); - const Autocomplete = (() => { - Blacklight.onLoad(function() { - - $('[data-autocomplete-enabled="true"]').each(function() { - var $el = $(this); - if($el.hasClass('tt-hint')) { - return; - } - var suggestUrl = $el.data().autocompletePath; - - var terms = new Bloodhound__default.default({ - datumTokenizer: Bloodhound__default.default.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound__default.default.tokenizers.whitespace, - remote: { - url: suggestUrl + '?q=%QUERY', - wildcard: '%QUERY' - } - }); - - terms.initialize(); - - $el.typeahead({ - hint: true, - highlight: true, - minLength: 2 - }, - { - name: 'terms', - displayKey: 'term', - source: terms.ttAdapter() - }); - }); - }); - })(); - /* Converts a "toggle" form, with single submit button to add/remove something, like used for Bookmarks, into an AJAXy checkbox instead. Apply to a form. Does require certain assumption about the form: @@ -381,7 +342,7 @@ this.type + ' ' + this.url + "\n" + jqXHR.status + ': ' + errorThrown + ''; $(Blacklight.modal.modalSelector).find('.modal-content').html(contents); - $(Blacklight.modal.modalSelector).modal('show'); + Blacklight.modal.show(); }; Blacklight.modal.receiveAjax = function (contents) { @@ -402,7 +363,7 @@ // if they did preventDefault, don't show the dialog if (e.isDefaultPrevented()) return; - $(Blacklight.modal.modalSelector).modal('show'); + Blacklight.modal.show(); }; @@ -463,7 +424,7 @@ if ($(event.target).find(Blacklight.modal.modalCloseSelector).length) { var modalFlashes = $(this).find('.flash_messages'); - $(event.target).modal('hide'); + Blacklight.modal.hide(event.target); event.preventDefault(); var mainFlashes = $('#main-flashes'); @@ -472,6 +433,22 @@ } }; + Blacklight.modal.hide = function(el) { + if (bootstrap.Modal.VERSION >= "5") { + bootstrap.Modal.getOrCreateInstance(el).hide(); + } else { + $(el || Blacklight.modal.modalSelector).modal('hide'); + } + }; + + Blacklight.modal.show = function(el) { + if (bootstrap.Modal.VERSION >= "5") { + bootstrap.Modal.getOrCreateInstance(el).show(); + } else { + $(el || Blacklight.modal.modalSelector).modal('show'); + } + }; + Blacklight.onLoad(function() { Blacklight.modal.setupModal(); }); @@ -540,7 +517,6 @@ })(); const index = { - Autocomplete, BookmarkToggle, ButtonFocus, FacetLoad, diff --git a/app/assets/javascripts/blacklight/blacklight.js.map b/app/assets/javascripts/blacklight/blacklight.js.map index 7f855bd874..a518517870 100644 --- a/app/assets/javascripts/blacklight/blacklight.js.map +++ b/app/assets/javascripts/blacklight/blacklight.js.map @@ -1 +1 @@ -{"version":3,"file":"blacklight.js","sources":["../../../javascript/blacklight/core.js","../../../javascript/blacklight/autocomplete.js","../../../javascript/blacklight/checkbox_submit.js","../../../javascript/blacklight/bookmark_toggle.js","../../../javascript/blacklight/button_focus.js","../../../javascript/blacklight/facet_load.js","../../../javascript/blacklight/modal.js","../../../javascript/blacklight/search_context.js","../../../javascript/blacklight/index.js"],"sourcesContent":["const Blacklight = function() {\n var buffer = new Array;\n return {\n onLoad: function(func) {\n buffer.push(func);\n },\n\n activate: function() {\n for(var i = 0; i < buffer.length; i++) {\n buffer[i].call();\n }\n },\n\n listeners: function () {\n var listeners = [];\n if (typeof Turbo !== 'undefined') {\n listeners.push('turbo:load');\n } else if (typeof Turbolinks !== 'undefined' && Turbolinks.supported) {\n // Turbolinks 5\n if (Turbolinks.BrowserAdapter) {\n listeners.push('turbolinks:load');\n } else {\n // Turbolinks < 5\n listeners.push('page:load', 'DOMContentLoaded');\n }\n } else {\n listeners.push('DOMContentLoaded');\n }\n\n return listeners;\n }\n };\n}();\n\n// turbolinks triggers page:load events on page transition\n// If app isn't using turbolinks, this event will never be triggered, no prob.\nBlacklight.listeners().forEach(function(listener) {\n document.addEventListener(listener, function() {\n Blacklight.activate()\n })\n})\n\nBlacklight.onLoad(function () {\n const elem = document.querySelector('.no-js');\n\n // The \"no-js\" class may already have been removed because this function is\n // run on every turbo:load event, in that case, it won't find an element.\n if (!elem) return;\n\n elem.classList.remove('no-js')\n elem.classList.add('js')\n})\n\n\nexport default Blacklight\n","import Bloodhound from 'typeahead.js/dist/bloodhound.js'\nimport Blacklight from './core'\n\nconst Autocomplete = (() => {\n Blacklight.onLoad(function() {\n 'use strict';\n\n $('[data-autocomplete-enabled=\"true\"]').each(function() {\n var $el = $(this);\n if($el.hasClass('tt-hint')) {\n return;\n }\n var suggestUrl = $el.data().autocompletePath;\n\n var terms = new Bloodhound({\n datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),\n queryTokenizer: Bloodhound.tokenizers.whitespace,\n remote: {\n url: suggestUrl + '?q=%QUERY',\n wildcard: '%QUERY'\n }\n });\n\n terms.initialize();\n\n $el.typeahead({\n hint: true,\n highlight: true,\n minLength: 2\n },\n {\n name: 'terms',\n displayKey: 'term',\n source: terms.ttAdapter()\n });\n });\n });\n})();\n\nexport default Autocomplete\n","/* Converts a \"toggle\" form, with single submit button to add/remove\n something, like used for Bookmarks, into an AJAXy checkbox instead.\n Apply to a form. Does require certain assumption about the form:\n 1) The same form 'action' href must be used for both ADD and REMOVE\n actions, with the different being the hidden input name=\"_method\"\n being set to \"put\" or \"delete\" -- that's the Rails method to pretend\n to be doing a certain HTTP verb. So same URL, PUT to add, DELETE\n to remove. This plugin assumes that.\n Plus, the form this is applied to should provide a data-doc-id\n attribute (HTML5-style doc-*) that contains the id/primary key\n of the object in question -- used by plugin for a unique value for\n DOM id's.\n Uses HTML for a checkbox compatible with Bootstrap 4.\n new CheckboxSubmit(document.querySelector('form.something')).render()\n*/\nexport default class CheckboxSubmit {\n constructor(form) {\n this.form = form\n this.cssClass = 'toggle-bookmark'\n\n //View needs to set data-doc-id so we know a unique value\n //for making DOM id\n const uniqueId = this.form.getAttribute('data-doc-id') || Math.random();\n const id = `${this.cssClass}_${uniqueId}`\n this.checkbox = this._buildCheckbox(this.cssClass, id)\n this.span = this._buildSpan()\n this.label = this._buildLabel(id, this.cssClass, this.checkbox, this.span)\n\n // if form is currently using method delete to change state,\n // then checkbox is currently checked\n this.checked = (this.form.querySelectorAll('input[name=_method][value=delete]').length != 0);\n }\n\n _buildCheckbox(cssClass, id) {\n const checkbox = document.createElement('input')\n checkbox.setAttribute('type', 'checkbox')\n checkbox.classList.add(cssClass)\n checkbox.id = id\n return checkbox\n }\n\n _buildLabel(id, cssClass, checkbox, span) {\n const label = document.createElement('label')\n label.classList.add(cssClass)\n label.for = id\n\n label.appendChild(checkbox)\n label.appendChild(document.createTextNode(' '))\n label.appendChild(span)\n return label\n }\n\n _buildSpan() {\n return document.createElement('span')\n }\n\n _buildCheckboxDiv() {\n const checkboxDiv = document.createElement('div')\n checkboxDiv.classList.add('checkbox')\n checkboxDiv.classList.add(this.cssClass)\n checkboxDiv.appendChild(this.label)\n return checkboxDiv\n }\n\n render() {\n const children = this.form.children\n Array.from(children).forEach((child) => child.classList.add('hidden'))\n\n //We're going to use the existing form to actually send our add/removes\n //This works conveneintly because the exact same action href is used\n //for both bookmarks/$doc_id. But let's take out the irrelevant parts\n //of the form to avoid any future confusion.\n this.form.querySelectorAll('input[type=submit]').forEach((el) => this.form.removeChild(el))\n this.form.appendChild(this._buildCheckboxDiv())\n this.updateStateFor(this.checked)\n\n this.checkbox.onclick = this._clicked.bind(this)\n }\n\n async _clicked(evt) {\n this.span.innerHTML = this.form.getAttribute('data-inprogress')\n this.label.setAttribute('disabled', 'disabled');\n this.checkbox.setAttribute('disabled', 'disabled');\n const response = await fetch(this.form.getAttribute('action'), {\n body: new FormData(this.form),\n method: this.form.getAttribute('method').toUpperCase(),\n headers: {\n 'Accept': 'application/json',\n 'X-Requested-With': 'XMLHttpRequest'\n }\n })\n this.label.removeAttribute('disabled')\n this.checkbox.removeAttribute('disabled')\n if (response.ok) {\n const json = await response.json()\n this.checked = !this.checked\n this.updateStateFor(this.checked)\n document.querySelector('[data-role=bookmark-counter]').innerHTML = json.bookmarks.count\n } else {\n alert('Error')\n }\n }\n\n updateStateFor(state) {\n this.checkbox.checked = state\n\n if (state) {\n this.label.classList.add('checked')\n //Set the Rails hidden field that fakes an HTTP verb\n //properly for current state action.\n this.form.querySelector('input[name=_method]').value = 'delete'\n this.span.innerHTML = this.form.getAttribute('data-present')\n } else {\n this.label.classList.remove('checked')\n this.form.querySelector('input[name=_method]').value = 'put'\n this.span.innerHTML = this.form.getAttribute('data-absent')\n }\n }\n}\n","import Blacklight from './core'\nimport CheckboxSubmit from './checkbox_submit'\n\nconst BookmarkToggle = (() => {\n // change form submit toggle to checkbox\n Blacklight.doBookmarkToggleBehavior = function() {\n document.querySelectorAll(Blacklight.doBookmarkToggleBehavior.selector).forEach((el) => {\n new CheckboxSubmit(el).render()\n })\n };\n Blacklight.doBookmarkToggleBehavior.selector = 'form.bookmark-toggle';\n\n Blacklight.onLoad(function() {\n Blacklight.doBookmarkToggleBehavior();\n });\n})()\n\nexport default BookmarkToggle\n","import Blacklight from './core'\n\nconst ButtonFocus = (() => {\n Blacklight.onLoad(function() {\n // Button clicks should change focus. As of 10/3/19, Firefox for Mac and\n // Safari both do not set focus to a button on button click.\n // See https://zellwk.com/blog/inconsistent-button-behavior/ for background information\n document.querySelectorAll('button.collapse-toggle').forEach((button) => {\n button.addEventListener('click', () => {\n event.target.focus();\n });\n });\n });\n})()\n\nexport default ButtonFocus\n","import Blacklight from './core'\n\nBlacklight.doResizeFacetLabelsAndCounts = function() {\n // adjust width of facet columns to fit their contents\n function longer (a,b) { return b.textContent.length - a.textContent.length }\n\n document.querySelectorAll('.facet-values, .pivot-facet').forEach(function(elem){\n const nodes = elem.querySelectorAll('.facet-count')\n // TODO: when we drop ie11 support, this can become the spread operator:\n const longest = Array.from(nodes).sort(longer)[0]\n if (longest && longest.textContent) {\n const width = longest.textContent.length + 1 + 'ch'\n elem.querySelector('.facet-count').style.width = width\n }\n })\n}\n\nconst FacetLoad = (() => {\n Blacklight.onLoad(function() {\n Blacklight.doResizeFacetLabelsAndCounts()\n })\n})()\n\nexport default FacetLoad\n","/*\n The blacklight modal plugin can display some interactions inside a Bootstrap\n modal window, including some multi-page interactions.\n\n It supports unobtrusive Javascript, where a link or form that would have caused\n a new page load is changed to display it's results inside a modal dialog,\n by this plugin. The plugin assumes there is a Bootstrap modal div\n on the page with id #blacklight-modal to use as the modal -- the standard Blacklight\n layout provides this.\n\n To make a link or form have their results display inside a modal, add\n `data-blacklight-modal=\"trigger\"` to the link or form. (Note, form itself not submit input)\n With Rails link_to helper, you'd do that like:\n\n link_to something, link, data: { blacklight_modal: \"trigger\" }\n\n The results of the link href or form submit will be displayed inside\n a modal -- they should include the proper HTML markup for a bootstrap modal's\n contents. Also, you ordinarily won't want the Rails template with wrapping\n navigational elements to be used. The Rails controller could suppress\n the layout when a JS AJAX request is detected, OR the response\n can include a `
Some message
\n <%= link_to \"This result will still be within modal\", some_link, data: { blacklight_modal: \"preserve\" } %>\nExpected a successful response from the server, but got an error
' +\n '' +\n this.type + ' ' + this.url + \"\\n\" + jqXHR.status + ': ' + errorThrown +\n '