/**
* @file
* Drupal's off-canvas library.
*/
(($, 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 = {
/**
* Storage for position information about the tray.
*
* @type {?String}
*/
position: null,
/**
* The minimum height of the tray when opened at the top of the page.
*
* @type {Number}
*/
minimumHeight: 30,
/**
* The minimum width to use body displace needs to match the width at which
* the tray will be 100% width. @see core/misc/dialog/off-canvas.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-off-canvas-dialog-open');
// @see http://api.jqueryui.com/position/
settings.position = {
my: 'left top',
at: `${Drupal.offCanvas.getEdge()} top`,
of: window,
};
/**
* Applies initial height and with to dialog based depending on position.
* @see http://api.jqueryui.com/dialog for all dialog options.
*/
const position = settings.drupalOffCanvasPosition;
const height = position === 'side' ? $(window).height() : settings.height;
const width = position === 'side' ? settings.width : '100%';
settings.height = height;
settings.width = width;
},
/**
* Handler fired after an off-canvas dialog has been closed.
*
* @return {undefined}
*/
beforeClose({ $element }) {
$('body').removeClass('js-off-canvas-dialog-open');
// Remove all *.off-canvas events
Drupal.offCanvas.removeOffCanvasEvents($element);
Drupal.offCanvas.resetPadding();
},
/**
* 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 $element = event.data.$element;
const container = Drupal.offCanvas.getContainer($element);
const position = event.data.settings.drupalOffCanvasPosition;
// Only remove the `data-offset-*` attribute if the value previously
// exists and the orientation is changing.
if (Drupal.offCanvas.position && Drupal.offCanvas.position !== position) {
container.removeAttr(`data-offset-${Drupal.offCanvas.position}`);
}
// Set a minimum height on $element
if (position === 'top') {
$element.css('min-height', `${Drupal.offCanvas.minimumHeight}px`);
}
displace();
const offsets = displace.offsets;
const topPosition =
position === 'side' && 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,
},
};
const height =
position === 'side'
? `${$(window).height() - (offsets.top + offsets.bottom)}px`
: event.data.settings.height;
container.css({
position: 'fixed',
height,
});
$element
.dialog('option', adjustedOptions)
.trigger('dialogContentResize.off-canvas');
Drupal.offCanvas.position = position;
},
/**
* 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) {
const position = event.data.settings.drupalOffCanvasPosition;
if (
position === 'side' &&
$('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth
) {
return;
}
Drupal.offCanvas.resetPadding();
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 (position === 'side' && width !== mainCanvasPadding) {
$mainCanvasWrapper.css(
`padding-${Drupal.offCanvas.getEdge()}`,
`${width}px`,
);
$container.attr(`data-offset-${Drupal.offCanvas.getEdge()}`, width);
displace();
}
const height = $container.outerHeight();
if (position === 'top') {
$mainCanvasWrapper.css('padding-top', `${height}px`);
$container.attr('data-offset-top', height);
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';
},
/**
* Resets main canvas wrapper and toolbar padding / margin.
*/
resetPadding() {
Drupal.offCanvas.$mainCanvasWrapper.css(
`padding-${Drupal.offCanvas.getEdge()}`,
0,
);
Drupal.offCanvas.$mainCanvasWrapper.css('padding-top', 0);
displace();
},
};
/**
* 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);