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');
+ }
+
+}