UNPKG

diagram-js

Version:

A toolbox for displaying and modifying diagrams on the web

520 lines (405 loc) 11 kB
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/Types').Element} Element * * @typedef {import('../../core/ElementRegistry').default} ElementRegistry * @typedef {import('../../core/EventBus').default} EventBus * @typedef {import('../../draw/Styles').default} Styles * * @typedef {import('../../util/Types').Point} Point */ 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 {Element} [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 {Element} 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 {Element} 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 {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} type * @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 {Element} 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 {Element} element * @property {SVGElement} gfx * @property {Event} originalEvent */ /** * An event indicating that the mouse has left an element * * @event element.out * * @type {Object} * @property {Element} element * @property {SVGElement} gfx * @property {Event} originalEvent */ /** * An event indicating that the mouse has clicked an element * * @event element.click * * @type {Object} * @property {Element} element * @property {SVGElement} gfx * @property {Event} originalEvent */ /** * An event indicating that the mouse has double clicked an element * * @event element.dblclick * * @type {Object} * @property {Element} 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 {Element} 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 {Element} 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 {Element} element * @property {SVGElement} gfx * @property {Event} originalEvent */