Chris@14: /** Chris@14: * @file Chris@14: * Drupal's off-canvas library. Chris@14: */ Chris@14: Chris@14: (($, Drupal, debounce, displace) => { Chris@14: /** Chris@14: * Off-canvas dialog implementation using jQuery Dialog. Chris@14: * Chris@14: * Transforms the regular dialogs created using Drupal.dialog when the dialog Chris@14: * element equals '#drupal-off-canvas' into an side-loading dialog. Chris@14: * Chris@14: * @namespace Chris@14: */ Chris@14: Drupal.offCanvas = { Chris@17: /** Chris@17: * Storage for position information about the tray. Chris@17: * Chris@17: * @type {?String} Chris@17: */ Chris@17: position: null, Chris@17: Chris@17: /** Chris@17: * The minimum height of the tray when opened at the top of the page. Chris@17: * Chris@17: * @type {Number} Chris@17: */ Chris@17: minimumHeight: 30, Chris@14: Chris@14: /** Chris@14: * The minimum width to use body displace needs to match the width at which Chris@14: * the tray will be 100% width. @see core/misc/dialog/off-canvas.css Chris@14: * Chris@14: * @type {Number} Chris@14: */ Chris@14: minDisplaceWidth: 768, Chris@14: Chris@14: /** Chris@14: * Wrapper used to position off-canvas dialog. Chris@14: * Chris@14: * @type {jQuery} Chris@14: */ Chris@14: $mainCanvasWrapper: $('[data-off-canvas-main-canvas]'), Chris@14: Chris@14: /** Chris@14: * Determines if an element is an off-canvas dialog. Chris@14: * Chris@14: * @param {jQuery} $element Chris@14: * The dialog element. Chris@14: * Chris@14: * @return {bool} Chris@14: * True this is currently an off-canvas dialog. Chris@14: */ Chris@14: isOffCanvas($element) { Chris@14: return $element.is('#drupal-off-canvas'); Chris@14: }, Chris@14: Chris@14: /** Chris@14: * Remove off-canvas dialog events. Chris@14: * Chris@14: * @param {jQuery} $element Chris@14: * The target element. Chris@14: */ Chris@14: removeOffCanvasEvents($element) { Chris@14: $element.off('.off-canvas'); Chris@14: $(document).off('.off-canvas'); Chris@14: $(window).off('.off-canvas'); Chris@14: }, Chris@14: Chris@14: /** Chris@14: * Handler fired before an off-canvas dialog has been opened. Chris@14: * Chris@14: * @param {Object} settings Chris@14: * Settings related to the composition of the dialog. Chris@14: * Chris@14: * @return {undefined} Chris@14: */ Chris@14: beforeCreate({ settings, $element }) { Chris@14: // Clean up previous dialog event handlers. Chris@14: Drupal.offCanvas.removeOffCanvasEvents($element); Chris@14: Chris@14: $('body').addClass('js-off-canvas-dialog-open'); Chris@14: // @see http://api.jqueryui.com/position/ Chris@14: settings.position = { Chris@14: my: 'left top', Chris@14: at: `${Drupal.offCanvas.getEdge()} top`, Chris@14: of: window, Chris@14: }; Chris@14: Chris@14: /** Chris@17: * Applies initial height and with to dialog based depending on position. Chris@14: * @see http://api.jqueryui.com/dialog for all dialog options. Chris@14: */ Chris@17: const position = settings.drupalOffCanvasPosition; Chris@17: const height = position === 'side' ? $(window).height() : settings.height; Chris@17: const width = position === 'side' ? settings.width : '100%'; Chris@17: settings.height = height; Chris@17: settings.width = width; Chris@14: }, Chris@14: Chris@14: /** Chris@14: * Handler fired after an off-canvas dialog has been closed. Chris@14: * Chris@14: * @return {undefined} Chris@14: */ Chris@14: beforeClose({ $element }) { Chris@14: $('body').removeClass('js-off-canvas-dialog-open'); Chris@14: // Remove all *.off-canvas events Chris@14: Drupal.offCanvas.removeOffCanvasEvents($element); Chris@17: Drupal.offCanvas.resetPadding(); Chris@14: }, Chris@14: Chris@14: /** Chris@14: * Handler fired when an off-canvas dialog has been opened. Chris@14: * Chris@14: * @param {jQuery} $element Chris@14: * The off-canvas dialog element. Chris@14: * @param {Object} settings Chris@14: * Settings related to the composition of the dialog. Chris@14: * Chris@14: * @return {undefined} Chris@14: */ Chris@14: afterCreate({ $element, settings }) { Chris@14: const eventData = { settings, $element, offCanvasDialog: this }; Chris@14: Chris@14: $element Chris@17: .on( Chris@17: 'dialogContentResize.off-canvas', Chris@17: eventData, Chris@17: Drupal.offCanvas.handleDialogResize, Chris@17: ) Chris@17: .on( Chris@17: 'dialogContentResize.off-canvas', Chris@17: eventData, Chris@17: Drupal.offCanvas.bodyPadding, Chris@17: ); Chris@14: Chris@17: Drupal.offCanvas Chris@17: .getContainer($element) Chris@17: .attr(`data-offset-${Drupal.offCanvas.getEdge()}`, ''); Chris@14: Chris@14: $(window) Chris@17: .on( Chris@17: 'resize.off-canvas', Chris@17: eventData, Chris@17: debounce(Drupal.offCanvas.resetSize, 100), Chris@17: ) Chris@14: .trigger('resize.off-canvas'); Chris@14: }, Chris@14: Chris@14: /** Chris@14: * Toggle classes based on title existence. Chris@14: * Called with Drupal.offCanvas.afterCreate. Chris@14: * Chris@14: * @param {Object} settings Chris@14: * Settings related to the composition of the dialog. Chris@14: * Chris@14: * @return {undefined} Chris@14: */ Chris@14: render({ settings }) { Chris@17: $( Chris@17: '.ui-dialog-off-canvas, .ui-dialog-off-canvas .ui-dialog-titlebar', Chris@17: ).toggleClass('ui-dialog-empty-title', !settings.title); Chris@14: }, Chris@14: Chris@14: /** Chris@14: * Adjusts the dialog on resize. Chris@14: * Chris@14: * @param {jQuery.Event} event Chris@14: * The event triggered. Chris@14: * @param {object} event.data Chris@14: * Data attached to the event. Chris@14: */ Chris@14: handleDialogResize(event) { Chris@14: const $element = event.data.$element; Chris@14: const $container = Drupal.offCanvas.getContainer($element); Chris@14: Chris@17: const $offsets = $container.find( Chris@17: '> :not(#drupal-off-canvas, .ui-resizable-handle)', Chris@17: ); Chris@14: let offset = 0; Chris@14: Chris@14: // Let scroll element take all the height available. Chris@14: $element.css({ height: 'auto' }); Chris@14: const modalHeight = $container.height(); Chris@14: Chris@14: $offsets.each((i, e) => { Chris@14: offset += $(e).outerHeight(); Chris@14: }); Chris@14: Chris@14: // Take internal padding into account. Chris@14: const scrollOffset = $element.outerHeight() - $element.height(); Chris@14: $element.height(modalHeight - offset - scrollOffset); Chris@14: }, Chris@14: Chris@14: /** Chris@14: * Resets the size of the dialog. Chris@14: * Chris@14: * @param {jQuery.Event} event Chris@14: * The event triggered. Chris@14: * @param {object} event.data Chris@14: * Data attached to the event. Chris@14: */ Chris@14: resetSize(event) { Chris@14: const $element = event.data.$element; Chris@14: const container = Drupal.offCanvas.getContainer($element); Chris@17: const position = event.data.settings.drupalOffCanvasPosition; Chris@14: Chris@17: // Only remove the `data-offset-*` attribute if the value previously Chris@17: // exists and the orientation is changing. Chris@17: if (Drupal.offCanvas.position && Drupal.offCanvas.position !== position) { Chris@17: container.removeAttr(`data-offset-${Drupal.offCanvas.position}`); Chris@17: } Chris@17: // Set a minimum height on $element Chris@17: if (position === 'top') { Chris@17: $element.css('min-height', `${Drupal.offCanvas.minimumHeight}px`); Chris@17: } Chris@17: Chris@17: displace(); Chris@17: Chris@17: const offsets = displace.offsets; Chris@17: Chris@17: const topPosition = Chris@17: position === 'side' && offsets.top !== 0 ? `+${offsets.top}` : ''; Chris@14: const adjustedOptions = { Chris@14: // @see http://api.jqueryui.com/position/ Chris@14: position: { Chris@14: my: `${Drupal.offCanvas.getEdge()} top`, Chris@14: at: `${Drupal.offCanvas.getEdge()} top${topPosition}`, Chris@14: of: window, Chris@14: }, Chris@14: }; Chris@14: Chris@17: const height = Chris@17: position === 'side' Chris@17: ? `${$(window).height() - (offsets.top + offsets.bottom)}px` Chris@17: : event.data.settings.height; Chris@14: container.css({ Chris@14: position: 'fixed', Chris@17: height, Chris@14: }); Chris@14: Chris@14: $element Chris@14: .dialog('option', adjustedOptions) Chris@14: .trigger('dialogContentResize.off-canvas'); Chris@17: Chris@17: Drupal.offCanvas.position = position; Chris@14: }, Chris@14: Chris@14: /** Chris@14: * Adjusts the body padding when the dialog is resized. Chris@14: * Chris@14: * @param {jQuery.Event} event Chris@14: * The event triggered. Chris@14: * @param {object} event.data Chris@14: * Data attached to the event. Chris@14: */ Chris@14: bodyPadding(event) { Chris@17: const position = event.data.settings.drupalOffCanvasPosition; Chris@17: if ( Chris@17: position === 'side' && Chris@17: $('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth Chris@17: ) { Chris@14: return; Chris@14: } Chris@17: Drupal.offCanvas.resetPadding(); Chris@14: const $element = event.data.$element; Chris@14: const $container = Drupal.offCanvas.getContainer($element); Chris@14: const $mainCanvasWrapper = Drupal.offCanvas.$mainCanvasWrapper; Chris@14: Chris@14: const width = $container.outerWidth(); Chris@17: const mainCanvasPadding = $mainCanvasWrapper.css( Chris@17: `padding-${Drupal.offCanvas.getEdge()}`, Chris@17: ); Chris@17: if (position === 'side' && width !== mainCanvasPadding) { Chris@17: $mainCanvasWrapper.css( Chris@17: `padding-${Drupal.offCanvas.getEdge()}`, Chris@17: `${width}px`, Chris@17: ); Chris@14: $container.attr(`data-offset-${Drupal.offCanvas.getEdge()}`, width); Chris@14: displace(); Chris@14: } Chris@17: Chris@17: const height = $container.outerHeight(); Chris@17: if (position === 'top') { Chris@17: $mainCanvasWrapper.css('padding-top', `${height}px`); Chris@17: $container.attr('data-offset-top', height); Chris@17: displace(); Chris@17: } Chris@14: }, Chris@14: Chris@14: /** Chris@14: * The HTML element that surrounds the dialog. Chris@14: * @param {HTMLElement} $element Chris@14: * The dialog element. Chris@14: * Chris@14: * @return {HTMLElement} Chris@14: * The containing element. Chris@14: */ Chris@14: getContainer($element) { Chris@14: return $element.dialog('widget'); Chris@14: }, Chris@14: Chris@14: /** Chris@14: * The edge of the screen that the dialog should appear on. Chris@14: * Chris@14: * @return {string} Chris@14: * The edge the tray will be shown on, left or right. Chris@14: */ Chris@14: getEdge() { Chris@14: return document.documentElement.dir === 'rtl' ? 'left' : 'right'; Chris@14: }, Chris@17: Chris@17: /** Chris@17: * Resets main canvas wrapper and toolbar padding / margin. Chris@17: */ Chris@17: resetPadding() { Chris@17: Drupal.offCanvas.$mainCanvasWrapper.css( Chris@17: `padding-${Drupal.offCanvas.getEdge()}`, Chris@17: 0, Chris@17: ); Chris@17: Drupal.offCanvas.$mainCanvasWrapper.css('padding-top', 0); Chris@17: displace(); Chris@17: }, Chris@14: }; Chris@14: Chris@14: /** Chris@14: * Attaches off-canvas dialog behaviors. Chris@14: * Chris@14: * @type {Drupal~behavior} Chris@14: * Chris@14: * @prop {Drupal~behaviorAttach} attach Chris@14: * Attaches event listeners for off-canvas dialogs. Chris@14: */ Chris@14: Drupal.behaviors.offCanvasEvents = { Chris@14: attach: () => { Chris@17: $(window) Chris@17: .once('off-canvas') Chris@17: .on({ Chris@17: 'dialog:beforecreate': (event, dialog, $element, settings) => { Chris@17: if (Drupal.offCanvas.isOffCanvas($element)) { Chris@17: Drupal.offCanvas.beforeCreate({ dialog, $element, settings }); Chris@17: } Chris@17: }, Chris@17: 'dialog:aftercreate': (event, dialog, $element, settings) => { Chris@17: if (Drupal.offCanvas.isOffCanvas($element)) { Chris@17: Drupal.offCanvas.render({ dialog, $element, settings }); Chris@17: Drupal.offCanvas.afterCreate({ $element, settings }); Chris@17: } Chris@17: }, Chris@17: 'dialog:beforeclose': (event, dialog, $element) => { Chris@17: if (Drupal.offCanvas.isOffCanvas($element)) { Chris@17: Drupal.offCanvas.beforeClose({ dialog, $element }); Chris@17: } Chris@17: }, Chris@17: }); Chris@14: }, Chris@14: }; Chris@14: })(jQuery, Drupal, Drupal.debounce, Drupal.displace);