[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
This commit is contained in:
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -15,4 +15,14 @@
|
||||
<t t-esc="message"/>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
<!-- A block containing the file name and a cross to delete the file. -->
|
||||
<t t-name="website.file_block">
|
||||
<div class="o_file_block col-4">
|
||||
<div class="o_file_wrap mb-1 px-2 py-1">
|
||||
<div class="o_file_name d-inline-block w-75 pt-1" t-att-title="fileName" t-out="fileName"/>
|
||||
<i class="o_file_delete fa fa-times mt-1 float-end"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
Reference in New Issue
Block a user