diff --git a/core/misc/machine-name.js b/core/misc/machine-name.js index bbad2e3..3aac825 100644 --- a/core/misc/machine-name.js +++ b/core/misc/machine-name.js @@ -12,7 +12,8 @@ * * @type {Drupal~behavior} * - * @prop {Drupal~behaviorAttach} attach + * @prop {Drupal~behaviorAttach} a + *ttach * Attaches machine-name behaviors. */ Drupal.behaviors.machineName = { @@ -43,165 +44,233 @@ * - 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(); + Object.keys(settings.machineName).forEach(function (source_id) { + if (settings.machineName.hasOwnProperty(source_id)) { + var $source = $context.find(source_id).once('machine-name'); - var rx = new RegExp(options.replace_pattern, 'g'); - var expected = baseValue.toLowerCase().replace(rx, options.replace).substr(0, options.maxlength); + if ($source.length) { + settings.machineName[source_id].instance = new Drupal.machineName(settings.machineName[source_id]); - // 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; + settings.machineName[source_id].instance.attachUI($context, $source); + } } + }); + }, + /** + * Detach machine name functionality from the provided scope. + * + * @param context + * @param settings + */ + detach: function (context, settings) { + Object.keys(settings.machineName).forEach(function (source_id) { + var $source = $context.find(source_id); - // Wait 300 milliseconds since the last event to update the machine name - // i.e., after the user has stopped typing. - if (timeout) { - clearTimeout(timeout); - timeout = null; + // Field within scope and there is currently a machine name instance. + if ($source.length && settings.machineName[source_id].instance) { + // Detach UI and unset the machine name instance. + settings.machineName[source_id].instance.detachUI(); + delete settings.machineName[source_id].instance; } - timeout = setTimeout(function () { - if (baseValue.toLowerCase() !== expected) { - xhr = self.transliterate(baseValue, options).done(function (machine) { - self.showMachineName(machine.substr(0, options.maxlength), data); - }); - } - else { - self.showMachineName(expected, data); - } - }, 300); - } + }); + } + }; - 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); + /** + * Constructor for the machine name. + * + * @see Drupal.behaviors.machineName.attach(). + */ + Drupal.machineName = function(options) { + this.options = options; + }; - // If the machine name cannot be edited, stop further processing. - if ($target.is(':disabled')) { - return; - } + /** + * Attach the UI to the source field within the provided context. + * + * @param $context + * The jQuery object the machine name field is contained in. + * @param $source + * The jQuery object for the source field. + */ + Drupal.machineName.prototype.attachUI = function($context, $source) { + var machine = ''; - 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'); - } + this.$source = $source; - // Add a listener for an invalid event on the machine name input - // to show its container and focus it. - $target.on('invalid', eventData, clickEditHandler); - }); - }, + this.$target = $context.find(this.options.target).addClass('machine-name-target'); + this.$suffix = $context.find(this.options.suffix); + this.$wrapper = this.$target.closest('.js-form-item'); - 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(); + // All elements have to exist. + if (!$source.length || !this.$target.length || !this.$suffix.length || !this.$wrapper.length) { + return; + } + // Skip processing upon a form validation error on the machine name. + if (this.$target.hasClass('error')) { + return; + } + + $source.addClass('machine-name-source'); + + // Figure out the maximum length for the machine name. + this.options.maxlength = this.$target.attr('maxlength'); + // Hide the form item container of the machine name form element. + this.$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 (this.$target.is(':disabled') || this.$target.val() !== '') { + machine = this.$target.val(); + } + else if ($source.val() !== '') { + machine = this.convert($source.val(), this.options); + } + // Append the machine name preview to the source field. + this.$preview = $('' + this.options.field_prefix + Drupal.checkPlain(machine) + this.options.field_suffix + ''); + this.$suffix.empty(); + + if (this.options.label) { + this.$preview.prepend($('' + this.options.label + ': ')); + } + this.$suffix.append(this.$preview); + + // If the machine name cannot be edited, stop further processing. + if (this.$target.is(':disabled')) { + return; + } + + // If it is editable, append an edit link. + this.$link = $('').on('click', this, this.editManually); + this.$suffix.append(this.$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 (this.$target.val() === '') { + $source.on('formUpdated.machineName', this, this.update); + + // Initialize machine name preview. + this.update({data: this}); + } + + // Add a listener for an invalid event on the machine name input + // to show its container and focus it. + this.$target.on('invalid', this, this.editManually); + }; + + /** + * Destroy the machineName instance and revert the field to it's original state. + */ + Drupal.machineName.prototype.detachUI = function() { + if (this.$wrapper) { + this.$wrapper.removeClass('visually-hidden'); + this.$target.trigger('focus'); + this.$suffix.hide(); + this.$source.off('.machineName'); + } + }; + + /** + * Switch to manual editing mode. + * + * @param {jQuery.Event} e + * The event triggered. + */ + Drupal.machineName.prototype.editManually = function(e) { + var machineName = e.data; + machineName.detachUI(); + }; + + /** + * Act on a machine name. + * + * Either shows the machine name or if empty, hide the machine name container. + * + * @param {string} machine + * The machine name. + * + * @param {Drupal.machineName} machineName + * The machineName instance to show the machine name for. + */ + Drupal.machineName.prototype.showMachineName = function (machine) { + // Set the machine name to the transliterated value. + if (machine !== '') { + if (machine !== this.options.replace) { + this.$target.val(machine); + this.$preview.html(this.options.field_prefix + Drupal.checkPlain(machine) + this.options.field_suffix); } - else { - data.$suffix.hide(); - data.$target.val(machine); - data.$preview.empty(); + this.$suffix.show(); + } + else { + this.$suffix.hide(); + this.$target.val(machine); + this.$preview.empty(); + } + }; + + /** + * Act on a change on the input field and update the machine name. + * + * @param {jQuery.Event} e + * The event triggered. + */ + Drupal.machineName.prototype.update = function(e) { + var machineName = e.data; + var baseValue = machineName.$source.val(); + + machineName.convert(baseValue, function(machine) { + machineName.showMachineName(machine.substr(0, machineName.options.maxlength)); + }); + }; + + /** + * Convert a human-readable name to a machine name. + * + * @param {string} source + * A string to transliterate. + * @param {function} callback + * The callback to run once the source is converted to a machine name. + */ + Drupal.machineName.prototype.convert = function (source, callback) { + var rx = new RegExp(this.options.replace_pattern, 'g'); + var expected = source.toLowerCase().replace(rx, this.options.replace).substr(0, this.options.maxlength); + var machineName = this; + + // Abort the last pending request because the label has changed and it + // is no longer valid. + if (this.xhr && this.xhr.readystate !== 4) { + this.xhr.abort(); + this.xhr = null; + } + + if (source.toLowerCase() !== expected) { + // Wait 300 milliseconds since the last event to update the machine name + // i.e., after the user has stopped typing. + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; } - }, + this.timeout = setTimeout(function () { + this.xhr = $.get(Drupal.url('machine_name/transliterate'), { + text: source, + langcode: drupalSettings.langcode, + replace_pattern: machineName.options.replace_pattern, + replace: machineName.options.replace, + lowercase: true + }); - /** - * 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 - * 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: settings.replace, - lowercase: true - }); + // + this.xhr.done(function(machine) { + callback.call(machineName, machine.substr(0, machineName.options.maxlength)); + }); + }, 300); + } + // No server side transliteration needed, directly update the machine name. + else { + callback.call(this, expected); } }; diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php new file mode 100644 index 0000000..bf4649f --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php @@ -0,0 +1,103 @@ +drupalCreateUser(array( + 'access content', + 'administer permissions', + )); + $this->drupalLogin($account); + + // Get the transliteration server for validating the generated machine name. + $this->transliteration = \Drupal::service('transliteration'); + } + + /** + * Tests that machine name field functions. + * + * Makes sure that the machine name field automatically provides a valid + * machine name and that the manual editing mode functions. + */ + public function testMachineName() { + // Visit the add role page which contains a machine name field. + $this->drupalGet('admin/people/roles/add'); + + // Test values for conversion. + $test_values = array( + 'Test value !0-9@' => 'A title that should be transliterated must be equal to the php generated machine name', + 'Test value' => 'A title that should not be transliterated must be equal to the php generated machine name', + ); + + // Find relevant elements on the page for this test. + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + $element = $page->findField('label'); + $machine_name = $page->find('css', '#edit-label-machine-name-suffix .machine-name-value'); + $button = $page->find('css', '#edit-label-machine-name-suffix button.link'); + $id = $page->findField('id'); + + // Test each value for conversion to a machine name. + foreach ($test_values as $test_value => $message) { + $transliterated = $this->transliteration->transliterate($test_value); + $transliterated = Unicode::strtolower($transliterated); + $transliterated = preg_replace('@' . strtr('[^a-z0-9_]+', [ + '@' => '\@', + chr(0) => '' + ]) . '@', '_', $transliterated); + + // Set the value for the role, triggering the machine name update. + $element->setValue($test_value); + + // Wait the set timeout for fetching the machine name. + $this->getSession()->wait(300); + + // And wait for the autocomplete to finish. + $assert_session->waitOnAutocomplete(); + + // Get the generated machine name. + $generated_value = $machine_name->getHTML(); + + // Validate the generated machine name. + $this->assertEquals($transliterated, $generated_value, $message); + } + + // Test switching back to the manual editing mode by clicking the edit link. + $button->click(); + + // Validate the visibility of the machine name field. + $this->assertTrue($id->isVisible(), 'The ID field must now be visible'); + + // Validate if the element contains the correct value. + $this->assertEquals($transliterated, $id->getValue(), 'The ID field value must be equal to the php generated machine name'); + } + +}