diff --git a/assets/css/form/off-canvas.form.css b/assets/css/form/off-canvas.form.css index 01da91d7680bf155df286e0b351db65eff5adb7c..c87f1a5b66f2d9e08d5d3a3b5e0036ef4f824625 100644 --- a/assets/css/form/off-canvas.form.css +++ b/assets/css/form/off-canvas.form.css @@ -5,6 +5,14 @@ * @see core/misc/dialog/off-canvas.form.css */ +.offcanvas-end { + left: auto !important; +} + +.offcanvas-bottom { + top: auto !important; +} + /* Remove form-text input styles applied on description. */ #drupal-off-canvas:not(.drupal-off-canvas-reset) .description.form-text, #drupal-off-canvas-wrapper .description.form-text { diff --git a/assets/js/misc/dialog/dialog.ajax.js b/assets/js/misc/dialog/dialog.ajax.js index 94acfb7e659ad35c402edbd7a7af517a7f72d0cb..b846aa26d8b111e148ba26b2b4bd91087140f9a9 100644 --- a/assets/js/misc/dialog/dialog.ajax.js +++ b/assets/js/misc/dialog/dialog.ajax.js @@ -30,8 +30,8 @@ const $dialog = $context.closest('#drupal-modal'); if ($dialog.length) { - const dialogSettings = $dialog.closest('.modal').data('settings'); - if (dialogSettings && dialogSettings.drupalAutoButtons) { + const drupalAutoButtons = $dialog.data('drupal-auto-buttons'); + if (drupalAutoButtons) { $dialog.trigger('dialogButtonsChange'); } } @@ -102,16 +102,57 @@ dialogUrlAjax.execute(); }; + function openOffCanvasDialog(ajax, response, status) { + if (!response.selector) { + return false; + } + let $dialog = $(response.selector); + if (!$dialog.length) { + $dialog = $( + `<div id="${response.selector.replace( + /^#/, + '', + )}" class="offcanvas" tabindex="-1" role="dialog"></div>`, + ) + .appendTo('body'); + } + + // Set up the wrapper, if there isn't one. + if (!ajax.wrapper) { + ajax.wrapper = $dialog.attr('id'); + } + + // Use the ajax.js insert command to populate the dialog contents. + response.command = 'insert'; + response.method = 'html'; + if ( + response.dialogOptions.modalDialogWrapBody === undefined || + response.dialogOptions.modalDialogWrapBody === true || + response.dialogOptions.modalDialogWrapBody === 'true' + ) { + response.data = `<div class="offcanvas-body">${response.data}</div>`; + } + ajax.commands.insert(ajax, response, status); + + // Open the dialog itself. + response.dialogOptions = response.dialogOptions || {}; + const dialog = Drupal.uiSuiteOffCanvas($dialog.get(0), response.dialogOptions); + if (response.dialogOptions.modal) { + dialog.showModal(); + } else { + dialog.show(); + } + + // Add the standard Drupal class for buttons for style consistency. + $dialog.parent().find('.ui-dialog-buttonset').addClass('form-actions'); + }; + Drupal.AjaxCommands.prototype.coreOpenDialog = Drupal.AjaxCommands.prototype.openDialog; Drupal.AjaxCommands.prototype.openDialog = (ajax, response, status) => { if (ajax.dialogRenderer === 'off_canvas') { - return Drupal.AjaxCommands.prototype.coreOpenDialog( - ajax, - response, - status, - ); + return openOffCanvasDialog(ajax, response, status); } if (!response.selector) { @@ -167,6 +208,7 @@ response.dialogOptions.drupalAutoButtons = !!response.dialogOptions.drupalAutoButtons; } + if ( !response.dialogOptions.buttons && response.dialogOptions.drupalAutoButtons @@ -174,7 +216,7 @@ response.dialogOptions.buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog); } - + $dialog.data('drupal-auto-buttons', response.dialogOptions.drupalAutoButtons) // Bind dialogButtonsChange. $dialog.on('dialogButtonsChange', () => { const buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog); @@ -199,7 +241,11 @@ Drupal.AjaxCommands.prototype.closeDialog = (ajax, response, status) => { const $dialog = $(response.selector); if ($dialog.length) { - Drupal.uiSuiteDialog($dialog.get(0)).close(); + if ($dialog.hasClass('offcanvas')) { + Drupal.uiSuiteOffCanvas($dialog.get(0)).close(); + } else { + Drupal.uiSuiteDialog($dialog.get(0)).close(); + } } $dialog.off('dialogButtonsChange'); @@ -214,9 +260,9 @@ }; // eslint-disable-next-line - $(window).on("dialog:aftercreate", (e, dialog, $element, settings) => { + $(window).on('dialog:aftercreate', (e, dialog, $element, settings) => { // eslint-disable-next-line - $element.on("click.dialog", ".dialog-cancel", (e) => { + $element.on('click.dialog', '.dialog-cancel', (e) => { dialog.close('cancel'); e.preventDefault(); e.stopPropagation(); diff --git a/assets/js/misc/dialog/dialog.js b/assets/js/misc/dialog/dialog.js index d4f270b471a8c4eedae9e8a0965fc1353fdd37cb..de9365e8218474d39b0f469bdd943ff2e4618ff5 100644 --- a/assets/js/misc/dialog/dialog.js +++ b/assets/js/misc/dialog/dialog.js @@ -91,6 +91,10 @@ const modalFooter = $( '<div class="modal-footer"><div class="ui-dialog-buttonpane"></div><div class="ui-dialog-buttonset d-flex justify-content-end flex-grow-1"></div>', ); + const $footer = $('.modal-dialog .modal-content .modal-footer', $element); + if ($footer.length > 0) { + $($footer).find('.ui-dialog-buttonset').empty(); + } // eslint-disable-next-line func-names $.each(buttons, function () { @@ -118,15 +122,13 @@ ) { $(button).addClass(classes.join(' ')); } - - $(modalFooter).find('.ui-dialog-buttonset').append(button); + if ($footer.length > 0) { + $($footer).find('.ui-dialog-buttonset').append(button); + } else { + $(modalFooter).find('.ui-dialog-buttonset').append(button); + } }); - if ( - $('.modal-dialog .modal-content .modal-footer', $element).length > 0 - ) { - $('.modal-dialog .modal-content .modal-footer', $element).remove(); - } - if ($(modalFooter).html().length > 0) { + if ($(modalFooter).html().length > 0 && $footer.length === 0) { $(modalFooter).appendTo($('.modal-dialog .modal-content', $element)); } } @@ -198,11 +200,13 @@ } function closeDialog(value) { + domElement.dispatchEvent(new DrupalDialogEvent('beforeclose', dialog)); if ($element.modal !== undefined) { $element.modal('hide'); } dialog.returnValue = value; dialog.open = false; + domElement.dispatchEvent(new DrupalDialogEvent('afterclose', dialog)); } dialog.updateButtons = (buttons) => { diff --git a/assets/js/misc/dialog/dialog.off-canvas.js b/assets/js/misc/dialog/dialog.off-canvas.js new file mode 100644 index 0000000000000000000000000000000000000000..2fe0751f377b936c68a396385b07a91bc3f0e0c7 --- /dev/null +++ b/assets/js/misc/dialog/dialog.off-canvas.js @@ -0,0 +1,199 @@ +/** + * @file + * Dialog API inspired by HTML5 dialog element. + * + * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#the-dialog-element + */ + +(($, Drupal, drupalSettings) => { + /** + * Default dialog options. + * + * @type {object} + * + * @prop {bool} [autoOpen=true] + * @prop {bool} [autoResize=undefined] + * @prop {bool} [backdrop=undefined] + * @prop {object} [classes=undefined] TODO + * @prop {function} close + * @prop {string} [dialogClasses=''] + * @prop {string} [dialogHeadingLevel=5] + * @prop {string} [dialogShowHeader=true] + * @prop {string} [dialogShowHeaderTitle=true] + * @prop {string} [dialogStatic=false] + * @prop {bool} [drupalAutoButtons=undefined] + * @prop {bool} [drupalOffCanvasPosition='side'] + * @prop {bool} [resizable=undefined] + * @prop {string} [title=undefined] + * @prop {string} [width=undefined] + */ + drupalSettings.offCanvas = { + autoOpen: true, + autoResize: undefined, + backdrop: undefined, + classes: undefined, + close: function close(event) { + Drupal.uiSuiteDialog(event.target).close(); + Drupal.detachBehaviors(event.target, null, "unload"); + }, + dialogHeadingLevel: 5, + dialogShowHeader: true, + dialogShowHeaderTitle: true, + dialogStatic: false, + drupalAutoButtons: undefined, + drupalOffCanvasPosition: "side", + resizable: undefined, + title: undefined, + width: undefined, + }; + + /** + * @typedef {object} Drupal.dialog~dialogDefinition + * + * @prop {boolean} open + * Is the dialog open or not. + * @prop {*} returnValue + * Return value of the dialog. + * @prop {function} show + * Method to display the dialog on the page. + * @prop {function} showModal + * Method to display the dialog as a modal on the page. + * @prop {function} close + * Method to hide the dialog from the page. + */ + + /** + * Polyfill HTML5 dialog element with jQueryUI. + * + * @param {HTMLElement} element + * The element that holds the dialog. + * @param {object} options + * jQuery UI options to be passed to the dialog. + * + * @return {Drupal.dialog~dialogDefinition} + * The dialog instance. + */ + Drupal.uiSuiteOffCanvas = (element, options) => { + let undef; + + const $element = $(element); + const domElement = $element.get(0); + + const dialog = { + open: false, + returnValue: undef, + }; + + options = $.extend({}, drupalSettings.offCanvas, options); + + function settingIsTrue(setting) { + return setting !== undefined && (setting === true || setting === "true"); + } + + function openDialog(settings) { + settings = $.extend({}, options, settings); + + const event = new DrupalDialogEvent("beforecreate", dialog, settings); + domElement.dispatchEvent(event); + dialog.open = true; + settings = event.settings; + + // Position + if (settings.drupalOffCanvasPosition === "side") { + $element.addClass("offcanvas-end"); + } else if (settings.drupalOffCanvasPosition === "top") { + $element.addClass("offcanvas-top"); + } else if (settings.drupalOffCanvasPosition === "bottom") { + $element.addClass("offcanvas-bottom"); + } + + // Classes + if (settings.classes) { + if (settings.classes['ui-dialog']) { + $element.addClass(settings.classes['ui-dialog']); + } + if (settings.classes['ui-dialog-content']) { + $('.offcanvas-body', $element).addClass(settings.classes['ui-dialog-content']); + } + } + + // The modal dialog header. + if (settingIsTrue(settings.dialogShowHeader)) { + let modalHeader = '<div class="offcanvas-header">'; + const heading = settings.dialogHeadingLevel; + + if (settingIsTrue(settings.dialogShowHeaderTitle)) { + modalHeader += `<h${heading} class="offcanvas-title" id="offcanvasLabel">${settings.title}</h${heading}>`; + } + + modalHeader += `<button type="button" class="close btn-close" data-bs-dismiss="offcanvas" aria-label="${Drupal.t( + "Close", + )}"></button></div>`; + + $(modalHeader).prependTo($element); + } + + if (settingIsTrue(settings.dialogStatic)) { + $element.attr("data-bs-backdrop", "static"); + } + + if (!settingIsTrue(settings.backdrop)) { + $element.attr("data-bs-scroll", "true"); + } + + if ($element.offcanvas !== undefined) { + $element.offcanvas(settings); + $element.offcanvas("show"); + } + + if (settings.width) { + $element.css( + "--bs-offcanvas-width", + typeof settings.width === "number" + ? `${settings.width}px` + : settings.width, + ); + } + + if ($element.resizable !== undefined && settings.resizable) { + $element.resizable({ + handles: "w", + }); + } + + domElement.dispatchEvent( + new DrupalDialogEvent("aftercreate", dialog, settings), + ); + } + + function closeDialog(value) { + if ($element.modal !== undefined) { + $element.offcanvas("hide"); + } + dialog.returnValue = value; + dialog.open = false; + } + + dialog.show = () => { + openDialog({ backdrop: false }); + }; + dialog.showModal = () => { + openDialog({ backdrop: true }); + }; + dialog.close = () => { + closeDialog({}); + }; + + $element.on("hide.bs.offcanvas", () => { + domElement.dispatchEvent(new DrupalDialogEvent("beforeclose", dialog)); + }); + + $element.on("hidden.bs.offcanvas", () => { + domElement.dispatchEvent(new DrupalDialogEvent("afterclose", dialog)); + }); + + return dialog; + }; + + Drupal.behaviors.offCanvasEvents = {}; +})(jQuery, Drupal, drupalSettings); diff --git a/ui_suite_bootstrap.libraries.yml b/ui_suite_bootstrap.libraries.yml index 4579c5cfb5196fcc30f41de3ca84e6084bcc6ce5..138d29b2cafe8bb777c92de5bd6d4b47fbcc853e 100644 --- a/ui_suite_bootstrap.libraries.yml +++ b/ui_suite_bootstrap.libraries.yml @@ -52,6 +52,12 @@ drupal.dialog.ajax: - core/drupal.ajax drupal.dialog.off_canvas: + js: + assets/js/misc/dialog/dialog.off-canvas.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupalSettings css: component: assets/css/form/off-canvas.button.css: {}