hunk.code_window:                             // Prevent double-clicking on the send button and
                               // add a upload loading effect (delay before success
                               // message)
                               await concurrency.delay(dom.DEBOUNCE);
                               self.el.classList.add('d-none');
                               self.el.parentElement.querySelector('.s_website_form_end_message').classList.remove('d-none');
                               break;
                           }
  
hunk.labels: keep keep keep replace keep keep keep keep keep
hunk.after_edit:                             await delay(dom.DEBOUNCE);
  
hunk.file_path: addons/website/static/src/snippets/s_website_form/000.js
hunk.type: replace
file: /** @odoo-module **/
  
      import time from "@web/legacy/js/core/time";
      import {ReCaptcha} from "@google_recaptcha/js/recaptcha";
      import session from "web.session";
      import ajax from "@web/legacy/js/core/ajax";
      import publicWidget from "@web/legacy/js/public/public_widget";
      import dom from "@web/legacy/js/core/dom";
      import concurrency from "@web/legacy/js/core/concurrency";
      import { debounce } from "@web/core/utils/timing";
      import { _t } from "@web/core/l10n/translation";
      import { renderToElement } from "@web/core/utils/render";
  
      publicWidget.registry.EditModeWebsiteForm = publicWidget.Widget.extend({
          selector: '.s_website_form form, form.s_website_form', // !compatibility
          disabledInEditableMode: false,
          /**
           * @override
           */
          start: function () {
              if (this.editableMode) {
                  // We do not initialize the datetime picker in edit mode but want the dates to be formated
                  const dateTimeFormat = time.getLangDatetimeFormat();
                  const dateFormat = time.getLangDateFormat();
                  this.el.querySelectorAll('.s_website_form_input.datetimepicker-input').forEach(el => {
                      const value = el.getAttribute('value');
                      if (value) {
                          const format = el.closest('.s_website_form_field').dataset.type === 'date' ? dateFormat : dateTimeFormat;
                          el.value = moment.unix(value).format(format);
                      }
                  });
              }
              return this._super(...arguments);
          },
      });
  
      publicWidget.registry.s_website_form = publicWidget.Widget.extend({
          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",
          },
  
          /**
           * @constructor
           */
          init: function () {
              this._super(...arguments);
              this._recaptcha = new ReCaptcha();
              this.initialValues = new Map();
              this._visibilityFunctionByFieldName = new Map();
              this._visibilityFunctionByFieldEl = new Map();
              this.__started = new Promise(resolve => this.__startResolve = resolve);
          },
          willStart: async function () {
              const res = this._super(...arguments);
              if (!this.el.classList.contains('s_website_form_no_recaptcha')) {
                  this._recaptchaLoaded = true;
                  this._recaptcha.loadLibs();
              }
              // fetch user data (required by fill-with behavior)
              this.preFillValues = {};
              if (session.user_id) {
                  this.preFillValues = (await this._rpc({
                      model: 'res.users',
                      method: 'read',
                      args: [session.user_id, this._getUserPreFillFields()],
                  }))[0] || {};
              }
              return res;
          },
          start: function () {
              // Reset the form first, as it is still filled when coming back
              // after a redirect.
              this.resetForm();
  
              // Prepare visibility data and update field visibilities
              const visibilityFunctionsByFieldName = new Map();
              for (const fieldEl of this.el.querySelectorAll('[data-visibility-dependency]')) {
                  const inputName = fieldEl.querySelector('.s_website_form_input').name;
                  if (!visibilityFunctionsByFieldName.has(inputName)) {
                      visibilityFunctionsByFieldName.set(inputName, []);
                  }
                  const func = this._buildVisibilityFunction(fieldEl);
                  visibilityFunctionsByFieldName.get(inputName).push(func);
                  this._visibilityFunctionByFieldEl.set(fieldEl, func);
              }
              for (const [name, funcs] of visibilityFunctionsByFieldName.entries()) {
                  this._visibilityFunctionByFieldName.set(name, () => funcs.some(func => func()));
              }
  
              this._onFieldInputDebounced = debounce(this._onFieldInput.bind(this), 400);
              this.$el.on('input.s_website_form', '.s_website_form_field', this._onFieldInputDebounced);
  
              // Initialize datetimepickers
              var datepickers_options = {
                  minDate: moment({y: 1000}),
                  maxDate: moment({y: 9999, M: 11, d: 31}),
                  calendarWeeks: true,
                  icons: {
                      time: 'fa fa-clock-o',
                      date: 'fa fa-calendar',
                      next: 'oi oi-chevron-right',
                      previous: 'oi oi-chevron-left',
                      up: 'oi oi-chevron-up',
                      down: 'oi oi-chevron-down',
                  },
                  locale: moment.locale(),
                  format: time.getLangDatetimeFormat(),
                  extraFormats: ['X'],
              };
              const $datetimes = this.$el.find('.s_website_form_datetime, .o_website_form_datetime'); // !compatibility
              $datetimes.datetimepicker(datepickers_options);
  
              // Adapt options to date-only pickers
              datepickers_options.format = time.getLangDateFormat();
              const $dates = this.$el.find('.s_website_form_date, .o_website_form_date'); // !compatibility
              $dates.datetimepicker(datepickers_options);
  
              this.$allDates = $datetimes.add($dates);
              this.$allDates.addClass('s_website_form_datepicker_initialized');
  
              // Display form values from tag having data-for attribute
              // It's necessary to handle field values generated on server-side
              // Because, using t-att- inside form make it non-editable
              // Data-fill-with attribute is given during registry and is used by
              // to know which user data should be used to prfill fields.
              const dataForEl = document.querySelector(`[data-for='${this.el.id}']`);
              this.editTranslations = !!this._getContext(true).edit_translations;
              // On the "edit_translations" mode, a <span/> with a translated term
              // will replace the attribute value, leading to some inconsistencies
              // (setting again the <span> on the attributes after the editor's
              // cleanup, setting wrong values on the attributes after translating
              // default values...)
              if (!this.editTranslations && (dataForEl || Object.keys(this.preFillValues).length)) {
                  const dataForValues = dataForEl ?
                      JSON.parse(dataForEl.dataset.values
                          // replaces `True` by `true` if they are after `,` or `:` or `[`
                          .replace(/([,:\[]\s*)True/g, '$1true')
                          // replaces `False` and `None` by `""` if they are after `,` or `:` or `[`
                          .replace(/([,:\[]\s*)(False|None)/g, '$1""')
                          // replaces the `'` by `"` if they are before `,` or `:` or `]` or `}`
                          .replace(/'(\s*[,:\]}])/g, '"$1')
                          // replaces the `'` by `"` if they are after `{` or `[` or `,` or `:`
                          .replace(/([{\[:,]\s*)'/g, '$1"')
                      ) : {};
                  const fieldNames = this.$el.serializeArray().map(el => el.name);
                  // All types of inputs do not have a value property (eg:hidden),
                  // for these inputs any function that is supposed to put a value
                  // property actually puts a HTML value attribute. Because of
                  // this, we have to clean up these values at destroy or else the
                  // data loaded here could become default values. We could set
                  // the values to submit() for these fields but this could break
                  // customizations that use the current behavior as a feature.
                  for (const name of fieldNames) {
                      const fieldEl = this.el.querySelector(`[name="${name}"]`);
  
                      // In general, we want the data-for and prefill values to
                      // take priority over set default values. The 'email_to'
                      // field is however treated as an exception at the moment
                      // so that values set by users are always used.
                      if (name === 'email_to' && fieldEl.value
                              // The following value is the default value that
                              // is set if the form is edited in any way. (see the
                              // @website/js/form_editor_registry module in editor
                              // assets bundle).
                              // TODO that value should probably never be forced
                              // unless explicitely manipulated by the user or on
                              // custom form addition but that seems risky to
                              // change as a stable fix.
                              && fieldEl.value !== 'info@yourcompany.example.com') {
                          continue;
                      }
  
                      let newValue;
                      if (dataForValues && dataForValues[name]) {
                          newValue = dataForValues[name];
                      } else if (this.preFillValues[fieldEl.dataset.fillWith]) {
                          newValue = this.preFillValues[fieldEl.dataset.fillWith];
                      }
                      if (newValue) {
                          this.initialValues.set(fieldEl, fieldEl.getAttribute('value'));
                          fieldEl.value = newValue;
                      }
                  }
              }
              this._updateFieldsVisibility();
  
              if (session.geoip_phone_code) {
                  this.el.querySelectorAll('input[type="tel"]').forEach(telField => {
                      if (!telField.value) {
                          telField.value = '+' + session.geoip_phone_code;
                      }
                  });
              }
              // Check disabled states
              this.inputEls = this.el.querySelectorAll('.s_website_form_field.s_website_form_field_hidden_if .s_website_form_input');
              this._disabledStates = new Map();
              for (const inputEl of this.inputEls) {
                  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());
          },
  
          destroy: function () {
              this._super.apply(this, arguments);
              this.$el.find('button').off('click');
  
              // Empty inputs
              this.resetForm();
  
              // Apply default values
              const dateTimeFormat = time.getLangDatetimeFormat();
              const dateFormat = time.getLangDateFormat();
              this.el.querySelectorAll('input[type="text"], input[type="email"], input[type="number"]').forEach(el => {
                  let value = el.getAttribute('value');
                  if (value) {
                      if (el.classList.contains('datetimepicker-input')) {
                          const format = el.closest('.s_website_form_field').dataset.type === 'date' ? dateFormat : dateTimeFormat;
                          value = moment.unix(value).format(format);
                      }
                      el.value = value;
                  }
              });
              this.el.querySelectorAll('textarea').forEach(el => el.value = el.textContent);
  
              // Remove saving of the error colors
              this.$el.find('.o_has_error').removeClass('o_has_error').find('.form-control, .form-select').removeClass('is-invalid');
  
              // Remove the status message
              this.$el.find('#s_website_form_result, #o_website_form_result').empty(); // !compatibility
  
              // Remove the success message and display the form
              this.$el.removeClass('d-none');
              this.$el.parent().find('.s_website_form_end_message').addClass('d-none');
  
              // Reinitialize dates
              this.$allDates.removeClass('s_website_form_datepicker_initialized');
  
              // Restore disabled attribute
              for (const inputEl of this.inputEls) {
                  inputEl.disabled = !!this._disabledStates.get(inputEl);
              }
  
              // All 'hidden if' fields start with d-none
              this.el.querySelectorAll('.s_website_form_field_hidden_if:not(.d-none)').forEach(el => el.classList.add('d-none'));
  
              // Reset the initial default values.
              for (const [fieldEl, initialValue] of this.initialValues.entries()) {
                  if (initialValue) {
                      fieldEl.setAttribute('value', initialValue);
                  } else {
                      fieldEl.removeAttribute('value');
                  }
              }
  
              this.$el.off('.s_website_form');
          },
  
          send: async function (e) {
              e.preventDefault(); // Prevent the default submit behavior
               // Prevent users from crazy clicking
              const $button = this.$el.find('.s_website_form_send, .o_website_form_send');
              $button.addClass('disabled') // !compatibility
                     .attr('disabled', 'disabled');
              this.restoreBtnLoading = dom.addButtonLoadingEffect($button[0]);
  
              var self = this;
  
              self.$el.find('#s_website_form_result, #o_website_form_result').empty(); // !compatibility
              if (!self.check_error_fields({})) {
                  if (this.fileInputError) {
                      const errorMessage = this.fileInputError.type === "number"
                          ? _t(
                              "Please fill in the form correctly. You uploaded too many files. (Maximum %s files)", 
                              this.fileInputError.limit
                          )
                          : _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;
              }
  
              // Prepare form inputs
              this.form_fields = this.$el.serializeArray();
              $.each(this.$el.find('input[type=file]:not([disabled])'), (outer_index, input) => {
                  $.each($(input).prop('files'), function (index, file) {
                      // Index field name as ajax won't accept arrays of files
                      // when aggregating multiple files into a single field value
                      self.form_fields.push({
                          name: input.name + '[' + outer_index + '][' + index + ']',
                          value: file
                      });
                  });
              });
  
              // Serialize form inputs into a single object
              // Aggregate multiple values into arrays
              var form_values = {};
              this.form_fields.forEach((input) => {
                  if (input.name in form_values) {
                      // If a value already exists for this field,
                      // we are facing a x2many field, so we store
                      // the values in an array.
                      if (Array.isArray(form_values[input.name])) {
                          form_values[input.name].push(input.value);
                      } else {
                          form_values[input.name] = [form_values[input.name], input.value];
                      }
                  } else {
                      if (input.value !== '') {
                          form_values[input.name] = input.value;
                      }
                  }
              });
  
              // force server date format usage for existing fields
              this.$el.find('.s_website_form_field:not(.s_website_form_custom)')
              .find('.s_website_form_date, .s_website_form_datetime').each(function () {
                  const inputEl = this.querySelector('input');
  
                  // Datetimepicker('viewDate') will return `new Date()` if the
                  // input is empty but we want to keep the empty value
                  if (!inputEl.value) {
                      return;
                  }
  
                  var date = $(this).datetimepicker('viewDate').clone().locale('en');
                  var format = 'YYYY-MM-DD';
                  if ($(this).hasClass('s_website_form_datetime')) {
                      date = date.utc();
                      format = 'YYYY-MM-DD HH:mm:ss';
                  }
                  form_values[inputEl.getAttribute('name')] = date.format(format);
              });
  
              if (this._recaptchaLoaded) {
                  const tokenObj = await this._recaptcha.getToken('website_form');
                  if (tokenObj.token) {
                      form_values['recaptcha_token_response'] = tokenObj.token;
                  } else if (tokenObj.error) {
                      self.update_status('error', tokenObj.error);
                      return false;
                  }
              }
  
              // Post form and handle result
              ajax.post(this.$el.attr('action') + (this.$el.data('force_action') || this.$el.data('model_name')), form_values)
              .then(async function (result_data) {
                  // Restore send button behavior
                  self.$el.find('.s_website_form_send, .o_website_form_send')
                      .removeAttr('disabled')
                      .removeClass('disabled'); // !compatibility
                  result_data = JSON.parse(result_data);
                  if (!result_data.id) {
                      // Failure, the server didn't return the created record ID
                      self.update_status('error', result_data.error ? result_data.error : false);
                      if (result_data.error_fields) {
                          // If the server return a list of bad fields, show these fields for users
                          self.check_error_fields(result_data.error_fields);
                      }
                  } else {
                      // Success, redirect or update status
                      let successMode = self.el.dataset.successMode;
                      let successPage = self.el.dataset.successPage;
                      if (!successMode) {
                          successPage = self.$el.attr('data-success_page'); // Compatibility
                          successMode = successPage ? 'redirect' : 'nothing';
                      }
                      switch (successMode) {
                          case 'redirect': {
                              let hashIndex = successPage.indexOf("#");
                              if (hashIndex > 0) {
                                  // URL containing an anchor detected: extract
                                  // the anchor from the URL if the URL is the
                                  // same as the current page URL so we can scroll
                                  // directly to the element (if found) later
                                  // instead of redirecting.
                                  // Note that both currentUrlPath and successPage
                                  // can exist with or without a trailing slash
                                  // before the hash (e.g. "domain.com#footer" or
                                  // "domain.com/#footer"). Therefore, if they are
                                  // not present, we add them to be able to
                                  // compare the two variables correctly.
                                  let currentUrlPath = window.location.pathname;
                                  if (!currentUrlPath.endsWith("/")) {
                                      currentUrlPath = currentUrlPath + "/";
                                  }
                                  if (!successPage.includes("/#")) {
                                      successPage = successPage.replace("#", "/#");
                                      hashIndex++;
                                  }
                                  if ([successPage, "/" + session.lang_url_code + successPage].some(link => link.startsWith(currentUrlPath + '#'))) {
                                      successPage = successPage.substring(hashIndex);
                                  }
                              }
                              if (successPage.charAt(0) === "#") {
                                  const successAnchorEl = document.getElementById(successPage.substring(1));
                                  if (successAnchorEl) {
                                      // Check if the target of the link is a modal.
                                      if (successAnchorEl.classList.contains("modal")) {
                                          // Trigger a "hashChange" event to
                                          // notify the popup widget to show the
                                          // popup.
                                          window.location.href = successPage;
                                      } else {
                                          await dom.scrollTo(successAnchorEl, {
                                              duration: 500,
                                              extraOffset: 0,
                                          });
                                      }
                                  }
                                  break;
                              }
                              $(window.location).attr('href', successPage);
                              return;
                          }
                          case 'message': {
                              // Prevent double-clicking on the send button and
                              // add a upload loading effect (delay before success
                              // message)
                              await concurrency.delay(dom.DEBOUNCE);
  
                              self.el.classList.add('d-none');
                              self.el.parentElement.querySelector('.s_website_form_end_message').classList.remove('d-none');
                              break;
                          }
                          default: {
                              // Prevent double-clicking on the send button and
                              // add a upload loading effect (delay before success
                              // message)
                              await concurrency.delay(dom.DEBOUNCE);
  
                              self.update_status('success');
                              break;
                          }
                      }
  
                      self.resetForm();
                      self.restoreBtnLoading();
                  }
              })
              .guardedCatch(error => {
                  this.update_status(
                      'error',
                      error.status && error.status === 413 ? _t("Uploaded file is too large.") : "",
                  );
              });
          },
  
          /**
           * 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;
              // Loop on all fields
              this.$el.find('.form-field, .s_website_form_field').each(function (k, field) { // !compatibility
                  var $field = $(field);
                  // FIXME that seems broken, "for" does not contain the field
                  // but this is used to retrieve errors sent from the server...
                  // need more investigation.
                  var field_name = $field.find('.col-form-label').attr('for');
  
                  // Validate inputs for this field
                  var inputs = $field.find('.s_website_form_input, .o_website_form_input').not('#editable_select'); // !compatibility
                  var invalid_inputs = inputs.toArray().filter(function (input, k, inputs) {
                      // Special check for multiple required checkbox for same
                      // field as it seems checkValidity forces every required
                      // checkbox to be checked, instead of looking at other
                      // checkboxes with the same name and only requiring one
                      // of them to be valid.
                      if (input.required && input.type === 'checkbox') {
                          // Considering we are currently processing a single
                          // field, we can assume that all checkboxes in the
                          // inputs variable have the same name
                          // TODO should be improved: probably do not need to
                          // filter neither on required, nor on checkbox and
                          // checking the validity of the group of checkbox is
                          // currently done for each checkbox of that group...
                          var checkboxes = inputs.filter(input => input.required && input.type === 'checkbox');
                          return !checkboxes.some((checkbox) => checkbox.checkValidity());
  
                      // Special cases for dates and datetimes
                      // FIXME this seems like dead code, the inputs do not use
                      // those classes, their parent does (but it seemed to work
                      // at some point given that https://github.com/odoo/odoo/commit/75e03c0f7692a112e1b0fa33267f4939363f3871
                      // was made)... need more investigation (if restored,
                      // consider checking the date inputs are not disabled before
                      // saying they are invalid (see checkValidity used here))
                      } else if ($(input).hasClass('s_website_form_date') || $(input).hasClass('o_website_form_date')) { // !compatibility
                          if (!self.is_datetime_valid(input.value, 'date')) {
                              return true;
                          }
                      } else if ($(input).hasClass('s_website_form_datetime') || $(input).hasClass('o_website_form_datetime')) { // !compatibility
                          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
                      // the input is disabled, in which case, it is considered
                      // valid (as the data will not be sent anyway).
                      // This takes care of conditionally-hidden fields (whose
                      // inputs are disabled while they are hidden) which should
                      // not require validation while they are hidden. Indeed,
                      // their purpose is to be able to enter additional data when
                      // some condition is fulfilled. If such a field is required,
                      // it is only required when visible for example.
                      return !input.checkValidity();
                  });
  
                  // Update field color if invalid or erroneous
                  const $controls = $field.find('.form-control, .form-select, .form-check-input');
                  $field.removeClass('o_has_error');
                  $controls.removeClass('is-invalid');
                  if (invalid_inputs.length || error_fields[field_name]) {
                      $field.addClass('o_has_error');
                      $controls.addClass('is-invalid');
                      if (typeof error_fields[field_name] === "string") {
                          $field.popover({content: error_fields[field_name], trigger: 'hover', container: 'body', placement: 'top'});
                          // update error message and show it.
                          const popover = Popover.getInstance($field);
                          popover._config.content = error_fields[field_name];
                          $field.popover('show');
                      }
                      form_valid = false;
                  }
              });
              return form_valid;
          },
  
          is_datetime_valid: function (value, type_of_date) {
              if (value === "") {
                  return true;
              } else {
                  try {
                      this.parse_date(value, type_of_date);
                      return true;
                  } catch {
                      return false;
                  }
              }
          },
  
          // This is a stripped down version of format.js parse_value function
          parse_date: function (value, type_of_date, value_if_empty) {
              var date_pattern = time.getLangDateFormat(),
                  time_pattern = time.getLangTimeFormat();
              var date_pattern_wo_zero = date_pattern.replace('MM', 'M').replace('DD', 'D'),
                  time_pattern_wo_zero = time_pattern.replace('HH', 'H').replace('mm', 'm').replace('ss', 's');
              switch (type_of_date) {
                  case 'datetime':
                      var datetime = moment(value, [date_pattern + ' ' + time_pattern, date_pattern_wo_zero + ' ' + time_pattern_wo_zero], true);
                      if (datetime.isValid()) {
                          return time.datetime_to_str(datetime.toDate());
                      }
                      throw new Error(_t("'%s' is not a correct datetime", value));
                  case 'date':
                      var date = moment(value, [date_pattern, date_pattern_wo_zero], true);
                      if (date.isValid()) {
                          return time.date_to_str(date.toDate());
                      }
                      throw new Error(_t("'%s' is not a correct date", value));
              }
              return value;
          },
  
          update_status: function (status, message) {
              if (status !== 'success') { // Restore send button behavior if result is an error
                  this.$el.find('.s_website_form_send, .o_website_form_send')
                      .removeAttr('disabled')
                      .removeClass('disabled'); // !compatibility
                  this.restoreBtnLoading();
              }
              var $result = this.$('#s_website_form_result, #o_website_form_result'); // !compatibility
  
              if (status === 'error' && !message) {
                  message = _t("An error has occured, the form has not been sent.");
              }
  
              // Note: we still need to wait that the widget is properly started
              // before any qweb rendering which depends on xml assets
              // because the event handlers are binded before the call to
              // willStart for public widgets...
              this.__started.then(() => $result.replaceWith(renderToElement(`website.s_website_form_status_${status}`, {
                  message: message,
              })));
          },
  
          /**
           * 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
          //----------------------------------------------------------------------
  
          /**
           * Gets the user's field needed to be fetched to pre-fill the form.
           *
           * @returns {string[]} List of user's field that have to be fetched.
           */
          _getUserPreFillFields() {
              return ['name', 'phone', 'email', 'commercial_company_name'];
          },
          /**
           * Compares the value with the comparable (and the between) with
           * comparator as a means to compare
           *
           * @private
           * @param {string} comparator The way that $value and $comparable have
           *      to be compared
           * @param {string} [value] The value of the field
           * @param {string} [comparable] The value to compare
           * @param {string} [between] The maximum date value in case comparator
           *      is between or !between
           * @returns {boolean}
           */
          _compareTo(comparator, value = '', comparable, between) {
              // Value can be null when the compared field is supposed to be
              // visible, but is not yet retrievable from the FormData() because
              // the field was conditionally hidden. It can be considered empty.
              if (value === null) {
                  value = '';
              }
  
              switch (comparator) {
                  case 'contains':
                      return value.includes(comparable);
                  case '!contains':
                      return !value.includes(comparable);
                  case 'equal':
                  case 'selected':
                      return value === comparable;
                  case '!equal':
                  case '!selected':
                      return value !== comparable;
                  case 'set':
                      return value;
                  case '!set':
                      return !value;
                  case 'greater':
                      return value > comparable;
                  case 'less':
                      return value < comparable;
                  case 'greater or equal':
                      return value >= comparable;
                  case 'less or equal':
                      return value <= comparable;
                  case 'fileSet':
                      return value.name !== '';
                  case '!fileSet':
                      return value.name === '';
              }
              // Date & Date Time comparison requires formatting the value
              if (value.includes(':')) {
                  const datetimeFormat = time.getLangDatetimeFormat();
                  value = moment(value, datetimeFormat)._d.getTime() / 1000;
              } else {
                  const dateFormat = time.getLangDateFormat();
                  value = moment(value, dateFormat)._d.getTime() / 1000;
              }
              comparable = parseInt(comparable);
              between = parseInt(between) || '';
              switch (comparator) {
                  case 'dateEqual':
                      return value === comparable;
                  case 'date!equal':
                      return value !== comparable;
                  case 'before':
                      return value < comparable;
                  case 'after':
                      return value > comparable;
                  case 'equal or before':
                      return value <= comparable;
                  case 'between':
                      return value >= comparable && value <= between;
                  case '!between':
                      return !(value >= comparable && value <= between);
                  case 'equal or after':
                      return value >= comparable;
              }
          },
          /**
           * @private
           * @param {HTMLElement} fieldEl the field we want to have a function
           *      that calculates its visibility
           * @returns {function} the function to be executed when we want to
           *      recalculate the visibility of fieldEl
           */
          _buildVisibilityFunction(fieldEl) {
              const visibilityCondition = fieldEl.dataset.visibilityCondition;
              const dependencyName = fieldEl.dataset.visibilityDependency;
              const comparator = fieldEl.dataset.visibilityComparator;
              const between = fieldEl.dataset.visibilityBetween;
              return () => {
                  // To be visible, at least one field with the dependency name must be visible.
                  const dependencyVisibilityFunction = this._visibilityFunctionByFieldName.get(dependencyName);
                  const dependencyIsVisible = !dependencyVisibilityFunction || dependencyVisibilityFunction();
                  if (!dependencyIsVisible) {
                      return false;
                  }
  
                  const formData = new FormData(this.el);
                  const currentValueOfDependency = ["contains", "!contains"].includes(comparator)
                      ? formData.getAll(dependencyName).join()
                      : formData.get(dependencyName);
                  return this._compareTo(comparator, currentValueOfDependency, visibilityCondition, between);
              };
          },
          /**
           * Calculates the visibility for each field with conditional visibility
           */
          _updateFieldsVisibility() {
              let anyFieldVisibilityUpdated = false;
              for (const [fieldEl, visibilityFunction] of this._visibilityFunctionByFieldEl.entries()) {
                  const wasVisible = !fieldEl.closest(".s_website_form_field")
                      .classList.contains("d-none");
                  const isVisible = !!visibilityFunction();
                  this._updateFieldVisibility(fieldEl, isVisible);
                  anyFieldVisibilityUpdated |= wasVisible !== isVisible;
              }
              // Recursive check needed in case of a field (C) that
              // conditionally displays a prefilled field (B), which in turn
              // triggers a conditional visibility on another field (A),
              // registered before B.
              if (anyFieldVisibilityUpdated) {
                  this._updateFieldsVisibility();
              }
          },
          /**
           * Changes the visibility of a field.
           *
           * @param {HTMLElement} fieldEl
           * @param {boolean} haveToBeVisible
           */
          _updateFieldVisibility(fieldEl, haveToBeVisible) {
              const fieldContainerEl = fieldEl.closest('.s_website_form_field');
              fieldContainerEl.classList.toggle('d-none', !haveToBeVisible);
              for (const inputEl of fieldContainerEl.querySelectorAll('.s_website_form_input')) {
                  // Hidden inputs should also be disabled so that their data are
                  // not sent on form submit.
                  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 = renderToElement("website.file_block", {fileName: fileDetails.name});
              fileBlockEl.fileDetails = fileDetails;
              filesZoneEl.append(fileBlockEl);
          },
          /**
           * 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
          //----------------------------------------------------------------------
  
          /**
           * Calculates the visibility of the fields at each input event on the
           * form (this method should be debounced in the start).
           */
          _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();
          },
      });
  