[IMP] website: add ability to conditionally show form fields
Allow the user to create forms whose fields can have a conditional visibility, meaning that the field is only visible if another field in the form meets certain conditions. The previous hidden checkbox is replaced by a select with 3 options (visible, hidden, visible only if). task-2431285 closes odoo/odoo#77255 X-original-commit: 2dcbfecf3c10687a2cd3af36335d5be70c904fce Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com> Co-authored-by: Arthur Detroux (ard) <ard@odoo.com> Co-authored-by: qsm-odoo <qsm@odoo.com>
This commit is contained in:
committed by
qsm-odoo
co-authored by
Arthur Detroux
qsm-odoo
parent
48092fcf85
commit
e91beddac0
@@ -48,6 +48,8 @@ odoo.define('website.s_website_form', function (require) {
|
||||
init: function () {
|
||||
this._super(...arguments);
|
||||
this._recaptcha = new ReCaptcha();
|
||||
this._visibilityFunctionByFieldName = new Map();
|
||||
this._visibilityFunctionByFieldEl = new Map();
|
||||
this.__started = new Promise(resolve => this.__startResolve = resolve);
|
||||
},
|
||||
willStart: async function () {
|
||||
@@ -71,9 +73,28 @@ odoo.define('website.s_website_form', function (require) {
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
// Prepare visibility data and update field visibilities
|
||||
const visibilityFunctionsByFieldName = new Map();
|
||||
for (const fieldEl of this.$target[0].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._updateFieldsVisibility();
|
||||
|
||||
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 }),
|
||||
minDate: moment({y: 1000}),
|
||||
maxDate: moment({y: 9999, M: 11, d: 31}),
|
||||
calendarWeeks: true,
|
||||
icons: {
|
||||
@@ -119,6 +140,13 @@ odoo.define('website.s_website_form', function (require) {
|
||||
});
|
||||
}
|
||||
|
||||
// Check disabled states
|
||||
this.inputEls = this.$target[0].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;
|
||||
}
|
||||
|
||||
return this._super(...arguments).then(() => this.__startResolve());
|
||||
},
|
||||
|
||||
@@ -156,6 +184,16 @@ odoo.define('website.s_website_form', function (require) {
|
||||
|
||||
// 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.$target[0].querySelectorAll('.s_website_form_field_hidden_if:not(.d-none)').forEach(el => el.classList.add('d-none'));
|
||||
|
||||
this.$el.off('.s_website_form');
|
||||
},
|
||||
|
||||
send: async function (e) {
|
||||
@@ -395,6 +433,11 @@ odoo.define('website.s_website_form', function (require) {
|
||||
message: message,
|
||||
})));
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets the user's field needed to be fetched to pre-fill the form.
|
||||
*
|
||||
@@ -403,5 +446,132 @@ odoo.define('website.s_website_form', function (require) {
|
||||
_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) {
|
||||
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;
|
||||
}
|
||||
// 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.$target[0]);
|
||||
const currentValueOfDependency = formData.get(dependencyName);
|
||||
return this._compareTo(comparator, currentValueOfDependency, visibilityCondition, between);
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Calculates the visibility for each field with conditional visibility
|
||||
*/
|
||||
_updateFieldsVisibility() {
|
||||
for (const [fieldEl, visibilityFunction] of this._visibilityFunctionByFieldEl.entries()) {
|
||||
this._updateFieldVisibility(fieldEl, visibilityFunction());
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// 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();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
display: block;
|
||||
opacity: 0.5;
|
||||
}
|
||||
// Fields with conditional visibility are visible and identifiable in the editor
|
||||
.s_website_form_field_hidden_if {
|
||||
display: block !important;
|
||||
background-color: $o-we-fg-light;
|
||||
}
|
||||
// Select inputs do not trigger the default browser behavior
|
||||
// Since we use a custom editable element
|
||||
.s_website_form_field select {
|
||||
|
||||
@@ -148,6 +148,8 @@ const FormEditor = options.Class.extend({
|
||||
});
|
||||
|
||||
const FieldEditor = FormEditor.extend({
|
||||
VISIBILITY_DATASET: ['visibilityDependency', 'visibilityCondition', 'visibilityComparator', 'visibilityBetween'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
@@ -908,6 +910,22 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
const fieldEl = this._renderField(field);
|
||||
this._replaceFieldElement(fieldEl);
|
||||
},
|
||||
/**
|
||||
* Removes the visibility conditions concerned by the deleted field
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
onRemove() {
|
||||
const fieldName = this.$target[0].querySelector('.s_website_form_input').name;
|
||||
const isMultipleField = this.formEl.querySelectorAll(`.s_website_form_input[name="${fieldName}"]`).length > 1;
|
||||
if (isMultipleField) {
|
||||
return;
|
||||
}
|
||||
const dependentFieldContainerEl = this.formEl.querySelectorAll(`[data-visibility-dependency="${fieldName}"]`);
|
||||
for (const fieldContainerEl of dependentFieldContainerEl) {
|
||||
this._deleteConditionalVisibility(fieldContainerEl);
|
||||
}
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Options
|
||||
@@ -959,7 +977,15 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
if (multiple) {
|
||||
multiple.dataset.name = value;
|
||||
}
|
||||
this.$target[0].querySelectorAll('.s_website_form_input').forEach(el => el.name = value);
|
||||
const inputEls = this.$target[0].querySelectorAll('.s_website_form_input');
|
||||
const previousInputName = inputEls[0].name;
|
||||
inputEls.forEach(el => el.name = value);
|
||||
|
||||
// Synchronize the fields whose visibility depends on this field
|
||||
const dependentEls = this.formEl.querySelectorAll(`.s_website_form_field[data-visibility-dependency="${previousInputName}"]`);
|
||||
for (const dependentEl of dependentEls) {
|
||||
dependentEl.dataset.visibilityDependency = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -1019,10 +1045,47 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
*/
|
||||
renderListItems: async function (previewMode, value, params) {
|
||||
const valueList = JSON.parse(value);
|
||||
|
||||
// Synchronize the possible values with the fields whose visibility
|
||||
// depends on the current field
|
||||
const newValuesText = valueList.map(value => value.name);
|
||||
const inputEls = this.$target[0].querySelectorAll('.s_website_form_input, option');
|
||||
const inputName = this.$target[0].querySelector('.s_website_form_input').name;
|
||||
for (let i = 0; i < inputEls.length; i++) {
|
||||
const input = inputEls[i];
|
||||
if (newValuesText[i] && input.value && !newValuesText.includes(input.value)) {
|
||||
for (const dependentEl of this.formEl.querySelectorAll(
|
||||
`[data-visibility-condition="${input.value}"][data-visibility-dependency="${inputName}"]`)) {
|
||||
dependentEl.dataset.visibilityCondition = newValuesText[i];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const field = this._getActiveField(true);
|
||||
field.records = valueList;
|
||||
await this._replaceField(field);
|
||||
},
|
||||
/**
|
||||
* Sets the visibility of the field.
|
||||
*
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
setVisibility(previewMode, widgetValue, params) {
|
||||
if (widgetValue === 'conditional') {
|
||||
// Set a default visibility dependency
|
||||
const firstInputOfForm = this.formEl.querySelector('.s_website_form_field:not(.s_website_form_dnone) input');
|
||||
this._setVisibilityDependency(firstInputOfForm.name);
|
||||
return;
|
||||
}
|
||||
this._deleteConditionalVisibility(this.$target[0]);
|
||||
},
|
||||
/**
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
setVisibilityDependency(previewMode, widgetValue, params) {
|
||||
this._setVisibilityDependency(widgetValue);
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -1059,6 +1122,8 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
return this.$target[0].classList.contains(params.activeValue) ? params.activeValue : 'false';
|
||||
case 'renderListItems':
|
||||
return JSON.stringify(this._getListItems());
|
||||
case 'setVisibilityDependency':
|
||||
return this.$target[0].dataset.visibilityDependency || '';
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
@@ -1066,7 +1131,52 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
* @override
|
||||
*/
|
||||
_computeWidgetVisibility: function (widgetName, params) {
|
||||
const dependencyEl = this._getDependencyEl();
|
||||
switch (widgetName) {
|
||||
case 'hidden_condition_time_comparators_opt':
|
||||
return dependencyEl && dependencyEl.dataset.target;
|
||||
case 'hidden_condition_date_between':
|
||||
return dependencyEl && dependencyEl.dataset.target && dependencyEl.dataset.target.includes('#datepicker')
|
||||
&& ['between', '!between'].includes(this.$target[0].getAttribute('data-visibility-comparator'));
|
||||
case 'hidden_condition_datetime_between':
|
||||
return dependencyEl && dependencyEl.dataset.target && dependencyEl.dataset.target.includes('#datetimepicker')
|
||||
&& ['between', '!between'].includes(this.$target[0].dataset.visibilityComparator);
|
||||
case 'hidden_condition_additional_datetime':
|
||||
return dependencyEl && dependencyEl.dataset.target && dependencyEl.dataset.target.includes('#datetimepicker')
|
||||
&& !['set', '!set'].includes(this.$target[0].dataset.visibilityComparator);
|
||||
case 'hidden_condition_additional_date':
|
||||
return dependencyEl && dependencyEl.dataset.target && dependencyEl.dataset.target.includes('#datepicker')
|
||||
&& !['set', '!set'].includes(this.$target[0].dataset.visibilityComparator);
|
||||
case 'hidden_condition_additional_text':
|
||||
if (!this.$target[0].classList.contains('s_website_form_field_hidden_if') ||
|
||||
(dependencyEl && (['checkbox', 'radio'].includes(dependencyEl.type) || dependencyEl.nodeName === 'SELECT'))) {
|
||||
return false;
|
||||
}
|
||||
if (!dependencyEl) {
|
||||
return true;
|
||||
}
|
||||
if (dependencyEl.dataset.target && dependencyEl.dataset.target.includes('#date')) {
|
||||
return false;
|
||||
}
|
||||
return (['text', 'email', 'tel', 'url', 'search', 'password', 'number'].includes(dependencyEl.type)
|
||||
|| dependencyEl.nodeName === 'TEXTAREA') && !['set', '!set'].includes(this.$target[0].dataset.visibilityComparator);
|
||||
case 'hidden_condition_no_text_opt':
|
||||
return dependencyEl && (dependencyEl.type === 'checkbox' || dependencyEl.type === 'radio' || dependencyEl.nodeName === 'SELECT');
|
||||
case 'hidden_condition_num_opt':
|
||||
return dependencyEl && dependencyEl.type === 'number';
|
||||
case 'hidden_condition_text_opt':
|
||||
if (!this.$target[0].classList.contains('s_website_form_field_hidden_if') || (dependencyEl &&
|
||||
dependencyEl.dataset.target && dependencyEl.dataset.target.includes('#date'))) {
|
||||
return false;
|
||||
}
|
||||
return !dependencyEl || (['text', 'email', 'tel', 'url', 'search', 'password'].includes(dependencyEl.type) ||
|
||||
dependencyEl.nodeName === 'TEXTAREA');
|
||||
case 'hidden_condition_date_opt':
|
||||
return dependencyEl && dependencyEl.dataset.target && dependencyEl.dataset.target.includes('#datepicker');
|
||||
case 'hidden_condition_datetime_opt':
|
||||
return dependencyEl && dependencyEl.dataset.target && dependencyEl.dataset.target.includes('#datetimepicker');
|
||||
case 'hidden_condition_opt':
|
||||
return this.$target[0].classList.contains('s_website_form_field_hidden_if');
|
||||
case 'char_input_type_opt':
|
||||
return !this.$target[0].classList.contains('s_website_form_custom') && ['char', 'email', 'tel', 'url'].includes(this.$target[0].dataset.type);
|
||||
case 'multi_check_display_opt':
|
||||
@@ -1078,10 +1188,93 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* Deletes all attributes related to conditional visibility.
|
||||
*
|
||||
* @param {HTMLElement} fieldEl
|
||||
*/
|
||||
_deleteConditionalVisibility(fieldEl) {
|
||||
for (const name of this.VISIBILITY_DATASET) {
|
||||
delete fieldEl.dataset[name];
|
||||
}
|
||||
fieldEl.classList.remove('s_website_form_field_hidden_if', 'd-none');
|
||||
},
|
||||
/**
|
||||
* @returns {HTMLElement} The visibility dependency of the field
|
||||
*/
|
||||
_getDependencyEl() {
|
||||
const dependencyName = this.$target[0].dataset.visibilityDependency;
|
||||
return this.formEl.querySelector(`.s_website_form_input[name="${dependencyName}"]`);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_renderCustomXML: async function (uiFragment) {
|
||||
// Update available visibility dependencies
|
||||
const selectDependencyEl = uiFragment.querySelector('we-select[data-name="hidden_condition_opt"]');
|
||||
if (selectDependencyEl) {
|
||||
const existingDependencyNames = [];
|
||||
for (const el of this.formEl.querySelectorAll('.s_website_form_field:not(.s_website_form_dnone)')) {
|
||||
const inputEl = el.querySelector('.s_website_form_input');
|
||||
if (el.querySelector('.s_website_form_label_content') && inputEl && inputEl.name
|
||||
&& inputEl.name !== this.$target[0].querySelector('.s_website_form_input').name
|
||||
&& !existingDependencyNames.includes(inputEl.name)) {
|
||||
const button = document.createElement('we-button');
|
||||
button.textContent = el.querySelector('.s_website_form_label_content').textContent;
|
||||
button.dataset.setVisibilityDependency = inputEl.name;
|
||||
selectDependencyEl.append(button);
|
||||
existingDependencyNames.push(inputEl.name);
|
||||
}
|
||||
}
|
||||
|
||||
const comparator = this.$target[0].dataset.visibilityComparator;
|
||||
const dependencyEl = this._getDependencyEl();
|
||||
if (dependencyEl) {
|
||||
if ((['radio', 'checkbox'].includes(dependencyEl.type) || dependencyEl.nodeName === 'SELECT')) {
|
||||
// Update available visibility options
|
||||
const selectOptEl = uiFragment.querySelectorAll('we-select[data-name="hidden_condition_no_text_opt"]')[1];
|
||||
const inputContainerEl = this.$target[0];
|
||||
const dependencyEl = this._getDependencyEl();
|
||||
if (dependencyEl.nodeName === 'SELECT') {
|
||||
for (const option of dependencyEl.querySelectorAll('option')) {
|
||||
const button = document.createElement('we-button');
|
||||
button.textContent = option.value;
|
||||
button.dataset.selectDataAttribute = option.value;
|
||||
selectOptEl.append(button);
|
||||
}
|
||||
if (!inputContainerEl.dataset.visibilityCondition) {
|
||||
inputContainerEl.dataset.visibilityCondition = dependencyEl.querySelector('option').value;
|
||||
}
|
||||
} else { // DependecyEl is a radio or a checkbox
|
||||
const dependencyContainerEl = dependencyEl.closest('.s_website_form_field');
|
||||
const inputsInDependencyContainer = dependencyContainerEl.querySelectorAll('.s_website_form_input');
|
||||
for (const el of inputsInDependencyContainer) {
|
||||
const button = document.createElement('we-button');
|
||||
button.textContent = el.value;
|
||||
button.dataset.selectDataAttribute = el.value;
|
||||
selectOptEl.append(button);
|
||||
}
|
||||
if (!inputContainerEl.dataset.visibilityCondition) {
|
||||
inputContainerEl.dataset.visibilityCondition = inputsInDependencyContainer[0].value;
|
||||
}
|
||||
}
|
||||
if (!inputContainerEl.dataset.visibilityComparator) {
|
||||
inputContainerEl.dataset.visibilityComparator = 'selected';
|
||||
}
|
||||
this.rerender = comparator ? this.rerender : true;
|
||||
}
|
||||
if (!comparator) {
|
||||
// Set a default comparator according to the type of dependency
|
||||
if (dependencyEl.dataset.target) {
|
||||
this.$target[0].dataset.visibilityComparator = 'after';
|
||||
} else if (['text', 'email', 'tel', 'url', 'search', 'password', 'number'].includes(dependencyEl.type)
|
||||
|| dependencyEl.nodeName === 'TEXTAREA') {
|
||||
this.$target[0].dataset.visibilityComparator = 'equal';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectEl = uiFragment.querySelector('we-select[data-name="type_opt"]');
|
||||
const currentFieldName = this._getFieldName();
|
||||
const fieldsInForm = Array.from(this.formEl.querySelectorAll('.s_website_form_field:not(.s_website_form_custom) .s_website_form_input')).map(el => el.name).filter(el => el !== currentFieldName);
|
||||
@@ -1119,7 +1312,11 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
await this._fetchFieldRecords(field);
|
||||
list.dataset.availableRecords = JSON.stringify(field.records);
|
||||
}
|
||||
uiFragment.appendChild(list);
|
||||
if (selectDependencyEl) {
|
||||
uiFragment.insertBefore(list, uiFragment.querySelector('we-select[string="Visibility"]'));
|
||||
} else {
|
||||
uiFragment.appendChild(list);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Replaces the target content with the field provided.
|
||||
@@ -1144,10 +1341,40 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
|
||||
* @param {HTMLElement} fieldEl
|
||||
*/
|
||||
_replaceFieldElement(fieldEl) {
|
||||
const hasConditionalVisibility = this.$target[0].classList.contains('s_website_form_field_hidden_if');
|
||||
const previousName = this.$target[0].querySelector('.s_website_form_input').name;
|
||||
[...this.$target[0].childNodes].forEach(node => node.remove());
|
||||
[...fieldEl.childNodes].forEach(node => this.$target[0].appendChild(node));
|
||||
[...fieldEl.attributes].forEach(el => this.$target[0].removeAttribute(el.nodeName));
|
||||
[...fieldEl.attributes].forEach(el => this.$target[0].setAttribute(el.nodeName, el.nodeValue));
|
||||
if (hasConditionalVisibility) {
|
||||
this.$target[0].classList.add('s_website_form_field_hidden_if', 'd-none');
|
||||
}
|
||||
const dependentFieldEls = this.formEl.querySelectorAll(`.s_website_form_field[data-visibility-dependency="${previousName}"]`);
|
||||
const newName = this.$target[0].querySelector('.s_website_form_input').name;
|
||||
if (previousName !== newName && dependentFieldEls) {
|
||||
// In order to keep the visibility conditions consistent,
|
||||
// when the name has changed, it means that the type has changed so
|
||||
// all fields whose visibility depends on this field must be updated so that
|
||||
// they no longer have conditional visibility
|
||||
for (const fieldEl of dependentFieldEls) {
|
||||
this._deleteConditionalVisibility(fieldEl);
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Sets the visibility dependency of the field.
|
||||
*
|
||||
* @param {string} value name of the dependency input
|
||||
*/
|
||||
_setVisibilityDependency(value) {
|
||||
delete this.$target[0].dataset.visibilityCondition;
|
||||
delete this.$target[0].dataset.visibilityComparator;
|
||||
const previousDependency = this._getDependencyEl();
|
||||
if (this.formEl.querySelector(`.s_website_form_input[name="${value}"]`).type !== (previousDependency && previousDependency.type)) {
|
||||
this.rerender = true;
|
||||
}
|
||||
this.$target[0].dataset.visibilityDependency = value;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -4,6 +4,11 @@ odoo.define('website.tour.form_editor', function (require) {
|
||||
const rpc = require('web.rpc');
|
||||
const tour = require("web_tour.tour");
|
||||
|
||||
// Visibility possible values:
|
||||
const VISIBLE = 'Always Visible';
|
||||
const HIDDEN = 'Hidden';
|
||||
const CONDITIONALVISIBILITY = 'Visible only if';
|
||||
|
||||
const selectButtonByText = function (text) {
|
||||
return [{
|
||||
content: "Open the select",
|
||||
@@ -23,7 +28,7 @@ odoo.define('website.tour.form_editor', function (require) {
|
||||
trigger: `we-select we-button[${data}]`,
|
||||
}];
|
||||
};
|
||||
const addField = function (data, name, type, label, required, hidden) {
|
||||
const addField = function (data, name, type, label, required, display = {visibility: VISIBLE, condition: ''}) {
|
||||
const ret = [{
|
||||
content: "Select form",
|
||||
extra_trigger: '.s_website_form_field',
|
||||
@@ -37,8 +42,17 @@ odoo.define('website.tour.form_editor', function (require) {
|
||||
content: "Wait for field to load",
|
||||
trigger: `.s_website_form_field[data-type="${name}"], .s_website_form_input[name="${name}"]`, //custom or existing field
|
||||
run: function () {},
|
||||
}];
|
||||
},
|
||||
...selectButtonByText(display.visibility),
|
||||
];
|
||||
let testText = '.s_website_form_field';
|
||||
if (display.condition) {
|
||||
ret.push({
|
||||
content: "Set the visibility condition",
|
||||
trigger: 'we-input[data-attribute-name="visibilityCondition"] input',
|
||||
run: `text ${display.condition}`,
|
||||
});
|
||||
}
|
||||
if (required) {
|
||||
testText += '.s_website_form_required';
|
||||
ret.push({
|
||||
@@ -46,13 +60,6 @@ odoo.define('website.tour.form_editor', function (require) {
|
||||
trigger: 'we-button[data-name="required_opt"] we-checkbox',
|
||||
});
|
||||
}
|
||||
if (hidden) {
|
||||
testText += '.s_website_form_field_hidden';
|
||||
ret.push({
|
||||
content: "Mark the field as hidden",
|
||||
trigger: 'we-button[data-name="hidden_opt"] we-checkbox',
|
||||
});
|
||||
}
|
||||
if (label) {
|
||||
testText += `:has(label:contains("${label}"))`;
|
||||
ret.push({
|
||||
@@ -72,11 +79,11 @@ odoo.define('website.tour.form_editor', function (require) {
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
const addCustomField = function (name, type, label, required, hidden) {
|
||||
return addField(`data-custom-field="${name}"`, name, type, label, required, hidden);
|
||||
const addCustomField = function (name, type, label, required, display) {
|
||||
return addField(`data-custom-field="${name}"`, name, type, label, required, display);
|
||||
};
|
||||
const addExistingField = function (name, type, label, required, hidden) {
|
||||
return addField(`data-existing-field="${name}"`, name, type, label, required, hidden);
|
||||
const addExistingField = function (name, type, label, required, display) {
|
||||
return addField(`data-existing-field="${name}"`, name, type, label, required, display);
|
||||
};
|
||||
|
||||
tour.register("website_form_editor_tour", {
|
||||
@@ -104,9 +111,11 @@ odoo.define('website.tour.form_editor', function (require) {
|
||||
trigger: '[data-field-name="email_to"] input',
|
||||
run: 'text_blur test@test.test',
|
||||
},
|
||||
...addExistingField('email_cc', 'text', 'Test conditional visibility', false, {visibility: CONDITIONALVISIBILITY, condition: 'odoo'}),
|
||||
|
||||
...addExistingField('date', 'text', 'Test Date', true),
|
||||
|
||||
...addExistingField('record_name', 'text', 'Awesome Label', false, true),
|
||||
...addExistingField('record_name', 'text', 'Awesome Label', false, {visibility: HIDDEN}),
|
||||
|
||||
...addExistingField('body_html', 'textarea', 'Your Message', true),
|
||||
|
||||
@@ -394,6 +403,25 @@ odoo.define('website.tour.form_editor', function (require) {
|
||||
trigger: "textarea[name='description']",
|
||||
run: "text magan"
|
||||
},
|
||||
{
|
||||
content: "Check if conditional field is visible, it shouldn't.",
|
||||
trigger: "body",
|
||||
run: function () {
|
||||
const style = window.getComputedStyle(this.$anchor[0].getElementsByClassName('s_website_form_field_hidden_if')[0]);
|
||||
if (style.display !== 'none') {
|
||||
console.error('error This field should be invisible when the name is not odoo');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
content: "Change name input",
|
||||
trigger: "input[name='name']",
|
||||
run: "text odoo",
|
||||
},
|
||||
{
|
||||
content: "Check if conditional field is visible, it should.",
|
||||
trigger: "input[name='email_cc']",
|
||||
},
|
||||
{
|
||||
content: "Send the form",
|
||||
trigger: ".s_website_form_send"
|
||||
|
||||
@@ -133,8 +133,61 @@
|
||||
data-apply-to=".s_website_form_date input"/>
|
||||
<we-checkbox string="Required" data-name="required_opt" data-no-preview="true"
|
||||
data-toggle-required="s_website_form_required"/>
|
||||
<we-checkbox string="Hidden" data-name="hidden_opt" data-no-preview="true"
|
||||
data-select-class="s_website_form_field_hidden"/>
|
||||
<we-select string="Visibility" data-no-preview="true">
|
||||
<we-button data-set-visibility="visible" data-select-class="">Always Visible</we-button>
|
||||
<we-button data-set-visibility="hidden" data-select-class="s_website_form_field_hidden">Hidden</we-button>
|
||||
<we-button data-set-visibility="conditional" data-select-class="s_website_form_field_hidden_if d-none">Visible only if</we-button>
|
||||
</we-select>
|
||||
<we-row data-name="hidden_condition_opt" string="">
|
||||
<we-select data-name="hidden_condition_opt" data-no-preview="true">
|
||||
<!-- Load every existing form input -->
|
||||
</we-select>
|
||||
<we-select data-name="hidden_condition_no_text_opt" data-attribute-name="visibilityComparator" data-no-preview="true">
|
||||
<we-button data-select-data-attribute="selected">Is equal to</we-button>
|
||||
<we-button data-select-data-attribute="!selected">Is not equal to</we-button>
|
||||
</we-select>
|
||||
<we-select data-name="hidden_condition_text_opt" data-attribute-name="visibilityComparator" data-no-preview="true">
|
||||
<!-- str comparator possibilities -->
|
||||
<we-button data-select-data-attribute="contains">Contains</we-button>
|
||||
<we-button data-select-data-attribute="!contains">Doesn't contain</we-button>
|
||||
<we-button data-select-data-attribute="equal">Is equal to</we-button>
|
||||
<we-button data-select-data-attribute="!equal">Is not equal to</we-button>
|
||||
<we-button data-select-data-attribute="set">Is set</we-button>
|
||||
<we-button data-select-data-attribute="!set">Is not set</we-button>
|
||||
</we-select>
|
||||
<we-select data-name="hidden_condition_num_opt" data-attribute-name="visibilityComparator" data-no-preview="true">
|
||||
<!-- number comparator possibilities -->
|
||||
<we-button data-select-data-attribute="equal">Is equal to</we-button>
|
||||
<we-button data-select-data-attribute="!equal">Is not equal to</we-button>
|
||||
<we-button data-select-data-attribute="greater">Is greater than</we-button>
|
||||
<we-button data-select-data-attribute="less">Is less than</we-button>
|
||||
<we-button data-select-data-attribute="greater or equal">Is greater than or equal to</we-button>
|
||||
<we-button data-select-data-attribute="less or equal">Is less than or equal to</we-button>
|
||||
<we-button data-select-data-attribute="set">Is set</we-button>
|
||||
<we-button data-select-data-attribute="!set">Is not set</we-button>
|
||||
</we-select>
|
||||
<we-select data-name="hidden_condition_time_comparators_opt" data-attribute-name="visibilityComparator" data-no-preview="true">
|
||||
<!-- date & datetime comparator possibilities -->
|
||||
<we-button data-select-data-attribute="dateEqual">Is equal to</we-button>
|
||||
<we-button data-select-data-attribute="date!equal">Is not equal to</we-button>
|
||||
<we-button data-select-data-attribute="after">Is after</we-button>
|
||||
<we-button data-select-data-attribute="before">Is before</we-button>
|
||||
<we-button data-select-data-attribute="equal or after">Is after or equal to</we-button>
|
||||
<we-button data-select-data-attribute="equal or before">Is before or equal to</we-button>
|
||||
<we-button data-select-data-attribute="set">Is set</we-button>
|
||||
<we-button data-select-data-attribute="!set">Is not set</we-button>
|
||||
<we-button data-select-data-attribute='between'>Is between (included)</we-button>
|
||||
<we-button data-select-data-attribute='!between'>Is not between (excluded)</we-button>
|
||||
</we-select>
|
||||
</we-row>
|
||||
<we-select class="o_we_large" data-name="hidden_condition_no_text_opt" data-attribute-name="visibilityCondition" data-no-preview="true">
|
||||
<!-- checkbox, select, radio possible values -->
|
||||
</we-select>
|
||||
<we-input class="o_we_large" data-name="hidden_condition_additional_text" data-attribute-name="visibilityCondition" data-select-data-attribute=""/>
|
||||
<we-datetimepicker data-name="hidden_condition_additional_datetime" data-attribute-name="visibilityCondition" data-select-data-attribute="" />
|
||||
<we-datepicker data-name="hidden_condition_additional_date" data-attribute-name="visibilityCondition" data-select-data-attribute=""/>
|
||||
<we-datetimepicker data-name="hidden_condition_datetime_between" data-attribute-name="visibilityBetween" data-select-data-attribute=""/>
|
||||
<we-datepicker data-name="hidden_condition_date_between" data-attribute-name="visibilityBetween" data-select-data-attribute="" />
|
||||
</div>
|
||||
|
||||
<div data-js="WebsiteFormSubmit" data-selector=".s_website_form_submit" data-exclude=".s_website_form_no_submit_options">
|
||||
|
||||
Reference in New Issue
Block a user