[IMP] web_editor, website: add drop-zone locking and exclusion

Since [1] form fields cannot be moved outside of their form and since
[2] table of contents cannot be nested within another table of content.
Those were implemented with specific references to website classes
within web_editor.

This commit introduces declarative mechanisms for both situations:
- `data-drop-lock-within`: prevents dropping outside the closest parent
that matches the specified selector.
- `data-drop-exclude-ancestor`: disallows dropping within a parent that
matches the specified selector.

[1]: https://github.com/odoo/odoo/commit/638d0e875dcb05191fe833d2f889235891dfb46a
[2]: https://github.com/odoo/odoo/commit/55339cd6a7b7916184893c0fe27b5483683a047a

task-3131384

closes odoo/odoo#130477

Signed-off-by: Guillaume Dieleman (gdi) <gdi@odoo.com>
This commit is contained in:
Benoit Socias
2023-08-09 13:20:57 +02:00
parent cdc35aed74
commit 205fad17a3
3 changed files with 46 additions and 33 deletions
@@ -693,6 +693,8 @@ var SnippetEditor = Widget.extend({
this.styles = {};
this.selectorSiblings = [];
this.selectorChildren = [];
this.selectorLockWithin = new Set();
const selectorExcludeAncestor = new Set();
var $element = this.$target.parent();
while ($element.length) {
@@ -740,6 +742,12 @@ var SnippetEditor = Widget.extend({
if (val['drop-in']) {
this.selectorChildren.push(val['drop-in']);
}
if (val['drop-lock-within']) {
this.selectorLockWithin.add(val['drop-lock-within']);
}
if (val['drop-exclude-ancestor']) {
selectorExcludeAncestor.add(val['drop-exclude-ancestor']);
}
var optionName = val.option;
var option = new (options.registry[optionName] || options.Class)(
@@ -774,6 +782,13 @@ var SnippetEditor = Widget.extend({
return option.appendTo(document.createDocumentFragment());
});
if (selectorExcludeAncestor.size) {
// Prevents dropping an element into another one.
// (E.g. ToC inside another ToC)
const excludedAncestorSelector = [...selectorExcludeAncestor].join(", ");
this.excludeAncestors = (i, el) => !el.closest(excludedAncestorSelector);
}
this.isTargetMovable = (this.selectorSiblings.length > 0 || this.selectorChildren.length > 0);
this.$el.find('[data-bs-toggle="dropdown"]').dropdown();
@@ -1006,40 +1021,34 @@ var SnippetEditor = Widget.extend({
width: self.$target.width(),
height: self.$target.height()
};
const closestFormEl = this.$target[0].closest('form');
self.$target.after('<div class="oe_drop_clone" style="display: none;"/>');
const dropCloneEl = document.createElement("div");
dropCloneEl.classList.add("oe_drop_clone");
dropCloneEl.style.setProperty("display", "none");
self.$target[0].after(dropCloneEl);
self.$target.detach();
self.$el.addClass('d-none');
var $selectorSiblings;
for (var i = 0; i < self.selectorSiblings.length; i++) {
if (!$selectorSiblings) {
$selectorSiblings = self.selectorSiblings[i].all();
} else {
$selectorSiblings = $selectorSiblings.add(self.selectorSiblings[i].all());
let $siblings = self.selectorSiblings[i].all();
if (this.excludeAncestors) {
$siblings = $siblings.filter(this.excludeAncestors);
}
$selectorSiblings = $selectorSiblings ? $selectorSiblings.add($siblings) : $siblings;
}
var $selectorChildren;
for (i = 0; i < self.selectorChildren.length; i++) {
if (!$selectorChildren) {
$selectorChildren = self.selectorChildren[i].all();
} else {
$selectorChildren = $selectorChildren.add(self.selectorChildren[i].all());
let $children = self.selectorChildren[i].all();
if (this.excludeAncestors) {
$children = $children.filter(this.excludeAncestors);
}
$selectorChildren = $selectorChildren ? $selectorChildren.add($children) : $children;
}
// TODO In master, do not reference other module class + find a better
// system to define such cases + avoid duplicated code (drag & drop from
// editor panel + drag & drop from move button of existing block).
// Prevent dropping ToC inside another ToC. grep: NO_DOUBLE_TOC
if (this.$target[0].classList.contains('s_table_of_content')) {
$selectorChildren = $selectorChildren.filter((i, el) => !el.closest('.s_table_of_content'));
}
// Disallow dropping form fields outside of their form.
// TODO this can probably be implemented by reviewing data-drop-near
// definitions in master but we should find a better to define those and
// such cases.
if (this.$target[0].classList.contains('s_website_form_field')) {
const filterFunc = (i, el) => el.closest('form') === closestFormEl;
// Disallow dropping an element outside a given direct or
// indirect parent. (E.g. form field must remain within its own form)
for (const lockedParentSelector of this.selectorLockWithin) {
const closestLockedParentEl = dropCloneEl.closest(lockedParentSelector);
const filterFunc = (i, el) => el.closest(lockedParentSelector) === closestLockedParentEl;
if ($selectorSiblings) {
$selectorSiblings = $selectorSiblings.filter(filterFunc);
}
@@ -2868,6 +2877,8 @@ var SnippetsMenu = Widget.extend({
'$el': $style,
'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), '', false, noCheck, true, excludeParent),
'drop-in': $style.data('drop-in') && self._computeSelectorFunctions($style.data('drop-in'), '', false, noCheck),
'drop-exclude-ancestor': this.dataset.dropExcludeAncestor,
'drop-lock-within': this.dataset.dropLockWithin,
'data': Object.assign({string: $style.attr('string')}, $style.data()),
};
self.templateOptions.push(option);
@@ -3241,6 +3252,7 @@ var SnippetsMenu = Widget.extend({
var $baseBody = $snippet.find('.oe_snippet_body');
var $selectorSiblings = $();
var $selectorChildren = $();
const selectorExcludeAncestor = [];
var temp = self.templateOptions;
for (var k in temp) {
if ($baseBody.is(temp[k].base_selector) && !$baseBody.is(temp[k].base_exclude)) {
@@ -3250,17 +3262,17 @@ var SnippetsMenu = Widget.extend({
if (temp[k]['drop-in']) {
$selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all());
}
if (temp[k]['drop-exclude-ancestor']) {
selectorExcludeAncestor.push(temp[k]['drop-exclude-ancestor']);
}
}
}
// TODO In master, do not reference other module class +
// find a better system to define such cases + avoid
// duplicated code (drag & drop from editor panel + drag &
// drop from move button of existing block).
// Prevent dropping ToC inside another ToC.
// grep: NO_DOUBLE_TOC
if ($baseBody[0].classList.contains('s_table_of_content')) {
$selectorChildren = $selectorChildren.filter((i, el) => !el.closest('.s_table_of_content'));
// Prevent dropping an element into another one.
// (E.g. ToC inside another ToC)
for (const excludedAncestorSelector of selectorExcludeAncestor) {
$selectorSiblings = $selectorSiblings.filter((i, el) => !el.closest(excludedAncestorSelector));
$selectorChildren = $selectorChildren.filter((i, el) => !el.closest(excludedAncestorSelector));
}
$toInsert = $baseBody.clone();
@@ -57,7 +57,7 @@
</div>
</xpath>
<xpath expr="." position="inside">
<div data-js="TableOfContent" data-selector=".s_table_of_content"/>
<div data-js="TableOfContent" data-selector=".s_table_of_content" data-drop-exclude-ancestor=".s_table_of_content"/>
<div data-js="TableOfContentNavbar" data-selector=".s_table_of_content_navbar_wrap">
<we-button-group string="Position">
<we-button class="fa fa-fw fa-long-arrow-left" title="Left" data-navbar-position="left"/>
@@ -74,7 +74,8 @@
</div>
<div data-js='WebsiteFieldEditor' data-selector=".s_website_form_field"
data-exclude=".s_website_form_dnone" data-drop-near=".s_website_form_field">
data-exclude=".s_website_form_dnone" data-drop-near=".s_website_form_field"
data-drop-lock-within="form">
<we-select data-name="type_opt" string="Type" data-no-preview="true">
<we-title>Custom field</we-title>
<we-button data-custom-field="char">Text</we-button>