diff --git a/core/includes/common.inc b/core/includes/common.inc index 5a46d80..50e7622 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -433,42 +433,6 @@ function date_iso8601($date) { } /** - * Translates a formatted date string. - * - * Callback for preg_replace_callback() within format_date(). - */ -function _format_date_callback(array $matches = NULL, $new_langcode = NULL) { - // We cache translations to avoid redundant and rather costly calls to t(). - static $cache, $langcode; - - if (!isset($matches)) { - $langcode = $new_langcode; - return; - } - - $code = $matches[1]; - $string = $matches[2]; - - if (!isset($cache[$langcode][$code][$string])) { - $options = array( - 'langcode' => $langcode, - ); - - if ($code == 'F') { - $options['context'] = 'Long month name'; - } - - if ($code == '') { - $cache[$langcode][$code][$string] = $string; - } - else { - $cache[$langcode][$code][$string] = t($string, array(), $options); - } - } - return $cache[$langcode][$code][$string]; -} - -/** * @} End of "defgroup format". */ diff --git a/core/lib/Drupal/Core/Datetime/DateFormatter.php b/core/lib/Drupal/Core/Datetime/DateFormatter.php index 0b4f817..7b56e77 100644 --- a/core/lib/Drupal/Core/Datetime/DateFormatter.php +++ b/core/lib/Drupal/Core/Datetime/DateFormatter.php @@ -193,6 +193,33 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) { } /** + * Provides values for all date formatting characters for a given timestamp. + * + * @param string|null $langcode + * (optional) Language code of the date format, if different from the site + * default language. + * @param int|null $timestamp + * (optional) The Unix timestamp to format, defaults to the request time. + * @param string|null $timezone + * (optional) The timezone to use, if different from the site's default + * timezone. + * + * @return array + * An array of formatted date values, indexed by the date format character. + * + * @see date() + */ + public function getSampleDateFormats($langcode = NULL, $timestamp = NULL, $timezone = NULL) { + $timestamp = $timestamp ?: (int) $_SERVER['REQUEST_TIME']; + // All date format characters for the PHP date() function. + $date_chars = str_split('dDjlNSwzWFmMntLoYyaABgGhHisueIOPTZcrU'); + $date_elements = array_combine($date_chars, $date_chars); + return array_map(function($character) use ($timestamp, $timezone, $langcode) { + return $this->format($timestamp, 'custom', $character, $timezone, $langcode); + }, $date_elements); + } + + /** * Loads the given format pattern for the given langcode. * * @param string $format @@ -200,8 +227,9 @@ public function formatInterval($interval, $granularity = 2, $langcode = NULL) { * @param string $langcode * The langcode of the language to use. * - * @return string - * The pattern for the date format in the given language. + * @return string|null + * The pattern for the date format in the given language for non-custom + * formats, NULL otherwise. */ protected function dateFormat($format, $langcode) { if (!isset($this->dateFormats[$format][$langcode])) { diff --git a/core/lib/Drupal/Core/Datetime/DrupalDateTime.php b/core/lib/Drupal/Core/Datetime/DrupalDateTime.php index 74bd68b..b55edac 100644 --- a/core/lib/Drupal/Core/Datetime/DrupalDateTime.php +++ b/core/lib/Drupal/Core/Datetime/DrupalDateTime.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Datetime; use Drupal\Component\Datetime\DateTimePlus; +use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Extends DateTimePlus(). @@ -23,6 +24,14 @@ */ class DrupalDateTime extends DateTimePlus { + use StringTranslationTrait; + + /** + * Format string translation cache. + * + */ + protected $formatTranslationCache; + /** * Constructs a date object. * @@ -84,7 +93,7 @@ protected function prepareTimezone($timezone) { * The formatted value of the date. */ public function format($format, $settings = array()) { - $settings['langcode'] = !empty($settings['langcode']) ? $settings['langcode'] : $this->langcode; + $langcode = !empty($settings['langcode']) ? $settings['langcode'] : $this->langcode; // Format the date and catch errors. try { // Encode markers that should be translated. 'A' becomes @@ -98,15 +107,33 @@ public function format($format, $settings = array()) { // Call date_format(). $format = parent::format($format); - // Pass the langcode to _format_date_callback(). - _format_date_callback(NULL, $settings['langcode']); + // Translates a formatted date string. + $translation_callback = function($matches) use ($langcode) { + $code = $matches[1]; + $string = $matches[2]; + if (!isset($this->formatTranslationCache[$langcode][$code][$string])) { + $options = array('langcode' => $langcode); + if ($code == 'F') { + $options['context'] = 'Long month name'; + } + + if ($code == '') { + $this->formatTranslationCache[$langcode][$code][$string] = $string; + } + else { + $this->formatTranslationCache[$langcode][$code][$string] = $this->t($string, array(), $options); + } + } + return $this->formatTranslationCache[$langcode][$code][$string]; + }; // Translate the marked sequences. - $value = preg_replace_callback('/\xEF([AaeDlMTF]?)(.*?)\xFF/', '_format_date_callback', $format); + $value = preg_replace_callback('/\xEF([AaeDlMTF]?)(.*?)\xFF/', $translation_callback, $format); } catch (\Exception $e) { $this->errors[] = $e->getMessage(); } return $value; } + } diff --git a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php index 9e5372f..c4dfa10 100644 --- a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php +++ b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php @@ -176,6 +176,9 @@ public function buildForm(array $form, FormStateInterface $form_state, Request $ if ($form_element = $this->createFormElement($schema)) { $parents = array('config_names', $name); $form['config_names'][$name] += $form_element->getTranslationBuild($this->sourceLanguage, $this->language, $source_config, $translation_config, $parents); + if ($attributes = $form_element->getFormAttributes()) { + $form = array_merge_recursive($form, $attributes); + } } } diff --git a/core/modules/config_translation/src/FormElement/DateFormat.php b/core/modules/config_translation/src/FormElement/DateFormat.php index 729afee..45aab74 100644 --- a/core/modules/config_translation/src/FormElement/DateFormat.php +++ b/core/modules/config_translation/src/FormElement/DateFormat.php @@ -22,51 +22,29 @@ class DateFormat extends FormElementBase { * {@inheritdoc} */ public function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) { + /** @var \Drupal\Core\Datetime\DateFormatter $date_formatter */ + $date_formatter = \Drupal::service('date.formatter'); $description = $this->t('A user-defined date format. See the PHP manual for available options.', array('@url' => 'http://php.net/manual/function.date.php')); - $format = $this->t('Displayed as %date_format', array('%date_format' => \Drupal::service('date.formatter')->format(REQUEST_TIME, 'custom', $translation_config))); + $format = $this->t('Displayed as %date_format', array('%date_format' => $date_formatter->format(REQUEST_TIME, 'custom', $translation_config))); - return array( + return [ '#type' => 'textfield', '#description' => $description, - '#field_suffix' => '
' . $format . '
', - '#ajax' => array( - 'callback' => 'Drupal\config_translation\FormElement\DateFormat::ajaxSample', - 'event' => 'keyup', - 'progress' => array('type' => 'throbber', 'message' => NULL), - ), - ) + parent::getTranslationElement($translation_language, $source_config, $translation_config); + '#field_suffix' => ' ' . $format . '', + '#attributes' => [ + 'data-drupal-date-formatter' => 'source', + ], + '#attached' => [ + 'drupalSettings' => array('dateFormats' => $date_formatter->getSampleDateFormats($translation_language->getId())), + ], + ] + parent::getTranslationElement($translation_language, $source_config, $translation_config); } /** - * Ajax callback to render a sample of the input date format. - * - * @param array $form - * Form API array structure. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * Form state information. - * - * @return AjaxResponse - * Ajax response with the rendered sample date using the given format. If - * the given format cannot be identified or was empty, the response will - * be empty as well. + * {@inheritdoc} */ - public static function ajaxSample(array $form, FormStateInterface $form_state) { - $response = new AjaxResponse(); - - $format_value = NestedArray::getValue($form_state->getValues(), $form_state->getTriggeringElement()['#array_parents']); - if (!empty($format_value)) { - // Format the date with a custom date format with the given pattern. - // The object is not instantiated in an Ajax context, so $this->t() - // cannot be used here. - $format = t('Displayed as %date_format', array('%date_format' => \Drupal::service('date.formatter')->format(REQUEST_TIME, 'custom', $format_value))); - - // Return a command instead of a string, since the Ajax framework - // automatically prepends an additional empty DIV element for a string, - // which breaks the layout. - $response->addCommand(new ReplaceCommand('#edit-date-format-suffix', '' . $format . '')); - } - - return $response; + public function getFormAttributes() { + return ['#attached' => ['library' => ['system/drupal.system.date']]]; } } diff --git a/core/modules/config_translation/src/FormElement/ElementInterface.php b/core/modules/config_translation/src/FormElement/ElementInterface.php index 286eeba..c9fb0b9 100644 --- a/core/modules/config_translation/src/FormElement/ElementInterface.php +++ b/core/modules/config_translation/src/FormElement/ElementInterface.php @@ -70,4 +70,12 @@ public function getTranslationBuild(LanguageInterface $source_language, Language */ public function setConfig(Config $base_config, LanguageConfigOverride $config_translation, $config_values, $base_key = NULL); + /** + * Allows to provide form attributes for the configuration form. + * + * @return array + * An array of form attributes. + */ + public function getFormAttributes(); + } diff --git a/core/modules/config_translation/src/FormElement/FormElementBase.php b/core/modules/config_translation/src/FormElement/FormElementBase.php index e13cc31..ef76ef4 100644 --- a/core/modules/config_translation/src/FormElement/FormElementBase.php +++ b/core/modules/config_translation/src/FormElement/FormElementBase.php @@ -75,6 +75,13 @@ public function getTranslationBuild(LanguageInterface $source_language, Language } /** + * {@inheritdoc} + */ + public function getFormAttributes() { + return []; + } + + /** * Returns the source element for a given configuration definition. * * This can be either a render array that actually outputs the source values diff --git a/core/modules/config_translation/src/FormElement/ListElement.php b/core/modules/config_translation/src/FormElement/ListElement.php index 9365fc2..4723203 100644 --- a/core/modules/config_translation/src/FormElement/ListElement.php +++ b/core/modules/config_translation/src/FormElement/ListElement.php @@ -97,6 +97,23 @@ public function setConfig(Config $base_config, LanguageConfigOverride $config_tr } /** + * {@inheritdoc} + */ + public function getFormAttributes() { + $attributes = []; + foreach ($this->element as $key => $element) { + if ($form_element = ConfigTranslationFormBase::createFormElement($element)) { + $form_attributes = $form_element->getFormAttributes(); + if (empty($form_attributes)) { + continue; + } + $attributes += $form_attributes; + } + } + return $attributes; + } + + /** * Returns the title for the 'details' element of a group of schema elements. * * For some configuration elements the same element structure can be repeated diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php index 807307c..00b36c5 100644 --- a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php +++ b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php @@ -446,6 +446,9 @@ public function testDateFormatTranslation() { $this->drupalGet($translation_page_url); $this->assertText($label); + // Make sure that the date library is added. + $this->assertRaw('core/modules/system/js/system.date.js'); + // Update translatable fields. $edit = array( 'translation[config_names][core.date_format.' . $id . '][label]' => $id . ' - FR', diff --git a/core/modules/system/js/system.date.js b/core/modules/system/js/system.date.js new file mode 100644 index 0000000..fcab3ef --- /dev/null +++ b/core/modules/system/js/system.date.js @@ -0,0 +1,46 @@ +(function ($, Drupal, drupalSettings) { + + "use strict"; + + var dateFormats = drupalSettings.dateFormats; + + /** + * Display the preview for date format entered. + */ + Drupal.behaviors.dateFormat = { + attach: function (context) { + var $context = $(context); + var $source = $context.find('[data-drupal-date-formatter="source"]').once('dateFormat'); + var $target = $context.find('[data-drupal-date-formatter="preview"]').once('dateFormat'); + var $preview = $target.find('em'); + + // All elements have to exist. + if (!$source.length || !$target.length) { + return; + } + + /** + * Event handler that replaces date characters with value. + * + * @param {object} e + */ + function dateFormatHandler(e) { + var baseValue = $(e.target).val() || ''; + var dateString = baseValue.replace(/\\?(.?)/gi, function (key, value) { + return dateFormats[key] ? dateFormats[key] : value; + }); + + $preview.html(dateString); + $target.toggleClass('js-hide', !dateString.length); + } + + /** + * On given event triggers the date character replacement. + */ + $source.on('keyup.dateFormat change.dateFormat input.dateFormat', dateFormatHandler) + // Initialize preview. + .trigger('keyup'); + } + }; + +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/system/system.js b/core/modules/system/js/system.js similarity index 100% rename from core/modules/system/system.js rename to core/modules/system/js/system.js diff --git a/core/modules/system/system.modules.js b/core/modules/system/js/system.modules.js similarity index 100% rename from core/modules/system/system.modules.js rename to core/modules/system/js/system.modules.js diff --git a/core/modules/system/src/Form/DateFormatEditForm.php b/core/modules/system/src/Form/DateFormatEditForm.php index f8e780f..511ad1a 100644 --- a/core/modules/system/src/Form/DateFormatEditForm.php +++ b/core/modules/system/src/Form/DateFormatEditForm.php @@ -21,7 +21,7 @@ public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); $now = t('Displayed as %date', array('%date' => $this->dateFormatter->format(REQUEST_TIME, $this->entity->id()))); - $form['date_format_pattern']['#field_suffix'] = ' ' . $now . ''; + $form['date_format_pattern']['#field_suffix'] = ' ' . $now . ''; $form['date_format_pattern']['#default_value'] = $this->entity->getPattern(); return $form; diff --git a/core/modules/system/src/Form/DateFormatFormBase.php b/core/modules/system/src/Form/DateFormatFormBase.php index c843ba2..9061d89 100644 --- a/core/modules/system/src/Form/DateFormatFormBase.php +++ b/core/modules/system/src/Form/DateFormatFormBase.php @@ -80,30 +80,6 @@ public function exists($entity_id, array $element) { } /** - * Returns the date for a given format string. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return \Drupal\Core\Ajax\AjaxResponse - * An AJAX Response to update the date-time value of the date format. - */ - public static function dateTimeLookup(array $form, FormStateInterface $form_state) { - $format = ''; - if (!$form_state->isValueEmpty('date_format_pattern')) { - $format = t('Displayed as %date_format', array('%date_format' => \Drupal::service('date.formatter')->format(REQUEST_TIME, 'custom', $form_state->getValue('date_format_pattern')))); - } - // Return a command instead of a string, since the Ajax framework - // automatically prepends an additional empty DIV element for a string, which - // breaks the layout. - $response = new AjaxResponse(); - $response->addCommand(new ReplaceCommand('#edit-date-format-suffix', '' . $format . '')); - return $response; - } - - /** * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { @@ -126,20 +102,16 @@ public function form(array $form, FormStateInterface $form_state) { 'error' => $this->t('The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".'), ), ); - $form['date_format_pattern'] = array( '#type' => 'textfield', '#title' => t('Format string'), '#maxlength' => 100, '#description' => $this->t('A user-defined date format. See the PHP manual for available options.', array('@url' => 'http://php.net/manual/function.date.php')), - '#default_value' => '', - '#field_suffix' => ' ', - '#ajax' => array( - 'callback' => '::dateTimeLookup', - 'event' => 'keyup', - 'progress' => array('type' => 'throbber', 'message' => NULL), - ), '#required' => TRUE, + '#attributes' => [ + 'data-drupal-date-formatter' => 'source', + ], + '#field_suffix' => ' ' . $this->t('Displayed as %date_format', ['%date_format' => '']) . '', ); $form['langcode'] = array( @@ -148,7 +120,8 @@ public function form(array $form, FormStateInterface $form_state) { '#languages' => LanguageInterface::STATE_ALL, '#default_value' => $this->entity->language()->getId(), ); - + $form['#attached']['drupalSettings']['dateFormats'] = $this->dateFormatter->getSampleDateFormats(); + $form['#attached']['library'][] = 'system/drupal.system.date'; return parent::form($form, $form_state); } diff --git a/core/modules/system/system.libraries.yml b/core/modules/system/system.libraries.yml index 34c90dd..b440e15 100644 --- a/core/modules/system/system.libraries.yml +++ b/core/modules/system/system.libraries.yml @@ -29,7 +29,7 @@ maintenance: drupal.system: version: VERSION js: - system.js: {} + js/system.js: {} dependencies: - core/jquery - core/drupal @@ -39,7 +39,7 @@ drupal.system: drupal.system.modules: version: VERSION js: - system.modules.js: {} + js/system.modules.js: {} dependencies: - core/jquery - core/drupal @@ -50,3 +50,14 @@ diff: css: component: css/system.diff.css: {} + +drupal.system.date: + version: VERSION + js: + js/system.date.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupalSettings + - core/jquery.once + - core/drupal.form diff --git a/core/tests/Drupal/Tests/Core/Datetime/DateTest.php b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php index da9ebaa..4786ee7 100644 --- a/core/tests/Drupal/Tests/Core/Datetime/DateTest.php +++ b/core/tests/Drupal/Tests/Core/Datetime/DateTest.php @@ -5,9 +5,10 @@ * Contains \Drupal\Tests\Core\Datetime\DateTest. */ -namespace Drupal\Tests\Core\Datetime; +namespace Drupal\Tests\Core\Datetime { use Drupal\Core\Datetime\DateFormatter; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Tests\UnitTestCase; /** @@ -45,10 +46,21 @@ class DateTest extends UnitTestCase { protected $dateFormatter; protected function setUp() { + parent::setUp(); + + $entity_storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface'); + $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + $this->entityManager->expects($this->once())->method('getStorage')->with('date_format')->willReturn($entity_storage); + $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface'); $this->stringTranslation = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface'); + $config_factory = $this->getConfigFactoryStub(['system.date' => ['country' => ['default' => 'GB']]]); + $container = new ContainerBuilder(); + $container->set('config.factory', $config_factory); + \Drupal::setContainer($container); + $this->dateFormatter = new DateFormatter($this->entityManager, $this->languageManager, $this->stringTranslation, $this->getConfigFactoryStub()); } @@ -57,7 +69,7 @@ protected function setUp() { * * @dataProvider providerTestFormatInterval * - * @see \Drupal\Core\Datetime\DateFormatter::formatInterval() + * @covers \Drupal\Core\Datetime\DateFormatter::formatInterval */ public function testFormatInterval($interval, $granularity, $expected, $langcode = NULL) { // Mocks a simple formatPlural implementation. @@ -128,4 +140,35 @@ public function testFormatIntervalZeroSecond() { $this->assertEquals('0 sec', $result); } + /** + * Tests the getSampleDateFormats method. + * + * @covers \Drupal\Core\Datetime\DateFormatter::getSampleDateFormats + */ + public function testGetSampleDateFormats() { + $timestamp = strtotime('2015-03-22 14:23:00'); + $expected = $this->dateFormatter->getSampleDateFormats('en', $timestamp, 'Europe/London'); + + // Removed characters related to timezone 'e' and 'T', as test does not have + // timezone set. + $date_characters = 'dDjlNSwzWFmMntLoYyaABgGhHisuIOPZcrU'; + $date_chars = str_split($date_characters); + + foreach ($date_chars as $val) { + $this->assertEquals($expected[$val], date($val, $timestamp)); + } + } + +} + +} + +namespace { + use Drupal\Component\Utility\String; + + if (!function_exists('t')) { + function t($string, array $args = []) { + return String::format($string, $args); + } + } }