Skip to content

Commit ae02ae5

Browse files
bso-odooged-odoo
authored andcommittedMar 27, 2025
form field options
1 parent 31bae45 commit ae02ae5

File tree

6 files changed

+1183
-6
lines changed

6 files changed

+1183
-6
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { BaseOptionComponent, useDomState } from "@html_builder/core/utils";
2+
import { onWillStart, onWillUpdateProps, useState } from "@odoo/owl";
3+
import { FormActionFieldsOption } from "./form_action_fields_option";
4+
import { getDependencyEl, getMultipleInputs, isFieldCustom } from "./utils";
5+
6+
export class FormFieldOption extends BaseOptionComponent {
7+
static template = "html_builder.website.s_website_form_field_option";
8+
static props = {
9+
loadFieldOptionData: Function,
10+
redrawSequence: { type: Number, optional: true },
11+
};
12+
static components = { FormActionFieldsOption };
13+
14+
setup() {
15+
super.setup();
16+
this.state = useState({
17+
availableFields: [],
18+
conditionInputs: [],
19+
conditionValueList: [],
20+
dependencyEl: null,
21+
});
22+
this.domState = useDomState((el) => ({ el }));
23+
onWillStart(async () => {
24+
const el = this.env.getEditingElement();
25+
const fieldOptionData = await this.props.loadFieldOptionData(el);
26+
this.state.availableFields.push(...fieldOptionData.availableFields);
27+
this.state.conditionInputs.push(...fieldOptionData.conditionInputs);
28+
this.state.conditionValueList.push(...fieldOptionData.conditionValueList);
29+
this.state.dependencyEl = getDependencyEl(el);
30+
});
31+
onWillUpdateProps(async (props) => {
32+
const el = this.env.getEditingElement();
33+
const fieldOptionData = await props.loadFieldOptionData(el);
34+
this.state.availableFields.length = 0;
35+
this.state.availableFields.push(...fieldOptionData.availableFields);
36+
this.state.conditionInputs.length = 0;
37+
this.state.conditionInputs.push(...fieldOptionData.conditionInputs);
38+
this.state.conditionValueList.length = 0;
39+
this.state.conditionValueList.push(...fieldOptionData.conditionValueList);
40+
this.state.dependencyEl = getDependencyEl(el);
41+
});
42+
// TODO select field's hack ?
43+
}
44+
get isTextConditionValueVisible() {
45+
const el = this.env.getEditingElement();
46+
const dependencyEl = getDependencyEl(el);
47+
if (
48+
!el.classList.contains("s_website_form_field_hidden_if") ||
49+
(dependencyEl &&
50+
(["checkbox", "radio"].includes(dependencyEl.type) ||
51+
dependencyEl.nodeName === "SELECT"))
52+
) {
53+
return false;
54+
}
55+
if (!dependencyEl) {
56+
return true;
57+
}
58+
if (dependencyEl?.classList.contains("datetimepicker-input")) {
59+
return false;
60+
}
61+
return (
62+
(["text", "email", "tel", "url", "search", "password", "number"].includes(
63+
dependencyEl.type
64+
) ||
65+
dependencyEl.nodeName === "TEXTAREA") &&
66+
!["set", "!set"].includes(el.dataset.visibilityComparator)
67+
);
68+
}
69+
get isTextConditionOperatorVisible() {
70+
const el = this.env.getEditingElement();
71+
const dependencyEl = getDependencyEl(el);
72+
if (
73+
!el.classList.contains("s_website_form_field_hidden_if") ||
74+
dependencyEl?.classList.contains("datetimepicker-input")
75+
) {
76+
return false;
77+
}
78+
return (
79+
!dependencyEl ||
80+
["text", "email", "tel", "url", "search", "password"].includes(dependencyEl.type) ||
81+
dependencyEl.nodeName === "TEXTAREA"
82+
);
83+
}
84+
get isExisingFieldSelectType() {
85+
const el = this.env.getEditingElement();
86+
return !isFieldCustom(el) && ["selection", "many2one"].includes(el.dataset.type);
87+
}
88+
get isMultipleInputs() {
89+
const el = this.env.getEditingElement();
90+
return !!getMultipleInputs(el);
91+
}
92+
get isMaxFilesVisible() {
93+
// Do not display the option if only one file is supposed to be
94+
// uploaded in the field.
95+
const el = this.env.getEditingElement();
96+
const fieldEl = el.closest(".s_website_form_field");
97+
return (
98+
fieldEl.classList.contains("s_website_form_custom") ||
99+
["one2many", "many2many"].includes(fieldEl.dataset.type)
100+
);
101+
}
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { BaseOptionComponent, useDomState } from "@html_builder/core/utils";
2+
import { FormFieldOption } from "./form_field_option";
3+
4+
export class FormFieldOptionRedraw extends BaseOptionComponent {
5+
static template = "html_builder.website.s_website_form_field_option_redraw";
6+
static props = FormFieldOption.props;
7+
static components = { FormFieldOption };
8+
9+
setup() {
10+
super.setup();
11+
this.count = 0;
12+
this.domState = useDomState((el) => {
13+
this.count++;
14+
return {
15+
redrawSequence: this.count++,
16+
};
17+
});
18+
}
19+
}

‎addons/html_builder/static/src/website_builder/plugins/form/form_option.inside.scss

+17-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@
77
}
88
}
99

10-
.s_website_form input:not(.o_translatable_attribute) {
11-
pointer-events: none;
12-
}
10+
.s_website_form {
11+
input, textarea, select, .s_website_form_label {
12+
&:not(.o_translatable_attribute) {
13+
pointer-events: none;
14+
}
15+
}
16+
// Hidden field is only partially hidden in editor
17+
.s_website_form_field_hidden {
18+
display: block !important;
19+
opacity: 0.5;
20+
}
21+
// Fields with conditional visibility are visible and identifiable in the editor
22+
.s_website_form_field_hidden_if {
23+
display: block !important;
24+
background-color: $o-we-fg-light;
25+
}
26+
}

‎addons/html_builder/static/src/website_builder/plugins/form/form_option.xml

+287
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,291 @@
8080
</button>
8181
</t>
8282

83+
<t t-name="html_builder.website.s_website_form_field_option_redraw">
84+
<FormFieldOption redrawSequence="domState.redrawSequence" t-props="props"/>
85+
</t>
86+
87+
<t t-name="html_builder.website.s_website_form_field_option">
88+
<BuilderRow label.translate="Type">
89+
<BuilderSelect t-if="!domState.el.classList.contains('s_website_form_model_required')"
90+
id="'type_opt'" preview="false"
91+
>
92+
<div class="we_bg_darker">Custom Field</div>
93+
<BuilderContext action="'customField'">
94+
<BuilderSelectItem actionValue="'char'">Text</BuilderSelectItem>
95+
<BuilderSelectItem actionValue="'text'">Long Text</BuilderSelectItem>
96+
<BuilderSelectItem actionValue="'email'">Email</BuilderSelectItem>
97+
<BuilderSelectItem actionValue="'tel'">Telephone</BuilderSelectItem>
98+
<BuilderSelectItem actionValue="'url'">Url</BuilderSelectItem>
99+
<BuilderSelectItem actionValue="'integer'">Number</BuilderSelectItem>
100+
<BuilderSelectItem actionValue="'float'">Decimal Number</BuilderSelectItem>
101+
<BuilderSelectItem actionValue="'boolean'">Checkbox</BuilderSelectItem>
102+
<BuilderSelectItem actionValue="'one2many'">Multiple Checkboxes</BuilderSelectItem>
103+
<BuilderSelectItem actionValue="'selection'">Radio Buttons</BuilderSelectItem>
104+
<BuilderSelectItem actionValue="'many2one'">Selection</BuilderSelectItem>
105+
<BuilderSelectItem actionValue="'date'">Date</BuilderSelectItem>
106+
<BuilderSelectItem actionValue="'datetime'">Date &amp; Time</BuilderSelectItem>
107+
<BuilderSelectItem actionValue="'binary'">File Upload</BuilderSelectItem>
108+
</BuilderContext>
109+
<t t-if="state.availableFields.length">
110+
<div class="we_bg_darker">Existing fields</div>
111+
<t t-foreach="state.availableFields" t-as="field" t-key="field.name">
112+
<BuilderSelectItem action="'existingField'" actionValue="field.name" t-out="field.string"/>
113+
</t>
114+
</t>
115+
</BuilderSelect>
116+
</BuilderRow>
117+
<BuilderRow label.translate="Input Type">
118+
<BuilderSelect t-if="!domState.el.classList.contains('s_website_form_custom') and ['char', 'email', 'tel', 'url'].includes(domState.el.dataset.type) and !domState.el.classList.contains('s_website_form_model_required')"
119+
id="'char_input_type_opt'" preview="false" action="'selectType'"
120+
>
121+
<BuilderSelectItem actionValue="'char'">Text</BuilderSelectItem>
122+
<BuilderSelectItem actionValue="'email'">Email</BuilderSelectItem>
123+
<BuilderSelectItem actionValue="'tel'">Telephone</BuilderSelectItem>
124+
<BuilderSelectItem actionValue="'url'">Url</BuilderSelectItem>
125+
</BuilderSelect>
126+
</BuilderRow>
127+
<BuilderRow label.translate="Selection type">
128+
<BuilderSelect t-if="isExisingFieldSelectType"
129+
id="'existing_field_select_type_opt'" preview="false" action="'existingFieldSelectType'"
130+
>
131+
<BuilderSelectItem actionValue="'many2one'">Dropdown List</BuilderSelectItem>
132+
<BuilderSelectItem actionValue="'selection'">Radio</BuilderSelectItem>
133+
</BuilderSelect>
134+
</BuilderRow>
135+
<BuilderRow label.translate="Display" level="1">
136+
<BuilderSelect t-if="isMultipleInputs"
137+
id="'multi_check_display_opt'" preview="false"
138+
>
139+
<BuilderSelectItem action="'multiCheckboxDisplay'" actionValue="'horizontal'">Horizontal</BuilderSelectItem>
140+
<BuilderSelectItem action="'multiCheckboxDisplay'" actionValue="'vertical'">Vertical</BuilderSelectItem>
141+
</BuilderSelect>
142+
</BuilderRow>
143+
<BuilderRow label.translate="Height" level="1" applyTo="'textarea'">
144+
<BuilderNumberInput unit.translate="rows" saveUnit="''" step="1" attributeAction="'rows'" default="3"/>
145+
</BuilderRow>
146+
<BuilderRow label.translate="Label">
147+
<BuilderTextInput action="'setLabelText'"/>
148+
</BuilderRow>
149+
<BuilderRow label.translate="Position" level="1">
150+
<BuilderButtonGroup action="'selectLabelPosition'">
151+
<BuilderButton title.translate="Hide" actionValue="'none'">
152+
<i class="fa fa-eye-slash"/>
153+
</BuilderButton>
154+
<BuilderButton title.translate="Top" actionValue="'top'">
155+
<img src="/website/static/src/img/snippets_options/pos_top.svg"/>
156+
</BuilderButton>
157+
<BuilderButton title.translate="Left" actionValue="'left'">
158+
<img src="/website/static/src/img/snippets_options/pos_left.svg"/>
159+
</BuilderButton>
160+
<BuilderButton title.translate="Right" actionValue="'right'">
161+
<img src="/website/static/src/img/snippets_options/pos_right.svg"/>
162+
</BuilderButton>
163+
</BuilderButtonGroup>
164+
</BuilderRow>
165+
<BuilderRow label.translate="Description">
166+
<BuilderCheckbox action="'toggleDescription'" preview="false"/>
167+
</BuilderRow>
168+
<BuilderRow label.translate="Placeholder">
169+
<BuilderTextInput attributeAction="'placeholder'"
170+
applyTo="`input[type='text'], input[type='email'], input[type='number'], input[type='tel'], input[type='url'], textarea`"
171+
/>
172+
</BuilderRow>
173+
<BuilderRow label.translate="Default Value">
174+
<BuilderTextInput action="'selectTextareaValue'" applyTo="'textarea'"/>
175+
<BuilderCheckbox attributeAction="'checked'" attributeActionValue="'checked'"
176+
applyTo="`.col-sm > * > input[type='checkbox']`" preview="false"
177+
/>
178+
<!-- TODO used to set both attribute & property -->
179+
<BuilderTextInput attributeAction="'value'"
180+
applyTo="`input[type='text']:not(.datetimepicker-input), input[type='email'], input[type='tel'], input[type='url']`"
181+
/>
182+
<!-- TODO used to set both attribute & property -->
183+
<BuilderNumberInput attributeAction="'value'" step="1"
184+
applyTo="`input[type='number']`"
185+
/>
186+
<!-- TODO used to set both attribute & "valueProperty" -->
187+
<BuilderDateTimePicker type="'datetime'" attributeAction="'value'"
188+
applyTo="'.s_website_form_datetime input'"
189+
/>
190+
<!-- TODO used to set both attribute & "valueProperty" -->
191+
<BuilderDateTimePicker type="'date'" attributeAction="'value'"
192+
applyTo="'.s_website_form_date input'"
193+
/>
194+
</BuilderRow>
195+
<BuilderRow label.translate="Required">
196+
<BuilderCheckbox t-if="!domState.el.classList.contains('s_website_form_model_required')"
197+
id="'required_opt'" preview="false"
198+
action="'toggleRequired'" actionParam="'s_website_form_required'"
199+
/>
200+
</BuilderRow>
201+
<BuilderRow label.translate="Max # of Files">
202+
<BuilderNumberInput id="'max_files_number_opt'" t-if="isMaxFilesVisible"
203+
title.translate="The maximum number of files that can be uploaded."
204+
dataAttributeAction="'maxFilesNumber'"
205+
default="1"
206+
applyTo="`input[type='file']`"
207+
step="1"
208+
/>
209+
</BuilderRow>
210+
<BuilderRow label.translate="Max File Size">
211+
<BuilderNumberInput
212+
title.translate="The maximum size (in MB) an uploaded file can have."
213+
dataAttributeAction="'maxFileSize'"
214+
applyTo="`input[type='file']`"
215+
default="1"
216+
unit="'MB'"
217+
/>
218+
</BuilderRow>
219+
<BuilderRow label.translate="TODO value list">
220+
<div>TODO</div>
221+
</BuilderRow>
222+
<!-- TODO Implement a BuilderList
223+
<WeList t-if="renderContext.valueList"
224+
title="renderContext.valueList.title"
225+
addItemTitle="renderContext.valueList.addItemTitle"
226+
renderListItems="''"
227+
hasDefault="renderContext.valueList.hasDefault"
228+
defaults="renderContext.valueList.defaults"
229+
availableRecords="renderContext.valueList.availableRecords"
230+
newRecordId="renderContext.valueList.newRecordId"
231+
/>
232+
-->
233+
<BuilderRow label.translate="Visibility">
234+
<BuilderSelect preview="false" action="'setVisibility'">
235+
<BuilderSelectItem actionValue="'visible'" classAction="''">Always Visible</BuilderSelectItem>
236+
<BuilderSelectItem actionValue="'hidden'" classAction="'s_website_form_field_hidden'">Hidden</BuilderSelectItem>
237+
<BuilderSelectItem id="'hidden_if_opt'" actionValue="'conditional'" classAction="'s_website_form_field_hidden_if d-none'">Visible only if</BuilderSelectItem>
238+
</BuilderSelect>
239+
</BuilderRow>
240+
<t t-if="isActiveItem('hidden_if_opt')">
241+
<div class="d-flex position-relative p-1 px-2 ps-3 hb-row">
242+
<BuilderSelect id="'hidden_condition_opt'" preview="false">
243+
<!-- Load every existing form input -->
244+
<BuilderSelectItem t-foreach="state.conditionInputs" t-as="input" t-key="input.name"
245+
action="'setVisibilityDependency'" actionValue="input.name"
246+
t-out="input.textContent"
247+
/>
248+
</BuilderSelect>
249+
<BuilderSelect t-if="state.dependencyEl and (state.dependencyEl.type === 'checkbox' || state.dependencyEl.type === 'radio' || state.dependencyEl.nodeName === 'SELECT')"
250+
id="'hidden_condition_no_text_opt'" preview="false" dataAttributeAction="'visibilityComparator'"
251+
>
252+
<BuilderSelectItem dataAttributeActionValue="'selected'">Is equal to</BuilderSelectItem>
253+
<BuilderSelectItem dataAttributeActionValue="'!selected'">Is not equal to</BuilderSelectItem>
254+
<BuilderSelectItem dataAttributeActionValue="'contains'">Contains</BuilderSelectItem>
255+
<BuilderSelectItem dataAttributeActionValue="'!contains'">Doesn't contain</BuilderSelectItem>
256+
</BuilderSelect>
257+
<BuilderSelect t-if="isTextConditionOperatorVisible"
258+
id="'hidden_condition_text_opt'" preview="false" dataAttributeAction="'visibilityComparator'"
259+
>
260+
<!-- string comparator possibilities -->
261+
<BuilderSelectItem dataAttributeActionValue="'contains'">Contains</BuilderSelectItem>
262+
<BuilderSelectItem dataAttributeActionValue="'!contains'">Doesn't contain</BuilderSelectItem>
263+
<BuilderSelectItem dataAttributeActionValue="'equal'">Is equal to</BuilderSelectItem>
264+
<BuilderSelectItem dataAttributeActionValue="'!equal'">Is not equal to</BuilderSelectItem>
265+
<BuilderSelectItem dataAttributeActionValue="'set'">Is set</BuilderSelectItem>
266+
<BuilderSelectItem dataAttributeActionValue="'!set'">Is not set</BuilderSelectItem>
267+
</BuilderSelect>
268+
<BuilderSelect t-if="state.dependencyEl and state.dependencyEl.type === 'number'"
269+
id="'hidden_condition_num_opt'" preview="false" dataAttributeAction="'visibilityComparator'"
270+
>
271+
<!-- number comparator possibilities -->
272+
<BuilderSelectItem dataAttributeActionValue="'equal'">Is equal to</BuilderSelectItem>
273+
<BuilderSelectItem dataAttributeActionValue="'!equal'">Is not equal to</BuilderSelectItem>
274+
<BuilderSelectItem dataAttributeActionValue="'greater'">Is greater than</BuilderSelectItem>
275+
<BuilderSelectItem dataAttributeActionValue="'less'">Is less than</BuilderSelectItem>
276+
<BuilderSelectItem dataAttributeActionValue="'greater or equal'">Is greater than or equal to</BuilderSelectItem>
277+
<BuilderSelectItem dataAttributeActionValue="'less or equal'">Is less than or equal to</BuilderSelectItem>
278+
<BuilderSelectItem dataAttributeActionValue="'set'">Is set</BuilderSelectItem>
279+
<BuilderSelectItem dataAttributeActionValue="'!set'">Is not set</BuilderSelectItem>
280+
</BuilderSelect>
281+
<BuilderSelect t-if="state.dependencyEl?.classList.contains('datetimepicker-input')"
282+
id="'hidden_condition_time_comparators_opt'" preview="false"
283+
dataAttributeAction="'visibilityComparator'"
284+
>
285+
<!-- date & datetime comparator possibilities -->
286+
<BuilderSelectItem dataAttributeActionValue="'dateEqual'">Is equal to</BuilderSelectItem>
287+
<BuilderSelectItem dataAttributeActionValue="'date!equal'">Is not equal to</BuilderSelectItem>
288+
<BuilderSelectItem dataAttributeActionValue="'after'">Is after</BuilderSelectItem>
289+
<BuilderSelectItem dataAttributeActionValue="'before'">Is before</BuilderSelectItem>
290+
<BuilderSelectItem dataAttributeActionValue="'equal or after'">Is after or equal to</BuilderSelectItem>
291+
<BuilderSelectItem dataAttributeActionValue="'equal or before'">Is before or equal to</BuilderSelectItem>
292+
<BuilderSelectItem dataAttributeActionValue="'set'">Is set</BuilderSelectItem>
293+
<BuilderSelectItem dataAttributeActionValue="'!set'">Is not set</BuilderSelectItem>
294+
<BuilderSelectItem dataAttributeActionValue="'between'">Is between (included)</BuilderSelectItem>
295+
<BuilderSelectItem dataAttributeActionValue="'!between'">Is not between (excluded)</BuilderSelectItem>
296+
</BuilderSelect>
297+
<BuilderSelect t-if="state.dependencyEl?.type === 'file'"
298+
id="'hidden_condition_file_opt'" preview="false" dataAttributeAction="'visibilityComparator'"
299+
>
300+
<!-- file comparator possibilities -->
301+
<BuilderSelectItem dataAttributeActionValue="'fileset'">Is set</BuilderSelectItem>
302+
<BuilderSelectItem dataAttributeActionValue="'!fileset'">Is not set</BuilderSelectItem>
303+
</BuilderSelect>
304+
<BuilderSelect t-if="state.dependencyEl?.closest('.s_website_form_field')?.dataset.type === 'record'"
305+
id="'hidden_condition_record_opt'" dataAttributeAction="'visibilityComparator'" preview="false"
306+
>
307+
<BuilderSelectItem dataAttributeActionValue="'selected'">Is equal to</BuilderSelectItem>
308+
<BuilderSelectItem dataAttributeActionValue="'!selected'">Is not equal to</BuilderSelectItem>
309+
</BuilderSelect>
310+
</div>
311+
<div class="d-flex position-relative p-1 px-2 ps-3 hb-row">
312+
<BuilderSelect t-if="state.conditionValueList and (state.dependencyEl and (state.dependencyEl.type === 'checkbox' || state.dependencyEl.type === 'radio' || state.dependencyEl.nodeName === 'SELECT'))"
313+
id="'hidden_condition_no_text_opt'" preview="false" dataAttributeAction="'visibilityCondition'"
314+
>
315+
<!-- checkbox, select, radio possible values -->
316+
<BuilderSelectItem t-foreach="state.conditionValueList" t-as="record" t-key="record.value"
317+
dataAttributeActionValue="record.value"
318+
t-out="record.textContent"
319+
/>
320+
</BuilderSelect>
321+
<BuilderSelect t-if="state.conditionValueList and state.dependencyEl?.closest('.s_website_form_field')?.dataset.type === 'record'"
322+
id="'hidden_condition_record_opt'" preview="false" dataAttributeAction="'visibilityCondition'"
323+
>
324+
<!-- checkbox, select, radio possible values -->
325+
<BuilderSelectItem t-foreach="state.conditionValueList" t-as="record" t-key="record.value"
326+
dataAttributeActionValue="record.value"
327+
t-out="record.textContent"
328+
/>
329+
</BuilderSelect>
330+
<BuilderTextInput t-if="isTextConditionValueVisible"
331+
id="'hidden_condition_additional_text'" dataAttributeAction="'visibilityCondition'"
332+
/>
333+
<BuilderDateTimePicker t-if="state.dependencyEl?.closest('.s_website_form_datetime') and !['set', '!set'].includes(domState.el.dataset.visibilityComparator)"
334+
id="'hidden_condition_additional_datetime'" dataAttributeAction="'visibilityCondition'" type="'datetime'"
335+
/>
336+
<BuilderDateTimePicker t-if="state.dependencyEl?.closest('.s_website_form_date') and !['set', '!set'].includes(domState.el.dataset.visibilityComparator)"
337+
id="'hidden_condition_additional_date'" dataAttributeAction="'visibilityCondition'" type="'date'"
338+
/>
339+
<BuilderDateTimePicker t-if="state.dependencyEl?.closest('.s_website_form_datetime') and ['between', '!between'].includes(domState.el.dataset.visibilityComparator)"
340+
id="'hidden_condition_datetime_between'" dataAttributeAction="'visibilityBetween'" type="'datetime'"
341+
/>
342+
<BuilderDateTimePicker t-if="state.dependencyEl?.closest('.s_website_form_date') and ['between', '!between'].includes(domState.el.getAttribute('data-visibility-comparator'))"
343+
id="'hidden_condition_date_between'" dataAttributeAction="'visibilityBetween'" type="'date'"
344+
/>
345+
</div>
346+
</t>
347+
</t>
348+
349+
<t t-name="html_builder.website.s_website_form_submit_option">
350+
<BuilderRow label.translate="Button Position">
351+
<BuilderSelect>
352+
<BuilderSelectItem classAction="'text-start s_website_form_no_submit_label'">Left</BuilderSelectItem>
353+
<BuilderSelectItem classAction="'text-center s_website_form_no_submit_label'">Center</BuilderSelectItem>
354+
<BuilderSelectItem classAction="'text-end s_website_form_no_submit_label'">Right</BuilderSelectItem>
355+
<BuilderSelectItem classAction="''">Input Aligned</BuilderSelectItem>
356+
</BuilderSelect>
357+
</BuilderRow>
358+
</t>
359+
360+
<!-- TODO -->
361+
<!-- Remove the duplicate option of model fields -->
362+
<!--
363+
<div data-js="WebsiteFormFieldModel" data-selector=".s_website_form .s_website_form_field:not(.s_website_form_custom)"/>
364+
-->
365+
<!-- Remove the delete and duplicate option of the submit button -->
366+
<!--
367+
<div data-js="WebsiteFormSubmitRequired" data-selector=".s_website_form .s_website_form_submit"/>
368+
-->
369+
83370
</templates>

‎addons/html_builder/static/src/website_builder/plugins/form/form_option_plugin.js

+445
Large diffs are not rendered by default.

‎addons/html_builder/static/src/website_builder/plugins/form/utils.js

+313-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import { _t } from "@web/core/l10n/translation";
22
import { renderToElement } from "@web/core/utils/render";
33
import { generateHTMLId } from "@html_builder/utils/utils_css";
44

5+
export const VISIBILITY_DATASET = [
6+
"visibilityDependency",
7+
"visibilityCondition",
8+
"visibilityComparator",
9+
"visibilityBetween",
10+
];
11+
512
/**
613
* Returns the parsed data coming from the data-for element for the given form.
714
* TODO Note that we should rely on the same util as the website form interaction.
@@ -133,9 +140,8 @@ export function renderField(field, resetId = false) {
133140
const renderType = field.type === "tags" ? "many2many" : field.type;
134141
template.content.append(renderToElement("website.form_field_" + renderType, params));
135142
if (field.description && field.description !== true) {
136-
$(template.content.querySelector(".s_website_form_field_description")).replaceWith(
137-
field.description
138-
);
143+
const descriptionEl = template.content.querySelector(".s_website_form_field_description");
144+
descriptionEl.replaceWith(field.description);
139145
}
140146
template.content
141147
.querySelectorAll("input.datetimepicker-input")
@@ -208,3 +214,307 @@ export function getFieldFormat(fieldEl) {
208214
};
209215
return format;
210216
}
217+
218+
/**
219+
* Returns true if the field is a custom field, false if it is an existing field
220+
*
221+
* @param {HTMLElement} fieldEl
222+
* @returns {boolean}
223+
*/
224+
export function isFieldCustom(fieldEl) {
225+
return !!fieldEl.classList.contains("s_website_form_custom");
226+
}
227+
228+
/**
229+
* Returns the name of the field
230+
*
231+
* @param {HTMLElement} fieldEl
232+
* @returns {string}
233+
*/
234+
export function getFieldName(fieldEl = this.$target[0]) {
235+
const multipleName = fieldEl.querySelector(".s_website_form_multiple");
236+
return multipleName
237+
? multipleName.dataset.name
238+
: fieldEl.querySelector(".s_website_form_input").name;
239+
}
240+
/**
241+
* Returns the type of the field, can be used for both custom and existing fields
242+
*
243+
* @param {HTMLElement} fieldEl
244+
* @returns {string}
245+
*/
246+
export function getFieldType(fieldEl) {
247+
return fieldEl.dataset.type;
248+
}
249+
250+
/**
251+
* Set the active field properties on the field Object
252+
*
253+
* @param {HTMLElement} fieldEl
254+
* @param {Object} field Field to complete with the active field info
255+
*/
256+
export function setActiveProperties(fieldEl, field) {
257+
const classList = fieldEl.classList;
258+
const textarea = fieldEl.querySelector("textarea");
259+
const input = fieldEl.querySelector(
260+
'input[type="text"], input[type="email"], input[type="number"], input[type="tel"], input[type="url"], textarea'
261+
);
262+
const fileInputEl = fieldEl.querySelector("input[type=file]");
263+
const description = fieldEl.querySelector(".s_website_form_field_description");
264+
field.placeholder = input && input.placeholder;
265+
if (input) {
266+
// textarea value has no attribute, date/datetime timestamp property is formated
267+
field.value = input.getAttribute("value") || input.value;
268+
} else if (field.type === "boolean") {
269+
field.value = !!fieldEl.querySelector('input[type="checkbox"][checked]');
270+
} else if (fileInputEl) {
271+
field.maxFilesNumber = fileInputEl.dataset.maxFilesNumber;
272+
field.maxFileSize = fileInputEl.dataset.maxFileSize;
273+
}
274+
// property value is needed for date/datetime (formated date).
275+
field.propertyValue = input && input.value;
276+
field.description = description && description.outerHTML;
277+
field.rows = textarea && textarea.rows;
278+
field.required = classList.contains("s_website_form_required");
279+
field.modelRequired = classList.contains("s_website_form_model_required");
280+
field.hidden = classList.contains("s_website_form_field_hidden");
281+
field.formatInfo = getFieldFormat(fieldEl);
282+
}
283+
284+
/**
285+
* Replaces the target with provided field.
286+
*
287+
* @param {HTMLElement} oldFieldEl
288+
* @param {HTMLElement} fieldEl
289+
*/
290+
export function replaceFieldElement(oldFieldEl, fieldEl) {
291+
const inputEl = oldFieldEl.querySelector("input");
292+
const dataFillWith = inputEl ? inputEl.dataset.fillWith : undefined;
293+
const hasConditionalVisibility = oldFieldEl.classList.contains(
294+
"s_website_form_field_hidden_if"
295+
);
296+
const previousInputEl = oldFieldEl.querySelector(".s_website_form_input");
297+
const previousName = previousInputEl.name;
298+
const previousType = previousInputEl.type;
299+
[...oldFieldEl.childNodes].forEach((node) => node.remove());
300+
[...fieldEl.childNodes].forEach((node) => oldFieldEl.appendChild(node));
301+
[...fieldEl.attributes].forEach((el) => oldFieldEl.removeAttribute(el.nodeName));
302+
[...fieldEl.attributes].forEach((el) => oldFieldEl.setAttribute(el.nodeName, el.nodeValue));
303+
if (hasConditionalVisibility) {
304+
oldFieldEl.classList.add("s_website_form_field_hidden_if", "d-none");
305+
}
306+
const dependentFieldEls = oldFieldEl
307+
.closest("form")
308+
.querySelectorAll(
309+
`.s_website_form_field[data-visibility-dependency="${CSS.escape(previousName)}"]`
310+
);
311+
const newFormInputEl = oldFieldEl.querySelector(".s_website_form_input");
312+
const newName = newFormInputEl.name;
313+
const newType = newFormInputEl.type;
314+
if ((previousName !== newName || previousType !== newType) && dependentFieldEls) {
315+
// In order to keep the visibility conditions consistent,
316+
// when the name has changed, it means that the type has changed so
317+
// all fields whose visibility depends on this field must be updated so that
318+
// they no longer have conditional visibility
319+
for (const fieldEl of dependentFieldEls) {
320+
deleteConditionalVisibility(fieldEl);
321+
}
322+
}
323+
const newInputEl = oldFieldEl.querySelector("input");
324+
if (newInputEl) {
325+
newInputEl.dataset.fillWith = dataFillWith;
326+
}
327+
}
328+
329+
/**
330+
* Returns the target as a field Object
331+
*
332+
* @param {HTMLElement} fieldEl
333+
* @param {boolean} noRecords
334+
* @returns {Object}
335+
*/
336+
export function getActiveField(fieldEl, { noRecords, fields } = {}) {
337+
let field;
338+
const labelText = fieldEl.querySelector(".s_website_form_label_content")?.innerText || "";
339+
if (isFieldCustom(fieldEl)) {
340+
field = getCustomField(fieldEl.dataset.type, labelText);
341+
} else {
342+
field = Object.assign({}, fields[getFieldName(fieldEl)]);
343+
field.string = labelText;
344+
field.type = getFieldType(fieldEl);
345+
}
346+
if (!noRecords) {
347+
field.records = getListItems(fieldEl);
348+
}
349+
setActiveProperties(fieldEl, field);
350+
return field;
351+
}
352+
353+
/**
354+
* Deletes all attributes related to conditional visibility.
355+
*
356+
* @param {HTMLElement} fieldEl
357+
*/
358+
export function deleteConditionalVisibility(fieldEl) {
359+
for (const name of VISIBILITY_DATASET) {
360+
delete fieldEl.dataset[name];
361+
}
362+
fieldEl.classList.remove("s_website_form_field_hidden_if", "d-none");
363+
}
364+
365+
/**
366+
* Returns the select element if it exist else null
367+
*
368+
* @param {HTMLElement} fieldEl
369+
* @returns {HTMLElement}
370+
*/
371+
export function getSelect(fieldEl) {
372+
return fieldEl.querySelector("select");
373+
}
374+
375+
/**
376+
* Returns the next new record id.
377+
*
378+
* @param {HTMLElement} fieldEl
379+
*/
380+
export function getNewRecordId(fieldEl) {
381+
const selectEl = getSelect(fieldEl);
382+
const multipleInputsEl = getMultipleInputs(fieldEl);
383+
let options = [];
384+
if (selectEl) {
385+
options = [...selectEl.querySelectorAll("option")];
386+
} else if (multipleInputsEl) {
387+
options = [...multipleInputsEl.querySelectorAll(".checkbox input, .radio input")];
388+
}
389+
// TODO: @owl-option factorize code above
390+
const targetEl = fieldEl.querySelector(".s_website_form_input");
391+
let id;
392+
if (["checkbox", "radio"].includes(targetEl.getAttribute("type"))) {
393+
// Remove first checkbox/radio's id's final '0'.
394+
id = targetEl.id.slice(0, -1);
395+
} else {
396+
id = targetEl.id;
397+
}
398+
return id + options.length;
399+
}
400+
401+
/**
402+
* @param {HTMLElement} fieldEl
403+
* @returns {HTMLElement} The visibility dependency of the field
404+
*/
405+
export function getDependencyEl(fieldEl) {
406+
const dependencyName = fieldEl.dataset.visibilityDependency;
407+
return fieldEl
408+
.closest("form")
409+
.querySelector(`.s_website_form_input[name="${CSS.escape(dependencyName)}"]`);
410+
}
411+
412+
/**
413+
* @param {HTMLElement} dependentFieldEl
414+
* @param {HTMLElement} targetFieldEl
415+
* @returns {boolean} "true" if adding "dependentFieldEl" or any other field
416+
* with the same label in the conditional visibility of "targetFieldEl"
417+
* would create a circular dependency involving "targetFieldEl".
418+
*/
419+
export function findCircular(dependentFieldEl, targetFieldEl) {
420+
const formEl = targetFieldEl.closest("form");
421+
// Keep a register of the already visited fields to not enter an
422+
// infinite check loop.
423+
const visitedFields = new Set();
424+
const recursiveFindCircular = (dependentFieldEl, targetFieldEl) => {
425+
const dependentFieldName = getFieldName(dependentFieldEl);
426+
// Get all the fields that have the same label as the dependent
427+
// field.
428+
let dependentFieldEls = Array.from(
429+
formEl.querySelectorAll(
430+
`.s_website_form_input[name="${CSS.escape(dependentFieldName)}"]`
431+
)
432+
).map((el) => el.closest(".s_website_form_field"));
433+
// Remove the duplicated fields. This could happen if the field has
434+
// multiple inputs ("Multiple Checkboxes" for example.)
435+
dependentFieldEls = new Set(dependentFieldEls);
436+
const fieldName = getFieldName(targetFieldEl);
437+
for (const dependentFieldEl of dependentFieldEls) {
438+
// Only check for circular dependencies on fields that do not
439+
// already have been checked.
440+
if (!visitedFields.has(dependentFieldEl)) {
441+
// Add the dependentFieldEl in the set of checked field.
442+
visitedFields.add(dependentFieldEl);
443+
if (dependentFieldEl.dataset.visibilityDependency === fieldName) {
444+
return true;
445+
}
446+
const dependencyInputEl = getDependencyEl(dependentFieldEl);
447+
if (
448+
dependencyInputEl &&
449+
recursiveFindCircular(
450+
dependencyInputEl.closest(".s_website_form_field"),
451+
targetFieldEl
452+
)
453+
) {
454+
return true;
455+
}
456+
}
457+
}
458+
return false;
459+
};
460+
return recursiveFindCircular(dependentFieldEl, targetFieldEl);
461+
}
462+
463+
/**
464+
* Returns the domain of a field.
465+
*
466+
* @param {HTMLElement} formEl
467+
* @param {String} name
468+
* @param {String} type
469+
* @param {String} relation
470+
* @returns {Object|false}
471+
*/
472+
// TODO Solve this variable differently
473+
const allFormsInfo = new Map();
474+
export function getDomain(formEl, name, type, relation) {
475+
// We need this because the field domain is in formInfo in the
476+
// WebsiteFormEditor but we need it in the WebsiteFieldEditor.
477+
if (!allFormsInfo.get(formEl) || !name || !type || !relation) {
478+
return false;
479+
}
480+
const field = allFormsInfo
481+
.get(formEl)
482+
.fields.find((el) => el.name === name && el.type === type && el.relation === relation);
483+
return field && field.domain;
484+
}
485+
486+
export function getListItems(fieldEl) {
487+
const selectEl = getSelect(fieldEl);
488+
const multipleInputsEl = getMultipleInputs(fieldEl);
489+
let options = [];
490+
if (selectEl) {
491+
options = [...selectEl.querySelectorAll("option")];
492+
} else if (multipleInputsEl) {
493+
options = [...multipleInputsEl.querySelectorAll(".checkbox input, .radio input")];
494+
}
495+
const isFieldElCustom = isFieldCustom(fieldEl);
496+
return options.map((opt) => {
497+
const name = selectEl ? opt : opt.nextElementSibling;
498+
return {
499+
id: isFieldElCustom
500+
? opt.id
501+
: /^-?[0-9]{1,15}$/.test(opt.value)
502+
? parseInt(opt.value)
503+
: opt.value,
504+
display_name: name.textContent.trim(),
505+
selected: selectEl ? opt.selected : opt.checked,
506+
};
507+
});
508+
}
509+
510+
/**
511+
* Sets the visibility dependency of the field.
512+
*
513+
* @param {HTMLElement} fieldEl
514+
* @param {string} value name of the dependency input
515+
*/
516+
export function setVisibilityDependency(fieldEl, value) {
517+
delete fieldEl.dataset.visibilityCondition;
518+
delete fieldEl.dataset.visibilityComparator;
519+
fieldEl.dataset.visibilityDependency = value;
520+
}

0 commit comments

Comments
 (0)
Please sign in to comment.