machine-name.es6.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /**
  2. * @file
  3. * Machine name functionality.
  4. */
  5. (function($, Drupal, drupalSettings) {
  6. /**
  7. * Attach the machine-readable name form element behavior.
  8. *
  9. * @type {Drupal~behavior}
  10. *
  11. * @prop {Drupal~behaviorAttach} attach
  12. * Attaches machine-name behaviors.
  13. */
  14. Drupal.behaviors.machineName = {
  15. /**
  16. * Attaches the behavior.
  17. *
  18. * @param {Element} context
  19. * The context for attaching the behavior.
  20. * @param {object} settings
  21. * Settings object.
  22. * @param {object} settings.machineName
  23. * A list of elements to process, keyed by the HTML ID of the form
  24. * element containing the human-readable value. Each element is an object
  25. * defining the following properties:
  26. * - target: The HTML ID of the machine name form element.
  27. * - suffix: The HTML ID of a container to show the machine name preview
  28. * in (usually a field suffix after the human-readable name
  29. * form element).
  30. * - label: The label to show for the machine name preview.
  31. * - replace_pattern: A regular expression (without modifiers) matching
  32. * disallowed characters in the machine name; e.g., '[^a-z0-9]+'.
  33. * - replace: A character to replace disallowed characters with; e.g.,
  34. * '_' or '-'.
  35. * - standalone: Whether the preview should stay in its own element
  36. * rather than the suffix of the source element.
  37. * - field_prefix: The #field_prefix of the form element.
  38. * - field_suffix: The #field_suffix of the form element.
  39. */
  40. attach(context, settings) {
  41. const self = this;
  42. const $context = $(context);
  43. let timeout = null;
  44. let xhr = null;
  45. function clickEditHandler(e) {
  46. const data = e.data;
  47. data.$wrapper.removeClass('visually-hidden');
  48. data.$target.trigger('focus');
  49. data.$suffix.hide();
  50. data.$source.off('.machineName');
  51. }
  52. function machineNameHandler(e) {
  53. const data = e.data;
  54. const options = data.options;
  55. const baseValue = $(e.target).val();
  56. const rx = new RegExp(options.replace_pattern, 'g');
  57. const expected = baseValue
  58. .toLowerCase()
  59. .replace(rx, options.replace)
  60. .substr(0, options.maxlength);
  61. // Abort the last pending request because the label has changed and it
  62. // is no longer valid.
  63. if (xhr && xhr.readystate !== 4) {
  64. xhr.abort();
  65. xhr = null;
  66. }
  67. // Wait 300 milliseconds for Ajax request since the last event to update
  68. // the machine name i.e., after the user has stopped typing.
  69. if (timeout) {
  70. clearTimeout(timeout);
  71. timeout = null;
  72. }
  73. if (baseValue.toLowerCase() !== expected) {
  74. timeout = setTimeout(() => {
  75. xhr = self.transliterate(baseValue, options).done(machine => {
  76. self.showMachineName(machine.substr(0, options.maxlength), data);
  77. });
  78. }, 300);
  79. } else {
  80. self.showMachineName(expected, data);
  81. }
  82. }
  83. Object.keys(settings.machineName).forEach(sourceId => {
  84. let machine = '';
  85. const options = settings.machineName[sourceId];
  86. const $source = $context
  87. .find(sourceId)
  88. .addClass('machine-name-source')
  89. .once('machine-name');
  90. const $target = $context
  91. .find(options.target)
  92. .addClass('machine-name-target');
  93. const $suffix = $context.find(options.suffix);
  94. const $wrapper = $target.closest('.js-form-item');
  95. // All elements have to exist.
  96. if (
  97. !$source.length ||
  98. !$target.length ||
  99. !$suffix.length ||
  100. !$wrapper.length
  101. ) {
  102. return;
  103. }
  104. // Skip processing upon a form validation error on the machine name.
  105. if ($target.hasClass('error')) {
  106. return;
  107. }
  108. // Figure out the maximum length for the machine name.
  109. options.maxlength = $target.attr('maxlength');
  110. // Hide the form item container of the machine name form element.
  111. $wrapper.addClass('visually-hidden');
  112. // Determine the initial machine name value. Unless the machine name
  113. // form element is disabled or not empty, the initial default value is
  114. // based on the human-readable form element value.
  115. if ($target.is(':disabled') || $target.val() !== '') {
  116. machine = $target.val();
  117. } else if ($source.val() !== '') {
  118. machine = self.transliterate($source.val(), options);
  119. }
  120. // Append the machine name preview to the source field.
  121. const $preview = $(
  122. `<span class="machine-name-value">${
  123. options.field_prefix
  124. }${Drupal.checkPlain(machine)}${options.field_suffix}</span>`,
  125. );
  126. $suffix.empty();
  127. if (options.label) {
  128. $suffix.append(
  129. `<span class="machine-name-label">${options.label}: </span>`,
  130. );
  131. }
  132. $suffix.append($preview);
  133. // If the machine name cannot be edited, stop further processing.
  134. if ($target.is(':disabled')) {
  135. return;
  136. }
  137. const eventData = {
  138. $source,
  139. $target,
  140. $suffix,
  141. $wrapper,
  142. $preview,
  143. options,
  144. };
  145. // If it is editable, append an edit link.
  146. const $link = $(
  147. `<span class="admin-link"><button type="button" class="link">${Drupal.t(
  148. 'Edit',
  149. )}</button></span>`,
  150. ).on('click', eventData, clickEditHandler);
  151. $suffix.append($link);
  152. // Preview the machine name in realtime when the human-readable name
  153. // changes, but only if there is no machine name yet; i.e., only upon
  154. // initial creation, not when editing.
  155. if ($target.val() === '') {
  156. $source
  157. .on('formUpdated.machineName', eventData, machineNameHandler)
  158. // Initialize machine name preview.
  159. .trigger('formUpdated.machineName');
  160. }
  161. // Add a listener for an invalid event on the machine name input
  162. // to show its container and focus it.
  163. $target.on('invalid', eventData, clickEditHandler);
  164. });
  165. },
  166. showMachineName(machine, data) {
  167. const settings = data.options;
  168. // Set the machine name to the transliterated value.
  169. if (machine !== '') {
  170. if (machine !== settings.replace) {
  171. data.$target.val(machine);
  172. data.$preview.html(
  173. settings.field_prefix +
  174. Drupal.checkPlain(machine) +
  175. settings.field_suffix,
  176. );
  177. }
  178. data.$suffix.show();
  179. } else {
  180. data.$suffix.hide();
  181. data.$target.val(machine);
  182. data.$preview.empty();
  183. }
  184. },
  185. /**
  186. * Transliterate a human-readable name to a machine name.
  187. *
  188. * @param {string} source
  189. * A string to transliterate.
  190. * @param {object} settings
  191. * The machine name settings for the corresponding field.
  192. * @param {string} settings.replace_pattern
  193. * A regular expression (without modifiers) matching disallowed characters
  194. * in the machine name; e.g., '[^a-z0-9]+'.
  195. * @param {string} settings.replace_token
  196. * A token to validate the regular expression.
  197. * @param {string} settings.replace
  198. * A character to replace disallowed characters with; e.g., '_' or '-'.
  199. * @param {number} settings.maxlength
  200. * The maximum length of the machine name.
  201. *
  202. * @return {jQuery}
  203. * The transliterated source string.
  204. */
  205. transliterate(source, settings) {
  206. return $.get(Drupal.url('machine_name/transliterate'), {
  207. text: source,
  208. langcode: drupalSettings.langcode,
  209. replace_pattern: settings.replace_pattern,
  210. replace_token: settings.replace_token,
  211. replace: settings.replace,
  212. lowercase: true,
  213. });
  214. },
  215. };
  216. })(jQuery, Drupal, drupalSettings);