diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 50af8d2a8d0..c1261df1f88 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -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 */ diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss index c57bbb0df68..8c6b62ac461 100644 --- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss +++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss @@ -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; + } } } } diff --git a/addons/website/static/src/js/editor/editor_menu_translate.js b/addons/website/static/src/js/editor/editor_menu_translate.js index 3016ade659c..5f3a6da898a 100644 --- a/addons/website/static/src/js/editor/editor_menu_translate.js +++ b/addons/website/static/src/js/editor/editor_menu_translate.js @@ -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); diff --git a/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js b/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js index 978ac4d98a6..8c480612925 100644 --- a/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js +++ b/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js @@ -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 = /]*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(/]*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; } diff --git a/addons/website_form/static/src/snippets/s_website_form/000.js b/addons/website_form/static/src/snippets/s_website_form/000.js index 9da1910985e..fe46bf943f0 100644 --- a/addons/website_form/static/src/snippets/s_website_form/000.js +++ b/addons/website_form/static/src/snippets/s_website_form/000.js @@ -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) { diff --git a/addons/website_form/static/src/snippets/s_website_form/001.scss b/addons/website_form/static/src/snippets/s_website_form/001.scss index 375a4bb5fe0..c4f5654053e 100644 --- a/addons/website_form/static/src/snippets/s_website_form/001.scss +++ b/addons/website_form/static/src/snippets/s_website_form/001.scss @@ -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; + } + } + } +} diff --git a/addons/website_form/static/src/snippets/s_website_form/options.js b/addons/website_form/static/src/snippets/s_website_form/options.js index 2b4c0c896dc..4384aae6bca 100644 --- a/addons/website_form/static/src/snippets/s_website_form/options.js +++ b/addons/website_form/static/src/snippets/s_website_form/options.js @@ -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, }; }); }, diff --git a/addons/website_form/static/src/xml/website_form_editor.xml b/addons/website_form/static/src/xml/website_form_editor.xml index 1f1b1c74d9b..889291c1e7f 100644 --- a/addons/website_form/static/src/xml/website_form_editor.xml +++ b/addons/website_form/static/src/xml/website_form_editor.xml @@ -125,6 +125,7 @@ t-att-placeholder="field.placeholder" t-att-id="field.id" t-att-rows="field.rows || 3" + t-esc="field.value" /> @@ -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 @@ - +
+ +
@@ -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 @@
-
+
@@ -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" /> diff --git a/addons/website_form/views/snippets/s_website_form.xml b/addons/website_form/views/snippets/s_website_form.xml index c94e8053d3d..beec88770d9 100644 --- a/addons/website_form/views/snippets/s_website_form.xml +++ b/addons/website_form/views/snippets/s_website_form.xml @@ -118,6 +118,15 @@ data-select-label-position="right" data-img="/website/static/src/img/snippets_options/pos_right.svg"/> + + + + + ]*>(.*)", 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