konva
Version:
HTML5 2d canvas library for interactive graphics, design editors, whiteboards, and diagrams.
204 lines (203 loc) • 8.71 kB
JavaScript
import { Util } from "./Util.js";
export const DD = {
get isDragging() {
let flag = false;
DD._dragElements.forEach((elem) => {
if (elem.dragStatus === 'dragging') {
flag = true;
}
});
return flag;
},
get node() {
let node;
for (const elem of DD._dragElements.values()) {
if (elem.dragStatus === 'dragging')
return elem.node;
node = elem.node;
}
// isDragReady() also uses this getter before movement starts.
return node;
},
_dragElements: new Map(),
// Konva is imported into one window, but a stage may be rendered in another
// one: an iframe, or a window opened with `window.open`. Such a window sends
// its pointer events to itself, so `Konva.Stage` asks for every window it is
// rendered in. The handlers get the window they listen to, as only that
// window has pointer positions the stages of that window can use
_listenToWindow(win) {
const endDragBefore = (evt) => DD._endDragBefore(evt, win);
const drag = (evt) => DD._batchEvents(() => DD._drag(evt, win), win);
const endDragAfter = (evt) => DD._batchEvents(() => DD._endDragAfter(evt));
win.addEventListener('mouseup', endDragBefore, true);
win.addEventListener('touchend', endDragBefore, true);
// add touchcancel to fix this: https://github.com/konvajs/konva/issues/1843
win.addEventListener('touchcancel', endDragBefore, true);
win.addEventListener('mousemove', drag);
win.addEventListener('touchmove', drag);
win.addEventListener('mouseup', endDragAfter, false);
win.addEventListener('touchend', endDragAfter, false);
win.addEventListener('touchcancel', endDragAfter, false);
},
_batchEvents(callback, win) {
if (!DD._dragElements.size)
return;
// One scope per integration, even when several stages use the same callback.
// Keep all positions and handlers in the original order across those stages.
const batches = new Set();
for (const { node } of DD._dragElements.values()) {
const stage = node.getStage();
if (!stage || (win && stage._getOwnerWindow() !== win))
continue;
const batch = stage.eventBatchFunc();
if (batch)
batches.add(batch);
}
let run = callback;
for (const batch of batches) {
const next = run;
run = () => Util._batchEvents(batch, next);
}
run();
},
// methods
_drag(evt, win) {
const nodesToFireEvents = [];
// reading the positions forces a layout, so do it once per stage
const positioned = new Set();
DD._dragElements.forEach((elem, key) => {
const { node } = elem;
// we need to find pointer relative to that node
const stage = node.getStage();
// a pointer position of another window is relative to another viewport,
// it would drag the node to a random place
if (win && stage._getOwnerWindow() !== win) {
return;
}
if (!positioned.has(stage)) {
stage.setPointersPositions(evt);
positioned.add(stage);
}
if (elem.pointerEventType &&
elem.pointerEventType !== stage._pointerEventType) {
return;
}
// it is possible that user call startDrag without any event
// it that case we need to detect first movable pointer and attach it into the node
if (elem.pointerId === undefined) {
elem.pointerId = Util._getFirstPointerId(evt);
elem.pointerEventType = stage._pointerEventType;
if (elem.dragStatus === 'dragging') {
stage._cancelClick(elem.pointerId, elem.pointerEventType);
}
}
const pos = stage._changedPointerPositions.find((pos) => pos.id === elem.pointerId);
// not related pointer
if (!pos) {
return;
}
if (elem.dragStatus !== 'dragging') {
const dragDistance = node.dragDistance();
const distance = Math.max(Math.abs(pos.x - elem.startPointerPos.x), Math.abs(pos.y - elem.startPointerPos.y));
if (distance < dragDistance) {
return;
}
node.startDrag({ evt });
// a user can stop dragging inside `dragstart`
if (!node.isDragging()) {
return;
}
}
node._setDragPosition(evt, elem);
nodesToFireEvents.push(node);
});
// call dragmove only after ALL positions are changed
nodesToFireEvents.forEach((node) => {
// node may have been destroyed during a previous dragmove handler
if (!node.getStage()) {
return;
}
node.fire('dragmove', {
type: 'dragmove',
target: node,
evt: evt,
}, true);
});
},
// dragBefore and dragAfter allows us to set correct order of events
// setup all in dragbefore, and stop dragging only after pointerup triggered.
_endDragBefore(evt, win, only) {
const drawNodes = [];
const positioned = new Set();
DD._dragElements.forEach((elem, key) => {
const { node } = elem;
// node.stopDrag() ends only the drags of the same gesture: a Transformer
// drags its other nodes with the same pointer, another pointer is its own
if (only && elem !== only && elem.pointerId !== only.pointerId) {
return;
}
// we need to find pointer relative to that node
const stage = node.getStage();
// a pointer released in another window ends the drag too - the pointer
// is up everywhere - but its position is not used for this stage
if (evt &&
!positioned.has(stage) &&
(!win || stage._getOwnerWindow() === win)) {
stage.setPointersPositions(evt);
positioned.add(stage);
}
if (!only) {
if (evt &&
elem.pointerEventType &&
elem.pointerEventType !== Util._getEventType(evt.type)) {
return;
}
// a drag started without an event (node.startDrag()) that has not
// moved yet has no pointer: any release ends it
const released = elem.pointerId === undefined ||
stage._changedPointerPositions.some((pos) => pos.id === elem.pointerId);
// that pointer is not related: a "ready" element of another pointer
// waits for its own release
if (!released) {
return;
}
}
if (elem.dragStatus === 'ready') {
DD._dragElements.delete(key);
}
else {
// if a node is stopped manually we still need to reset events:
stage._cancelClick(elem.pointerId, elem.pointerEventType);
elem.dragStatus = 'stopped';
// a node that has not moved needs no redraw
const drawNode = node.getLayer() || stage;
if (drawNode && drawNodes.indexOf(drawNode) === -1) {
drawNodes.push(drawNode);
}
}
});
// draw in a sync way
// because mousemove event may trigger BEFORE batch draw is called
// but as we have not hit canvas updated yet, it will trigger incorrect mouseover/mouseout events
drawNodes.forEach((drawNode) => {
drawNode.draw();
});
},
_endDragAfter(evt, only) {
DD._dragElements.forEach((elem, key) => {
if (elem.dragStatus !== 'stopped' ||
(only && elem !== only && elem.pointerId !== only.pointerId)) {
return;
}
elem.node.fire('dragend', {
type: 'dragend',
target: elem.node,
evt: evt,
}, true);
// a dragend handler may have started a new drag of the node
if (elem.dragStatus !== 'dragging') {
DD._dragElements.delete(key);
}
});
},
};