From 00cfc77ad298f291c97432fac80d8698ceb8ed6f Mon Sep 17 00:00:00 2001 From: Jevon Wright Date: Tue, 7 Aug 2018 14:49:39 +1200 Subject: [PATCH 1/2] Show/hide modals when navigating with back/forward buttons Using pushState and popState event handlers, we can understand when the modal needs to be closed (and prevents the browser from navigating away), and when the modal needs to be reopened (using the new modal:reopen event). By default this is turned off, but can be enabled in the modal options with `updateHistory: true`. Note that as this stands, this doesn't work very well with Turbolinks: I'm not sure yet if this is a bug on the Turbolinks side or not, but in the meantime, this behaviour works for non-Turbolinks sites. --- README.md | 26 ++++++++++++++++++++++++++ jquery.modal.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/README.md b/README.md index e999680..2d9c65c 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,30 @@ Similar to how links can be automatically bound to open modals, they can be boun _(Note that modals loaded with AJAX are removed from the DOM when closed)._ +# Interacting with window.history + +By default, jquery-modal will not interact with the browser history: if you open a modal dialog, then clicking +the back button will not close the modal, but will rather take you back to the previous page. + +By setting the `updateHistory` option to `true`, and defining a `modal:reopen` event handler on your modal, +the browser history will be updated (using pushState/popState), and your users can use back/forward buttons. +For example: + +```js +$("#open-modal").on('click', function() { + var target = $("#my-modal"); + var options = { + // insert your other default options here + updateHistory: true, + }; + + target.on('modal:reopen', function() { + target.modal(options); + }); + target.modal(options); +} +``` + # Checking current state * Use `$.modal.isActive()` to check if a modal is currently being displayed. @@ -186,6 +210,7 @@ $.modal.defaults = { showClose: true, // Shows a (X) icon/link in the top-right corner modalClass: "modal", // CSS class added to the element being displayed in the modal. blockerClass: "modal", // CSS class added to the overlay (blocker). + updateHistory: false, // Whether to update the browser history, enabling back/forward buttons (must implement `modal:reopen` event). // HTML appended to the default spinner during AJAX requests. spinnerHtml: '
', @@ -208,6 +233,7 @@ $.modal.OPEN = 'modal:open'; // Fires after the modal has fin $.modal.BEFORE_CLOSE = 'modal:before-close'; // Fires when the modal has been requested to close. $.modal.CLOSE = 'modal:close'; // Fires when the modal begins closing (including animations). $.modal.AFTER_CLOSE = 'modal:after-close'; // Fires after the modal has fully closed (including animations). +$.modal.REOPEN = 'modal:reopen'; // Fires when navigation needs a modal to re-open (see above). ``` The first and only argument passed to these event handlers is the `modal` object, which has three properties: diff --git a/jquery.modal.js b/jquery.modal.js index 02c522d..a77ee9d 100644 --- a/jquery.modal.js +++ b/jquery.modal.js @@ -77,6 +77,31 @@ this.$body.append(this.$elm); this.open(); } + + if (this.options.updateHistory) { + // we can only have a single popstate handler for the entire document, otherwise + // callbacks will start to be duplicated, and the UI may start having bugs + if (!$.modal.handlePopstate) { + // popstate gets the _after_ state, not the _before_ state + $.modal.handlePopstate = function(event) { + if (event.state.no_modals) { + $.modal.close(); + } else if (event.state.modal) { + elm = $(event.state.modal); + elm.trigger($.modal.REOPEN, event); + } + }; + + $(window).on("popstate", function(jqueryEvent) { + $.modal.handlePopstate(jqueryEvent.originalEvent); + }); + } + + var copyState = history.state || {}; + copyState.no_modals = true; + copyState.modal = null; + history.pushState(copyState, "", ""); + } }; $.modal.prototype = { @@ -149,6 +174,17 @@ this.$elm.css('display', 'inline-block'); } this.$elm.trigger($.modal.OPEN, [this._ctx()]); + if (this.options.updateHistory && this.$elm.length == 1) { + var id = this.$elm[0].id; + if (typeof id !== 'undefined') { + var copyState = history.state || {}; + if (copyState.modal != "#" + id) { + copyState.no_modals = false; + copyState.modal = "#" + id; + history.pushState(copyState, "", ""); // ideally we'd set the location (third arg) to #id, but then we'd have to support opening modals on pageload from #id hash + } + } + } }, hide: function() { @@ -211,6 +247,7 @@ spinnerHtml: '
', showSpinner: true, showClose: true, + updateHistory: false, fadeDuration: null, // Number of milliseconds the fade animation takes. fadeDelay: 1.0 // Point during the overlay's fade-in that the modal begins to fade in (.5 = 50%, 1.5 = 150%, etc.) }; @@ -223,6 +260,7 @@ $.modal.BEFORE_CLOSE = 'modal:before-close'; $.modal.CLOSE = 'modal:close'; $.modal.AFTER_CLOSE = 'modal:after-close'; + $.modal.REOPEN = 'modal:reopen'; $.modal.AJAX_SEND = 'modal:ajax:send'; $.modal.AJAX_SUCCESS = 'modal:ajax:success'; $.modal.AJAX_FAIL = 'modal:ajax:fail'; From d786ff6a55c825773a8e1880fbfb83b6cd74ece3 Mon Sep 17 00:00:00 2001 From: Jevon Wright Date: Tue, 7 Aug 2018 16:37:28 +1200 Subject: [PATCH 2/2] Do not hide modal on back button if Turbolinks enabled There doesn't seem to be any way to prevent Turbolinks from reloading and replacing the entire browser window when a user clicks Back (since turbolinks:before-visit is not triggered for _when navigating by history_), So, rather than hiding the displayed modal when clicking Back (and then having to reload the page), we keep the Modal displayed while Turbolinks does its stuff in the background (including progress bar). See https://github.com/turbolinks/turbolinks/issues/264 for a related issue (suggested `turbolinks:before-history-change` event). --- jquery.modal.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/jquery.modal.js b/jquery.modal.js index a77ee9d..cb72199 100644 --- a/jquery.modal.js +++ b/jquery.modal.js @@ -85,7 +85,13 @@ // popstate gets the _after_ state, not the _before_ state $.modal.handlePopstate = function(event) { if (event.state.no_modals) { - $.modal.close(); + if (typeof Turbolinks != 'undefined' && Turbolinks.supported) { + // do nothing; Turbolinks will be reloading the page anyway, unfortunately, + // so rather than hiding a modal (suggesting the page is ready) and then refreshing + // the page, we keep the modal displayed while the page reloads + } else { + $.modal.close(); + } } else if (event.state.modal) { elm = $(event.state.modal); elm.trigger($.modal.REOPEN, event);