[IMP] website_form, *: allow form default input value

* = web_editor

Add support for default values on website form

Compatible fields are: textarea, text, number, date, datetime, unique
checkox, multiple checkboxes, radio buttons, select.

Part of: https://github.com/odoo/odoo/pull/47949
task-2011583
This commit is contained in:
fja-odoo
2021-02-26 14:40:49 +00:00
committed by qsm-odoo
parent a51a8af931
commit 9ea276ae2f
10 changed files with 240 additions and 41 deletions
@@ -1733,6 +1733,7 @@ const ListUserValueWidget = UserValueWidget.extend({
'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',
'click we-button.o_we_checkbox_wrapper': '_onAddItemCheckboxClick',
'input table input': '_onListItemInput',
},
@@ -1746,6 +1747,10 @@ const ListUserValueWidget = UserValueWidget.extend({
} else {
this.isCustom = true;
}
if (this.el.dataset.defaults || this.el.dataset.hasDefault) {
this.hasDefault = this.el.dataset.hasDefault || 'unique';
this.selected = this.el.dataset.defaults ? JSON.parse(this.el.dataset.defaults) : [];
}
this.listTable = document.createElement('table');
const tableWrapper = document.createElement('div');
tableWrapper.classList.add('o_we_table_wrapper');
@@ -1839,6 +1844,21 @@ const ListUserValueWidget = UserValueWidget.extend({
trEl.appendChild(draggableTdEl);
inputTdEl.appendChild(inputEl);
trEl.appendChild(inputTdEl);
if (this.hasDefault) {
const checkboxEl = document.createElement('we-button');
checkboxEl.classList.add('o_we_user_value_widget', 'o_we_checkbox_wrapper');
if (this.selected.includes(id)) {
checkboxEl.classList.add('active');
}
const div = document.createElement('div');
const checkbox = document.createElement('we-checkbox');
div.appendChild(checkbox);
checkboxEl.appendChild(div);
checkboxEl.appendChild(checkbox);
const checkboxTdEl = document.createElement('td');
checkboxTdEl.appendChild(checkboxEl);
trEl.appendChild(checkboxTdEl);
}
buttonTdEl.appendChild(buttonEl);
trEl.appendChild(buttonTdEl);
this.listTable.appendChild(trEl);
@@ -1873,6 +1893,18 @@ const ListUserValueWidget = UserValueWidget.extend({
if (this.isCustom) {
this.records = values.map(v => ({id: v, display_name: v}));
}
if (this.hasDefault) {
const checkboxes = [...this.listTable.querySelectorAll('we-button.o_we_checkbox_wrapper.active')];
this.selected = checkboxes.map(el => {
const input = el.parentElement.previousSibling.firstChild;
const id = input.name || input.value;
const idInt = parseInt(id);
return isNaN(idInt) ? id : idInt;
});
this.records.forEach(r => {
r.selected = this.selected.includes(r.id);
});
}
this._value = JSON.stringify(values);
this.notifyValueChange(true);
if (!this.isCustom) {
@@ -1931,6 +1963,18 @@ const ListUserValueWidget = UserValueWidget.extend({
_onAddItemSelectClick(ev) {
ev.currentTarget.querySelector('we-toggler').classList.toggle('active');
},
/**
* @private
* @param {Event} ev
*/
_onAddItemCheckboxClick: function (ev) {
const isActive = ev.currentTarget.classList.contains('active');
if (this.hasDefault === 'unique') {
this.listTable.querySelectorAll('we-button.o_we_checkbox_wrapper.active').forEach(el => el.classList.remove('active'));
}
ev.currentTarget.classList.toggle('active', !isActive);
this._notifyCurrentState();
},
/**
* @private
*/
@@ -1207,6 +1207,10 @@ body.editor_enable.editor_has_snippets {
&:first-child, &:last-child {
width: 1px;
}
we-button.o_we_checkbox_wrapper {
margin: 0 0 0 0.3em;
}
}
}
}
@@ -81,6 +81,8 @@ var TranslatorMenu = EditorMenu.extend({
'[data-oe-model][data-oe-id][data-oe-field], ' +
'[placeholder*="data-oe-translation-id="], ' +
'[title*="data-oe-translation-id="], ' +
'[value*="data-oe-translation-id="], ' +
'textarea:contains(data-oe-translation-id), ' +
'[alt*="data-oe-translation-id="]';
var $edit = $wrapwrap.find(selector);
$edit.filter(':has(' + selector + ')').attr('data-oe-readonly', true);
@@ -60,7 +60,11 @@ var AttributeTranslateDialog = Dialog.extend({
$input.on('change keyup', function () {
var value = $input.val();
$node.html(value).trigger('change', node);
$node.data('$node').attr($node.data('attribute'), value).trigger('translate');
if ($node.data('attribute')) {
$node.data('$node').attr($node.data('attribute'), value).trigger('translate');
} else {
$node.data('$node').text(value).trigger('translate');
}
$node.trigger('change');
});
$group.append($label).append($input);
@@ -100,36 +104,60 @@ var WysiwygTranslate = WysiwygMultizone.extend({
return promise.then(function () {
self._relocateEditorBar();
var attrs = ['placeholder', 'title', 'alt'];
var attrs = ['placeholder', 'title', 'alt', 'value'];
const $editable = self._getEditableArea();
const translationRegex = /<span [^>]*data-oe-translation-id="([0-9]+)"[^>]*>(.*)<\/span>/;
let $edited = $();
_.each(attrs, function (attr) {
self._getEditableArea().filter('[' + attr + '*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () {
const attrEdit = $editable.filter('[' + attr + '*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img');
attrEdit.each(function () {
var $node = $(this);
var translation = $node.data('translation') || {};
var trans = $node.attr(attr);
var match = trans.match(/<span [^>]*data-oe-translation-id="([0-9]+)"[^>]*>(.*)<\/span>/);
var match = trans.match(translationRegex);
var $trans = $(trans).addClass('d-none o_editable o_editable_translatable_attribute').appendTo('body');
$trans.data('$node', $node).data('attribute', attr);
translation[attr] = $trans[0];
$node.attr(attr, match[2]);
var select2 = $node.data('select2');
if (select2) {
select2.blur();
$node.on('translate', function () {
select2.blur();
});
$node = select2.container.find('input');
}
$node.addClass('o_translatable_attribute').data('translation', translation);
});
$edited = $edited.add(attrEdit);
});
const textEdit = $editable.filter('textarea:contains(data-oe-translation-id)');
textEdit.each(function () {
var $node = $(this);
var translation = $node.data('translation') || {};
var trans = $node.text();
var match = trans.match(translationRegex);
var $trans = $(trans).addClass('d-none o_editable o_editable_translatable_text').appendTo('body');
$trans.data('$node', $node);
translation['textContent'] = $trans[0];
$node.text(match[2]);
$node.addClass('o_translatable_text').data('translation', translation);
});
$edited = $edited.add(textEdit);
$edited.each(function () {
var $node = $(this);
var select2 = $node.data('select2');
if (select2) {
select2.blur();
$node.on('translate', function () {
select2.blur();
});
$node = select2.container.find('input');
}
});
self.translations = [];
self.$editables_attr = self._getEditableArea().filter('.o_translatable_attribute');
self.$editables_attribute = $('.o_editable_translatable_attribute');
self.$translations = self._getEditableArea().filter('.o_translatable_attribute, .o_translatable_text');
self.$editables = $('.o_editable_translatable_attribute, .o_editable_translatable_text');
self.$editables_attribute.on('change', function () {
self.$editables.on('change', function () {
self.trigger_up('rte_change', {target: this});
});
@@ -153,7 +181,7 @@ var WysiwygTranslate = WysiwygMultizone.extend({
* @returns {Boolean}
*/
isDirty: function () {
return this._super() || this.$editables_attribute.hasClass('o_dirty');
return this._super() || this.$editables.hasClass('o_dirty');
},
//--------------------------------------------------------------------------
@@ -168,7 +196,7 @@ var WysiwygTranslate = WysiwygMultizone.extend({
*/
_getEditableArea: function () {
var $editables = this._super();
return $editables.add(this.$editables_attribute);
return $editables.add(this.$editables);
},
/**
* Return an object describing the linked record.
@@ -262,9 +290,9 @@ var WysiwygTranslate = WysiwygMultizone.extend({
ev.stopPropagation();
});
// attributes
// attributes and textarea text
this.$editables_attr.each(function () {
this.$translations.each(function () {
var $node = $(this);
var translation = $node.data('translation');
_.each(translation, function (node, attr) {
@@ -274,7 +302,7 @@ var WysiwygTranslate = WysiwygMultizone.extend({
});
});
this.$editables_attr.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
this.$translations.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
if (ev.ctrlKey) {
return;
}
@@ -10,6 +10,29 @@ odoo.define('website_form.s_website_form', function (require) {
var _t = core._t;
var qweb = core.qweb;
publicWidget.registry.EditModeWebsiteForm = publicWidget.Widget.extend({
selector: '.s_website_form form, form.s_website_form', // !compatibility
disabledInEditableMode: false,
/**
* @override
*/
start: function () {
if (this.editableMode) {
// We do not initialize the datetime picker in edit mode but want the dates to be formated
const dateTimeFormat = time.getLangDatetimeFormat();
const dateFormat = time.getLangDateFormat();
this.$target[0].querySelectorAll('.s_website_form_input.datetimepicker-input').forEach(el => {
const value = el.getAttribute('value');
if (value) {
const format = el.closest('.s_website_form_field').dataset.type === 'date' ? dateFormat : dateTimeFormat;
el.value = moment.unix(value).format(format);
}
});
}
return this._super(...arguments);
},
});
publicWidget.registry.s_website_form = publicWidget.Widget.extend({
selector: '.s_website_form form, form.s_website_form', // !compatibility
xmlDependencies: ['/website_form/static/src/xml/website_form.xml'],
@@ -48,15 +71,21 @@ odoo.define('website_form.s_website_form', function (require) {
previous: 'fa fa-chevron-left',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
},
},
locale: moment.locale(),
format: time.getLangDatetimeFormat(),
extraFormats: ['X'],
};
this.$target.find('.s_website_form_datetime, .o_website_form_datetime').datetimepicker(datepickers_options); // !compatibility
const $datetimes = this.$target.find('.s_website_form_datetime, .o_website_form_datetime'); // !compatibility
$datetimes.datetimepicker(datepickers_options);
// Adapt options to date-only pickers
datepickers_options.format = time.getLangDateFormat();
this.$target.find('.s_website_form_date, .o_website_form_date').datetimepicker(datepickers_options); // !compatibility
const $dates = this.$target.find('.s_website_form_date, .o_website_form_date'); // !compatibility
$dates.datetimepicker(datepickers_options);
this.$allDates = $datetimes.add($dates);
this.$allDates.addClass('s_website_form_datepicker_initialized');
// Display form values from tag having data-for attribute
// It's necessary to handle field values generated on server-side
@@ -85,6 +114,21 @@ odoo.define('website_form.s_website_form', function (require) {
// Empty imputs
this.$target[0].reset();
// Apply default values
const dateTimeFormat = time.getLangDatetimeFormat();
const dateFormat = time.getLangDateFormat();
this.$target[0].querySelectorAll('input[type="text"], input[type="email"], input[type="number"]').forEach(el => {
let value = el.getAttribute('value');
if (value) {
if (el.classList.contains('datetimepicker-input')) {
const format = el.closest('.s_website_form_field').dataset.type === 'date' ? dateFormat : dateTimeFormat;
value = moment.unix(value).format(format);
}
el.value = value;
}
});
this.$target[0].querySelectorAll('textarea').forEach(el => el.value = el.textContent);
// Remove saving of the error colors
this.$target.find('.o_has_error').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
@@ -94,6 +138,9 @@ odoo.define('website_form.s_website_form', function (require) {
// Remove the success message and display the form
this.$target.removeClass('d-none');
this.$target.parent().find('.s_website_form_end_message').addClass('d-none');
// Reinitialize dates
this.$allDates.removeClass('s_website_form_datepicker_initialized');
},
send: async function (e) {
@@ -12,6 +12,10 @@
// Display the editable select as a single big field
#editable_select.form-control {
height: auto;
.s_website_form_select_item.selected {
font-weight: bold;
}
}
}
@@ -55,3 +59,13 @@
}
}
}
body:not(.editor_enable) .s_website_form[data-vcss="001"] {
.s_website_form_date, .s_website_form_datetime {
&:not(.s_website_form_datepicker_initialized) {
[value] {
color: transparent;
}
}
}
}
@@ -259,6 +259,14 @@ const FieldEditor = FormEditor.extend({
const textarea = this.$target[0].querySelector('textarea');
const input = this.$target[0].querySelector('input[type="text"], input[type="email"], input[type="number"], input[type="tel"], input[type="url"], textarea');
field.placeholder = input && input.placeholder;
if (input) {
// textarea value has no attribute, date/datetime timestamp property is formated
field.value = input.getAttribute('value') || input.value;
} else if (field.type === 'boolean') {
field.value = !!this.$target[0].querySelector('input[type="checkbox"][checked]');
}
// property value is needed for date/datetime (formated date).
field.propertyValue = input && input.value;
field.rows = textarea && textarea.rows;
field.required = classList.contains('s_website_form_required');
field.modelRequired = classList.contains('s_website_form_model_required');
@@ -836,6 +844,19 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
field.type = value;
await this._replaceField(field);
},
/**
* Select the textarea default value
*/
selectTextareaValue: function (previewMode, value, params) {
this.$target[0].textContent = value;
this.$target[0].value = value;
},
/**
* Select the date as value property and convert it to the right format
*/
selectValueProperty: function (previewMode, value, params) {
this.$target[0].value = value ? moment.unix(value).format(params.format) : '';
},
/**
* Select the display of the multicheckbox field (vertical & horizontal)
*/
@@ -891,6 +912,10 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
return this._getLabelPosition();
case 'selectType':
return this._getFieldType();
case 'selectTextareaValue':
return this.$target[0].textContent;
case 'selectValueProperty':
return this.$target[0].getAttribute('value') || '';
case 'multiCheckboxDisplay': {
const target = this._getMultipleInputs();
return target ? target.dataset.display : '';
@@ -950,6 +975,13 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
list.dataset.addItemTitle = _.str.sprintf(_t("Add new %s"), optionText);
list.dataset.renderListItems = '';
list.dataset.hasDefault = ['one2many', 'many2many'].includes(type) ? 'multiple' : 'unique';
const defaults = [...this.$target[0].querySelectorAll('[checked], [selected]')].map(el => {
const idInt = parseInt(el.value);
return isNaN(idInt) ? el.value : idInt;
});
list.dataset.defaults = JSON.stringify(defaults);
if (!this._isFieldCustom()) {
await this._fetchFieldRecords(field);
list.dataset.availableRecords = JSON.stringify(field.records);
@@ -965,11 +997,16 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
*/
_replaceField: async function (field) {
await this._fetchFieldRecords(field);
const activeField = this._getActiveField();
if (activeField.type !== field.type) {
field.value = '';
}
const htmlField = this._renderField(field);
[...this.$target[0].childNodes].forEach(node => node.remove());
[...htmlField.childNodes].forEach(node => this.$target[0].appendChild(node));
[...htmlField.attributes].forEach(el => this.$target[0].removeAttribute(el.nodeName));
[...htmlField.attributes].forEach(el => this.$target[0].setAttribute(el.nodeName, el.nodeValue));
this.$target[0].querySelectorAll('input.datetimepicker-input').forEach(el => el.value = field.propertyValue);
},
/**
* @private
@@ -985,9 +1022,11 @@ options.registry.WebsiteFieldEditor = FieldEditor.extend({
}
return options.map(opt => {
const id = parseInt(opt.value);
const name = select ? opt : opt.nextElementSibling;
return {
id: isNaN(id) ? opt.value : id,
display_name: select ? opt.textContent : opt.value
display_name: name.textContent.trim(),
selected: select ? opt.selected : opt.checked,
};
});
},
@@ -125,6 +125,7 @@
t-att-placeholder="field.placeholder"
t-att-id="field.id"
t-att-rows="field.rows || 3"
t-esc="field.value"
/>
</t>
</t>
@@ -147,6 +148,7 @@
t-att-name="field.name"
step="1"
t-att-required="field.required || field.modelRequired || None"
t-att-value="field.value"
t-att-placeholder="field.placeholder"
t-att-id="field.id"
/>
@@ -162,6 +164,7 @@
t-att-name="field.name"
step="any"
t-att-required="field.required || field.modelRequired || None"
t-att-value="field.value"
t-att-placeholder="field.placeholder"
t-att-id="field.id"
/>
@@ -179,6 +182,7 @@
t-attf-data-target="##{datepickerID}"
t-att-name="field.name"
t-att-required="field.required || field.modelRequired || None"
t-att-value="field.value"
t-att-placeholder="field.placeholder"
t-att-id="field.id"
/>
@@ -200,6 +204,7 @@
t-attf-data-target="##{datetimepickerID}"
t-att-name="field.name"
t-att-required="field.required || field.modelRequired || None"
t-att-value="field.value"
t-att-placeholder="field.placeholder"
t-att-id="field.id"
/>
@@ -214,14 +219,17 @@
<t t-name="website_form.field_boolean">
<t t-set="field.isCheck" t-value="true"/>
<t t-call="website_form.field">
<input
type="checkbox"
value="Yes"
class="s_website_form_input"
t-att-name="field.name"
t-att-required="field.required || field.modelRequired || None"
t-att-id="field.id"
/>
<div class="form-check">
<input
type="checkbox"
value="Yes"
class="s_website_form_input"
t-att-name="field.name"
t-att-checked="field.value and 'checked' or None"
t-att-required="field.required || field.modelRequired || None"
t-att-id="field.id"
/>
</div>
</t>
</t>
@@ -256,6 +264,7 @@
class="s_website_form_input form-check-input"
t-att-id="recordId"
t-att-name="field.name"
t-att-checked="record.selected and 'checked' or None"
t-att-value="record.id"
t-att-required="field.required || field.modelRequired || None"
/>
@@ -277,14 +286,12 @@
<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" 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>
<option t-esc="record.display_name" t-att-value="record.id" t-att-selected="record.selected and 'selected' or None"/>
</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"/>
<div t-esc="record.display_name" t-att-id="record.id" t-attf-class="s_website_form_select_item #{record.selected and 'selected' or ''}"/>
</t>
</div>
</t>
@@ -331,6 +338,7 @@
class="s_website_form_input form-check-input"
t-att-id="recordId"
t-att-name="field.name"
t-att-checked="record.selected and 'checked' or None"
t-att-value="record.id"
t-att-required="field.required || field.modelRequired || None"
/>
@@ -118,6 +118,15 @@
data-select-label-position="right"
data-img="/website/static/src/img/snippets_options/pos_right.svg"/>
</we-button-group>
<we-input string="Default Value" data-select-textarea-value="" data-apply-to="textarea"/>
<we-checkbox string="Default Value" data-select-attribute="checked" data-attribute-name="checked"
data-apply-to=".col-sm > * > input[type='checkbox']" data-no-preview="true"/>
<we-input string="Default Value" data-select-attribute="" data-attribute-name="value" data-select-property=""
data-property-name="value" data-apply-to="input[type='text']:not(.datetimepicker-input), input[type='email'], input[type='number'], input[type='tel'], input[type='url']"/>
<we-datetimepicker string="Default Value" data-select-attribute="" data-attribute-name="value" data-select-value-property=""
data-apply-to=".s_website_form_datetime input"/>
<we-datepicker string="Default Value" data-select-attribute="" data-attribute-name="value" data-select-value-property=""
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"
+9 -5
View File
@@ -135,6 +135,7 @@ def encode(s):
assert isinstance(s, str)
return s
# which elements are translated inline
TRANSLATED_ELEMENTS = {
'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'del', 'dfn', 'em',
@@ -143,14 +144,16 @@ TRANSLATED_ELEMENTS = {
'sup', 'time', 'u', 'var', 'wbr', 'text',
}
# which attributes must be translated
TRANSLATED_ATTRS = {
# Which attributes must be translated. This is a dict, where the value indicates
# a condition for a node to have the attribute translatable.
TRANSLATED_ATTRS = dict.fromkeys({
'string', 'help', 'sum', 'avg', 'confirm', 'placeholder', 'alt', 'title', 'aria-label',
'aria-keyshortcuts', 'aria-placeholder', 'aria-roledescription', 'aria-valuetext',
'value_label',
}
}, lambda e: True)
TRANSLATED_ATTRS['value'] = lambda e: (e.tag == 'input' and e.attrib.get('type', 'text') == 'text') and 'datetimepicker-input' not in e.attrib['class'].split(' ')
TRANSLATED_ATTRS = TRANSLATED_ATTRS | {'t-attf-' + attr for attr in TRANSLATED_ATTRS}
TRANSLATED_ATTRS.update({f't-attf-{attr}': cond for attr, cond in TRANSLATED_ATTRS.items()})
avoid_pattern = re.compile(r"\s*<!DOCTYPE", re.IGNORECASE | re.MULTILINE | re.UNICODE)
node_pattern = re.compile(r"<[^>]*>(.*)</[^<]*>", re.DOTALL | re.MULTILINE | re.UNICODE)
@@ -268,7 +271,8 @@ def translate_xml_node(node, callback, parse, serialize):
# translate the required attributes
for name, value in result.attrib.items():
if name in TRANSLATED_ATTRS:
condition = TRANSLATED_ATTRS.get(name)
if condition and condition(result):
result.set(name, translate_text(value) or value)
# add the untranslated tail to result