PageSourceSearch

https://www.barmen1873bourbon.com/modules/custom/mc_multiform/js/mc_multiform_a11y.js?v=1.2

js barmen1873bourbon.com collected 2026-10-08 12:29:14 UTC 11,456 bytes, 341 lines download raw bytes

1/**
2 * @file
3 * JavaScript behaviors for file validation error placement and accessibility.
4 */
5
6(function ($, Drupal, once) {
7  "use strict";
8
9  const MANAGED_FILE_SELECTOR = 'div.js-form-managed-file';
10  const FILE_CHECKBOX_SELECTOR = 'input.form-checkbox[type="checkbox"][name*="selected"]';
11  const REMOVE_BUTTON_SELECTOR = 'button[data-drupal-selector$="-remove-button"]';
12  const ROLE_BUTTON_ANCHOR_SELECTOR = 'a[role="button"]';
13  const MODAL_FOCUSABLE_SELECTOR = [
14    'a[href]',
15    'area[href]',
16    'input:not([disabled]):not([type="hidden"])',
17    'select:not([disabled])',
18    'textarea:not([disabled])',
19    'button:not([disabled])',
20    'iframe',
21    'object',
22    'embed',
23    '[contenteditable="true"]',
24    '[tabindex]:not([tabindex="-1"])'
25  ].join(',');
26
27  const requestNextFrame = window.requestAnimationFrame
28    ? window.requestAnimationFrame.bind(window)
29    : function (callback) {
30      setTimeout(callback, 0);
31    };
32
33  function getFileErrorId($input) {
34    const baseId = $input.attr('id') || $input.attr('data-drupal-selector') || 'file-upload';
35    return baseId.replace(/[^A-Za-z0-9\-_:]/g, '-') + '-error';
36  }
37
38  function getFocusableElements($scope) {
39    return $scope
40      .find(MODAL_FOCUSABLE_SELECTOR)
41      .filter(':visible')
42      .filter(function () {
43        return !$(this).is('[aria-hidden="true"]');
44      });
45  }
46  function syncAriaDescribedBy($input, errorId, hasError) {
47    const describedBy = ($input.attr('aria-describedby') || '')
48      .split(/\s+/)
49      .filter(function (id) {
50        return id && id !== errorId;
51      });
52
53    if (hasError) {
54      describedBy.push(errorId);
55    }
56
57    if (describedBy.length) {
58      $input.attr('aria-describedby', describedBy.join(' '));
59    } else {
60      $input.removeAttr('aria-describedby');
61    }
62  }
63
64  function extractServerError($input) {
65    const $formItem = $input.closest('div.form-item.form-type-managed-file');
66    const $ajaxWrapper = $formItem.closest('div[id^="ajax-wrapper"]');
67    let $serverError = $();
68
69    if ($ajaxWrapper.length) {
70      $serverError = $ajaxWrapper.find('div[data-drupal-messages] .alert-danger').first();
71    }
72
73    if (!$serverError.length) {
74      $serverError = $formItem
75        .siblings('div[data-drupal-messages]')
76        .find('.alert-danger')
77        .first();
78    }
79
80    if (!$serverError.length) {
81      return '';
82    }
83
84    const $clone = $serverError.clone();
85    $clone.find('span[aria-hidden="true"], h2.sr-only').remove();
86
87    const errorText = $clone.text().replace(/\s+/g, ' ').trim();
88
89    $serverError.remove();
90
91    // Clean empty wrappers after removing the alert.
92    if ($ajaxWrapper.length) {
93      $ajaxWrapper.find('div.messages__wrapper, div[data-drupal-messages]').each(function () {
94        if (!$(this).children().length) {
95          $(this).remove();
96        }
97      });
98    }
99
100    return errorText;
101  }
102
103  function processFileErrorPlacement($input) {
104    const errorId = getFileErrorId($input);
105    const $managedFile = $input.closest(MANAGED_FILE_SELECTOR);
106    const $coreError = $managedFile.find('div.file-upload-js-error').first();
107    let errorText = '';
108
109    // Clear any previous custom file error reference.
110    syncAriaDescribedBy($input, errorId, false);
111    $input.attr('aria-invalid', 'false');
112    $input.siblings('span#' + errorId + '.file-upload-js-error').remove();
113
114    let hasCoreError = false;
115    if ($coreError.length) {
116      errorText = $coreError.text();
117      hasCoreError = true;
118      $coreError.remove();
119    } else {
120      errorText = extractServerError($input);
121    }
122
123    if (errorText) {
124      if (typeof drupalSettings !== 'undefined' && drupalSettings.multiform && drupalSettings.multiform.validations) {
125        if (hasCoreError) {
126          if (drupalSettings.multiform.validations.fileextension) {
127            errorText = drupalSettings.multiform.validations.fileextension;
128          }
129        } else {
130          if (drupalSettings.multiform.validations.filesize) {
131            errorText = drupalSettings.multiform.validations.filesize;
132          }
133        }
134      }
135
136      $input.siblings('span.file-upload-js-error').remove();
137      $('<span/>', {
138        id: errorId,
139        'class': 'error file-upload-js-error',
140        text: errorText
141      }).insertAfter($input);
142
143      syncAriaDescribedBy($input, errorId, true);
144      $input.attr('aria-invalid', 'true');
145    }
146  }
147
148  function scheduleFileErrorPlacement($input) {
149    if ($input.data('mcFileErrorPlacementScheduled')) {
150      return;
151    }
152
153    $input.data('mcFileErrorPlacementScheduled', true);
154
155    requestNextFrame(function () {
156      $input.removeData('mcFileErrorPlacementScheduled');
157      processFileErrorPlacement($input);
158    });
159  }
160
161  function toTitleCase(str) {
162    return str.replace(/\b\w/g, function (c) { return c.toUpperCase(); });
163  }
164
165  function updateRemoveButtonState($managedFile) {
166    const $removeButton = $managedFile.find(REMOVE_BUTTON_SELECTOR);
167    const anyChecked = $managedFile.find(FILE_CHECKBOX_SELECTOR + ':checked').length > 0;
168
169    if ($removeButton.length) {
170      if (!$removeButton.data('mcTitleCased')) {
171        const rawText = $removeButton.text().trim();
172        const titleText = toTitleCase(rawText);
173        if (rawText !== titleText) {
174          $removeButton.text(titleText);
175        }
176        $removeButton.data('mcTitleCased', true);
177      }
178      $removeButton.prop('disabled', !anyChecked);
179      // Also update aria-disabled for better accessibility
180      $removeButton.attr('aria-disabled', !anyChecked);
181    }
182  }
183
184  function scheduleRemoveButtonStateUpdate($managedFile) {
185    if ($managedFile.data('mcRemoveStateScheduled')) {
186      return;
187    }
188
189    $managedFile.data('mcRemoveStateScheduled', true);
190
191    requestNextFrame(function () {
192      $managedFile.removeData('mcRemoveStateScheduled');
193      updateRemoveButtonState($managedFile);
194    });
195  }
196
197  Drupal.behaviors.fileValidateErrorPlacement = {
198    attach: function (context, settings) {
199      // Keep Drupal core file validation and only alter where the message is rendered.
200      if (settings.file && settings.file.elements) {
201        Object.keys(settings.file.elements).forEach(function (selector) {
202          once('fileValidateErrorPlacement', selector, context).forEach(function (input) {
203            const $input = $(input);
204            const $managedFile = $input.closest(MANAGED_FILE_SELECTOR);
205
206            // This behavior only applies to managed file widgets.
207            if (!$managedFile.length) {
208              return;
209            }
210
211            // Process errors already present after an Ajax refresh.
212            scheduleFileErrorPlacement($input);
213
214            // Handle valid file selections: core does NOT call stopImmediatePropagation()
215            // in this case, so the change event reaches this handler normally.
216            $(input).on('change.fileValidateErrorPlacement', function () {
217              // Run after core validation has injected any error message.
218              scheduleFileErrorPlacement($input);
219            });
220
221            // Handle invalid file selections: core's change.fileValidate calls
222            // stopImmediatePropagation() after injecting div.file-upload-js-error,
223            // which prevents the change.fileValidateErrorPlacement handler above
224            // from ever firing when it is bound after change.fileValidate (i.e. on
225            // initial page load).  A MutationObserver watching for the core error
226            // div being added to the DOM is independent of event-handler order and
227            // guarantees processFileErrorPlacement always runs.
228            const observer = new MutationObserver(function (mutations) {
229              const coreErrorAdded = mutations.some(function (mutation) {
230                return Array.from(mutation.addedNodes).some(function (node) {
231                  // Only react to the specific <div class="file-upload-js-error">
232                  // element that core injects on validation failure.
233                  return node.nodeType === Node.ELEMENT_NODE &&
234                    node.tagName === 'DIV' &&
235                    node.classList.contains('file-upload-js-error');
236                });
237              });
238              if (coreErrorAdded) {
239                scheduleFileErrorPlacement($input);
240              }
241            });
242
243            observer.observe($managedFile[0], { childList: true, subtree: true });
244          });
245        });
246      }
247    }
248  };
249
250  Drupal.behaviors.manageRemoveButtonState = {
251    attach: function (context, settings) {
252      once('manageRemoveButtonState', MANAGED_FILE_SELECTOR, context).forEach(function (element) {
253        const $managedFile = $(element);
254
255        // Initialize button state
256        updateRemoveButtonState($managedFile);
257
258        // Update button state when file selection checkboxes change
259        $managedFile.on('change.manageRemoveButtonState', FILE_CHECKBOX_SELECTOR, function () {
260          updateRemoveButtonState($managedFile);
261        });
262
263        // Update button state after Ajax file uploads complete
264        $managedFile.on('ajaxComplete.manageRemoveButtonState', function () {
265          scheduleRemoveButtonStateUpdate($managedFile);
266        });
267      });
268    }
269  };
270
271  Drupal.behaviors.modalFocusTrap = {
272    attach: function (context, settings) {
273      // Trap focus for modals within forms.
274      once('mcmultiform-contact-modal-focus', '.contact-modal', context).forEach(function (modal) {
275        const $modal = $(modal);
276
277        $modal.on('shown.bs.modal', function () {
278          const $focusable = getFocusableElements($modal);
279
280          if ($focusable.length) {
281            $focusable.first().trigger('focus');
282          }
283
284          $modal.on('keydown.mcmultiformFocusTrap', function (event) {
285            if (event.key !== 'Tab' && event.which !== 9) {
286              return;
287            }
288
289            const $tabbable = getFocusableElements($modal);
290
291            if (!$tabbable.length) {
292              event.preventDefault();
293              $modal.trigger('focus');
294              return;
295            }
296
297            if ($tabbable.length === 1) {
298              event.preventDefault();
299              $tabbable.first().trigger('focus');
300              return;
301            }
302
303            const first = $tabbable.get(0);
304            const last = $tabbable.get($tabbable.length - 1);
305            const activeElement = document.activeElement;
306
307            if (!event.shiftKey && activeElement === last) {
308              event.preventDefault();
309              first.focus();
310            }
311
312            if (event.shiftKey && activeElement === first) {
313              event.preventDefault();
314              last.focus();
315            }
316          });
317        });
318
319        $modal.on('hidden.bs.modal', function () {
320          $modal.off('keydown.mcmultiformFocusTrap');
321        });
322      });
323    }
324  };
325
326  Drupal.behaviors.roleButtonSpaceKeySupport = {
327    attach: function (context) {
328      once('mcmultiform-role-button-space-key', ROLE_BUTTON_ANCHOR_SELECTOR, context).forEach(function (element) {
329        $(element).on('keydown.mcmultiformRoleButtonSpace', function (event) {
330          if (event.key !== ' ' && event.key !== 'Space' && event.key !== 'Spacebar') {
331            return;
332          }
333
334          // Match native button keyboard activation with Space key.
335          event.preventDefault();
336          event.currentTarget.click();
337        });
338      });
339    }
340  };
341})(jQuery, Drupal, once);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.