From 1dff3912cb49228d6f11a88aefa797885fa45f6a Mon Sep 17 00:00:00 2001 From: Romain Estievenart Date: Wed, 6 Sep 2023 16:16:41 +0200 Subject: [PATCH] [IMP] web: add a maxLevels params for nest mode on nested_sortable hooks Add a maxLevels param like the jQuery.nestedSortable has. The goal of this param is to set the maximum depth of nested items the list can accept. If set to '0' the levels are unlimited, it's the default value. task-3439226 Part-of: odoo/odoo#136375 --- .../static/src/core/utils/nested_sortable.js | 36 +++++++++++ .../tests/core/utils/nested_sortable_tests.js | 60 ++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/core/utils/nested_sortable.js b/addons/web/static/src/core/utils/nested_sortable.js index 58937372c51..8085493d4ce 100644 --- a/addons/web/static/src/core/utils/nested_sortable.js +++ b/addons/web/static/src/core/utils/nested_sortable.js @@ -34,6 +34,8 @@ import { makeDraggableHook } from "@web/core/utils/draggable_hook_builder"; * @property {string | () => string} [listTagName] type of lists ("ul" or "ol"). * @property {number | () => number} [nestInterval] Horizontal distance needed to trigger * a change in the list hierarchy (i.e. changing parent when moving horizontally) + * @property {number | () => number} [maxLevels] The maximum depth of nested items + * the list can accept. If set to '0' the levels are unlimited. Default: 0 * * HANDLERS (also optional) * @@ -78,6 +80,7 @@ export const useNestedSortable = makeDraggableHook({ nest: [Boolean], listTagName: [String], nestInterval: [Number], + maxLevels: [Number], }, defaultParams: { connectGroups: false, @@ -89,6 +92,7 @@ export const useNestedSortable = makeDraggableHook({ nest: false, listTagName: "ul", nestInterval: 15, + maxLevels: 0, }, // Set the parameters. @@ -108,6 +112,7 @@ export const useNestedSortable = makeDraggableHook({ // (i.e. changing parent when moving horizontally) ctx.nestInterval = params.nestInterval; ctx.isRTL = localization.direction === "rtl"; + ctx.maxLevels = params.maxLevels || 0; }, // Set the current group and create the placeholder row that will take the @@ -164,10 +169,38 @@ export const useNestedSortable = makeDraggableHook({ group: ctx.currentGroup, }; }, + _getDeepestChildLevel(ctx, node, depth = 0) { + let result = 0; + const childSelector = `${ctx.listTagName} ${ctx.elementSelector}`; + for (const childNode of node.querySelectorAll(childSelector)) { + result = Math.max(this._getDeepestChildLevel(ctx, childNode, depth + 1), result); + } + return depth ? result + 1 : result; + }, + _hasReachMaxAllowedLevel(ctx) { + if (!ctx.nest || ctx.maxLevels < 1) { + return false; + } + let level = this._getDeepestChildLevel(ctx, ctx.current.element); + let list = ctx.current.placeHolder.closest(ctx.listTagName); + while (list) { + level++; + list = list.parentNode.closest(ctx.listTagName); + } + return level > ctx.maxLevels; + }, + _isAllowedNodeMove(ctx) { + return !this._hasReachMaxAllowedLevel(ctx); + }, // Check if the cursor moved enough to trigger a move. If it did, move the // placeholder accordingly. onDrag({ ctx, callHandler }) { const onMove = (prevPos) => { + if (!this._isAllowedNodeMove(ctx)) { + ctx.current.placeHolder.classList.add("d-none"); + return; + } + ctx.current.placeHolder.classList.remove("d-none"); callHandler("onMove", { element: ctx.current.element, previous: ctx.current.placeHolder.previousElementSibling, @@ -351,6 +384,9 @@ export const useNestedSortable = makeDraggableHook({ // If the drop position is different from the starting position, run the // onDrop handler from the parameters. onDrop({ ctx }) { + if (!this._isAllowedNodeMove(ctx)) { + return; + } const previous = ctx.current.placeHolder.previousElementSibling; const next = ctx.current.placeHolder.nextElementSibling; if (previous !== ctx.current.element && next !== ctx.current.element) { diff --git a/addons/web/static/tests/core/utils/nested_sortable_tests.js b/addons/web/static/tests/core/utils/nested_sortable_tests.js index 2f54294d0a5..39b9dd6aad6 100644 --- a/addons/web/static/tests/core/utils/nested_sortable_tests.js +++ b/addons/web/static/tests/core/utils/nested_sortable_tests.js @@ -924,12 +924,68 @@ QUnit.module("Draggable", ({ beforeEach }) => { }); assert.verifySteps( ["Initiation of the drag sequence"], - "A drag sequence should have been initiated", + "A drag sequence should have been initiated" ); await drop(); - }, + } ); + QUnit.test("shouldn't drag above max level", async (assert) => { + assert.expect(6); + class NestedSortable extends Component { + static template = xml` +
+ +
+ `; + + setup() { + useNestedSortable({ + ref: useRef("root"), + elements: ".item", + nest: true, + maxLevels: 2, + onDragStart() { + assert.step("start"); + }, + onMove() { + assert.step("move"); + }, + onDrop() { + assert.step("drop"); + }, + onDragEnd({ element }) { + assert.step("end"); + assert.strictEqual(element.id, "dragged"); + assert.notOk(element.parentElement.closest("#parent")); + assert.containsOnce(target, ".o_nested_sortable_placeholder.d-none"); + }, + }); + } + } + + await mount(NestedSortable, target); + + // cant move draggable under parent + const draggedNode = target.querySelector(".item#dragged"); + const { drop, moveTo } = await drag(draggedNode); + await moveTo("#parent", "right"); + await drop(); + assert.verifySteps(["start", "end"]); + }); + QUnit.test("Ignore specified elements", async (assert) => { assert.expect(6);