upgrades core to 8.4.2
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @file
|
||||
* Attaches behaviors for Drupal's active link marking.
|
||||
*/
|
||||
|
||||
(function (Drupal, drupalSettings) {
|
||||
/**
|
||||
* Append is-active class.
|
||||
*
|
||||
* The link is only active if its path corresponds to the current path, the
|
||||
* language of the linked path is equal to the current language, and if the
|
||||
* query parameters of the link equal those of the current request, since the
|
||||
* same request with different query parameters may yield a different page
|
||||
* (e.g. pagers, exposed View filters).
|
||||
*
|
||||
* Does not discriminate based on element type, so allows you to set the
|
||||
* is-active class on any element: a, li…
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.activeLinks = {
|
||||
attach(context) {
|
||||
// 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}"]`];
|
||||
let selectors;
|
||||
|
||||
// If this is the front page, we have to check for the <front> path as
|
||||
// well.
|
||||
if (path.isFront) {
|
||||
originalSelectors.push('[data-drupal-link-system-path="<front>"]');
|
||||
}
|
||||
|
||||
// Add language filtering.
|
||||
selectors = [].concat(
|
||||
// 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}"]`),
|
||||
);
|
||||
|
||||
// Add query string selector for pagers, exposed filters.
|
||||
selectors = selectors.map(current => current + querySelector);
|
||||
|
||||
// Query the DOM.
|
||||
const activeLinks = context.querySelectorAll(selectors.join(','));
|
||||
const il = activeLinks.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
activeLinks[i].classList.add('is-active');
|
||||
}
|
||||
},
|
||||
detach(context, settings, trigger) {
|
||||
if (trigger === 'unload') {
|
||||
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');
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}(Drupal, drupalSettings));
|
||||
+18
-39
@@ -1,60 +1,40 @@
|
||||
/**
|
||||
* @file
|
||||
* Attaches behaviors for Drupal's active link marking.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (Drupal, drupalSettings) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Append is-active class.
|
||||
*
|
||||
* The link is only active if its path corresponds to the current path, the
|
||||
* language of the linked path is equal to the current language, and if the
|
||||
* query parameters of the link equal those of the current request, since the
|
||||
* same request with different query parameters may yield a different page
|
||||
* (e.g. pagers, exposed View filters).
|
||||
*
|
||||
* Does not discriminate based on element type, so allows you to set the
|
||||
* is-active class on any element: a, li…
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.activeLinks = {
|
||||
attach: function (context) {
|
||||
// Start by finding all potentially active links.
|
||||
attach: function attach(context) {
|
||||
var path = drupalSettings.path;
|
||||
var queryString = JSON.stringify(path.currentQuery);
|
||||
var querySelector = path.currentQuery ? "[data-drupal-link-query='" + queryString + "']" : ':not([data-drupal-link-query])';
|
||||
var querySelector = path.currentQuery ? '[data-drupal-link-query=\'' + queryString + '\']' : ':not([data-drupal-link-query])';
|
||||
var originalSelectors = ['[data-drupal-link-system-path="' + path.currentPath + '"]'];
|
||||
var selectors;
|
||||
var selectors = void 0;
|
||||
|
||||
// If this is the front page, we have to check for the <front> path as
|
||||
// well.
|
||||
if (path.isFront) {
|
||||
originalSelectors.push('[data-drupal-link-system-path="<front>"]');
|
||||
}
|
||||
|
||||
// Add language filtering.
|
||||
selectors = [].concat(
|
||||
// Links without any hreflang attributes (most of them).
|
||||
originalSelectors.map(function (selector) { return selector + ':not([hreflang])'; }),
|
||||
// Links with hreflang equals to the current language.
|
||||
originalSelectors.map(function (selector) { return selector + '[hreflang="' + path.currentLanguage + '"]'; })
|
||||
);
|
||||
selectors = [].concat(originalSelectors.map(function (selector) {
|
||||
return selector + ':not([hreflang])';
|
||||
}), originalSelectors.map(function (selector) {
|
||||
return selector + '[hreflang="' + path.currentLanguage + '"]';
|
||||
}));
|
||||
|
||||
// Add query string selector for pagers, exposed filters.
|
||||
selectors = selectors.map(function (current) { return current + querySelector; });
|
||||
selectors = selectors.map(function (current) {
|
||||
return current + querySelector;
|
||||
});
|
||||
|
||||
// Query the DOM.
|
||||
var activeLinks = context.querySelectorAll(selectors.join(','));
|
||||
var il = activeLinks.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
activeLinks[i].classList.add('is-active');
|
||||
}
|
||||
},
|
||||
detach: function (context, settings, trigger) {
|
||||
detach: function detach(context, settings, trigger) {
|
||||
if (trigger === 'unload') {
|
||||
var activeLinks = context.querySelectorAll('[data-drupal-link-system-path].is-active');
|
||||
var il = activeLinks.length;
|
||||
@@ -64,5 +44,4 @@
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})(Drupal, drupalSettings);
|
||||
})(Drupal, drupalSettings);
|
||||
File diff suppressed because it is too large
Load Diff
+83
-836
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @file
|
||||
* Adds an HTML element and method to trigger audio UAs to read system messages.
|
||||
*
|
||||
* Use {@link Drupal.announce} to indicate to screen reader users that an
|
||||
* element on the page has changed state. For instance, if clicking a link
|
||||
* loads 10 more items into a list, one might announce the change like this.
|
||||
*
|
||||
* @example
|
||||
* $('#search-list')
|
||||
* .on('itemInsert', function (event, data) {
|
||||
* // Insert the new items.
|
||||
* $(data.container.el).append(data.items.el);
|
||||
* // Announce the change to the page contents.
|
||||
* Drupal.announce(Drupal.t('@count items added to @container',
|
||||
* {'@count': data.items.length, '@container': data.container.title}
|
||||
* ));
|
||||
* });
|
||||
*/
|
||||
|
||||
(function (Drupal, debounce) {
|
||||
let liveElement;
|
||||
const announcements = [];
|
||||
|
||||
/**
|
||||
* Builds a div element with the aria-live attribute and add it to the DOM.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the behavior for drupalAnnouce.
|
||||
*/
|
||||
Drupal.behaviors.drupalAnnounce = {
|
||||
attach(context) {
|
||||
// Create only one aria-live element.
|
||||
if (!liveElement) {
|
||||
liveElement = document.createElement('div');
|
||||
liveElement.id = 'drupal-live-announce';
|
||||
liveElement.className = 'visually-hidden';
|
||||
liveElement.setAttribute('aria-live', 'polite');
|
||||
liveElement.setAttribute('aria-busy', 'false');
|
||||
document.body.appendChild(liveElement);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Concatenates announcements to a single string; appends to the live region.
|
||||
*/
|
||||
function announce() {
|
||||
const text = [];
|
||||
let priority = 'polite';
|
||||
let announcement;
|
||||
|
||||
// Create an array of announcement strings to be joined and appended to the
|
||||
// aria live region.
|
||||
const il = announcements.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
announcement = announcements.pop();
|
||||
text.unshift(announcement.text);
|
||||
// If any of the announcements has a priority of assertive then the group
|
||||
// of joined announcements will have this priority.
|
||||
if (announcement.priority === 'assertive') {
|
||||
priority = 'assertive';
|
||||
}
|
||||
}
|
||||
|
||||
if (text.length) {
|
||||
// Clear the liveElement so that repeated strings will be read.
|
||||
liveElement.innerHTML = '';
|
||||
// Set the busy state to true until the node changes are complete.
|
||||
liveElement.setAttribute('aria-busy', 'true');
|
||||
// Set the priority to assertive, or default to polite.
|
||||
liveElement.setAttribute('aria-live', priority);
|
||||
// Print the text to the live region. Text should be run through
|
||||
// Drupal.t() before being passed to Drupal.announce().
|
||||
liveElement.innerHTML = text.join('\n');
|
||||
// The live text area is updated. Allow the AT to announce the text.
|
||||
liveElement.setAttribute('aria-busy', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers audio UAs to read the supplied text.
|
||||
*
|
||||
* The aria-live region will only read the text that currently populates its
|
||||
* text node. Replacing text quickly in rapid calls to announce results in
|
||||
* only the text from the most recent call to {@link Drupal.announce} being
|
||||
* read. By wrapping the call to announce in a debounce function, we allow for
|
||||
* time for multiple calls to {@link Drupal.announce} to queue up their
|
||||
* messages. These messages are then joined and append to the aria-live region
|
||||
* as one text node.
|
||||
*
|
||||
* @param {string} text
|
||||
* A string to be read by the UA.
|
||||
* @param {string} [priority='polite']
|
||||
* A string to indicate the priority of the message. Can be either
|
||||
* 'polite' or 'assertive'.
|
||||
*
|
||||
* @return {function}
|
||||
* The return of the call to debounce.
|
||||
*
|
||||
* @see http://www.w3.org/WAI/PF/aria-practices/#liveprops
|
||||
*/
|
||||
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({
|
||||
text,
|
||||
priority,
|
||||
});
|
||||
// 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)());
|
||||
};
|
||||
}(Drupal, Drupal.debounce));
|
||||
+16
-75
@@ -1,41 +1,16 @@
|
||||
/**
|
||||
* @file
|
||||
* Adds an HTML element and method to trigger audio UAs to read system messages.
|
||||
*
|
||||
* Use {@link Drupal.announce} to indicate to screen reader users that an
|
||||
* element on the page has changed state. For instance, if clicking a link
|
||||
* loads 10 more items into a list, one might announce the change like this.
|
||||
*
|
||||
* @example
|
||||
* $('#search-list')
|
||||
* .on('itemInsert', function (event, data) {
|
||||
* // Insert the new items.
|
||||
* $(data.container.el).append(data.items.el);
|
||||
* // Announce the change to the page contents.
|
||||
* Drupal.announce(Drupal.t('@count items added to @container',
|
||||
* {'@count': data.items.length, '@container': data.container.title}
|
||||
* ));
|
||||
* });
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (Drupal, debounce) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var liveElement;
|
||||
var liveElement = void 0;
|
||||
var announcements = [];
|
||||
|
||||
/**
|
||||
* Builds a div element with the aria-live attribute and add it to the DOM.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the behavior for drupalAnnouce.
|
||||
*/
|
||||
Drupal.behaviors.drupalAnnounce = {
|
||||
attach: function (context) {
|
||||
// Create only one aria-live element.
|
||||
attach: function attach(context) {
|
||||
if (!liveElement) {
|
||||
liveElement = document.createElement('div');
|
||||
liveElement.id = 'drupal-live-announce';
|
||||
@@ -47,74 +22,40 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Concatenates announcements to a single string; appends to the live region.
|
||||
*/
|
||||
function announce() {
|
||||
var text = [];
|
||||
var priority = 'polite';
|
||||
var announcement;
|
||||
var announcement = void 0;
|
||||
|
||||
// Create an array of announcement strings to be joined and appended to the
|
||||
// aria live region.
|
||||
var il = announcements.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
announcement = announcements.pop();
|
||||
text.unshift(announcement.text);
|
||||
// If any of the announcements has a priority of assertive then the group
|
||||
// of joined announcements will have this priority.
|
||||
|
||||
if (announcement.priority === 'assertive') {
|
||||
priority = 'assertive';
|
||||
}
|
||||
}
|
||||
|
||||
if (text.length) {
|
||||
// Clear the liveElement so that repeated strings will be read.
|
||||
liveElement.innerHTML = '';
|
||||
// Set the busy state to true until the node changes are complete.
|
||||
|
||||
liveElement.setAttribute('aria-busy', 'true');
|
||||
// Set the priority to assertive, or default to polite.
|
||||
|
||||
liveElement.setAttribute('aria-live', priority);
|
||||
// Print the text to the live region. Text should be run through
|
||||
// Drupal.t() before being passed to Drupal.announce().
|
||||
|
||||
liveElement.innerHTML = text.join('\n');
|
||||
// The live text area is updated. Allow the AT to announce the text.
|
||||
|
||||
liveElement.setAttribute('aria-busy', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers audio UAs to read the supplied text.
|
||||
*
|
||||
* The aria-live region will only read the text that currently populates its
|
||||
* text node. Replacing text quickly in rapid calls to announce results in
|
||||
* only the text from the most recent call to {@link Drupal.announce} being
|
||||
* read. By wrapping the call to announce in a debounce function, we allow for
|
||||
* time for multiple calls to {@link Drupal.announce} to queue up their
|
||||
* messages. These messages are then joined and append to the aria-live region
|
||||
* as one text node.
|
||||
*
|
||||
* @param {string} text
|
||||
* A string to be read by the UA.
|
||||
* @param {string} [priority='polite']
|
||||
* A string to indicate the priority of the message. Can be either
|
||||
* 'polite' or 'assertive'.
|
||||
*
|
||||
* @return {function}
|
||||
* The return of the call to debounce.
|
||||
*
|
||||
* @see http://www.w3.org/WAI/PF/aria-practices/#liveprops
|
||||
*/
|
||||
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({
|
||||
text: text,
|
||||
priority: priority
|
||||
});
|
||||
// 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);
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* @file
|
||||
* Autocomplete based on jQuery UI.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
let autocomplete;
|
||||
|
||||
/**
|
||||
* Helper splitting terms from the autocomplete value.
|
||||
*
|
||||
* @function Drupal.autocomplete.splitValues
|
||||
*
|
||||
* @param {string} value
|
||||
* The value being entered by the user.
|
||||
*
|
||||
* @return {Array}
|
||||
* Array of values, split by comma.
|
||||
*/
|
||||
function autocompleteSplitValues(value) {
|
||||
// We will match the value against comma-separated terms.
|
||||
const result = [];
|
||||
let quote = false;
|
||||
let current = '';
|
||||
const valueLength = value.length;
|
||||
let character;
|
||||
|
||||
for (let i = 0; i < valueLength; i++) {
|
||||
character = value.charAt(i);
|
||||
if (character === '"') {
|
||||
current += character;
|
||||
quote = !quote;
|
||||
}
|
||||
else if (character === ',' && !quote) {
|
||||
result.push(current.trim());
|
||||
current = '';
|
||||
}
|
||||
else {
|
||||
current += character;
|
||||
}
|
||||
}
|
||||
if (value.length > 0) {
|
||||
result.push($.trim(current));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last value of an multi-value textfield.
|
||||
*
|
||||
* @function Drupal.autocomplete.extractLastTerm
|
||||
*
|
||||
* @param {string} terms
|
||||
* The value of the field.
|
||||
*
|
||||
* @return {string}
|
||||
* The last value of the input field.
|
||||
*/
|
||||
function extractLastTerm(terms) {
|
||||
return autocomplete.splitValues(terms).pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* The search handler is called before a search is performed.
|
||||
*
|
||||
* @function Drupal.autocomplete.options.search
|
||||
*
|
||||
* @param {object} event
|
||||
* The event triggered.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether to perform a search or not.
|
||||
*/
|
||||
function searchHandler(event) {
|
||||
const options = autocomplete.options;
|
||||
|
||||
if (options.isComposing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
// Only search when the term is at least the minimum length.
|
||||
return term.length >= options.minLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* JQuery UI autocomplete source callback.
|
||||
*
|
||||
* @param {object} request
|
||||
* The request object.
|
||||
* @param {function} response
|
||||
* The function to call with the response.
|
||||
*/
|
||||
function sourceData(request, response) {
|
||||
const elementId = this.element.attr('id');
|
||||
|
||||
if (!(elementId in autocomplete.cache)) {
|
||||
autocomplete.cache[elementId] = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter through the suggestions removing all terms already tagged and
|
||||
* display the available terms to the user.
|
||||
*
|
||||
* @param {object} suggestions
|
||||
* Suggestions returned by the server.
|
||||
*/
|
||||
function showSuggestions(suggestions) {
|
||||
const tagged = autocomplete.splitValues(request.term);
|
||||
const il = tagged.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
const index = suggestions.indexOf(tagged[i]);
|
||||
if (index >= 0) {
|
||||
suggestions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
response(suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the data object into an array and update autocomplete results.
|
||||
*
|
||||
* @param {object} data
|
||||
* The data sent back from the server.
|
||||
*/
|
||||
function sourceCallbackHandler(data) {
|
||||
autocomplete.cache[elementId][term] = data;
|
||||
|
||||
// Send the new string array of terms to the jQuery UI list.
|
||||
showSuggestions(data);
|
||||
}
|
||||
|
||||
// Get the desired term and construct the autocomplete URL for it.
|
||||
var 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);
|
||||
$.ajax(this.element.attr('data-autocomplete-path'), options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an autocompletefocus event.
|
||||
*
|
||||
* @return {bool}
|
||||
* Always returns false.
|
||||
*/
|
||||
function focusHandler() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an autocompleteselect event.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered.
|
||||
* @param {object} ui
|
||||
* The jQuery UI settings object.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns false to indicate the event status.
|
||||
*/
|
||||
function selectHandler(event, ui) {
|
||||
const terms = autocomplete.splitValues(event.target.value);
|
||||
// Remove the current input.
|
||||
terms.pop();
|
||||
// Add the selected item.
|
||||
terms.push(ui.item.value);
|
||||
|
||||
event.target.value = terms.join(', ');
|
||||
// Return false to tell jQuery UI that we've filled in the value already.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override jQuery UI _renderItem function to output HTML by default.
|
||||
*
|
||||
* @param {jQuery} ul
|
||||
* jQuery collection of the ul element.
|
||||
* @param {object} item
|
||||
* The list item to append.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* jQuery collection of the ul element.
|
||||
*/
|
||||
function renderItem(ul, item) {
|
||||
return $('<li>')
|
||||
.append($('<a>').html(item.label))
|
||||
.appendTo(ul);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the autocomplete behavior to all required fields.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the autocomplete behaviors.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detaches the autocomplete behaviors.
|
||||
*/
|
||||
Drupal.behaviors.autocomplete = {
|
||||
attach(context) {
|
||||
// Act on textfields with the "form-autocomplete" class.
|
||||
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');
|
||||
$.extend(autocomplete.options, {
|
||||
firstCharacterBlacklist: (blacklist) || '',
|
||||
});
|
||||
// Use jQuery UI Autocomplete on the textfield.
|
||||
$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', () => {
|
||||
autocomplete.options.isComposing = true;
|
||||
});
|
||||
$autocomplete.on('compositionend.autocomplete', () => {
|
||||
autocomplete.options.isComposing = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
detach(context, settings, trigger) {
|
||||
if (trigger === 'unload') {
|
||||
$(context).find('input.form-autocomplete')
|
||||
.removeOnce('autocomplete')
|
||||
.autocomplete('destroy');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Autocomplete object implementation.
|
||||
*
|
||||
* @namespace Drupal.autocomplete
|
||||
*/
|
||||
autocomplete = {
|
||||
cache: {},
|
||||
// Exposes options to allow overriding by contrib.
|
||||
splitValues: autocompleteSplitValues,
|
||||
extractLastTerm,
|
||||
// jQuery UI autocomplete options.
|
||||
|
||||
/**
|
||||
* JQuery UI option object.
|
||||
*
|
||||
* @name Drupal.autocomplete.options
|
||||
*/
|
||||
options: {
|
||||
source: sourceData,
|
||||
focus: focusHandler,
|
||||
search: searchHandler,
|
||||
select: selectHandler,
|
||||
renderItem,
|
||||
minLength: 1,
|
||||
// Custom options, used by Drupal.autocomplete.
|
||||
firstCharacterBlacklist: '',
|
||||
// Custom options, indicate IME usage status.
|
||||
isComposing: false,
|
||||
},
|
||||
ajax: {
|
||||
dataType: 'json',
|
||||
},
|
||||
};
|
||||
|
||||
Drupal.autocomplete = autocomplete;
|
||||
}(jQuery, Drupal));
|
||||
+29
-149
@@ -1,44 +1,29 @@
|
||||
/**
|
||||
* @file
|
||||
* Autocomplete based on jQuery UI.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
var autocomplete = void 0;
|
||||
|
||||
'use strict';
|
||||
|
||||
var autocomplete;
|
||||
|
||||
/**
|
||||
* Helper splitting terms from the autocomplete value.
|
||||
*
|
||||
* @function Drupal.autocomplete.splitValues
|
||||
*
|
||||
* @param {string} value
|
||||
* The value being entered by the user.
|
||||
*
|
||||
* @return {Array}
|
||||
* Array of values, split by comma.
|
||||
*/
|
||||
function autocompleteSplitValues(value) {
|
||||
// We will match the value against comma-separated terms.
|
||||
var result = [];
|
||||
var quote = false;
|
||||
var current = '';
|
||||
var valueLength = value.length;
|
||||
var character;
|
||||
var character = void 0;
|
||||
|
||||
for (var i = 0; i < valueLength; i++) {
|
||||
character = value.charAt(i);
|
||||
if (character === '"') {
|
||||
current += character;
|
||||
quote = !quote;
|
||||
}
|
||||
else if (character === ',' && !quote) {
|
||||
} else if (character === ',' && !quote) {
|
||||
result.push(current.trim());
|
||||
current = '';
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
current += character;
|
||||
}
|
||||
}
|
||||
@@ -49,32 +34,10 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last value of an multi-value textfield.
|
||||
*
|
||||
* @function Drupal.autocomplete.extractLastTerm
|
||||
*
|
||||
* @param {string} terms
|
||||
* The value of the field.
|
||||
*
|
||||
* @return {string}
|
||||
* The last value of the input field.
|
||||
*/
|
||||
function extractLastTerm(terms) {
|
||||
return autocomplete.splitValues(terms).pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* The search handler is called before a search is performed.
|
||||
*
|
||||
* @function Drupal.autocomplete.options.search
|
||||
*
|
||||
* @param {object} event
|
||||
* The event triggered.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether to perform a search or not.
|
||||
*/
|
||||
function searchHandler(event) {
|
||||
var options = autocomplete.options;
|
||||
|
||||
@@ -83,22 +46,14 @@
|
||||
}
|
||||
|
||||
var 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) {
|
||||
return false;
|
||||
}
|
||||
// Only search when the term is at least the minimum length.
|
||||
|
||||
return term.length >= options.minLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* JQuery UI autocomplete source callback.
|
||||
*
|
||||
* @param {object} request
|
||||
* The request object.
|
||||
* @param {function} response
|
||||
* The function to call with the response.
|
||||
*/
|
||||
function sourceData(request, response) {
|
||||
var elementId = this.element.attr('id');
|
||||
|
||||
@@ -106,13 +61,6 @@
|
||||
autocomplete.cache[elementId] = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter through the suggestions removing all terms already tagged and
|
||||
* display the available terms to the user.
|
||||
*
|
||||
* @param {object} suggestions
|
||||
* Suggestions returned by the server.
|
||||
*/
|
||||
function showSuggestions(suggestions) {
|
||||
var tagged = autocomplete.splitValues(request.term);
|
||||
var il = tagged.length;
|
||||
@@ -125,109 +73,55 @@
|
||||
response(suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the data object into an array and update autocomplete results.
|
||||
*
|
||||
* @param {object} data
|
||||
* The data sent back from the server.
|
||||
*/
|
||||
function sourceCallbackHandler(data) {
|
||||
autocomplete.cache[elementId][term] = data;
|
||||
|
||||
// Send the new string array of terms to the jQuery UI list.
|
||||
showSuggestions(data);
|
||||
}
|
||||
|
||||
// Get the desired term and construct the autocomplete URL for it.
|
||||
var term = autocomplete.extractLastTerm(request.term);
|
||||
|
||||
// Check if the term is already cached.
|
||||
if (autocomplete.cache[elementId].hasOwnProperty(term)) {
|
||||
showSuggestions(autocomplete.cache[elementId][term]);
|
||||
}
|
||||
else {
|
||||
var options = $.extend({success: sourceCallbackHandler, data: {q: term}}, autocomplete.ajax);
|
||||
} else {
|
||||
var options = $.extend({ success: sourceCallbackHandler, data: { q: term } }, autocomplete.ajax);
|
||||
$.ajax(this.element.attr('data-autocomplete-path'), options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an autocompletefocus event.
|
||||
*
|
||||
* @return {bool}
|
||||
* Always returns false.
|
||||
*/
|
||||
function focusHandler() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an autocompleteselect event.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered.
|
||||
* @param {object} ui
|
||||
* The jQuery UI settings object.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns false to indicate the event status.
|
||||
*/
|
||||
function selectHandler(event, ui) {
|
||||
var terms = autocomplete.splitValues(event.target.value);
|
||||
// Remove the current input.
|
||||
|
||||
terms.pop();
|
||||
// Add the selected item.
|
||||
|
||||
terms.push(ui.item.value);
|
||||
|
||||
event.target.value = terms.join(', ');
|
||||
// Return false to tell jQuery UI that we've filled in the value already.
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override jQuery UI _renderItem function to output HTML by default.
|
||||
*
|
||||
* @param {jQuery} ul
|
||||
* jQuery collection of the ul element.
|
||||
* @param {object} item
|
||||
* The list item to append.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* jQuery collection of the ul element.
|
||||
*/
|
||||
function renderItem(ul, item) {
|
||||
return $('<li>')
|
||||
.append($('<a>').html(item.label))
|
||||
.appendTo(ul);
|
||||
return $('<li>').append($('<a>').html(item.label)).appendTo(ul);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the autocomplete behavior to all required fields.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the autocomplete behaviors.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detaches the autocomplete behaviors.
|
||||
*/
|
||||
Drupal.behaviors.autocomplete = {
|
||||
attach: function (context) {
|
||||
// Act on textfields with the "form-autocomplete" class.
|
||||
attach: function attach(context) {
|
||||
var $autocomplete = $(context).find('input.form-autocomplete').once('autocomplete');
|
||||
if ($autocomplete.length) {
|
||||
// Allow options to be overriden per instance.
|
||||
var blacklist = $autocomplete.attr('data-autocomplete-first-character-blacklist');
|
||||
$.extend(autocomplete.options, {
|
||||
firstCharacterBlacklist: (blacklist) ? blacklist : ''
|
||||
firstCharacterBlacklist: blacklist || ''
|
||||
});
|
||||
|
||||
$autocomplete.autocomplete(autocomplete.options).each(function () {
|
||||
$(this).data('ui-autocomplete')._renderItem = autocomplete.options.renderItem;
|
||||
});
|
||||
// Use jQuery UI Autocomplete on the textfield.
|
||||
$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', function () {
|
||||
autocomplete.options.isComposing = true;
|
||||
});
|
||||
@@ -236,32 +130,19 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
detach: function (context, settings, trigger) {
|
||||
detach: function detach(context, settings, trigger) {
|
||||
if (trigger === 'unload') {
|
||||
$(context).find('input.form-autocomplete')
|
||||
.removeOnce('autocomplete')
|
||||
.autocomplete('destroy');
|
||||
$(context).find('input.form-autocomplete').removeOnce('autocomplete').autocomplete('destroy');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Autocomplete object implementation.
|
||||
*
|
||||
* @namespace Drupal.autocomplete
|
||||
*/
|
||||
autocomplete = {
|
||||
cache: {},
|
||||
// Exposes options to allow overriding by contrib.
|
||||
|
||||
splitValues: autocompleteSplitValues,
|
||||
extractLastTerm: extractLastTerm,
|
||||
// jQuery UI autocomplete options.
|
||||
|
||||
/**
|
||||
* JQuery UI option object.
|
||||
*
|
||||
* @name Drupal.autocomplete.options
|
||||
*/
|
||||
options: {
|
||||
source: sourceData,
|
||||
focus: focusHandler,
|
||||
@@ -269,9 +150,9 @@
|
||||
select: selectHandler,
|
||||
renderItem: renderItem,
|
||||
minLength: 1,
|
||||
// Custom options, used by Drupal.autocomplete.
|
||||
|
||||
firstCharacterBlacklist: '',
|
||||
// Custom options, indicate IME usage status.
|
||||
|
||||
isComposing: false
|
||||
},
|
||||
ajax: {
|
||||
@@ -280,5 +161,4 @@
|
||||
};
|
||||
|
||||
Drupal.autocomplete = autocomplete;
|
||||
|
||||
})(jQuery, Drupal);
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @file
|
||||
* Drupal's batch API.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Attaches the batch behavior to progress bars.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.batch = {
|
||||
attach(context, settings) {
|
||||
const batch = settings.batch;
|
||||
const $progress = $('[data-drupal-progress]').once('batch');
|
||||
let progressBar;
|
||||
|
||||
// Success: redirect to the summary.
|
||||
function updateCallback(progress, status, pb) {
|
||||
if (progress === '100') {
|
||||
pb.stopMonitoring();
|
||||
window.location = `${batch.uri}&op=finished`;
|
||||
}
|
||||
}
|
||||
|
||||
function errorCallback(pb) {
|
||||
$progress.prepend($('<p class="error"></p>').html(batch.errorMessage));
|
||||
$('#wait').hide();
|
||||
}
|
||||
|
||||
if ($progress.length) {
|
||||
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.
|
||||
$progress.empty();
|
||||
// Append the JS progressbar element.
|
||||
$progress.append(progressBar.element);
|
||||
}
|
||||
},
|
||||
};
|
||||
}(jQuery, Drupal));
|
||||
+10
-18
@@ -1,24 +1,17 @@
|
||||
/**
|
||||
* @file
|
||||
* Drupal's batch API.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Attaches the batch behavior to progress bars.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.batch = {
|
||||
attach: function (context, settings) {
|
||||
attach: function attach(context, settings) {
|
||||
var batch = settings.batch;
|
||||
var $progress = $('[data-drupal-progress]').once('batch');
|
||||
var progressBar;
|
||||
var progressBar = void 0;
|
||||
|
||||
// Success: redirect to the summary.
|
||||
function updateCallback(progress, status, pb) {
|
||||
if (progress === '100') {
|
||||
pb.stopMonitoring();
|
||||
@@ -35,12 +28,11 @@
|
||||
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.
|
||||
|
||||
$progress.empty();
|
||||
// Append the JS progressbar element.
|
||||
|
||||
$progress.append(progressBar.element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @file
|
||||
* Polyfill for HTML5 details elements.
|
||||
*/
|
||||
|
||||
(function ($, Modernizr, Drupal) {
|
||||
/**
|
||||
* The collapsible details object represents a single details element.
|
||||
*
|
||||
* @constructor Drupal.CollapsibleDetails
|
||||
*
|
||||
* @param {HTMLElement} node
|
||||
* The details element.
|
||||
*/
|
||||
function CollapsibleDetails(node) {
|
||||
this.$node = $(node);
|
||||
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}` : '';
|
||||
if (this.$node.find(`.error${anchor}`).length) {
|
||||
this.$node.attr('open', true);
|
||||
}
|
||||
// Initialize and setup the summary,
|
||||
this.setupSummary();
|
||||
// Initialize and setup the legend.
|
||||
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');
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize and setup legend markup.
|
||||
*/
|
||||
setupLegend() {
|
||||
// Turn the summary into a clickable link.
|
||||
const $legend = this.$node.find('> summary');
|
||||
|
||||
$('<span class="details-summary-prefix visually-hidden"></span>')
|
||||
.append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show'))
|
||||
.prependTo($legend)
|
||||
.after(document.createTextNode(' '));
|
||||
|
||||
// .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);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Polyfill HTML5 details element.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches behavior for the details element.
|
||||
*/
|
||||
Drupal.behaviors.collapse = {
|
||||
attach(context) {
|
||||
if (Modernizr.details) {
|
||||
return;
|
||||
}
|
||||
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]));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Open parent details elements of a targeted page fragment.
|
||||
*
|
||||
* Opens all (nested) details element on a hash change or fragment link click
|
||||
* when the target is a child element, in order to make sure the targeted
|
||||
* element is visible. Aria attributes on the summary
|
||||
* are set by triggering the click event listener in details-aria.js.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
* @param {jQuery} $target
|
||||
* The targeted node as a jQuery object.
|
||||
*/
|
||||
const handleFragmentLinkClickOrHashChange = (e, $target) => {
|
||||
$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);
|
||||
|
||||
// Expose constructor in the public space.
|
||||
Drupal.CollapsibleDetails = CollapsibleDetails;
|
||||
}(jQuery, Modernizr, Drupal));
|
||||
+32
-91
@@ -1,133 +1,70 @@
|
||||
/**
|
||||
* @file
|
||||
* Polyfill for HTML5 details elements.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Modernizr, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* The collapsible details object represents a single details element.
|
||||
*
|
||||
* @constructor Drupal.CollapsibleDetails
|
||||
*
|
||||
* @param {HTMLElement} node
|
||||
* The details element.
|
||||
*/
|
||||
function CollapsibleDetails(node) {
|
||||
this.$node = $(node);
|
||||
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.
|
||||
|
||||
var anchor = location.hash && location.hash !== '#' ? ', ' + location.hash : '';
|
||||
if (this.$node.find('.error' + anchor).length) {
|
||||
this.$node.attr('open', true);
|
||||
}
|
||||
// Initialize and setup the summary,
|
||||
|
||||
this.setupSummary();
|
||||
// Initialize and setup the legend.
|
||||
|
||||
this.setupLegend();
|
||||
}
|
||||
|
||||
$.extend(CollapsibleDetails, /** @lends Drupal.CollapsibleDetails */{
|
||||
|
||||
/**
|
||||
* Holds references to instantiated CollapsibleDetails objects.
|
||||
*
|
||||
* @type {Array.<Drupal.CollapsibleDetails>}
|
||||
*/
|
||||
$.extend(CollapsibleDetails, {
|
||||
instances: []
|
||||
});
|
||||
|
||||
$.extend(CollapsibleDetails.prototype, /** @lends Drupal.CollapsibleDetails# */{
|
||||
|
||||
/**
|
||||
* Initialize and setup summary events and markup.
|
||||
*
|
||||
* @fires event:summaryUpdated
|
||||
*
|
||||
* @listens event:summaryUpdated
|
||||
*/
|
||||
setupSummary: function () {
|
||||
$.extend(CollapsibleDetails.prototype, {
|
||||
setupSummary: function setupSummary() {
|
||||
this.$summary = $('<span class="summary"></span>');
|
||||
this.$node
|
||||
.on('summaryUpdated', $.proxy(this.onSummaryUpdated, this))
|
||||
.trigger('summaryUpdated');
|
||||
this.$node.on('summaryUpdated', $.proxy(this.onSummaryUpdated, this)).trigger('summaryUpdated');
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize and setup legend markup.
|
||||
*/
|
||||
setupLegend: function () {
|
||||
// Turn the summary into a clickable link.
|
||||
setupLegend: function setupLegend() {
|
||||
var $legend = this.$node.find('> summary');
|
||||
|
||||
$('<span class="details-summary-prefix visually-hidden"></span>')
|
||||
.append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show'))
|
||||
.prependTo($legend)
|
||||
.after(document.createTextNode(' '));
|
||||
$('<span class="details-summary-prefix visually-hidden"></span>').append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show')).prependTo($legend).after(document.createTextNode(' '));
|
||||
|
||||
// .wrapInner() does not retain bound events.
|
||||
$('<a class="details-title"></a>')
|
||||
.attr('href', '#' + this.$node.attr('id'))
|
||||
.prepend($legend.contents())
|
||||
.appendTo($legend);
|
||||
$('<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));
|
||||
$legend.append(this.$summary).on('click', $.proxy(this.onLegendClick, this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle legend clicks.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
*/
|
||||
onLegendClick: function (e) {
|
||||
onLegendClick: function onLegendClick(e) {
|
||||
this.toggle();
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update summary.
|
||||
*/
|
||||
onSummaryUpdated: function () {
|
||||
onSummaryUpdated: function onSummaryUpdated() {
|
||||
var text = $.trim(this.$node.drupalGetSummary());
|
||||
this.$summary.html(text ? ' (' + text + ')' : '');
|
||||
},
|
||||
toggle: function toggle() {
|
||||
var _this = this;
|
||||
|
||||
/**
|
||||
* Toggle the visibility of a details element using smooth animations.
|
||||
*/
|
||||
toggle: function () {
|
||||
var isOpen = !!this.$node.attr('open');
|
||||
var $summaryPrefix = this.$node.find('> summary span.details-summary-prefix');
|
||||
if (isOpen) {
|
||||
$summaryPrefix.html(Drupal.t('Show'));
|
||||
}
|
||||
else {
|
||||
} 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(function () {
|
||||
this.$node.attr('open', !isOpen);
|
||||
}.bind(this), 0);
|
||||
_this.$node.attr('open', !isOpen);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Polyfill HTML5 details element.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches behavior for the details element.
|
||||
*/
|
||||
Drupal.behaviors.collapse = {
|
||||
attach: function (context) {
|
||||
attach: function attach(context) {
|
||||
if (Modernizr.details) {
|
||||
return;
|
||||
}
|
||||
@@ -140,7 +77,11 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Expose constructor in the public space.
|
||||
Drupal.CollapsibleDetails = CollapsibleDetails;
|
||||
var handleFragmentLinkClickOrHashChange = function handleFragmentLinkClickOrHashChange(e, $target) {
|
||||
$target.parents('details').not('[open]').find('> summary').trigger('click');
|
||||
};
|
||||
|
||||
})(jQuery, Modernizr, Drupal);
|
||||
$('body').on('formFragmentLinkClickOrHashChange.details', handleFragmentLinkClickOrHashChange);
|
||||
|
||||
Drupal.CollapsibleDetails = CollapsibleDetails;
|
||||
})(jQuery, Modernizr, Drupal);
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @file
|
||||
* Polyfill for HTML5 date input.
|
||||
*/
|
||||
|
||||
(function ($, Modernizr, Drupal) {
|
||||
/**
|
||||
* Attach datepicker fallback on date elements.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the behavior. Accepts in `settings.date` an object listing
|
||||
* elements to process, keyed by the HTML ID of the form element containing
|
||||
* the human-readable value. Each element is an datepicker settings object.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detach the behavior destroying datepickers on effected elements.
|
||||
*/
|
||||
Drupal.behaviors.date = {
|
||||
attach(context, settings) {
|
||||
const $context = $(context);
|
||||
// Skip if date are supported by the browser.
|
||||
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);
|
||||
});
|
||||
},
|
||||
detach(context, settings, trigger) {
|
||||
if (trigger === 'unload') {
|
||||
$(context).find('input[data-drupal-date-format]').findOnce('datePicker').datepicker('destroy');
|
||||
}
|
||||
},
|
||||
};
|
||||
}(jQuery, Modernizr, Drupal));
|
||||
+12
-30
@@ -1,28 +1,15 @@
|
||||
/**
|
||||
* @file
|
||||
* Polyfill for HTML5 date input.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Modernizr, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Attach datepicker fallback on date elements.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the behavior. Accepts in `settings.date` an object listing
|
||||
* elements to process, keyed by the HTML ID of the form element containing
|
||||
* the human-readable value. Each element is an datepicker settings object.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detach the behavior destroying datepickers on effected elements.
|
||||
*/
|
||||
Drupal.behaviors.date = {
|
||||
attach: function (context, settings) {
|
||||
attach: function attach(context, settings) {
|
||||
var $context = $(context);
|
||||
// Skip if date are supported by the browser.
|
||||
|
||||
if (Modernizr.inputtypes.date === true) {
|
||||
return;
|
||||
}
|
||||
@@ -30,13 +17,9 @@
|
||||
var $input = $(this);
|
||||
var datepickerSettings = {};
|
||||
var 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.
|
||||
|
||||
datepickerSettings.dateFormat = dateFormat.replace('Y', 'yy').replace('m', 'mm').replace('d', 'dd');
|
||||
|
||||
if ($input.attr('min')) {
|
||||
datepickerSettings.minDate = $input.attr('min');
|
||||
}
|
||||
@@ -46,11 +29,10 @@
|
||||
$input.datepicker(datepickerSettings);
|
||||
});
|
||||
},
|
||||
detach: function (context, settings, trigger) {
|
||||
detach: function detach(context, settings, trigger) {
|
||||
if (trigger === 'unload') {
|
||||
$(context).find('input[data-drupal-date-format]').findOnce('datePicker').datepicker('destroy');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Modernizr, Drupal);
|
||||
})(jQuery, Modernizr, Drupal);
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @file
|
||||
* Adapted from underscore.js with the addition Drupal namespace.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Limits the invocations of a function in a given time frame.
|
||||
*
|
||||
* The debounce function wrapper should be used sparingly. One clear use case
|
||||
* is limiting the invocation of a callback attached to the window resize event.
|
||||
*
|
||||
* Before using the debounce function wrapper, consider first whether the
|
||||
* callback could be attached to an event that fires less frequently or if the
|
||||
* function can be written in such a way that it is only invoked under specific
|
||||
* conditions.
|
||||
*
|
||||
* @param {function} func
|
||||
* The function to be invoked.
|
||||
* @param {number} wait
|
||||
* The time period within which the callback function should only be
|
||||
* invoked once. For example if the wait period is 250ms, then the callback
|
||||
* will only be called at most 4 times per second.
|
||||
* @param {bool} immediate
|
||||
* Whether we wait at the beginning or end to execute the function.
|
||||
*
|
||||
* @return {function}
|
||||
* The debounced function.
|
||||
*/
|
||||
Drupal.debounce = function (func, wait, immediate) {
|
||||
let timeout;
|
||||
let result;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
const later = function () {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
result = func.apply(context, args);
|
||||
}
|
||||
};
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
result = func.apply(context, args);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
};
|
||||
+9
-33
@@ -1,41 +1,17 @@
|
||||
/**
|
||||
* @file
|
||||
* Adapted from underscore.js with the addition Drupal namespace.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
/**
|
||||
* Limits the invocations of a function in a given time frame.
|
||||
*
|
||||
* The debounce function wrapper should be used sparingly. One clear use case
|
||||
* is limiting the invocation of a callback attached to the window resize event.
|
||||
*
|
||||
* Before using the debounce function wrapper, consider first whether the
|
||||
* callback could be attached to an event that fires less frequently or if the
|
||||
* function can be written in such a way that it is only invoked under specific
|
||||
* conditions.
|
||||
*
|
||||
* @param {function} func
|
||||
* The function to be invoked.
|
||||
* @param {number} wait
|
||||
* The time period within which the callback function should only be
|
||||
* invoked once. For example if the wait period is 250ms, then the callback
|
||||
* will only be called at most 4 times per second.
|
||||
* @param {bool} immediate
|
||||
* Whether we wait at the beginning or end to execute the function.
|
||||
*
|
||||
* @return {function}
|
||||
* The debounced function.
|
||||
*/
|
||||
Drupal.debounce = function (func, wait, immediate) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var timeout;
|
||||
var result;
|
||||
var timeout = void 0;
|
||||
var result = void 0;
|
||||
return function () {
|
||||
var context = this;
|
||||
var args = arguments;
|
||||
var later = function () {
|
||||
var later = function later() {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
result = func.apply(context, args);
|
||||
@@ -49,4 +25,4 @@ Drupal.debounce = function (func, wait, immediate) {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @file
|
||||
* Add aria attribute handling for details and summary elements.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Handles `aria-expanded` and `aria-pressed` attributes on details elements.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
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';
|
||||
|
||||
$summary.attr({
|
||||
'aria-expanded': open,
|
||||
'aria-pressed': open,
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}(jQuery, Drupal));
|
||||
@@ -1,19 +1,13 @@
|
||||
/**
|
||||
* @file
|
||||
* Add aria attribute handling for details and summary elements.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Handles `aria-expanded` and `aria-pressed` attributes on details elements.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.detailsAria = {
|
||||
attach: function () {
|
||||
attach: function attach() {
|
||||
$('body').once('detailsAria').on('click.detailsAria', 'summary', function (event) {
|
||||
var $summary = $(event.currentTarget);
|
||||
var open = $(event.currentTarget.parentNode).attr('open') === 'open' ? 'false' : 'true';
|
||||
@@ -25,5 +19,4 @@
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* @file
|
||||
* Extends the Drupal AJAX functionality to integrate the dialog API.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Initialize dialogs for Ajax purposes.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the behaviors for dialog ajax functionality.
|
||||
*/
|
||||
Drupal.behaviors.dialog = {
|
||||
attach(context, settings) {
|
||||
const $context = $(context);
|
||||
|
||||
// Provide a known 'drupal-modal' DOM element for Drupal-based modal
|
||||
// dialogs. Non-modal dialogs are responsible for creating their own
|
||||
// elements, since there can be multiple non-modal dialogs at a time.
|
||||
if (!$('#drupal-modal').length) {
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Special behaviors specific when attaching content within a dialog.
|
||||
// These behaviors usually fire after a validation error inside a dialog.
|
||||
const $dialog = $context.closest('.ui-dialog-content');
|
||||
if ($dialog.length) {
|
||||
// Remove and replace the dialog buttons with those from the new form.
|
||||
if ($dialog.dialog('option', 'drupalAutoButtons')) {
|
||||
// Trigger an event to detect/sync changes to buttons.
|
||||
$dialog.trigger('dialogButtonsChange');
|
||||
}
|
||||
|
||||
// Force focus on the modal when the behavior is run.
|
||||
$dialog.dialog('widget').trigger('focus');
|
||||
}
|
||||
|
||||
const originalClose = settings.dialog.close;
|
||||
// Overwrite the close method to remove the dialog on closing.
|
||||
settings.dialog.close = function (event) {
|
||||
originalClose.apply(settings.dialog, arguments);
|
||||
$(event.target).remove();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Scan a dialog for any primary buttons and move them to the button area.
|
||||
*
|
||||
* @param {jQuery} $dialog
|
||||
* An jQuery object containing the element that is the dialog target.
|
||||
*
|
||||
* @return {Array}
|
||||
* An array of buttons that need to be added to the button area.
|
||||
*/
|
||||
prepareDialogButtons($dialog) {
|
||||
const buttons = [];
|
||||
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
|
||||
// "display: none", we set its dimensions to zero.
|
||||
// See http://mattsnider.com/how-forms-submit-when-pressing-enter/
|
||||
const $originalButton = $(this).css({
|
||||
display: 'block',
|
||||
width: 0,
|
||||
height: 0,
|
||||
padding: 0,
|
||||
border: 0,
|
||||
overflow: 'hidden',
|
||||
});
|
||||
buttons.push({
|
||||
text: $originalButton.html() || $originalButton.attr('value'),
|
||||
class: $originalButton.attr('class'),
|
||||
click(e) {
|
||||
// If the original button is an anchor tag, triggering the "click"
|
||||
// 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');
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
return buttons;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Command to open a dialog.
|
||||
*
|
||||
* @param {Drupal.Ajax} ajax
|
||||
* The Drupal Ajax object.
|
||||
* @param {object} response
|
||||
* Object holding the server response.
|
||||
* @param {number} [status]
|
||||
* The HTTP status code.
|
||||
*
|
||||
* @return {bool|undefined}
|
||||
* Returns false if there was no selector property in the response object.
|
||||
*/
|
||||
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');
|
||||
}
|
||||
// Set up the wrapper, if there isn't one.
|
||||
if (!ajax.wrapper) {
|
||||
ajax.wrapper = $dialog.attr('id');
|
||||
}
|
||||
|
||||
// Use the ajax.js insert command to populate the dialog contents.
|
||||
response.command = 'insert';
|
||||
response.method = 'html';
|
||||
ajax.commands.insert(ajax, response, status);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Bind dialogButtonsChange.
|
||||
$dialog.on('dialogButtonsChange', () => {
|
||||
const buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog);
|
||||
$dialog.dialog('option', 'buttons', buttons);
|
||||
});
|
||||
|
||||
// Open the dialog itself.
|
||||
response.dialogOptions = response.dialogOptions || {};
|
||||
const dialog = Drupal.dialog($dialog.get(0), response.dialogOptions);
|
||||
if (response.dialogOptions.modal) {
|
||||
dialog.showModal();
|
||||
}
|
||||
else {
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
// Add the standard Drupal class for buttons for style consistency.
|
||||
$dialog.parent().find('.ui-dialog-buttonset').addClass('form-actions');
|
||||
};
|
||||
|
||||
/**
|
||||
* Command to close a dialog.
|
||||
*
|
||||
* If no selector is given, it defaults to trying to close the modal.
|
||||
*
|
||||
* @param {Drupal.Ajax} [ajax]
|
||||
* The ajax object.
|
||||
* @param {object} response
|
||||
* Object holding the server response.
|
||||
* @param {string} response.selector
|
||||
* The selector of the dialog.
|
||||
* @param {bool} response.persist
|
||||
* Whether to persist the dialog element or not.
|
||||
* @param {number} [status]
|
||||
* The HTTP status code.
|
||||
*/
|
||||
Drupal.AjaxCommands.prototype.closeDialog = function (ajax, response, status) {
|
||||
const $dialog = $(response.selector);
|
||||
if ($dialog.length) {
|
||||
Drupal.dialog($dialog.get(0)).close();
|
||||
if (!response.persist) {
|
||||
$dialog.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Unbind dialogButtonsChange.
|
||||
$dialog.off('dialogButtonsChange');
|
||||
};
|
||||
|
||||
/**
|
||||
* Command to set a dialog property.
|
||||
*
|
||||
* JQuery UI specific way of setting dialog options.
|
||||
*
|
||||
* @param {Drupal.Ajax} [ajax]
|
||||
* The Drupal Ajax object.
|
||||
* @param {object} response
|
||||
* Object holding the server response.
|
||||
* @param {string} response.selector
|
||||
* Selector for the dialog element.
|
||||
* @param {string} response.optionsName
|
||||
* Name of a key to set.
|
||||
* @param {string} response.optionValue
|
||||
* Value to set.
|
||||
* @param {number} [status]
|
||||
* The HTTP status code.
|
||||
*/
|
||||
Drupal.AjaxCommands.prototype.setDialogOption = function (ajax, response, status) {
|
||||
const $dialog = $(response.selector);
|
||||
if ($dialog.length) {
|
||||
$dialog.dialog('option', response.optionName, response.optionValue);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds a listener on dialog creation to handle the cancel link.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
* @param {Drupal.dialog~dialogDefinition} dialog
|
||||
* The dialog instance.
|
||||
* @param {jQuery} $element
|
||||
* The jQuery collection of the dialog element.
|
||||
* @param {object} [settings]
|
||||
* Dialog settings.
|
||||
*/
|
||||
$(window).on('dialog:aftercreate', (e, dialog, $element, settings) => {
|
||||
$element.on('click.dialog', '.dialog-cancel', (e) => {
|
||||
dialog.close('cancel');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Removes all 'dialog' listeners.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
* @param {Drupal.dialog~dialogDefinition} dialog
|
||||
* The dialog instance.
|
||||
* @param {jQuery} $element
|
||||
* jQuery collection of the dialog element.
|
||||
*/
|
||||
$(window).on('dialog:beforeclose', (e, dialog, $element) => {
|
||||
$element.off('.dialog');
|
||||
});
|
||||
}(jQuery, Drupal));
|
||||
+13
-129
@@ -1,74 +1,39 @@
|
||||
/**
|
||||
* @file
|
||||
* Extends the Drupal AJAX functionality to integrate the dialog API.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialize dialogs for Ajax purposes.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the behaviors for dialog ajax functionality.
|
||||
*/
|
||||
Drupal.behaviors.dialog = {
|
||||
attach: function (context, settings) {
|
||||
attach: function attach(context, settings) {
|
||||
var $context = $(context);
|
||||
|
||||
// Provide a known 'drupal-modal' DOM element for Drupal-based modal
|
||||
// dialogs. Non-modal dialogs are responsible for creating their own
|
||||
// elements, since there can be multiple non-modal dialogs at a time.
|
||||
if (!$('#drupal-modal').length) {
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Special behaviors specific when attaching content within a dialog.
|
||||
// These behaviors usually fire after a validation error inside a dialog.
|
||||
var $dialog = $context.closest('.ui-dialog-content');
|
||||
if ($dialog.length) {
|
||||
// Remove and replace the dialog buttons with those from the new form.
|
||||
if ($dialog.dialog('option', 'drupalAutoButtons')) {
|
||||
// Trigger an event to detect/sync changes to buttons.
|
||||
$dialog.trigger('dialogButtonsChange');
|
||||
}
|
||||
|
||||
// Force focus on the modal when the behavior is run.
|
||||
$dialog.dialog('widget').trigger('focus');
|
||||
}
|
||||
|
||||
var originalClose = settings.dialog.close;
|
||||
// Overwrite the close method to remove the dialog on closing.
|
||||
|
||||
settings.dialog.close = function (event) {
|
||||
originalClose.apply(settings.dialog, arguments);
|
||||
$(event.target).remove();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Scan a dialog for any primary buttons and move them to the button area.
|
||||
*
|
||||
* @param {jQuery} $dialog
|
||||
* An jQuery object containing the element that is the dialog target.
|
||||
*
|
||||
* @return {Array}
|
||||
* An array of buttons that need to be added to the button area.
|
||||
*/
|
||||
prepareDialogButtons: function ($dialog) {
|
||||
prepareDialogButtons: function prepareDialogButtons($dialog) {
|
||||
var buttons = [];
|
||||
var $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
|
||||
// "display: none", we set its dimensions to zero.
|
||||
// See http://mattsnider.com/how-forms-submit-when-pressing-enter/
|
||||
var $originalButton = $(this).css({
|
||||
display: 'block',
|
||||
width: 0,
|
||||
@@ -80,13 +45,10 @@
|
||||
buttons.push({
|
||||
text: $originalButton.html() || $originalButton.attr('value'),
|
||||
class: $originalButton.attr('class'),
|
||||
click: function (e) {
|
||||
// If the original button is an anchor tag, triggering the "click"
|
||||
// event will not simulate a click. Use the click method instead.
|
||||
click: function click(e) {
|
||||
if ($originalButton.is('a')) {
|
||||
$originalButton[0].click();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$originalButton.trigger('mousedown').trigger('mouseup').trigger('click');
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -97,80 +59,44 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Command to open a dialog.
|
||||
*
|
||||
* @param {Drupal.Ajax} ajax
|
||||
* The Drupal Ajax object.
|
||||
* @param {object} response
|
||||
* Object holding the server response.
|
||||
* @param {number} [status]
|
||||
* The HTTP status code.
|
||||
*
|
||||
* @return {bool|undefined}
|
||||
* Returns false if there was no selector property in the response object.
|
||||
*/
|
||||
Drupal.AjaxCommands.prototype.openDialog = function (ajax, response, status) {
|
||||
if (!response.selector) {
|
||||
return false;
|
||||
}
|
||||
var $dialog = $(response.selector);
|
||||
if (!$dialog.length) {
|
||||
// Create the element if needed.
|
||||
$dialog = $('<div id="' + response.selector.replace(/^#/, '') + '" class="ui-front"/>').appendTo('body');
|
||||
}
|
||||
// Set up the wrapper, if there isn't one.
|
||||
|
||||
if (!ajax.wrapper) {
|
||||
ajax.wrapper = $dialog.attr('id');
|
||||
}
|
||||
|
||||
// Use the ajax.js insert command to populate the dialog contents.
|
||||
response.command = 'insert';
|
||||
response.method = 'html';
|
||||
ajax.commands.insert(ajax, response, status);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Bind dialogButtonsChange.
|
||||
$dialog.on('dialogButtonsChange', function () {
|
||||
var buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog);
|
||||
$dialog.dialog('option', 'buttons', buttons);
|
||||
});
|
||||
|
||||
// Open the dialog itself.
|
||||
response.dialogOptions = response.dialogOptions || {};
|
||||
var 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');
|
||||
};
|
||||
|
||||
/**
|
||||
* Command to close a dialog.
|
||||
*
|
||||
* If no selector is given, it defaults to trying to close the modal.
|
||||
*
|
||||
* @param {Drupal.Ajax} [ajax]
|
||||
* The ajax object.
|
||||
* @param {object} response
|
||||
* Object holding the server response.
|
||||
* @param {string} response.selector
|
||||
* The selector of the dialog.
|
||||
* @param {bool} response.persist
|
||||
* Whether to persist the dialog element or not.
|
||||
* @param {number} [status]
|
||||
* The HTTP status code.
|
||||
*/
|
||||
Drupal.AjaxCommands.prototype.closeDialog = function (ajax, response, status) {
|
||||
var $dialog = $(response.selector);
|
||||
if ($dialog.length) {
|
||||
@@ -180,28 +106,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Unbind dialogButtonsChange.
|
||||
$dialog.off('dialogButtonsChange');
|
||||
};
|
||||
|
||||
/**
|
||||
* Command to set a dialog property.
|
||||
*
|
||||
* JQuery UI specific way of setting dialog options.
|
||||
*
|
||||
* @param {Drupal.Ajax} [ajax]
|
||||
* The Drupal Ajax object.
|
||||
* @param {object} response
|
||||
* Object holding the server response.
|
||||
* @param {string} response.selector
|
||||
* Selector for the dialog element.
|
||||
* @param {string} response.optionsName
|
||||
* Name of a key to set.
|
||||
* @param {string} response.optionValue
|
||||
* Value to set.
|
||||
* @param {number} [status]
|
||||
* The HTTP status code.
|
||||
*/
|
||||
Drupal.AjaxCommands.prototype.setDialogOption = function (ajax, response, status) {
|
||||
var $dialog = $(response.selector);
|
||||
if ($dialog.length) {
|
||||
@@ -209,18 +116,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds a listener on dialog creation to handle the cancel link.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
* @param {Drupal.dialog~dialogDefinition} dialog
|
||||
* The dialog instance.
|
||||
* @param {jQuery} $element
|
||||
* The jQuery collection of the dialog element.
|
||||
* @param {object} [settings]
|
||||
* Dialog settings.
|
||||
*/
|
||||
$(window).on('dialog:aftercreate', function (e, dialog, $element, settings) {
|
||||
$element.on('click.dialog', '.dialog-cancel', function (e) {
|
||||
dialog.close('cancel');
|
||||
@@ -229,18 +124,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Removes all 'dialog' listeners.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
* @param {Drupal.dialog~dialogDefinition} dialog
|
||||
* The dialog instance.
|
||||
* @param {jQuery} $element
|
||||
* jQuery collection of the dialog element.
|
||||
*/
|
||||
$(window).on('dialog:beforeclose', function (e, dialog, $element) {
|
||||
$element.off('.dialog');
|
||||
});
|
||||
|
||||
})(jQuery, Drupal);
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @file
|
||||
* Dialog API inspired by HTML5 dialog element.
|
||||
*
|
||||
* @see http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#the-dialog-element
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
/**
|
||||
* Default dialog options.
|
||||
*
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {bool} [autoOpen=true]
|
||||
* @prop {string} [dialogClass='']
|
||||
* @prop {string} [buttonClass='button']
|
||||
* @prop {string} [buttonPrimaryClass='button--primary']
|
||||
* @prop {function} close
|
||||
*/
|
||||
drupalSettings.dialog = {
|
||||
autoOpen: true,
|
||||
dialogClass: '',
|
||||
// Drupal-specific extensions: see dialog.jquery-ui.js.
|
||||
buttonClass: 'button',
|
||||
buttonPrimaryClass: 'button--primary',
|
||||
// When using this API directly (when generating dialogs on the client
|
||||
// side), you may want to override this method and do
|
||||
// `jQuery(event.target).remove()` as well, to remove the dialog on
|
||||
// closing.
|
||||
close(event) {
|
||||
Drupal.dialog(event.target).close();
|
||||
Drupal.detachBehaviors(event.target, null, 'unload');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} Drupal.dialog~dialogDefinition
|
||||
*
|
||||
* @prop {boolean} open
|
||||
* Is the dialog open or not.
|
||||
* @prop {*} returnValue
|
||||
* Return value of the dialog.
|
||||
* @prop {function} show
|
||||
* Method to display the dialog on the page.
|
||||
* @prop {function} showModal
|
||||
* Method to display the dialog as a modal on the page.
|
||||
* @prop {function} close
|
||||
* Method to hide the dialog from the page.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Polyfill HTML5 dialog element with jQueryUI.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element that holds the dialog.
|
||||
* @param {object} options
|
||||
* jQuery UI options to be passed to the dialog.
|
||||
*
|
||||
* @return {Drupal.dialog~dialogDefinition}
|
||||
* The dialog instance.
|
||||
*/
|
||||
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) {
|
||||
settings = $.extend({}, drupalSettings.dialog, options, settings);
|
||||
// Trigger a global event to allow scripts to bind events to the dialog.
|
||||
$(window).trigger('dialog:beforecreate', [dialog, $element, settings]);
|
||||
$element.dialog(settings);
|
||||
dialog.open = true;
|
||||
$(window).trigger('dialog:aftercreate', [dialog, $element, settings]);
|
||||
}
|
||||
|
||||
function closeDialog(value) {
|
||||
$(window).trigger('dialog:beforeclose', [dialog, $element]);
|
||||
$element.dialog('close');
|
||||
dialog.returnValue = value;
|
||||
dialog.open = false;
|
||||
$(window).trigger('dialog:afterclose', [dialog, $element]);
|
||||
}
|
||||
|
||||
return dialog;
|
||||
};
|
||||
}(jQuery, Drupal, drupalSettings));
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @file
|
||||
* Adds default classes to buttons for styling purposes.
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
$.widget('ui.dialog', $.ui.dialog, {
|
||||
options: {
|
||||
buttonClass: 'button',
|
||||
buttonPrimaryClass: 'button--primary',
|
||||
},
|
||||
_createButtons() {
|
||||
const opts = this.options;
|
||||
let primaryIndex;
|
||||
let $buttons;
|
||||
let index;
|
||||
const il = opts.buttons.length;
|
||||
for (index = 0; index < il; index++) {
|
||||
if (opts.buttons[index].primary && opts.buttons[index].primary === true) {
|
||||
primaryIndex = index;
|
||||
delete opts.buttons[index].primary;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._super();
|
||||
$buttons = this.uiButtonSet.children().addClass(opts.buttonClass);
|
||||
if (typeof primaryIndex !== 'undefined') {
|
||||
$buttons.eq(index).addClass(opts.buttonPrimaryClass);
|
||||
}
|
||||
},
|
||||
});
|
||||
}(jQuery));
|
||||
@@ -1,22 +1,21 @@
|
||||
/**
|
||||
* @file
|
||||
* Adds default classes to buttons for styling purposes.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($) {
|
||||
|
||||
'use strict';
|
||||
|
||||
$.widget('ui.dialog', $.ui.dialog, {
|
||||
options: {
|
||||
buttonClass: 'button',
|
||||
buttonPrimaryClass: 'button--primary'
|
||||
},
|
||||
_createButtons: function () {
|
||||
_createButtons: function _createButtons() {
|
||||
var opts = this.options;
|
||||
var primaryIndex;
|
||||
var $buttons;
|
||||
var index;
|
||||
var primaryIndex = void 0;
|
||||
var $buttons = void 0;
|
||||
var index = void 0;
|
||||
var il = opts.buttons.length;
|
||||
for (index = 0; index < il; index++) {
|
||||
if (opts.buttons[index].primary && opts.buttons[index].primary === true) {
|
||||
@@ -32,5 +31,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
})(jQuery);
|
||||
+15
-59
@@ -1,85 +1,42 @@
|
||||
/**
|
||||
* @file
|
||||
* Dialog API inspired by HTML5 dialog element.
|
||||
*
|
||||
* @see http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#the-dialog-element
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Default dialog options.
|
||||
*
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {bool} [autoOpen=true]
|
||||
* @prop {string} [dialogClass='']
|
||||
* @prop {string} [buttonClass='button']
|
||||
* @prop {string} [buttonPrimaryClass='button--primary']
|
||||
* @prop {function} close
|
||||
*/
|
||||
drupalSettings.dialog = {
|
||||
autoOpen: true,
|
||||
dialogClass: '',
|
||||
// Drupal-specific extensions: see dialog.jquery-ui.js.
|
||||
|
||||
buttonClass: 'button',
|
||||
buttonPrimaryClass: 'button--primary',
|
||||
// When using this API directly (when generating dialogs on the client
|
||||
// side), you may want to override this method and do
|
||||
// `jQuery(event.target).remove()` as well, to remove the dialog on
|
||||
// closing.
|
||||
close: function (event) {
|
||||
close: function close(event) {
|
||||
Drupal.dialog(event.target).close();
|
||||
Drupal.detachBehaviors(event.target, null, 'unload');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} Drupal.dialog~dialogDefinition
|
||||
*
|
||||
* @prop {boolean} open
|
||||
* Is the dialog open or not.
|
||||
* @prop {*} returnValue
|
||||
* Return value of the dialog.
|
||||
* @prop {function} show
|
||||
* Method to display the dialog on the page.
|
||||
* @prop {function} showModal
|
||||
* Method to display the dialog as a modal on the page.
|
||||
* @prop {function} close
|
||||
* Method to hide the dialog from the page.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Polyfill HTML5 dialog element with jQueryUI.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element that holds the dialog.
|
||||
* @param {object} options
|
||||
* jQuery UI options to be passed to the dialog.
|
||||
*
|
||||
* @return {Drupal.dialog~dialogDefinition}
|
||||
* The dialog instance.
|
||||
*/
|
||||
Drupal.dialog = function (element, options) {
|
||||
var undef;
|
||||
var undef = void 0;
|
||||
var $element = $(element);
|
||||
var dialog = {
|
||||
open: false,
|
||||
returnValue: undef,
|
||||
show: function () {
|
||||
openDialog({modal: false});
|
||||
show: function show() {
|
||||
openDialog({ modal: false });
|
||||
},
|
||||
showModal: function () {
|
||||
openDialog({modal: true});
|
||||
showModal: function showModal() {
|
||||
openDialog({ modal: true });
|
||||
},
|
||||
|
||||
close: closeDialog
|
||||
};
|
||||
|
||||
function openDialog(settings) {
|
||||
settings = $.extend({}, drupalSettings.dialog, options, settings);
|
||||
// Trigger a global event to allow scripts to bind events to the dialog.
|
||||
|
||||
$(window).trigger('dialog:beforecreate', [dialog, $element, settings]);
|
||||
$element.dialog(settings);
|
||||
dialog.open = true;
|
||||
@@ -96,5 +53,4 @@
|
||||
|
||||
return dialog;
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings);
|
||||
})(jQuery, Drupal, drupalSettings);
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @file
|
||||
* Positioning extensions for dialogs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggers when content inside a dialog changes.
|
||||
*
|
||||
* @event dialogContentResize
|
||||
*/
|
||||
|
||||
(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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the dialog's center at the center of displace.offsets boundaries.
|
||||
*
|
||||
* @function Drupal.dialog~resetPosition
|
||||
*
|
||||
* @param {object} options
|
||||
* Options object.
|
||||
*
|
||||
* @return {object}
|
||||
* Altered options object.
|
||||
*/
|
||||
function resetPosition(options) {
|
||||
const offsets = displace.offsets;
|
||||
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`;
|
||||
options.position = {
|
||||
my: `center${left !== 0 ? leftString : ''} center${top !== 0 ? topString : ''}`,
|
||||
of: window,
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
$(window).on({
|
||||
'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');
|
||||
$(window)
|
||||
.on('resize.dialogResize scroll.dialogResize', eventData, autoResize)
|
||||
.trigger('resize.dialogResize');
|
||||
$(document).on('drupalViewportOffsetChange.dialogResize', eventData, autoResize);
|
||||
}
|
||||
},
|
||||
'dialog:beforeclose': function (event, dialog, $element) {
|
||||
$(window).off('.dialogResize');
|
||||
$(document).off('.dialogResize');
|
||||
},
|
||||
});
|
||||
}(jQuery, Drupal, drupalSettings, Drupal.debounce, Drupal.displace));
|
||||
@@ -1,53 +1,28 @@
|
||||
/**
|
||||
* @file
|
||||
* Positioning extensions for dialogs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggers when content inside a dialog changes.
|
||||
*
|
||||
* @event dialogContentResize
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings, debounce, displace) {
|
||||
drupalSettings.dialog = $.extend({ autoResize: true, maxHeight: '95%' }, drupalSettings.dialog);
|
||||
|
||||
'use strict';
|
||||
|
||||
// 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) {
|
||||
var positionOptions = ['width', 'height', 'minWidth', 'minHeight', 'maxHeight', 'maxWidth', 'position'];
|
||||
var adjustedOptions = {};
|
||||
var windowHeight = $(window).height();
|
||||
var option;
|
||||
var optionValue;
|
||||
var adjustedValue;
|
||||
var option = void 0;
|
||||
var optionValue = void 0;
|
||||
var adjustedValue = void 0;
|
||||
for (var 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';
|
||||
}
|
||||
@@ -55,26 +30,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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');
|
||||
event.data.$element.dialog('option', adjustedOptions).trigger('dialogContentResize');
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the dialog's center at the center of displace.offsets boundaries.
|
||||
*
|
||||
* @function Drupal.dialog~resetPosition
|
||||
*
|
||||
* @param {object} options
|
||||
* Options object.
|
||||
*
|
||||
* @return {object}
|
||||
* Altered options object.
|
||||
*/
|
||||
function resetPosition(options) {
|
||||
var offsets = displace.offsets;
|
||||
var left = offsets.left - offsets.right;
|
||||
@@ -90,23 +52,18 @@
|
||||
}
|
||||
|
||||
$(window).on({
|
||||
'dialog:aftercreate': function (event, dialog, $element, settings) {
|
||||
'dialog:aftercreate': function dialogAftercreate(event, dialog, $element, settings) {
|
||||
var autoResize = debounce(resetSize, 20);
|
||||
var eventData = {settings: settings, $element: $element};
|
||||
var eventData = { settings: settings, $element: $element };
|
||||
if (settings.autoResize === true || settings.autoResize === 'true') {
|
||||
$element
|
||||
.dialog('option', {resizable: false, draggable: false})
|
||||
.dialog('widget').css('position', 'fixed');
|
||||
$(window)
|
||||
.on('resize.dialogResize scroll.dialogResize', eventData, autoResize)
|
||||
.trigger('resize.dialogResize');
|
||||
$element.dialog('option', { resizable: false, draggable: false }).dialog('widget').css('position', 'fixed');
|
||||
$(window).on('resize.dialogResize scroll.dialogResize', eventData, autoResize).trigger('resize.dialogResize');
|
||||
$(document).on('drupalViewportOffsetChange.dialogResize', eventData, autoResize);
|
||||
}
|
||||
},
|
||||
'dialog:beforeclose': function (event, dialog, $element) {
|
||||
'dialog:beforeclose': function dialogBeforeclose(event, dialog, $element) {
|
||||
$(window).off('.dialogResize');
|
||||
$(document).off('.dialogResize');
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, Drupal, drupalSettings, Drupal.debounce, Drupal.displace);
|
||||
})(jQuery, Drupal, drupalSettings, Drupal.debounce, Drupal.displace);
|
||||
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* @file
|
||||
* Manages elements that can offset the size of the viewport.
|
||||
*
|
||||
* Measures and reports viewport offset dimensions from elements like the
|
||||
* toolbar that can potentially displace the positioning of other elements.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Drupal~displaceOffset
|
||||
*
|
||||
* @prop {number} top
|
||||
* @prop {number} left
|
||||
* @prop {number} right
|
||||
* @prop {number} bottom
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggers when layout of the page changes.
|
||||
*
|
||||
* This is used to position fixed element on the page during page resize and
|
||||
* Toolbar toggling.
|
||||
*
|
||||
* @event drupalViewportOffsetChange
|
||||
*/
|
||||
|
||||
(function ($, Drupal, debounce) {
|
||||
/**
|
||||
* @name Drupal.displace.offsets
|
||||
*
|
||||
* @type {Drupal~displaceOffset}
|
||||
*/
|
||||
let offsets = {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
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 = Drupal.displace.offsets = calculateOffsets();
|
||||
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.
|
||||
*
|
||||
* @param {HTMLElement} el
|
||||
* The jQuery element whose dimensions and placement will be measured.
|
||||
*
|
||||
* @param {string} edge
|
||||
* The name of the edge of the viewport that the element is associated
|
||||
* with.
|
||||
*
|
||||
* @return {number}
|
||||
* The viewport displacement distance for the requested edge.
|
||||
*/
|
||||
function getRawOffset(el, edge) {
|
||||
const $el = $(el);
|
||||
const documentElement = document.documentElement;
|
||||
let displacement = 0;
|
||||
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;
|
||||
// Find the displacement value according to the edge.
|
||||
switch (edge) {
|
||||
// Left and top elements displace as a sum of their own offset value
|
||||
// plus their size.
|
||||
case 'top':
|
||||
// Total displacement is the sum of the elements placement and size.
|
||||
displacement = placement + $el.outerHeight();
|
||||
break;
|
||||
|
||||
case 'left':
|
||||
// Total displacement is the sum of the elements placement and size.
|
||||
displacement = placement + $el.outerWidth();
|
||||
break;
|
||||
|
||||
// Right and bottom elements displace according to their left and
|
||||
// top offset. Their size isn't important.
|
||||
case 'bottom':
|
||||
displacement = documentElement.clientHeight - placement;
|
||||
break;
|
||||
|
||||
case 'right':
|
||||
displacement = documentElement.clientWidth - placement;
|
||||
break;
|
||||
|
||||
default:
|
||||
displacement = 0;
|
||||
}
|
||||
return displacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign the displace function to a property of the Drupal global object.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
Drupal.displace = displace;
|
||||
$.extend(Drupal.displace, {
|
||||
|
||||
/**
|
||||
* Expose offsets to other scripts to avoid having to recalculate offsets.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
offsets,
|
||||
|
||||
/**
|
||||
* Expose method to compute a single edge offsets.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
calculateOffset,
|
||||
});
|
||||
}(jQuery, Drupal, Drupal.debounce));
|
||||
+15
-131
@@ -1,38 +1,11 @@
|
||||
/**
|
||||
* @file
|
||||
* Manages elements that can offset the size of the viewport.
|
||||
*
|
||||
* Measures and reports viewport offset dimensions from elements like the
|
||||
* toolbar that can potentially displace the positioning of other elements.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Drupal~displaceOffset
|
||||
*
|
||||
* @prop {number} top
|
||||
* @prop {number} left
|
||||
* @prop {number} right
|
||||
* @prop {number} bottom
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggers when layout of the page changes.
|
||||
*
|
||||
* This is used to position fixed element on the page during page resize and
|
||||
* Toolbar toggling.
|
||||
*
|
||||
* @event drupalViewportOffsetChange
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, debounce) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @name Drupal.displace.offsets
|
||||
*
|
||||
* @type {Drupal~displaceOffset}
|
||||
*/
|
||||
var offsets = {
|
||||
top: 0,
|
||||
right: 0,
|
||||
@@ -40,14 +13,8 @@
|
||||
left: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a resize handler on the window.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.drupalDisplace = {
|
||||
attach: function () {
|
||||
// Mark this behavior as processed on the first pass.
|
||||
attach: function attach() {
|
||||
if (this.displaceProcessed) {
|
||||
return;
|
||||
}
|
||||
@@ -57,24 +24,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = Drupal.displace.offsets = calculateOffsets();
|
||||
if (typeof broadcast === 'undefined' || broadcast) {
|
||||
@@ -83,14 +32,6 @@
|
||||
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'),
|
||||
@@ -100,88 +41,48 @@
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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 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.
|
||||
|
||||
var 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.
|
||||
*
|
||||
* @param {HTMLElement} el
|
||||
* The jQuery element whose dimensions and placement will be measured.
|
||||
*
|
||||
* @param {string} edge
|
||||
* The name of the edge of the viewport that the element is associated
|
||||
* with.
|
||||
*
|
||||
* @return {number}
|
||||
* The viewport displacement distance for the requested edge.
|
||||
*/
|
||||
function getRawOffset(el, edge) {
|
||||
var $el = $(el);
|
||||
var documentElement = document.documentElement;
|
||||
var displacement = 0;
|
||||
var horizontal = (edge === 'left' || edge === 'right');
|
||||
// Get the offset of the element itself.
|
||||
var horizontal = edge === 'left' || edge === 'right';
|
||||
|
||||
var 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;
|
||||
// Find the displacement value according to the edge.
|
||||
|
||||
switch (edge) {
|
||||
// Left and top elements displace as a sum of their own offset value
|
||||
// plus their size.
|
||||
case 'top':
|
||||
// Total displacement is the sum of the elements placement and size.
|
||||
displacement = placement + $el.outerHeight();
|
||||
break;
|
||||
|
||||
case 'left':
|
||||
// Total displacement is the sum of the elements placement and size.
|
||||
displacement = placement + $el.outerWidth();
|
||||
break;
|
||||
|
||||
// Right and bottom elements displace according to their left and
|
||||
// top offset. Their size isn't important.
|
||||
case 'bottom':
|
||||
displacement = documentElement.clientHeight - placement;
|
||||
break;
|
||||
@@ -196,27 +97,10 @@
|
||||
return displacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign the displace function to a property of the Drupal global object.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
Drupal.displace = displace;
|
||||
$.extend(Drupal.displace, {
|
||||
|
||||
/**
|
||||
* Expose offsets to other scripts to avoid having to recalculate offsets.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
offsets: offsets,
|
||||
|
||||
/**
|
||||
* Expose method to compute a single edge offsets.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
calculateOffset: calculateOffset
|
||||
});
|
||||
|
||||
})(jQuery, Drupal, Drupal.debounce);
|
||||
})(jQuery, Drupal, Drupal.debounce);
|
||||
@@ -17,14 +17,14 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media screen and (max-width:600px) {
|
||||
@media screen and (max-width: 600px) {
|
||||
.js .dropbutton-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Splitbuttons */
|
||||
@media screen and (min-width:600px) {
|
||||
@media screen and (min-width: 600px) {
|
||||
.form-actions .dropbutton-wrapper {
|
||||
float: left; /* LTR */
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* @file
|
||||
* 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* A DropButton presents an HTML list as a button with a primary action.
|
||||
*
|
||||
* All secondary actions beyond the first in the list are presented in a
|
||||
* dropdown list accessible through a toggle arrow associated with the button.
|
||||
*
|
||||
* @constructor Drupal.DropButton
|
||||
*
|
||||
* @param {HTMLElement} dropbutton
|
||||
* A DOM element.
|
||||
* @param {object} settings
|
||||
* A list of options including:
|
||||
* @param {string} settings.title
|
||||
* The text inside the toggle link element. This text is hidden
|
||||
* from visual UAs.
|
||||
*/
|
||||
function DropButton(dropbutton, settings) {
|
||||
// Merge defaults with settings.
|
||||
const options = $.extend({ title: Drupal.t('List additional actions') }, settings);
|
||||
const $dropbutton = $(dropbutton);
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$dropbutton = $dropbutton;
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$list = $dropbutton.find('.dropbutton');
|
||||
|
||||
/**
|
||||
* Find actions and mark them.
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$actions = this.$list.find('li').addClass('dropbutton-action');
|
||||
|
||||
// Add the special dropdown only if there are hidden actions.
|
||||
if (this.$actions.length > 1) {
|
||||
// Identify the first element of the collection.
|
||||
const $primary = this.$actions.slice(0, 1);
|
||||
// Identify the secondary actions.
|
||||
const $secondary = this.$actions.slice(1);
|
||||
$secondary.addClass('secondary-action');
|
||||
// Add toggle link.
|
||||
$primary.after(Drupal.theme('dropbuttonToggle', options));
|
||||
// Bind mouse events.
|
||||
this.$dropbutton
|
||||
.addClass('dropbutton-multiple')
|
||||
.on({
|
||||
|
||||
/**
|
||||
* 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),
|
||||
|
||||
/**
|
||||
* Similar to mouseleave/mouseenter, but for keyboard navigation.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'focusout.dropbutton': $.proxy(this.focusOut, this),
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
'focusin.dropbutton': $.proxy(this.focusIn, this),
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.$dropbutton.addClass('dropbutton-single');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the DropButton constructor.
|
||||
*/
|
||||
$.extend(DropButton, /** @lends Drupal.DropButton */{
|
||||
/**
|
||||
* Store all processed DropButtons.
|
||||
*
|
||||
* @type {Array.<Drupal.DropButton>}
|
||||
*/
|
||||
dropbuttons: [],
|
||||
});
|
||||
|
||||
/**
|
||||
* Extend the DropButton prototype.
|
||||
*/
|
||||
$.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);
|
||||
},
|
||||
|
||||
/**
|
||||
* @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);
|
||||
},
|
||||
});
|
||||
|
||||
$.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));
|
||||
@@ -1,30 +1,20 @@
|
||||
/**
|
||||
* @file
|
||||
* Dropbutton feature.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Process elements with the .dropbutton class on page load.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches dropButton behaviors.
|
||||
*/
|
||||
Drupal.behaviors.dropButton = {
|
||||
attach: function (context, settings) {
|
||||
attach: function attach(context, settings) {
|
||||
var $dropbuttons = $(context).find('.dropbutton-wrapper').once('dropbutton');
|
||||
if ($dropbuttons.length) {
|
||||
// Adds the delegated handler that will toggle dropdowns on click.
|
||||
var $body = $('body').once('dropbutton-click');
|
||||
if ($body.length) {
|
||||
$body.on('click', '.dropbutton-toggle', dropbuttonClickHandler);
|
||||
}
|
||||
// Initialize all buttons.
|
||||
|
||||
var il = $dropbuttons.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
DropButton.dropbuttons.push(new DropButton($dropbuttons[i], settings.dropbutton));
|
||||
@@ -33,201 +23,80 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* A DropButton presents an HTML list as a button with a primary action.
|
||||
*
|
||||
* All secondary actions beyond the first in the list are presented in a
|
||||
* dropdown list accessible through a toggle arrow associated with the button.
|
||||
*
|
||||
* @constructor Drupal.DropButton
|
||||
*
|
||||
* @param {HTMLElement} dropbutton
|
||||
* A DOM element.
|
||||
* @param {object} settings
|
||||
* A list of options including:
|
||||
* @param {string} settings.title
|
||||
* The text inside the toggle link element. This text is hidden
|
||||
* from visual UAs.
|
||||
*/
|
||||
function DropButton(dropbutton, settings) {
|
||||
// Merge defaults with settings.
|
||||
var options = $.extend({title: Drupal.t('List additional actions')}, settings);
|
||||
var options = $.extend({ title: Drupal.t('List additional actions') }, settings);
|
||||
var $dropbutton = $(dropbutton);
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$dropbutton = $dropbutton;
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$list = $dropbutton.find('.dropbutton');
|
||||
|
||||
/**
|
||||
* Find actions and mark them.
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$actions = this.$list.find('li').addClass('dropbutton-action');
|
||||
|
||||
// Add the special dropdown only if there are hidden actions.
|
||||
if (this.$actions.length > 1) {
|
||||
// Identify the first element of the collection.
|
||||
var $primary = this.$actions.slice(0, 1);
|
||||
// Identify the secondary actions.
|
||||
|
||||
var $secondary = this.$actions.slice(1);
|
||||
$secondary.addClass('secondary-action');
|
||||
// Add toggle link.
|
||||
|
||||
$primary.after(Drupal.theme('dropbuttonToggle', options));
|
||||
// Bind mouse events.
|
||||
this.$dropbutton
|
||||
.addClass('dropbutton-multiple')
|
||||
.on({
|
||||
|
||||
/**
|
||||
* Adds a timeout to close the dropdown on mouseleave.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'mouseleave.dropbutton': $.proxy(this.hoverOut, this),
|
||||
this.$dropbutton.addClass('dropbutton-multiple').on({
|
||||
'mouseleave.dropbutton': $.proxy(this.hoverOut, this),
|
||||
|
||||
/**
|
||||
* Clears timeout when mouseout of the dropdown.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'mouseenter.dropbutton': $.proxy(this.hoverIn, this),
|
||||
'mouseenter.dropbutton': $.proxy(this.hoverIn, this),
|
||||
|
||||
/**
|
||||
* Similar to mouseleave/mouseenter, but for keyboard navigation.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'focusout.dropbutton': $.proxy(this.focusOut, this),
|
||||
'focusout.dropbutton': $.proxy(this.focusOut, this),
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
'focusin.dropbutton': $.proxy(this.focusIn, this)
|
||||
});
|
||||
}
|
||||
else {
|
||||
'focusin.dropbutton': $.proxy(this.focusIn, this)
|
||||
});
|
||||
} else {
|
||||
this.$dropbutton.addClass('dropbutton-single');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the DropButton constructor.
|
||||
*/
|
||||
$.extend(DropButton, /** @lends Drupal.DropButton */{
|
||||
/**
|
||||
* Store all processed DropButtons.
|
||||
*
|
||||
* @type {Array.<Drupal.DropButton>}
|
||||
*/
|
||||
$.extend(DropButton, {
|
||||
dropbuttons: []
|
||||
});
|
||||
|
||||
/**
|
||||
* Extend the DropButton prototype.
|
||||
*/
|
||||
$.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: function (show) {
|
||||
$.extend(DropButton.prototype, {
|
||||
toggle: function toggle(show) {
|
||||
var isBool = typeof show === 'boolean';
|
||||
show = isBool ? show : !this.$dropbutton.hasClass('open');
|
||||
this.$dropbutton.toggleClass('open', show);
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*/
|
||||
hoverIn: function () {
|
||||
// Clear any previous timer we were using.
|
||||
hoverIn: function hoverIn() {
|
||||
if (this.timerID) {
|
||||
window.clearTimeout(this.timerID);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*/
|
||||
hoverOut: function () {
|
||||
// Wait half a second before closing.
|
||||
hoverOut: function hoverOut() {
|
||||
this.timerID = window.setTimeout($.proxy(this, 'close'), 500);
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*/
|
||||
open: function () {
|
||||
open: function open() {
|
||||
this.toggle(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*/
|
||||
close: function () {
|
||||
close: function close() {
|
||||
this.toggle(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
*/
|
||||
focusOut: function (e) {
|
||||
focusOut: function focusOut(e) {
|
||||
this.hoverOut.call(this, e);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
*/
|
||||
focusIn: function (e) {
|
||||
focusIn: function 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: function (options) {
|
||||
$.extend(Drupal.theme, {
|
||||
dropbuttonToggle: function 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);
|
||||
@@ -0,0 +1,581 @@
|
||||
/**
|
||||
* @file
|
||||
* Defines the Drupal JavaScript API.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A jQuery object, typically the return value from a `$(selector)` call.
|
||||
*
|
||||
* Holds an HTMLElement or a collection of HTMLElements.
|
||||
*
|
||||
* @typedef {object} jQuery
|
||||
*
|
||||
* @prop {number} length=0
|
||||
* Number of elements contained in the jQuery object.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Variable generated by Drupal that holds all translated strings from PHP.
|
||||
*
|
||||
* Content of this variable is automatically created by Drupal when using the
|
||||
* Interface Translation module. It holds the translation of strings used on
|
||||
* the page.
|
||||
*
|
||||
* This variable is used to pass data from the backend to the frontend. Data
|
||||
* contained in `drupalSettings` is used during behavior initialization.
|
||||
*
|
||||
* @global
|
||||
*
|
||||
* @var {object} drupalTranslations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Global Drupal object.
|
||||
*
|
||||
* All Drupal JavaScript APIs are contained in this namespace.
|
||||
*
|
||||
* @global
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
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) {
|
||||
/**
|
||||
* Helper to rethrow errors asynchronously.
|
||||
*
|
||||
* This way Errors bubbles up outside of the original callstack, making it
|
||||
* easier to debug errors in the browser.
|
||||
*
|
||||
* @param {Error|string} error
|
||||
* The error to be thrown.
|
||||
*/
|
||||
Drupal.throwError = function (error) {
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom error thrown after attach/detach if one or more behaviors failed.
|
||||
* Initializes the JavaScript behaviors for page loads and Ajax requests.
|
||||
*
|
||||
* @callback Drupal~behaviorAttach
|
||||
*
|
||||
* @param {HTMLDocument|HTMLElement} context
|
||||
* An element to detach behaviors from.
|
||||
* @param {?object} settings
|
||||
* An object containing settings for the current context. It is rarely used.
|
||||
*
|
||||
* @see Drupal.attachBehaviors
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reverts and cleans up JavaScript behavior initialization.
|
||||
*
|
||||
* @callback Drupal~behaviorDetach
|
||||
*
|
||||
* @param {HTMLDocument|HTMLElement} context
|
||||
* An element to attach behaviors to.
|
||||
* @param {object} settings
|
||||
* An object containing settings for the current context.
|
||||
* @param {string} trigger
|
||||
* One of `'unload'`, `'move'`, or `'serialize'`.
|
||||
*
|
||||
* @see Drupal.detachBehaviors
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Drupal~behavior
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Function run on page load and after an Ajax call.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Function run when content is serialized or removed from the page.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Holds all initialization methods.
|
||||
*
|
||||
* @namespace Drupal.behaviors
|
||||
*
|
||||
* @type {Object.<string, Drupal~behavior>}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines a behavior to be run during attach and detach phases.
|
||||
*
|
||||
* Attaches all registered behaviors to a page element.
|
||||
*
|
||||
* Behaviors are event-triggered actions that attach to page elements,
|
||||
* enhancing default non-JavaScript UIs. Behaviors are registered in the
|
||||
* {@link Drupal.behaviors} object using the method 'attach' and optionally
|
||||
* also 'detach'.
|
||||
*
|
||||
* {@link Drupal.attachBehaviors} is added below to the `jQuery.ready` event
|
||||
* and therefore runs on initial page load. Developers implementing Ajax in
|
||||
* their solutions should also call this function after new page content has
|
||||
* been loaded, feeding in an element to be processed, in order to attach all
|
||||
* behaviors to the new content.
|
||||
*
|
||||
* Behaviors should use `var elements =
|
||||
* $(context).find(selector).once('behavior-name');` to ensure the behavior is
|
||||
* attached only once to a given element. (Doing so enables the reprocessing
|
||||
* of given elements, which may be needed on occasion despite the ability to
|
||||
* limit behavior attachment to a particular element.)
|
||||
*
|
||||
* @example
|
||||
* Drupal.behaviors.behaviorName = {
|
||||
* attach: function (context, settings) {
|
||||
* // ...
|
||||
* },
|
||||
* detach: function (context, settings, trigger) {
|
||||
* // ...
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* @param {HTMLDocument|HTMLElement} [context=document]
|
||||
* An element to attach behaviors to.
|
||||
* @param {object} [settings=drupalSettings]
|
||||
* An object containing settings for the current context. If none is given,
|
||||
* the global {@link drupalSettings} object is used.
|
||||
*
|
||||
* @see Drupal~behaviorAttach
|
||||
* @see Drupal.detachBehaviors
|
||||
*
|
||||
* @throws {Drupal~DrupalBehaviorError}
|
||||
*/
|
||||
Drupal.attachBehaviors = function (context, settings) {
|
||||
context = context || document;
|
||||
settings = settings || drupalSettings;
|
||||
const behaviors = Drupal.behaviors;
|
||||
// Execute all of them.
|
||||
for (const i in behaviors) {
|
||||
if (behaviors.hasOwnProperty(i) && 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) {
|
||||
Drupal.throwError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches registered behaviors from a page element.
|
||||
*
|
||||
* Developers implementing Ajax in their solutions should call this function
|
||||
* before page content is about to be removed, feeding in an element to be
|
||||
* processed, in order to allow special behaviors to detach from the content.
|
||||
*
|
||||
* Such implementations should use `.findOnce()` and `.removeOnce()` to find
|
||||
* elements with their corresponding `Drupal.behaviors.behaviorName.attach`
|
||||
* implementation, i.e. `.removeOnce('behaviorName')`, to ensure the behavior
|
||||
* is detached only from previously processed elements.
|
||||
*
|
||||
* @param {HTMLDocument|HTMLElement} [context=document]
|
||||
* An element to detach behaviors from.
|
||||
* @param {object} [settings=drupalSettings]
|
||||
* An object containing settings for the current context. If none given,
|
||||
* the global {@link drupalSettings} object is used.
|
||||
* @param {string} [trigger='unload']
|
||||
* A string containing what's causing the behaviors to be detached. The
|
||||
* possible triggers are:
|
||||
* - `'unload'`: The context element is being removed from the DOM.
|
||||
* - `'move'`: The element is about to be moved within the DOM (for example,
|
||||
* during a tabledrag row swap). After the move is completed,
|
||||
* {@link Drupal.attachBehaviors} is called, so that the behavior can undo
|
||||
* whatever it did in response to the move. Many behaviors won't need to
|
||||
* do anything simply in response to the element being moved, but because
|
||||
* IFRAME elements reload their "src" when being moved within the DOM,
|
||||
* behaviors bound to IFRAME elements (like WYSIWYG editors) may need to
|
||||
* take some action.
|
||||
* - `'serialize'`: When an Ajax form is submitted, this is called with the
|
||||
* form as the context. This provides every behavior within the form an
|
||||
* opportunity to ensure that the field elements have correct content
|
||||
* in them before the form is serialized. The canonical use-case is so
|
||||
* that WYSIWYG editors can update the hidden textarea to which they are
|
||||
* bound.
|
||||
*
|
||||
* @throws {Drupal~DrupalBehaviorError}
|
||||
*
|
||||
* @see Drupal~behaviorDetach
|
||||
* @see Drupal.attachBehaviors
|
||||
*/
|
||||
Drupal.detachBehaviors = function (context, settings, trigger) {
|
||||
context = context || document;
|
||||
settings = settings || drupalSettings;
|
||||
trigger = trigger || 'unload';
|
||||
const behaviors = Drupal.behaviors;
|
||||
// Execute all of them.
|
||||
for (const i in behaviors) {
|
||||
if (behaviors.hasOwnProperty(i) && 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) {
|
||||
Drupal.throwError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Encodes special characters in a plain-text string for display as HTML.
|
||||
*
|
||||
* @param {string} str
|
||||
* The string to be encoded.
|
||||
*
|
||||
* @return {string}
|
||||
* The encoded string.
|
||||
*
|
||||
* @ingroup sanitization
|
||||
*/
|
||||
Drupal.checkPlain = function (str) {
|
||||
str = str.toString()
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces placeholders with sanitized values in a string.
|
||||
*
|
||||
* @param {string} str
|
||||
* A string with placeholders.
|
||||
* @param {object} args
|
||||
* An object of replacements pairs to make. Incidences of any key in this
|
||||
* array are replaced with the corresponding value. Based on the first
|
||||
* character of the key, the value is escaped and/or themed:
|
||||
* - `'!variable'`: inserted as is.
|
||||
* - `'@variable'`: escape plain text to HTML ({@link Drupal.checkPlain}).
|
||||
* - `'%variable'`: escape text and theme as a placeholder for user-
|
||||
* submitted content ({@link Drupal.checkPlain} +
|
||||
* `{@link Drupal.theme}('placeholder')`).
|
||||
*
|
||||
* @return {string}
|
||||
* The formatted string.
|
||||
*
|
||||
* @see Drupal.t
|
||||
*/
|
||||
Drupal.formatString = function (str, args) {
|
||||
// Keep args intact.
|
||||
const processedArgs = {};
|
||||
// Transform arguments before inserting them.
|
||||
for (const key in args) {
|
||||
if (args.hasOwnProperty(key)) {
|
||||
switch (key.charAt(0)) {
|
||||
// Escaped only.
|
||||
case '@':
|
||||
processedArgs[key] = Drupal.checkPlain(args[key]);
|
||||
break;
|
||||
|
||||
// Pass-through.
|
||||
case '!':
|
||||
processedArgs[key] = args[key];
|
||||
break;
|
||||
|
||||
// Escaped and placeholder.
|
||||
default:
|
||||
processedArgs[key] = Drupal.theme('placeholder', args[key]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Drupal.stringReplace(str, processedArgs, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces substring.
|
||||
*
|
||||
* The longest keys will be tried first. Once a substring has been replaced,
|
||||
* its new value will not be searched again.
|
||||
*
|
||||
* @param {string} str
|
||||
* A string with placeholders.
|
||||
* @param {object} args
|
||||
* Key-value pairs.
|
||||
* @param {Array|null} keys
|
||||
* Array of keys from `args`. Internal use only.
|
||||
*
|
||||
* @return {string}
|
||||
* The replaced string.
|
||||
*/
|
||||
Drupal.stringReplace = function (str, args, keys) {
|
||||
if (str.length === 0) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// If the array of keys is not passed then collect the keys from the args.
|
||||
if (!Array.isArray(keys)) {
|
||||
keys = [];
|
||||
for (const k in args) {
|
||||
if (args.hasOwnProperty(k)) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
|
||||
// Order the keys by the character length. The shortest one is the first.
|
||||
keys.sort((a, b) => a.length - b.length);
|
||||
}
|
||||
|
||||
if (keys.length === 0) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// Take next longest one from the end.
|
||||
const key = keys.pop();
|
||||
const fragments = str.split(key);
|
||||
|
||||
if (keys.length) {
|
||||
for (let i = 0; i < fragments.length; i++) {
|
||||
// Process each fragment with a copy of remaining keys.
|
||||
fragments[i] = Drupal.stringReplace(fragments[i], args, keys.slice(0));
|
||||
}
|
||||
}
|
||||
|
||||
return fragments.join(args[key]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates strings to the page language, or a given language.
|
||||
*
|
||||
* See the documentation of the server-side t() function for further details.
|
||||
*
|
||||
* @param {string} str
|
||||
* A string containing the English text to translate.
|
||||
* @param {Object.<string, string>} [args]
|
||||
* An object of replacements pairs to make after translation. Incidences
|
||||
* of any key in this array are replaced with the corresponding value.
|
||||
* See {@link Drupal.formatString}.
|
||||
* @param {object} [options]
|
||||
* Additional options for translation.
|
||||
* @param {string} [options.context='']
|
||||
* The context the source string belongs to.
|
||||
*
|
||||
* @return {string}
|
||||
* The formatted string.
|
||||
* The translated string.
|
||||
*/
|
||||
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]) {
|
||||
str = drupalTranslations.strings[options.context][str];
|
||||
}
|
||||
|
||||
if (args) {
|
||||
str = Drupal.formatString(str, args);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the URL to a Drupal page.
|
||||
*
|
||||
* @param {string} path
|
||||
* Drupal path to transform to URL.
|
||||
*
|
||||
* @return {string}
|
||||
* The full URL.
|
||||
*/
|
||||
Drupal.url = function (path) {
|
||||
return drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix + path;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the passed in URL as an absolute URL.
|
||||
*
|
||||
* @param {string} url
|
||||
* The URL string to be normalized to an absolute URL.
|
||||
*
|
||||
* @return {string}
|
||||
* The normalized, absolute URL.
|
||||
*
|
||||
* @see https://github.com/angular/angular.js/blob/v1.4.4/src/ng/urlUtils.js
|
||||
* @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) {
|
||||
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) {
|
||||
// Empty.
|
||||
}
|
||||
|
||||
urlParsingNode.setAttribute('href', url);
|
||||
|
||||
// IE <= 7 normalizes the URL when assigned to the anchor node similar to
|
||||
// the other browsers.
|
||||
return urlParsingNode.cloneNode(false).href;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the URL is within Drupal's base path.
|
||||
*
|
||||
* @param {string} url
|
||||
* The URL string to be tested.
|
||||
*
|
||||
* @return {bool}
|
||||
* `true` if local.
|
||||
*
|
||||
* @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L58
|
||||
*/
|
||||
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;
|
||||
|
||||
// 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)}`;
|
||||
|
||||
// Decoding non-UTF-8 strings may throw an exception.
|
||||
try {
|
||||
absoluteUrl = decodeURIComponent(absoluteUrl);
|
||||
}
|
||||
catch (e) {
|
||||
// Empty.
|
||||
}
|
||||
try {
|
||||
baseUrl = decodeURIComponent(baseUrl);
|
||||
}
|
||||
catch (e) {
|
||||
// Empty.
|
||||
}
|
||||
|
||||
// The given URL matches the site's base URL, or has a path under the site's
|
||||
// base URL.
|
||||
return absoluteUrl === baseUrl || absoluteUrl.indexOf(`${baseUrl}/`) === 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a string containing a count of items.
|
||||
*
|
||||
* This function ensures that the string is pluralized correctly. Since
|
||||
* {@link Drupal.t} is called by this function, make sure not to pass
|
||||
* already-localized strings to it.
|
||||
*
|
||||
* See the documentation of the server-side
|
||||
* \Drupal\Core\StringTranslation\TranslationInterface::formatPlural()
|
||||
* function for more details.
|
||||
*
|
||||
* @param {number} count
|
||||
* The item count to display.
|
||||
* @param {string} singular
|
||||
* The string for the singular case. Please make sure it is clear this is
|
||||
* singular, to ease translation (e.g. use "1 new comment" instead of "1
|
||||
* new"). Do not use @count in the singular string.
|
||||
* @param {string} plural
|
||||
* The string for the plural case. Please make sure it is clear this is
|
||||
* plural, to ease translation. Use @count in place of the item count, as in
|
||||
* "@count new comments".
|
||||
* @param {object} [args]
|
||||
* An object of replacements pairs to make after translation. Incidences
|
||||
* of any key in this array are replaced with the corresponding value.
|
||||
* See {@link Drupal.formatString}.
|
||||
* Note that you do not need to include @count in this array.
|
||||
* This replacement is done automatically for the plural case.
|
||||
* @param {object} [options]
|
||||
* The options to pass to the {@link Drupal.t} function.
|
||||
*
|
||||
* @return {string}
|
||||
* A translated string.
|
||||
*/
|
||||
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);
|
||||
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) {
|
||||
index = 1;
|
||||
}
|
||||
|
||||
return translations[index];
|
||||
};
|
||||
|
||||
/**
|
||||
* Encodes a Drupal path for use in a URL.
|
||||
*
|
||||
* For aesthetic reasons slashes are not escaped.
|
||||
*
|
||||
* @param {string} item
|
||||
* Unencoded path.
|
||||
*
|
||||
* @return {string}
|
||||
* The encoded path.
|
||||
*/
|
||||
Drupal.encodePath = function (item) {
|
||||
return window.encodeURIComponent(item).replace(/%2F/g, '/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the themed representation of a Drupal object.
|
||||
*
|
||||
* All requests for themed output must go through this function. It examines
|
||||
* the request and routes it to the appropriate theme function. If the current
|
||||
* theme does not provide an override function, the generic theme function is
|
||||
* called.
|
||||
*
|
||||
* @example
|
||||
* <caption>To retrieve the HTML for text that should be emphasized and
|
||||
* displayed as a placeholder inside a sentence.</caption>
|
||||
* Drupal.theme('placeholder', text);
|
||||
*
|
||||
* @namespace
|
||||
*
|
||||
* @param {function} func
|
||||
* The name of the theme function to call.
|
||||
* @param {...args}
|
||||
* Additional arguments to pass along to the theme function.
|
||||
*
|
||||
* @return {string|object|HTMLElement|jQuery}
|
||||
* Any data the theme function returns. This could be a plain HTML string,
|
||||
* but also a complex object.
|
||||
*/
|
||||
Drupal.theme = function (func) {
|
||||
const args = Array.prototype.slice.apply(arguments, [1]);
|
||||
if (func in Drupal.theme) {
|
||||
return Drupal.theme[func].apply(this, args);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats text for emphasized display in a placeholder inside a sentence.
|
||||
*
|
||||
* @param {string} str
|
||||
* The text to format (plain-text).
|
||||
*
|
||||
* @return {string}
|
||||
* The formatted text (html).
|
||||
*/
|
||||
Drupal.theme.placeholder = function (str) {
|
||||
return `<em class="placeholder">${Drupal.checkPlain(str)}</em>`;
|
||||
};
|
||||
}(Drupal, window.drupalSettings, window.drupalTranslations));
|
||||
@@ -0,0 +1,17 @@
|
||||
// Allow other JavaScript libraries to use $.
|
||||
if (window.jQuery) {
|
||||
jQuery.noConflict();
|
||||
}
|
||||
|
||||
// Class indicating that JS is enabled; used for styling purpose.
|
||||
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) {
|
||||
// Attach all behaviors.
|
||||
domready(() => {
|
||||
Drupal.attachBehaviors(document, drupalSettings);
|
||||
});
|
||||
}(domready, Drupal, window.drupalSettings));
|
||||
+11
-12
@@ -1,19 +1,18 @@
|
||||
// Allow other JavaScript libraries to use $.
|
||||
/**
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
if (window.jQuery) {
|
||||
jQuery.noConflict();
|
||||
}
|
||||
|
||||
// Class indicating that JS is enabled; used for styling purpose.
|
||||
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) {
|
||||
|
||||
'use strict';
|
||||
|
||||
// Attach all behaviors.
|
||||
domready(function () { Drupal.attachBehaviors(document, drupalSettings); });
|
||||
|
||||
})(domready, Drupal, window.drupalSettings);
|
||||
domready(function () {
|
||||
Drupal.attachBehaviors(document, drupalSettings);
|
||||
});
|
||||
})(domready, Drupal, window.drupalSettings);
|
||||
+24
-414
@@ -1,289 +1,71 @@
|
||||
/**
|
||||
* @file
|
||||
* Defines the Drupal JavaScript API.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
/**
|
||||
* A jQuery object, typically the return value from a `$(selector)` call.
|
||||
*
|
||||
* Holds an HTMLElement or a collection of HTMLElements.
|
||||
*
|
||||
* @typedef {object} jQuery
|
||||
*
|
||||
* @prop {number} length=0
|
||||
* Number of elements contained in the jQuery object.
|
||||
*/
|
||||
window.Drupal = { behaviors: {}, locale: {} };
|
||||
|
||||
/**
|
||||
* Variable generated by Drupal that holds all translated strings from PHP.
|
||||
*
|
||||
* Content of this variable is automatically created by Drupal when using the
|
||||
* Interface Translation module. It holds the translation of strings used on
|
||||
* the page.
|
||||
*
|
||||
* This variable is used to pass data from the backend to the frontend. Data
|
||||
* contained in `drupalSettings` is used during behavior initialization.
|
||||
*
|
||||
* @global
|
||||
*
|
||||
* @var {object} drupalTranslations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Global Drupal object.
|
||||
*
|
||||
* All Drupal JavaScript APIs are contained in this namespace.
|
||||
*
|
||||
* @global
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
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) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Helper to rethrow errors asynchronously.
|
||||
*
|
||||
* This way Errors bubbles up outside of the original callstack, making it
|
||||
* easier to debug errors in the browser.
|
||||
*
|
||||
* @param {Error|string} error
|
||||
* The error to be thrown.
|
||||
*/
|
||||
Drupal.throwError = function (error) {
|
||||
setTimeout(function () { throw error; }, 0);
|
||||
setTimeout(function () {
|
||||
throw error;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom error thrown after attach/detach if one or more behaviors failed.
|
||||
* Initializes the JavaScript behaviors for page loads and Ajax requests.
|
||||
*
|
||||
* @callback Drupal~behaviorAttach
|
||||
*
|
||||
* @param {HTMLDocument|HTMLElement} context
|
||||
* An element to detach behaviors from.
|
||||
* @param {?object} settings
|
||||
* An object containing settings for the current context. It is rarely used.
|
||||
*
|
||||
* @see Drupal.attachBehaviors
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reverts and cleans up JavaScript behavior initialization.
|
||||
*
|
||||
* @callback Drupal~behaviorDetach
|
||||
*
|
||||
* @param {HTMLDocument|HTMLElement} context
|
||||
* An element to attach behaviors to.
|
||||
* @param {object} settings
|
||||
* An object containing settings for the current context.
|
||||
* @param {string} trigger
|
||||
* One of `'unload'`, `'move'`, or `'serialize'`.
|
||||
*
|
||||
* @see Drupal.detachBehaviors
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Drupal~behavior
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Function run on page load and after an Ajax call.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Function run when content is serialized or removed from the page.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Holds all initialization methods.
|
||||
*
|
||||
* @namespace Drupal.behaviors
|
||||
*
|
||||
* @type {Object.<string, Drupal~behavior>}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines a behavior to be run during attach and detach phases.
|
||||
*
|
||||
* Attaches all registered behaviors to a page element.
|
||||
*
|
||||
* Behaviors are event-triggered actions that attach to page elements,
|
||||
* enhancing default non-JavaScript UIs. Behaviors are registered in the
|
||||
* {@link Drupal.behaviors} object using the method 'attach' and optionally
|
||||
* also 'detach'.
|
||||
*
|
||||
* {@link Drupal.attachBehaviors} is added below to the `jQuery.ready` event
|
||||
* and therefore runs on initial page load. Developers implementing Ajax in
|
||||
* their solutions should also call this function after new page content has
|
||||
* been loaded, feeding in an element to be processed, in order to attach all
|
||||
* behaviors to the new content.
|
||||
*
|
||||
* Behaviors should use `var elements =
|
||||
* $(context).find(selector).once('behavior-name');` to ensure the behavior is
|
||||
* attached only once to a given element. (Doing so enables the reprocessing
|
||||
* of given elements, which may be needed on occasion despite the ability to
|
||||
* limit behavior attachment to a particular element.)
|
||||
*
|
||||
* @example
|
||||
* Drupal.behaviors.behaviorName = {
|
||||
* attach: function (context, settings) {
|
||||
* // ...
|
||||
* },
|
||||
* detach: function (context, settings, trigger) {
|
||||
* // ...
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* @param {HTMLDocument|HTMLElement} [context=document]
|
||||
* An element to attach behaviors to.
|
||||
* @param {object} [settings=drupalSettings]
|
||||
* An object containing settings for the current context. If none is given,
|
||||
* the global {@link drupalSettings} object is used.
|
||||
*
|
||||
* @see Drupal~behaviorAttach
|
||||
* @see Drupal.detachBehaviors
|
||||
*
|
||||
* @throws {Drupal~DrupalBehaviorError}
|
||||
*/
|
||||
Drupal.attachBehaviors = function (context, settings) {
|
||||
context = context || document;
|
||||
settings = settings || drupalSettings;
|
||||
var behaviors = Drupal.behaviors;
|
||||
// Execute all of them.
|
||||
|
||||
for (var i in behaviors) {
|
||||
if (behaviors.hasOwnProperty(i) && 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches registered behaviors from a page element.
|
||||
*
|
||||
* Developers implementing Ajax in their solutions should call this function
|
||||
* before page content is about to be removed, feeding in an element to be
|
||||
* processed, in order to allow special behaviors to detach from the content.
|
||||
*
|
||||
* Such implementations should use `.findOnce()` and `.removeOnce()` to find
|
||||
* elements with their corresponding `Drupal.behaviors.behaviorName.attach`
|
||||
* implementation, i.e. `.removeOnce('behaviorName')`, to ensure the behavior
|
||||
* is detached only from previously processed elements.
|
||||
*
|
||||
* @param {HTMLDocument|HTMLElement} [context=document]
|
||||
* An element to detach behaviors from.
|
||||
* @param {object} [settings=drupalSettings]
|
||||
* An object containing settings for the current context. If none given,
|
||||
* the global {@link drupalSettings} object is used.
|
||||
* @param {string} [trigger='unload']
|
||||
* A string containing what's causing the behaviors to be detached. The
|
||||
* possible triggers are:
|
||||
* - `'unload'`: The context element is being removed from the DOM.
|
||||
* - `'move'`: The element is about to be moved within the DOM (for example,
|
||||
* during a tabledrag row swap). After the move is completed,
|
||||
* {@link Drupal.attachBehaviors} is called, so that the behavior can undo
|
||||
* whatever it did in response to the move. Many behaviors won't need to
|
||||
* do anything simply in response to the element being moved, but because
|
||||
* IFRAME elements reload their "src" when being moved within the DOM,
|
||||
* behaviors bound to IFRAME elements (like WYSIWYG editors) may need to
|
||||
* take some action.
|
||||
* - `'serialize'`: When an Ajax form is submitted, this is called with the
|
||||
* form as the context. This provides every behavior within the form an
|
||||
* opportunity to ensure that the field elements have correct content
|
||||
* in them before the form is serialized. The canonical use-case is so
|
||||
* that WYSIWYG editors can update the hidden textarea to which they are
|
||||
* bound.
|
||||
*
|
||||
* @throws {Drupal~DrupalBehaviorError}
|
||||
*
|
||||
* @see Drupal~behaviorDetach
|
||||
* @see Drupal.attachBehaviors
|
||||
*/
|
||||
Drupal.detachBehaviors = function (context, settings, trigger) {
|
||||
context = context || document;
|
||||
settings = settings || drupalSettings;
|
||||
trigger = trigger || 'unload';
|
||||
var behaviors = Drupal.behaviors;
|
||||
// Execute all of them.
|
||||
|
||||
for (var i in behaviors) {
|
||||
if (behaviors.hasOwnProperty(i) && 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Encodes special characters in a plain-text string for display as HTML.
|
||||
*
|
||||
* @param {string} str
|
||||
* The string to be encoded.
|
||||
*
|
||||
* @return {string}
|
||||
* The encoded string.
|
||||
*
|
||||
* @ingroup sanitization
|
||||
*/
|
||||
Drupal.checkPlain = function (str) {
|
||||
str = str.toString()
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
str = str.toString().replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces placeholders with sanitized values in a string.
|
||||
*
|
||||
* @param {string} str
|
||||
* A string with placeholders.
|
||||
* @param {object} args
|
||||
* An object of replacements pairs to make. Incidences of any key in this
|
||||
* array are replaced with the corresponding value. Based on the first
|
||||
* character of the key, the value is escaped and/or themed:
|
||||
* - `'!variable'`: inserted as is.
|
||||
* - `'@variable'`: escape plain text to HTML ({@link Drupal.checkPlain}).
|
||||
* - `'%variable'`: escape text and theme as a placeholder for user-
|
||||
* submitted content ({@link Drupal.checkPlain} +
|
||||
* `{@link Drupal.theme}('placeholder')`).
|
||||
*
|
||||
* @return {string}
|
||||
* The formatted string.
|
||||
*
|
||||
* @see Drupal.t
|
||||
*/
|
||||
Drupal.formatString = function (str, args) {
|
||||
// Keep args intact.
|
||||
var processedArgs = {};
|
||||
// Transform arguments before inserting them.
|
||||
|
||||
for (var key in args) {
|
||||
if (args.hasOwnProperty(key)) {
|
||||
switch (key.charAt(0)) {
|
||||
// Escaped only.
|
||||
case '@':
|
||||
processedArgs[key] = Drupal.checkPlain(args[key]);
|
||||
break;
|
||||
|
||||
// Pass-through.
|
||||
case '!':
|
||||
processedArgs[key] = args[key];
|
||||
break;
|
||||
|
||||
// Escaped and placeholder.
|
||||
default:
|
||||
processedArgs[key] = Drupal.theme('placeholder', args[key]);
|
||||
break;
|
||||
@@ -294,28 +76,11 @@ window.Drupal = {behaviors: {}, locale: {}};
|
||||
return Drupal.stringReplace(str, processedArgs, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces substring.
|
||||
*
|
||||
* The longest keys will be tried first. Once a substring has been replaced,
|
||||
* its new value will not be searched again.
|
||||
*
|
||||
* @param {string} str
|
||||
* A string with placeholders.
|
||||
* @param {object} args
|
||||
* Key-value pairs.
|
||||
* @param {Array|null} keys
|
||||
* Array of keys from `args`. Internal use only.
|
||||
*
|
||||
* @return {string}
|
||||
* The replaced string.
|
||||
*/
|
||||
Drupal.stringReplace = function (str, args, keys) {
|
||||
if (str.length === 0) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// If the array of keys is not passed then collect the keys from the args.
|
||||
if (!Array.isArray(keys)) {
|
||||
keys = [];
|
||||
for (var k in args) {
|
||||
@@ -324,21 +89,20 @@ window.Drupal = {behaviors: {}, locale: {}};
|
||||
}
|
||||
}
|
||||
|
||||
// Order the keys by the character length. The shortest one is the first.
|
||||
keys.sort(function (a, b) { return a.length - b.length; });
|
||||
keys.sort(function (a, b) {
|
||||
return a.length - b.length;
|
||||
});
|
||||
}
|
||||
|
||||
if (keys.length === 0) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// Take next longest one from the end.
|
||||
var key = keys.pop();
|
||||
var fragments = str.split(key);
|
||||
|
||||
if (keys.length) {
|
||||
for (var i = 0; i < fragments.length; i++) {
|
||||
// Process each fragment with a copy of remaining keys.
|
||||
fragments[i] = Drupal.stringReplace(fragments[i], args, keys.slice(0));
|
||||
}
|
||||
}
|
||||
@@ -346,31 +110,10 @@ window.Drupal = {behaviors: {}, locale: {}};
|
||||
return fragments.join(args[key]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates strings to the page language, or a given language.
|
||||
*
|
||||
* See the documentation of the server-side t() function for further details.
|
||||
*
|
||||
* @param {string} str
|
||||
* A string containing the English text to translate.
|
||||
* @param {Object.<string, string>} [args]
|
||||
* An object of replacements pairs to make after translation. Incidences
|
||||
* of any key in this array are replaced with the corresponding value.
|
||||
* See {@link Drupal.formatString}.
|
||||
* @param {object} [options]
|
||||
* Additional options for translation.
|
||||
* @param {string} [options.context='']
|
||||
* The context the source string belongs to.
|
||||
*
|
||||
* @return {string}
|
||||
* The formatted string.
|
||||
* The translated string.
|
||||
*/
|
||||
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]) {
|
||||
str = drupalTranslations.strings[options.context][str];
|
||||
}
|
||||
@@ -381,127 +124,41 @@ window.Drupal = {behaviors: {}, locale: {}};
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the URL to a Drupal page.
|
||||
*
|
||||
* @param {string} path
|
||||
* Drupal path to transform to URL.
|
||||
*
|
||||
* @return {string}
|
||||
* The full URL.
|
||||
*/
|
||||
Drupal.url = function (path) {
|
||||
return drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix + path;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the passed in URL as an absolute URL.
|
||||
*
|
||||
* @param {string} url
|
||||
* The URL string to be normalized to an absolute URL.
|
||||
*
|
||||
* @return {string}
|
||||
* The normalized, absolute URL.
|
||||
*
|
||||
* @see https://github.com/angular/angular.js/blob/v1.4.4/src/ng/urlUtils.js
|
||||
* @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) {
|
||||
var 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) {
|
||||
// Empty.
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
urlParsingNode.setAttribute('href', url);
|
||||
|
||||
// IE <= 7 normalizes the URL when assigned to the anchor node similar to
|
||||
// the other browsers.
|
||||
return urlParsingNode.cloneNode(false).href;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the URL is within Drupal's base path.
|
||||
*
|
||||
* @param {string} url
|
||||
* The URL string to be tested.
|
||||
*
|
||||
* @return {bool}
|
||||
* `true` if local.
|
||||
*
|
||||
* @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L58
|
||||
*/
|
||||
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.
|
||||
var absoluteUrl = Drupal.url.toAbsolute(url);
|
||||
var protocol = location.protocol;
|
||||
|
||||
// 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:';
|
||||
}
|
||||
var baseUrl = protocol + '//' + location.host + drupalSettings.path.baseUrl.slice(0, -1);
|
||||
|
||||
// Decoding non-UTF-8 strings may throw an exception.
|
||||
try {
|
||||
absoluteUrl = decodeURIComponent(absoluteUrl);
|
||||
}
|
||||
catch (e) {
|
||||
// Empty.
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
baseUrl = decodeURIComponent(baseUrl);
|
||||
}
|
||||
catch (e) {
|
||||
// Empty.
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// The given URL matches the site's base URL, or has a path under the site's
|
||||
// base URL.
|
||||
return absoluteUrl === baseUrl || absoluteUrl.indexOf(baseUrl + '/') === 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a string containing a count of items.
|
||||
*
|
||||
* This function ensures that the string is pluralized correctly. Since
|
||||
* {@link Drupal.t} is called by this function, make sure not to pass
|
||||
* already-localized strings to it.
|
||||
*
|
||||
* See the documentation of the server-side
|
||||
* \Drupal\Core\StringTranslation\TranslationInterface::formatPlural()
|
||||
* function for more details.
|
||||
*
|
||||
* @param {number} count
|
||||
* The item count to display.
|
||||
* @param {string} singular
|
||||
* The string for the singular case. Please make sure it is clear this is
|
||||
* singular, to ease translation (e.g. use "1 new comment" instead of "1
|
||||
* new"). Do not use @count in the singular string.
|
||||
* @param {string} plural
|
||||
* The string for the plural case. Please make sure it is clear this is
|
||||
* plural, to ease translation. Use @count in place of the item count, as in
|
||||
* "@count new comments".
|
||||
* @param {object} [args]
|
||||
* An object of replacements pairs to make after translation. Incidences
|
||||
* of any key in this array are replaced with the corresponding value.
|
||||
* See {@link Drupal.formatString}.
|
||||
* Note that you do not need to include @count in this array.
|
||||
* This replacement is done automatically for the plural case.
|
||||
* @param {object} [options]
|
||||
* The options to pass to the {@link Drupal.t} function.
|
||||
*
|
||||
* @return {string}
|
||||
* A translated string.
|
||||
*/
|
||||
Drupal.formatPlural = function (count, singular, plural, args, options) {
|
||||
args = args || {};
|
||||
args['@count'] = count;
|
||||
@@ -510,56 +167,19 @@ window.Drupal = {behaviors: {}, locale: {}};
|
||||
var translations = Drupal.t(singular + pluralDelimiter + plural, args, options).split(pluralDelimiter);
|
||||
var 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) {
|
||||
index = count in drupalTranslations.pluralFormula ? drupalTranslations.pluralFormula[count] : drupalTranslations.pluralFormula.default;
|
||||
} else if (args['@count'] !== 1) {
|
||||
index = 1;
|
||||
}
|
||||
|
||||
return translations[index];
|
||||
};
|
||||
|
||||
/**
|
||||
* Encodes a Drupal path for use in a URL.
|
||||
*
|
||||
* For aesthetic reasons slashes are not escaped.
|
||||
*
|
||||
* @param {string} item
|
||||
* Unencoded path.
|
||||
*
|
||||
* @return {string}
|
||||
* The encoded path.
|
||||
*/
|
||||
Drupal.encodePath = function (item) {
|
||||
return window.encodeURIComponent(item).replace(/%2F/g, '/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the themed representation of a Drupal object.
|
||||
*
|
||||
* All requests for themed output must go through this function. It examines
|
||||
* the request and routes it to the appropriate theme function. If the current
|
||||
* theme does not provide an override function, the generic theme function is
|
||||
* called.
|
||||
*
|
||||
* @example
|
||||
* <caption>To retrieve the HTML for text that should be emphasized and
|
||||
* displayed as a placeholder inside a sentence.</caption>
|
||||
* Drupal.theme('placeholder', text);
|
||||
*
|
||||
* @namespace
|
||||
*
|
||||
* @param {function} func
|
||||
* The name of the theme function to call.
|
||||
* @param {...args}
|
||||
* Additional arguments to pass along to the theme function.
|
||||
*
|
||||
* @return {string|object|HTMLElement|jQuery}
|
||||
* Any data the theme function returns. This could be a plain HTML string,
|
||||
* but also a complex object.
|
||||
*/
|
||||
Drupal.theme = function (func) {
|
||||
var args = Array.prototype.slice.apply(arguments, [1]);
|
||||
if (func in Drupal.theme) {
|
||||
@@ -567,17 +187,7 @@ window.Drupal = {behaviors: {}, locale: {}};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats text for emphasized display in a placeholder inside a sentence.
|
||||
*
|
||||
* @param {string} str
|
||||
* The text to format (plain-text).
|
||||
*
|
||||
* @return {string}
|
||||
* The formatted text (html).
|
||||
*/
|
||||
Drupal.theme.placeholder = function (str) {
|
||||
return '<em class="placeholder">' + Drupal.checkPlain(str) + '</em>';
|
||||
};
|
||||
|
||||
})(Drupal, window.drupalSettings, window.drupalTranslations);
|
||||
})(Drupal, window.drupalSettings, window.drupalTranslations);
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @file
|
||||
* Parse inline JSON and initialize the drupalSettings global object.
|
||||
*/
|
||||
|
||||
(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"]');
|
||||
|
||||
/**
|
||||
* Variable generated by Drupal with all the configuration created from PHP.
|
||||
*
|
||||
* @global
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
window.drupalSettings = {};
|
||||
|
||||
if (settingsElement !== null) {
|
||||
window.drupalSettings = JSON.parse(settingsElement.textContent);
|
||||
}
|
||||
}());
|
||||
@@ -1,25 +1,16 @@
|
||||
/**
|
||||
* @file
|
||||
* Parse inline JSON and initialize the drupalSettings global object.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
// Use direct child elements to harden against XSS exploits when CSP is on.
|
||||
var 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.
|
||||
*
|
||||
* @global
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
window.drupalSettings = {};
|
||||
|
||||
if (settingsElement !== null) {
|
||||
window.drupalSettings = JSON.parse(settingsElement.textContent);
|
||||
}
|
||||
})();
|
||||
})();
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @file
|
||||
* Defines Javascript behaviors for the block_content module.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Sets summaries about revision and translation of entities.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches summary behaviour entity form tabs.
|
||||
*
|
||||
* Specifically, it updates summaries to the revision information and the
|
||||
* translation options.
|
||||
*/
|
||||
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');
|
||||
|
||||
// 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');
|
||||
});
|
||||
|
||||
$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');
|
||||
}
|
||||
|
||||
return translate;
|
||||
});
|
||||
},
|
||||
};
|
||||
}(jQuery, Drupal));
|
||||
+10
-28
@@ -1,35 +1,19 @@
|
||||
/**
|
||||
* @file
|
||||
* Defines Javascript behaviors for the block_content module.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Sets summaries about revision and translation of entities.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches summary behaviour entity form tabs.
|
||||
*
|
||||
* Specifically, it updates summaries to the revision information and the
|
||||
* translation options.
|
||||
*/
|
||||
Drupal.behaviors.entityContentDetailsSummaries = {
|
||||
attach: function (context) {
|
||||
attach: function attach(context) {
|
||||
var $context = $(context);
|
||||
$context.find('.entity-content-form-revision-information').drupalSetSummary(function (context) {
|
||||
var $revisionContext = $(context);
|
||||
var 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)) {
|
||||
if (revisionCheckbox.is(':checked') || !revisionCheckbox.length && $revisionContext.find('.js-form-item-revision-log textarea').length) {
|
||||
return Drupal.t('New revision');
|
||||
}
|
||||
|
||||
@@ -38,13 +22,12 @@
|
||||
|
||||
$context.find('details.entity-translation-options').drupalSetSummary(function (context) {
|
||||
var $translationContext = $(context);
|
||||
var translate;
|
||||
var translate = void 0;
|
||||
var $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 {
|
||||
} 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');
|
||||
}
|
||||
@@ -53,5 +36,4 @@
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* @file
|
||||
* Form features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggers when a value in the form changed.
|
||||
*
|
||||
* The event triggers when content is typed or pasted in a text field, before
|
||||
* the change event triggers.
|
||||
*
|
||||
* @event formUpdated
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggers when a click on a page fragment link or hash change is detected.
|
||||
*
|
||||
* The event triggers when the fragment in the URL changes (a hash change) and
|
||||
* when a link containing a fragment identifier is clicked. In case the hash
|
||||
* changes due to a click this event will only be triggered once.
|
||||
*
|
||||
* @event formFragmentLinkClickOrHashChange
|
||||
*/
|
||||
|
||||
(function ($, Drupal, debounce) {
|
||||
/**
|
||||
* Retrieves the summary for the first element.
|
||||
*
|
||||
* @return {string}
|
||||
* The text of the summary.
|
||||
*/
|
||||
$.fn.drupalGetSummary = function () {
|
||||
const callback = this.data('summaryCallback');
|
||||
return (this[0] && callback) ? $.trim(callback(this[0])) : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the summary for all matched elements.
|
||||
*
|
||||
* @param {function} callback
|
||||
* Either a function that will be called each time the summary is
|
||||
* retrieved or a string (which is returned each time).
|
||||
*
|
||||
* @return {jQuery}
|
||||
* jQuery collection of the current element.
|
||||
*
|
||||
* @fires event:summaryUpdated
|
||||
*
|
||||
* @listens event:formUpdated
|
||||
*/
|
||||
$.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 () {
|
||||
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');
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevents consecutive form submissions of identical form values.
|
||||
*
|
||||
* Repetitive form submissions that would submit the identical form values
|
||||
* are prevented, unless the form values are different to the previously
|
||||
* submitted values.
|
||||
*
|
||||
* This is a simplified re-implementation of a user-agent behavior that
|
||||
* should be natively supported by major web browsers, but at this time, only
|
||||
* Firefox has a built-in protection.
|
||||
*
|
||||
* A form value-based approach ensures that the constraint is triggered for
|
||||
* consecutive, identical form submissions only. Compared to that, a form
|
||||
* button-based approach would (1) rely on [visible] buttons to exist where
|
||||
* technically not required and (2) require more complex state management if
|
||||
* there are multiple buttons in a form.
|
||||
*
|
||||
* This implementation is based on form-level submit events only and relies
|
||||
* on jQuery's serialize() method to determine submitted form values. As such,
|
||||
* the following limitations exist:
|
||||
*
|
||||
* - Event handlers on form buttons that preventDefault() do not receive a
|
||||
* double-submit protection. That is deemed to be fine, since such button
|
||||
* events typically trigger reversible client-side or server-side
|
||||
* operations that are local to the context of a form only.
|
||||
* - Changed values in advanced form controls, such as file inputs, are not
|
||||
* part of the form values being compared between consecutive form submits
|
||||
* (due to limitations of jQuery.serialize()). That is deemed to be
|
||||
* acceptable, because if the user forgot to attach a file, then the size of
|
||||
* HTTP payload will most likely be small enough to be fully passed to the
|
||||
* server endpoint within (milli)seconds. If a user mistakenly attached a
|
||||
* wrong file and is technically versed enough to cancel the form submission
|
||||
* (and HTTP payload) in order to attach a different file, then that
|
||||
* edge-case is not supported here.
|
||||
*
|
||||
* Lastly, all forms submitted via HTTP GET are idempotent by definition of
|
||||
* HTTP standards, so excluded in this implementation.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.formSingleSubmit = {
|
||||
attach() {
|
||||
function onFormSubmit(e) {
|
||||
const $form = $(e.currentTarget);
|
||||
const formValues = $form.serialize();
|
||||
const previousValues = $form.attr('data-drupal-form-submit-last');
|
||||
if (previousValues === formValues) {
|
||||
e.preventDefault();
|
||||
}
|
||||
else {
|
||||
$form.attr('data-drupal-form-submit-last', formValues);
|
||||
}
|
||||
}
|
||||
|
||||
$('body').once('form-single-submit')
|
||||
.on('submit.singleSubmit', 'form:not([method~="GET"])', onFormSubmit);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a 'formUpdated' event each time a form element is modified.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to trigger a form updated event on.
|
||||
*
|
||||
* @fires event:formUpdated
|
||||
*/
|
||||
function triggerFormUpdated(element) {
|
||||
$(element).trigger('formUpdated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the IDs of all form fields in the given form.
|
||||
*
|
||||
* @param {HTMLFormElement} form
|
||||
* The form element to search.
|
||||
*
|
||||
* @return {Array}
|
||||
* 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'));
|
||||
// Return a true array.
|
||||
return $.makeArray($fieldList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the 'formUpdated' event on form elements when they are modified.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches formUpdated behaviors.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detaches formUpdated behaviors.
|
||||
*
|
||||
* @fires event:formUpdated
|
||||
*/
|
||||
Drupal.behaviors.formUpdated = {
|
||||
attach(context) {
|
||||
const $context = $(context);
|
||||
const contextIsForm = $context.is('form');
|
||||
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) => {
|
||||
const events = 'change.formUpdated input.formUpdated ';
|
||||
const eventHandler = debounce((event) => {
|
||||
triggerFormUpdated(event.target);
|
||||
}, 300);
|
||||
formFields = fieldsList(form).join(',');
|
||||
|
||||
form.setAttribute('data-drupal-form-fields', formFields);
|
||||
$(form).on(events, eventHandler);
|
||||
});
|
||||
}
|
||||
// On ajax requests context is the form element.
|
||||
if (contextIsForm) {
|
||||
formFields = fieldsList(context).join(',');
|
||||
// @todo replace with form.getAttribute() when #1979468 is in.
|
||||
const currentFields = $(context).attr('data-drupal-form-fields');
|
||||
// If there has been a change in the fields or their order, trigger
|
||||
// formUpdated.
|
||||
if (formFields !== currentFields) {
|
||||
triggerFormUpdated(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
detach(context, settings, trigger) {
|
||||
const $context = $(context);
|
||||
const contextIsForm = $context.is('form');
|
||||
if (trigger === 'unload') {
|
||||
const $forms = (contextIsForm ? $context : $context.find('form')).removeOnce('form-updated');
|
||||
if ($forms.length) {
|
||||
$.makeArray($forms).forEach((form) => {
|
||||
form.removeAttribute('data-drupal-form-fields');
|
||||
$(form).off('.formUpdated');
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepopulate form fields with information from the visitor browser.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the behavior for filling user info from browser.
|
||||
*/
|
||||
Drupal.behaviors.fillUserInfoFromBrowser = {
|
||||
attach(context, settings) {
|
||||
const userInfo = ['name', 'mail', 'homepage'];
|
||||
const $forms = $('[data-user-info-from-browser]').once('user-info-from-browser');
|
||||
if ($forms.length) {
|
||||
userInfo.map((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()));
|
||||
if ($element.length && emptyOrDefault && browserData) {
|
||||
$element.val(browserData);
|
||||
}
|
||||
});
|
||||
}
|
||||
$forms.on('submit', () => {
|
||||
userInfo.map((info) => {
|
||||
const $element = $forms.find(`[name=${info}]`);
|
||||
if ($element.length) {
|
||||
localStorage.setItem(`Drupal.visitor.${info}`, $element.val());
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a fragment interaction event on a hash change or fragment link click.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
*
|
||||
* @fires event:formFragmentLinkClickOrHashChange
|
||||
*/
|
||||
const handleFragmentLinkClickOrHashChange = (e) => {
|
||||
let url;
|
||||
if (e.type === 'click') {
|
||||
url = e.currentTarget.location ? e.currentTarget.location : e.currentTarget;
|
||||
}
|
||||
else {
|
||||
url = location;
|
||||
}
|
||||
const hash = url.hash.substr(1);
|
||||
if (hash) {
|
||||
const $target = $(`#${hash}`);
|
||||
$('body').trigger('formFragmentLinkClickOrHashChange', [$target]);
|
||||
|
||||
/**
|
||||
* Clicking a fragment link or a hash change should focus the target
|
||||
* element, but event timing issues in multiple browsers require a timeout.
|
||||
*/
|
||||
setTimeout(() => $target.trigger('focus'), 300);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedHandleFragmentLinkClickOrHashChange = debounce(handleFragmentLinkClickOrHashChange, 300, true);
|
||||
|
||||
// Binds a listener to handle URL fragment changes.
|
||||
$(window).on('hashchange.form-fragment', debouncedHandleFragmentLinkClickOrHashChange);
|
||||
|
||||
/**
|
||||
* Binds a listener to handle clicks on fragment links and absolute URL links
|
||||
* containing a fragment, this is needed next to the hash change listener
|
||||
* 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));
|
||||
+51
-150
@@ -1,205 +1,91 @@
|
||||
/**
|
||||
* @file
|
||||
* Form features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggers when a value in the form changed.
|
||||
*
|
||||
* The event triggers when content is typed or pasted in a text field, before
|
||||
* the change event triggers.
|
||||
*
|
||||
* @event formUpdated
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, debounce) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Retrieves the summary for the first element.
|
||||
*
|
||||
* @return {string}
|
||||
* The text of the summary.
|
||||
*/
|
||||
$.fn.drupalGetSummary = function () {
|
||||
var callback = this.data('summaryCallback');
|
||||
return (this[0] && callback) ? $.trim(callback(this[0])) : '';
|
||||
return this[0] && callback ? $.trim(callback(this[0])) : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the summary for all matched elements.
|
||||
*
|
||||
* @param {function} callback
|
||||
* Either a function that will be called each time the summary is
|
||||
* retrieved or a string (which is returned each time).
|
||||
*
|
||||
* @return {jQuery}
|
||||
* jQuery collection of the current element.
|
||||
*
|
||||
* @fires event:summaryUpdated
|
||||
*
|
||||
* @listens event:formUpdated
|
||||
*/
|
||||
$.fn.drupalSetSummary = function (callback) {
|
||||
var 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') {
|
||||
var val = callback;
|
||||
callback = function () { return val; };
|
||||
callback = function callback() {
|
||||
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', function () {
|
||||
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).off('formUpdated.summary').on('formUpdated.summary', function () {
|
||||
self.trigger('summaryUpdated');
|
||||
}).trigger('summaryUpdated');
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevents consecutive form submissions of identical form values.
|
||||
*
|
||||
* Repetitive form submissions that would submit the identical form values
|
||||
* are prevented, unless the form values are different to the previously
|
||||
* submitted values.
|
||||
*
|
||||
* This is a simplified re-implementation of a user-agent behavior that
|
||||
* should be natively supported by major web browsers, but at this time, only
|
||||
* Firefox has a built-in protection.
|
||||
*
|
||||
* A form value-based approach ensures that the constraint is triggered for
|
||||
* consecutive, identical form submissions only. Compared to that, a form
|
||||
* button-based approach would (1) rely on [visible] buttons to exist where
|
||||
* technically not required and (2) require more complex state management if
|
||||
* there are multiple buttons in a form.
|
||||
*
|
||||
* This implementation is based on form-level submit events only and relies
|
||||
* on jQuery's serialize() method to determine submitted form values. As such,
|
||||
* the following limitations exist:
|
||||
*
|
||||
* - Event handlers on form buttons that preventDefault() do not receive a
|
||||
* double-submit protection. That is deemed to be fine, since such button
|
||||
* events typically trigger reversible client-side or server-side
|
||||
* operations that are local to the context of a form only.
|
||||
* - Changed values in advanced form controls, such as file inputs, are not
|
||||
* part of the form values being compared between consecutive form submits
|
||||
* (due to limitations of jQuery.serialize()). That is deemed to be
|
||||
* acceptable, because if the user forgot to attach a file, then the size of
|
||||
* HTTP payload will most likely be small enough to be fully passed to the
|
||||
* server endpoint within (milli)seconds. If a user mistakenly attached a
|
||||
* wrong file and is technically versed enough to cancel the form submission
|
||||
* (and HTTP payload) in order to attach a different file, then that
|
||||
* edge-case is not supported here.
|
||||
*
|
||||
* Lastly, all forms submitted via HTTP GET are idempotent by definition of
|
||||
* HTTP standards, so excluded in this implementation.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.formSingleSubmit = {
|
||||
attach: function () {
|
||||
attach: function attach() {
|
||||
function onFormSubmit(e) {
|
||||
var $form = $(e.currentTarget);
|
||||
var formValues = $form.serialize();
|
||||
var 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')
|
||||
.on('submit.singleSubmit', 'form:not([method~="GET"])', onFormSubmit);
|
||||
$('body').once('form-single-submit').on('submit.singleSubmit', 'form:not([method~="GET"])', onFormSubmit);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a 'formUpdated' event each time a form element is modified.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to trigger a form updated event on.
|
||||
*
|
||||
* @fires event:formUpdated
|
||||
*/
|
||||
function triggerFormUpdated(element) {
|
||||
$(element).trigger('formUpdated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the IDs of all form fields in the given form.
|
||||
*
|
||||
* @param {HTMLFormElement} form
|
||||
* The form element to search.
|
||||
*
|
||||
* @return {Array}
|
||||
* Array of IDs for form fields.
|
||||
*/
|
||||
function fieldsList(form) {
|
||||
var $fieldList = $(form).find('[name]').map(function (index, element) {
|
||||
// We use id to avoid name duplicates on radio fields and filter out
|
||||
// elements with a name but no id.
|
||||
return element.getAttribute('id');
|
||||
});
|
||||
// Return a true array.
|
||||
|
||||
return $.makeArray($fieldList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the 'formUpdated' event on form elements when they are modified.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches formUpdated behaviors.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detaches formUpdated behaviors.
|
||||
*
|
||||
* @fires event:formUpdated
|
||||
*/
|
||||
Drupal.behaviors.formUpdated = {
|
||||
attach: function (context) {
|
||||
attach: function attach(context) {
|
||||
var $context = $(context);
|
||||
var contextIsForm = $context.is('form');
|
||||
var $forms = (contextIsForm ? $context : $context.find('form')).once('form-updated');
|
||||
var formFields;
|
||||
var formFields = void 0;
|
||||
|
||||
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(function (form) {
|
||||
var events = 'change.formUpdated input.formUpdated ';
|
||||
var eventHandler = debounce(function (event) { triggerFormUpdated(event.target); }, 300);
|
||||
var eventHandler = debounce(function (event) {
|
||||
triggerFormUpdated(event.target);
|
||||
}, 300);
|
||||
formFields = fieldsList(form).join(',');
|
||||
|
||||
form.setAttribute('data-drupal-form-fields', formFields);
|
||||
$(form).on(events, eventHandler);
|
||||
});
|
||||
}
|
||||
// On ajax requests context is the form element.
|
||||
|
||||
if (contextIsForm) {
|
||||
formFields = fieldsList(context).join(',');
|
||||
// @todo replace with form.getAttribute() when #1979468 is in.
|
||||
|
||||
var currentFields = $(context).attr('data-drupal-form-fields');
|
||||
// If there has been a change in the fields or their order, trigger
|
||||
// formUpdated.
|
||||
|
||||
if (formFields !== currentFields) {
|
||||
triggerFormUpdated(context);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
detach: function (context, settings, trigger) {
|
||||
detach: function detach(context, settings, trigger) {
|
||||
var $context = $(context);
|
||||
var contextIsForm = $context.is('form');
|
||||
if (trigger === 'unload') {
|
||||
@@ -214,23 +100,15 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepopulate form fields with information from the visitor browser.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the behavior for filling user info from browser.
|
||||
*/
|
||||
Drupal.behaviors.fillUserInfoFromBrowser = {
|
||||
attach: function (context, settings) {
|
||||
attach: function attach(context, settings) {
|
||||
var userInfo = ['name', 'mail', 'homepage'];
|
||||
var $forms = $('[data-user-info-from-browser]').once('user-info-from-browser');
|
||||
if ($forms.length) {
|
||||
userInfo.map(function (info) {
|
||||
var $element = $forms.find('[name=' + info + ']');
|
||||
var browserData = localStorage.getItem('Drupal.visitor.' + info);
|
||||
var emptyOrDefault = ($element.val() === '' || ($element.attr('data-drupal-default-value') === $element.val()));
|
||||
var emptyOrDefault = $element.val() === '' || $element.attr('data-drupal-default-value') === $element.val();
|
||||
if ($element.length && emptyOrDefault && browserData) {
|
||||
$element.val(browserData);
|
||||
}
|
||||
@@ -247,4 +125,27 @@
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, Drupal.debounce);
|
||||
var handleFragmentLinkClickOrHashChange = function handleFragmentLinkClickOrHashChange(e) {
|
||||
var url = void 0;
|
||||
if (e.type === 'click') {
|
||||
url = e.currentTarget.location ? e.currentTarget.location : e.currentTarget;
|
||||
} else {
|
||||
url = location;
|
||||
}
|
||||
var hash = url.hash.substr(1);
|
||||
if (hash) {
|
||||
var $target = $('#' + hash);
|
||||
$('body').trigger('formFragmentLinkClickOrHashChange', [$target]);
|
||||
|
||||
setTimeout(function () {
|
||||
return $target.trigger('focus');
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
var debouncedHandleFragmentLinkClickOrHashChange = debounce(handleFragmentLinkClickOrHashChange, 300, true);
|
||||
|
||||
$(window).on('hashchange.form-fragment', debouncedHandleFragmentLinkClickOrHashChange);
|
||||
|
||||
$(document).on('click.form-fragment', 'a[href*="#"]', debouncedHandleFragmentLinkClickOrHashChange);
|
||||
})(jQuery, Drupal, Drupal.debounce);
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* @file
|
||||
* Machine name functionality.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
/**
|
||||
* Attach the machine-readable name form element behavior.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches machine-name behaviors.
|
||||
*/
|
||||
Drupal.behaviors.machineName = {
|
||||
|
||||
/**
|
||||
* Attaches the behavior.
|
||||
*
|
||||
* @param {Element} context
|
||||
* The context for attaching the behavior.
|
||||
* @param {object} settings
|
||||
* Settings object.
|
||||
* @param {object} settings.machineName
|
||||
* A list of elements to process, keyed by the HTML ID of the form
|
||||
* element containing the human-readable value. Each element is an object
|
||||
* defining the following properties:
|
||||
* - target: The HTML ID of the machine name form element.
|
||||
* - suffix: The HTML ID of a container to show the machine name preview
|
||||
* in (usually a field suffix after the human-readable name
|
||||
* form element).
|
||||
* - label: The label to show for the machine name preview.
|
||||
* - replace_pattern: A regular expression (without modifiers) matching
|
||||
* disallowed characters in the machine name; e.g., '[^a-z0-9]+'.
|
||||
* - replace: A character to replace disallowed characters with; e.g.,
|
||||
* '_' or '-'.
|
||||
* - standalone: Whether the preview should stay in its own element
|
||||
* rather than the suffix of the source element.
|
||||
* - field_prefix: The #field_prefix of the form element.
|
||||
* - field_suffix: The #field_suffix of the form element.
|
||||
*/
|
||||
attach(context, settings) {
|
||||
const self = this;
|
||||
const $context = $(context);
|
||||
let timeout = null;
|
||||
let xhr = null;
|
||||
|
||||
function clickEditHandler(e) {
|
||||
const data = e.data;
|
||||
data.$wrapper.removeClass('visually-hidden');
|
||||
data.$target.trigger('focus');
|
||||
data.$suffix.hide();
|
||||
data.$source.off('.machineName');
|
||||
}
|
||||
|
||||
function machineNameHandler(e) {
|
||||
const data = e.data;
|
||||
const options = data.options;
|
||||
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);
|
||||
|
||||
// Abort the last pending request because the label has changed and it
|
||||
// is no longer valid.
|
||||
if (xhr && xhr.readystate !== 4) {
|
||||
xhr.abort();
|
||||
xhr = null;
|
||||
}
|
||||
|
||||
// Wait 300 milliseconds for Ajax request since the last event to update
|
||||
// the machine name i.e., after the user has stopped typing.
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
if (baseValue.toLowerCase() !== expected) {
|
||||
timeout = setTimeout(() => {
|
||||
xhr = self.transliterate(baseValue, options).done((machine) => {
|
||||
self.showMachineName(machine.substr(0, options.maxlength), data);
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
else {
|
||||
self.showMachineName(expected, data);
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(settings.machineName).forEach((source_id) => {
|
||||
let machine = '';
|
||||
let eventData;
|
||||
const options = settings.machineName[source_id];
|
||||
|
||||
const $source = $context.find(source_id).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) {
|
||||
return;
|
||||
}
|
||||
// Skip processing upon a form validation error on the machine name.
|
||||
if ($target.hasClass('error')) {
|
||||
return;
|
||||
}
|
||||
// Figure out the maximum length for the machine name.
|
||||
options.maxlength = $target.attr('maxlength');
|
||||
// Hide the form item container of the machine name form element.
|
||||
$wrapper.addClass('visually-hidden');
|
||||
// Determine the initial machine name value. Unless the machine name
|
||||
// form element is disabled or not empty, the initial default value is
|
||||
// based on the human-readable form element value.
|
||||
if ($target.is(':disabled') || $target.val() !== '') {
|
||||
machine = $target.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>`);
|
||||
$suffix.empty();
|
||||
if (options.label) {
|
||||
$suffix.append(`<span class="machine-name-label">${options.label}: </span>`);
|
||||
}
|
||||
$suffix.append($preview);
|
||||
|
||||
// If the machine name cannot be edited, stop further processing.
|
||||
if ($target.is(':disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventData = {
|
||||
$source,
|
||||
$target,
|
||||
$suffix,
|
||||
$wrapper,
|
||||
$preview,
|
||||
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);
|
||||
$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)
|
||||
// Initialize machine name preview.
|
||||
.trigger('formUpdated.machineName');
|
||||
}
|
||||
|
||||
// Add a listener for an invalid event on the machine name input
|
||||
// to show its container and focus it.
|
||||
$target.on('invalid', eventData, clickEditHandler);
|
||||
});
|
||||
},
|
||||
|
||||
showMachineName(machine, data) {
|
||||
const settings = data.options;
|
||||
// Set the machine name to the transliterated value.
|
||||
if (machine !== '') {
|
||||
if (machine !== settings.replace) {
|
||||
data.$target.val(machine);
|
||||
data.$preview.html(settings.field_prefix + Drupal.checkPlain(machine) + settings.field_suffix);
|
||||
}
|
||||
data.$suffix.show();
|
||||
}
|
||||
else {
|
||||
data.$suffix.hide();
|
||||
data.$target.val(machine);
|
||||
data.$preview.empty();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Transliterate a human-readable name to a machine name.
|
||||
*
|
||||
* @param {string} source
|
||||
* A string to transliterate.
|
||||
* @param {object} settings
|
||||
* The machine name settings for the corresponding field.
|
||||
* @param {string} settings.replace_pattern
|
||||
* A regular expression (without modifiers) matching disallowed characters
|
||||
* in the machine name; e.g., '[^a-z0-9]+'.
|
||||
* @param {string} settings.replace_token
|
||||
* A token to validate the regular expression.
|
||||
* @param {string} settings.replace
|
||||
* A character to replace disallowed characters with; e.g., '_' or '-'.
|
||||
* @param {number} settings.maxlength
|
||||
* The maximum length of the machine name.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The transliterated source string.
|
||||
*/
|
||||
transliterate(source, settings) {
|
||||
return $.get(Drupal.url('machine_name/transliterate'), {
|
||||
text: source,
|
||||
langcode: drupalSettings.langcode,
|
||||
replace_pattern: settings.replace_pattern,
|
||||
replace_token: settings.replace_token,
|
||||
replace: settings.replace,
|
||||
lowercase: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
}(jQuery, Drupal, drupalSettings));
|
||||
+22
-97
@@ -1,48 +1,13 @@
|
||||
/**
|
||||
* @file
|
||||
* Machine name functionality.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Attach the machine-readable name form element behavior.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches machine-name behaviors.
|
||||
*/
|
||||
Drupal.behaviors.machineName = {
|
||||
|
||||
/**
|
||||
* Attaches the behavior.
|
||||
*
|
||||
* @param {Element} context
|
||||
* The context for attaching the behavior.
|
||||
* @param {object} settings
|
||||
* Settings object.
|
||||
* @param {object} settings.machineName
|
||||
* A list of elements to process, keyed by the HTML ID of the form
|
||||
* element containing the human-readable value. Each element is an object
|
||||
* defining the following properties:
|
||||
* - target: The HTML ID of the machine name form element.
|
||||
* - suffix: The HTML ID of a container to show the machine name preview
|
||||
* in (usually a field suffix after the human-readable name
|
||||
* form element).
|
||||
* - label: The label to show for the machine name preview.
|
||||
* - replace_pattern: A regular expression (without modifiers) matching
|
||||
* disallowed characters in the machine name; e.g., '[^a-z0-9]+'.
|
||||
* - replace: A character to replace disallowed characters with; e.g.,
|
||||
* '_' or '-'.
|
||||
* - standalone: Whether the preview should stay in its own element
|
||||
* rather than the suffix of the source element.
|
||||
* - field_prefix: The #field_prefix of the form element.
|
||||
* - field_suffix: The #field_suffix of the form element.
|
||||
*/
|
||||
attach: function (context, settings) {
|
||||
attach: function attach(context, settings) {
|
||||
var self = this;
|
||||
var $context = $(context);
|
||||
var timeout = null;
|
||||
@@ -64,15 +29,11 @@
|
||||
var rx = new RegExp(options.replace_pattern, 'g');
|
||||
var 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.
|
||||
if (xhr && xhr.readystate !== 4) {
|
||||
xhr.abort();
|
||||
xhr = null;
|
||||
}
|
||||
|
||||
// Wait 300 milliseconds for Ajax request since the last event to update
|
||||
// the machine name i.e., after the user has stopped typing.
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
@@ -83,43 +44,39 @@
|
||||
self.showMachineName(machine.substr(0, options.maxlength), data);
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
self.showMachineName(expected, data);
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(settings.machineName).forEach(function (source_id) {
|
||||
var machine = '';
|
||||
var eventData;
|
||||
var eventData = void 0;
|
||||
var options = settings.machineName[source_id];
|
||||
|
||||
var $source = $context.find(source_id).addClass('machine-name-source').once('machine-name');
|
||||
var $target = $context.find(options.target).addClass('machine-name-target');
|
||||
var $suffix = $context.find(options.suffix);
|
||||
var $wrapper = $target.closest('.js-form-item');
|
||||
// All elements have to exist.
|
||||
|
||||
if (!$source.length || !$target.length || !$suffix.length || !$wrapper.length) {
|
||||
return;
|
||||
}
|
||||
// Skip processing upon a form validation error on the machine name.
|
||||
|
||||
if ($target.hasClass('error')) {
|
||||
return;
|
||||
}
|
||||
// Figure out the maximum length for the machine name.
|
||||
|
||||
options.maxlength = $target.attr('maxlength');
|
||||
// Hide the form item container of the machine name form element.
|
||||
|
||||
$wrapper.addClass('visually-hidden');
|
||||
// Determine the initial machine name value. Unless the machine name
|
||||
// form element is disabled or not empty, the initial default value is
|
||||
// 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.
|
||||
|
||||
var $preview = $('<span class="machine-name-value">' + options.field_prefix + Drupal.checkPlain(machine) + options.field_suffix + '</span>');
|
||||
$suffix.empty();
|
||||
if (options.label) {
|
||||
@@ -127,7 +84,6 @@
|
||||
}
|
||||
$suffix.append($preview);
|
||||
|
||||
// If the machine name cannot be edited, stop further processing.
|
||||
if ($target.is(':disabled')) {
|
||||
return;
|
||||
}
|
||||
@@ -140,63 +96,33 @@
|
||||
$preview: $preview,
|
||||
options: options
|
||||
};
|
||||
// If it is editable, append an edit link.
|
||||
|
||||
var $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)
|
||||
// Initialize machine name preview.
|
||||
.trigger('formUpdated.machineName');
|
||||
$source.on('formUpdated.machineName', eventData, machineNameHandler).trigger('formUpdated.machineName');
|
||||
}
|
||||
|
||||
// Add a listener for an invalid event on the machine name input
|
||||
// to show its container and focus it.
|
||||
$target.on('invalid', eventData, clickEditHandler);
|
||||
});
|
||||
},
|
||||
|
||||
showMachineName: function (machine, data) {
|
||||
showMachineName: function showMachineName(machine, data) {
|
||||
var settings = data.options;
|
||||
// Set the machine name to the transliterated value.
|
||||
|
||||
if (machine !== '') {
|
||||
if (machine !== settings.replace) {
|
||||
data.$target.val(machine);
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Transliterate a human-readable name to a machine name.
|
||||
*
|
||||
* @param {string} source
|
||||
* A string to transliterate.
|
||||
* @param {object} settings
|
||||
* The machine name settings for the corresponding field.
|
||||
* @param {string} settings.replace_pattern
|
||||
* A regular expression (without modifiers) matching disallowed characters
|
||||
* in the machine name; e.g., '[^a-z0-9]+'.
|
||||
* @param {string} settings.replace_token
|
||||
* A token to validate the regular expression.
|
||||
* @param {string} settings.replace
|
||||
* A character to replace disallowed characters with; e.g., '_' or '-'.
|
||||
* @param {number} settings.maxlength
|
||||
* The maximum length of the machine name.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The transliterated source string.
|
||||
*/
|
||||
transliterate: function (source, settings) {
|
||||
transliterate: function transliterate(source, settings) {
|
||||
return $.get(Drupal.url('machine_name/transliterate'), {
|
||||
text: source,
|
||||
langcode: drupalSettings.langcode,
|
||||
@@ -207,5 +133,4 @@
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings);
|
||||
})(jQuery, Drupal, drupalSettings);
|
||||
+1
-1
@@ -17,7 +17,7 @@ th {
|
||||
tr:nth-child(odd) {
|
||||
background-color: #ddd;
|
||||
}
|
||||
tr:nth-child(even){
|
||||
tr:nth-child(even) {
|
||||
background-color: #fff;
|
||||
}
|
||||
td {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @file
|
||||
* Progress bar.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Theme function for the progress bar.
|
||||
*
|
||||
* @param {string} id
|
||||
* The id for the progress bar.
|
||||
*
|
||||
* @return {string}
|
||||
* The HTML for the progress bar.
|
||||
*/
|
||||
Drupal.theme.progressBar = function (id) {
|
||||
return `<div id="${id}" class="progress" aria-live="polite">` +
|
||||
'<div class="progress__label"> </div>' +
|
||||
'<div class="progress__track"><div class="progress__bar"></div></div>' +
|
||||
'<div class="progress__percentage"></div>' +
|
||||
'<div class="progress__description"> </div>' +
|
||||
'</div>';
|
||||
};
|
||||
|
||||
/**
|
||||
* A progressbar object. Initialized with the given id. Must be inserted into
|
||||
* the DOM afterwards through progressBar.element.
|
||||
*
|
||||
* Method is the function which will perform the HTTP request to get the
|
||||
* progress bar state. Either "GET" or "POST".
|
||||
*
|
||||
* @example
|
||||
* pb = new Drupal.ProgressBar('myProgressBar');
|
||||
* some_element.appendChild(pb.element);
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} id
|
||||
* The id for the progressbar.
|
||||
* @param {function} updateCallback
|
||||
* Callback to run on update.
|
||||
* @param {string} method
|
||||
* HTTP method to use.
|
||||
* @param {function} errorCallback
|
||||
* Callback to call on error.
|
||||
*/
|
||||
Drupal.ProgressBar = function (id, updateCallback, method, errorCallback) {
|
||||
this.id = id;
|
||||
this.method = method || 'GET';
|
||||
this.updateCallback = updateCallback;
|
||||
this.errorCallback = errorCallback;
|
||||
|
||||
// The WAI-ARIA setting aria-live="polite" will announce changes after
|
||||
// users
|
||||
// have completed their current activity and not interrupt the screen
|
||||
// reader.
|
||||
this.element = $(Drupal.theme('progressBar', id));
|
||||
};
|
||||
|
||||
$.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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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>`);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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));
|
||||
+23
-101
@@ -1,78 +1,26 @@
|
||||
/**
|
||||
* @file
|
||||
* Progress bar.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Theme function for the progress bar.
|
||||
*
|
||||
* @param {string} id
|
||||
* The id for the progress bar.
|
||||
*
|
||||
* @return {string}
|
||||
* The HTML for the progress bar.
|
||||
*/
|
||||
Drupal.theme.progressBar = function (id) {
|
||||
return '<div id="' + id + '" class="progress" aria-live="polite">' +
|
||||
'<div class="progress__label"> </div>' +
|
||||
'<div class="progress__track"><div class="progress__bar"></div></div>' +
|
||||
'<div class="progress__percentage"></div>' +
|
||||
'<div class="progress__description"> </div>' +
|
||||
'</div>';
|
||||
return '<div id="' + id + '" class="progress" aria-live="polite">' + '<div class="progress__label"> </div>' + '<div class="progress__track"><div class="progress__bar"></div></div>' + '<div class="progress__percentage"></div>' + '<div class="progress__description"> </div>' + '</div>';
|
||||
};
|
||||
|
||||
/**
|
||||
* A progressbar object. Initialized with the given id. Must be inserted into
|
||||
* the DOM afterwards through progressBar.element.
|
||||
*
|
||||
* Method is the function which will perform the HTTP request to get the
|
||||
* progress bar state. Either "GET" or "POST".
|
||||
*
|
||||
* @example
|
||||
* pb = new Drupal.ProgressBar('myProgressBar');
|
||||
* some_element.appendChild(pb.element);
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} id
|
||||
* The id for the progressbar.
|
||||
* @param {function} updateCallback
|
||||
* Callback to run on update.
|
||||
* @param {string} method
|
||||
* HTTP method to use.
|
||||
* @param {function} errorCallback
|
||||
* Callback to call on error.
|
||||
*/
|
||||
Drupal.ProgressBar = function (id, updateCallback, method, errorCallback) {
|
||||
this.id = id;
|
||||
this.method = method || 'GET';
|
||||
this.updateCallback = updateCallback;
|
||||
this.errorCallback = errorCallback;
|
||||
|
||||
// The WAI-ARIA setting aria-live="polite" will announce changes after
|
||||
// users
|
||||
// have completed their current activity and not interrupt the screen
|
||||
// reader.
|
||||
this.element = $(Drupal.theme('progressBar', id));
|
||||
};
|
||||
|
||||
$.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: function (percentage, message, label) {
|
||||
$.extend(Drupal.ProgressBar.prototype, {
|
||||
setProgress: function 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 + '%');
|
||||
@@ -83,46 +31,27 @@
|
||||
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: function (uri, delay) {
|
||||
startMonitoring: function startMonitoring(uri, delay) {
|
||||
this.delay = delay;
|
||||
this.uri = uri;
|
||||
this.sendPing();
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop monitoring progress via Ajax.
|
||||
*/
|
||||
stopMonitoring: function () {
|
||||
stopMonitoring: function stopMonitoring() {
|
||||
clearTimeout(this.timer);
|
||||
// This allows monitoring to be stopped from within the callback.
|
||||
|
||||
this.uri = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Request progress data from server.
|
||||
*/
|
||||
sendPing: function () {
|
||||
sendPing: function sendPing() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
if (this.uri) {
|
||||
var 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.
|
||||
|
||||
var uri = this.uri;
|
||||
if (uri.indexOf('?') === -1) {
|
||||
uri += '?';
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
uri += '&';
|
||||
}
|
||||
uri += '_format=json';
|
||||
@@ -131,32 +60,26 @@
|
||||
url: uri,
|
||||
data: '',
|
||||
dataType: 'json',
|
||||
success: function (progress) {
|
||||
// Display errors.
|
||||
success: function success(progress) {
|
||||
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(function () { pb.sendPing(); }, pb.delay);
|
||||
|
||||
pb.timer = setTimeout(function () {
|
||||
pb.sendPing();
|
||||
}, pb.delay);
|
||||
},
|
||||
error: function (xmlhttp) {
|
||||
error: function error(xmlhttp) {
|
||||
var e = new Drupal.AjaxError(xmlhttp, pb.uri);
|
||||
pb.displayError('<pre>' + e.message + '</pre>');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Display errors on the page.
|
||||
*
|
||||
* @param {string} string
|
||||
* The error message to show the user.
|
||||
*/
|
||||
displayError: function (string) {
|
||||
displayError: function displayError(string) {
|
||||
var error = $('<div class="messages messages--error"></div>').html(string);
|
||||
$(this.element).before(error).hide();
|
||||
|
||||
@@ -165,5 +88,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, Drupal);
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,716 @@
|
||||
/**
|
||||
* @file
|
||||
* Drupal's states library.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* The base States namespace.
|
||||
*
|
||||
* Having the local states variable allows us to use the States namespace
|
||||
* without having to always declare "Drupal.states".
|
||||
*
|
||||
* @namespace Drupal.states
|
||||
*/
|
||||
const states = Drupal.states = {
|
||||
|
||||
/**
|
||||
* An array of functions that should be postponed.
|
||||
*/
|
||||
postponed: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches the states.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches states behaviors.
|
||||
*/
|
||||
Drupal.behaviors.states = {
|
||||
attach(context, settings) {
|
||||
const $states = $(context).find('[data-drupal-states]');
|
||||
let config;
|
||||
let state;
|
||||
const il = $states.length;
|
||||
for (let i = 0; i < il; i++) {
|
||||
config = JSON.parse($states[i].getAttribute('data-drupal-states'));
|
||||
for (state in config) {
|
||||
if (config.hasOwnProperty(state)) {
|
||||
new states.Dependent({
|
||||
element: $($states[i]),
|
||||
state: states.State.sanitize(state),
|
||||
constraints: config[state],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute all postponed functions now.
|
||||
while (states.postponed.length) {
|
||||
(states.postponed.shift())();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Object representing an element that depends on other elements.
|
||||
*
|
||||
* @constructor Drupal.states.Dependent
|
||||
*
|
||||
* @param {object} args
|
||||
* Object with the following keys (all of which are required)
|
||||
* @param {jQuery} args.element
|
||||
* A jQuery object of the dependent element
|
||||
* @param {Drupal.states.State} args.state
|
||||
* A State object describing the state that is dependent
|
||||
* @param {object} args.constraints
|
||||
* An object with dependency specifications. Lists all elements that this
|
||||
* element depends on. It can be nested and can contain
|
||||
* arbitrary AND and OR clauses.
|
||||
*/
|
||||
states.Dependent = function (args) {
|
||||
$.extend(this, { values: {}, oldValue: null }, args);
|
||||
|
||||
this.dependees = this.getDependees();
|
||||
for (const selector in this.dependees) {
|
||||
if (this.dependees.hasOwnProperty(selector)) {
|
||||
this.initializeDependee(selector, this.dependees[selector]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Comparison functions for comparing the value of an element with the
|
||||
* specification from the dependency settings. If the object type can't be
|
||||
* found in this list, the === operator is used by default.
|
||||
*
|
||||
* @name Drupal.states.Dependent.comparisons
|
||||
*
|
||||
* @prop {function} RegExp
|
||||
* @prop {function} Function
|
||||
* @prop {function} Number
|
||||
*/
|
||||
states.Dependent.comparisons = {
|
||||
RegExp(reference, value) {
|
||||
return reference.test(value);
|
||||
},
|
||||
Function(reference, value) {
|
||||
// The "reference" variable is a comparison function.
|
||||
return reference(value);
|
||||
},
|
||||
Number(reference, value) {
|
||||
// If "reference" is a number and "value" is a string, then cast
|
||||
// reference as a string before applying the strict comparison in
|
||||
// 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);
|
||||
},
|
||||
};
|
||||
|
||||
states.Dependent.prototype = {
|
||||
|
||||
/**
|
||||
* Initializes one of the elements this dependent depends on.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @param {string} selector
|
||||
* The CSS selector describing the dependee.
|
||||
* @param {object} dependeeStates
|
||||
* The list of states that have to be monitored for tracking the
|
||||
* 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] = {};
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Compares a value with a reference value.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @param {object} reference
|
||||
* The value used for reference.
|
||||
* @param {string} selector
|
||||
* CSS selector describing the dependee.
|
||||
* @param {Drupal.states.State} state
|
||||
* A State object describing the dependee's updated state.
|
||||
*
|
||||
* @return {bool}
|
||||
* true or false.
|
||||
*/
|
||||
compare(reference, selector, state) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Do a plain comparison otherwise.
|
||||
return compare(reference, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the value of a dependee's state.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @param {string} selector
|
||||
* CSS selector describing the dependee.
|
||||
* @param {Drupal.states.state} state
|
||||
* A State object describing the dependee's updated state.
|
||||
* @param {string} value
|
||||
* The new value for the dependee's updated state.
|
||||
*/
|
||||
update(selector, state, value) {
|
||||
// Only act when the 'new' value is actually new.
|
||||
if (value !== this.values[selector][state.name]) {
|
||||
this.values[selector][state.name] = value;
|
||||
this.reevaluate();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers change events in case a state changed.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*/
|
||||
reevaluate() {
|
||||
// Check whether any constraint for this dependent state is satisfied.
|
||||
let value = this.verifyConstraints(this.constraints);
|
||||
|
||||
// Only invoke a state change event when the value actually changed.
|
||||
if (value !== this.oldValue) {
|
||||
// Store the new value so that we can compare later whether the value
|
||||
// actually changed.
|
||||
this.oldValue = value;
|
||||
|
||||
// Normalize the value to match the normalized state name.
|
||||
value = invert(value, this.state.invert);
|
||||
|
||||
// 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 });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Evaluates child constraints to determine if a constraint is satisfied.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @param {object|Array} constraints
|
||||
* A constraint object or an array of constraints.
|
||||
* @param {string} selector
|
||||
* The selector for these constraints. If undefined, there isn't yet a
|
||||
* selector that these constraints apply to. In that case, the keys of the
|
||||
* object are interpreted as the selector if encountered.
|
||||
*
|
||||
* @return {bool}
|
||||
* true or false, depending on whether these constraints are satisfied.
|
||||
*/
|
||||
verifyConstraints(constraints, selector) {
|
||||
let result;
|
||||
if ($.isArray(constraints)) {
|
||||
// This constraint is an array (OR or XOR).
|
||||
const hasXor = $.inArray('xor', constraints) === -1;
|
||||
const len = constraints.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (constraints[i] !== 'xor') {
|
||||
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)) {
|
||||
return hasXor;
|
||||
}
|
||||
result = result || constraint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make sure we don't try to iterate over things other than objects. This
|
||||
// shouldn't normally occur, but in case the condition definition is
|
||||
// bogus, we don't want to end up with an infinite loop.
|
||||
else if ($.isPlainObject(constraints)) {
|
||||
// This constraint is an object (AND).
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether the value matches the requirements for this constraint.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @param {string|Array|object} value
|
||||
* Either the value of a state or an array/object of constraints. In the
|
||||
* latter case, resolving the constraint continues.
|
||||
* @param {string} [selector]
|
||||
* The selector for this constraint. If undefined, there isn't yet a
|
||||
* selector that this constraint applies to. In that case, the state key
|
||||
* is propagates to a selector and resolving continues.
|
||||
* @param {Drupal.states.State} [state]
|
||||
* The state to check for this constraint. If undefined, resolving
|
||||
* continues. If both selector and state aren't undefined and valid
|
||||
* non-numeric strings, a lookup for the actual value of that selector's
|
||||
* state is performed. This parameter is not a State object but a pristine
|
||||
* state string.
|
||||
*
|
||||
* @return {bool}
|
||||
* true or false, depending on whether this constraint is satisfied.
|
||||
*/
|
||||
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])) {
|
||||
state = null;
|
||||
}
|
||||
else if (typeof selector === 'undefined') {
|
||||
// Propagate the state to the selector when there isn't one yet.
|
||||
selector = state;
|
||||
state = null;
|
||||
}
|
||||
|
||||
if (state !== null) {
|
||||
// Constraints is the actual constraints of an element to check for.
|
||||
state = states.State.sanitize(state);
|
||||
return invert(this.compare(value, selector, state), state.invert);
|
||||
}
|
||||
|
||||
// Resolve this constraint as an AND/OR operator.
|
||||
return this.verifyConstraints(value, selector);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gathers information about all required triggers.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @return {object}
|
||||
* An object describing the required triggers.
|
||||
*/
|
||||
getDependees() {
|
||||
const cache = {};
|
||||
// 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) {
|
||||
(cache[selector] || (cache[selector] = [])).push(state.name);
|
||||
// Return nothing (=== undefined) so that the constraint loops are not
|
||||
// broken.
|
||||
};
|
||||
|
||||
// This call doesn't actually verify anything but uses the resolving
|
||||
// mechanism to go through the constraints array, trying to look up each
|
||||
// value. Since we swivelled the compare function, this comparison returns
|
||||
// undefined and lookup continues until the very end. Instead of lookup up
|
||||
// the value, we record that combination of selector and state so that we
|
||||
// can initialize all triggers.
|
||||
this.verifyConstraints(this.constraints);
|
||||
// Restore the original function.
|
||||
this.compare = _compare;
|
||||
|
||||
return cache;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @constructor Drupal.states.Trigger
|
||||
*
|
||||
* @param {object} args
|
||||
* Trigger arguments.
|
||||
*/
|
||||
states.Trigger = function (args) {
|
||||
$.extend(this, args);
|
||||
|
||||
if (this.state in states.Trigger.states) {
|
||||
this.element = $(this.selector);
|
||||
|
||||
// Only call the trigger initializer when it wasn't yet attached to this
|
||||
// element. Otherwise we'd end up with duplicate events.
|
||||
if (!this.element.data(`trigger:${this.state}`)) {
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
states.Trigger.prototype = {
|
||||
|
||||
/**
|
||||
* @memberof Drupal.states.Trigger#
|
||||
*/
|
||||
initialize() {
|
||||
const trigger = states.Trigger.states[this.state];
|
||||
|
||||
if (typeof trigger === 'function') {
|
||||
// We have a custom trigger initialization function.
|
||||
trigger.call(window, this.element);
|
||||
}
|
||||
else {
|
||||
for (const event in trigger) {
|
||||
if (trigger.hasOwnProperty(event)) {
|
||||
this.defaultTrigger(event, trigger[event]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark this trigger as initialized for this element.
|
||||
this.element.data(`trigger:${this.state}`, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* @memberof Drupal.states.Trigger#
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered.
|
||||
* @param {function} valueFn
|
||||
* The function to call.
|
||||
*/
|
||||
defaultTrigger(event, valueFn) {
|
||||
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));
|
||||
|
||||
states.postponed.push($.proxy(function () {
|
||||
// Trigger the event once for initialization purposes.
|
||||
this.element.trigger({ type: `state:${this.state}`, value: oldValue, oldValue: null });
|
||||
}, this));
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This list of states contains functions that are used to monitor the state
|
||||
* of an element. Whenever an element depends on the state of another element,
|
||||
* one of these trigger functions is added to the dependee so that the
|
||||
* dependent element can be updated.
|
||||
*
|
||||
* @name Drupal.states.Trigger.states
|
||||
*
|
||||
* @prop empty
|
||||
* @prop checked
|
||||
* @prop value
|
||||
* @prop collapsed
|
||||
*/
|
||||
states.Trigger.states = {
|
||||
// 'empty' describes the state to be monitored.
|
||||
empty: {
|
||||
// 'keyup' is the (native DOM) event that we watch for.
|
||||
keyup() {
|
||||
// The function associated with that trigger returns the new value for
|
||||
// the state.
|
||||
return this.val() === '';
|
||||
},
|
||||
},
|
||||
|
||||
checked: {
|
||||
change() {
|
||||
// prop() and attr() only takes the first element into account. To
|
||||
// support selectors matching multiple checkboxes, iterate over all and
|
||||
// return whether any is checked.
|
||||
let checked = false;
|
||||
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');
|
||||
// Break the each() loop if this is checked.
|
||||
return !checked;
|
||||
});
|
||||
return checked;
|
||||
},
|
||||
},
|
||||
|
||||
// For radio buttons, only return the value if the radio button is selected.
|
||||
value: {
|
||||
keyup() {
|
||||
// Radio buttons share the same :input[name="key"] selector.
|
||||
if (this.length > 1) {
|
||||
// Initial checked value of radios is undefined, so we return false.
|
||||
return this.filter(':checked').val() || false;
|
||||
}
|
||||
return this.val();
|
||||
},
|
||||
change() {
|
||||
// Radio buttons share the same :input[name="key"] selector.
|
||||
if (this.length > 1) {
|
||||
// Initial checked value of radios is undefined, so we return false.
|
||||
return this.filter(':checked').val() || false;
|
||||
}
|
||||
return this.val();
|
||||
},
|
||||
},
|
||||
|
||||
collapsed: {
|
||||
collapsed(e) {
|
||||
return (typeof e !== 'undefined' && 'value' in e) ? e.value : !this.is('[open]');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A state object is used for describing the state and performing aliasing.
|
||||
*
|
||||
* @constructor Drupal.states.State
|
||||
*
|
||||
* @param {string} state
|
||||
* The name of the state.
|
||||
*/
|
||||
states.State = function (state) {
|
||||
/**
|
||||
* Original unresolved name.
|
||||
*/
|
||||
this.pristine = this.name = state;
|
||||
|
||||
// Normalize the state name.
|
||||
let process = true;
|
||||
do {
|
||||
// Iteratively remove exclamation marks and invert the value.
|
||||
while (this.name.charAt(0) === '!') {
|
||||
this.name = this.name.substring(1);
|
||||
this.invert = !this.invert;
|
||||
}
|
||||
|
||||
// Replace the state with its normalized name.
|
||||
if (this.name in states.State.aliases) {
|
||||
this.name = states.State.aliases[this.name];
|
||||
}
|
||||
else {
|
||||
process = false;
|
||||
}
|
||||
} while (process);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new State object by sanitizing the passed value.
|
||||
*
|
||||
* @name Drupal.states.State.sanitize
|
||||
*
|
||||
* @param {string|Drupal.states.State} state
|
||||
* A state object or the name of a state.
|
||||
*
|
||||
* @return {Drupal.states.state}
|
||||
* A state object.
|
||||
*/
|
||||
states.State.sanitize = function (state) {
|
||||
if (state instanceof states.State) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return new states.State(state);
|
||||
};
|
||||
|
||||
/**
|
||||
* This list of aliases is used to normalize states and associates negated
|
||||
* names with their respective inverse state.
|
||||
*
|
||||
* @name Drupal.states.State.aliases
|
||||
*/
|
||||
states.State.aliases = {
|
||||
enabled: '!disabled',
|
||||
invisible: '!visible',
|
||||
invalid: '!valid',
|
||||
untouched: '!touched',
|
||||
optional: '!required',
|
||||
filled: '!empty',
|
||||
unchecked: '!checked',
|
||||
irrelevant: '!relevant',
|
||||
expanded: '!collapsed',
|
||||
open: '!collapsed',
|
||||
closed: 'collapsed',
|
||||
readwrite: '!readonly',
|
||||
};
|
||||
|
||||
states.State.prototype = {
|
||||
|
||||
/**
|
||||
* @memberof Drupal.states.State#
|
||||
*/
|
||||
invert: false,
|
||||
|
||||
/**
|
||||
* Ensures that just using the state object returns the name.
|
||||
*
|
||||
* @memberof Drupal.states.State#
|
||||
*
|
||||
* @return {string}
|
||||
* The name of the state.
|
||||
*/
|
||||
toString() {
|
||||
return this.name;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Global state change handlers. These are bound to "document" to cover all
|
||||
* elements whose state changes. Events sent to elements within the page
|
||||
* bubble up to these handlers. We use this system so that themes and modules
|
||||
* can override these state change handlers for particular parts of a page.
|
||||
*/
|
||||
|
||||
const $document = $(document);
|
||||
$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) {
|
||||
$(e.target)
|
||||
.prop('disabled', e.value)
|
||||
.closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggleClass('form-disabled', e.value)
|
||||
.find('select, input, textarea').prop('disabled', e.value);
|
||||
|
||||
// Note: WebKit nightlies don't reflect that change correctly.
|
||||
// See https://bugs.webkit.org/show_bug.cgi?id=23789
|
||||
}
|
||||
});
|
||||
|
||||
$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);
|
||||
// Avoids duplicate required markers on initialization.
|
||||
if (!$label.hasClass('js-form-required').length) {
|
||||
$label.addClass('js-form-required form-required');
|
||||
}
|
||||
}
|
||||
else {
|
||||
$(e.target).removeAttr('required aria-required').closest('.js-form-item, .js-form-wrapper').find('label.js-form-required').removeClass('js-form-required form-required');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$document.on('state:visible', (e) => {
|
||||
if (e.trigger) {
|
||||
$(e.target).closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggle(e.value);
|
||||
}
|
||||
});
|
||||
|
||||
$document.on('state:checked', (e) => {
|
||||
if (e.trigger) {
|
||||
$(e.target).prop('checked', e.value);
|
||||
}
|
||||
});
|
||||
|
||||
$document.on('state:collapsed', (e) => {
|
||||
if (e.trigger) {
|
||||
if ($(e.target).is('[open]') === e.value) {
|
||||
$(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));
|
||||
+74
-425
@@ -1,41 +1,20 @@
|
||||
/**
|
||||
* @file
|
||||
* Drupal's states library.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* The base States namespace.
|
||||
*
|
||||
* Having the local states variable allows us to use the States namespace
|
||||
* without having to always declare "Drupal.states".
|
||||
*
|
||||
* @namespace Drupal.states
|
||||
*/
|
||||
var states = Drupal.states = {
|
||||
|
||||
/**
|
||||
* An array of functions that should be postponed.
|
||||
*/
|
||||
postponed: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches the states.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches states behaviors.
|
||||
*/
|
||||
Drupal.behaviors.states = {
|
||||
attach: function (context, settings) {
|
||||
attach: function attach(context, settings) {
|
||||
var $states = $(context).find('[data-drupal-states]');
|
||||
var config;
|
||||
var state;
|
||||
var config = void 0;
|
||||
var state = void 0;
|
||||
var il = $states.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
config = JSON.parse($states[i].getAttribute('data-drupal-states'));
|
||||
@@ -50,31 +29,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Execute all postponed functions now.
|
||||
while (states.postponed.length) {
|
||||
(states.postponed.shift())();
|
||||
states.postponed.shift()();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Object representing an element that depends on other elements.
|
||||
*
|
||||
* @constructor Drupal.states.Dependent
|
||||
*
|
||||
* @param {object} args
|
||||
* Object with the following keys (all of which are required)
|
||||
* @param {jQuery} args.element
|
||||
* A jQuery object of the dependent element
|
||||
* @param {Drupal.states.State} args.state
|
||||
* A State object describing the state that is dependent
|
||||
* @param {object} args.constraints
|
||||
* An object with dependency specifications. Lists all elements that this
|
||||
* element depends on. It can be nested and can contain
|
||||
* arbitrary AND and OR clauses.
|
||||
*/
|
||||
states.Dependent = function (args) {
|
||||
$.extend(this, {values: {}, oldValue: null}, args);
|
||||
$.extend(this, { values: {}, oldValue: null }, args);
|
||||
|
||||
this.dependees = this.getDependees();
|
||||
for (var selector in this.dependees) {
|
||||
@@ -84,295 +46,137 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Comparison functions for comparing the value of an element with the
|
||||
* specification from the dependency settings. If the object type can't be
|
||||
* found in this list, the === operator is used by default.
|
||||
*
|
||||
* @name Drupal.states.Dependent.comparisons
|
||||
*
|
||||
* @prop {function} RegExp
|
||||
* @prop {function} Function
|
||||
* @prop {function} Number
|
||||
*/
|
||||
states.Dependent.comparisons = {
|
||||
RegExp: function (reference, value) {
|
||||
RegExp: function RegExp(reference, value) {
|
||||
return reference.test(value);
|
||||
},
|
||||
Function: function (reference, value) {
|
||||
// The "reference" variable is a comparison function.
|
||||
Function: function Function(reference, value) {
|
||||
return reference(value);
|
||||
},
|
||||
Number: function (reference, value) {
|
||||
// If "reference" is a number and "value" is a string, then cast
|
||||
// reference as a string before applying the strict comparison in
|
||||
// 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);
|
||||
Number: function Number(reference, value) {
|
||||
return typeof value === 'string' ? _compare2(reference.toString(), value) : _compare2(reference, value);
|
||||
}
|
||||
};
|
||||
|
||||
states.Dependent.prototype = {
|
||||
|
||||
/**
|
||||
* Initializes one of the elements this dependent depends on.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @param {string} selector
|
||||
* The CSS selector describing the dependee.
|
||||
* @param {object} dependeeStates
|
||||
* The list of states that have to be monitored for tracking the
|
||||
* dependee's compliance status.
|
||||
*/
|
||||
initializeDependee: function (selector, dependeeStates) {
|
||||
var state;
|
||||
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);
|
||||
}
|
||||
|
||||
// Cache for the states of this dependee.
|
||||
this.values[selector] = {};
|
||||
|
||||
for (var 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: selector, state: state}, stateEventHandler);
|
||||
$(selector).on('state:' + state, { selector: selector, state: state }, stateEventHandler);
|
||||
|
||||
// Make sure the event we just bound ourselves to is actually fired.
|
||||
new states.Trigger({selector: selector, state: state});
|
||||
new states.Trigger({ selector: selector, state: state });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Compares a value with a reference value.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @param {object} reference
|
||||
* The value used for reference.
|
||||
* @param {string} selector
|
||||
* CSS selector describing the dependee.
|
||||
* @param {Drupal.states.State} state
|
||||
* A State object describing the dependee's updated state.
|
||||
*
|
||||
* @return {bool}
|
||||
* true or false.
|
||||
*/
|
||||
compare: function (reference, selector, state) {
|
||||
compare: function compare(reference, selector, state) {
|
||||
var 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);
|
||||
}
|
||||
else {
|
||||
// Do a plain comparison otherwise.
|
||||
return compare(reference, value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the value of a dependee's state.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @param {string} selector
|
||||
* CSS selector describing the dependee.
|
||||
* @param {Drupal.states.state} state
|
||||
* A State object describing the dependee's updated state.
|
||||
* @param {string} value
|
||||
* The new value for the dependee's updated state.
|
||||
*/
|
||||
update: function (selector, state, value) {
|
||||
// Only act when the 'new' value is actually new.
|
||||
return _compare2(reference, value);
|
||||
},
|
||||
update: function update(selector, state, value) {
|
||||
if (value !== this.values[selector][state.name]) {
|
||||
this.values[selector][state.name] = value;
|
||||
this.reevaluate();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers change events in case a state changed.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*/
|
||||
reevaluate: function () {
|
||||
// Check whether any constraint for this dependent state is satisfied.
|
||||
reevaluate: function reevaluate() {
|
||||
var value = this.verifyConstraints(this.constraints);
|
||||
|
||||
// Only invoke a state change event when the value actually changed.
|
||||
if (value !== this.oldValue) {
|
||||
// Store the new value so that we can compare later whether the value
|
||||
// actually changed.
|
||||
this.oldValue = value;
|
||||
|
||||
// Normalize the value to match the normalized state name.
|
||||
value = invert(value, this.state.invert);
|
||||
|
||||
// By adding "trigger: true", we ensure that state changes don't go into
|
||||
// infinite loops.
|
||||
this.element.trigger({type: 'state:' + this.state, value: value, trigger: true});
|
||||
this.element.trigger({ type: 'state:' + this.state, value: value, trigger: true });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Evaluates child constraints to determine if a constraint is satisfied.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @param {object|Array} constraints
|
||||
* A constraint object or an array of constraints.
|
||||
* @param {string} selector
|
||||
* The selector for these constraints. If undefined, there isn't yet a
|
||||
* selector that these constraints apply to. In that case, the keys of the
|
||||
* object are interpreted as the selector if encountered.
|
||||
*
|
||||
* @return {bool}
|
||||
* true or false, depending on whether these constraints are satisfied.
|
||||
*/
|
||||
verifyConstraints: function (constraints, selector) {
|
||||
var result;
|
||||
verifyConstraints: function verifyConstraints(constraints, selector) {
|
||||
var result = void 0;
|
||||
if ($.isArray(constraints)) {
|
||||
// This constraint is an array (OR or XOR).
|
||||
var hasXor = $.inArray('xor', constraints) === -1;
|
||||
var len = constraints.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (constraints[i] !== 'xor') {
|
||||
var 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)) {
|
||||
return hasXor;
|
||||
}
|
||||
result = result || constraint;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make sure we don't try to iterate over things other than objects. This
|
||||
// shouldn't normally occur, but in case the condition definition is
|
||||
// bogus, we don't want to end up with an infinite loop.
|
||||
else if ($.isPlainObject(constraints)) {
|
||||
// This constraint is an object (AND).
|
||||
for (var 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; }
|
||||
} else if ($.isPlainObject(constraints)) {
|
||||
for (var n in constraints) {
|
||||
if (constraints.hasOwnProperty(n)) {
|
||||
result = ternary(result, this.checkConstraints(constraints[n], selector, n));
|
||||
|
||||
if (result === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether the value matches the requirements for this constraint.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @param {string|Array|object} value
|
||||
* Either the value of a state or an array/object of constraints. In the
|
||||
* latter case, resolving the constraint continues.
|
||||
* @param {string} [selector]
|
||||
* The selector for this constraint. If undefined, there isn't yet a
|
||||
* selector that this constraint applies to. In that case, the state key
|
||||
* is propagates to a selector and resolving continues.
|
||||
* @param {Drupal.states.State} [state]
|
||||
* The state to check for this constraint. If undefined, resolving
|
||||
* continues. If both selector and state aren't undefined and valid
|
||||
* non-numeric strings, a lookup for the actual value of that selector's
|
||||
* state is performed. This parameter is not a State object but a pristine
|
||||
* state string.
|
||||
*
|
||||
* @return {bool}
|
||||
* true or false, depending on whether this constraint is satisfied.
|
||||
*/
|
||||
checkConstraints: function (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])) {
|
||||
checkConstraints: function checkConstraints(value, selector, state) {
|
||||
if (typeof state !== 'string' || /[0-9]/.test(state[0])) {
|
||||
state = null;
|
||||
}
|
||||
else if (typeof selector === 'undefined') {
|
||||
// Propagate the state to the selector when there isn't one yet.
|
||||
} else if (typeof selector === 'undefined') {
|
||||
selector = state;
|
||||
state = null;
|
||||
}
|
||||
|
||||
if (state !== null) {
|
||||
// Constraints is the actual constraints of an element to check for.
|
||||
state = states.State.sanitize(state);
|
||||
return invert(this.compare(value, selector, state), state.invert);
|
||||
}
|
||||
else {
|
||||
// Resolve this constraint as an AND/OR operator.
|
||||
return this.verifyConstraints(value, selector);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gathers information about all required triggers.
|
||||
*
|
||||
* @memberof Drupal.states.Dependent#
|
||||
*
|
||||
* @return {object}
|
||||
* An object describing the required triggers.
|
||||
*/
|
||||
getDependees: function () {
|
||||
return this.verifyConstraints(value, selector);
|
||||
},
|
||||
getDependees: function getDependees() {
|
||||
var cache = {};
|
||||
// Swivel the lookup function so that we can record all available
|
||||
// selector- state combinations for initialization.
|
||||
|
||||
var _compare = this.compare;
|
||||
this.compare = function (reference, selector, state) {
|
||||
(cache[selector] || (cache[selector] = [])).push(state.name);
|
||||
// Return nothing (=== undefined) so that the constraint loops are not
|
||||
// broken.
|
||||
};
|
||||
|
||||
// This call doesn't actually verify anything but uses the resolving
|
||||
// mechanism to go through the constraints array, trying to look up each
|
||||
// value. Since we swivelled the compare function, this comparison returns
|
||||
// undefined and lookup continues until the very end. Instead of lookup up
|
||||
// the value, we record that combination of selector and state so that we
|
||||
// can initialize all triggers.
|
||||
this.verifyConstraints(this.constraints);
|
||||
// Restore the original function.
|
||||
|
||||
this.compare = _compare;
|
||||
|
||||
return cache;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @constructor Drupal.states.Trigger
|
||||
*
|
||||
* @param {object} args
|
||||
* Trigger arguments.
|
||||
*/
|
||||
states.Trigger = function (args) {
|
||||
$.extend(this, args);
|
||||
|
||||
if (this.state in states.Trigger.states) {
|
||||
this.element = $(this.selector);
|
||||
|
||||
// Only call the trigger initializer when it wasn't yet attached to this
|
||||
// element. Otherwise we'd end up with duplicate events.
|
||||
if (!this.element.data('trigger:' + this.state)) {
|
||||
this.initialize();
|
||||
}
|
||||
@@ -380,18 +184,12 @@
|
||||
};
|
||||
|
||||
states.Trigger.prototype = {
|
||||
|
||||
/**
|
||||
* @memberof Drupal.states.Trigger#
|
||||
*/
|
||||
initialize: function () {
|
||||
initialize: function initialize() {
|
||||
var trigger = states.Trigger.states[this.state];
|
||||
|
||||
if (typeof trigger === 'function') {
|
||||
// We have a custom trigger initialization function.
|
||||
trigger.call(window, this.element);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
for (var event in trigger) {
|
||||
if (trigger.hasOwnProperty(event)) {
|
||||
this.defaultTrigger(event, trigger[event]);
|
||||
@@ -399,93 +197,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Mark this trigger as initialized for this element.
|
||||
this.element.data('trigger:' + this.state, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* @memberof Drupal.states.Trigger#
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered.
|
||||
* @param {function} valueFn
|
||||
* The function to call.
|
||||
*/
|
||||
defaultTrigger: function (event, valueFn) {
|
||||
defaultTrigger: function defaultTrigger(event, valueFn) {
|
||||
var oldValue = valueFn.call(this.element);
|
||||
|
||||
// Attach the event callback.
|
||||
this.element.on(event, $.proxy(function (e) {
|
||||
var 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: value, oldValue: oldValue});
|
||||
this.element.trigger({ type: 'state:' + this.state, value: value, oldValue: 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.element.trigger({ type: 'state:' + this.state, value: oldValue, oldValue: null });
|
||||
}, this));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This list of states contains functions that are used to monitor the state
|
||||
* of an element. Whenever an element depends on the state of another element,
|
||||
* one of these trigger functions is added to the dependee so that the
|
||||
* dependent element can be updated.
|
||||
*
|
||||
* @name Drupal.states.Trigger.states
|
||||
*
|
||||
* @prop empty
|
||||
* @prop checked
|
||||
* @prop value
|
||||
* @prop collapsed
|
||||
*/
|
||||
states.Trigger.states = {
|
||||
// 'empty' describes the state to be monitored.
|
||||
empty: {
|
||||
// 'keyup' is the (native DOM) event that we watch for.
|
||||
keyup: function () {
|
||||
// The function associated with that trigger returns the new value for
|
||||
// the state.
|
||||
keyup: function keyup() {
|
||||
return this.val() === '';
|
||||
}
|
||||
},
|
||||
|
||||
checked: {
|
||||
change: function () {
|
||||
// prop() and attr() only takes the first element into account. To
|
||||
// support selectors matching multiple checkboxes, iterate over all and
|
||||
// return whether any is checked.
|
||||
change: function change() {
|
||||
var checked = false;
|
||||
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');
|
||||
// Break the each() loop if this is checked.
|
||||
|
||||
return !checked;
|
||||
});
|
||||
return checked;
|
||||
}
|
||||
},
|
||||
|
||||
// For radio buttons, only return the value if the radio button is selected.
|
||||
value: {
|
||||
keyup: function () {
|
||||
// Radio buttons share the same :input[name="key"] selector.
|
||||
keyup: function keyup() {
|
||||
if (this.length > 1) {
|
||||
// Initial checked value of radios is undefined, so we return false.
|
||||
return this.filter(':checked').val() || false;
|
||||
}
|
||||
return this.val();
|
||||
},
|
||||
change: function () {
|
||||
// Radio buttons share the same :input[name="key"] selector.
|
||||
change: function change() {
|
||||
if (this.length > 1) {
|
||||
// Initial checked value of radios is undefined, so we return false.
|
||||
return this.filter(':checked').val() || false;
|
||||
}
|
||||
return this.val();
|
||||
@@ -493,72 +252,38 @@
|
||||
},
|
||||
|
||||
collapsed: {
|
||||
collapsed: function (e) {
|
||||
return (typeof e !== 'undefined' && 'value' in e) ? e.value : !this.is('[open]');
|
||||
collapsed: function collapsed(e) {
|
||||
return typeof e !== 'undefined' && 'value' in e ? e.value : !this.is('[open]');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A state object is used for describing the state and performing aliasing.
|
||||
*
|
||||
* @constructor Drupal.states.State
|
||||
*
|
||||
* @param {string} state
|
||||
* The name of the state.
|
||||
*/
|
||||
states.State = function (state) {
|
||||
|
||||
/**
|
||||
* Original unresolved name.
|
||||
*/
|
||||
this.pristine = this.name = state;
|
||||
|
||||
// Normalize the state name.
|
||||
var process = true;
|
||||
do {
|
||||
// Iteratively remove exclamation marks and invert the value.
|
||||
while (this.name.charAt(0) === '!') {
|
||||
this.name = this.name.substring(1);
|
||||
this.invert = !this.invert;
|
||||
}
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new State object by sanitizing the passed value.
|
||||
*
|
||||
* @name Drupal.states.State.sanitize
|
||||
*
|
||||
* @param {string|Drupal.states.State} state
|
||||
* A state object or the name of a state.
|
||||
*
|
||||
* @return {Drupal.states.state}
|
||||
* A state object.
|
||||
*/
|
||||
states.State.sanitize = function (state) {
|
||||
if (state instanceof states.State) {
|
||||
return state;
|
||||
}
|
||||
else {
|
||||
return new states.State(state);
|
||||
}
|
||||
|
||||
return new states.State(state);
|
||||
};
|
||||
|
||||
/**
|
||||
* This list of aliases is used to normalize states and associates negated
|
||||
* names with their respective inverse state.
|
||||
*
|
||||
* @name Drupal.states.State.aliases
|
||||
*/
|
||||
states.State.aliases = {
|
||||
enabled: '!disabled',
|
||||
invisible: '!visible',
|
||||
@@ -575,44 +300,17 @@
|
||||
};
|
||||
|
||||
states.State.prototype = {
|
||||
|
||||
/**
|
||||
* @memberof Drupal.states.State#
|
||||
*/
|
||||
invert: false,
|
||||
|
||||
/**
|
||||
* Ensures that just using the state object returns the name.
|
||||
*
|
||||
* @memberof Drupal.states.State#
|
||||
*
|
||||
* @return {string}
|
||||
* The name of the state.
|
||||
*/
|
||||
toString: function () {
|
||||
toString: function toString() {
|
||||
return this.name;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Global state change handlers. These are bound to "document" to cover all
|
||||
* elements whose state changes. Events sent to elements within the page
|
||||
* bubble up to these handlers. We use this system so that themes and modules
|
||||
* can override these state change handlers for particular parts of a page.
|
||||
*/
|
||||
|
||||
var $document = $(document);
|
||||
$document.on('state:disabled', function (e) {
|
||||
// Only act when this change was triggered by a dependency and not by the
|
||||
// element monitoring itself.
|
||||
if (e.trigger) {
|
||||
$(e.target)
|
||||
.prop('disabled', e.value)
|
||||
.closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggleClass('form-disabled', e.value)
|
||||
.find('select, input, textarea').prop('disabled', e.value);
|
||||
|
||||
// Note: WebKit nightlies don't reflect that change correctly.
|
||||
// See https://bugs.webkit.org/show_bug.cgi?id=23789
|
||||
$(e.target).prop('disabled', e.value).closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggleClass('form-disabled', e.value).find('select, input, textarea').prop('disabled', e.value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -620,13 +318,12 @@
|
||||
if (e.trigger) {
|
||||
if (e.value) {
|
||||
var label = 'label' + (e.target.id ? '[for=' + e.target.id + ']' : '');
|
||||
var $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.
|
||||
var $label = $(e.target).attr({ required: 'required', 'aria-required': 'aria-required' }).closest('.js-form-item, .js-form-wrapper').find(label);
|
||||
|
||||
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').find('label.js-form-required').removeClass('js-form-required form-required');
|
||||
}
|
||||
}
|
||||
@@ -652,73 +349,25 @@
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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') {
|
||||
} else if (typeof b === 'undefined') {
|
||||
return a;
|
||||
}
|
||||
else {
|
||||
return a && b;
|
||||
}
|
||||
|
||||
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;
|
||||
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) {
|
||||
function _compare2(a, b) {
|
||||
if (a === b) {
|
||||
return typeof a === 'undefined' ? a : true;
|
||||
}
|
||||
else {
|
||||
return typeof a === 'undefined' || typeof b === 'undefined';
|
||||
}
|
||||
}
|
||||
|
||||
})(jQuery, Drupal);
|
||||
return typeof a === 'undefined' || typeof b === 'undefined';
|
||||
}
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* @file
|
||||
* Manages page tabbing modifications made by modules.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow modules to respond to the constrain event.
|
||||
*
|
||||
* @event drupalTabbingConstrained
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow modules to respond to the tabbingContext release event.
|
||||
*
|
||||
* @event drupalTabbingContextReleased
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow modules to respond to the constrain event.
|
||||
*
|
||||
* @event drupalTabbingContextActivated
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow modules to respond to the constrain event.
|
||||
*
|
||||
* @event drupalTabbingContextDeactivated
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Provides an API for managing page tabbing order modifications.
|
||||
*
|
||||
* @constructor Drupal~TabbingManager
|
||||
*/
|
||||
function TabbingManager() {
|
||||
/**
|
||||
* Tabbing sets are stored as a stack. The active set is at the top of the
|
||||
* stack. We use a JavaScript array as if it were a stack; we consider the
|
||||
* first element to be the bottom and the last element to be the top. This
|
||||
* allows us to use JavaScript's built-in Array.push() and Array.pop()
|
||||
* methods.
|
||||
*
|
||||
* @type {Array.<Drupal~TabbingContext>}
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* This constraint can be removed with the release() method.
|
||||
*
|
||||
* @constructor Drupal~TabbingContext
|
||||
*
|
||||
* @param {object} options
|
||||
* A set of initiating values
|
||||
* @param {number} options.level
|
||||
* The level in the TabbingManager's stack of this tabbingContext.
|
||||
* @param {jQuery} options.$tabbableElements
|
||||
* The DOM elements that should be reachable via the tab key when this
|
||||
* tabbingContext is active.
|
||||
* @param {jQuery} options.$disabledElements
|
||||
* The DOM elements that should not be reachable via the tab key when this
|
||||
* tabbingContext is active.
|
||||
* @param {bool} options.released
|
||||
* A released tabbingContext can never be activated again. It will be
|
||||
* cleaned up when the TabbingManager unwinds its stack.
|
||||
* @param {bool} options.active
|
||||
* When true, the tabbable elements of this tabbingContext will be reachable
|
||||
* via the tab key and the disabled elements will not. Only one
|
||||
* tabbingContext can be active at a time.
|
||||
*/
|
||||
function TabbingContext(options) {
|
||||
$.extend(this, /** @lends Drupal~TabbingContext# */{
|
||||
|
||||
/**
|
||||
* @type {?number}
|
||||
*/
|
||||
level: null,
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$tabbableElements: $(),
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$disabledElements: $(),
|
||||
|
||||
/**
|
||||
* @type {bool}
|
||||
*/
|
||||
released: false,
|
||||
|
||||
/**
|
||||
* @type {bool}
|
||||
*/
|
||||
active: false,
|
||||
}, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add public methods to the TabbingContext class.
|
||||
*/
|
||||
$.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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Mark this behavior as processed on the first pass and return if it is
|
||||
// already processed.
|
||||
if (Drupal.tabbingManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Drupal~TabbingManager}
|
||||
*/
|
||||
Drupal.tabbingManager = new TabbingManager();
|
||||
}(jQuery, Drupal));
|
||||
+32
-242
@@ -1,184 +1,72 @@
|
||||
/**
|
||||
* @file
|
||||
* Manages page tabbing modifications made by modules.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow modules to respond to the constrain event.
|
||||
*
|
||||
* @event drupalTabbingConstrained
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow modules to respond to the tabbingContext release event.
|
||||
*
|
||||
* @event drupalTabbingContextReleased
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow modules to respond to the constrain event.
|
||||
*
|
||||
* @event drupalTabbingContextActivated
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow modules to respond to the constrain event.
|
||||
*
|
||||
* @event drupalTabbingContextDeactivated
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides an API for managing page tabbing order modifications.
|
||||
*
|
||||
* @constructor Drupal~TabbingManager
|
||||
*/
|
||||
function TabbingManager() {
|
||||
|
||||
/**
|
||||
* Tabbing sets are stored as a stack. The active set is at the top of the
|
||||
* stack. We use a JavaScript array as if it were a stack; we consider the
|
||||
* first element to be the bottom and the last element to be the top. This
|
||||
* allows us to use JavaScript's built-in Array.push() and Array.pop()
|
||||
* methods.
|
||||
*
|
||||
* @type {Array.<Drupal~TabbingContext>}
|
||||
*/
|
||||
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: function (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.
|
||||
$.extend(TabbingManager.prototype, {
|
||||
constrain: function constrain(elements) {
|
||||
var il = this.stack.length;
|
||||
for (var i = 0; i < il; i++) {
|
||||
this.stack[i].deactivate();
|
||||
}
|
||||
|
||||
// The "active tabbing set" are the elements tabbing should be constrained
|
||||
// to.
|
||||
var $elements = $(elements).find(':tabbable').addBack(':tabbable');
|
||||
|
||||
var 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: function () {
|
||||
// Unwind as far as possible: find the topmost non-released
|
||||
// tabbingContext.
|
||||
release: function release() {
|
||||
var 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: function (tabbingContext) {
|
||||
activate: function activate(tabbingContext) {
|
||||
var $set = tabbingContext.$tabbableElements;
|
||||
var level = tabbingContext.level;
|
||||
// Determine which elements are reachable via tabbing by default.
|
||||
var $disabledSet = $(':tabbable')
|
||||
// Exclude elements of the active tabbing set.
|
||||
.not($set);
|
||||
// Set the disabled set on the tabbingContext.
|
||||
|
||||
var $disabledSet = $(':tabbable').not($set);
|
||||
|
||||
tabbingContext.$disabledElements = $disabledSet;
|
||||
// Record the tabindex for each element, so we can restore it later.
|
||||
|
||||
var il = $disabledSet.length;
|
||||
for (var 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.
|
||||
$disabledSet.prop('tabindex', -1).prop('autofocus', false);
|
||||
|
||||
var $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: function (tabbingContext) {
|
||||
deactivate: function deactivate(tabbingContext) {
|
||||
var $set = tabbingContext.$disabledElements;
|
||||
var level = tabbingContext.level;
|
||||
var il = $set.length;
|
||||
@@ -186,16 +74,7 @@
|
||||
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: function ($el, level) {
|
||||
recordTabindex: function recordTabindex($el, level) {
|
||||
var tabInfo = $el.data('drupalOriginalTabIndices') || {};
|
||||
tabInfo[level] = {
|
||||
tabindex: $el[0].getAttribute('tabindex'),
|
||||
@@ -203,38 +82,22 @@
|
||||
};
|
||||
$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: function ($el, level) {
|
||||
restoreTabindex: function restoreTabindex($el, level) {
|
||||
var tabInfo = $el.data('drupalOriginalTabIndices');
|
||||
if (tabInfo && tabInfo[level]) {
|
||||
var 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');
|
||||
}
|
||||
} 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.
|
||||
} else {
|
||||
var levelToDelete = level;
|
||||
while (tabInfo.hasOwnProperty(levelToDelete)) {
|
||||
delete tabInfo[levelToDelete];
|
||||
@@ -246,124 +109,51 @@
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Stores a set of tabbable elements.
|
||||
*
|
||||
* This constraint can be removed with the release() method.
|
||||
*
|
||||
* @constructor Drupal~TabbingContext
|
||||
*
|
||||
* @param {object} options
|
||||
* A set of initiating values
|
||||
* @param {number} options.level
|
||||
* The level in the TabbingManager's stack of this tabbingContext.
|
||||
* @param {jQuery} options.$tabbableElements
|
||||
* The DOM elements that should be reachable via the tab key when this
|
||||
* tabbingContext is active.
|
||||
* @param {jQuery} options.$disabledElements
|
||||
* The DOM elements that should not be reachable via the tab key when this
|
||||
* tabbingContext is active.
|
||||
* @param {bool} options.released
|
||||
* A released tabbingContext can never be activated again. It will be
|
||||
* cleaned up when the TabbingManager unwinds its stack.
|
||||
* @param {bool} options.active
|
||||
* When true, the tabbable elements of this tabbingContext will be reachable
|
||||
* via the tab key and the disabled elements will not. Only one
|
||||
* tabbingContext can be active at a time.
|
||||
*/
|
||||
function TabbingContext(options) {
|
||||
|
||||
$.extend(this, /** @lends Drupal~TabbingContext# */{
|
||||
|
||||
/**
|
||||
* @type {?number}
|
||||
*/
|
||||
$.extend(this, {
|
||||
level: null,
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$tabbableElements: $(),
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$disabledElements: $(),
|
||||
|
||||
/**
|
||||
* @type {bool}
|
||||
*/
|
||||
released: false,
|
||||
|
||||
/**
|
||||
* @type {bool}
|
||||
*/
|
||||
active: false
|
||||
}, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add public methods to the TabbingContext class.
|
||||
*/
|
||||
$.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: function () {
|
||||
$.extend(TabbingContext.prototype, {
|
||||
release: function 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: function () {
|
||||
// A released TabbingContext object can never be activated again.
|
||||
activate: function activate() {
|
||||
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: function () {
|
||||
deactivate: function 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.
|
||||
if (Drupal.tabbingManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Drupal~TabbingManager}
|
||||
*/
|
||||
Drupal.tabbingManager = new TabbingManager();
|
||||
|
||||
}(jQuery, Drupal));
|
||||
})(jQuery, Drupal);
|
||||
File diff suppressed because it is too large
Load Diff
+153
-746
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* @file
|
||||
* 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,
|
||||
});
|
||||
|
||||
/**
|
||||
* Constructor for the tableHeader object. Provides sticky table headers.
|
||||
*
|
||||
* TableHeader will make the current table header stick to the top of the page
|
||||
* if the table is very long.
|
||||
*
|
||||
* @constructor Drupal.TableHeader
|
||||
*
|
||||
* @param {HTMLElement} table
|
||||
* DOM object for the table to add a sticky header to.
|
||||
*
|
||||
* @listens event:columnschange
|
||||
*/
|
||||
function TableHeader(table) {
|
||||
const $table = $(table);
|
||||
|
||||
/**
|
||||
* @name Drupal.TableHeader#$originalTable
|
||||
*
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
this.$originalTable = $table;
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$originalHeader = $table.children('thead');
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
|
||||
|
||||
/**
|
||||
* @type {null|bool}
|
||||
*/
|
||||
this.displayWeight = null;
|
||||
this.$originalTable.addClass('sticky-table');
|
||||
this.tableHeight = $table[0].clientHeight;
|
||||
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;
|
||||
});
|
||||
|
||||
// Create and display sticky header.
|
||||
this.createSticky();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 prototype.
|
||||
*/
|
||||
$.extend(TableHeader.prototype, /** @lends Drupal.TableHeader# */{
|
||||
|
||||
/**
|
||||
* 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 {?number}
|
||||
*/
|
||||
tableHeight: null,
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
this.$stickyHeaderCells = $stickyHeader.find('> tr > th');
|
||||
|
||||
// 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 });
|
||||
}
|
||||
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));
|
||||
+29
-178
@@ -1,23 +1,14 @@
|
||||
/**
|
||||
* @file
|
||||
* Sticky table headers.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, displace) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Attaches sticky table headers.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches the sticky table header behavior.
|
||||
*/
|
||||
Drupal.behaviors.tableHeader = {
|
||||
attach: function (context) {
|
||||
$(window).one('scroll.TableHeaderInit', {context: context}, tableHeaderInitHandler);
|
||||
attach: function attach(context) {
|
||||
$(window).one('scroll.TableHeaderInit', { context: context }, tableHeaderInitHandler);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +16,6 @@
|
||||
return document.documentElement[position] || document.body[position];
|
||||
}
|
||||
|
||||
// Select and initialize sticky table headers.
|
||||
function tableHeaderInitHandler(e) {
|
||||
var $tables = $(e.data.context).find('table.sticky-enabled').once('tableheader');
|
||||
var il = $tables.length;
|
||||
@@ -35,7 +25,6 @@
|
||||
forTables('onScroll');
|
||||
}
|
||||
|
||||
// Helper method to loop through tables and execute a method.
|
||||
function forTables(method, arg) {
|
||||
var tables = TableHeader.tables;
|
||||
var il = tables.length;
|
||||
@@ -56,85 +45,33 @@
|
||||
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
|
||||
*/
|
||||
$(document).on({
|
||||
'columnschange.TableHeader': tableHeaderResizeHandler,
|
||||
|
||||
/**
|
||||
* Recalculate TableHeader.topOffset when viewport is resized.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler
|
||||
});
|
||||
|
||||
/**
|
||||
* Constructor for the tableHeader object. Provides sticky table headers.
|
||||
*
|
||||
* TableHeader will make the current table header stick to the top of the page
|
||||
* if the table is very long.
|
||||
*
|
||||
* @constructor Drupal.TableHeader
|
||||
*
|
||||
* @param {HTMLElement} table
|
||||
* DOM object for the table to add a sticky header to.
|
||||
*
|
||||
* @listens event:columnschange
|
||||
*/
|
||||
function TableHeader(table) {
|
||||
var $table = $(table);
|
||||
|
||||
/**
|
||||
* @name Drupal.TableHeader#$originalTable
|
||||
*
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
this.$originalTable = $table;
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$originalHeader = $table.children('thead');
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
|
||||
|
||||
/**
|
||||
* @type {null|bool}
|
||||
*/
|
||||
this.displayWeight = null;
|
||||
this.$originalTable.addClass('sticky-table');
|
||||
this.tableHeight = $table[0].clientHeight;
|
||||
this.tableOffset = this.$originalTable.offset();
|
||||
|
||||
// React to columns change to avoid making checks in the scroll callback.
|
||||
this.$originalTable.on('columnschange', {tableHeader: this}, function (e, display) {
|
||||
this.$originalTable.on('columnschange', { tableHeader: this }, function (e, display) {
|
||||
var tableHeader = e.data.tableHeader;
|
||||
if (tableHeader.displayWeight === null || tableHeader.displayWeight !== display) {
|
||||
tableHeader.recalculateSticky();
|
||||
@@ -142,167 +79,83 @@
|
||||
tableHeader.displayWeight = display;
|
||||
});
|
||||
|
||||
// Create and display sticky header.
|
||||
this.createSticky();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the state of TableHeader.
|
||||
*/
|
||||
$.extend(TableHeader, /** @lends Drupal.TableHeader */{
|
||||
|
||||
/**
|
||||
* This will store the state of all processed tables.
|
||||
*
|
||||
* @type {Array.<Drupal.TableHeader>}
|
||||
*/
|
||||
$.extend(TableHeader, {
|
||||
tables: []
|
||||
});
|
||||
|
||||
/**
|
||||
* Extend TableHeader prototype.
|
||||
*/
|
||||
$.extend(TableHeader.prototype, /** @lends Drupal.TableHeader# */{
|
||||
|
||||
/**
|
||||
* Minimum height in pixels for the table to have a sticky header.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
$.extend(TableHeader.prototype, {
|
||||
minHeight: 100,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
/**
|
||||
* Boolean storing the sticky header visibility state.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
stickyVisible: false,
|
||||
|
||||
/**
|
||||
* Create the duplicate header.
|
||||
*/
|
||||
createSticky: function () {
|
||||
// Clone the table header so it inherits original jQuery properties.
|
||||
createSticky: function createSticky() {
|
||||
var $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.$stickyTable = $('<table class="sticky-header"/>').css({
|
||||
visibility: 'hidden',
|
||||
position: 'fixed',
|
||||
top: '0px'
|
||||
}).append($stickyHeader).insertBefore(this.$originalTable);
|
||||
|
||||
this.$stickyHeaderCells = $stickyHeader.find('> tr > th');
|
||||
|
||||
// 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: function (offsetTop, offsetLeft) {
|
||||
stickyPosition: function stickyPosition(offsetTop, offsetLeft) {
|
||||
var css = {};
|
||||
if (typeof offsetTop === 'number') {
|
||||
css.top = offsetTop + 'px';
|
||||
}
|
||||
if (typeof offsetLeft === 'number') {
|
||||
css.left = (this.tableOffset.left - offsetLeft) + 'px';
|
||||
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: function () {
|
||||
checkStickyVisible: function checkStickyVisible() {
|
||||
var scrollTop = scrollValue('scrollTop');
|
||||
var tableTop = this.tableOffset.top - displace.offsets.top;
|
||||
var tableBottom = tableTop + this.tableHeight;
|
||||
var visible = false;
|
||||
|
||||
if (tableTop < scrollTop && scrollTop < (tableBottom - this.minHeight)) {
|
||||
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: function (e) {
|
||||
onScroll: function 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: function (event) {
|
||||
// Update table size.
|
||||
recalculateSticky: function recalculateSticky(event) {
|
||||
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.
|
||||
var $that = null;
|
||||
var $stickyCell = null;
|
||||
var 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.
|
||||
|
||||
var il = this.$originalHeaderCells.length;
|
||||
for (var 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: display});
|
||||
}
|
||||
else {
|
||||
$stickyCell.css({ width: $that.css('width'), display: display });
|
||||
} else {
|
||||
$stickyCell.css('display', 'none');
|
||||
}
|
||||
}
|
||||
@@ -310,7 +163,5 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Expose constructor in the public space.
|
||||
Drupal.TableHeader = TableHeader;
|
||||
|
||||
}(jQuery, Drupal, window.parent.Drupal.displace));
|
||||
})(jQuery, Drupal, window.parent.Drupal.displace);
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @file
|
||||
* 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]));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The TableResponsive object optimizes table presentation for screen size.
|
||||
*
|
||||
* A responsive table hides columns at small screen sizes, leaving the most
|
||||
* important columns visible to the end user. Users should not be prevented
|
||||
* from accessing all columns, however. This class adds a toggle to a table
|
||||
* with hidden columns that exposes the columns. Exposing the columns will
|
||||
* likely break layouts, but it provides the user with a means to access
|
||||
* data, which is a guiding principle of responsive design.
|
||||
*
|
||||
* @constructor Drupal.TableResponsive
|
||||
*
|
||||
* @param {HTMLElement} table
|
||||
* The table element to initialize the responsive table on.
|
||||
*/
|
||||
function TableResponsive(table) {
|
||||
this.table = table;
|
||||
this.$table = $(table);
|
||||
this.showText = Drupal.t('Show all columns');
|
||||
this.hideText = Drupal.t('Hide lower priority columns');
|
||||
// Store a reference to the header elements of the table so that the DOM is
|
||||
// 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.'))
|
||||
.on('click', $.proxy(this, 'eventhandlerToggleColumns'));
|
||||
|
||||
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'))
|
||||
.trigger('resize.tableresponsive');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the TableResponsive function with a list of managed 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.
|
||||
*
|
||||
* Columns are assumed to be hidden if their header has the class priority-low
|
||||
* or priority-medium.
|
||||
*/
|
||||
$.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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
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);
|
||||
}
|
||||
// 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));
|
||||
+45
-115
@@ -1,22 +1,13 @@
|
||||
/**
|
||||
* @file
|
||||
* Responsive table functionality.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, window) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Attach the tableResponsive function to {@link Drupal.behaviors}.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches tableResponsive functionality.
|
||||
*/
|
||||
Drupal.behaviors.tableResponsive = {
|
||||
attach: function (context, settings) {
|
||||
attach: function attach(context, settings) {
|
||||
var $tables = $(context).find('table.responsive-enabled').once('tableresponsive');
|
||||
if ($tables.length) {
|
||||
var il = $tables.length;
|
||||
@@ -27,97 +18,44 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The TableResponsive object optimizes table presentation for screen size.
|
||||
*
|
||||
* A responsive table hides columns at small screen sizes, leaving the most
|
||||
* important columns visible to the end user. Users should not be prevented
|
||||
* from accessing all columns, however. This class adds a toggle to a table
|
||||
* with hidden columns that exposes the columns. Exposing the columns will
|
||||
* likely break layouts, but it provides the user with a means to access
|
||||
* data, which is a guiding principle of responsive design.
|
||||
*
|
||||
* @constructor Drupal.TableResponsive
|
||||
*
|
||||
* @param {HTMLElement} table
|
||||
* The table element to initialize the responsive table on.
|
||||
*/
|
||||
function TableResponsive(table) {
|
||||
this.table = table;
|
||||
this.$table = $(table);
|
||||
this.showText = Drupal.t('Show all columns');
|
||||
this.hideText = Drupal.t('Hide lower priority columns');
|
||||
// Store a reference to the header elements of the table so that the DOM is
|
||||
// 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.'))
|
||||
.on('click', $.proxy(this, 'eventhandlerToggleColumns'));
|
||||
|
||||
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));
|
||||
|
||||
// Attach a resize handler to the window.
|
||||
$(window)
|
||||
.on('resize.tableresponsive', $.proxy(this, 'eventhandlerEvaluateColumnVisibility'))
|
||||
.trigger('resize.tableresponsive');
|
||||
$(window).on('resize.tableresponsive', $.proxy(this, 'eventhandlerEvaluateColumnVisibility')).trigger('resize.tableresponsive');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the TableResponsive function with a list of managed tables.
|
||||
*/
|
||||
$.extend(TableResponsive, /** @lends Drupal.TableResponsive */{
|
||||
|
||||
/**
|
||||
* Store all created instances.
|
||||
*
|
||||
* @type {Array.<Drupal.TableResponsive>}
|
||||
*/
|
||||
$.extend(TableResponsive, {
|
||||
tables: []
|
||||
});
|
||||
|
||||
/**
|
||||
* Associates an action link with the table that will show hidden columns.
|
||||
*
|
||||
* Columns are assumed to be hidden if their header has the class priority-low
|
||||
* or priority-medium.
|
||||
*/
|
||||
$.extend(TableResponsive.prototype, /** @lends Drupal.TableResponsive# */{
|
||||
|
||||
/**
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
*/
|
||||
eventhandlerEvaluateColumnVisibility: function (e) {
|
||||
$.extend(TableResponsive.prototype, {
|
||||
eventhandlerEvaluateColumnVisibility: function eventhandlerEvaluateColumnVisibility(e) {
|
||||
var pegged = parseInt(this.$link.data('pegged'), 10);
|
||||
var 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: function (e) {
|
||||
eventhandlerToggleColumns: function eventhandlerToggleColumns(e) {
|
||||
e.preventDefault();
|
||||
var self = this;
|
||||
var $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) {
|
||||
var $header = $(this);
|
||||
@@ -125,50 +63,42 @@
|
||||
self.$table.find('tbody tr').each(function () {
|
||||
var $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);
|
||||
});
|
||||
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) {
|
||||
var $cell = $(this);
|
||||
var properties = $cell.attr('style').split(';');
|
||||
var 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.
|
||||
var match = /^display\s*\:\s*none$/;
|
||||
for (var i = 0; i < properties.length; i++) {
|
||||
var prop = properties[i];
|
||||
prop.trim();
|
||||
// Find the display:none property and remove it.
|
||||
var isDisplayNone = match.exec(prop);
|
||||
if (isDisplayNone) {
|
||||
continue;
|
||||
} else {
|
||||
this.$revealedCells.hide();
|
||||
|
||||
this.$revealedCells.each(function (index, element) {
|
||||
var $cell = $(this);
|
||||
var properties = $cell.attr('style').split(';');
|
||||
var newProps = [];
|
||||
|
||||
var match = /^display\s*\:\s*none$/;
|
||||
for (var i = 0; i < properties.length; i++) {
|
||||
var prop = properties[i];
|
||||
prop.trim();
|
||||
|
||||
var 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');
|
||||
}
|
||||
|
||||
$cell.attr('style', newProps.join(';'));
|
||||
});
|
||||
this.$link.text(this.showText).data('pegged', 0);
|
||||
|
||||
$(window).trigger('resize.tableresponsive');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Make the TableResponsive object available in the Drupal namespace.
|
||||
Drupal.TableResponsive = TableResponsive;
|
||||
|
||||
})(jQuery, Drupal, window);
|
||||
})(jQuery, Drupal, window);
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @file
|
||||
* Table select functionality.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Initialize tableSelects.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches tableSelect functionality.
|
||||
*/
|
||||
Drupal.behaviors.tableSelect = {
|
||||
attach(context, settings) {
|
||||
// Select the inner-most table in case of nested tables.
|
||||
$(context).find('th.select-all').closest('table').once('table-select').each(Drupal.tableSelect);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used in {@link Drupal.behaviors.tableSelect}.
|
||||
*/
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep track of the table, which checkbox is checked and alias the
|
||||
// settings.
|
||||
const table = this;
|
||||
let checkboxes;
|
||||
let lastChecked;
|
||||
const $table = $(table);
|
||||
const strings = {
|
||||
selectAll: Drupal.t('Select all rows in this table'),
|
||||
selectNone: Drupal.t('Deselect all rows in this table'),
|
||||
};
|
||||
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 () {
|
||||
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.
|
||||
|
||||
/**
|
||||
* @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 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;
|
||||
});
|
||||
|
||||
// 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));
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} from
|
||||
* The HTML element representing the "from" part of the range.
|
||||
* @param {HTMLElement} to
|
||||
* The HTML element representing the "to" part of the range.
|
||||
* @param {bool} state
|
||||
* The state to set on the range.
|
||||
*/
|
||||
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';
|
||||
|
||||
// Traverse through the sibling nodes.
|
||||
for (let i = from[mode]; i; i = i[mode]) {
|
||||
var $i;
|
||||
// Make sure that we're only dealing with elements.
|
||||
if (i.nodeType !== 1) {
|
||||
continue;
|
||||
}
|
||||
$i = $(i);
|
||||
// Either add or remove the selected class based on the state of the
|
||||
// target checkbox.
|
||||
$i.toggleClass('selected', state);
|
||||
$i.find('input[type="checkbox"]').prop('checked', state);
|
||||
|
||||
if (to.nodeType) {
|
||||
// If we are at the end of the range, stop.
|
||||
if (i === to) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A faster alternative to doing $(i).filter(to).length.
|
||||
else if ($.filter(to, [i]).r.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}(jQuery, Drupal));
|
||||
+18
-81
@@ -1,159 +1,96 @@
|
||||
/**
|
||||
* @file
|
||||
* Table select functionality.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialize tableSelects.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches tableSelect functionality.
|
||||
*/
|
||||
Drupal.behaviors.tableSelect = {
|
||||
attach: function (context, settings) {
|
||||
// Select the inner-most table in case of nested tables.
|
||||
attach: function attach(context, settings) {
|
||||
$(context).find('th.select-all').closest('table').once('table-select').each(Drupal.tableSelect);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used in {@link Drupal.behaviors.tableSelect}.
|
||||
*/
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep track of the table, which checkbox is checked and alias the
|
||||
// settings.
|
||||
var table = this;
|
||||
var checkboxes;
|
||||
var lastChecked;
|
||||
var checkboxes = void 0;
|
||||
var lastChecked = void 0;
|
||||
var $table = $(table);
|
||||
var strings = {
|
||||
selectAll: Drupal.t('Select all rows in this table'),
|
||||
selectNone: Drupal.t('Deselect all rows in this table')
|
||||
};
|
||||
var updateSelectAll = function (state) {
|
||||
// Update table's select-all checkbox (and sticky header's if available).
|
||||
var updateSelectAll = function updateSelectAll(state) {
|
||||
$table.prev('table.sticky-header').addBack().find('th.select-all input[type="checkbox"]').each(function () {
|
||||
var $checkbox = $(this);
|
||||
var 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', function (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 () {
|
||||
var $checkbox = $(this);
|
||||
var 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.
|
||||
|
||||
/**
|
||||
* @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 all checkboxes are checked, 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);
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} from
|
||||
* The HTML element representing the "from" part of the range.
|
||||
* @param {HTMLElement} to
|
||||
* The HTML element representing the "to" part of the range.
|
||||
* @param {bool} state
|
||||
* The state to set on the range.
|
||||
*/
|
||||
Drupal.tableSelectRange = function (from, to, state) {
|
||||
// We determine the looping mode based on the order of from and to.
|
||||
var mode = from.rowIndex > to.rowIndex ? 'previousSibling' : 'nextSibling';
|
||||
|
||||
// Traverse through the sibling nodes.
|
||||
for (var i = from[mode]; i; i = i[mode]) {
|
||||
var $i;
|
||||
// Make sure that we're only dealing with elements.
|
||||
|
||||
if (i.nodeType !== 1) {
|
||||
continue;
|
||||
}
|
||||
$i = $(i);
|
||||
// Either add or remove the selected class based on the state of the
|
||||
// target checkbox.
|
||||
|
||||
$i.toggleClass('selected', state);
|
||||
$i.find('input[type="checkbox"]').prop('checked', state);
|
||||
|
||||
if (to.nodeType) {
|
||||
// If we are at the end of the range, stop.
|
||||
if (i === to) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A faster alternative to doing $(i).filter(to).length.
|
||||
else if ($.filter(to, [i]).r.length) {
|
||||
break;
|
||||
}
|
||||
} else if ($.filter(to, [i]).r.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @file
|
||||
* Timezone detection.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Set the client's system time zone as default values of form fields.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.setTimezone = {
|
||||
attach(context, settings) {
|
||||
const $timezone = $(context).find('.timezone-detect').once('timezone');
|
||||
if ($timezone.length) {
|
||||
const dateString = Date();
|
||||
// In some client environments, date strings include a time zone
|
||||
// abbreviation, between 3 and 5 letters enclosed in parentheses,
|
||||
// which can be interpreted by PHP.
|
||||
const matches = dateString.match(/\(([A-Z]{3,5})\)/);
|
||||
const abbreviation = matches ? matches[1] : 0;
|
||||
|
||||
// For all other client environments, the abbreviation is set to "0"
|
||||
// and the current offset from UTC and daylight saving time status are
|
||||
// used to guess the time zone.
|
||||
const dateNow = new Date();
|
||||
const offsetNow = dateNow.getTimezoneOffset() * -60;
|
||||
|
||||
// Use January 1 and July 1 as test dates for determining daylight
|
||||
// saving time status by comparing their offsets.
|
||||
const dateJan = new Date(dateNow.getFullYear(), 0, 1, 12, 0, 0, 0);
|
||||
const dateJul = new Date(dateNow.getFullYear(), 6, 1, 12, 0, 0, 0);
|
||||
const offsetJan = dateJan.getTimezoneOffset() * -60;
|
||||
const offsetJul = dateJul.getTimezoneOffset() * -60;
|
||||
|
||||
let isDaylightSavingTime;
|
||||
// If the offset from UTC is identical on January 1 and July 1,
|
||||
// assume daylight saving time is not used in this time zone.
|
||||
if (offsetJan === offsetJul) {
|
||||
isDaylightSavingTime = '';
|
||||
}
|
||||
// If the maximum annual offset is equivalent to the current offset,
|
||||
// assume daylight saving time is in effect.
|
||||
else if (Math.max(offsetJan, offsetJul) === offsetNow) {
|
||||
isDaylightSavingTime = 1;
|
||||
}
|
||||
// Otherwise, assume daylight saving time is not in effect.
|
||||
else {
|
||||
isDaylightSavingTime = 0;
|
||||
}
|
||||
|
||||
// Submit request to the system/timezone callback and set the form
|
||||
// field to the response time zone. The client date is passed to the
|
||||
// callback for debugging purposes. Submit a synchronous request to
|
||||
// avoid database errors associated with concurrent requests
|
||||
// during install.
|
||||
const path = `system/timezone/${abbreviation}/${offsetNow}/${isDaylightSavingTime}`;
|
||||
$.ajax({
|
||||
async: false,
|
||||
url: Drupal.url(path),
|
||||
data: { date: dateString },
|
||||
dataType: 'json',
|
||||
success(data) {
|
||||
if (data) {
|
||||
$timezone.val(data);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}(jQuery, Drupal));
|
||||
+17
-42
@@ -1,69 +1,45 @@
|
||||
/**
|
||||
* @file
|
||||
* Timezone detection.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Set the client's system time zone as default values of form fields.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.setTimezone = {
|
||||
attach: function (context, settings) {
|
||||
attach: function attach(context, settings) {
|
||||
var $timezone = $(context).find('.timezone-detect').once('timezone');
|
||||
if ($timezone.length) {
|
||||
var dateString = Date();
|
||||
// In some client environments, date strings include a time zone
|
||||
// abbreviation, between 3 and 5 letters enclosed in parentheses,
|
||||
// which can be interpreted by PHP.
|
||||
|
||||
var matches = dateString.match(/\(([A-Z]{3,5})\)/);
|
||||
var abbreviation = matches ? matches[1] : 0;
|
||||
|
||||
// For all other client environments, the abbreviation is set to "0"
|
||||
// and the current offset from UTC and daylight saving time status are
|
||||
// used to guess the time zone.
|
||||
var dateNow = new Date();
|
||||
var offsetNow = dateNow.getTimezoneOffset() * -60;
|
||||
|
||||
// Use January 1 and July 1 as test dates for determining daylight
|
||||
// saving time status by comparing their offsets.
|
||||
var dateJan = new Date(dateNow.getFullYear(), 0, 1, 12, 0, 0, 0);
|
||||
var dateJul = new Date(dateNow.getFullYear(), 6, 1, 12, 0, 0, 0);
|
||||
var offsetJan = dateJan.getTimezoneOffset() * -60;
|
||||
var offsetJul = dateJul.getTimezoneOffset() * -60;
|
||||
|
||||
var isDaylightSavingTime;
|
||||
// If the offset from UTC is identical on January 1 and July 1,
|
||||
// assume daylight saving time is not used in this time zone.
|
||||
var isDaylightSavingTime = void 0;
|
||||
|
||||
if (offsetJan === offsetJul) {
|
||||
isDaylightSavingTime = '';
|
||||
}
|
||||
// If the maximum annual offset is equivalent to the current offset,
|
||||
// assume daylight saving time is in effect.
|
||||
else if (Math.max(offsetJan, offsetJul) === offsetNow) {
|
||||
isDaylightSavingTime = 1;
|
||||
}
|
||||
// Otherwise, assume daylight saving time is not in effect.
|
||||
else {
|
||||
isDaylightSavingTime = 0;
|
||||
}
|
||||
} else if (Math.max(offsetJan, offsetJul) === offsetNow) {
|
||||
isDaylightSavingTime = 1;
|
||||
} else {
|
||||
isDaylightSavingTime = 0;
|
||||
}
|
||||
|
||||
// Submit request to the system/timezone callback and set the form
|
||||
// field to the response time zone. The client date is passed to the
|
||||
// callback for debugging purposes. Submit a synchronous request to
|
||||
// avoid database errors associated with concurrent requests
|
||||
// during install.
|
||||
var path = 'system/timezone/' + abbreviation + '/' + offsetNow + '/' + isDaylightSavingTime;
|
||||
$.ajax({
|
||||
async: false,
|
||||
url: Drupal.url(path),
|
||||
data: {date: dateString},
|
||||
data: { date: dateString },
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
success: function success(data) {
|
||||
if (data) {
|
||||
$timezone.val(data);
|
||||
}
|
||||
@@ -72,5 +48,4 @@
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
||||
})(jQuery, Drupal);
|
||||
@@ -8,8 +8,8 @@
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
[dir="rtl"] .vertical-tabs {
|
||||
margin-left: 0;
|
||||
margin-right: 15em;
|
||||
margin-left: 0;
|
||||
margin-right: 15em;
|
||||
}
|
||||
.vertical-tabs__menu {
|
||||
float: left; /* LTR */
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* @file
|
||||
* Define vertical tabs functionality.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggers when form values inside a vertical tab changes.
|
||||
*
|
||||
* This is used to update the summary in vertical tabs in order to know what
|
||||
* are the important fields' values.
|
||||
*
|
||||
* @event summaryUpdated
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
/**
|
||||
* Show the parent vertical tab pane of a targeted page fragment.
|
||||
*
|
||||
* In order to make sure a targeted element inside a vertical tab pane is
|
||||
* visible on a hash change or fragment link click, show all parent panes.
|
||||
*
|
||||
* @param {jQuery.Event} e
|
||||
* The event triggered.
|
||||
* @param {jQuery} $target
|
||||
* The targeted node as a jQuery object.
|
||||
*/
|
||||
const handleFragmentLinkClickOrHashChange = (e, $target) => {
|
||||
$target.parents('.vertical-tabs__pane').each((index, pane) => {
|
||||
$(pane).data('verticalTab').focus();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This script transforms a set of details into a stack of vertical tabs.
|
||||
*
|
||||
* Each tab may have a summary which can be updated by another
|
||||
* script. For that to work, each details element has an associated
|
||||
* 'verticalTabCallback' (with jQuery.data() attached to the details),
|
||||
* which is called every time the user performs an update to a form
|
||||
* element inside the tab pane.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches behaviors for vertical tabs.
|
||||
*/
|
||||
Drupal.behaviors.verticalTabs = {
|
||||
attach(context) {
|
||||
const width = drupalSettings.widthBreakpoint || 640;
|
||||
const mq = `(max-width: ${width}px)`;
|
||||
|
||||
if (window.matchMedia(mq).matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a listener to handle fragment link clicks and URL hash changes.
|
||||
*/
|
||||
$('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 tab_focus;
|
||||
|
||||
// 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 tab_list = $('<ul class="vertical-tabs__menu"></ul>');
|
||||
$this.wrap('<div class="vertical-tabs clearfix"></div>').before(tab_list);
|
||||
|
||||
// Transform each details into a tab.
|
||||
$details.each(function () {
|
||||
const $that = $(this);
|
||||
const vertical_tab = new Drupal.verticalTab({
|
||||
title: $that.find('> summary').text(),
|
||||
details: $that,
|
||||
});
|
||||
tab_list.append(vertical_tab.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', vertical_tab);
|
||||
if (this.id === focusID) {
|
||||
tab_focus = $that;
|
||||
}
|
||||
});
|
||||
|
||||
$(tab_list).find('> li').eq(0).addClass('first');
|
||||
$(tab_list).find('> li').eq(-1).addClass('last');
|
||||
|
||||
if (!tab_focus) {
|
||||
// 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) {
|
||||
tab_focus = $locationHash.closest('.vertical-tabs__pane');
|
||||
}
|
||||
else {
|
||||
tab_focus = $this.find('> .vertical-tabs__pane').eq(0);
|
||||
}
|
||||
}
|
||||
if (tab_focus.length) {
|
||||
tab_focus.data('verticalTab').focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The vertical tab object represents a single tab within a tab group.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {object} settings
|
||||
* Settings object.
|
||||
* @param {string} settings.title
|
||||
* The name of the tab.
|
||||
* @param {jQuery} settings.details
|
||||
* The jQuery object of the details element that is the tab pane.
|
||||
*
|
||||
* @fires event:summaryUpdated
|
||||
*
|
||||
* @listens event:summaryUpdated
|
||||
*/
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
self.focus();
|
||||
});
|
||||
|
||||
// Keyboard events added:
|
||||
// Pressing the Enter key will open the tab pane.
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
this.details
|
||||
.on('summaryUpdated', () => {
|
||||
self.updateSummary();
|
||||
})
|
||||
.trigger('summaryUpdated');
|
||||
};
|
||||
|
||||
Drupal.verticalTab.prototype = {
|
||||
|
||||
/**
|
||||
* Displays the tab's content pane.
|
||||
*/
|
||||
focus() {
|
||||
this.details
|
||||
.siblings('.vertical-tabs__pane')
|
||||
.each(function () {
|
||||
const tab = $(this).data('verticalTab');
|
||||
tab.details.hide();
|
||||
tab.item.removeClass('is-selected');
|
||||
})
|
||||
.end()
|
||||
.show()
|
||||
.siblings(':hidden.vertical-tabs__active-tab')
|
||||
.val(this.details.attr('id'));
|
||||
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>`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the tab's summary.
|
||||
*/
|
||||
updateSummary() {
|
||||
this.summary.html(this.details.drupalGetSummary());
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a vertical tab pane.
|
||||
*
|
||||
* @return {Drupal.verticalTab}
|
||||
* The verticalTab instance.
|
||||
*/
|
||||
tabShow() {
|
||||
// Display the tab.
|
||||
this.item.show();
|
||||
// Show the vertical tabs.
|
||||
this.item.closest('.js-form-type-vertical-tabs').show();
|
||||
// Update .first marker for items. We need recurse from parent to retain
|
||||
// the actual DOM element order as jQuery implements sortOrder, but not
|
||||
// as public method.
|
||||
this.item.parent().children('.vertical-tabs__menu-item').removeClass('first')
|
||||
.filter(':visible').eq(0).addClass('first');
|
||||
// Display the details element.
|
||||
this.details.removeClass('vertical-tab--hidden').show();
|
||||
// Focus this tab.
|
||||
this.focus();
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides a vertical tab pane.
|
||||
*
|
||||
* @return {Drupal.verticalTab}
|
||||
* The verticalTab instance.
|
||||
*/
|
||||
tabHide() {
|
||||
// Hide this tab.
|
||||
this.item.hide();
|
||||
// Update .first marker for items. We need recurse from parent to retain
|
||||
// the actual DOM element order as jQuery implements sortOrder, but not
|
||||
// as public method.
|
||||
this.item.parent().children('.vertical-tabs__menu-item').removeClass('first')
|
||||
.filter(':visible').eq(0).addClass('first');
|
||||
// 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);
|
||||
if ($firstTab.length) {
|
||||
$firstTab.data('verticalTab').focus();
|
||||
}
|
||||
// Hide the vertical tabs (if no tabs remain).
|
||||
else {
|
||||
this.item.closest('.js-form-type-vertical-tabs').hide();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme function for a vertical tab.
|
||||
*
|
||||
* @param {object} settings
|
||||
* An object with the following keys:
|
||||
* @param {string} settings.title
|
||||
* The name of the tab.
|
||||
*
|
||||
* @return {object}
|
||||
* This function has to return an object with at least these keys:
|
||||
* - item: The root tab jQuery element
|
||||
* - link: The anchor tag that acts as the clickable area of the tab
|
||||
* (jQuery version)
|
||||
* - summary: The jQuery element that contains the tab summary
|
||||
*/
|
||||
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>'),
|
||||
),
|
||||
);
|
||||
return tab;
|
||||
};
|
||||
}(jQuery, Drupal, drupalSettings));
|
||||
+44
-152
@@ -1,37 +1,19 @@
|
||||
/**
|
||||
* @file
|
||||
* Define vertical tabs functionality.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triggers when form values inside a vertical tab changes.
|
||||
*
|
||||
* This is used to update the summary in vertical tabs in order to know what
|
||||
* are the important fields' values.
|
||||
*
|
||||
* @event summaryUpdated
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
var handleFragmentLinkClickOrHashChange = function handleFragmentLinkClickOrHashChange(e, $target) {
|
||||
$target.parents('.vertical-tabs__pane').each(function (index, pane) {
|
||||
$(pane).data('verticalTab').focus();
|
||||
});
|
||||
};
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This script transforms a set of details into a stack of vertical tabs.
|
||||
*
|
||||
* Each tab may have a summary which can be updated by another
|
||||
* script. For that to work, each details element has an associated
|
||||
* 'verticalTabCallback' (with jQuery.data() attached to the details),
|
||||
* which is called every time the user performs an update to a form
|
||||
* element inside the tab pane.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches behaviors for vertical tabs.
|
||||
*/
|
||||
Drupal.behaviors.verticalTabs = {
|
||||
attach: function (context) {
|
||||
attach: function attach(context) {
|
||||
var width = drupalSettings.widthBreakpoint || 640;
|
||||
var mq = '(max-width: ' + width + 'px)';
|
||||
|
||||
@@ -39,23 +21,21 @@
|
||||
return;
|
||||
}
|
||||
|
||||
$('body').once('vertical-tabs-fragments').on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange);
|
||||
|
||||
$(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function () {
|
||||
var $this = $(this).addClass('vertical-tabs__panes');
|
||||
var focusID = $this.find(':hidden.vertical-tabs__active-tab').val();
|
||||
var tab_focus;
|
||||
var tab_focus = void 0;
|
||||
|
||||
// Check if there are some details that can be converted to
|
||||
// vertical-tabs.
|
||||
var $details = $this.find('> details');
|
||||
if ($details.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the tab column.
|
||||
var tab_list = $('<ul class="vertical-tabs__menu"></ul>');
|
||||
$this.wrap('<div class="vertical-tabs clearfix"></div>').before(tab_list);
|
||||
|
||||
// Transform each details into a tab.
|
||||
$details.each(function () {
|
||||
var $that = $(this);
|
||||
var vertical_tab = new Drupal.verticalTab({
|
||||
@@ -63,13 +43,7 @@
|
||||
details: $that
|
||||
});
|
||||
tab_list.append(vertical_tab.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', vertical_tab);
|
||||
$that.removeClass('collapsed').attr('open', true).addClass('vertical-tabs__pane').data('verticalTab', vertical_tab);
|
||||
if (this.id === focusID) {
|
||||
tab_focus = $that;
|
||||
}
|
||||
@@ -79,13 +53,10 @@
|
||||
$(tab_list).find('> li').eq(-1).addClass('last');
|
||||
|
||||
if (!tab_focus) {
|
||||
// If the current URL has a fragment and one of the tabs contains an
|
||||
// element that matches the URL fragment, activate that tab.
|
||||
var $locationHash = $this.find(window.location.hash);
|
||||
if (window.location.hash && $locationHash.length) {
|
||||
tab_focus = $locationHash.closest('.vertical-tabs__pane');
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
tab_focus = $this.find('> .vertical-tabs__pane').eq(0);
|
||||
}
|
||||
}
|
||||
@@ -96,22 +67,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The vertical tab object represents a single tab within a tab group.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {object} settings
|
||||
* Settings object.
|
||||
* @param {string} settings.title
|
||||
* The name of the tab.
|
||||
* @param {jQuery} settings.details
|
||||
* The jQuery object of the details element that is the tab pane.
|
||||
*
|
||||
* @fires event:summaryUpdated
|
||||
*
|
||||
* @listens event:summaryUpdated
|
||||
*/
|
||||
Drupal.verticalTab = function (settings) {
|
||||
var self = this;
|
||||
$.extend(this, settings, Drupal.theme('verticalTab', settings));
|
||||
@@ -123,130 +78,67 @@
|
||||
self.focus();
|
||||
});
|
||||
|
||||
// Keyboard events added:
|
||||
// Pressing the Enter key will open the tab pane.
|
||||
this.link.on('keydown', function (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');
|
||||
}
|
||||
});
|
||||
|
||||
this.details
|
||||
.on('summaryUpdated', function () {
|
||||
self.updateSummary();
|
||||
})
|
||||
.trigger('summaryUpdated');
|
||||
this.details.on('summaryUpdated', function () {
|
||||
self.updateSummary();
|
||||
}).trigger('summaryUpdated');
|
||||
};
|
||||
|
||||
Drupal.verticalTab.prototype = {
|
||||
|
||||
/**
|
||||
* Displays the tab's content pane.
|
||||
*/
|
||||
focus: function () {
|
||||
this.details
|
||||
.siblings('.vertical-tabs__pane')
|
||||
.each(function () {
|
||||
var tab = $(this).data('verticalTab');
|
||||
tab.details.hide();
|
||||
tab.item.removeClass('is-selected');
|
||||
})
|
||||
.end()
|
||||
.show()
|
||||
.siblings(':hidden.vertical-tabs__active-tab')
|
||||
.val(this.details.attr('id'));
|
||||
focus: function focus() {
|
||||
this.details.siblings('.vertical-tabs__pane').each(function () {
|
||||
var tab = $(this).data('verticalTab');
|
||||
tab.details.hide();
|
||||
tab.item.removeClass('is-selected');
|
||||
}).end().show().siblings(':hidden.vertical-tabs__active-tab').val(this.details.attr('id'));
|
||||
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>');
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the tab's summary.
|
||||
*/
|
||||
updateSummary: function () {
|
||||
updateSummary: function updateSummary() {
|
||||
this.summary.html(this.details.drupalGetSummary());
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a vertical tab pane.
|
||||
*
|
||||
* @return {Drupal.verticalTab}
|
||||
* The verticalTab instance.
|
||||
*/
|
||||
tabShow: function () {
|
||||
// Display the tab.
|
||||
tabShow: function tabShow() {
|
||||
this.item.show();
|
||||
// Show the vertical tabs.
|
||||
|
||||
this.item.closest('.js-form-type-vertical-tabs').show();
|
||||
// Update .first marker for items. We need recurse from parent to retain
|
||||
// the actual DOM element order as jQuery implements sortOrder, but not
|
||||
// as public method.
|
||||
this.item.parent().children('.vertical-tabs__menu-item').removeClass('first')
|
||||
.filter(':visible').eq(0).addClass('first');
|
||||
// Display the details element.
|
||||
|
||||
this.item.parent().children('.vertical-tabs__menu-item').removeClass('first').filter(':visible').eq(0).addClass('first');
|
||||
|
||||
this.details.removeClass('vertical-tab--hidden').show();
|
||||
// Focus this tab.
|
||||
|
||||
this.focus();
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides a vertical tab pane.
|
||||
*
|
||||
* @return {Drupal.verticalTab}
|
||||
* The verticalTab instance.
|
||||
*/
|
||||
tabHide: function () {
|
||||
// Hide this tab.
|
||||
tabHide: function tabHide() {
|
||||
this.item.hide();
|
||||
// Update .first marker for items. We need recurse from parent to retain
|
||||
// the actual DOM element order as jQuery implements sortOrder, but not
|
||||
// as public method.
|
||||
this.item.parent().children('.vertical-tabs__menu-item').removeClass('first')
|
||||
.filter(':visible').eq(0).addClass('first');
|
||||
// Hide the details element.
|
||||
|
||||
this.item.parent().children('.vertical-tabs__menu-item').removeClass('first').filter(':visible').eq(0).addClass('first');
|
||||
|
||||
this.details.addClass('vertical-tab--hidden').hide();
|
||||
// Focus the first visible tab (if there is one).
|
||||
|
||||
var $firstTab = this.details.siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)').eq(0);
|
||||
if ($firstTab.length) {
|
||||
$firstTab.data('verticalTab').focus();
|
||||
}
|
||||
// Hide the vertical tabs (if no tabs remain).
|
||||
else {
|
||||
this.item.closest('.js-form-type-vertical-tabs').hide();
|
||||
}
|
||||
} else {
|
||||
this.item.closest('.js-form-type-vertical-tabs').hide();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme function for a vertical tab.
|
||||
*
|
||||
* @param {object} settings
|
||||
* An object with the following keys:
|
||||
* @param {string} settings.title
|
||||
* The name of the tab.
|
||||
*
|
||||
* @return {object}
|
||||
* This function has to return an object with at least these keys:
|
||||
* - item: The root tab jQuery element
|
||||
* - link: The anchor tag that acts as the clickable area of the tab
|
||||
* (jQuery version)
|
||||
* - summary: The jQuery element that contains the tab summary
|
||||
*/
|
||||
Drupal.theme.verticalTab = function (settings) {
|
||||
var 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);
|
||||
Reference in New Issue
Block a user