diff --git a/core/misc/active-link.es6.js b/core/misc/active-link.es6.js new file mode 100644 index 0000000000..9cf55b4344 --- /dev/null +++ b/core/misc/active-link.es6.js @@ -0,0 +1,68 @@ +/** + * @file + * Attaches behaviors for Drupal's active link marking. + */ + +(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. + 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 originalSelectors = ['[data-drupal-link-system-path="' + path.currentPath + '"]']; + var selectors; + + // If this is the front page, we have to check for the path as + // well. + if (path.isFront) { + originalSelectors.push('[data-drupal-link-system-path=""]'); + } + + // 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 + '"]'; }) + ); + + // Add query string selector for pagers, exposed filters. + 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) { + if (trigger === 'unload') { + var activeLinks = context.querySelectorAll('[data-drupal-link-system-path].is-active'); + var il = activeLinks.length; + for (var i = 0; i < il; i++) { + activeLinks[i].classList.remove('is-active'); + } + } + } + }; + +})(Drupal, drupalSettings); diff --git a/core/misc/active-link.js b/core/misc/active-link.js index 9cf55b4344..d489fb5312 100644 --- a/core/misc/active-link.js +++ b/core/misc/active-link.js @@ -1,60 +1,42 @@ /** - * @file - * Attaches behaviors for Drupal's active link marking. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/active-link.es6.js +* @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 originalSelectors = ['[data-drupal-link-system-path="' + path.currentPath + '"]']; var selectors; - // If this is the front page, we have to check for the path as - // well. if (path.isFront) { originalSelectors.push('[data-drupal-link-system-path=""]'); } - // 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 +46,4 @@ } } }; - -})(Drupal, drupalSettings); +})(Drupal, drupalSettings); \ No newline at end of file diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js new file mode 100644 index 0000000000..fefe9f3031 --- /dev/null +++ b/core/misc/ajax.es6.js @@ -0,0 +1,1344 @@ +/** + * @file + * Provides Ajax page updating via jQuery $.ajax. + * + * Ajax is a method of making a request via JavaScript while viewing an HTML + * page. The request returns an array of commands encoded in JSON, which is + * then executed to make any changes that are necessary to the page. + * + * Drupal uses this file to enhance form elements with `#ajax['url']` and + * `#ajax['wrapper']` properties. If set, this file will automatically be + * included to provide Ajax capabilities. + */ + +(function ($, window, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Attaches the Ajax behavior to each Ajax form element. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Initialize all {@link Drupal.Ajax} objects declared in + * `drupalSettings.ajax` or initialize {@link Drupal.Ajax} objects from + * DOM elements having the `use-ajax-submit` or `use-ajax` css class. + * @prop {Drupal~behaviorDetach} detach + * During `unload` remove all {@link Drupal.Ajax} objects related to + * the removed content. + */ + Drupal.behaviors.AJAX = { + attach: function (context, settings) { + + function loadAjaxBehavior(base) { + var element_settings = settings.ajax[base]; + if (typeof element_settings.selector === 'undefined') { + element_settings.selector = '#' + base; + } + $(element_settings.selector).once('drupal-ajax').each(function () { + element_settings.element = this; + element_settings.base = base; + Drupal.ajax(element_settings); + }); + } + + // Load all Ajax behaviors specified in the settings. + for (var base in settings.ajax) { + if (settings.ajax.hasOwnProperty(base)) { + loadAjaxBehavior(base); + } + } + + // Bind Ajax behaviors to all items showing the class. + $('.use-ajax').once('ajax').each(function () { + var element_settings = {}; + // Clicked links look better with the throbber than the progress bar. + element_settings.progress = {type: 'throbber'}; + + // For anchor tags, these will go to the target of the anchor rather + // than the usual location. + var href = $(this).attr('href'); + if (href) { + element_settings.url = href; + element_settings.event = 'click'; + } + element_settings.dialogType = $(this).data('dialog-type'); + element_settings.dialog = $(this).data('dialog-options'); + element_settings.base = $(this).attr('id'); + element_settings.element = this; + Drupal.ajax(element_settings); + }); + + // This class means to submit the form to the action using Ajax. + $('.use-ajax-submit').once('ajax').each(function () { + var element_settings = {}; + + // Ajax submits specified in this manner automatically submit to the + // normal form action. + element_settings.url = $(this.form).attr('action'); + // Form submit button clicks need to tell the form what was clicked so + // it gets passed in the POST request. + element_settings.setClick = true; + // Form buttons use the 'click' event rather than mousedown. + element_settings.event = 'click'; + // Clicked form buttons look better with the throbber than the progress + // bar. + element_settings.progress = {type: 'throbber'}; + element_settings.base = $(this).attr('id'); + element_settings.element = this; + + Drupal.ajax(element_settings); + }); + }, + + detach: function (context, settings, trigger) { + if (trigger === 'unload') { + Drupal.ajax.expired().forEach(function (instance) { + // Set this to null and allow garbage collection to reclaim + // the memory. + Drupal.ajax.instances[instance.instanceIndex] = null; + }); + } + } + }; + + /** + * Extends Error to provide handling for Errors in Ajax. + * + * @constructor + * + * @augments Error + * + * @param {XMLHttpRequest} xmlhttp + * XMLHttpRequest object used for the failed request. + * @param {string} uri + * The URI where the error occurred. + * @param {string} customMessage + * The custom message. + */ + Drupal.AjaxError = function (xmlhttp, uri, customMessage) { + + var statusCode; + var statusText; + var pathText; + var responseText; + var readyStateText; + if (xmlhttp.status) { + statusCode = '\n' + Drupal.t('An AJAX HTTP error occurred.') + '\n' + Drupal.t('HTTP Result Code: !status', {'!status': xmlhttp.status}); + } + else { + statusCode = '\n' + Drupal.t('An AJAX HTTP request terminated abnormally.'); + } + statusCode += '\n' + Drupal.t('Debugging information follows.'); + pathText = '\n' + Drupal.t('Path: !uri', {'!uri': uri}); + statusText = ''; + // In some cases, when statusCode === 0, xmlhttp.statusText may not be + // defined. Unfortunately, testing for it with typeof, etc, doesn't seem to + // catch that and the test causes an exception. So we need to catch the + // exception here. + try { + statusText = '\n' + Drupal.t('StatusText: !statusText', {'!statusText': $.trim(xmlhttp.statusText)}); + } + catch (e) { + // Empty. + } + + responseText = ''; + // Again, we don't have a way to know for sure whether accessing + // xmlhttp.responseText is going to throw an exception. So we'll catch it. + try { + responseText = '\n' + Drupal.t('ResponseText: !responseText', {'!responseText': $.trim(xmlhttp.responseText)}); + } + catch (e) { + // Empty. + } + + // Make the responseText more readable by stripping HTML tags and newlines. + responseText = responseText.replace(/<("[^"]*"|'[^']*'|[^'">])*>/gi, ''); + responseText = responseText.replace(/[\n]+\s+/g, '\n'); + + // We don't need readyState except for status == 0. + readyStateText = xmlhttp.status === 0 ? ('\n' + Drupal.t('ReadyState: !readyState', {'!readyState': xmlhttp.readyState})) : ''; + + customMessage = customMessage ? ('\n' + Drupal.t('CustomMessage: !customMessage', {'!customMessage': customMessage})) : ''; + + /** + * Formatted and translated error message. + * + * @type {string} + */ + this.message = statusCode + pathText + statusText + customMessage + responseText + readyStateText; + + /** + * Used by some browsers to display a more accurate stack trace. + * + * @type {string} + */ + this.name = 'AjaxError'; + }; + + Drupal.AjaxError.prototype = new Error(); + Drupal.AjaxError.prototype.constructor = Drupal.AjaxError; + + /** + * Provides Ajax page updating via jQuery $.ajax. + * + * This function is designed to improve developer experience by wrapping the + * initialization of {@link Drupal.Ajax} objects and storing all created + * objects in the {@link Drupal.ajax.instances} array. + * + * @example + * Drupal.behaviors.myCustomAJAXStuff = { + * attach: function (context, settings) { + * + * var ajaxSettings = { + * url: 'my/url/path', + * // If the old version of Drupal.ajax() needs to be used those + * // properties can be added + * base: 'myBase', + * element: $(context).find('.someElement') + * }; + * + * var myAjaxObject = Drupal.ajax(ajaxSettings); + * + * // Declare a new Ajax command specifically for this Ajax object. + * myAjaxObject.commands.insert = function (ajax, response, status) { + * $('#my-wrapper').append(response.data); + * alert('New content was appended to #my-wrapper'); + * }; + * + * // This command will remove this Ajax object from the page. + * myAjaxObject.commands.destroyObject = function (ajax, response, status) { + * Drupal.ajax.instances[this.instanceIndex] = null; + * }; + * + * // Programmatically trigger the Ajax request. + * myAjaxObject.execute(); + * } + * }; + * + * @param {object} settings + * The settings object passed to {@link Drupal.Ajax} constructor. + * @param {string} [settings.base] + * Base is passed to {@link Drupal.Ajax} constructor as the 'base' + * parameter. + * @param {HTMLElement} [settings.element] + * Element parameter of {@link Drupal.Ajax} constructor, element on which + * event listeners will be bound. + * + * @return {Drupal.Ajax} + * The created Ajax object. + * + * @see Drupal.AjaxCommands + */ + Drupal.ajax = function (settings) { + if (arguments.length !== 1) { + throw new Error('Drupal.ajax() function must be called with one configuration object only'); + } + // Map those config keys to variables for the old Drupal.ajax function. + var base = settings.base || false; + var element = settings.element || false; + delete settings.base; + delete settings.element; + + // By default do not display progress for ajax calls without an element. + if (!settings.progress && !element) { + settings.progress = false; + } + + var ajax = new Drupal.Ajax(base, element, settings); + ajax.instanceIndex = Drupal.ajax.instances.length; + Drupal.ajax.instances.push(ajax); + + return ajax; + }; + + /** + * Contains all created Ajax objects. + * + * @type {Array.} + */ + Drupal.ajax.instances = []; + + /** + * List all objects where the associated element is not in the DOM + * + * This method ignores {@link Drupal.Ajax} objects not bound to DOM elements + * when created with {@link Drupal.ajax}. + * + * @return {Array.} + * The list of expired {@link Drupal.Ajax} objects. + */ + Drupal.ajax.expired = function () { + return Drupal.ajax.instances.filter(function (instance) { + return instance && instance.element !== false && !document.body.contains(instance.element); + }); + }; + + /** + * Settings for an Ajax object. + * + * @typedef {object} Drupal.Ajax~element_settings + * + * @prop {string} url + * Target of the Ajax request. + * @prop {?string} [event] + * Event bound to settings.element which will trigger the Ajax request. + * @prop {bool} [keypress=true] + * Triggers a request on keypress events. + * @prop {?string} selector + * jQuery selector targeting the element to bind events to or used with + * {@link Drupal.AjaxCommands}. + * @prop {string} [effect='none'] + * Name of the jQuery method to use for displaying new Ajax content. + * @prop {string|number} [speed='none'] + * Speed with which to apply the effect. + * @prop {string} [method] + * Name of the jQuery method used to insert new content in the targeted + * element. + * @prop {object} [progress] + * Settings for the display of a user-friendly loader. + * @prop {string} [progress.type='throbber'] + * Type of progress element, core provides `'bar'`, `'throbber'` and + * `'fullscreen'`. + * @prop {string} [progress.message=Drupal.t('Please wait...')] + * Custom message to be used with the bar indicator. + * @prop {object} [submit] + * Extra data to be sent with the Ajax request. + * @prop {bool} [submit.js=true] + * Allows the PHP side to know this comes from an Ajax request. + * @prop {object} [dialog] + * Options for {@link Drupal.dialog}. + * @prop {string} [dialogType] + * One of `'modal'` or `'dialog'`. + * @prop {string} [prevent] + * List of events on which to stop default action and stop propagation. + */ + + /** + * Ajax constructor. + * + * The Ajax request returns an array of commands encoded in JSON, which is + * then executed to make any changes that are necessary to the page. + * + * Drupal uses this file to enhance form elements with `#ajax['url']` and + * `#ajax['wrapper']` properties. If set, this file will automatically be + * included to provide Ajax capabilities. + * + * @constructor + * + * @param {string} [base] + * Base parameter of {@link Drupal.Ajax} constructor + * @param {HTMLElement} [element] + * Element parameter of {@link Drupal.Ajax} constructor, element on which + * event listeners will be bound. + * @param {Drupal.Ajax~element_settings} element_settings + * Settings for this Ajax object. + */ + Drupal.Ajax = function (base, element, element_settings) { + var defaults = { + event: element ? 'mousedown' : null, + keypress: true, + selector: base ? '#' + base : null, + effect: 'none', + speed: 'none', + method: 'replaceWith', + progress: { + type: 'throbber', + message: Drupal.t('Please wait...') + }, + submit: { + js: true + } + }; + + $.extend(this, defaults, element_settings); + + /** + * @type {Drupal.AjaxCommands} + */ + this.commands = new Drupal.AjaxCommands(); + + /** + * @type {bool|number} + */ + this.instanceIndex = false; + + // @todo Remove this after refactoring the PHP code to: + // - Call this 'selector'. + // - Include the '#' for ID-based selectors. + // - Support non-ID-based selectors. + if (this.wrapper) { + + /** + * @type {string} + */ + this.wrapper = '#' + this.wrapper; + } + + /** + * @type {HTMLElement} + */ + this.element = element; + + /** + * @type {Drupal.Ajax~element_settings} + */ + this.element_settings = element_settings; + + // If there isn't a form, jQuery.ajax() will be used instead, allowing us to + // bind Ajax to links as well. + if (this.element && this.element.form) { + + /** + * @type {jQuery} + */ + this.$form = $(this.element.form); + } + + // If no Ajax callback URL was given, use the link href or form action. + if (!this.url) { + var $element = $(this.element); + if ($element.is('a')) { + this.url = $element.attr('href'); + } + else if (this.element && element.form) { + this.url = this.$form.attr('action'); + } + } + + // Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let + // the server detect when it needs to degrade gracefully. + // There are four scenarios to check for: + // 1. /nojs/ + // 2. /nojs$ - The end of a URL string. + // 3. /nojs? - Followed by a query (e.g. path/nojs?destination=foobar). + // 4. /nojs# - Followed by a fragment (e.g.: path/nojs#myfragment). + var originalUrl = this.url; + + /** + * Processed Ajax URL. + * + * @type {string} + */ + this.url = this.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1'); + // If the 'nojs' version of the URL is trusted, also trust the 'ajax' + // version. + if (drupalSettings.ajaxTrustedUrl[originalUrl]) { + drupalSettings.ajaxTrustedUrl[this.url] = true; + } + + // Set the options for the ajaxSubmit function. + // The 'this' variable will not persist inside of the options object. + var ajax = this; + + /** + * Options for the jQuery.ajax function. + * + * @name Drupal.Ajax#options + * + * @type {object} + * + * @prop {string} url + * Ajax URL to be called. + * @prop {object} data + * Ajax payload. + * @prop {function} beforeSerialize + * Implement jQuery beforeSerialize function to call + * {@link Drupal.Ajax#beforeSerialize}. + * @prop {function} beforeSubmit + * Implement jQuery beforeSubmit function to call + * {@link Drupal.Ajax#beforeSubmit}. + * @prop {function} beforeSend + * Implement jQuery beforeSend function to call + * {@link Drupal.Ajax#beforeSend}. + * @prop {function} success + * Implement jQuery success function to call + * {@link Drupal.Ajax#success}. + * @prop {function} complete + * Implement jQuery success function to clean up ajax state and trigger an + * error if needed. + * @prop {string} dataType='json' + * Type of the response expected. + * @prop {string} type='POST' + * HTTP method to use for the Ajax request. + */ + ajax.options = { + url: ajax.url, + data: ajax.submit, + beforeSerialize: function (element_settings, options) { + return ajax.beforeSerialize(element_settings, options); + }, + beforeSubmit: function (form_values, element_settings, options) { + ajax.ajaxing = true; + return ajax.beforeSubmit(form_values, element_settings, options); + }, + beforeSend: function (xmlhttprequest, options) { + ajax.ajaxing = true; + return ajax.beforeSend(xmlhttprequest, options); + }, + success: function (response, status, xmlhttprequest) { + // Sanity check for browser support (object expected). + // When using iFrame uploads, responses must be returned as a string. + if (typeof response === 'string') { + response = $.parseJSON(response); + } + + // Prior to invoking the response's commands, verify that they can be + // trusted by checking for a response header. See + // \Drupal\Core\EventSubscriber\AjaxResponseSubscriber for details. + // - Empty responses are harmless so can bypass verification. This + // avoids an alert message for server-generated no-op responses that + // skip Ajax rendering. + // - Ajax objects with trusted URLs (e.g., ones defined server-side via + // #ajax) can bypass header verification. This is especially useful + // for Ajax with multipart forms. Because IFRAME transport is used, + // the response headers cannot be accessed for verification. + if (response !== null && !drupalSettings.ajaxTrustedUrl[ajax.url]) { + if (xmlhttprequest.getResponseHeader('X-Drupal-Ajax-Token') !== '1') { + var customMessage = Drupal.t('The response failed verification so will not be processed.'); + return ajax.error(xmlhttprequest, ajax.url, customMessage); + } + } + + return ajax.success(response, status); + }, + complete: function (xmlhttprequest, status) { + ajax.ajaxing = false; + if (status === 'error' || status === 'parsererror') { + return ajax.error(xmlhttprequest, ajax.url); + } + }, + dataType: 'json', + type: 'POST' + }; + + if (element_settings.dialog) { + ajax.options.data.dialogOptions = element_settings.dialog; + } + + // Ensure that we have a valid URL by adding ? when no query parameter is + // yet available, otherwise append using &. + if (ajax.options.url.indexOf('?') === -1) { + ajax.options.url += '?'; + } + else { + ajax.options.url += '&'; + } + ajax.options.url += Drupal.ajax.WRAPPER_FORMAT + '=drupal_' + (element_settings.dialogType || 'ajax'); + + // Bind the ajaxSubmit function to the element event. + $(ajax.element).on(element_settings.event, function (event) { + if (!drupalSettings.ajaxTrustedUrl[ajax.url] && !Drupal.url.isLocal(ajax.url)) { + throw new Error(Drupal.t('The callback URL is not local and not trusted: !url', {'!url': ajax.url})); + } + return ajax.eventResponse(this, event); + }); + + // If necessary, enable keyboard submission so that Ajax behaviors + // can be triggered through keyboard input as well as e.g. a mousedown + // action. + if (element_settings.keypress) { + $(ajax.element).on('keypress', function (event) { + return ajax.keypressResponse(this, event); + }); + } + + // If necessary, prevent the browser default action of an additional event. + // For example, prevent the browser default action of a click, even if the + // Ajax behavior binds to mousedown. + if (element_settings.prevent) { + $(ajax.element).on(element_settings.prevent, false); + } + }; + + /** + * URL query attribute to indicate the wrapper used to render a request. + * + * The wrapper format determines how the HTML is wrapped, for example in a + * modal dialog. + * + * @const {string} + * + * @default + */ + Drupal.ajax.WRAPPER_FORMAT = '_wrapper_format'; + + /** + * Request parameter to indicate that a request is a Drupal Ajax request. + * + * @const {string} + * + * @default + */ + Drupal.Ajax.AJAX_REQUEST_PARAMETER = '_drupal_ajax'; + + /** + * Execute the ajax request. + * + * Allows developers to execute an Ajax request manually without specifying + * an event to respond to. + * + * @return {object} + * Returns the jQuery.Deferred object underlying the Ajax request. If + * pre-serialization fails, the Deferred will be returned in the rejected + * state. + */ + Drupal.Ajax.prototype.execute = function () { + // Do not perform another ajax command if one is already in progress. + if (this.ajaxing) { + return; + } + + try { + this.beforeSerialize(this.element, this.options); + // Return the jqXHR so that external code can hook into the Deferred API. + return $.ajax(this.options); + } + catch (e) { + // Unset the ajax.ajaxing flag here because it won't be unset during + // the complete response. + this.ajaxing = false; + window.alert('An error occurred while attempting to process ' + this.options.url + ': ' + e.message); + // For consistency, return a rejected Deferred (i.e., jqXHR's superclass) + // so that calling code can take appropriate action. + return $.Deferred().reject(); + } + }; + + /** + * Handle a key press. + * + * The Ajax object will, if instructed, bind to a key press response. This + * will test to see if the key press is valid to trigger this event and + * if it is, trigger it for us and prevent other keypresses from triggering. + * In this case we're handling RETURN and SPACEBAR keypresses (event codes 13 + * and 32. RETURN is often used to submit a form when in a textfield, and + * SPACE is often used to activate an element without submitting. + * + * @param {HTMLElement} element + * Element the event was triggered on. + * @param {jQuery.Event} event + * Triggered event. + */ + Drupal.Ajax.prototype.keypressResponse = function (element, event) { + // Create a synonym for this to reduce code confusion. + var ajax = this; + + // Detect enter key and space bar and allow the standard response for them, + // except for form elements of type 'text', 'tel', 'number' and 'textarea', + // where the spacebar activation causes inappropriate activation if + // #ajax['keypress'] is TRUE. On a text-type widget a space should always + // be a space. + if (event.which === 13 || (event.which === 32 && element.type !== 'text' && + element.type !== 'textarea' && element.type !== 'tel' && element.type !== 'number')) { + event.preventDefault(); + event.stopPropagation(); + $(ajax.element_settings.element).trigger(ajax.element_settings.event); + } + }; + + /** + * Handle an event that triggers an Ajax response. + * + * When an event that triggers an Ajax response happens, this method will + * perform the actual Ajax call. It is bound to the event using + * bind() in the constructor, and it uses the options specified on the + * Ajax object. + * + * @param {HTMLElement} element + * Element the event was triggered on. + * @param {jQuery.Event} event + * Triggered event. + */ + Drupal.Ajax.prototype.eventResponse = function (element, event) { + event.preventDefault(); + event.stopPropagation(); + + // Create a synonym for this to reduce code confusion. + var ajax = this; + + // Do not perform another Ajax command if one is already in progress. + if (ajax.ajaxing) { + return; + } + + try { + if (ajax.$form) { + // If setClick is set, we must set this to ensure that the button's + // value is passed. + if (ajax.setClick) { + // Mark the clicked button. 'form.clk' is a special variable for + // ajaxSubmit that tells the system which element got clicked to + // trigger the submit. Without it there would be no 'op' or + // equivalent. + element.form.clk = element; + } + + ajax.$form.ajaxSubmit(ajax.options); + } + else { + ajax.beforeSerialize(ajax.element, ajax.options); + $.ajax(ajax.options); + } + } + catch (e) { + // Unset the ajax.ajaxing flag here because it won't be unset during + // the complete response. + ajax.ajaxing = false; + window.alert('An error occurred while attempting to process ' + ajax.options.url + ': ' + e.message); + } + }; + + /** + * Handler for the form serialization. + * + * Runs before the beforeSend() handler (see below), and unlike that one, runs + * before field data is collected. + * + * @param {object} [element] + * Ajax object's `element_settings`. + * @param {object} options + * jQuery.ajax options. + */ + Drupal.Ajax.prototype.beforeSerialize = function (element, options) { + // Allow detaching behaviors to update field values before collecting them. + // This is only needed when field values are added to the POST data, so only + // when there is a form such that this.$form.ajaxSubmit() is used instead of + // $.ajax(). When there is no form and $.ajax() is used, beforeSerialize() + // isn't called, but don't rely on that: explicitly check this.$form. + if (this.$form) { + var settings = this.settings || drupalSettings; + Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize'); + } + + // Inform Drupal that this is an AJAX request. + options.data[Drupal.Ajax.AJAX_REQUEST_PARAMETER] = 1; + + // Allow Drupal to return new JavaScript and CSS files to load without + // returning the ones already loaded. + // @see \Drupal\Core\Theme\AjaxBasePageNegotiator + // @see \Drupal\Core\Asset\LibraryDependencyResolverInterface::getMinimalRepresentativeSubset() + // @see system_js_settings_alter() + var pageState = drupalSettings.ajaxPageState; + options.data['ajax_page_state[theme]'] = pageState.theme; + options.data['ajax_page_state[theme_token]'] = pageState.theme_token; + options.data['ajax_page_state[libraries]'] = pageState.libraries; + }; + + /** + * Modify form values prior to form submission. + * + * @param {Array.} form_values + * Processed form values. + * @param {jQuery} element + * The form node as a jQuery object. + * @param {object} options + * jQuery.ajax options. + */ + Drupal.Ajax.prototype.beforeSubmit = function (form_values, element, options) { + // This function is left empty to make it simple to override for modules + // that wish to add functionality here. + }; + + /** + * Prepare the Ajax request before it is sent. + * + * @param {XMLHttpRequest} xmlhttprequest + * Native Ajax object. + * @param {object} options + * jQuery.ajax options. + */ + Drupal.Ajax.prototype.beforeSend = function (xmlhttprequest, options) { + // For forms without file inputs, the jQuery Form plugin serializes the + // form values, and then calls jQuery's $.ajax() function, which invokes + // this handler. In this circumstance, options.extraData is never used. For + // forms with file inputs, the jQuery Form plugin uses the browser's normal + // form submission mechanism, but captures the response in a hidden IFRAME. + // In this circumstance, it calls this handler first, and then appends + // hidden fields to the form to submit the values in options.extraData. + // There is no simple way to know which submission mechanism will be used, + // so we add to extraData regardless, and allow it to be ignored in the + // former case. + if (this.$form) { + options.extraData = options.extraData || {}; + + // Let the server know when the IFRAME submission mechanism is used. The + // server can use this information to wrap the JSON response in a + // TEXTAREA, as per http://jquery.malsup.com/form/#file-upload. + options.extraData.ajax_iframe_upload = '1'; + + // The triggering element is about to be disabled (see below), but if it + // contains a value (e.g., a checkbox, textfield, select, etc.), ensure + // that value is included in the submission. As per above, submissions + // that use $.ajax() are already serialized prior to the element being + // disabled, so this is only needed for IFRAME submissions. + var v = $.fieldValue(this.element); + if (v !== null) { + options.extraData[this.element.name] = v; + } + } + + // Disable the element that received the change to prevent user interface + // interaction while the Ajax request is in progress. ajax.ajaxing prevents + // the element from triggering a new request, but does not prevent the user + // from changing its value. + $(this.element).prop('disabled', true); + + if (!this.progress || !this.progress.type) { + return; + } + + // Insert progress indicator. + var progressIndicatorMethod = 'setProgressIndicator' + this.progress.type.slice(0, 1).toUpperCase() + this.progress.type.slice(1).toLowerCase(); + if (progressIndicatorMethod in this && typeof this[progressIndicatorMethod] === 'function') { + this[progressIndicatorMethod].call(this); + } + }; + + /** + * Sets the progress bar progress indicator. + */ + Drupal.Ajax.prototype.setProgressIndicatorBar = function () { + var progressBar = new Drupal.ProgressBar('ajax-progress-' + this.element.id, $.noop, this.progress.method, $.noop); + if (this.progress.message) { + progressBar.setProgress(-1, this.progress.message); + } + if (this.progress.url) { + progressBar.startMonitoring(this.progress.url, this.progress.interval || 1500); + } + this.progress.element = $(progressBar.element).addClass('ajax-progress ajax-progress-bar'); + this.progress.object = progressBar; + $(this.element).after(this.progress.element); + }; + + /** + * Sets the throbber progress indicator. + */ + Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () { + this.progress.element = $('
 
'); + if (this.progress.message) { + this.progress.element.find('.throbber').after('
' + this.progress.message + '
'); + } + $(this.element).after(this.progress.element); + }; + + /** + * Sets the fullscreen progress indicator. + */ + Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function () { + this.progress.element = $('
 
'); + $('body').after(this.progress.element); + }; + + /** + * Handler for the form redirection completion. + * + * @param {Array.} response + * Drupal Ajax response. + * @param {number} status + * XMLHttpRequest status. + */ + Drupal.Ajax.prototype.success = function (response, status) { + // Remove the progress element. + if (this.progress.element) { + $(this.progress.element).remove(); + } + if (this.progress.object) { + this.progress.object.stopMonitoring(); + } + $(this.element).prop('disabled', false); + + // Save element's ancestors tree so if the element is removed from the dom + // we can try to refocus one of its parents. Using addBack reverse the + // result array, meaning that index 0 is the highest parent in the hierarchy + // in this situation it is usually a
element. + var elementParents = $(this.element).parents('[data-drupal-selector]').addBack().toArray(); + + // Track if any command is altering the focus so we can avoid changing the + // focus set by the Ajax command. + var focusChanged = false; + for (var i in response) { + if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) { + this.commands[response[i].command](this, response[i], status); + if (response[i].command === 'invoke' && response[i].method === 'focus') { + focusChanged = true; + } + } + } + + // If the focus hasn't be changed by the ajax commands, try to refocus the + // triggering element or one of its parents if that element does not exist + // anymore. + if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) { + var target = false; + + for (var n = elementParents.length - 1; !target && n > 0; n--) { + target = document.querySelector('[data-drupal-selector="' + elementParents[n].getAttribute('data-drupal-selector') + '"]'); + } + + if (target) { + $(target).trigger('focus'); + } + } + + // Reattach behaviors, if they were detached in beforeSerialize(). The + // attachBehaviors() called on the new content from processing the response + // commands is not sufficient, because behaviors from the entire form need + // to be reattached. + if (this.$form) { + var settings = this.settings || drupalSettings; + Drupal.attachBehaviors(this.$form.get(0), settings); + } + + // Remove any response-specific settings so they don't get used on the next + // call by mistake. + this.settings = null; + }; + + /** + * Build an effect object to apply an effect when adding new HTML. + * + * @param {object} response + * Drupal Ajax response. + * @param {string} [response.effect] + * Override the default value of {@link Drupal.Ajax#element_settings}. + * @param {string|number} [response.speed] + * Override the default value of {@link Drupal.Ajax#element_settings}. + * + * @return {object} + * Returns an object with `showEffect`, `hideEffect` and `showSpeed` + * properties. + */ + Drupal.Ajax.prototype.getEffect = function (response) { + var type = response.effect || this.effect; + var speed = response.speed || this.speed; + + var effect = {}; + if (type === 'none') { + effect.showEffect = 'show'; + effect.hideEffect = 'hide'; + effect.showSpeed = ''; + } + else if (type === 'fade') { + effect.showEffect = 'fadeIn'; + effect.hideEffect = 'fadeOut'; + effect.showSpeed = speed; + } + else { + effect.showEffect = type + 'Toggle'; + effect.hideEffect = type + 'Toggle'; + effect.showSpeed = speed; + } + + return effect; + }; + + /** + * Handler for the form redirection error. + * + * @param {object} xmlhttprequest + * Native XMLHttpRequest object. + * @param {string} uri + * Ajax Request URI. + * @param {string} [customMessage] + * Extra message to print with the Ajax error. + */ + Drupal.Ajax.prototype.error = function (xmlhttprequest, uri, customMessage) { + // Remove the progress element. + if (this.progress.element) { + $(this.progress.element).remove(); + } + if (this.progress.object) { + this.progress.object.stopMonitoring(); + } + // Undo hide. + $(this.wrapper).show(); + // Re-enable the element. + $(this.element).prop('disabled', false); + // Reattach behaviors, if they were detached in beforeSerialize(). + if (this.$form) { + var settings = this.settings || drupalSettings; + Drupal.attachBehaviors(this.$form.get(0), settings); + } + throw new Drupal.AjaxError(xmlhttprequest, uri, customMessage); + }; + + /** + * @typedef {object} Drupal.AjaxCommands~commandDefinition + * + * @prop {string} command + * @prop {string} [method] + * @prop {string} [selector] + * @prop {string} [data] + * @prop {object} [settings] + * @prop {bool} [asterisk] + * @prop {string} [text] + * @prop {string} [title] + * @prop {string} [url] + * @prop {object} [argument] + * @prop {string} [name] + * @prop {string} [value] + * @prop {string} [old] + * @prop {string} [new] + * @prop {bool} [merge] + * @prop {Array} [args] + * + * @see Drupal.AjaxCommands + */ + + /** + * Provide a series of commands that the client will perform. + * + * @constructor + */ + Drupal.AjaxCommands = function () {}; + Drupal.AjaxCommands.prototype = { + + /** + * Command to insert new content into the DOM. + * + * @param {Drupal.Ajax} ajax + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {string} response.data + * The data to use with the jQuery method. + * @param {string} [response.method] + * The jQuery DOM manipulation method to be used. + * @param {string} [response.selector] + * A optional jQuery selector string. + * @param {object} [response.settings] + * An optional array of settings that will be used. + * @param {number} [status] + * The XMLHttpRequest status. + */ + insert: function (ajax, response, status) { + // Get information from the response. If it is not there, default to + // our presets. + var $wrapper = response.selector ? $(response.selector) : $(ajax.wrapper); + var method = response.method || ajax.method; + var effect = ajax.getEffect(response); + var settings; + + // We don't know what response.data contains: it might be a string of text + // without HTML, so don't rely on jQuery correctly interpreting + // $(response.data) as new HTML rather than a CSS selector. Also, if + // response.data contains top-level text nodes, they get lost with either + // $(response.data) or $('
').replaceWith(response.data). + var $new_content_wrapped = $('
').html(response.data); + var $new_content = $new_content_wrapped.contents(); + + // For legacy reasons, the effects processing code assumes that + // $new_content consists of a single top-level element. Also, it has not + // been sufficiently tested whether attachBehaviors() can be successfully + // called with a context object that includes top-level text nodes. + // However, to give developers full control of the HTML appearing in the + // page, and to enable Ajax content to be inserted in places where
+ // elements are not allowed (e.g., within , , and + // parents), we check if the new content satisfies the requirement + // of a single top-level element, and only use the container
created + // above when it doesn't. For more information, please see + // https://www.drupal.org/node/736066. + if ($new_content.length !== 1 || $new_content.get(0).nodeType !== 1) { + $new_content = $new_content_wrapped; + } + + // If removing content from the wrapper, detach behaviors first. + switch (method) { + case 'html': + case 'replaceWith': + case 'replaceAll': + case 'empty': + case 'remove': + settings = response.settings || ajax.settings || drupalSettings; + Drupal.detachBehaviors($wrapper.get(0), settings); + } + + // Add the new content to the page. + $wrapper[method]($new_content); + + // Immediately hide the new content if we're using any effects. + if (effect.showEffect !== 'show') { + $new_content.hide(); + } + + // Determine which effect to use and what content will receive the + // effect, then show the new content. + if ($new_content.find('.ajax-new-content').length > 0) { + $new_content.find('.ajax-new-content').hide(); + $new_content.show(); + $new_content.find('.ajax-new-content')[effect.showEffect](effect.showSpeed); + } + else if (effect.showEffect !== 'show') { + $new_content[effect.showEffect](effect.showSpeed); + } + + // Attach all JavaScript behaviors to the new content, if it was + // successfully added to the page, this if statement allows + // `#ajax['wrapper']` to be optional. + if ($new_content.parents('html').length > 0) { + // Apply any settings from the returned JSON if available. + settings = response.settings || ajax.settings || drupalSettings; + Drupal.attachBehaviors($new_content.get(0), settings); + } + }, + + /** + * Command to remove a chunk from the page. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {string} response.selector + * A jQuery selector string. + * @param {object} [response.settings] + * An optional array of settings that will be used. + * @param {number} [status] + * The XMLHttpRequest status. + */ + remove: function (ajax, response, status) { + var settings = response.settings || ajax.settings || drupalSettings; + $(response.selector).each(function () { + Drupal.detachBehaviors(this, settings); + }) + .remove(); + }, + + /** + * Command to mark a chunk changed. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The JSON response object from the Ajax request. + * @param {string} response.selector + * A jQuery selector string. + * @param {bool} [response.asterisk] + * An optional CSS selector. If specified, an asterisk will be + * appended to the HTML inside the provided selector. + * @param {number} [status] + * The request status. + */ + changed: function (ajax, response, status) { + var $element = $(response.selector); + if (!$element.hasClass('ajax-changed')) { + $element.addClass('ajax-changed'); + if (response.asterisk) { + $element.find(response.asterisk).append(' * '); + } + } + }, + + /** + * Command to provide an alert. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The JSON response from the Ajax request. + * @param {string} response.text + * The text that will be displayed in an alert dialog. + * @param {number} [status] + * The XMLHttpRequest status. + */ + alert: function (ajax, response, status) { + window.alert(response.text, response.title); + }, + + /** + * Command to set the window.location, redirecting the browser. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {string} response.url + * The URL to redirect to. + * @param {number} [status] + * The XMLHttpRequest status. + */ + redirect: function (ajax, response, status) { + window.location = response.url; + }, + + /** + * Command to provide the jQuery css() function. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {string} response.selector + * A jQuery selector string. + * @param {object} response.argument + * An array of key/value pairs to set in the CSS for the selector. + * @param {number} [status] + * The XMLHttpRequest status. + */ + css: function (ajax, response, status) { + $(response.selector).css(response.argument); + }, + + /** + * Command to set the settings used for other commands in this response. + * + * This method will also remove expired `drupalSettings.ajax` settings. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {bool} response.merge + * Determines whether the additional settings should be merged to the + * global settings. + * @param {object} response.settings + * Contains additional settings to add to the global settings. + * @param {number} [status] + * The XMLHttpRequest status. + */ + settings: function (ajax, response, status) { + var ajaxSettings = drupalSettings.ajax; + + // Clean up drupalSettings.ajax. + if (ajaxSettings) { + Drupal.ajax.expired().forEach(function (instance) { + // If the Ajax object has been created through drupalSettings.ajax + // it will have a selector. When there is no selector the object + // has been initialized with a special class name picked up by the + // Ajax behavior. + + if (instance.selector) { + var selector = instance.selector.replace('#', ''); + if (selector in ajaxSettings) { + delete ajaxSettings[selector]; + } + } + }); + } + + if (response.merge) { + $.extend(true, drupalSettings, response.settings); + } + else { + ajax.settings = response.settings; + } + }, + + /** + * Command to attach data using jQuery's data API. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {string} response.name + * The name or key (in the key value pair) of the data attached to this + * selector. + * @param {string} response.selector + * A jQuery selector string. + * @param {string|object} response.value + * The value of to be attached. + * @param {number} [status] + * The XMLHttpRequest status. + */ + data: function (ajax, response, status) { + $(response.selector).data(response.name, response.value); + }, + + /** + * Command to apply a jQuery method. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {Array} response.args + * An array of arguments to the jQuery method, if any. + * @param {string} response.method + * The jQuery method to invoke. + * @param {string} response.selector + * A jQuery selector string. + * @param {number} [status] + * The XMLHttpRequest status. + */ + invoke: function (ajax, response, status) { + var $element = $(response.selector); + $element[response.method].apply($element, response.args); + }, + + /** + * Command to restripe a table. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {string} response.selector + * A jQuery selector string. + * @param {number} [status] + * The XMLHttpRequest status. + */ + restripe: function (ajax, response, status) { + // :even and :odd are reversed because jQuery counts from 0 and + // we count from 1, so we're out of sync. + // Match immediate children of the parent element to allow nesting. + $(response.selector).find('> tbody > tr:visible, > tr:visible') + .removeClass('odd even') + .filter(':even').addClass('odd').end() + .filter(':odd').addClass('even'); + }, + + /** + * Command to update a form's build ID. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {string} response.old + * The old form build ID. + * @param {string} response.new + * The new form build ID. + * @param {number} [status] + * The XMLHttpRequest status. + */ + update_build_id: function (ajax, response, status) { + $('input[name="form_build_id"][value="' + response.old + '"]').val(response.new); + }, + + /** + * Command to add css. + * + * Uses the proprietary addImport method if available as browsers which + * support that method ignore @import statements in dynamically added + * stylesheets. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {string} response.data + * A string that contains the styles to be added. + * @param {number} [status] + * The XMLHttpRequest status. + */ + add_css: function (ajax, response, status) { + // Add the styles in the normal way. + $('head').prepend(response.data); + // Add imports in the styles using the addImport method if available. + var match; + var importMatch = /^@import url\("(.*)"\);$/igm; + if (document.styleSheets[0].addImport && importMatch.test(response.data)) { + importMatch.lastIndex = 0; + do { + match = importMatch.exec(response.data); + document.styleSheets[0].addImport(match[1]); + } while (match); + } + } + }; + +})(jQuery, window, Drupal, drupalSettings); diff --git a/core/misc/ajax.js b/core/misc/ajax.js index fefe9f3031..b8adccbc8a 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -1,35 +1,15 @@ /** - * @file - * Provides Ajax page updating via jQuery $.ajax. - * - * Ajax is a method of making a request via JavaScript while viewing an HTML - * page. The request returns an array of commands encoded in JSON, which is - * then executed to make any changes that are necessary to the page. - * - * Drupal uses this file to enhance form elements with `#ajax['url']` and - * `#ajax['wrapper']` properties. If set, this file will automatically be - * included to provide Ajax capabilities. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/ajax.es6.js +* @preserve +**/ (function ($, window, Drupal, drupalSettings) { 'use strict'; - /** - * Attaches the Ajax behavior to each Ajax form element. - * - * @type {Drupal~behavior} - * - * @prop {Drupal~behaviorAttach} attach - * Initialize all {@link Drupal.Ajax} objects declared in - * `drupalSettings.ajax` or initialize {@link Drupal.Ajax} objects from - * DOM elements having the `use-ajax-submit` or `use-ajax` css class. - * @prop {Drupal~behaviorDetach} detach - * During `unload` remove all {@link Drupal.Ajax} objects related to - * the removed content. - */ Drupal.behaviors.AJAX = { - attach: function (context, settings) { + attach: function attach(context, settings) { function loadAjaxBehavior(base) { var element_settings = settings.ajax[base]; @@ -43,21 +23,17 @@ }); } - // Load all Ajax behaviors specified in the settings. for (var base in settings.ajax) { if (settings.ajax.hasOwnProperty(base)) { loadAjaxBehavior(base); } } - // Bind Ajax behaviors to all items showing the class. $('.use-ajax').once('ajax').each(function () { var element_settings = {}; - // Clicked links look better with the throbber than the progress bar. - element_settings.progress = {type: 'throbber'}; - // For anchor tags, these will go to the target of the anchor rather - // than the usual location. + element_settings.progress = { type: 'throbber' }; + var href = $(this).attr('href'); if (href) { element_settings.url = href; @@ -70,21 +46,16 @@ Drupal.ajax(element_settings); }); - // This class means to submit the form to the action using Ajax. $('.use-ajax-submit').once('ajax').each(function () { var element_settings = {}; - // Ajax submits specified in this manner automatically submit to the - // normal form action. element_settings.url = $(this.form).attr('action'); - // Form submit button clicks need to tell the form what was clicked so - // it gets passed in the POST request. + element_settings.setClick = true; - // Form buttons use the 'click' event rather than mousedown. + element_settings.event = 'click'; - // Clicked form buttons look better with the throbber than the progress - // bar. - element_settings.progress = {type: 'throbber'}; + + element_settings.progress = { type: 'throbber' }; element_settings.base = $(this).attr('id'); element_settings.element = this; @@ -92,31 +63,15 @@ }); }, - detach: function (context, settings, trigger) { + detach: function detach(context, settings, trigger) { if (trigger === 'unload') { Drupal.ajax.expired().forEach(function (instance) { - // Set this to null and allow garbage collection to reclaim - // the memory. Drupal.ajax.instances[instance.instanceIndex] = null; }); } } }; - /** - * Extends Error to provide handling for Errors in Ajax. - * - * @constructor - * - * @augments Error - * - * @param {XMLHttpRequest} xmlhttp - * XMLHttpRequest object used for the failed request. - * @param {string} uri - * The URI where the error occurred. - * @param {string} customMessage - * The custom message. - */ Drupal.AjaxError = function (xmlhttp, uri, customMessage) { var statusCode; @@ -125,124 +80,49 @@ var responseText; var readyStateText; if (xmlhttp.status) { - statusCode = '\n' + Drupal.t('An AJAX HTTP error occurred.') + '\n' + Drupal.t('HTTP Result Code: !status', {'!status': xmlhttp.status}); - } - else { + statusCode = '\n' + Drupal.t('An AJAX HTTP error occurred.') + '\n' + Drupal.t('HTTP Result Code: !status', { '!status': xmlhttp.status }); + } else { statusCode = '\n' + Drupal.t('An AJAX HTTP request terminated abnormally.'); } statusCode += '\n' + Drupal.t('Debugging information follows.'); - pathText = '\n' + Drupal.t('Path: !uri', {'!uri': uri}); + pathText = '\n' + Drupal.t('Path: !uri', { '!uri': uri }); statusText = ''; - // In some cases, when statusCode === 0, xmlhttp.statusText may not be - // defined. Unfortunately, testing for it with typeof, etc, doesn't seem to - // catch that and the test causes an exception. So we need to catch the - // exception here. + try { - statusText = '\n' + Drupal.t('StatusText: !statusText', {'!statusText': $.trim(xmlhttp.statusText)}); - } - catch (e) { - // Empty. - } + statusText = '\n' + Drupal.t('StatusText: !statusText', { '!statusText': $.trim(xmlhttp.statusText) }); + } catch (e) {} responseText = ''; - // Again, we don't have a way to know for sure whether accessing - // xmlhttp.responseText is going to throw an exception. So we'll catch it. + try { - responseText = '\n' + Drupal.t('ResponseText: !responseText', {'!responseText': $.trim(xmlhttp.responseText)}); - } - catch (e) { - // Empty. - } + responseText = '\n' + Drupal.t('ResponseText: !responseText', { '!responseText': $.trim(xmlhttp.responseText) }); + } catch (e) {} - // Make the responseText more readable by stripping HTML tags and newlines. responseText = responseText.replace(/<("[^"]*"|'[^']*'|[^'">])*>/gi, ''); responseText = responseText.replace(/[\n]+\s+/g, '\n'); - // We don't need readyState except for status == 0. - readyStateText = xmlhttp.status === 0 ? ('\n' + Drupal.t('ReadyState: !readyState', {'!readyState': xmlhttp.readyState})) : ''; + readyStateText = xmlhttp.status === 0 ? '\n' + Drupal.t('ReadyState: !readyState', { '!readyState': xmlhttp.readyState }) : ''; - customMessage = customMessage ? ('\n' + Drupal.t('CustomMessage: !customMessage', {'!customMessage': customMessage})) : ''; + customMessage = customMessage ? '\n' + Drupal.t('CustomMessage: !customMessage', { '!customMessage': customMessage }) : ''; - /** - * Formatted and translated error message. - * - * @type {string} - */ this.message = statusCode + pathText + statusText + customMessage + responseText + readyStateText; - /** - * Used by some browsers to display a more accurate stack trace. - * - * @type {string} - */ this.name = 'AjaxError'; }; Drupal.AjaxError.prototype = new Error(); Drupal.AjaxError.prototype.constructor = Drupal.AjaxError; - /** - * Provides Ajax page updating via jQuery $.ajax. - * - * This function is designed to improve developer experience by wrapping the - * initialization of {@link Drupal.Ajax} objects and storing all created - * objects in the {@link Drupal.ajax.instances} array. - * - * @example - * Drupal.behaviors.myCustomAJAXStuff = { - * attach: function (context, settings) { - * - * var ajaxSettings = { - * url: 'my/url/path', - * // If the old version of Drupal.ajax() needs to be used those - * // properties can be added - * base: 'myBase', - * element: $(context).find('.someElement') - * }; - * - * var myAjaxObject = Drupal.ajax(ajaxSettings); - * - * // Declare a new Ajax command specifically for this Ajax object. - * myAjaxObject.commands.insert = function (ajax, response, status) { - * $('#my-wrapper').append(response.data); - * alert('New content was appended to #my-wrapper'); - * }; - * - * // This command will remove this Ajax object from the page. - * myAjaxObject.commands.destroyObject = function (ajax, response, status) { - * Drupal.ajax.instances[this.instanceIndex] = null; - * }; - * - * // Programmatically trigger the Ajax request. - * myAjaxObject.execute(); - * } - * }; - * - * @param {object} settings - * The settings object passed to {@link Drupal.Ajax} constructor. - * @param {string} [settings.base] - * Base is passed to {@link Drupal.Ajax} constructor as the 'base' - * parameter. - * @param {HTMLElement} [settings.element] - * Element parameter of {@link Drupal.Ajax} constructor, element on which - * event listeners will be bound. - * - * @return {Drupal.Ajax} - * The created Ajax object. - * - * @see Drupal.AjaxCommands - */ Drupal.ajax = function (settings) { if (arguments.length !== 1) { throw new Error('Drupal.ajax() function must be called with one configuration object only'); } - // Map those config keys to variables for the old Drupal.ajax function. + var base = settings.base || false; var element = settings.element || false; delete settings.base; delete settings.element; - // By default do not display progress for ajax calls without an element. if (!settings.progress && !element) { settings.progress = false; } @@ -254,88 +134,14 @@ return ajax; }; - /** - * Contains all created Ajax objects. - * - * @type {Array.} - */ Drupal.ajax.instances = []; - /** - * List all objects where the associated element is not in the DOM - * - * This method ignores {@link Drupal.Ajax} objects not bound to DOM elements - * when created with {@link Drupal.ajax}. - * - * @return {Array.} - * The list of expired {@link Drupal.Ajax} objects. - */ Drupal.ajax.expired = function () { return Drupal.ajax.instances.filter(function (instance) { return instance && instance.element !== false && !document.body.contains(instance.element); }); }; - /** - * Settings for an Ajax object. - * - * @typedef {object} Drupal.Ajax~element_settings - * - * @prop {string} url - * Target of the Ajax request. - * @prop {?string} [event] - * Event bound to settings.element which will trigger the Ajax request. - * @prop {bool} [keypress=true] - * Triggers a request on keypress events. - * @prop {?string} selector - * jQuery selector targeting the element to bind events to or used with - * {@link Drupal.AjaxCommands}. - * @prop {string} [effect='none'] - * Name of the jQuery method to use for displaying new Ajax content. - * @prop {string|number} [speed='none'] - * Speed with which to apply the effect. - * @prop {string} [method] - * Name of the jQuery method used to insert new content in the targeted - * element. - * @prop {object} [progress] - * Settings for the display of a user-friendly loader. - * @prop {string} [progress.type='throbber'] - * Type of progress element, core provides `'bar'`, `'throbber'` and - * `'fullscreen'`. - * @prop {string} [progress.message=Drupal.t('Please wait...')] - * Custom message to be used with the bar indicator. - * @prop {object} [submit] - * Extra data to be sent with the Ajax request. - * @prop {bool} [submit.js=true] - * Allows the PHP side to know this comes from an Ajax request. - * @prop {object} [dialog] - * Options for {@link Drupal.dialog}. - * @prop {string} [dialogType] - * One of `'modal'` or `'dialog'`. - * @prop {string} [prevent] - * List of events on which to stop default action and stop propagation. - */ - - /** - * Ajax constructor. - * - * The Ajax request returns an array of commands encoded in JSON, which is - * then executed to make any changes that are necessary to the page. - * - * Drupal uses this file to enhance form elements with `#ajax['url']` and - * `#ajax['wrapper']` properties. If set, this file will automatically be - * included to provide Ajax capabilities. - * - * @constructor - * - * @param {string} [base] - * Base parameter of {@link Drupal.Ajax} constructor - * @param {HTMLElement} [element] - * Element parameter of {@link Drupal.Ajax} constructor, element on which - * event listeners will be bound. - * @param {Drupal.Ajax~element_settings} element_settings - * Settings for this Ajax object. - */ Drupal.Ajax = function (base, element, element_settings) { var defaults = { event: element ? 'mousedown' : null, @@ -355,146 +161,60 @@ $.extend(this, defaults, element_settings); - /** - * @type {Drupal.AjaxCommands} - */ this.commands = new Drupal.AjaxCommands(); - /** - * @type {bool|number} - */ this.instanceIndex = false; - // @todo Remove this after refactoring the PHP code to: - // - Call this 'selector'. - // - Include the '#' for ID-based selectors. - // - Support non-ID-based selectors. if (this.wrapper) { - - /** - * @type {string} - */ this.wrapper = '#' + this.wrapper; } - /** - * @type {HTMLElement} - */ this.element = element; - /** - * @type {Drupal.Ajax~element_settings} - */ this.element_settings = element_settings; - // If there isn't a form, jQuery.ajax() will be used instead, allowing us to - // bind Ajax to links as well. if (this.element && this.element.form) { - - /** - * @type {jQuery} - */ this.$form = $(this.element.form); } - // If no Ajax callback URL was given, use the link href or form action. if (!this.url) { var $element = $(this.element); if ($element.is('a')) { this.url = $element.attr('href'); - } - else if (this.element && element.form) { + } else if (this.element && element.form) { this.url = this.$form.attr('action'); } } - // Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let - // the server detect when it needs to degrade gracefully. - // There are four scenarios to check for: - // 1. /nojs/ - // 2. /nojs$ - The end of a URL string. - // 3. /nojs? - Followed by a query (e.g. path/nojs?destination=foobar). - // 4. /nojs# - Followed by a fragment (e.g.: path/nojs#myfragment). var originalUrl = this.url; - /** - * Processed Ajax URL. - * - * @type {string} - */ this.url = this.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1'); - // If the 'nojs' version of the URL is trusted, also trust the 'ajax' - // version. + if (drupalSettings.ajaxTrustedUrl[originalUrl]) { drupalSettings.ajaxTrustedUrl[this.url] = true; } - // Set the options for the ajaxSubmit function. - // The 'this' variable will not persist inside of the options object. var ajax = this; - /** - * Options for the jQuery.ajax function. - * - * @name Drupal.Ajax#options - * - * @type {object} - * - * @prop {string} url - * Ajax URL to be called. - * @prop {object} data - * Ajax payload. - * @prop {function} beforeSerialize - * Implement jQuery beforeSerialize function to call - * {@link Drupal.Ajax#beforeSerialize}. - * @prop {function} beforeSubmit - * Implement jQuery beforeSubmit function to call - * {@link Drupal.Ajax#beforeSubmit}. - * @prop {function} beforeSend - * Implement jQuery beforeSend function to call - * {@link Drupal.Ajax#beforeSend}. - * @prop {function} success - * Implement jQuery success function to call - * {@link Drupal.Ajax#success}. - * @prop {function} complete - * Implement jQuery success function to clean up ajax state and trigger an - * error if needed. - * @prop {string} dataType='json' - * Type of the response expected. - * @prop {string} type='POST' - * HTTP method to use for the Ajax request. - */ ajax.options = { url: ajax.url, data: ajax.submit, - beforeSerialize: function (element_settings, options) { + beforeSerialize: function beforeSerialize(element_settings, options) { return ajax.beforeSerialize(element_settings, options); }, - beforeSubmit: function (form_values, element_settings, options) { + beforeSubmit: function beforeSubmit(form_values, element_settings, options) { ajax.ajaxing = true; return ajax.beforeSubmit(form_values, element_settings, options); }, - beforeSend: function (xmlhttprequest, options) { + beforeSend: function beforeSend(xmlhttprequest, options) { ajax.ajaxing = true; return ajax.beforeSend(xmlhttprequest, options); }, - success: function (response, status, xmlhttprequest) { - // Sanity check for browser support (object expected). - // When using iFrame uploads, responses must be returned as a string. + success: function success(response, status, xmlhttprequest) { if (typeof response === 'string') { response = $.parseJSON(response); } - // Prior to invoking the response's commands, verify that they can be - // trusted by checking for a response header. See - // \Drupal\Core\EventSubscriber\AjaxResponseSubscriber for details. - // - Empty responses are harmless so can bypass verification. This - // avoids an alert message for server-generated no-op responses that - // skip Ajax rendering. - // - Ajax objects with trusted URLs (e.g., ones defined server-side via - // #ajax) can bypass header verification. This is especially useful - // for Ajax with multipart forms. Because IFRAME transport is used, - // the response headers cannot be accessed for verification. if (response !== null && !drupalSettings.ajaxTrustedUrl[ajax.url]) { if (xmlhttprequest.getResponseHeader('X-Drupal-Ajax-Token') !== '1') { var customMessage = Drupal.t('The response failed verification so will not be processed.'); @@ -504,7 +224,7 @@ return ajax.success(response, status); }, - complete: function (xmlhttprequest, status) { + complete: function complete(xmlhttprequest, status) { ajax.ajaxing = false; if (status === 'error' || status === 'parsererror') { return ajax.error(xmlhttprequest, ajax.url); @@ -518,288 +238,129 @@ ajax.options.data.dialogOptions = element_settings.dialog; } - // Ensure that we have a valid URL by adding ? when no query parameter is - // yet available, otherwise append using &. if (ajax.options.url.indexOf('?') === -1) { ajax.options.url += '?'; - } - else { + } else { ajax.options.url += '&'; } ajax.options.url += Drupal.ajax.WRAPPER_FORMAT + '=drupal_' + (element_settings.dialogType || 'ajax'); - // Bind the ajaxSubmit function to the element event. $(ajax.element).on(element_settings.event, function (event) { if (!drupalSettings.ajaxTrustedUrl[ajax.url] && !Drupal.url.isLocal(ajax.url)) { - throw new Error(Drupal.t('The callback URL is not local and not trusted: !url', {'!url': ajax.url})); + throw new Error(Drupal.t('The callback URL is not local and not trusted: !url', { '!url': ajax.url })); } return ajax.eventResponse(this, event); }); - // If necessary, enable keyboard submission so that Ajax behaviors - // can be triggered through keyboard input as well as e.g. a mousedown - // action. if (element_settings.keypress) { $(ajax.element).on('keypress', function (event) { return ajax.keypressResponse(this, event); }); } - // If necessary, prevent the browser default action of an additional event. - // For example, prevent the browser default action of a click, even if the - // Ajax behavior binds to mousedown. if (element_settings.prevent) { $(ajax.element).on(element_settings.prevent, false); } }; - /** - * URL query attribute to indicate the wrapper used to render a request. - * - * The wrapper format determines how the HTML is wrapped, for example in a - * modal dialog. - * - * @const {string} - * - * @default - */ Drupal.ajax.WRAPPER_FORMAT = '_wrapper_format'; - /** - * Request parameter to indicate that a request is a Drupal Ajax request. - * - * @const {string} - * - * @default - */ Drupal.Ajax.AJAX_REQUEST_PARAMETER = '_drupal_ajax'; - /** - * Execute the ajax request. - * - * Allows developers to execute an Ajax request manually without specifying - * an event to respond to. - * - * @return {object} - * Returns the jQuery.Deferred object underlying the Ajax request. If - * pre-serialization fails, the Deferred will be returned in the rejected - * state. - */ Drupal.Ajax.prototype.execute = function () { - // Do not perform another ajax command if one is already in progress. if (this.ajaxing) { return; } try { this.beforeSerialize(this.element, this.options); - // Return the jqXHR so that external code can hook into the Deferred API. + return $.ajax(this.options); - } - catch (e) { - // Unset the ajax.ajaxing flag here because it won't be unset during - // the complete response. + } catch (e) { this.ajaxing = false; window.alert('An error occurred while attempting to process ' + this.options.url + ': ' + e.message); - // For consistency, return a rejected Deferred (i.e., jqXHR's superclass) - // so that calling code can take appropriate action. + return $.Deferred().reject(); } }; - /** - * Handle a key press. - * - * The Ajax object will, if instructed, bind to a key press response. This - * will test to see if the key press is valid to trigger this event and - * if it is, trigger it for us and prevent other keypresses from triggering. - * In this case we're handling RETURN and SPACEBAR keypresses (event codes 13 - * and 32. RETURN is often used to submit a form when in a textfield, and - * SPACE is often used to activate an element without submitting. - * - * @param {HTMLElement} element - * Element the event was triggered on. - * @param {jQuery.Event} event - * Triggered event. - */ Drupal.Ajax.prototype.keypressResponse = function (element, event) { - // Create a synonym for this to reduce code confusion. var ajax = this; - // Detect enter key and space bar and allow the standard response for them, - // except for form elements of type 'text', 'tel', 'number' and 'textarea', - // where the spacebar activation causes inappropriate activation if - // #ajax['keypress'] is TRUE. On a text-type widget a space should always - // be a space. - if (event.which === 13 || (event.which === 32 && element.type !== 'text' && - element.type !== 'textarea' && element.type !== 'tel' && element.type !== 'number')) { + if (event.which === 13 || event.which === 32 && element.type !== 'text' && element.type !== 'textarea' && element.type !== 'tel' && element.type !== 'number') { event.preventDefault(); event.stopPropagation(); $(ajax.element_settings.element).trigger(ajax.element_settings.event); } }; - /** - * Handle an event that triggers an Ajax response. - * - * When an event that triggers an Ajax response happens, this method will - * perform the actual Ajax call. It is bound to the event using - * bind() in the constructor, and it uses the options specified on the - * Ajax object. - * - * @param {HTMLElement} element - * Element the event was triggered on. - * @param {jQuery.Event} event - * Triggered event. - */ Drupal.Ajax.prototype.eventResponse = function (element, event) { event.preventDefault(); event.stopPropagation(); - // Create a synonym for this to reduce code confusion. var ajax = this; - // Do not perform another Ajax command if one is already in progress. if (ajax.ajaxing) { return; } try { if (ajax.$form) { - // If setClick is set, we must set this to ensure that the button's - // value is passed. if (ajax.setClick) { - // Mark the clicked button. 'form.clk' is a special variable for - // ajaxSubmit that tells the system which element got clicked to - // trigger the submit. Without it there would be no 'op' or - // equivalent. element.form.clk = element; } ajax.$form.ajaxSubmit(ajax.options); - } - else { + } else { ajax.beforeSerialize(ajax.element, ajax.options); $.ajax(ajax.options); } - } - catch (e) { - // Unset the ajax.ajaxing flag here because it won't be unset during - // the complete response. + } catch (e) { ajax.ajaxing = false; window.alert('An error occurred while attempting to process ' + ajax.options.url + ': ' + e.message); } }; - /** - * Handler for the form serialization. - * - * Runs before the beforeSend() handler (see below), and unlike that one, runs - * before field data is collected. - * - * @param {object} [element] - * Ajax object's `element_settings`. - * @param {object} options - * jQuery.ajax options. - */ Drupal.Ajax.prototype.beforeSerialize = function (element, options) { - // Allow detaching behaviors to update field values before collecting them. - // This is only needed when field values are added to the POST data, so only - // when there is a form such that this.$form.ajaxSubmit() is used instead of - // $.ajax(). When there is no form and $.ajax() is used, beforeSerialize() - // isn't called, but don't rely on that: explicitly check this.$form. if (this.$form) { var settings = this.settings || drupalSettings; Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize'); } - // Inform Drupal that this is an AJAX request. options.data[Drupal.Ajax.AJAX_REQUEST_PARAMETER] = 1; - // Allow Drupal to return new JavaScript and CSS files to load without - // returning the ones already loaded. - // @see \Drupal\Core\Theme\AjaxBasePageNegotiator - // @see \Drupal\Core\Asset\LibraryDependencyResolverInterface::getMinimalRepresentativeSubset() - // @see system_js_settings_alter() var pageState = drupalSettings.ajaxPageState; options.data['ajax_page_state[theme]'] = pageState.theme; options.data['ajax_page_state[theme_token]'] = pageState.theme_token; options.data['ajax_page_state[libraries]'] = pageState.libraries; }; - /** - * Modify form values prior to form submission. - * - * @param {Array.} form_values - * Processed form values. - * @param {jQuery} element - * The form node as a jQuery object. - * @param {object} options - * jQuery.ajax options. - */ - Drupal.Ajax.prototype.beforeSubmit = function (form_values, element, options) { - // This function is left empty to make it simple to override for modules - // that wish to add functionality here. - }; + Drupal.Ajax.prototype.beforeSubmit = function (form_values, element, options) {}; - /** - * Prepare the Ajax request before it is sent. - * - * @param {XMLHttpRequest} xmlhttprequest - * Native Ajax object. - * @param {object} options - * jQuery.ajax options. - */ Drupal.Ajax.prototype.beforeSend = function (xmlhttprequest, options) { - // For forms without file inputs, the jQuery Form plugin serializes the - // form values, and then calls jQuery's $.ajax() function, which invokes - // this handler. In this circumstance, options.extraData is never used. For - // forms with file inputs, the jQuery Form plugin uses the browser's normal - // form submission mechanism, but captures the response in a hidden IFRAME. - // In this circumstance, it calls this handler first, and then appends - // hidden fields to the form to submit the values in options.extraData. - // There is no simple way to know which submission mechanism will be used, - // so we add to extraData regardless, and allow it to be ignored in the - // former case. if (this.$form) { options.extraData = options.extraData || {}; - // Let the server know when the IFRAME submission mechanism is used. The - // server can use this information to wrap the JSON response in a - // TEXTAREA, as per http://jquery.malsup.com/form/#file-upload. options.extraData.ajax_iframe_upload = '1'; - // The triggering element is about to be disabled (see below), but if it - // contains a value (e.g., a checkbox, textfield, select, etc.), ensure - // that value is included in the submission. As per above, submissions - // that use $.ajax() are already serialized prior to the element being - // disabled, so this is only needed for IFRAME submissions. var v = $.fieldValue(this.element); if (v !== null) { options.extraData[this.element.name] = v; } } - // Disable the element that received the change to prevent user interface - // interaction while the Ajax request is in progress. ajax.ajaxing prevents - // the element from triggering a new request, but does not prevent the user - // from changing its value. $(this.element).prop('disabled', true); if (!this.progress || !this.progress.type) { return; } - // Insert progress indicator. var progressIndicatorMethod = 'setProgressIndicator' + this.progress.type.slice(0, 1).toUpperCase() + this.progress.type.slice(1).toLowerCase(); if (progressIndicatorMethod in this && typeof this[progressIndicatorMethod] === 'function') { this[progressIndicatorMethod].call(this); } }; - /** - * Sets the progress bar progress indicator. - */ Drupal.Ajax.prototype.setProgressIndicatorBar = function () { var progressBar = new Drupal.ProgressBar('ajax-progress-' + this.element.id, $.noop, this.progress.method, $.noop); if (this.progress.message) { @@ -813,9 +374,6 @@ $(this.element).after(this.progress.element); }; - /** - * Sets the throbber progress indicator. - */ Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () { this.progress.element = $('
 
'); if (this.progress.message) { @@ -824,24 +382,12 @@ $(this.element).after(this.progress.element); }; - /** - * Sets the fullscreen progress indicator. - */ Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function () { this.progress.element = $('
 
'); $('body').after(this.progress.element); }; - /** - * Handler for the form redirection completion. - * - * @param {Array.} response - * Drupal Ajax response. - * @param {number} status - * XMLHttpRequest status. - */ Drupal.Ajax.prototype.success = function (response, status) { - // Remove the progress element. if (this.progress.element) { $(this.progress.element).remove(); } @@ -850,14 +396,8 @@ } $(this.element).prop('disabled', false); - // Save element's ancestors tree so if the element is removed from the dom - // we can try to refocus one of its parents. Using addBack reverse the - // result array, meaning that index 0 is the highest parent in the hierarchy - // in this situation it is usually a element. var elementParents = $(this.element).parents('[data-drupal-selector]').addBack().toArray(); - // Track if any command is altering the focus so we can avoid changing the - // focus set by the Ajax command. var focusChanged = false; for (var i in response) { if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) { @@ -868,9 +408,6 @@ } } - // If the focus hasn't be changed by the ajax commands, try to refocus the - // triggering element or one of its parents if that element does not exist - // anymore. if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) { var target = false; @@ -883,34 +420,14 @@ } } - // Reattach behaviors, if they were detached in beforeSerialize(). The - // attachBehaviors() called on the new content from processing the response - // commands is not sufficient, because behaviors from the entire form need - // to be reattached. if (this.$form) { var settings = this.settings || drupalSettings; Drupal.attachBehaviors(this.$form.get(0), settings); } - // Remove any response-specific settings so they don't get used on the next - // call by mistake. this.settings = null; }; - /** - * Build an effect object to apply an effect when adding new HTML. - * - * @param {object} response - * Drupal Ajax response. - * @param {string} [response.effect] - * Override the default value of {@link Drupal.Ajax#element_settings}. - * @param {string|number} [response.speed] - * Override the default value of {@link Drupal.Ajax#element_settings}. - * - * @return {object} - * Returns an object with `showEffect`, `hideEffect` and `showSpeed` - * properties. - */ Drupal.Ajax.prototype.getEffect = function (response) { var type = response.effect || this.effect; var speed = response.speed || this.speed; @@ -920,13 +437,11 @@ effect.showEffect = 'show'; effect.hideEffect = 'hide'; effect.showSpeed = ''; - } - else if (type === 'fade') { + } else if (type === 'fade') { effect.showEffect = 'fadeIn'; effect.hideEffect = 'fadeOut'; effect.showSpeed = speed; - } - else { + } else { effect.showEffect = type + 'Toggle'; effect.hideEffect = type + 'Toggle'; effect.showSpeed = speed; @@ -935,29 +450,18 @@ return effect; }; - /** - * Handler for the form redirection error. - * - * @param {object} xmlhttprequest - * Native XMLHttpRequest object. - * @param {string} uri - * Ajax Request URI. - * @param {string} [customMessage] - * Extra message to print with the Ajax error. - */ Drupal.Ajax.prototype.error = function (xmlhttprequest, uri, customMessage) { - // Remove the progress element. if (this.progress.element) { $(this.progress.element).remove(); } if (this.progress.object) { this.progress.object.stopMonitoring(); } - // Undo hide. + $(this.wrapper).show(); - // Re-enable the element. + $(this.element).prop('disabled', false); - // Reattach behaviors, if they were detached in beforeSerialize(). + if (this.$form) { var settings = this.settings || drupalSettings; Drupal.attachBehaviors(this.$form.get(0), settings); @@ -965,87 +469,21 @@ throw new Drupal.AjaxError(xmlhttprequest, uri, customMessage); }; - /** - * @typedef {object} Drupal.AjaxCommands~commandDefinition - * - * @prop {string} command - * @prop {string} [method] - * @prop {string} [selector] - * @prop {string} [data] - * @prop {object} [settings] - * @prop {bool} [asterisk] - * @prop {string} [text] - * @prop {string} [title] - * @prop {string} [url] - * @prop {object} [argument] - * @prop {string} [name] - * @prop {string} [value] - * @prop {string} [old] - * @prop {string} [new] - * @prop {bool} [merge] - * @prop {Array} [args] - * - * @see Drupal.AjaxCommands - */ - - /** - * Provide a series of commands that the client will perform. - * - * @constructor - */ Drupal.AjaxCommands = function () {}; Drupal.AjaxCommands.prototype = { - - /** - * Command to insert new content into the DOM. - * - * @param {Drupal.Ajax} ajax - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The response from the Ajax request. - * @param {string} response.data - * The data to use with the jQuery method. - * @param {string} [response.method] - * The jQuery DOM manipulation method to be used. - * @param {string} [response.selector] - * A optional jQuery selector string. - * @param {object} [response.settings] - * An optional array of settings that will be used. - * @param {number} [status] - * The XMLHttpRequest status. - */ - insert: function (ajax, response, status) { - // Get information from the response. If it is not there, default to - // our presets. + insert: function insert(ajax, response, status) { var $wrapper = response.selector ? $(response.selector) : $(ajax.wrapper); var method = response.method || ajax.method; var effect = ajax.getEffect(response); var settings; - // We don't know what response.data contains: it might be a string of text - // without HTML, so don't rely on jQuery correctly interpreting - // $(response.data) as new HTML rather than a CSS selector. Also, if - // response.data contains top-level text nodes, they get lost with either - // $(response.data) or $('
').replaceWith(response.data). var $new_content_wrapped = $('
').html(response.data); var $new_content = $new_content_wrapped.contents(); - // For legacy reasons, the effects processing code assumes that - // $new_content consists of a single top-level element. Also, it has not - // been sufficiently tested whether attachBehaviors() can be successfully - // called with a context object that includes top-level text nodes. - // However, to give developers full control of the HTML appearing in the - // page, and to enable Ajax content to be inserted in places where
- // elements are not allowed (e.g., within
, , and - // parents), we check if the new content satisfies the requirement - // of a single top-level element, and only use the container
created - // above when it doesn't. For more information, please see - // https://www.drupal.org/node/736066. if ($new_content.length !== 1 || $new_content.get(0).nodeType !== 1) { $new_content = $new_content_wrapped; } - // If removing content from the wrapper, detach behaviors first. switch (method) { case 'html': case 'replaceWith': @@ -1056,73 +494,34 @@ Drupal.detachBehaviors($wrapper.get(0), settings); } - // Add the new content to the page. $wrapper[method]($new_content); - // Immediately hide the new content if we're using any effects. if (effect.showEffect !== 'show') { $new_content.hide(); } - // Determine which effect to use and what content will receive the - // effect, then show the new content. if ($new_content.find('.ajax-new-content').length > 0) { $new_content.find('.ajax-new-content').hide(); $new_content.show(); $new_content.find('.ajax-new-content')[effect.showEffect](effect.showSpeed); - } - else if (effect.showEffect !== 'show') { + } else if (effect.showEffect !== 'show') { $new_content[effect.showEffect](effect.showSpeed); } - // Attach all JavaScript behaviors to the new content, if it was - // successfully added to the page, this if statement allows - // `#ajax['wrapper']` to be optional. if ($new_content.parents('html').length > 0) { - // Apply any settings from the returned JSON if available. settings = response.settings || ajax.settings || drupalSettings; Drupal.attachBehaviors($new_content.get(0), settings); } }, - /** - * Command to remove a chunk from the page. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The response from the Ajax request. - * @param {string} response.selector - * A jQuery selector string. - * @param {object} [response.settings] - * An optional array of settings that will be used. - * @param {number} [status] - * The XMLHttpRequest status. - */ - remove: function (ajax, response, status) { + remove: function remove(ajax, response, status) { var settings = response.settings || ajax.settings || drupalSettings; $(response.selector).each(function () { Drupal.detachBehaviors(this, settings); - }) - .remove(); + }).remove(); }, - /** - * Command to mark a chunk changed. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The JSON response object from the Ajax request. - * @param {string} response.selector - * A jQuery selector string. - * @param {bool} [response.asterisk] - * An optional CSS selector. If specified, an asterisk will be - * appended to the HTML inside the provided selector. - * @param {number} [status] - * The request status. - */ - changed: function (ajax, response, status) { + changed: function changed(ajax, response, status) { var $element = $(response.selector); if (!$element.hasClass('ajax-changed')) { $element.addClass('ajax-changed'); @@ -1132,83 +531,23 @@ } }, - /** - * Command to provide an alert. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The JSON response from the Ajax request. - * @param {string} response.text - * The text that will be displayed in an alert dialog. - * @param {number} [status] - * The XMLHttpRequest status. - */ - alert: function (ajax, response, status) { + alert: function alert(ajax, response, status) { window.alert(response.text, response.title); }, - /** - * Command to set the window.location, redirecting the browser. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The response from the Ajax request. - * @param {string} response.url - * The URL to redirect to. - * @param {number} [status] - * The XMLHttpRequest status. - */ - redirect: function (ajax, response, status) { + redirect: function redirect(ajax, response, status) { window.location = response.url; }, - /** - * Command to provide the jQuery css() function. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The response from the Ajax request. - * @param {string} response.selector - * A jQuery selector string. - * @param {object} response.argument - * An array of key/value pairs to set in the CSS for the selector. - * @param {number} [status] - * The XMLHttpRequest status. - */ - css: function (ajax, response, status) { + css: function css(ajax, response, status) { $(response.selector).css(response.argument); }, - /** - * Command to set the settings used for other commands in this response. - * - * This method will also remove expired `drupalSettings.ajax` settings. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The response from the Ajax request. - * @param {bool} response.merge - * Determines whether the additional settings should be merged to the - * global settings. - * @param {object} response.settings - * Contains additional settings to add to the global settings. - * @param {number} [status] - * The XMLHttpRequest status. - */ - settings: function (ajax, response, status) { + settings: function settings(ajax, response, status) { var ajaxSettings = drupalSettings.ajax; - // Clean up drupalSettings.ajax. if (ajaxSettings) { Drupal.ajax.expired().forEach(function (instance) { - // If the Ajax object has been created through drupalSettings.ajax - // it will have a selector. When there is no selector the object - // has been initialized with a special class name picked up by the - // Ajax behavior. if (instance.selector) { var selector = instance.selector.replace('#', ''); @@ -1221,114 +560,31 @@ if (response.merge) { $.extend(true, drupalSettings, response.settings); - } - else { + } else { ajax.settings = response.settings; } }, - /** - * Command to attach data using jQuery's data API. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The response from the Ajax request. - * @param {string} response.name - * The name or key (in the key value pair) of the data attached to this - * selector. - * @param {string} response.selector - * A jQuery selector string. - * @param {string|object} response.value - * The value of to be attached. - * @param {number} [status] - * The XMLHttpRequest status. - */ - data: function (ajax, response, status) { + data: function data(ajax, response, status) { $(response.selector).data(response.name, response.value); }, - /** - * Command to apply a jQuery method. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The response from the Ajax request. - * @param {Array} response.args - * An array of arguments to the jQuery method, if any. - * @param {string} response.method - * The jQuery method to invoke. - * @param {string} response.selector - * A jQuery selector string. - * @param {number} [status] - * The XMLHttpRequest status. - */ - invoke: function (ajax, response, status) { + invoke: function invoke(ajax, response, status) { var $element = $(response.selector); $element[response.method].apply($element, response.args); }, - /** - * Command to restripe a table. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The response from the Ajax request. - * @param {string} response.selector - * A jQuery selector string. - * @param {number} [status] - * The XMLHttpRequest status. - */ - restripe: function (ajax, response, status) { - // :even and :odd are reversed because jQuery counts from 0 and - // we count from 1, so we're out of sync. - // Match immediate children of the parent element to allow nesting. - $(response.selector).find('> tbody > tr:visible, > tr:visible') - .removeClass('odd even') - .filter(':even').addClass('odd').end() - .filter(':odd').addClass('even'); + restripe: function restripe(ajax, response, status) { + $(response.selector).find('> tbody > tr:visible, > tr:visible').removeClass('odd even').filter(':even').addClass('odd').end().filter(':odd').addClass('even'); }, - /** - * Command to update a form's build ID. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The response from the Ajax request. - * @param {string} response.old - * The old form build ID. - * @param {string} response.new - * The new form build ID. - * @param {number} [status] - * The XMLHttpRequest status. - */ - update_build_id: function (ajax, response, status) { + update_build_id: function update_build_id(ajax, response, status) { $('input[name="form_build_id"][value="' + response.old + '"]').val(response.new); }, - /** - * Command to add css. - * - * Uses the proprietary addImport method if available as browsers which - * support that method ignore @import statements in dynamically added - * stylesheets. - * - * @param {Drupal.Ajax} [ajax] - * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. - * @param {object} response - * The response from the Ajax request. - * @param {string} response.data - * A string that contains the styles to be added. - * @param {number} [status] - * The XMLHttpRequest status. - */ - add_css: function (ajax, response, status) { - // Add the styles in the normal way. + add_css: function add_css(ajax, response, status) { $('head').prepend(response.data); - // Add imports in the styles using the addImport method if available. + var match; var importMatch = /^@import url\("(.*)"\);$/igm; if (document.styleSheets[0].addImport && importMatch.test(response.data)) { @@ -1340,5 +596,4 @@ } } }; - -})(jQuery, window, Drupal, drupalSettings); +})(jQuery, window, Drupal, drupalSettings); \ No newline at end of file diff --git a/core/misc/announce.es6.js b/core/misc/announce.es6.js new file mode 100644 index 0000000000..acf850a641 --- /dev/null +++ b/core/misc/announce.es6.js @@ -0,0 +1,120 @@ +/** + * @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) { + + 'use strict'; + + var liveElement; + 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. + 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() { + var text = []; + var priority = 'polite'; + var announcement; + + // 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)()); + }; +}(Drupal, Drupal.debounce)); diff --git a/core/misc/announce.js b/core/misc/announce.js index acf850a641..a6766edb04 100644 --- a/core/misc/announce.js +++ b/core/misc/announce.js @@ -1,22 +1,8 @@ /** - * @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. +* All changes should be applied to ./misc/announce.es6.js +* @preserve +**/ (function (Drupal, debounce) { @@ -25,17 +11,8 @@ var liveElement; 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 +24,40 @@ } }; - /** - * Concatenates announcements to a single string; appends to the live region. - */ function announce() { var text = []; var priority = 'polite'; var announcement; - // 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); \ No newline at end of file diff --git a/core/misc/autocomplete.es6.js b/core/misc/autocomplete.es6.js new file mode 100644 index 0000000000..5a1c156d0b --- /dev/null +++ b/core/misc/autocomplete.es6.js @@ -0,0 +1,288 @@ +/** + * @file + * Autocomplete based on jQuery UI. + */ + +(function ($, Drupal) { + + '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; + + for (var 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) { + var options = autocomplete.options; + + if (options.isComposing) { + return false; + } + + 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'); + + 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) { + var tagged = autocomplete.splitValues(request.term); + var il = tagged.length; + for (var i = 0; i < il; i++) { + var 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 { + 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. + if (ui.item.value.search(',') > 0) { + terms.push('"' + ui.item.value + '"'); + } + else { + 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 $('
  • ') + .append($('').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. + 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 : '' + }); + // 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; + }); + $autocomplete.on('compositionend.autocomplete', function () { + autocomplete.options.isComposing = false; + }); + } + }, + detach: function (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: extractLastTerm, + // jQuery UI autocomplete options. + + /** + * JQuery UI option object. + * + * @name Drupal.autocomplete.options + */ + options: { + source: sourceData, + focus: focusHandler, + search: searchHandler, + select: selectHandler, + renderItem: 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); diff --git a/core/misc/autocomplete.js b/core/misc/autocomplete.js index 5a1c156d0b..d5ee7f9a65 100644 --- a/core/misc/autocomplete.js +++ b/core/misc/autocomplete.js @@ -1,7 +1,8 @@ /** - * @file - * Autocomplete based on jQuery UI. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/autocomplete.es6.js +* @preserve +**/ (function ($, Drupal) { @@ -9,19 +10,7 @@ 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 = ''; @@ -33,12 +22,10 @@ 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 +36,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 +48,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 +63,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,113 +75,58 @@ 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. + if (ui.item.value.search(',') > 0) { terms.push('"' + ui.item.value + '"'); - } - else { + } else { 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 $('
  • ') - .append($('').html(item.label)) - .appendTo(ul); + return $('
  • ').append($('').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 ? 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; }); @@ -240,32 +135,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, @@ -273,9 +155,9 @@ select: selectHandler, renderItem: renderItem, minLength: 1, - // Custom options, used by Drupal.autocomplete. + firstCharacterBlacklist: '', - // Custom options, indicate IME usage status. + isComposing: false }, ajax: { @@ -284,5 +166,4 @@ }; Drupal.autocomplete = autocomplete; - -})(jQuery, Drupal); +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/misc/batch.es6.js b/core/misc/batch.es6.js new file mode 100644 index 0000000000..411badba42 --- /dev/null +++ b/core/misc/batch.es6.js @@ -0,0 +1,46 @@ +/** + * @file + * Drupal's batch API. + */ + +(function ($, Drupal) { + + '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 = $('[data-drupal-progress]').once('batch'); + 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($('

    ').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); diff --git a/core/misc/batch.js b/core/misc/batch.js index 411badba42..c8ecb96117 100644 --- a/core/misc/batch.js +++ b/core/misc/batch.js @@ -1,24 +1,19 @@ /** - * @file - * Drupal's batch API. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/batch.es6.js +* @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; - // Success: redirect to the summary. function updateCallback(progress, status, pb) { if (progress === '100') { pb.stopMonitoring(); @@ -35,12 +30,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); \ No newline at end of file diff --git a/core/misc/collapse.es6.js b/core/misc/collapse.es6.js new file mode 100644 index 0000000000..767325ec1a --- /dev/null +++ b/core/misc/collapse.es6.js @@ -0,0 +1,146 @@ +/** + * @file + * Polyfill for HTML5 details elements. + */ + +(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.} + */ + instances: [] + }); + + $.extend(CollapsibleDetails.prototype, /** @lends Drupal.CollapsibleDetails# */{ + + /** + * Initialize and setup summary events and markup. + * + * @fires event:summaryUpdated + * + * @listens event:summaryUpdated + */ + setupSummary: function () { + this.$summary = $(''); + this.$node + .on('summaryUpdated', $.proxy(this.onSummaryUpdated, this)) + .trigger('summaryUpdated'); + }, + + /** + * Initialize and setup legend markup. + */ + setupLegend: function () { + // Turn the summary into a clickable link. + var $legend = this.$node.find('> summary'); + + $('') + .append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show')) + .prependTo($legend) + .after(document.createTextNode(' ')); + + // .wrapInner() does not retain bound events. + $('
    ') + .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: function (e) { + this.toggle(); + e.preventDefault(); + }, + + /** + * Update summary. + */ + onSummaryUpdated: function () { + var text = $.trim(this.$node.drupalGetSummary()); + this.$summary.html(text ? ' (' + text + ')' : ''); + }, + + /** + * 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 { + $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); + } + }); + + /** + * Polyfill HTML5 details element. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches behavior for the details element. + */ + Drupal.behaviors.collapse = { + attach: function (context) { + if (Modernizr.details) { + return; + } + var $collapsibleDetails = $(context).find('details').once('collapse').addClass('collapse-processed'); + if ($collapsibleDetails.length) { + for (var i = 0; i < $collapsibleDetails.length; i++) { + CollapsibleDetails.instances.push(new CollapsibleDetails($collapsibleDetails[i])); + } + } + } + }; + + // Expose constructor in the public space. + Drupal.CollapsibleDetails = CollapsibleDetails; + +})(jQuery, Modernizr, Drupal); diff --git a/core/misc/collapse.js b/core/misc/collapse.js index 767325ec1a..6f3124404b 100644 --- a/core/misc/collapse.js +++ b/core/misc/collapse.js @@ -1,133 +1,74 @@ /** - * @file - * Polyfill for HTML5 details elements. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/collapse.es6.js +* @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.} - */ + $.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 = $(''); - 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'); - $('') - .append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show')) - .prependTo($legend) - .after(document.createTextNode(' ')); + $('').append(this.$node.attr('open') ? Drupal.t('Hide') : Drupal.t('Show')).prependTo($legend).after(document.createTextNode(' ')); - // .wrapInner() does not retain bound events. - $('') - .attr('href', '#' + this.$node.attr('id')) - .prepend($legend.contents()) - .appendTo($legend); + $('').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 the visibility of a details element using smooth animations. - */ - toggle: function () { + toggle: function toggle() { 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); } }); - /** - * 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 +81,5 @@ } }; - // Expose constructor in the public space. Drupal.CollapsibleDetails = CollapsibleDetails; - -})(jQuery, Modernizr, Drupal); +})(jQuery, Modernizr, Drupal); \ No newline at end of file diff --git a/core/misc/date.es6.js b/core/misc/date.es6.js new file mode 100644 index 0000000000..8b6b71cb1d --- /dev/null +++ b/core/misc/date.es6.js @@ -0,0 +1,56 @@ +/** + * @file + * Polyfill for HTML5 date input. + */ + +(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) { + var $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 () { + 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. + if ($input.attr('min')) { + datepickerSettings.minDate = $input.attr('min'); + } + if ($input.attr('max')) { + datepickerSettings.maxDate = $input.attr('max'); + } + $input.datepicker(datepickerSettings); + }); + }, + detach: function (context, settings, trigger) { + if (trigger === 'unload') { + $(context).find('input[data-drupal-date-format]').findOnce('datePicker').datepicker('destroy'); + } + } + }; + +})(jQuery, Modernizr, Drupal); diff --git a/core/misc/date.js b/core/misc/date.js index 8b6b71cb1d..6b9b215c17 100644 --- a/core/misc/date.js +++ b/core/misc/date.js @@ -1,28 +1,17 @@ /** - * @file - * Polyfill for HTML5 date input. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/date.es6.js +* @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 +19,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 +31,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); \ No newline at end of file diff --git a/core/misc/debounce.es6.js b/core/misc/debounce.es6.js new file mode 100644 index 0000000000..995e3d7135 --- /dev/null +++ b/core/misc/debounce.es6.js @@ -0,0 +1,52 @@ +/** + * @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) { + + 'use strict'; + + var timeout; + var result; + return function () { + var context = this; + var args = arguments; + var later = function () { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + } + return result; + }; +}; diff --git a/core/misc/debounce.js b/core/misc/debounce.js index 995e3d7135..4d6f3360b5 100644 --- a/core/misc/debounce.js +++ b/core/misc/debounce.js @@ -1,31 +1,9 @@ /** - * @file - * Adapted from underscore.js with the addition Drupal namespace. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/debounce.es6.js +* @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'; @@ -35,7 +13,7 @@ Drupal.debounce = function (func, wait, immediate) { 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 +27,4 @@ Drupal.debounce = function (func, wait, immediate) { } return result; }; -}; +}; \ No newline at end of file diff --git a/core/misc/details-aria.es6.js b/core/misc/details-aria.es6.js new file mode 100644 index 0000000000..d341422351 --- /dev/null +++ b/core/misc/details-aria.es6.js @@ -0,0 +1,29 @@ +/** + * @file + * Add aria attribute handling for details and summary elements. + */ + +(function ($, Drupal) { + + 'use strict'; + + /** + * Handles `aria-expanded` and `aria-pressed` attributes on details elements. + * + * @type {Drupal~behavior} + */ + Drupal.behaviors.detailsAria = { + attach: function () { + $('body').once('detailsAria').on('click.detailsAria', 'summary', function (event) { + var $summary = $(event.currentTarget); + var open = $(event.currentTarget.parentNode).attr('open') === 'open' ? 'false' : 'true'; + + $summary.attr({ + 'aria-expanded': open, + 'aria-pressed': open + }); + }); + } + }; + +})(jQuery, Drupal); diff --git a/core/misc/details-aria.js b/core/misc/details-aria.js index d341422351..045ee897a7 100644 --- a/core/misc/details-aria.js +++ b/core/misc/details-aria.js @@ -1,19 +1,15 @@ /** - * @file - * Add aria attribute handling for details and summary elements. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/details-aria.es6.js +* @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 +21,4 @@ }); } }; - -})(jQuery, Drupal); +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/misc/dialog/dialog.ajax.es6.js b/core/misc/dialog/dialog.ajax.es6.js new file mode 100644 index 0000000000..3f1b0c2efd --- /dev/null +++ b/core/misc/dialog/dialog.ajax.es6.js @@ -0,0 +1,246 @@ +/** + * @file + * Extends the Drupal AJAX functionality to integrate the dialog API. + */ + +(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) { + 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/. + $('
    ').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) { + 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, + height: 0, + padding: 0, + border: 0, + overflow: 'hidden' + }); + 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. + 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; + } + var $dialog = $(response.selector); + if (!$dialog.length) { + // Create the element if needed. + $dialog = $('
    ').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 { + 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) { + 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) { + var $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', function (e, dialog, $element, settings) { + $element.on('click.dialog', '.dialog-cancel', function (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', function (e, dialog, $element) { + $element.off('.dialog'); + }); + +})(jQuery, Drupal); diff --git a/core/misc/dialog/dialog.ajax.js b/core/misc/dialog/dialog.ajax.js index 3f1b0c2efd..e85e54aee2 100644 --- a/core/misc/dialog/dialog.ajax.js +++ b/core/misc/dialog/dialog.ajax.js @@ -1,74 +1,42 @@ /** - * @file - * Extends the Drupal AJAX functionality to integrate the dialog API. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/dialog/dialog.ajax.es6.js +* @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/. $('
    ').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 +48,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 +62,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 = $('
    ').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 +109,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 +119,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 +127,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); \ No newline at end of file diff --git a/core/misc/dialog/dialog.es6.js b/core/misc/dialog/dialog.es6.js new file mode 100644 index 0000000000..ea1e52c8dc --- /dev/null +++ b/core/misc/dialog/dialog.es6.js @@ -0,0 +1,100 @@ +/** + * @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) { + + '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) { + 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 $element = $(element); + var dialog = { + open: false, + returnValue: undef, + show: function () { + openDialog({modal: false}); + }, + showModal: function () { + 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); diff --git a/core/misc/dialog/dialog.jquery-ui.es6.js b/core/misc/dialog/dialog.jquery-ui.es6.js new file mode 100644 index 0000000000..2526e30953 --- /dev/null +++ b/core/misc/dialog/dialog.jquery-ui.es6.js @@ -0,0 +1,36 @@ +/** + * @file + * Adds default classes to buttons for styling purposes. + */ + +(function ($) { + + 'use strict'; + + $.widget('ui.dialog', $.ui.dialog, { + options: { + buttonClass: 'button', + buttonPrimaryClass: 'button--primary' + }, + _createButtons: function () { + var opts = this.options; + var primaryIndex; + var $buttons; + var index; + var 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); diff --git a/core/misc/dialog/dialog.jquery-ui.js b/core/misc/dialog/dialog.jquery-ui.js index 2526e30953..a123f26e69 100644 --- a/core/misc/dialog/dialog.jquery-ui.js +++ b/core/misc/dialog/dialog.jquery-ui.js @@ -1,7 +1,8 @@ /** - * @file - * Adds default classes to buttons for styling purposes. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/dialog/dialog.jquery-ui.es6.js +* @preserve +**/ (function ($) { @@ -12,7 +13,7 @@ buttonClass: 'button', buttonPrimaryClass: 'button--primary' }, - _createButtons: function () { + _createButtons: function _createButtons() { var opts = this.options; var primaryIndex; var $buttons; @@ -32,5 +33,4 @@ } } }); - -})(jQuery); +})(jQuery); \ No newline at end of file diff --git a/core/misc/dialog/dialog.js b/core/misc/dialog/dialog.js index ea1e52c8dc..ab2ceef804 100644 --- a/core/misc/dialog/dialog.js +++ b/core/misc/dialog/dialog.js @@ -1,85 +1,44 @@ /** - * @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. +* All changes should be applied to ./misc/dialog/dialog.es6.js +* @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 $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 +55,4 @@ return dialog; }; - -})(jQuery, Drupal, drupalSettings); +})(jQuery, Drupal, drupalSettings); \ No newline at end of file diff --git a/core/misc/dialog/dialog.position.es6.js b/core/misc/dialog/dialog.position.es6.js new file mode 100644 index 0000000000..e3c058f844 --- /dev/null +++ b/core/misc/dialog/dialog.position.es6.js @@ -0,0 +1,112 @@ +/** + * @file + * Positioning extensions for dialogs. + */ + +/** + * Triggers when content inside a dialog changes. + * + * @event dialogContentResize + */ + +(function ($, Drupal, drupalSettings, debounce, displace) { + + '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; + 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'; + } + 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) { + var offsets = displace.offsets; + var left = offsets.left - offsets.right; + var top = offsets.top - offsets.bottom; + + var leftString = (left > 0 ? '+' : '-') + Math.abs(Math.round(left / 2)) + 'px'; + var topString = (top > 0 ? '+' : '-') + Math.abs(Math.round(top / 2)) + 'px'; + options.position = { + my: 'center' + (left !== 0 ? leftString : '') + ' center' + (top !== 0 ? topString : ''), + of: window + }; + return options; + } + + $(window).on({ + 'dialog:aftercreate': function (event, dialog, $element, settings) { + var autoResize = debounce(resetSize, 20); + 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'); + $(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); diff --git a/core/misc/dialog/dialog.position.js b/core/misc/dialog/dialog.position.js index e3c058f844..84ef175455 100644 --- a/core/misc/dialog/dialog.position.js +++ b/core/misc/dialog/dialog.position.js @@ -1,36 +1,15 @@ /** - * @file - * Positioning extensions for dialogs. - */ - -/** - * Triggers when content inside a dialog changes. - * - * @event dialogContentResize - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/dialog/dialog.position.es6.js +* @preserve +**/ (function ($, Drupal, drupalSettings, debounce, displace) { 'use strict'; - // autoResize option will turn off resizable and draggable. - drupalSettings.dialog = $.extend({autoResize: true, maxHeight: '95%'}, drupalSettings.dialog); + 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 = {}; @@ -42,12 +21,10 @@ 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 +32,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 +54,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); \ No newline at end of file diff --git a/core/misc/displace.es6.js b/core/misc/displace.es6.js new file mode 100644 index 0000000000..3e89c563ec --- /dev/null +++ b/core/misc/displace.es6.js @@ -0,0 +1,222 @@ +/** + * @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) { + + 'use strict'; + + /** + * @name Drupal.displace.offsets + * + * @type {Drupal~displaceOffset} + */ + var offsets = { + top: 0, + right: 0, + bottom: 0, + 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. + 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) { + 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 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: offsets, + + /** + * Expose method to compute a single edge offsets. + * + * @ignore + */ + calculateOffset: calculateOffset + }); + +})(jQuery, Drupal, Drupal.debounce); diff --git a/core/misc/displace.js b/core/misc/displace.js index 3e89c563ec..14f6d1134d 100644 --- a/core/misc/displace.js +++ b/core/misc/displace.js @@ -1,38 +1,13 @@ /** - * @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. +* All changes should be applied to ./misc/displace.es6.js +* @preserve +**/ (function ($, Drupal, debounce) { 'use strict'; - /** - * @name Drupal.displace.offsets - * - * @type {Drupal~displaceOffset} - */ var offsets = { top: 0, right: 0, @@ -40,14 +15,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 +26,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 +34,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 +43,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 +99,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); \ No newline at end of file diff --git a/core/misc/dropbutton/dropbutton.es6.js b/core/misc/dropbutton/dropbutton.es6.js new file mode 100644 index 0000000000..04f9491ad7 --- /dev/null +++ b/core/misc/dropbutton/dropbutton.es6.js @@ -0,0 +1,233 @@ +/** + * @file + * Dropbutton feature. + */ + +(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) { + 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)); + } + } + } + }; + + /** + * 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 $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), + + /** + * 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.} + */ + 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) { + 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. + if (this.timerID) { + window.clearTimeout(this.timerID); + } + }, + + /** + * @method + */ + hoverOut: function () { + // Wait half a second before closing. + this.timerID = window.setTimeout($.proxy(this, 'close'), 500); + }, + + /** + * @method + */ + open: function () { + this.toggle(true); + }, + + /** + * @method + */ + close: function () { + this.toggle(false); + }, + + /** + * @param {jQuery.Event} e + * The event triggered. + */ + focusOut: function (e) { + this.hoverOut.call(this, e); + }, + + /** + * @param {jQuery.Event} e + * The event triggered. + */ + focusIn: function (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) { + return '
  • '; + } + }); + + // Expose constructor in the public space. + Drupal.DropButton = DropButton; + +})(jQuery, Drupal); diff --git a/core/misc/dropbutton/dropbutton.js b/core/misc/dropbutton/dropbutton.js index 04f9491ad7..c90445f9b3 100644 --- a/core/misc/dropbutton/dropbutton.js +++ b/core/misc/dropbutton/dropbutton.js @@ -1,30 +1,22 @@ /** - * @file - * Dropbutton feature. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/dropbutton/dropbutton.es6.js +* @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 +25,86 @@ } }; - /** - * 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), - - /** - * 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-multiple').on({ + 'mouseleave.dropbutton': $.proxy(this.hoverOut, this), + + 'mouseenter.dropbutton': $.proxy(this.hoverIn, this), + + 'focusout.dropbutton': $.proxy(this.focusOut, this), + + '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.} - */ + $.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 '
  • '; } }); - // Expose constructor in the public space. Drupal.DropButton = DropButton; - -})(jQuery, Drupal); +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/misc/drupal.es6.js b/core/misc/drupal.es6.js new file mode 100644 index 0000000000..d509795b86 --- /dev/null +++ b/core/misc/drupal.es6.js @@ -0,0 +1,583 @@ +/** + * @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) { + + '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); + }; + + /** + * 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.} + */ + + /** + * 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) { + 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) { + 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, '>'); + 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; + } + } + } + + 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) { + if (args.hasOwnProperty(k)) { + keys.push(k); + } + } + + // Order the keys by the character length. The shortest one is the first. + 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)); + } + } + + 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.} [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) { + 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. + } + + 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. + } + 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; + + var pluralDelimiter = drupalSettings.pluralDelimiter; + 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 = 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 + *
    + * 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) { + 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 '' + Drupal.checkPlain(str) + ''; + }; + +})(Drupal, window.drupalSettings, window.drupalTranslations); diff --git a/core/misc/drupal.init.es6.js b/core/misc/drupal.init.es6.js new file mode 100644 index 0000000000..0e55e190ef --- /dev/null +++ b/core/misc/drupal.init.es6.js @@ -0,0 +1,19 @@ +// 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) { + + 'use strict'; + + // Attach all behaviors. + domready(function () { Drupal.attachBehaviors(document, drupalSettings); }); + +})(domready, Drupal, window.drupalSettings); diff --git a/core/misc/drupal.init.js b/core/misc/drupal.init.js index 0e55e190ef..597dacc76f 100644 --- a/core/misc/drupal.init.js +++ b/core/misc/drupal.init.js @@ -1,19 +1,20 @@ -// Allow other JavaScript libraries to use $. +/** +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/drupal.init.es6.js +* @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); \ No newline at end of file diff --git a/core/misc/drupal.js b/core/misc/drupal.js index d509795b86..3b3131ed73 100644 --- a/core/misc/drupal.js +++ b/core/misc/drupal.js @@ -1,289 +1,73 @@ /** - * @file - * Defines the Drupal JavaScript API. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/drupal.es6.js +* @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. - */ - -/** - * 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 - */ +window.Drupal = { behaviors: {}, locale: {} }; -/** - * 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.} - */ - - /** - * 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, '>'); + str = str.toString().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 +78,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 +91,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 +112,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.} [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 +126,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 +169,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) { + } 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 - * - * 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 +189,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 '' + Drupal.checkPlain(str) + ''; }; - -})(Drupal, window.drupalSettings, window.drupalTranslations); +})(Drupal, window.drupalSettings, window.drupalTranslations); \ No newline at end of file diff --git a/core/misc/drupalSettingsLoader.es6.js b/core/misc/drupalSettingsLoader.es6.js new file mode 100644 index 0000000000..7ff292efbf --- /dev/null +++ b/core/misc/drupalSettingsLoader.es6.js @@ -0,0 +1,25 @@ +/** + * @file + * Parse inline JSON and initialize the drupalSettings global object. + */ + +(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); + } +})(); diff --git a/core/misc/drupalSettingsLoader.js b/core/misc/drupalSettingsLoader.js index 7ff292efbf..1e4fea9041 100644 --- a/core/misc/drupalSettingsLoader.js +++ b/core/misc/drupalSettingsLoader.js @@ -1,25 +1,18 @@ /** - * @file - * Parse inline JSON and initialize the drupalSettings global object. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/drupalSettingsLoader.es6.js +* @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); } -})(); +})(); \ No newline at end of file diff --git a/core/misc/entity-form.es6.js b/core/misc/entity-form.es6.js new file mode 100644 index 0000000000..87253c9022 --- /dev/null +++ b/core/misc/entity-form.es6.js @@ -0,0 +1,57 @@ +/** + * @file + * Defines Javascript behaviors for the block_content module. + */ + +(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) { + 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)) { + return Drupal.t('New revision'); + } + + return Drupal.t('No revision'); + }); + + $context.find('details.entity-translation-options').drupalSetSummary(function (context) { + var $translationContext = $(context); + var translate; + 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 { + $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); diff --git a/core/misc/entity-form.js b/core/misc/entity-form.js index 87253c9022..732dbf345e 100644 --- a/core/misc/entity-form.js +++ b/core/misc/entity-form.js @@ -1,35 +1,21 @@ /** - * @file - * Defines Javascript behaviors for the block_content module. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/entity-form.es6.js +* @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'); } @@ -43,8 +29,7 @@ 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 +38,4 @@ }); } }; - -})(jQuery, Drupal); +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/misc/form.es6.js b/core/misc/form.es6.js new file mode 100644 index 0000000000..7ca64fc425 --- /dev/null +++ b/core/misc/form.es6.js @@ -0,0 +1,250 @@ +/** + * @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 + */ + +(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])) : ''; + }; + + /** + * 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; }; + } + + 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'); + }; + + /** + * 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 () { + 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 { + $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) { + 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) { + var $context = $(context); + var contextIsForm = $context.is('form'); + var $forms = (contextIsForm ? $context : $context.find('form')).once('form-updated'); + var 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(function (form) { + var events = 'change.formUpdated input.formUpdated '; + 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) { + var $context = $(context); + var contextIsForm = $context.is('form'); + if (trigger === 'unload') { + var $forms = (contextIsForm ? $context : $context.find('form')).removeOnce('form-updated'); + if ($forms.length) { + $.makeArray($forms).forEach(function (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: function (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())); + if ($element.length && emptyOrDefault && browserData) { + $element.val(browserData); + } + }); + } + $forms.on('submit', function () { + userInfo.map(function (info) { + var $element = $forms.find('[name=' + info + ']'); + if ($element.length) { + localStorage.setItem('Drupal.visitor.' + info, $element.val()); + } + }); + }); + } + }; + +})(jQuery, Drupal, Drupal.debounce); diff --git a/core/misc/form.js b/core/misc/form.js index 7ca64fc425..2d046c67b3 100644 --- a/core/misc/form.js +++ b/core/misc/form.js @@ -1,205 +1,93 @@ /** - * @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. +* All changes should be applied to ./misc/form.es6.js +* @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; 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 +102,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); } @@ -246,5 +126,4 @@ }); } }; - -})(jQuery, Drupal, Drupal.debounce); +})(jQuery, Drupal, Drupal.debounce); \ No newline at end of file diff --git a/core/misc/machine-name.es6.js b/core/misc/machine-name.es6.js new file mode 100644 index 0000000000..e76292e265 --- /dev/null +++ b/core/misc/machine-name.es6.js @@ -0,0 +1,211 @@ +/** + * @file + * Machine name functionality. + */ + +(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) { + var self = this; + var $context = $(context); + var timeout = null; + var xhr = null; + + function clickEditHandler(e) { + var data = e.data; + data.$wrapper.removeClass('visually-hidden'); + data.$target.trigger('focus'); + data.$suffix.hide(); + data.$source.off('.machineName'); + } + + function machineNameHandler(e) { + var data = e.data; + var options = data.options; + var baseValue = $(e.target).val(); + + 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; + } + if (baseValue.toLowerCase() !== expected) { + timeout = setTimeout(function () { + xhr = self.transliterate(baseValue, options).done(function (machine) { + self.showMachineName(machine.substr(0, options.maxlength), data); + }); + }, 300); + } + else { + self.showMachineName(expected, data); + } + } + + Object.keys(settings.machineName).forEach(function (source_id) { + var machine = ''; + var eventData; + 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() !== '') { + machine = self.transliterate($source.val(), options); + } + // Append the machine name preview to the source field. + var $preview = $('' + options.field_prefix + Drupal.checkPlain(machine) + options.field_suffix + ''); + $suffix.empty(); + if (options.label) { + $suffix.append('' + options.label + ': '); + } + $suffix.append($preview); + + // If the machine name cannot be edited, stop further processing. + if ($target.is(':disabled')) { + return; + } + + eventData = { + $source: $source, + $target: $target, + $suffix: $suffix, + $wrapper: $wrapper, + $preview: $preview, + options: options + }; + // If it is editable, append an edit link. + var $link = $('').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: function (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 { + 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) { + 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); diff --git a/core/misc/machine-name.js b/core/misc/machine-name.js index e76292e265..0942add173 100644 --- a/core/misc/machine-name.js +++ b/core/misc/machine-name.js @@ -1,48 +1,15 @@ /** - * @file - * Machine name functionality. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/machine-name.es6.js +* @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 +31,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,8 +46,7 @@ self.showMachineName(machine.substr(0, options.maxlength), data); }); }, 300); - } - else { + } else { self.showMachineName(expected, data); } } @@ -98,28 +60,25 @@ 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 = $('' + options.field_prefix + Drupal.checkPlain(machine) + options.field_suffix + ''); $suffix.empty(); if (options.label) { @@ -127,7 +86,6 @@ } $suffix.append($preview); - // If the machine name cannot be edited, stop further processing. if ($target.is(':disabled')) { return; } @@ -140,63 +98,35 @@ $preview: $preview, options: options }; - // If it is editable, append an edit link. + var $link = $('').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 +137,4 @@ }); } }; - -})(jQuery, Drupal, drupalSettings); +})(jQuery, Drupal, drupalSettings); \ No newline at end of file diff --git a/core/misc/progress.es6.js b/core/misc/progress.es6.js new file mode 100644 index 0000000000..a6694892b2 --- /dev/null +++ b/core/misc/progress.es6.js @@ -0,0 +1,169 @@ +/** + * @file + * Progress bar. + */ + +(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 '
    ' + + '
     
    ' + + '
    ' + + '
    ' + + '
     
    ' + + '
    '; + }; + + /** + * 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) { + 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: function (uri, delay) { + this.delay = delay; + this.uri = uri; + this.sendPing(); + }, + + /** + * Stop monitoring progress via Ajax. + */ + stopMonitoring: function () { + clearTimeout(this.timer); + // This allows monitoring to be stopped from within the callback. + this.uri = null; + }, + + /** + * Request progress data from server. + */ + sendPing: function () { + 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 { + uri += '&'; + } + uri += '_format=json'; + $.ajax({ + type: this.method, + url: uri, + data: '', + dataType: 'json', + success: function (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(function () { pb.sendPing(); }, pb.delay); + }, + error: function (xmlhttp) { + var e = new Drupal.AjaxError(xmlhttp, pb.uri); + pb.displayError('
    ' + e.message + '
    '); + } + }); + } + }, + + /** + * Display errors on the page. + * + * @param {string} string + * The error message to show the user. + */ + displayError: function (string) { + var error = $('
    ').html(string); + $(this.element).before(error).hide(); + + if (this.errorCallback) { + this.errorCallback(this); + } + } + }); + +})(jQuery, Drupal); diff --git a/core/misc/progress.js b/core/misc/progress.js index a6694892b2..17ef7ada49 100644 --- a/core/misc/progress.js +++ b/core/misc/progress.js @@ -1,78 +1,28 @@ /** - * @file - * Progress bar. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/progress.es6.js +* @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 '
    ' + - '
     
    ' + - '
    ' + - '
    ' + - '
     
    ' + - '
    '; + return '
    ' + '
     
    ' + '
    ' + '
    ' + '
     
    ' + '
    '; }; - /** - * 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 + '%'); @@ -84,45 +34,29 @@ } }, - /** - * 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,18 +65,19 @@ 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('
    ' + e.message + '
    '); } @@ -150,13 +85,7 @@ } }, - /** - * Display errors on the page. - * - * @param {string} string - * The error message to show the user. - */ - displayError: function (string) { + displayError: function displayError(string) { var error = $('
    ').html(string); $(this.element).before(error).hide(); @@ -165,5 +94,4 @@ } } }); - -})(jQuery, Drupal); +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/misc/states.es6.js b/core/misc/states.es6.js new file mode 100644 index 0000000000..24374b625f --- /dev/null +++ b/core/misc/states.es6.js @@ -0,0 +1,724 @@ +/** + * @file + * Drupal's states library. + */ + +(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) { + var $states = $(context).find('[data-drupal-states]'); + var config; + var state; + var il = $states.length; + for (var 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 (var 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: function (reference, value) { + return reference.test(value); + }, + Function: function (reference, value) { + // The "reference" variable is a comparison function. + 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); + } + }; + + 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; + 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); + + // Make sure the event we just bound ourselves to is actually fired. + 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) { + 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. + 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. + 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}); + } + }, + + /** + * 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; + 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; } + } + } + } + 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])) { + 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); + } + 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 () { + 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(); + } + } + }; + + states.Trigger.prototype = { + + /** + * @memberof Drupal.states.Trigger# + */ + initialize: function () { + var trigger = states.Trigger.states[this.state]; + + if (typeof trigger === 'function') { + // We have a custom trigger initialization function. + trigger.call(window, this.element); + } + else { + for (var 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: function (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}); + 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: function () { + // The function associated with that trigger returns the new value for + // the state. + 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. + 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. + 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. + 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: function (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 { + 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); + } + }; + + /** + * 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: function () { + 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 + } + }); + + $document.on('state:required', function (e) { + 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. + 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', function (e) { + if (e.trigger) { + $(e.target).closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggle(e.value); + } + }); + + $document.on('state:checked', function (e) { + if (e.trigger) { + $(e.target).prop('checked', e.value); + } + }); + + $document.on('state:collapsed', function (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; + } + else { + 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; + } + else { + return typeof a === 'undefined' || typeof b === 'undefined'; + } + } + +})(jQuery, Drupal); diff --git a/core/misc/states.js b/core/misc/states.js index 24374b625f..52ccbab32d 100644 --- a/core/misc/states.js +++ b/core/misc/states.js @@ -1,38 +1,19 @@ /** - * @file - * Drupal's states library. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/states.es6.js +* @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; @@ -50,31 +31,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,49 +48,20 @@ } }; - /** - * 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) { + initializeDependee: function initializeDependee(selector, dependeeStates) { var state; var self = this; @@ -134,245 +69,122 @@ 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); + } else { + return _compare2(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. + 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) { + verifyConstraints: function verifyConstraints(constraints, selector) { var result; 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. + } else { 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 () { + 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 +192,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 +205,55 @@ } } - // 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 +261,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 { + } else { 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 +309,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 +327,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 +358,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 { + } else { 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 { + } else { return typeof a === 'undefined' || typeof b === 'undefined'; } } - -})(jQuery, Drupal); +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/misc/tabbingmanager.es6.js b/core/misc/tabbingmanager.es6.js new file mode 100644 index 0000000000..134523f5bf --- /dev/null +++ b/core/misc/tabbingmanager.es6.js @@ -0,0 +1,369 @@ +/** + * @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) { + + '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.} + */ + 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. + 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. + 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) { + 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. + 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. + 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) { + var $set = tabbingContext.$disabledElements; + var level = tabbingContext.level; + var il = $set.length; + for (var 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: function ($el, level) { + var 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: function ($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'); + } + 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. + var 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: function () { + 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. + 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 () { + 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)); diff --git a/core/misc/tabbingmanager.js b/core/misc/tabbingmanager.js index 134523f5bf..d937f1308f 100644 --- a/core/misc/tabbingmanager.js +++ b/core/misc/tabbingmanager.js @@ -1,184 +1,77 @@ /** - * @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. +* All changes should be applied to ./misc/tabbingmanager.es6.js +* @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.} - */ 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; @@ -187,15 +80,7 @@ } }, - /** - * 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'), @@ -204,37 +89,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 +116,54 @@ } }); - /** - * 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); \ No newline at end of file diff --git a/core/misc/tabledrag.es6.js b/core/misc/tabledrag.es6.js new file mode 100644 index 0000000000..75468e60dd --- /dev/null +++ b/core/misc/tabledrag.es6.js @@ -0,0 +1,1557 @@ +/** + * @file + * Provide dragging capabilities to admin uis. + */ + +/** + * Triggers when weights columns are toggled. + * + * @event columnschange + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Store the state of weight columns display for all tables. + * + * Default value is to hide weight columns. + */ + var showWeight = JSON.parse(localStorage.getItem('Drupal.tableDrag.showWeight')); + + /** + * Drag and drop table rows with field manipulation. + * + * Using the drupal_attach_tabledrag() function, any table with weights or + * parent relationships may be made into draggable tables. Columns containing + * a field may optionally be hidden, providing a better user experience. + * + * Created tableDrag instances may be modified with custom behaviors by + * overriding the .onDrag, .onDrop, .row.onSwap, and .row.onIndent methods. + * See blocks.js for an example of adding additional functionality to + * tableDrag. + * + * @type {Drupal~behavior} + */ + Drupal.behaviors.tableDrag = { + attach: function (context, settings) { + function initTableDrag(table, base) { + if (table.length) { + // Create the new tableDrag instance. Save in the Drupal variable + // to allow other scripts access to the object. + Drupal.tableDrag[base] = new Drupal.tableDrag(table[0], settings.tableDrag[base]); + } + } + + for (var base in settings.tableDrag) { + if (settings.tableDrag.hasOwnProperty(base)) { + initTableDrag($(context).find('#' + base).once('tabledrag'), base); + } + } + } + }; + + /** + * 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; + + /** + * Check this table's settings for parent relationships. + * + * 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 = $('').addClass('draggable').appendTo(table); + var testCell = $('').addClass('draggable').appendTo(table); var testCell = $('
    To retrieve the HTML for text that should be emphasized and + * displayed as a placeholder inside a sentence.To retrieve the HTML for text that should be emphasized and - * displayed as a placeholder inside a sentence.
    ').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. + $table.before($('') + .attr('title', Drupal.t('Re-order rows by numerical weight instead of dragging.')) + .on('click', $.proxy(function (e) { + e.preventDefault(); + this.toggleColumns(); + }, this)) + .wrap('
    ') + .parent() + ); + + // 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)); + }; + + /** + * Initialize columns containing form elements to be hidden by default. + * + * Identify and mark each cell with a CSS class so we can easily toggle + * show/hide it. Finally, hide columns if user does not have a + * 'Drupal.tableDrag.showWeight' localStorage value. + */ + Drupal.tableDrag.prototype.initColumns = function () { + var $table = this.$table; + var hidden; + var cell; + var columnIndex; + for (var group in this.tableSettings) { + if (this.tableSettings.hasOwnProperty(group)) { + + // Find the first field in this group. + for (var d in this.tableSettings[group]) { + if (this.tableSettings[group].hasOwnProperty(d)) { + var field = $table.find('.' + this.tableSettings[group][d].target).eq(0); + if (field.length && this.tableSettings[group][d].hidden) { + hidden = this.tableSettings[group][d].hidden; + cell = field.closest('td'); + break; + } + } + } + + // Mark the column containing this field so it can be hidden. + if (hidden && cell[0]) { + // Add 1 to our indexes. The nth-child selector is 1 based, not 0 + // based. Match immediate children of the parent element to allow + // nesting. + columnIndex = cell.parent().find('> td').index(cell.get(0)) + 1; + $table.find('> thead > tr, > tbody > tr, > tr').each(this.addColspanClass(columnIndex)); + } + } + } + this.displayColumns(showWeight); + }; + + /** + * Mark cells that have colspan. + * + * In order to adjust the colspan instead of hiding them altogether. + * + * @param {number} columnIndex + * The column index to add colspan class to. + * + * @return {function} + * Function to add colspan class. + */ + Drupal.tableDrag.prototype.addColspanClass = function (columnIndex) { + return function () { + // Get the columnIndex and adjust for any colspans in this row. + var $row = $(this); + var index = columnIndex; + var cells = $row.children(); + var cell; + cells.each(function (n) { + if (n < index && this.colSpan && this.colSpan > 1) { + index -= this.colSpan - 1; + } + }); + if (index > 0) { + cell = cells.filter(':nth-child(' + index + ')'); + if (cell[0].colSpan && cell[0].colSpan > 1) { + // If this cell has a colspan, mark it so we can reduce the colspan. + cell.addClass('tabledrag-has-colspan'); + } + else { + // Mark this cell so we can hide it. + cell.addClass('tabledrag-hide'); + } + } + }; + }; + + /** + * Hide or display weight columns. Triggers an event on change. + * + * @fires event:columnschange + * + * @param {bool} displayWeight + * 'true' will show weight columns. + */ + Drupal.tableDrag.prototype.displayColumns = function (displayWeight) { + if (displayWeight) { + this.showColumns(); + } + // Default action is to hide columns. + else { + this.hideColumns(); + } + // Trigger an event to allow other scripts to react to this display change. + // Force the extra parameter as a bool. + $('table').findOnce('tabledrag').trigger('columnschange', !!displayWeight); + }; + + /** + * Toggle the weight column depending on 'showWeight' value. + * + * Store only default override. + */ + Drupal.tableDrag.prototype.toggleColumns = function () { + showWeight = !showWeight; + this.displayColumns(showWeight); + if (showWeight) { + // Save default override. + localStorage.setItem('Drupal.tableDrag.showWeight', showWeight); + } + else { + // Reset the value to its default. + localStorage.removeItem('Drupal.tableDrag.showWeight'); + } + }; + + /** + * Hide the columns containing weight/parent form elements. + * + * Undo showColumns(). + */ + Drupal.tableDrag.prototype.hideColumns = function () { + var $tables = $('table').findOnce('tabledrag'); + // Hide weight/parent cells and headers. + $tables.find('.tabledrag-hide').css('display', 'none'); + // Show TableDrag handles. + $tables.find('.tabledrag-handle').css('display', ''); + // Reduce the colspan of any effected multi-span columns. + $tables.find('.tabledrag-has-colspan').each(function () { + this.colSpan = this.colSpan - 1; + }); + // Change link text. + $('.tabledrag-toggle-weight').text(Drupal.t('Show row weights')); + }; + + /** + * Show the columns containing weight/parent form elements. + * + * Undo hideColumns(). + */ + Drupal.tableDrag.prototype.showColumns = function () { + var $tables = $('table').findOnce('tabledrag'); + // Show weight/parent cells and headers. + $tables.find('.tabledrag-hide').css('display', ''); + // Hide TableDrag handles. + $tables.find('.tabledrag-handle').css('display', 'none'); + // Increase the colspan for any columns where it was previously reduced. + $tables.find('.tabledrag-has-colspan').each(function () { + this.colSpan = this.colSpan + 1; + }); + // Change link text. + $('.tabledrag-toggle-weight').text(Drupal.t('Hide row weights')); + }; + + /** + * Find the target used within a particular row and group. + * + * @param {string} group + * Group selector. + * @param {HTMLElement} row + * The row HTML element. + * + * @return {object} + * The table row settings. + */ + Drupal.tableDrag.prototype.rowSettings = function (group, row) { + var field = $(row).find('.' + group); + var tableSettingsGroup = this.tableSettings[group]; + for (var delta in tableSettingsGroup) { + if (tableSettingsGroup.hasOwnProperty(delta)) { + var targetClass = tableSettingsGroup[delta].target; + if (field.is('.' + targetClass)) { + // Return a copy of the row settings. + var rowSettings = {}; + for (var n in tableSettingsGroup[delta]) { + if (tableSettingsGroup[delta].hasOwnProperty(n)) { + rowSettings[n] = tableSettingsGroup[delta][n]; + } + } + return rowSettings; + } + } + } + }; + + /** + * Take an item and add event handlers to make it become draggable. + * + * @param {HTMLElement} item + * The item to add event handlers to. + */ + 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 = $('
     
    ').attr('title', Drupal.t('Drag to re-order')); + // 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); + } + + 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; + + /* eslint-disable no-fallthrough */ + + 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; + } + + /* eslint-enable no-fallthrough */ + + 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) { + + /* eslint-disable no-fallthrough */ + + switch (event.keyCode) { + // Left arrow. + case 37: + // Up arrow. + case 38: + // Right arrow. + case 39: + // Down arrow. + case 40: + return false; + } + + /* eslint-enable no-fallthrough */ + + }); + }; + + /** + * Pointer event initiator, creates drag object and information. + * + * @param {jQuery.Event} event + * The event object that trigger the drag. + * @param {Drupal.tableDrag} self + * The drag handle. + * @param {HTMLElement} item + * The item that that is being dragged. + */ + Drupal.tableDrag.prototype.dragStart = function (event, self, item) { + // Create a new dragObject recording the pointer information. + self.dragObject = {}; + self.dragObject.initOffset = self.getPointerOffset(item, event); + self.dragObject.initPointerCoords = self.pointerCoords(event); + if (self.indentEnabled) { + self.dragObject.indentPointerPos = self.dragObject.initPointerCoords; + } + + // If there's a lingering row object from the keyboard, remove its focus. + if (self.rowObject) { + $(self.rowObject.element).find('a.tabledrag-handle').trigger('blur'); + } + + // Create a new rowObject for manipulation of this row. + self.rowObject = new self.row(item, 'pointer', self.indentEnabled, self.maxDepth, true); + + // Save the position of the table. + self.table.topY = $(self.table).offset().top; + self.table.bottomY = self.table.topY + self.table.offsetHeight; + + // Add classes to the handle and row. + $(item).addClass('drag'); + + // Set the document to use the move cursor during drag. + $('body').addClass('drag'); + if (self.oldRowElement) { + $(self.oldRowElement).removeClass('drag-previous'); + } + }; + + /** + * Pointer movement handler, bound to document. + * + * @param {jQuery.Event} event + * The pointer event. + * @param {Drupal.tableDrag} self + * The tableDrag instance. + * + * @return {bool|undefined} + * Undefined if no dragObject is defined, false otherwise. + */ + Drupal.tableDrag.prototype.dragRow = function (event, self) { + if (self.dragObject) { + self.currentPointerCoords = self.pointerCoords(event); + var y = self.currentPointerCoords.y - self.dragObject.initOffset.y; + var x = self.currentPointerCoords.x - self.dragObject.initOffset.x; + + // Check for row swapping and vertical scrolling. + if (y !== self.oldY) { + self.rowObject.direction = y > self.oldY ? 'down' : 'up'; + // Update the old value. + self.oldY = y; + // Check if the window should be scrolled (and how fast). + var scrollAmount = self.checkScroll(self.currentPointerCoords.y); + // Stop any current scrolling. + clearInterval(self.scrollInterval); + // Continue scrolling if the mouse has moved in the scroll direction. + if (scrollAmount > 0 && self.rowObject.direction === 'down' || scrollAmount < 0 && self.rowObject.direction === 'up') { + self.setScroll(scrollAmount); + } + + // If we have a valid target, perform the swap and restripe the table. + var currentRow = self.findDropTargetRow(x, y); + if (currentRow) { + if (self.rowObject.direction === 'down') { + self.rowObject.swap('after', currentRow, self); + } + else { + self.rowObject.swap('before', currentRow, self); + } + if (self.striping === true) { + self.restripeTable(); + } + } + } + + // Similar to row swapping, handle indentations. + if (self.indentEnabled) { + var xDiff = self.currentPointerCoords.x - self.dragObject.indentPointerPos.x; + // Set the number of indentations the pointer has been moved left or + // right. + var indentDiff = Math.round(xDiff / self.indentAmount); + // Indent the row with our estimated diff, which may be further + // restricted according to the rows around this row. + var indentChange = self.rowObject.indent(indentDiff); + // Update table and pointer indentations. + self.dragObject.indentPointerPos.x += self.indentAmount * indentChange * self.rtl; + self.indentCount = Math.max(self.indentCount, self.rowObject.indents); + } + + return false; + } + }; + + /** + * Pointerup behavior. + * + * @param {jQuery.Event} event + * The pointer event. + * @param {Drupal.tableDrag} self + * The tableDrag instance. + */ + Drupal.tableDrag.prototype.dropRow = function (event, self) { + var droppedRow; + var $droppedRow; + + // Drop row functionality. + if (self.rowObject !== null) { + droppedRow = self.rowObject.element; + $droppedRow = $(droppedRow); + // The row is already in the right place so we just release it. + if (self.rowObject.changed === true) { + // Update the fields in the dropped row. + self.updateFields(droppedRow); + + // If a setting exists for affecting the entire group, update all the + // fields in the entire dragged group. + for (var group in self.tableSettings) { + if (self.tableSettings.hasOwnProperty(group)) { + var rowSettings = self.rowSettings(group, droppedRow); + if (rowSettings.relationship === 'group') { + for (var n in self.rowObject.children) { + if (self.rowObject.children.hasOwnProperty(n)) { + self.updateField(self.rowObject.children[n], group); + } + } + } + } + } + + self.rowObject.markChanged(); + if (self.changed === false) { + $(Drupal.theme('tableDragChangedWarning')).insertBefore(self.table).hide().fadeIn('slow'); + self.changed = true; + } + } + + if (self.indentEnabled) { + self.rowObject.removeIndentClasses(); + } + if (self.oldRowElement) { + $(self.oldRowElement).removeClass('drag-previous'); + } + $droppedRow.removeClass('drag').addClass('drag-previous'); + self.oldRowElement = droppedRow; + self.onDrop(); + self.rowObject = null; + } + + // Functionality specific only to pointerup events. + if (self.dragObject !== null) { + self.dragObject = null; + $('body').removeClass('drag'); + clearInterval(self.scrollInterval); + } + }; + + /** + * Get the coordinates from the event (allowing for browser differences). + * + * @param {jQuery.Event} event + * The pointer event. + * + * @return {object} + * An object with `x` and `y` keys indicating the position. + */ + Drupal.tableDrag.prototype.pointerCoords = function (event) { + if (event.pageX || event.pageY) { + return {x: event.pageX, y: event.pageY}; + } + return { + x: event.clientX + document.body.scrollLeft - document.body.clientLeft, + y: event.clientY + document.body.scrollTop - document.body.clientTop + }; + }; + + /** + * Get the event offset from the target element. + * + * Given a target element and a pointer event, get the event offset from that + * element. To do this we need the element's position and the target position. + * + * @param {HTMLElement} target + * The target HTML element. + * @param {jQuery.Event} event + * The pointer event. + * + * @return {object} + * An object with `x` and `y` keys indicating the position. + */ + Drupal.tableDrag.prototype.getPointerOffset = function (target, event) { + var docPos = $(target).offset(); + var pointerPos = this.pointerCoords(event); + return {x: pointerPos.x - docPos.left, y: pointerPos.y - docPos.top}; + }; + + /** + * Find the row the mouse is currently over. + * + * This row is then taken and swapped with the one being dragged. + * + * @param {number} x + * The x coordinate of the mouse on the page (not the screen). + * @param {number} y + * The y coordinate of the mouse on the page (not the screen). + * + * @return {*} + * The drop target row, if found. + */ + Drupal.tableDrag.prototype.findDropTargetRow = function (x, y) { + var rows = $(this.table.tBodies[0].rows).not(':hidden'); + for (var n = 0; n < rows.length; n++) { + var row = rows[n]; + var $row = $(row); + var rowY = $row.offset().top; + var rowHeight; + // Because Safari does not report offsetHeight on table rows, but does on + // table cells, grab the firstChild of the row and use that instead. + // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari. + if (row.offsetHeight === 0) { + rowHeight = parseInt(row.firstChild.offsetHeight, 10) / 2; + } + // Other browsers. + else { + rowHeight = parseInt(row.offsetHeight, 10) / 2; + } + + // Because we always insert before, we need to offset the height a bit. + if ((y > (rowY - rowHeight)) && (y < (rowY + rowHeight))) { + if (this.indentEnabled) { + // Check that this row is not a child of the row being dragged. + for (n in this.rowObject.group) { + if (this.rowObject.group[n] === row) { + return null; + } + } + } + else { + // Do not allow a row to be swapped with itself. + if (row === this.rowObject.element) { + return null; + } + } + + // Check that swapping with this row is allowed. + if (!this.rowObject.isValidSwap(row)) { + return null; + } + + // We may have found the row the mouse just passed over, but it doesn't + // take into account hidden rows. Skip backwards until we find a + // draggable row. + while ($row.is(':hidden') && $row.prev('tr').is(':hidden')) { + $row = $row.prev('tr:first-of-type'); + row = $row.get(0); + } + return row; + } + } + return null; + }; + + /** + * After the row is dropped, update the table fields. + * + * @param {HTMLElement} changedRow + * DOM object for the row that was just dropped. + */ + Drupal.tableDrag.prototype.updateFields = function (changedRow) { + for (var group in this.tableSettings) { + if (this.tableSettings.hasOwnProperty(group)) { + // Each group may have a different setting for relationship, so we find + // the source rows for each separately. + this.updateField(changedRow, group); + } + } + }; + + /** + * After the row is dropped, update a single table field. + * + * @param {HTMLElement} changedRow + * DOM object for the row that was just dropped. + * @param {string} group + * The settings group on which field updates will occur. + */ + Drupal.tableDrag.prototype.updateField = function (changedRow, group) { + var rowSettings = this.rowSettings(group, changedRow); + var $changedRow = $(changedRow); + var sourceRow; + var $previousRow; + var previousRow; + var useSibling; + // Set the row as its own target. + if (rowSettings.relationship === 'self' || rowSettings.relationship === 'group') { + sourceRow = changedRow; + } + // Siblings are easy, check previous and next rows. + else if (rowSettings.relationship === 'sibling') { + $previousRow = $changedRow.prev('tr:first-of-type'); + previousRow = $previousRow.get(0); + var $nextRow = $changedRow.next('tr:first-of-type'); + var nextRow = $nextRow.get(0); + sourceRow = changedRow; + if ($previousRow.is('.draggable') && $previousRow.find('.' + group).length) { + if (this.indentEnabled) { + if ($previousRow.find('.js-indentations').length === $changedRow.find('.js-indentations').length) { + sourceRow = previousRow; + } + } + else { + sourceRow = previousRow; + } + } + else if ($nextRow.is('.draggable') && $nextRow.find('.' + group).length) { + if (this.indentEnabled) { + if ($nextRow.find('.js-indentations').length === $changedRow.find('.js-indentations').length) { + sourceRow = nextRow; + } + } + else { + sourceRow = nextRow; + } + } + } + // Parents, look up the tree until we find a field not in this group. + // Go up as many parents as indentations in the changed row. + else if (rowSettings.relationship === 'parent') { + $previousRow = $changedRow.prev('tr'); + previousRow = $previousRow; + while ($previousRow.length && $previousRow.find('.js-indentation').length >= this.rowObject.indents) { + $previousRow = $previousRow.prev('tr'); + previousRow = $previousRow; + } + // If we found a row. + if ($previousRow.length) { + sourceRow = $previousRow.get(0); + } + // Otherwise we went all the way to the left of the table without finding + // a parent, meaning this item has been placed at the root level. + else { + // Use the first row in the table as source, because it's guaranteed to + // be at the root level. Find the first item, then compare this row + // against it as a sibling. + sourceRow = $(this.table).find('tr.draggable:first-of-type').get(0); + if (sourceRow === this.rowObject.element) { + sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0); + } + useSibling = true; + } + } + + // Because we may have moved the row from one category to another, + // take a look at our sibling and borrow its sources and targets. + this.copyDragClasses(sourceRow, changedRow, group); + rowSettings = this.rowSettings(group, changedRow); + + // In the case that we're looking for a parent, but the row is at the top + // of the tree, copy our sibling's values. + if (useSibling) { + rowSettings.relationship = 'sibling'; + rowSettings.source = rowSettings.target; + } + + var targetClass = '.' + rowSettings.target; + var targetElement = $changedRow.find(targetClass).get(0); + + // Check if a target element exists in this row. + if (targetElement) { + var sourceClass = '.' + rowSettings.source; + var sourceElement = $(sourceClass, sourceRow).get(0); + switch (rowSettings.action) { + case 'depth': + // Get the depth of the target row. + targetElement.value = $(sourceElement).closest('tr').find('.js-indentation').length; + break; + + case 'match': + // Update the value. + targetElement.value = sourceElement.value; + break; + + case 'order': + var siblings = this.rowObject.findSiblings(rowSettings); + if ($(targetElement).is('select')) { + // Get a list of acceptable values. + var values = []; + $(targetElement).find('option').each(function () { + values.push(this.value); + }); + var maxVal = values[values.length - 1]; + // Populate the values in the siblings. + $(siblings).find(targetClass).each(function () { + // If there are more items than possible values, assign the + // maximum value to the row. + if (values.length > 0) { + this.value = values.shift(); + } + else { + this.value = maxVal; + } + }); + } + else { + // Assume a numeric input field. + var weight = parseInt($(siblings[0]).find(targetClass).val(), 10) || 0; + $(siblings).find(targetClass).each(function () { + this.value = weight; + weight++; + }); + } + break; + } + } + }; + + /** + * Copy all tableDrag related classes from one row to another. + * + * Copy all special tableDrag classes from one row's form elements to a + * different one, removing any special classes that the destination row + * may have had. + * + * @param {HTMLElement} sourceRow + * The element for the source row. + * @param {HTMLElement} targetRow + * The element for the target row. + * @param {string} group + * The group selector. + */ + Drupal.tableDrag.prototype.copyDragClasses = function (sourceRow, targetRow, group) { + var sourceElement = $(sourceRow).find('.' + group); + var targetElement = $(targetRow).find('.' + group); + if (sourceElement.length && targetElement.length) { + targetElement[0].className = sourceElement[0].className; + } + }; + + /** + * Check the suggested scroll of the table. + * + * @param {number} cursorY + * The Y position of the cursor. + * + * @return {number} + * The suggested scroll. + */ + Drupal.tableDrag.prototype.checkScroll = function (cursorY) { + var de = document.documentElement; + var b = document.body; + + var windowHeight = this.windowHeight = window.innerHeight || (de.clientHeight && de.clientWidth !== 0 ? de.clientHeight : b.offsetHeight); + var scrollY; + if (document.all) { + scrollY = this.scrollY = !de.scrollTop ? b.scrollTop : de.scrollTop; + } + else { + scrollY = this.scrollY = window.pageYOffset ? window.pageYOffset : window.scrollY; + } + var trigger = this.scrollSettings.trigger; + var delta = 0; + + // Return a scroll speed relative to the edge of the screen. + if (cursorY - scrollY > windowHeight - trigger) { + delta = trigger / (windowHeight + scrollY - cursorY); + delta = (delta > 0 && delta < trigger) ? delta : trigger; + return delta * this.scrollSettings.amount; + } + else if (cursorY - scrollY < trigger) { + delta = trigger / (cursorY - scrollY); + delta = (delta > 0 && delta < trigger) ? delta : trigger; + return -delta * this.scrollSettings.amount; + } + }; + + /** + * Set the scroll for the table. + * + * @param {number} scrollAmount + * The amount of scroll to apply to the window. + */ + Drupal.tableDrag.prototype.setScroll = function (scrollAmount) { + var self = this; + + this.scrollInterval = setInterval(function () { + // Update the scroll values stored in the object. + self.checkScroll(self.currentPointerCoords.y); + var aboveTable = self.scrollY > self.table.topY; + var belowTable = self.scrollY + self.windowHeight < self.table.bottomY; + if (scrollAmount > 0 && belowTable || scrollAmount < 0 && aboveTable) { + window.scrollBy(0, scrollAmount); + } + }, this.scrollSettings.interval); + }; + + /** + * Command to restripe table properly. + */ + Drupal.tableDrag.prototype.restripeTable = function () { + // :even and :odd are reversed because jQuery counts from 0 and + // we count from 1, so we're out of sync. + // Match immediate children of the parent element to allow nesting. + $(this.table).find('> tbody > tr.draggable, > tr.draggable') + .filter(':visible') + .filter(':odd').removeClass('odd').addClass('even').end() + .filter(':even').removeClass('even').addClass('odd'); + }; + + /** + * Stub function. Allows a custom handler when a row begins dragging. + * + * @return {null} + * Returns null when the stub function is used. + */ + Drupal.tableDrag.prototype.onDrag = function () { + return null; + }; + + /** + * Stub function. Allows a custom handler when a row is dropped. + * + * @return {null} + * Returns null when the stub function is used. + */ + Drupal.tableDrag.prototype.onDrop = function () { + return null; + }; + + /** + * Constructor to make a new object to manipulate a table row. + * + * @param {HTMLElement} tableRow + * The DOM element for the table row we will be manipulating. + * @param {string} method + * The method in which this row is being moved. Either 'keyboard' or + * 'mouse'. + * @param {bool} indentEnabled + * Whether the containing table uses indentations. Used for optimizations. + * @param {number} maxDepth + * The maximum amount of indentations this row may contain. + * @param {bool} addClasses + * Whether we want to add classes to this row to indicate child + * relationships. + */ + Drupal.tableDrag.prototype.row = function (tableRow, method, indentEnabled, maxDepth, addClasses) { + var $tableRow = $(tableRow); + + this.element = tableRow; + this.method = method; + this.group = [tableRow]; + this.groupDepth = $tableRow.find('.js-indentation').length; + this.changed = false; + this.table = $tableRow.closest('table')[0]; + this.indentEnabled = indentEnabled; + this.maxDepth = maxDepth; + // Direction the row is being moved. + this.direction = ''; + if (this.indentEnabled) { + this.indents = $tableRow.find('.js-indentation').length; + this.children = this.findChildren(addClasses); + this.group = $.merge(this.group, this.children); + // Find the depth of this entire group. + for (var n = 0; n < this.group.length; n++) { + this.groupDepth = Math.max($(this.group[n]).find('.js-indentation').length, this.groupDepth); + } + } + }; + + /** + * Find all children of rowObject by indentation. + * + * @param {bool} addClasses + * Whether we want to add classes to this row to indicate child + * relationships. + * + * @return {Array} + * An array of children of the row. + */ + Drupal.tableDrag.prototype.row.prototype.findChildren = function (addClasses) { + var parentIndentation = this.indents; + var currentRow = $(this.element, this.table).next('tr.draggable'); + var rows = []; + var child = 0; + + function rowIndentation(indentNum, el) { + var self = $(el); + if (child === 1 && (indentNum === parentIndentation)) { + self.addClass('tree-child-first'); + } + if (indentNum === parentIndentation) { + self.addClass('tree-child'); + } + else if (indentNum > parentIndentation) { + self.addClass('tree-child-horizontal'); + } + } + + while (currentRow.length) { + // A greater indentation indicates this is a child. + if (currentRow.find('.js-indentation').length > parentIndentation) { + child++; + rows.push(currentRow[0]); + if (addClasses) { + currentRow.find('.js-indentation').each(rowIndentation); + } + } + else { + break; + } + currentRow = currentRow.next('tr.draggable'); + } + if (addClasses && rows.length) { + $(rows[rows.length - 1]).find('.js-indentation:nth-child(' + (parentIndentation + 1) + ')').addClass('tree-child-last'); + } + return rows; + }; + + /** + * Ensure that two rows are allowed to be swapped. + * + * @param {HTMLElement} row + * DOM object for the row being considered for swapping. + * + * @return {bool} + * Whether the swap is a valid swap or not. + */ + Drupal.tableDrag.prototype.row.prototype.isValidSwap = function (row) { + var $row = $(row); + if (this.indentEnabled) { + var prevRow; + var nextRow; + if (this.direction === 'down') { + prevRow = row; + nextRow = $row.next('tr').get(0); + } + else { + prevRow = $row.prev('tr').get(0); + nextRow = row; + } + this.interval = this.validIndentInterval(prevRow, nextRow); + + // We have an invalid swap if the valid indentations interval is empty. + if (this.interval.min > this.interval.max) { + return false; + } + } + + // Do not let an un-draggable first row have anything put before it. + if (this.table.tBodies[0].rows[0] === row && $row.is(':not(.draggable)')) { + return false; + } + + return true; + }; + + /** + * Perform the swap between two rows. + * + * @param {string} position + * Whether the swap will occur 'before' or 'after' the given row. + * @param {HTMLElement} row + * DOM element what will be swapped with the row group. + */ + Drupal.tableDrag.prototype.row.prototype.swap = function (position, row) { + // Makes sure only DOM object are passed to Drupal.detachBehaviors(). + this.group.forEach(function (row) { + Drupal.detachBehaviors(row, drupalSettings, 'move'); + }); + $(row)[position](this.group); + // Makes sure only DOM object are passed to Drupal.attachBehaviors()s. + this.group.forEach(function (row) { + Drupal.attachBehaviors(row, drupalSettings); + }); + this.changed = true; + this.onSwap(row); + }; + + /** + * Determine the valid indentations interval for the row at a given position. + * + * @param {?HTMLElement} prevRow + * DOM object for the row before the tested position + * (or null for first position in the table). + * @param {?HTMLElement} nextRow + * DOM object for the row after the tested position + * (or null for last position in the table). + * + * @return {object} + * An object with the keys `min` and `max` to indicate the valid indent + * interval. + */ + Drupal.tableDrag.prototype.row.prototype.validIndentInterval = function (prevRow, nextRow) { + var $prevRow = $(prevRow); + var minIndent; + var maxIndent; + + // Minimum indentation: + // Do not orphan the next row. + minIndent = nextRow ? $(nextRow).find('.js-indentation').length : 0; + + // Maximum indentation: + if (!prevRow || $prevRow.is(':not(.draggable)') || $(this.element).is('.tabledrag-root')) { + // Do not indent: + // - the first row in the table, + // - rows dragged below a non-draggable row, + // - 'root' rows. + maxIndent = 0; + } + else { + // Do not go deeper than as a child of the previous row. + maxIndent = $prevRow.find('.js-indentation').length + ($prevRow.is('.tabledrag-leaf') ? 0 : 1); + // Limit by the maximum allowed depth for the table. + if (this.maxDepth) { + maxIndent = Math.min(maxIndent, this.maxDepth - (this.groupDepth - this.indents)); + } + } + + return {min: minIndent, max: maxIndent}; + }; + + /** + * Indent a row within the legal bounds of the table. + * + * @param {number} indentDiff + * The number of additional indentations proposed for the row (can be + * positive or negative). This number will be adjusted to nearest valid + * indentation level for the row. + * + * @return {number} + * The number of indentations applied. + */ + Drupal.tableDrag.prototype.row.prototype.indent = function (indentDiff) { + var $group = $(this.group); + // Determine the valid indentations interval if not available yet. + if (!this.interval) { + var prevRow = $(this.element).prev('tr').get(0); + var nextRow = $group.eq(-1).next('tr').get(0); + this.interval = this.validIndentInterval(prevRow, nextRow); + } + + // Adjust to the nearest valid indentation. + var indent = this.indents + indentDiff; + indent = Math.max(indent, this.interval.min); + indent = Math.min(indent, this.interval.max); + indentDiff = indent - this.indents; + + for (var n = 1; n <= Math.abs(indentDiff); n++) { + // Add or remove indentations. + if (indentDiff < 0) { + $group.find('.js-indentation:first-of-type').remove(); + this.indents--; + } + else { + $group.find('td:first-of-type').prepend(Drupal.theme('tableDragIndentation')); + this.indents++; + } + } + if (indentDiff) { + // Update indentation for this row. + this.changed = true; + this.groupDepth += indentDiff; + this.onIndent(); + } + + return indentDiff; + }; + + /** + * Find all siblings for a row. + * + * According to its subgroup or indentation. Note that the passed-in row is + * included in the list of siblings. + * + * @param {object} rowSettings + * The field settings we're using to identify what constitutes a sibling. + * + * @return {Array} + * An array of siblings. + */ + Drupal.tableDrag.prototype.row.prototype.findSiblings = function (rowSettings) { + var siblings = []; + var directions = ['prev', 'next']; + var rowIndentation = this.indents; + var checkRowIndentation; + for (var d = 0; d < directions.length; d++) { + var checkRow = $(this.element)[directions[d]](); + while (checkRow.length) { + // Check that the sibling contains a similar target field. + if (checkRow.find('.' + rowSettings.target)) { + // Either add immediately if this is a flat table, or check to ensure + // that this row has the same level of indentation. + if (this.indentEnabled) { + checkRowIndentation = checkRow.find('.js-indentation').length; + } + + if (!(this.indentEnabled) || (checkRowIndentation === rowIndentation)) { + siblings.push(checkRow[0]); + } + else if (checkRowIndentation < rowIndentation) { + // No need to keep looking for siblings when we get to a parent. + break; + } + } + else { + break; + } + checkRow = checkRow[directions[d]](); + } + // Since siblings are added in reverse order for previous, reverse the + // completed list of previous siblings. Add the current row and continue. + if (directions[d] === 'prev') { + siblings.reverse(); + siblings.push(this.element); + } + } + return siblings; + }; + + /** + * Remove indentation helper classes from the current row group. + */ + Drupal.tableDrag.prototype.row.prototype.removeIndentClasses = function () { + for (var n in this.children) { + if (this.children.hasOwnProperty(n)) { + $(this.children[n]).find('.js-indentation') + .removeClass('tree-child') + .removeClass('tree-child-first') + .removeClass('tree-child-last') + .removeClass('tree-child-horizontal'); + } + } + }; + + /** + * Add an asterisk or other marker to the changed row. + */ + Drupal.tableDrag.prototype.row.prototype.markChanged = function () { + var marker = Drupal.theme('tableDragChangedMarker'); + var cell = $(this.element).find('td:first-of-type'); + if (cell.find('abbr.tabledrag-changed').length === 0) { + cell.append(marker); + } + }; + + /** + * Stub function. Allows a custom handler when a row is indented. + * + * @return {null} + * Returns null when the stub function is used. + */ + Drupal.tableDrag.prototype.row.prototype.onIndent = function () { + return null; + }; + + /** + * Stub function. Allows a custom handler when a row is swapped. + * + * @param {HTMLElement} swappedRow + * The element for the swapped row. + * + * @return {null} + * Returns null when the stub function is used. + */ + Drupal.tableDrag.prototype.row.prototype.onSwap = function (swappedRow) { + return null; + }; + + $.extend(Drupal.theme, /** @lends Drupal.theme */{ + + /** + * @return {string} + * Markup for the marker. + */ + tableDragChangedMarker: function () { + return '*'; + }, + + /** + * @return {string} + * Markup for the indentation. + */ + tableDragIndentation: function () { + return '
     
    '; + }, + + /** + * @return {string} + * Markup for the warning. + */ + tableDragChangedWarning: function () { + return ''; + } + }); + +})(jQuery, Drupal, drupalSettings); diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js index 75468e60dd..e92f061204 100644 --- a/core/misc/tabledrag.js +++ b/core/misc/tabledrag.js @@ -1,45 +1,19 @@ /** - * @file - * Provide dragging capabilities to admin uis. - */ - -/** - * Triggers when weights columns are toggled. - * - * @event columnschange - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/tabledrag.es6.js +* @preserve +**/ (function ($, Drupal, drupalSettings) { 'use strict'; - /** - * Store the state of weight columns display for all tables. - * - * Default value is to hide weight columns. - */ var showWeight = JSON.parse(localStorage.getItem('Drupal.tableDrag.showWeight')); - /** - * Drag and drop table rows with field manipulation. - * - * Using the drupal_attach_tabledrag() function, any table with weights or - * parent relationships may be made into draggable tables. Columns containing - * a field may optionally be hidden, providing a better user experience. - * - * Created tableDrag instances may be modified with custom behaviors by - * overriding the .onDrag, .onDrop, .row.onSwap, and .row.onIndent methods. - * See blocks.js for an example of adding additional functionality to - * tableDrag. - * - * @type {Drupal~behavior} - */ Drupal.behaviors.tableDrag = { - attach: function (context, settings) { + attach: function attach(context, settings) { function initTableDrag(table, base) { if (table.length) { - // Create the new tableDrag instance. Save in the Drupal variable - // to allow other scripts access to the object. Drupal.tableDrag[base] = new Drupal.tableDrag(table[0], settings.tableDrag[base]); } } @@ -52,128 +26,40 @@ } }; - /** - * 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.scrollSettings = { amount: 4, interval: 50, trigger: 70 }; + this.scrollInterval = null; - /** - * - * @type {number} - */ this.scrollY = 0; - /** - * - * @type {number} - */ this.windowHeight = 0; - /** - * Check this table's settings for parent relationships. - * - * 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)) { @@ -190,77 +76,49 @@ } } 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 = $('
    ').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. - $table.before($('') - .attr('title', Drupal.t('Re-order rows by numerical weight instead of dragging.')) - .on('click', $.proxy(function (e) { - e.preventDefault(); - this.toggleColumns(); - }, this)) - .wrap('
    ') - .parent() - ); - - // 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.. + $table.find('> tr.draggable, > tbody > tr.draggable').each(function () { + self.makeDraggable(this); + }); + + $table.before($('').attr('title', Drupal.t('Re-order rows by numerical weight instead of dragging.')).on('click', $.proxy(function (e) { + e.preventDefault(); + this.toggleColumns(); + }, this)).wrap('
    ').parent()); + 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); }); + $(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)); }; - /** - * Initialize columns containing form elements to be hidden by default. - * - * Identify and mark each cell with a CSS class so we can easily toggle - * show/hide it. Finally, hide columns if user does not have a - * 'Drupal.tableDrag.showWeight' localStorage value. - */ Drupal.tableDrag.prototype.initColumns = function () { var $table = this.$table; var hidden; @@ -268,8 +126,6 @@ var columnIndex; for (var group in this.tableSettings) { if (this.tableSettings.hasOwnProperty(group)) { - - // Find the first field in this group. for (var d in this.tableSettings[group]) { if (this.tableSettings[group].hasOwnProperty(d)) { var field = $table.find('.' + this.tableSettings[group][d].target).eq(0); @@ -281,11 +137,7 @@ } } - // Mark the column containing this field so it can be hidden. if (hidden && cell[0]) { - // Add 1 to our indexes. The nth-child selector is 1 based, not 0 - // based. Match immediate children of the parent element to allow - // nesting. columnIndex = cell.parent().find('> td').index(cell.get(0)) + 1; $table.find('> thead > tr, > tbody > tr, > tr').each(this.addColspanClass(columnIndex)); } @@ -294,20 +146,8 @@ this.displayColumns(showWeight); }; - /** - * Mark cells that have colspan. - * - * In order to adjust the colspan instead of hiding them altogether. - * - * @param {number} columnIndex - * The column index to add colspan class to. - * - * @return {function} - * Function to add colspan class. - */ Drupal.tableDrag.prototype.addColspanClass = function (columnIndex) { return function () { - // Get the columnIndex and adjust for any colspans in this row. var $row = $(this); var index = columnIndex; var cells = $row.children(); @@ -320,105 +160,62 @@ if (index > 0) { cell = cells.filter(':nth-child(' + index + ')'); if (cell[0].colSpan && cell[0].colSpan > 1) { - // If this cell has a colspan, mark it so we can reduce the colspan. cell.addClass('tabledrag-has-colspan'); - } - else { - // Mark this cell so we can hide it. + } else { cell.addClass('tabledrag-hide'); } } }; }; - /** - * Hide or display weight columns. Triggers an event on change. - * - * @fires event:columnschange - * - * @param {bool} displayWeight - * 'true' will show weight columns. - */ Drupal.tableDrag.prototype.displayColumns = function (displayWeight) { if (displayWeight) { this.showColumns(); - } - // Default action is to hide columns. - else { - this.hideColumns(); - } - // Trigger an event to allow other scripts to react to this display change. - // Force the extra parameter as a bool. + } else { + this.hideColumns(); + } + $('table').findOnce('tabledrag').trigger('columnschange', !!displayWeight); }; - /** - * Toggle the weight column depending on 'showWeight' value. - * - * Store only default override. - */ Drupal.tableDrag.prototype.toggleColumns = function () { showWeight = !showWeight; this.displayColumns(showWeight); if (showWeight) { - // Save default override. localStorage.setItem('Drupal.tableDrag.showWeight', showWeight); - } - else { - // Reset the value to its default. + } else { localStorage.removeItem('Drupal.tableDrag.showWeight'); } }; - /** - * Hide the columns containing weight/parent form elements. - * - * Undo showColumns(). - */ Drupal.tableDrag.prototype.hideColumns = function () { var $tables = $('table').findOnce('tabledrag'); - // Hide weight/parent cells and headers. + $tables.find('.tabledrag-hide').css('display', 'none'); - // Show TableDrag handles. + $tables.find('.tabledrag-handle').css('display', ''); - // Reduce the colspan of any effected multi-span columns. + $tables.find('.tabledrag-has-colspan').each(function () { this.colSpan = this.colSpan - 1; }); - // Change link text. + $('.tabledrag-toggle-weight').text(Drupal.t('Show row weights')); }; - /** - * Show the columns containing weight/parent form elements. - * - * Undo hideColumns(). - */ Drupal.tableDrag.prototype.showColumns = function () { var $tables = $('table').findOnce('tabledrag'); - // Show weight/parent cells and headers. + $tables.find('.tabledrag-hide').css('display', ''); - // Hide TableDrag handles. + $tables.find('.tabledrag-handle').css('display', 'none'); - // Increase the colspan for any columns where it was previously reduced. + $tables.find('.tabledrag-has-colspan').each(function () { this.colSpan = this.colSpan + 1; }); - // Change link text. + $('.tabledrag-toggle-weight').text(Drupal.t('Hide row weights')); }; - /** - * Find the target used within a particular row and group. - * - * @param {string} group - * Group selector. - * @param {HTMLElement} row - * The row HTML element. - * - * @return {object} - * The table row settings. - */ Drupal.tableDrag.prototype.rowSettings = function (group, row) { var field = $(row).find('.' + group); var tableSettingsGroup = this.tableSettings[group]; @@ -426,7 +223,6 @@ if (tableSettingsGroup.hasOwnProperty(delta)) { var targetClass = tableSettingsGroup[delta].target; if (field.is('.' + targetClass)) { - // Return a copy of the row settings. var rowSettings = {}; for (var n in tableSettingsGroup[delta]) { if (tableSettingsGroup[delta].hasOwnProperty(n)) { @@ -439,27 +235,20 @@ } }; - /** - * Take an item and add event handlers to make it become draggable. - * - * @param {HTMLElement} item - * The item to add event handlers to. - */ 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 = $('
     
    ').attr('title', Drupal.t('Drag to re-order')); - // 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 { + } else { $item.find('td').eq(0).prepend(handle); } @@ -471,27 +260,21 @@ 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); } @@ -499,20 +282,14 @@ var keyChange = false; var groupHeight; - /* eslint-disable no-fallthrough */ - 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); @@ -521,13 +298,11 @@ 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'); @@ -536,34 +311,27 @@ } 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). + } else if (self.table.tBodies[0].rows[0] !== previousRow || $previousRow.is('.draggable')) { 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); @@ -572,13 +340,11 @@ 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) { @@ -587,25 +353,21 @@ }); 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. + } else { 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; } - /* eslint-enable no-fallthrough */ - if (self.rowObject && self.rowObject.changed === true) { $(item).addClass('drag'); if (self.oldRowElement) { @@ -618,49 +380,24 @@ 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) { - /* eslint-disable no-fallthrough */ - switch (event.keyCode) { - // Left arrow. case 37: - // Up arrow. case 38: - // Right arrow. case 39: - // Down arrow. case 40: return false; } - - /* eslint-enable no-fallthrough */ - }); }; - /** - * Pointer event initiator, creates drag object and information. - * - * @param {jQuery.Event} event - * The event object that trigger the drag. - * @param {Drupal.tableDrag} self - * The drag handle. - * @param {HTMLElement} item - * The item that that is being dragged. - */ Drupal.tableDrag.prototype.dragStart = function (event, self, item) { - // Create a new dragObject recording the pointer information. self.dragObject = {}; self.dragObject.initOffset = self.getPointerOffset(item, event); self.dragObject.initPointerCoords = self.pointerCoords(event); @@ -668,66 +405,47 @@ self.dragObject.indentPointerPos = self.dragObject.initPointerCoords; } - // If there's a lingering row object from the keyboard, remove its focus. if (self.rowObject) { $(self.rowObject.element).find('a.tabledrag-handle').trigger('blur'); } - // Create a new rowObject for manipulation of this row. self.rowObject = new self.row(item, 'pointer', self.indentEnabled, self.maxDepth, true); - // Save the position of the table. self.table.topY = $(self.table).offset().top; self.table.bottomY = self.table.topY + self.table.offsetHeight; - // Add classes to the handle and row. $(item).addClass('drag'); - // Set the document to use the move cursor during drag. $('body').addClass('drag'); if (self.oldRowElement) { $(self.oldRowElement).removeClass('drag-previous'); } }; - /** - * Pointer movement handler, bound to document. - * - * @param {jQuery.Event} event - * The pointer event. - * @param {Drupal.tableDrag} self - * The tableDrag instance. - * - * @return {bool|undefined} - * Undefined if no dragObject is defined, false otherwise. - */ Drupal.tableDrag.prototype.dragRow = function (event, self) { if (self.dragObject) { self.currentPointerCoords = self.pointerCoords(event); var y = self.currentPointerCoords.y - self.dragObject.initOffset.y; var x = self.currentPointerCoords.x - self.dragObject.initOffset.x; - // Check for row swapping and vertical scrolling. if (y !== self.oldY) { self.rowObject.direction = y > self.oldY ? 'down' : 'up'; - // Update the old value. + self.oldY = y; - // Check if the window should be scrolled (and how fast). + var scrollAmount = self.checkScroll(self.currentPointerCoords.y); - // Stop any current scrolling. + clearInterval(self.scrollInterval); - // Continue scrolling if the mouse has moved in the scroll direction. + if (scrollAmount > 0 && self.rowObject.direction === 'down' || scrollAmount < 0 && self.rowObject.direction === 'up') { self.setScroll(scrollAmount); } - // If we have a valid target, perform the swap and restripe the table. var currentRow = self.findDropTargetRow(x, y); if (currentRow) { if (self.rowObject.direction === 'down') { self.rowObject.swap('after', currentRow, self); - } - else { + } else { self.rowObject.swap('before', currentRow, self); } if (self.striping === true) { @@ -736,16 +454,13 @@ } } - // Similar to row swapping, handle indentations. if (self.indentEnabled) { var xDiff = self.currentPointerCoords.x - self.dragObject.indentPointerPos.x; - // Set the number of indentations the pointer has been moved left or - // right. + var indentDiff = Math.round(xDiff / self.indentAmount); - // Indent the row with our estimated diff, which may be further - // restricted according to the rows around this row. + var indentChange = self.rowObject.indent(indentDiff); - // Update table and pointer indentations. + self.dragObject.indentPointerPos.x += self.indentAmount * indentChange * self.rtl; self.indentCount = Math.max(self.indentCount, self.rowObject.indents); } @@ -754,29 +469,17 @@ } }; - /** - * Pointerup behavior. - * - * @param {jQuery.Event} event - * The pointer event. - * @param {Drupal.tableDrag} self - * The tableDrag instance. - */ Drupal.tableDrag.prototype.dropRow = function (event, self) { var droppedRow; var $droppedRow; - // Drop row functionality. if (self.rowObject !== null) { droppedRow = self.rowObject.element; $droppedRow = $(droppedRow); - // The row is already in the right place so we just release it. + if (self.rowObject.changed === true) { - // Update the fields in the dropped row. self.updateFields(droppedRow); - // If a setting exists for affecting the entire group, update all the - // fields in the entire dragged group. for (var group in self.tableSettings) { if (self.tableSettings.hasOwnProperty(group)) { var rowSettings = self.rowSettings(group, droppedRow); @@ -809,7 +512,6 @@ self.rowObject = null; } - // Functionality specific only to pointerup events. if (self.dragObject !== null) { self.dragObject = null; $('body').removeClass('drag'); @@ -817,18 +519,9 @@ } }; - /** - * Get the coordinates from the event (allowing for browser differences). - * - * @param {jQuery.Event} event - * The pointer event. - * - * @return {object} - * An object with `x` and `y` keys indicating the position. - */ Drupal.tableDrag.prototype.pointerCoords = function (event) { if (event.pageX || event.pageY) { - return {x: event.pageX, y: event.pageY}; + return { x: event.pageX, y: event.pageY }; } return { x: event.clientX + document.body.scrollLeft - document.body.clientLeft, @@ -836,39 +529,12 @@ }; }; - /** - * Get the event offset from the target element. - * - * Given a target element and a pointer event, get the event offset from that - * element. To do this we need the element's position and the target position. - * - * @param {HTMLElement} target - * The target HTML element. - * @param {jQuery.Event} event - * The pointer event. - * - * @return {object} - * An object with `x` and `y` keys indicating the position. - */ Drupal.tableDrag.prototype.getPointerOffset = function (target, event) { var docPos = $(target).offset(); var pointerPos = this.pointerCoords(event); - return {x: pointerPos.x - docPos.left, y: pointerPos.y - docPos.top}; + return { x: pointerPos.x - docPos.left, y: pointerPos.y - docPos.top }; }; - /** - * Find the row the mouse is currently over. - * - * This row is then taken and swapped with the one being dragged. - * - * @param {number} x - * The x coordinate of the mouse on the page (not the screen). - * @param {number} y - * The y coordinate of the mouse on the page (not the screen). - * - * @return {*} - * The drop target row, if found. - */ Drupal.tableDrag.prototype.findDropTargetRow = function (x, y) { var rows = $(this.table.tBodies[0].rows).not(':hidden'); for (var n = 0; n < rows.length; n++) { @@ -876,42 +542,30 @@ var $row = $(row); var rowY = $row.offset().top; var rowHeight; - // Because Safari does not report offsetHeight on table rows, but does on - // table cells, grab the firstChild of the row and use that instead. - // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari. + if (row.offsetHeight === 0) { rowHeight = parseInt(row.firstChild.offsetHeight, 10) / 2; - } - // Other browsers. - else { - rowHeight = parseInt(row.offsetHeight, 10) / 2; - } + } else { + rowHeight = parseInt(row.offsetHeight, 10) / 2; + } - // Because we always insert before, we need to offset the height a bit. - if ((y > (rowY - rowHeight)) && (y < (rowY + rowHeight))) { + if (y > rowY - rowHeight && y < rowY + rowHeight) { if (this.indentEnabled) { - // Check that this row is not a child of the row being dragged. for (n in this.rowObject.group) { if (this.rowObject.group[n] === row) { return null; } } - } - else { - // Do not allow a row to be swapped with itself. + } else { if (row === this.rowObject.element) { return null; } } - // Check that swapping with this row is allowed. if (!this.rowObject.isValidSwap(row)) { return null; } - // We may have found the row the mouse just passed over, but it doesn't - // take into account hidden rows. Skip backwards until we find a - // draggable row. while ($row.is(':hidden') && $row.prev('tr').is(':hidden')) { $row = $row.prev('tr:first-of-type'); row = $row.get(0); @@ -922,30 +576,14 @@ return null; }; - /** - * After the row is dropped, update the table fields. - * - * @param {HTMLElement} changedRow - * DOM object for the row that was just dropped. - */ Drupal.tableDrag.prototype.updateFields = function (changedRow) { for (var group in this.tableSettings) { if (this.tableSettings.hasOwnProperty(group)) { - // Each group may have a different setting for relationship, so we find - // the source rows for each separately. this.updateField(changedRow, group); } } }; - /** - * After the row is dropped, update a single table field. - * - * @param {HTMLElement} changedRow - * DOM object for the row that was just dropped. - * @param {string} group - * The settings group on which field updates will occur. - */ Drupal.tableDrag.prototype.updateField = function (changedRow, group) { var rowSettings = this.rowSettings(group, changedRow); var $changedRow = $(changedRow); @@ -953,72 +591,54 @@ var $previousRow; var previousRow; var useSibling; - // Set the row as its own target. + if (rowSettings.relationship === 'self' || rowSettings.relationship === 'group') { sourceRow = changedRow; - } - // Siblings are easy, check previous and next rows. - else if (rowSettings.relationship === 'sibling') { - $previousRow = $changedRow.prev('tr:first-of-type'); - previousRow = $previousRow.get(0); - var $nextRow = $changedRow.next('tr:first-of-type'); - var nextRow = $nextRow.get(0); - sourceRow = changedRow; - if ($previousRow.is('.draggable') && $previousRow.find('.' + group).length) { - if (this.indentEnabled) { - if ($previousRow.find('.js-indentations').length === $changedRow.find('.js-indentations').length) { + } else if (rowSettings.relationship === 'sibling') { + $previousRow = $changedRow.prev('tr:first-of-type'); + previousRow = $previousRow.get(0); + var $nextRow = $changedRow.next('tr:first-of-type'); + var nextRow = $nextRow.get(0); + sourceRow = changedRow; + if ($previousRow.is('.draggable') && $previousRow.find('.' + group).length) { + if (this.indentEnabled) { + if ($previousRow.find('.js-indentations').length === $changedRow.find('.js-indentations').length) { + sourceRow = previousRow; + } + } else { sourceRow = previousRow; } - } - else { - sourceRow = previousRow; - } - } - else if ($nextRow.is('.draggable') && $nextRow.find('.' + group).length) { - if (this.indentEnabled) { - if ($nextRow.find('.js-indentations').length === $changedRow.find('.js-indentations').length) { + } else if ($nextRow.is('.draggable') && $nextRow.find('.' + group).length) { + if (this.indentEnabled) { + if ($nextRow.find('.js-indentations').length === $changedRow.find('.js-indentations').length) { + sourceRow = nextRow; + } + } else { sourceRow = nextRow; } } - else { - sourceRow = nextRow; - } - } - } - // Parents, look up the tree until we find a field not in this group. - // Go up as many parents as indentations in the changed row. - else if (rowSettings.relationship === 'parent') { - $previousRow = $changedRow.prev('tr'); - previousRow = $previousRow; - while ($previousRow.length && $previousRow.find('.js-indentation').length >= this.rowObject.indents) { - $previousRow = $previousRow.prev('tr'); - previousRow = $previousRow; - } - // If we found a row. - if ($previousRow.length) { - sourceRow = $previousRow.get(0); - } - // Otherwise we went all the way to the left of the table without finding - // a parent, meaning this item has been placed at the root level. - else { - // Use the first row in the table as source, because it's guaranteed to - // be at the root level. Find the first item, then compare this row - // against it as a sibling. - sourceRow = $(this.table).find('tr.draggable:first-of-type').get(0); - if (sourceRow === this.rowObject.element) { - sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0); + } else if (rowSettings.relationship === 'parent') { + $previousRow = $changedRow.prev('tr'); + previousRow = $previousRow; + while ($previousRow.length && $previousRow.find('.js-indentation').length >= this.rowObject.indents) { + $previousRow = $previousRow.prev('tr'); + previousRow = $previousRow; + } + + if ($previousRow.length) { + sourceRow = $previousRow.get(0); + } else { + sourceRow = $(this.table).find('tr.draggable:first-of-type').get(0); + if (sourceRow === this.rowObject.element) { + sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0); + } + useSibling = true; + } } - useSibling = true; - } - } - // Because we may have moved the row from one category to another, - // take a look at our sibling and borrow its sources and targets. this.copyDragClasses(sourceRow, changedRow, group); rowSettings = this.rowSettings(group, changedRow); - // In the case that we're looking for a parent, but the row is at the top - // of the tree, copy our sibling's values. if (useSibling) { rowSettings.relationship = 'sibling'; rowSettings.source = rowSettings.target; @@ -1027,44 +647,35 @@ var targetClass = '.' + rowSettings.target; var targetElement = $changedRow.find(targetClass).get(0); - // Check if a target element exists in this row. if (targetElement) { var sourceClass = '.' + rowSettings.source; var sourceElement = $(sourceClass, sourceRow).get(0); switch (rowSettings.action) { case 'depth': - // Get the depth of the target row. targetElement.value = $(sourceElement).closest('tr').find('.js-indentation').length; break; case 'match': - // Update the value. targetElement.value = sourceElement.value; break; case 'order': var siblings = this.rowObject.findSiblings(rowSettings); if ($(targetElement).is('select')) { - // Get a list of acceptable values. var values = []; $(targetElement).find('option').each(function () { values.push(this.value); }); var maxVal = values[values.length - 1]; - // Populate the values in the siblings. + $(siblings).find(targetClass).each(function () { - // If there are more items than possible values, assign the - // maximum value to the row. if (values.length > 0) { this.value = values.shift(); - } - else { + } else { this.value = maxVal; } }); - } - else { - // Assume a numeric input field. + } else { var weight = parseInt($(siblings[0]).find(targetClass).val(), 10) || 0; $(siblings).find(targetClass).each(function () { this.value = weight; @@ -1076,20 +687,6 @@ } }; - /** - * Copy all tableDrag related classes from one row to another. - * - * Copy all special tableDrag classes from one row's form elements to a - * different one, removing any special classes that the destination row - * may have had. - * - * @param {HTMLElement} sourceRow - * The element for the source row. - * @param {HTMLElement} targetRow - * The element for the target row. - * @param {string} group - * The group selector. - */ Drupal.tableDrag.prototype.copyDragClasses = function (sourceRow, targetRow, group) { var sourceElement = $(sourceRow).find('.' + group); var targetElement = $(targetRow).find('.' + group); @@ -1098,15 +695,6 @@ } }; - /** - * Check the suggested scroll of the table. - * - * @param {number} cursorY - * The Y position of the cursor. - * - * @return {number} - * The suggested scroll. - */ Drupal.tableDrag.prototype.checkScroll = function (cursorY) { var de = document.documentElement; var b = document.body; @@ -1115,37 +703,27 @@ var scrollY; if (document.all) { scrollY = this.scrollY = !de.scrollTop ? b.scrollTop : de.scrollTop; - } - else { + } else { scrollY = this.scrollY = window.pageYOffset ? window.pageYOffset : window.scrollY; } var trigger = this.scrollSettings.trigger; var delta = 0; - // Return a scroll speed relative to the edge of the screen. if (cursorY - scrollY > windowHeight - trigger) { delta = trigger / (windowHeight + scrollY - cursorY); - delta = (delta > 0 && delta < trigger) ? delta : trigger; + delta = delta > 0 && delta < trigger ? delta : trigger; return delta * this.scrollSettings.amount; - } - else if (cursorY - scrollY < trigger) { + } else if (cursorY - scrollY < trigger) { delta = trigger / (cursorY - scrollY); - delta = (delta > 0 && delta < trigger) ? delta : trigger; + delta = delta > 0 && delta < trigger ? delta : trigger; return -delta * this.scrollSettings.amount; } }; - /** - * Set the scroll for the table. - * - * @param {number} scrollAmount - * The amount of scroll to apply to the window. - */ Drupal.tableDrag.prototype.setScroll = function (scrollAmount) { var self = this; this.scrollInterval = setInterval(function () { - // Update the scroll values stored in the object. self.checkScroll(self.currentPointerCoords.y); var aboveTable = self.scrollY > self.table.topY; var belowTable = self.scrollY + self.windowHeight < self.table.bottomY; @@ -1155,55 +733,18 @@ }, this.scrollSettings.interval); }; - /** - * Command to restripe table properly. - */ Drupal.tableDrag.prototype.restripeTable = function () { - // :even and :odd are reversed because jQuery counts from 0 and - // we count from 1, so we're out of sync. - // Match immediate children of the parent element to allow nesting. - $(this.table).find('> tbody > tr.draggable, > tr.draggable') - .filter(':visible') - .filter(':odd').removeClass('odd').addClass('even').end() - .filter(':even').removeClass('even').addClass('odd'); + $(this.table).find('> tbody > tr.draggable, > tr.draggable').filter(':visible').filter(':odd').removeClass('odd').addClass('even').end().filter(':even').removeClass('even').addClass('odd'); }; - /** - * Stub function. Allows a custom handler when a row begins dragging. - * - * @return {null} - * Returns null when the stub function is used. - */ Drupal.tableDrag.prototype.onDrag = function () { return null; }; - /** - * Stub function. Allows a custom handler when a row is dropped. - * - * @return {null} - * Returns null when the stub function is used. - */ Drupal.tableDrag.prototype.onDrop = function () { return null; }; - /** - * Constructor to make a new object to manipulate a table row. - * - * @param {HTMLElement} tableRow - * The DOM element for the table row we will be manipulating. - * @param {string} method - * The method in which this row is being moved. Either 'keyboard' or - * 'mouse'. - * @param {bool} indentEnabled - * Whether the containing table uses indentations. Used for optimizations. - * @param {number} maxDepth - * The maximum amount of indentations this row may contain. - * @param {bool} addClasses - * Whether we want to add classes to this row to indicate child - * relationships. - */ Drupal.tableDrag.prototype.row = function (tableRow, method, indentEnabled, maxDepth, addClasses) { var $tableRow = $(tableRow); @@ -1215,29 +756,19 @@ this.table = $tableRow.closest('table')[0]; this.indentEnabled = indentEnabled; this.maxDepth = maxDepth; - // Direction the row is being moved. + this.direction = ''; if (this.indentEnabled) { this.indents = $tableRow.find('.js-indentation').length; this.children = this.findChildren(addClasses); this.group = $.merge(this.group, this.children); - // Find the depth of this entire group. + for (var n = 0; n < this.group.length; n++) { this.groupDepth = Math.max($(this.group[n]).find('.js-indentation').length, this.groupDepth); } } }; - /** - * Find all children of rowObject by indentation. - * - * @param {bool} addClasses - * Whether we want to add classes to this row to indicate child - * relationships. - * - * @return {Array} - * An array of children of the row. - */ Drupal.tableDrag.prototype.row.prototype.findChildren = function (addClasses) { var parentIndentation = this.indents; var currentRow = $(this.element, this.table).next('tr.draggable'); @@ -1246,27 +777,24 @@ function rowIndentation(indentNum, el) { var self = $(el); - if (child === 1 && (indentNum === parentIndentation)) { + if (child === 1 && indentNum === parentIndentation) { self.addClass('tree-child-first'); } if (indentNum === parentIndentation) { self.addClass('tree-child'); - } - else if (indentNum > parentIndentation) { + } else if (indentNum > parentIndentation) { self.addClass('tree-child-horizontal'); } } while (currentRow.length) { - // A greater indentation indicates this is a child. if (currentRow.find('.js-indentation').length > parentIndentation) { child++; rows.push(currentRow[0]); if (addClasses) { currentRow.find('.js-indentation').each(rowIndentation); } - } - else { + } else { break; } currentRow = currentRow.next('tr.draggable'); @@ -1277,15 +805,6 @@ return rows; }; - /** - * Ensure that two rows are allowed to be swapped. - * - * @param {HTMLElement} row - * DOM object for the row being considered for swapping. - * - * @return {bool} - * Whether the swap is a valid swap or not. - */ Drupal.tableDrag.prototype.row.prototype.isValidSwap = function (row) { var $row = $(row); if (this.indentEnabled) { @@ -1294,20 +813,17 @@ if (this.direction === 'down') { prevRow = row; nextRow = $row.next('tr').get(0); - } - else { + } else { prevRow = $row.prev('tr').get(0); nextRow = row; } this.interval = this.validIndentInterval(prevRow, nextRow); - // We have an invalid swap if the valid indentations interval is empty. if (this.interval.min > this.interval.max) { return false; } } - // Do not let an un-draggable first row have anything put before it. if (this.table.tBodies[0].rows[0] === row && $row.is(':not(.draggable)')) { return false; } @@ -1315,21 +831,12 @@ return true; }; - /** - * Perform the swap between two rows. - * - * @param {string} position - * Whether the swap will occur 'before' or 'after' the given row. - * @param {HTMLElement} row - * DOM element what will be swapped with the row group. - */ Drupal.tableDrag.prototype.row.prototype.swap = function (position, row) { - // Makes sure only DOM object are passed to Drupal.detachBehaviors(). this.group.forEach(function (row) { Drupal.detachBehaviors(row, drupalSettings, 'move'); }); $(row)[position](this.group); - // Makes sure only DOM object are passed to Drupal.attachBehaviors()s. + this.group.forEach(function (row) { Drupal.attachBehaviors(row, drupalSettings); }); @@ -1337,88 +844,50 @@ this.onSwap(row); }; - /** - * Determine the valid indentations interval for the row at a given position. - * - * @param {?HTMLElement} prevRow - * DOM object for the row before the tested position - * (or null for first position in the table). - * @param {?HTMLElement} nextRow - * DOM object for the row after the tested position - * (or null for last position in the table). - * - * @return {object} - * An object with the keys `min` and `max` to indicate the valid indent - * interval. - */ Drupal.tableDrag.prototype.row.prototype.validIndentInterval = function (prevRow, nextRow) { var $prevRow = $(prevRow); var minIndent; var maxIndent; - // Minimum indentation: - // Do not orphan the next row. minIndent = nextRow ? $(nextRow).find('.js-indentation').length : 0; - // Maximum indentation: if (!prevRow || $prevRow.is(':not(.draggable)') || $(this.element).is('.tabledrag-root')) { - // Do not indent: - // - the first row in the table, - // - rows dragged below a non-draggable row, - // - 'root' rows. maxIndent = 0; - } - else { - // Do not go deeper than as a child of the previous row. + } else { maxIndent = $prevRow.find('.js-indentation').length + ($prevRow.is('.tabledrag-leaf') ? 0 : 1); - // Limit by the maximum allowed depth for the table. + if (this.maxDepth) { maxIndent = Math.min(maxIndent, this.maxDepth - (this.groupDepth - this.indents)); } } - return {min: minIndent, max: maxIndent}; + return { min: minIndent, max: maxIndent }; }; - /** - * Indent a row within the legal bounds of the table. - * - * @param {number} indentDiff - * The number of additional indentations proposed for the row (can be - * positive or negative). This number will be adjusted to nearest valid - * indentation level for the row. - * - * @return {number} - * The number of indentations applied. - */ Drupal.tableDrag.prototype.row.prototype.indent = function (indentDiff) { var $group = $(this.group); - // Determine the valid indentations interval if not available yet. + if (!this.interval) { var prevRow = $(this.element).prev('tr').get(0); var nextRow = $group.eq(-1).next('tr').get(0); this.interval = this.validIndentInterval(prevRow, nextRow); } - // Adjust to the nearest valid indentation. var indent = this.indents + indentDiff; indent = Math.max(indent, this.interval.min); indent = Math.min(indent, this.interval.max); indentDiff = indent - this.indents; for (var n = 1; n <= Math.abs(indentDiff); n++) { - // Add or remove indentations. if (indentDiff < 0) { $group.find('.js-indentation:first-of-type').remove(); this.indents--; - } - else { + } else { $group.find('td:first-of-type').prepend(Drupal.theme('tableDragIndentation')); this.indents++; } } if (indentDiff) { - // Update indentation for this row. this.changed = true; this.groupDepth += indentDiff; this.onIndent(); @@ -1427,18 +896,6 @@ return indentDiff; }; - /** - * Find all siblings for a row. - * - * According to its subgroup or indentation. Note that the passed-in row is - * included in the list of siblings. - * - * @param {object} rowSettings - * The field settings we're using to identify what constitutes a sibling. - * - * @return {Array} - * An array of siblings. - */ Drupal.tableDrag.prototype.row.prototype.findSiblings = function (rowSettings) { var siblings = []; var directions = ['prev', 'next']; @@ -1447,29 +904,22 @@ for (var d = 0; d < directions.length; d++) { var checkRow = $(this.element)[directions[d]](); while (checkRow.length) { - // Check that the sibling contains a similar target field. if (checkRow.find('.' + rowSettings.target)) { - // Either add immediately if this is a flat table, or check to ensure - // that this row has the same level of indentation. if (this.indentEnabled) { checkRowIndentation = checkRow.find('.js-indentation').length; } - if (!(this.indentEnabled) || (checkRowIndentation === rowIndentation)) { + if (!this.indentEnabled || checkRowIndentation === rowIndentation) { siblings.push(checkRow[0]); - } - else if (checkRowIndentation < rowIndentation) { - // No need to keep looking for siblings when we get to a parent. + } else if (checkRowIndentation < rowIndentation) { break; } - } - else { + } else { break; } checkRow = checkRow[directions[d]](); } - // Since siblings are added in reverse order for previous, reverse the - // completed list of previous siblings. Add the current row and continue. + if (directions[d] === 'prev') { siblings.reverse(); siblings.push(this.element); @@ -1478,24 +928,14 @@ return siblings; }; - /** - * Remove indentation helper classes from the current row group. - */ Drupal.tableDrag.prototype.row.prototype.removeIndentClasses = function () { for (var n in this.children) { if (this.children.hasOwnProperty(n)) { - $(this.children[n]).find('.js-indentation') - .removeClass('tree-child') - .removeClass('tree-child-first') - .removeClass('tree-child-last') - .removeClass('tree-child-horizontal'); + $(this.children[n]).find('.js-indentation').removeClass('tree-child').removeClass('tree-child-first').removeClass('tree-child-last').removeClass('tree-child-horizontal'); } } }; - /** - * Add an asterisk or other marker to the changed row. - */ Drupal.tableDrag.prototype.row.prototype.markChanged = function () { var marker = Drupal.theme('tableDragChangedMarker'); var cell = $(this.element).find('td:first-of-type'); @@ -1504,54 +944,25 @@ } }; - /** - * Stub function. Allows a custom handler when a row is indented. - * - * @return {null} - * Returns null when the stub function is used. - */ Drupal.tableDrag.prototype.row.prototype.onIndent = function () { return null; }; - /** - * Stub function. Allows a custom handler when a row is swapped. - * - * @param {HTMLElement} swappedRow - * The element for the swapped row. - * - * @return {null} - * Returns null when the stub function is used. - */ Drupal.tableDrag.prototype.row.prototype.onSwap = function (swappedRow) { return null; }; - $.extend(Drupal.theme, /** @lends Drupal.theme */{ - - /** - * @return {string} - * Markup for the marker. - */ - tableDragChangedMarker: function () { + $.extend(Drupal.theme, { + tableDragChangedMarker: function tableDragChangedMarker() { return '*'; }, - /** - * @return {string} - * Markup for the indentation. - */ - tableDragIndentation: function () { + tableDragIndentation: function tableDragIndentation() { return '
     
    '; }, - /** - * @return {string} - * Markup for the warning. - */ - tableDragChangedWarning: function () { + tableDragChangedWarning: function tableDragChangedWarning() { return ''; } }); - -})(jQuery, Drupal, drupalSettings); +})(jQuery, Drupal, drupalSettings); \ No newline at end of file diff --git a/core/misc/tableheader.es6.js b/core/misc/tableheader.es6.js new file mode 100644 index 0000000000..8fc1b04896 --- /dev/null +++ b/core/misc/tableheader.es6.js @@ -0,0 +1,316 @@ +/** + * @file + * Sticky table headers. + */ + +(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); + } + }; + + function scrollValue(position) { + 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; + for (var 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) { + var tables = TableHeader.tables; + var il = tables.length; + for (var 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) { + 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) { + var 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.} + */ + 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: function () { + // Clone the table header so it inherits original jQuery properties. + var $stickyHeader = this.$originalHeader.clone(true); + // Hide the table to avoid a flash of the header clone upon page load. + this.$stickyTable = $('') + .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) { + var 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: function () { + 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)) { + 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) { + 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. + 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('display', 'none'); + } + } + this.$stickyTable.css('width', this.$originalTable.outerWidth()); + } + }); + + // Expose constructor in the public space. + Drupal.TableHeader = TableHeader; + +}(jQuery, Drupal, window.parent.Drupal.displace)); diff --git a/core/misc/tableheader.js b/core/misc/tableheader.js index 8fc1b04896..ebae716e00 100644 --- a/core/misc/tableheader.js +++ b/core/misc/tableheader.js @@ -1,23 +1,16 @@ /** - * @file - * Sticky table headers. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/tableheader.es6.js +* @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 +18,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 +27,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 +47,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,113 +81,54 @@ 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.} - */ + $.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 = $('') - .css({ - visibility: 'hidden', - position: 'fixed', - top: '0px' - }) - .append($stickyHeader) - .insertBefore(this.$originalTable); + + this.$stickyTable = $('').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; } @@ -256,53 +136,32 @@ 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 +169,5 @@ } }); - // Expose constructor in the public space. Drupal.TableHeader = TableHeader; - -}(jQuery, Drupal, window.parent.Drupal.displace)); +})(jQuery, Drupal, window.parent.Drupal.displace); \ No newline at end of file diff --git a/core/misc/tableresponsive.es6.js b/core/misc/tableresponsive.es6.js new file mode 100644 index 0000000000..0127ec8873 --- /dev/null +++ b/core/misc/tableresponsive.es6.js @@ -0,0 +1,174 @@ +/** + * @file + * Responsive table functionality. + */ + +(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) { + var $tables = $(context).find('table.responsive-enabled').once('tableresponsive'); + if ($tables.length) { + var il = $tables.length; + for (var i = 0; i < il; i++) { + TableResponsive.tables.push(new TableResponsive($tables[i])); + } + } + } + }; + + /** + * 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 = $('') + .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($('
    ').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.} + */ + 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) { + 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) { + 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); + var position = $header.prevAll('th').length; + 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; + } + 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); diff --git a/core/misc/tableresponsive.js b/core/misc/tableresponsive.js index 0127ec8873..b9beaf6cfb 100644 --- a/core/misc/tableresponsive.js +++ b/core/misc/tableresponsive.js @@ -1,22 +1,15 @@ /** - * @file - * Responsive table functionality. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/tableresponsive.es6.js +* @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 +20,45 @@ } }; - /** - * 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 = $('') - .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 = $('').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($('
    ').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.} - */ + $.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 +66,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); \ No newline at end of file diff --git a/core/misc/tableselect.es6.js b/core/misc/tableselect.es6.js new file mode 100644 index 0000000000..243e000110 --- /dev/null +++ b/core/misc/tableselect.es6.js @@ -0,0 +1,159 @@ +/** + * @file + * Table select functionality. + */ + +(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. + $(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 $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). + $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
    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. + 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; + } + } + }; + +})(jQuery, Drupal); diff --git a/core/misc/tableselect.js b/core/misc/tableselect.js index 243e000110..20f285fc8a 100644 --- a/core/misc/tableselect.js +++ b/core/misc/tableselect.js @@ -1,39 +1,24 @@ /** - * @file - * Table select functionality. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/tableselect.es6.js +* @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; @@ -42,118 +27,72 @@ 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 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); \ No newline at end of file diff --git a/core/misc/timezone.es6.js b/core/misc/timezone.es6.js new file mode 100644 index 0000000000..3c88d463dd --- /dev/null +++ b/core/misc/timezone.es6.js @@ -0,0 +1,76 @@ +/** + * @file + * Timezone detection. + */ + +(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) { + 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. + 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. + var path = 'system/timezone/' + abbreviation + '/' + offsetNow + '/' + isDaylightSavingTime; + $.ajax({ + async: false, + url: Drupal.url(path), + data: {date: dateString}, + dataType: 'json', + success: function (data) { + if (data) { + $timezone.val(data); + } + } + }); + } + } + }; + +})(jQuery, Drupal); diff --git a/core/misc/timezone.js b/core/misc/timezone.js index 3c88d463dd..12e43b033c 100644 --- a/core/misc/timezone.js +++ b/core/misc/timezone.js @@ -1,69 +1,47 @@ /** - * @file - * Timezone detection. - */ +* DO NOT EDIT THIS FILE. +* All changes should be applied to ./misc/timezone.es6.js +* @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. + 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 +50,4 @@ } } }; - -})(jQuery, Drupal); +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/misc/vertical-tabs.es6.js b/core/misc/vertical-tabs.es6.js new file mode 100644 index 0000000000..c7ad2fd2b4 --- /dev/null +++ b/core/misc/vertical-tabs.es6.js @@ -0,0 +1,252 @@ +/** + * @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) { + + '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) { + var width = drupalSettings.widthBreakpoint || 640; + var mq = '(max-width: ' + width + 'px)'; + + if (window.matchMedia(mq).matches) { + return; + } + + $(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; + + // 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 = $('
      '); + $this.wrap('
      ').before(tab_list); + + // Transform each details into a tab. + $details.each(function () { + var $that = $(this); + var 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. + 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').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) { + 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').eq(0).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 () { + 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('' + Drupal.t('(active tab)') + ''); + }, + + /** + * Updates the tab's summary. + */ + updateSummary: function () { + this.summary.html(this.details.drupalGetSummary()); + }, + + /** + * Shows a vertical tab pane. + * + * @return {Drupal.verticalTab} + * The verticalTab instance. + */ + tabShow: function () { + // 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: 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-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). + 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(); + } + 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 = $('
    • ') + .append(tab.link = $('') + .append(tab.title = $('').text(settings.title)) + .append(tab.summary = $('') + ) + ); + return tab; + }; + +})(jQuery, Drupal, drupalSettings); diff --git a/core/misc/vertical-tabs.js b/core/misc/vertical-tabs.js index c7ad2fd2b4..187e7bfeb2 100644 --- a/core/misc/vertical-tabs.js +++ b/core/misc/vertical-tabs.js @@ -1,37 +1,15 @@ /** - * @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. +* All changes should be applied to ./misc/vertical-tabs.es6.js +* @preserve +**/ (function ($, Drupal, drupalSettings) { '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)'; @@ -44,18 +22,14 @@ var focusID = $this.find(':hidden.vertical-tabs__active-tab').val(); var tab_focus; - // 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 = $('
        '); $this.wrap('
        ').before(tab_list); - // Transform each details into a tab. $details.each(function () { var $that = $(this); var vertical_tab = new Drupal.verticalTab({ @@ -63,13 +37,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 +47,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 +61,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 +72,70 @@ 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('' + Drupal.t('(active tab)') + ''); }, - /** - * 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 = $('
      • ') - .append(tab.link = $('') - .append(tab.title = $('').text(settings.title)) - .append(tab.summary = $('') - ) - ); + tab.item = $('
      • ').append(tab.link = $('').append(tab.title = $('').text(settings.title)).append(tab.summary = $(''))); return tab; }; - -})(jQuery, Drupal, drupalSettings); +})(jQuery, Drupal, drupalSettings); \ No newline at end of file diff --git a/core/modules/big_pipe/js/big_pipe.es6.js b/core/modules/big_pipe/js/big_pipe.es6.js new file mode 100644 index 0000000000..cdfd766f13 --- /dev/null +++ b/core/modules/big_pipe/js/big_pipe.es6.js @@ -0,0 +1,110 @@ +/** + * @file + * Renders BigPipe placeholders using Drupal's Ajax system. + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Executes Ajax commands in