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