From fdfd5042f8ea4bbb42f6841837f6d2df2e4d0417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Souk=C3=A9ina=20Bojabza?= Date: Tue, 24 May 2022 07:30:19 +0000 Subject: [PATCH] [IMP] website: manage better the files uploaded in a form Before this commit, now that multiple files can be uploaded in a form, we could observe that the file input is not convenient to manage this situation: - Clicking again on the input to choose other files replaces the previously uploaded ones, we can therefore not upload files one by one. - A file cannot be deleted in an obvious way: we need to cancel a new upload, as it will replace the file by nothing and therefore delete it. - When multiple files are uploaded, only the number of files is displayed and not their name (they are only displayed on hover). This commit improves these behaviors: - Clicking again on the input adds additional files instead of replacing them (unless the number of files allowed is 1); - The button text content is modified after the upload of files, displaying "Add Files"/"Replace File" (if multiple/single files are allowed) to show its role better. - When files are uploaded, blocks containing each files name and a cross to delete them are displayed. In order to make these improvements, the file input is hidden and replaced by an input of type=`button` when files are uploaded. This was done for two reasons: - It is not possible to modify the content of an input of type=`file` but we can change the value of a button. - The input did not look good with the file blocks, because it has a text displaying file informations, which was redundant with the blocks. task-2798576 Part-of: odoo/odoo#89055 --- .../static/src/snippets/s_website_form/000.js | 156 +++++++++++++++++- .../src/snippets/s_website_form/001.scss | 36 ++++ .../website/static/src/xml/website_form.xml | 10 ++ 3 files changed, 198 insertions(+), 4 deletions(-) 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 ea5dedb5e6c..5716d9a2983 100644 --- a/addons/website/static/src/snippets/s_website_form/000.js +++ b/addons/website/static/src/snippets/s_website_form/000.js @@ -41,6 +41,9 @@ selector: '.s_website_form form, form.s_website_form', // !compatibility events: { 'click .s_website_form_send, .o_website_form_send': 'send', // !compatibility + "change input[type=file]": "_onFileChange", + "click input.o_add_files_button": "_onAddFilesButtonClick", + "click .o_file_delete": "_onFileDeleteClick", }, /** @@ -74,7 +77,7 @@ start: function () { // Reset the form first, as it is still filled when coming back // after a redirect. - this.el.reset(); + this.resetForm(); // Prepare visibility data and update field visibilities const visibilityFunctionsByFieldName = new Map(); @@ -196,6 +199,13 @@ this._disabledStates[inputEl] = inputEl.disabled; } + // Add the files zones where the file blocks will be displayed. + this.el.querySelectorAll("input[type=file]").forEach(inputEl => { + const filesZoneEl = document.createElement("DIV"); + filesZoneEl.classList.add("o_files_zone", "row", "gx-1"); + inputEl.parentNode.insertBefore(filesZoneEl, inputEl); + }); + return this._super(...arguments).then(() => this.__startResolve()); }, @@ -203,8 +213,8 @@ this._super.apply(this, arguments); this.$el.find('button').off('click'); - // Empty imputs - this.el.reset(); + // Empty inputs + this.resetForm(); // Apply default values const dateTimeFormat = time.getLangDatetimeFormat(); @@ -394,7 +404,7 @@ } } - self.el.reset(); + self.resetForm(); self.restoreBtnLoading(); } }) @@ -406,6 +416,23 @@ }); }, + /** + * Resets a form. + */ + resetForm() { + this.el.reset(); + + // For file inputs, remove the files zone, restore the file input + // and remove the files list. + this.el.querySelectorAll("input[type=file]").forEach(inputEl => { + const fieldEl = inputEl.closest(".s_website_form_field"); + fieldEl.querySelectorAll(".o_files_zone").forEach(el => el.remove()); + fieldEl.querySelectorAll(".o_add_files_button").forEach(el => el.remove()); + inputEl.classList.remove("d-none"); + delete inputEl.fileList; + }); + }, + check_error_fields: function (error_fields) { var self = this; var form_valid = true; @@ -709,6 +736,35 @@ inputEl.disabled = !haveToBeVisible; } }, + /** + * Creates a block containing the file name and a cross to delete it. + * + * @private + * @param {Object} fileDetails the details of the file being uploaded + * @param {HTMLElement} filesZoneEl the zone where the file blocks are + * displayed + */ + _createFileBlock(fileDetails, filesZoneEl) { + const fileBlockEl = qweb.render("website.file_block", {fileName: fileDetails.name}); + filesZoneEl.insertAdjacentHTML("beforeend", fileBlockEl); + filesZoneEl.lastElementChild.fileDetails = fileDetails; + }, + /** + * Creates the file upload button (= a button to replace the file input, + * in order to modify its text content more easily). + * + * @private + * @param {HTMLElement} inputEl the file input + */ + _createAddFilesButton(inputEl) { + const addFilesButtonEl = document.createElement("INPUT"); + addFilesButtonEl.classList.add("o_add_files_button", "form-control"); + addFilesButtonEl.type = "button"; + addFilesButtonEl.value = inputEl.hasAttribute("multiple") + ? _t("Add Files") : _t("Replace File"); + inputEl.parentNode.insertBefore(addFilesButtonEl, inputEl); + inputEl.classList.add("d-none"); + }, //---------------------------------------------------------------------- // Handlers @@ -721,4 +777,96 @@ _onFieldInput() { this._updateFieldsVisibility(); }, + /** + * Called when files are uploaded: updates the button text content, + * displays the file blocks (containing the files name and a cross to + * delete them) and manages the files. + * + * @private + * @param {Event} ev + */ + _onFileChange(ev) { + const fileInputEl = ev.currentTarget; + const fieldEl = fileInputEl.closest(".s_website_form_field"); + const uploadedFiles = fileInputEl.files; + const addFilesButtonEl = fieldEl.querySelector(".o_add_files_button"); + + // The zone where the file blocks are displayed. + let filesZoneEl = fieldEl.querySelector(".o_files_zone"); + // Update the button text content. + if (!addFilesButtonEl) { + this._createAddFilesButton(fileInputEl); + } + + // Create a list to keep track of the files. + if (!fileInputEl.fileList) { + fileInputEl.fileList = new DataTransfer(); + } + + // If only one file can be uploaded, delete the previous file. + if (!fileInputEl.hasAttribute("multiple") && uploadedFiles.length > 0) { + fileInputEl.fileList = new DataTransfer(); + const fileBlockEl = fieldEl.querySelector(".o_file_block"); + if (fileBlockEl) { + fileBlockEl.remove(); + } + } + + // Add the uploaded files if they are not already there. + for (const newFile of uploadedFiles) { + if (![...fileInputEl.fileList.files].some(file => newFile.name === file.name && + newFile.size === file.size && newFile.type === file.type)) { + fileInputEl.fileList.items.add(newFile); + const fileDetails = {name: newFile.name, size: newFile.size, type: newFile.type}; + this._createFileBlock(fileDetails, filesZoneEl); + } + } + // Update the input files. + fileInputEl.files = fileInputEl.fileList.files; + }, + /** + * Called when a file is deleted by clicking on the cross on the block + * describing it. + * + * @private + * @param {Event} ev + */ + _onFileDeleteClick(ev) { + const fileBlockEl = ev.target.closest(".o_file_block"); + const fieldEl = fileBlockEl.closest(".s_website_form_field"); + const fileInputEl = fieldEl.querySelector("input[type=file]"); + const fileDetails = fileBlockEl.fileDetails; + const addFilesButtonEl = fieldEl.querySelector(".o_add_files_button"); + + // Create a new file list containing the remaining files. + const newFileList = new DataTransfer(); + for (const file of Object.values(fileInputEl.fileList.files)) { + if (file.name !== fileDetails.name || file.size !== fileDetails.size + || file.type !== fileDetails.type) { + newFileList.items.add(file); + } + } + // Update the input lists and remove the file block. + Object.assign(fileInputEl, {fileList: newFileList, files: newFileList.files}); + fileBlockEl.remove(); + + // Restore the file input if there are no files uploaded and update + // the fields visibility. + if (!newFileList.files.length) { + fileInputEl.classList.remove("d-none"); + addFilesButtonEl.remove(); + this._updateFieldsVisibility(); + } + }, + /** + * Detects when the fake input file button is clicked to simulate a + * click on the real input. + * + * @private + * @param {MouseEvent} ev + */ + _onAddFilesButtonClick(ev) { + const fileInputEl = ev.target.parentNode.querySelector("input[type=file]"); + fileInputEl.click(); + }, }); diff --git a/addons/website/static/src/snippets/s_website_form/001.scss b/addons/website/static/src/snippets/s_website_form/001.scss index dcd5accd916..df2db1ff133 100644 --- a/addons/website/static/src/snippets/s_website_form/001.scss +++ b/addons/website/static/src/snippets/s_website_form/001.scss @@ -64,6 +64,42 @@ display: none; } } + + // File blocks. + div.o_files_zone { + div.o_file_wrap { + border: 1px solid $o-gray-400; + border-radius: 0.25rem; + font-size: 0.9em; + line-height: normal; + + div.o_file_name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + i.o_file_delete { + max-width: 25%; + padding: 2px 4px 3px 4px; + color: $primary; + cursor: pointer; + + &:hover { + color: darken($primary, 7.5%); + } + } + } + } + + .o_add_files_button { + background-color: $o-gray-200; + width: fit-content; + + &:hover { + background-color: darken($o-gray-200, 4.5%); + } + } } body:not(.editor_enable) .s_website_form[data-vcss="001"] { diff --git a/addons/website/static/src/xml/website_form.xml b/addons/website/static/src/xml/website_form.xml index b8397688e50..4596493684c 100644 --- a/addons/website/static/src/xml/website_form.xml +++ b/addons/website/static/src/xml/website_form.xml @@ -15,4 +15,14 @@ + + + +
+
+
+ +
+
+