[REF] web_editor, *: make form list a UserValueWidget

* = website_form

The form we-list is now a generic UserValueWidget.

Part of: https://github.com/odoo/odoo/pull/47949
task-2011583

Co-authored-by: qsm-odoo <qsm@odoo.com>
This commit is contained in:
fja-odoo
2021-02-26 13:51:13 +00:00
committed by qsm-odoo
co-authored by qsm-odoo
parent eb80d33471
commit 9304f8fa7b
6 changed files with 339 additions and 315 deletions
@@ -1695,6 +1695,229 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({
},
});
const ListUserValueWidget = UserValueWidget.extend({
tagName: 'we-list',
events: {
'click we-button.o_we_select_remove_option': '_onRemoveItemClick',
'click we-button.o_we_list_add_optional': '_onAddCustomItemClick',
'click we-button.o_we_list_add_existing': '_onAddExistingItemClick',
'click we-select.o_we_user_value_widget': '_onAddItemSelectClick',
'input table input': '_onListItemInput',
},
/**
* @override
*/
start() {
this.addItemTitle = this.el.dataset.addItemTitle;
if (this.el.dataset.availableRecords) {
this.records = JSON.parse(this.el.dataset.availableRecords);
} else {
this.isCustom = true;
}
this.listTable = document.createElement('table');
const tableWrapper = document.createElement('div');
tableWrapper.classList.add('o_we_table_wrapper');
tableWrapper.appendChild(this.listTable);
this.containerEl.appendChild(tableWrapper);
this.el.classList.add('o_we_fw');
this._makeListItemsSortable();
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
getMethodsParams() {
return _.extend(this._super(...arguments), {
records: this.records,
});
},
/**
* @override
*/
setValue() {
this._super(...arguments);
const currentValues = JSON.parse(this._value);
this.listTable.innerHTML = '';
if (this.addItemButton) {
this.addItemButton.remove();
}
if (this.isCustom) {
this.addItemButton = document.createElement('we-button');
this.addItemButton.textContent = this.addItemTitle;
this.addItemButton.classList.add('o_we_list_add_optional');
currentValues.forEach(el => this._addItemToTable(el, el));
} else {
// TODO use a real select widget ?
this.addItemButton = document.createElement('we-select');
this.addItemButton.classList.add('o_we_user_value_widget');
const divEl = document.createElement('div');
this.addItemButton.appendChild(divEl);
const togglerEl = document.createElement('we-toggler');
togglerEl.textContent = this.addItemTitle;
divEl.appendChild(togglerEl);
this.selectMenuEl = document.createElement('we-selection-items');
divEl.appendChild(this.selectMenuEl);
currentValues.forEach(val => {
const record = this.records.find(rec => rec.id === val);
this._addItemToTable(record.id, record.display_name);
});
this._reloadSelectDropdown(currentValues);
}
this.containerEl.appendChild(this.addItemButton);
this._makeListItemsSortable();
},
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @private
* @param {string || integer} id
* @param {string} text
*/
_addItemToTable(id, text) {
if (text === undefined) {
text = _t("Item");
}
const draggableEl = document.createElement('we-button');
draggableEl.classList.add('o_we_drag_handle', 'o_we_link', 'fa', 'fa-fw', 'fa-arrows');
draggableEl.dataset.noPreview = 'true';
const inputEl = document.createElement('input');
inputEl.type = 'text';
if (text) {
inputEl.value = text;
}
if (!this.isCustom && id) {
inputEl.name = id;
}
inputEl.disabled = !this.isCustom;
const trEl = document.createElement('tr');
const buttonEl = document.createElement('we-button');
buttonEl.classList.add('o_we_select_remove_option', 'o_we_link', 'o_we_text_danger', 'fa', 'fa-fw', 'fa-minus');
buttonEl.dataset.removeOption = id;
const draggableTdEl = document.createElement('td');
const inputTdEl = document.createElement('td');
const buttonTdEl = document.createElement('td');
draggableTdEl.appendChild(draggableEl);
trEl.appendChild(draggableTdEl);
inputTdEl.appendChild(inputEl);
trEl.appendChild(inputTdEl);
buttonTdEl.appendChild(buttonEl);
trEl.appendChild(buttonTdEl);
this.listTable.appendChild(trEl);
if (this.isCustom) {
inputEl.focus();
}
},
/**
* @private
*/
_makeListItemsSortable() {
$(this.listTable).sortable({
axis: 'y',
handle: '.o_we_drag_handle',
items: 'tr',
cursor: 'move',
opacity: 0.6,
stop: (event, ui) => {
this._notifyCurrentState();
},
});
},
/**
* @private
*/
_notifyCurrentState() {
const values = [...this.listTable.querySelectorAll('input')].map(el => {
const id = this.isCustom ? el.value : el.name;
const idInt = parseInt(id);
return isNaN(idInt) ? id : idInt;
});
if (this.isCustom) {
this.records = values.map(v => ({id: v, display_name: v}));
}
this._value = JSON.stringify(values);
this.notifyValueChange(true);
if (!this.isCustom) {
this._reloadSelectDropdown(values);
}
},
/**
* @private
* @param {Array} currentValues
*/
_reloadSelectDropdown(currentValues) {
this.selectMenuEl.innerHTML = '';
this.records.forEach(el => {
if (!currentValues.includes(el.id)) {
const option = document.createElement('we-button');
option.classList.add('o_we_list_add_existing');
option.dataset.addOption = el.id;
option.dataset.noPreview = 'true';
const divEl = document.createElement('div');
divEl.textContent = el.display_name;
option.appendChild(divEl);
this.selectMenuEl.appendChild(option);
}
});
if (!this.selectMenuEl.children.length) {
const title = document.createElement('we-title');
title.textContent = _("No more records");
this.selectMenuEl.appendChild(title);
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onAddCustomItemClick() {
this._addItemToTable();
this._notifyCurrentState();
},
/**
* @private
* @param {Event} ev
*/
_onAddExistingItemClick(ev) {
const value = ev.currentTarget.dataset.addOption;
this._addItemToTable(value, ev.currentTarget.textContent);
this._notifyCurrentState();
},
/**
* @private
* @param {Event} ev
*/
_onAddItemSelectClick(ev) {
ev.currentTarget.querySelector('we-toggler').classList.toggle('active');
},
/**
* @private
*/
_onListItemInput() {
this._notifyCurrentState();
},
/**
* @private
* @param {Event} ev
*/
_onRemoveItemClick(ev) {
if (ev.target.closest('table').querySelectorAll('tr').length > 1) {
ev.target.closest('tr').remove();
this._notifyCurrentState();
}
},
});
const RangeUserValueWidget = UnitUserValueWidget.extend({
tagName: 'we-range',
events: {
@@ -2075,6 +2298,7 @@ const userValueWidgetsRegistry = {
'we-multi': MultiUserValueWidget,
'we-colorpicker': ColorpickerUserValueWidget,
'we-datetimepicker': DatetimePickerUserValueWidget,
'we-list': ListUserValueWidget,
'we-imagepicker': ImagepickerUserValueWidget,
'we-videopicker': VideopickerUserValueWidget,
'we-range': RangeUserValueWidget,
@@ -1174,6 +1174,47 @@ body.editor_enable.editor_has_snippets {
}
}
// Lists
we-list {
> div {
flex-flow: row wrap;
}
.o_we_table_wrapper {
width: 100%;
max-height: 200px;
overflow-y: auto;
margin-bottom: $o-we-sidebar-content-field-spacing / 2;
table {
table-layout: auto;
width: 100%;
input {
width: 100%;
border: $o-we-sidebar-content-field-border-width solid $o-we-sidebar-content-field-border-color;
border-radius: $o-we-sidebar-content-field-border-radius;
padding: 0 $o-we-sidebar-content-field-clickable-spacing;
background-color: $o-we-sidebar-content-field-input-bg;
color: inherit;
font-family: $o-we-sidebar-content-field-input-font-family;
}
tr {
border: none;
}
td {
padding-bottom: $o-we-sidebar-content-field-spacing / 2;
&:first-child, &:last-child {
width: 1px;
}
}
}
}
.o_we_user_value_widget {
flex: 1 1 auto;
}
}
// Multi widgets
we-multi.o_we_user_value_widget {
margin-top: 0;
@@ -1,37 +0,0 @@
#oe_snippets > .o_we_customize_panel we-customizeblock-option {
we-list {
we-title, we-button {
margin-top: $o-we-sidebar-content-field-spacing;
}
.oe_we_table_wraper {
margin-top: $o-we-sidebar-content-field-spacing;
max-height: 200px;
overflow-y: auto;
table {
table-layout: fixed;
width: 100%;
input {
width: 100%;
border: $o-we-sidebar-content-field-border-width solid $o-we-sidebar-content-field-border-color;
border-radius: $o-we-sidebar-content-field-border-radius;
padding: 0 $o-we-sidebar-content-field-clickable-spacing;
background-color: $o-we-sidebar-content-field-input-bg;
color: inherit;
font-family: $o-we-sidebar-content-field-input-font-family;
}
tr {
border: 1px solid rgba(white, 0.1);
border-left: none;
border-right: none;
}
td {
&:first-child, &:last-child {
width: 28px;
}
}
}
}
}
}
@@ -146,9 +146,10 @@ const FieldEditor = FormEditor.extend({
* Returns the target as a field Object
*
* @private
* @param {boolean} noRecords
* @returns {Object}
*/
_getActiveField: function () {
_getActiveField: function (noRecords) {
let field;
const labelText = this.$target.find('.s_website_form_label_content').text();
if (this._isFieldCustom()) {
@@ -157,7 +158,9 @@ const FieldEditor = FormEditor.extend({
field = Object.assign({}, this.fields[this._getFieldName()]);
field.string = labelText;
}
field.records = this._getListItems();
if (!noRecords) {
field.records = this._getListItems();
}
this._setActiveProperties(field);
return field;
},
@@ -689,14 +692,6 @@ options.registry.WebsiteFormEditor = FormEditor.extend({
const authorizedFieldsCache = {};
options.registry.WebsiteFieldEditor = FieldEditor.extend({
events: _.extend({}, FieldEditor.prototype.events, {
'click we-button.o_we_select_remove_option': '_onRemoveItemClick',
'click we-button.o_we_list_add_optional': '_onAddCustomItemClick',
'click we-button.o_we_list_add_existing': '_onAddExistingItemClick',
'click we-list we-select': '_onAddItemSelectClick',
'input we-list input': '_onListItemInput',
}),
/**
* @override
*/
@@ -739,22 +734,27 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
});
return _super(...arguments);
},
/**
* @override
*/
start: async function () {
const _super = this._super.bind(this);
// Build the custom select
const select = this._getSelect();
if (select) {
const field = this._getActiveField();
await this._replaceField(field);
}
return _super(...arguments);
},
/**
* @override
*/
cleanForSave: function () {
this.$target[0].querySelectorAll('#editable_select').forEach(el => el.remove());
const select = this._getSelect();
if (select && this.listTable) {
if (select) {
select.style.display = '';
$(select).empty();
// Rebuild the select from the we-list
this.listTable.querySelectorAll('input').forEach(el => {
const option = document.createElement('option');
option.textContent = el.value;
option.value = this._isFieldCustom() ? el.value : el.name;
select.appendChild(option);
});
}
},
/**
@@ -763,16 +763,8 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
updateUI: async function () {
// See Form updateUI
if (this.rerender) {
const select = this._getSelect();
if (select && !this.$target[0].querySelector('#editable_select')) {
select.style.display = 'none';
const editableSelect = document.createElement('div');
editableSelect.id = 'editable_select';
editableSelect.classList = 'form-control s_website_form_input';
select.parentElement.appendChild(editableSelect);
}
this.rerender = false;
await this._rerenderXML().then(() => this._renderList());
await this._rerenderXML();
return;
}
await this._super.apply(this, arguments);
@@ -838,7 +830,6 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
const field = this._getActiveField();
field.formatInfo.labelPosition = value;
await this._replaceField(field);
this.rerender = true;
},
selectType: async function (previewMode, value, params) {
const field = this._getActiveField();
@@ -871,6 +862,15 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
name: 'field_mark',
});
},
/**
* Apply the we-list on the target and rebuild the input(s)
*/
renderListItems: async function (previewMode, value, params) {
const valueList = JSON.parse(value);
const field = this._getActiveField(true);
field.records = valueList.map(val => params.records.find(rec => rec.id === val));
await this._replaceField(field);
},
//----------------------------------------------------------------------
// Private
@@ -897,6 +897,10 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
}
case 'toggleRequired':
return this.$target[0].classList.contains(params.activeValue) ? params.activeValue : 'false';
case 'renderListItems': {
const values = this._getListItems().map(el => el.id);
return JSON.stringify(values);
}
}
return this._super(...arguments);
},
@@ -919,7 +923,7 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
/**
* @override
*/
_renderCustomXML: function (uiFragment) {
_renderCustomXML: async function (uiFragment) {
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);
@@ -930,6 +934,27 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
availableFields.unshift(title);
availableFields.forEach(option => selectEl.append(option.cloneNode(true)));
}
const select = this._getSelect();
const multipleInputs = this._getMultipleInputs();
if (!select && !multipleInputs) {
return;
}
const field = Object.assign({}, this.fields[this._getFieldName()]);
const type = this._getFieldType();
const list = document.createElement('we-list');
const optionText = select ? 'Option' : type === 'selection' ? 'Radio' : 'Checkbox';
list.setAttribute('string', `${optionText} List`);
list.dataset.addItemTitle = _.str.sprintf(_t("Add new %s"), optionText);
list.dataset.renderListItems = '';
if (!this._isFieldCustom()) {
await this._fetchFieldRecords(field);
list.dataset.availableRecords = JSON.stringify(field.records);
}
uiFragment.appendChild(list);
},
/**
* Replace the target content with the field provided
@@ -946,211 +971,25 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
[...htmlField.attributes].forEach(el => this.$target[0].removeAttribute(el.nodeName));
[...htmlField.attributes].forEach(el => this.$target[0].setAttribute(el.nodeName, el.nodeValue));
},
/**
* To do after rerenderXML to add the list to the options
*
* @private
*/
_renderList: function () {
let addItemButton, addItemTitle, listTitle;
const select = this._getSelect();
const multipleInputs = this._getMultipleInputs();
this.listTable = document.createElement('table');
const isCustomField = this._isFieldCustom();
if (select) {
listTitle = 'Options List';
addItemTitle = 'Add new Option';
select.querySelectorAll('option').forEach(opt => {
this._addItemToTable(opt.value, opt.textContent.trim());
});
this._renderListItems();
} else if (multipleInputs) {
listTitle = multipleInputs.querySelector('.radio') ? 'Radio List' : 'Checkbox List';
addItemTitle = 'Add new Checkbox';
multipleInputs.querySelectorAll('.checkbox, .radio').forEach(opt => {
this._addItemToTable(opt.querySelector('input').value, opt.querySelector('.s_website_form_check_label').textContent.trim());
});
} else {
return;
}
if (isCustomField) {
addItemButton = document.createElement('we-button');
addItemButton.textContent = addItemTitle;
addItemButton.classList.add('o_we_list_add_optional');
addItemButton.dataset.noPreview = 'true';
} else {
addItemButton = document.createElement('we-select');
addItemButton.classList.add('o_we_user_value_widget'); // Todo dont use user value widget class
const togglerEl = document.createElement('we-toggler');
togglerEl.textContent = addItemTitle;
addItemButton.appendChild(togglerEl);
const selectMenuEl = document.createElement('we-selection-items');
addItemButton.appendChild(selectMenuEl);
this._loadListDropdown(selectMenuEl);
}
const selectInputEl = document.createElement('we-list');
const title = document.createElement('we-title');
title.textContent = listTitle;
selectInputEl.appendChild(title);
const tableWrapper = document.createElement('div');
tableWrapper.classList.add('oe_we_table_wraper');
tableWrapper.appendChild(this.listTable);
selectInputEl.appendChild(tableWrapper);
selectInputEl.appendChild(addItemButton);
this.el.insertBefore(selectInputEl, this.el.querySelector('[data-set-placeholder]'));
this._makeListItemsSortable();
},
/**
* Load the dropdown of the list with the records missing from the list.
*
* @private
* @param {HTMLElement} selectMenu
*/
_loadListDropdown: function (selectMenu) {
selectMenu = selectMenu || this.el.querySelector('we-list we-selection-items');
if (selectMenu) {
$(selectMenu).empty();
const field = Object.assign({}, this.fields[this._getFieldName()]);
this._fetchFieldRecords(field).then(() => {
let buttonItems;
const optionIds = Array.from(this.listTable.querySelectorAll('input')).map(opt => {
return field.type === 'selection' ? opt.name : parseInt(opt.name);
});
const availableRecords = (field.records || []).filter(el => !optionIds.includes(el.id));
if (availableRecords.length) {
buttonItems = availableRecords.map(el => {
const option = document.createElement('we-button');
option.classList.add('o_we_list_add_existing');
option.dataset.addOption = el.id;
option.dataset.noPreview = 'true';
option.textContent = el.display_name;
return option;
});
} else {
const title = document.createElement('we-title');
title.textContent = 'No more records';
buttonItems = [title];
}
buttonItems.forEach(button => selectMenu.appendChild(button));
});
}
},
/**
* @private
*/
_makeListItemsSortable: function () {
$(this.listTable).sortable({
axis: 'y',
handle: '.o_we_drag_handle',
items: 'tr',
cursor: 'move',
opacity: 0.6,
stop: (event, ui) => {
this._renderListItems();
},
});
},
/**
* @private
* @param {string} id
* @param {string} text
*/
_addItemToTable: function (id, text) {
const isCustomField = this._isFieldCustom();
const draggableEl = document.createElement('we-button');
draggableEl.classList.add('o_we_drag_handle', 'o_we_link', 'fa', 'fa-fw', 'fa-arrows');
draggableEl.dataset.noPreview = 'true';
const inputEl = document.createElement('input');
inputEl.type = 'text';
if (text) {
inputEl.value = text;
}
if (!isCustomField && id) {
inputEl.name = id;
}
inputEl.disabled = !isCustomField;
const trEl = document.createElement('tr');
const buttonEl = document.createElement('we-button');
buttonEl.classList.add('o_we_select_remove_option', 'o_we_link', 'o_we_text_danger', 'fa', 'fa-fw', 'fa-minus');
buttonEl.dataset.removeOption = id;
buttonEl.dataset.noPreview = 'true';
const draggableTdEl = document.createElement('td');
const inputTdEl = document.createElement('td');
const buttonTdEl = document.createElement('td');
draggableTdEl.appendChild(draggableEl);
trEl.appendChild(draggableTdEl);
inputTdEl.appendChild(inputEl);
trEl.appendChild(inputTdEl);
buttonTdEl.appendChild(buttonEl);
trEl.appendChild(buttonTdEl);
this.listTable.appendChild(trEl);
if (isCustomField) {
inputEl.focus();
}
},
/**
* Apply the we-list on the target and rebuild the input(s)
*
* @private
*/
_renderListItems: function () {
const multiInputsWrap = this._getMultipleInputs();
const selectWrap = this.$target[0].querySelector('#editable_select');
const isRequiredField = this._isFieldRequired();
const name = this._getFieldName();
if (multiInputsWrap) {
const type = multiInputsWrap.querySelector('.radio') ? 'radio' : 'checkbox';
$(multiInputsWrap).empty();
const params = {
field: {
name: name,
id: Math.random().toString(36).substring(2, 15), // Big unique ID
required: isRequiredField,
formatInfo: {
multiPosition: multiInputsWrap.dataset.display,
}
}
};
this._getListItems().forEach((record, idx) => {
params.record_index = idx;
params.record = record;
const template = document.createElement('template');
$(template).html(qweb.render(`website_form.${type}`, params));
multiInputsWrap.appendChild(template.content.firstElementChild);
});
} else if (selectWrap) {
$(selectWrap).empty();
this.listTable.querySelectorAll('input').forEach(el => {
const option = document.createElement('div');
option.id = (el.name || el.value);
option.classList.add('s_website_form_select_item');
option.textContent = el.value;
selectWrap.appendChild(option);
});
}
},
/**
* Returns an array based on the we-list containing the field's records
*
* @returns {Array}
*/
_getListItems: function () {
if (!this.listTable) {
return null;
const select = this._getSelect();
const multipleInputs = this._getMultipleInputs();
let options = [];
if (select) {
options = [...select.querySelectorAll('option')];
} else if (multipleInputs) {
options = [...multipleInputs.querySelectorAll('.checkbox input, .radio input')];
}
const isCustomField = this._isFieldCustom();
const records = [];
this.listTable.querySelectorAll('input').forEach(el => {
const id = isCustomField ? el.value : el.name;
records.push({
id: id,
display_name: el.value,
});
return options.map(opt => {
const id = parseInt(opt.value);
return {
id: isNaN(id) ? opt.value : id,
display_name: select ? opt.textContent : opt.value
};
});
return records;
},
/**
* Returns the select element if it exist else null
@@ -1161,53 +1000,6 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
_getSelect: function () {
return this.$target[0].querySelector('select');
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Event} ev
*/
_onRemoveItemClick: function (ev) {
ev.target.closest('tr').remove();
this._loadListDropdown();
this._renderListItems();
},
/**
* @private
* @param {Event} ev
*/
_onAddCustomItemClick: function (ev) {
this._addItemToTable();
this._makeListItemsSortable();
this._renderListItems();
},
/**
* @private
* @param {Event} ev
*/
_onAddExistingItemClick: function (ev) {
const value = ev.currentTarget.dataset.addOption;
this._addItemToTable(value, ev.currentTarget.textContent);
this._makeListItemsSortable();
this._loadListDropdown();
this._renderListItems();
},
/**
* @private
* @param {Event} ev
*/
_onAddItemSelectClick: function (ev) {
ev.currentTarget.querySelector('we-toggler').classList.toggle('active');
},
/**
* @private
*/
_onListItemInput: function () {
this._renderListItems();
},
});
options.registry.AddFieldForm = FormEditor.extend({
@@ -275,13 +275,18 @@
<!-- Generic one2many -->
<t t-if="field.relation != 'ir.attachment'">
<t t-call="website_form.field">
<select class="form-control s_website_form_input" t-att-name="field.name" t-att-required="field.required || field.modelRequired || None" t-att-id="field.id">
<select class="form-control s_website_form_input" t-att-name="field.name" t-att-required="field.required || field.modelRequired || None" t-att-id="field.id" style="display: none">
<t t-foreach="field.records" t-as="record">
<option t-att-value="record.id" t-att-selected="record.selected">
<t t-esc="record.display_name"/>
</option>
</t>
</select>
<div id="editable_select" class="form-control s_website_form_input">
<t t-foreach="field.records" t-as="record">
<div t-esc="record.display_name" t-att-id="record.id" class="s_website_form_select_item"/>
</t>
</div>
</t>
</t>
</t>
-1
View File
@@ -3,7 +3,6 @@
<data>
<template id="assets_editor" name="Website Form Editor Assets Editor" inherit_id="website.assets_editor">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/website_form/static/src/scss/wysiwyg_snippets.scss"/>
<script type="text/javascript" src="/website_form/static/src/snippets/s_website_form/options.js"/>
<script type="text/javascript" src="/website_form/static/src/js/website_form_editor_registry.js"/>
</xpath>