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