Commit dc5ca923 authored by xjm's avatar xjm

Issue #2830882 by tedbow, droplet, drpal, Adita, Cottser, tim.plunkett,...

Issue #2830882 by tedbow, droplet, drpal, Adita, Cottser, tim.plunkett, markcarver, Wim Leers, nod_, catch: Introduce Drupal.offCanvas, mirrored after Drupal.dialog, to avoid Settings Tray using Drupal.Dialog in unintended ways
parent 96de2d9e
...@@ -9,101 +9,220 @@ ...@@ -9,101 +9,220 @@
* @private * @private
*/ */
(function ($, Drupal, debounce, displace) { (($, Drupal, debounce, displace) => {
// The minimum width to use body displace needs to match the width at which
// the tray will be %100 width. @see outside_in.module.css
const minDisplaceWidth = 768;
/** /**
* The edge of the screen that the dialog should appear on. * 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.
* *
* @type {string} * @namespace
*/ */
const edge = document.documentElement.dir === 'rtl' ? 'left' : 'right'; Drupal.offCanvas = {
const $mainCanvasWrapper = $('[data-off-canvas-main-canvas]'); /**
* The minimum width to use body displace needs to match the width at which
* the tray will be %100 width. @see outside_in.module.css
* @type {Number}
*/
minDisplaceWidth: 768,
/** /**
* Resets the size of the dialog. * Wrapper used to position off-canvas dialog.
* * @type {jQuery}
* @param {jQuery.Event} event */
* The event triggered. $mainCanvasWrapper: $('[data-off-canvas-main-canvas]'),
*/
function resetSize(event) {
const offsets = displace.offsets;
const $element = event.data.$element;
const $widget = $element.dialog('widget');
const $elementScroll = $element.scrollTop();
const adjustedOptions = { /**
* 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');
},
/**
* 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 }) {
$('body').addClass('js-tray-open');
settings.dialogClass += ' ui-dialog-off-canvas';
// @see http://api.jqueryui.com/position/ // @see http://api.jqueryui.com/position/
position: { settings.position = {
my: `${edge} top`, my: 'left top',
at: `${edge} top${offsets.top !== 0 ? `+${offsets.top}` : ''}`, at: `${Drupal.offCanvas.getEdge()} top`,
of: window, of: window,
}, };
};
$widget.css({ /**
position: 'fixed', * Applies initial height to dialog based on window height.
height: `${$(window).height() - (offsets.top + offsets.bottom)}px`, * @see http://api.jqueryui.com/dialog for all dialog options.
}); */
settings.height = $(window).height();
},
$element /**
.dialog('option', adjustedOptions) * Handler fired after an off-canvas dialog has been closed.
.trigger('dialogContentResize.off-canvas'); * @return {undefined}
*/
beforeClose() {
$('body').removeClass('js-tray-open');
// Remove all *.off-canvas events
$(document).off('.off-canvas');
$(window).off('.off-canvas');
Drupal.offCanvas.$mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`, 0);
},
// Reset the elements scroll position after being repositioned. /**
$element.scrollTop($elementScroll); * 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
* Adjusts the dialog on resize. .on('dialogresize.off-canvas', eventData, debounce(Drupal.offCanvas.bodyPadding, 100))
* .on('dialogContentResize.off-canvas', eventData, Drupal.offCanvas.handleDialogResize)
* @param {jQuery.Event} event .on('dialogContentResize.off-canvas', eventData, debounce(Drupal.offCanvas.bodyPadding, 100))
* The event triggered. .trigger('dialogresize.off-canvas');
*/
function handleDialogResize(event) {
const $element = event.data.$element;
const $widget = $element.dialog('widget');
const $offsets = $widget.find('> :not(#drupal-off-canvas, .ui-resizable-handle)');
let offset = 0;
let modalHeight;
// Let scroll element take all the height available.
$element.css({ height: 'auto' });
modalHeight = $widget.height();
$offsets.each(function () {
offset += $(this).outerHeight();
});
// Take internal padding into account.
const scrollOffset = $element.outerHeight() - $element.height();
$element.height(modalHeight - offset - scrollOffset);
}
/** Drupal.offCanvas.getContainer($element).attr(`data-offset-${Drupal.offCanvas.getEdge()}`, '');
* Adjusts the body padding when the dialog is resized.
* $(window)
* @param {jQuery.Event} event .on('resize.off-canvas scroll.off-canvas', eventData, debounce(Drupal.offCanvas.resetSize, 100))
* The event triggered. .trigger('resize.off-canvas');
*/ },
function bodyPadding(event) {
if ($('body').outerWidth() < minDisplaceWidth) { /**
return; * Toggle classes based on title existence.
} * Called with Drupal.offCanvas.afterCreate.
const $element = event.data.$element; * @param {Object} settings
const $widget = $element.dialog('widget'); * Settings related to the composition of the dialog.
* @return {undefined}
const width = $widget.outerWidth(); */
const mainCanvasPadding = $mainCanvasWrapper.css(`padding-${edge}`); render({ settings }) {
if (width !== mainCanvasPadding) { $('.ui-dialog-off-canvas, .ui-dialog-off-canvas .ui-dialog-titlebar').toggleClass('ui-dialog-empty-title', !settings.title);
$mainCanvasWrapper.css(`padding-${edge}`, `${width}px`); },
$widget.attr(`data-offset-${edge}`, width);
displace(); /**
} * 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. * Attaches off-canvas dialog behaviors.
...@@ -114,51 +233,25 @@ ...@@ -114,51 +233,25 @@
* Attaches event listeners for off-canvas dialogs. * Attaches event listeners for off-canvas dialogs.
*/ */
Drupal.behaviors.offCanvasEvents = { Drupal.behaviors.offCanvasEvents = {
attach() { attach: () => {
$(window).once('off-canvas').on({ $(window).once('off-canvas').on({
'dialog:aftercreate': function (event, dialog, $element, settings) { 'dialog:beforecreate': (event, dialog, $element, settings) => {
if ($element.is('#drupal-off-canvas')) { if (Drupal.offCanvas.isOffCanvas($element)) {
const eventData = { settings, $element }; Drupal.offCanvas.beforeCreate({ dialog, $element, settings });
$('.ui-dialog-off-canvas, .ui-dialog-off-canvas .ui-dialog-titlebar').toggleClass('ui-dialog-empty-title', !settings.title);
$element
.on('dialogresize.off-canvas', eventData, debounce(bodyPadding, 100))
.on('dialogContentResize.off-canvas', eventData, handleDialogResize)
.on('dialogContentResize.off-canvas', eventData, debounce(bodyPadding, 100))
.trigger('dialogresize.off-canvas');
$element.dialog('widget').attr(`data-offset-${edge}`, '');
$(window)
.on('resize.off-canvas scroll.off-canvas', eventData, debounce(resetSize, 100))
.trigger('resize.off-canvas');
} }
}, },
'dialog:beforecreate': function (event, dialog, $element, settings) { 'dialog:aftercreate': (event, dialog, $element, settings) => {
if ($element.is('#drupal-off-canvas')) { if (Drupal.offCanvas.isOffCanvas($element)) {
$('body').addClass('js-tray-open'); Drupal.offCanvas.render({ dialog, $element, settings });
// @see http://api.jqueryui.com/position/ Drupal.offCanvas.afterCreate({ $element, settings });
settings.position = {
my: 'left top',
at: `${edge} top`,
of: window,
};
settings.dialogClass += ' ui-dialog-off-canvas';
// Applies initial height to dialog based on window height.
// See http://api.jqueryui.com/dialog for all dialog options.
settings.height = $(window).height();
} }
}, },
'dialog:beforeclose': function (event, dialog, $element) { 'dialog:beforeclose': (event, dialog, $element) => {
if ($element.is('#drupal-off-canvas')) { if (Drupal.offCanvas.isOffCanvas($element)) {
$('body').removeClass('js-tray-open'); Drupal.offCanvas.beforeClose({ dialog, $element });
// Remove all *.off-canvas events
$(document).off('.off-canvas');
$(window).off('.off-canvas');
$mainCanvasWrapper.css(`padding-${edge}`, 0);
} }
}, },
}); });
}, },
}; };
}(jQuery, Drupal, Drupal.debounce, Drupal.displace)); })(jQuery, Drupal, Drupal.debounce, Drupal.displace);
...@@ -6,106 +6,131 @@ ...@@ -6,106 +6,131 @@
**/ **/
(function ($, Drupal, debounce, displace) { (function ($, Drupal, debounce, displace) {
var minDisplaceWidth = 768; Drupal.offCanvas = {
minDisplaceWidth: 768,
var edge = document.documentElement.dir === 'rtl' ? 'left' : 'right'; $mainCanvasWrapper: $('[data-off-canvas-main-canvas]'),
var $mainCanvasWrapper = $('[data-off-canvas-main-canvas]'); isOffCanvas: function isOffCanvas($element) {
return $element.is('#drupal-off-canvas');
},
beforeCreate: function beforeCreate(_ref) {
var settings = _ref.settings;
function resetSize(event) { $('body').addClass('js-tray-open');
var offsets = displace.offsets; settings.dialogClass += ' ui-dialog-off-canvas';
var $element = event.data.$element;
var $widget = $element.dialog('widget');
var $elementScroll = $element.scrollTop();
var adjustedOptions = { settings.position = {
position: { my: 'left top',
my: edge + ' top', at: Drupal.offCanvas.getEdge() + ' top',
at: edge + ' top' + (offsets.top !== 0 ? '+' + offsets.top : ''),
of: window of: window
} };
};
$widget.css({ settings.height = $(window).height();
position: 'fixed', },
height: $(window).height() - (offsets.top + offsets.bottom) + 'px' beforeClose: function beforeClose() {
}); $('body').removeClass('js-tray-open');
$element.dialog('option', adjustedOptions).trigger('dialogContentResize.off-canvas'); $(document).off('.off-canvas');
$(window).off('.off-canvas');
Drupal.offCanvas.$mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge(), 0);
},
afterCreate: function afterCreate(_ref2) {
var $element = _ref2.$element,
settings = _ref2.settings;
$element.scrollTop($elementScroll); var eventData = { settings: settings, $element: $element, offCanvasDialog: this };
}
function handleDialogResize(event) { $element.on('dialogresize.off-canvas', eventData, debounce(Drupal.offCanvas.bodyPadding, 100)).on('dialogContentResize.off-canvas', eventData, Drupal.offCanvas.handleDialogResize).on('dialogContentResize.off-canvas', eventData, debounce(Drupal.offCanvas.bodyPadding, 100)).trigger('dialogresize.off-canvas');
var $element = event.data.$element;
var $widget = $element.dialog('widget');
var $offsets = $widget.find('> :not(#drupal-off-canvas, .ui-resizable-handle)'); Drupal.offCanvas.getContainer($element).attr('data-offset-' + Drupal.offCanvas.getEdge(), '');
var offset = 0;
var modalHeight = void 0;
$element.css({ height: 'auto' }); $(window).on('resize.off-canvas scroll.off-canvas', eventData, debounce(Drupal.offCanvas.resetSize, 100)).trigger('resize.off-canvas');
modalHeight = $widget.height(); },
$offsets.each(function () { render: function render(_ref3) {
offset += $(this).outerHeight(); var settings = _ref3.settings;
});
var scrollOffset = $element.outerHeight() - $element.height(); $('.ui-dialog-off-canvas, .ui-dialog-off-canvas .ui-dialog-titlebar').toggleClass('ui-dialog-empty-title', !settings.title);
$element.height(modalHeight - offset - scrollOffset); },
} handleDialogResize: function handleDialogResize(event) {
var $element = event.data.$element;
var $container = Drupal.offCanvas.getContainer($element);
function bodyPadding(event) { var $offsets = $container.find('> :not(#drupal-off-canvas, .ui-resizable-handle)');
if ($('body').outerWidth() < minDisplaceWidth) { var offset = 0;
return;
} $element.css({ height: 'auto' });
var $element = event.data.$element; var modalHeight = $container.height();
var $widget = $element.dialog('widget');
$offsets.each(function (i, e) {
var width = $widget.outerWidth(); offset += $(e).outerHeight();
var mainCanvasPadding = $mainCanvasWrapper.css('padding-' + edge); });
if (width !== mainCanvasPadding) {
$mainCanvasWrapper.css('padding-' + edge, width + 'px'); var scrollOffset = $element.outerHeight() - $element.height();
$widget.attr('data-offset-' + edge, width); $element.height(modalHeight - offset - scrollOffset);
displace(); },
resetSize: function resetSize(event) {
var offsets = displace.offsets;
var $element = event.data.$element;
var container = Drupal.offCanvas.getContainer($element);
var topPosition = offsets.top !== 0 ? '+' + offsets.top : '';
var adjustedOptions = {
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');
},
bodyPadding: function bodyPadding(event) {
if ($('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth) {
return;
}
var $element = event.data.$element;
var $container = Drupal.offCanvas.getContainer($element);
var $mainCanvasWrapper = Drupal.offCanvas.$mainCanvasWrapper;
var width = $container.outerWidth();
var 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();
}
},
getContainer: function getContainer($element) {
return $element.dialog('widget');
},
getEdge: function getEdge() {
return document.documentElement.dir === 'rtl' ? 'left' : 'right';
} }
} };
Drupal.behaviors.offCanvasEvents = { Drupal.behaviors.offCanvasEvents = {
attach: function attach() { attach: function attach() {
$(window).once('off-canvas').on({ $(window).once('off-canvas').on({
'dialog:aftercreate': function dialogAftercreate(event, dialog, $element, settings) { 'dialog:beforecreate': function dialogBeforecreate(event, dialog, $element, settings) {
if ($element.is('#drupal-off-canvas')) { if (Drupal.offCanvas.isOffCanvas($element)) {
var eventData = { settings: settings, $element: $element }; Drupal.offCanvas.beforeCreate({ dialog: dialog, $element: $element, settings: settings });
$('.ui-dialog-off-canvas, .ui-dialog-off-canvas .ui-dialog-titlebar').toggleClass('ui-dialog-empty-title', !settings.title);
$element.on('dialogresize.off-canvas', eventData, debounce(bodyPadding, 100)).on('dialogContentResize.off-canvas', eventData, handleDialogResize).on('dialogContentResize.off-canvas', eventData, debounce(bodyPadding, 100)).trigger('dialogresize.off-canvas');
$element.dialog('widget').attr('data-offset-' + edge, '');
$(window).on('resize.off-canvas scroll.off-canvas', eventData, debounce(resetSize, 100)).trigger('resize.off-canvas');
} }
}, },
'dialog:beforecreate': function dialogBeforecreate(event, dialog, $element, settings) { 'dialog:aftercreate': function dialogAftercreate(event, dialog, $element, settings) {
if ($element.is('#drupal-off-canvas')) { if (Drupal.offCanvas.isOffCanvas($element)) {
$('body').addClass('js-tray-open'); Drupal.offCanvas.render({ dialog: dialog, $element: $element, settings: settings });
Drupal.offCanvas.afterCreate({ $element: $element, settings: settings });
settings.position = {
my: 'left top',
at: edge + ' top',
of: window
};
settings.dialogClass += ' ui-dialog-off-canvas';
settings.height = $(window).height();
} }
}, },
'dialog:beforeclose': function dialogBeforeclose(event, dialog, $element) { 'dialog:beforeclose': function dialogBeforeclose(event, dialog, $element) {
if ($element.is('#drupal-off-canvas'