diff --git a/addons/web/static/src/core/utils/draggable_hook_builder.js b/addons/web/static/src/core/utils/draggable_hook_builder.js index df0bd02724b..11310493ca4 100644 --- a/addons/web/static/src/core/utils/draggable_hook_builder.js +++ b/addons/web/static/src/core/utils/draggable_hook_builder.js @@ -34,6 +34,7 @@ import { hasTouch, isBrowserFirefox, isIOS } from "../browser/feature_detection" * @property {boolean} [followCursor=true] * @property {string | null} [cursor=null] * @property {() => boolean} [enable=() => false] + * @property {(HTMLElement) => boolean} [preventDrag=(el) => false] * @property {Position} [pointer={ x: 0, y: 0 }] * @property {EdgeScrollingOptions} [edgeScrolling] * @property {number} [delay] @@ -77,6 +78,7 @@ const DRAGGED_CLASS = "o_dragged"; const DEFAULT_ACCEPTED_PARAMS = { enable: [Boolean, Function], + preventDrag: [Function], ref: [Object], elements: [String], handle: [String, Function], @@ -89,6 +91,7 @@ const DEFAULT_ACCEPTED_PARAMS = { const DEFAULT_DEFAULT_PARAMS = { elements: `.${DRAGGABLE_CLASS}`, enable: true, + preventDrag: () => false, edgeScrolling: { speed: 10, threshold: 30, @@ -658,11 +661,13 @@ export function makeDraggableHook(hookParams) { // outside of the window. dragEnd(null); + const fullSelectorEl = ev.target.closest(ctx.fullSelector); if ( ev.button !== LEFT_CLICK || !ctx.enable() || - !ev.target.closest(ctx.fullSelector) || - (ctx.ignoreSelector && ev.target.closest(ctx.ignoreSelector)) + !fullSelectorEl || + (ctx.ignoreSelector && ev.target.closest(ctx.ignoreSelector)) || + ctx.preventDrag(fullSelectorEl) ) { return; } @@ -886,6 +891,7 @@ export function makeDraggableHook(hookParams) { /** @type {DraggableHookContext} */ const ctx = { enable: () => false, + preventDrag: () => false, ref: params.ref, ignoreSelector: null, fullSelector: null, @@ -911,6 +917,11 @@ export function makeDraggableHook(hookParams) { // Enable getter ctx.enable = actualParams.enable; + // Dragging constraint + if (actualParams.preventDrag) { + ctx.preventDrag = actualParams.preventDrag; + } + // Selectors ctx.elementSelector = actualParams.elements; if (!ctx.elementSelector) { diff --git a/addons/web/static/src/core/utils/nested_sortable.js b/addons/web/static/src/core/utils/nested_sortable.js index 553edbcde0b..e58f99e70f6 100644 --- a/addons/web/static/src/core/utils/nested_sortable.js +++ b/addons/web/static/src/core/utils/nested_sortable.js @@ -22,7 +22,11 @@ import { makeDraggableHook } from "@web/core/utils/draggable_hook_builder"; * @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 targeting elements that must - * initiate a drag. + * initiate a drag (recursive in depth: all draggable elements under an + * ignored element will be locked in place) + * @property {(HTMLElement) => boolean} [preventDrag] function receiving a + * the current target for dragging (element) and returning a boolean, whether + * the element can be effectively dragged or not. * @property {boolean | () => boolean} [connectGroups] whether elements can be dragged * across different parent groups. Note that it requires a `groups` param to work. * @property {string | () => string} [cursor] cursor style during the dragging sequence. diff --git a/addons/web/static/tests/core/utils/draggable_tests.js b/addons/web/static/tests/core/utils/draggable_tests.js index 6b82c994852..beab3836274 100644 --- a/addons/web/static/tests/core/utils/draggable_tests.js +++ b/addons/web/static/tests/core/utils/draggable_tests.js @@ -215,6 +215,71 @@ QUnit.module("Draggable", ({ beforeEach }) => { assert.verifySteps([]); }); + QUnit.test("Ignore specific elements in a nested draggable", async (assert) => { + assert.expect(7); + + class List extends Component { + static components = { List }; + static template = xml` +
+ +
`; + setup() { + useDraggable({ + ref: useRef("root"), + elements: ".item", + preventDrag: (el) => el.classList.contains('ignored'), + onDragStart() { + assert.step("drag"); + }, + }); + } + } + + await mount(List, target); + + assert.verifySteps([]); + + // Drag ignored under non-ignored -> block + await dragAndDrop( + ".not-ignored.parent .ignored.child", + ".not-ignored.parent .not-ignored.child" + ); + assert.verifySteps([]); + + // Drag not-ignored-under not-ignored -> succeed + await dragAndDrop( + ".not-ignored.parent .not-ignored.child", + ".not-ignored.parent .ignored.child" + ); + assert.verifySteps(["drag"]); + + // Drag ignored under ignored -> block + await dragAndDrop( + ".ignored.parent .ignored.child", + ".ignored.parent .not-ignored.child" + ); + assert.verifySteps([]); + + // Drag not-ignored under ignored -> succeed + await dragAndDrop( + ".ignored.parent .not-ignored.child", + ".ignored.parent .ignored.child" + ); + assert.verifySteps(["drag"]); + }); + QUnit.test("Dragging element with touch event", async (assert) => { assert.expect(10);