diff --git a/addons/website/static/src/snippets/s_website_form/000.js b/addons/website/static/src/snippets/s_website_form/000.js index f0fd4e04102..ea5dedb5e6c 100644 --- a/addons/website/static/src/snippets/s_website_form/000.js +++ b/addons/website/static/src/snippets/s_website_form/000.js @@ -266,7 +266,19 @@ self.$el.find('#s_website_form_result, #o_website_form_result').empty(); // !compatibility if (!self.check_error_fields({})) { - self.update_status('error', _t("Please fill in the form correctly.")); + if (this.fileInputError) { + const errorMessage = this.fileInputError.type === "number" + ? _.str.sprintf(_t( + "Please fill in the form correctly. You uploaded too many files. (Maximum %s files)" + ), this.fileInputError.limit) + : _.str.sprintf(_t( + "Please fill in the form correctly. The file \"%s\" is too big. (Maximum %s MB)" + ), this.fileInputError.fileName, this.fileInputError.limit); + this.update_status("error", errorMessage); + delete this.fileInputError; + } else { + this.update_status("error", _t("Please fill in the form correctly.")); + } return false; } @@ -439,6 +451,8 @@ if (!self.is_datetime_valid(input.value, 'datetime')) { return true; } + } else if (input.type === "file" && !self.isFileInputValid(input)) { + return true; } // Note that checkValidity also takes care of the case where @@ -531,6 +545,39 @@ }))); }, + /** + * Checks if the file input is valid: if the number of files uploaded + * and their size do not exceed the limits that were set. + * + * @param {HTMLElement} inputEl an input of type file + * @returns {Boolean} true if the input is valid, false otherwise. + */ + isFileInputValid(inputEl) { + // Note: the `maxFilesNumber` and `maxFileSize` data-attributes may + // not always be present, if the Form comes from an older version + // for example. + + // Checking the number of files. + const maxFilesNumber = inputEl.dataset.maxFilesNumber; + if (maxFilesNumber && inputEl.files.length > maxFilesNumber) { + // Store information to display the error message later. + this.fileInputError = {type: "number", limit: maxFilesNumber}; + return false; + } + // Checking the files size. + const maxFileSize = inputEl.dataset.maxFileSize; // in megabytes. + const bytesInMegabyte = 1_000_000; + if (maxFileSize) { + for (const file of Object.values(inputEl.files)) { + if (file.size / bytesInMegabyte > maxFileSize) { + this.fileInputError = {type: "size", limit: maxFileSize, fileName: file.name}; + return false; + } + } + } + return true; + }, + //---------------------------------------------------------------------- // Private //---------------------------------------------------------------------- diff --git a/addons/website/static/src/snippets/s_website_form/options.js b/addons/website/static/src/snippets/s_website_form/options.js index 46fb5b08715..d7d5e8e7e25 100644 --- a/addons/website/static/src/snippets/s_website_form/options.js +++ b/addons/website/static/src/snippets/s_website_form/options.js @@ -266,6 +266,7 @@ const FieldEditor = FormEditor.extend({ const classList = this.$target[0].classList; const textarea = this.$target[0].querySelector('textarea'); const input = this.$target[0].querySelector('input[type="text"], input[type="email"], input[type="number"], input[type="tel"], input[type="url"], textarea'); + const fileInputEl = this.$target[0].querySelector("input[type=file]"); const description = this.$target[0].querySelector('.s_website_form_field_description'); field.placeholder = input && input.placeholder; if (input) { @@ -273,6 +274,9 @@ const FieldEditor = FormEditor.extend({ field.value = input.getAttribute('value') || input.value; } else if (field.type === 'boolean') { field.value = !!this.$target[0].querySelector('input[type="checkbox"][checked]'); + } else if (fileInputEl) { + field.maxFilesNumber = fileInputEl.dataset.maxFilesNumber; + field.maxFileSize = fileInputEl.dataset.maxFileSize; } // property value is needed for date/datetime (formated date). field.propertyValue = input && input.value; @@ -1071,6 +1075,16 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({ setVisibilityDependency(previewMode, widgetValue, params) { this._setVisibilityDependency(widgetValue); }, + /** + * @override + */ + async selectDataAttribute(previewMode, widgetValue, params) { + await this._super(...arguments); + if (params.attributeName === "maxFilesNumber") { + const allowMultipleFiles = params.activeValue > 1; + this.$target[0].toggleAttribute("multiple", allowMultipleFiles); + } + }, //---------------------------------------------------------------------- // Private @@ -1172,6 +1186,13 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({ case 'hidden_opt': case 'type_opt': return !this.$target[0].classList.contains('s_website_form_model_required'); + case "max_files_number_opt": { + // Do not display the option if only one file is supposed to be + // uploaded in the field. + const fieldEl = this.$target[0].closest(".s_website_form_field"); + return fieldEl.classList.contains("s_website_form_custom") || + ["one2many", "many2many"].includes(fieldEl.dataset.type); + } } return this._super(...arguments); }, diff --git a/addons/website/static/src/xml/website_form_editor.xml b/addons/website/static/src/xml/website_form_editor.xml index f5a5ef57eb7..619c39d445c 100644 --- a/addons/website/static/src/xml/website_form_editor.xml +++ b/addons/website/static/src/xml/website_form_editor.xml @@ -379,8 +379,10 @@ class="form-control s_website_form_input" t-att-name="field.name" t-att-required="field.required || field.modelRequired || None" - t-att-multiple="multiple" + t-att="field.maxFilesNumber > 1 and {'multiple': ''} or {}" t-att-id="field.id" + t-att-data-max-files-number="field.maxFilesNumber or '1'" + t-att-data-max-file-size="field.maxFileSize or '1'" /> diff --git a/addons/website/views/snippets/s_website_form.xml b/addons/website/views/snippets/s_website_form.xml index d3e5f6a8768..a3dc44b249f 100644 --- a/addons/website/views/snippets/s_website_form.xml +++ b/addons/website/views/snippets/s_website_form.xml @@ -137,6 +137,22 @@ data-apply-to=".s_website_form_date input"/> + + + + + Always Visible Hidden