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);