app/javascript/blacklight/modal.js in blacklight-7.40.0 vs app/javascript/blacklight/modal.js in blacklight-8.0.0.beta1

- old
+ new

@@ -21,16 +21,10 @@ the layout when a JS AJAX request is detected, OR the response can include a `<div data-blacklight-modal="container">` -- only the contents of the container will be placed inside the modal, the rest of the page will be ignored. - If you'd like to have a link or button that closes the modal, - you can just add a `data-dismiss="modal"` to the link, - standard Bootstrap convention. But you can also have - an href on this link for non-JS contexts, we'll make sure - inside the modal it closes the modal and the link is NOT followed. - Link or forms inside the modal will ordinarily cause page loads when they are triggered. However, if you'd like their results to stay within the modal, just add `data-blacklight-modal="preserve"` to the link or form. @@ -47,180 +41,121 @@ <%= link_to "This result will still be within modal", some_link, data: { blacklight_modal: "preserve" } %> </div> <div class="modal-footer"> - <%= link_to "Close the modal", request_done_path, class: "submit button dialog-close", data: { dismiss: "modal" } %> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> </div> </div> - One additional feature. If the content returned from the AJAX modal load - has an element with `data-blacklight-modal=close`, that will trigger the modal - to be closed. And if this element includes a node with class "flash_messages", - the flash-messages node will be added to the main page inside #main-flahses. - - == Events - - We'll send out an event 'loaded.blacklight.blacklight-modal' with the #blacklight-modal - dialog as the target, right after content is loaded into the modal but before - it is shown (if not already a shown modal). In an event handler, you can - inspect loaded content by looking inside $(this). If you call event.preventDefault(), - we won't 'show' the dialog (although it may already have been shown, you may want to - $(this).modal("hide") if you want to ensure hidden/closed. - - The data-blacklight-modal=close behavior is implemented with this event, see for example. + One additional feature. If the content returned from the AJAX form submission + can be a turbo-stream that defines some HTML fragementsand where on the page to put them: + https://turbo.hotwired.dev/handbook/streams */ +import Blacklight from './core' +import ModalForm from './modalForm' -// We keep all our data in Blacklight.modal object. -// Create lazily if someone else created first. -if (Blacklight.modal === undefined) { - Blacklight.modal = {}; -} +const Modal = (() => { + // We keep all our data in Blacklight.modal object. + // Create lazily if someone else created first. + if (Blacklight.modal === undefined) { + Blacklight.modal = {}; + } -// a Bootstrap modal div that should be already on the page hidden -Blacklight.modal.modalSelector = '#blacklight-modal'; + const modal = Blacklight.modal -// Trigger selectors identify forms or hyperlinks that should open -// inside a modal dialog. -Blacklight.modal.triggerLinkSelector = 'a[data-blacklight-modal~=trigger]'; -Blacklight.modal.triggerFormSelector = 'form[data-blacklight-modal~=trigger]'; + // a Bootstrap modal div that should be already on the page hidden + modal.modalSelector = '#blacklight-modal'; -// preserve selectors identify forms or hyperlinks that, if activated already -// inside a modal dialog, should have destinations remain inside the modal -- but -// won't trigger a modal if not already in one. -// -// No need to repeat selectors from trigger selectors, those will already -// be preserved. MUST be manually prefixed with the modal selector, -// so they only apply to things inside a modal. -Blacklight.modal.preserveLinkSelector = Blacklight.modal.modalSelector + ' a[data-blacklight-modal~=preserve]'; + // Trigger selectors identify forms or hyperlinks that should open + // inside a modal dialog. + modal.triggerLinkSelector = 'a[data-blacklight-modal~=trigger]'; -Blacklight.modal.containerSelector = '[data-blacklight-modal~=container]'; + // preserve selectors identify forms or hyperlinks that, if activated already + // inside a modal dialog, should have destinations remain inside the modal -- but + // won't trigger a modal if not already in one. + // + // No need to repeat selectors from trigger selectors, those will already + // be preserved. MUST be manually prefixed with the modal selector, + // so they only apply to things inside a modal. + modal.preserveLinkSelector = modal.modalSelector + ' a[data-blacklight-modal~=preserve]'; -Blacklight.modal.modalCloseSelector = '[data-blacklight-modal~=close]'; + modal.containerSelector = '[data-blacklight-modal~=container]'; -// Called on fatal failure of ajax load, function returns content -// to show to user in modal. Right now called only for extreme -// network errors. -Blacklight.modal.onFailure = function(jqXHR, textStatus, errorThrown) { - console.error('Server error:', this.url, jqXHR.status, errorThrown); + // Called on fatal failure of ajax load, function returns content + // to show to user in modal. Right now called only for extreme + // network errors. + modal.onFailure = function (jqXHR, textStatus, errorThrown) { + console.error('Server error:', this.url, jqXHR.status, errorThrown); - var contents = '<div class="modal-header">' + - '<div class="modal-title">There was a problem with your request.</div>' + - '<button type="button" class="blacklight-modal-close btn-close close" data-dismiss="modal" aria-label="Close">' + - ' <span aria-hidden="true">&times;</span>' + - '</button></div>' + - ' <div class="modal-body"><p>Expected a successful response from the server, but got an error</p>' + - '<pre>' + - this.type + ' ' + this.url + "\n" + jqXHR.status + ': ' + errorThrown + - '</pre></div>'; - $(Blacklight.modal.modalSelector).find('.modal-content').html(contents); - Blacklight.modal.show(); -} + const contents = `<div class="modal-header"> + <div class="modal-title">There was a problem with your request.</div> + <button type="button" class="blacklight-modal-close btn-close close" data-dismiss="modal" data-bs-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">&times;</span> + </button> + </div> + <div class="modal-body"> + <p>Expected a successful response from the server, but got an error</p> + <pre>${this.type} ${this.url}\n${jqXHR.status}: ${errorThrown}</pre> + </div>` -Blacklight.modal.receiveAjax = function (contents) { - // does it have a data- selector for container? - // important we don't execute script tags, we shouldn't. - // code modelled off of JQuery ajax.load. https://github.com/jquery/jquery/blob/main/src/ajax/load.js?source=c#L62 - var container = $('<div>'). - append( jQuery.parseHTML(contents) ).find( Blacklight.modal.containerSelector ).first(); - if (container.length !== 0) { - contents = container.html(); - } + document.querySelector(`${modal.modalSelector} .modal-content`).innerHTML = contents - $(Blacklight.modal.modalSelector).find('.modal-content').html(contents); + modal.show(); + } - // send custom event with the modal dialog div as the target - var e = $.Event('loaded.blacklight.blacklight-modal') - $(Blacklight.modal.modalSelector).trigger(e); - // if they did preventDefault, don't show the dialog - if (e.isDefaultPrevented()) return; + // Add the passed in contents to the modal and display it. +modal.receiveAjax = function (contents) { + const domparser = new DOMParser(); + const dom = domparser.parseFromString(contents, "text/html") + const elements = dom.querySelectorAll(`${modal.containerSelector} > *`) + document.querySelector(`${modal.modalSelector} .modal-content`).replaceChildren(...elements) - Blacklight.modal.show(); -}; + modal.show(); + }; -Blacklight.modal.modalAjaxLinkClick = function(e) { - e.preventDefault(); - - $.ajax({ - url: $(this).attr('href') - }) - .fail(Blacklight.modal.onFailure) - .done(Blacklight.modal.receiveAjax) -}; - -Blacklight.modal.modalAjaxFormSubmit = function(e) { + modal.modalAjaxLinkClick = function(e) { e.preventDefault(); + const href = e.target.getAttribute('href') + fetch(href) + .then(response => { + if (!response.ok) { + throw new TypeError("Request failed"); + } + return response.text(); + }) + .then(data => modal.receiveAjax(data)) + .catch(error => modal.onFailure(error)) + }; - $.ajax({ - url: $(this).attr('action'), - data: $(this).serialize(), - type: $(this).attr('method') // POST + modal.setupModal = function() { + // Register both trigger and preserve selectors in ONE event handler, combining + // into one selector with a comma, so if something matches BOTH selectors, it + // still only gets the event handler called once. + document.addEventListener('click', (e) => { + if (e.target.matches(`${modal.triggerLinkSelector}, ${modal.preserveLinkSelector}`)) + modal.modalAjaxLinkClick(e) + else if (e.target.matches('[data-bl-dismiss="modal"]')) + modal.hide() }) - .fail(Blacklight.modal.onFailure) - .done(Blacklight.modal.receiveAjax) -} + }; + modal.hide = function (el) { + const dom = document.querySelector(Blacklight.modal.modalSelector) - -Blacklight.modal.setupModal = function() { - // Event indicating blacklight is setting up a modal link, - // you can catch it and call e.preventDefault() to abort - // setup. - var e = $.Event('setup.blacklight.blacklight-modal'); - $('body').trigger(e); - if (e.isDefaultPrevented()) return; - - // Register both trigger and preserve selectors in ONE event handler, combining - // into one selector with a comma, so if something matches BOTH selectors, it - // still only gets the event handler called once. - $('body').on('click', Blacklight.modal.triggerLinkSelector + ', ' + Blacklight.modal.preserveLinkSelector, - Blacklight.modal.modalAjaxLinkClick); - $('body').on('submit', Blacklight.modal.triggerFormSelector + ', ' + Blacklight.modal.preserveFormSelector, - Blacklight.modal.modalAjaxFormSubmit); - - // Catch our own custom loaded event to implement data-blacklight-modal=closed - $('body').on('loaded.blacklight.blacklight-modal', Blacklight.modal.checkCloseModal); - - // we support doing data-dismiss=modal on a <a> with a href for non-ajax - // use, we need to suppress following the a's href that's there for - // non-JS contexts. - $('body').on('click', Blacklight.modal.modalSelector + ' a[data-dismiss~=modal]', function (e) { - e.preventDefault(); - }); -}; - -// A function used as an event handler on loaded.blacklight.blacklight-modal -// to catch contained data-blacklight-modal=closed directions -Blacklight.modal.checkCloseModal = function(event) { - if ($(event.target).find(Blacklight.modal.modalCloseSelector).length) { - var modalFlashes = $(this).find('.flash_messages'); - - Blacklight.modal.hide(event.target); - event.preventDefault(); - - var mainFlashes = $('#main-flashes'); - mainFlashes.append(modalFlashes); - modalFlashes.fadeIn(500); + if (!dom.open) return + dom.close() } -} -Blacklight.modal.hide = function(el) { - if (typeof bootstrap !== 'undefined' && typeof bootstrap.Modal !== 'undefined' && bootstrap.Modal.VERSION >= "5") { - bootstrap.Modal.getOrCreateInstance(el || document.querySelector(Blacklight.modal.modalSelector)).hide(); - } else { - $(el || Blacklight.modal.modalSelector).modal('hide'); - } -} + modal.show = function(el) { + const dom = document.querySelector(Blacklight.modal.modalSelector) -Blacklight.modal.show = function(el) { - if (typeof bootstrap !== 'undefined' && typeof bootstrap.Modal !== 'undefined' && bootstrap.Modal.VERSION >= "5") { - bootstrap.Modal.getOrCreateInstance(el || document.querySelector(Blacklight.modal.modalSelector)).show(); - } else { - $(el || Blacklight.modal.modalSelector).modal('show'); + if (dom.open) return + dom.showModal() } -} -Blacklight.onLoad(function() { - Blacklight.modal.setupModal(); -}); + modal.setupModal() +})() + +export default Modal