[IMP] web_editor: allow qweb edition
Add a plugin for the Odoo editor that includes: - make dynamic variables (eg. `<t t-out="variable"/>`) not editable (for preventing the user to shoot himself in the foot) - group and hide related logical branching (ie. t-if, t-elif and t-else) in order to see only one at once - a floating select input to switch visibility of a particular logical branching - style t-tags to make them stand out Task-27033 X-original-commit: odoo/odoo@300da82eb4 Part-of: odoo/odoo#77377
This commit is contained in:
committed by
Thibault Delavallée
parent
810f832703
commit
e52bde906f
@@ -153,6 +153,7 @@ export class OdooEditor extends EventTarget {
|
||||
isRootEditable: true,
|
||||
placeholder: false,
|
||||
defaultLinkAttributes: {},
|
||||
plugins: [],
|
||||
getContentEditableAreas: () => [],
|
||||
getPowerboxElement: () => {
|
||||
const selection = document.getSelection();
|
||||
@@ -201,6 +202,12 @@ export class OdooEditor extends EventTarget {
|
||||
// Map that from an node id to the dom node.
|
||||
this._idToNodeMap = new Map();
|
||||
|
||||
// Instanciate plugins.
|
||||
this._plugins = [];
|
||||
for (const plugin of this.options.plugins) {
|
||||
this._pluginAdd(plugin);
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// Alter the editable
|
||||
// -------------------
|
||||
@@ -230,6 +237,8 @@ export class OdooEditor extends EventTarget {
|
||||
this._historyStepsActive = true;
|
||||
this.historyReset();
|
||||
|
||||
this._pluginCall('sanitizeElement', [editable]);
|
||||
|
||||
this._createCommandBar();
|
||||
|
||||
this.toolbarTablePicker = new TablePicker({ document: this.document });
|
||||
@@ -329,6 +338,7 @@ export class OdooEditor extends EventTarget {
|
||||
this._collabSelectionsContainer.remove();
|
||||
this._resizeObserver.disconnect();
|
||||
clearInterval(this._snapshotInterval);
|
||||
this._pluginCall('destroy', []);
|
||||
}
|
||||
|
||||
sanitize() {
|
||||
@@ -347,6 +357,7 @@ export class OdooEditor extends EventTarget {
|
||||
|
||||
// sanitize and mark current position as sanitized
|
||||
sanitize(commonAncestor);
|
||||
this._pluginCall('sanitizeElement', [commonAncestor]);
|
||||
}
|
||||
|
||||
addDomListener(element, eventName, callback) {
|
||||
@@ -408,9 +419,11 @@ export class OdooEditor extends EventTarget {
|
||||
}
|
||||
observerUnactive(label) {
|
||||
this._observerUnactiveLabels.add(label);
|
||||
clearTimeout(this.observerTimeout);
|
||||
this.observer.disconnect();
|
||||
this.observerFlush();
|
||||
if (this.observer) {
|
||||
clearTimeout(this.observerTimeout);
|
||||
this.observer.disconnect();
|
||||
this.observerFlush();
|
||||
}
|
||||
}
|
||||
observerFlush() {
|
||||
this.observerApply(this.observer.takeRecords());
|
||||
@@ -2237,8 +2250,12 @@ export class OdooEditor extends EventTarget {
|
||||
hint.classList.remove('oe-hint', 'oe-command-temporary-hint');
|
||||
hint.removeAttribute('placeholder');
|
||||
}
|
||||
this.cleanForSave();
|
||||
this.observerActive();
|
||||
}
|
||||
cleanForSave(element = this.editable) {
|
||||
this._pluginCall('cleanForSave', [element]);
|
||||
}
|
||||
/**
|
||||
* Handle the hint preview for the commandbar.
|
||||
* @private
|
||||
@@ -2816,4 +2833,14 @@ export class OdooEditor extends EventTarget {
|
||||
);
|
||||
}
|
||||
}
|
||||
_pluginAdd(Plugin) {
|
||||
this._plugins.push(new Plugin(this));
|
||||
}
|
||||
_pluginCall(method, args) {
|
||||
for (const plugin of this._plugins) {
|
||||
if (plugin[method]) {
|
||||
plugin[method](...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
export const qwebSample = /* xml */ `
|
||||
<h1>Qweb examples</h1>
|
||||
<div>
|
||||
<t t-set="foo1" t-value="2 + 1"></t>
|
||||
<t t-esc="object">foo</t>
|
||||
<t t-raw="object">foo_raw</t>
|
||||
<t t-esc="invisible"></t>
|
||||
</div>
|
||||
|
||||
<h2>t-esc in link</h2>
|
||||
<a href="#"="foo">Link without t-esc</a>
|
||||
<a href="#" t-esc="foo">Link with t-esc</a>
|
||||
<a href="#"><strong t-esc="foo">Link with t-esc and strong</strong></a>
|
||||
|
||||
<h2>if else part 1</h2>
|
||||
<div>
|
||||
<t t-if="record.partner_id.parent_id">
|
||||
<t t-esc="record.partner_id.name">Brandon Freeman</t> (<t t-esc="record.partner_id.parent_id.name">Azure Interior</t>),
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="record.partner_id.name">Brandon Freeman</t>,
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<h2>if else part 2</h2>
|
||||
<div>
|
||||
<t t-if="condition">
|
||||
<p>if1</p>
|
||||
<t t-if="condition">
|
||||
<p>if1.a</p>
|
||||
</t>
|
||||
<t t-elif="condition">
|
||||
<p>elif1.b</p>
|
||||
</t>
|
||||
<t t-else="condition">
|
||||
<p>elif1.c</p>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="condition">
|
||||
<p>if2</p>
|
||||
</t>
|
||||
<t t-elif="condition">
|
||||
<p>elif2.1</p>
|
||||
</t>
|
||||
<t t-else="condition">
|
||||
<p>elif2.1</p>
|
||||
|
||||
<t t-if="condition">
|
||||
<p>if2.1.1</p>
|
||||
</t>
|
||||
<t t-elif="condition">
|
||||
<p>elif2.1.2</p>
|
||||
</t>
|
||||
<t t-else="condition">
|
||||
<p>elif2.1.3</p>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<h2>if else part 3</h2>
|
||||
<div>
|
||||
<t t-if="condition">
|
||||
<p>if</p>
|
||||
<t t-if="condition">
|
||||
<p>if/if</p>
|
||||
</t>
|
||||
<t t-elif="condition">
|
||||
<p>if/elsif</p>
|
||||
</t>
|
||||
<t t-else="condition">
|
||||
<p>if/else</p>
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="condition">
|
||||
<p>elif</p>
|
||||
<t t-if="condition">
|
||||
<p>elif/if</p>
|
||||
</t>
|
||||
<t t-elif="condition">
|
||||
<p>elif/elif</p>
|
||||
</t>
|
||||
<t t-else="condition">
|
||||
<p>elif/else</p>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="condition">
|
||||
<p>else</p>
|
||||
<t t-if="condition">
|
||||
<p>else/if</p>
|
||||
</t>
|
||||
<t t-elif="condition">
|
||||
<p>else/elif</p>
|
||||
</t>
|
||||
<t t-else="condition">
|
||||
<p>else/else</p>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p t-esc="value">the value</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p t-if="condition">ok</p>
|
||||
</div>
|
||||
<div>
|
||||
<p t-if="user.birthday == today()">Happy birthday!</p>
|
||||
<p t-elif="user.login == 'root'">Welcome master!</p>
|
||||
<p t-else="">Welcome!</p>
|
||||
</div>
|
||||
|
||||
<t t-foreach="[1, 2, 3]" t-as="i">
|
||||
<p><t t-esc="i"></t></p>
|
||||
</t>
|
||||
|
||||
<p t-foreach="[1, 2, 3]" t-as="i">
|
||||
<t t-esc="i"></t>
|
||||
</p>
|
||||
|
||||
<t t-set="foo2">
|
||||
<li>ok</li>
|
||||
</t>
|
||||
<t t-esc="foo2"></t>
|
||||
|
||||
<t t-call="other-template"></t>
|
||||
|
||||
<t t-call="other-template">
|
||||
<t t-set="foo3" t-value="1"></t>
|
||||
</t>
|
||||
|
||||
<div>
|
||||
This template was called with content:
|
||||
<t t-raw="0"></t>
|
||||
</div>
|
||||
|
||||
<h2>t-if should be inline</h2>
|
||||
<div style="text-align: center; margin: 16px 0px 16px 0px;">
|
||||
<t t-if="not is_online or object.state != 'accepted'">
|
||||
<a t-attf-href="/calendar/meeting/accept?token={{object.access_token}}&id={{object.event_id.id}}" style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
Accept</a>
|
||||
<a t-attf-href="/calendar/meeting/decline?token={{object.access_token}}&id={{object.event_id.id}}" style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
|
||||
Decline</a>
|
||||
</t>
|
||||
<a t-attf-href="/calendar/meeting/view?token={{object.access_token}}&id={{object.event_id.id}}" style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px"><t t-esc="'Reschedule' if is_online and target_customer else 'View'">View</t></a>
|
||||
</div>
|
||||
|
||||
<h2>should see the t-if background color</h2>
|
||||
<div style="padding-top:5px;">
|
||||
<ul>
|
||||
<t t-if="not is_online and object.event_id.description">
|
||||
<li>Description: <t t-esc="object.event_id.description or ''" data-oe-t-inline="true">Meeting to discuss project plan and hash out the details of implementation.</t></li>
|
||||
</t>
|
||||
<t t-elif="is_online and object.event_id.description">
|
||||
<t t-set="object.event_id.description_to_html_lines()" t-value="splitted_description" data-oe-t-inline="true"></t>
|
||||
<li>Description:
|
||||
<ul t-foreach="splitted_description" t-as="description_line">
|
||||
<li t-out="description_line or ''">Email: my.email@test.example.com</li>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
@@ -328,56 +328,3 @@ i.oe-commandbar-commandImg {
|
||||
border-radius: 2px;
|
||||
padding: 0.3em 0.6em;
|
||||
}
|
||||
|
||||
/* QWEB */
|
||||
|
||||
.odoo-editor t,
|
||||
.odoo-editor [t-if],
|
||||
.odoo-editor [t-elif],
|
||||
.odoo-editor [t-else],
|
||||
.odoo-editor [t-foreach] {
|
||||
background-color: rgba(0, 0, 102, 0.1) !important;
|
||||
}
|
||||
.odoo-editor t,
|
||||
.odoo-editor [t-esc],
|
||||
.odoo-editor [t-out],
|
||||
.odoo-editor [t-raw] {
|
||||
border-radius: 2px;
|
||||
}
|
||||
.odoo-editor [t-esc],
|
||||
.odoo-editor [t-out],
|
||||
.odoo-editor [t-raw] {
|
||||
background-color: rgb(36 154 255 / 16%) !important;
|
||||
}
|
||||
.odoo-editor [t-esc]:empty::before {
|
||||
content: attr(t-esc);
|
||||
}
|
||||
.odoo-editor [t-raw]:empty::before {
|
||||
content: attr(t-raw);
|
||||
}
|
||||
.odoo-editor [t-out]:empty::before {
|
||||
content: attr(t-out);
|
||||
}
|
||||
.odoo-editor t[t-set] {
|
||||
display: none;
|
||||
}
|
||||
.odoo-editor t[data-oe-t-inline] {
|
||||
display: inline;
|
||||
}
|
||||
.odoo-editor t:not([data-oe-t-inline]) {
|
||||
display: block;
|
||||
}
|
||||
.odoo-editor t[data-oe-t-inline]:not([data-oe-t-group-active]) {
|
||||
display: unset;
|
||||
}
|
||||
.odoo-editor [data-oe-t-group]:not([data-oe-t-group-active]) {
|
||||
display: none !important;
|
||||
}
|
||||
.odoo-editor [data-oe-t-group][data-oe-t-selectable] {
|
||||
outline: 1px dashed rgba(0, 0, 102, 0.4) !important;
|
||||
}
|
||||
.oe-qweb-select {
|
||||
position: absolute;
|
||||
z-index: 1051;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@@ -851,7 +851,13 @@ export function isBlock(node) {
|
||||
}
|
||||
const tagName = node.nodeName.toUpperCase();
|
||||
// Every custom jw-* node will be considered as blocks.
|
||||
if (tagName.startsWith('JW-') || tagName === 'T') {
|
||||
if (
|
||||
tagName.startsWith('JW-') ||
|
||||
(tagName === 'T' &&
|
||||
node.getAttribute('t-esc') === null &&
|
||||
node.getAttribute('t-out') === null &&
|
||||
node.getAttribute('t-raw') === null)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (tagName === 'BR') {
|
||||
@@ -903,6 +909,16 @@ export function isUnbreakable(node) {
|
||||
isUnremovable(node) || // An unremovable node is always unbreakable.
|
||||
['THEAD', 'TBODY', 'TFOOT', 'TR', 'TH', 'TD', 'SECTION', 'DIV'].includes(node.tagName) ||
|
||||
node.hasAttribute('t') ||
|
||||
(node.nodeType === Node.ELEMENT_NODE &&
|
||||
(node.nodeName === 'T' ||
|
||||
node.getAttribute('t-if') ||
|
||||
node.getAttribute('t-esc') ||
|
||||
node.getAttribute('t-elif') ||
|
||||
node.getAttribute('t-else') ||
|
||||
node.getAttribute('t-foreach') ||
|
||||
node.getAttribute('t-value') ||
|
||||
node.getAttribute('t-out') ||
|
||||
node.getAttribute('t-raw'))) ||
|
||||
node.classList.contains('oe_unbreakable')
|
||||
);
|
||||
}
|
||||
@@ -916,7 +932,12 @@ export function isUnremovable(node) {
|
||||
node.parentElement &&
|
||||
!node.parentElement.isContentEditable &&
|
||||
node.nodeName !== 'A'; // links can be their own contenteditable but should be removable by default.
|
||||
return isEditableRoot || (node.classList && node.classList.contains('oe_unremovable'));
|
||||
return (
|
||||
isEditableRoot ||
|
||||
(node.nodeType === Node.ELEMENT_NODE &&
|
||||
(node.getAttribute('t-set') || node.getAttribute('t-call'))) ||
|
||||
(node.classList && node.classList.contains('oe_unremovable'))
|
||||
);
|
||||
}
|
||||
|
||||
export function containsUnbreakable(node) {
|
||||
|
||||
@@ -17,6 +17,12 @@ describe('Autostep', () => {
|
||||
contentBefore: '<p>a[]</p>',
|
||||
stepFunction: async editor => {
|
||||
const originalHistoryLength = editor._historySteps.length;
|
||||
|
||||
// Wait for some plugin (e.g. QWEB) that currently use a
|
||||
// setTimeout in order to wait for the editable to be inserted
|
||||
// in the DOM to perform logic that reset the autostep timeout.
|
||||
await timeoutPromise(20);
|
||||
|
||||
appendSpan(editor.editable.querySelector('p'));
|
||||
await timeoutPromise(20).then(() => {
|
||||
window.chai.assert.strictEqual(
|
||||
@@ -66,6 +72,12 @@ describe('Autostep', () => {
|
||||
contentBefore: '<p>a[]</p>',
|
||||
stepFunction: async editor => {
|
||||
const originalHistoryLength = editor._historySteps.length;
|
||||
|
||||
// Wait for some plugin (e.g. QWEB) that currently use a
|
||||
// setTimeout in order to wait for the editable to be inserted
|
||||
// in the DOM to perform logic that reset the autostep timeout.
|
||||
await timeoutPromise(20);
|
||||
|
||||
editor.automaticStepUnactive('test');
|
||||
appendSpan(editor.editable.querySelector('p'));
|
||||
editor.automaticStepActive('test');
|
||||
@@ -114,6 +126,11 @@ describe('Autostep', () => {
|
||||
stepFunction: async editor => {
|
||||
const originalHistoryLength = editor._historySteps.length;
|
||||
|
||||
// Wait for some plugin (e.g. QWEB) that currently use a
|
||||
// setTimeout in order to wait for the editable to be inserted
|
||||
// in the DOM to perform logic that reset the autostep timeout.
|
||||
await timeoutPromise(20);
|
||||
|
||||
editor.automaticStepUnactive('test');
|
||||
editor.automaticStepUnactive('test2');
|
||||
appendSpan(editor.editable.querySelector('p'));
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Returns a list of all the ancestors nodes of the provided node.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @param {Node} [stopElement] include to prevent bubbling up further than the stopElement.
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
function ancestors(node, stopElement) {
|
||||
if (!node || !node.parentElement || node === stopElement) return [];
|
||||
return [node.parentElement, ...ancestors(node.parentElement, stopElement)];
|
||||
}
|
||||
|
||||
export class QWebPlugin {
|
||||
constructor(options = {}) {
|
||||
this._options = options;
|
||||
this._tGroupCount = 0;
|
||||
this._hideBranchingSelection = this._hideBranchingSelection.bind(this);
|
||||
this._makeBranchingSelection();
|
||||
this._clickListeners = [];
|
||||
}
|
||||
destroy() {
|
||||
this._selectElWrapper.remove();
|
||||
for (const listener of this._clickListeners) {
|
||||
document.removeEventListener('click', listener);
|
||||
}
|
||||
}
|
||||
cleanForSave(editable) {
|
||||
for (const node of editable.querySelectorAll('[data-oe-t-group]')) {
|
||||
node.removeAttribute('data-oe-t-group');
|
||||
node.removeAttribute('data-oe-t-inline');
|
||||
node.removeAttribute('data-oe-t-selectable');
|
||||
}
|
||||
}
|
||||
sanitizeElement(subRoot) {
|
||||
if (subRoot.nodeType !== Node.ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
if (this._editor) {
|
||||
this._editor.observerUnactive('qweb-plugin-sanitize');
|
||||
}
|
||||
|
||||
this._fixInlines(subRoot);
|
||||
|
||||
const demoElements = subRoot.querySelectorAll('[t-esc], [t-raw], [t-out]');
|
||||
for (const element of demoElements) {
|
||||
element.setAttribute('contenteditable', 'false');
|
||||
}
|
||||
|
||||
this._groupQwebBranching(subRoot);
|
||||
if (this._editor) {
|
||||
this._editor.observerActive('qweb-plugin-sanitize');
|
||||
}
|
||||
}
|
||||
_groupQwebBranching(subRoot) {
|
||||
const tNodes = subRoot.querySelectorAll('[t-if], [t-elif], [t-else]');
|
||||
const groupsEncounter = new Set();
|
||||
for (const node of tNodes) {
|
||||
const parentTNode = [...node.parentElement.children];
|
||||
const index = parentTNode.indexOf(node);
|
||||
const prevNode = parentTNode[index - 1];
|
||||
|
||||
let groupId;
|
||||
if (
|
||||
prevNode &&
|
||||
node.previousElementSibling === prevNode &&
|
||||
!node.hasAttribute('t-if')
|
||||
) {
|
||||
// Make the first t-if selectable, if prevNode is not a t-if,
|
||||
// it's already data-oe-t-selectable.
|
||||
prevNode.setAttribute('data-oe-t-selectable', 'true');
|
||||
groupId = parseInt(prevNode.getAttribute('data-oe-t-group'));
|
||||
node.setAttribute('data-oe-t-selectable', 'true');
|
||||
} else {
|
||||
groupId = this._tGroupCount++;
|
||||
}
|
||||
groupsEncounter.add(groupId);
|
||||
node.setAttribute('data-oe-t-group', groupId);
|
||||
|
||||
const clickListener = e => {
|
||||
e.stopImmediatePropagation();
|
||||
this._showBranchingSelection(node);
|
||||
};
|
||||
this._clickListeners.push(clickListener);
|
||||
node.addEventListener('click', clickListener);
|
||||
}
|
||||
for (const groupId of groupsEncounter) {
|
||||
const isOneElementActive = subRoot.querySelector(
|
||||
`[data-oe-t-group='${groupId}'][data-oe-t-group-active]`,
|
||||
);
|
||||
// If there is no element in groupId activated, activate the first
|
||||
// one.
|
||||
if (!isOneElementActive) {
|
||||
subRoot
|
||||
.querySelector(`[data-oe-t-group='${groupId}']`)
|
||||
.setAttribute('data-oe-t-group-active', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
_fixInlines(subRoot) {
|
||||
const checkAllInline = el => {
|
||||
return [...el.children].every(child => {
|
||||
if (child.tagName === 'T') {
|
||||
return checkAllInline(child);
|
||||
} else {
|
||||
return (
|
||||
child.nodeType !== Node.ELEMENT_NODE ||
|
||||
window.getComputedStyle(child).display === 'inline'
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
const tElements = subRoot.querySelectorAll('t');
|
||||
// Wait for the content to be on the dom to check checkAllInline
|
||||
// otherwise the getComputedStyle will be wrong.
|
||||
// todo: remove the setTimeout when the editor will provide a signal
|
||||
// that the editable is on the dom.
|
||||
setTimeout(() => {
|
||||
if (this._editor) {
|
||||
this._editor.observerUnactive('qweb-plugin-checkAllInline');
|
||||
}
|
||||
for (const tElement of tElements) {
|
||||
if (checkAllInline(tElement)) {
|
||||
tElement.setAttribute('data-oe-t-inline', 'true');
|
||||
}
|
||||
}
|
||||
if (this._editor) {
|
||||
this._editor.observerActive('qweb-plugin-checkAllInline');
|
||||
}
|
||||
});
|
||||
}
|
||||
_makeBranchingSelection() {
|
||||
const document = this._options.document || window.document;
|
||||
this._selectElWrapper = document.createElement('div');
|
||||
this._selectElWrapper.classList.add('oe-qweb-select');
|
||||
this._selectElWrapper.innerHTML = '';
|
||||
document.body.append(this._selectElWrapper);
|
||||
this._hideBranchingSelection();
|
||||
}
|
||||
_showBranchingSelection(target) {
|
||||
this._hideBranchingSelection();
|
||||
|
||||
const branchingHierarchyElements = [target, ...ancestors(target, this._options.editable)]
|
||||
.filter(element => element.getAttribute('data-oe-t-group-active') === 'true')
|
||||
.filter(element => {
|
||||
const itemGroupId = element.getAttribute('data-oe-t-group');
|
||||
|
||||
const groupItemsNodes = element.parentElement.querySelectorAll(
|
||||
`[data-oe-t-group='${itemGroupId}']`,
|
||||
);
|
||||
return groupItemsNodes.length > 1;
|
||||
});
|
||||
|
||||
if (!branchingHierarchyElements.length) return;
|
||||
|
||||
const groupsActive = branchingHierarchyElements.map(node =>
|
||||
node.getAttribute('data-oe-t-group'),
|
||||
);
|
||||
for (const branchingElement of branchingHierarchyElements) {
|
||||
this._selectElWrapper.prepend(this._renderBranchingSelection(branchingElement));
|
||||
}
|
||||
const closeSelectHandler = event => {
|
||||
const path = [event.target, ...ancestors(event.target)];
|
||||
const shouldClose = !path.find(
|
||||
element =>
|
||||
element === this._selectElWrapper ||
|
||||
groupsActive.includes(element.getAttribute('data-oe-t-group')),
|
||||
);
|
||||
if (shouldClose) {
|
||||
this._hideBranchingSelection();
|
||||
document.removeEventListener('mousedown', closeSelectHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', closeSelectHandler);
|
||||
this._selectElWrapper.style.display = 'flex';
|
||||
this._updateBranchingSelectionPosition(
|
||||
branchingHierarchyElements[branchingHierarchyElements.length - 1],
|
||||
);
|
||||
}
|
||||
_updateBranchingSelectionPosition(target) {
|
||||
window.addEventListener('mousewheel', this._hideBranchingSelection);
|
||||
|
||||
const box = target.getBoundingClientRect();
|
||||
const selBox = this._selectElWrapper.getBoundingClientRect();
|
||||
|
||||
this._selectElWrapper.style.left = `${window.scrollX + box.left}px`;
|
||||
this._selectElWrapper.style.top = `${window.scrollY + box.top - selBox.height}px`;
|
||||
}
|
||||
_renderBranchingSelection(target) {
|
||||
const selectEl = document.createElement('select');
|
||||
const groupId = parseInt(target.getAttribute('data-oe-t-group'));
|
||||
const groupElements = target.parentElement.querySelectorAll(
|
||||
`[data-oe-t-group='${groupId}']`,
|
||||
);
|
||||
for (const element of groupElements) {
|
||||
const optionElement = document.createElement('option');
|
||||
if (element.hasAttribute('t-if')) {
|
||||
optionElement.innerText = 'if';
|
||||
} else if (element.hasAttribute('t-elif')) {
|
||||
optionElement.innerText = 'elif';
|
||||
} else if (element.hasAttribute('t-else')) {
|
||||
optionElement.innerText = 'else';
|
||||
}
|
||||
if (element.hasAttribute('data-oe-t-group-active')) {
|
||||
optionElement.selected = true;
|
||||
}
|
||||
selectEl.appendChild(optionElement);
|
||||
}
|
||||
|
||||
selectEl.onchange = () => {
|
||||
let activeElement;
|
||||
for (let i = 0; i < groupElements.length; i++) {
|
||||
if (i === selectEl.selectedIndex) {
|
||||
activeElement = groupElements[i];
|
||||
groupElements[i].setAttribute('data-oe-t-group-active', 'true');
|
||||
} else {
|
||||
groupElements[i].removeAttribute('data-oe-t-group-active');
|
||||
}
|
||||
}
|
||||
this._showBranchingSelection(activeElement);
|
||||
};
|
||||
return selectEl;
|
||||
}
|
||||
_hideBranchingSelection() {
|
||||
this._selectElWrapper.style.display = 'none';
|
||||
this._selectElWrapper.innerHTML = ``;
|
||||
window.removeEventListener('mousewheel', this._hideBranchingSelection);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ var basic_fields = require('web.basic_fields');
|
||||
var core = require('web.core');
|
||||
var wysiwygLoader = require('web_editor.loader');
|
||||
var field_registry = require('web.field_registry');
|
||||
const {QWebPlugin} = require('@web_editor/js/backend/QWebPlugin');
|
||||
// must wait for web/ to add the default html widget, otherwise it would override the web_editor one
|
||||
require('web._field_registry');
|
||||
|
||||
@@ -48,6 +49,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
|
||||
* @override
|
||||
*/
|
||||
willStart: async function () {
|
||||
console.log("this:", this);
|
||||
this.isRendered = false;
|
||||
this._onUpdateIframeId = 'onLoad_' + _.uniqueId('FieldHtml');
|
||||
await this._super();
|
||||
@@ -66,6 +68,9 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
|
||||
if (this.$iframe) {
|
||||
this.$iframe.remove();
|
||||
}
|
||||
if (this._qwebPlugin) {
|
||||
this._qwebPlugin.destroy();
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
|
||||
@@ -207,6 +212,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
|
||||
tabsize: 0,
|
||||
height: this.nodeOptions.height || 110,
|
||||
resizable: 'resizable' in this.nodeOptions ? this.nodeOptions.resizable : true,
|
||||
editorPlugins: [QWebPlugin],
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -366,6 +372,8 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
|
||||
} else {
|
||||
this.$content = $('<div class="o_readonly"/>').html(value);
|
||||
this.$content.appendTo(this.$el);
|
||||
this._qwebPlugin = new QWebPlugin();
|
||||
this._qwebPlugin.sanitizeElement(this.$content[0]);
|
||||
resolver();
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ const Wysiwyg = Widget.extend({
|
||||
},
|
||||
noScrollSelector: 'body, .note-editable, .o_content, #wrapwrap',
|
||||
commands: commands,
|
||||
plugins: options.editorPlugins,
|
||||
}, editorCollaborationOptions));
|
||||
|
||||
const $wrapwrap = $('#wrapwrap');
|
||||
@@ -686,7 +687,6 @@ const Wysiwyg = Widget.extend({
|
||||
*/
|
||||
setValue: function (value) {
|
||||
this.$editable.html(value);
|
||||
// todo: check if it's still usefull
|
||||
this.odooEditor.sanitize();
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -816,3 +816,59 @@ section, .oe_img_bg, [data-oe-shape-data] {
|
||||
-webkit-text-fill-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
/* QWEB */
|
||||
|
||||
.odoo-editor, .o_readonly {
|
||||
t,
|
||||
[t-if],
|
||||
[t-elif],
|
||||
[t-else],
|
||||
[t-foreach] {
|
||||
background-color: rgba(0, 0, 102, 0.1) !important;
|
||||
}
|
||||
t,
|
||||
[t-esc],
|
||||
[t-out],
|
||||
[t-raw] {
|
||||
border-radius: 2px;
|
||||
}
|
||||
[t-esc],
|
||||
[t-out],
|
||||
[t-raw] {
|
||||
background-color: rgba(36, 154, 255, 0.16) !important;
|
||||
}
|
||||
[t-esc]:empty::before {
|
||||
content: attr(t-esc);
|
||||
}
|
||||
[t-raw]:empty::before {
|
||||
content: attr(t-raw);
|
||||
}
|
||||
[t-out]:empty::before {
|
||||
content: attr(t-out);
|
||||
}
|
||||
t[t-set] {
|
||||
display: none;
|
||||
}
|
||||
t[data-oe-t-inline] {
|
||||
display: inline;
|
||||
}
|
||||
t:not([data-oe-t-inline]) {
|
||||
display: block;
|
||||
}
|
||||
t[data-oe-t-inline]:not([data-oe-t-group-active]) {
|
||||
display: unset;
|
||||
}
|
||||
[data-oe-t-group]:not([data-oe-t-group-active]) {
|
||||
display: none !important;
|
||||
}
|
||||
[data-oe-t-group][data-oe-t-selectable] {
|
||||
outline: 1px dashed rgba(0, 0, 102, 0.4) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.oe-qweb-select {
|
||||
position: absolute;
|
||||
z-index: 1051;
|
||||
background-color: white;
|
||||
}
|
||||
Reference in New Issue
Block a user