[FIX] web,web_editor,mail : Fix ModelFieldSelector and DynamicPlaceholder

Add missing feature of the ModelFieldSelector popover :
* keyboard navigation
* additional debug value
* add default value page ( for dynamic placeholder)
* add validate callback

Migrate dynamic placeholder to be compatible with new OWL field.

task-2978722

X-original-commit: 929108e29168058fad5da85e0ff9c66d2c5b2525
Part-of: odoo/odoo#102603
This commit is contained in:
Sébastien Geelen (sge)
2022-10-07 14:37:20 +02:00
parent 5a09449ddc
commit e483ae5fb4
16 changed files with 402 additions and 63 deletions
+1 -1
View File
@@ -54,7 +54,7 @@
<page string="Content" name="content">
<field name="can_write" invisible="1"/>
<field name="body_html" widget="html" class="oe-bordered-editor"
options="{'style-inline': true, 'codeview': true }"
options="{'style-inline': true, 'codeview': true, 'dynamic_placeholder': true}"
attrs="{'readonly': [('can_write', '=', False), ('id', '!=', False)]}"/>
<field name="attachment_ids" widget="many2many_binary"/>
</page>
@@ -13,6 +13,7 @@ import { HtmlField } from "@web_editor/js/backend/html_field";
import { getWysiwygClass } from 'web_editor.loader';
import { device } from 'web.config';
import { MassMailingMobilePreviewDialog } from "./mass_mailing_mobile_preview";
import { getRangePosition } from '@web_editor/js/editor/odoo-editor/src/utils/utils';
const {
useEffect,
@@ -79,6 +80,29 @@ export class MassMailingHtmlField extends HtmlField {
};
}
/**
* @param {HTMLElement} popover
* @param {Object} position
* @override
*/
positionDynamicPlaceholder(popover, position) {
const editable = this.wysiwyg.$iframe ? this.wysiwyg.$iframe[0] : this.wysiwyg.$editable[0];
const relativeParentPosition = editable.getBoundingClientRect();
let topPosition = relativeParentPosition.top;
let leftPosition = relativeParentPosition.left;
const rangePosition = getRangePosition(popover, this.wysiwyg.options.document);
topPosition += rangePosition.top;
// Offset the popover to ensure the arrow is pointing at
// the precise range location.
leftPosition += rangePosition.left - 14;
// Apply the position back to the element.
popover.style.top = topPosition + 'px';
popover.style.left = leftPosition + 'px';
}
async commitChanges() {
if (this.props.readonly || !this.isRendered) {
return super.commitChanges();
@@ -1,28 +1,11 @@
/** @odoo-module **/
import { usePopover } from "@web/core/popover/popover_hook";
import { useModelField } from "./model_field_hook";
import { useUniquePopover } from "./unique_popover_hook";
import { ModelFieldSelectorPopover } from "./model_field_selector_popover";
const { Component, onWillStart, onWillUpdateProps } = owl;
function useUniquePopover() {
const popover = usePopover();
let remove = null;
return Object.assign(Object.create(popover), {
add(target, component, props, options) {
if (remove) {
remove();
}
remove = popover.add(target, component, props, options);
return () => {
remove();
remove = null;
};
},
});
}
export class ModelFieldSelector extends Component {
setup() {
this.popover = useUniquePopover();
@@ -12,12 +12,17 @@ export class ModelFieldSelectorPopover extends Component {
setup() {
this.chain = Array.from(this.props.chain);
this.modelField = useModelField();
this.unfilteredFields = {};
this.fields = {};
this.fieldKeys = [];
this.currentActiveFieldId = 0;
this.searchValue = "";
this.defaultValue = "";
this.isDefaultValueVisible = false;
this.fullFieldName = this.fieldNameChain.join(".");
if (!this.env.isSmall) {
useAutofocus();
useAutofocus({ refName: 'autofocusDefaultValue', selectAll: true });
}
onWillStart(async () => {
@@ -25,6 +30,10 @@ export class ModelFieldSelectorPopover extends Component {
});
}
get currentActiveField() {
return this.fieldKeys[this.currentActiveFieldId];
}
get currentNode() {
return this.chain[this.chain.length - 1];
}
@@ -38,7 +47,15 @@ export class ModelFieldSelectorPopover extends Component {
}
async loadFields() {
this.fields = await this.modelField.loadModelFields(this.currentNode.resModel);
this.unfilteredFields = await this.modelField.loadModelFields(this.currentNode.resModel);
this.fields = {...this.unfilteredFields};
this.fieldKeys = this.sortedKeys(this.fields);
for (let key of this.fieldKeys) {
const field = this.fields[key];
if (!field.searchable || !this.props.filter(field)) {
delete this.fields[key];
}
}
this.fieldKeys = this.sortedKeys(this.fields);
}
sortedKeys(obj) {
@@ -53,6 +70,47 @@ export class ModelFieldSelectorPopover extends Component {
this.render();
}
async onInputKeydown(ev) {
switch (ev.key) {
case "ArrowUp":
ev.preventDefault();
ev.stopPropagation();
if (this.currentActiveFieldId> 0) {
this.currentActiveFieldId--;
await this.render();
}
break;
case "ArrowDown":
ev.preventDefault();
ev.stopPropagation();
if (this.currentActiveFieldId < this.fieldKeys.length-1) {
this.currentActiveFieldId++;
await this.render();
}
break;
case "ArrowLeft":
ev.preventDefault();
ev.stopPropagation();
this.onPreviousBtnClick();
break;
case "Escape":
ev.preventDefault();
ev.stopPropagation();
this.props.close();
break;
case "Enter":
case "ArrowRight":
ev.preventDefault();
ev.stopPropagation();
if (this.isDefaultValueVisible) {
this.selectDefaultValue(true);
} else {
const field = { ...this.fields[this.currentActiveField], name: this.currentActiveField }
this.onFieldSelected(field);
}
break;
}
}
onSearch(ev) {
this.searchValue = ev.target.value;
let fieldKeys = this.sortedKeys(this.fields);
@@ -62,6 +120,10 @@ export class ModelFieldSelectorPopover extends Component {
this.fieldKeys = fieldKeys;
this.render();
}
onDefaultValue(ev) {
this.defaultValue = ev.target.value;
this.render();
}
onPreviousBtnClick() {
this.searchValue = "";
if (this.currentNode.field === null) {
@@ -72,16 +134,31 @@ export class ModelFieldSelectorPopover extends Component {
}
onFieldSelected(field) {
this.searchValue = "";
this.currentActiveFieldId = 0;
this.currentNode.field = field;
if (field.relation && this.props.followRelations) {
this.chain.push({
resModel: field.relation,
field: null,
});
this.update();
} else if(this.props.needDefaultValue) {
this.isDefaultValueVisible = true;
this.render();
this.update();
} else {
this.update();
this.props.close();
this.props.validate(this.fieldNameChain, this.defaultValue);
}
}
selectDefaultValue (acceptDefaultValue) {
if (!acceptDefaultValue) {
this.defaultValue = "";
}
this.props.close();
this.update();
this.props.validate(this.fieldNameChain, this.defaultValue);
}
async onFieldNameChange(ev) {
this.fullFieldName = ev.target.value.replace(/\s+/g, "");
@@ -98,19 +175,24 @@ export class ModelFieldSelectorPopover extends Component {
}
}
Object.assign(ModelFieldSelectorPopover, {
template: "web.ModelFieldSelectorPopover",
props: {
chain: Array,
update: Function,
showSearchInput: Boolean,
isDebugMode: Boolean,
loadChain: Function,
filter: Function,
close: Function,
followRelations: { type: Boolean, optional: true },
},
defaultProps: {
followRelations: true,
},
});
ModelFieldSelectorPopover.defaultProps = {
validate: () => {},
needDefaultValue: false,
isDebugMode: false,
followRelations: true,
};
ModelFieldSelectorPopover.props = {
chain: Array,
update: Function,
showSearchInput: Boolean,
isDebugMode: { type: Boolean, optional: true},
loadChain: Function,
filter: Function,
close: Function,
followRelations: { type: Boolean, optional: true },
needDefaultValue: { type: Boolean, optional: true},
validate: { type: Function, optional: true},
};
ModelFieldSelectorPopover.template = "web.ModelFieldSelectorPopover";
@@ -2,7 +2,8 @@
<templates xml:space="preserve">
<t t-name="web.ModelFieldSelectorPopover" owl="1">
<div class="o_field_selector_popover" tabindex="0">
<div class="o_field_selector_popover"
t-on-keydown="onInputKeydown" tabindex="0">
<div class="o_field_selector_popover_header text-center">
<t t-if="chain.length > 1">
<i
@@ -13,7 +14,12 @@
t-on-click="onPreviousBtnClick"
/>
</t>
<div class="o_field_selector_title" t-esc="currentFieldName" />
<t t-if="isDefaultValueVisible">
<div class="o_field_selector_title">Default value</div>
</t>
<t t-else="">
<div class="o_field_selector_title" t-out="currentFieldName" />
</t>
<i
class="fa fa-times o_field_selector_popover_option o_field_selector_close"
title="Close"
@@ -21,20 +27,37 @@
aria-label="Close"
t-on-click="props.close"
/>
<t t-if="props.showSearchInput">
<t t-if="props.showSearchInput &amp;&amp; !isDefaultValueVisible">
<div class="o_field_selector_search mt-2">
<input type="text" placeholder='Search...' class="o_input" t-att-value="searchValue" t-on-input="onSearch" t-ref="autofocus" />
<input type="text"
placeholder='Search...'
class="o_input"
t-att-value="searchValue"
t-on-input="onSearch"
t-ref="autofocus" />
</div>
</t>
<t t-if="isDefaultValueVisible">
<div class="o_field_selector_default_value_input mt-2">
<input type="text"
placeholder='Type a default text or press ENTER'
class="o_input"
t-att-value="defaultValue"
t-on-input="onDefaultValue"
t-ref="autofocusDefaultValue"/>
</div>
</t>
</div>
<div class="o_field_selector_popover_body">
<ul class="o_field_selector_page">
<ul class="o_field_selector_page" t-if="!isDefaultValueVisible">
<t t-foreach="fieldKeys" t-as="fieldKey" t-key="fieldKey">
<t t-set="field" t-value="fields[fieldKey]" />
<t t-if="field.searchable and props.filter(field)">
<li class="o_field_selector_item" t-on-click="() => this.onFieldSelected({ ...field, name: fieldKey })">
<t t-esc="field.string" />
<li
t-on-click="() => this.onFieldSelected({ ...field, name: fieldKey })"
t-attf-class="o_field_selector_item #{fieldKey === currentActiveField and ' active' or ''}">
<t t-out="field.string" />
<div t-if="props.isDebugMode" class="text-muted o_field_selector_item_title"><t t-out="fieldKey"/> (<t t-out="field.type"/>)</div>
<t t-if="field.relation and props.followRelations">
<i
class="fa fa-chevron-right o_field_selector_relation_icon"
@@ -47,6 +70,16 @@
</t>
</t>
</ul>
<ul class="o_field_selector_page" t-if="isDefaultValueVisible">
<li class="o_field_selector_item" t-on-click="() => this.selectDefaultValue(defaultValue !== '')">
<t t-if="defaultValue === ''">
<div class="text-muted o_field_selector_item_title">Default text is used when no values are set</div>
</t>
<t t-else="">
<div class="text-muted o_field_selector_item_title">As a default text when no value are set</div>
</t>
</li>
</ul>
</div>
<t t-if="props.isDebugMode">
<div class="o_field_selector_popover_footer">
@@ -0,0 +1,20 @@
/** @odoo-module **/
import { usePopover } from "@web/core/popover/popover_hook";
export function useUniquePopover() {
const popover = usePopover();
let remove = null;
return Object.assign(Object.create(popover), {
add(target, component, props, options) {
if (remove) {
remove();
}
remove = popover.add(target, component, props, options);
return () => {
remove();
remove = null;
};
},
});
}
@@ -7,7 +7,7 @@ const { Component } = owl;
export class Popover extends Component {
setup() {
usePosition(this.props.target, {
onPositioned: this.onPositioned,
onPositioned: this.props.onPositioned || this.onPositioned,
position: this.props.position,
});
}
@@ -72,6 +72,10 @@ Popover.props = {
validate: (p) => ["top", "bottom", "left", "right"].includes(p),
optional: true,
},
onPositioned: {
type: Function,
optional: true,
},
target: HTMLElement,
slots: {
type: Object,
@@ -25,6 +25,7 @@ class PopoverController extends Component {
target: this.target,
position: this.props.position,
popoverClass: this.props.popoverClass,
onPositioned: this.props.onPositioned,
};
}
@@ -26,6 +26,7 @@ export const popoverService = {
* @param {function(): void} [options.onClose]
* @param {string} [options.popoverClass]
* @param {string} [options.position]
* @param {function} [options.onPositioned]
* @returns {function(): void}
*/
function add(target, Component, props, options = {}) {
@@ -39,6 +40,7 @@ export const popoverService = {
close: closeFn,
onClose: options.onClose,
position: options.position,
onPositioned: options.onPositioned,
popoverClass: options.popoverClass,
closeOnClickAway: options.closeOnClickAway,
preventClose: options.preventClose,
@@ -7,12 +7,57 @@ import { formatChar } from "../formatters";
import { useInputField } from "../input_field_hook";
import { standardFieldProps } from "../standard_field_props";
import { TranslationButton } from "../translation_button";
import { useDynamicPlaceholder } from "../dynamicplaceholder_hook";
const { Component } = owl;
const { Component, onMounted, onWillUnmount, useRef } = owl;
export class CharField extends Component {
setup() {
if (this.props.dynamicPlaceholder) {
this.dynamicPlaceholder = useDynamicPlaceholder();
}
this.input = useRef("input");
useInputField({ getValue: () => this.props.value || "", parse: (v) => this.parse(v) });
onMounted(this.onMounted);
onWillUnmount(this.onWillUnmount);
}
async onKeydownListener(ev) {
if (ev.key === this.dynamicPlaceholder.TRIGGER_KEY && ev.target === this.input.el) {
const baseModel = this.props.record.data.mailing_model_real;
if (baseModel) {
await this.dynamicPlaceholder.open(
this.input.el,
baseModel,
{
validateCallback: this.onDynamicPlaceholderValidate.bind(this),
closeCallback: this.onDynamicPlaceholderClose.bind(this)
}
);
}
}
}
onMounted() {
if (this.props.dynamicPlaceholder) {
this.keydownListenerCallback = this.onKeydownListener.bind(this);
document.addEventListener('keydown', this.keydownListenerCallback);
}
}
onWillUnmount() {
if (this.props.dynamicPlaceholder) {
document.removeEventListener('keydown', this.keydownListenerCallback);
}
}
onDynamicPlaceholderValidate(chain, defaultValue) {
if (chain) {
const triggerKeyReplaceRegex = new RegExp(`${this.dynamicPlaceholder.TRIGGER_KEY}$`);
let dynamicPlaceholder = "{{object." + chain.join('.');
dynamicPlaceholder += defaultValue && defaultValue !== '' ? ` or '''${defaultValue}'''}}` : '}}';
this.props.update(this.input.el.value.replace(triggerKeyReplaceRegex, '') + dynamicPlaceholder);
}
}
onDynamicPlaceholderClose() {
this.input.el.focus();
}
get formattedValue() {
@@ -31,11 +76,13 @@ CharField.template = "web.CharField";
CharField.components = {
TranslationButton,
};
CharField.defaultProps = { dynamicPlaceholder: false };
CharField.props = {
...standardFieldProps,
autocomplete: { type: String, optional: true },
isPassword: { type: Boolean, optional: true },
placeholder: { type: String, optional: true },
dynamicPlaceholder: { type: Boolean, optional: true},
shouldTrim: { type: Boolean, optional: true },
maxLength: { type: Number, optional: true },
isTranslatable: { type: Boolean, optional: true },
@@ -49,6 +96,7 @@ CharField.extractProps = ({ attrs, field }) => {
shouldTrim: field.trim && !archParseBoolean(attrs.password), // passwords shouldn't be trimmed
maxLength: field.size,
isTranslatable: field.translate,
dynamicPlaceholder: attrs.options.dynamic_placeholder,
autocomplete: attrs.autocomplete,
isPassword: archParseBoolean(attrs.password),
placeholder: attrs.placeholder,
@@ -0,0 +1,61 @@
/** @odoo-module **/
import { useUniquePopover } from "@web/core/model_field_selector/unique_popover_hook";
import { useModelField } from "@web/core/model_field_selector/model_field_hook";
import { ModelFieldSelectorPopover } from "@web/core/model_field_selector/model_field_selector_popover";
export function useDynamicPlaceholder() {
const popover = useUniquePopover();
const modelField = useModelField();
let dynamicPlaceholderChain = [];
function update(chain) {
dynamicPlaceholderChain = chain;
}
return {
TRIGGER_KEY: '#',
/**
* Open a Model Field Selector which can select fields to create a dynamic
* placeholder string in the Input with or without a default text value.
*
* @public
* @param {HTMLElement} element
* @param {String} baseModel
* @param {Object} options
* @param {function} options.validateCallback
* @param {function} options.closeCallback
* @param {function} [options.positionCallback]
*/
async open(
element,
baseModel,
options = {}
) {
dynamicPlaceholderChain = await modelField.loadChain(baseModel, "");
popover.add(
element,
ModelFieldSelectorPopover,
{
chain: dynamicPlaceholderChain,
update: update,
validate: options.validateCallback,
showSearchInput: true,
isDebugMode: true,
needDefaultValue: true,
loadChain: modelField.loadChain,
filter: (model) => !["one2many", "boolean", "many2many"].includes(model.type),
},
{
closeOnClickAway: true,
onClose: options.closeCallback,
onPositioned: options.positionCallback,
}
);
}
};
}
@@ -5,11 +5,15 @@ import { _lt } from "@web/core/l10n/translation";
import { useInputField } from "../input_field_hook";
import { standardFieldProps } from "../standard_field_props";
import { TranslationButton } from "../translation_button";
import { useDynamicPlaceholder } from "../dynamicplaceholder_hook";
const { Component, useEffect, useRef } = owl;
const { Component, useEffect, onMounted, onWillUnmount, useRef } = owl;
export class TextField extends Component {
setup() {
if (this.props.dynamicPlaceholder) {
this.dynamicPlaceholder = useDynamicPlaceholder();
}
this.textareaRef = useRef("textarea");
useInputField({ getValue: () => this.props.value || "", refName: "textarea" });
@@ -18,6 +22,45 @@ export class TextField extends Component {
this.resize();
}
});
onMounted(this.onMounted);
onWillUnmount(this.onWillUnmount);
}
async onKeydownListener(ev) {
if (ev.key === this.dynamicPlaceholder.TRIGGER_KEY && ev.target === this.textareaRef.el) {
const baseModel = this.props.record.data.mailing_model_real;
if (baseModel) {
await this.dynamicPlaceholder.open(
this.textareaRef.el,
baseModel,
{
validateCallback: this.onDynamicPlaceholderValidate.bind(this),
closeCallback: this.onDynamicPlaceholderClose.bind(this)
}
);
}
}
}
onMounted() {
if (this.props.dynamicPlaceholder) {
this.keydownListenerCallback = this.onKeydownListener.bind(this);
document.addEventListener('keydown', this.keydownListenerCallback);
}
}
onWillUnmount() {
if (this.props.dynamicPlaceholder) {
document.removeEventListener('keydown', this.keydownListenerCallback);
}
}
onDynamicPlaceholderValidate(chain, defaultValue) {
if (chain) {
const triggerKeyReplaceRegex = new RegExp(`${this.dynamicPlaceholder.TRIGGER_KEY}$`);
let dynamicPlaceholder = "{{object." + chain.join('.');
dynamicPlaceholder += defaultValue && defaultValue !== '' ? ` or '''${defaultValue}'''}}` : '}}';
this.props.update(this.textareaRef.el.value.replace(triggerKeyReplaceRegex, '') + dynamicPlaceholder);
}
}
onDynamicPlaceholderClose() {
this.textareaRef.el.focus();
}
get minimumHeight() {
@@ -62,10 +105,12 @@ TextField.template = "web.TextField";
TextField.components = {
TranslationButton,
};
TextField.defaultProps = {dynamicPlaceholder: false};
TextField.props = {
...standardFieldProps,
isTranslatable: { type: Boolean, optional: true },
placeholder: { type: String, optional: true },
dynamicPlaceholder: { type: Boolean, optional: true},
};
TextField.displayName = _lt("Multiline Text");
@@ -75,6 +120,7 @@ TextField.extractProps = ({ attrs, field }) => {
return {
isTranslatable: field.translate,
placeholder: attrs.placeholder,
dynamicPlaceholder: attrs.options.dynamic_placeholder,
};
};
@@ -94,14 +94,7 @@ QUnit.module("Components", (hooks) => {
`;
// Create the domain selector and its mock environment
await mountComponent(Parent, {
props: {
resModel: "partner",
value: "[]",
readonly: false,
isDebugMode: true,
},
});
await mountComponent(Parent);
// As we gave an empty domain, there should be a visible button to add
// the first domain part
@@ -125,10 +118,11 @@ QUnit.module("Components", (hooks) => {
);
// The field selector popover should contain the list of "partner"
// fields. "Bar" should be among them.
// fields. "Bar" should be among them. "Bar" result li will display the
// name of the field and some debug info.
assert.strictEqual(
document.body.querySelector(".o_field_selector_popover li").textContent,
"Bar",
"Barbar (boolean)",
"field selector popover should contain the 'Bar' field"
);
@@ -89,7 +89,7 @@ QUnit.module("Components", (hooks) => {
readonly="false"
resModel="'partner'"
fieldName="fieldName"
isDebugMode="true"
isDebugMode="false"
update="(value) => this.onUpdate(value)"
/>
`;
@@ -222,7 +222,7 @@ QUnit.module("Components", (hooks) => {
readonly: false,
fieldName: "product_id",
resModel: "partner",
isDebugMode: true,
isDebugMode: false,
},
});
@@ -8,15 +8,19 @@ import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { getWysiwygClass } from 'web_editor.loader';
import { QWebPlugin } from '@web_editor/js/backend/QWebPlugin';
import { TranslationButton } from "@web/views/fields/translation_button";
import { useDynamicPlaceholder } from "@web/views/fields/dynamicplaceholder_hook";
import { QWeb } from 'web.core';
import ajax from 'web.ajax';
import {
useBus,
useService,
} from "@web/core/utils/hooks";
import { getAdjacentPreviousSiblings, getAdjacentNextSiblings } from '@web_editor/js/editor/odoo-editor/src/utils/utils';
import {
getAdjacentPreviousSiblings,
getAdjacentNextSiblings,
getRangePosition
} from '@web_editor/js/editor/odoo-editor/src/utils/utils';
import { toInline } from 'web_editor.convertInline';
const {
markup,
Component,
@@ -66,6 +70,9 @@ export class HtmlField extends Component {
this.iframeRef = useRef("iframe");
this.codeViewButtonRef = useRef("codeViewButton");
if (this.props.dynamicPlaceholder) {
this.dynamicPlaceholder = useDynamicPlaceholder();
}
this.rpc = useService("rpc");
this.onIframeUpdated = this.env.onIframeUpdated || (() => {});
@@ -148,6 +155,7 @@ export class HtmlField extends Component {
onAttachmentChange: this._onAttachmentChange.bind(this),
onWysiwygBlur: this._onWysiwygBlur.bind(this),
...this.props.wysiwygOptions,
...dynamicPlaceholderOptions,
recordInfo: {
res_model: this.props.record.resModel,
res_id: this.props.record.resId,
@@ -235,6 +243,33 @@ export class HtmlField extends Component {
}
}
onDynamicPlaceholderValidate(chain, defaultValue) {
if (chain) {
let dynamicPlaceholder = "object." + chain.join('.');
dynamicPlaceholder += defaultValue && defaultValue !== '' ? ` or '''${defaultValue}'''` : '';
const t = document.createElement('T');
t.setAttribute('t-out', dynamicPlaceholder);
this.wysiwyg.odooEditor.execCommand('insert', t);
}
}
onDynamicPlaceholderClose() {
this.wysiwyg.focus();
}
/**
* @param {HTMLElement} popover
* @param {Object} position
*/
positionDynamicPlaceholder(popover, position) {
let topPosition = this.wysiwygRangePosition.top;
// Offset the popover to ensure the arrow is pointing at
// the precise range location.
let leftPosition = this.wysiwygRangePosition.left - 14;
// Apply the position back to the element.
popover.style.top = topPosition + 'px';
popover.style.left = leftPosition + 'px';
}
async commitChanges({ urgent } = {}) {
if (this._isDirty() || urgent) {
if (this.wysiwyg) {
@@ -386,6 +421,9 @@ export class HtmlField extends Component {
const $editable = this.wysiwyg.getEditable();
const html = this.wysiwyg.getValue();
const $odooEditor = $editable.closest('.odoo-editor-editable');
// Save correct nodes references.
const originalContents = document.createDocumentFragment();
originalContents.append(...$editable[0].childNodes);
// Remove temporarily the class so that css editing will not be converted.
$odooEditor.removeClass('odoo-editor-editable');
$editable.html(html);
@@ -393,7 +431,7 @@ export class HtmlField extends Component {
await toInline($editable, this.cssRules, this.wysiwyg.$iframe);
$odooEditor.addClass('odoo-editor-editable');
this.wysiwyg.setValue($editable.html());
$editable[0].replaceChildren(...originalContents.childNodes);
}
async _getWysiwygClass() {
return getWysiwygClass();
@@ -467,6 +505,7 @@ HtmlField.components = {
TranslationButton,
HtmlFieldWysiwygAdapterComponent,
};
HtmlField.defaultProps = {dynamicPlaceholder: false};
HtmlField.props = {
...standardFieldProps,
isTranslatable: { type: Boolean, optional: true },
@@ -474,6 +513,7 @@ HtmlField.props = {
fieldName: { type: String, optional: true },
codeview: { type: Boolean, optional: true },
isCollaborative: { type: Boolean, optional: true },
dynamicPlaceholder: { type: Boolean, optional: true, default: false },
cssReadonlyAssetId: { type: String, optional: true },
cssEditAssetId: { type: String, optional: true },
isInlineStyle: { type: Boolean, optional: true },
@@ -493,6 +533,7 @@ HtmlField.extractProps = ({ attrs, field }) => {
isCollaborative: attrs.options.collaborative,
cssReadonlyAssetId: attrs.options.cssReadonly,
dynamicPlaceholder: attrs.options.dynamic_placeholder,
cssEditAssetId: attrs.options.cssEdit,
isInlineStyle: attrs.options['style-inline'],
wrapper: attrs.options.wrapper,
@@ -977,7 +977,7 @@ const Wysiwyg = Widget.extend({
* Set cursor to the editor latest position before blur or to the last editable node, ready to type.
*/
focus: function () {
if (this.odooEditor && !this.odooEditor.historyResetLatestComputedSelection()) {
if (this.odooEditor && !this.odooEditor.historyResetLatestComputedSelection(true)) {
// If the editor don't have an history step to focus to,
// We place the cursor after the end of the editor exiting content.
const range = document.createRange();