Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions addons/web/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': [
Expand Down
40 changes: 40 additions & 0 deletions addons/web/static/src/legacy/js/public/interaction_util.js
Original file line number Diff line number Diff line change
@@ -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;
}
41 changes: 1 addition & 40 deletions addons/web/static/src/legacy/js/public/public_root.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions addons/website/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*',
],
Expand Down
2 changes: 2 additions & 0 deletions addons/website/static/src/interactions/animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down
45 changes: 45 additions & 0 deletions addons/website/static/src/snippets/s_website_form/form.edit.js
Original file line number Diff line number Diff line change
@@ -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,
});
185 changes: 81 additions & 104 deletions addons/website/static/src/snippets/s_website_form/form.js
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 <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
&& (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();

Expand All @@ -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;
}
Expand Down Expand Up @@ -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 <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 (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
Expand Down
Loading