updated core to 8.6.1 via composer

This commit is contained in:
2018-09-12 13:58:26 +02:00
parent a9a219f2ed
commit ea56b9fba3
4443 changed files with 112098 additions and 40708 deletions
+14 -6
View File
@@ -3,7 +3,7 @@
* Attaches behaviors for Drupal's active link marking.
*/
(function (Drupal, drupalSettings) {
(function(Drupal, drupalSettings) {
/**
* Append is-active class.
*
@@ -23,8 +23,12 @@
// Start by finding all potentially active links.
const path = drupalSettings.path;
const queryString = JSON.stringify(path.currentQuery);
const querySelector = path.currentQuery ? `[data-drupal-link-query='${queryString}']` : ':not([data-drupal-link-query])';
const originalSelectors = [`[data-drupal-link-system-path="${path.currentPath}"]`];
const querySelector = path.currentQuery
? `[data-drupal-link-query='${queryString}']`
: ':not([data-drupal-link-query])';
const originalSelectors = [
`[data-drupal-link-system-path="${path.currentPath}"]`,
];
let selectors;
// If this is the front page, we have to check for the <front> path as
@@ -38,7 +42,9 @@
// Links without any hreflang attributes (most of them).
originalSelectors.map(selector => `${selector}:not([hreflang])`),
// Links with hreflang equals to the current language.
originalSelectors.map(selector => `${selector}[hreflang="${path.currentLanguage}"]`),
originalSelectors.map(
selector => `${selector}[hreflang="${path.currentLanguage}"]`,
),
);
// Add query string selector for pagers, exposed filters.
@@ -53,7 +59,9 @@
},
detach(context, settings, trigger) {
if (trigger === 'unload') {
const activeLinks = context.querySelectorAll('[data-drupal-link-system-path].is-active');
const activeLinks = context.querySelectorAll(
'[data-drupal-link-system-path].is-active',
);
const il = activeLinks.length;
for (let i = 0; i < il; i++) {
activeLinks[i].classList.remove('is-active');
@@ -61,4 +69,4 @@
}
},
};
}(Drupal, drupalSettings));
})(Drupal, drupalSettings);
+346 -169
View File
@@ -11,7 +11,7 @@
* included to provide Ajax capabilities.
*/
(function ($, window, Drupal, drupalSettings) {
(function($, window, Drupal, drupalSettings) {
/**
* Attaches the Ajax behavior to each Ajax form element.
*
@@ -32,11 +32,13 @@
if (typeof elementSettings.selector === 'undefined') {
elementSettings.selector = `#${base}`;
}
$(elementSettings.selector).once('drupal-ajax').each(function () {
elementSettings.element = this;
elementSettings.base = base;
Drupal.ajax(elementSettings);
});
$(elementSettings.selector)
.once('drupal-ajax')
.each(function() {
elementSettings.element = this;
elementSettings.base = base;
Drupal.ajax(elementSettings);
});
}
// Load all Ajax behaviors specified in the settings.
@@ -45,30 +47,32 @@
Drupal.ajax.bindAjaxLinks(document.body);
// This class means to submit the form to the action using Ajax.
$('.use-ajax-submit').once('ajax').each(function () {
const elementSettings = {};
$('.use-ajax-submit')
.once('ajax')
.each(function() {
const elementSettings = {};
// Ajax submits specified in this manner automatically submit to the
// normal form action.
elementSettings.url = $(this.form).attr('action');
// Form submit button clicks need to tell the form what was clicked so
// it gets passed in the POST request.
elementSettings.setClick = true;
// Form buttons use the 'click' event rather than mousedown.
elementSettings.event = 'click';
// Clicked form buttons look better with the throbber than the progress
// bar.
elementSettings.progress = { type: 'throbber' };
elementSettings.base = $(this).attr('id');
elementSettings.element = this;
// Ajax submits specified in this manner automatically submit to the
// normal form action.
elementSettings.url = $(this.form).attr('action');
// Form submit button clicks need to tell the form what was clicked so
// it gets passed in the POST request.
elementSettings.setClick = true;
// Form buttons use the 'click' event rather than mousedown.
elementSettings.event = 'click';
// Clicked form buttons look better with the throbber than the progress
// bar.
elementSettings.progress = { type: 'throbber' };
elementSettings.base = $(this).attr('id');
elementSettings.element = this;
Drupal.ajax(elementSettings);
});
Drupal.ajax(elementSettings);
});
},
detach(context, settings, trigger) {
if (trigger === 'unload') {
Drupal.ajax.expired().forEach((instance) => {
Drupal.ajax.expired().forEach(instance => {
// Set this to null and allow garbage collection to reclaim
// the memory.
Drupal.ajax.instances[instance.instanceIndex] = null;
@@ -91,15 +95,19 @@
* @param {string} customMessage
* The custom message.
*/
Drupal.AjaxError = function (xmlhttp, uri, customMessage) {
Drupal.AjaxError = function(xmlhttp, uri, customMessage) {
let statusCode;
let statusText;
let responseText;
if (xmlhttp.status) {
statusCode = `\n${Drupal.t('An AJAX HTTP error occurred.')}\n${Drupal.t('HTTP Result Code: !status', { '!status': xmlhttp.status })}`;
}
else {
statusCode = `\n${Drupal.t('An AJAX HTTP request terminated abnormally.')}`;
statusCode = `\n${Drupal.t('An AJAX HTTP error occurred.')}\n${Drupal.t(
'HTTP Result Code: !status',
{ '!status': xmlhttp.status },
)}`;
} else {
statusCode = `\n${Drupal.t(
'An AJAX HTTP request terminated abnormally.',
)}`;
}
statusCode += `\n${Drupal.t('Debugging information follows.')}`;
const pathText = `\n${Drupal.t('Path: !uri', { '!uri': uri })}`;
@@ -109,9 +117,10 @@
// catch that and the test causes an exception. So we need to catch the
// exception here.
try {
statusText = `\n${Drupal.t('StatusText: !statusText', { '!statusText': $.trim(xmlhttp.statusText) })}`;
}
catch (e) {
statusText = `\n${Drupal.t('StatusText: !statusText', {
'!statusText': $.trim(xmlhttp.statusText),
})}`;
} catch (e) {
// Empty.
}
@@ -119,9 +128,10 @@
// Again, we don't have a way to know for sure whether accessing
// xmlhttp.responseText is going to throw an exception. So we'll catch it.
try {
responseText = `\n${Drupal.t('ResponseText: !responseText', { '!responseText': $.trim(xmlhttp.responseText) })}`;
}
catch (e) {
responseText = `\n${Drupal.t('ResponseText: !responseText', {
'!responseText': $.trim(xmlhttp.responseText),
})}`;
} catch (e) {
// Empty.
}
@@ -130,16 +140,31 @@
responseText = responseText.replace(/[\n]+\s+/g, '\n');
// We don't need readyState except for status == 0.
const readyStateText = xmlhttp.status === 0 ? (`\n${Drupal.t('ReadyState: !readyState', { '!readyState': xmlhttp.readyState })}`) : '';
const readyStateText =
xmlhttp.status === 0
? `\n${Drupal.t('ReadyState: !readyState', {
'!readyState': xmlhttp.readyState,
})}`
: '';
customMessage = customMessage ? (`\n${Drupal.t('CustomMessage: !customMessage', { '!customMessage': customMessage })}`) : '';
customMessage = customMessage
? `\n${Drupal.t('CustomMessage: !customMessage', {
'!customMessage': customMessage,
})}`
: '';
/**
* Formatted and translated error message.
*
* @type {string}
*/
this.message = statusCode + pathText + statusText + customMessage + responseText + readyStateText;
this.message =
statusCode +
pathText +
statusText +
customMessage +
responseText +
readyStateText;
/**
* Used by some browsers to display a more accurate stack trace.
@@ -203,9 +228,11 @@
*
* @see Drupal.AjaxCommands
*/
Drupal.ajax = function (settings) {
Drupal.ajax = function(settings) {
if (arguments.length !== 1) {
throw new Error('Drupal.ajax() function must be called with one configuration object only');
throw new Error(
'Drupal.ajax() function must be called with one configuration object only',
);
}
// Map those config keys to variables for the old Drupal.ajax function.
const base = settings.base || false;
@@ -241,8 +268,13 @@
* @return {Array.<Drupal.Ajax>}
* The list of expired {@link Drupal.Ajax} objects.
*/
Drupal.ajax.expired = function () {
return Drupal.ajax.instances.filter(instance => instance && instance.element !== false && !document.body.contains(instance.element));
Drupal.ajax.expired = function() {
return Drupal.ajax.instances.filter(
instance =>
instance &&
instance.element !== false &&
!document.body.contains(instance.element),
);
};
/**
@@ -251,31 +283,34 @@
* @param {HTMLElement} element
* Element to enable Ajax functionality for.
*/
Drupal.ajax.bindAjaxLinks = (element) => {
Drupal.ajax.bindAjaxLinks = element => {
// Bind Ajax behaviors to all items showing the class.
$(element).find('.use-ajax').once('ajax').each((i, ajaxLink) => {
const $linkElement = $(ajaxLink);
$(element)
.find('.use-ajax')
.once('ajax')
.each((i, ajaxLink) => {
const $linkElement = $(ajaxLink);
const elementSettings = {
// Clicked links look better with the throbber than the progress bar.
progress: { type: 'throbber' },
dialogType: $linkElement.data('dialog-type'),
dialog: $linkElement.data('dialog-options'),
dialogRenderer: $linkElement.data('dialog-renderer'),
base: $linkElement.attr('id'),
element: ajaxLink,
};
const href = $linkElement.attr('href');
/**
* For anchor tags, these will go to the target of the anchor rather
* than the usual location.
*/
if (href) {
elementSettings.url = href;
elementSettings.event = 'click';
}
Drupal.ajax(elementSettings);
});
const elementSettings = {
// Clicked links look better with the throbber than the progress bar.
progress: { type: 'throbber' },
dialogType: $linkElement.data('dialog-type'),
dialog: $linkElement.data('dialog-options'),
dialogRenderer: $linkElement.data('dialog-renderer'),
base: $linkElement.attr('id'),
element: ajaxLink,
};
const href = $linkElement.attr('href');
/**
* For anchor tags, these will go to the target of the anchor rather
* than the usual location.
*/
if (href) {
elementSettings.url = href;
elementSettings.event = 'click';
}
Drupal.ajax(elementSettings);
});
};
/**
@@ -338,7 +373,7 @@
* @param {Drupal.Ajax~elementSettings} elementSettings
* Settings for this Ajax object.
*/
Drupal.Ajax = function (base, element, elementSettings) {
Drupal.Ajax = function(base, element, elementSettings) {
const defaults = {
event: element ? 'mousedown' : null,
keypress: true,
@@ -410,8 +445,7 @@
const $element = $(this.element);
if ($element.is('a')) {
this.url = $element.attr('href');
}
else if (this.element && element.form) {
} else if (this.element && element.form) {
this.url = this.$form.attr('action');
}
}
@@ -430,7 +464,7 @@
*
* @type {string}
*/
this.url = this.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1');
this.url = this.url.replace(/\/nojs(\/|$|\?|#)/, '/ajax$1');
// If the 'nojs' version of the URL is trusted, also trust the 'ajax'
// version.
if (drupalSettings.ajaxTrustedUrl[originalUrl]) {
@@ -505,7 +539,9 @@
// the response headers cannot be accessed for verification.
if (response !== null && !drupalSettings.ajaxTrustedUrl[ajax.url]) {
if (xmlhttprequest.getResponseHeader('X-Drupal-Ajax-Token') !== '1') {
const customMessage = Drupal.t('The response failed verification so will not be processed.');
const customMessage = Drupal.t(
'The response failed verification so will not be processed.',
);
return ajax.error(xmlhttprequest, ajax.url, customMessage);
}
}
@@ -530,22 +566,27 @@
// yet available, otherwise append using &.
if (ajax.options.url.indexOf('?') === -1) {
ajax.options.url += '?';
}
else {
} else {
ajax.options.url += '&';
}
// If this element has a dialog type use if for the wrapper if not use 'ajax'.
let wrapper = `drupal_${(elementSettings.dialogType || 'ajax')}`;
let wrapper = `drupal_${elementSettings.dialogType || 'ajax'}`;
if (elementSettings.dialogRenderer) {
wrapper += `.${elementSettings.dialogRenderer}`;
}
ajax.options.url += `${Drupal.ajax.WRAPPER_FORMAT}=${wrapper}`;
// Bind the ajaxSubmit function to the element event.
$(ajax.element).on(elementSettings.event, function (event) {
if (!drupalSettings.ajaxTrustedUrl[ajax.url] && !Drupal.url.isLocal(ajax.url)) {
throw new Error(Drupal.t('The callback URL is not local and not trusted: !url', { '!url': ajax.url }));
$(ajax.element).on(elementSettings.event, function(event) {
if (
!drupalSettings.ajaxTrustedUrl[ajax.url] &&
!Drupal.url.isLocal(ajax.url)
) {
throw new Error(
Drupal.t('The callback URL is not local and not trusted: !url', {
'!url': ajax.url,
}),
);
}
return ajax.eventResponse(this, event);
});
@@ -554,7 +595,7 @@
// can be triggered through keyboard input as well as e.g. a mousedown
// action.
if (elementSettings.keypress) {
$(ajax.element).on('keypress', function (event) {
$(ajax.element).on('keypress', function(event) {
return ajax.keypressResponse(this, event);
});
}
@@ -599,7 +640,7 @@
* pre-serialization fails, the Deferred will be returned in the rejected
* state.
*/
Drupal.Ajax.prototype.execute = function () {
Drupal.Ajax.prototype.execute = function() {
// Do not perform another ajax command if one is already in progress.
if (this.ajaxing) {
return;
@@ -609,12 +650,15 @@
this.beforeSerialize(this.element, this.options);
// Return the jqXHR so that external code can hook into the Deferred API.
return $.ajax(this.options);
}
catch (e) {
} catch (e) {
// Unset the ajax.ajaxing flag here because it won't be unset during
// the complete response.
this.ajaxing = false;
window.alert(`An error occurred while attempting to process ${this.options.url}: ${e.message}`);
window.alert(
`An error occurred while attempting to process ${this.options.url}: ${
e.message
}`,
);
// For consistency, return a rejected Deferred (i.e., jqXHR's superclass)
// so that calling code can take appropriate action.
return $.Deferred().reject();
@@ -636,7 +680,7 @@
* @param {jQuery.Event} event
* Triggered event.
*/
Drupal.Ajax.prototype.keypressResponse = function (element, event) {
Drupal.Ajax.prototype.keypressResponse = function(element, event) {
// Create a synonym for this to reduce code confusion.
const ajax = this;
@@ -645,8 +689,14 @@
// where the spacebar activation causes inappropriate activation if
// #ajax['keypress'] is TRUE. On a text-type widget a space should always
// be a space.
if (event.which === 13 || (event.which === 32 && element.type !== 'text' &&
element.type !== 'textarea' && element.type !== 'tel' && element.type !== 'number')) {
if (
event.which === 13 ||
(event.which === 32 &&
element.type !== 'text' &&
element.type !== 'textarea' &&
element.type !== 'tel' &&
element.type !== 'number')
) {
event.preventDefault();
event.stopPropagation();
$(element).trigger(ajax.elementSettings.event);
@@ -666,7 +716,7 @@
* @param {jQuery.Event} event
* Triggered event.
*/
Drupal.Ajax.prototype.eventResponse = function (element, event) {
Drupal.Ajax.prototype.eventResponse = function(element, event) {
event.preventDefault();
event.stopPropagation();
@@ -691,17 +741,19 @@
}
ajax.$form.ajaxSubmit(ajax.options);
}
else {
} else {
ajax.beforeSerialize(ajax.element, ajax.options);
$.ajax(ajax.options);
}
}
catch (e) {
} catch (e) {
// Unset the ajax.ajaxing flag here because it won't be unset during
// the complete response.
ajax.ajaxing = false;
window.alert(`An error occurred while attempting to process ${ajax.options.url}: ${e.message}`);
window.alert(
`An error occurred while attempting to process ${ajax.options.url}: ${
e.message
}`,
);
}
};
@@ -716,7 +768,7 @@
* @param {object} options
* jQuery.ajax options.
*/
Drupal.Ajax.prototype.beforeSerialize = function (element, options) {
Drupal.Ajax.prototype.beforeSerialize = function(element, options) {
// Allow detaching behaviors to update field values before collecting them.
// This is only needed when field values are added to the POST data, so only
// when there is a form such that this.$form.ajaxSubmit() is used instead of
@@ -751,7 +803,7 @@
* @param {object} options
* jQuery.ajax options.
*/
Drupal.Ajax.prototype.beforeSubmit = function (formValues, element, options) {
Drupal.Ajax.prototype.beforeSubmit = function(formValues, element, options) {
// This function is left empty to make it simple to override for modules
// that wish to add functionality here.
};
@@ -764,7 +816,7 @@
* @param {object} options
* jQuery.ajax options.
*/
Drupal.Ajax.prototype.beforeSend = function (xmlhttprequest, options) {
Drupal.Ajax.prototype.beforeSend = function(xmlhttprequest, options) {
// For forms without file inputs, the jQuery Form plugin serializes the
// form values, and then calls jQuery's $.ajax() function, which invokes
// this handler. In this circumstance, options.extraData is never used. For
@@ -805,24 +857,78 @@
}
// Insert progress indicator.
const progressIndicatorMethod = `setProgressIndicator${this.progress.type.slice(0, 1).toUpperCase()}${this.progress.type.slice(1).toLowerCase()}`;
if (progressIndicatorMethod in this && typeof this[progressIndicatorMethod] === 'function') {
const progressIndicatorMethod = `setProgressIndicator${this.progress.type
.slice(0, 1)
.toUpperCase()}${this.progress.type.slice(1).toLowerCase()}`;
if (
progressIndicatorMethod in this &&
typeof this[progressIndicatorMethod] === 'function'
) {
this[progressIndicatorMethod].call(this);
}
};
/**
* An animated progress throbber and container element for AJAX operations.
*
* @param {string} [message]
* (optional) The message shown on the UI.
* @return {string}
* The HTML markup for the throbber.
*/
Drupal.theme.ajaxProgressThrobber = message => {
// Build markup without adding extra white space since it affects rendering.
const messageMarkup =
typeof message === 'string'
? Drupal.theme('ajaxProgressMessage', message)
: '';
const throbber = '<div class="throbber">&nbsp;</div>';
return `<div class="ajax-progress ajax-progress-throbber">${throbber}${messageMarkup}</div>`;
};
/**
* An animated progress throbber and container element for AJAX operations.
*
* @return {string}
* The HTML markup for the throbber.
*/
Drupal.theme.ajaxProgressIndicatorFullscreen = () =>
'<div class="ajax-progress ajax-progress-fullscreen">&nbsp;</div>';
/**
* Formats text accompanying the AJAX progress throbber.
*
* @param {string} message
* The message shown on the UI.
* @return {string}
* The HTML markup for the throbber.
*/
Drupal.theme.ajaxProgressMessage = message =>
`<div class="message">${message}</div>`;
/**
* Sets the progress bar progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorBar = function () {
const progressBar = new Drupal.ProgressBar(`ajax-progress-${this.element.id}`, $.noop, this.progress.method, $.noop);
Drupal.Ajax.prototype.setProgressIndicatorBar = function() {
const progressBar = new Drupal.ProgressBar(
`ajax-progress-${this.element.id}`,
$.noop,
this.progress.method,
$.noop,
);
if (this.progress.message) {
progressBar.setProgress(-1, this.progress.message);
}
if (this.progress.url) {
progressBar.startMonitoring(this.progress.url, this.progress.interval || 1500);
progressBar.startMonitoring(
this.progress.url,
this.progress.interval || 1500,
);
}
this.progress.element = $(progressBar.element).addClass('ajax-progress ajax-progress-bar');
this.progress.element = $(progressBar.element).addClass(
'ajax-progress ajax-progress-bar',
);
this.progress.object = progressBar;
$(this.element).after(this.progress.element);
};
@@ -830,19 +936,18 @@
/**
* Sets the throbber progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () {
this.progress.element = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
if (this.progress.message) {
this.progress.element.find('.throbber').after(`<div class="message">${this.progress.message}</div>`);
}
Drupal.Ajax.prototype.setProgressIndicatorThrobber = function() {
this.progress.element = $(
Drupal.theme('ajaxProgressThrobber', this.progress.message),
);
$(this.element).after(this.progress.element);
};
/**
* Sets the fullscreen progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function () {
this.progress.element = $('<div class="ajax-progress ajax-progress-fullscreen">&nbsp;</div>');
Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function() {
this.progress.element = $(Drupal.theme('ajaxProgressIndicatorFullscreen'));
$('body').after(this.progress.element);
};
@@ -854,7 +959,7 @@
* @param {number} status
* XMLHttpRequest status.
*/
Drupal.Ajax.prototype.success = function (response, status) {
Drupal.Ajax.prototype.success = function(response, status) {
// Remove the progress element.
if (this.progress.element) {
$(this.progress.element).remove();
@@ -868,15 +973,21 @@
// we can try to refocus one of its parents. Using addBack reverse the
// result array, meaning that index 0 is the highest parent in the hierarchy
// in this situation it is usually a <form> element.
const elementParents = $(this.element).parents('[data-drupal-selector]').addBack().toArray();
const elementParents = $(this.element)
.parents('[data-drupal-selector]')
.addBack()
.toArray();
// Track if any command is altering the focus so we can avoid changing the
// focus set by the Ajax command.
let focusChanged = false;
Object.keys(response || {}).forEach((i) => {
Object.keys(response || {}).forEach(i => {
if (response[i].command && this.commands[response[i].command]) {
this.commands[response[i].command](this, response[i], status);
if (response[i].command === 'invoke' && response[i].method === 'focus') {
if (
response[i].command === 'invoke' &&
response[i].method === 'focus'
) {
focusChanged = true;
}
}
@@ -885,11 +996,19 @@
// If the focus hasn't be changed by the ajax commands, try to refocus the
// triggering element or one of its parents if that element does not exist
// anymore.
if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) {
if (
!focusChanged &&
this.element &&
!$(this.element).data('disable-refocus')
) {
let target = false;
for (let n = elementParents.length - 1; !target && n > 0; n--) {
target = document.querySelector(`[data-drupal-selector="${elementParents[n].getAttribute('data-drupal-selector')}"]`);
for (let n = elementParents.length - 1; !target && n >= 0; n--) {
target = document.querySelector(
`[data-drupal-selector="${elementParents[n].getAttribute(
'data-drupal-selector',
)}"]`,
);
}
if (target) {
@@ -925,7 +1044,7 @@
* Returns an object with `showEffect`, `hideEffect` and `showSpeed`
* properties.
*/
Drupal.Ajax.prototype.getEffect = function (response) {
Drupal.Ajax.prototype.getEffect = function(response) {
const type = response.effect || this.effect;
const speed = response.speed || this.speed;
@@ -934,13 +1053,11 @@
effect.showEffect = 'show';
effect.hideEffect = 'hide';
effect.showSpeed = '';
}
else if (type === 'fade') {
} else if (type === 'fade') {
effect.showEffect = 'fadeIn';
effect.hideEffect = 'fadeOut';
effect.showSpeed = speed;
}
else {
} else {
effect.showEffect = `${type}Toggle`;
effect.hideEffect = `${type}Toggle`;
effect.showSpeed = speed;
@@ -959,7 +1076,7 @@
* @param {string} [customMessage]
* Extra message to print with the Ajax error.
*/
Drupal.Ajax.prototype.error = function (xmlhttprequest, uri, customMessage) {
Drupal.Ajax.prototype.error = function(xmlhttprequest, uri, customMessage) {
// Remove the progress element.
if (this.progress.element) {
$(this.progress.element).remove();
@@ -979,6 +1096,59 @@
throw new Drupal.AjaxError(xmlhttprequest, uri, customMessage);
};
/**
* Provide a wrapper for new content via Ajax.
*
* Wrap the inserted markup when inserting multiple root elements with an
* ajax effect.
*
* @param {jQuery} $newContent
* Response elements after parsing.
* @param {Drupal.Ajax} ajax
* {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
* @param {object} response
* The response from the Ajax request.
*
* @deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0.
* Use data with desired wrapper. See https://www.drupal.org/node/2974880.
*
* @todo Add deprecation warning after it is possible. For more information
* see: https://www.drupal.org/project/drupal/issues/2973400
*
* @see https://www.drupal.org/node/2940704
*/
Drupal.theme.ajaxWrapperNewContent = ($newContent, ajax, response) =>
(response.effect || ajax.effect) !== 'none' &&
$newContent.filter(
i =>
!// We can not consider HTML comments or whitespace text as separate
// roots, since they do not cause visual regression with effect.
(
$newContent[i].nodeName === '#comment' ||
($newContent[i].nodeName === '#text' &&
/^(\s|\n|\r)*$/.test($newContent[i].textContent))
),
).length > 1
? Drupal.theme('ajaxWrapperMultipleRootElements', $newContent)
: $newContent;
/**
* Provide a wrapper for multiple root elements via Ajax.
*
* @param {jQuery} $elements
* Response elements after parsing.
*
* @deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0.
* Use data with desired wrapper. See https://www.drupal.org/node/2974880.
*
* @todo Add deprecation warning after it is possible. For more information
* see: https://www.drupal.org/project/drupal/issues/2973400
*
* @see https://www.drupal.org/node/2940704
*/
Drupal.theme.ajaxWrapperMultipleRootElements = $elements =>
$('<div></div>').append($elements);
/**
* @typedef {object} Drupal.AjaxCommands~commandDefinition
*
@@ -1007,9 +1177,8 @@
*
* @constructor
*/
Drupal.AjaxCommands = function () {};
Drupal.AjaxCommands = function() {};
Drupal.AjaxCommands.prototype = {
/**
* Command to insert new content into the DOM.
*
@@ -1025,39 +1194,31 @@
* A optional jQuery selector string.
* @param {object} [response.settings]
* An optional array of settings that will be used.
* @param {number} [status]
* The XMLHttpRequest status.
*/
insert(ajax, response, status) {
insert(ajax, response) {
// Get information from the response. If it is not there, default to
// our presets.
const $wrapper = response.selector ? $(response.selector) : $(ajax.wrapper);
const $wrapper = response.selector
? $(response.selector)
: $(ajax.wrapper);
const method = response.method || ajax.method;
const effect = ajax.getEffect(response);
let settings;
// We don't know what response.data contains: it might be a string of text
// without HTML, so don't rely on jQuery correctly interpreting
// $(response.data) as new HTML rather than a CSS selector. Also, if
// response.data contains top-level text nodes, they get lost with either
// $(response.data) or $('<div></div>').replaceWith(response.data).
const $newContentWrapped = $('<div></div>').html(response.data);
let $newContent = $newContentWrapped.contents();
// Apply any settings from the returned JSON if available.
const settings = response.settings || ajax.settings || drupalSettings;
// For legacy reasons, the effects processing code assumes that
// $newContent consists of a single top-level element. Also, it has not
// been sufficiently tested whether attachBehaviors() can be successfully
// called with a context object that includes top-level text nodes.
// However, to give developers full control of the HTML appearing in the
// page, and to enable Ajax content to be inserted in places where <div>
// elements are not allowed (e.g., within <table>, <tr>, and <span>
// parents), we check if the new content satisfies the requirement
// of a single top-level element, and only use the container <div> created
// above when it doesn't. For more information, please see
// https://www.drupal.org/node/736066.
if ($newContent.length !== 1 || $newContent.get(0).nodeType !== 1) {
$newContent = $newContentWrapped;
}
// Parse response.data into an element collection.
let $newContent = $($.parseHTML(response.data, document, true));
// For backward compatibility, in some cases a wrapper will be added. This
// behavior will be removed before Drupal 9.0.0. If different behavior is
// needed, the theme functions can be overriden.
// @see https://www.drupal.org/node/2940704
$newContent = Drupal.theme(
'ajaxWrapperNewContent',
$newContent,
ajax,
response,
);
// If removing content from the wrapper, detach behaviors first.
switch (method) {
@@ -1066,8 +1227,10 @@
case 'replaceAll':
case 'empty':
case 'remove':
settings = response.settings || ajax.settings || drupalSettings;
Drupal.detachBehaviors($wrapper.get(0), settings);
break;
default:
break;
}
// Add the new content to the page.
@@ -1080,22 +1243,25 @@
// Determine which effect to use and what content will receive the
// effect, then show the new content.
if ($newContent.find('.ajax-new-content').length > 0) {
$newContent.find('.ajax-new-content').hide();
const $ajaxNewContent = $newContent.find('.ajax-new-content');
if ($ajaxNewContent.length) {
$ajaxNewContent.hide();
$newContent.show();
$newContent.find('.ajax-new-content')[effect.showEffect](effect.showSpeed);
}
else if (effect.showEffect !== 'show') {
$ajaxNewContent[effect.showEffect](effect.showSpeed);
} else if (effect.showEffect !== 'show') {
$newContent[effect.showEffect](effect.showSpeed);
}
// Attach all JavaScript behaviors to the new content, if it was
// successfully added to the page, this if statement allows
// `#ajax['wrapper']` to be optional.
if ($newContent.parents('html').length > 0) {
// Apply any settings from the returned JSON if available.
settings = response.settings || ajax.settings || drupalSettings;
Drupal.attachBehaviors($newContent.get(0), settings);
if ($newContent.parents('html').length) {
// Attach behaviors to all element nodes.
$newContent.each((index, element) => {
if (element.nodeType === Node.ELEMENT_NODE) {
Drupal.attachBehaviors(element, settings);
}
});
}
},
@@ -1115,9 +1281,10 @@
*/
remove(ajax, response, status) {
const settings = response.settings || ajax.settings || drupalSettings;
$(response.selector).each(function () {
Drupal.detachBehaviors(this, settings);
})
$(response.selector)
.each(function() {
Drupal.detachBehaviors(this, settings);
})
.remove();
},
@@ -1141,7 +1308,13 @@
if (!$element.hasClass('ajax-changed')) {
$element.addClass('ajax-changed');
if (response.asterisk) {
$element.find(response.asterisk).append(` <abbr class="ajax-changed" title="${Drupal.t('Changed')}">*</abbr> `);
$element
.find(response.asterisk)
.append(
` <abbr class="ajax-changed" title="${Drupal.t(
'Changed',
)}">*</abbr> `,
);
}
}
},
@@ -1218,7 +1391,7 @@
// Clean up drupalSettings.ajax.
if (ajaxSettings) {
Drupal.ajax.expired().forEach((instance) => {
Drupal.ajax.expired().forEach(instance => {
// If the Ajax object has been created through drupalSettings.ajax
// it will have a selector. When there is no selector the object
// has been initialized with a special class name picked up by the
@@ -1235,8 +1408,7 @@
if (response.merge) {
$.extend(true, drupalSettings, response.settings);
}
else {
} else {
ajax.settings = response.settings;
}
},
@@ -1324,7 +1496,9 @@
* The XMLHttpRequest status.
*/
update_build_id(ajax, response, status) {
$(`input[name="form_build_id"][value="${response.old}"]`).val(response.new);
$(`input[name="form_build_id"][value="${response.old}"]`).val(
response.new,
);
},
/**
@@ -1348,8 +1522,11 @@
$('head').prepend(response.data);
// Add imports in the styles using the addImport method if available.
let match;
const importMatch = /^@import url\("(.*)"\);$/igm;
if (document.styleSheets[0].addImport && importMatch.test(response.data)) {
const importMatch = /^@import url\("(.*)"\);$/gim;
if (
document.styleSheets[0].addImport &&
importMatch.test(response.data)
) {
importMatch.lastIndex = 0;
do {
match = importMatch.exec(response.data);
@@ -1358,4 +1535,4 @@
}
},
};
}(jQuery, window, Drupal, drupalSettings));
})(jQuery, window, Drupal, drupalSettings);
+63 -27
View File
@@ -66,21 +66,29 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
statusText = '';
try {
statusText = '\n' + Drupal.t('StatusText: !statusText', { '!statusText': $.trim(xmlhttp.statusText) });
statusText = '\n' + Drupal.t('StatusText: !statusText', {
'!statusText': $.trim(xmlhttp.statusText)
});
} catch (e) {}
responseText = '';
try {
responseText = '\n' + Drupal.t('ResponseText: !responseText', { '!responseText': $.trim(xmlhttp.responseText) });
responseText = '\n' + Drupal.t('ResponseText: !responseText', {
'!responseText': $.trim(xmlhttp.responseText)
});
} catch (e) {}
responseText = responseText.replace(/<("[^"]*"|'[^']*'|[^'">])*>/gi, '');
responseText = responseText.replace(/[\n]+\s+/g, '\n');
var readyStateText = xmlhttp.status === 0 ? '\n' + Drupal.t('ReadyState: !readyState', { '!readyState': xmlhttp.readyState }) : '';
var readyStateText = xmlhttp.status === 0 ? '\n' + Drupal.t('ReadyState: !readyState', {
'!readyState': xmlhttp.readyState
}) : '';
customMessage = customMessage ? '\n' + Drupal.t('CustomMessage: !customMessage', { '!customMessage': customMessage }) : '';
customMessage = customMessage ? '\n' + Drupal.t('CustomMessage: !customMessage', {
'!customMessage': customMessage
}) : '';
this.message = statusCode + pathText + statusText + customMessage + responseText + readyStateText;
@@ -189,7 +197,7 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
var originalUrl = this.url;
this.url = this.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1');
this.url = this.url.replace(/\/nojs(\/|$|\?|#)/, '/ajax$1');
if (drupalSettings.ajaxTrustedUrl[originalUrl]) {
drupalSettings.ajaxTrustedUrl[this.url] = true;
@@ -254,7 +262,9 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
$(ajax.element).on(elementSettings.event, function (event) {
if (!drupalSettings.ajaxTrustedUrl[ajax.url] && !Drupal.url.isLocal(ajax.url)) {
throw new Error(Drupal.t('The callback URL is not local and not trusted: !url', { '!url': ajax.url }));
throw new Error(Drupal.t('The callback URL is not local and not trusted: !url', {
'!url': ajax.url
}));
}
return ajax.eventResponse(this, event);
});
@@ -368,6 +378,21 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
}
};
Drupal.theme.ajaxProgressThrobber = function (message) {
var messageMarkup = typeof message === 'string' ? Drupal.theme('ajaxProgressMessage', message) : '';
var throbber = '<div class="throbber">&nbsp;</div>';
return '<div class="ajax-progress ajax-progress-throbber">' + throbber + messageMarkup + '</div>';
};
Drupal.theme.ajaxProgressIndicatorFullscreen = function () {
return '<div class="ajax-progress ajax-progress-fullscreen">&nbsp;</div>';
};
Drupal.theme.ajaxProgressMessage = function (message) {
return '<div class="message">' + message + '</div>';
};
Drupal.Ajax.prototype.setProgressIndicatorBar = function () {
var progressBar = new Drupal.ProgressBar('ajax-progress-' + this.element.id, $.noop, this.progress.method, $.noop);
if (this.progress.message) {
@@ -382,15 +407,12 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
};
Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () {
this.progress.element = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
if (this.progress.message) {
this.progress.element.find('.throbber').after('<div class="message">' + this.progress.message + '</div>');
}
this.progress.element = $(Drupal.theme('ajaxProgressThrobber', this.progress.message));
$(this.element).after(this.progress.element);
};
Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function () {
this.progress.element = $('<div class="ajax-progress ajax-progress-fullscreen">&nbsp;</div>');
this.progress.element = $(Drupal.theme('ajaxProgressIndicatorFullscreen'));
$('body').after(this.progress.element);
};
@@ -420,7 +442,7 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) {
var target = false;
for (var n = elementParents.length - 1; !target && n > 0; n--) {
for (var n = elementParents.length - 1; !target && n >= 0; n--) {
target = document.querySelector('[data-drupal-selector="' + elementParents[n].getAttribute('data-drupal-selector') + '"]');
}
@@ -478,20 +500,28 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
throw new Drupal.AjaxError(xmlhttprequest, uri, customMessage);
};
Drupal.theme.ajaxWrapperNewContent = function ($newContent, ajax, response) {
return (response.effect || ajax.effect) !== 'none' && $newContent.filter(function (i) {
return !($newContent[i].nodeName === '#comment' || $newContent[i].nodeName === '#text' && /^(\s|\n|\r)*$/.test($newContent[i].textContent));
}).length > 1 ? Drupal.theme('ajaxWrapperMultipleRootElements', $newContent) : $newContent;
};
Drupal.theme.ajaxWrapperMultipleRootElements = function ($elements) {
return $('<div></div>').append($elements);
};
Drupal.AjaxCommands = function () {};
Drupal.AjaxCommands.prototype = {
insert: function insert(ajax, response, status) {
insert: function insert(ajax, response) {
var $wrapper = response.selector ? $(response.selector) : $(ajax.wrapper);
var method = response.method || ajax.method;
var effect = ajax.getEffect(response);
var settings = void 0;
var $newContentWrapped = $('<div></div>').html(response.data);
var $newContent = $newContentWrapped.contents();
var settings = response.settings || ajax.settings || drupalSettings;
if ($newContent.length !== 1 || $newContent.get(0).nodeType !== 1) {
$newContent = $newContentWrapped;
}
var $newContent = $($.parseHTML(response.data, document, true));
$newContent = Drupal.theme('ajaxWrapperNewContent', $newContent, ajax, response);
switch (method) {
case 'html':
@@ -499,8 +529,10 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
case 'replaceAll':
case 'empty':
case 'remove':
settings = response.settings || ajax.settings || drupalSettings;
Drupal.detachBehaviors($wrapper.get(0), settings);
break;
default:
break;
}
$wrapper[method]($newContent);
@@ -509,17 +541,21 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
$newContent.hide();
}
if ($newContent.find('.ajax-new-content').length > 0) {
$newContent.find('.ajax-new-content').hide();
var $ajaxNewContent = $newContent.find('.ajax-new-content');
if ($ajaxNewContent.length) {
$ajaxNewContent.hide();
$newContent.show();
$newContent.find('.ajax-new-content')[effect.showEffect](effect.showSpeed);
$ajaxNewContent[effect.showEffect](effect.showSpeed);
} else if (effect.showEffect !== 'show') {
$newContent[effect.showEffect](effect.showSpeed);
}
if ($newContent.parents('html').length > 0) {
settings = response.settings || ajax.settings || drupalSettings;
Drupal.attachBehaviors($newContent.get(0), settings);
if ($newContent.parents('html').length) {
$newContent.each(function (index, element) {
if (element.nodeType === Node.ELEMENT_NODE) {
Drupal.attachBehaviors(element, settings);
}
});
}
},
remove: function remove(ajax, response, status) {
@@ -584,7 +620,7 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
$('head').prepend(response.data);
var match = void 0;
var importMatch = /^@import url\("(.*)"\);$/igm;
var importMatch = /^@import url\("(.*)"\);$/gim;
if (document.styleSheets[0].addImport && importMatch.test(response.data)) {
importMatch.lastIndex = 0;
do {
+4 -4
View File
@@ -18,7 +18,7 @@
* });
*/
(function (Drupal, debounce) {
(function(Drupal, debounce) {
let liveElement;
const announcements = [];
@@ -102,7 +102,7 @@
*
* @see http://www.w3.org/WAI/PF/aria-practices/#liveprops
*/
Drupal.announce = function (text, priority) {
Drupal.announce = function(text, priority) {
// Save the text and priority into a closure variable. Multiple simultaneous
// announcements will be concatenated and read in sequence.
announcements.push({
@@ -112,6 +112,6 @@
// Immediately invoke the function that debounce returns. 200 ms is right at
// the cusp where humans notice a pause, so we will wait
// at most this much time before the set of queued announcements is read.
return (debounce(announce, 200)());
return debounce(announce, 200)();
};
}(Drupal, Drupal.debounce));
})(Drupal, Drupal.debounce);
+29 -21
View File
@@ -3,7 +3,7 @@
* Autocomplete based on jQuery UI.
*/
(function ($, Drupal) {
(function($, Drupal) {
let autocomplete;
/**
@@ -30,12 +30,10 @@
if (character === '"') {
current += character;
quote = !quote;
}
else if (character === ',' && !quote) {
} else if (character === ',' && !quote) {
result.push(current.trim());
current = '';
}
else {
} else {
current += character;
}
}
@@ -81,7 +79,10 @@
const term = autocomplete.extractLastTerm(event.target.value);
// Abort search if the first character is in firstCharacterBlacklist.
if (term.length > 0 && options.firstCharacterBlacklist.indexOf(term[0]) !== -1) {
if (
term.length > 0 &&
options.firstCharacterBlacklist.indexOf(term[0]) !== -1
) {
return false;
}
// Only search when the term is at least the minimum length.
@@ -122,6 +123,9 @@
response(suggestions);
}
// Get the desired term and construct the autocomplete URL for it.
const term = autocomplete.extractLastTerm(request.term);
/**
* Transforms the data object into an array and update autocomplete results.
*
@@ -135,15 +139,14 @@
showSuggestions(data);
}
// Get the desired term and construct the autocomplete URL for it.
const term = autocomplete.extractLastTerm(request.term);
// Check if the term is already cached.
if (autocomplete.cache[elementId].hasOwnProperty(term)) {
showSuggestions(autocomplete.cache[elementId][term]);
}
else {
const options = $.extend({ success: sourceCallbackHandler, data: { q: term } }, autocomplete.ajax);
} else {
const options = $.extend(
{ success: sourceCallbackHandler, data: { q: term } },
autocomplete.ajax,
);
$.ajax(this.element.attr('data-autocomplete-path'), options);
}
}
@@ -211,18 +214,22 @@
Drupal.behaviors.autocomplete = {
attach(context) {
// Act on textfields with the "form-autocomplete" class.
const $autocomplete = $(context).find('input.form-autocomplete').once('autocomplete');
const $autocomplete = $(context)
.find('input.form-autocomplete')
.once('autocomplete');
if ($autocomplete.length) {
// Allow options to be overriden per instance.
const blacklist = $autocomplete.attr('data-autocomplete-first-character-blacklist');
const blacklist = $autocomplete.attr(
'data-autocomplete-first-character-blacklist',
);
$.extend(autocomplete.options, {
firstCharacterBlacklist: (blacklist) || '',
firstCharacterBlacklist: blacklist || '',
});
// Use jQuery UI Autocomplete on the textfield.
$autocomplete.autocomplete(autocomplete.options)
.each(function () {
$(this).data('ui-autocomplete')._renderItem = autocomplete.options.renderItem;
});
$autocomplete.autocomplete(autocomplete.options).each(function() {
$(this).data('ui-autocomplete')._renderItem =
autocomplete.options.renderItem;
});
// Use CompositionEvent to handle IME inputs. It requests remote server on "compositionend" event only.
$autocomplete.on('compositionstart.autocomplete', () => {
@@ -235,7 +242,8 @@
},
detach(context, settings, trigger) {
if (trigger === 'unload') {
$(context).find('input.form-autocomplete')
$(context)
.find('input.form-autocomplete')
.removeOnce('autocomplete')
.autocomplete('destroy');
}
@@ -277,4 +285,4 @@
};
Drupal.autocomplete = autocomplete;
}(jQuery, Drupal));
})(jQuery, Drupal);
+2 -2
View File
@@ -73,14 +73,14 @@
response(suggestions);
}
var term = autocomplete.extractLastTerm(request.term);
function sourceCallbackHandler(data) {
autocomplete.cache[elementId][term] = data;
showSuggestions(data);
}
var term = autocomplete.extractLastTerm(request.term);
if (autocomplete.cache[elementId].hasOwnProperty(term)) {
showSuggestions(autocomplete.cache[elementId][term]);
} else {
+8 -3
View File
@@ -3,7 +3,7 @@
* Drupal's batch API.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Attaches the batch behavior to progress bars.
*
@@ -29,7 +29,12 @@
}
if ($progress.length) {
progressBar = new Drupal.ProgressBar('updateprogress', updateCallback, 'POST', errorCallback);
progressBar = new Drupal.ProgressBar(
'updateprogress',
updateCallback,
'POST',
errorCallback,
);
progressBar.setProgress(-1, batch.initMessage);
progressBar.startMonitoring(`${batch.uri}&op=do`, 10);
// Remove HTML from no-js progress bar.
@@ -39,4 +44,4 @@
}
},
};
}(jQuery, Drupal));
})(jQuery, Drupal);
+108 -88
View File
@@ -3,7 +3,7 @@
* Polyfill for HTML5 details elements.
*/
(function ($, Modernizr, Drupal) {
(function($, Modernizr, Drupal) {
/**
* The collapsible details object represents a single details element.
*
@@ -17,7 +17,10 @@
this.$node.data('details', this);
// Expand details if there are errors inside, or if it contains an
// element that is targeted by the URI fragment identifier.
const anchor = location.hash && location.hash !== '#' ? `, ${location.hash}` : '';
const anchor =
window.location.hash && window.location.hash !== '#'
? `, ${window.location.hash}`
: '';
if (this.$node.find(`.error${anchor}`).length) {
this.$node.attr('open', true);
}
@@ -27,93 +30,98 @@
this.setupLegend();
}
$.extend(CollapsibleDetails, /** @lends Drupal.CollapsibleDetails */{
/**
* Holds references to instantiated CollapsibleDetails objects.
*
* @type {Array.<Drupal.CollapsibleDetails>}
*/
instances: [],
});
$.extend(CollapsibleDetails.prototype, /** @lends Drupal.CollapsibleDetails# */{
/**
* Initialize and setup summary events and markup.
*
* @fires event:summaryUpdated
*
* @listens event:summaryUpdated
*/
setupSummary() {
this.$summary = $('<span class="summary"></span>');
this.$node
.on('summaryUpdated', $.proxy(this.onSummaryUpdated, this))
.trigger('summaryUpdated');
$.extend(
CollapsibleDetails,
/** @lends Drupal.CollapsibleDetails */ {
/**
* Holds references to instantiated CollapsibleDetails objects.
*
* @type {Array.<Drupal.CollapsibleDetails>}
*/
instances: [],
},
);
/**
* Initialize and setup legend markup.
*/
setupLegend() {
// Turn the summary into a clickable link.
const $legend = this.$node.find('> summary');
$.extend(
CollapsibleDetails.prototype,
/** @lends Drupal.CollapsibleDetails# */ {
/**
* Initialize and setup summary events and markup.
*
* @fires event:summaryUpdated
*
* @listens event:summaryUpdated
*/
setupSummary() {
this.$summary = $('<span class="summary"></span>');
this.$node
.on('summaryUpdated', $.proxy(this.onSummaryUpdated, this))
.trigger('summaryUpdated');
},
$('<span class="details-summary-prefix visually-hidden"></span>')
.append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show'))
.prependTo($legend)
.after(document.createTextNode(' '));
/**
* Initialize and setup legend markup.
*/
setupLegend() {
// Turn the summary into a clickable link.
const $legend = this.$node.find('> summary');
// .wrapInner() does not retain bound events.
$('<a class="details-title"></a>')
.attr('href', `#${this.$node.attr('id')}`)
.prepend($legend.contents())
.appendTo($legend);
$('<span class="details-summary-prefix visually-hidden"></span>')
.append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show'))
.prependTo($legend)
.after(document.createTextNode(' '));
$legend
.append(this.$summary)
.on('click', $.proxy(this.onLegendClick, this));
// .wrapInner() does not retain bound events.
$('<a class="details-title"></a>')
.attr('href', `#${this.$node.attr('id')}`)
.prepend($legend.contents())
.appendTo($legend);
$legend
.append(this.$summary)
.on('click', $.proxy(this.onLegendClick, this));
},
/**
* Handle legend clicks.
*
* @param {jQuery.Event} e
* The event triggered.
*/
onLegendClick(e) {
this.toggle();
e.preventDefault();
},
/**
* Update summary.
*/
onSummaryUpdated() {
const text = $.trim(this.$node.drupalGetSummary());
this.$summary.html(text ? ` (${text})` : '');
},
/**
* Toggle the visibility of a details element using smooth animations.
*/
toggle() {
const isOpen = !!this.$node.attr('open');
const $summaryPrefix = this.$node.find(
'> summary span.details-summary-prefix',
);
if (isOpen) {
$summaryPrefix.html(Drupal.t('Show'));
} else {
$summaryPrefix.html(Drupal.t('Hide'));
}
// Delay setting the attribute to emulate chrome behavior and make
// details-aria.js work as expected with this polyfill.
setTimeout(() => {
this.$node.attr('open', !isOpen);
}, 0);
},
},
/**
* Handle legend clicks.
*
* @param {jQuery.Event} e
* The event triggered.
*/
onLegendClick(e) {
this.toggle();
e.preventDefault();
},
/**
* Update summary.
*/
onSummaryUpdated() {
const text = $.trim(this.$node.drupalGetSummary());
this.$summary.html(text ? ` (${text})` : '');
},
/**
* Toggle the visibility of a details element using smooth animations.
*/
toggle() {
const isOpen = !!this.$node.attr('open');
const $summaryPrefix = this.$node.find('> summary span.details-summary-prefix');
if (isOpen) {
$summaryPrefix.html(Drupal.t('Show'));
}
else {
$summaryPrefix.html(Drupal.t('Hide'));
}
// Delay setting the attribute to emulate chrome behavior and make
// details-aria.js work as expected with this polyfill.
setTimeout(() => {
this.$node.attr('open', !isOpen);
}, 0);
},
});
);
/**
* Polyfill HTML5 details element.
@@ -128,10 +136,15 @@
if (Modernizr.details) {
return;
}
const $collapsibleDetails = $(context).find('details').once('collapse').addClass('collapse-processed');
const $collapsibleDetails = $(context)
.find('details')
.once('collapse')
.addClass('collapse-processed');
if ($collapsibleDetails.length) {
for (let i = 0; i < $collapsibleDetails.length; i++) {
CollapsibleDetails.instances.push(new CollapsibleDetails($collapsibleDetails[i]));
CollapsibleDetails.instances.push(
new CollapsibleDetails($collapsibleDetails[i]),
);
}
}
},
@@ -151,14 +164,21 @@
* The targeted node as a jQuery object.
*/
const handleFragmentLinkClickOrHashChange = (e, $target) => {
$target.parents('details').not('[open]').find('> summary').trigger('click');
$target
.parents('details')
.not('[open]')
.find('> summary')
.trigger('click');
};
/**
* Binds a listener to handle fragment link clicks and URL hash changes.
*/
$('body').on('formFragmentLinkClickOrHashChange.details', handleFragmentLinkClickOrHashChange);
$('body').on(
'formFragmentLinkClickOrHashChange.details',
handleFragmentLinkClickOrHashChange,
);
// Expose constructor in the public space.
Drupal.CollapsibleDetails = CollapsibleDetails;
}(jQuery, Modernizr, Drupal));
})(jQuery, Modernizr, Drupal);
+1 -1
View File
@@ -10,7 +10,7 @@
this.$node = $(node);
this.$node.data('details', this);
var anchor = location.hash && location.hash !== '#' ? ', ' + location.hash : '';
var anchor = window.location.hash && window.location.hash !== '#' ? ', ' + window.location.hash : '';
if (this.$node.find('.error' + anchor).length) {
this.$node.attr('open', true);
}
+28 -22
View File
@@ -3,7 +3,7 @@
* Polyfill for HTML5 date input.
*/
(function ($, Modernizr, Drupal) {
(function($, Modernizr, Drupal) {
/**
* Attach datepicker fallback on date elements.
*
@@ -23,30 +23,36 @@
if (Modernizr.inputtypes.date === true) {
return;
}
$context.find('input[data-drupal-date-format]').once('datePicker').each(function () {
const $input = $(this);
const datepickerSettings = {};
const dateFormat = $input.data('drupalDateFormat');
// The date format is saved in PHP style, we need to convert to jQuery
// datepicker.
datepickerSettings.dateFormat = dateFormat
.replace('Y', 'yy')
.replace('m', 'mm')
.replace('d', 'dd');
// Add min and max date if set on the input.
if ($input.attr('min')) {
datepickerSettings.minDate = $input.attr('min');
}
if ($input.attr('max')) {
datepickerSettings.maxDate = $input.attr('max');
}
$input.datepicker(datepickerSettings);
});
$context
.find('input[data-drupal-date-format]')
.once('datePicker')
.each(function() {
const $input = $(this);
const datepickerSettings = {};
const dateFormat = $input.data('drupalDateFormat');
// The date format is saved in PHP style, we need to convert to jQuery
// datepicker.
datepickerSettings.dateFormat = dateFormat
.replace('Y', 'yy')
.replace('m', 'mm')
.replace('d', 'dd');
// Add min and max date if set on the input.
if ($input.attr('min')) {
datepickerSettings.minDate = $input.attr('min');
}
if ($input.attr('max')) {
datepickerSettings.maxDate = $input.attr('max');
}
$input.datepicker(datepickerSettings);
});
},
detach(context, settings, trigger) {
if (trigger === 'unload') {
$(context).find('input[data-drupal-date-format]').findOnce('datePicker').datepicker('destroy');
$(context)
.find('input[data-drupal-date-format]')
.findOnce('datePicker')
.datepicker('destroy');
}
},
};
}(jQuery, Modernizr, Drupal));
})(jQuery, Modernizr, Drupal);
+3 -3
View File
@@ -26,12 +26,12 @@
* @return {function}
* The debounced function.
*/
Drupal.debounce = function (func, wait, immediate) {
Drupal.debounce = function(func, wait, immediate) {
let timeout;
let result;
return function (...args) {
return function(...args) {
const context = this;
const later = function () {
const later = function() {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
+14 -9
View File
@@ -3,7 +3,7 @@
* Add aria attribute handling for details and summary elements.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Handles `aria-expanded` and `aria-pressed` attributes on details elements.
*
@@ -11,15 +11,20 @@
*/
Drupal.behaviors.detailsAria = {
attach() {
$('body').once('detailsAria').on('click.detailsAria', 'summary', (event) => {
const $summary = $(event.currentTarget);
const open = $(event.currentTarget.parentNode).attr('open') === 'open' ? 'false' : 'true';
$('body')
.once('detailsAria')
.on('click.detailsAria', 'summary', event => {
const $summary = $(event.currentTarget);
const open =
$(event.currentTarget.parentNode).attr('open') === 'open'
? 'false'
: 'true';
$summary.attr({
'aria-expanded': open,
'aria-pressed': open,
$summary.attr({
'aria-expanded': open,
'aria-pressed': open,
});
});
});
},
};
}(jQuery, Drupal));
})(jQuery, Drupal);
+34 -18
View File
@@ -3,7 +3,7 @@
* Extends the Drupal AJAX functionality to integrate the dialog API.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Initialize dialogs for Ajax purposes.
*
@@ -23,7 +23,9 @@
// Add 'ui-front' jQuery UI class so jQuery UI widgets like autocomplete
// sit on top of dialogs. For more information see
// http://api.jqueryui.com/theming/stacking-elements/.
$('<div id="drupal-modal" class="ui-front"/>').hide().appendTo('body');
$('<div id="drupal-modal" class="ui-front"/>')
.hide()
.appendTo('body');
}
// Special behaviors specific when attaching content within a dialog.
@@ -42,7 +44,7 @@
const originalClose = settings.dialog.close;
// Overwrite the close method to remove the dialog on closing.
settings.dialog.close = function (event, ...args) {
settings.dialog.close = function(event, ...args) {
originalClose.apply(settings.dialog, [event, ...args]);
$(event.target).remove();
};
@@ -59,8 +61,10 @@
*/
prepareDialogButtons($dialog) {
const buttons = [];
const $buttons = $dialog.find('.form-actions input[type=submit], .form-actions a.button');
$buttons.each(function () {
const $buttons = $dialog.find(
'.form-actions input[type=submit], .form-actions a.button',
);
$buttons.each(function() {
// Hidden form buttons need special attention. For browser consistency,
// the button needs to be "visible" in order to have the enter key fire
// the form submit event. So instead of a simple "hide" or
@@ -82,9 +86,11 @@
// event will not simulate a click. Use the click method instead.
if ($originalButton.is('a')) {
$originalButton[0].click();
}
else {
$originalButton.trigger('mousedown').trigger('mouseup').trigger('click');
} else {
$originalButton
.trigger('mousedown')
.trigger('mouseup')
.trigger('click');
e.preventDefault();
}
},
@@ -107,14 +113,16 @@
* @return {bool|undefined}
* Returns false if there was no selector property in the response object.
*/
Drupal.AjaxCommands.prototype.openDialog = function (ajax, response, status) {
Drupal.AjaxCommands.prototype.openDialog = function(ajax, response, status) {
if (!response.selector) {
return false;
}
let $dialog = $(response.selector);
if (!$dialog.length) {
// Create the element if needed.
$dialog = $(`<div id="${response.selector.replace(/^#/, '')}" class="ui-front"/>`).appendTo('body');
$dialog = $(
`<div id="${response.selector.replace(/^#/, '')}" class="ui-front"/>`,
).appendTo('body');
}
// Set up the wrapper, if there isn't one.
if (!ajax.wrapper) {
@@ -129,7 +137,9 @@
// Move the buttons to the jQuery UI dialog buttons area.
if (!response.dialogOptions.buttons) {
response.dialogOptions.drupalAutoButtons = true;
response.dialogOptions.buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog);
response.dialogOptions.buttons = Drupal.behaviors.dialog.prepareDialogButtons(
$dialog,
);
}
// Bind dialogButtonsChange.
@@ -143,13 +153,15 @@
const dialog = Drupal.dialog($dialog.get(0), response.dialogOptions);
if (response.dialogOptions.modal) {
dialog.showModal();
}
else {
} else {
dialog.show();
}
// Add the standard Drupal class for buttons for style consistency.
$dialog.parent().find('.ui-dialog-buttonset').addClass('form-actions');
$dialog
.parent()
.find('.ui-dialog-buttonset')
.addClass('form-actions');
};
/**
@@ -168,7 +180,7 @@
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.closeDialog = function (ajax, response, status) {
Drupal.AjaxCommands.prototype.closeDialog = function(ajax, response, status) {
const $dialog = $(response.selector);
if ($dialog.length) {
Drupal.dialog($dialog.get(0)).close();
@@ -199,7 +211,11 @@
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.setDialogOption = function (ajax, response, status) {
Drupal.AjaxCommands.prototype.setDialogOption = function(
ajax,
response,
status,
) {
const $dialog = $(response.selector);
if ($dialog.length) {
$dialog.dialog('option', response.optionName, response.optionValue);
@@ -219,7 +235,7 @@
* Dialog settings.
*/
$(window).on('dialog:aftercreate', (e, dialog, $element, settings) => {
$element.on('click.dialog', '.dialog-cancel', (e) => {
$element.on('click.dialog', '.dialog-cancel', e => {
dialog.close('cancel');
e.preventDefault();
e.stopPropagation();
@@ -239,4 +255,4 @@
$(window).on('dialog:beforeclose', (e, dialog, $element) => {
$element.off('.dialog');
});
}(jQuery, Drupal));
})(jQuery, Drupal);
+11 -10
View File
@@ -5,7 +5,7 @@
* @see http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#the-dialog-element
*/
(function ($, Drupal, drupalSettings) {
(function($, Drupal, drupalSettings) {
/**
* Default dialog options.
*
@@ -59,19 +59,12 @@
* @return {Drupal.dialog~dialogDefinition}
* The dialog instance.
*/
Drupal.dialog = function (element, options) {
Drupal.dialog = function(element, options) {
let undef;
const $element = $(element);
const dialog = {
open: false,
returnValue: undef,
show() {
openDialog({ modal: false });
},
showModal() {
openDialog({ modal: true });
},
close: closeDialog,
};
function openDialog(settings) {
@@ -91,6 +84,14 @@
$(window).trigger('dialog:afterclose', [dialog, $element]);
}
dialog.show = () => {
openDialog({ modal: false });
};
dialog.showModal = () => {
openDialog({ modal: true });
};
dialog.close = closeDialog;
return dialog;
};
}(jQuery, Drupal, drupalSettings));
})(jQuery, Drupal, drupalSettings);
+6 -3
View File
@@ -3,7 +3,7 @@
* Adds default classes to buttons for styling purposes.
*/
(function ($) {
(function($) {
$.widget('ui.dialog', $.ui.dialog, {
options: {
buttonClass: 'button',
@@ -15,7 +15,10 @@
let index;
const il = opts.buttons.length;
for (index = 0; index < il; index++) {
if (opts.buttons[index].primary && opts.buttons[index].primary === true) {
if (
opts.buttons[index].primary &&
opts.buttons[index].primary === true
) {
primaryIndex = index;
delete opts.buttons[index].primary;
break;
@@ -28,4 +31,4 @@
}
},
});
}(jQuery));
})(jQuery);
+9 -9
View File
@@ -23,15 +23,7 @@
var $element = $(element);
var dialog = {
open: false,
returnValue: undef,
show: function show() {
openDialog({ modal: false });
},
showModal: function showModal() {
openDialog({ modal: true });
},
close: closeDialog
returnValue: undef
};
function openDialog(settings) {
@@ -51,6 +43,14 @@
$(window).trigger('dialog:afterclose', [dialog, $element]);
}
dialog.show = function () {
openDialog({ modal: false });
};
dialog.showModal = function () {
openDialog({ modal: true });
};
dialog.close = closeDialog;
return dialog;
};
})(jQuery, Drupal, drupalSettings);
+88 -58
View File
@@ -9,57 +9,12 @@
* @event dialogContentResize
*/
(function ($, Drupal, drupalSettings, debounce, displace) {
(function($, Drupal, drupalSettings, debounce, displace) {
// autoResize option will turn off resizable and draggable.
drupalSettings.dialog = $.extend({ autoResize: true, maxHeight: '95%' }, drupalSettings.dialog);
/**
* Resets the current options for positioning.
*
* This is used as a window resize and scroll callback to reposition the
* jQuery UI dialog. Although not a built-in jQuery UI option, this can
* be disabled by setting autoResize: false in the options array when creating
* a new {@link Drupal.dialog}.
*
* @function Drupal.dialog~resetSize
*
* @param {jQuery.Event} event
* The event triggered.
*
* @fires event:dialogContentResize
*/
function resetSize(event) {
const positionOptions = ['width', 'height', 'minWidth', 'minHeight', 'maxHeight', 'maxWidth', 'position'];
let adjustedOptions = {};
let windowHeight = $(window).height();
let option;
let optionValue;
let adjustedValue;
for (let n = 0; n < positionOptions.length; n++) {
option = positionOptions[n];
optionValue = event.data.settings[option];
if (optionValue) {
// jQuery UI does not support percentages on heights, convert to pixels.
if (typeof optionValue === 'string' && /%$/.test(optionValue) && /height/i.test(option)) {
// Take offsets in account.
windowHeight -= displace.offsets.top + displace.offsets.bottom;
adjustedValue = parseInt(0.01 * parseInt(optionValue, 10) * windowHeight, 10);
// Don't force the dialog to be bigger vertically than needed.
if (option === 'height' && event.data.$element.parent().outerHeight() < adjustedValue) {
adjustedValue = 'auto';
}
adjustedOptions[option] = adjustedValue;
}
}
}
// Offset the dialog center to be at the center of Drupal.displace.offsets.
if (!event.data.settings.modal) {
adjustedOptions = resetPosition(adjustedOptions);
}
event.data.$element
.dialog('option', adjustedOptions)
.trigger('dialogContentResize');
}
drupalSettings.dialog = $.extend(
{ autoResize: true, maxHeight: '95%' },
drupalSettings.dialog,
);
/**
* Position the dialog's center at the center of displace.offsets boundaries.
@@ -77,32 +32,107 @@
const left = offsets.left - offsets.right;
const top = offsets.top - offsets.bottom;
const leftString = `${(left > 0 ? '+' : '-') + Math.abs(Math.round(left / 2))}px`;
const topString = `${(top > 0 ? '+' : '-') + Math.abs(Math.round(top / 2))}px`;
const leftString = `${(left > 0 ? '+' : '-') +
Math.abs(Math.round(left / 2))}px`;
const topString = `${(top > 0 ? '+' : '-') +
Math.abs(Math.round(top / 2))}px`;
options.position = {
my: `center${left !== 0 ? leftString : ''} center${top !== 0 ? topString : ''}`,
my: `center${left !== 0 ? leftString : ''} center${
top !== 0 ? topString : ''
}`,
of: window,
};
return options;
}
/**
* Resets the current options for positioning.
*
* This is used as a window resize and scroll callback to reposition the
* jQuery UI dialog. Although not a built-in jQuery UI option, this can
* be disabled by setting autoResize: false in the options array when creating
* a new {@link Drupal.dialog}.
*
* @function Drupal.dialog~resetSize
*
* @param {jQuery.Event} event
* The event triggered.
*
* @fires event:dialogContentResize
*/
function resetSize(event) {
const positionOptions = [
'width',
'height',
'minWidth',
'minHeight',
'maxHeight',
'maxWidth',
'position',
];
let adjustedOptions = {};
let windowHeight = $(window).height();
let option;
let optionValue;
let adjustedValue;
for (let n = 0; n < positionOptions.length; n++) {
option = positionOptions[n];
optionValue = event.data.settings[option];
if (optionValue) {
// jQuery UI does not support percentages on heights, convert to pixels.
if (
typeof optionValue === 'string' &&
/%$/.test(optionValue) &&
/height/i.test(option)
) {
// Take offsets in account.
windowHeight -= displace.offsets.top + displace.offsets.bottom;
adjustedValue = parseInt(
0.01 * parseInt(optionValue, 10) * windowHeight,
10,
);
// Don't force the dialog to be bigger vertically than needed.
if (
option === 'height' &&
event.data.$element.parent().outerHeight() < adjustedValue
) {
adjustedValue = 'auto';
}
adjustedOptions[option] = adjustedValue;
}
}
}
// Offset the dialog center to be at the center of Drupal.displace.offsets.
if (!event.data.settings.modal) {
adjustedOptions = resetPosition(adjustedOptions);
}
event.data.$element
.dialog('option', adjustedOptions)
.trigger('dialogContentResize');
}
$(window).on({
'dialog:aftercreate': function (event, dialog, $element, settings) {
'dialog:aftercreate': function(event, dialog, $element, settings) {
const autoResize = debounce(resetSize, 20);
const eventData = { settings, $element };
if (settings.autoResize === true || settings.autoResize === 'true') {
$element
.dialog('option', { resizable: false, draggable: false })
.dialog('widget').css('position', 'fixed');
.dialog('widget')
.css('position', 'fixed');
$(window)
.on('resize.dialogResize scroll.dialogResize', eventData, autoResize)
.trigger('resize.dialogResize');
$(document).on('drupalViewportOffsetChange.dialogResize', eventData, autoResize);
$(document).on(
'drupalViewportOffsetChange.dialogResize',
eventData,
autoResize,
);
}
},
'dialog:beforeclose': function (event, dialog, $element) {
'dialog:beforeclose': function(event, dialog, $element) {
$(window).off('.dialogResize');
$(document).off('.dialogResize');
},
});
}(jQuery, Drupal, drupalSettings, Drupal.debounce, Drupal.displace));
})(jQuery, Drupal, drupalSettings, Drupal.debounce, Drupal.displace);
+14 -14
View File
@@ -8,6 +8,20 @@
(function ($, Drupal, drupalSettings, debounce, displace) {
drupalSettings.dialog = $.extend({ autoResize: true, maxHeight: '95%' }, drupalSettings.dialog);
function resetPosition(options) {
var offsets = displace.offsets;
var left = offsets.left - offsets.right;
var top = offsets.top - offsets.bottom;
var leftString = (left > 0 ? '+' : '-') + Math.abs(Math.round(left / 2)) + 'px';
var topString = (top > 0 ? '+' : '-') + Math.abs(Math.round(top / 2)) + 'px';
options.position = {
my: 'center' + (left !== 0 ? leftString : '') + ' center' + (top !== 0 ? topString : ''),
of: window
};
return options;
}
function resetSize(event) {
var positionOptions = ['width', 'height', 'minWidth', 'minHeight', 'maxHeight', 'maxWidth', 'position'];
var adjustedOptions = {};
@@ -37,20 +51,6 @@
event.data.$element.dialog('option', adjustedOptions).trigger('dialogContentResize');
}
function resetPosition(options) {
var offsets = displace.offsets;
var left = offsets.left - offsets.right;
var top = offsets.top - offsets.bottom;
var leftString = (left > 0 ? '+' : '-') + Math.abs(Math.round(left / 2)) + 'px';
var topString = (top > 0 ? '+' : '-') + Math.abs(Math.round(top / 2)) + 'px';
options.position = {
my: 'center' + (left !== 0 ? leftString : '') + ' center' + (top !== 0 ? topString : ''),
of: window
};
return options;
}
$(window).on({
'dialog:aftercreate': function dialogAftercreate(event, dialog, $element, settings) {
var autoResize = debounce(resetSize, 20);
+1 -1
View File
@@ -30,7 +30,7 @@
/* Wrap the form that's inside the off-canvas dialog. */
.ui-dialog-off-canvas #drupal-off-canvas {
padding: 0 20px;
padding: 0 20px 20px;
/* Prevent horizontal scrollbar. */
overflow-x: hidden;
overflow-y: auto;
+121 -35
View File
@@ -13,6 +13,19 @@
* @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
@@ -75,10 +88,14 @@
};
/**
* Applies initial height to dialog based on window height.
* Applies initial height and with to dialog based depending on position.
* @see http://api.jqueryui.com/dialog for all dialog options.
*/
settings.height = $(window).height();
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;
},
/**
@@ -90,8 +107,7 @@
$('body').removeClass('js-off-canvas-dialog-open');
// Remove all *.off-canvas events
Drupal.offCanvas.removeOffCanvasEvents($element);
Drupal.offCanvas.$mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`, 0);
Drupal.offCanvas.resetPadding();
},
/**
@@ -108,13 +124,27 @@
const eventData = { settings, $element, offCanvasDialog: this };
$element
.on('dialogContentResize.off-canvas', eventData, Drupal.offCanvas.handleDialogResize)
.on('dialogContentResize.off-canvas', eventData, Drupal.offCanvas.bodyPadding);
.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()}`, '');
Drupal.offCanvas
.getContainer($element)
.attr(`data-offset-${Drupal.offCanvas.getEdge()}`, '');
$(window)
.on('resize.off-canvas', eventData, debounce(Drupal.offCanvas.resetSize, 100))
.on(
'resize.off-canvas',
eventData,
debounce(Drupal.offCanvas.resetSize, 100),
)
.trigger('resize.off-canvas');
},
@@ -128,7 +158,9 @@
* @return {undefined}
*/
render({ settings }) {
$('.ui-dialog-off-canvas, .ui-dialog-off-canvas .ui-dialog-titlebar').toggleClass('ui-dialog-empty-title', !settings.title);
$(
'.ui-dialog-off-canvas, .ui-dialog-off-canvas .ui-dialog-titlebar',
).toggleClass('ui-dialog-empty-title', !settings.title);
},
/**
@@ -143,7 +175,9 @@
const $element = event.data.$element;
const $container = Drupal.offCanvas.getContainer($element);
const $offsets = $container.find('> :not(#drupal-off-canvas, .ui-resizable-handle)');
const $offsets = $container.find(
'> :not(#drupal-off-canvas, .ui-resizable-handle)',
);
let offset = 0;
// Let scroll element take all the height available.
@@ -168,11 +202,26 @@
* Data attached to the event.
*/
resetSize(event) {
const offsets = displace.offsets;
const $element = event.data.$element;
const container = Drupal.offCanvas.getContainer($element);
const position = event.data.settings.drupalOffCanvasPosition;
const topPosition = (offsets.top !== 0 ? `+${offsets.top}` : '');
// 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: {
@@ -182,14 +231,20 @@
},
};
const height =
position === 'side'
? `${$(window).height() - (offsets.top + offsets.bottom)}px`
: event.data.settings.height;
container.css({
position: 'fixed',
height: `${$(window).height() - (offsets.top + offsets.bottom)}px`,
height,
});
$element
.dialog('option', adjustedOptions)
.trigger('dialogContentResize.off-canvas');
Drupal.offCanvas.position = position;
},
/**
@@ -201,20 +256,37 @@
* Data attached to the event.
*/
bodyPadding(event) {
if ($('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth) {
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 (width !== mainCanvasPadding) {
$mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`, `${width}px`);
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();
}
},
/**
@@ -238,6 +310,18 @@
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();
},
};
/**
@@ -250,24 +334,26 @@
*/
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 });
}
},
});
$(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);
+44 -8
View File
@@ -7,6 +7,10 @@
(function ($, Drupal, debounce, displace) {
Drupal.offCanvas = {
position: null,
minimumHeight: 30,
minDisplaceWidth: 768,
$mainCanvasWrapper: $('[data-off-canvas-main-canvas]'),
@@ -33,7 +37,11 @@
of: window
};
settings.height = $(window).height();
var position = settings.drupalOffCanvasPosition;
var height = position === 'side' ? $(window).height() : settings.height;
var width = position === 'side' ? settings.width : '100%';
settings.height = height;
settings.width = width;
},
beforeClose: function beforeClose(_ref2) {
var $element = _ref2.$element;
@@ -41,8 +49,7 @@
$('body').removeClass('js-off-canvas-dialog-open');
Drupal.offCanvas.removeOffCanvasEvents($element);
Drupal.offCanvas.$mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge(), 0);
Drupal.offCanvas.resetPadding();
},
afterCreate: function afterCreate(_ref3) {
var $element = _ref3.$element,
@@ -79,11 +86,23 @@
$element.height(modalHeight - offset - scrollOffset);
},
resetSize: function resetSize(event) {
var offsets = displace.offsets;
var $element = event.data.$element;
var container = Drupal.offCanvas.getContainer($element);
var position = event.data.settings.drupalOffCanvasPosition;
var topPosition = offsets.top !== 0 ? '+' + offsets.top : '';
if (Drupal.offCanvas.position && Drupal.offCanvas.position !== position) {
container.removeAttr('data-offset-' + Drupal.offCanvas.position);
}
if (position === 'top') {
$element.css('min-height', Drupal.offCanvas.minimumHeight + 'px');
}
displace();
var offsets = displace.offsets;
var topPosition = position === 'side' && offsets.top !== 0 ? '+' + offsets.top : '';
var adjustedOptions = {
position: {
my: Drupal.offCanvas.getEdge() + ' top',
@@ -92,34 +111,51 @@
}
};
var height = position === 'side' ? $(window).height() - (offsets.top + offsets.bottom) + 'px' : event.data.settings.height;
container.css({
position: 'fixed',
height: $(window).height() - (offsets.top + offsets.bottom) + 'px'
height: height
});
$element.dialog('option', adjustedOptions).trigger('dialogContentResize.off-canvas');
Drupal.offCanvas.position = position;
},
bodyPadding: function bodyPadding(event) {
if ($('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth) {
var position = event.data.settings.drupalOffCanvasPosition;
if (position === 'side' && $('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth) {
return;
}
Drupal.offCanvas.resetPadding();
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) {
if (position === 'side' && width !== mainCanvasPadding) {
$mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge(), width + 'px');
$container.attr('data-offset-' + Drupal.offCanvas.getEdge(), width);
displace();
}
var height = $container.outerHeight();
if (position === 'top') {
$mainCanvasWrapper.css('padding-top', height + 'px');
$container.attr('data-offset-top', height);
displace();
}
},
getContainer: function getContainer($element) {
return $element.dialog('widget');
},
getEdge: function getEdge() {
return document.documentElement.dir === 'rtl' ? 'left' : 'right';
},
resetPadding: function resetPadding() {
Drupal.offCanvas.$mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge(), 0);
Drupal.offCanvas.$mainCanvasWrapper.css('padding-top', 0);
displace();
}
};
+1 -1
View File
@@ -7,5 +7,5 @@
*/
.dialog-off-canvas-main-canvas {
transition: all 0.7s ease;
transition: padding-right 0.7s ease, padding-left 0.7s ease, padding-top 0.3s ease;
}
+3 -1
View File
@@ -6,7 +6,6 @@
/* Style the dialog-off-canvas container. */
.ui-dialog.ui-dialog-off-canvas {
background: #444;
border: 0 solid transparent;
border-radius: 0;
box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.3333);
padding: 0;
@@ -14,6 +13,9 @@
/* Layer the dialog just under the toolbar. */
z-index: 501;
}
.ui-widget.ui-dialog.ui-dialog-off-canvas {
border: 1px solid transparent;
}
/* Style the off-canvas dialog header. */
.ui-dialog.ui-dialog-off-canvas .ui-dialog-titlebar {
+114 -109
View File
@@ -24,7 +24,7 @@
* @event drupalViewportOffsetChange
*/
(function ($, Drupal, debounce) {
(function($, Drupal, debounce) {
/**
* @name Drupal.displace.offsets
*
@@ -37,110 +37,6 @@
left: 0,
};
/**
* Registers a resize handler on the window.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.drupalDisplace = {
attach() {
// Mark this behavior as processed on the first pass.
if (this.displaceProcessed) {
return;
}
this.displaceProcessed = true;
$(window).on('resize.drupalDisplace', debounce(displace, 200));
},
};
/**
* Informs listeners of the current offset dimensions.
*
* @function Drupal.displace
*
* @prop {Drupal~displaceOffset} offsets
*
* @param {bool} [broadcast]
* When true or undefined, causes the recalculated offsets values to be
* broadcast to listeners.
*
* @return {Drupal~displaceOffset}
* An object whose keys are the for sides an element -- top, right, bottom
* and left. The value of each key is the viewport displacement distance for
* that edge.
*
* @fires event:drupalViewportOffsetChange
*/
function displace(broadcast) {
offsets = calculateOffsets();
Drupal.displace.offsets = offsets;
if (typeof broadcast === 'undefined' || broadcast) {
$(document).trigger('drupalViewportOffsetChange', offsets);
}
return offsets;
}
/**
* Determines the viewport offsets.
*
* @return {Drupal~displaceOffset}
* An object whose keys are the for sides an element -- top, right, bottom
* and left. The value of each key is the viewport displacement distance for
* that edge.
*/
function calculateOffsets() {
return {
top: calculateOffset('top'),
right: calculateOffset('right'),
bottom: calculateOffset('bottom'),
left: calculateOffset('left'),
};
}
/**
* Gets a specific edge's offset.
*
* Any element with the attribute data-offset-{edge} e.g. data-offset-top will
* be considered in the viewport offset calculations. If the attribute has a
* numeric value, that value will be used. If no value is provided, one will
* be calculated using the element's dimensions and placement.
*
* @function Drupal.displace.calculateOffset
*
* @param {string} edge
* The name of the edge to calculate. Can be 'top', 'right',
* 'bottom' or 'left'.
*
* @return {number}
* The viewport displacement distance for the requested edge.
*/
function calculateOffset(edge) {
let edgeOffset = 0;
const displacingElements = document.querySelectorAll(`[data-offset-${edge}]`);
const n = displacingElements.length;
for (let i = 0; i < n; i++) {
const el = displacingElements[i];
// If the element is not visible, do consider its dimensions.
if (el.style.display === 'none') {
continue;
}
// If the offset data attribute contains a displacing value, use it.
let displacement = parseInt(el.getAttribute(`data-offset-${edge}`), 10);
// If the element's offset data attribute exits
// but is not a valid number then get the displacement
// dimensions directly from the element.
if (isNaN(displacement)) {
displacement = getRawOffset(el, edge);
}
// If the displacement value is larger than the current value for this
// edge, use the displacement value.
edgeOffset = Math.max(edgeOffset, displacement);
}
return edgeOffset;
}
/**
* Calculates displacement for element based on its dimensions and placement.
*
@@ -158,12 +54,15 @@
const $el = $(el);
const documentElement = document.documentElement;
let displacement = 0;
const horizontal = (edge === 'left' || edge === 'right');
const horizontal = edge === 'left' || edge === 'right';
// Get the offset of the element itself.
let placement = $el.offset()[horizontal ? 'left' : 'top'];
// Subtract scroll distance from placement to get the distance
// to the edge of the viewport.
placement -= window[`scroll${horizontal ? 'X' : 'Y'}`] || document.documentElement[`scroll${horizontal ? 'Left' : 'Top'}`] || 0;
placement -=
window[`scroll${horizontal ? 'X' : 'Y'}`] ||
document.documentElement[`scroll${horizontal ? 'Left' : 'Top'}`] ||
0;
// Find the displacement value according to the edge.
switch (edge) {
// Left and top elements displace as a sum of their own offset value
@@ -194,6 +93,113 @@
return displacement;
}
/**
* Gets a specific edge's offset.
*
* Any element with the attribute data-offset-{edge} e.g. data-offset-top will
* be considered in the viewport offset calculations. If the attribute has a
* numeric value, that value will be used. If no value is provided, one will
* be calculated using the element's dimensions and placement.
*
* @function Drupal.displace.calculateOffset
*
* @param {string} edge
* The name of the edge to calculate. Can be 'top', 'right',
* 'bottom' or 'left'.
*
* @return {number}
* The viewport displacement distance for the requested edge.
*/
function calculateOffset(edge) {
let edgeOffset = 0;
const displacingElements = document.querySelectorAll(
`[data-offset-${edge}]`,
);
const n = displacingElements.length;
for (let i = 0; i < n; i++) {
const el = displacingElements[i];
// If the element is not visible, do consider its dimensions.
if (el.style.display === 'none') {
continue;
}
// If the offset data attribute contains a displacing value, use it.
let displacement = parseInt(el.getAttribute(`data-offset-${edge}`), 10);
// If the element's offset data attribute exits
// but is not a valid number then get the displacement
// dimensions directly from the element.
// eslint-disable-next-line no-restricted-globals
if (isNaN(displacement)) {
displacement = getRawOffset(el, edge);
}
// If the displacement value is larger than the current value for this
// edge, use the displacement value.
edgeOffset = Math.max(edgeOffset, displacement);
}
return edgeOffset;
}
/**
* Determines the viewport offsets.
*
* @return {Drupal~displaceOffset}
* An object whose keys are the for sides an element -- top, right, bottom
* and left. The value of each key is the viewport displacement distance for
* that edge.
*/
function calculateOffsets() {
return {
top: calculateOffset('top'),
right: calculateOffset('right'),
bottom: calculateOffset('bottom'),
left: calculateOffset('left'),
};
}
/**
* Informs listeners of the current offset dimensions.
*
* @function Drupal.displace
*
* @prop {Drupal~displaceOffset} offsets
*
* @param {bool} [broadcast]
* When true or undefined, causes the recalculated offsets values to be
* broadcast to listeners.
*
* @return {Drupal~displaceOffset}
* An object whose keys are the for sides an element -- top, right, bottom
* and left. The value of each key is the viewport displacement distance for
* that edge.
*
* @fires event:drupalViewportOffsetChange
*/
function displace(broadcast) {
offsets = calculateOffsets();
Drupal.displace.offsets = offsets;
if (typeof broadcast === 'undefined' || broadcast) {
$(document).trigger('drupalViewportOffsetChange', offsets);
}
return offsets;
}
/**
* Registers a resize handler on the window.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.drupalDisplace = {
attach() {
// Mark this behavior as processed on the first pass.
if (this.displaceProcessed) {
return;
}
this.displaceProcessed = true;
$(window).on('resize.drupalDisplace', debounce(displace, 200));
},
};
/**
* Assign the displace function to a property of the Drupal global object.
*
@@ -201,7 +207,6 @@
*/
Drupal.displace = displace;
$.extend(Drupal.displace, {
/**
* Expose offsets to other scripts to avoid having to recalculate offsets.
*
@@ -216,4 +221,4 @@
*/
calculateOffset,
});
}(jQuery, Drupal, Drupal.debounce));
})(jQuery, Drupal, Drupal.debounce);
+52 -52
View File
@@ -13,58 +13,6 @@
left: 0
};
Drupal.behaviors.drupalDisplace = {
attach: function attach() {
if (this.displaceProcessed) {
return;
}
this.displaceProcessed = true;
$(window).on('resize.drupalDisplace', debounce(displace, 200));
}
};
function displace(broadcast) {
offsets = calculateOffsets();
Drupal.displace.offsets = offsets;
if (typeof broadcast === 'undefined' || broadcast) {
$(document).trigger('drupalViewportOffsetChange', offsets);
}
return offsets;
}
function calculateOffsets() {
return {
top: calculateOffset('top'),
right: calculateOffset('right'),
bottom: calculateOffset('bottom'),
left: calculateOffset('left')
};
}
function calculateOffset(edge) {
var edgeOffset = 0;
var displacingElements = document.querySelectorAll('[data-offset-' + edge + ']');
var n = displacingElements.length;
for (var i = 0; i < n; i++) {
var el = displacingElements[i];
if (el.style.display === 'none') {
continue;
}
var displacement = parseInt(el.getAttribute('data-offset-' + edge), 10);
if (isNaN(displacement)) {
displacement = getRawOffset(el, edge);
}
edgeOffset = Math.max(edgeOffset, displacement);
}
return edgeOffset;
}
function getRawOffset(el, edge) {
var $el = $(el);
var documentElement = document.documentElement;
@@ -98,6 +46,58 @@
return displacement;
}
function calculateOffset(edge) {
var edgeOffset = 0;
var displacingElements = document.querySelectorAll('[data-offset-' + edge + ']');
var n = displacingElements.length;
for (var i = 0; i < n; i++) {
var el = displacingElements[i];
if (el.style.display === 'none') {
continue;
}
var displacement = parseInt(el.getAttribute('data-offset-' + edge), 10);
if (isNaN(displacement)) {
displacement = getRawOffset(el, edge);
}
edgeOffset = Math.max(edgeOffset, displacement);
}
return edgeOffset;
}
function calculateOffsets() {
return {
top: calculateOffset('top'),
right: calculateOffset('right'),
bottom: calculateOffset('bottom'),
left: calculateOffset('left')
};
}
function displace(broadcast) {
offsets = calculateOffsets();
Drupal.displace.offsets = offsets;
if (typeof broadcast === 'undefined' || broadcast) {
$(document).trigger('drupalViewportOffsetChange', offsets);
}
return offsets;
}
Drupal.behaviors.drupalDisplace = {
attach: function attach() {
if (this.displaceProcessed) {
return;
}
this.displaceProcessed = true;
$(window).on('resize.drupalDisplace', debounce(displace, 200));
}
};
Drupal.displace = displace;
$.extend(Drupal.displace, {
offsets: offsets,
+170 -156
View File
@@ -3,46 +3,7 @@
* Dropbutton feature.
*/
(function ($, Drupal) {
/**
* Process elements with the .dropbutton class on page load.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches dropButton behaviors.
*/
Drupal.behaviors.dropButton = {
attach(context, settings) {
const $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton');
if ($dropbuttons.length) {
// Adds the delegated handler that will toggle dropdowns on click.
const $body = $('body').once('dropbutton-click');
if ($body.length) {
$body.on('click', '.dropbutton-toggle', dropbuttonClickHandler);
}
// Initialize all buttons.
const il = $dropbuttons.length;
for (let i = 0; i < il; i++) {
DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton));
}
}
},
};
/**
* Delegated callback for opening and closing dropbutton secondary actions.
*
* @function Drupal.DropButton~dropbuttonClickHandler
*
* @param {jQuery.Event} e
* The event triggered.
*/
function dropbuttonClickHandler(e) {
e.preventDefault();
$(e.target).closest('.dropbutton-wrapper').toggleClass('open');
}
(function($, Drupal) {
/**
* A DropButton presents an HTML list as a button with a primary action.
*
@@ -61,7 +22,10 @@
*/
function DropButton(dropbutton, settings) {
// Merge defaults with settings.
const options = $.extend({ title: Drupal.t('List additional actions') }, settings);
const options = $.extend(
{ title: Drupal.t('List additional actions') },
settings,
);
const $dropbutton = $(dropbutton);
/**
@@ -91,139 +55,189 @@
// Add toggle link.
$primary.after(Drupal.theme('dropbuttonToggle', options));
// Bind mouse events.
this.$dropbutton
.addClass('dropbutton-multiple')
.on({
this.$dropbutton.addClass('dropbutton-multiple').on({
/**
* Adds a timeout to close the dropdown on mouseleave.
*
* @ignore
*/
'mouseleave.dropbutton': $.proxy(this.hoverOut, this),
/**
* Adds a timeout to close the dropdown on mouseleave.
*
* @ignore
*/
'mouseleave.dropbutton': $.proxy(this.hoverOut, this),
/**
* Clears timeout when mouseout of the dropdown.
*
* @ignore
*/
'mouseenter.dropbutton': $.proxy(this.hoverIn, this),
/**
* Clears timeout when mouseout of the dropdown.
*
* @ignore
*/
'mouseenter.dropbutton': $.proxy(this.hoverIn, this),
/**
* Similar to mouseleave/mouseenter, but for keyboard navigation.
*
* @ignore
*/
'focusout.dropbutton': $.proxy(this.focusOut, this),
/**
* Similar to mouseleave/mouseenter, but for keyboard navigation.
*
* @ignore
*/
'focusout.dropbutton': $.proxy(this.focusOut, this),
/**
* @ignore
*/
'focusin.dropbutton': $.proxy(this.focusIn, this),
});
}
else {
/**
* @ignore
*/
'focusin.dropbutton': $.proxy(this.focusIn, this),
});
} else {
this.$dropbutton.addClass('dropbutton-single');
}
}
/**
* Delegated callback for opening and closing dropbutton secondary actions.
*
* @function Drupal.DropButton~dropbuttonClickHandler
*
* @param {jQuery.Event} e
* The event triggered.
*/
function dropbuttonClickHandler(e) {
e.preventDefault();
$(e.target)
.closest('.dropbutton-wrapper')
.toggleClass('open');
}
/**
* Process elements with the .dropbutton class on page load.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches dropButton behaviors.
*/
Drupal.behaviors.dropButton = {
attach(context, settings) {
const $dropbuttons = $(context)
.find('.dropbutton-wrapper')
.once('dropbutton');
if ($dropbuttons.length) {
// Adds the delegated handler that will toggle dropdowns on click.
const $body = $('body').once('dropbutton-click');
if ($body.length) {
$body.on('click', '.dropbutton-toggle', dropbuttonClickHandler);
}
// Initialize all buttons.
const il = $dropbuttons.length;
for (let i = 0; i < il; i++) {
DropButton.dropbuttons.push(
new DropButton($dropbuttons[i], settings.dropbutton),
);
}
}
},
};
/**
* Extend the DropButton constructor.
*/
$.extend(DropButton, /** @lends Drupal.DropButton */{
/**
* Store all processed DropButtons.
*
* @type {Array.<Drupal.DropButton>}
*/
dropbuttons: [],
});
$.extend(
DropButton,
/** @lends Drupal.DropButton */ {
/**
* Store all processed DropButtons.
*
* @type {Array.<Drupal.DropButton>}
*/
dropbuttons: [],
},
);
/**
* Extend the DropButton prototype.
*/
$.extend(DropButton.prototype, /** @lends Drupal.DropButton# */{
$.extend(
DropButton.prototype,
/** @lends Drupal.DropButton# */ {
/**
* Toggle the dropbutton open and closed.
*
* @param {bool} [show]
* Force the dropbutton to open by passing true or to close by
* passing false.
*/
toggle(show) {
const isBool = typeof show === 'boolean';
show = isBool ? show : !this.$dropbutton.hasClass('open');
this.$dropbutton.toggleClass('open', show);
},
/**
* Toggle the dropbutton open and closed.
*
* @param {bool} [show]
* Force the dropbutton to open by passing true or to close by
* passing false.
*/
toggle(show) {
const isBool = typeof show === 'boolean';
show = isBool ? show : !this.$dropbutton.hasClass('open');
this.$dropbutton.toggleClass('open', show);
/**
* @method
*/
hoverIn() {
// Clear any previous timer we were using.
if (this.timerID) {
window.clearTimeout(this.timerID);
}
},
/**
* @method
*/
hoverOut() {
// Wait half a second before closing.
this.timerID = window.setTimeout($.proxy(this, 'close'), 500);
},
/**
* @method
*/
open() {
this.toggle(true);
},
/**
* @method
*/
close() {
this.toggle(false);
},
/**
* @param {jQuery.Event} e
* The event triggered.
*/
focusOut(e) {
this.hoverOut.call(this, e);
},
/**
* @param {jQuery.Event} e
* The event triggered.
*/
focusIn(e) {
this.hoverIn.call(this, e);
},
},
);
/**
* @method
*/
hoverIn() {
// Clear any previous timer we were using.
if (this.timerID) {
window.clearTimeout(this.timerID);
}
$.extend(
Drupal.theme,
/** @lends Drupal.theme */ {
/**
* A toggle is an interactive element often bound to a click handler.
*
* @param {object} options
* Options object.
* @param {string} [options.title]
* The button text.
*
* @return {string}
* A string representing a DOM fragment.
*/
dropbuttonToggle(options) {
return `<li class="dropbutton-toggle"><button type="button"><span class="dropbutton-arrow"><span class="visually-hidden">${
options.title
}</span></span></button></li>`;
},
},
/**
* @method
*/
hoverOut() {
// Wait half a second before closing.
this.timerID = window.setTimeout($.proxy(this, 'close'), 500);
},
/**
* @method
*/
open() {
this.toggle(true);
},
/**
* @method
*/
close() {
this.toggle(false);
},
/**
* @param {jQuery.Event} e
* The event triggered.
*/
focusOut(e) {
this.hoverOut.call(this, e);
},
/**
* @param {jQuery.Event} e
* The event triggered.
*/
focusIn(e) {
this.hoverIn.call(this, e);
},
});
$.extend(Drupal.theme, /** @lends Drupal.theme */{
/**
* A toggle is an interactive element often bound to a click handler.
*
* @param {object} options
* Options object.
* @param {string} [options.title]
* The HTML anchor title attribute and text for the inner span element.
*
* @return {string}
* A string representing a DOM fragment.
*/
dropbuttonToggle(options) {
return `<li class="dropbutton-toggle"><button type="button"><span class="dropbutton-arrow"><span class="visually-hidden">${options.title}</span></span></button></li>`;
},
});
);
// Expose constructor in the public space.
Drupal.DropButton = DropButton;
}(jQuery, Drupal));
})(jQuery, Drupal);
+22 -22
View File
@@ -6,28 +6,6 @@
**/
(function ($, Drupal) {
Drupal.behaviors.dropButton = {
attach: function attach(context, settings) {
var $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton');
if ($dropbuttons.length) {
var $body = $('body').once('dropbutton-click');
if ($body.length) {
$body.on('click', '.dropbutton-toggle', dropbuttonClickHandler);
}
var il = $dropbuttons.length;
for (var i = 0; i < il; i++) {
DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton));
}
}
}
};
function dropbuttonClickHandler(e) {
e.preventDefault();
$(e.target).closest('.dropbutton-wrapper').toggleClass('open');
}
function DropButton(dropbutton, settings) {
var options = $.extend({ title: Drupal.t('List additional actions') }, settings);
var $dropbutton = $(dropbutton);
@@ -60,6 +38,28 @@
}
}
function dropbuttonClickHandler(e) {
e.preventDefault();
$(e.target).closest('.dropbutton-wrapper').toggleClass('open');
}
Drupal.behaviors.dropButton = {
attach: function attach(context, settings) {
var $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton');
if ($dropbuttons.length) {
var $body = $('body').once('dropbutton-click');
if ($body.length) {
$body.on('click', '.dropbutton-toggle', dropbuttonClickHandler);
}
var il = $dropbuttons.length;
for (var i = 0; i < il; i++) {
DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton));
}
}
}
};
$.extend(DropButton, {
dropbuttons: []
});
+50 -38
View File
@@ -42,7 +42,7 @@ window.Drupal = { behaviors: {}, locale: {} };
// JavaScript should be made compatible with libraries other than jQuery by
// wrapping it in an anonymous closure.
(function (Drupal, drupalSettings, drupalTranslations) {
(function(Drupal, drupalSettings, drupalTranslations) {
/**
* Helper to rethrow errors asynchronously.
*
@@ -52,7 +52,7 @@ window.Drupal = { behaviors: {}, locale: {} };
* @param {Error|string} error
* The error to be thrown.
*/
Drupal.throwError = function (error) {
Drupal.throwError = function(error) {
setTimeout(() => {
throw error;
}, 0);
@@ -147,18 +147,17 @@ window.Drupal = { behaviors: {}, locale: {} };
*
* @throws {Drupal~DrupalBehaviorError}
*/
Drupal.attachBehaviors = function (context, settings) {
Drupal.attachBehaviors = function(context, settings) {
context = context || document;
settings = settings || drupalSettings;
const behaviors = Drupal.behaviors;
// Execute all of them.
Object.keys(behaviors || {}).forEach((i) => {
Object.keys(behaviors || {}).forEach(i => {
if (typeof behaviors[i].attach === 'function') {
// Don't stop the execution of behaviors in case of an error.
try {
behaviors[i].attach(context, settings);
}
catch (e) {
} catch (e) {
Drupal.throwError(e);
}
}
@@ -206,19 +205,18 @@ window.Drupal = { behaviors: {}, locale: {} };
* @see Drupal~behaviorDetach
* @see Drupal.attachBehaviors
*/
Drupal.detachBehaviors = function (context, settings, trigger) {
Drupal.detachBehaviors = function(context, settings, trigger) {
context = context || document;
settings = settings || drupalSettings;
trigger = trigger || 'unload';
const behaviors = Drupal.behaviors;
// Execute all of them.
Object.keys(behaviors || {}).forEach((i) => {
Object.keys(behaviors || {}).forEach(i => {
if (typeof behaviors[i].detach === 'function') {
// Don't stop the execution of behaviors in case of an error.
try {
behaviors[i].detach(context, settings, trigger);
}
catch (e) {
} catch (e) {
Drupal.throwError(e);
}
}
@@ -236,8 +234,9 @@ window.Drupal = { behaviors: {}, locale: {} };
*
* @ingroup sanitization
*/
Drupal.checkPlain = function (str) {
str = str.toString()
Drupal.checkPlain = function(str) {
str = str
.toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
@@ -266,11 +265,11 @@ window.Drupal = { behaviors: {}, locale: {} };
*
* @see Drupal.t
*/
Drupal.formatString = function (str, args) {
Drupal.formatString = function(str, args) {
// Keep args intact.
const processedArgs = {};
// Transform arguments before inserting them.
Object.keys(args || {}).forEach((key) => {
Object.keys(args || {}).forEach(key => {
switch (key.charAt(0)) {
// Escaped only.
case '@':
@@ -308,7 +307,7 @@ window.Drupal = { behaviors: {}, locale: {} };
* @return {string}
* The replaced string.
*/
Drupal.stringReplace = function (str, args, keys) {
Drupal.stringReplace = function(str, args, keys) {
if (str.length === 0) {
return str;
}
@@ -359,12 +358,17 @@ window.Drupal = { behaviors: {}, locale: {} };
* The formatted string.
* The translated string.
*/
Drupal.t = function (str, args, options) {
Drupal.t = function(str, args, options) {
options = options || {};
options.context = options.context || '';
// Fetch the localized version of the string.
if (typeof drupalTranslations !== 'undefined' && drupalTranslations.strings && drupalTranslations.strings[options.context] && drupalTranslations.strings[options.context][str]) {
if (
typeof drupalTranslations !== 'undefined' &&
drupalTranslations.strings &&
drupalTranslations.strings[options.context] &&
drupalTranslations.strings[options.context][str]
) {
str = drupalTranslations.strings[options.context][str];
}
@@ -383,7 +387,7 @@ window.Drupal = { behaviors: {}, locale: {} };
* @return {string}
* The full URL.
*/
Drupal.url = function (path) {
Drupal.url = function(path) {
return drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix + path;
};
@@ -400,15 +404,14 @@ window.Drupal = { behaviors: {}, locale: {} };
* @see https://grack.com/blog/2009/11/17/absolutizing-url-in-javascript
* @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L53
*/
Drupal.url.toAbsolute = function (url) {
Drupal.url.toAbsolute = function(url) {
const urlParsingNode = document.createElement('a');
// Decode the URL first; this is required by IE <= 6. Decoding non-UTF-8
// strings may throw an exception.
try {
url = decodeURIComponent(url);
}
catch (e) {
} catch (e) {
// Empty.
}
@@ -430,30 +433,30 @@ window.Drupal = { behaviors: {}, locale: {} };
*
* @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L58
*/
Drupal.url.isLocal = function (url) {
Drupal.url.isLocal = function(url) {
// Always use browser-derived absolute URLs in the comparison, to avoid
// attempts to break out of the base path using directory traversal.
let absoluteUrl = Drupal.url.toAbsolute(url);
let protocol = location.protocol;
let { protocol } = window.location;
// Consider URLs that match this site's base URL but use HTTPS instead of HTTP
// as local as well.
if (protocol === 'http:' && absoluteUrl.indexOf('https:') === 0) {
protocol = 'https:';
}
let baseUrl = `${protocol}//${location.host}${drupalSettings.path.baseUrl.slice(0, -1)}`;
let baseUrl = `${protocol}//${
window.location.host
}${drupalSettings.path.baseUrl.slice(0, -1)}`;
// Decoding non-UTF-8 strings may throw an exception.
try {
absoluteUrl = decodeURIComponent(absoluteUrl);
}
catch (e) {
} catch (e) {
// Empty.
}
try {
baseUrl = decodeURIComponent(baseUrl);
}
catch (e) {
} catch (e) {
// Empty.
}
@@ -495,19 +498,28 @@ window.Drupal = { behaviors: {}, locale: {} };
* @return {string}
* A translated string.
*/
Drupal.formatPlural = function (count, singular, plural, args, options) {
Drupal.formatPlural = function(count, singular, plural, args, options) {
args = args || {};
args['@count'] = count;
const pluralDelimiter = drupalSettings.pluralDelimiter;
const translations = Drupal.t(singular + pluralDelimiter + plural, args, options).split(pluralDelimiter);
const translations = Drupal.t(
singular + pluralDelimiter + plural,
args,
options,
).split(pluralDelimiter);
let index = 0;
// Determine the index of the plural form.
if (typeof drupalTranslations !== 'undefined' && drupalTranslations.pluralFormula) {
index = count in drupalTranslations.pluralFormula ? drupalTranslations.pluralFormula[count] : drupalTranslations.pluralFormula.default;
}
else if (args['@count'] !== 1) {
if (
typeof drupalTranslations !== 'undefined' &&
drupalTranslations.pluralFormula
) {
index =
count in drupalTranslations.pluralFormula
? drupalTranslations.pluralFormula[count]
: drupalTranslations.pluralFormula.default;
} else if (args['@count'] !== 1) {
index = 1;
}
@@ -525,7 +537,7 @@ window.Drupal = { behaviors: {}, locale: {} };
* @return {string}
* The encoded path.
*/
Drupal.encodePath = function (item) {
Drupal.encodePath = function(item) {
return window.encodeURIComponent(item).replace(/%2F/g, '/');
};
@@ -553,7 +565,7 @@ window.Drupal = { behaviors: {}, locale: {} };
* Any data the theme function returns. This could be a plain HTML string,
* but also a complex object.
*/
Drupal.theme = function (func, ...args) {
Drupal.theme = function(func, ...args) {
if (func in Drupal.theme) {
return Drupal.theme[func](...args);
}
@@ -568,7 +580,7 @@ window.Drupal = { behaviors: {}, locale: {} };
* @return {string}
* The formatted text (html).
*/
Drupal.theme.placeholder = function (str) {
Drupal.theme.placeholder = function(str) {
return `<em class="placeholder">${Drupal.checkPlain(str)}</em>`;
};
}(Drupal, window.drupalSettings, window.drupalTranslations));
})(Drupal, window.drupalSettings, window.drupalTranslations);
+2 -2
View File
@@ -9,9 +9,9 @@ document.documentElement.className += ' js';
// JavaScript should be made compatible with libraries other than jQuery by
// wrapping it in an anonymous closure.
(function (domready, Drupal, drupalSettings) {
(function(domready, Drupal, drupalSettings) {
// Attach all behaviors.
domready(() => {
Drupal.attachBehaviors(document, drupalSettings);
});
}(domready, Drupal, window.drupalSettings));
})(domready, Drupal, window.drupalSettings);
+2 -2
View File
@@ -135,12 +135,12 @@ window.Drupal = { behaviors: {}, locale: {} };
Drupal.url.isLocal = function (url) {
var absoluteUrl = Drupal.url.toAbsolute(url);
var protocol = location.protocol;
var protocol = window.location.protocol;
if (protocol === 'http:' && absoluteUrl.indexOf('https:') === 0) {
protocol = 'https:';
}
var baseUrl = protocol + '//' + location.host + drupalSettings.path.baseUrl.slice(0, -1);
var baseUrl = protocol + '//' + window.location.host + drupalSettings.path.baseUrl.slice(0, -1);
try {
absoluteUrl = decodeURIComponent(absoluteUrl);
+5 -3
View File
@@ -3,9 +3,11 @@
* Parse inline JSON and initialize the drupalSettings global object.
*/
(function () {
(function() {
// Use direct child elements to harden against XSS exploits when CSP is on.
const settingsElement = document.querySelector('head > script[type="application/json"][data-drupal-selector="drupal-settings-json"], body > script[type="application/json"][data-drupal-selector="drupal-settings-json"]');
const settingsElement = document.querySelector(
'head > script[type="application/json"][data-drupal-selector="drupal-settings-json"], body > script[type="application/json"][data-drupal-selector="drupal-settings-json"]',
);
/**
* Variable generated by Drupal with all the configuration created from PHP.
@@ -19,4 +21,4 @@
if (settingsElement !== null) {
window.drupalSettings = JSON.parse(settingsElement.textContent);
}
}());
})();
+45 -27
View File
@@ -3,7 +3,7 @@
* Defines Javascript behaviors for the block_content module.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Sets summaries about revision and translation of entities.
*
@@ -18,36 +18,54 @@
Drupal.behaviors.entityContentDetailsSummaries = {
attach(context) {
const $context = $(context);
$context.find('.entity-content-form-revision-information').drupalSetSummary((context) => {
const $revisionContext = $(context);
const revisionCheckbox = $revisionContext.find('.js-form-item-revision input');
$context
.find('.entity-content-form-revision-information')
.drupalSetSummary(context => {
const $revisionContext = $(context);
const revisionCheckbox = $revisionContext.find(
'.js-form-item-revision input',
);
// Return 'New revision' if the 'Create new revision' checkbox is checked,
// or if the checkbox doesn't exist, but the revision log does. For users
// without the "Administer content" permission the checkbox won't appear,
// but the revision log will if the content type is set to auto-revision.
if (revisionCheckbox.is(':checked') || (!revisionCheckbox.length && $revisionContext.find('.js-form-item-revision-log textarea').length)) {
return Drupal.t('New revision');
}
// Return 'New revision' if the 'Create new revision' checkbox is checked,
// or if the checkbox doesn't exist, but the revision log does. For users
// without the "Administer content" permission the checkbox won't appear,
// but the revision log will if the content type is set to auto-revision.
if (
revisionCheckbox.is(':checked') ||
(!revisionCheckbox.length &&
$revisionContext.find('.js-form-item-revision-log textarea')
.length)
) {
return Drupal.t('New revision');
}
return Drupal.t('No revision');
});
return Drupal.t('No revision');
});
$context.find('details.entity-translation-options').drupalSetSummary((context) => {
const $translationContext = $(context);
let translate;
let $checkbox = $translationContext.find('.js-form-item-translation-translate input');
$context
.find('details.entity-translation-options')
.drupalSetSummary(context => {
const $translationContext = $(context);
let translate;
let $checkbox = $translationContext.find(
'.js-form-item-translation-translate input',
);
if ($checkbox.length) {
translate = $checkbox.is(':checked') ? Drupal.t('Needs to be updated') : Drupal.t('Does not need to be updated');
}
else {
$checkbox = $translationContext.find('.js-form-item-translation-retranslate input');
translate = $checkbox.is(':checked') ? Drupal.t('Flag other translations as outdated') : Drupal.t('Do not flag other translations as outdated');
}
if ($checkbox.length) {
translate = $checkbox.is(':checked')
? Drupal.t('Needs to be updated')
: Drupal.t('Does not need to be updated');
} else {
$checkbox = $translationContext.find(
'.js-form-item-translation-retranslate input',
);
translate = $checkbox.is(':checked')
? Drupal.t('Flag other translations as outdated')
: Drupal.t('Do not flag other translations as outdated');
}
return translate;
});
return translate;
});
},
};
}(jQuery, Drupal));
})(jQuery, Drupal);
+66 -41
View File
@@ -22,16 +22,16 @@
* @event formFragmentLinkClickOrHashChange
*/
(function ($, Drupal, debounce) {
(function($, Drupal, debounce) {
/**
* Retrieves the summary for the first element.
*
* @return {string}
* The text of the summary.
*/
$.fn.drupalGetSummary = function () {
$.fn.drupalGetSummary = function() {
const callback = this.data('summaryCallback');
return (this[0] && callback) ? $.trim(callback(this[0])) : '';
return this[0] && callback ? $.trim(callback(this[0])) : '';
};
/**
@@ -48,29 +48,30 @@
*
* @listens event:formUpdated
*/
$.fn.drupalSetSummary = function (callback) {
$.fn.drupalSetSummary = function(callback) {
const self = this;
// To facilitate things, the callback should always be a function. If it's
// not, we wrap it into an anonymous function which just returns the value.
if (typeof callback !== 'function') {
const val = callback;
callback = function () {
callback = function() {
return val;
};
}
return this
.data('summaryCallback', callback)
// To prevent duplicate events, the handlers are first removed and then
// (re-)added.
.off('formUpdated.summary')
.on('formUpdated.summary', () => {
self.trigger('summaryUpdated');
})
// The actual summaryUpdated handler doesn't fire when the callback is
// changed, so we have to do this manually.
.trigger('summaryUpdated');
return (
this.data('summaryCallback', callback)
// To prevent duplicate events, the handlers are first removed and then
// (re-)added.
.off('formUpdated.summary')
.on('formUpdated.summary', () => {
self.trigger('summaryUpdated');
})
// The actual summaryUpdated handler doesn't fire when the callback is
// changed, so we have to do this manually.
.trigger('summaryUpdated')
);
};
/**
@@ -121,13 +122,13 @@
const previousValues = $form.attr('data-drupal-form-submit-last');
if (previousValues === formValues) {
e.preventDefault();
}
else {
} else {
$form.attr('data-drupal-form-submit-last', formValues);
}
}
$('body').once('form-single-submit')
$('body')
.once('form-single-submit')
.on('submit.singleSubmit', 'form:not([method~="GET"])', onFormSubmit);
},
};
@@ -154,10 +155,13 @@
* Array of IDs for form fields.
*/
function fieldsList(form) {
const $fieldList = $(form).find('[name]').map((index, element) =>
// We use id to avoid name duplicates on radio fields and filter out
// elements with a name but no id.
element.getAttribute('id'));
const $fieldList = $(form)
.find('[name]')
.map(
// We use id to avoid name duplicates on radio fields and filter out
// elements with a name but no id.
(index, element) => element.getAttribute('id'),
);
// Return a true array.
return $.makeArray($fieldList);
}
@@ -178,16 +182,18 @@
attach(context) {
const $context = $(context);
const contextIsForm = $context.is('form');
const $forms = (contextIsForm ? $context : $context.find('form')).once('form-updated');
const $forms = (contextIsForm ? $context : $context.find('form')).once(
'form-updated',
);
let formFields;
if ($forms.length) {
// Initialize form behaviors, use $.makeArray to be able to use native
// forEach array method and have the callback parameters in the right
// order.
$.makeArray($forms).forEach((form) => {
$.makeArray($forms).forEach(form => {
const events = 'change.formUpdated input.formUpdated ';
const eventHandler = debounce((event) => {
const eventHandler = debounce(event => {
triggerFormUpdated(event.target);
}, 300);
formFields = fieldsList(form).join(',');
@@ -212,9 +218,12 @@
const $context = $(context);
const contextIsForm = $context.is('form');
if (trigger === 'unload') {
const $forms = (contextIsForm ? $context : $context.find('form')).removeOnce('form-updated');
const $forms = (contextIsForm
? $context
: $context.find('form')
).removeOnce('form-updated');
if ($forms.length) {
$.makeArray($forms).forEach((form) => {
$.makeArray($forms).forEach(form => {
form.removeAttribute('data-drupal-form-fields');
$(form).off('.formUpdated');
});
@@ -234,19 +243,23 @@
Drupal.behaviors.fillUserInfoFromBrowser = {
attach(context, settings) {
const userInfo = ['name', 'mail', 'homepage'];
const $forms = $('[data-user-info-from-browser]').once('user-info-from-browser');
const $forms = $('[data-user-info-from-browser]').once(
'user-info-from-browser',
);
if ($forms.length) {
userInfo.forEach((info) => {
userInfo.forEach(info => {
const $element = $forms.find(`[name=${info}]`);
const browserData = localStorage.getItem(`Drupal.visitor.${info}`);
const emptyOrDefault = ($element.val() === '' || ($element.attr('data-drupal-default-value') === $element.val()));
const emptyOrDefault =
$element.val() === '' ||
$element.attr('data-drupal-default-value') === $element.val();
if ($element.length && emptyOrDefault && browserData) {
$element.val(browserData);
}
});
}
$forms.on('submit', () => {
userInfo.forEach((info) => {
userInfo.forEach(info => {
const $element = $forms.find(`[name=${info}]`);
if ($element.length) {
localStorage.setItem(`Drupal.visitor.${info}`, $element.val());
@@ -264,13 +277,14 @@
*
* @fires event:formFragmentLinkClickOrHashChange
*/
const handleFragmentLinkClickOrHashChange = (e) => {
const handleFragmentLinkClickOrHashChange = e => {
let url;
if (e.type === 'click') {
url = e.currentTarget.location ? e.currentTarget.location : e.currentTarget;
}
else {
url = location;
url = e.currentTarget.location
? e.currentTarget.location
: e.currentTarget;
} else {
url = window.location;
}
const hash = url.hash.substr(1);
if (hash) {
@@ -285,10 +299,17 @@
}
};
const debouncedHandleFragmentLinkClickOrHashChange = debounce(handleFragmentLinkClickOrHashChange, 300, true);
const debouncedHandleFragmentLinkClickOrHashChange = debounce(
handleFragmentLinkClickOrHashChange,
300,
true,
);
// Binds a listener to handle URL fragment changes.
$(window).on('hashchange.form-fragment', debouncedHandleFragmentLinkClickOrHashChange);
$(window).on(
'hashchange.form-fragment',
debouncedHandleFragmentLinkClickOrHashChange,
);
/**
* Binds a listener to handle clicks on fragment links and absolute URL links
@@ -296,5 +317,9 @@
* because clicking such links doesn't trigger a hash change when the fragment
* is already in the URL.
*/
$(document).on('click.form-fragment', 'a[href*="#"]', debouncedHandleFragmentLinkClickOrHashChange);
}(jQuery, Drupal, Drupal.debounce));
$(document).on(
'click.form-fragment',
'a[href*="#"]',
debouncedHandleFragmentLinkClickOrHashChange,
);
})(jQuery, Drupal, Drupal.debounce);
+1 -1
View File
@@ -130,7 +130,7 @@
if (e.type === 'click') {
url = e.currentTarget.location ? e.currentTarget.location : e.currentTarget;
} else {
url = location;
url = window.location;
}
var hash = url.hash.substr(1);
if (hash) {
+44 -20
View File
@@ -3,7 +3,7 @@
* Machine name functionality.
*/
(function ($, Drupal, drupalSettings) {
(function($, Drupal, drupalSettings) {
/**
* Attach the machine-readable name form element behavior.
*
@@ -13,7 +13,6 @@
* Attaches machine-name behaviors.
*/
Drupal.behaviors.machineName = {
/**
* Attaches the behavior.
*
@@ -59,7 +58,10 @@
const baseValue = $(e.target).val();
const rx = new RegExp(options.replace_pattern, 'g');
const expected = baseValue.toLowerCase().replace(rx, options.replace).substr(0, options.maxlength);
const expected = baseValue
.toLowerCase()
.replace(rx, options.replace)
.substr(0, options.maxlength);
// Abort the last pending request because the label has changed and it
// is no longer valid.
@@ -76,26 +78,35 @@
}
if (baseValue.toLowerCase() !== expected) {
timeout = setTimeout(() => {
xhr = self.transliterate(baseValue, options).done((machine) => {
xhr = self.transliterate(baseValue, options).done(machine => {
self.showMachineName(machine.substr(0, options.maxlength), data);
});
}, 300);
}
else {
} else {
self.showMachineName(expected, data);
}
}
Object.keys(settings.machineName).forEach((sourceId) => {
Object.keys(settings.machineName).forEach(sourceId => {
let machine = '';
const options = settings.machineName[sourceId];
const $source = $context.find(sourceId).addClass('machine-name-source').once('machine-name');
const $target = $context.find(options.target).addClass('machine-name-target');
const $source = $context
.find(sourceId)
.addClass('machine-name-source')
.once('machine-name');
const $target = $context
.find(options.target)
.addClass('machine-name-target');
const $suffix = $context.find(options.suffix);
const $wrapper = $target.closest('.js-form-item');
// All elements have to exist.
if (!$source.length || !$target.length || !$suffix.length || !$wrapper.length) {
if (
!$source.length ||
!$target.length ||
!$suffix.length ||
!$wrapper.length
) {
return;
}
// Skip processing upon a form validation error on the machine name.
@@ -111,15 +122,20 @@
// based on the human-readable form element value.
if ($target.is(':disabled') || $target.val() !== '') {
machine = $target.val();
}
else if ($source.val() !== '') {
} else if ($source.val() !== '') {
machine = self.transliterate($source.val(), options);
}
// Append the machine name preview to the source field.
const $preview = $(`<span class="machine-name-value">${options.field_prefix}${Drupal.checkPlain(machine)}${options.field_suffix}</span>`);
const $preview = $(
`<span class="machine-name-value">${
options.field_prefix
}${Drupal.checkPlain(machine)}${options.field_suffix}</span>`,
);
$suffix.empty();
if (options.label) {
$suffix.append(`<span class="machine-name-label">${options.label}: </span>`);
$suffix.append(
`<span class="machine-name-label">${options.label}: </span>`,
);
}
$suffix.append($preview);
@@ -137,14 +153,19 @@
options,
};
// If it is editable, append an edit link.
const $link = $(`<span class="admin-link"><button type="button" class="link">${Drupal.t('Edit')}</button></span>`).on('click', eventData, clickEditHandler);
const $link = $(
`<span class="admin-link"><button type="button" class="link">${Drupal.t(
'Edit',
)}</button></span>`,
).on('click', eventData, clickEditHandler);
$suffix.append($link);
// Preview the machine name in realtime when the human-readable name
// changes, but only if there is no machine name yet; i.e., only upon
// initial creation, not when editing.
if ($target.val() === '') {
$source.on('formUpdated.machineName', eventData, machineNameHandler)
$source
.on('formUpdated.machineName', eventData, machineNameHandler)
// Initialize machine name preview.
.trigger('formUpdated.machineName');
}
@@ -161,11 +182,14 @@
if (machine !== '') {
if (machine !== settings.replace) {
data.$target.val(machine);
data.$preview.html(settings.field_prefix + Drupal.checkPlain(machine) + settings.field_suffix);
data.$preview.html(
settings.field_prefix +
Drupal.checkPlain(machine) +
settings.field_suffix,
);
}
data.$suffix.show();
}
else {
} else {
data.$suffix.hide();
data.$target.val(machine);
data.$preview.empty();
@@ -203,4 +227,4 @@
});
},
};
}(jQuery, Drupal, drupalSettings));
})(jQuery, Drupal, drupalSettings);
+13
View File
@@ -0,0 +1,13 @@
/**
* @file
* Fixes for core/assets/vendor/normalize-css/normalize.css since version 3.
*/
/**
* Fix problem with details/summary lines missing the drop arrows.
*/
@-moz-document url-prefix() {
summary {
display: list-item;
}
}
+122 -107
View File
@@ -3,7 +3,7 @@
* Progress bar.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Theme function for the progress bar.
*
@@ -13,13 +13,15 @@
* @return {string}
* The HTML for the progress bar.
*/
Drupal.theme.progressBar = function (id) {
return `<div id="${id}" class="progress" aria-live="polite">` +
Drupal.theme.progressBar = function(id) {
return (
`<div id="${id}" class="progress" aria-live="polite">` +
'<div class="progress__label">&nbsp;</div>' +
'<div class="progress__track"><div class="progress__bar"></div></div>' +
'<div class="progress__percentage"></div>' +
'<div class="progress__description">&nbsp;</div>' +
'</div>';
'</div>'
);
};
/**
@@ -44,7 +46,7 @@
* @param {function} errorCallback
* Callback to call on error.
*/
Drupal.ProgressBar = function (id, updateCallback, method, errorCallback) {
Drupal.ProgressBar = function(id, updateCallback, method, errorCallback) {
this.id = id;
this.method = method || 'GET';
this.updateCallback = updateCallback;
@@ -57,111 +59,124 @@
this.element = $(Drupal.theme('progressBar', id));
};
$.extend(Drupal.ProgressBar.prototype, /** @lends Drupal.ProgressBar# */{
$.extend(
Drupal.ProgressBar.prototype,
/** @lends Drupal.ProgressBar# */ {
/**
* Set the percentage and status message for the progressbar.
*
* @param {number} percentage
* The progress percentage.
* @param {string} message
* The message to show the user.
* @param {string} label
* The text for the progressbar label.
*/
setProgress(percentage, message, label) {
if (percentage >= 0 && percentage <= 100) {
$(this.element)
.find('div.progress__bar')
.css('width', `${percentage}%`);
$(this.element)
.find('div.progress__percentage')
.html(`${percentage}%`);
}
$('div.progress__description', this.element).html(message);
$('div.progress__label', this.element).html(label);
if (this.updateCallback) {
this.updateCallback(percentage, message, this);
}
},
/**
* Set the percentage and status message for the progressbar.
*
* @param {number} percentage
* The progress percentage.
* @param {string} message
* The message to show the user.
* @param {string} label
* The text for the progressbar label.
*/
setProgress(percentage, message, label) {
if (percentage >= 0 && percentage <= 100) {
$(this.element).find('div.progress__bar').css('width', `${percentage}%`);
$(this.element).find('div.progress__percentage').html(`${percentage}%`);
}
$('div.progress__description', this.element).html(message);
$('div.progress__label', this.element).html(label);
if (this.updateCallback) {
this.updateCallback(percentage, message, this);
}
},
/**
* Start monitoring progress via Ajax.
*
* @param {string} uri
* The URI to use for monitoring.
* @param {number} delay
* The delay for calling the monitoring URI.
*/
startMonitoring(uri, delay) {
this.delay = delay;
this.uri = uri;
this.sendPing();
},
/**
* Start monitoring progress via Ajax.
*
* @param {string} uri
* The URI to use for monitoring.
* @param {number} delay
* The delay for calling the monitoring URI.
*/
startMonitoring(uri, delay) {
this.delay = delay;
this.uri = uri;
this.sendPing();
},
/**
* Stop monitoring progress via Ajax.
*/
stopMonitoring() {
clearTimeout(this.timer);
// This allows monitoring to be stopped from within the callback.
this.uri = null;
},
/**
* Request progress data from server.
*/
sendPing() {
if (this.timer) {
/**
* Stop monitoring progress via Ajax.
*/
stopMonitoring() {
clearTimeout(this.timer);
}
if (this.uri) {
const pb = this;
// When doing a post request, you need non-null data. Otherwise a
// HTTP 411 or HTTP 406 (with Apache mod_security) error may result.
let uri = this.uri;
if (uri.indexOf('?') === -1) {
uri += '?';
}
else {
uri += '&';
}
uri += '_format=json';
$.ajax({
type: this.method,
url: uri,
data: '',
dataType: 'json',
success(progress) {
// Display errors.
if (progress.status === 0) {
pb.displayError(progress.data);
return;
}
// Update display.
pb.setProgress(progress.percentage, progress.message, progress.label);
// Schedule next timer.
pb.timer = setTimeout(() => {
pb.sendPing();
}, pb.delay);
},
error(xmlhttp) {
const e = new Drupal.AjaxError(xmlhttp, pb.uri);
pb.displayError(`<pre>${e.message}</pre>`);
},
});
}
},
// This allows monitoring to be stopped from within the callback.
this.uri = null;
},
/**
* Display errors on the page.
*
* @param {string} string
* The error message to show the user.
*/
displayError(string) {
const error = $('<div class="messages messages--error"></div>').html(string);
$(this.element).before(error).hide();
/**
* Request progress data from server.
*/
sendPing() {
if (this.timer) {
clearTimeout(this.timer);
}
if (this.uri) {
const pb = this;
// When doing a post request, you need non-null data. Otherwise a
// HTTP 411 or HTTP 406 (with Apache mod_security) error may result.
let uri = this.uri;
if (uri.indexOf('?') === -1) {
uri += '?';
} else {
uri += '&';
}
uri += '_format=json';
$.ajax({
type: this.method,
url: uri,
data: '',
dataType: 'json',
success(progress) {
// Display errors.
if (progress.status === 0) {
pb.displayError(progress.data);
return;
}
// Update display.
pb.setProgress(
progress.percentage,
progress.message,
progress.label,
);
// Schedule next timer.
pb.timer = setTimeout(() => {
pb.sendPing();
}, pb.delay);
},
error(xmlhttp) {
const e = new Drupal.AjaxError(xmlhttp, pb.uri);
pb.displayError(`<pre>${e.message}</pre>`);
},
});
}
},
if (this.errorCallback) {
this.errorCallback(this);
}
/**
* Display errors on the page.
*
* @param {string} string
* The error message to show the user.
*/
displayError(string) {
const error = $('<div class="messages messages--error"></div>').html(
string,
);
$(this.element)
.before(error)
.hide();
if (this.errorCallback) {
this.errorCallback(this);
}
},
},
});
}(jQuery, Drupal));
);
})(jQuery, Drupal);
+152 -159
View File
@@ -3,7 +3,7 @@
* Drupal's states library.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* The base States namespace.
*
@@ -13,7 +13,6 @@
* @namespace Drupal.states
*/
const states = {
/**
* An array of functions that should be postponed.
*/
@@ -22,6 +21,44 @@
Drupal.states = states;
/**
* Inverts a (if it's not undefined) when invertState is true.
*
* @function Drupal.states~invert
*
* @param {*} a
* The value to maybe invert.
* @param {bool} invertState
* Whether to invert state or not.
*
* @return {bool}
* The result.
*/
function invert(a, invertState) {
return invertState && typeof a !== 'undefined' ? !a : a;
}
/**
* Compares two values while ignoring undefined values.
*
* @function Drupal.states~compare
*
* @param {*} a
* Value a.
* @param {*} b
* Value b.
*
* @return {bool}
* The comparison result.
*/
function compare(a, b) {
if (a === b) {
return typeof a === 'undefined' ? a : true;
}
return typeof a === 'undefined' || typeof b === 'undefined';
}
/**
* Attaches the states.
*
@@ -35,8 +72,10 @@
const $states = $(context).find('[data-drupal-states]');
const il = $states.length;
for (let i = 0; i < il; i++) {
const config = JSON.parse($states[i].getAttribute('data-drupal-states'));
Object.keys(config || {}).forEach((state) => {
const config = JSON.parse(
$states[i].getAttribute('data-drupal-states'),
);
Object.keys(config || {}).forEach(state => {
new states.Dependent({
element: $($states[i]),
state: states.State.sanitize(state),
@@ -47,7 +86,7 @@
// Execute all postponed functions now.
while (states.postponed.length) {
(states.postponed.shift())();
states.postponed.shift()();
}
},
};
@@ -68,11 +107,11 @@
* element depends on. It can be nested and can contain
* arbitrary AND and OR clauses.
*/
states.Dependent = function (args) {
states.Dependent = function(args) {
$.extend(this, { values: {}, oldValue: null }, args);
this.dependees = this.getDependees();
Object.keys(this.dependees || {}).forEach((selector) => {
Object.keys(this.dependees || {}).forEach(selector => {
this.initializeDependee(selector, this.dependees[selector]);
});
};
@@ -102,12 +141,13 @@
// compare().
// Otherwise numeric keys in the form's #states array fail to match
// string values returned from jQuery's val().
return (typeof value === 'string') ? compare(reference.toString(), value) : compare(reference, value);
return typeof value === 'string'
? compare(reference.toString(), value)
: compare(reference, value);
},
};
states.Dependent.prototype = {
/**
* Initializes one of the elements this dependent depends on.
*
@@ -120,38 +160,30 @@
* dependee's compliance status.
*/
initializeDependee(selector, dependeeStates) {
let state;
const self = this;
function stateEventHandler(e) {
self.update(e.data.selector, e.data.state, e.value);
}
// Cache for the states of this dependee.
this.values[selector] = {};
// eslint-disable-next-line no-restricted-syntax
for (const i in dependeeStates) {
if (dependeeStates.hasOwnProperty(i)) {
state = dependeeStates[i];
// Make sure we're not initializing this selector/state combination
// twice.
if ($.inArray(state, dependeeStates) === -1) {
continue;
}
state = states.State.sanitize(state);
// Initialize the value of this state.
this.values[selector][state.name] = null;
// Monitor state changes of the specified state for this dependee.
$(selector).on(`state:${state}`, { selector, state }, stateEventHandler);
// Make sure the event we just bound ourselves to is actually fired.
new states.Trigger({ selector, state });
Object.keys(dependeeStates).forEach(i => {
let state = dependeeStates[i];
// Make sure we're not initializing this selector/state combination
// twice.
if ($.inArray(state, dependeeStates) === -1) {
return;
}
}
state = states.State.sanitize(state);
// Initialize the value of this state.
this.values[selector][state.name] = null;
// Monitor state changes of the specified state for this dependee.
$(selector).on(`state:${state}`, { selector, state }, e => {
this.update(e.data.selector, e.data.state, e.value);
});
// Make sure the event we just bound ourselves to is actually fired.
new states.Trigger({ selector, state });
});
},
/**
@@ -173,10 +205,13 @@
const value = this.values[selector][state.name];
if (reference.constructor.name in states.Dependent.comparisons) {
// Use a custom compare function for certain reference value types.
return states.Dependent.comparisons[reference.constructor.name](reference, value);
return states.Dependent.comparisons[reference.constructor.name](
reference,
value,
);
}
// Do a plain comparison otherwise.
// Do a plain comparison otherwise.
return compare(reference, value);
},
@@ -220,7 +255,11 @@
// By adding "trigger: true", we ensure that state changes don't go into
// infinite loops.
this.element.trigger({ type: `state:${this.state}`, value, trigger: true });
this.element.trigger({
type: `state:${this.state}`,
value,
trigger: true,
});
}
},
@@ -247,7 +286,11 @@
const len = constraints.length;
for (let i = 0; i < len; i++) {
if (constraints[i] !== 'xor') {
const constraint = this.checkConstraints(constraints[i], selector, i);
const constraint = this.checkConstraints(
constraints[i],
selector,
i,
);
// Return if this is OR and we have a satisfied constraint or if
// this is XOR and we have a second satisfied constraint.
if (constraint && (hasXor || result)) {
@@ -262,17 +305,18 @@
// bogus, we don't want to end up with an infinite loop.
else if ($.isPlainObject(constraints)) {
// This constraint is an object (AND).
// eslint-disable-next-line no-restricted-syntax
for (const n in constraints) {
if (constraints.hasOwnProperty(n)) {
result = ternary(result, this.checkConstraints(constraints[n], selector, n));
// False and anything else will evaluate to false, so return when
// any false condition is found.
if (result === false) {
return false;
}
}
}
result = Object.keys(constraints).every(constraint => {
const check = this.checkConstraints(
constraints[constraint],
selector,
constraint,
);
/**
* The checkConstraints() function's return value can be undefined. If
* this so, consider it to have returned true.
*/
return typeof check === 'undefined' ? true : check;
});
}
return result;
},
@@ -302,10 +346,9 @@
checkConstraints(value, selector, state) {
// Normalize the last parameter. If it's non-numeric, we treat it either
// as a selector (in case there isn't one yet) or as a trigger/state.
if (typeof state !== 'string' || (/[0-9]/).test(state[0])) {
if (typeof state !== 'string' || /[0-9]/.test(state[0])) {
state = null;
}
else if (typeof selector === 'undefined') {
} else if (typeof selector === 'undefined') {
// Propagate the state to the selector when there isn't one yet.
selector = state;
state = null;
@@ -317,7 +360,7 @@
return invert(this.compare(value, selector, state), state.invert);
}
// Resolve this constraint as an AND/OR operator.
// Resolve this constraint as an AND/OR operator.
return this.verifyConstraints(value, selector);
},
@@ -334,7 +377,7 @@
// Swivel the lookup function so that we can record all available
// selector- state combinations for initialization.
const _compare = this.compare;
this.compare = function (reference, selector, state) {
this.compare = function(reference, selector, state) {
(cache[selector] || (cache[selector] = [])).push(state.name);
// Return nothing (=== undefined) so that the constraint loops are not
// broken.
@@ -360,7 +403,7 @@
* @param {object} args
* Trigger arguments.
*/
states.Trigger = function (args) {
states.Trigger = function(args) {
$.extend(this, args);
if (this.state in states.Trigger.states) {
@@ -375,7 +418,6 @@
};
states.Trigger.prototype = {
/**
* @memberof Drupal.states.Trigger#
*/
@@ -385,9 +427,8 @@
if (typeof trigger === 'function') {
// We have a custom trigger initialization function.
trigger.call(window, this.element);
}
else {
Object.keys(trigger || {}).forEach((event) => {
} else {
Object.keys(trigger || {}).forEach(event => {
this.defaultTrigger(event, trigger[event]);
});
}
@@ -408,19 +449,32 @@
let oldValue = valueFn.call(this.element);
// Attach the event callback.
this.element.on(event, $.proxy(function (e) {
const value = valueFn.call(this.element, e);
// Only trigger the event if the value has actually changed.
if (oldValue !== value) {
this.element.trigger({ type: `state:${this.state}`, value, oldValue });
oldValue = value;
}
}, this));
this.element.on(
event,
$.proxy(function(e) {
const value = valueFn.call(this.element, e);
// Only trigger the event if the value has actually changed.
if (oldValue !== value) {
this.element.trigger({
type: `state:${this.state}`,
value,
oldValue,
});
oldValue = value;
}
}, this),
);
states.postponed.push($.proxy(function () {
// Trigger the event once for initialization purposes.
this.element.trigger({ type: `state:${this.state}`, value: oldValue, oldValue: null });
}, this));
states.postponed.push(
$.proxy(function() {
// Trigger the event once for initialization purposes.
this.element.trigger({
type: `state:${this.state}`,
value: oldValue,
oldValue: null,
});
}, this),
);
},
};
@@ -454,7 +508,7 @@
// support selectors matching multiple checkboxes, iterate over all and
// return whether any is checked.
let checked = false;
this.each(function () {
this.each(function() {
// Use prop() here as we want a boolean of the checkbox state.
// @see http://api.jquery.com/prop/
checked = $(this).prop('checked');
@@ -487,7 +541,9 @@
collapsed: {
collapsed(e) {
return (typeof e !== 'undefined' && 'value' in e) ? e.value : !this.is('[open]');
return typeof e !== 'undefined' && 'value' in e
? e.value
: !this.is('[open]');
},
},
};
@@ -500,7 +556,7 @@
* @param {string} state
* The name of the state.
*/
states.State = function (state) {
states.State = function(state) {
/**
* Original unresolved name.
*/
@@ -519,8 +575,7 @@
// Replace the state with its normalized name.
if (this.name in states.State.aliases) {
this.name = states.State.aliases[this.name];
}
else {
} else {
process = false;
}
} while (process);
@@ -537,7 +592,7 @@
* @return {Drupal.states.state}
* A state object.
*/
states.State.sanitize = function (state) {
states.State.sanitize = function(state) {
if (state instanceof states.State) {
return state;
}
@@ -567,7 +622,6 @@
};
states.State.prototype = {
/**
* @memberof Drupal.states.State#
*/
@@ -594,7 +648,7 @@
*/
const $document = $(document);
$document.on('state:disabled', (e) => {
$document.on('state:disabled', e => {
// Only act when this change was triggered by a dependency and not by the
// element monitoring itself.
if (e.trigger) {
@@ -610,17 +664,19 @@
}
});
$document.on('state:required', (e) => {
$document.on('state:required', e => {
if (e.trigger) {
if (e.value) {
const label = `label${e.target.id ? `[for=${e.target.id}]` : ''}`;
const $label = $(e.target).attr({ required: 'required', 'aria-required': 'aria-required' }).closest('.js-form-item, .js-form-wrapper').find(label);
const $label = $(e.target)
.attr({ required: 'required', 'aria-required': 'aria-required' })
.closest('.js-form-item, .js-form-wrapper')
.find(label);
// Avoids duplicate required markers on initialization.
if (!$label.hasClass('js-form-required').length) {
$label.addClass('js-form-required form-required');
}
}
else {
} else {
$(e.target)
.removeAttr('required aria-required')
.closest('.js-form-item, .js-form-wrapper')
@@ -630,90 +686,27 @@
}
});
$document.on('state:visible', (e) => {
$document.on('state:visible', e => {
if (e.trigger) {
$(e.target).closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggle(e.value);
$(e.target)
.closest('.js-form-item, .js-form-submit, .js-form-wrapper')
.toggle(e.value);
}
});
$document.on('state:checked', (e) => {
$document.on('state:checked', e => {
if (e.trigger) {
$(e.target).prop('checked', e.value);
}
});
$document.on('state:collapsed', (e) => {
$document.on('state:collapsed', e => {
if (e.trigger) {
if ($(e.target).is('[open]') === e.value) {
$(e.target).find('> summary').trigger('click');
$(e.target)
.find('> summary')
.trigger('click');
}
}
});
/**
* These are helper functions implementing addition "operators" and don't
* implement any logic that is particular to states.
*/
/**
* Bitwise AND with a third undefined state.
*
* @function Drupal.states~ternary
*
* @param {*} a
* Value a.
* @param {*} b
* Value b
*
* @return {bool}
* The result.
*/
function ternary(a, b) {
if (typeof a === 'undefined') {
return b;
}
else if (typeof b === 'undefined') {
return a;
}
return a && b;
}
/**
* Inverts a (if it's not undefined) when invertState is true.
*
* @function Drupal.states~invert
*
* @param {*} a
* The value to maybe invert.
* @param {bool} invertState
* Whether to invert state or not.
*
* @return {bool}
* The result.
*/
function invert(a, invertState) {
return (invertState && typeof a !== 'undefined') ? !a : a;
}
/**
* Compares two values while ignoring undefined values.
*
* @function Drupal.states~compare
*
* @param {*} a
* Value a.
* @param {*} b
* Value b.
*
* @return {bool}
* The comparison result.
*/
function compare(a, b) {
if (a === b) {
return typeof a === 'undefined' ? a : true;
}
return typeof a === 'undefined' || typeof b === 'undefined';
}
}(jQuery, Drupal));
})(jQuery, Drupal);
+51 -56
View File
@@ -12,6 +12,18 @@
Drupal.states = states;
function invert(a, invertState) {
return invertState && typeof a !== 'undefined' ? !a : a;
}
function _compare2(a, b) {
if (a === b) {
return typeof a === 'undefined' ? a : true;
}
return typeof a === 'undefined' || typeof b === 'undefined';
}
Drupal.behaviors.states = {
attach: function attach(context, settings) {
var $states = $(context).find('[data-drupal-states]');
@@ -63,32 +75,27 @@
states.Dependent.prototype = {
initializeDependee: function initializeDependee(selector, dependeeStates) {
var state = void 0;
var self = this;
function stateEventHandler(e) {
self.update(e.data.selector, e.data.state, e.value);
}
var _this2 = this;
this.values[selector] = {};
for (var i in dependeeStates) {
if (dependeeStates.hasOwnProperty(i)) {
state = dependeeStates[i];
Object.keys(dependeeStates).forEach(function (i) {
var state = dependeeStates[i];
if ($.inArray(state, dependeeStates) === -1) {
continue;
}
state = states.State.sanitize(state);
this.values[selector][state.name] = null;
$(selector).on('state:' + state, { selector: selector, state: state }, stateEventHandler);
new states.Trigger({ selector: selector, state: state });
if ($.inArray(state, dependeeStates) === -1) {
return;
}
}
state = states.State.sanitize(state);
_this2.values[selector][state.name] = null;
$(selector).on('state:' + state, { selector: selector, state: state }, function (e) {
_this2.update(e.data.selector, e.data.state, e.value);
});
new states.Trigger({ selector: selector, state: state });
});
},
compare: function compare(reference, selector, state) {
var value = this.values[selector][state.name];
@@ -112,10 +119,16 @@
value = invert(value, this.state.invert);
this.element.trigger({ type: 'state:' + this.state, value: value, trigger: true });
this.element.trigger({
type: 'state:' + this.state,
value: value,
trigger: true
});
}
},
verifyConstraints: function verifyConstraints(constraints, selector) {
var _this3 = this;
var result = void 0;
if ($.isArray(constraints)) {
var hasXor = $.inArray('xor', constraints) === -1;
@@ -131,15 +144,11 @@
}
}
} else if ($.isPlainObject(constraints)) {
for (var n in constraints) {
if (constraints.hasOwnProperty(n)) {
result = ternary(result, this.checkConstraints(constraints[n], selector, n));
result = Object.keys(constraints).every(function (constraint) {
var check = _this3.checkConstraints(constraints[constraint], selector, constraint);
if (result === false) {
return false;
}
}
}
return typeof check === 'undefined' ? true : check;
});
}
return result;
},
@@ -188,7 +197,7 @@
states.Trigger.prototype = {
initialize: function initialize() {
var _this2 = this;
var _this4 = this;
var trigger = states.Trigger.states[this.state];
@@ -196,7 +205,7 @@
trigger.call(window, this.element);
} else {
Object.keys(trigger || {}).forEach(function (event) {
_this2.defaultTrigger(event, trigger[event]);
_this4.defaultTrigger(event, trigger[event]);
});
}
@@ -209,13 +218,21 @@
var value = valueFn.call(this.element, e);
if (oldValue !== value) {
this.element.trigger({ type: 'state:' + this.state, value: value, oldValue: oldValue });
this.element.trigger({
type: 'state:' + this.state,
value: value,
oldValue: oldValue
});
oldValue = value;
}
}, this));
states.postponed.push($.proxy(function () {
this.element.trigger({ type: 'state:' + this.state, value: oldValue, oldValue: null });
this.element.trigger({
type: 'state:' + this.state,
value: oldValue,
oldValue: null
});
}, this));
}
};
@@ -352,26 +369,4 @@
}
}
});
function ternary(a, b) {
if (typeof a === 'undefined') {
return b;
} else if (typeof b === 'undefined') {
return a;
}
return a && b;
}
function invert(a, invertState) {
return invertState && typeof a !== 'undefined' ? !a : a;
}
function _compare2(a, b) {
if (a === b) {
return typeof a === 'undefined' ? a : true;
}
return typeof a === 'undefined' || typeof b === 'undefined';
}
})(jQuery, Drupal);
+273 -267
View File
@@ -27,7 +27,7 @@
* @event drupalTabbingContextDeactivated
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Provides an API for managing page tabbing order modifications.
*
@@ -46,202 +46,6 @@
this.stack = [];
}
/**
* Add public methods to the TabbingManager class.
*/
$.extend(TabbingManager.prototype, /** @lends Drupal~TabbingManager# */{
/**
* Constrain tabbing to the specified set of elements only.
*
* Makes elements outside of the specified set of elements unreachable via
* the tab key.
*
* @param {jQuery} elements
* The set of elements to which tabbing should be constrained. Can also
* be a jQuery-compatible selector string.
*
* @return {Drupal~TabbingContext}
* The TabbingContext instance.
*
* @fires event:drupalTabbingConstrained
*/
constrain(elements) {
// Deactivate all tabbingContexts to prepare for the new constraint. A
// tabbingContext instance will only be reactivated if the stack is
// unwound to it in the _unwindStack() method.
const il = this.stack.length;
for (let i = 0; i < il; i++) {
this.stack[i].deactivate();
}
// The "active tabbing set" are the elements tabbing should be constrained
// to.
const $elements = $(elements).find(':tabbable').addBack(':tabbable');
const tabbingContext = new TabbingContext({
// The level is the current height of the stack before this new
// tabbingContext is pushed on top of the stack.
level: this.stack.length,
$tabbableElements: $elements,
});
this.stack.push(tabbingContext);
// Activates the tabbingContext; this will manipulate the DOM to constrain
// tabbing.
tabbingContext.activate();
// Allow modules to respond to the constrain event.
$(document).trigger('drupalTabbingConstrained', tabbingContext);
return tabbingContext;
},
/**
* Restores a former tabbingContext when an active one is released.
*
* The TabbingManager stack of tabbingContext instances will be unwound
* from the top-most released tabbingContext down to the first non-released
* tabbingContext instance. This non-released instance is then activated.
*/
release() {
// Unwind as far as possible: find the topmost non-released
// tabbingContext.
let toActivate = this.stack.length - 1;
while (toActivate >= 0 && this.stack[toActivate].released) {
toActivate--;
}
// Delete all tabbingContexts after the to be activated one. They have
// already been deactivated, so their effect on the DOM has been reversed.
this.stack.splice(toActivate + 1);
// Get topmost tabbingContext, if one exists, and activate it.
if (toActivate >= 0) {
this.stack[toActivate].activate();
}
},
/**
* Makes all elements outside of the tabbingContext's set untabbable.
*
* Elements made untabbable have their original tabindex and autofocus
* values stored so that they might be restored later when this
* tabbingContext is deactivated.
*
* @param {Drupal~TabbingContext} tabbingContext
* The TabbingContext instance that has been activated.
*/
activate(tabbingContext) {
const $set = tabbingContext.$tabbableElements;
const level = tabbingContext.level;
// Determine which elements are reachable via tabbing by default.
const $disabledSet = $(':tabbable')
// Exclude elements of the active tabbing set.
.not($set);
// Set the disabled set on the tabbingContext.
tabbingContext.$disabledElements = $disabledSet;
// Record the tabindex for each element, so we can restore it later.
const il = $disabledSet.length;
for (let i = 0; i < il; i++) {
this.recordTabindex($disabledSet.eq(i), level);
}
// Make all tabbable elements outside of the active tabbing set
// unreachable.
$disabledSet
.prop('tabindex', -1)
.prop('autofocus', false);
// Set focus on an element in the tabbingContext's set of tabbable
// elements. First, check if there is an element with an autofocus
// attribute. Select the last one from the DOM order.
let $hasFocus = $set.filter('[autofocus]').eq(-1);
// If no element in the tabbable set has an autofocus attribute, select
// the first element in the set.
if ($hasFocus.length === 0) {
$hasFocus = $set.eq(0);
}
$hasFocus.trigger('focus');
},
/**
* Restores that tabbable state of a tabbingContext's disabled elements.
*
* Elements that were made untabbable have their original tabindex and
* autofocus values restored.
*
* @param {Drupal~TabbingContext} tabbingContext
* The TabbingContext instance that has been deactivated.
*/
deactivate(tabbingContext) {
const $set = tabbingContext.$disabledElements;
const level = tabbingContext.level;
const il = $set.length;
for (let i = 0; i < il; i++) {
this.restoreTabindex($set.eq(i), level);
}
},
/**
* Records the tabindex and autofocus values of an untabbable element.
*
* @param {jQuery} $el
* The set of elements that have been disabled.
* @param {number} level
* The stack level for which the tabindex attribute should be recorded.
*/
recordTabindex($el, level) {
const tabInfo = $el.data('drupalOriginalTabIndices') || {};
tabInfo[level] = {
tabindex: $el[0].getAttribute('tabindex'),
autofocus: $el[0].hasAttribute('autofocus'),
};
$el.data('drupalOriginalTabIndices', tabInfo);
},
/**
* Restores the tabindex and autofocus values of a reactivated element.
*
* @param {jQuery} $el
* The element that is being reactivated.
* @param {number} level
* The stack level for which the tabindex attribute should be restored.
*/
restoreTabindex($el, level) {
const tabInfo = $el.data('drupalOriginalTabIndices');
if (tabInfo && tabInfo[level]) {
const data = tabInfo[level];
if (data.tabindex) {
$el[0].setAttribute('tabindex', data.tabindex);
}
// If the element did not have a tabindex at this stack level then
// remove it.
else {
$el[0].removeAttribute('tabindex');
}
if (data.autofocus) {
$el[0].setAttribute('autofocus', 'autofocus');
}
// Clean up $.data.
if (level === 0) {
// Remove all data.
$el.removeData('drupalOriginalTabIndices');
}
else {
// Remove the data for this stack level and higher.
let levelToDelete = level;
while (tabInfo.hasOwnProperty(levelToDelete)) {
delete tabInfo[levelToDelete];
levelToDelete++;
}
$el.data('drupalOriginalTabIndices', tabInfo);
}
}
},
});
/**
* Stores a set of tabbable elements.
*
@@ -268,87 +72,289 @@
* tabbingContext can be active at a time.
*/
function TabbingContext(options) {
$.extend(this, /** @lends Drupal~TabbingContext# */{
$.extend(
this,
/** @lends Drupal~TabbingContext# */ {
/**
* @type {?number}
*/
level: null,
/**
* @type {?number}
*/
level: null,
/**
* @type {jQuery}
*/
$tabbableElements: $(),
/**
* @type {jQuery}
*/
$tabbableElements: $(),
/**
* @type {jQuery}
*/
$disabledElements: $(),
/**
* @type {jQuery}
*/
$disabledElements: $(),
/**
* @type {bool}
*/
released: false,
/**
* @type {bool}
*/
released: false,
/**
* @type {bool}
*/
active: false,
}, options);
/**
* @type {bool}
*/
active: false,
},
options,
);
}
/**
* Add public methods to the TabbingManager class.
*/
$.extend(
TabbingManager.prototype,
/** @lends Drupal~TabbingManager# */ {
/**
* Constrain tabbing to the specified set of elements only.
*
* Makes elements outside of the specified set of elements unreachable via
* the tab key.
*
* @param {jQuery} elements
* The set of elements to which tabbing should be constrained. Can also
* be a jQuery-compatible selector string.
*
* @return {Drupal~TabbingContext}
* The TabbingContext instance.
*
* @fires event:drupalTabbingConstrained
*/
constrain(elements) {
// Deactivate all tabbingContexts to prepare for the new constraint. A
// tabbingContext instance will only be reactivated if the stack is
// unwound to it in the _unwindStack() method.
const il = this.stack.length;
for (let i = 0; i < il; i++) {
this.stack[i].deactivate();
}
// The "active tabbing set" are the elements tabbing should be constrained
// to.
const $elements = $(elements)
.find(':tabbable')
.addBack(':tabbable');
const tabbingContext = new TabbingContext({
// The level is the current height of the stack before this new
// tabbingContext is pushed on top of the stack.
level: this.stack.length,
$tabbableElements: $elements,
});
this.stack.push(tabbingContext);
// Activates the tabbingContext; this will manipulate the DOM to constrain
// tabbing.
tabbingContext.activate();
// Allow modules to respond to the constrain event.
$(document).trigger('drupalTabbingConstrained', tabbingContext);
return tabbingContext;
},
/**
* Restores a former tabbingContext when an active one is released.
*
* The TabbingManager stack of tabbingContext instances will be unwound
* from the top-most released tabbingContext down to the first non-released
* tabbingContext instance. This non-released instance is then activated.
*/
release() {
// Unwind as far as possible: find the topmost non-released
// tabbingContext.
let toActivate = this.stack.length - 1;
while (toActivate >= 0 && this.stack[toActivate].released) {
toActivate--;
}
// Delete all tabbingContexts after the to be activated one. They have
// already been deactivated, so their effect on the DOM has been reversed.
this.stack.splice(toActivate + 1);
// Get topmost tabbingContext, if one exists, and activate it.
if (toActivate >= 0) {
this.stack[toActivate].activate();
}
},
/**
* Makes all elements outside of the tabbingContext's set untabbable.
*
* Elements made untabbable have their original tabindex and autofocus
* values stored so that they might be restored later when this
* tabbingContext is deactivated.
*
* @param {Drupal~TabbingContext} tabbingContext
* The TabbingContext instance that has been activated.
*/
activate(tabbingContext) {
const $set = tabbingContext.$tabbableElements;
const level = tabbingContext.level;
// Determine which elements are reachable via tabbing by default.
const $disabledSet = $(':tabbable')
// Exclude elements of the active tabbing set.
.not($set);
// Set the disabled set on the tabbingContext.
tabbingContext.$disabledElements = $disabledSet;
// Record the tabindex for each element, so we can restore it later.
const il = $disabledSet.length;
for (let i = 0; i < il; i++) {
this.recordTabindex($disabledSet.eq(i), level);
}
// Make all tabbable elements outside of the active tabbing set
// unreachable.
$disabledSet.prop('tabindex', -1).prop('autofocus', false);
// Set focus on an element in the tabbingContext's set of tabbable
// elements. First, check if there is an element with an autofocus
// attribute. Select the last one from the DOM order.
let $hasFocus = $set.filter('[autofocus]').eq(-1);
// If no element in the tabbable set has an autofocus attribute, select
// the first element in the set.
if ($hasFocus.length === 0) {
$hasFocus = $set.eq(0);
}
$hasFocus.trigger('focus');
},
/**
* Restores that tabbable state of a tabbingContext's disabled elements.
*
* Elements that were made untabbable have their original tabindex and
* autofocus values restored.
*
* @param {Drupal~TabbingContext} tabbingContext
* The TabbingContext instance that has been deactivated.
*/
deactivate(tabbingContext) {
const $set = tabbingContext.$disabledElements;
const level = tabbingContext.level;
const il = $set.length;
for (let i = 0; i < il; i++) {
this.restoreTabindex($set.eq(i), level);
}
},
/**
* Records the tabindex and autofocus values of an untabbable element.
*
* @param {jQuery} $el
* The set of elements that have been disabled.
* @param {number} level
* The stack level for which the tabindex attribute should be recorded.
*/
recordTabindex($el, level) {
const tabInfo = $el.data('drupalOriginalTabIndices') || {};
tabInfo[level] = {
tabindex: $el[0].getAttribute('tabindex'),
autofocus: $el[0].hasAttribute('autofocus'),
};
$el.data('drupalOriginalTabIndices', tabInfo);
},
/**
* Restores the tabindex and autofocus values of a reactivated element.
*
* @param {jQuery} $el
* The element that is being reactivated.
* @param {number} level
* The stack level for which the tabindex attribute should be restored.
*/
restoreTabindex($el, level) {
const tabInfo = $el.data('drupalOriginalTabIndices');
if (tabInfo && tabInfo[level]) {
const data = tabInfo[level];
if (data.tabindex) {
$el[0].setAttribute('tabindex', data.tabindex);
}
// If the element did not have a tabindex at this stack level then
// remove it.
else {
$el[0].removeAttribute('tabindex');
}
if (data.autofocus) {
$el[0].setAttribute('autofocus', 'autofocus');
}
// Clean up $.data.
if (level === 0) {
// Remove all data.
$el.removeData('drupalOriginalTabIndices');
} else {
// Remove the data for this stack level and higher.
let levelToDelete = level;
while (tabInfo.hasOwnProperty(levelToDelete)) {
delete tabInfo[levelToDelete];
levelToDelete++;
}
$el.data('drupalOriginalTabIndices', tabInfo);
}
}
},
},
);
/**
* Add public methods to the TabbingContext class.
*/
$.extend(TabbingContext.prototype, /** @lends Drupal~TabbingContext# */{
$.extend(
TabbingContext.prototype,
/** @lends Drupal~TabbingContext# */ {
/**
* Releases this TabbingContext.
*
* Once a TabbingContext object is released, it can never be activated
* again.
*
* @fires event:drupalTabbingContextReleased
*/
release() {
if (!this.released) {
this.deactivate();
this.released = true;
Drupal.tabbingManager.release(this);
// Allow modules to respond to the tabbingContext release event.
$(document).trigger('drupalTabbingContextReleased', this);
}
},
/**
* Releases this TabbingContext.
*
* Once a TabbingContext object is released, it can never be activated
* again.
*
* @fires event:drupalTabbingContextReleased
*/
release() {
if (!this.released) {
this.deactivate();
this.released = true;
Drupal.tabbingManager.release(this);
// Allow modules to respond to the tabbingContext release event.
$(document).trigger('drupalTabbingContextReleased', this);
}
},
/**
* Activates this TabbingContext.
*
* @fires event:drupalTabbingContextActivated
*/
activate() {
// A released TabbingContext object can never be activated again.
if (!this.active && !this.released) {
this.active = true;
Drupal.tabbingManager.activate(this);
// Allow modules to respond to the constrain event.
$(document).trigger('drupalTabbingContextActivated', this);
}
},
/**
* Activates this TabbingContext.
*
* @fires event:drupalTabbingContextActivated
*/
activate() {
// A released TabbingContext object can never be activated again.
if (!this.active && !this.released) {
this.active = true;
Drupal.tabbingManager.activate(this);
// Allow modules to respond to the constrain event.
$(document).trigger('drupalTabbingContextActivated', this);
}
/**
* Deactivates this TabbingContext.
*
* @fires event:drupalTabbingContextDeactivated
*/
deactivate() {
if (this.active) {
this.active = false;
Drupal.tabbingManager.deactivate(this);
// Allow modules to respond to the constrain event.
$(document).trigger('drupalTabbingContextDeactivated', this);
}
},
},
/**
* Deactivates this TabbingContext.
*
* @fires event:drupalTabbingContextDeactivated
*/
deactivate() {
if (this.active) {
this.active = false;
Drupal.tabbingManager.deactivate(this);
// Allow modules to respond to the constrain event.
$(document).trigger('drupalTabbingContextDeactivated', this);
}
},
});
);
// Mark this behavior as processed on the first pass and return if it is
// already processed.
@@ -360,4 +366,4 @@
* @type {Drupal~TabbingManager}
*/
Drupal.tabbingManager = new TabbingManager();
}(jQuery, Drupal));
})(jQuery, Drupal);
+14 -14
View File
@@ -10,6 +10,20 @@
this.stack = [];
}
function TabbingContext(options) {
$.extend(this, {
level: null,
$tabbableElements: $(),
$disabledElements: $(),
released: false,
active: false
}, options);
}
$.extend(TabbingManager.prototype, {
constrain: function constrain(elements) {
var il = this.stack.length;
@@ -109,20 +123,6 @@
}
});
function TabbingContext(options) {
$.extend(this, {
level: null,
$tabbableElements: $(),
$disabledElements: $(),
released: false,
active: false
}, options);
}
$.extend(TabbingContext.prototype, {
release: function release() {
if (!this.released) {
+392 -237
View File
File diff suppressed because it is too large Load Diff
+55 -40
View File
@@ -4,6 +4,7 @@
* https://www.drupal.org/node/2815083
* @preserve
**/
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
(function ($, Drupal, drupalSettings) {
var showWeight = JSON.parse(localStorage.getItem('Drupal.tableDrag.showWeight'));
@@ -121,16 +122,15 @@
var cell = void 0;
var columnIndex = void 0;
Object.keys(this.tableSettings || {}).forEach(function (group) {
for (var d in _this2.tableSettings[group]) {
if (_this2.tableSettings[group].hasOwnProperty(d)) {
var field = $table.find('.' + _this2.tableSettings[group][d].target).eq(0);
if (field.length && _this2.tableSettings[group][d].hidden) {
hidden = _this2.tableSettings[group][d].hidden;
cell = field.closest('td');
break;
}
Object.keys(_this2.tableSettings[group]).some(function (tableSetting) {
var field = $table.find('.' + _this2.tableSettings[group][tableSetting].target).eq(0);
if (field.length && _this2.tableSettings[group][tableSetting].hidden) {
hidden = _this2.tableSettings[group][tableSetting].hidden;
cell = field.closest('td');
return true;
}
}
return false;
});
if (hidden && cell[0]) {
columnIndex = cell.parent().find('> td').index(cell.get(0)) + 1;
@@ -213,22 +213,19 @@
Drupal.tableDrag.prototype.rowSettings = function (group, row) {
var field = $(row).find('.' + group);
var tableSettingsGroup = this.tableSettings[group];
for (var delta in tableSettingsGroup) {
if (tableSettingsGroup.hasOwnProperty(delta)) {
var targetClass = tableSettingsGroup[delta].target;
if (field.is('.' + targetClass)) {
var rowSettings = {};
for (var n in tableSettingsGroup[delta]) {
if (tableSettingsGroup[delta].hasOwnProperty(n)) {
rowSettings[n] = tableSettingsGroup[delta][n];
}
}
return rowSettings;
}
return Object.keys(tableSettingsGroup).map(function (delta) {
var targetClass = tableSettingsGroup[delta].target;
var rowSettings = void 0;
if (field.is('.' + targetClass)) {
rowSettings = {};
Object.keys(tableSettingsGroup[delta]).forEach(function (n) {
rowSettings[n] = tableSettingsGroup[delta][n];
});
}
}
return rowSettings;
}).filter(function (rowSetting) {
return rowSetting;
})[0];
};
Drupal.tableDrag.prototype.makeDraggable = function (item) {
@@ -532,8 +529,11 @@
};
Drupal.tableDrag.prototype.findDropTargetRow = function (x, y) {
var _this3 = this;
var rows = $(this.table.tBodies[0].rows).not(':hidden');
for (var n = 0; n < rows.length; n++) {
var _loop = function _loop(n) {
var row = rows[n];
var $row = $(row);
var rowY = $row.offset().top;
@@ -546,35 +546,49 @@
}
if (y > rowY - rowHeight && y < rowY + rowHeight) {
if (this.indentEnabled) {
for (n in this.rowObject.group) {
if (this.rowObject.group[n] === row) {
return null;
}
if (_this3.indentEnabled) {
if (Object.keys(_this3.rowObject.group).some(function (o) {
return _this3.rowObject.group[o] === row;
})) {
return {
v: null
};
}
} else if (row === this.rowObject.element) {
return null;
} else if (row === _this3.rowObject.element) {
return {
v: null
};
}
if (!this.rowObject.isValidSwap(row)) {
return null;
if (!_this3.rowObject.isValidSwap(row)) {
return {
v: null
};
}
while ($row.is(':hidden') && $row.prev('tr').is(':hidden')) {
$row = $row.prev('tr:first-of-type');
row = $row.get(0);
}
return row;
return {
v: row
};
}
};
for (var n = 0; n < rows.length; n++) {
var _ret = _loop(n);
if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v;
}
return null;
};
Drupal.tableDrag.prototype.updateFields = function (changedRow) {
var _this3 = this;
var _this4 = this;
Object.keys(this.tableSettings || {}).forEach(function (group) {
_this3.updateField(changedRow, group);
_this4.updateField(changedRow, group);
});
};
@@ -711,7 +725,8 @@
delta = trigger / (windowHeight + scrollY - cursorY);
delta = delta > 0 && delta < trigger ? delta : trigger;
return delta * this.scrollSettings.amount;
} else if (cursorY - scrollY < trigger) {
}
if (cursorY - scrollY < trigger) {
delta = trigger / (cursorY - scrollY);
delta = delta > 0 && delta < trigger ? delta : trigger;
return -delta * this.scrollSettings.amount;
@@ -926,10 +941,10 @@
};
Drupal.tableDrag.prototype.row.prototype.removeIndentClasses = function () {
var _this4 = this;
var _this5 = this;
Object.keys(this.children || {}).forEach(function (n) {
$(_this4.children[n]).find('.js-indentation').removeClass('tree-child').removeClass('tree-child-first').removeClass('tree-child-last').removeClass('tree-child-horizontal');
$(_this5.children[n]).find('.js-indentation').removeClass('tree-child').removeClass('tree-child-first').removeClass('tree-child-last').removeClass('tree-child-horizontal');
});
};
+257 -240
View File
@@ -3,92 +3,7 @@
* Sticky table headers.
*/
(function ($, Drupal, displace) {
/**
* Attaches sticky table headers.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the sticky table header behavior.
*/
Drupal.behaviors.tableHeader = {
attach(context) {
$(window).one('scroll.TableHeaderInit', { context }, tableHeaderInitHandler);
},
};
function scrollValue(position) {
return document.documentElement[position] || document.body[position];
}
// Select and initialize sticky table headers.
function tableHeaderInitHandler(e) {
const $tables = $(e.data.context).find('table.sticky-enabled').once('tableheader');
const il = $tables.length;
for (let i = 0; i < il; i++) {
TableHeader.tables.push(new TableHeader($tables[i]));
}
forTables('onScroll');
}
// Helper method to loop through tables and execute a method.
function forTables(method, arg) {
const tables = TableHeader.tables;
const il = tables.length;
for (let i = 0; i < il; i++) {
tables[i][method](arg);
}
}
function tableHeaderResizeHandler(e) {
forTables('recalculateSticky');
}
function tableHeaderOnScrollHandler(e) {
forTables('onScroll');
}
function tableHeaderOffsetChangeHandler(e, offsets) {
forTables('stickyPosition', offsets.top);
}
// Bind event that need to change all tables.
$(window).on({
/**
* When resizing table width can change, recalculate everything.
*
* @ignore
*/
'resize.TableHeader': tableHeaderResizeHandler,
/**
* Bind only one event to take care of calling all scroll callbacks.
*
* @ignore
*/
'scroll.TableHeader': tableHeaderOnScrollHandler,
});
// Bind to custom Drupal events.
$(document).on({
/**
* Recalculate columns width when window is resized and when show/hide
* weight is triggered.
*
* @ignore
*/
'columnschange.TableHeader': tableHeaderResizeHandler,
/**
* Recalculate TableHeader.topOffset when viewport is resized.
*
* @ignore
*/
'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler,
});
(function($, Drupal, displace) {
/**
* Constructor for the tableHeader object. Provides sticky table headers.
*
@@ -131,182 +46,284 @@
this.tableOffset = this.$originalTable.offset();
// React to columns change to avoid making checks in the scroll callback.
this.$originalTable.on('columnschange', { tableHeader: this }, (e, display) => {
const tableHeader = e.data.tableHeader;
if (tableHeader.displayWeight === null || tableHeader.displayWeight !== display) {
tableHeader.recalculateSticky();
}
tableHeader.displayWeight = display;
});
this.$originalTable.on(
'columnschange',
{ tableHeader: this },
(e, display) => {
const tableHeader = e.data.tableHeader;
if (
tableHeader.displayWeight === null ||
tableHeader.displayWeight !== display
) {
tableHeader.recalculateSticky();
}
tableHeader.displayWeight = display;
},
);
// Create and display sticky header.
this.createSticky();
}
// Helper method to loop through tables and execute a method.
function forTables(method, arg) {
const tables = TableHeader.tables;
const il = tables.length;
for (let i = 0; i < il; i++) {
tables[i][method](arg);
}
}
// Select and initialize sticky table headers.
function tableHeaderInitHandler(e) {
const $tables = $(e.data.context)
.find('table.sticky-enabled')
.once('tableheader');
const il = $tables.length;
for (let i = 0; i < il; i++) {
TableHeader.tables.push(new TableHeader($tables[i]));
}
forTables('onScroll');
}
/**
* Attaches sticky table headers.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the sticky table header behavior.
*/
Drupal.behaviors.tableHeader = {
attach(context) {
$(window).one(
'scroll.TableHeaderInit',
{ context },
tableHeaderInitHandler,
);
},
};
function scrollValue(position) {
return document.documentElement[position] || document.body[position];
}
function tableHeaderResizeHandler(e) {
forTables('recalculateSticky');
}
function tableHeaderOnScrollHandler(e) {
forTables('onScroll');
}
function tableHeaderOffsetChangeHandler(e, offsets) {
forTables('stickyPosition', offsets.top);
}
// Bind event that need to change all tables.
$(window).on({
/**
* When resizing table width can change, recalculate everything.
*
* @ignore
*/
'resize.TableHeader': tableHeaderResizeHandler,
/**
* Bind only one event to take care of calling all scroll callbacks.
*
* @ignore
*/
'scroll.TableHeader': tableHeaderOnScrollHandler,
});
// Bind to custom Drupal events.
$(document).on({
/**
* Recalculate columns width when window is resized and when show/hide
* weight is triggered.
*
* @ignore
*/
'columnschange.TableHeader': tableHeaderResizeHandler,
/**
* Recalculate TableHeader.topOffset when viewport is resized.
*
* @ignore
*/
'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler,
});
/**
* Store the state of TableHeader.
*/
$.extend(TableHeader, /** @lends Drupal.TableHeader */{
/**
* This will store the state of all processed tables.
*
* @type {Array.<Drupal.TableHeader>}
*/
tables: [],
});
$.extend(
TableHeader,
/** @lends Drupal.TableHeader */ {
/**
* This will store the state of all processed tables.
*
* @type {Array.<Drupal.TableHeader>}
*/
tables: [],
},
);
/**
* Extend TableHeader prototype.
*/
$.extend(TableHeader.prototype, /** @lends Drupal.TableHeader# */{
$.extend(
TableHeader.prototype,
/** @lends Drupal.TableHeader# */ {
/**
* Minimum height in pixels for the table to have a sticky header.
*
* @type {number}
*/
minHeight: 100,
/**
* Minimum height in pixels for the table to have a sticky header.
*
* @type {number}
*/
minHeight: 100,
/**
* Absolute position of the table on the page.
*
* @type {?Drupal~displaceOffset}
*/
tableOffset: null,
/**
* Absolute position of the table on the page.
*
* @type {?Drupal~displaceOffset}
*/
tableOffset: null,
/**
* Absolute position of the table on the page.
*
* @type {?number}
*/
tableHeight: null,
/**
* Absolute position of the table on the page.
*
* @type {?number}
*/
tableHeight: null,
/**
* Boolean storing the sticky header visibility state.
*
* @type {bool}
*/
stickyVisible: false,
/**
* Boolean storing the sticky header visibility state.
*
* @type {bool}
*/
stickyVisible: false,
/**
* Create the duplicate header.
*/
createSticky() {
// Clone the table header so it inherits original jQuery properties.
const $stickyHeader = this.$originalHeader.clone(true);
// Hide the table to avoid a flash of the header clone upon page load.
this.$stickyTable = $('<table class="sticky-header"/>')
.css({
visibility: 'hidden',
position: 'fixed',
top: '0px',
})
.append($stickyHeader)
.insertBefore(this.$originalTable);
/**
* Create the duplicate header.
*/
createSticky() {
// Clone the table header so it inherits original jQuery properties.
const $stickyHeader = this.$originalHeader.clone(true);
// Hide the table to avoid a flash of the header clone upon page load.
this.$stickyTable = $('<table class="sticky-header"/>')
.css({
visibility: 'hidden',
position: 'fixed',
top: '0px',
})
.append($stickyHeader)
.insertBefore(this.$originalTable);
this.$stickyHeaderCells = $stickyHeader.find('> tr > th');
this.$stickyHeaderCells = $stickyHeader.find('> tr > th');
// Initialize all computations.
this.recalculateSticky();
},
// Initialize all computations.
this.recalculateSticky();
},
/**
* Set absolute position of sticky.
*
* @param {number} offsetTop
* The top offset for the sticky header.
* @param {number} offsetLeft
* The left offset for the sticky header.
*
* @return {jQuery}
* The sticky table as a jQuery collection.
*/
stickyPosition(offsetTop, offsetLeft) {
const css = {};
if (typeof offsetTop === 'number') {
css.top = `${offsetTop}px`;
}
if (typeof offsetLeft === 'number') {
css.left = `${this.tableOffset.left - offsetLeft}px`;
}
return this.$stickyTable.css(css);
},
/**
* Returns true if sticky is currently visible.
*
* @return {bool}
* The visibility status.
*/
checkStickyVisible() {
const scrollTop = scrollValue('scrollTop');
const tableTop = this.tableOffset.top - displace.offsets.top;
const tableBottom = tableTop + this.tableHeight;
let visible = false;
if (tableTop < scrollTop && scrollTop < (tableBottom - this.minHeight)) {
visible = true;
}
this.stickyVisible = visible;
return visible;
},
/**
* Check if sticky header should be displayed.
*
* This function is throttled to once every 250ms to avoid unnecessary
* calls.
*
* @param {jQuery.Event} e
* The scroll event.
*/
onScroll(e) {
this.checkStickyVisible();
// Track horizontal positioning relative to the viewport.
this.stickyPosition(null, scrollValue('scrollLeft'));
this.$stickyTable.css('visibility', this.stickyVisible ? 'visible' : 'hidden');
},
/**
* Event handler: recalculates position of the sticky table header.
*
* @param {jQuery.Event} event
* Event being triggered.
*/
recalculateSticky(event) {
// Update table size.
this.tableHeight = this.$originalTable[0].clientHeight;
// Update offset top.
displace.offsets.top = displace.calculateOffset('top');
this.tableOffset = this.$originalTable.offset();
this.stickyPosition(displace.offsets.top, scrollValue('scrollLeft'));
// Update columns width.
let $that = null;
let $stickyCell = null;
let display = null;
// Resize header and its cell widths.
// Only apply width to visible table cells. This prevents the header from
// displaying incorrectly when the sticky header is no longer visible.
const il = this.$originalHeaderCells.length;
for (let i = 0; i < il; i++) {
$that = $(this.$originalHeaderCells[i]);
$stickyCell = this.$stickyHeaderCells.eq($that.index());
display = $that.css('display');
if (display !== 'none') {
$stickyCell.css({ width: $that.css('width'), display });
/**
* Set absolute position of sticky.
*
* @param {number} offsetTop
* The top offset for the sticky header.
* @param {number} offsetLeft
* The left offset for the sticky header.
*
* @return {jQuery}
* The sticky table as a jQuery collection.
*/
stickyPosition(offsetTop, offsetLeft) {
const css = {};
if (typeof offsetTop === 'number') {
css.top = `${offsetTop}px`;
}
else {
$stickyCell.css('display', 'none');
if (typeof offsetLeft === 'number') {
css.left = `${this.tableOffset.left - offsetLeft}px`;
}
}
this.$stickyTable.css('width', this.$originalTable.outerWidth());
return this.$stickyTable.css(css);
},
/**
* Returns true if sticky is currently visible.
*
* @return {bool}
* The visibility status.
*/
checkStickyVisible() {
const scrollTop = scrollValue('scrollTop');
const tableTop = this.tableOffset.top - displace.offsets.top;
const tableBottom = tableTop + this.tableHeight;
let visible = false;
if (tableTop < scrollTop && scrollTop < tableBottom - this.minHeight) {
visible = true;
}
this.stickyVisible = visible;
return visible;
},
/**
* Check if sticky header should be displayed.
*
* This function is throttled to once every 250ms to avoid unnecessary
* calls.
*
* @param {jQuery.Event} e
* The scroll event.
*/
onScroll(e) {
this.checkStickyVisible();
// Track horizontal positioning relative to the viewport.
this.stickyPosition(null, scrollValue('scrollLeft'));
this.$stickyTable.css(
'visibility',
this.stickyVisible ? 'visible' : 'hidden',
);
},
/**
* Event handler: recalculates position of the sticky table header.
*
* @param {jQuery.Event} event
* Event being triggered.
*/
recalculateSticky(event) {
// Update table size.
this.tableHeight = this.$originalTable[0].clientHeight;
// Update offset top.
displace.offsets.top = displace.calculateOffset('top');
this.tableOffset = this.$originalTable.offset();
this.stickyPosition(displace.offsets.top, scrollValue('scrollLeft'));
// Update columns width.
let $that = null;
let $stickyCell = null;
let display = null;
// Resize header and its cell widths.
// Only apply width to visible table cells. This prevents the header from
// displaying incorrectly when the sticky header is no longer visible.
const il = this.$originalHeaderCells.length;
for (let i = 0; i < il; i++) {
$that = $(this.$originalHeaderCells[i]);
$stickyCell = this.$stickyHeaderCells.eq($that.index());
display = $that.css('display');
if (display !== 'none') {
$stickyCell.css({ width: $that.css('width'), display });
} else {
$stickyCell.css('display', 'none');
}
}
this.$stickyTable.css('width', this.$originalTable.outerWidth());
},
},
});
);
// Expose constructor in the public space.
Drupal.TableHeader = TableHeader;
}(jQuery, Drupal, window.parent.Drupal.displace));
})(jQuery, Drupal, window.parent.Drupal.displace);
+37 -37
View File
@@ -6,14 +6,37 @@
**/
(function ($, Drupal, displace) {
Drupal.behaviors.tableHeader = {
attach: function attach(context) {
$(window).one('scroll.TableHeaderInit', { context: context }, tableHeaderInitHandler);
}
};
function TableHeader(table) {
var $table = $(table);
function scrollValue(position) {
return document.documentElement[position] || document.body[position];
this.$originalTable = $table;
this.$originalHeader = $table.children('thead');
this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
this.displayWeight = null;
this.$originalTable.addClass('sticky-table');
this.tableHeight = $table[0].clientHeight;
this.tableOffset = this.$originalTable.offset();
this.$originalTable.on('columnschange', { tableHeader: this }, function (e, display) {
var tableHeader = e.data.tableHeader;
if (tableHeader.displayWeight === null || tableHeader.displayWeight !== display) {
tableHeader.recalculateSticky();
}
tableHeader.displayWeight = display;
});
this.createSticky();
}
function forTables(method, arg) {
var tables = TableHeader.tables;
var il = tables.length;
for (var i = 0; i < il; i++) {
tables[i][method](arg);
}
}
function tableHeaderInitHandler(e) {
@@ -25,12 +48,14 @@
forTables('onScroll');
}
function forTables(method, arg) {
var tables = TableHeader.tables;
var il = tables.length;
for (var i = 0; i < il; i++) {
tables[i][method](arg);
Drupal.behaviors.tableHeader = {
attach: function attach(context) {
$(window).one('scroll.TableHeaderInit', { context: context }, tableHeaderInitHandler);
}
};
function scrollValue(position) {
return document.documentElement[position] || document.body[position];
}
function tableHeaderResizeHandler(e) {
@@ -57,31 +82,6 @@
'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler
});
function TableHeader(table) {
var $table = $(table);
this.$originalTable = $table;
this.$originalHeader = $table.children('thead');
this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
this.displayWeight = null;
this.$originalTable.addClass('sticky-table');
this.tableHeight = $table[0].clientHeight;
this.tableOffset = this.$originalTable.offset();
this.$originalTable.on('columnschange', { tableHeader: this }, function (e, display) {
var tableHeader = e.data.tableHeader;
if (tableHeader.displayWeight === null || tableHeader.displayWeight !== display) {
tableHeader.recalculateSticky();
}
tableHeader.displayWeight = display;
});
this.createSticky();
}
$.extend(TableHeader, {
tables: []
});
+146 -116
View File
@@ -3,27 +3,7 @@
* Responsive table functionality.
*/
(function ($, Drupal, window) {
/**
* Attach the tableResponsive function to {@link Drupal.behaviors}.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches tableResponsive functionality.
*/
Drupal.behaviors.tableResponsive = {
attach(context, settings) {
const $tables = $(context).find('table.responsive-enabled').once('tableresponsive');
if ($tables.length) {
const il = $tables.length;
for (let i = 0; i < il; i++) {
TableResponsive.tables.push(new TableResponsive($tables[i]));
}
}
},
};
(function($, Drupal, window) {
/**
* The TableResponsive object optimizes table presentation for screen size.
*
@@ -48,30 +28,68 @@
// traversed only once to find them.
this.$headers = this.$table.find('th');
// Add a link before the table for users to show or hide weight columns.
this.$link = $('<button type="button" class="link tableresponsive-toggle"></button>')
.attr('title', Drupal.t('Show table cells that were hidden to make the table fit within a small screen.'))
this.$link = $(
'<button type="button" class="link tableresponsive-toggle"></button>',
)
.attr(
'title',
Drupal.t(
'Show table cells that were hidden to make the table fit within a small screen.',
),
)
.on('click', $.proxy(this, 'eventhandlerToggleColumns'));
this.$table.before($('<div class="tableresponsive-toggle-columns"></div>').append(this.$link));
this.$table.before(
$('<div class="tableresponsive-toggle-columns"></div>').append(
this.$link,
),
);
// Attach a resize handler to the window.
$(window)
.on('resize.tableresponsive', $.proxy(this, 'eventhandlerEvaluateColumnVisibility'))
.on(
'resize.tableresponsive',
$.proxy(this, 'eventhandlerEvaluateColumnVisibility'),
)
.trigger('resize.tableresponsive');
}
/**
* Attach the tableResponsive function to {@link Drupal.behaviors}.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches tableResponsive functionality.
*/
Drupal.behaviors.tableResponsive = {
attach(context, settings) {
const $tables = $(context)
.find('table.responsive-enabled')
.once('tableresponsive');
if ($tables.length) {
const il = $tables.length;
for (let i = 0; i < il; i++) {
TableResponsive.tables.push(new TableResponsive($tables[i]));
}
}
},
};
/**
* Extend the TableResponsive function with a list of managed tables.
*/
$.extend(TableResponsive, /** @lends Drupal.TableResponsive */{
/**
* Store all created instances.
*
* @type {Array.<Drupal.TableResponsive>}
*/
tables: [],
});
$.extend(
TableResponsive,
/** @lends Drupal.TableResponsive */ {
/**
* Store all created instances.
*
* @type {Array.<Drupal.TableResponsive>}
*/
tables: [],
},
);
/**
* Associates an action link with the table that will show hidden columns.
@@ -79,92 +97,104 @@
* Columns are assumed to be hidden if their header has the class priority-low
* or priority-medium.
*/
$.extend(TableResponsive.prototype, /** @lends Drupal.TableResponsive# */{
$.extend(
TableResponsive.prototype,
/** @lends Drupal.TableResponsive# */ {
/**
* @param {jQuery.Event} e
* The event triggered.
*/
eventhandlerEvaluateColumnVisibility(e) {
const pegged = parseInt(this.$link.data('pegged'), 10);
const hiddenLength = this.$headers.filter(
'.priority-medium:hidden, .priority-low:hidden',
).length;
// If the table has hidden columns, associate an action link with the
// table to show the columns.
if (hiddenLength > 0) {
this.$link.show().text(this.showText);
}
// When the toggle is pegged, its presence is maintained because the user
// has interacted with it. This is necessary to keep the link visible if
// the user adjusts screen size and changes the visibility of columns.
if (!pegged && hiddenLength === 0) {
this.$link.hide().text(this.hideText);
}
},
/**
* @param {jQuery.Event} e
* The event triggered.
*/
eventhandlerEvaluateColumnVisibility(e) {
const pegged = parseInt(this.$link.data('pegged'), 10);
const hiddenLength = this.$headers.filter('.priority-medium:hidden, .priority-low:hidden').length;
// If the table has hidden columns, associate an action link with the
// table to show the columns.
if (hiddenLength > 0) {
this.$link.show().text(this.showText);
}
// When the toggle is pegged, its presence is maintained because the user
// has interacted with it. This is necessary to keep the link visible if
// the user adjusts screen size and changes the visibility of columns.
if (!pegged && hiddenLength === 0) {
this.$link.hide().text(this.hideText);
}
},
/**
* Toggle the visibility of columns based on their priority.
*
* Columns are classed with either 'priority-low' or 'priority-medium'.
*
* @param {jQuery.Event} e
* The event triggered.
*/
eventhandlerToggleColumns(e) {
e.preventDefault();
const self = this;
const $hiddenHeaders = this.$headers.filter('.priority-medium:hidden, .priority-low:hidden');
this.$revealedCells = this.$revealedCells || $();
// Reveal hidden columns.
if ($hiddenHeaders.length > 0) {
$hiddenHeaders.each(function (index, element) {
const $header = $(this);
const position = $header.prevAll('th').length;
self.$table.find('tbody tr').each(function () {
const $cells = $(this).find('td').eq(position);
$cells.show();
// Keep track of the revealed cells, so they can be hidden later.
self.$revealedCells = $().add(self.$revealedCells).add($cells);
/**
* Toggle the visibility of columns based on their priority.
*
* Columns are classed with either 'priority-low' or 'priority-medium'.
*
* @param {jQuery.Event} e
* The event triggered.
*/
eventhandlerToggleColumns(e) {
e.preventDefault();
const self = this;
const $hiddenHeaders = this.$headers.filter(
'.priority-medium:hidden, .priority-low:hidden',
);
this.$revealedCells = this.$revealedCells || $();
// Reveal hidden columns.
if ($hiddenHeaders.length > 0) {
$hiddenHeaders.each(function(index, element) {
const $header = $(this);
const position = $header.prevAll('th').length;
self.$table.find('tbody tr').each(function() {
const $cells = $(this)
.find('td')
.eq(position);
$cells.show();
// Keep track of the revealed cells, so they can be hidden later.
self.$revealedCells = $()
.add(self.$revealedCells)
.add($cells);
});
$header.show();
// Keep track of the revealed headers, so they can be hidden later.
self.$revealedCells = $()
.add(self.$revealedCells)
.add($header);
});
$header.show();
// Keep track of the revealed headers, so they can be hidden later.
self.$revealedCells = $().add(self.$revealedCells).add($header);
});
this.$link.text(this.hideText).data('pegged', 1);
}
// Hide revealed columns.
else {
this.$revealedCells.hide();
// Strip the 'display:none' declaration from the style attributes of
// the table cells that .hide() added.
this.$revealedCells.each(function (index, element) {
const $cell = $(this);
const properties = $cell.attr('style').split(';');
const newProps = [];
// The hide method adds display none to the element. The element
// should be returned to the same state it was in before the columns
// were revealed, so it is necessary to remove the display none value
// from the style attribute.
const match = /^display\s*:\s*none$/;
for (let i = 0; i < properties.length; i++) {
const prop = properties[i];
prop.trim();
// Find the display:none property and remove it.
const isDisplayNone = match.exec(prop);
if (isDisplayNone) {
continue;
this.$link.text(this.hideText).data('pegged', 1);
}
// Hide revealed columns.
else {
this.$revealedCells.hide();
// Strip the 'display:none' declaration from the style attributes of
// the table cells that .hide() added.
this.$revealedCells.each(function(index, element) {
const $cell = $(this);
const properties = $cell.attr('style').split(';');
const newProps = [];
// The hide method adds display none to the element. The element
// should be returned to the same state it was in before the columns
// were revealed, so it is necessary to remove the display none value
// from the style attribute.
const match = /^display\s*:\s*none$/;
for (let i = 0; i < properties.length; i++) {
const prop = properties[i];
prop.trim();
// Find the display:none property and remove it.
const isDisplayNone = match.exec(prop);
if (isDisplayNone) {
continue;
}
newProps.push(prop);
}
newProps.push(prop);
}
// Return the rest of the style attribute values to the element.
$cell.attr('style', newProps.join(';'));
});
this.$link.text(this.showText).data('pegged', 0);
// Refresh the toggle link.
$(window).trigger('resize.tableresponsive');
}
// Return the rest of the style attribute values to the element.
$cell.attr('style', newProps.join(';'));
});
this.$link.text(this.showText).data('pegged', 0);
// Refresh the toggle link.
$(window).trigger('resize.tableresponsive');
}
},
},
});
);
// Make the TableResponsive object available in the Drupal namespace.
Drupal.TableResponsive = TableResponsive;
}(jQuery, Drupal, window));
})(jQuery, Drupal, window);
+12 -12
View File
@@ -6,18 +6,6 @@
**/
(function ($, Drupal, window) {
Drupal.behaviors.tableResponsive = {
attach: function attach(context, settings) {
var $tables = $(context).find('table.responsive-enabled').once('tableresponsive');
if ($tables.length) {
var il = $tables.length;
for (var i = 0; i < il; i++) {
TableResponsive.tables.push(new TableResponsive($tables[i]));
}
}
}
};
function TableResponsive(table) {
this.table = table;
this.$table = $(table);
@@ -33,6 +21,18 @@
$(window).on('resize.tableresponsive', $.proxy(this, 'eventhandlerEvaluateColumnVisibility')).trigger('resize.tableresponsive');
}
Drupal.behaviors.tableResponsive = {
attach: function attach(context, settings) {
var $tables = $(context).find('table.responsive-enabled').once('tableresponsive');
if ($tables.length) {
var il = $tables.length;
for (var i = 0; i < il; i++) {
TableResponsive.tables.push(new TableResponsive($tables[i]));
}
}
}
};
$.extend(TableResponsive, {
tables: []
});
+88 -61
View File
@@ -3,7 +3,7 @@
* Table select functionality.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Initialize tableSelects.
*
@@ -26,7 +26,7 @@
/**
* Callback used in {@link Drupal.behaviors.tableSelect}.
*/
Drupal.tableSelect = function () {
Drupal.tableSelect = function() {
// Do not add a "Select all" checkbox if there are no rows with checkboxes
// in the table.
if ($(this).find('td input[type="checkbox"]').length === 0) {
@@ -43,80 +43,106 @@
selectAll: Drupal.t('Select all rows in this table'),
selectNone: Drupal.t('Deselect all rows in this table'),
};
const updateSelectAll = function (state) {
const updateSelectAll = function(state) {
// Update table's select-all checkbox (and sticky header's if available).
$table.prev('table.sticky-header').addBack().find('th.select-all input[type="checkbox"]').each(function () {
const $checkbox = $(this);
const stateChanged = $checkbox.prop('checked') !== state;
$checkbox.attr('title', state ? strings.selectNone : strings.selectAll);
/**
* @checkbox {HTMLElement}
*/
if (stateChanged) {
$checkbox.prop('checked', state).trigger('change');
}
});
};
// Find all <th> with class select-all, and insert the check all checkbox.
$table.find('th.select-all').prepend($('<input type="checkbox" class="form-checkbox" />').attr('title', strings.selectAll)).on('click', (event) => {
if ($(event.target).is('input[type="checkbox"]')) {
// Loop through all checkboxes and set their state to the select all
// checkbox' state.
checkboxes.each(function () {
$table
.prev('table.sticky-header')
.addBack()
.find('th.select-all input[type="checkbox"]')
.each(function() {
const $checkbox = $(this);
const stateChanged = $checkbox.prop('checked') !== event.target.checked;
const stateChanged = $checkbox.prop('checked') !== state;
$checkbox.attr(
'title',
state ? strings.selectNone : strings.selectAll,
);
/**
* @checkbox {HTMLElement}
*/
if (stateChanged) {
$checkbox.prop('checked', event.target.checked).trigger('change');
$checkbox.prop('checked', state).trigger('change');
}
// Either add or remove the selected class based on the state of the
// check all checkbox.
/**
* @checkbox {HTMLElement}
*/
$checkbox.closest('tr').toggleClass('selected', this.checked);
});
// Update the title and the state of the check all box.
updateSelectAll(event.target.checked);
}
});
};
// Find all <th> with class select-all, and insert the check all checkbox.
$table
.find('th.select-all')
.prepend(
$('<input type="checkbox" class="form-checkbox" />').attr(
'title',
strings.selectAll,
),
)
.on('click', event => {
if ($(event.target).is('input[type="checkbox"]')) {
// Loop through all checkboxes and set their state to the select all
// checkbox' state.
checkboxes.each(function() {
const $checkbox = $(this);
const stateChanged =
$checkbox.prop('checked') !== event.target.checked;
/**
* @checkbox {HTMLElement}
*/
if (stateChanged) {
$checkbox.prop('checked', event.target.checked).trigger('change');
}
// Either add or remove the selected class based on the state of the
// check all checkbox.
/**
* @checkbox {HTMLElement}
*/
$checkbox.closest('tr').toggleClass('selected', this.checked);
});
// Update the title and the state of the check all box.
updateSelectAll(event.target.checked);
}
});
// For each of the checkboxes within the table that are not disabled.
checkboxes = $table.find('td input[type="checkbox"]:enabled').on('click', function (e) {
// Either add or remove the selected class based on the state of the
// check all checkbox.
checkboxes = $table
.find('td input[type="checkbox"]:enabled')
.on('click', function(e) {
// Either add or remove the selected class based on the state of the
// check all checkbox.
/**
* @this {HTMLElement}
*/
$(this).closest('tr').toggleClass('selected', this.checked);
/**
* @this {HTMLElement}
*/
$(this)
.closest('tr')
.toggleClass('selected', this.checked);
// If this is a shift click, we need to highlight everything in the
// range. Also make sure that we are actually checking checkboxes
// over a range and that a checkbox has been checked or unchecked before.
if (e.shiftKey && lastChecked && lastChecked !== e.target) {
// We use the checkbox's parent <tr> to do our range searching.
Drupal.tableSelectRange($(e.target).closest('tr')[0], $(lastChecked).closest('tr')[0], e.target.checked);
}
// If this is a shift click, we need to highlight everything in the
// range. Also make sure that we are actually checking checkboxes
// over a range and that a checkbox has been checked or unchecked before.
if (e.shiftKey && lastChecked && lastChecked !== e.target) {
// We use the checkbox's parent <tr> to do our range searching.
Drupal.tableSelectRange(
$(e.target).closest('tr')[0],
$(lastChecked).closest('tr')[0],
e.target.checked,
);
}
// If all checkboxes are checked, make sure the select-all one is checked
// too, otherwise keep unchecked.
updateSelectAll((checkboxes.length === checkboxes.filter(':checked').length));
// If all checkboxes are checked, make sure the select-all one is checked
// too, otherwise keep unchecked.
updateSelectAll(
checkboxes.length === checkboxes.filter(':checked').length,
);
// Keep track of the last checked checkbox.
lastChecked = e.target;
});
// Keep track of the last checked checkbox.
lastChecked = e.target;
});
// If all checkboxes are checked on page load, make sure the select-all one
// is checked too, otherwise keep unchecked.
updateSelectAll((checkboxes.length === checkboxes.filter(':checked').length));
updateSelectAll(checkboxes.length === checkboxes.filter(':checked').length);
};
/**
@@ -127,9 +153,10 @@
* @param {bool} state
* The state to set on the range.
*/
Drupal.tableSelectRange = function (from, to, state) {
Drupal.tableSelectRange = function(from, to, state) {
// We determine the looping mode based on the order of from and to.
const mode = from.rowIndex > to.rowIndex ? 'previousSibling' : 'nextSibling';
const mode =
from.rowIndex > to.rowIndex ? 'previousSibling' : 'nextSibling';
// Traverse through the sibling nodes.
for (let i = from[mode]; i; i = i[mode]) {
@@ -155,4 +182,4 @@
}
}
};
}(jQuery, Drupal));
})(jQuery, Drupal);
+5 -3
View File
@@ -3,7 +3,7 @@
* Timezone detection.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Set the client's system time zone as default values of form fields.
*
@@ -11,7 +11,9 @@
*/
Drupal.behaviors.setTimezone = {
attach(context, settings) {
const $timezone = $(context).find('.timezone-detect').once('timezone');
const $timezone = $(context)
.find('.timezone-detect')
.once('timezone');
if ($timezone.length) {
const dateString = Date();
// In some client environments, date strings include a time zone
@@ -69,4 +71,4 @@
}
},
};
}(jQuery, Drupal));
})(jQuery, Drupal);
+100 -67
View File
@@ -12,7 +12,7 @@
* @event summaryUpdated
*/
(function ($, Drupal, drupalSettings) {
(function($, Drupal, drupalSettings) {
/**
* Show the parent vertical tab pane of a targeted page fragment.
*
@@ -26,7 +26,9 @@
*/
const handleFragmentLinkClickOrHashChange = (e, $target) => {
$target.parents('.vertical-tabs__pane').each((index, pane) => {
$(pane).data('verticalTab').focus();
$(pane)
.data('verticalTab')
.focus();
});
};
@@ -56,62 +58,77 @@
/**
* Binds a listener to handle fragment link clicks and URL hash changes.
*/
$('body').once('vertical-tabs-fragments').on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange);
$('body')
.once('vertical-tabs-fragments')
.on(
'formFragmentLinkClickOrHashChange.verticalTabs',
handleFragmentLinkClickOrHashChange,
);
$(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function () {
const $this = $(this).addClass('vertical-tabs__panes');
const focusID = $this.find(':hidden.vertical-tabs__active-tab').val();
let tabFocus;
$(context)
.find('[data-vertical-tabs-panes]')
.once('vertical-tabs')
.each(function() {
const $this = $(this).addClass('vertical-tabs__panes');
const focusID = $this.find(':hidden.vertical-tabs__active-tab').val();
let tabFocus;
// Check if there are some details that can be converted to
// vertical-tabs.
const $details = $this.find('> details');
if ($details.length === 0) {
return;
}
// Check if there are some details that can be converted to
// vertical-tabs.
const $details = $this.find('> details');
if ($details.length === 0) {
return;
}
// Create the tab column.
const tabList = $('<ul class="vertical-tabs__menu"></ul>');
$this.wrap('<div class="vertical-tabs clearfix"></div>').before(tabList);
// Create the tab column.
const tabList = $('<ul class="vertical-tabs__menu"></ul>');
$this
.wrap('<div class="vertical-tabs clearfix"></div>')
.before(tabList);
// Transform each details into a tab.
$details.each(function () {
const $that = $(this);
const verticalTab = new Drupal.verticalTab({
title: $that.find('> summary').text(),
details: $that,
// Transform each details into a tab.
$details.each(function() {
const $that = $(this);
const verticalTab = new Drupal.verticalTab({
title: $that.find('> summary').text(),
details: $that,
});
tabList.append(verticalTab.item);
$that
.removeClass('collapsed')
// prop() can't be used on browsers not supporting details element,
// the style won't apply to them if prop() is used.
.attr('open', true)
.addClass('vertical-tabs__pane')
.data('verticalTab', verticalTab);
if (this.id === focusID) {
tabFocus = $that;
}
});
tabList.append(verticalTab.item);
$that
.removeClass('collapsed')
// prop() can't be used on browsers not supporting details element,
// the style won't apply to them if prop() is used.
.attr('open', true)
.addClass('vertical-tabs__pane')
.data('verticalTab', verticalTab);
if (this.id === focusID) {
tabFocus = $that;
$(tabList)
.find('> li')
.eq(0)
.addClass('first');
$(tabList)
.find('> li')
.eq(-1)
.addClass('last');
if (!tabFocus) {
// If the current URL has a fragment and one of the tabs contains an
// element that matches the URL fragment, activate that tab.
const $locationHash = $this.find(window.location.hash);
if (window.location.hash && $locationHash.length) {
tabFocus = $locationHash.closest('.vertical-tabs__pane');
} else {
tabFocus = $this.find('> .vertical-tabs__pane').eq(0);
}
}
if (tabFocus.length) {
tabFocus.data('verticalTab').focus();
}
});
$(tabList).find('> li').eq(0).addClass('first');
$(tabList).find('> li').eq(-1).addClass('last');
if (!tabFocus) {
// If the current URL has a fragment and one of the tabs contains an
// element that matches the URL fragment, activate that tab.
const $locationHash = $this.find(window.location.hash);
if (window.location.hash && $locationHash.length) {
tabFocus = $locationHash.closest('.vertical-tabs__pane');
}
else {
tabFocus = $this.find('> .vertical-tabs__pane').eq(0);
}
}
if (tabFocus.length) {
tabFocus.data('verticalTab').focus();
}
});
},
};
@@ -131,25 +148,27 @@
*
* @listens event:summaryUpdated
*/
Drupal.verticalTab = function (settings) {
Drupal.verticalTab = function(settings) {
const self = this;
$.extend(this, settings, Drupal.theme('verticalTab', settings));
this.link.attr('href', `#${settings.details.attr('id')}`);
this.link.on('click', (e) => {
this.link.on('click', e => {
e.preventDefault();
self.focus();
});
// Keyboard events added:
// Pressing the Enter key will open the tab pane.
this.link.on('keydown', (event) => {
this.link.on('keydown', event => {
if (event.keyCode === 13) {
event.preventDefault();
self.focus();
// Set focus on the first input field of the visible details/tab pane.
$('.vertical-tabs__pane :input:visible:enabled').eq(0).trigger('focus');
$('.vertical-tabs__pane :input:visible:enabled')
.eq(0)
.trigger('focus');
}
});
@@ -161,14 +180,13 @@
};
Drupal.verticalTab.prototype = {
/**
* Displays the tab's content pane.
*/
focus() {
this.details
.siblings('.vertical-tabs__pane')
.each(function () {
.each(function() {
const tab = $(this).data('verticalTab');
tab.details.hide();
tab.item.removeClass('is-selected');
@@ -180,7 +198,11 @@
this.item.addClass('is-selected');
// Mark the active tab for screen readers.
$('#active-vertical-tab').remove();
this.link.append(`<span id="active-vertical-tab" class="visually-hidden">${Drupal.t('(active tab)')}</span>`);
this.link.append(
`<span id="active-vertical-tab" class="visually-hidden">${Drupal.t(
'(active tab)',
)}</span>`,
);
},
/**
@@ -240,7 +262,9 @@
// Hide the details element.
this.details.addClass('vertical-tab--hidden').hide();
// Focus the first visible tab (if there is one).
const $firstTab = this.details.siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)').eq(0);
const $firstTab = this.details
.siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)')
.eq(0);
if ($firstTab.length) {
$firstTab.data('verticalTab').focus();
}
@@ -267,14 +291,23 @@
* (jQuery version)
* - summary: The jQuery element that contains the tab summary
*/
Drupal.theme.verticalTab = function (settings) {
Drupal.theme.verticalTab = function(settings) {
const tab = {};
tab.item = $('<li class="vertical-tabs__menu-item" tabindex="-1"></li>')
.append(tab.link = $('<a href="#"></a>')
.append(tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>').text(settings.title))
.append(tab.summary = $('<span class="vertical-tabs__menu-item-summary"></span>'),
),
);
tab.item = $(
'<li class="vertical-tabs__menu-item" tabindex="-1"></li>',
).append(
(tab.link = $('<a href="#"></a>')
.append(
(tab.title = $(
'<strong class="vertical-tabs__menu-item-title"></strong>',
).text(settings.title)),
)
.append(
(tab.summary = $(
'<span class="vertical-tabs__menu-item-summary"></span>',
)),
)),
);
return tab;
};
}(jQuery, Drupal, drupalSettings));
})(jQuery, Drupal, drupalSettings);