Skip to Content

useDraggable і useSortable: готовий drag-and-drop для OWL-компонентів

Задача: перетягнути елемент мишкою у власному OWL-компоненті — картку між колонками дошки, рядок у списку, маркер на карті. Перший інстинкт — узяти нативний HTML5 Drag and Drop API: draggable="true", dragstart/dragover/drop, dataTransfer. Він працює в будь-якому браузері і не потребує бібліотек. Ось так це зазвичай і пишуть:

Неправильно. Ручні нативні обробники:

class TaskBoard extends Component {
    static template = "my_module.TaskBoard";

    setup() {
        this.orm = useService("orm");
    }

    onDragStart(ev, taskId) {
        ev.dataTransfer.setData("text/plain", taskId);
    }

    onDragOver(ev) {
        ev.preventDefault(); // без цього drop не спрацює
    }

    async onDrop(ev, stageId) {
        const taskId = parseInt(ev.dataTransfer.getData("text/plain"));
        await this.orm.write("project.task", [taskId], { stage_id: stageId });
    }
}
<div t-foreach="tasks" t-as="task" t-key="task.id"
     draggable="true" t-on-dragstart="(ev) => this.onDragStart(ev, task.id)">
    <t t-esc="task.name"/>
</div>
<div t-on-dragover="onDragOver" t-on-drop="(ev) => this.onDrop(ev, stage.id)"/>

Працює. Але dataTransfer — це нативне HTML5 Drag and Drop API, а воно взагалі не породжує події dragstart/dragover/drop на тач-екранах — там немає такого поняття драгу мишкою. Плейсхолдер під час перетягування, скрол краю екрана при підведенні курсора до межі, обмеження «перетягувати можна тільки за цей елемент» — усе це довелось би дописувати руками.

У ядрі Odoo є готове рішення, і воно там не випадково: useDraggable (@web/core/utils/draggable.js) і useSortable (@web/core/utils/sortable_owl.js). Обидва хуки — надбудова над одним білдером makeDraggableHook (draggable_hook_builder.js), а Owl-версія (draggable_hook_builder_owl.js) підключає до нього useEffect, useExternalListener і reactive — тобто хук сам стежить за mount/unmount компонента, ви нічого вручну не відписуєте.

Важливо! Ці хуки НЕ побудовані на нативному HTML5 Drag and Drop API. У draggable_hook_builder.js слухач вішається на pointerdown/pointermove/pointerup, а на mousedown/mousemove/mouseup перемикається автоматично — це внутрішня перевірка useMouseEvents для Firefox без touch-підтримки, коли переданий iframeWindow, а не параметр, який задає розробник. Тому й тач-екрани підтримуються нативно — є навіть окремий параметр touchDelay.

Правильно. Для дошки з колонками (найближчий кейс до задачі вище) — useSortable:

import { useSortable } from "@web/core/utils/sortable_owl";

class TaskBoard extends Component {
    static template = "my_module.TaskBoard";

    setup() {
        this.orm = useService("orm");
        this.rootRef = useRef("root");

        useSortable({
            ref: this.rootRef,
            elements: ".o_task_card",
            groups: ".o_task_column",
            connectGroups: true,
            handle: ".o_task_handle",
            cursor: "move",
            onDrop: async ({ element, parent }) => {
                const taskId = Number(element.dataset.id);
                const stageId = Number(parent.dataset.id);
                await this.orm.write("project.task", [taskId], { stage_id: stageId });
            },
        });
    }
}

Тут ми задаємо ref — кореневий елемент (t-ref="root" у шаблоні), elements — селектор карток всередині нього, groups — селектор контейнерів-колонок, connectGroups дозволяє переносити картку в іншу колонку (без нього сортувати можна тільки всередині одного контейнера). handle обмежує точку захоплення — корисно, якщо всередині картки є кнопки, які не мають запускати драг. onDrop отримує не тільки element, а й group, previous, next, parent — цього досить, щоб одразу переписати і stage_id, і sequence. Плейсхолдер, підсвітка колонки під курсором, автоскрол — усе це вже всередині хука, за замовчуванням.

Саме так, до речі, влаштований kanban-view в самому ядрі: kanban_renderer.js викликає useSortable двічі — один раз для карток (elements: ".o_draggable", groups: ".o_kanban_group", connectGroups), другий раз для самих колонок (elements: ".o_group_draggable", handle: ".o_column_title").

Якщо задача не «впорядкувати список», а вільне позиціювання — маркер на карті, стіл на плані залу — сортування не підходить. Я б і тут не хапався за найочевидніший варіант: публічний useDraggable (@web/core/utils/draggable.js) виглядає як готове рішення, але в нього followCursor завжди true — хук сам пересуває елемент за курсором, з офсетом і клемпінгом до контейнера, а публічного параметра, щоб це вимкнути, немає. Якщо потрібні сирі координати без автопозиціювання — доведеться зібрати свій хук на тому ж білдері makeDraggableHook (draggable_hook_builder_owl.js) і вимкнути followCursor через onComputeParams:

import { pick } from "@web/core/utils/objects";
import { makeDraggableHook } from "@web/core/utils/draggable_hook_builder_owl";

const useMovable = makeDraggableHook({
    name: "useMovable",
    onComputeParams: ({ ctx }) => {
        ctx.followCursor = false;
    },
    onWillStartDrag: ({ ctx }) => pick(ctx.current, "element"),
    onDrag: ({ ctx }) => pick(ctx.current, "element"),
    onDrop: ({ ctx }) => pick(ctx.current, "element"),
});

У setup() компонента синтаксис виклику той самий, що і для публічного useDraggable, — просто хук тепер називається useMovable:

useMovable({
    ref: this.mapRef,
    elements: ".o_movable_marker",
    onDrag: ({ element, x, y }) => {
        element.style.left = `${x}px`;
        element.style.top = `${y}px`;
    },
    onDrop: ({ element }) => {
        // тут x/y уже кінцеві — записуємо в модель
    },
});

План залу в POS (floor_screen.js) зроблено так само: не через публічний useDraggable, а через свій локальний хук на тому ж білдері — той самий патерн, що і вище, з вимкненим followCursor. onDrag отримує { element, x, y } і напряму рахує позицію столу на карті, а onDrop фіксує координати в restaurant.table. Ну ок, різниця з нативним варіантом — рядків двадцять коду. Але саме ці двадцять рядків — плейсхолдер, автоскрол, робота на тач-екрані, обмеження за handle — і є те, що довелось би писати руками при виборі нативного шляху.

Тема за матеріалом https://www.cybrosys.com/blog/how-to-implement-drag-and-drop-in-owl-components. Код і поведінку перевірено на Odoo 19.0.

useDraggable і useSortable: готовий drag-and-drop для OWL-компонентів
KitWorks, Volodymyr Karabanov 14 липня 2026 р.
Поділитися цією публікацією
Теги
Архів