diff --git a/core/js/Component/Transliteration.es6.js b/core/js/Component/Transliteration.es6.js new file mode 100644 index 0000000..dea530f --- /dev/null +++ b/core/js/Component/Transliteration.es6.js @@ -0,0 +1,57 @@ + +/** + * Transliteration service. + */ +class Transliteration { + + /** + * Constructor + * + * @param {object} options + * - replace_pattern: A regular expression (without modifiers) matching + * disallowed characters in the machine name; e.g., '[^a-z0-9]+'. + * - replace: A character to replace disallowed characters with; e.g., + * '_' or '-'. + * - max_length: The maximum length of the transliterated value (default: 10). + */ + constructor(options = {replace_pattern: '[^a-z0-9_]+', replace: '_', max_length: 10}) { + this.replace_pattern = options.replace_pattern; + this.replace = options.replace; + this.max_length = options.max_length; + } + + /** + * 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. + */ + convert(source, callback = function (result) {}) { + const rx = new RegExp(this.replace_pattern, 'g'); + const expected = source.toLowerCase().replace(rx, this.replace).substr(0, this.max_length); + + // 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) { + this.xhr = jQuery.get(Drupal.url('machine_name/transliterate'), { + text: source, + langcode: drupalSettings.langcode, + replace_pattern: this.replace_pattern, + replace: this.replace, + lowercase: true + }, (response) => callback(response)); + } + else { + callback(expected); + } + } +} + +export default Transliteration; \ No newline at end of file diff --git a/core/js/Render/Element/MachineName.es6.js b/core/js/Render/Element/MachineName.es6.js new file mode 100644 index 0000000..03c00b8 --- /dev/null +++ b/core/js/Render/Element/MachineName.es6.js @@ -0,0 +1,163 @@ + +import Transliteration from '/core/js/Component/Transliteration.es6'; + +/** + * Constructor for the machine name. + */ +class MachineName { + + /** + * Constructor. + * + * @param {object} options + * The options. + */ + constructor(options = { + target: '', + suffix: '', + max_length: null, + field_prefix: '', + field_suffix: '', + label: '' + }) { + this.options = options; + + this.transliteration = new Transliteration(options); + } + + /** + * Attach the UI to the source field within the provided context. + * + * @param {jQuery} $context + * The jQuery object the machine name field is contained in. + * @param {jQuery} $source + * The jQuery object for the source field. + */ + attachUI($context = jQuery, $source = jQuery) { + this.$context = $context; + this.$source = $source; + + this.$target = this.$context.find(this.options.target).addClass('machine-name-target'); + this.$suffix = this.$context.find(this.options.suffix); + this.$wrapper = this.$target.closest('.js-form-item'); + + // All elements have to exist. + if (!this.$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; + } + + this.$source.addClass('machine-name-source'); + + // Figure out the maximum length for the machine name. + if (!this.options.max_length) { + this.options.max_length = 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() !== '') { + this.addSuffix(this.$target.val()); + } + else if (this.$source.val() !== '') { + this.transliteration.convert(this.$source.val(), (result) => this.addSuffix(result)); + } + + // If the machine name cannot be edited, stop further processing. + if (this.$target.is(':disabled')) { + return; + } + + // 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() === '') { + // Initialize machine name preview. + this.update(); + } + + // Add listener to the formUpdated to update the machine name. + this.$source.on('formUpdated.machineName', this, () => this.update()); + + // 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.detachUI()); + } + + /** + * Set the suffix for the field. + * + * @param {string} machine_name + * The machine name to set. + */ + addSuffix(machine_name = '') { + this.$preview = jQuery('' + this.options.field_prefix + Drupal.checkPlain(machine_name) + this.options.field_suffix + ''); + this.$suffix.empty(); + + if (this.options.label) { + this.$preview.prepend(jQuery('' + this.options.label + ': ')); + } + this.$suffix.append(this.$preview); + + // If it is editable, append an edit link. + const $link = jQuery('').on('click', () => this.detachUI()); + this.$suffix.append($link); + + } + + /** + * Destroy the machineName instance and revert the field to it's original state. + */ + detachUI() { + if (this.$wrapper) { + this.$wrapper.removeClass('visually-hidden'); + this.$target.trigger('focus'); + this.$suffix.hide(); + this.$source.off('.machineName'); + } + } + + /** + * Act on a machine name. + * + * Either shows the machine name or if empty, hide the machine name container. + * + * @param {string} machine + * The machine name. + */ + showMachineName(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); + } + 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. + */ + update() { + const baseValue = this.$source.val(); + + this.transliteration.convert(baseValue, (machine) => { + this.showMachineName(machine.substr(0, this.options.max_length)); + }); + } +} + +export default MachineName; diff --git a/core/misc/machine-name.js b/core/misc/machine-name.js index e76292e..600b8bd 100644 --- a/core/misc/machine-name.js +++ b/core/misc/machine-name.js @@ -1,13 +1,222 @@ -/** - * @file - * Machine name functionality. - */ (function ($, Drupal, drupalSettings) { - 'use strict'; /** + * Transliteration service. + */ + class Transliteration { + + /** + * Constructor + * + * @param {object} options + * - replace_pattern: A regular expression (without modifiers) matching + * disallowed characters in the machine name; e.g., '[^a-z0-9]+'. + * - replace: A character to replace disallowed characters with; e.g., + * '_' or '-'. + * - max_length: The maximum length of the transliterated value (default: 10). + */ + constructor(options = {replace_pattern: '[^a-z0-9_]+', replace: '_', max_length: 10}) { + this.replace_pattern = options.replace_pattern; + this.replace = options.replace; + this.max_length = options.max_length; + } + + /** + * 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. + */ + convert(source, callback = function (result) {}) { + const rx = new RegExp(this.replace_pattern, 'g'); + const expected = source.toLowerCase().replace(rx, this.replace).substr(0, this.max_length); + + // 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) { + this.xhr = jQuery.get(Drupal.url('machine_name/transliterate'), { + text: source, + langcode: drupalSettings.langcode, + replace_pattern: this.replace_pattern, + replace: this.replace, + lowercase: true + }, (response) => callback(response)); + } + else { + callback(expected); + } + } + } + + /** + * Constructor for the machine name. + */ + class MachineName { + + /** + * Constructor. + * + * @param {object} options + * The options. + */ + constructor(options = { + target: '', + suffix: '', + max_length: null, + field_prefix: '', + field_suffix: '', + label: '' + }) { + this.options = options; + + this.transliteration = new Transliteration(options); + } + + /** + * Attach the UI to the source field within the provided context. + * + * @param {jQuery} $context + * The jQuery object the machine name field is contained in. + * @param {jQuery} $source + * The jQuery object for the source field. + */ + attachUI($context = jQuery, $source = jQuery) { + this.$context = $context; + this.$source = $source; + + this.$target = this.$context.find(this.options.target).addClass('machine-name-target'); + this.$suffix = this.$context.find(this.options.suffix); + this.$wrapper = this.$target.closest('.js-form-item'); + + // All elements have to exist. + if (!this.$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; + } + + this.$source.addClass('machine-name-source'); + + // Figure out the maximum length for the machine name. + if (!this.options.max_length) { + this.options.max_length = 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() !== '') { + this.addSuffix(this.$target.val()); + } + else if (this.$source.val() !== '') { + this.transliteration.convert(this.$source.val(), (result) => this.addSuffix(result)); + } + + // If the machine name cannot be edited, stop further processing. + if (this.$target.is(':disabled')) { + return; + } + + // 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() === '') { + // Initialize machine name preview. + this.update(); + } + + // Add listener to the formUpdated to update the machine name. + this.$source.on('formUpdated.machineName', this, () => this.update()); + + // 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.detachUI()); + } + + /** + * Set the suffix for the field. + * + * @param {string} machine_name + * The machine name to set. + */ + addSuffix(machine_name = '') { + this.$preview = $('' + this.options.field_prefix + Drupal.checkPlain(machine_name) + this.options.field_suffix + ''); + this.$suffix.empty(); + + if (this.options.label) { + this.$preview.prepend($('' + this.options.label + ': ')); + } + this.$suffix.append(this.$preview); + + // If it is editable, append an edit link. + const $link = $('').on('click', () => this.detachUI()); + this.$suffix.append($link); + + } + + /** + * Destroy the machineName instance and revert the field to it's original state. + */ + detachUI() { + if (this.$wrapper) { + this.$wrapper.removeClass('visually-hidden'); + this.$target.trigger('focus'); + this.$suffix.hide(); + this.$source.off('.machineName'); + } + } + + /** + * Act on a machine name. + * + * Either shows the machine name or if empty, hide the machine name container. + * + * @param {string} machine + * The machine name. + */ + showMachineName(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); + } + 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. + */ + update() { + const baseValue = this.$source.val(); + + this.transliteration.convert(baseValue, (machine) => { + this.showMachineName(machine.substr(0, this.options.max_length)); + }); + } + } + + /** * Attach the machine-readable name form element behavior. * * @type {Drupal~behavior} @@ -43,167 +252,28 @@ * - field_suffix: The #field_suffix of the form element. */ attach: function (context, settings) { - var self = this; - var $context = $(context); - var timeout = null; - var xhr = null; - - function clickEditHandler(e) { - var data = e.data; - data.$wrapper.removeClass('visually-hidden'); - data.$target.trigger('focus'); - data.$suffix.hide(); - data.$source.off('.machineName'); - } - - function machineNameHandler(e) { - var data = e.data; - var options = data.options; - var baseValue = $(e.target).val(); - - var rx = new RegExp(options.replace_pattern, 'g'); - var expected = baseValue.toLowerCase().replace(rx, options.replace).substr(0, options.maxlength); - - // Abort the last pending request because the label has changed and it - // is no longer valid. - if (xhr && xhr.readystate !== 4) { - xhr.abort(); - xhr = null; - } - - // Wait 300 milliseconds for Ajax request since the last event to update - // the machine name i.e., after the user has stopped typing. - if (timeout) { - clearTimeout(timeout); - timeout = null; - } - if (baseValue.toLowerCase() !== expected) { - timeout = setTimeout(function () { - xhr = self.transliterate(baseValue, options).done(function (machine) { - self.showMachineName(machine.substr(0, options.maxlength), data); - }); - }, 300); - } - else { - self.showMachineName(expected, data); - } - } + const $context = $(context); Object.keys(settings.machineName).forEach(function (source_id) { - var machine = ''; - var eventData; - var options = settings.machineName[source_id]; - - var $source = $context.find(source_id).addClass('machine-name-source').once('machine-name'); - var $target = $context.find(options.target).addClass('machine-name-target'); - var $suffix = $context.find(options.suffix); - var $wrapper = $target.closest('.js-form-item'); - // All elements have to exist. - if (!$source.length || !$target.length || !$suffix.length || !$wrapper.length) { - return; - } - // Skip processing upon a form validation error on the machine name. - if ($target.hasClass('error')) { - return; - } - // Figure out the maximum length for the machine name. - options.maxlength = $target.attr('maxlength'); - // Hide the form item container of the machine name form element. - $wrapper.addClass('visually-hidden'); - // Determine the initial machine name value. Unless the machine name - // form element is disabled or not empty, the initial default value is - // based on the human-readable form element value. - if ($target.is(':disabled') || $target.val() !== '') { - machine = $target.val(); - } - else if ($source.val() !== '') { - machine = self.transliterate($source.val(), options); - } - // Append the machine name preview to the source field. - var $preview = $('' + options.field_prefix + Drupal.checkPlain(machine) + options.field_suffix + ''); - $suffix.empty(); - if (options.label) { - $suffix.append('' + options.label + ': '); - } - $suffix.append($preview); + if (settings.machineName.hasOwnProperty(source_id)) { + const $source = $context.find(source_id).once('machine-name'); - // If the machine name cannot be edited, stop further processing. - if ($target.is(':disabled')) { - return; + if ($source.length) { + settings.machineName[source_id].instance = new MachineName(settings.machineName[source_id]); + settings.machineName[source_id].instance.attachUI($context, $source); + } } - - eventData = { - $source: $source, - $target: $target, - $suffix: $suffix, - $wrapper: $wrapper, - $preview: $preview, - options: options - }; - // If it is editable, append an edit link. - var $link = $('').on('click', eventData, clickEditHandler); - $suffix.append($link); - - // Preview the machine name in realtime when the human-readable name - // changes, but only if there is no machine name yet; i.e., only upon - // initial creation, not when editing. - if ($target.val() === '') { - $source.on('formUpdated.machineName', eventData, machineNameHandler) - // Initialize machine name preview. - .trigger('formUpdated.machineName'); - } - - // Add a listener for an invalid event on the machine name input - // to show its container and focus it. - $target.on('invalid', eventData, clickEditHandler); }); }, - - showMachineName: function (machine, data) { - var settings = data.options; - // Set the machine name to the transliterated value. - if (machine !== '') { - if (machine !== settings.replace) { - data.$target.val(machine); - data.$preview.html(settings.field_prefix + Drupal.checkPlain(machine) + settings.field_suffix); - } - data.$suffix.show(); - } - else { - data.$suffix.hide(); - data.$target.val(machine); - data.$preview.empty(); - } - }, - /** - * Transliterate a human-readable name to a machine name. - * - * @param {string} source - * A string to transliterate. - * @param {object} settings - * The machine name settings for the corresponding field. - * @param {string} settings.replace_pattern - * A regular expression (without modifiers) matching disallowed characters - * in the machine name; e.g., '[^a-z0-9]+'. - * @param {string} settings.replace_token - * A token to validate the regular expression. - * @param {string} settings.replace - * A character to replace disallowed characters with; e.g., '_' or '-'. - * @param {number} settings.maxlength - * The maximum length of the machine name. - * - * @return {jQuery} - * The transliterated source string. + * Detaches the behavior. */ - transliterate: function (source, settings) { - return $.get(Drupal.url('machine_name/transliterate'), { - text: source, - langcode: drupalSettings.langcode, - replace_pattern: settings.replace_pattern, - replace_token: settings.replace_token, - replace: settings.replace, - lowercase: true + detach: function (context, settings) { + Object.keys(settings.machineName).forEach(function (source_id) { + if (settings.machineName[source_id].instance) { + settings.machineName[source_id].instance.detachUI(); + delete settings.machineName[source_id].instance; + } }); } };