3 * Provides JavaScript additions to the managed file field type.
5 * This file provides progress bar support (if available), popup windows for
6 * file previews, and disabling of other file fields during Ajax uploads (which
7 * prevents separate file fields from accidentally uploading files).
10 (function ($, Drupal) {
12 * Attach behaviors to the file fields passed in the settings.
14 * @type {Drupal~behavior}
16 * @prop {Drupal~behaviorAttach} attach
17 * Attaches validation for file extensions.
18 * @prop {Drupal~behaviorDetach} detach
19 * Detaches validation for file extensions.
21 Drupal.behaviors.fileValidateAutoAttach = {
22 attach(context, settings) {
23 const $context = $(context);
26 function initFileValidation(selector) {
27 $context.find(selector)
29 .on('change.fileValidate', { extensions: elements[selector] }, Drupal.file.validateExtension);
32 if (settings.file && settings.file.elements) {
33 elements = settings.file.elements;
34 Object.keys(elements).forEach(initFileValidation);
37 detach(context, settings, trigger) {
38 const $context = $(context);
41 function removeFileValidation(selector) {
42 $context.find(selector)
43 .removeOnce('fileValidate')
44 .off('change.fileValidate', Drupal.file.validateExtension);
47 if (trigger === 'unload' && settings.file && settings.file.elements) {
48 elements = settings.file.elements;
49 Object.keys(elements).forEach(removeFileValidation);
55 * Attach behaviors to file element auto upload.
57 * @type {Drupal~behavior}
59 * @prop {Drupal~behaviorAttach} attach
60 * Attaches triggers for the upload button.
61 * @prop {Drupal~behaviorDetach} detach
62 * Detaches auto file upload trigger.
64 Drupal.behaviors.fileAutoUpload = {
66 $(context).find('input[type="file"]').once('auto-file-upload').on('change.autoFileUpload', Drupal.file.triggerUploadButton);
68 detach(context, setting, trigger) {
69 if (trigger === 'unload') {
70 $(context).find('input[type="file"]').removeOnce('auto-file-upload').off('.autoFileUpload');
76 * Attach behaviors to the file upload and remove buttons.
78 * @type {Drupal~behavior}
80 * @prop {Drupal~behaviorAttach} attach
81 * Attaches form submit events.
82 * @prop {Drupal~behaviorDetach} detach
83 * Detaches form submit events.
85 Drupal.behaviors.fileButtons = {
87 const $context = $(context);
88 $context.find('.js-form-submit').on('mousedown', Drupal.file.disableFields);
89 $context.find('.js-form-managed-file .js-form-submit').on('mousedown', Drupal.file.progressBar);
92 const $context = $(context);
93 $context.find('.js-form-submit').off('mousedown', Drupal.file.disableFields);
94 $context.find('.js-form-managed-file .js-form-submit').off('mousedown', Drupal.file.progressBar);
99 * Attach behaviors to links within managed file elements for preview windows.
101 * @type {Drupal~behavior}
103 * @prop {Drupal~behaviorAttach} attach
105 * @prop {Drupal~behaviorDetach} detach
108 Drupal.behaviors.filePreviewLinks = {
110 $(context).find('div.js-form-managed-file .file a').on('click', Drupal.file.openInNewWindow);
113 $(context).find('div.js-form-managed-file .file a').off('click', Drupal.file.openInNewWindow);
118 * File upload utility functions.
122 Drupal.file = Drupal.file || {
125 * Client-side file input validation of file extensions.
127 * @name Drupal.file.validateExtension
129 * @param {jQuery.Event} event
130 * The event triggered. For example `change.fileValidate`.
132 validateExtension(event) {
133 event.preventDefault();
134 // Remove any previous errors.
135 $('.file-upload-js-error').remove();
137 // Add client side validation for the input[type=file].
138 const extensionPattern = event.data.extensions.replace(/,\s*/g, '|');
139 if (extensionPattern.length > 1 && this.value.length > 0) {
140 const acceptableMatch = new RegExp(`\\.(${extensionPattern})$`, 'gi');
141 if (!acceptableMatch.test(this.value)) {
142 const error = Drupal.t('The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.', {
143 // According to the specifications of HTML5, a file upload control
144 // should not reveal the real local path to the file that a user
145 // has selected. Some web browsers implement this restriction by
146 // replacing the local path with "C:\fakepath\", which can cause
147 // confusion by leaving the user thinking perhaps Drupal could not
148 // find the file because it messed up the file path. To avoid this
149 // confusion, therefore, we strip out the bogus fakepath string.
150 '%filename': this.value.replace('C:\\fakepath\\', ''),
151 '%extensions': extensionPattern.replace(/\|/g, ', '),
153 $(this).closest('div.js-form-managed-file').prepend(`<div class="messages messages--error file-upload-js-error" aria-live="polite">${error}</div>`);
155 // Cancel all other change event handlers.
156 event.stopImmediatePropagation();
162 * Trigger the upload_button mouse event to auto-upload as a managed file.
164 * @name Drupal.file.triggerUploadButton
166 * @param {jQuery.Event} event
167 * The event triggered. For example `change.autoFileUpload`.
169 triggerUploadButton(event) {
170 $(event.target).closest('.js-form-managed-file').find('.js-form-submit').trigger('mousedown');
174 * Prevent file uploads when using buttons not intended to upload.
176 * @name Drupal.file.disableFields
178 * @param {jQuery.Event} event
179 * The event triggered, most likely a `mousedown` event.
181 disableFields(event) {
182 const $clickedButton = $(this).findOnce('ajax');
184 // Only disable upload fields for Ajax buttons.
185 if (!$clickedButton.length) {
189 // Check if we're working with an "Upload" button.
190 let $enabledFields = [];
191 if ($clickedButton.closest('div.js-form-managed-file').length > 0) {
192 $enabledFields = $clickedButton.closest('div.js-form-managed-file').find('input.js-form-file');
195 // Temporarily disable upload fields other than the one we're currently
196 // working with. Filter out fields that are already disabled so that they
197 // do not get enabled when we re-enable these fields at the end of
198 // behavior processing. Re-enable in a setTimeout set to a relatively
199 // short amount of time (1 second). All the other mousedown handlers
200 // (like Drupal's Ajax behaviors) are executed before any timeout
201 // functions are called, so we don't have to worry about the fields being
202 // re-enabled too soon. @todo If the previous sentence is true, why not
203 // set the timeout to 0?
204 const $fieldsToTemporarilyDisable = $('div.js-form-managed-file input.js-form-file').not($enabledFields).not(':disabled');
205 $fieldsToTemporarilyDisable.prop('disabled', true);
207 $fieldsToTemporarilyDisable.prop('disabled', false);
212 * Add progress bar support if possible.
214 * @name Drupal.file.progressBar
216 * @param {jQuery.Event} event
217 * The event triggered, most likely a `mousedown` event.
220 const $clickedButton = $(this);
221 const $progressId = $clickedButton.closest('div.js-form-managed-file').find('input.file-progress');
222 if ($progressId.length) {
223 const originalName = $progressId.attr('name');
225 // Replace the name with the required identifier.
226 $progressId.attr('name', originalName.match(/APC_UPLOAD_PROGRESS|UPLOAD_IDENTIFIER/)[0]);
228 // Restore the original name after the upload begins.
230 $progressId.attr('name', originalName);
233 // Show the progress bar if the upload takes longer than half a second.
235 $clickedButton.closest('div.js-form-managed-file').find('div.ajax-progress-bar').slideDown();
240 * Open links to files within forms in a new window.
242 * @name Drupal.file.openInNewWindow
244 * @param {jQuery.Event} event
245 * The event triggered, most likely a `click` event.
247 openInNewWindow(event) {
248 event.preventDefault();
249 $(this).attr('target', '_blank');
250 window.open(this.href, 'filePreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550');