diff --git a/addons/web/static/src/core/utils/nested_sortable.js b/addons/web/static/src/core/utils/nested_sortable.js
index 7971fc1d478..553edbcde0b 100644
--- a/addons/web/static/src/core/utils/nested_sortable.js
+++ b/addons/web/static/src/core/utils/nested_sortable.js
@@ -21,13 +21,13 @@ import { makeDraggableHook } from "@web/core/utils/draggable_hook_builder";
* 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
+ * @property {string | () => string} [ignore] selector targeting 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.
+ * across different parent groups. Note that it requires a `groups` param to work.
* @property {string | () => string} [cursor] cursor style during the dragging sequence.
* @property {boolean | () => boolean} [nest] whether elements are nested or not.
- * @property {string | () => string} [lisType] type of lists ("ul" or "ol").
+ * @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)
*
@@ -72,7 +72,7 @@ export const useNestedSortable = makeDraggableHook({
groups: [String, Function],
connectGroups: [Boolean, Function],
nest: [Boolean],
- listType: [String],
+ listTagName: [String],
nestInterval: [Number],
},
defaultParams: {
@@ -83,7 +83,7 @@ export const useNestedSortable = makeDraggableHook({
elements: "li",
groupSelector: null,
nest: false,
- listType: "ul",
+ listTagName: "ul",
nestInterval: 15,
},
@@ -94,12 +94,12 @@ export const useNestedSortable = makeDraggableHook({
if (ctx.groupSelector) {
ctx.fullSelector = [ctx.groupSelector, ctx.fullSelector].join(" ");
}
- // Connection accross groups
+ // Connection across groups
ctx.connectGroups = params.connectGroups;
// Nested elements
ctx.nest = params.nest;
- // List type
- ctx.listType = params.listType;
+ // List tag name
+ ctx.listTagName = params.listTagName;
// Horizontal distance needed to trigger a change in the list hierarchy
// (i.e. changing parent when moving horizontally)
ctx.nestInterval = params.nestInterval;
@@ -115,8 +115,12 @@ export const useNestedSortable = makeDraggableHook({
ctx.current.container = ctx.currentGroup;
}
}
+
+ if (ctx.nest) {
+ ctx.prevNestX = ctx.pointer.x;
+ }
ctx.current.placeHolder = ctx.current.element.cloneNode(false);
- ctx.current.placeHolder.style = `display: block; width: 100%; height: 5px; background-color: deepskyblue`;
+ ctx.current.placeHolder.classList.add("o_nested_sortable_placeholder");
addCleanup(() => ctx.current.placeHolder.remove());
},
@@ -127,11 +131,11 @@ export const useNestedSortable = makeDraggableHook({
// Horizontal position which will be used to detect row changes when moving vertically, so that
// we do not need to be on the row to trigger row changes (only the vertical position matters).
// Nested rows are shorter than "root" rows, and do not start at the same horizontal position.
- // However, every row spans until the end of the container. Therefore, we use the end of the
- // container - 1 as horizontal position.
+ // However, every row ends at the same horizontal position. Therefore, we use the end of the
+ // current element - 1 as horizontal position.
ctx.selectorX = ctx.isRTL
- ? ctx.current.containerRect.left + 1
- : ctx.current.containerRect.right - 1;
+ ? ctx.current.elementRect.left + 1
+ : ctx.current.elementRect.right - 1;
// Placeholder is initially added right after the current element.
ctx.current.element.after(ctx.current.placeHolder);
@@ -145,8 +149,6 @@ export const useNestedSortable = makeDraggableHook({
addStyle(document.querySelector(".o_action_manager"), { "pointer-events": "none" });
addStyle(ctx.current.container, { "pointer-events": "auto" });
- ctx.prevNestX = ctx.pointer.x;
-
// Calls "onDragStart" handler
return {
element: ctx.current.element,
@@ -179,9 +181,9 @@ export const useNestedSortable = makeDraggableHook({
* @return {HTMLElement} list
*/
const getChildList = (el) => {
- let list = el.querySelector(ctx.listType);
+ let list = el.querySelector(ctx.listTagName);
if (!list) {
- list = document.createElement(ctx.listType);
+ list = document.createElement(ctx.listTagName);
el.appendChild(list);
}
return list;
@@ -229,7 +231,7 @@ export const useNestedSortable = makeDraggableHook({
*/
if (ctx.nest) {
const xInterval = ctx.prevNestX - ctx.pointer.x;
- if (ctx.nestInterval - (-1) ** ctx.isRTL * xInterval < 0) {
+ if (ctx.nestInterval - (-1) ** ctx.isRTL * xInterval < 1) {
// Place placeholder after its parent in its parent's list only
// if the placeholder is the last child of its parent
// (ignoring the current element which is in the dom)
@@ -246,22 +248,25 @@ export const useNestedSortable = makeDraggableHook({
}
// Recenter the pointer coordinates to this step
ctx.prevNestX = ctx.pointer.x;
- } else if (ctx.nestInterval + (-1) ** ctx.isRTL * xInterval < 0) {
+ return;
+ } else if (ctx.nestInterval + (-1) ** ctx.isRTL * xInterval < 1) {
// Place placeholder as the last child of its previous sibling,
// (ignoring the current element which is in the dom)
let parent = position.previous;
if (parent === ctx.current.element) {
parent = parent.previousElementSibling;
}
- if (parent) {
+ if (parent && parent.matches(ctx.elementSelector)) {
getChildList(parent).appendChild(ctx.current.placeHolder);
onMove(position);
}
// Recenter the pointer coordinates to this step
ctx.prevNestX = ctx.pointer.x;
+ return;
}
}
- const closestEl = document.elementFromPoint(ctx.selectorX, ctx.pointer.y);
+ const currentTop = ctx.pointer.y - ctx.current.offset.y;
+ const closestEl = document.elementFromPoint(ctx.selectorX, currentTop);
if (!closestEl) {
// Cursor outside of viewport
return;
@@ -277,23 +282,30 @@ export const useNestedSortable = makeDraggableHook({
// element. If the position is not allowed but nesting is allowed,
// place the placeholder as the last child of the previous sibling
// instead.
- if (ctx.pointer.y - eRect.y < 10) {
+ if (currentTop - eRect.y < 10) {
if (pos === 2 || pos === 10) {
element.before(ctx.current.placeHolder);
onMove(position);
// Recenter the pointer coordinates to this step
ctx.prevNestX = ctx.pointer.x;
}
- } else if (ctx.pointer.y - eRect.y > 15 && pos === 4) {
+ } else if (currentTop - eRect.y > 15 && pos === 4) {
// Place placeholder after the hovered element in its parent's
// list if the cursor is not in the upper part of the
// element and if the placeholder is currently before the
// hovered element.
- // If nesting is allowed, place the placeholder as the first
- // child of the hovered element instead.
+ // If nesting is allowed and if the element has at least one
+ // child, place the placeholder above the first child of the
+ // hovered element instead.
if (ctx.nest) {
- getChildList(element).prepend(ctx.current.placeHolder);
- onMove(position);
+ const elementChildList = getChildList(element);
+ if (elementChildList.querySelector(ctx.elementSelector)) {
+ elementChildList.prepend(ctx.current.placeHolder);
+ onMove(position);
+ } else {
+ element.after(ctx.current.placeHolder);
+ onMove(position);
+ }
// Recenter the pointer coordinates to this step
ctx.prevNestX = ctx.pointer.x;
} else {
diff --git a/addons/web/static/src/core/utils/nested_sortable.scss b/addons/web/static/src/core/utils/nested_sortable.scss
new file mode 100644
index 00000000000..de8d7c27d70
--- /dev/null
+++ b/addons/web/static/src/core/utils/nested_sortable.scss
@@ -0,0 +1,9 @@
+.o_nested_sortable_placeholder {
+ background-clip: content-box;
+ background-color: deepskyblue;
+ display: block;
+ height: 5px;
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ width: 100%;
+}
diff --git a/addons/web/static/tests/core/utils/nested_sortable_tests.js b/addons/web/static/tests/core/utils/nested_sortable_tests.js
new file mode 100644
index 00000000000..2f54294d0a5
--- /dev/null
+++ b/addons/web/static/tests/core/utils/nested_sortable_tests.js
@@ -0,0 +1,979 @@
+/** @odoo-module **/
+
+import { drag, getFixture, mount, nextTick } from "@web/../tests/helpers/utils";
+import { registerCleanup } from "@web/../tests/helpers/cleanup";
+import { useNestedSortable } from "@web/core/utils/nested_sortable";
+
+import { Component, reactive, useRef, useState, xml } from "@odoo/owl";
+
+/**
+ * Dragging methods taking into account the fact that it's the top of the
+ * dragged element that triggers the moves (not the position of the cursor),
+ * and the fact that during the first move, the dragged element is replaced by
+ * a placeholder that does not have the same height. The moves are done with
+ * the same x position to prevent triggering horizontal moves.
+ * @param {string} from
+ */
+const sortableDrag = async (from) => {
+ const fixture = getFixture();
+ const fromEl = fixture.querySelector(from);
+ const fromRect = fromEl.getBoundingClientRect();
+ const { drop, moveTo } = await drag(from);
+ let isFirstMove = true;
+
+ /**
+ * @param {string} [targetSelector]
+ */
+ const moveAbove = async (targetSelector) => {
+ const el = fixture.querySelector(targetSelector);
+ await moveTo(el, {
+ x: fromRect.x - el.getBoundingClientRect().x + fromRect.width / 2,
+ y: fromRect.height / 2 + 5,
+ });
+ isFirstMove = false;
+ };
+
+ /**
+ * @param {string} [targetSelector]
+ */
+ const moveUnder = async (targetSelector) => {
+ const el = fixture.querySelector(targetSelector);
+ const elRect = el.getBoundingClientRect();
+ let firstMoveBelow = false;
+ if (isFirstMove && elRect.y > fromRect.y) {
+ // Need to consider that the moved element will be replaced by a
+ // placeholder with a height of 5px
+ firstMoveBelow = true;
+ }
+ await moveTo(el, {
+ x: fromRect.x - elRect.x + fromRect.width / 2,
+ y:
+ ((firstMoveBelow ? -1 : 1) * fromRect.height) / 2 +
+ elRect.height +
+ (firstMoveBelow ? 4 : -1),
+ });
+ isFirstMove = false;
+ };
+
+ return { moveAbove, moveUnder, drop };
+};
+
+const dragAndDrop = async (from, to) => {
+ const { drop, moveUnder } = await sortableDrag(from);
+ await moveUnder(to);
+ await drop();
+};
+
+let target;
+QUnit.module("Draggable", ({ beforeEach }) => {
+ beforeEach(() => {
+ target = getFixture();
+ // make fixture in visible range, so that document.elementFromPoint
+ // work as expected
+ target.style.position = "absolute";
+ target.style.top = "0";
+ target.style.left = "0";
+ target.style.height = "100%";
+ target.style.opacity = QUnit.config.debug ? "" : "0";
+ registerCleanup(async () => {
+ target.style.position = "";
+ target.style.top = "";
+ target.style.left = "";
+ target.style.height = "";
+ target.style.opacity = "";
+ });
+ });
+
+ QUnit.module("NestedSortable hook");
+
+ QUnit.test("Parameters error handling", async (assert) => {
+ assert.expect(9);
+
+ const mountNestedSortableAndAssert = async (setupList, shouldThrow) => {
+ class NestedSortable extends Component {
+ static template = xml`
+
+ `;
+
+ setup() {
+ setupList();
+ }
+ }
+
+ await mount(NestedSortable, target).catch(() => assert.step("thrown"));
+
+ assert.verifySteps(shouldThrow ? ["thrown"] : []);
+ };
+
+ // Incorrect params
+ await mountNestedSortableAndAssert(() => {
+ useNestedSortable({});
+ }, true);
+ await mountNestedSortableAndAssert(() => {
+ useNestedSortable({
+ elements: ".item",
+ });
+ }, true);
+ await mountNestedSortableAndAssert(() => {
+ useNestedSortable({
+ elements: ".item",
+ groups: ".list",
+ });
+ }, true);
+
+ // Correct params
+ await mountNestedSortableAndAssert(() => {
+ useNestedSortable({
+ ref: useRef("root"),
+ });
+ }, false);
+ await mountNestedSortableAndAssert(() => {
+ useNestedSortable({
+ ref: {},
+ elements: ".item",
+ groups: ".list",
+ enable: false,
+ });
+ }, false);
+ await mountNestedSortableAndAssert(() => {
+ useNestedSortable({
+ ref: useRef("root"),
+ groups: ".list",
+ connectGroups: true,
+ nest: true,
+ listTagName: "ol",
+ nestIndent: 20,
+ });
+ }, false);
+ });
+
+ QUnit.test("Sorting in a single group without nesting", async (assert) => {
+ assert.expect(34);
+
+ class NestedSortable extends Component {
+ static template = xml`
+
+ `;
+
+ setup() {
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".sortable_list > li",
+ onDragStart({ element, group }) {
+ assert.step("start");
+ assert.strictEqual(element.id, "1");
+ assert.notOk(group);
+ },
+ onMove({
+ element,
+ previous,
+ next,
+ parent,
+ group,
+ newGroup,
+ prevPos,
+ placeholder,
+ }) {
+ assert.step("move");
+ assert.strictEqual(element.id, "1");
+ assert.strictEqual(previous.id, "2");
+ assert.strictEqual(next.id, "3");
+ assert.notOk(parent);
+ assert.notOk(group);
+ assert.notOk(newGroup);
+ assert.strictEqual(prevPos.previous.id, "1");
+ assert.strictEqual(prevPos.next.id, "2");
+ assert.notOk(prevPos.parent);
+ assert.notOk(prevPos.group);
+ assert.strictEqual(placeholder.previousElementSibling.id, "2");
+ },
+ onDrop({ element, previous, next, parent, group, newGroup, placeholder }) {
+ assert.step("drop");
+ assert.strictEqual(element.id, "1");
+ assert.strictEqual(previous.id, "2");
+ assert.strictEqual(next.id, "3");
+ assert.notOk(parent);
+ assert.notOk(group);
+ assert.notOk(newGroup);
+ assert.strictEqual(placeholder.previousElementSibling.id, "2");
+ },
+ onDragEnd({ element, group }) {
+ assert.step("end");
+ assert.strictEqual(element.id, "1");
+ assert.notOk(group);
+ assert.containsN(target, ".sortable_list > .item", 4);
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, target);
+
+ assert.containsN(target, ".sortable_list > .item", 3);
+ assert.containsNone(target, ".o_dragged");
+ assert.verifySteps([]);
+
+ // Move first item after second item
+ const { drop, moveUnder } = await sortableDrag(".sortable_list > .item:first-child");
+ await moveUnder(".sortable_list > .item:nth-child(2)");
+
+ assert.hasClass(target.querySelector(".sortable_list > .item"), "o_dragged");
+
+ await drop();
+
+ assert.containsN(target, ".sortable_list > .item", 3);
+ assert.containsNone(target, ".o_dragged");
+ assert.verifySteps(["start", "move", "drop", "end"]);
+ });
+
+ QUnit.test("Sorting in groups without nesting", async (assert) => {
+ assert.expect(38);
+ class NestedSortable extends Component {
+ static template = xml`
+
+ `;
+
+ setup() {
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".sortable_list > li",
+ groups: "section",
+ connectGroups: true,
+ onDragStart({ element, group }) {
+ assert.step("start");
+ assert.strictEqual(element.id, "2.2");
+ assert.strictEqual(group.id, "2");
+ },
+ onMove({
+ element,
+ previous,
+ next,
+ parent,
+ group,
+ newGroup,
+ prevPos,
+ placeholder,
+ }) {
+ assert.step("move");
+ assert.strictEqual(element.id, "2.2");
+ assert.strictEqual(previous.id, "1.2");
+ assert.notOk(next);
+ assert.notOk(parent);
+ assert.strictEqual(group.id, "2");
+ assert.strictEqual(newGroup.id, "1");
+ assert.strictEqual(prevPos.previous.id, "2.2");
+ assert.notOk(prevPos.next);
+ assert.notOk(prevPos.parent);
+ assert.strictEqual(prevPos.group.id, "2");
+ assert.strictEqual(placeholder.previousElementSibling.id, "1.2");
+ },
+ onGroupEnter({ element, group }) {
+ assert.step("enter");
+ assert.strictEqual(element.id, "2.2");
+ assert.strictEqual(group.id, "1");
+ },
+ onGroupLeave({ element, group }) {
+ assert.step("leave");
+ assert.strictEqual(element.id, "2.2");
+ assert.strictEqual(group.id, "2");
+ },
+ onDrop({ element, previous, next, parent, group, newGroup, placeholder }) {
+ assert.step("drop");
+ assert.strictEqual(element.id, "2.2");
+ assert.strictEqual(previous.id, "1.2");
+ assert.notOk(next);
+ assert.notOk(parent);
+ assert.strictEqual(group.id, "2");
+ assert.strictEqual(newGroup.id, "1");
+ assert.strictEqual(placeholder.previousElementSibling.id, "1.2");
+ },
+ onDragEnd({ element, group }) {
+ assert.step("end");
+ assert.strictEqual(element.id, "2.2");
+ assert.strictEqual(group.id, "2");
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, target);
+
+ assert.containsN(target, ".sortable_list", 3);
+ assert.containsN(target, ".item", 18);
+ assert.verifySteps([]);
+
+ // Append second item of second list to first list
+ await dragAndDrop("section:nth-child(2) > ul > .item:nth-child(2)", "section:first-child");
+
+ assert.containsN(target, ".sortable_list", 3);
+ assert.containsN(target, ".item", 18);
+ assert.verifySteps(["start", "move", "enter", "leave", "drop", "end"]);
+ });
+
+ QUnit.test("Sorting with nesting - move right", async (assert) => {
+ assert.expect(29);
+ class NestedSortable extends Component {
+ static template = xml`
+
+ `;
+
+ setup() {
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".item",
+ nest: true,
+ onDragStart({ element }) {
+ assert.step("start");
+ assert.strictEqual(element.id, "2");
+ this.firstMove = true;
+ },
+ onMove({ element, previous, next, parent, prevPos }) {
+ if (this.firstMove) {
+ assert.step("move 1");
+ assert.strictEqual(element.id, "2");
+ assert.strictEqual(previous.id, "sub1");
+ assert.notOk(next);
+ assert.strictEqual(parent.id, "1");
+ assert.strictEqual(prevPos.previous.id, "2");
+ assert.strictEqual(prevPos.next.id, "3");
+ assert.notOk(prevPos.parent);
+ this.firstMove = false;
+ } else {
+ assert.step("move 2");
+ assert.strictEqual(element.id, "2");
+ assert.notOk(previous);
+ assert.notOk(next);
+ assert.strictEqual(parent.id, "sub1");
+ assert.strictEqual(prevPos.previous.id, "sub1");
+ assert.notOk(prevPos.next);
+ assert.strictEqual(prevPos.parent.id, "1");
+ }
+ },
+ onDrop({ element, previous, next, parent }) {
+ assert.step("drop");
+ assert.strictEqual(element.id, "2");
+ assert.notOk(previous);
+ assert.notOk(next);
+ assert.strictEqual(parent.id, "sub1");
+ },
+ onDragEnd({ element }) {
+ assert.step("end");
+ assert.strictEqual(element.id, "2");
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, target);
+
+ assert.containsN(target, ".item", 6);
+ assert.verifySteps([]);
+
+ const movedEl = target.querySelector(".sortable_list > .item:nth-child(2)");
+ const { drop, moveTo } = await drag(movedEl);
+ await moveTo(movedEl, { x: movedEl.getBoundingClientRect().width / 2 + 15 });
+ await moveTo(movedEl, { x: movedEl.getBoundingClientRect().width / 2 + 30 });
+ // No move if row is already child
+ await drop(movedEl, { x: movedEl.getBoundingClientRect().width / 2 + 45 });
+
+ assert.containsN(target, ".item", 6);
+ assert.verifySteps(["start", "move 1", "move 2", "drop", "end"]);
+ });
+
+ QUnit.test("Sorting with nesting - move left", async (assert) => {
+ assert.expect(21);
+
+ class NestedSortable extends Component {
+ static template = xml`
+
+
+ -
+ parent
+
+ -
+ sub1
+
+
+ -
+ sub2
+
+
+
+
+
+ `;
+
+ setup() {
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".item",
+ nest: true,
+ nestInterval: 20,
+ onDragStart({ element }) {
+ assert.step("start");
+ assert.strictEqual(element.id, "dragged");
+ },
+ onMove({ element, previous, next, parent, prevPos }) {
+ assert.step("move");
+ assert.strictEqual(element.id, "dragged");
+ assert.strictEqual(previous.id, "sub1");
+ assert.strictEqual(next.id, "sub2");
+ assert.strictEqual(parent.id, "parent");
+ assert.strictEqual(prevPos.previous.id, "dragged");
+ assert.notOk(prevPos.next);
+ assert.strictEqual(prevPos.parent.id, "sub1");
+ },
+ onDrop({ element, previous, next, parent }) {
+ assert.step("drop");
+ assert.strictEqual(element.id, "dragged");
+ assert.strictEqual(previous.id, "sub1");
+ assert.strictEqual(next.id, "sub2");
+ assert.strictEqual(parent.id, "parent");
+ },
+ onDragEnd({ element }) {
+ assert.step("end");
+ assert.strictEqual(element.id, "dragged");
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, target);
+
+ assert.containsN(target, ".item", 4);
+ assert.verifySteps([]);
+
+ const movedEl = target.querySelector(".item#dragged");
+ const { drop, moveTo } = await drag(movedEl);
+ // No move if distance traveled is smaller than the nest interval
+ await moveTo(movedEl, { x: movedEl.getBoundingClientRect().width / 2 - 10 });
+ await moveTo(movedEl, { x: movedEl.getBoundingClientRect().width / 2 - 20 });
+ // No move if there is one element before and one after
+ await drop(movedEl, { x: movedEl.getBoundingClientRect().width / 2 - 40 });
+
+ assert.containsN(target, ".item", 4);
+ assert.verifySteps(["start", "move", "drop", "end"]);
+ });
+
+ QUnit.test("Sorting with nesting - move root down", async (assert) => {
+ assert.expect(28);
+
+ class NestedSortable extends Component {
+ static template = xml`
+
+
+ -
+ dragged
+
+ -
+ noChild
+
+ -
+ parent
+
+
+
+
+ `;
+
+ setup() {
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".item",
+ nest: true,
+ onDragStart({ element }) {
+ assert.step("start");
+ assert.strictEqual(element.id, "dragged");
+ this.firstMove = true;
+ },
+ onMove({ element, previous, next, parent, prevPos }) {
+ if (this.firstMove) {
+ assert.step("move 1");
+ assert.strictEqual(element.id, "dragged");
+ assert.strictEqual(previous.id, "noChild");
+ assert.strictEqual(next.id, "parent");
+ assert.notOk(parent);
+ assert.strictEqual(prevPos.previous.id, "dragged");
+ assert.strictEqual(prevPos.next.id, "noChild");
+ assert.notOk(prevPos.parent);
+ this.firstMove = false;
+ } else {
+ assert.step("move 2");
+ assert.strictEqual(element.id, "dragged");
+ assert.notOk(previous);
+ assert.strictEqual(next.id, "child");
+ assert.strictEqual(parent.id, "parent");
+ assert.strictEqual(prevPos.previous.id, "noChild");
+ assert.strictEqual(prevPos.next.id, "parent");
+ assert.notOk(prevPos.parent);
+ }
+ },
+ onDrop({ element, previous, next, parent }) {
+ assert.step("drop");
+ assert.strictEqual(element.id, "dragged");
+ assert.notOk(previous);
+ assert.strictEqual(next.id, "child");
+ assert.strictEqual(parent.id, "parent");
+ },
+ onDragEnd({ element }) {
+ assert.step("end");
+ assert.strictEqual(element.id, "dragged");
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, target);
+ assert.verifySteps([]);
+
+ const { drop, moveUnder } = await sortableDrag(".item#dragged");
+ await moveUnder(".item#noChild");
+ // Move under the content of the row, not under the rows nested inside the row
+ await moveUnder(".item#parent > span");
+ await drop();
+
+ assert.containsN(target, ".item", 4);
+ assert.verifySteps(["start", "move 1", "move 2", "drop", "end"]);
+ });
+
+ QUnit.test("Sorting with nesting - move child down", async (assert) => {
+ assert.expect(28);
+
+ class NestedSortable extends Component {
+ static template = xml`
+
+
+ -
+ parent
+
+ -
+ dragged
+
+ -
+ item
+
+
+
+ -
+ noChild
+
+
+
+ `;
+
+ setup() {
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".item",
+ nest: true,
+ onDragStart({ element }) {
+ assert.step("start");
+ assert.strictEqual(element.id, "dragged");
+ this.firstMove = true;
+ },
+ onMove({ element, previous, next, parent, prevPos }) {
+ if (this.firstMove) {
+ assert.step("move 1");
+ assert.strictEqual(element.id, "dragged");
+ assert.strictEqual(previous.id, "child");
+ assert.notOk(next);
+ assert.strictEqual(parent.id, "parent");
+ assert.strictEqual(prevPos.previous.id, "dragged");
+ assert.strictEqual(prevPos.next.id, "child");
+ assert.strictEqual(prevPos.parent.id, "parent");
+ this.firstMove = false;
+ } else {
+ assert.step("move 2");
+ assert.strictEqual(element.id, "dragged");
+ assert.strictEqual(previous.id, "noChild");
+ assert.notOk(next);
+ assert.notOk(parent);
+ assert.strictEqual(prevPos.previous.id, "child");
+ assert.notOk(prevPos.next);
+ assert.strictEqual(prevPos.parent.id, "parent");
+ }
+ },
+ onDrop({ element, previous, next, parent }) {
+ assert.step("drop");
+ assert.strictEqual(element.id, "dragged");
+ assert.strictEqual(previous.id, "noChild");
+ assert.notOk(next);
+ assert.notOk(parent);
+ },
+ onDragEnd({ element }) {
+ assert.step("end");
+ assert.strictEqual(element.id, "dragged");
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, target);
+ assert.verifySteps([]);
+
+ const { drop, moveUnder } = await sortableDrag(".item#dragged");
+ await moveUnder(".item#child");
+ await moveUnder(".item#noChild");
+ await drop();
+
+ assert.containsN(target, ".item", 4);
+ assert.verifySteps(["start", "move 1", "move 2", "drop", "end"]);
+ });
+
+ QUnit.test("Sorting with nesting - move root up", async (assert) => {
+ assert.expect(28);
+ class NestedSortable extends Component {
+ static template = xml`
+
+
+ -
+ parent
+
+
+ -
+ noChild
+
+ -
+ dragged
+
+
+
+ `;
+
+ setup() {
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".item",
+ nest: true,
+ onDragStart({ element }) {
+ assert.step("start");
+ assert.strictEqual(element.id, "dragged");
+ this.firstMove = true;
+ },
+ onMove({ element, previous, next, parent, prevPos }) {
+ if (this.firstMove) {
+ assert.step("move 1");
+ assert.strictEqual(element.id, "dragged");
+ assert.strictEqual(previous.id, "parent");
+ assert.strictEqual(next.id, "noChild");
+ assert.notOk(parent);
+ assert.strictEqual(prevPos.previous.id, "dragged");
+ assert.notOk(prevPos.next);
+ assert.notOk(prevPos.parent);
+ this.firstMove = false;
+ } else {
+ assert.step("move 2");
+ assert.strictEqual(element.id, "dragged");
+ assert.notOk(previous);
+ assert.strictEqual(next.id, "child");
+ assert.strictEqual(parent.id, "parent");
+ assert.strictEqual(prevPos.previous.id, "parent");
+ assert.strictEqual(prevPos.next.id, "noChild");
+ assert.notOk(prevPos.parent);
+ }
+ },
+ onDrop({ element, previous, next, parent }) {
+ assert.step("drop");
+ assert.strictEqual(element.id, "dragged");
+ assert.notOk(previous);
+ assert.strictEqual(next.id, "child");
+ assert.strictEqual(parent.id, "parent");
+ },
+ onDragEnd({ element }) {
+ assert.step("end");
+ assert.strictEqual(element.id, "dragged");
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, target);
+ assert.verifySteps([]);
+
+ const { drop, moveAbove } = await sortableDrag(".item#dragged");
+ await moveAbove(".item#noChild");
+ await moveAbove(".item#child");
+ await drop();
+
+ assert.containsN(target, ".item", 4);
+ assert.verifySteps(["start", "move 1", "move 2", "drop", "end"]);
+ });
+
+ QUnit.test("Sorting with nesting - move child up", async (assert) => {
+ assert.expect(28);
+
+ class NestedSortable extends Component {
+ static template = xml`
+
+
+ -
+ parent
+
+ -
+ child
+
+ -
+ dragged
+
+
+
+
+
+ `;
+
+ setup() {
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".item",
+ nest: true,
+ onDragStart({ element }) {
+ assert.step("start");
+ assert.strictEqual(element.id, "dragged");
+ this.firstMove = true;
+ },
+ onMove({ element, previous, next, parent, prevPos }) {
+ if (this.firstMove) {
+ assert.step("move 1");
+ assert.strictEqual(element.id, "dragged");
+ assert.notOk(previous);
+ assert.strictEqual(next.id, "child");
+ assert.strictEqual(parent.id, "parent");
+ assert.strictEqual(prevPos.previous.id, "dragged");
+ assert.notOk(prevPos.next);
+ assert.strictEqual(prevPos.parent.id, "parent");
+ this.firstMove = false;
+ } else {
+ assert.step("move 2");
+ assert.strictEqual(element.id, "dragged");
+ assert.notOk(previous);
+ assert.strictEqual(next.id, "parent");
+ assert.notOk(parent);
+ assert.notOk(prevPos.previous);
+ assert.strictEqual(prevPos.next.id, "child");
+ assert.strictEqual(prevPos.parent.id, "parent");
+ }
+ },
+ onDrop({ element, previous, next, parent }) {
+ assert.step("drop");
+ assert.strictEqual(element.id, "dragged");
+ assert.notOk(previous);
+ assert.strictEqual(next.id, "parent");
+ assert.notOk(parent);
+ },
+ onDragEnd({ element }) {
+ assert.step("end");
+ assert.strictEqual(element.id, "dragged");
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, target);
+ assert.verifySteps([]);
+
+ const { drop, moveAbove } = await sortableDrag(".item#dragged");
+ await moveAbove(".item#child");
+ await moveAbove(".item#parent");
+ await drop();
+
+ assert.containsN(target, ".item", 3);
+ assert.verifySteps(["start", "move 1", "move 2", "drop", "end"]);
+ });
+
+ QUnit.test("Dynamically disable NestedSortable feature", async (assert) => {
+ assert.expect(4);
+
+ const state = reactive({ enableNestedSortable: true });
+ class NestedSortable extends Component {
+ static template = xml`
+
+ `;
+
+ setup() {
+ this.state = useState(state);
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".item",
+ enable: () => this.state.enableNestedSortable,
+ onDragStart() {
+ assert.step("start");
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, target);
+
+ assert.verifySteps([]);
+
+ await dragAndDrop(".item:first-child", ".item:last-child");
+ // Drag should have occurred
+ assert.verifySteps(["start"]);
+
+ state.enableNestedSortable = false;
+ await nextTick();
+
+ await dragAndDrop(".item:first-child", ".item:last-child");
+
+ // Drag shouldn't have occurred
+ assert.verifySteps([]);
+ });
+
+ QUnit.test(
+ "Drag has a default tolerance of 10 pixels before initiating the dragging",
+ async (assert) => {
+ assert.expect(3);
+
+ class NestedSortable extends Component {
+ static template = xml`
+
+ `;
+
+ setup() {
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".item",
+ onDragStart() {
+ assert.step("Initiation of the drag sequence");
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, target);
+
+ // Move the element from only 5 pixels
+ const listItem = target.querySelector(".item:first-child");
+ const { drop, moveTo } = await drag(listItem);
+ await moveTo(listItem, {
+ x: listItem.getBoundingClientRect().width / 2,
+ y: listItem.getBoundingClientRect().height / 2 + 5,
+ });
+ assert.verifySteps([], "No drag sequence should have been initiated");
+
+ // Move the element from more than 10 pixels
+ await moveTo(listItem, {
+ x: listItem.getBoundingClientRect().width / 2,
+ y: listItem.getBoundingClientRect().height / 2 + 10,
+ });
+ assert.verifySteps(
+ ["Initiation of the drag sequence"],
+ "A drag sequence should have been initiated",
+ );
+ await drop();
+ },
+ );
+
+ QUnit.test("Ignore specified elements", async (assert) => {
+ assert.expect(6);
+
+ class NestedSortable extends Component {
+ static template = xml`
+
+ `;
+
+ setup() {
+ useNestedSortable({
+ ref: useRef("root"),
+ elements: ".item",
+ ignore: ".ignored",
+ onDragStart() {
+ assert.step("drag");
+ },
+ });
+ }
+ }
+
+ await mount(NestedSortable, 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([]);
+ });
+});