first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
/**
* @file
* Extends methods from core/misc/ajax.js.
*/
(function ($, window, Drupal, drupalSettings) {
/**
* Attempts to find the closest glyphicon progress indicator.
*
* @param {jQuery|Element} element
* A DOM element.
*
* @returns {jQuery}
* A jQuery object.
*/
Drupal.Ajax.prototype.findGlyphicon = function (element) {
return $(element).closest('.form-item').find('.ajax-progress.glyphicon')
};
/**
* Starts the spinning of the glyphicon progress indicator.
*
* @param {jQuery|Element} element
* A DOM element.
* @param {string} [message]
* An optional message to display (tooltip) for the progress.
*
* @returns {jQuery}
* A jQuery object.
*/
Drupal.Ajax.prototype.glyphiconStart = function (element, message) {
var $glyphicon = this.findGlyphicon(element);
if ($glyphicon[0]) {
$glyphicon.addClass('glyphicon-spin');
// Add any message as a tooltip to the glyphicon.
if ($.fn.tooltip && drupalSettings.bootstrap.tooltip_enabled) {
$glyphicon
.removeAttr('data-toggle')
.removeAttr('data-original-title')
.removeAttr('title')
.tooltip('destroy')
;
if (message) {
$glyphicon.attr('data-toggle', 'tooltip').attr('title', message).tooltip();
}
}
// Append a message for screen readers.
if (message) {
$glyphicon.parent().append('<div class="sr-only message">' + message + '</div>');
}
}
return $glyphicon;
};
/**
* Stop the spinning of a glyphicon progress indicator.
*
* @param {jQuery|Element} element
* A DOM element.
*/
Drupal.Ajax.prototype.glyphiconStop = function (element) {
var $glyphicon = this.findGlyphicon(element);
if ($glyphicon[0]) {
$glyphicon.removeClass('glyphicon-spin');
if ($.fn.tooltip && drupalSettings.bootstrap.tooltip_enabled) {
$glyphicon
.removeAttr('data-toggle')
.removeAttr('data-original-title')
.removeAttr('title')
.tooltip('destroy')
;
}
}
};
/**
* Sets the throbber progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () {
var $element = $(this.element);
// Find an existing glyphicon progress indicator.
var $glyphicon = this.glyphiconStart($element, this.progress.message);
if ($glyphicon[0]) {
this.progress.element = $glyphicon.parent();
this.progress.glyphicon = true;
return;
}
// Otherwise, add a glyphicon throbber after the element.
if (!this.progress.element) {
this.progress.element = $(Drupal.theme('ajaxThrobber'));
}
if (this.progress.message) {
this.progress.element.after('<div class="message">' + this.progress.message + '</div>');
}
// If element is an input DOM element type (not :input), append after.
if ($element.is('input') || $element.is('select')) {
$element.after(this.progress.element);
}
// Otherwise append the throbber inside the element.
else {
$element.append(this.progress.element);
}
};
/**
* Handler for the form redirection completion.
*
* @param {Array.<Drupal.AjaxCommands~commandDefinition>} response
* @param {number} status
*/
var success = Drupal.Ajax.prototype.success;
Drupal.Ajax.prototype.success = function (response, status) {
if (this.progress.element) {
// Remove any message set.
this.progress.element.parent().find('.message').remove();
}
// Invoke the original success handler.
return success.apply(this, [response, status]);
};
})(jQuery, this, Drupal, drupalSettings);

View File

@@ -0,0 +1,36 @@
/**
* @file
* Extends autocomplete based on jQuery UI.
*
* @todo Remove once jQuery UI is no longer used?
*/
(function ($, Drupal) {
'use strict';
// Ensure the input element has a "change" event triggered. This is important
// so that summaries in vertical tabs can be updated properly.
// @see Drupal.behaviors.formUpdated
$(document).on('autocompleteselect', '.form-autocomplete', function (e) {
$(e.target).trigger('change.formUpdated');
});
// Extend ui.autocomplete widget so it triggers the glyphicon throbber.
$.widget('ui.autocomplete', $.ui.autocomplete, {
_search: function (value) {
this.pending++;
Drupal.Ajax.prototype.glyphiconStart(this.element);
this.cancelSearch = false;
this.source({term: value}, this._response());
},
_response: function () {
var index = ++this.requestIndex;
return $.proxy(function (content) {
if (index === this.requestIndex) this.__response(content);
this.pending--;
if (!this.pending) Drupal.Ajax.prototype.glyphiconStop(this.element);
}, this);
}
});
})(jQuery, Drupal);

View File

@@ -0,0 +1,43 @@
/**
* @file
* Drupal's batch API.
*/
(function ($, Drupal, once) {
'use strict';
/**
* Attaches the batch behavior to progress bars.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.batch = {
attach: function (context, settings) {
var batch = settings.batch;
var $progress = $(once('batch', '[data-drupal-progress]', context));
var 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) {
var id = $progress.find('.progress').attr('id') || 'updateprogress';
progressBar = new Drupal.ProgressBar(id, updateCallback, 'POST', errorCallback);
$progress.replaceWith(progressBar.element);
progressBar.setProgress(-1, batch.initMessage);
progressBar.startMonitoring(batch.uri + '&op=do', 10);
}
}
};
})(jQuery, Drupal, once);

View File

@@ -0,0 +1,115 @@
/**
* @file
* dialog.ajax.js
*/
(function ($, Drupal, Bootstrap) {
Drupal.behaviors.dialog.ajaxCurrentButton = null;
Drupal.behaviors.dialog.ajaxOriginalButton = null;
// Intercept the success event to add the dialog type to commands.
var success = Drupal.Ajax.prototype.success;
Drupal.Ajax.prototype.success = function (response, status) {
if (this.dialogType) {
for (var i = 0, l = response.length; i < l; i++) {
if (response[i].dialogOptions) {
response[i].dialogType = response[i].dialogOptions.dialogType = this.dialogType;
response[i].$trigger = response[i].dialogOptions.$trigger = $(this.element);
}
}
}
return success.apply(this, [response, status]);
};
var beforeSerialize = Drupal.Ajax.prototype.beforeSerialize;
Drupal.Ajax.prototype.beforeSerialize = function (element, options) {
// Add the dialog type currently in use.
if (this.dialogType) {
options.data['ajax_page_state[dialogType]'] = this.dialogType;
// Add the dialog element ID if it can be found (useful for closing it).
var id = $(this.element).parents('.js-drupal-dialog:first').attr('id');
if (id) {
options.data['ajax_page_state[dialogId]'] = id;
}
}
return beforeSerialize.apply(this, arguments);
};
/**
* Synchronizes a faux button with its original counterpart.
*
* @param {Boolean} [reset = false]
* Whether to reset the current and original buttons after synchronizing.
*/
Drupal.behaviors.dialog.ajaxUpdateButtons = function (reset) {
if (this.ajaxCurrentButton && this.ajaxOriginalButton) {
this.ajaxCurrentButton.html(this.ajaxOriginalButton.html() || this.ajaxOriginalButton.attr('value'));
this.ajaxCurrentButton.prop('disabled', this.ajaxOriginalButton.prop('disabled'));
}
if (reset) {
this.ajaxCurrentButton = null;
this.ajaxOriginalButton = null;
}
};
$(document)
.ajaxSend(function () {
Drupal.behaviors.dialog.ajaxUpdateButtons();
})
.ajaxComplete(function () {
Drupal.behaviors.dialog.ajaxUpdateButtons(true);
})
;
/**
* {@inheritdoc}
*/
Drupal.behaviors.dialog.prepareDialogButtons = function prepareDialogButtons($dialog) {
var _this = this;
var buttons = [];
var $buttons = $dialog.find('.form-actions').find('button, input[type=submit], a.button, .btn');
$buttons.each(function () {
var $originalButton = $(this)
// Prevent original button from being tabbed to.
.attr('tabindex', -1)
// Visually make the original button invisible, but don't actually hide
// or remove it from the DOM because the click needs to be proxied from
// the faux button created in the footer to its original counterpart.
.css({
display: 'block',
width: 0,
height: 0,
padding: 0,
border: 0,
overflow: 'hidden'
});
buttons.push({
// Strip all HTML from the actual text value. This value is escaped.
// It actual HTML value will be synced with the original button's HTML
// below in the "create" method.
text: Bootstrap.stripHtml($originalButton) || $originalButton.attr('value'),
class: $originalButton.attr('class').replace('use-ajax-submit', ''),
click: function click(e) {
e.preventDefault();
e.stopPropagation();
_this.ajaxCurrentButton = $(e.target);
_this.ajaxOriginalButton = $originalButton;
// Some core JS binds dialog buttons to the mousedown or mouseup
// events instead of click; all three events must be simulated here.
// @see https://www.drupal.org/project/bootstrap/issues/3016254
Bootstrap.simulate($originalButton, ['mousedown', 'mouseup', 'click']);
},
create: function () {
_this.ajaxCurrentButton = $(this);
_this.ajaxOriginalButton = $originalButton;
_this.ajaxUpdateButtons(true);
}
});
});
return buttons;
};
})(window.jQuery, window.Drupal, window.Drupal.bootstrap);

View File

@@ -0,0 +1,30 @@
/**
* @file
* Extends methods from core/misc/form.js.
*/
(function ($, window, Drupal, drupalSettings, once) {
/**
* Behavior for "forms_has_error_value_toggle" theme setting.
*/
Drupal.behaviors.bootstrapForm = {
attach: function (context) {
if (drupalSettings.bootstrap && drupalSettings.bootstrap.forms_has_error_value_toggle) {
var $context = $(context);
$(once('error', '.form-item.has-error:not(.form-type-password.has-feedback)', context)).each(function () {
var $formItem = $(this);
var $input = $formItem.find(':input');
$input.on('keyup focus blur', function () {
if (this.defaultValue !== void 0) {
$formItem[this.defaultValue !== this.value ? 'removeClass' : 'addClass']('has-error');
$input[this.defaultValue !== this.value ? 'removeClass' : 'addClass']('error');
}
});
});
}
}
};
})(jQuery, this, Drupal, drupalSettings, once);

View File

@@ -0,0 +1,181 @@
/**
* @file
* message.js
*/
(function ($, Drupal) {
/**
* Retrieves the classes for a specific message type.
*
* @param {String} type
* The type of message.
*
* @return {String}
* The classes to add, space separated.
*/
Drupal.Message.getMessageTypeClass = function (type) {
var classes = this.getMessageTypeClasses();
return 'alert alert-' + (classes[type] || 'success');
};
/**
* Helper function to map Drupal types to Bootstrap classes.
*
* @return {Object<String, String>}
* A map of classes, keyed by message type.
*/
Drupal.Message.getMessageTypeClasses = function () {
return {
status: 'success',
error: 'danger',
warning: 'warning',
info: 'info',
};
};
/**
* Retrieves a label for a specific message type.
*
* @param {String} type
* The type of message.
*
* @return {String}
* The message type label.
*/
Drupal.Message.getMessageTypeLabel = function (type) {
var labels = this.getMessageTypeLabels();
return labels[type];
};
/**
* @inheritDoc
*/
Drupal.Message.getMessageTypeLabels = function () {
return {
status: Drupal.t('Status message'),
error: Drupal.t('Error message'),
warning: Drupal.t('Warning message'),
info: Drupal.t('Informative message'),
};
};
/**
* Retrieves the aria-role for a specific message type.
*
* @param {String} type
* The type of message.
*
* @return {String}
* The message type role.
*/
Drupal.Message.getMessageTypeRole = function (type) {
var labels = this.getMessageTypeRoles();
return labels[type];
};
/**
* Map of the message type aria-role values.
*
* @return {Object<String, String>}
* A map of roles, keyed by message type.
*/
Drupal.Message.getMessageTypeRoles = function () {
return {
status: 'status',
error: 'alert',
warning: 'alert',
info: 'status',
};
};
/**
* @inheritDoc
*/
Drupal.theme.message = function (message, options) {
options = options || {};
var wrapper = Drupal.theme('messageWrapper', options.id || (new Date()).getTime(), options.type || 'status');
if (options.dismissible === void 0 || !!options.dismissible) {
wrapper.classList.add('alert-dismissible');
wrapper.appendChild(Drupal.theme('messageClose'));
}
wrapper.appendChild(Drupal.theme('messageContents', message && message.text));
return wrapper;
};
/**
* Themes the message container.
*
* @param {String} id
* The message identifier.
* @param {String} type
* The type of message.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageWrapper = function (id, type) {
var wrapper = document.createElement('div');
var label = Drupal.Message.getMessageTypeLabel(type);
wrapper.setAttribute('class', Drupal.Message.getMessageTypeClass(type));
wrapper.setAttribute('role', Drupal.Message.getMessageTypeRole(type));
wrapper.setAttribute('aria-label', label);
wrapper.setAttribute('data-drupal-message-id', id);
wrapper.setAttribute('data-drupal-message-type', type);
if (label) {
wrapper.appendChild(Drupal.theme('messageLabel', label));
}
return wrapper;
};
/**
* Themes the message close button.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageClose = function () {
var element = document.createElement('button');
element.setAttribute('class', 'close');
element.setAttribute('type', 'button');
element.setAttribute('role', 'button');
element.setAttribute('data-dismiss', 'alert');
element.setAttribute('aria-label', Drupal.t('Close'));
element.innerHTML = '<span aria-hidden="true">&times;</span>';
return element;
};
/**
* Themes the message container.
*
* @param {String} label
* The message label.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageLabel = function (label) {
var element = document.createElement('h2');
element.setAttribute('class', 'sr-only');
element.innerHTML = label;
return element;
};
/**
* Themes the message contents.
*
* @param {String} html
* The message identifier.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageContents = function (html) {
var element = document.createElement('p');
element.innerHTML = '' + html;
return element;
}
})(window.jQuery, window.Drupal);

View File

@@ -0,0 +1,73 @@
/**
* @file
* Extends methods from core/misc/progress.js.
*/
(function ($, Drupal) {
'use strict';
/**
* Theme function for the progress bar.
*
* @param {string} id
*
* @return {string}
* The HTML for the progress bar.
*/
Drupal.theme.progressBar = function (id) {
return '<div class="progress-wrapper" aria-live="polite">' +
'<div class="message"></div>'+
'<div id ="' + id + '" class="progress progress-striped active">' +
'<div class="progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">' +
'<span class="percentage"></span>' +
'</div>' +
'</div>' +
'<div class="progress-label"></div>' +
'</div>';
};
$.extend(Drupal.ProgressBar.prototype, /** @lends Drupal.ProgressBar */{
/**
* Set the percentage and status message for the progressbar.
*
* @param {number} percentage
* @param {string} message
* @param {string} label
*/
setProgress: function (percentage, message, label) {
if (percentage >= 0 && percentage <= 100) {
$(this.element).find('.progress-bar').css('width', percentage + '%').attr('aria-valuenow', percentage);
$(this.element).find('.percentage').html(percentage + '%');
}
if (message) {
// Remove the unnecessary whitespace at the end of the message.
message = message.replace(/<br\/>&nbsp;|\s*$/, '');
$('.message', this.element).html(message);
}
if (label) {
$('.progress-label', this.element).html(label);
}
if (this.updateCallback) {
this.updateCallback(percentage, message, this);
}
},
/**
* Display errors on the page.
*
* @param {string} string
*/
displayError: function (string) {
var error = $('<div class="alert alert-block alert-error"><button class="close" data-dismiss="alert">&times;</button><h4>' + Drupal.t('Error message') + '</h4></div>').append(string);
$(this.element).before(error).hide();
if (this.errorCallback) {
this.errorCallback(this);
}
}
});
})(jQuery, Drupal);

View File

@@ -0,0 +1,30 @@
/**
* @file
* Extends core/misc/states.js.
*/
(function($) {
// Unbind core state.js from document first so we can then override below.
$(document).unbind('state:disabled');
/**
* 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.
*/
$(document).bind('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)
.attr('disabled', e.value)
.closest('.form-item, .form-submit, .form-wrapper').toggleClass('form-disabled', e.value)
.find(':input').attr('disabled', e.value);
// Note: WebKit nightlies don't reflect that change correctly.
// See https://bugs.webkit.org/show_bug.cgi?id=23789
}
});
})(jQuery);

View File

@@ -0,0 +1,463 @@
/**
* @file
* Extends methods from core/misc/tabledrag.js.
*/
(function ($) {
// Save the original prototype.
var prototype = Drupal.tableDrag.prototype;
/**
* Provides table and field manipulation.
*
* @constructor
*
* @param {HTMLElement} table
* DOM object for the table to be made draggable.
* @param {object} tableSettings
* Settings for the table added via drupal_add_dragtable().
*/
Drupal.tableDrag = function (table, tableSettings) {
var self = this;
var $table = $(table);
/**
* @type {jQuery}
*/
this.$table = $(table);
/**
*
* @type {HTMLElement}
*/
this.table = table;
/**
* @type {object}
*/
this.tableSettings = tableSettings;
/**
* Used to hold information about a current drag operation.
*
* @type {?HTMLElement}
*/
this.dragObject = null;
/**
* Provides operations for row manipulation.
*
* @type {?HTMLElement}
*/
this.rowObject = null;
/**
* Remember the previous element.
*
* @type {?HTMLElement}
*/
this.oldRowElement = null;
/**
* Used to determine up or down direction from last mouse move.
*
* @type {number}
*/
this.oldY = 0;
/**
* Whether anything in the entire table has changed.
*
* @type {bool}
*/
this.changed = false;
/**
* Maximum amount of allowed parenting.
*
* @type {number}
*/
this.maxDepth = 0;
/**
* Direction of the table.
*
* @type {number}
*/
this.rtl = $(this.table).css('direction') === 'rtl' ? -1 : 1;
/**
*
* @type {bool}
*/
this.striping = $(this.table).data('striping') === 1;
/**
* Configure the scroll settings.
*
* @type {object}
*
* @prop {number} amount
* @prop {number} interval
* @prop {number} trigger
*/
this.scrollSettings = {amount: 4, interval: 50, trigger: 70};
/**
*
* @type {?number}
*/
this.scrollInterval = null;
/**
*
* @type {number}
*/
this.scrollY = 0;
/**
*
* @type {number}
*/
this.windowHeight = 0;
/**
* @type {?HTMLElement}
*/
this.$toggleWeightButton = null;
/**
* Check this table's settings to see if there are parent relationships in
* this table. For efficiency, large sections of code can be skipped if we
* don't need to track horizontal movement and indentations.
*
* @type {bool}
*/
this.indentEnabled = false;
for (var group in tableSettings) {
if (tableSettings.hasOwnProperty(group)) {
for (var n in tableSettings[group]) {
if (tableSettings[group].hasOwnProperty(n)) {
if (tableSettings[group][n].relationship === 'parent') {
this.indentEnabled = true;
}
if (tableSettings[group][n].limit > 0) {
this.maxDepth = tableSettings[group][n].limit;
}
}
}
}
}
if (this.indentEnabled) {
/**
* Total width of indents, set in makeDraggable.
*
* @type {number}
*/
this.indentCount = 1;
// Find the width of indentations to measure mouse movements against.
// Because the table doesn't need to start with any indentations, we
// manually append 2 indentations in the first draggable row, measure
// the offset, then remove.
var indent = Drupal.theme('tableDragIndentation');
var testRow = $('<tr/>').addClass('draggable').appendTo(table);
var testCell = $('<td/>').appendTo(testRow).prepend(indent).prepend(indent);
var $indentation = testCell.find('.js-indentation');
/**
*
* @type {number}
*/
this.indentAmount = $indentation.get(1).offsetLeft - $indentation.get(0).offsetLeft;
testRow.remove();
}
// Make each applicable row draggable.
// Match immediate children of the parent element to allow nesting.
$table.find('> tr.draggable, > tbody > tr.draggable').each(function () { self.makeDraggable(this); });
// Add a link before the table for users to show or hide weight columns.
self.$toggleWeightButton = $(Drupal.theme('btn-sm', {
'class': ['tabledrag-toggle-weight'],
'data-drupal-selector': ['tabledrag-toggle-weight'],
title: Drupal.t('Re-order rows by numerical weight instead of dragging.'),
'data-toggle': 'tooltip'
}));
self.$toggleWeightButton
.on('click', $.proxy(function (e) {
e.preventDefault();
this.toggleColumns();
}, this))
.wrap('<div class="tabledrag-toggle-weight-wrapper"></div>')
.parent();
$table.before(self.$toggleWeightButton);
// Initialize the specified columns (for example, weight or parent columns)
// to show or hide according to user preference. This aids accessibility
// so that, e.g., screen reader users can choose to enter weight values and
// manipulate form elements directly, rather than using drag-and-drop..
self.initColumns();
// Add event bindings to the document. The self variable is passed along
// as event handlers do not have direct access to the tableDrag object.
$(document).on('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); });
$(document).on('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); });
$(document).on('mousemove pointermove', function (event) { return self.dragRow(event, self); });
$(document).on('mouseup pointerup', function (event) { return self.dropRow(event, self); });
// React to localStorage event showing or hiding weight columns.
$(window).on('storage', $.proxy(function (e) {
// Only react to 'Drupal.tableDrag.showWeight' value change.
if (e.originalEvent.key === 'Drupal.tableDrag.showWeight') {
// This was changed in another window, get the new value for this
// window.
showWeight = JSON.parse(e.originalEvent.newValue);
this.displayColumns(showWeight);
}
}, this));
};
// Restore the original prototype.
Drupal.tableDrag.prototype = prototype;
/**
* Take an item and add event handlers to make it become draggable.
*
* @param {HTMLElement} item
*/
Drupal.tableDrag.prototype.makeDraggable = function (item) {
var self = this;
var $item = $(item);
// Add a class to the title link
$item.find('td:first-of-type').find('a').addClass('menu-item__link');
// Create the handle.
var handle = $('<a href="#" class="tabledrag-handle"/>');
// Insert the handle after indentations (if any).
var $indentationLast = $item.find('td:first-of-type').find('.js-indentation').eq(-1);
if ($indentationLast.length) {
$indentationLast.after(handle);
// Update the total width of indentation in this entire table.
self.indentCount = Math.max($item.find('.js-indentation').length, self.indentCount);
}
else {
$item.find('td').eq(0).prepend(handle);
}
// Add the glyphicon to the handle.
handle
.attr('title', Drupal.t('Drag to re-order'))
.attr('data-toggle', 'tooltip')
.append(Drupal.theme('bootstrapIcon', 'move'))
;
handle.on('mousedown touchstart pointerdown', function (event) {
event.preventDefault();
if (event.originalEvent.type === 'touchstart') {
event = event.originalEvent.touches[0];
}
self.dragStart(event, self, item);
});
// Prevent the anchor tag from jumping us to the top of the page.
handle.on('click', function (e) {
e.preventDefault();
});
// Set blur cleanup when a handle is focused.
handle.on('focus', function () {
self.safeBlur = true;
});
// On blur, fire the same function as a touchend/mouseup. This is used to
// update values after a row has been moved through the keyboard support.
handle.on('blur', function (event) {
if (self.rowObject && self.safeBlur) {
self.dropRow(event, self);
}
});
// Add arrow-key support to the handle.
handle.on('keydown', function (event) {
// If a rowObject doesn't yet exist and this isn't the tab key.
if (event.keyCode !== 9 && !self.rowObject) {
self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true);
}
var keyChange = false;
var groupHeight;
switch (event.keyCode) {
// Left arrow.
case 37:
// Safari left arrow.
case 63234:
keyChange = true;
self.rowObject.indent(-1 * self.rtl);
break;
// Up arrow.
case 38:
// Safari up arrow.
case 63232:
var $previousRow = $(self.rowObject.element).prev('tr:first-of-type');
var previousRow = $previousRow.get(0);
while (previousRow && $previousRow.is(':hidden')) {
$previousRow = $(previousRow).prev('tr:first-of-type');
previousRow = $previousRow.get(0);
}
if (previousRow) {
// Do not allow the onBlur cleanup.
self.safeBlur = false;
self.rowObject.direction = 'up';
keyChange = true;
if ($(item).is('.tabledrag-root')) {
// Swap with the previous top-level row.
groupHeight = 0;
while (previousRow && $previousRow.find('.js-indentation').length) {
$previousRow = $(previousRow).prev('tr:first-of-type');
previousRow = $previousRow.get(0);
groupHeight += $previousRow.is(':hidden') ? 0 : previousRow.offsetHeight;
}
if (previousRow) {
self.rowObject.swap('before', previousRow);
// No need to check for indentation, 0 is the only valid one.
window.scrollBy(0, -groupHeight);
}
}
else if (self.table.tBodies[0].rows[0] !== previousRow || $previousRow.is('.draggable')) {
// Swap with the previous row (unless previous row is the first
// one and undraggable).
self.rowObject.swap('before', previousRow);
self.rowObject.interval = null;
self.rowObject.indent(0);
window.scrollBy(0, -parseInt(item.offsetHeight, 10));
}
// Regain focus after the DOM manipulation.
handle.trigger('focus');
}
break;
// Right arrow.
case 39:
// Safari right arrow.
case 63235:
keyChange = true;
self.rowObject.indent(self.rtl);
break;
// Down arrow.
case 40:
// Safari down arrow.
case 63233:
var $nextRow = $(self.rowObject.group).eq(-1).next('tr:first-of-type');
var nextRow = $nextRow.get(0);
while (nextRow && $nextRow.is(':hidden')) {
$nextRow = $(nextRow).next('tr:first-of-type');
nextRow = $nextRow.get(0);
}
if (nextRow) {
// Do not allow the onBlur cleanup.
self.safeBlur = false;
self.rowObject.direction = 'down';
keyChange = true;
if ($(item).is('.tabledrag-root')) {
// Swap with the next group (necessarily a top-level one).
groupHeight = 0;
var nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false);
if (nextGroup) {
$(nextGroup.group).each(function () {
groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight;
});
var nextGroupRow = $(nextGroup.group).eq(-1).get(0);
self.rowObject.swap('after', nextGroupRow);
// No need to check for indentation, 0 is the only valid one.
window.scrollBy(0, parseInt(groupHeight, 10));
}
}
else {
// Swap with the next row.
self.rowObject.swap('after', nextRow);
self.rowObject.interval = null;
self.rowObject.indent(0);
window.scrollBy(0, parseInt(item.offsetHeight, 10));
}
// Regain focus after the DOM manipulation.
handle.trigger('focus');
}
break;
}
if (self.rowObject && self.rowObject.changed === true) {
$(item).addClass('drag');
if (self.oldRowElement) {
$(self.oldRowElement).removeClass('drag-previous');
}
self.oldRowElement = item;
if (self.striping === true) {
self.restripeTable();
}
self.onDrag();
}
// Returning false if we have an arrow key to prevent scrolling.
if (keyChange) {
return false;
}
});
// Compatibility addition, return false on keypress to prevent unwanted
// scrolling. IE and Safari will suppress scrolling on keydown, but all
// other browsers need to return false on keypress.
// http://www.quirksmode.org/js/keys.html
handle.on('keypress', function (event) {
switch (event.keyCode) {
// Left arrow.
case 37:
// Up arrow.
case 38:
// Right arrow.
case 39:
// Down arrow.
case 40:
return false;
}
});
};
/**
* Add an asterisk or other marker to the changed row.
*/
Drupal.tableDrag.prototype.row.prototype.markChanged = function () {
var $cell = $('td:first', this.element);
// Find the first appropriate place to insert the marker.
var $target = $($cell.find('.file-size').get(0) || $cell.find('.file').get(0) || $cell.find('.tabledrag-handle').get(0));
if (!$cell.find('.tabledrag-changed').length) {
$target.after(' ' + Drupal.theme('tableDragChangedMarker') + ' ');
}
};
$.extend(Drupal.theme, /** @lends Drupal.theme */{
/**
* @return {string}
*/
tableDragChangedMarker: function () {
return Drupal.theme('bootstrapIcon', 'warning-sign', {'class': ['tabledrag-changed', 'text-warning']});
},
/**
* @return {string}
*/
tableDragChangedWarning: function () {
return '<div class="tabledrag-changed-warning alert alert-sm alert-warning messages warning">' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t('You have unsaved changes.') + '</div>';
}
});
})(jQuery);

View File

@@ -0,0 +1,15 @@
/**
* @file
* Overrides core/misc/vertical-tabs.js.
*/
(function ($, Drupal) {
"use strict";
var createSticky = Drupal.TableHeader.prototype.createSticky;
Drupal.TableHeader.prototype.createSticky = function () {
createSticky.call(this);
this.$stickyTable.addClass(this.$originalTable.attr('class')).removeClass('sticky-enabled sticky-table');
};
})(window.jQuery, window.Drupal);

View File

@@ -0,0 +1,289 @@
/**
* @file
* Overrides core/misc/vertical-tabs.js.
*/
(function ($, window, Drupal, drupalSettings, once) {
"use strict";
/**
* 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.
*/
var handleFragmentLinkClickOrHashChange = function handleFragmentLinkClickOrHashChange(e, $target) {
$target.parents('.vertical-tabs-pane').each(function (index, pane) {
$(pane).data('verticalTab').focus();
});
};
/**
* This script transforms a set of details into a stack of vertical
* tabs. Another tab pane can be selected by clicking on the respective
* tab.
*
* 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.
*/
Drupal.behaviors.verticalTabs = {
attach: function (context) {
var width = drupalSettings.widthBreakpoint || 640;
var mq = '(max-width: ' + width + 'px)';
if (window.matchMedia(mq).matches) {
return;
}
/**
* Binds a listener to handle fragment link clicks and URL hash changes.
*/
$(once('vertical-tabs-fragments', 'body')).on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange);
$(once('vertical-tabs', '[data-vertical-tabs-panes]', context)).each(function () {
var $this = $(this).addClass('tab-content vertical-tabs-panes');
var focusID = $(':hidden.vertical-tabs__active-tab', this).val();
if (typeof focusID === 'undefined' || !focusID.length) {
focusID = false;
}
var tab_focus;
// Check if there are some details that can be converted to vertical-tabs
var $details = $this.find('> .panel');
if ($details.length === 0) {
return;
}
// Create the tab column.
var tab_list = $('<ul class="nav nav-tabs vertical-tabs-list"></ul>');
$this.wrap('<div class="tabbable tabs-left 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({
title: $that.find('> .panel-heading > .panel-title, > .panel-heading').last().html(),
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)
.removeClass('collapsible collapsed panel panel-default')
.addClass('tab-pane vertical-tabs-pane')
.data('verticalTab', vertical_tab)
.find('> .panel-heading').remove();
if (this.id === focusID) {
tab_focus = $that;
}
});
$(tab_list).find('> li:first').addClass('first');
$(tab_list).find('> li:last').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 {
tab_focus = $this.find('> .vertical-tabs-pane:first');
}
}
if (tab_focus.length) {
tab_focus.data('verticalTab').focus();
}
});
// Provide some Bootstrap tab/Drupal integration.
// @todo merge this into the above code from core.
$(once('bootstrap-tabs', '.tabbable', context)).each(function () {
var $wrapper = $(this);
var $tabs = $wrapper.find('.nav-tabs');
var $content = $wrapper.find('.tab-content');
var borderRadius = parseInt($content.css('borderBottomRightRadius'), 10);
var bootstrapTabResize = function() {
if ($wrapper.hasClass('tabs-left') || $wrapper.hasClass('tabs-right')) {
$content.css('min-height', $tabs.outerHeight());
}
};
// Add min-height on content for left and right tabs.
bootstrapTabResize();
// Detect tab switch.
if ($wrapper.hasClass('tabs-left') || $wrapper.hasClass('tabs-right')) {
$tabs.on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
bootstrapTabResize();
if ($wrapper.hasClass('tabs-left')) {
if ($(e.target).parent().is(':first-child')) {
$content.css('borderTopLeftRadius', '0');
}
else {
$content.css('borderTopLeftRadius', borderRadius + 'px');
}
}
else {
if ($(e.target).parent().is(':first-child')) {
$content.css('borderTopRightRadius', '0');
}
else {
$content.css('borderTopRightRadius', borderRadius + 'px');
}
}
});
}
});
}
};
/**
* The vertical tab object represents a single tab within a tab group.
*
* @param settings
* An object with the following keys:
* - title: The name of the tab.
* - details: The jQuery object of the details element that is the tab pane.
*/
Drupal.verticalTab = function (settings) {
var self = this;
$.extend(this, settings, Drupal.theme('verticalTab', settings));
this.link.attr('href', '#' + settings.details.attr('id'));
this.link.on('click', function (e) {
e.preventDefault();
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:first").trigger('focus');
}
});
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 () {
$(this).removeClass('active').find('> div').removeClass('in');
var tab = $(this).data('verticalTab');
tab.item.removeClass('selected');
})
.end()
.addClass('active')
.siblings(':hidden.vertical-tabs-active-tab')
.val(this.details.attr('id'));
this.details.find('> div').addClass('in');
this.details.data('verticalTab').item.find('a').tab('show');
this.item.addClass('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 () {
this.summary.html(this.details.drupalGetSummary());
},
/**
* Shows a vertical tab pane.
*/
tabShow: function () {
// Display the tab.
this.item.show();
// Show the vertical tabs.
this.item.closest('.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-tab-button').removeClass('first')
.filter(':visible:first').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.
*/
tabHide: function () {
// 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-tab-button').removeClass('first')
.filter(':visible:first').addClass('first');
// Hide the details element.
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):first');
if ($firstTab.length) {
$firstTab.data('verticalTab').focus();
}
// Hide the vertical tabs (if no tabs remain).
else {
this.item.closest('.form-type-vertical-tabs').hide();
}
return this;
}
};
/**
* Theme function for a vertical tab.
*
* @param settings
* An object with the following keys:
* - title: The name of the tab.
* @return
* 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-tab-button" tabindex="-1"></li>')
.append(tab.link = $('<a href="#' + settings.details[0].id + '" data-toggle="tab"></a>')
.append(tab.title = $('<span></span>').html(settings.title))
.append(tab.summary = $('<div class="summary"></div>')
)
);
return tab;
};
})(jQuery, this, Drupal, drupalSettings, once);