123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549 |
- /* global TouchEvent */
- var round = Math.round;
- import { assign } from 'min-dash';
- import {
- event as domEvent
- } from 'min-dom';
- import {
- getOriginal,
- toPoint,
- stopPropagation
- } from '../../util/Event';
- import {
- set as cursorSet,
- unset as cursorUnset
- } from '../../util/Cursor';
- import {
- install as installClickTrap
- } from '../../util/ClickTrap';
- import {
- delta as deltaPos
- } from '../../util/PositionUtil';
- var DRAG_ACTIVE_CLS = 'djs-drag-active';
- function preventDefault(event) {
- event.preventDefault();
- }
- function isTouchEvent(event) {
- // check for TouchEvent being available first
- // (i.e. not available on desktop Firefox)
- return typeof TouchEvent !== 'undefined' && event instanceof TouchEvent;
- }
- function getLength(point) {
- return Math.sqrt(Math.pow(point.x, 2) + Math.pow(point.y, 2));
- }
- /**
- * A helper that fires canvas localized drag events and realizes
- * the general "drag-and-drop" look and feel.
- *
- * Calling {@link Dragging#activate} activates dragging on a canvas.
- *
- * It provides the following:
- *
- * * emits life cycle events, namespaced with a prefix assigned
- * during dragging activation
- * * sets and restores the cursor
- * * sets and restores the selection
- * * ensures there can be only one drag operation active at a time
- *
- * Dragging may be canceled manually by calling {@link Dragging#cancel}
- * or by pressing ESC.
- *
- *
- * ## Life-cycle events
- *
- * Dragging can be in three different states, off, initialized
- * and active.
- *
- * (1) off: no dragging operation is in progress
- * (2) initialized: a new drag operation got initialized but not yet
- * started (i.e. because of no initial move)
- * (3) started: dragging is in progress
- *
- * Eventually dragging will be off again after a drag operation has
- * been ended or canceled via user click or ESC key press.
- *
- * To indicate transitions between these states dragging emits generic
- * life-cycle events with the `drag.` prefix _and_ events namespaced
- * to a prefix choosen by a user during drag initialization.
- *
- * The following events are emitted (appropriately prefixed) via
- * the {@link EventBus}.
- *
- * * `init`
- * * `start`
- * * `move`
- * * `end`
- * * `ended` (dragging already in off state)
- * * `cancel` (only if previously started)
- * * `canceled` (dragging already in off state, only if previously started)
- * * `cleanup`
- *
- *
- * @example
- *
- * function MyDragComponent(eventBus, dragging) {
- *
- * eventBus.on('mydrag.start', function(event) {
- * console.log('yes, we start dragging');
- * });
- *
- * eventBus.on('mydrag.move', function(event) {
- * console.log('canvas local coordinates', event.x, event.y, event.dx, event.dy);
- *
- * // local drag data is passed with the event
- * event.context.foo; // "BAR"
- *
- * // the original mouse event, too
- * event.originalEvent; // MouseEvent(...)
- * });
- *
- * eventBus.on('element.click', function(event) {
- * dragging.init(event, 'mydrag', {
- * cursor: 'grabbing',
- * data: {
- * context: {
- * foo: "BAR"
- * }
- * }
- * });
- * });
- * }
- */
- export default function Dragging(eventBus, canvas, selection) {
- var defaultOptions = {
- threshold: 5,
- trapClick: true
- };
- // the currently active drag operation
- // dragging is active as soon as this context exists.
- //
- // it is visually _active_ only when a context.active flag is set to true.
- var context;
- /* convert a global event into local coordinates */
- function toLocalPoint(globalPosition) {
- var viewbox = canvas.viewbox();
- var clientRect = canvas._container.getBoundingClientRect();
- return {
- x: viewbox.x + (globalPosition.x - clientRect.left) / viewbox.scale,
- y: viewbox.y + (globalPosition.y - clientRect.top) / viewbox.scale
- };
- }
- // helpers
- function fire(type, dragContext) {
- dragContext = dragContext || context;
- var event = eventBus.createEvent(
- assign(
- {},
- dragContext.payload,
- dragContext.data,
- { isTouch: dragContext.isTouch }
- )
- );
- // default integration
- if (eventBus.fire('drag.' + type, event) === false) {
- return false;
- }
- return eventBus.fire(dragContext.prefix + '.' + type, event);
- }
- // event listeners
- function move(event, activate) {
- var payload = context.payload,
- displacement = context.displacement;
- var globalStart = context.globalStart,
- globalCurrent = toPoint(event),
- globalDelta = deltaPos(globalCurrent, globalStart);
- var localStart = context.localStart,
- localCurrent = toLocalPoint(globalCurrent),
- localDelta = deltaPos(localCurrent, localStart);
- // activate context explicitly or once threshold is reached
- if (!context.active && (activate || getLength(globalDelta) > context.threshold)) {
- // fire start event with original
- // starting coordinates
- assign(payload, {
- x: round(localStart.x + displacement.x),
- y: round(localStart.y + displacement.y),
- dx: 0,
- dy: 0
- }, { originalEvent: event });
- if (false === fire('start')) {
- return cancel();
- }
- context.active = true;
- // unset selection and remember old selection
- // the previous (old) selection will always passed
- // with the event via the event.previousSelection property
- if (!context.keepSelection) {
- payload.previousSelection = selection.get();
- selection.select(null);
- }
- // allow custom cursor
- if (context.cursor) {
- cursorSet(context.cursor);
- }
- // indicate dragging via marker on root element
- canvas.addMarker(canvas.getRootElement(), DRAG_ACTIVE_CLS);
- }
- stopPropagation(event);
- if (context.active) {
- // update payload with actual coordinates
- assign(payload, {
- x: round(localCurrent.x + displacement.x),
- y: round(localCurrent.y + displacement.y),
- dx: round(localDelta.x),
- dy: round(localDelta.y)
- }, { originalEvent: event });
- // emit move event
- fire('move');
- }
- }
- function end(event) {
- var previousContext,
- returnValue = true;
- if (context.active) {
- if (event) {
- context.payload.originalEvent = event;
- // suppress original event (click, ...)
- // because we just ended a drag operation
- stopPropagation(event);
- }
- // implementations may stop restoring the
- // original state (selections, ...) by preventing the
- // end events default action
- returnValue = fire('end');
- }
- if (returnValue === false) {
- fire('rejected');
- }
- previousContext = cleanup(returnValue !== true);
- // last event to be fired when all drag operations are done
- // at this point in time no drag operation is in progress anymore
- fire('ended', previousContext);
- }
- // cancel active drag operation if the user presses
- // the ESC key on the keyboard
- function checkCancel(event) {
- if (event.which === 27) {
- preventDefault(event);
- cancel();
- }
- }
- // prevent ghost click that might occur after a finished
- // drag and drop session
- function trapClickAndEnd(event) {
- var untrap;
- // trap the click in case we are part of an active
- // drag operation. This will effectively prevent
- // the ghost click that cannot be canceled otherwise.
- if (context.active) {
- untrap = installClickTrap(eventBus);
- // remove trap after minimal delay
- setTimeout(untrap, 400);
- // prevent default action (click)
- preventDefault(event);
- }
- end(event);
- }
- function trapTouch(event) {
- move(event);
- }
- // update the drag events hover (djs.model.Base) and hoverGfx (Snap<SVGElement>)
- // properties during hover and out and fire {prefix}.hover and {prefix}.out properties
- // respectively
- function hover(event) {
- var payload = context.payload;
- payload.hoverGfx = event.gfx;
- payload.hover = event.element;
- fire('hover');
- }
- function out(event) {
- fire('out');
- var payload = context.payload;
- payload.hoverGfx = null;
- payload.hover = null;
- }
- // life-cycle methods
- function cancel(restore) {
- var previousContext;
- if (!context) {
- return;
- }
- var wasActive = context.active;
- if (wasActive) {
- fire('cancel');
- }
- previousContext = cleanup(restore);
- if (wasActive) {
- // last event to be fired when all drag operations are done
- // at this point in time no drag operation is in progress anymore
- fire('canceled', previousContext);
- }
- }
- function cleanup(restore) {
- var previousContext,
- endDrag;
- fire('cleanup');
- // reset cursor
- cursorUnset();
- if (context.trapClick) {
- endDrag = trapClickAndEnd;
- } else {
- endDrag = end;
- }
- // reset dom listeners
- domEvent.unbind(document, 'mousemove', move);
- domEvent.unbind(document, 'dragstart', preventDefault);
- domEvent.unbind(document, 'selectstart', preventDefault);
- domEvent.unbind(document, 'mousedown', endDrag, true);
- domEvent.unbind(document, 'mouseup', endDrag, true);
- domEvent.unbind(document, 'keyup', checkCancel);
- domEvent.unbind(document, 'touchstart', trapTouch, true);
- domEvent.unbind(document, 'touchcancel', cancel, true);
- domEvent.unbind(document, 'touchmove', move, true);
- domEvent.unbind(document, 'touchend', end, true);
- eventBus.off('element.hover', hover);
- eventBus.off('element.out', out);
- // remove drag marker on root element
- canvas.removeMarker(canvas.getRootElement(), DRAG_ACTIVE_CLS);
- // restore selection, unless it has changed
- var previousSelection = context.payload.previousSelection;
- if (restore !== false && previousSelection && !selection.get().length) {
- selection.select(previousSelection);
- }
- previousContext = context;
- context = null;
- return previousContext;
- }
- /**
- * Initialize a drag operation.
- *
- * If `localPosition` is given, drag events will be emitted
- * relative to it.
- *
- * @param {MouseEvent|TouchEvent} [event]
- * @param {Point} [localPosition] actual diagram local position this drag operation should start at
- * @param {String} prefix
- * @param {Object} [options]
- */
- function init(event, relativeTo, prefix, options) {
- // only one drag operation may be active, at a time
- if (context) {
- cancel(false);
- }
- if (typeof relativeTo === 'string') {
- options = prefix;
- prefix = relativeTo;
- relativeTo = null;
- }
- options = assign({}, defaultOptions, options || {});
- var data = options.data || {},
- originalEvent,
- globalStart,
- localStart,
- endDrag,
- isTouch;
- if (options.trapClick) {
- endDrag = trapClickAndEnd;
- } else {
- endDrag = end;
- }
- if (event) {
- originalEvent = getOriginal(event) || event;
- globalStart = toPoint(event);
- stopPropagation(event);
- // prevent default browser dragging behavior
- if (originalEvent.type === 'dragstart') {
- preventDefault(originalEvent);
- }
- } else {
- originalEvent = null;
- globalStart = { x: 0, y: 0 };
- }
- localStart = toLocalPoint(globalStart);
- if (!relativeTo) {
- relativeTo = localStart;
- }
- isTouch = isTouchEvent(originalEvent);
- context = assign({
- prefix: prefix,
- data: data,
- payload: {},
- globalStart: globalStart,
- displacement: deltaPos(relativeTo, localStart),
- localStart: localStart,
- isTouch: isTouch
- }, options);
- // skip dom registration if trigger
- // is set to manual (during testing)
- if (!options.manual) {
- // add dom listeners
- if (isTouch) {
- domEvent.bind(document, 'touchstart', trapTouch, true);
- domEvent.bind(document, 'touchcancel', cancel, true);
- domEvent.bind(document, 'touchmove', move, true);
- domEvent.bind(document, 'touchend', end, true);
- } else {
- // assume we use the mouse to interact per default
- domEvent.bind(document, 'mousemove', move);
- // prevent default browser drag and text selection behavior
- domEvent.bind(document, 'dragstart', preventDefault);
- domEvent.bind(document, 'selectstart', preventDefault);
- domEvent.bind(document, 'mousedown', endDrag, true);
- domEvent.bind(document, 'mouseup', endDrag, true);
- }
- domEvent.bind(document, 'keyup', checkCancel);
- eventBus.on('element.hover', hover);
- eventBus.on('element.out', out);
- }
- fire('init');
- if (options.autoActivate) {
- move(event, true);
- }
- }
- // cancel on diagram destruction
- eventBus.on('diagram.destroy', cancel);
- // API
- this.init = init;
- this.move = move;
- this.hover = hover;
- this.out = out;
- this.end = end;
- this.cancel = cancel;
- // for introspection
- this.context = function() {
- return context;
- };
- this.setOptions = function(options) {
- assign(defaultOptions, options);
- };
- }
- Dragging.$inject = [
- 'eventBus',
- 'canvas',
- 'selection'
- ];
|