diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index a44ba5f..bad9d50 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -11,6 +11,7 @@ whole. The branch maintainers for Drupal 8 are: - Dries Buytaert 'dries' http://drupal.org/user/1 - Nathaniel Catchpole 'catch' http://drupal.org/user/35733 - Angela Byron 'webchick' http://drupal.org/user/24967 +- Alex Pott 'alexpott' http://drupal.org/user/157725 Component maintainers --------------------- @@ -66,7 +67,6 @@ Database system - Károly Négyesi 'chx' http://drupal.org/user/9446 Database update system -- Ashok Modi 'BTMash' http://drupal.org/user/60422 - Károly Négyesi 'chx' http://drupal.org/user/9446 Entity system @@ -180,7 +180,7 @@ Module maintainers ------------------ Aggregator module -- ? +- Paris Liakos 'rootatwc' http://drupal.org/user/1011436 Block module - John Albin Wilkins 'JohnAlbin' http://drupal.org/user/32095 diff --git a/core/misc/announce.js b/core/misc/announce.js new file mode 100644 index 0000000..88081ca --- /dev/null +++ b/core/misc/announce.js @@ -0,0 +1,103 @@ +/** + * Adds an HTML element and method to trigger audio UAs to read system messages. + * + * Use 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. + * $('#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) { + + var liveElement; + var announcements = []; + + /** + * Builds a div element with the aria-live attribute and attaches it + * to the DOM. + */ + Drupal.behaviors.drupalAnnounce = { + attach: function (context, settings) { + liveElement = document.createElement('div'); + liveElement.id = 'drupal-live-announce'; + liveElement.className = 'element-invisible'; + 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. + // + // If any of the announcements has a priority of assertive, the entire + // group of announcements will have this priority. + while (announcement = announcements.pop()) { + text.unshift(announcement.text); + 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('. '); + // 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 Drupal.announce() being read. + * By wrapping the call to announce in a debounce function, we allow for + * time for multiple calls to 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 + * A string to indicate the priority of the message. Can be either + * 'polite' or 'assertive'. Polite is the default. + * + * @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. + (debounce(announce, 200)()); + }; +}(Drupal, Drupal.debounce)); diff --git a/core/misc/drupal.js b/core/misc/drupal.js index d7a4e40..627e264 100644 --- a/core/misc/drupal.js +++ b/core/misc/drupal.js @@ -259,69 +259,6 @@ Drupal.t = function (str, args, options) { }; /** - * Adds an HTML element and method to trigger audio UAs to read system messages. - */ -(function (document, Drupal) { - - var liveElement; - - /** - * Builds a div element with the aria-live attribute and attaches it - * to the DOM. - */ - Drupal.behaviors.drupalAnnounce = { - attach: function (settings, context) { - liveElement = document.createElement('div'); - liveElement.id = 'drupal-live-announce'; - liveElement.className = 'element-invisible'; - liveElement.setAttribute('aria-live', 'polite'); - liveElement.setAttribute('aria-busy', 'false'); - document.body.appendChild(liveElement); - } - }; - - /** - * Triggers audio UAs to read the supplied text. - * - * @param {String} text - * - A string to be read by the UA. - * - * @param {String} priority - * - A string to indicate the priority of the message. Can be either - * 'polite' or 'assertive'. Polite is the default. - * - * Use 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. - * $('#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} - * )); - * }); - * - * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops - */ - Drupal.announce = function (text, priority) { - if (typeof text === 'string') { - // 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 === 'assertive') ? 'assertive' : 'polite'); - // Print the text to the live region. - liveElement.innerHTML = Drupal.checkPlain(text); - // The live text area is updated. Allow the AT to announce the text. - liveElement.setAttribute('aria-busy', 'false'); - } - }; -}(document, Drupal)); - -/** * Returns the URL to a Drupal page. */ Drupal.url = function (path) { diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index f859409..43e7689 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -91,6 +91,7 @@ function contextual_library_info() { 'dependencies' => array( array('system', 'jquery'), array('system', 'drupal'), + array('system', 'drupal.announce'), array('system', 'jquery.once'), ), ); @@ -109,7 +110,7 @@ function contextual_library_info() { array('system', 'jquery'), array('system', 'jquery.once'), array('system', 'backbone'), - array('system', 'drupal.tabbingmanager'), + array('system', 'drupal.announce'), ), ); diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js index 6b468b0..d8a8434 100644 --- a/core/modules/contextual/contextual.toolbar.js +++ b/core/modules/contextual/contextual.toolbar.js @@ -20,7 +20,8 @@ Drupal.behaviors.contextualToolbar = { var $contextuals = $(context).find('.contextual-links'); var $tab = $('.js .toolbar .bar .contextual-toolbar-tab'); var model = new Drupal.contextualToolbar.models.EditToggleModel({ - isViewing: true + isViewing: true, + contextuals: $contextuals.get() }); var view = new Drupal.contextualToolbar.views.EditToggleView({ el: $tab, @@ -69,7 +70,9 @@ Drupal.contextualToolbar.models.EditToggleModel = Backbone.Model.extend({ isVisible: false, // The set of elements that can be reached via the tab key when edit mode // is enabled. - tabbingContext: null + tabbingContext: null, + // The set of contextual links stored as an Array. + contextuals: [] } }); @@ -135,14 +138,23 @@ Drupal.contextualToolbar.views.EditToggleView = Backbone.View.extend({ * The value of the isViewing attribute in the model. */ manageTabbing: function (model, isViewing) { + var contextuals = this.model.get('contextuals'); // Always release an existing tabbing context and create a new one. var tabbingContext = this.model.get('tabbingContext'); if (tabbingContext) { tabbingContext.release(); + if (isViewing) { + Drupal.announce(Drupal.t('Tabbing is no longer constrained by the Contextual module.')) + } } if (!isViewing) { tabbingContext = Drupal.TabbingManager.constrain($('.contextual-toolbar-tab, .contextual')); this.model.set('tabbingContext', tabbingContext); + Drupal.announce( + Drupal.t('Tabbing is constrained to set of @contextualsCount and the Edit mode toggle.', { + '@contextualsCount': Drupal.formatPlural(contextuals.length, '@count contextual link', '@count contextual links') + }) + ); } }, diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js index a3c0aaf..bee33d1 100644 --- a/core/modules/edit/js/views/propertyeditordecoration-view.js +++ b/core/modules/edit/js/views/propertyeditordecoration-view.js @@ -212,7 +212,8 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ this._widthAttributeIsEmpty = true; this.$el .addClass('edit-animate-disable-width') - .css('width', this.$el.width()); + .css('width', this.$el.width()) + .css('background-color', this._getBgColor(this.$el)); } // 2) Add padding; use animations. @@ -243,7 +244,8 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ if (this._widthAttributeIsEmpty) { this.$el .addClass('edit-animate-disable-width') - .css('width', ''); + .css('width', '') + .css('background-color', ''); } // 2) Remove padding; use animations (these will run simultaneously with) @@ -269,6 +271,28 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ }, /** + * Gets the background color of an element (or the inherited one). + * + * @param $e + * A DOM element. + */ + _getBgColor: function($e) { + var c; + + if ($e === null || $e[0].nodeName === 'HTML') { + // Fallback to white. + return 'rgb(255, 255, 255)'; + } + c = $e.css('background-color'); + // TRICKY: edge case for Firefox' "transparent" here; this is a + // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724 + if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') { + return this._getBgColor($e.parent()); + } + return c; + }, + + /** * Gets the top and left properties of an element and convert extraneous * values and information into numbers ready for subtraction. * diff --git a/core/modules/language/language.admin.inc b/core/modules/language/language.admin.inc index a3500fd..f59fb38 100644 --- a/core/modules/language/language.admin.inc +++ b/core/modules/language/language.admin.inc @@ -562,205 +562,6 @@ function language_negotiation_configure_form_submit($form, &$form_state) { } /** - * Builds the URL language negotiation method configuration form. - * - * @see language_negotiation_configure_url_form_validate() - * @see language_negotiation_configure_url_form_submit() - */ -function language_negotiation_configure_url_form($form, &$form_state) { - global $base_url; - language_negotiation_include(); - - $form['language_negotiation_url_part'] = array( - '#title' => t('Part of the URL that determines language'), - '#type' => 'radios', - '#options' => array( - LANGUAGE_NEGOTIATION_URL_PREFIX => t('Path prefix'), - LANGUAGE_NEGOTIATION_URL_DOMAIN => t('Domain'), - ), - '#default_value' => config('language.negotiation')->get('url.source'), - ); - - $form['prefix'] = array( - '#type' => 'details', - '#tree' => TRUE, - '#title' => t('Path prefix configuration'), - '#description' => t('Language codes or other custom text to use as a path prefix for URL language detection. For the default language, this value may be left blank. Modifying this value may break existing URLs. Use with caution in a production environment. Example: Specifying "deutsch" as the path prefix code for German results in URLs like "example.com/deutsch/contact".'), - '#states' => array( - 'visible' => array( - ':input[name="language_negotiation_url_part"]' => array( - 'value' => (string) LANGUAGE_NEGOTIATION_URL_PREFIX, - ), - ), - ), - ); - $form['domain'] = array( - '#type' => 'details', - '#tree' => TRUE, - '#title' => t('Domain configuration'), - '#description' => t('The domain names to use for these languages. Leave blank for the default language. Use with caution in a production environment.Modifying this value may break existing URLs. Use with caution in a production environment. Example: Specifying "de.example.com" as language domain for German will result in an URL like "http://de.example.com/contact".'), - '#states' => array( - 'visible' => array( - ':input[name="language_negotiation_url_part"]' => array( - 'value' => (string) LANGUAGE_NEGOTIATION_URL_DOMAIN, - ), - ), - ), - ); - - $languages = language_list(); - $prefixes = language_negotiation_url_prefixes(); - $domains = language_negotiation_url_domains(); - foreach ($languages as $langcode => $language) { - $t_args = array('%language' => $language->name, '%langcode' => $language->langcode); - $form['prefix'][$langcode] = array( - '#type' => 'textfield', - '#title' => $language->default ? t('%language (%langcode) path prefix (Default language)', $t_args) : t('%language (%langcode) path prefix', $t_args), - '#maxlength' => 64, - '#default_value' => isset($prefixes[$langcode]) ? $prefixes[$langcode] : '', - '#field_prefix' => $base_url . '/', - ); - $form['domain'][$langcode] = array( - '#type' => 'textfield', - '#title' => t('%language (%langcode) domain', array('%language' => $language->name, '%langcode' => $language->langcode)), - '#maxlength' => 128, - '#default_value' => isset($domains[$langcode]) ? $domains[$langcode] : '', - ); - } - - $form_state['redirect'] = 'admin/config/regional/language/detection'; - - return system_config_form($form, $form_state); -} - -/** - * Validates the URL language negotiation method configuration. - * - * Validate that the prefixes and domains are unique, and make sure that - * the prefix and domain are only blank for the default. - */ -function language_negotiation_configure_url_form_validate($form, &$form_state) { - $languages = language_list(); - - // Count repeated values for uniqueness check. - $count = array_count_values($form_state['values']['prefix']); - foreach ($languages as $langcode => $language) { - $value = $form_state['values']['prefix'][$langcode]; - - if ($value === '') { - if (!$language->default && $form_state['values']['language_negotiation_url_part'] == LANGUAGE_NEGOTIATION_URL_PREFIX) { - // Throw a form error if the prefix is blank for a non-default language, - // although it is required for selected negotiation type. - form_error($form['prefix'][$langcode], t('The prefix may only be left blank for the default language.')); - } - } - elseif (strpos($value, '/') !== FALSE) { - // Throw a form error if the string contains a slash, - // which would not work. - form_error($form['prefix'][$langcode], t('The prefix may not contain a slash.')); - } - elseif (isset($count[$value]) && $count[$value] > 1) { - // Throw a form error if there are two languages with the same - // domain/prefix. - form_error($form['prefix'][$langcode], t('The prefix for %language, %value, is not unique.', array('%language' => $language->name, '%value' => $value))); - } - } - - // Count repeated values for uniqueness check. - $count = array_count_values($form_state['values']['domain']); - foreach ($languages as $langcode => $language) { - $value = $form_state['values']['domain'][$langcode]; - - if ($value === '') { - if (!$language->default && $form_state['values']['language_negotiation_url_part'] == LANGUAGE_NEGOTIATION_URL_DOMAIN) { - // Throw a form error if the domain is blank for a non-default language, - // although it is required for selected negotiation type. - form_error($form['domain'][$langcode], t('The domain may only be left blank for the default language.')); - } - } - elseif (isset($count[$value]) && $count[$value] > 1) { - // Throw a form error if there are two languages with the same - // domain/domain. - form_error($form['domain'][$langcode], t('The domain for %language, %value, is not unique.', array('%language' => $language->name, '%value' => $value))); - } - } - - // Domain names should not contain protocol and/or ports. - foreach ($languages as $langcode => $name) { - $value = $form_state['values']['domain'][$langcode]; - if (!empty($value)) { - // Ensure we have exactly one protocol when checking the hostname. - $host = 'http://' . str_replace(array('http://', 'https://'), '', $value); - if (parse_url($host, PHP_URL_HOST) != $value) { - form_error($form['domain'][$langcode], t('The domain for %language may only contain the domain name, not a protocol and/or port.', array('%language' => $name))); - } - } - } -} - -/** - * Form submission handler for language_negotiation_configure_url_form(). - */ -function language_negotiation_configure_url_form_submit($form, &$form_state) { - // Save selected format (prefix or domain). - config('language.negotiation') - ->set('url.source', $form_state['values']['language_negotiation_url_part']) - ->save(); - - // Save new domain and prefix values. - language_negotiation_url_prefixes_save($form_state['values']['prefix']); - language_negotiation_url_domains_save($form_state['values']['domain']); -} - -/** - * Builds the session language negotiation method configuration form. - * - * @see language_negotiation_configure_session_form_submit() - */ -function language_negotiation_configure_session_form($form, &$form_state) { - $form['language_negotiation_session_param'] = array( - '#title' => t('Request/session parameter'), - '#type' => 'textfield', - '#default_value' => config('language.negotiation')->get('session.parameter'), - '#description' => t('Name of the request/session parameter used to determine the desired language.'), - ); - - $form_state['redirect'] = 'admin/config/regional/language/detection'; - - return system_config_form($form, $form_state); -} - -/** - * Form submission handler for language_negotiation_configure_session_form(). - */ -function language_negotiation_configure_session_form_submit($form, &$form_state) { - config('language.negotiation') - ->set('session.parameter', $form_state['values']['language_negotiation_session_param']) - ->save(); -} - -/** - * Builds the selected language negotiation method configuration form. - */ -function language_negotiation_configure_selected_form($form, &$form_state) { - $form['selected_langcode'] = array( - '#type' => 'language_select', - '#title' => t('Language'), - '#languages' => LANGUAGE_CONFIGURABLE | LANGUAGE_SITE_DEFAULT, - '#default_value' => config('language.negotiation')->get('selected_langcode'), - ); - - return system_config_form($form, $form_state); -} - -/** - * Form submission handler for language_negotiation_configure_selected_form(). - */ -function language_negotiation_configure_selected_form_submit($form, &$form_state) { - config('language.negotiation')->set('selected_langcode', $form_state['values']['selected_langcode'])->save(); -} - -/** * Builds the browser language negotiation method configuration form. */ function language_negotiation_configure_browser_form($form, &$form_state) { diff --git a/core/modules/language/language.module b/core/modules/language/language.module index 1c20695..e42316e 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -114,18 +114,14 @@ function language_menu() { ); $items['admin/config/regional/language/detection/url'] = array( 'title' => 'URL language detection configuration', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('language_negotiation_configure_url_form'), + 'page callback' => 'NOT_USED', 'access arguments' => array('administer languages'), - 'file' => 'language.admin.inc', 'type' => MENU_VISIBLE_IN_BREADCRUMB, ); $items['admin/config/regional/language/detection/session'] = array( 'title' => 'Session language detection configuration', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('language_negotiation_configure_session_form'), + 'page callback' => 'NOT_USED', 'access arguments' => array('administer languages'), - 'file' => 'language.admin.inc', 'type' => MENU_VISIBLE_IN_BREADCRUMB, ); $items['admin/config/regional/language/detection/browser'] = array( @@ -145,10 +141,8 @@ function language_menu() { ); $items['admin/config/regional/language/detection/selected'] = array( 'title' => 'Selected language detection configuration', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('language_negotiation_configure_selected_form'), + 'page callback' => 'NOT_USED', 'access arguments' => array('administer languages'), - 'file' => 'language.admin.inc', 'type' => MENU_VISIBLE_IN_BREADCRUMB, ); diff --git a/core/modules/language/language.routing.yml b/core/modules/language/language.routing.yml new file mode 100644 index 0000000..4a2fc8c --- /dev/null +++ b/core/modules/language/language.routing.yml @@ -0,0 +1,20 @@ +language_negotiation_url: + pattern: '/admin/config/regional/language/detection/url' + defaults: + _form: 'Drupal\language\Form\NegotiationUrlForm' + requirements: + _permission: 'administer languages' + +language_negotiation_session: + pattern: '/admin/config/regional/language/detection/session' + defaults: + _form: 'Drupal\language\Form\NegotiationSessionForm' + requirements: + _permission: 'administer languages' + +language_negotiation_selected: + pattern: '/admin/config/regional/language/detection/selected' + defaults: + _form: 'Drupal\language\Form\NegotiationSelectedForm' + requirements: + _permission: 'administer languages' diff --git a/core/modules/language/lib/Drupal/language/Form/NegotiationSelectedForm.php b/core/modules/language/lib/Drupal/language/Form/NegotiationSelectedForm.php new file mode 100644 index 0000000..9ef236d --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Form/NegotiationSelectedForm.php @@ -0,0 +1,50 @@ +configFactory->get('language.negotiation'); + $form['selected_langcode'] = array( + '#type' => 'language_select', + '#title' => t('Language'), + '#languages' => LANGUAGE_CONFIGURABLE | LANGUAGE_SITE_DEFAULT, + '#default_value' => $config->get('selected_langcode'), + ); + + return parent::buildForm($form, $form_state); + } + + /** + * Implements \Drupal\Core\Form\FormInterface::submitForm(). + */ + public function submitForm(array &$form, array &$form_state) { + $this->configFactory->get('language.negotiation') + ->set('selected_langcode', $form_state['values']['selected_langcode']) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/core/modules/language/lib/Drupal/language/Form/NegotiationSessionForm.php b/core/modules/language/lib/Drupal/language/Form/NegotiationSessionForm.php new file mode 100644 index 0000000..7372d81 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Form/NegotiationSessionForm.php @@ -0,0 +1,52 @@ +configFactory->get('language.negotiation'); + $form['language_negotiation_session_param'] = array( + '#title' => t('Request/session parameter'), + '#type' => 'textfield', + '#default_value' => $config->get('session.parameter'), + '#description' => t('Name of the request/session parameter used to determine the desired language.'), + ); + + $form_state['redirect'] = 'admin/config/regional/language/detection'; + + return parent::buildForm($form, $form_state); + } + + /** + * Implements \Drupal\Core\Form\FormInterface::submitForm(). + */ + public function submitForm(array &$form, array &$form_state) { + $this->configFactory->get('language.settings') + ->set('session.parameter', $form_state['values']['language_negotiation_session_param']) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/core/modules/language/lib/Drupal/language/Form/NegotiationUrlForm.php b/core/modules/language/lib/Drupal/language/Form/NegotiationUrlForm.php new file mode 100644 index 0000000..9a95329 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Form/NegotiationUrlForm.php @@ -0,0 +1,174 @@ +configFactory->get('language.negotiation'); + language_negotiation_include(); + + $form['language_negotiation_url_part'] = array( + '#title' => t('Part of the URL that determines language'), + '#type' => 'radios', + '#options' => array( + LANGUAGE_NEGOTIATION_URL_PREFIX => t('Path prefix'), + LANGUAGE_NEGOTIATION_URL_DOMAIN => t('Domain'), + ), + '#default_value' => $config->get('url.source'), + ); + + $form['prefix'] = array( + '#type' => 'details', + '#tree' => TRUE, + '#title' => t('Path prefix configuration'), + '#description' => t('Language codes or other custom text to use as a path prefix for URL language detection. For the default language, this value may be left blank. Modifying this value may break existing URLs. Use with caution in a production environment. Example: Specifying "deutsch" as the path prefix code for German results in URLs like "example.com/deutsch/contact".'), + '#states' => array( + 'visible' => array( + ':input[name="language_negotiation_url_part"]' => array( + 'value' => (string) LANGUAGE_NEGOTIATION_URL_PREFIX, + ), + ), + ), + ); + $form['domain'] = array( + '#type' => 'details', + '#tree' => TRUE, + '#title' => t('Domain configuration'), + '#description' => t('The domain names to use for these languages. Leave blank for the default language. Use with caution in a production environment.Modifying this value may break existing URLs. Use with caution in a production environment. Example: Specifying "de.example.com" as language domain for German will result in an URL like "http://de.example.com/contact".'), + '#states' => array( + 'visible' => array( + ':input[name="language_negotiation_url_part"]' => array( + 'value' => (string) LANGUAGE_NEGOTIATION_URL_DOMAIN, + ), + ), + ), + ); + + $languages = language_list(); + $prefixes = language_negotiation_url_prefixes(); + $domains = language_negotiation_url_domains(); + foreach ($languages as $langcode => $language) { + $t_args = array('%language' => $language->name, '%langcode' => $language->langcode); + $form['prefix'][$langcode] = array( + '#type' => 'textfield', + '#title' => $language->default ? t('%language (%langcode) path prefix (Default language)', $t_args) : t('%language (%langcode) path prefix', $t_args), + '#maxlength' => 64, + '#default_value' => isset($prefixes[$langcode]) ? $prefixes[$langcode] : '', + '#field_prefix' => $base_url . '/', + ); + $form['domain'][$langcode] = array( + '#type' => 'textfield', + '#title' => t('%language (%langcode) domain', array('%language' => $language->name, '%langcode' => $language->langcode)), + '#maxlength' => 128, + '#default_value' => isset($domains[$langcode]) ? $domains[$langcode] : '', + ); + } + + $form_state['redirect'] = 'admin/config/regional/language/detection'; + + return parent::buildForm($form, $form_state); + } + + /** + * Implements \Drupal\Core\Form\FormInterface::validateForm(). + */ + public function validateForm(array &$form, array &$form_state) { + $languages = language_list(); + + // Count repeated values for uniqueness check. + $count = array_count_values($form_state['values']['prefix']); + foreach ($languages as $langcode => $language) { + $value = $form_state['values']['prefix'][$langcode]; + + if ($value === '') { + if (!$language->default && $form_state['values']['language_negotiation_url_part'] == LANGUAGE_NEGOTIATION_URL_PREFIX) { + // Throw a form error if the prefix is blank for a non-default language, + // although it is required for selected negotiation type. + form_error($form['prefix'][$langcode], t('The prefix may only be left blank for the default language.')); + } + } + elseif (strpos($value, '/') !== FALSE) { + // Throw a form error if the string contains a slash, + // which would not work. + form_error($form['prefix'][$langcode], t('The prefix may not contain a slash.')); + } + elseif (isset($count[$value]) && $count[$value] > 1) { + // Throw a form error if there are two languages with the same + // domain/prefix. + form_error($form['prefix'][$langcode], t('The prefix for %language, %value, is not unique.', array('%language' => $language->name, '%value' => $value))); + } + } + + // Count repeated values for uniqueness check. + $count = array_count_values($form_state['values']['domain']); + foreach ($languages as $langcode => $language) { + $value = $form_state['values']['domain'][$langcode]; + + if ($value === '') { + if (!$language->default && $form_state['values']['language_negotiation_url_part'] == LANGUAGE_NEGOTIATION_URL_DOMAIN) { + // Throw a form error if the domain is blank for a non-default language, + // although it is required for selected negotiation type. + form_error($form['domain'][$langcode], t('The domain may only be left blank for the default language.')); + } + } + elseif (isset($count[$value]) && $count[$value] > 1) { + // Throw a form error if there are two languages with the same + // domain/domain. + form_error($form['domain'][$langcode], t('The domain for %language, %value, is not unique.', array('%language' => $language->name, '%value' => $value))); + } + } + + // Domain names should not contain protocol and/or ports. + foreach ($languages as $langcode => $name) { + $value = $form_state['values']['domain'][$langcode]; + if (!empty($value)) { + // Ensure we have exactly one protocol when checking the hostname. + $host = 'http://' . str_replace(array('http://', 'https://'), '', $value); + if (parse_url($host, PHP_URL_HOST) != $value) { + form_error($form['domain'][$langcode], t('The domain for %language may only contain the domain name, not a protocol and/or port.', array('%language' => $name))); + } + } + } + + parent::validateForm($form, $form_state); + } + + /** + * Implements \Drupal\Core\Form\FormInterface::submitForm(). + */ + public function submitForm(array &$form, array &$form_state) { + // Save selected format (prefix or domain). + $this->configFactory->get('language.negotiation') + ->set('url.source', $form_state['values']['language_negotiation_url_part']) + ->save(); + + // Save new domain and prefix values. + language_negotiation_url_prefixes_save($form_state['values']['prefix']); + language_negotiation_url_domains_save($form_state['values']['domain']); + + parent::submitForm($form, $form_state); + } + +} diff --git a/core/modules/overlay/overlay-parent.js b/core/modules/overlay/overlay-parent.js index 3087c26..0abb950 100644 --- a/core/modules/overlay/overlay-parent.js +++ b/core/modules/overlay/overlay-parent.js @@ -7,6 +7,9 @@ "use strict"; +// The set of elements a user may tab between when the overlay is open. +var tabset; + /** * Open the overlay, or load content into it, when an admin link is clicked. */ @@ -203,6 +206,8 @@ Drupal.overlay.close = function () { // Allow other scripts to respond to this event. $(document).trigger('drupalOverlayClose'); + Drupal.announce(Drupal.t('Tabbing is no longer constrained by the Overlay module.')); + // When the iframe is still loading don't destroy it immediately but after // the content is loaded (see Drupal.overlay.loadChild). if (!this.isLoading) { @@ -295,6 +300,8 @@ Drupal.overlay.loadChild = function (event) { .attr('title', Drupal.t('@title dialog', { '@title': iframeWindow.jQuery('#overlay-title').text() })).removeAttr('tabindex'); this.inactiveFrame = event.data.sibling; + Drupal.announce(Drupal.t('The overlay has been opened to @title', {'@title': iframeWindow.jQuery('#overlay-title').text()})); + // Load an empty document into the inactive iframe. (this.inactiveFrame[0].contentDocument || this.inactiveFrame[0].contentWindow.document).location.replace('about:blank'); @@ -306,6 +313,8 @@ Drupal.overlay.loadChild = function (event) { Drupal.overlay.releaseTabbing(); Drupal.overlay.constrainTabbing($(iframeWindow.document).add('#toolbar-administration')); + Drupal.announce(Drupal.t('Tabbing is constrained to items in the administrative toolbar and the overlay.')); + // Allow other scripts to respond to this event. $(document).trigger('drupalOverlayLoad'); } diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module index 4686d3d..a186ea2 100644 --- a/core/modules/overlay/overlay.module +++ b/core/modules/overlay/overlay.module @@ -225,6 +225,7 @@ function overlay_library_info() { array('system', 'jquery'), array('system', 'drupal'), array('system', 'drupalSettings'), + array('system', 'drupal.announce'), array('system', 'drupal.displace'), array('system', 'drupal.tabbingmanager'), array('system', 'jquery.ui.core'), @@ -246,6 +247,7 @@ function overlay_library_info() { array('system', 'jquery'), array('system', 'drupal'), array('system', 'drupalSettings'), + array('system', 'drupal.announce'), array('system', 'jquery.once'), ), ); diff --git a/core/modules/search/search.pages.inc b/core/modules/search/search.pages.inc index 680f9a8..eaae71a 100644 --- a/core/modules/search/search.pages.inc +++ b/core/modules/search/search.pages.inc @@ -72,15 +72,15 @@ function search_view($module = NULL, $keys = '') { } /** - * Prepocesses the variables for search-results.tpl.php. + * Prepares variables for search results templates. * - * @param $variables + * Default template: search-results.html.twig. + * + * @param array $variables * An array with the following elements: * - results: Search results array. * - module: Module the search results came from (module implementing * hook_search_info()). - * - * @see search-results.tpl.php */ function template_preprocess_search_results(&$variables) { $variables['search_results'] = ''; @@ -88,22 +88,38 @@ function template_preprocess_search_results(&$variables) { $variables['module'] = check_plain($variables['module']); } foreach ($variables['results'] as $result) { - $variables['search_results'] .= theme('search_result', array('result' => $result, 'module' => $variables['module'])); + $variables['search_results'][] = array( + '#theme' => 'search_result', + '#result' => $result, + '#module' => $variables['module'], + ); } - $variables['pager'] = theme('pager', array('tags' => NULL)); + $variables['pager'] = array( + '#theme' => 'pager', + '#tags' => NULL, + ); + // @todo Revisit where this help text is added, see also + // http://drupal.org/node/1918856. + $variables['help'] = search_help('search#noresults', drupal_help_arg()); $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module']; } /** - * Preprocesses the variables for search-result.tpl.php. + * Prepares variables for individual search result templates. * - * @param $variables + * Default template: search-result.html.twig + * + * @param array $variables * An array with the following elements: - * - result: Search results array. + * - result: Individual search result. * - module: Module the search results came from (module implementing * hook_search_info()). - * - * @see search-result.tpl.php + * - title_prefix: Additional output populated by modules, intended to be + * displayed in front of the main title tag that appears in the template. + * - title_suffix: Additional output populated by modules, intended to be + * displayed after the main title tag that appears in the template. + * - title_attributes: HTML attributes for the title. + * - content_attributes: HTML attributes for the content. */ function template_preprocess_search_result(&$variables) { $language_interface = language(LANGUAGE_TYPE_INTERFACE); diff --git a/core/modules/search/templates/search-result.html.twig b/core/modules/search/templates/search-result.html.twig new file mode 100644 index 0000000..be4ed35 --- /dev/null +++ b/core/modules/search/templates/search-result.html.twig @@ -0,0 +1,76 @@ +{# +/** + * @file + * Default theme implementation for displaying a single search result. + * + * This template renders a single search result and is collected into + * search-results.html.twig. This and the parent template are + * dependent to one another sharing the markup for ordered lists. + * + * Available variables: + * - url: URL of the result. + * - title: Title of the result. + * - snippet: A small preview of the result. Does not apply to user searches. + * - info: String of all the meta information ready for print. Does not apply + * to user searches. + * - module: The machine-readable name of the module (tab) being searched, such + * as "node" or "user". + * - title_prefix: Additional output populated by modules, intended to be + * displayed in front of the main title tag that appears in the template. + * - title_suffix: Additional output populated by modules, intended to be + * displayed after the main title tag that appears in the template. + * - info_split: Contains same data as info, but split into separate parts. + * - info_split.type: Node type (or item type string supplied by module). + * - info_split.user: Author of the node linked to users profile. Depends + * on permission. + * - info_split.date: Last update of the node. Short formatted. + * - info_split.comment: Number of comments output as "% comments", % + * being the count. (Depends on comment.module). + * @todo The info variable needs to be made drillable and each of these sub + * items should instead be within info and renamed info.foo, info.bar, etc. + * + * Other variables: + * - title_attributes: HTML attributes for the title. + * - content_attributes: HTML attributes for the content. + * + * Since info_split is keyed, a direct print of the item is possible. + * This array does not apply to user searches so it is recommended to check + * for its existence before printing. The default keys of 'type', 'user' and + * 'date' always exist for node searches. Modules may provide other data. + * @code + * {% if (info_split.comment) %} + * + * {{ info_split.comment }} + * + * {% endif %} + * @endcode + * + * To check for all available data within info_split, use the code below. + * @code + *
+ *     {{ dump(info_split) }}
+ *   
+ * @endcode + * + * @see template_preprocess() + * @see template_preprocess_search_result() + * @see template_process() + * + * @ingroup themeable + */ +#} +
  • + {{ title_prefix }} +

    + {{ title }} +

    + {{ title_suffix }} +
    + {% if snippet %} +

    {{ snippet }}

    + {% endif %} + {% if info %} +

    {{ info }}

    + {% endif %} +
    +
  • diff --git a/core/modules/search/templates/search-result.tpl.php b/core/modules/search/templates/search-result.tpl.php deleted file mode 100644 index 850797b..0000000 --- a/core/modules/search/templates/search-result.tpl.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * - * - * - * @endcode - * - * To check for all available data within $info_split, use the code below. - * @code - * '. check_plain(print_r($info_split, 1)) .''; ?> - * @endcode - * - * @see template_preprocess() - * @see template_preprocess_search_result() - * @see template_process() - * - * @ingroup themeable - */ -?> -
  • > - -

    > - -

    - -
    - -

    >

    - - -

    - -
    -
  • diff --git a/core/modules/search/templates/search-results.html.twig b/core/modules/search/templates/search-results.html.twig new file mode 100644 index 0000000..7ae530a --- /dev/null +++ b/core/modules/search/templates/search-results.html.twig @@ -0,0 +1,35 @@ +{# +/** + * @file + * Default theme implementation for displaying search results. + * + * This template collects each invocation of theme_search_result(). This and + * the child template are dependent to one another sharing the markup for + * definition lists. + * + * Note that modules may implement their own search type and theme function + * completely bypassing this template. + * + * Available variables: + * - search_results: All results as it is rendered through + * search-result.html.twig. + * - module: The machine-readable name of the module (tab) being searched, such + * as 'node' or 'user'. + * - pager: The pager next/prev links to display, if any. + * - help: HTML for help text to display when no results are found. + * + * @see template_preprocess() + * @see template_preprocess_search_results() + * + * @ingroup themeable +#} +{% if search_results %} +

    {{ 'Search results'|t }}

    +
      + {{ search_results }} +
    + {{ pager }} +{% else %} +

    {{ 'Your search yielded no results'|t }}

    + {{ help }} +{% endif %} diff --git a/core/modules/search/templates/search-results.tpl.php b/core/modules/search/templates/search-results.tpl.php deleted file mode 100644 index aa9bf8d..0000000 --- a/core/modules/search/templates/search-results.tpl.php +++ /dev/null @@ -1,35 +0,0 @@ - - -

    -
      - -
    - - -

    - - diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 92e2e2b..4a42d43 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1248,6 +1248,19 @@ function system_library_info() { ), ); + // Drupal's Screen Reader change announcement utility. + $libraries['drupal.announce'] = array( + 'title' => 'Drupal announce', + 'version' => VERSION, + 'js' => array( + 'core/misc/announce.js' => array('group' => JS_LIBRARY), + ), + 'dependencies' => array( + array('system', 'drupal'), + array('system', 'drupal.debounce'), + ), + ); + // Drupal's batch API. $libraries['drupal.batch'] = array( 'title' => 'Drupal batch API', @@ -1420,7 +1433,7 @@ function system_library_info() { 'title' => 'Drupal tabbing manager', 'version' => VERSION, 'js' => array( - 'core/misc/tabbingmanager.js' => array('group' => JS_LIBRARY), + 'core/misc/tabbingmanager.js' => array('group', JS_LIBRARY), ), 'dependencies' => array( array('system', 'jquery'), diff --git a/core/modules/tour/config/schema/tour.schema.yml b/core/modules/tour/config/schema/tour.schema.yml new file mode 100644 index 0000000..3975fb4 --- /dev/null +++ b/core/modules/tour/config/schema/tour.schema.yml @@ -0,0 +1,60 @@ +# Schema for the configuration files of the Tour module. + +tour.tour.*: + type: mapping + label: 'Tour settings' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + langcode: + type: string + label: 'Language' + paths: + type: sequence + label: 'Path settings' + sequence: + - type: path + label: 'Path' + tips: + type: sequence + label: 'Tips' + sequence: + - type: tour.tip.[plugin] + label: 'Tour tip' + +tour.tip: + type: mapping + label: 'Tour tip' + mapping: + id: + type: string + label: 'ID' + plugin: + type: string + label: 'Plugin' + label: + type: label + label: 'Label' + weight: + type: integer + label: 'Weight' + attributes: + type: sequence + label: 'Attributes' + sequence: + - type: string + label: 'Attribute' + +tour.tip.text: + type: tour.tip + label: 'Textual tour tip' + mapping: + body: + type: text + label: 'Body' + + diff --git a/core/modules/tour/tests/tour_test/config/schema/tour_test.schema.yml b/core/modules/tour/tests/tour_test/config/schema/tour_test.schema.yml new file mode 100644 index 0000000..a43cc26 --- /dev/null +++ b/core/modules/tour/tests/tour_test/config/schema/tour_test.schema.yml @@ -0,0 +1,9 @@ +# Schema for the configuration files of the Tour Test module. + +tour.tip.image: + type: tour.tip + label: 'Image tour tip' + mapping: + url: + type: uri + label: 'Image URL'