diff --git a/addons/web/static/src/core/utils/sortable.js b/addons/web/static/src/core/utils/sortable.js index e3c78a3f694..1e5d8c8ee08 100644 --- a/addons/web/static/src/core/utils/sortable.js +++ b/addons/web/static/src/core/utils/sortable.js @@ -22,6 +22,8 @@ const { useEffect, useEnv, useExternalListener, onWillUnmount } = owl; * work on group elements during the dragging sequence. * @property {string | () => string} [handle] additional selector for when the dragging * sequence must be initiated when dragging on a certain part of the element. + * @property {string | () => string} [ignore] selector targetting elements that must + * initiate a drag. * @property {boolean | () => boolean} [connectGroups] whether elements can be dragged * accross different parent groups. Note that it requires a `groups` param to work. * @property {string | () => string} [cursor] cursor style during the dragging sequence. @@ -63,6 +65,7 @@ const SORTABLE_PARAMS = { elements: ["string"], groups: ["string", "function"], handle: ["string", "function"], + ignore: ["string", "function"], connectGroups: ["boolean", "function"], cursor: ["string"], }; @@ -155,6 +158,11 @@ export function useSortable(params) { * @type {string | null} */ let elementSelector = null; + /** + * Stores the full selector used to initiate a drag sequence. + * @type {string | null} + */ + let ignoreSelector = null; /** * Stores the full selector used to initiate a drag sequence. * @type {string | null} @@ -339,7 +347,12 @@ export function useSortable(params) { // outside of the window. dragStop(true); - if (ev.button !== LEFT_CLICK || !enabled || !ev.target.closest(fullSelector)) { + if ( + ev.button !== LEFT_CLICK || + !enabled || + !ev.target.closest(fullSelector) || + (ignoreSelector && ev.target.closest(ignoreSelector)) + ) { return; } @@ -407,8 +420,8 @@ export function useSortable(params) { currentContainerRect.height -= ptop + pbottom; // Prepares the ghost element - ghost = currentElement.cloneNode(true); - ghost.style.visibility = "hidden"; + ghost = currentElement.cloneNode(false); + ghost.style = `visibility: hidden; display: block; width: ${width}px; height:${height}px;`; cleanups.push(() => ghost.remove()); // Binds handlers on eligible groups, if the elements are not confined to @@ -526,6 +539,9 @@ export function useSortable(params) { if (actualParams.handle) { allSelectors.push(actualParams.handle); } + if (actualParams.ignore) { + ignoreSelector = actualParams.ignore; + } fullSelector = allSelectors.join(" "); // Connection accross groups diff --git a/addons/web/static/tests/core/utils/ui_tests.js b/addons/web/static/tests/core/utils/ui_tests.js index 61e27c8a429..b78114b2d60 100644 --- a/addons/web/static/tests/core/utils/ui_tests.js +++ b/addons/web/static/tests/core/utils/ui_tests.js @@ -201,7 +201,7 @@ QUnit.module("UI", ({ beforeEach }) => { QUnit.test("Dynamically disable sortable feature", async (assert) => { assert.expect(4); - let state = reactive({ enableSortable: true }); + const state = reactive({ enableSortable: true }); class List extends Component { setup() { this.state = useState(state); @@ -274,4 +274,50 @@ QUnit.module("UI", ({ beforeEach }) => { assert.ok(true, "No drag sequence should have been initiated"); }); + + QUnit.test("Ignore specified elements elements", async (assert) => { + assert.expect(6); + + class List extends Component { + setup() { + useSortable({ + ref: useRef("root"), + elements: ".item", + ignore: ".ignored", + onStart() { + assert.step("drag"); + }, + }); + } + } + + List.template = xml` +
+ +
`; + + await mount(List, target); + + assert.verifySteps([]); + + // Drag root item element + await dragAndDrop(".item:first-child", ".item:nth-child(2)"); + + assert.verifySteps(["drag"]); + + // Drag ignored element + await dragAndDrop(".item:first-child .not-ignored", ".item:nth-child(2)"); + + assert.verifySteps(["drag"]); + + // Drag non-ignored element + await dragAndDrop(".item:first-child .ignored", ".item:nth-child(2)"); + + assert.verifySteps([]); + }); });