diff --git a/addons/web/static/src/core/utils/draggable.js b/addons/web/static/src/core/utils/draggable.js new file mode 100644 index 00000000000..9d6e0ae8d22 --- /dev/null +++ b/addons/web/static/src/core/utils/draggable.js @@ -0,0 +1,62 @@ +/** @odoo-module **/ + +import { makeDraggableHook } from "@web/core/utils/draggable_hook_builder"; + +/** + * @typedef DraggableParams + * + * MANDATORY + * + * @property {{ el: HTMLElement | null }} ref + * @property {string} elements defines draggable elements + * + * OPTIONAL + * + * @property {boolean | () => boolean} [enable] whether the draggable system should + * be enabled. + * @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 {string | () => string} [cursor] cursor style during the dragging sequence. + * + * HANDLERS (also optional) + * + * @property {(DraggableHandlerParams) => any} [onDragStart] + * called when a dragging sequence is initiated. + * @property {(DraggableHandlerParams) => any} [onDrag] + * called on each "mousemove" during the drag sequence. + * @property {(DraggableHandlerParams) => any} [onDragEnd] + * called when the dragging sequence ends, regardless of the reason. + * @property {(DraggableHandlerParams) => any} [onDrop] called when the dragging sequence + * ends on a mouseup action. + */ + +/** + * @typedef DraggableHandlerParams + * @property {number} x current mouse position on the X axis + * @property {number} y current mouse position on the Y axis + * @property {HTMLElement} element + */ + +/** + * @typedef DraggableState + * @property {boolean} dragging + */ + +/** @type {(params: DraggableParams) => DraggableState} */ +export const useDraggable = makeDraggableHook({ + name: "useDraggable", + onDragStart({ ctx, helpers }) { + helpers.execHandler("onDragStart", { element: ctx.currentElement }); + }, + onDrag({ ctx, helpers }) { + helpers.execHandler("onDrag", { element: ctx.currentElement }); + }, + onDragEnd({ ctx, helpers }) { + helpers.execHandler("onDragEnd", { element: ctx.currentElement }); + }, + onDrop({ ctx, helpers }) { + helpers.execHandler("onDrop", { element: ctx.currentElement }); + }, +}); 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 21a7e2d1780..1bc6bf93d80 100644 --- a/addons/web/static/src/core/utils/draggable_hook_builder.js +++ b/addons/web/static/src/core/utils/draggable_hook_builder.js @@ -9,6 +9,26 @@ import { debounce } from "@web/core/utils/timing"; * @property {number} y */ +/** + * @typedef DraggableBuilderParams + * + * Hook params + * @property {string} [name="useAnonymousDraggable"] + * @property {Record} [acceptedParams] + * + * Build handlers + * @property {(params: DraggableBuilderHookParams) => any} onBuildSetup + * @property {(params: DraggableBuilderHookParams) => any} onComputeParams + * + * Runtime handlers + * @property {(params: DraggableBuilderHookParams) => any} onWillStartDrag + * @property {(params: DraggableBuilderHookParams) => any} onDragStart + * @property {(params: DraggableBuilderHookParams) => any} onDrag + * @property {(params: DraggableBuilderHookParams) => any} onDragEnd + * @property {(params: DraggableBuilderHookParams) => any} onDrop + * @property {(params: DraggableBuilderHookParams) => any} onCleanup + */ + /** * @typedef DraggableHookRunningContext * @property {{ el: HTMLElement | null }} ref @@ -25,6 +45,15 @@ import { debounce } from "@web/core/utils/timing"; * @property {Position} [offset={ x: 0, y: 0 }] */ +/** + * @typedef DraggableBuilderHookParams + * @property {DraggableHookRunningContext} ctx + * @property {Object} helpers + * @property {Function} helpers.addListener + * @property {Function} helpers.addStyle + * @property {Function} helpers.execHandler + */ + const { useEffect, useEnv, useExternalListener, onWillUnmount, reactive } = owl; const DEFAULT_ACCEPTED_PARAMS = { @@ -57,6 +86,10 @@ function cssValueToNumber(val) { return Number(val.slice(0, -2)); } +/** + * @param {DraggableBuilderParams} hookParams + * @returns {(params: Record) => { dragging: boolean }} + */ export function makeDraggableHook(hookParams = {}) { const hookName = hookParams.name || "useAnonymousDraggable"; const allAcceptedParams = { ...DEFAULT_ACCEPTED_PARAMS, ...hookParams.acceptedParams }; diff --git a/addons/web/static/src/core/utils/sortable.js b/addons/web/static/src/core/utils/sortable.js index 62f023f5fc4..ff885eddaa8 100644 --- a/addons/web/static/src/core/utils/sortable.js +++ b/addons/web/static/src/core/utils/sortable.js @@ -27,17 +27,17 @@ import { makeDraggableHook } from "@web/core/utils/draggable_hook_builder"; * * HANDLERS (also optional) * - * @property {(group: HTMLElement | null, element: HTMLElement) => any} [onStart] + * @property {({ element: HTMLElement, group: HTMLElement | null }) => any} [onDragStart] * called when a dragging sequence is initiated. - * @property {(element: HTMLElement) => any} [onElementEnter] called when the cursor + * @property {({ element: HTMLElement }) => any} [onElementEnter] called when the cursor * enters another sortable element. - * @property {(element: HTMLElement) => any} [onElementLeave] called when the cursor + * @property {({ element: HTMLElement }) => any} [onElementLeave] called when the cursor * leaves another sortable element. - * @property {(group: HTMLElement) => any} [onGroupEnter] (if a `groups` is specified): + * @property {({ group: HTMLElement }) => any} [onGroupEnter] (if a `groups` is specified): * will be called when the cursor enters another group element. - * @property {(group: HTMLElement) => any} [onGroupLeave] (if a `groups` is specified): + * @property {({ group: HTMLElement }) => any} [onGroupLeave] (if a `groups` is specified): * will be called when the cursor leaves another group element. - * @property {(group: HTMLElement | null, element: HTMLElement) => any} [onStop] + * @property {({ element: HTMLElement group: HTMLElement | null }) => any} [onDragEnd] * called when the dragging sequence ends, regardless of the reason. * @property {(params: DropParams) => any} [onDrop] called when the dragging sequence * ends on a mouseup action AND the dragged element has been moved elsewhere. The @@ -54,6 +54,12 @@ import { makeDraggableHook } from "@web/core/utils/draggable_hook_builder"; * @property {HTMLElement | null} parent */ +/** + * @typedef SortableState + * @property {boolean} dragging + */ + +/** @type {(params: SortableParams) => SortableState} */ export const useSortable = makeDraggableHook({ name: "useSortable", acceptedParams: { @@ -159,11 +165,14 @@ export const useSortable = makeDraggableHook({ // Ghost is initially added right after the current element. ctx.currentElement.after(ctx.ghostElement); - // Calls "onStart" handler - helpers.execHandler("onStart", { element: ctx.currentElement, group: ctx.currentGroup }); + // Calls "onDragStart" handler + helpers.execHandler("onDragStart", { + element: ctx.currentElement, + group: ctx.currentGroup, + }); }, onDragEnd({ ctx, helpers }) { - helpers.execHandler("onStop", { element: ctx.currentElement, group: ctx.currentGroup }); + helpers.execHandler("onDragEnd", { element: ctx.currentElement, group: ctx.currentGroup }); }, onDrop({ ctx, helpers }) { const previous = ctx.ghostElement.previousElementSibling; diff --git a/addons/web/static/src/views/kanban/kanban_renderer.js b/addons/web/static/src/views/kanban/kanban_renderer.js index 0e4a58a52c3..60bb00b7b13 100644 --- a/addons/web/static/src/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/views/kanban/kanban_renderer.js @@ -56,13 +56,13 @@ export class KanbanRenderer extends Component { connectGroups: () => this.canMoveRecords, cursor: "move", // Hooks - onStart: (params) => { + onDragStart: (params) => { const { element, group } = params; dataRecordId = element.dataset.id; dataGroupId = group && group.dataset.id; return this.sortStart(params); }, - onStop: (params) => this.sortStop(params), + onDragEnd: (params) => this.sortStop(params), onGroupEnter: (params) => this.sortRecordGroupEnter(params), onGroupLeave: (params) => this.sortRecordGroupLeave(params), onDrop: (params) => this.sortRecordDrop(dataRecordId, dataGroupId, params), @@ -75,12 +75,12 @@ export class KanbanRenderer extends Component { handle: ".o_column_title", cursor: "move", // Hooks - onStart: (params) => { + onDragStart: (params) => { const { element } = params; dataGroupId = element.dataset.id; return this.sortStart(params); }, - onStop: (params) => this.sortStop(params), + onDragEnd: (params) => this.sortStop(params), onDrop: (params) => this.sortGroupDrop(dataGroupId, params), }); } diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js index 98c270a16c7..9d920579e41 100644 --- a/addons/web/static/src/views/list/list_renderer.js +++ b/addons/web/static/src/views/list/list_renderer.js @@ -116,12 +116,12 @@ export class ListRenderer extends Component { handle: ".o_handle_cell", cursor: "grabbing", // Hooks - onStart: (params) => { + onDragStart: (params) => { const { element } = params; dataRowId = element.dataset.id; return this.sortStart(params); }, - onStop: (params) => this.sortStop(params), + onDragEnd: (params) => this.sortStop(params), onDrop: (params) => this.sortDrop(dataRowId, params), }); diff --git a/addons/web/static/tests/core/utils/ui_tests.js b/addons/web/static/tests/core/utils/ui_tests.js index 63dbaba19ed..b871f32442d 100644 --- a/addons/web/static/tests/core/utils/ui_tests.js +++ b/addons/web/static/tests/core/utils/ui_tests.js @@ -95,7 +95,7 @@ QUnit.module("UI", ({ beforeEach }) => { useSortable({ ref: useRef("root"), elements: ".item", - onStart({ element, group }) { + onDragStart({ element, group }) { assert.step("start"); assert.notOk(group); assert.strictEqual(element.innerText, "1"); @@ -104,7 +104,7 @@ QUnit.module("UI", ({ beforeEach }) => { assert.step("elemententer"); assert.strictEqual(element.innerText, "2"); }, - onStop({ element, group }) { + onDragEnd({ element, group }) { assert.step("stop"); assert.notOk(group); assert.strictEqual(element.innerText, "1"); @@ -151,7 +151,7 @@ QUnit.module("UI", ({ beforeEach }) => { elements: ".item", groups: ".list", connectGroups: true, - onStart({ element, group }) { + onDragStart({ element, group }) { assert.step("start"); assert.hasClass(group, "list2"); assert.strictEqual(element.innerText, "2 1"); @@ -160,7 +160,7 @@ QUnit.module("UI", ({ beforeEach }) => { assert.step("groupenter"); assert.hasClass(group, "list1"); }, - onStop({ element, group }) { + onDragEnd({ element, group }) { assert.step("stop"); assert.hasClass(group, "list2"); assert.strictEqual(element.innerText, "2 1"); @@ -209,7 +209,7 @@ QUnit.module("UI", ({ beforeEach }) => { ref: useRef("root"), elements: ".item", enable: () => this.state.enableSortable, - onStart() { + onDragStart() { assert.step("start"); }, }); @@ -251,7 +251,7 @@ QUnit.module("UI", ({ beforeEach }) => { useSortable({ ref: useRef("root"), elements: ".item", - onStart() { + onDragStart() { throw new Error("Shouldn't start the sortable feature."); }, }); @@ -284,7 +284,7 @@ QUnit.module("UI", ({ beforeEach }) => { ref: useRef("root"), elements: ".item", ignore: ".ignored", - onStart() { + onDragStart() { assert.step("drag"); }, });