machine-name.es6.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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. const options = settings.machineName[sourceId];
  85. const $source = $context
  86. .find(sourceId)
  87. .addClass('machine-name-source')
  88. .once('machine-name');
  89. const $target = $context
  90. .find(options.target)
  91. .addClass('machine-name-target');
  92. const $suffix = $context.find(options.suffix);
  93. const $wrapper = $target.closest('.js-form-item');
  94. // All elements have to exist.
  95. if (
  96. !$source.length ||
  97. !$target.length ||
  98. !$suffix.length ||
  99. !$wrapper.length
  100. ) {
  101. return;
  102. }
  103. // Skip processing upon a form validation error on the machine name.
  104. if ($target.hasClass('error')) {
  105. return;
  106. }
  107. // Figure out the maximum length for the machine name.
  108. options.maxlength = $target.attr('maxlength');
  109. // Hide the form item container of the machine name form element.
  110. $wrapper.addClass('visually-hidden');
  111. // Initial machine name from the target field default value.
  112. const machine = $target.val();
  113. // Append the machine name preview to the source field.
  114. const $preview = $(
  115. `<span class="machine-name-value">${
  116. options.field_prefix
  117. }${Drupal.checkPlain(machine)}${options.field_suffix}</span>`,
  118. );
  119. $suffix.empty();
  120. if (options.label) {
  121. $suffix.append(
  122. `<span class="machine-name-label">${options.label}: </span>`,
  123. );
  124. }
  125. $suffix.append($preview);
  126. // If the machine name cannot be edited, stop further processing.
  127. if ($target.is(':disabled')) {
  128. return;
  129. }
  130. const eventData = {
  131. $source,
  132. $target,
  133. $suffix,
  134. $wrapper,
  135. $preview,
  136. options,
  137. };
  138. // If no initial value, determine machine name based on the
  139. // human-readable form element value.
  140. if (machine === '' && $source.val() !== '') {
  141. self.transliterate($source.val(), options).done(machineName => {
  142. self.showMachineName(
  143. machineName.substr(0, options.maxlength),
  144. eventData,
  145. );
  146. });
  147. }
  148. // If it is editable, append an edit link.
  149. const $link = $(
  150. `<span class="admin-link"><button type="button" class="link">${Drupal.t(
  151. 'Edit',
  152. )}</button></span>`,
  153. ).on('click', eventData, clickEditHandler);
  154. $suffix.append($link);
  155. // Preview the machine name in realtime when the human-readable name
  156. // changes, but only if there is no machine name yet; i.e., only upon
  157. // initial creation, not when editing.
  158. if ($target.val() === '') {
  159. $source
  160. .on('formUpdated.machineName', eventData, machineNameHandler)
  161. // Initialize machine name preview.
  162. .trigger('formUpdated.machineName');
  163. }
  164. // Add a listener for an invalid event on the machine name input
  165. // to show its container and focus it.
  166. $target.on('invalid', eventData, clickEditHandler);
  167. });
  168. },
  169. showMachineName(machine, data) {
  170. const settings = data.options;
  171. // Set the machine name to the transliterated value.
  172. if (machine !== '') {
  173. if (machine !== settings.replace) {
  174. data.$target.val(machine);
  175. data.$preview.html(
  176. settings.field_prefix +
  177. Drupal.checkPlain(machine) +
  178. settings.field_suffix,
  179. );
  180. }
  181. data.$suffix.show();
  182. } else {
  183. data.$suffix.hide();
  184. data.$target.val(machine);
  185. data.$preview.empty();
  186. }
  187. },
  188. /**
  189. * Transliterate a human-readable name to a machine name.
  190. *
  191. * @param {string} source
  192. * A string to transliterate.
  193. * @param {object} settings
  194. * The machine name settings for the corresponding field.
  195. * @param {string} settings.replace_pattern
  196. * A regular expression (without modifiers) matching disallowed characters
  197. * in the machine name; e.g., '[^a-z0-9]+'.
  198. * @param {string} settings.replace_token
  199. * A token to validate the regular expression.
  200. * @param {string} settings.replace
  201. * A character to replace disallowed characters with; e.g., '_' or '-'.
  202. * @param {number} settings.maxlength
  203. * The maximum length of the machine name.
  204. *
  205. * @return {jQuery}
  206. * The transliterated source string.
  207. */
  208. transliterate(source, settings) {
  209. return $.get(Drupal.url('machine_name/transliterate'), {
  210. text: source,
  211. langcode: drupalSettings.langcode,
  212. replace_pattern: settings.replace_pattern,
  213. replace_token: settings.replace_token,
  214. replace: settings.replace,
  215. lowercase: true,
  216. });
  217. },
  218. };
  219. })(jQuery, Drupal, drupalSettings);