diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py
index e5e730ee5480a..4cb7d1a0f07f3 100644
--- a/addons/web/__manifest__.py
+++ b/addons/web/__manifest__.py
@@ -236,6 +236,7 @@
'web/static/src/legacy/js/public/public_root_instance.js',
'web/static/src/legacy/js/public/public_widget.js',
'web/static/src/legacy/js/public/signin.js',
+ 'web/static/src/legacy/js/public/interaction_util.js',
],
'web.assets_frontend_lazy': [
diff --git a/addons/web/static/src/legacy/js/public/interaction_util.js b/addons/web/static/src/legacy/js/public/interaction_util.js
new file mode 100644
index 0000000000000..d7c4d1e7be167
--- /dev/null
+++ b/addons/web/static/src/legacy/js/public/interaction_util.js
@@ -0,0 +1,40 @@
+
+export function buildEditableInteractions(builders) {
+ const result = [];
+
+ const mixinPerInteraction = new Map();
+ for (const makeEditable of builders) {
+ mixinPerInteraction.set(makeEditable.Interaction, makeEditable.mixin || ((C) => C));
+ }
+ for (const makeEditable of builders) {
+ if (makeEditable.isAbstract) {
+ continue;
+ }
+ let I = makeEditable.Interaction;
+ // Collect mixins to up to Interaction class in reverse order.
+ const mixins = [];
+ while (I.name !== "Interaction") {
+ const mixin = mixinPerInteraction.get(I);
+ if (mixin === null) {
+ console.log(`No mixin defined for: ${I.name}`);
+ } else {
+ mixins.push(mixin);
+ }
+ I = I.__proto__;
+ }
+ // Apply mixins from top-most class.
+ let EI = makeEditable.Interaction;
+ while (mixins.length) {
+ EI = mixins.pop()(EI);
+ }
+ if (!EI.name) {
+ // if we get here, this is most likely because we have an anonymous
+ // class. To make it easier to work with, we can add the name property
+ // by doing a little hack
+ const name = makeEditable.Interaction.name + "__mixin";
+ EI = {[name]: class extends EI {}} [name];
+ }
+ result.push(EI);
+ }
+ return result;
+}
diff --git a/addons/web/static/src/legacy/js/public/public_root.js b/addons/web/static/src/legacy/js/public/public_root.js
index 541015f717a4d..49c1b4dfd95f3 100644
--- a/addons/web/static/src/legacy/js/public/public_root.js
+++ b/addons/web/static/src/legacy/js/public/public_root.js
@@ -12,6 +12,7 @@ import { jsToPyLocale, pyToJsLocale } from "@web/core/l10n/utils";
import { App, Component, whenReady } from "@odoo/owl";
import { RPCError } from '@web/core/network/rpc';
import { registry } from "@web/core/registry";
+import { buildEditableInteractions } from "@web/legacy/js/public/interaction_util";
const { Settings } = luxon;
@@ -24,46 +25,6 @@ function getLang() {
const lang = cookie.get('frontend_lang') || getLang(); // FIXME the cookie value should maybe be in the ctx?
-export function buildEditableInteractions(builders) {
- const result = [];
-
- const mixinPerInteraction = new Map();
- for (const makeEditable of builders) {
- mixinPerInteraction.set(makeEditable.Interaction, makeEditable.mixin || ((C) => C));
- }
- for (const makeEditable of builders) {
- if (makeEditable.isAbstract) {
- continue;
- }
- let I = makeEditable.Interaction;
- // Collect mixins to up to Interaction class in reverse order.
- const mixins = [];
- while (I.name !== "Interaction") {
- const mixin = mixinPerInteraction.get(I);
- if (mixin === null) {
- console.log(`No mixin defined for: ${I.name}`);
- } else {
- mixins.push(mixin);
- }
- I = I.__proto__;
- }
- // Apply mixins from top-most class.
- let EI = makeEditable.Interaction;
- while (mixins.length) {
- EI = mixins.pop()(EI);
- }
- if (!EI.name) {
- // if we get here, this is most likely because we have an anonymous
- // class. To make it easier to work with, we can add the name property
- // by doing a little hack
- const name = makeEditable.Interaction.name + "__mixin";
- EI = {[name]: class extends EI {}} [name];
- }
- result.push(EI);
- }
- return result;
-}
-
/**
* Element which is designed to be unique and that will be the top-most element
* in the widget hierarchy. So, all other widgets will be indirectly linked to
diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py
index 2ef7e336c418e..e575b82329dda 100644
--- a/addons/website/__manifest__.py
+++ b/addons/website/__manifest__.py
@@ -290,6 +290,7 @@
'website/static/tests/redirect_field_tests.js',
],
'web.assets_unit_tests': [
+ 'web/static/src/legacy/js/public/interaction_util.js',
'website/static/tests/core/**/*',
'website/static/tests/interactions/**/*',
],
diff --git a/addons/website/static/src/interactions/animation.js b/addons/website/static/src/interactions/animation.js
index b846f25f645aa..d5f946435960c 100644
--- a/addons/website/static/src/interactions/animation.js
+++ b/addons/website/static/src/interactions/animation.js
@@ -192,6 +192,8 @@ export class Animation extends Interaction {
}
}
+registry.category("active_elements").add("website.animation", Animation);
+
registry
.category("website.editable_active_elements_builders")
.add("website.animation", {
diff --git a/addons/website/static/src/snippets/s_website_form/form.edit.js b/addons/website/static/src/snippets/s_website_form/form.edit.js
new file mode 100644
index 0000000000000..df97d5a24dc89
--- /dev/null
+++ b/addons/website/static/src/snippets/s_website_form/form.edit.js
@@ -0,0 +1,45 @@
+import { registry } from "@web/core/registry";
+import {
+ formatDate,
+ formatDateTime,
+} from "@web/core/l10n/dates";
+const { DateTime } = luxon;
+import { Form } from "./form";
+
+const FormEdit = I => class extends I {
+ setup() {
+ super.setup();
+ // TODO Translation behavior.
+ this.editTranslations = false;
+ // this.editTranslations = !!this.getContext(true).edit_translations;
+ }
+
+ prepareDateFields() {
+ // We do not initialize the datetime picker in edit mode but want the dates to be formated
+ 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"
+ ? formatDate
+ : formatDateTime;
+ el.value = format(DateTime.fromSeconds(parseInt(value)));
+ }
+ });
+ // Do not call super !
+ }
+
+ prefillValues() {
+ if (this.editTranslations) {
+ return;
+ }
+ super.prefillValues();
+ }
+};
+
+registry
+ .category("website.editable_active_elements_builders")
+ .add("website.form", {
+ Interaction: Form,
+ mixin: FormEdit,
+ });
diff --git a/addons/website/static/src/snippets/s_website_form/form.js b/addons/website/static/src/snippets/s_website_form/form.js
index 6a803fa13ba53..ed29e36f818b3 100644
--- a/addons/website/static/src/snippets/s_website_form/form.js
+++ b/addons/website/static/src/snippets/s_website_form/form.js
@@ -1,4 +1,4 @@
-import {ReCaptcha} from "@google_recaptcha/js/recaptcha";
+import { ReCaptcha } from "@google_recaptcha/js/recaptcha";
import { session } from "@web/session";
import { registry } from "@web/core/registry";
import { Interaction } from "@website/core/interaction";
@@ -23,28 +23,6 @@ const DEBOUNCE = 400;
const { DateTime } = luxon;
import wUtils from "@website/js/utils";
-// TODO Editor behavior.
-/*
-publicWidget.registry.EditModeWebsiteForm = publicWidget.Widget.extend({
- selector: ".s_website_form form, form.s_website_form", // !compatibility
- disabledInEditableMode: false,
- start: function () {
- if (this.editableMode) {
- // We do not initialize the datetime picker in edit mode but want the dates to be formated
- 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"
- ? formatDate
- : formatDateTime;
- el.value = format(DateTime.fromSeconds(parseInt(value)));
- }
- });
- }
- },
-});
-*/
export class Form extends Interaction {
static selector = ".s_website_form form, form.s_website_form"; // !compatibility
@@ -79,12 +57,9 @@ export class Form extends Interaction {
this.visibilityFunctionByFieldName = new Map();
this.visibilityFunctionByFieldEl = new Map();
this.disabledStates = new Map();
- this.inputEls = undefined;
- this.dateFieldEls = undefined;
+ this.inputEls = this.el.querySelectorAll(".s_website_form_field.s_website_form_field_hidden_if .s_website_form_input");
+ this.dateFieldEls = this.el.querySelectorAll(".s_website_form_datetime, .o_website_form_datetime, .s_website_form_date, .o_website_form_date");
this.disableDateTimePickers = [];
- // TODO Translation behavior.
- this.editTranslations = false;
- // this.editTranslations = !!this.getContext(true).edit_translations;
this.preFillValues = {};
}
async willStart() {
@@ -120,82 +95,9 @@ export class Form extends Interaction {
}
}
start() {
- this.dateFieldEls = this.el.querySelectorAll(".s_website_form_datetime, .o_website_form_datetime, .s_website_form_date, .o_website_form_date");
- if (!this.editableMode) {
- for (const fieldEl of this.dateFieldEls) {
- const inputEl = fieldEl.querySelector("input");
- const defaultValue = inputEl.getAttribute("value");
- this.disableDateTimePickers.push(this.services.datetime_picker.create({
- target: inputEl,
- onChange: () => inputEl.dispatchEvent(new Event("input", { bubbles: true })),
- pickerProps: {
- type: fieldEl.matches(".s_website_form_date, .o_website_form_date") ? "date" : "datetime",
- value: defaultValue && DateTime.fromSeconds(parseInt(defaultValue)),
- },
- }).enable());
- }
- for (const fieldEl of this.dateFieldEls) {
- fieldEl.classList.add("s_website_form_datepicker_initialized");
- }
- }
+ this.prepareDateFields();
+ this.prefillValues();
- // 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.
- let dataForValues = wUtils.getParsedDataFor(this.el.id, document);
- // TODO Translation behavior.
- // On the "edit_translations" mode, a with a translated term
- // will replace the attribute value, leading to some inconsistencies
- // (setting again the on the attributes after the editor's
- // cleanup, setting wrong values on the attributes after translating
- // default values...)
- if (!this.editTranslations
- && (dataForValues || Object.keys(this.preFillValues).length)) {
- dataForValues = dataForValues || {};
- const fieldNames = [...this.el.querySelectorAll("[name]")].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="${CSS.escape(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;
- }
- }
- }
// Visibility might need to be adapted according to pre-filled values.
this.updateContent();
@@ -207,7 +109,6 @@ export class Form extends Interaction {
});
}
// Check disabled states
- this.inputEls = this.el.querySelectorAll(".s_website_form_field.s_website_form_field_hidden_if .s_website_form_input");
for (const inputEl of this.inputEls) {
this.disabledStates[inputEl] = inputEl.disabled;
}
@@ -285,6 +186,82 @@ export class Form extends Interaction {
}
}
+ prepareDateFields() {
+ for (const fieldEl of this.dateFieldEls) {
+ const inputEl = fieldEl.querySelector("input");
+ const defaultValue = inputEl.getAttribute("value");
+ this.disableDateTimePickers.push(this.services.datetime_picker.create({
+ target: inputEl,
+ onChange: () => inputEl.dispatchEvent(new Event("input", { bubbles: true })),
+ pickerProps: {
+ type: fieldEl.matches(".s_website_form_date, .o_website_form_date") ? "date" : "datetime",
+ value: defaultValue && DateTime.fromSeconds(parseInt(defaultValue)),
+ },
+ }).enable());
+ }
+ for (const fieldEl of this.dateFieldEls) {
+ fieldEl.classList.add("s_website_form_datepicker_initialized");
+ }
+ }
+
+ prefillValues() {
+ // 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.
+ let dataForValues = wUtils.getParsedDataFor(this.el.id, document);
+ // On the "edit_translations" mode, a with a translated term
+ // will replace the attribute value, leading to some inconsistencies
+ // (setting again the on the attributes after the editor's
+ // cleanup, setting wrong values on the attributes after translating
+ // default values...)
+ if (dataForValues || Object.keys(this.preFillValues).length) {
+ dataForValues = dataForValues || {};
+ const fieldNames = [...this.el.querySelectorAll("[name]")].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="${CSS.escape(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;
+ }
+ }
+ }
+ }
+
async send(e) {
e.preventDefault(); // Prevent the default submit behavior
// Prevent users from crazy clicking
diff --git a/addons/website/static/tests/core/helpers.js b/addons/website/static/tests/core/helpers.js
index 33929429755ef..a413622d89de0 100644
--- a/addons/website/static/tests/core/helpers.js
+++ b/addons/website/static/tests/core/helpers.js
@@ -6,6 +6,7 @@ import {
} from "@web/../tests/web_test_helpers";
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { registry } from "@web/core/registry";
+import { buildEditableInteractions } from "@web/legacy/js/public/interaction_util";
let activeInteractions = null;
let elementRegistry = registry.category("website.active_elements");
@@ -29,7 +30,7 @@ export async function startInteraction(I, html, options) {
export async function startInteractions(
html,
- options = { waitForStart: true },
+ options = { waitForStart: true, editMode: false },
) {
defineMailModels();
const fixture = getFixture();
@@ -49,6 +50,17 @@ export async function startInteractions(
}
const env = await makeMockEnv();
const core = env.services.website_core;
+ if (options.editMode) {
+ core.stopInteractions();
+ const builders = registry.category("website.editable_active_elements_builders").getEntries();
+ for (const [key, builder] of builders) {
+ if (activeInteractions && !activeInteractions.includes(key)) {
+ builder.isAbstract = true;
+ }
+ }
+ const editableInteractions = buildEditableInteractions(builders.map((builder) => builder[1]));
+ core.activate(editableInteractions);
+ }
if (options.waitForStart) {
await core.isReady;
}
diff --git a/addons/website/static/tests/interactions/snippets/form.edit.test.js b/addons/website/static/tests/interactions/snippets/form.edit.test.js
new file mode 100644
index 0000000000000..33af96094bd6e
--- /dev/null
+++ b/addons/website/static/tests/interactions/snippets/form.edit.test.js
@@ -0,0 +1,52 @@
+import { expect, test } from "@odoo/hoot";
+import { animationFrame } from "@odoo/hoot-dom";
+import { advanceTime, Deferred } from "@odoo/hoot-mock";
+import { MockServer, onRpc, patchWithCleanup, webModels } from "@web/../tests/web_test_helpers";
+import {
+ startInteractions,
+ setupInteractionWhiteList,
+} from "../../core/helpers";
+
+setupInteractionWhiteList("website.form");
+
+test("form formats date in edit mode", async () => {
+ const { core, el } = await startInteractions(`
+
+ `, { editMode: true });
+ expect(core.interactions.length).toBe(1);
+ const formEl = el.querySelector("form");
+ const dateEl = el.querySelector("input[name=When]");
+ expect(dateEl.value).toBe("01/01/2025 10:00:00");
+ // Verify that non-edit code did not run.
+ const dateField = dateEl.closest(".s_website_form_datetime");
+ expect(dateField).not.toHaveClass("s_website_form_datepicker_initialized");
+});