518 lines
11 KiB
JavaScript
518 lines
11 KiB
JavaScript
import {
|
|
forEach,
|
|
assign
|
|
} from 'min-dash';
|
|
|
|
import {
|
|
delegate as domDelegate,
|
|
query as domQuery,
|
|
queryAll as domQueryAll
|
|
} from 'min-dom';
|
|
|
|
import {
|
|
isPrimaryButton,
|
|
isAuxiliaryButton
|
|
} from '../../util/Mouse';
|
|
|
|
import {
|
|
append as svgAppend,
|
|
attr as svgAttr,
|
|
create as svgCreate,
|
|
remove as svgRemove
|
|
} from 'tiny-svg';
|
|
|
|
import {
|
|
createLine,
|
|
updateLine
|
|
} from '../../util/RenderUtil';
|
|
|
|
/**
|
|
* @typedef {import('../../model').Base} Base
|
|
*
|
|
* @typedef {import('../../core/ElementRegistry').default} ElementRegistry
|
|
* @typedef {import('../../core/EventBus').default} EventBus
|
|
* @typedef {import('../../draw/Styles').default} Styles
|
|
*/
|
|
|
|
function allowAll(event) { return true; }
|
|
|
|
function allowPrimaryAndAuxiliary(event) {
|
|
return isPrimaryButton(event) || isAuxiliaryButton(event);
|
|
}
|
|
|
|
var LOW_PRIORITY = 500;
|
|
|
|
|
|
/**
|
|
* A plugin that provides interaction events for diagram elements.
|
|
*
|
|
* It emits the following events:
|
|
*
|
|
* * element.click
|
|
* * element.contextmenu
|
|
* * element.dblclick
|
|
* * element.hover
|
|
* * element.mousedown
|
|
* * element.mousemove
|
|
* * element.mouseup
|
|
* * element.out
|
|
*
|
|
* Each event is a tuple { element, gfx, originalEvent }.
|
|
*
|
|
* Canceling the event via Event#preventDefault()
|
|
* prevents the original DOM operation.
|
|
*
|
|
* @param {EventBus} eventBus
|
|
* @param {ElementRegistry} elementRegistry
|
|
* @param {Styles} styles
|
|
*/
|
|
export default function InteractionEvents(eventBus, elementRegistry, styles) {
|
|
|
|
var self = this;
|
|
|
|
/**
|
|
* Fire an interaction event.
|
|
*
|
|
* @param {string} type local event name, e.g. element.click.
|
|
* @param {MouseEvent|TouchEvent} event native event
|
|
* @param {Base} [element] the diagram element to emit the event on;
|
|
* defaults to the event target
|
|
*/
|
|
function fire(type, event, element) {
|
|
|
|
if (isIgnored(type, event)) {
|
|
return;
|
|
}
|
|
|
|
var target, gfx, returnValue;
|
|
|
|
if (!element) {
|
|
target = event.delegateTarget || event.target;
|
|
|
|
if (target) {
|
|
gfx = target;
|
|
element = elementRegistry.get(gfx);
|
|
}
|
|
} else {
|
|
gfx = elementRegistry.getGraphics(element);
|
|
}
|
|
|
|
if (!gfx || !element) {
|
|
return;
|
|
}
|
|
|
|
returnValue = eventBus.fire(type, {
|
|
element: element,
|
|
gfx: gfx,
|
|
originalEvent: event
|
|
});
|
|
|
|
if (returnValue === false) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
// TODO(nikku): document this
|
|
var handlers = {};
|
|
|
|
function mouseHandler(localEventName) {
|
|
return handlers[localEventName];
|
|
}
|
|
|
|
function isIgnored(localEventName, event) {
|
|
|
|
var filter = ignoredFilters[localEventName] || isPrimaryButton;
|
|
|
|
// only react on left mouse button interactions
|
|
// except for interaction events that are enabled
|
|
// for secundary mouse button
|
|
return !filter(event);
|
|
}
|
|
|
|
var bindings = {
|
|
click: 'element.click',
|
|
contextmenu: 'element.contextmenu',
|
|
dblclick: 'element.dblclick',
|
|
mousedown: 'element.mousedown',
|
|
mousemove: 'element.mousemove',
|
|
mouseover: 'element.hover',
|
|
mouseout: 'element.out',
|
|
mouseup: 'element.mouseup',
|
|
};
|
|
|
|
var ignoredFilters = {
|
|
'element.contextmenu': allowAll,
|
|
'element.mousedown': allowPrimaryAndAuxiliary,
|
|
'element.mouseup': allowPrimaryAndAuxiliary,
|
|
'element.click': allowPrimaryAndAuxiliary,
|
|
'element.dblclick': allowPrimaryAndAuxiliary
|
|
};
|
|
|
|
|
|
// manual event trigger //////////
|
|
|
|
/**
|
|
* Trigger an interaction event (based on a native dom event)
|
|
* on the target shape or connection.
|
|
*
|
|
* @param {string} eventName the name of the triggered DOM event
|
|
* @param {MouseEvent|TouchEvent} event
|
|
* @param {Base} targetElement
|
|
*/
|
|
function triggerMouseEvent(eventName, event, targetElement) {
|
|
|
|
// i.e. element.mousedown...
|
|
var localEventName = bindings[eventName];
|
|
|
|
if (!localEventName) {
|
|
throw new Error('unmapped DOM event name <' + eventName + '>');
|
|
}
|
|
|
|
return fire(localEventName, event, targetElement);
|
|
}
|
|
|
|
|
|
var ELEMENT_SELECTOR = 'svg, .djs-element';
|
|
|
|
// event handling ///////
|
|
|
|
function registerEvent(node, event, localEvent, ignoredFilter) {
|
|
|
|
var handler = handlers[localEvent] = function(event) {
|
|
fire(localEvent, event);
|
|
};
|
|
|
|
if (ignoredFilter) {
|
|
ignoredFilters[localEvent] = ignoredFilter;
|
|
}
|
|
|
|
handler.$delegate = domDelegate.bind(node, ELEMENT_SELECTOR, event, handler);
|
|
}
|
|
|
|
function unregisterEvent(node, event, localEvent) {
|
|
|
|
var handler = mouseHandler(localEvent);
|
|
|
|
if (!handler) {
|
|
return;
|
|
}
|
|
|
|
domDelegate.unbind(node, event, handler.$delegate);
|
|
}
|
|
|
|
function registerEvents(svg) {
|
|
forEach(bindings, function(val, key) {
|
|
registerEvent(svg, key, val);
|
|
});
|
|
}
|
|
|
|
function unregisterEvents(svg) {
|
|
forEach(bindings, function(val, key) {
|
|
unregisterEvent(svg, key, val);
|
|
});
|
|
}
|
|
|
|
eventBus.on('canvas.destroy', function(event) {
|
|
unregisterEvents(event.svg);
|
|
});
|
|
|
|
eventBus.on('canvas.init', function(event) {
|
|
registerEvents(event.svg);
|
|
});
|
|
|
|
|
|
// hit box updating ////////////////
|
|
|
|
eventBus.on([ 'shape.added', 'connection.added' ], function(event) {
|
|
var element = event.element,
|
|
gfx = event.gfx;
|
|
|
|
eventBus.fire('interactionEvents.createHit', { element: element, gfx: gfx });
|
|
});
|
|
|
|
// Update djs-hit on change.
|
|
// A low priortity is necessary, because djs-hit of labels has to be updated
|
|
// after the label bounds have been updated in the renderer.
|
|
eventBus.on([
|
|
'shape.changed',
|
|
'connection.changed'
|
|
], LOW_PRIORITY, function(event) {
|
|
|
|
var element = event.element,
|
|
gfx = event.gfx;
|
|
|
|
eventBus.fire('interactionEvents.updateHit', { element: element, gfx: gfx });
|
|
});
|
|
|
|
eventBus.on('interactionEvents.createHit', LOW_PRIORITY, function(event) {
|
|
var element = event.element,
|
|
gfx = event.gfx;
|
|
|
|
self.createDefaultHit(element, gfx);
|
|
});
|
|
|
|
eventBus.on('interactionEvents.updateHit', function(event) {
|
|
var element = event.element,
|
|
gfx = event.gfx;
|
|
|
|
self.updateDefaultHit(element, gfx);
|
|
});
|
|
|
|
|
|
// hit styles ////////////
|
|
|
|
var STROKE_HIT_STYLE = createHitStyle('djs-hit djs-hit-stroke');
|
|
|
|
var CLICK_STROKE_HIT_STYLE = createHitStyle('djs-hit djs-hit-click-stroke');
|
|
|
|
var ALL_HIT_STYLE = createHitStyle('djs-hit djs-hit-all');
|
|
|
|
var NO_MOVE_HIT_STYLE = createHitStyle('djs-hit djs-hit-no-move');
|
|
|
|
var HIT_TYPES = {
|
|
'all': ALL_HIT_STYLE,
|
|
'click-stroke': CLICK_STROKE_HIT_STYLE,
|
|
'stroke': STROKE_HIT_STYLE,
|
|
'no-move': NO_MOVE_HIT_STYLE
|
|
};
|
|
|
|
function createHitStyle(classNames, attrs) {
|
|
|
|
attrs = assign({
|
|
stroke: 'white',
|
|
strokeWidth: 15
|
|
}, attrs || {});
|
|
|
|
return styles.cls(classNames, [ 'no-fill', 'no-border' ], attrs);
|
|
}
|
|
|
|
|
|
// style helpers ///////////////
|
|
|
|
function applyStyle(hit, type) {
|
|
|
|
var attrs = HIT_TYPES[type];
|
|
|
|
if (!attrs) {
|
|
throw new Error('invalid hit type <' + type + '>');
|
|
}
|
|
|
|
svgAttr(hit, attrs);
|
|
|
|
return hit;
|
|
}
|
|
|
|
function appendHit(gfx, hit) {
|
|
svgAppend(gfx, hit);
|
|
}
|
|
|
|
|
|
// API
|
|
|
|
/**
|
|
* Remove hints on the given graphics.
|
|
*
|
|
* @param {SVGElement} gfx
|
|
*/
|
|
this.removeHits = function(gfx) {
|
|
var hits = domQueryAll('.djs-hit', gfx);
|
|
|
|
forEach(hits, svgRemove);
|
|
};
|
|
|
|
/**
|
|
* Create default hit for the given element.
|
|
*
|
|
* @param {Base} element
|
|
* @param {SVGElement} gfx
|
|
*
|
|
* @return {SVGElement} created hit
|
|
*/
|
|
this.createDefaultHit = function(element, gfx) {
|
|
var waypoints = element.waypoints,
|
|
isFrame = element.isFrame,
|
|
boxType;
|
|
|
|
if (waypoints) {
|
|
return this.createWaypointsHit(gfx, waypoints);
|
|
} else {
|
|
|
|
boxType = isFrame ? 'stroke' : 'all';
|
|
|
|
return this.createBoxHit(gfx, boxType, {
|
|
width: element.width,
|
|
height: element.height
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create hits for the given waypoints.
|
|
*
|
|
* @param {SVGElement} gfx
|
|
* @param {Array<Point>} waypoints
|
|
*
|
|
* @return {SVGElement}
|
|
*/
|
|
this.createWaypointsHit = function(gfx, waypoints) {
|
|
|
|
var hit = createLine(waypoints);
|
|
|
|
applyStyle(hit, 'stroke');
|
|
|
|
appendHit(gfx, hit);
|
|
|
|
return hit;
|
|
};
|
|
|
|
/**
|
|
* Create hits for a box.
|
|
*
|
|
* @param {SVGElement} gfx
|
|
* @param {string} hitType
|
|
* @param {Object} attrs
|
|
*
|
|
* @return {SVGElement}
|
|
*/
|
|
this.createBoxHit = function(gfx, type, attrs) {
|
|
|
|
attrs = assign({
|
|
x: 0,
|
|
y: 0
|
|
}, attrs);
|
|
|
|
var hit = svgCreate('rect');
|
|
|
|
applyStyle(hit, type);
|
|
|
|
svgAttr(hit, attrs);
|
|
|
|
appendHit(gfx, hit);
|
|
|
|
return hit;
|
|
};
|
|
|
|
/**
|
|
* Update default hit of the element.
|
|
*
|
|
* @param {Base} element
|
|
* @param {SVGElement} gfx
|
|
*
|
|
* @return {SVGElement} updated hit
|
|
*/
|
|
this.updateDefaultHit = function(element, gfx) {
|
|
|
|
var hit = domQuery('.djs-hit', gfx);
|
|
|
|
if (!hit) {
|
|
return;
|
|
}
|
|
|
|
if (element.waypoints) {
|
|
updateLine(hit, element.waypoints);
|
|
} else {
|
|
svgAttr(hit, {
|
|
width: element.width,
|
|
height: element.height
|
|
});
|
|
}
|
|
|
|
return hit;
|
|
};
|
|
|
|
this.fire = fire;
|
|
|
|
this.triggerMouseEvent = triggerMouseEvent;
|
|
|
|
this.mouseHandler = mouseHandler;
|
|
|
|
this.registerEvent = registerEvent;
|
|
this.unregisterEvent = unregisterEvent;
|
|
}
|
|
|
|
|
|
InteractionEvents.$inject = [
|
|
'eventBus',
|
|
'elementRegistry',
|
|
'styles'
|
|
];
|
|
|
|
|
|
/**
|
|
* An event indicating that the mouse hovered over an element
|
|
*
|
|
* @event element.hover
|
|
*
|
|
* @type {Object}
|
|
* @property {Base} element
|
|
* @property {SVGElement} gfx
|
|
* @property {Event} originalEvent
|
|
*/
|
|
|
|
/**
|
|
* An event indicating that the mouse has left an element
|
|
*
|
|
* @event element.out
|
|
*
|
|
* @type {Object}
|
|
* @property {Base} element
|
|
* @property {SVGElement} gfx
|
|
* @property {Event} originalEvent
|
|
*/
|
|
|
|
/**
|
|
* An event indicating that the mouse has clicked an element
|
|
*
|
|
* @event element.click
|
|
*
|
|
* @type {Object}
|
|
* @property {Base} element
|
|
* @property {SVGElement} gfx
|
|
* @property {Event} originalEvent
|
|
*/
|
|
|
|
/**
|
|
* An event indicating that the mouse has double clicked an element
|
|
*
|
|
* @event element.dblclick
|
|
*
|
|
* @type {Object}
|
|
* @property {Base} element
|
|
* @property {SVGElement} gfx
|
|
* @property {Event} originalEvent
|
|
*/
|
|
|
|
/**
|
|
* An event indicating that the mouse has gone down on an element.
|
|
*
|
|
* @event element.mousedown
|
|
*
|
|
* @type {Object}
|
|
* @property {Base} element
|
|
* @property {SVGElement} gfx
|
|
* @property {Event} originalEvent
|
|
*/
|
|
|
|
/**
|
|
* An event indicating that the mouse has gone up on an element.
|
|
*
|
|
* @event element.mouseup
|
|
*
|
|
* @type {Object}
|
|
* @property {Base} element
|
|
* @property {SVGElement} gfx
|
|
* @property {Event} originalEvent
|
|
*/
|
|
|
|
/**
|
|
* An event indicating that the context menu action is triggered
|
|
* via mouse or touch controls.
|
|
*
|
|
* @event element.contextmenu
|
|
*
|
|
* @type {Object}
|
|
* @property {Base} element
|
|
* @property {SVGElement} gfx
|
|
* @property {Event} originalEvent
|
|
*/ |