Chris@0: /** Chris@0: * @file Chris@0: * Machine name functionality. Chris@0: */ Chris@0: Chris@17: (function($, Drupal, drupalSettings) { Chris@0: /** Chris@0: * Attach the machine-readable name form element behavior. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches machine-name behaviors. Chris@0: */ Chris@0: Drupal.behaviors.machineName = { Chris@0: /** Chris@0: * Attaches the behavior. Chris@0: * Chris@0: * @param {Element} context Chris@0: * The context for attaching the behavior. Chris@0: * @param {object} settings Chris@0: * Settings object. Chris@0: * @param {object} settings.machineName Chris@0: * A list of elements to process, keyed by the HTML ID of the form Chris@0: * element containing the human-readable value. Each element is an object Chris@0: * defining the following properties: Chris@0: * - target: The HTML ID of the machine name form element. Chris@0: * - suffix: The HTML ID of a container to show the machine name preview Chris@0: * in (usually a field suffix after the human-readable name Chris@0: * form element). Chris@0: * - label: The label to show for the machine name preview. Chris@0: * - replace_pattern: A regular expression (without modifiers) matching Chris@0: * disallowed characters in the machine name; e.g., '[^a-z0-9]+'. Chris@0: * - replace: A character to replace disallowed characters with; e.g., Chris@0: * '_' or '-'. Chris@0: * - standalone: Whether the preview should stay in its own element Chris@0: * rather than the suffix of the source element. Chris@0: * - field_prefix: The #field_prefix of the form element. Chris@0: * - field_suffix: The #field_suffix of the form element. Chris@0: */ Chris@0: attach(context, settings) { Chris@0: const self = this; Chris@0: const $context = $(context); Chris@0: let timeout = null; Chris@0: let xhr = null; Chris@0: Chris@0: function clickEditHandler(e) { Chris@0: const data = e.data; Chris@0: data.$wrapper.removeClass('visually-hidden'); Chris@0: data.$target.trigger('focus'); Chris@0: data.$suffix.hide(); Chris@0: data.$source.off('.machineName'); Chris@0: } Chris@0: Chris@0: function machineNameHandler(e) { Chris@0: const data = e.data; Chris@0: const options = data.options; Chris@0: const baseValue = $(e.target).val(); Chris@0: Chris@0: const rx = new RegExp(options.replace_pattern, 'g'); Chris@17: const expected = baseValue Chris@17: .toLowerCase() Chris@17: .replace(rx, options.replace) Chris@17: .substr(0, options.maxlength); Chris@0: Chris@0: // Abort the last pending request because the label has changed and it Chris@0: // is no longer valid. Chris@0: if (xhr && xhr.readystate !== 4) { Chris@0: xhr.abort(); Chris@0: xhr = null; Chris@0: } Chris@0: Chris@0: // Wait 300 milliseconds for Ajax request since the last event to update Chris@0: // the machine name i.e., after the user has stopped typing. Chris@0: if (timeout) { Chris@0: clearTimeout(timeout); Chris@0: timeout = null; Chris@0: } Chris@0: if (baseValue.toLowerCase() !== expected) { Chris@0: timeout = setTimeout(() => { Chris@17: xhr = self.transliterate(baseValue, options).done(machine => { Chris@0: self.showMachineName(machine.substr(0, options.maxlength), data); Chris@0: }); Chris@0: }, 300); Chris@17: } else { Chris@0: self.showMachineName(expected, data); Chris@0: } Chris@0: } Chris@0: Chris@17: Object.keys(settings.machineName).forEach(sourceId => { Chris@0: let machine = ''; Chris@14: const options = settings.machineName[sourceId]; Chris@0: Chris@17: const $source = $context Chris@17: .find(sourceId) Chris@17: .addClass('machine-name-source') Chris@17: .once('machine-name'); Chris@17: const $target = $context Chris@17: .find(options.target) Chris@17: .addClass('machine-name-target'); Chris@0: const $suffix = $context.find(options.suffix); Chris@0: const $wrapper = $target.closest('.js-form-item'); Chris@0: // All elements have to exist. Chris@17: if ( Chris@17: !$source.length || Chris@17: !$target.length || Chris@17: !$suffix.length || Chris@17: !$wrapper.length Chris@17: ) { Chris@0: return; Chris@0: } Chris@0: // Skip processing upon a form validation error on the machine name. Chris@0: if ($target.hasClass('error')) { Chris@0: return; Chris@0: } Chris@0: // Figure out the maximum length for the machine name. Chris@0: options.maxlength = $target.attr('maxlength'); Chris@0: // Hide the form item container of the machine name form element. Chris@0: $wrapper.addClass('visually-hidden'); Chris@0: // Determine the initial machine name value. Unless the machine name Chris@0: // form element is disabled or not empty, the initial default value is Chris@0: // based on the human-readable form element value. Chris@0: if ($target.is(':disabled') || $target.val() !== '') { Chris@0: machine = $target.val(); Chris@17: } else if ($source.val() !== '') { Chris@0: machine = self.transliterate($source.val(), options); Chris@0: } Chris@0: // Append the machine name preview to the source field. Chris@17: const $preview = $( Chris@17: `${ Chris@17: options.field_prefix Chris@17: }${Drupal.checkPlain(machine)}${options.field_suffix}`, Chris@17: ); Chris@0: $suffix.empty(); Chris@0: if (options.label) { Chris@17: $suffix.append( Chris@17: `${options.label}: `, Chris@17: ); Chris@0: } Chris@0: $suffix.append($preview); Chris@0: Chris@0: // If the machine name cannot be edited, stop further processing. Chris@0: if ($target.is(':disabled')) { Chris@0: return; Chris@0: } Chris@0: Chris@14: const eventData = { Chris@0: $source, Chris@0: $target, Chris@0: $suffix, Chris@0: $wrapper, Chris@0: $preview, Chris@0: options, Chris@0: }; Chris@0: // If it is editable, append an edit link. Chris@17: const $link = $( Chris@17: ``, Chris@17: ).on('click', eventData, clickEditHandler); Chris@0: $suffix.append($link); Chris@0: Chris@0: // Preview the machine name in realtime when the human-readable name Chris@0: // changes, but only if there is no machine name yet; i.e., only upon Chris@0: // initial creation, not when editing. Chris@0: if ($target.val() === '') { Chris@17: $source Chris@17: .on('formUpdated.machineName', eventData, machineNameHandler) Chris@0: // Initialize machine name preview. Chris@0: .trigger('formUpdated.machineName'); Chris@0: } Chris@0: Chris@0: // Add a listener for an invalid event on the machine name input Chris@0: // to show its container and focus it. Chris@0: $target.on('invalid', eventData, clickEditHandler); Chris@0: }); Chris@0: }, Chris@0: Chris@0: showMachineName(machine, data) { Chris@0: const settings = data.options; Chris@0: // Set the machine name to the transliterated value. Chris@0: if (machine !== '') { Chris@0: if (machine !== settings.replace) { Chris@0: data.$target.val(machine); Chris@17: data.$preview.html( Chris@17: settings.field_prefix + Chris@17: Drupal.checkPlain(machine) + Chris@17: settings.field_suffix, Chris@17: ); Chris@0: } Chris@0: data.$suffix.show(); Chris@17: } else { Chris@0: data.$suffix.hide(); Chris@0: data.$target.val(machine); Chris@0: data.$preview.empty(); Chris@0: } Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Transliterate a human-readable name to a machine name. Chris@0: * Chris@0: * @param {string} source Chris@0: * A string to transliterate. Chris@0: * @param {object} settings Chris@0: * The machine name settings for the corresponding field. Chris@0: * @param {string} settings.replace_pattern Chris@0: * A regular expression (without modifiers) matching disallowed characters Chris@0: * in the machine name; e.g., '[^a-z0-9]+'. Chris@0: * @param {string} settings.replace_token Chris@0: * A token to validate the regular expression. Chris@0: * @param {string} settings.replace Chris@0: * A character to replace disallowed characters with; e.g., '_' or '-'. Chris@0: * @param {number} settings.maxlength Chris@0: * The maximum length of the machine name. Chris@0: * Chris@0: * @return {jQuery} Chris@0: * The transliterated source string. Chris@0: */ Chris@0: transliterate(source, settings) { Chris@0: return $.get(Drupal.url('machine_name/transliterate'), { Chris@0: text: source, Chris@0: langcode: drupalSettings.langcode, Chris@0: replace_pattern: settings.replace_pattern, Chris@0: replace_token: settings.replace_token, Chris@0: replace: settings.replace, Chris@0: lowercase: true, Chris@0: }); Chris@0: }, Chris@0: }; Chris@17: })(jQuery, Drupal, drupalSettings);