/*! * bpmn-js - bpmn-modeler v9.4.0 * * Copyright (c) 2014-present, camunda Services GmbH * * Released under the bpmn.io license * http://bpmn.io/license * * Source Code: https://github.com/bpmn-io/bpmn-js * * Date: 2022-08-22 */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BpmnJS = factory()); })(this, (function () { 'use strict'; function e(e,t){t&&(e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}));} function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var hat_1 = createCommonjsModule(function (module) { var hat = module.exports = function (bits, base) { if (!base) base = 16; if (bits === undefined) bits = 128; if (bits <= 0) return '0'; var digits = Math.log(Math.pow(2, bits)) / Math.log(base); for (var i = 2; digits === Infinity; i *= 2) { digits = Math.log(Math.pow(2, bits / i)) / Math.log(base) * i; } var rem = digits - Math.floor(digits); var res = ''; for (var i = 0; i < Math.floor(digits); i++) { var x = Math.floor(Math.random() * base).toString(base); res = x + res; } if (rem) { var b = Math.pow(base, rem); var x = Math.floor(Math.random() * b).toString(base); res = x + res; } var parsed = parseInt(res, base); if (parsed !== Infinity && parsed >= Math.pow(2, bits)) { return hat(bits, base) } else return res; }; hat.rack = function (bits, base, expandBy) { var fn = function (data) { var iters = 0; do { if (iters ++ > 10) { if (expandBy) bits += expandBy; else throw new Error('too many ID collisions, use more bits') } var id = hat(bits, base); } while (Object.hasOwnProperty.call(hats, id)); hats[id] = data; return id; }; var hats = fn.hats = {}; fn.get = function (id) { return fn.hats[id]; }; fn.set = function (id, value) { fn.hats[id] = value; return fn; }; fn.bits = bits || 128; fn.base = base || 16; return fn; }; }); /** * Create a new id generator / cache instance. * * You may optionally provide a seed that is used internally. * * @param {Seed} seed */ function Ids(seed) { if (!(this instanceof Ids)) { return new Ids(seed); } seed = seed || [128, 36, 1]; this._seed = seed.length ? hat_1.rack(seed[0], seed[1], seed[2]) : seed; } /** * Generate a next id. * * @param {Object} [element] element to bind the id to * * @return {String} id */ Ids.prototype.next = function (element) { return this._seed(element || true); }; /** * Generate a next id with a given prefix. * * @param {Object} [element] element to bind the id to * * @return {String} id */ Ids.prototype.nextPrefixed = function (prefix, element) { var id; do { id = prefix + this.next(true); } while (this.assigned(id)); // claim {prefix}{random} this.claim(id, element); // return return id; }; /** * Manually claim an existing id. * * @param {String} id * @param {String} [element] element the id is claimed by */ Ids.prototype.claim = function (id, element) { this._seed.set(id, element || true); }; /** * Returns true if the given id has already been assigned. * * @param {String} id * @return {Boolean} */ Ids.prototype.assigned = function (id) { return this._seed.get(id) || false; }; /** * Unclaim an id. * * @param {String} id the id to unclaim */ Ids.prototype.unclaim = function (id) { delete this._seed.hats[id]; }; /** * Clear all claimed ids. */ Ids.prototype.clear = function () { var hats = this._seed.hats, id; for (id in hats) { this.unclaim(id); } }; /** * Flatten array, one level deep. * * @param {Array} arr * * @return {Array} */ function flatten(arr) { return Array.prototype.concat.apply([], arr); } var nativeToString$1 = Object.prototype.toString; var nativeHasOwnProperty$1 = Object.prototype.hasOwnProperty; function isUndefined$2(obj) { return obj === undefined; } function isDefined(obj) { return obj !== undefined; } function isNil(obj) { return obj == null; } function isArray$3(obj) { return nativeToString$1.call(obj) === '[object Array]'; } function isObject(obj) { return nativeToString$1.call(obj) === '[object Object]'; } function isNumber(obj) { return nativeToString$1.call(obj) === '[object Number]'; } function isFunction(obj) { var tag = nativeToString$1.call(obj); return tag === '[object Function]' || tag === '[object AsyncFunction]' || tag === '[object GeneratorFunction]' || tag === '[object AsyncGeneratorFunction]' || tag === '[object Proxy]'; } function isString(obj) { return nativeToString$1.call(obj) === '[object String]'; } /** * Ensure collection is an array. * * @param {Object} obj */ function ensureArray(obj) { if (isArray$3(obj)) { return; } throw new Error('must supply array'); } /** * Return true, if target owns a property with the given key. * * @param {Object} target * @param {String} key * * @return {Boolean} */ function has$1(target, key) { return nativeHasOwnProperty$1.call(target, key); } /** * Find element in collection. * * @param {Array|Object} collection * @param {Function|Object} matcher * * @return {Object} */ function find(collection, matcher) { matcher = toMatcher(matcher); var match; forEach$1(collection, function (val, key) { if (matcher(val, key)) { match = val; return false; } }); return match; } /** * Find element index in collection. * * @param {Array|Object} collection * @param {Function} matcher * * @return {Object} */ function findIndex(collection, matcher) { matcher = toMatcher(matcher); var idx = isArray$3(collection) ? -1 : undefined; forEach$1(collection, function (val, key) { if (matcher(val, key)) { idx = key; return false; } }); return idx; } /** * Find element in collection. * * @param {Array|Object} collection * @param {Function} matcher * * @return {Array} result */ function filter(collection, matcher) { var result = []; forEach$1(collection, function (val, key) { if (matcher(val, key)) { result.push(val); } }); return result; } /** * Iterate over collection; returning something * (non-undefined) will stop iteration. * * @param {Array|Object} collection * @param {Function} iterator * * @return {Object} return result that stopped the iteration */ function forEach$1(collection, iterator) { var val, result; if (isUndefined$2(collection)) { return; } var convertKey = isArray$3(collection) ? toNum$1 : identity$1; for (var key in collection) { if (has$1(collection, key)) { val = collection[key]; result = iterator(val, convertKey(key)); if (result === false) { return val; } } } } /** * Return collection without element. * * @param {Array} arr * @param {Function} matcher * * @return {Array} */ function without(arr, matcher) { if (isUndefined$2(arr)) { return []; } ensureArray(arr); matcher = toMatcher(matcher); return arr.filter(function (el, idx) { return !matcher(el, idx); }); } /** * Reduce collection, returning a single result. * * @param {Object|Array} collection * @param {Function} iterator * @param {Any} result * * @return {Any} result returned from last iterator */ function reduce(collection, iterator, result) { forEach$1(collection, function (value, idx) { result = iterator(result, value, idx); }); return result; } /** * Return true if every element in the collection * matches the criteria. * * @param {Object|Array} collection * @param {Function} matcher * * @return {Boolean} */ function every(collection, matcher) { return !!reduce(collection, function (matches, val, key) { return matches && matcher(val, key); }, true); } /** * Return true if some elements in the collection * match the criteria. * * @param {Object|Array} collection * @param {Function} matcher * * @return {Boolean} */ function some(collection, matcher) { return !!find(collection, matcher); } /** * Transform a collection into another collection * by piping each member through the given fn. * * @param {Object|Array} collection * @param {Function} fn * * @return {Array} transformed collection */ function map(collection, fn) { var result = []; forEach$1(collection, function (val, key) { result.push(fn(val, key)); }); return result; } /** * Get the collections keys. * * @param {Object|Array} collection * * @return {Array} */ function keys(collection) { return collection && Object.keys(collection) || []; } /** * Shorthand for `keys(o).length`. * * @param {Object|Array} collection * * @return {Number} */ function size(collection) { return keys(collection).length; } /** * Get the values in the collection. * * @param {Object|Array} collection * * @return {Array} */ function values(collection) { return map(collection, function (val) { return val; }); } /** * Group collection members by attribute. * * @param {Object|Array} collection * @param {Function} extractor * * @return {Object} map with { attrValue => [ a, b, c ] } */ function groupBy(collection, extractor) { var grouped = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; extractor = toExtractor(extractor); forEach$1(collection, function (val) { var discriminator = extractor(val) || '_'; var group = grouped[discriminator]; if (!group) { group = grouped[discriminator] = []; } group.push(val); }); return grouped; } function uniqueBy(extractor) { extractor = toExtractor(extractor); var grouped = {}; for (var _len = arguments.length, collections = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { collections[_key - 1] = arguments[_key]; } forEach$1(collections, function (c) { return groupBy(c, extractor, grouped); }); var result = map(grouped, function (val, key) { return val[0]; }); return result; } var unionBy = uniqueBy; /** * Sort collection by criteria. * * @param {Object|Array} collection * @param {String|Function} extractor * * @return {Array} */ function sortBy(collection, extractor) { extractor = toExtractor(extractor); var sorted = []; forEach$1(collection, function (value, key) { var disc = extractor(value, key); var entry = { d: disc, v: value }; for (var idx = 0; idx < sorted.length; idx++) { var d = sorted[idx].d; if (disc < d) { sorted.splice(idx, 0, entry); return; } } // not inserted, append (!) sorted.push(entry); }); return map(sorted, function (e) { return e.v; }); } /** * Create an object pattern matcher. * * @example * * const matcher = matchPattern({ id: 1 }); * * let element = find(elements, matcher); * * @param {Object} pattern * * @return {Function} matcherFn */ function matchPattern(pattern) { return function (el) { return every(pattern, function (val, key) { return el[key] === val; }); }; } function toExtractor(extractor) { return isFunction(extractor) ? extractor : function (e) { return e[extractor]; }; } function toMatcher(matcher) { return isFunction(matcher) ? matcher : function (e) { return e === matcher; }; } function identity$1(arg) { return arg; } function toNum$1(arg) { return Number(arg); } /** * Debounce fn, calling it only once if the given time * elapsed between calls. * * Lodash-style the function exposes methods to `#clear` * and `#flush` to control internal behavior. * * @param {Function} fn * @param {Number} timeout * * @return {Function} debounced function */ function debounce(fn, timeout) { var timer; var lastArgs; var lastThis; var lastNow; function fire(force) { var now = Date.now(); var scheduledDiff = force ? 0 : lastNow + timeout - now; if (scheduledDiff > 0) { return schedule(scheduledDiff); } fn.apply(lastThis, lastArgs); clear(); } function schedule(timeout) { timer = setTimeout(fire, timeout); } function clear() { if (timer) { clearTimeout(timer); } timer = lastNow = lastArgs = lastThis = undefined; } function flush() { if (timer) { fire(true); } clear(); } function callback() { lastNow = Date.now(); for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } lastArgs = args; lastThis = this; // ensure an execution is scheduled if (!timer) { schedule(timeout); } } callback.flush = flush; callback.cancel = clear; return callback; } /** * Bind function against target . * * @param {Function} fn * @param {Object} target * * @return {Function} bound function */ function bind(fn, target) { return fn.bind(target); } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } /** * Convenience wrapper for `Object.assign`. * * @param {Object} target * @param {...Object} others * * @return {Object} the target */ function assign(target) { for (var _len = arguments.length, others = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { others[_key - 1] = arguments[_key]; } return _extends.apply(void 0, [target].concat(others)); } /** * Pick given properties from the target object. * * @param {Object} target * @param {Array} properties * * @return {Object} target */ function pick(target, properties) { var result = {}; var obj = Object(target); forEach$1(properties, function (prop) { if (prop in obj) { result[prop] = target[prop]; } }); return result; } /** * Pick all target properties, excluding the given ones. * * @param {Object} target * @param {Array} properties * * @return {Object} target */ function omit(target, properties) { var result = {}; var obj = Object(target); forEach$1(obj, function (prop, key) { if (properties.indexOf(key) === -1) { result[key] = prop; } }); return result; } /** * Flatten array, one level deep. * * @param {Array} arr * * @return {Array} */ var nativeToString = Object.prototype.toString; var nativeHasOwnProperty = Object.prototype.hasOwnProperty; function isUndefined$1(obj) { return obj === undefined; } function isArray$2(obj) { return nativeToString.call(obj) === '[object Array]'; } /** * Return true, if target owns a property with the given key. * * @param {Object} target * @param {String} key * * @return {Boolean} */ function has(target, key) { return nativeHasOwnProperty.call(target, key); } /** * Iterate over collection; returning something * (non-undefined) will stop iteration. * * @param {Array|Object} collection * @param {Function} iterator * * @return {Object} return result that stopped the iteration */ function forEach(collection, iterator) { var val, result; if (isUndefined$1(collection)) { return; } var convertKey = isArray$2(collection) ? toNum : identity; for (var key in collection) { if (has(collection, key)) { val = collection[key]; result = iterator(val, convertKey(key)); if (result === false) { return val; } } } } function identity(arg) { return arg; } function toNum(arg) { return Number(arg); } /** * Assigns style attributes in a style-src compliant way. * * @param {Element} element * @param {...Object} styleSources * * @return {Element} the element */ function assign$1(element) { var target = element.style; for (var _len = arguments.length, styleSources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { styleSources[_key - 1] = arguments[_key]; } forEach(styleSources, function (style) { if (!style) { return; } forEach(style, function (value, key) { target[key] = value; }); }); return element; } /** * Set attribute `name` to `val`, or get attr `name`. * * @param {Element} el * @param {String} name * @param {String} [val] * @api public */ function attr$1(el, name, val) { // get if (arguments.length == 2) { return el.getAttribute(name); } // remove if (val === null) { return el.removeAttribute(name); } // set el.setAttribute(name, val); return el; } var indexOf$1 = [].indexOf; var indexof = function(arr, obj){ if (indexOf$1) return arr.indexOf(obj); for (var i = 0; i < arr.length; ++i) { if (arr[i] === obj) return i; } return -1; }; /** * Taken from https://github.com/component/classes * * Without the component bits. */ /** * Whitespace regexp. */ var re$1 = /\s+/; /** * toString reference. */ var toString$1 = Object.prototype.toString; /** * Wrap `el` in a `ClassList`. * * @param {Element} el * @return {ClassList} * @api public */ function classes$1(el) { return new ClassList$1(el); } /** * Initialize a new ClassList for `el`. * * @param {Element} el * @api private */ function ClassList$1(el) { if (!el || !el.nodeType) { throw new Error('A DOM element reference is required'); } this.el = el; this.list = el.classList; } /** * Add class `name` if not already present. * * @param {String} name * @return {ClassList} * @api public */ ClassList$1.prototype.add = function (name) { // classList if (this.list) { this.list.add(name); return this; } // fallback var arr = this.array(); var i = indexof(arr, name); if (!~i) arr.push(name); this.el.className = arr.join(' '); return this; }; /** * Remove class `name` when present, or * pass a regular expression to remove * any which match. * * @param {String|RegExp} name * @return {ClassList} * @api public */ ClassList$1.prototype.remove = function (name) { if ('[object RegExp]' == toString$1.call(name)) { return this.removeMatching(name); } // classList if (this.list) { this.list.remove(name); return this; } // fallback var arr = this.array(); var i = indexof(arr, name); if (~i) arr.splice(i, 1); this.el.className = arr.join(' '); return this; }; /** * Remove all classes matching `re`. * * @param {RegExp} re * @return {ClassList} * @api private */ ClassList$1.prototype.removeMatching = function (re) { var arr = this.array(); for (var i = 0; i < arr.length; i++) { if (re.test(arr[i])) { this.remove(arr[i]); } } return this; }; /** * Toggle class `name`, can force state via `force`. * * For browsers that support classList, but do not support `force` yet, * the mistake will be detected and corrected. * * @param {String} name * @param {Boolean} force * @return {ClassList} * @api public */ ClassList$1.prototype.toggle = function (name, force) { // classList if (this.list) { if ('undefined' !== typeof force) { if (force !== this.list.toggle(name, force)) { this.list.toggle(name); // toggle again to correct } } else { this.list.toggle(name); } return this; } // fallback if ('undefined' !== typeof force) { if (!force) { this.remove(name); } else { this.add(name); } } else { if (this.has(name)) { this.remove(name); } else { this.add(name); } } return this; }; /** * Return an array of classes. * * @return {Array} * @api public */ ClassList$1.prototype.array = function () { var className = this.el.getAttribute('class') || ''; var str = className.replace(/^\s+|\s+$/g, ''); var arr = str.split(re$1); if ('' === arr[0]) arr.shift(); return arr; }; /** * Check if class `name` is present. * * @param {String} name * @return {ClassList} * @api public */ ClassList$1.prototype.has = ClassList$1.prototype.contains = function (name) { return this.list ? this.list.contains(name) : !!~indexof(this.array(), name); }; /** * Remove all children from the given element. */ function clear$1(el) { var c; while (el.childNodes.length) { c = el.childNodes[0]; el.removeChild(c); } return el; } var proto = typeof Element !== 'undefined' ? Element.prototype : {}; var vendor = proto.matches || proto.matchesSelector || proto.webkitMatchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector || proto.oMatchesSelector; var matchesSelector = match; /** * Match `el` to `selector`. * * @param {Element} el * @param {String} selector * @return {Boolean} * @api public */ function match(el, selector) { if (!el || el.nodeType !== 1) return false; if (vendor) return vendor.call(el, selector); var nodes = el.parentNode.querySelectorAll(selector); for (var i = 0; i < nodes.length; i++) { if (nodes[i] == el) return true; } return false; } /** * Closest * * @param {Element} el * @param {String} selector * @param {Boolean} checkYourSelf (optional) */ function closest (element, selector, checkYourSelf) { var currentElem = checkYourSelf ? element : element.parentNode; while (currentElem && currentElem.nodeType !== document.DOCUMENT_NODE && currentElem.nodeType !== document.DOCUMENT_FRAGMENT_NODE) { if (matchesSelector(currentElem, selector)) { return currentElem; } currentElem = currentElem.parentNode; } return matchesSelector(currentElem, selector) ? currentElem : null; } var bind$1 = window.addEventListener ? 'addEventListener' : 'attachEvent', unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent', prefix$6 = bind$1 !== 'addEventListener' ? 'on' : ''; /** * Bind `el` event `type` to `fn`. * * @param {Element} el * @param {String} type * @param {Function} fn * @param {Boolean} capture * @return {Function} * @api public */ var bind_1 = function(el, type, fn, capture){ el[bind$1](prefix$6 + type, fn, capture || false); return fn; }; /** * Unbind `el` event `type`'s callback `fn`. * * @param {Element} el * @param {String} type * @param {Function} fn * @param {Boolean} capture * @return {Function} * @api public */ var unbind_1 = function(el, type, fn, capture){ el[unbind](prefix$6 + type, fn, capture || false); return fn; }; var componentEvent = { bind: bind_1, unbind: unbind_1 }; /** * Module dependencies. */ /** * Delegate event `type` to `selector` * and invoke `fn(e)`. A callback function * is returned which may be passed to `.unbind()`. * * @param {Element} el * @param {String} selector * @param {String} type * @param {Function} fn * @param {Boolean} capture * @return {Function} * @api public */ // Some events don't bubble, so we want to bind to the capture phase instead // when delegating. var forceCaptureEvents = ['focus', 'blur']; function bind$2(el, selector, type, fn, capture) { if (forceCaptureEvents.indexOf(type) !== -1) { capture = true; } return componentEvent.bind(el, type, function (e) { var target = e.target || e.srcElement; e.delegateTarget = closest(target, selector, true); if (e.delegateTarget) { fn.call(el, e); } }, capture); } /** * Unbind event `type`'s callback `fn`. * * @param {Element} el * @param {String} type * @param {Function} fn * @param {Boolean} capture * @api public */ function unbind$1(el, type, fn, capture) { if (forceCaptureEvents.indexOf(type) !== -1) { capture = true; } return componentEvent.unbind(el, type, fn, capture); } var delegate = { bind: bind$2, unbind: unbind$1 }; /** * Expose `parse`. */ var domify = parse$1; /** * Tests for browser support. */ var innerHTMLBug = false; var bugTestDiv; if (typeof document !== 'undefined') { bugTestDiv = document.createElement('div'); // Setup bugTestDiv.innerHTML = '
a'; // Make sure that link elements get serialized correctly by innerHTML // This requires a wrapper element in IE innerHTMLBug = !bugTestDiv.getElementsByTagName('link').length; bugTestDiv = undefined; } /** * Wrap map from jquery. */ var map$1 = { legend: [1, '
', '
'], tr: [2, '', '
'], col: [2, '', '
'], // for script/link/style tags to work in IE6-8, you have to wrap // in a div with a non-whitespace character in front, ha! _default: innerHTMLBug ? [1, 'X
', '
'] : [0, '', ''] }; map$1.td = map$1.th = [3, '', '
']; map$1.option = map$1.optgroup = [1, '']; map$1.thead = map$1.tbody = map$1.colgroup = map$1.caption = map$1.tfoot = [1, '', '
']; map$1.polyline = map$1.ellipse = map$1.polygon = map$1.circle = map$1.text = map$1.line = map$1.path = map$1.rect = map$1.g = [1, '','']; /** * Parse `html` and return a DOM Node instance, which could be a TextNode, * HTML DOM Node of some kind (
for example), or a DocumentFragment * instance, depending on the contents of the `html` string. * * @param {String} html - HTML string to "domify" * @param {Document} doc - The `document` instance to create the Node for * @return {DOMNode} the TextNode, DOM Node, or DocumentFragment instance * @api private */ function parse$1(html, doc) { if ('string' != typeof html) throw new TypeError('String expected'); // default to the global `document` object if (!doc) doc = document; // tag name var m = /<([\w:]+)/.exec(html); if (!m) return doc.createTextNode(html); html = html.replace(/^\s+|\s+$/g, ''); // Remove leading/trailing whitespace var tag = m[1]; // body support if (tag == 'body') { var el = doc.createElement('html'); el.innerHTML = html; return el.removeChild(el.lastChild); } // wrap map var wrap = map$1[tag] || map$1._default; var depth = wrap[0]; var prefix = wrap[1]; var suffix = wrap[2]; var el = doc.createElement('div'); el.innerHTML = prefix + html + suffix; while (depth--) el = el.lastChild; // one element if (el.firstChild == el.lastChild) { return el.removeChild(el.firstChild); } // several elements var fragment = doc.createDocumentFragment(); while (el.firstChild) { fragment.appendChild(el.removeChild(el.firstChild)); } return fragment; } function query(selector, el) { el = el || document; return el.querySelector(selector); } function all(selector, el) { el = el || document; return el.querySelectorAll(selector); } function remove$2(el) { el.parentNode && el.parentNode.removeChild(el); } function ensureImported(element, target) { if (element.ownerDocument !== target.ownerDocument) { try { // may fail on webkit return target.ownerDocument.importNode(element, true); } catch (e) { // ignore } } return element; } /** * appendTo utility */ /** * Append a node to a target element and return the appended node. * * @param {SVGElement} element * @param {SVGElement} target * * @return {SVGElement} the appended node */ function appendTo(element, target) { return target.appendChild(ensureImported(element, target)); } /** * append utility */ /** * Append a node to an element * * @param {SVGElement} element * @param {SVGElement} node * * @return {SVGElement} the element */ function append(target, node) { appendTo(node, target); return target; } /** * attribute accessor utility */ var LENGTH_ATTR = 2; var CSS_PROPERTIES = { 'alignment-baseline': 1, 'baseline-shift': 1, 'clip': 1, 'clip-path': 1, 'clip-rule': 1, 'color': 1, 'color-interpolation': 1, 'color-interpolation-filters': 1, 'color-profile': 1, 'color-rendering': 1, 'cursor': 1, 'direction': 1, 'display': 1, 'dominant-baseline': 1, 'enable-background': 1, 'fill': 1, 'fill-opacity': 1, 'fill-rule': 1, 'filter': 1, 'flood-color': 1, 'flood-opacity': 1, 'font': 1, 'font-family': 1, 'font-size': LENGTH_ATTR, 'font-size-adjust': 1, 'font-stretch': 1, 'font-style': 1, 'font-variant': 1, 'font-weight': 1, 'glyph-orientation-horizontal': 1, 'glyph-orientation-vertical': 1, 'image-rendering': 1, 'kerning': 1, 'letter-spacing': 1, 'lighting-color': 1, 'marker': 1, 'marker-end': 1, 'marker-mid': 1, 'marker-start': 1, 'mask': 1, 'opacity': 1, 'overflow': 1, 'pointer-events': 1, 'shape-rendering': 1, 'stop-color': 1, 'stop-opacity': 1, 'stroke': 1, 'stroke-dasharray': 1, 'stroke-dashoffset': 1, 'stroke-linecap': 1, 'stroke-linejoin': 1, 'stroke-miterlimit': 1, 'stroke-opacity': 1, 'stroke-width': LENGTH_ATTR, 'text-anchor': 1, 'text-decoration': 1, 'text-rendering': 1, 'unicode-bidi': 1, 'visibility': 1, 'word-spacing': 1, 'writing-mode': 1 }; function getAttribute(node, name) { if (CSS_PROPERTIES[name]) { return node.style[name]; } else { return node.getAttributeNS(null, name); } } function setAttribute(node, name, value) { var hyphenated = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); var type = CSS_PROPERTIES[hyphenated]; if (type) { // append pixel unit, unless present if (type === LENGTH_ATTR && typeof value === 'number') { value = String(value) + 'px'; } node.style[hyphenated] = value; } else { node.setAttributeNS(null, name, value); } } function setAttributes(node, attrs) { var names = Object.keys(attrs), i, name; for (i = 0, name; (name = names[i]); i++) { setAttribute(node, name, attrs[name]); } } /** * Gets or sets raw attributes on a node. * * @param {SVGElement} node * @param {Object} [attrs] * @param {String} [name] * @param {String} [value] * * @return {String} */ function attr(node, name, value) { if (typeof name === 'string') { if (value !== undefined) { setAttribute(node, name, value); } else { return getAttribute(node, name); } } else { setAttributes(node, name); } return node; } /** * Clear utility */ function index(arr, obj) { if (arr.indexOf) { return arr.indexOf(obj); } for (var i = 0; i < arr.length; ++i) { if (arr[i] === obj) { return i; } } return -1; } var re = /\s+/; var toString = Object.prototype.toString; function defined(o) { return typeof o !== 'undefined'; } /** * Wrap `el` in a `ClassList`. * * @param {Element} el * @return {ClassList} * @api public */ function classes(el) { return new ClassList(el); } function ClassList(el) { if (!el || !el.nodeType) { throw new Error('A DOM element reference is required'); } this.el = el; this.list = el.classList; } /** * Add class `name` if not already present. * * @param {String} name * @return {ClassList} * @api public */ ClassList.prototype.add = function(name) { // classList if (this.list) { this.list.add(name); return this; } // fallback var arr = this.array(); var i = index(arr, name); if (!~i) { arr.push(name); } if (defined(this.el.className.baseVal)) { this.el.className.baseVal = arr.join(' '); } else { this.el.className = arr.join(' '); } return this; }; /** * Remove class `name` when present, or * pass a regular expression to remove * any which match. * * @param {String|RegExp} name * @return {ClassList} * @api public */ ClassList.prototype.remove = function(name) { if ('[object RegExp]' === toString.call(name)) { return this.removeMatching(name); } // classList if (this.list) { this.list.remove(name); return this; } // fallback var arr = this.array(); var i = index(arr, name); if (~i) { arr.splice(i, 1); } this.el.className.baseVal = arr.join(' '); return this; }; /** * Remove all classes matching `re`. * * @param {RegExp} re * @return {ClassList} * @api private */ ClassList.prototype.removeMatching = function(re) { var arr = this.array(); for (var i = 0; i < arr.length; i++) { if (re.test(arr[i])) { this.remove(arr[i]); } } return this; }; /** * Toggle class `name`, can force state via `force`. * * For browsers that support classList, but do not support `force` yet, * the mistake will be detected and corrected. * * @param {String} name * @param {Boolean} force * @return {ClassList} * @api public */ ClassList.prototype.toggle = function(name, force) { // classList if (this.list) { if (defined(force)) { if (force !== this.list.toggle(name, force)) { this.list.toggle(name); // toggle again to correct } } else { this.list.toggle(name); } return this; } // fallback if (defined(force)) { if (!force) { this.remove(name); } else { this.add(name); } } else { if (this.has(name)) { this.remove(name); } else { this.add(name); } } return this; }; /** * Return an array of classes. * * @return {Array} * @api public */ ClassList.prototype.array = function() { var className = this.el.getAttribute('class') || ''; var str = className.replace(/^\s+|\s+$/g, ''); var arr = str.split(re); if ('' === arr[0]) { arr.shift(); } return arr; }; /** * Check if class `name` is present. * * @param {String} name * @return {ClassList} * @api public */ ClassList.prototype.has = ClassList.prototype.contains = function(name) { return ( this.list ? this.list.contains(name) : !! ~index(this.array(), name) ); }; function remove$1(element) { var parent = element.parentNode; if (parent) { parent.removeChild(element); } return element; } /** * Clear utility */ /** * Removes all children from the given element * * @param {DOMElement} element * @return {DOMElement} the element (for chaining) */ function clear(element) { var child; while ((child = element.firstChild)) { remove$1(child); } return element; } function clone$1(element) { return element.cloneNode(true); } var ns = { svg: 'http://www.w3.org/2000/svg' }; /** * DOM parsing utility */ var SVG_START = '' + svg + ''; unwrap = true; } var parsed = parseDocument(svg); if (!unwrap) { return parsed; } var fragment = document.createDocumentFragment(); var parent = parsed.firstChild; while (parent.firstChild) { fragment.appendChild(parent.firstChild); } return fragment; } function parseDocument(svg) { var parser; // parse parser = new DOMParser(); parser.async = false; return parser.parseFromString(svg, 'text/xml'); } /** * Create utility for SVG elements */ /** * Create a specific type from name or SVG markup. * * @param {String} name the name or markup of the element * @param {Object} [attrs] attributes to set on the element * * @returns {SVGElement} */ function create$1(name, attrs) { var element; if (name.charAt(0) === '<') { element = parse(name).firstChild; element = document.importNode(element, true); } else { element = document.createElementNS(ns.svg, name); } if (attrs) { attr(element, attrs); } return element; } /** * Geometry helpers */ // fake node used to instantiate svg geometry elements var node = null; function getNode() { if (node === null) { node = create$1('svg'); } return node; } function extend$1(object, props) { var i, k, keys = Object.keys(props); for (i = 0; (k = keys[i]); i++) { object[k] = props[k]; } return object; } /** * Create matrix via args. * * @example * * createMatrix({ a: 1, b: 1 }); * createMatrix(); * createMatrix(1, 2, 0, 0, 30, 20); * * @return {SVGMatrix} */ function createMatrix(a, b, c, d, e, f) { var matrix = getNode().createSVGMatrix(); switch (arguments.length) { case 0: return matrix; case 1: return extend$1(matrix, a); case 6: return extend$1(matrix, { a: a, b: b, c: c, d: d, e: e, f: f }); } } function createTransform(matrix) { if (matrix) { return getNode().createSVGTransformFromMatrix(matrix); } else { return getNode().createSVGTransform(); } } /** * Serialization util */ var TEXT_ENTITIES = /([&<>]{1})/g; var ATTR_ENTITIES = /([\n\r"]{1})/g; var ENTITY_REPLACEMENT = { '&': '&', '<': '<', '>': '>', '"': '\'' }; function escape$1(str, pattern) { function replaceFn(match, entity) { return ENTITY_REPLACEMENT[entity] || entity; } return str.replace(pattern, replaceFn); } function serialize(node, output) { var i, len, attrMap, attrNode, childNodes; switch (node.nodeType) { // TEXT case 3: // replace special XML characters output.push(escape$1(node.textContent, TEXT_ENTITIES)); break; // ELEMENT case 1: output.push('<', node.tagName); if (node.hasAttributes()) { attrMap = node.attributes; for (i = 0, len = attrMap.length; i < len; ++i) { attrNode = attrMap.item(i); output.push(' ', attrNode.name, '="', escape$1(attrNode.value, ATTR_ENTITIES), '"'); } } if (node.hasChildNodes()) { output.push('>'); childNodes = node.childNodes; for (i = 0, len = childNodes.length; i < len; ++i) { serialize(childNodes.item(i), output); } output.push(''); } else { output.push('/>'); } break; // COMMENT case 8: output.push(''); break; // CDATA case 4: output.push(''); break; default: throw new Error('unable to handle node ' + node.nodeType); } return output; } /** * innerHTML like functionality for SVG elements. * based on innerSVG (https://code.google.com/p/innersvg) */ function set$1(element, svg) { var parsed = parse(svg); // clear element contents clear(element); if (!svg) { return; } if (!isFragment(parsed)) { // extract from parsed document parsed = parsed.documentElement; } var nodes = slice$1(parsed.childNodes); // import + append each node for (var i = 0; i < nodes.length; i++) { appendTo(nodes[i], element); } } function get$1(element) { var child = element.firstChild, output = []; while (child) { serialize(child, output); child = child.nextSibling; } return output.join(''); } function isFragment(node) { return node.nodeName === '#document-fragment'; } function innerSVG(element, svg) { if (svg !== undefined) { try { set$1(element, svg); } catch (e) { throw new Error('error parsing SVG: ' + e.message); } return element; } else { return get$1(element); } } function slice$1(arr) { return Array.prototype.slice.call(arr); } /** * transform accessor utility */ function wrapMatrix(transformList, transform) { if (transform instanceof SVGMatrix) { return transformList.createSVGTransformFromMatrix(transform); } return transform; } function setTransforms(transformList, transforms) { var i, t; transformList.clear(); for (i = 0; (t = transforms[i]); i++) { transformList.appendItem(wrapMatrix(transformList, t)); } } /** * Get or set the transforms on the given node. * * @param {SVGElement} node * @param {SVGTransform|SVGMatrix|Array} [transforms] * * @return {SVGTransform} the consolidated transform */ function transform$1(node, transforms) { var transformList = node.transform.baseVal; if (transforms) { if (!Array.isArray(transforms)) { transforms = [ transforms ]; } setTransforms(transformList, transforms); } return transformList.consolidate(); } var CLASS_PATTERN = /^class /; /** * @param {function} fn * * @return {boolean} */ function isClass(fn) { return CLASS_PATTERN.test(fn.toString()); } /** * @param {any} obj * * @return {boolean} */ function isArray$1(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; } /** * @param {any} obj * @param {string} prop * * @return {boolean} */ function hasOwnProp(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } /** * @typedef {import('./index').InjectAnnotated } InjectAnnotated */ /** * @template T * * @params {[...string[], T] | ...string[], T} args * * @return {T & InjectAnnotated} */ function annotate() { var args = Array.prototype.slice.call(arguments); if (args.length === 1 && isArray$1(args[0])) { args = args[0]; } var fn = args.pop(); fn.$inject = args; return fn; } // Current limitations: // - can't put into "function arg" comments // function /* (no parenthesis like this) */ (){} // function abc( /* xx (no parenthesis like this) */ a, b) {} // // Just put the comment before function or inside: // /* (((this is fine))) */ function(a, b) {} // function abc(a) { /* (((this is fine))) */} // // - can't reliably auto-annotate constructor; we'll match the // first constructor(...) pattern found which may be the one // of a nested class, too. var CONSTRUCTOR_ARGS = /constructor\s*[^(]*\(\s*([^)]*)\)/m; var FN_ARGS = /^(?:async\s+)?(?:function\s*[^(]*)?(?:\(\s*([^)]*)\)|(\w+))/m; var FN_ARG = /\/\*([^*]*)\*\//m; /** * @param {unknown} fn * * @return {string[]} */ function parseAnnotations(fn) { if (typeof fn !== 'function') { throw new Error('Cannot annotate "' + fn + '". Expected a function!'); } var match = fn.toString().match(isClass(fn) ? CONSTRUCTOR_ARGS : FN_ARGS); // may parse class without constructor if (!match) { return []; } var args = match[1] || match[2]; return args && args.split(',').map(function(arg) { var argMatch = arg.match(FN_ARG); return (argMatch && argMatch[1] || arg).trim(); }) || []; } /** * @typedef { import('./index').ModuleDeclaration } ModuleDeclaration * @typedef { import('./index').ModuleDefinition } ModuleDefinition * @typedef { import('./index').InjectorContext } InjectorContext */ /** * Create a new injector with the given modules. * * @param {ModuleDefinition[]} modules * @param {InjectorContext} [parent] */ function Injector(modules, parent) { parent = parent || { get: function(name, strict) { currentlyResolving.push(name); if (strict === false) { return null; } else { throw error('No provider for "' + name + '"!'); } } }; var currentlyResolving = []; var providers = this._providers = Object.create(parent._providers || null); var instances = this._instances = Object.create(null); var self = instances.injector = this; var error = function(msg) { var stack = currentlyResolving.join(' -> '); currentlyResolving.length = 0; return new Error(stack ? msg + ' (Resolving: ' + stack + ')' : msg); }; /** * Return a named service. * * @param {string} name * @param {boolean} [strict=true] if false, resolve missing services to null * * @return {any} */ function get(name, strict) { if (!providers[name] && name.indexOf('.') !== -1) { var parts = name.split('.'); var pivot = get(parts.shift()); while (parts.length) { pivot = pivot[parts.shift()]; } return pivot; } if (hasOwnProp(instances, name)) { return instances[name]; } if (hasOwnProp(providers, name)) { if (currentlyResolving.indexOf(name) !== -1) { currentlyResolving.push(name); throw error('Cannot resolve circular dependency!'); } currentlyResolving.push(name); instances[name] = providers[name][0](providers[name][1]); currentlyResolving.pop(); return instances[name]; } return parent.get(name, strict); } function fnDef(fn, locals) { if (typeof locals === 'undefined') { locals = {}; } if (typeof fn !== 'function') { if (isArray$1(fn)) { fn = annotate(fn.slice()); } else { throw new Error('Cannot invoke "' + fn + '". Expected a function!'); } } var inject = fn.$inject || parseAnnotations(fn); var dependencies = inject.map(function(dep) { if (hasOwnProp(locals, dep)) { return locals[dep]; } else { return get(dep); } }); return { fn: fn, dependencies: dependencies }; } function instantiate(Type) { var def = fnDef(Type); var fn = def.fn, dependencies = def.dependencies; // instantiate var args constructor var Constructor = Function.prototype.bind.apply(fn, [ null ].concat(dependencies)); return new Constructor(); } function invoke(func, context, locals) { var def = fnDef(func, locals); var fn = def.fn, dependencies = def.dependencies; return fn.apply(context, dependencies); } /** * @param {Injector} childInjector * * @return {Function} */ function createPrivateInjectorFactory(childInjector) { return annotate(function(key) { return childInjector.get(key); }); } /** * @param {ModuleDefinition[]} modules * @param {string[]} [forceNewInstances] * * @return {Injector} */ function createChild(modules, forceNewInstances) { if (forceNewInstances && forceNewInstances.length) { var fromParentModule = Object.create(null); var matchedScopes = Object.create(null); var privateInjectorsCache = []; var privateChildInjectors = []; var privateChildFactories = []; var provider; var cacheIdx; var privateChildInjector; var privateChildInjectorFactory; for (var name in providers) { provider = providers[name]; if (forceNewInstances.indexOf(name) !== -1) { if (provider[2] === 'private') { cacheIdx = privateInjectorsCache.indexOf(provider[3]); if (cacheIdx === -1) { privateChildInjector = provider[3].createChild([], forceNewInstances); privateChildInjectorFactory = createPrivateInjectorFactory(privateChildInjector); privateInjectorsCache.push(provider[3]); privateChildInjectors.push(privateChildInjector); privateChildFactories.push(privateChildInjectorFactory); fromParentModule[name] = [ privateChildInjectorFactory, name, 'private', privateChildInjector ]; } else { fromParentModule[name] = [ privateChildFactories[cacheIdx], name, 'private', privateChildInjectors[cacheIdx] ]; } } else { fromParentModule[name] = [ provider[2], provider[1] ]; } matchedScopes[name] = true; } if ((provider[2] === 'factory' || provider[2] === 'type') && provider[1].$scope) { /* jshint -W083 */ forceNewInstances.forEach(function(scope) { if (provider[1].$scope.indexOf(scope) !== -1) { fromParentModule[name] = [ provider[2], provider[1] ]; matchedScopes[scope] = true; } }); } } forceNewInstances.forEach(function(scope) { if (!matchedScopes[scope]) { throw new Error('No provider for "' + scope + '". Cannot use provider from the parent!'); } }); modules.unshift(fromParentModule); } return new Injector(modules, self); } var factoryMap = { factory: invoke, type: instantiate, value: function(value) { return value; } }; /** * @param {ModuleDefinition} moduleDefinition * @param {Injector} injector */ function createInitializer(moduleDefinition, injector) { var initializers = moduleDefinition.__init__ || []; return function() { initializers.forEach(function(initializer) { // eagerly resolve component (fn or string) if (typeof initializer === 'string') { injector.get(initializer); } else { injector.invoke(initializer); } }); }; } /** * @param {ModuleDefinition} moduleDefinition */ function loadModule(moduleDefinition) { var moduleExports = moduleDefinition.__exports__; // private module if (moduleExports) { var nestedModules = moduleDefinition.__modules__; var clonedModule = Object.keys(moduleDefinition).reduce(function(clonedModule, key) { if (key !== '__exports__' && key !== '__modules__' && key !== '__init__' && key !== '__depends__') { clonedModule[key] = moduleDefinition[key]; } return clonedModule; }, Object.create(null)); var childModules = (nestedModules || []).concat(clonedModule); var privateInjector = createChild(childModules); var getFromPrivateInjector = annotate(function(key) { return privateInjector.get(key); }); moduleExports.forEach(function(key) { providers[key] = [ getFromPrivateInjector, key, 'private', privateInjector ]; }); // ensure child injector initializes var initializers = (moduleDefinition.__init__ || []).slice(); initializers.unshift(function() { privateInjector.init(); }); moduleDefinition = Object.assign({}, moduleDefinition, { __init__: initializers }); return createInitializer(moduleDefinition, privateInjector); } // normal module Object.keys(moduleDefinition).forEach(function(key) { if (key === '__init__' || key === '__depends__') { return; } if (moduleDefinition[key][2] === 'private') { providers[key] = moduleDefinition[key]; return; } var type = moduleDefinition[key][0]; var value = moduleDefinition[key][1]; providers[key] = [ factoryMap[type], arrayUnwrap(type, value), type ]; }); return createInitializer(moduleDefinition, self); } /** * @param {ModuleDefinition[]} moduleDefinitions * @param {ModuleDefinition} moduleDefinition * * @return {ModuleDefinition[]} */ function resolveDependencies(moduleDefinitions, moduleDefinition) { if (moduleDefinitions.indexOf(moduleDefinition) !== -1) { return moduleDefinitions; } moduleDefinitions = (moduleDefinition.__depends__ || []).reduce(resolveDependencies, moduleDefinitions); if (moduleDefinitions.indexOf(moduleDefinition) !== -1) { return moduleDefinitions; } return moduleDefinitions.concat(moduleDefinition); } /** * @param {ModuleDefinition[]} moduleDefinitions * * @return { () => void } initializerFn */ function bootstrap(moduleDefinitions) { var initializers = moduleDefinitions .reduce(resolveDependencies, []) .map(loadModule); var initialized = false; return function() { if (initialized) { return; } initialized = true; initializers.forEach(function(initializer) { return initializer(); }); }; } // public API this.get = get; this.invoke = invoke; this.instantiate = instantiate; this.createChild = createChild; // setup this.init = bootstrap(modules); } // helpers /////////////// function arrayUnwrap(type, value) { if (type !== 'value' && isArray$1(value)) { value = annotate(value.slice()); } return value; } var DEFAULT_RENDER_PRIORITY$1 = 1000; /** * The base implementation of shape and connection renderers. * * @param {EventBus} eventBus * @param {number} [renderPriority=1000] */ function BaseRenderer(eventBus, renderPriority) { var self = this; renderPriority = renderPriority || DEFAULT_RENDER_PRIORITY$1; eventBus.on([ 'render.shape', 'render.connection' ], renderPriority, function(evt, context) { var type = evt.type, element = context.element, visuals = context.gfx, attrs = context.attrs; if (self.canRender(element)) { if (type === 'render.shape') { return self.drawShape(visuals, element, attrs); } else { return self.drawConnection(visuals, element, attrs); } } }); eventBus.on([ 'render.getShapePath', 'render.getConnectionPath' ], renderPriority, function(evt, element) { if (self.canRender(element)) { if (evt.type === 'render.getShapePath') { return self.getShapePath(element); } else { return self.getConnectionPath(element); } } }); } /** * Should check whether *this* renderer can render * the element/connection. * * @param {element} element * * @returns {boolean} */ BaseRenderer.prototype.canRender = function() {}; /** * Provides the shape's snap svg element to be drawn on the `canvas`. * * @param {djs.Graphics} visuals * @param {Shape} shape * * @returns {Snap.svg} [returns a Snap.svg paper element ] */ BaseRenderer.prototype.drawShape = function() {}; /** * Provides the shape's snap svg element to be drawn on the `canvas`. * * @param {djs.Graphics} visuals * @param {Connection} connection * * @returns {Snap.svg} [returns a Snap.svg paper element ] */ BaseRenderer.prototype.drawConnection = function() {}; /** * Gets the SVG path of a shape that represents it's visual bounds. * * @param {Shape} shape * * @return {string} svg path */ BaseRenderer.prototype.getShapePath = function() {}; /** * Gets the SVG path of a connection that represents it's visual bounds. * * @param {Connection} connection * * @return {string} svg path */ BaseRenderer.prototype.getConnectionPath = function() {}; function componentsToPath(elements) { return elements.join(',').replace(/,?([A-z]),?/g, '$1'); } function toSVGPoints(points) { var result = ''; for (var i = 0, p; (p = points[i]); i++) { result += p.x + ',' + p.y + ' '; } return result; } function createLine(points, attrs) { var line = create$1('polyline'); attr(line, { points: toSVGPoints(points) }); if (attrs) { attr(line, attrs); } return line; } function updateLine(gfx, points) { attr(gfx, { points: toSVGPoints(points) }); return gfx; } /** * @typedef { {x:number, y: number, width: number, height: number} } Bounds */ /** * Get parent elements. * * @param {Array} elements * * @returns {Array} */ function getParents$1(elements) { // find elements that are not children of any other elements return filter(elements, function(element) { return !find(elements, function(e) { return e !== element && getParent$1(element, e); }); }); } function getParent$1(element, parent) { if (!parent) { return; } if (element === parent) { return parent; } if (!element.parent) { return; } return getParent$1(element.parent, parent); } /** * Adds an element to a collection and returns true if the * element was added. * * @param {Array} elements * @param {Object} e * @param {boolean} unique */ function add$1(elements, e, unique) { var canAdd = !unique || elements.indexOf(e) === -1; if (canAdd) { elements.push(e); } return canAdd; } /** * Iterate over each element in a collection, calling the iterator function `fn` * with (element, index, recursionDepth). * * Recurse into all elements that are returned by `fn`. * * @param {Object|Array} elements * @param {Function} fn iterator function called with (element, index, recursionDepth) * @param {number} [depth] maximum recursion depth */ function eachElement(elements, fn, depth) { depth = depth || 0; if (!isArray$3(elements)) { elements = [ elements ]; } forEach$1(elements, function(s, i) { var filter = fn(s, i, depth); if (isArray$3(filter) && filter.length) { eachElement(filter, fn, depth + 1); } }); } /** * Collects self + child elements up to a given depth from a list of elements. * * @param {djs.model.Base|Array} elements the elements to select the children from * @param {boolean} unique whether to return a unique result set (no duplicates) * @param {number} maxDepth the depth to search through or -1 for infinite * * @return {Array} found elements */ function selfAndChildren(elements, unique, maxDepth) { var result = [], processedChildren = []; eachElement(elements, function(element, i, depth) { add$1(result, element, unique); var children = element.children; // max traversal depth not reached yet if (maxDepth === -1 || depth < maxDepth) { // children exist && children not yet processed if (children && add$1(processedChildren, children, unique)) { return children; } } }); return result; } /** * Return self + ALL children for a number of elements * * @param {Array} elements to query * @param {boolean} allowDuplicates to allow duplicates in the result set * * @return {Array} the collected elements */ function selfAndAllChildren(elements, allowDuplicates) { return selfAndChildren(elements, !allowDuplicates, -1); } /** * Gets the the closure for all selected elements, * their enclosed children and connections. * * @param {Array} elements * @param {boolean} [isTopLevel=true] * @param {Object} [existingClosure] * * @return {Object} newClosure */ function getClosure(elements, isTopLevel, closure) { if (isUndefined$2(isTopLevel)) { isTopLevel = true; } if (isObject(isTopLevel)) { closure = isTopLevel; isTopLevel = true; } closure = closure || {}; var allShapes = copyObject(closure.allShapes), allConnections = copyObject(closure.allConnections), enclosedElements = copyObject(closure.enclosedElements), enclosedConnections = copyObject(closure.enclosedConnections); var topLevel = copyObject( closure.topLevel, isTopLevel && groupBy(elements, function(e) { return e.id; }) ); function handleConnection(c) { if (topLevel[c.source.id] && topLevel[c.target.id]) { topLevel[c.id] = [ c ]; } // not enclosed as a child, but maybe logically // (connecting two moved elements?) if (allShapes[c.source.id] && allShapes[c.target.id]) { enclosedConnections[c.id] = enclosedElements[c.id] = c; } allConnections[c.id] = c; } function handleElement(element) { enclosedElements[element.id] = element; if (element.waypoints) { // remember connection enclosedConnections[element.id] = allConnections[element.id] = element; } else { // remember shape allShapes[element.id] = element; // remember all connections forEach$1(element.incoming, handleConnection); forEach$1(element.outgoing, handleConnection); // recurse into children return element.children; } } eachElement(elements, handleElement); return { allShapes: allShapes, allConnections: allConnections, topLevel: topLevel, enclosedConnections: enclosedConnections, enclosedElements: enclosedElements }; } /** * Returns the surrounding bbox for all elements in * the array or the element primitive. * * @param {Array|djs.model.Shape} elements * @param {boolean} [stopRecursion=false] * * @return {Bounds} */ function getBBox(elements, stopRecursion) { stopRecursion = !!stopRecursion; if (!isArray$3(elements)) { elements = [ elements ]; } var minX, minY, maxX, maxY; forEach$1(elements, function(element) { // If element is a connection the bbox must be computed first var bbox = element; if (element.waypoints && !stopRecursion) { bbox = getBBox(element.waypoints, true); } var x = bbox.x, y = bbox.y, height = bbox.height || 0, width = bbox.width || 0; if (x < minX || minX === undefined) { minX = x; } if (y < minY || minY === undefined) { minY = y; } if ((x + width) > maxX || maxX === undefined) { maxX = x + width; } if ((y + height) > maxY || maxY === undefined) { maxY = y + height; } }); return { x: minX, y: minY, height: maxY - minY, width: maxX - minX }; } /** * Returns all elements that are enclosed from the bounding box. * * * If bbox.(width|height) is not specified the method returns * all elements with element.x/y > bbox.x/y * * If only bbox.x or bbox.y is specified, method return all elements with * e.x > bbox.x or e.y > bbox.y * * @param {Array} elements List of Elements to search through * @param {djs.model.Shape} bbox the enclosing bbox. * * @return {Array} enclosed elements */ function getEnclosedElements(elements, bbox) { var filteredElements = {}; forEach$1(elements, function(element) { var e = element; if (e.waypoints) { e = getBBox(e); } if (!isNumber(bbox.y) && (e.x > bbox.x)) { filteredElements[element.id] = element; } if (!isNumber(bbox.x) && (e.y > bbox.y)) { filteredElements[element.id] = element; } if (e.x > bbox.x && e.y > bbox.y) { if (isNumber(bbox.width) && isNumber(bbox.height) && e.width + e.x < bbox.width + bbox.x && e.height + e.y < bbox.height + bbox.y) { filteredElements[element.id] = element; } else if (!isNumber(bbox.width) || !isNumber(bbox.height)) { filteredElements[element.id] = element; } } }); return filteredElements; } function getType(element) { if ('waypoints' in element) { return 'connection'; } if ('x' in element) { return 'shape'; } return 'root'; } function isFrameElement$1(element) { return !!(element && element.isFrame); } // helpers /////////////////////////////// function copyObject(src1, src2) { return assign({}, src1 || {}, src2 || {}); } // apply default renderer with lowest possible priority // so that it only kicks in if noone else could render var DEFAULT_RENDER_PRIORITY = 1; /** * The default renderer used for shapes and connections. * * @param {EventBus} eventBus * @param {Styles} styles */ function DefaultRenderer(eventBus, styles) { // BaseRenderer.call(this, eventBus, DEFAULT_RENDER_PRIORITY); this.CONNECTION_STYLE = styles.style([ 'no-fill' ], { strokeWidth: 5, stroke: 'fuchsia' }); this.SHAPE_STYLE = styles.style({ fill: 'white', stroke: 'fuchsia', strokeWidth: 2 }); this.FRAME_STYLE = styles.style([ 'no-fill' ], { stroke: 'fuchsia', strokeDasharray: 4, strokeWidth: 2 }); } e(DefaultRenderer, BaseRenderer); DefaultRenderer.prototype.canRender = function() { return true; }; DefaultRenderer.prototype.drawShape = function drawShape(visuals, element, attrs) { var rect = create$1('rect'); attr(rect, { x: 0, y: 0, width: element.width || 0, height: element.height || 0 }); if (isFrameElement$1(element)) { attr(rect, assign({}, this.FRAME_STYLE, attrs || {})); } else { attr(rect, assign({}, this.SHAPE_STYLE, attrs || {})); } append(visuals, rect); return rect; }; DefaultRenderer.prototype.drawConnection = function drawConnection(visuals, connection, attrs) { var line = createLine(connection.waypoints, assign({}, this.CONNECTION_STYLE, attrs || {})); append(visuals, line); return line; }; DefaultRenderer.prototype.getShapePath = function getShapePath(shape) { var x = shape.x, y = shape.y, width = shape.width, height = shape.height; var shapePath = [ [ 'M', x, y ], [ 'l', width, 0 ], [ 'l', 0, height ], [ 'l', -width, 0 ], [ 'z' ] ]; return componentsToPath(shapePath); }; DefaultRenderer.prototype.getConnectionPath = function getConnectionPath(connection) { var waypoints = connection.waypoints; var idx, point, connectionPath = []; for (idx = 0; (point = waypoints[idx]); idx++) { // take invisible docking into account // when creating the path point = point.original || point; connectionPath.push([ idx === 0 ? 'M' : 'L', point.x, point.y ]); } return componentsToPath(connectionPath); }; DefaultRenderer.$inject = [ 'eventBus', 'styles' ]; /** * A component that manages shape styles */ function Styles() { var defaultTraits = { 'no-fill': { fill: 'none' }, 'no-border': { strokeOpacity: 0.0 }, 'no-events': { pointerEvents: 'none' } }; var self = this; /** * Builds a style definition from a className, a list of traits and an object of additional attributes. * * @param {string} className * @param {Array} traits * @param {Object} additionalAttrs * * @return {Object} the style defintion */ this.cls = function(className, traits, additionalAttrs) { var attrs = this.style(traits, additionalAttrs); return assign(attrs, { 'class': className }); }; /** * Builds a style definition from a list of traits and an object of additional attributes. * * @param {Array} traits * @param {Object} additionalAttrs * * @return {Object} the style defintion */ this.style = function(traits, additionalAttrs) { if (!isArray$3(traits) && !additionalAttrs) { additionalAttrs = traits; traits = []; } var attrs = reduce(traits, function(attrs, t) { return assign(attrs, defaultTraits[t] || {}); }, {}); return additionalAttrs ? assign(attrs, additionalAttrs) : attrs; }; this.computeStyle = function(custom, traits, defaultStyles) { if (!isArray$3(traits)) { defaultStyles = traits; traits = []; } return self.style(traits || [], assign({}, defaultStyles, custom || {})); }; } var DrawModule$1 = { __init__: [ 'defaultRenderer' ], defaultRenderer: [ 'type', DefaultRenderer ], styles: [ 'type', Styles ] }; /** * Failsafe remove an element from a collection * * @param {Array} [collection] * @param {Object} [element] * * @return {number} the previous index of the element */ function remove(collection, element) { if (!collection || !element) { return -1; } var idx = collection.indexOf(element); if (idx !== -1) { collection.splice(idx, 1); } return idx; } /** * Fail save add an element to the given connection, ensuring * it does not yet exist. * * @param {Array} collection * @param {Object} element * @param {number} idx */ function add(collection, element, idx) { if (!collection || !element) { return; } if (typeof idx !== 'number') { idx = -1; } var currentIdx = collection.indexOf(element); if (currentIdx !== -1) { if (currentIdx === idx) { // nothing to do, position has not changed return; } else { if (idx !== -1) { // remove from current position collection.splice(currentIdx, 1); } else { // already exists in collection return; } } } if (idx !== -1) { // insert at specified position collection.splice(idx, 0, element); } else { // push to end collection.push(element); } } /** * Fail save get the index of an element in a collection. * * @param {Array} collection * @param {Object} element * * @return {number} the index or -1 if collection or element do * not exist or the element is not contained. */ function indexOf(collection, element) { if (!collection || !element) { return -1; } return collection.indexOf(element); } /** * Computes the distance between two points * * @param {Point} p * @param {Point} q * * @return {number} distance */ function pointDistance(a, b) { if (!a || !b) { return -1; } return Math.sqrt( Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2) ); } /** * Returns true if the point r is on the line between p and q * * @param {Point} p * @param {Point} q * @param {Point} r * @param {number} [accuracy=5] accuracy for points on line check (lower is better) * * @return {boolean} */ function pointsOnLine(p, q, r, accuracy) { if (typeof accuracy === 'undefined') { accuracy = 5; } if (!p || !q || !r) { return false; } var val = (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x), dist = pointDistance(p, q); // @see http://stackoverflow.com/a/907491/412190 return Math.abs(val / dist) <= accuracy; } var ALIGNED_THRESHOLD = 2; /** * Check whether two points are horizontally or vertically aligned. * * @param {Array|Point} * @param {Point} * * @return {string|boolean} */ function pointsAligned(a, b) { var points; if (isArray$3(a)) { points = a; } else { points = [ a, b ]; } if (pointsAlignedHorizontally(points)) { return 'h'; } if (pointsAlignedVertically(points)) { return 'v'; } return false; } function pointsAlignedHorizontally(a, b) { var points; if (isArray$3(a)) { points = a; } else { points = [ a, b ]; } var firstPoint = points.slice().shift(); return every(points, function(point) { return Math.abs(firstPoint.y - point.y) <= ALIGNED_THRESHOLD; }); } function pointsAlignedVertically(a, b) { var points; if (isArray$3(a)) { points = a; } else { points = [ a, b ]; } var firstPoint = points.slice().shift(); return every(points, function(point) { return Math.abs(firstPoint.x - point.x) <= ALIGNED_THRESHOLD; }); } /** * Returns true if the point p is inside the rectangle rect * * @param {Point} p * @param {Rect} rect * @param {number} tolerance * * @return {boolean} */ function pointInRect(p, rect, tolerance) { tolerance = tolerance || 0; return p.x > rect.x - tolerance && p.y > rect.y - tolerance && p.x < rect.x + rect.width + tolerance && p.y < rect.y + rect.height + tolerance; } /** * Returns a point in the middle of points p and q * * @param {Point} p * @param {Point} q * * @return {Point} middle point */ function getMidPoint(p, q) { return { x: Math.round(p.x + ((q.x - p.x) / 2.0)), y: Math.round(p.y + ((q.y - p.y) / 2.0)) }; } var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } /** * This file contains source code adapted from Snap.svg (licensed Apache-2.0). * * @see https://github.com/adobe-webplatform/Snap.svg/blob/master/src/path.js */ /* eslint no-fallthrough: "off" */ var p2s = /,?([a-z]),?/gi, toFloat = parseFloat, math = Math, PI = math.PI, mmin = math.min, mmax = math.max, pow = math.pow, abs$7 = math.abs, pathCommand = /([a-z])[\s,]*((-?\d*\.?\d*(?:e[-+]?\d+)?[\s]*,?[\s]*)+)/ig, pathValues = /(-?\d*\.?\d*(?:e[-+]?\d+)?)[\s]*,?[\s]*/ig; var isArray = Array.isArray || function(o) { return o instanceof Array; }; function hasProperty(obj, property) { return Object.prototype.hasOwnProperty.call(obj, property); } function clone(obj) { if (typeof obj == 'function' || Object(obj) !== obj) { return obj; } var res = new obj.constructor; for (var key in obj) { if (hasProperty(obj, key)) { res[key] = clone(obj[key]); } } return res; } function repush(array, item) { for (var i = 0, ii = array.length; i < ii; i++) if (array[i] === item) { return array.push(array.splice(i, 1)[0]); } } function cacher(f) { function newf() { var arg = Array.prototype.slice.call(arguments, 0), args = arg.join('\u2400'), cache = newf.cache = newf.cache || {}, count = newf.count = newf.count || []; if (hasProperty(cache, args)) { repush(count, args); return cache[args]; } count.length >= 1e3 && delete cache[count.shift()]; count.push(args); cache[args] = f.apply(0, arg); return cache[args]; } return newf; } function parsePathString(pathString) { if (!pathString) { return null; } var pth = paths(pathString); if (pth.arr) { return clone(pth.arr); } var paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0 }, data = []; if (isArray(pathString) && isArray(pathString[0])) { // rough assumption data = clone(pathString); } if (!data.length) { String(pathString).replace(pathCommand, function(a, b, c) { var params = [], name = b.toLowerCase(); c.replace(pathValues, function(a, b) { b && params.push(+b); }); if (name == 'm' && params.length > 2) { data.push([b].concat(params.splice(0, 2))); name = 'l'; b = b == 'm' ? 'l' : 'L'; } while (params.length >= paramCounts[name]) { data.push([b].concat(params.splice(0, paramCounts[name]))); if (!paramCounts[name]) { break; } } }); } data.toString = paths.toString; pth.arr = clone(data); return data; } function paths(ps) { var p = paths.ps = paths.ps || {}; if (p[ps]) { p[ps].sleep = 100; } else { p[ps] = { sleep: 100 }; } setTimeout(function() { for (var key in p) { if (hasProperty(p, key) && key != ps) { p[key].sleep--; !p[key].sleep && delete p[key]; } } }); return p[ps]; } function rectBBox(x, y, width, height) { if (arguments.length === 1) { y = x.y; width = x.width; height = x.height; x = x.x; } return { x: x, y: y, width: width, height: height, x2: x + width, y2: y + height }; } function pathToString() { return this.join(',').replace(p2s, '$1'); } function pathClone(pathArray) { var res = clone(pathArray); res.toString = pathToString; return res; } function findDotsAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) { var t1 = 1 - t, t13 = pow(t1, 3), t12 = pow(t1, 2), t2 = t * t, t3 = t2 * t, x = t13 * p1x + t12 * 3 * t * c1x + t1 * 3 * t * t * c2x + t3 * p2x, y = t13 * p1y + t12 * 3 * t * c1y + t1 * 3 * t * t * c2y + t3 * p2y; return { x: fixError(x), y: fixError(y) }; } function bezierBBox(points) { var bbox = curveBBox.apply(null, points); return rectBBox( bbox.x0, bbox.y0, bbox.x1 - bbox.x0, bbox.y1 - bbox.y0 ); } function isPointInsideBBox$2(bbox, x, y) { return x >= bbox.x && x <= bbox.x + bbox.width && y >= bbox.y && y <= bbox.y + bbox.height; } function isBBoxIntersect(bbox1, bbox2) { bbox1 = rectBBox(bbox1); bbox2 = rectBBox(bbox2); return isPointInsideBBox$2(bbox2, bbox1.x, bbox1.y) || isPointInsideBBox$2(bbox2, bbox1.x2, bbox1.y) || isPointInsideBBox$2(bbox2, bbox1.x, bbox1.y2) || isPointInsideBBox$2(bbox2, bbox1.x2, bbox1.y2) || isPointInsideBBox$2(bbox1, bbox2.x, bbox2.y) || isPointInsideBBox$2(bbox1, bbox2.x2, bbox2.y) || isPointInsideBBox$2(bbox1, bbox2.x, bbox2.y2) || isPointInsideBBox$2(bbox1, bbox2.x2, bbox2.y2) || (bbox1.x < bbox2.x2 && bbox1.x > bbox2.x || bbox2.x < bbox1.x2 && bbox2.x > bbox1.x) && (bbox1.y < bbox2.y2 && bbox1.y > bbox2.y || bbox2.y < bbox1.y2 && bbox2.y > bbox1.y); } function base3(t, p1, p2, p3, p4) { var t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4, t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3; return t * t2 - 3 * p1 + 3 * p2; } function bezlen(x1, y1, x2, y2, x3, y3, x4, y4, z) { if (z == null) { z = 1; } z = z > 1 ? 1 : z < 0 ? 0 : z; var z2 = z / 2, n = 12, Tvalues = [-.1252,.1252,-.3678,.3678,-.5873,.5873,-.7699,.7699,-.9041,.9041,-.9816,.9816], Cvalues = [0.2491,0.2491,0.2335,0.2335,0.2032,0.2032,0.1601,0.1601,0.1069,0.1069,0.0472,0.0472], sum = 0; for (var i = 0; i < n; i++) { var ct = z2 * Tvalues[i] + z2, xbase = base3(ct, x1, x2, x3, x4), ybase = base3(ct, y1, y2, y3, y4), comb = xbase * xbase + ybase * ybase; sum += Cvalues[i] * math.sqrt(comb); } return z2 * sum; } function intersectLines(x1, y1, x2, y2, x3, y3, x4, y4) { if ( mmax(x1, x2) < mmin(x3, x4) || mmin(x1, x2) > mmax(x3, x4) || mmax(y1, y2) < mmin(y3, y4) || mmin(y1, y2) > mmax(y3, y4) ) { return; } var nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4), ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4), denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (!denominator) { return; } var px = fixError(nx / denominator), py = fixError(ny / denominator), px2 = +px.toFixed(2), py2 = +py.toFixed(2); if ( px2 < +mmin(x1, x2).toFixed(2) || px2 > +mmax(x1, x2).toFixed(2) || px2 < +mmin(x3, x4).toFixed(2) || px2 > +mmax(x3, x4).toFixed(2) || py2 < +mmin(y1, y2).toFixed(2) || py2 > +mmax(y1, y2).toFixed(2) || py2 < +mmin(y3, y4).toFixed(2) || py2 > +mmax(y3, y4).toFixed(2) ) { return; } return { x: px, y: py }; } function fixError(number) { return Math.round(number * 100000000000) / 100000000000; } function findBezierIntersections(bez1, bez2, justCount) { var bbox1 = bezierBBox(bez1), bbox2 = bezierBBox(bez2); if (!isBBoxIntersect(bbox1, bbox2)) { return justCount ? 0 : []; } // As an optimization, lines will have only 1 segment var l1 = bezlen.apply(0, bez1), l2 = bezlen.apply(0, bez2), n1 = isLine(bez1) ? 1 : ~~(l1 / 5) || 1, n2 = isLine(bez2) ? 1 : ~~(l2 / 5) || 1, dots1 = [], dots2 = [], xy = {}, res = justCount ? 0 : []; for (var i = 0; i < n1 + 1; i++) { var p = findDotsAtSegment.apply(0, bez1.concat(i / n1)); dots1.push({ x: p.x, y: p.y, t: i / n1 }); } for (i = 0; i < n2 + 1; i++) { p = findDotsAtSegment.apply(0, bez2.concat(i / n2)); dots2.push({ x: p.x, y: p.y, t: i / n2 }); } for (i = 0; i < n1; i++) { for (var j = 0; j < n2; j++) { var di = dots1[i], di1 = dots1[i + 1], dj = dots2[j], dj1 = dots2[j + 1], ci = abs$7(di1.x - di.x) < .01 ? 'y' : 'x', cj = abs$7(dj1.x - dj.x) < .01 ? 'y' : 'x', is = intersectLines(di.x, di.y, di1.x, di1.y, dj.x, dj.y, dj1.x, dj1.y), key; if (is) { key = is.x.toFixed(9) + '#' + is.y.toFixed(9); if (xy[key]) { continue; } xy[key] = true; var t1 = di.t + abs$7((is[ci] - di[ci]) / (di1[ci] - di[ci])) * (di1.t - di.t), t2 = dj.t + abs$7((is[cj] - dj[cj]) / (dj1[cj] - dj[cj])) * (dj1.t - dj.t); if (t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1) { if (justCount) { res++; } else { res.push({ x: is.x, y: is.y, t1: t1, t2: t2 }); } } } } } return res; } /** * Find or counts the intersections between two SVG paths. * * Returns a number in counting mode and a list of intersections otherwise. * * A single intersection entry contains the intersection coordinates (x, y) * as well as additional information regarding the intersecting segments * on each path (segment1, segment2) and the relative location of the * intersection on these segments (t1, t2). * * The path may be an SVG path string or a list of path components * such as `[ [ 'M', 0, 10 ], [ 'L', 20, 0 ] ]`. * * @example * * var intersections = findPathIntersections( * 'M0,0L100,100', * [ [ 'M', 0, 100 ], [ 'L', 100, 0 ] ] * ); * * // intersections = [ * // { x: 50, y: 50, segment1: 1, segment2: 1, t1: 0.5, t2: 0.5 } * // ] * * @param {String|Array} path1 * @param {String|Array} path2 * @param {Boolean} [justCount=false] * * @return {Array|Number} */ function findPathIntersections(path1, path2, justCount) { path1 = pathToCurve(path1); path2 = pathToCurve(path2); var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2, res = justCount ? 0 : []; for (var i = 0, ii = path1.length; i < ii; i++) { var pi = path1[i]; if (pi[0] == 'M') { x1 = x1m = pi[1]; y1 = y1m = pi[2]; } else { if (pi[0] == 'C') { bez1 = [x1, y1].concat(pi.slice(1)); x1 = bez1[6]; y1 = bez1[7]; } else { bez1 = [x1, y1, x1, y1, x1m, y1m, x1m, y1m]; x1 = x1m; y1 = y1m; } for (var j = 0, jj = path2.length; j < jj; j++) { var pj = path2[j]; if (pj[0] == 'M') { x2 = x2m = pj[1]; y2 = y2m = pj[2]; } else { if (pj[0] == 'C') { bez2 = [x2, y2].concat(pj.slice(1)); x2 = bez2[6]; y2 = bez2[7]; } else { bez2 = [x2, y2, x2, y2, x2m, y2m, x2m, y2m]; x2 = x2m; y2 = y2m; } var intr = findBezierIntersections(bez1, bez2, justCount); if (justCount) { res += intr; } else { for (var k = 0, kk = intr.length; k < kk; k++) { intr[k].segment1 = i; intr[k].segment2 = j; intr[k].bez1 = bez1; intr[k].bez2 = bez2; } res = res.concat(intr); } } } } } return res; } function pathToAbsolute(pathArray) { var pth = paths(pathArray); if (pth.abs) { return pathClone(pth.abs); } if (!isArray(pathArray) || !isArray(pathArray && pathArray[0])) { // rough assumption pathArray = parsePathString(pathArray); } if (!pathArray || !pathArray.length) { return [['M', 0, 0]]; } var res = [], x = 0, y = 0, mx = 0, my = 0, start = 0, pa0; if (pathArray[0][0] == 'M') { x = +pathArray[0][1]; y = +pathArray[0][2]; mx = x; my = y; start++; res[0] = ['M', x, y]; } for (var r, pa, i = start, ii = pathArray.length; i < ii; i++) { res.push(r = []); pa = pathArray[i]; pa0 = pa[0]; if (pa0 != pa0.toUpperCase()) { r[0] = pa0.toUpperCase(); switch (r[0]) { case 'A': r[1] = pa[1]; r[2] = pa[2]; r[3] = pa[3]; r[4] = pa[4]; r[5] = pa[5]; r[6] = +pa[6] + x; r[7] = +pa[7] + y; break; case 'V': r[1] = +pa[1] + y; break; case 'H': r[1] = +pa[1] + x; break; case 'M': mx = +pa[1] + x; my = +pa[2] + y; default: for (var j = 1, jj = pa.length; j < jj; j++) { r[j] = +pa[j] + ((j % 2) ? x : y); } } } else { for (var k = 0, kk = pa.length; k < kk; k++) { r[k] = pa[k]; } } pa0 = pa0.toUpperCase(); switch (r[0]) { case 'Z': x = +mx; y = +my; break; case 'H': x = r[1]; break; case 'V': y = r[1]; break; case 'M': mx = r[r.length - 2]; my = r[r.length - 1]; default: x = r[r.length - 2]; y = r[r.length - 1]; } } res.toString = pathToString; pth.abs = pathClone(res); return res; } function isLine(bez) { return ( bez[0] === bez[2] && bez[1] === bez[3] && bez[4] === bez[6] && bez[5] === bez[7] ); } function lineToCurve(x1, y1, x2, y2) { return [ x1, y1, x2, y2, x2, y2 ]; } function qubicToCurve(x1, y1, ax, ay, x2, y2) { var _13 = 1 / 3, _23 = 2 / 3; return [ _13 * x1 + _23 * ax, _13 * y1 + _23 * ay, _13 * x2 + _23 * ax, _13 * y2 + _23 * ay, x2, y2 ]; } function arcToCurve(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) { // for more information of where this math came from visit: // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes var _120 = PI * 120 / 180, rad = PI / 180 * (+angle || 0), res = [], xy, rotate = cacher(function(x, y, rad) { var X = x * math.cos(rad) - y * math.sin(rad), Y = x * math.sin(rad) + y * math.cos(rad); return { x: X, y: Y }; }); if (!recursive) { xy = rotate(x1, y1, -rad); x1 = xy.x; y1 = xy.y; xy = rotate(x2, y2, -rad); x2 = xy.x; y2 = xy.y; var x = (x1 - x2) / 2, y = (y1 - y2) / 2; var h = (x * x) / (rx * rx) + (y * y) / (ry * ry); if (h > 1) { h = math.sqrt(h); rx = h * rx; ry = h * ry; } var rx2 = rx * rx, ry2 = ry * ry, k = (large_arc_flag == sweep_flag ? -1 : 1) * math.sqrt(abs$7((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))), cx = k * rx * y / ry + (x1 + x2) / 2, cy = k * -ry * x / rx + (y1 + y2) / 2, f1 = math.asin(((y1 - cy) / ry).toFixed(9)), f2 = math.asin(((y2 - cy) / ry).toFixed(9)); f1 = x1 < cx ? PI - f1 : f1; f2 = x2 < cx ? PI - f2 : f2; f1 < 0 && (f1 = PI * 2 + f1); f2 < 0 && (f2 = PI * 2 + f2); if (sweep_flag && f1 > f2) { f1 = f1 - PI * 2; } if (!sweep_flag && f2 > f1) { f2 = f2 - PI * 2; } } else { f1 = recursive[0]; f2 = recursive[1]; cx = recursive[2]; cy = recursive[3]; } var df = f2 - f1; if (abs$7(df) > _120) { var f2old = f2, x2old = x2, y2old = y2; f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1); x2 = cx + rx * math.cos(f2); y2 = cy + ry * math.sin(f2); res = arcToCurve(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]); } df = f2 - f1; var c1 = math.cos(f1), s1 = math.sin(f1), c2 = math.cos(f2), s2 = math.sin(f2), t = math.tan(df / 4), hx = 4 / 3 * rx * t, hy = 4 / 3 * ry * t, m1 = [x1, y1], m2 = [x1 + hx * s1, y1 - hy * c1], m3 = [x2 + hx * s2, y2 - hy * c2], m4 = [x2, y2]; m2[0] = 2 * m1[0] - m2[0]; m2[1] = 2 * m1[1] - m2[1]; if (recursive) { return [m2, m3, m4].concat(res); } else { res = [m2, m3, m4].concat(res).join().split(','); var newres = []; for (var i = 0, ii = res.length; i < ii; i++) { newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x; } return newres; } } // Returns bounding box of cubic bezier curve. // Source: http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html // Original version: NISHIO Hirokazu // Modifications: https://github.com/timo22345 function curveBBox(x0, y0, x1, y1, x2, y2, x3, y3) { var tvalues = [], bounds = [[], []], a, b, c, t, t1, t2, b2ac, sqrtb2ac; for (var i = 0; i < 2; ++i) { if (i == 0) { b = 6 * x0 - 12 * x1 + 6 * x2; a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; c = 3 * x1 - 3 * x0; } else { b = 6 * y0 - 12 * y1 + 6 * y2; a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; c = 3 * y1 - 3 * y0; } if (abs$7(a) < 1e-12) { if (abs$7(b) < 1e-12) { continue; } t = -c / b; if (0 < t && t < 1) { tvalues.push(t); } continue; } b2ac = b * b - 4 * c * a; sqrtb2ac = math.sqrt(b2ac); if (b2ac < 0) { continue; } t1 = (-b + sqrtb2ac) / (2 * a); if (0 < t1 && t1 < 1) { tvalues.push(t1); } t2 = (-b - sqrtb2ac) / (2 * a); if (0 < t2 && t2 < 1) { tvalues.push(t2); } } var j = tvalues.length, jlen = j, mt; while (j--) { t = tvalues[j]; mt = 1 - t; bounds[0][j] = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); bounds[1][j] = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); } bounds[0][jlen] = x0; bounds[1][jlen] = y0; bounds[0][jlen + 1] = x3; bounds[1][jlen + 1] = y3; bounds[0].length = bounds[1].length = jlen + 2; return { x0: mmin.apply(0, bounds[0]), y0: mmin.apply(0, bounds[1]), x1: mmax.apply(0, bounds[0]), y1: mmax.apply(0, bounds[1]) }; } function pathToCurve(path) { var pth = paths(path); // return cached curve, if existing if (pth.curve) { return pathClone(pth.curve); } var curvedPath = pathToAbsolute(path), attrs = { x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null }, processPath = function(path, d, pathCommand) { var nx, ny; if (!path) { return ['C', d.x, d.y, d.x, d.y, d.x, d.y]; } !(path[0] in { T: 1, Q: 1 }) && (d.qx = d.qy = null); switch (path[0]) { case 'M': d.X = path[1]; d.Y = path[2]; break; case 'A': path = ['C'].concat(arcToCurve.apply(0, [d.x, d.y].concat(path.slice(1)))); break; case 'S': if (pathCommand == 'C' || pathCommand == 'S') { // In 'S' case we have to take into account, if the previous command is C/S. nx = d.x * 2 - d.bx; // And reflect the previous ny = d.y * 2 - d.by; // command's control point relative to the current point. } else { // or some else or nothing nx = d.x; ny = d.y; } path = ['C', nx, ny].concat(path.slice(1)); break; case 'T': if (pathCommand == 'Q' || pathCommand == 'T') { // In 'T' case we have to take into account, if the previous command is Q/T. d.qx = d.x * 2 - d.qx; // And make a reflection similar d.qy = d.y * 2 - d.qy; // to case 'S'. } else { // or something else or nothing d.qx = d.x; d.qy = d.y; } path = ['C'].concat(qubicToCurve(d.x, d.y, d.qx, d.qy, path[1], path[2])); break; case 'Q': d.qx = path[1]; d.qy = path[2]; path = ['C'].concat(qubicToCurve(d.x, d.y, path[1], path[2], path[3], path[4])); break; case 'L': path = ['C'].concat(lineToCurve(d.x, d.y, path[1], path[2])); break; case 'H': path = ['C'].concat(lineToCurve(d.x, d.y, path[1], d.y)); break; case 'V': path = ['C'].concat(lineToCurve(d.x, d.y, d.x, path[1])); break; case 'Z': path = ['C'].concat(lineToCurve(d.x, d.y, d.X, d.Y)); break; } return path; }, fixArc = function(pp, i) { if (pp[i].length > 7) { pp[i].shift(); var pi = pp[i]; while (pi.length) { pathCommands[i] = 'A'; // if created multiple C:s, their original seg is saved pp.splice(i++, 0, ['C'].concat(pi.splice(0, 6))); } pp.splice(i, 1); ii = curvedPath.length; } }, pathCommands = [], // path commands of original path p pfirst = '', // temporary holder for original path command pathCommand = ''; // holder for previous path command of original path for (var i = 0, ii = curvedPath.length; i < ii; i++) { curvedPath[i] && (pfirst = curvedPath[i][0]); // save current path command if (pfirst != 'C') // C is not saved yet, because it may be result of conversion { pathCommands[i] = pfirst; // Save current path command i && (pathCommand = pathCommands[i - 1]); // Get previous path command pathCommand } curvedPath[i] = processPath(curvedPath[i], attrs, pathCommand); // Previous path command is inputted to processPath if (pathCommands[i] != 'A' && pfirst == 'C') pathCommands[i] = 'C'; // A is the only command // which may produce multiple C:s // so we have to make sure that C is also C in original path fixArc(curvedPath, i); // fixArc adds also the right amount of A:s to pathCommands var seg = curvedPath[i], seglen = seg.length; attrs.x = seg[seglen - 2]; attrs.y = seg[seglen - 1]; attrs.bx = toFloat(seg[seglen - 4]) || attrs.x; attrs.by = toFloat(seg[seglen - 3]) || attrs.y; } // cache curve pth.curve = pathClone(curvedPath); return curvedPath; } var intersect = findPathIntersections; function roundBounds(bounds) { return { x: Math.round(bounds.x), y: Math.round(bounds.y), width: Math.round(bounds.width), height: Math.round(bounds.height) }; } function roundPoint(point) { return { x: Math.round(point.x), y: Math.round(point.y) }; } /** * Convert the given bounds to a { top, left, bottom, right } descriptor. * * @param {Bounds|Point} bounds * * @return {Object} */ function asTRBL(bounds) { return { top: bounds.y, right: bounds.x + (bounds.width || 0), bottom: bounds.y + (bounds.height || 0), left: bounds.x }; } /** * Convert a { top, left, bottom, right } to an objects bounds. * * @param {Object} trbl * * @return {Bounds} */ function asBounds(trbl) { return { x: trbl.left, y: trbl.top, width: trbl.right - trbl.left, height: trbl.bottom - trbl.top }; } /** * Get the mid of the given bounds or point. * * @param {Bounds|Point} bounds * * @return {Point} */ function getBoundsMid(bounds) { return roundPoint({ x: bounds.x + (bounds.width || 0) / 2, y: bounds.y + (bounds.height || 0) / 2 }); } /** * Get the mid of the given Connection. * * @param {djs.Base.Connection} connection * * @return {Point} */ function getConnectionMid(connection) { var waypoints = connection.waypoints; // calculate total length and length of each segment var parts = waypoints.reduce(function(parts, point, index) { var lastPoint = waypoints[index - 1]; if (lastPoint) { var lastPart = parts[parts.length - 1]; var startLength = lastPart && lastPart.endLength || 0; var length = distance(lastPoint, point); parts.push({ start: lastPoint, end: point, startLength: startLength, endLength: startLength + length, length: length }); } return parts; }, []); var totalLength = parts.reduce(function(length, part) { return length + part.length; }, 0); // find which segement contains middle point var midLength = totalLength / 2; var i = 0; var midSegment = parts[i]; while (midSegment.endLength < midLength) { midSegment = parts[++i]; } // calculate relative position on mid segment var segmentProgress = (midLength - midSegment.startLength) / midSegment.length; var midPoint = { x: midSegment.start.x + (midSegment.end.x - midSegment.start.x) * segmentProgress, y: midSegment.start.y + (midSegment.end.y - midSegment.start.y) * segmentProgress }; return midPoint; } /** * Get the mid of the given Element. * * @param {djs.Base.Connection} connection * * @return {Point} */ function getMid(element) { if (isConnection$f(element)) { return getConnectionMid(element); } return getBoundsMid(element); } // orientation utils ////////////////////// /** * Get orientation of the given rectangle with respect to * the reference rectangle. * * A padding (positive or negative) may be passed to influence * horizontal / vertical orientation and intersection. * * @param {Bounds} rect * @param {Bounds} reference * @param {Point|number} padding * * @return {string} the orientation; one of top, top-left, left, ..., bottom, right or intersect. */ function getOrientation(rect, reference, padding) { padding = padding || 0; // make sure we can use an object, too // for individual { x, y } padding if (!isObject(padding)) { padding = { x: padding, y: padding }; } var rectOrientation = asTRBL(rect), referenceOrientation = asTRBL(reference); var top = rectOrientation.bottom + padding.y <= referenceOrientation.top, right = rectOrientation.left - padding.x >= referenceOrientation.right, bottom = rectOrientation.top - padding.y >= referenceOrientation.bottom, left = rectOrientation.right + padding.x <= referenceOrientation.left; var vertical = top ? 'top' : (bottom ? 'bottom' : null), horizontal = left ? 'left' : (right ? 'right' : null); if (horizontal && vertical) { return vertical + '-' + horizontal; } else { return horizontal || vertical || 'intersect'; } } // intersection utils ////////////////////// /** * Get intersection between an element and a line path. * * @param {PathDef} elementPath * @param {PathDef} linePath * @param {boolean} cropStart crop from start or end * * @return {Point} */ function getElementLineIntersection(elementPath, linePath, cropStart) { var intersections = getIntersections(elementPath, linePath); // recognize intersections // only one -> choose // two close together -> choose first // two or more distinct -> pull out appropriate one // none -> ok (fallback to point itself) if (intersections.length === 1) { return roundPoint(intersections[0]); } else if (intersections.length === 2 && pointDistance(intersections[0], intersections[1]) < 1) { return roundPoint(intersections[0]); } else if (intersections.length > 1) { // sort by intersections based on connection segment + // distance from start intersections = sortBy(intersections, function(i) { var distance = Math.floor(i.t2 * 100) || 1; distance = 100 - distance; distance = (distance < 10 ? '0' : '') + distance; // create a sort string that makes sure we sort // line segment ASC + line segment position DESC (for cropStart) // line segment ASC + line segment position ASC (for cropEnd) return i.segment2 + '#' + distance; }); return roundPoint(intersections[cropStart ? 0 : intersections.length - 1]); } return null; } function getIntersections(a, b) { return intersect(a, b); } function filterRedundantWaypoints(waypoints) { // alter copy of waypoints, not original waypoints = waypoints.slice(); var idx = 0, point, previousPoint, nextPoint; while (waypoints[idx]) { point = waypoints[idx]; previousPoint = waypoints[idx - 1]; nextPoint = waypoints[idx + 1]; if (pointDistance(point, nextPoint) === 0 || pointsOnLine(previousPoint, nextPoint, point)) { // remove point, if overlapping with {nextPoint} // or on line with {previousPoint} -> {point} -> {nextPoint} waypoints.splice(idx, 1); } else { idx++; } } return waypoints; } // helpers ////////////////////// function distance(a, b) { return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); } function isConnection$f(element) { return !!element.waypoints; } function round$b(number, resolution) { return Math.round(number * resolution) / resolution; } function ensurePx(number) { return isNumber(number) ? number + 'px' : number; } function findRoot(element) { while (element.parent) { element = element.parent; } return element; } /** * Creates a HTML container element for a SVG element with * the given configuration * * @param {Object} options * @return {HTMLElement} the container element */ function createContainer(options) { options = assign({}, { width: '100%', height: '100%' }, options); var container = options.container || document.body; // create a
around the svg element with the respective size // this way we can always get the correct container size // (this is impossible for elements at the moment) var parent = document.createElement('div'); parent.setAttribute('class', 'djs-container'); assign$1(parent, { position: 'relative', overflow: 'hidden', width: ensurePx(options.width), height: ensurePx(options.height) }); container.appendChild(parent); return parent; } function createGroup(parent, cls, childIndex) { var group = create$1('g'); classes(group).add(cls); var index = childIndex !== undefined ? childIndex : parent.childNodes.length - 1; // must ensure second argument is node or _null_ // cf. https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore parent.insertBefore(group, parent.childNodes[index] || null); return group; } var BASE_LAYER = 'base'; // render plane contents behind utility layers var PLANE_LAYER_INDEX = 0; var UTILITY_LAYER_INDEX = 1; var REQUIRED_MODEL_ATTRS = { shape: [ 'x', 'y', 'width', 'height' ], connection: [ 'waypoints' ] }; /** * The main drawing canvas. * * @class * @constructor * * @emits Canvas#canvas.init * * @param {Object} config * @param {EventBus} eventBus * @param {GraphicsFactory} graphicsFactory * @param {ElementRegistry} elementRegistry */ function Canvas(config, eventBus, graphicsFactory, elementRegistry) { this._eventBus = eventBus; this._elementRegistry = elementRegistry; this._graphicsFactory = graphicsFactory; this._rootsIdx = 0; this._layers = {}; this._planes = []; this._rootElement = null; this._init(config || {}); } Canvas.$inject = [ 'config.canvas', 'eventBus', 'graphicsFactory', 'elementRegistry' ]; /** * Creates a element that is wrapped into a
. * This way we are always able to correctly figure out the size of the svg element * by querying the parent node. * (It is not possible to get the size of a svg element cross browser @ 2014-04-01) *
* * ... * *
*/ Canvas.prototype._init = function(config) { var eventBus = this._eventBus; // html container var container = this._container = createContainer(config); var svg = this._svg = create$1('svg'); attr(svg, { width: '100%', height: '100%' }); append(container, svg); var viewport = this._viewport = createGroup(svg, 'viewport'); // debounce canvas.viewbox.changed events // for smoother diagram interaction if (config.deferUpdate !== false) { this._viewboxChanged = debounce(bind(this._viewboxChanged, this), 300); } eventBus.on('diagram.init', function() { /** * An event indicating that the canvas is ready to be drawn on. * * @memberOf Canvas * * @event canvas.init * * @type {Object} * @property {SVGElement} svg the created svg element * @property {SVGElement} viewport the direct parent of diagram elements and shapes */ eventBus.fire('canvas.init', { svg: svg, viewport: viewport }); }, this); // reset viewbox on shape changes to // recompute the viewbox eventBus.on([ 'shape.added', 'connection.added', 'shape.removed', 'connection.removed', 'elements.changed', 'root.set' ], function() { delete this._cachedViewbox; }, this); eventBus.on('diagram.destroy', 500, this._destroy, this); eventBus.on('diagram.clear', 500, this._clear, this); }; Canvas.prototype._destroy = function(emit) { this._eventBus.fire('canvas.destroy', { svg: this._svg, viewport: this._viewport }); var parent = this._container.parentNode; if (parent) { parent.removeChild(this._container); } delete this._svg; delete this._container; delete this._layers; delete this._planes; delete this._rootElement; delete this._viewport; }; Canvas.prototype._clear = function() { var self = this; var allElements = this._elementRegistry.getAll(); // remove all elements allElements.forEach(function(element) { var type = getType(element); if (type === 'root') { self.removeRootElement(element); } else { self._removeElement(element, type); } }); // remove all planes this._planes = []; this._rootElement = null; // force recomputation of view box delete this._cachedViewbox; }; /** * Returns the default layer on which * all elements are drawn. * * @returns {SVGElement} */ Canvas.prototype.getDefaultLayer = function() { return this.getLayer(BASE_LAYER, PLANE_LAYER_INDEX); }; /** * Returns a layer that is used to draw elements * or annotations on it. * * Non-existing layers retrieved through this method * will be created. During creation, the optional index * may be used to create layers below or above existing layers. * A layer with a certain index is always created above all * existing layers with the same index. * * @param {string} name * @param {number} index * * @returns {SVGElement} */ Canvas.prototype.getLayer = function(name, index) { if (!name) { throw new Error('must specify a name'); } var layer = this._layers[name]; if (!layer) { layer = this._layers[name] = this._createLayer(name, index); } // throw an error if layer creation / retrival is // requested on different index if (typeof index !== 'undefined' && layer.index !== index) { throw new Error('layer <' + name + '> already created at index <' + index + '>'); } return layer.group; }; /** * For a given index, return the number of layers that have a higher index and * are visible. * * This is used to determine the node a layer should be inserted at. * * @param {Number} index * @returns {Number} */ Canvas.prototype._getChildIndex = function(index) { return reduce(this._layers, function(childIndex, layer) { if (layer.visible && index >= layer.index) { childIndex++; } return childIndex; }, 0); }; /** * Creates a given layer and returns it. * * @param {string} name * @param {number} [index=0] * * @return {Object} layer descriptor with { index, group: SVGGroup } */ Canvas.prototype._createLayer = function(name, index) { if (typeof index === 'undefined') { index = UTILITY_LAYER_INDEX; } var childIndex = this._getChildIndex(index); return { group: createGroup(this._viewport, 'layer-' + name, childIndex), index: index, visible: true }; }; /** * Shows a given layer. * * @param {String} layer * @returns {SVGElement} */ Canvas.prototype.showLayer = function(name) { if (!name) { throw new Error('must specify a name'); } var layer = this._layers[name]; if (!layer) { throw new Error('layer <' + name + '> does not exist'); } var viewport = this._viewport; var group = layer.group; var index = layer.index; if (layer.visible) { return group; } var childIndex = this._getChildIndex(index); viewport.insertBefore(group, viewport.childNodes[childIndex] || null); layer.visible = true; return group; }; /** * Hides a given layer. * * @param {String} layer * @returns {SVGElement} */ Canvas.prototype.hideLayer = function(name) { if (!name) { throw new Error('must specify a name'); } var layer = this._layers[name]; if (!layer) { throw new Error('layer <' + name + '> does not exist'); } var group = layer.group; if (!layer.visible) { return group; } remove$1(group); layer.visible = false; return group; }; Canvas.prototype._removeLayer = function(name) { var layer = this._layers[name]; if (layer) { delete this._layers[name]; remove$1(layer.group); } }; /** * Returns the currently active layer. Can be null. * * @returns {SVGElement|null} */ Canvas.prototype.getActiveLayer = function() { var plane = this._findPlaneForRoot(this.getRootElement()); if (!plane) { return null; } return plane.layer; }; /** * Returns the plane which contains the given element. * * @param {string|djs.model.Base} element * * @return {djs.model.Base} root for element */ Canvas.prototype.findRoot = function(element) { if (typeof element === 'string') { element = this._elementRegistry.get(element); } if (!element) { return; } var plane = this._findPlaneForRoot( findRoot(element) ) || {}; return plane.rootElement; }; /** * Return a list of all root elements on the diagram. * * @return {djs.model.Root[]} */ Canvas.prototype.getRootElements = function() { return this._planes.map(function(plane) { return plane.rootElement; }); }; Canvas.prototype._findPlaneForRoot = function(rootElement) { return find(this._planes, function(plane) { return plane.rootElement === rootElement; }); }; /** * Returns the html element that encloses the * drawing canvas. * * @return {DOMNode} */ Canvas.prototype.getContainer = function() { return this._container; }; // markers ////////////////////// Canvas.prototype._updateMarker = function(element, marker, add) { var container; if (!element.id) { element = this._elementRegistry.get(element); } // we need to access all container = this._elementRegistry._elements[element.id]; if (!container) { return; } forEach$1([ container.gfx, container.secondaryGfx ], function(gfx) { if (gfx) { // invoke either addClass or removeClass based on mode if (add) { classes(gfx).add(marker); } else { classes(gfx).remove(marker); } } }); /** * An event indicating that a marker has been updated for an element * * @event element.marker.update * @type {Object} * @property {djs.model.Element} element the shape * @property {Object} gfx the graphical representation of the shape * @property {string} marker * @property {boolean} add true if the marker was added, false if it got removed */ this._eventBus.fire('element.marker.update', { element: element, gfx: container.gfx, marker: marker, add: !!add }); }; /** * Adds a marker to an element (basically a css class). * * Fires the element.marker.update event, making it possible to * integrate extension into the marker life-cycle, too. * * @example * canvas.addMarker('foo', 'some-marker'); * * var fooGfx = canvas.getGraphics('foo'); * * fooGfx; // ... * * @param {string|djs.model.Base} element * @param {string} marker */ Canvas.prototype.addMarker = function(element, marker) { this._updateMarker(element, marker, true); }; /** * Remove a marker from an element. * * Fires the element.marker.update event, making it possible to * integrate extension into the marker life-cycle, too. * * @param {string|djs.model.Base} element * @param {string} marker */ Canvas.prototype.removeMarker = function(element, marker) { this._updateMarker(element, marker, false); }; /** * Check the existence of a marker on element. * * @param {string|djs.model.Base} element * @param {string} marker */ Canvas.prototype.hasMarker = function(element, marker) { if (!element.id) { element = this._elementRegistry.get(element); } var gfx = this.getGraphics(element); return classes(gfx).has(marker); }; /** * Toggles a marker on an element. * * Fires the element.marker.update event, making it possible to * integrate extension into the marker life-cycle, too. * * @param {string|djs.model.Base} element * @param {string} marker */ Canvas.prototype.toggleMarker = function(element, marker) { if (this.hasMarker(element, marker)) { this.removeMarker(element, marker); } else { this.addMarker(element, marker); } }; /** * Returns the current root element. * * Supports two different modes for handling root elements: * * 1. if no root element has been added before, an implicit root will be added * and returned. This is used in applications that don't require explicit * root elements. * * 2. when root elements have been added before calling `getRootElement`, * root elements can be null. This is used for applications that want to manage * root elements themselves. * * @returns {Object|djs.model.Root|null} rootElement. */ Canvas.prototype.getRootElement = function() { var rootElement = this._rootElement; // can return null if root elements are present but none was set yet if (rootElement || this._planes.length) { return rootElement; } return this.setRootElement(this.addRootElement(null)); }; /** * Adds a given root element and returns it. * * @param {Object|djs.model.Root} rootElement * * @return {Object|djs.model.Root} rootElement */ Canvas.prototype.addRootElement = function(rootElement) { var idx = this._rootsIdx++; if (!rootElement) { rootElement = { id: '__implicitroot_' + idx, children: [], isImplicit: true }; } var layerName = rootElement.layer = 'root-' + idx; this._ensureValid('root', rootElement); var layer = this.getLayer(layerName, PLANE_LAYER_INDEX); this.hideLayer(layerName); this._addRoot(rootElement, layer); this._planes.push({ rootElement: rootElement, layer: layer }); return rootElement; }; /** * Removes a given rootElement and returns it. * * @param {djs.model.Root|String} rootElement * * @return {Object|djs.model.Root} rootElement */ Canvas.prototype.removeRootElement = function(rootElement) { if (typeof rootElement === 'string') { rootElement = this._elementRegistry.get(rootElement); } var plane = this._findPlaneForRoot(rootElement); if (!plane) { return; } // hook up life-cycle events this._removeRoot(rootElement); // clean up layer this._removeLayer(rootElement.layer); // clean up plane this._planes = this._planes.filter(function(plane) { return plane.rootElement !== rootElement; }); // clean up active root if (this._rootElement === rootElement) { this._rootElement = null; } return rootElement; }; // root element handling ////////////////////// /** * Sets a given element as the new root element for the canvas * and returns the new root element. * * @param {Object|djs.model.Root} rootElement * * @return {Object|djs.model.Root} new root element */ Canvas.prototype.setRootElement = function(rootElement, override) { if (isDefined(override)) { throw new Error('override not supported'); } if (rootElement === this._rootElement) { return; } var plane; if (!rootElement) { throw new Error('rootElement required'); } plane = this._findPlaneForRoot(rootElement); // give set add semantics for backwards compatibility if (!plane) { rootElement = this.addRootElement(rootElement); } this._setRoot(rootElement); return rootElement; }; Canvas.prototype._removeRoot = function(element) { var elementRegistry = this._elementRegistry, eventBus = this._eventBus; // simulate element remove event sequence eventBus.fire('root.remove', { element: element }); eventBus.fire('root.removed', { element: element }); elementRegistry.remove(element); }; Canvas.prototype._addRoot = function(element, gfx) { var elementRegistry = this._elementRegistry, eventBus = this._eventBus; // resemble element add event sequence eventBus.fire('root.add', { element: element }); elementRegistry.add(element, gfx); eventBus.fire('root.added', { element: element, gfx: gfx }); }; Canvas.prototype._setRoot = function(rootElement, layer) { var currentRoot = this._rootElement; if (currentRoot) { // un-associate previous root element this._elementRegistry.updateGraphics(currentRoot, null, true); // hide previous layer this.hideLayer(currentRoot.layer); } if (rootElement) { if (!layer) { layer = this._findPlaneForRoot(rootElement).layer; } // associate element with this._elementRegistry.updateGraphics(rootElement, this._svg, true); // show root layer this.showLayer(rootElement.layer); } this._rootElement = rootElement; this._eventBus.fire('root.set', { element: rootElement }); }; // add functionality ////////////////////// Canvas.prototype._ensureValid = function(type, element) { if (!element.id) { throw new Error('element must have an id'); } if (this._elementRegistry.get(element.id)) { throw new Error('element <' + element.id + '> already exists'); } var requiredAttrs = REQUIRED_MODEL_ATTRS[type]; var valid = every(requiredAttrs, function(attr) { return typeof element[attr] !== 'undefined'; }); if (!valid) { throw new Error( 'must supply { ' + requiredAttrs.join(', ') + ' } with ' + type); } }; Canvas.prototype._setParent = function(element, parent, parentIndex) { add(parent.children, element, parentIndex); element.parent = parent; }; /** * Adds an element to the canvas. * * This wires the parent <-> child relationship between the element and * a explicitly specified parent or an implicit root element. * * During add it emits the events * * * <{type}.add> (element, parent) * * <{type}.added> (element, gfx) * * Extensions may hook into these events to perform their magic. * * @param {string} type * @param {Object|djs.model.Base} element * @param {Object|djs.model.Base} [parent] * @param {number} [parentIndex] * * @return {Object|djs.model.Base} the added element */ Canvas.prototype._addElement = function(type, element, parent, parentIndex) { parent = parent || this.getRootElement(); var eventBus = this._eventBus, graphicsFactory = this._graphicsFactory; this._ensureValid(type, element); eventBus.fire(type + '.add', { element: element, parent: parent }); this._setParent(element, parent, parentIndex); // create graphics var gfx = graphicsFactory.create(type, element, parentIndex); this._elementRegistry.add(element, gfx); // update its visual graphicsFactory.update(type, element, gfx); eventBus.fire(type + '.added', { element: element, gfx: gfx }); return element; }; /** * Adds a shape to the canvas * * @param {Object|djs.model.Shape} shape to add to the diagram * @param {djs.model.Base} [parent] * @param {number} [parentIndex] * * @return {djs.model.Shape} the added shape */ Canvas.prototype.addShape = function(shape, parent, parentIndex) { return this._addElement('shape', shape, parent, parentIndex); }; /** * Adds a connection to the canvas * * @param {Object|djs.model.Connection} connection to add to the diagram * @param {djs.model.Base} [parent] * @param {number} [parentIndex] * * @return {djs.model.Connection} the added connection */ Canvas.prototype.addConnection = function(connection, parent, parentIndex) { return this._addElement('connection', connection, parent, parentIndex); }; /** * Internal remove element */ Canvas.prototype._removeElement = function(element, type) { var elementRegistry = this._elementRegistry, graphicsFactory = this._graphicsFactory, eventBus = this._eventBus; element = elementRegistry.get(element.id || element); if (!element) { // element was removed already return; } eventBus.fire(type + '.remove', { element: element }); graphicsFactory.remove(element); // unset parent <-> child relationship remove(element.parent && element.parent.children, element); element.parent = null; eventBus.fire(type + '.removed', { element: element }); elementRegistry.remove(element); return element; }; /** * Removes a shape from the canvas * * @param {string|djs.model.Shape} shape or shape id to be removed * * @return {djs.model.Shape} the removed shape */ Canvas.prototype.removeShape = function(shape) { /** * An event indicating that a shape is about to be removed from the canvas. * * @memberOf Canvas * * @event shape.remove * @type {Object} * @property {djs.model.Shape} element the shape descriptor * @property {Object} gfx the graphical representation of the shape */ /** * An event indicating that a shape has been removed from the canvas. * * @memberOf Canvas * * @event shape.removed * @type {Object} * @property {djs.model.Shape} element the shape descriptor * @property {Object} gfx the graphical representation of the shape */ return this._removeElement(shape, 'shape'); }; /** * Removes a connection from the canvas * * @param {string|djs.model.Connection} connection or connection id to be removed * * @return {djs.model.Connection} the removed connection */ Canvas.prototype.removeConnection = function(connection) { /** * An event indicating that a connection is about to be removed from the canvas. * * @memberOf Canvas * * @event connection.remove * @type {Object} * @property {djs.model.Connection} element the connection descriptor * @property {Object} gfx the graphical representation of the connection */ /** * An event indicating that a connection has been removed from the canvas. * * @memberOf Canvas * * @event connection.removed * @type {Object} * @property {djs.model.Connection} element the connection descriptor * @property {Object} gfx the graphical representation of the connection */ return this._removeElement(connection, 'connection'); }; /** * Return the graphical object underlaying a certain diagram element * * @param {string|djs.model.Base} element descriptor of the element * @param {boolean} [secondary=false] whether to return the secondary connected element * * @return {SVGElement} */ Canvas.prototype.getGraphics = function(element, secondary) { return this._elementRegistry.getGraphics(element, secondary); }; /** * Perform a viewbox update via a given change function. * * @param {Function} changeFn */ Canvas.prototype._changeViewbox = function(changeFn) { // notify others of the upcoming viewbox change this._eventBus.fire('canvas.viewbox.changing'); // perform actual change changeFn.apply(this); // reset the cached viewbox so that // a new get operation on viewbox or zoom // triggers a viewbox re-computation this._cachedViewbox = null; // notify others of the change; this step // may or may not be debounced this._viewboxChanged(); }; Canvas.prototype._viewboxChanged = function() { this._eventBus.fire('canvas.viewbox.changed', { viewbox: this.viewbox() }); }; /** * Gets or sets the view box of the canvas, i.e. the * area that is currently displayed. * * The getter may return a cached viewbox (if it is currently * changing). To force a recomputation, pass `false` as the first argument. * * @example * * canvas.viewbox({ x: 100, y: 100, width: 500, height: 500 }) * * // sets the visible area of the diagram to (100|100) -> (600|100) * // and and scales it according to the diagram width * * var viewbox = canvas.viewbox(); // pass `false` to force recomputing the box. * * console.log(viewbox); * // { * // inner: Dimensions, * // outer: Dimensions, * // scale, * // x, y, * // width, height * // } * * // if the current diagram is zoomed and scrolled, you may reset it to the * // default zoom via this method, too: * * var zoomedAndScrolledViewbox = canvas.viewbox(); * * canvas.viewbox({ * x: 0, * y: 0, * width: zoomedAndScrolledViewbox.outer.width, * height: zoomedAndScrolledViewbox.outer.height * }); * * @param {Object} [box] the new view box to set * @param {number} box.x the top left X coordinate of the canvas visible in view box * @param {number} box.y the top left Y coordinate of the canvas visible in view box * @param {number} box.width the visible width * @param {number} box.height * * @return {Object} the current view box */ Canvas.prototype.viewbox = function(box) { if (box === undefined && this._cachedViewbox) { return this._cachedViewbox; } var viewport = this._viewport, innerBox, outerBox = this.getSize(), matrix, activeLayer, transform, scale, x, y; if (!box) { // compute the inner box based on the // diagrams active layer. This allows us to exclude // external components, such as overlays activeLayer = this._rootElement ? this.getActiveLayer() : null; innerBox = activeLayer && activeLayer.getBBox() || {}; transform = transform$1(viewport); matrix = transform ? transform.matrix : createMatrix(); scale = round$b(matrix.a, 1000); x = round$b(-matrix.e || 0, 1000); y = round$b(-matrix.f || 0, 1000); box = this._cachedViewbox = { x: x ? x / scale : 0, y: y ? y / scale : 0, width: outerBox.width / scale, height: outerBox.height / scale, scale: scale, inner: { width: innerBox.width || 0, height: innerBox.height || 0, x: innerBox.x || 0, y: innerBox.y || 0 }, outer: outerBox }; return box; } else { this._changeViewbox(function() { scale = Math.min(outerBox.width / box.width, outerBox.height / box.height); var matrix = this._svg.createSVGMatrix() .scale(scale) .translate(-box.x, -box.y); transform$1(viewport, matrix); }); } return box; }; /** * Gets or sets the scroll of the canvas. * * @param {Object} [delta] the new scroll to apply. * * @param {number} [delta.dx] * @param {number} [delta.dy] */ Canvas.prototype.scroll = function(delta) { var node = this._viewport; var matrix = node.getCTM(); if (delta) { this._changeViewbox(function() { delta = assign({ dx: 0, dy: 0 }, delta || {}); matrix = this._svg.createSVGMatrix().translate(delta.dx, delta.dy).multiply(matrix); setCTM(node, matrix); }); } return { x: matrix.e, y: matrix.f }; }; /** * Scrolls the viewbox to contain the given element. * Optionally specify a padding to be applied to the edges. * * @param {Object|String} [element] the element to scroll to. * @param {Object|Number} [padding=100] the padding to be applied. Can also specify top, bottom, left and right. * */ Canvas.prototype.scrollToElement = function(element, padding) { var defaultPadding = 100; if (typeof element === 'string') { element = this._elementRegistry.get(element); } // set to correct rootElement var rootElement = this.findRoot(element); if (rootElement !== this.getRootElement()) { this.setRootElement(rootElement); } if (!padding) { padding = {}; } if (typeof padding === 'number') { defaultPadding = padding; } padding = { top: padding.top || defaultPadding, right: padding.right || defaultPadding, bottom: padding.bottom || defaultPadding, left: padding.left || defaultPadding }; var elementBounds = getBBox(element), elementTrbl = asTRBL(elementBounds), viewboxBounds = this.viewbox(), zoom = this.zoom(), dx, dy; // shrink viewboxBounds with padding viewboxBounds.y += padding.top / zoom; viewboxBounds.x += padding.left / zoom; viewboxBounds.width -= (padding.right + padding.left) / zoom; viewboxBounds.height -= (padding.bottom + padding.top) / zoom; var viewboxTrbl = asTRBL(viewboxBounds); var canFit = elementBounds.width < viewboxBounds.width && elementBounds.height < viewboxBounds.height; if (!canFit) { // top-left when element can't fit dx = elementBounds.x - viewboxBounds.x; dy = elementBounds.y - viewboxBounds.y; } else { var dRight = Math.max(0, elementTrbl.right - viewboxTrbl.right), dLeft = Math.min(0, elementTrbl.left - viewboxTrbl.left), dBottom = Math.max(0, elementTrbl.bottom - viewboxTrbl.bottom), dTop = Math.min(0, elementTrbl.top - viewboxTrbl.top); dx = dRight || dLeft; dy = dBottom || dTop; } this.scroll({ dx: -dx * zoom, dy: -dy * zoom }); }; /** * Gets or sets the current zoom of the canvas, optionally zooming * to the specified position. * * The getter may return a cached zoom level. Call it with `false` as * the first argument to force recomputation of the current level. * * @param {string|number} [newScale] the new zoom level, either a number, i.e. 0.9, * or `fit-viewport` to adjust the size to fit the current viewport * @param {string|Point} [center] the reference point { x: .., y: ..} to zoom to, 'auto' to zoom into mid or null * * @return {number} the current scale */ Canvas.prototype.zoom = function(newScale, center) { if (!newScale) { return this.viewbox(newScale).scale; } if (newScale === 'fit-viewport') { return this._fitViewport(center); } var outer, matrix; this._changeViewbox(function() { if (typeof center !== 'object') { outer = this.viewbox().outer; center = { x: outer.width / 2, y: outer.height / 2 }; } matrix = this._setZoom(newScale, center); }); return round$b(matrix.a, 1000); }; function setCTM(node, m) { var mstr = 'matrix(' + m.a + ',' + m.b + ',' + m.c + ',' + m.d + ',' + m.e + ',' + m.f + ')'; node.setAttribute('transform', mstr); } Canvas.prototype._fitViewport = function(center) { var vbox = this.viewbox(), outer = vbox.outer, inner = vbox.inner, newScale, newViewbox; // display the complete diagram without zooming in. // instead of relying on internal zoom, we perform a // hard reset on the canvas viewbox to realize this // // if diagram does not need to be zoomed in, we focus it around // the diagram origin instead if (inner.x >= 0 && inner.y >= 0 && inner.x + inner.width <= outer.width && inner.y + inner.height <= outer.height && !center) { newViewbox = { x: 0, y: 0, width: Math.max(inner.width + inner.x, outer.width), height: Math.max(inner.height + inner.y, outer.height) }; } else { newScale = Math.min(1, outer.width / inner.width, outer.height / inner.height); newViewbox = { x: inner.x + (center ? inner.width / 2 - outer.width / newScale / 2 : 0), y: inner.y + (center ? inner.height / 2 - outer.height / newScale / 2 : 0), width: outer.width / newScale, height: outer.height / newScale }; } this.viewbox(newViewbox); return this.viewbox(false).scale; }; Canvas.prototype._setZoom = function(scale, center) { var svg = this._svg, viewport = this._viewport; var matrix = svg.createSVGMatrix(); var point = svg.createSVGPoint(); var centerPoint, originalPoint, currentMatrix, scaleMatrix, newMatrix; currentMatrix = viewport.getCTM(); var currentScale = currentMatrix.a; if (center) { centerPoint = assign(point, center); // revert applied viewport transformations originalPoint = centerPoint.matrixTransform(currentMatrix.inverse()); // create scale matrix scaleMatrix = matrix .translate(originalPoint.x, originalPoint.y) .scale(1 / currentScale * scale) .translate(-originalPoint.x, -originalPoint.y); newMatrix = currentMatrix.multiply(scaleMatrix); } else { newMatrix = matrix.scale(scale); } setCTM(this._viewport, newMatrix); return newMatrix; }; /** * Returns the size of the canvas * * @return {Dimensions} */ Canvas.prototype.getSize = function() { return { width: this._container.clientWidth, height: this._container.clientHeight }; }; /** * Return the absolute bounding box for the given element * * The absolute bounding box may be used to display overlays in the * callers (browser) coordinate system rather than the zoomed in/out * canvas coordinates. * * @param {ElementDescriptor} element * @return {Bounds} the absolute bounding box */ Canvas.prototype.getAbsoluteBBox = function(element) { var vbox = this.viewbox(); var bbox; // connection // use svg bbox if (element.waypoints) { var gfx = this.getGraphics(element); bbox = gfx.getBBox(); } // shapes // use data else { bbox = element; } var x = bbox.x * vbox.scale - vbox.x * vbox.scale; var y = bbox.y * vbox.scale - vbox.y * vbox.scale; var width = bbox.width * vbox.scale; var height = bbox.height * vbox.scale; return { x: x, y: y, width: width, height: height }; }; /** * Fires an event in order other modules can react to the * canvas resizing */ Canvas.prototype.resized = function() { // force recomputation of view box delete this._cachedViewbox; this._eventBus.fire('canvas.resized'); }; var ELEMENT_ID = 'data-element-id'; /** * @class * * A registry that keeps track of all shapes in the diagram. */ function ElementRegistry(eventBus) { this._elements = {}; this._eventBus = eventBus; } ElementRegistry.$inject = [ 'eventBus' ]; /** * Register a pair of (element, gfx, (secondaryGfx)). * * @param {djs.model.Base} element * @param {SVGElement} gfx * @param {SVGElement} [secondaryGfx] optional other element to register, too */ ElementRegistry.prototype.add = function(element, gfx, secondaryGfx) { var id = element.id; this._validateId(id); // associate dom node with element attr(gfx, ELEMENT_ID, id); if (secondaryGfx) { attr(secondaryGfx, ELEMENT_ID, id); } this._elements[id] = { element: element, gfx: gfx, secondaryGfx: secondaryGfx }; }; /** * Removes an element from the registry. * * @param {djs.model.Base} element */ ElementRegistry.prototype.remove = function(element) { var elements = this._elements, id = element.id || element, container = id && elements[id]; if (container) { // unset element id on gfx attr(container.gfx, ELEMENT_ID, ''); if (container.secondaryGfx) { attr(container.secondaryGfx, ELEMENT_ID, ''); } delete elements[id]; } }; /** * Update the id of an element * * @param {djs.model.Base} element * @param {string} newId */ ElementRegistry.prototype.updateId = function(element, newId) { this._validateId(newId); if (typeof element === 'string') { element = this.get(element); } this._eventBus.fire('element.updateId', { element: element, newId: newId }); var gfx = this.getGraphics(element), secondaryGfx = this.getGraphics(element, true); this.remove(element); element.id = newId; this.add(element, gfx, secondaryGfx); }; /** * Update the graphics of an element * * @param {djs.model.Base} element * @param {SVGElement} gfx * @param {boolean} [secondary=false] whether to update the secondary connected element */ ElementRegistry.prototype.updateGraphics = function(filter, gfx, secondary) { var id = filter.id || filter; var container = this._elements[id]; if (secondary) { container.secondaryGfx = gfx; } else { container.gfx = gfx; } if (gfx) { attr(gfx, ELEMENT_ID, id); } return gfx; }; /** * Return the model element for a given id or graphics. * * @example * * elementRegistry.get('SomeElementId_1'); * elementRegistry.get(gfx); * * * @param {string|SVGElement} filter for selecting the element * * @return {djs.model.Base} */ ElementRegistry.prototype.get = function(filter) { var id; if (typeof filter === 'string') { id = filter; } else { id = filter && attr(filter, ELEMENT_ID); } var container = this._elements[id]; return container && container.element; }; /** * Return all elements that match a given filter function. * * @param {Function} fn * * @return {Array} */ ElementRegistry.prototype.filter = function(fn) { var filtered = []; this.forEach(function(element, gfx) { if (fn(element, gfx)) { filtered.push(element); } }); return filtered; }; /** * Return the first element that satisfies the provided testing function. * * @param {Function} fn * * @return {djs.model.Base} */ ElementRegistry.prototype.find = function(fn) { var map = this._elements, keys = Object.keys(map); for (var i = 0; i < keys.length; i++) { var id = keys[i], container = map[id], element = container.element, gfx = container.gfx; if (fn(element, gfx)) { return element; } } }; /** * Return all rendered model elements. * * @return {Array} */ ElementRegistry.prototype.getAll = function() { return this.filter(function(e) { return e; }); }; /** * Iterate over all diagram elements. * * @param {Function} fn */ ElementRegistry.prototype.forEach = function(fn) { var map = this._elements; Object.keys(map).forEach(function(id) { var container = map[id], element = container.element, gfx = container.gfx; return fn(element, gfx); }); }; /** * Return the graphical representation of an element or its id. * * @example * elementRegistry.getGraphics('SomeElementId_1'); * elementRegistry.getGraphics(rootElement); // * * elementRegistry.getGraphics(rootElement, true); // * * * @param {string|djs.model.Base} filter * @param {boolean} [secondary=false] whether to return the secondary connected element * * @return {SVGElement} */ ElementRegistry.prototype.getGraphics = function(filter, secondary) { var id = filter.id || filter; var container = this._elements[id]; return container && (secondary ? container.secondaryGfx : container.gfx); }; /** * Validate the suitability of the given id and signals a problem * with an exception. * * @param {string} id * * @throws {Error} if id is empty or already assigned */ ElementRegistry.prototype._validateId = function(id) { if (!id) { throw new Error('element must have an id'); } if (this._elements[id]) { throw new Error('element with id ' + id + ' already added'); } }; var objectRefs = {exports: {}}; var collection = {}; /** * An empty collection stub. Use {@link RefsCollection.extend} to extend a * collection with ref semantics. * * @class RefsCollection */ /** * Extends a collection with {@link Refs} aware methods * * @memberof RefsCollection * @static * * @param {Array} collection * @param {Refs} refs instance * @param {Object} property represented by the collection * @param {Object} target object the collection is attached to * * @return {RefsCollection} the extended array */ function extend(collection, refs, property, target) { var inverseProperty = property.inverse; /** * Removes the given element from the array and returns it. * * @method RefsCollection#remove * * @param {Object} element the element to remove */ Object.defineProperty(collection, 'remove', { value: function(element) { var idx = this.indexOf(element); if (idx !== -1) { this.splice(idx, 1); // unset inverse refs.unset(element, inverseProperty, target); } return element; } }); /** * Returns true if the collection contains the given element * * @method RefsCollection#contains * * @param {Object} element the element to check for */ Object.defineProperty(collection, 'contains', { value: function(element) { return this.indexOf(element) !== -1; } }); /** * Adds an element to the array, unless it exists already (set semantics). * * @method RefsCollection#add * * @param {Object} element the element to add * @param {Number} optional index to add element to * (possibly moving other elements around) */ Object.defineProperty(collection, 'add', { value: function(element, idx) { var currentIdx = this.indexOf(element); if (typeof idx === 'undefined') { if (currentIdx !== -1) { // element already in collection (!) return; } // add to end of array, as no idx is specified idx = this.length; } // handle already in collection if (currentIdx !== -1) { // remove element from currentIdx this.splice(currentIdx, 1); } // add element at idx this.splice(idx, 0, element); if (currentIdx === -1) { // set inverse, unless element was // in collection already refs.set(element, inverseProperty, target); } } }); // a simple marker, identifying this element // as being a refs collection Object.defineProperty(collection, '__refs_collection', { value: true }); return collection; } function isExtended(collection) { return collection.__refs_collection === true; } collection.extend = extend; collection.isExtended = isExtended; var Collection = collection; function hasOwnProperty$1(e, property) { return Object.prototype.hasOwnProperty.call(e, property.name || property); } function defineCollectionProperty(ref, property, target) { var collection = Collection.extend(target[property.name] || [], ref, property, target); Object.defineProperty(target, property.name, { enumerable: property.enumerable, value: collection }); if (collection.length) { collection.forEach(function(o) { ref.set(o, property.inverse, target); }); } } function defineProperty$1(ref, property, target) { var inverseProperty = property.inverse; var _value = target[property.name]; Object.defineProperty(target, property.name, { configurable: property.configurable, enumerable: property.enumerable, get: function() { return _value; }, set: function(value) { // return if we already performed all changes if (value === _value) { return; } var old = _value; // temporary set null _value = null; if (old) { ref.unset(old, inverseProperty, target); } // set new value _value = value; // set inverse value ref.set(_value, inverseProperty, target); } }); } /** * Creates a new references object defining two inversly related * attribute descriptors a and b. * *

* When bound to an object using {@link Refs#bind} the references * get activated and ensure that add and remove operations are applied * reversely, too. *

* *

* For attributes represented as collections {@link Refs} provides the * {@link RefsCollection#add}, {@link RefsCollection#remove} and {@link RefsCollection#contains} extensions * that must be used to properly hook into the inverse change mechanism. *

* * @class Refs * * @classdesc A bi-directional reference between two attributes. * * @param {Refs.AttributeDescriptor} a property descriptor * @param {Refs.AttributeDescriptor} b property descriptor * * @example * * var refs = Refs({ name: 'wheels', collection: true, enumerable: true }, { name: 'car' }); * * var car = { name: 'toyota' }; * var wheels = [{ pos: 'front-left' }, { pos: 'front-right' }]; * * refs.bind(car, 'wheels'); * * car.wheels // [] * car.wheels.add(wheels[0]); * car.wheels.add(wheels[1]); * * car.wheels // [{ pos: 'front-left' }, { pos: 'front-right' }] * * wheels[0].car // { name: 'toyota' }; * car.wheels.remove(wheels[0]); * * wheels[0].car // undefined */ function Refs$1(a, b) { if (!(this instanceof Refs$1)) { return new Refs$1(a, b); } // link a.inverse = b; b.inverse = a; this.props = {}; this.props[a.name] = a; this.props[b.name] = b; } /** * Binds one side of a bi-directional reference to a * target object. * * @memberOf Refs * * @param {Object} target * @param {String} property */ Refs$1.prototype.bind = function(target, property) { if (typeof property === 'string') { if (!this.props[property]) { throw new Error('no property <' + property + '> in ref'); } property = this.props[property]; } if (property.collection) { defineCollectionProperty(this, property, target); } else { defineProperty$1(this, property, target); } }; Refs$1.prototype.ensureRefsCollection = function(target, property) { var collection = target[property.name]; if (!Collection.isExtended(collection)) { defineCollectionProperty(this, property, target); } return collection; }; Refs$1.prototype.ensureBound = function(target, property) { if (!hasOwnProperty$1(target, property)) { this.bind(target, property); } }; Refs$1.prototype.unset = function(target, property, value) { if (target) { this.ensureBound(target, property); if (property.collection) { this.ensureRefsCollection(target, property).remove(value); } else { target[property.name] = undefined; } } }; Refs$1.prototype.set = function(target, property, value) { if (target) { this.ensureBound(target, property); if (property.collection) { this.ensureRefsCollection(target, property).add(value); } else { target[property.name] = value; } } }; var refs = Refs$1; (function (module) { module.exports = refs; module.exports.Collection = collection; } (objectRefs)); var Refs = /*@__PURE__*/getDefaultExportFromCjs(objectRefs.exports); var parentRefs = new Refs({ name: 'children', enumerable: true, collection: true }, { name: 'parent' }), labelRefs = new Refs({ name: 'labels', enumerable: true, collection: true }, { name: 'labelTarget' }), attacherRefs = new Refs({ name: 'attachers', collection: true }, { name: 'host' }), outgoingRefs = new Refs({ name: 'outgoing', collection: true }, { name: 'source' }), incomingRefs = new Refs({ name: 'incoming', collection: true }, { name: 'target' }); /** * @namespace djs.model */ /** * @memberOf djs.model */ /** * The basic graphical representation * * @class * * @abstract */ function Base$1() { /** * The object that backs up the shape * * @name Base#businessObject * @type Object */ Object.defineProperty(this, 'businessObject', { writable: true }); /** * Single label support, will mapped to multi label array * * @name Base#label * @type Object */ Object.defineProperty(this, 'label', { get: function() { return this.labels[0]; }, set: function(newLabel) { var label = this.label, labels = this.labels; if (!newLabel && label) { labels.remove(label); } else { labels.add(newLabel, 0); } } }); /** * The parent shape * * @name Base#parent * @type Shape */ parentRefs.bind(this, 'parent'); /** * The list of labels * * @name Base#labels * @type Label */ labelRefs.bind(this, 'labels'); /** * The list of outgoing connections * * @name Base#outgoing * @type Array */ outgoingRefs.bind(this, 'outgoing'); /** * The list of incoming connections * * @name Base#incoming * @type Array */ incomingRefs.bind(this, 'incoming'); } /** * A graphical object * * @class * @constructor * * @extends Base */ function Shape() { Base$1.call(this); /** * Indicates frame shapes * * @name Shape#isFrame * @type boolean */ /** * The list of children * * @name Shape#children * @type Array */ parentRefs.bind(this, 'children'); /** * @name Shape#host * @type Shape */ attacherRefs.bind(this, 'host'); /** * @name Shape#attachers * @type Shape */ attacherRefs.bind(this, 'attachers'); } e(Shape, Base$1); /** * A root graphical object * * @class * @constructor * * @extends Shape */ function Root() { Shape.call(this); } e(Root, Shape); /** * A label for an element * * @class * @constructor * * @extends Shape */ function Label() { Shape.call(this); /** * The labeled element * * @name Label#labelTarget * @type Base */ labelRefs.bind(this, 'labelTarget'); } e(Label, Shape); /** * A connection between two elements * * @class * @constructor * * @extends Base */ function Connection() { Base$1.call(this); /** * The element this connection originates from * * @name Connection#source * @type Base */ outgoingRefs.bind(this, 'source'); /** * The element this connection points to * * @name Connection#target * @type Base */ incomingRefs.bind(this, 'target'); } e(Connection, Base$1); var types$6 = { connection: Connection, shape: Shape, label: Label, root: Root }; /** * Creates a new model element of the specified type * * @method create * * @example * * var shape1 = Model.create('shape', { x: 10, y: 10, width: 100, height: 100 }); * var shape2 = Model.create('shape', { x: 210, y: 210, width: 100, height: 100 }); * * var connection = Model.create('connection', { waypoints: [ { x: 110, y: 55 }, {x: 210, y: 55 } ] }); * * @param {string} type lower-cased model name * @param {Object} attrs attributes to initialize the new model instance with * * @return {Base} the new model instance */ function create(type, attrs) { var Type = types$6[type]; if (!Type) { throw new Error('unknown type: <' + type + '>'); } return assign(new Type(), attrs); } /** * A factory for diagram-js shapes */ function ElementFactory$1() { this._uid = 12; } ElementFactory$1.prototype.createRoot = function(attrs) { return this.create('root', attrs); }; ElementFactory$1.prototype.createLabel = function(attrs) { return this.create('label', attrs); }; ElementFactory$1.prototype.createShape = function(attrs) { return this.create('shape', attrs); }; ElementFactory$1.prototype.createConnection = function(attrs) { return this.create('connection', attrs); }; /** * Create a model element with the given type and * a number of pre-set attributes. * * @param {string} type * @param {Object} attrs * @return {djs.model.Base} the newly created model instance */ ElementFactory$1.prototype.create = function(type, attrs) { attrs = assign({}, attrs || {}); if (!attrs.id) { attrs.id = type + '_' + (this._uid++); } return create(type, attrs); }; var FN_REF = '__fn'; var DEFAULT_PRIORITY$5 = 1000; var slice = Array.prototype.slice; /** * A general purpose event bus. * * This component is used to communicate across a diagram instance. * Other parts of a diagram can use it to listen to and broadcast events. * * * ## Registering for Events * * The event bus provides the {@link EventBus#on} and {@link EventBus#once} * methods to register for events. {@link EventBus#off} can be used to * remove event registrations. Listeners receive an instance of {@link Event} * as the first argument. It allows them to hook into the event execution. * * ```javascript * * // listen for event * eventBus.on('foo', function(event) { * * // access event type * event.type; // 'foo' * * // stop propagation to other listeners * event.stopPropagation(); * * // prevent event default * event.preventDefault(); * }); * * // listen for event with custom payload * eventBus.on('bar', function(event, payload) { * console.log(payload); * }); * * // listen for event returning value * eventBus.on('foobar', function(event) { * * // stop event propagation + prevent default * return false; * * // stop event propagation + return custom result * return { * complex: 'listening result' * }; * }); * * * // listen with custom priority (default=1000, higher is better) * eventBus.on('priorityfoo', 1500, function(event) { * console.log('invoked first!'); * }); * * * // listen for event and pass the context (`this`) * eventBus.on('foobar', function(event) { * this.foo(); * }, this); * ``` * * * ## Emitting Events * * Events can be emitted via the event bus using {@link EventBus#fire}. * * ```javascript * * // false indicates that the default action * // was prevented by listeners * if (eventBus.fire('foo') === false) { * console.log('default has been prevented!'); * }; * * * // custom args + return value listener * eventBus.on('sum', function(event, a, b) { * return a + b; * }); * * // you can pass custom arguments + retrieve result values. * var sum = eventBus.fire('sum', 1, 2); * console.log(sum); // 3 * ``` */ function EventBus() { this._listeners = {}; // cleanup on destroy on lowest priority to allow // message passing until the bitter end this.on('diagram.destroy', 1, this._destroy, this); } /** * Register an event listener for events with the given name. * * The callback will be invoked with `event, ...additionalArguments` * that have been passed to {@link EventBus#fire}. * * Returning false from a listener will prevent the events default action * (if any is specified). To stop an event from being processed further in * other listeners execute {@link Event#stopPropagation}. * * Returning anything but `undefined` from a listener will stop the listener propagation. * * @param {string|Array} events * @param {number} [priority=1000] the priority in which this listener is called, larger is higher * @param {Function} callback * @param {Object} [that] Pass context (`this`) to the callback */ EventBus.prototype.on = function(events, priority, callback, that) { events = isArray$3(events) ? events : [ events ]; if (isFunction(priority)) { that = callback; callback = priority; priority = DEFAULT_PRIORITY$5; } if (!isNumber(priority)) { throw new Error('priority must be a number'); } var actualCallback = callback; if (that) { actualCallback = bind(callback, that); // make sure we remember and are able to remove // bound callbacks via {@link #off} using the original // callback actualCallback[FN_REF] = callback[FN_REF] || callback; } var self = this; events.forEach(function(e) { self._addListener(e, { priority: priority, callback: actualCallback, next: null }); }); }; /** * Register an event listener that is executed only once. * * @param {string} event the event name to register for * @param {number} [priority=1000] the priority in which this listener is called, larger is higher * @param {Function} callback the callback to execute * @param {Object} [that] Pass context (`this`) to the callback */ EventBus.prototype.once = function(event, priority, callback, that) { var self = this; if (isFunction(priority)) { that = callback; callback = priority; priority = DEFAULT_PRIORITY$5; } if (!isNumber(priority)) { throw new Error('priority must be a number'); } function wrappedCallback() { wrappedCallback.__isTomb = true; var result = callback.apply(that, arguments); self.off(event, wrappedCallback); return result; } // make sure we remember and are able to remove // bound callbacks via {@link #off} using the original // callback wrappedCallback[FN_REF] = callback; this.on(event, priority, wrappedCallback); }; /** * Removes event listeners by event and callback. * * If no callback is given, all listeners for a given event name are being removed. * * @param {string|Array} events * @param {Function} [callback] */ EventBus.prototype.off = function(events, callback) { events = isArray$3(events) ? events : [ events ]; var self = this; events.forEach(function(event) { self._removeListener(event, callback); }); }; /** * Create an EventBus event. * * @param {Object} data * * @return {Object} event, recognized by the eventBus */ EventBus.prototype.createEvent = function(data) { var event = new InternalEvent(); event.init(data); return event; }; /** * Fires a named event. * * @example * * // fire event by name * events.fire('foo'); * * // fire event object with nested type * var event = { type: 'foo' }; * events.fire(event); * * // fire event with explicit type * var event = { x: 10, y: 20 }; * events.fire('element.moved', event); * * // pass additional arguments to the event * events.on('foo', function(event, bar) { * alert(bar); * }); * * events.fire({ type: 'foo' }, 'I am bar!'); * * @param {string} [name] the optional event name * @param {Object} [event] the event object * @param {...Object} additional arguments to be passed to the callback functions * * @return {boolean} the events return value, if specified or false if the * default action was prevented by listeners */ EventBus.prototype.fire = function(type, data) { var event, firstListener, returnValue, args; args = slice.call(arguments); if (typeof type === 'object') { data = type; type = data.type; } if (!type) { throw new Error('no event type specified'); } firstListener = this._listeners[type]; if (!firstListener) { return; } // we make sure we fire instances of our home made // events here. We wrap them only once, though if (data instanceof InternalEvent) { // we are fine, we alread have an event event = data; } else { event = this.createEvent(data); } // ensure we pass the event as the first parameter args[0] = event; // original event type (in case we delegate) var originalType = event.type; // update event type before delegation if (type !== originalType) { event.type = type; } try { returnValue = this._invokeListeners(event, args, firstListener); } finally { // reset event type after delegation if (type !== originalType) { event.type = originalType; } } // set the return value to false if the event default // got prevented and no other return value exists if (returnValue === undefined && event.defaultPrevented) { returnValue = false; } return returnValue; }; EventBus.prototype.handleError = function(error) { return this.fire('error', { error: error }) === false; }; EventBus.prototype._destroy = function() { this._listeners = {}; }; EventBus.prototype._invokeListeners = function(event, args, listener) { var returnValue; while (listener) { // handle stopped propagation if (event.cancelBubble) { break; } returnValue = this._invokeListener(event, args, listener); listener = listener.next; } return returnValue; }; EventBus.prototype._invokeListener = function(event, args, listener) { var returnValue; if (listener.callback.__isTomb) { return returnValue; } try { // returning false prevents the default action returnValue = invokeFunction(listener.callback, args); // stop propagation on return value if (returnValue !== undefined) { event.returnValue = returnValue; event.stopPropagation(); } // prevent default on return false if (returnValue === false) { event.preventDefault(); } } catch (error) { if (!this.handleError(error)) { console.error('unhandled error in event listener', error); throw error; } } return returnValue; }; /* * Add new listener with a certain priority to the list * of listeners (for the given event). * * The semantics of listener registration / listener execution are * first register, first serve: New listeners will always be inserted * after existing listeners with the same priority. * * Example: Inserting two listeners with priority 1000 and 1300 * * * before: [ 1500, 1500, 1000, 1000 ] * * after: [ 1500, 1500, (new=1300), 1000, 1000, (new=1000) ] * * @param {string} event * @param {Object} listener { priority, callback } */ EventBus.prototype._addListener = function(event, newListener) { var listener = this._getListeners(event), previousListener; // no prior listeners if (!listener) { this._setListeners(event, newListener); return; } // ensure we order listeners by priority from // 0 (high) to n > 0 (low) while (listener) { if (listener.priority < newListener.priority) { newListener.next = listener; if (previousListener) { previousListener.next = newListener; } else { this._setListeners(event, newListener); } return; } previousListener = listener; listener = listener.next; } // add new listener to back previousListener.next = newListener; }; EventBus.prototype._getListeners = function(name) { return this._listeners[name]; }; EventBus.prototype._setListeners = function(name, listener) { this._listeners[name] = listener; }; EventBus.prototype._removeListener = function(event, callback) { var listener = this._getListeners(event), nextListener, previousListener, listenerCallback; if (!callback) { // clear listeners this._setListeners(event, null); return; } while (listener) { nextListener = listener.next; listenerCallback = listener.callback; if (listenerCallback === callback || listenerCallback[FN_REF] === callback) { if (previousListener) { previousListener.next = nextListener; } else { // new first listener this._setListeners(event, nextListener); } } previousListener = listener; listener = nextListener; } }; /** * A event that is emitted via the event bus. */ function InternalEvent() { } InternalEvent.prototype.stopPropagation = function() { this.cancelBubble = true; }; InternalEvent.prototype.preventDefault = function() { this.defaultPrevented = true; }; InternalEvent.prototype.init = function(data) { assign(this, data || {}); }; /** * Invoke function. Be fast... * * @param {Function} fn * @param {Array} args * * @return {Any} */ function invokeFunction(fn, args) { return fn.apply(null, args); } /** * SVGs for elements are generated by the {@link GraphicsFactory}. * * This utility gives quick access to the important semantic * parts of an element. */ /** * Returns the visual part of a diagram element * * @param {Snap} gfx * * @return {Snap} */ function getVisual(gfx) { return gfx.childNodes[0]; } /** * Returns the children for a given diagram element. * * @param {Snap} gfx * @return {Snap} */ function getChildren$1(gfx) { return gfx.parentNode.childNodes[1]; } /** * @param {} element * @param {number} x * @param {number} y * @param {number} angle * @param {number} amount */ function transform(gfx, x, y, angle, amount) { var translate = createTransform(); translate.setTranslate(x, y); var rotate = createTransform(); rotate.setRotate(angle || 0, 0, 0); var scale = createTransform(); scale.setScale(amount || 1, amount || 1); transform$1(gfx, [ translate, rotate, scale ]); } /** * @param {SVGElement} element * @param {number} x * @param {number} y */ function translate$2(gfx, x, y) { var translate = createTransform(); translate.setTranslate(x, y); transform$1(gfx, translate); } /** * @param {SVGElement} element * @param {number} angle */ function rotate(gfx, angle) { var rotate = createTransform(); rotate.setRotate(angle, 0, 0); transform$1(gfx, rotate); } /** * A factory that creates graphical elements * * @param {EventBus} eventBus * @param {ElementRegistry} elementRegistry */ function GraphicsFactory(eventBus, elementRegistry) { this._eventBus = eventBus; this._elementRegistry = elementRegistry; } GraphicsFactory.$inject = [ 'eventBus' , 'elementRegistry' ]; GraphicsFactory.prototype._getChildrenContainer = function(element) { var gfx = this._elementRegistry.getGraphics(element); var childrenGfx; // root element if (!element.parent) { childrenGfx = gfx; } else { childrenGfx = getChildren$1(gfx); if (!childrenGfx) { childrenGfx = create$1('g'); classes(childrenGfx).add('djs-children'); append(gfx.parentNode, childrenGfx); } } return childrenGfx; }; /** * Clears the graphical representation of the element and returns the * cleared visual (the element). */ GraphicsFactory.prototype._clear = function(gfx) { var visual = getVisual(gfx); clear$1(visual); return visual; }; /** * Creates a gfx container for shapes and connections * * The layout is as follows: * * * * * * * * * * * * * * @param {string} type the type of the element, i.e. shape | connection * @param {SVGElement} [childrenGfx] * @param {number} [parentIndex] position to create container in parent * @param {boolean} [isFrame] is frame element * * @return {SVGElement} */ GraphicsFactory.prototype._createContainer = function( type, childrenGfx, parentIndex, isFrame ) { var outerGfx = create$1('g'); classes(outerGfx).add('djs-group'); // insert node at position if (typeof parentIndex !== 'undefined') { prependTo(outerGfx, childrenGfx, childrenGfx.childNodes[parentIndex]); } else { append(childrenGfx, outerGfx); } var gfx = create$1('g'); classes(gfx).add('djs-element'); classes(gfx).add('djs-' + type); if (isFrame) { classes(gfx).add('djs-frame'); } append(outerGfx, gfx); // create visual var visual = create$1('g'); classes(visual).add('djs-visual'); append(gfx, visual); return gfx; }; GraphicsFactory.prototype.create = function(type, element, parentIndex) { var childrenGfx = this._getChildrenContainer(element.parent); return this._createContainer(type, childrenGfx, parentIndex, isFrameElement$1(element)); }; GraphicsFactory.prototype.updateContainments = function(elements) { var self = this, elementRegistry = this._elementRegistry, parents; parents = reduce(elements, function(map, e) { if (e.parent) { map[e.parent.id] = e.parent; } return map; }, {}); // update all parents of changed and reorganized their children // in the correct order (as indicated in our model) forEach$1(parents, function(parent) { var children = parent.children; if (!children) { return; } var childrenGfx = self._getChildrenContainer(parent); forEach$1(children.slice().reverse(), function(child) { var childGfx = elementRegistry.getGraphics(child); prependTo(childGfx.parentNode, childrenGfx); }); }); }; GraphicsFactory.prototype.drawShape = function(visual, element) { var eventBus = this._eventBus; return eventBus.fire('render.shape', { gfx: visual, element: element }); }; GraphicsFactory.prototype.getShapePath = function(element) { var eventBus = this._eventBus; return eventBus.fire('render.getShapePath', element); }; GraphicsFactory.prototype.drawConnection = function(visual, element) { var eventBus = this._eventBus; return eventBus.fire('render.connection', { gfx: visual, element: element }); }; GraphicsFactory.prototype.getConnectionPath = function(waypoints) { var eventBus = this._eventBus; return eventBus.fire('render.getConnectionPath', waypoints); }; GraphicsFactory.prototype.update = function(type, element, gfx) { // do NOT update root element if (!element.parent) { return; } var visual = this._clear(gfx); // redraw if (type === 'shape') { this.drawShape(visual, element); // update positioning translate$2(gfx, element.x, element.y); } else if (type === 'connection') { this.drawConnection(visual, element); } else { throw new Error('unknown type: ' + type); } if (element.hidden) { attr(gfx, 'display', 'none'); } else { attr(gfx, 'display', 'block'); } }; GraphicsFactory.prototype.remove = function(element) { var gfx = this._elementRegistry.getGraphics(element); // remove remove$1(gfx.parentNode); }; // helpers ////////// function prependTo(newNode, parentNode, siblingNode) { var node = siblingNode || parentNode.firstChild; // do not prepend node to itself to prevent IE from crashing // https://github.com/bpmn-io/bpmn-js/issues/746 if (newNode === node) { return; } parentNode.insertBefore(newNode, node); } var CoreModule$1 = { __depends__: [ DrawModule$1 ], __init__: [ 'canvas' ], canvas: [ 'type', Canvas ], elementRegistry: [ 'type', ElementRegistry ], elementFactory: [ 'type', ElementFactory$1 ], eventBus: [ 'type', EventBus ], graphicsFactory: [ 'type', GraphicsFactory ] }; /** * @typedef { import('didi').ModuleDeclaration } Module */ /** * Bootstrap an injector from a list of modules, instantiating a number of default components * * @param {Array} modules * * @return {Injector} a injector to use to access the components */ function bootstrap(modules) { var injector = new Injector(modules); injector.init(); return injector; } /** * Creates an injector from passed options. * * @param {Object} options * @return {Injector} */ function createInjector(options) { options = options || {}; var configModule = { 'config': [ 'value', options ] }; var modules = [ configModule, CoreModule$1 ].concat(options.modules || []); return bootstrap(modules); } /** * The main diagram-js entry point that bootstraps the diagram with the given * configuration. * * To register extensions with the diagram, pass them as Array to the constructor. * * @class djs.Diagram * @memberOf djs * @constructor * * @example * * Creating a plug-in that logs whenever a shape is added to the canvas. * * // plug-in implemenentation * function MyLoggingPlugin(eventBus) { * eventBus.on('shape.added', function(event) { * console.log('shape ', event.shape, ' was added to the diagram'); * }); * } * * // export as module * export default { * __init__: [ 'myLoggingPlugin' ], * myLoggingPlugin: [ 'type', MyLoggingPlugin ] * }; * * * // instantiate the diagram with the new plug-in * * import MyLoggingModule from 'path-to-my-logging-plugin'; * * var diagram = new Diagram({ * modules: [ * MyLoggingModule * ] * }); * * diagram.invoke([ 'canvas', function(canvas) { * // add shape to drawing canvas * canvas.addShape({ x: 10, y: 10 }); * }); * * // 'shape ... was added to the diagram' logged to console * * @param {Object} options * @param {Array} [options.modules] external modules to instantiate with the diagram * @param {Injector} [injector] an (optional) injector to bootstrap the diagram with */ function Diagram(options, injector) { // create injector unless explicitly specified this.injector = injector = injector || createInjector(options); // API /** * Resolves a diagram service * * @method Diagram#get * * @param {string} name the name of the diagram service to be retrieved * @param {boolean} [strict=true] if false, resolve missing services to null */ this.get = injector.get; /** * Executes a function into which diagram services are injected * * @method Diagram#invoke * * @param {Function|Object[]} fn the function to resolve * @param {Object} locals a number of locals to use to resolve certain dependencies */ this.invoke = injector.invoke; // init // indicate via event /** * An event indicating that all plug-ins are loaded. * * Use this event to fire other events to interested plug-ins * * @memberOf Diagram * * @event diagram.init * * @example * * eventBus.on('diagram.init', function() { * eventBus.fire('my-custom-event', { foo: 'BAR' }); * }); * * @type {Object} */ this.get('eventBus').fire('diagram.init'); } /** * Destroys the diagram * * @method Diagram#destroy */ Diagram.prototype.destroy = function() { this.get('eventBus').fire('diagram.destroy'); }; /** * Clear the diagram, removing all contents. */ Diagram.prototype.clear = function() { this.get('eventBus').fire('diagram.clear'); }; /** * Moddle base element. */ function Base() { } Base.prototype.get = function(name) { return this.$model.properties.get(this, name); }; Base.prototype.set = function(name, value) { this.$model.properties.set(this, name, value); }; /** * A model element factory. * * @param {Moddle} model * @param {Properties} properties */ function Factory(model, properties) { this.model = model; this.properties = properties; } Factory.prototype.createType = function(descriptor) { var model = this.model; var props = this.properties, prototype = Object.create(Base.prototype); // initialize default values forEach$1(descriptor.properties, function(p) { if (!p.isMany && p.default !== undefined) { prototype[p.name] = p.default; } }); props.defineModel(prototype, model); props.defineDescriptor(prototype, descriptor); var name = descriptor.ns.name; /** * The new type constructor */ function ModdleElement(attrs) { props.define(this, '$type', { value: name, enumerable: true }); props.define(this, '$attrs', { value: {} }); props.define(this, '$parent', { writable: true }); forEach$1(attrs, bind(function(val, key) { this.set(key, val); }, this)); } ModdleElement.prototype = prototype; ModdleElement.hasType = prototype.$instanceOf = this.model.hasType; // static links props.defineModel(ModdleElement, model); props.defineDescriptor(ModdleElement, descriptor); return ModdleElement; }; /** * Built-in moddle types */ var BUILTINS = { String: true, Boolean: true, Integer: true, Real: true, Element: true }; /** * Converters for built in types from string representations */ var TYPE_CONVERTERS = { String: function(s) { return s; }, Boolean: function(s) { return s === 'true'; }, Integer: function(s) { return parseInt(s, 10); }, Real: function(s) { return parseFloat(s); } }; /** * Convert a type to its real representation */ function coerceType(type, value) { var converter = TYPE_CONVERTERS[type]; if (converter) { return converter(value); } else { return value; } } /** * Return whether the given type is built-in */ function isBuiltIn(type) { return !!BUILTINS[type]; } /** * Return whether the given type is simple */ function isSimple(type) { return !!TYPE_CONVERTERS[type]; } /** * Parses a namespaced attribute name of the form (ns:)localName to an object, * given a default prefix to assume in case no explicit namespace is given. * * @param {String} name * @param {String} [defaultPrefix] the default prefix to take, if none is present. * * @return {Object} the parsed name */ function parseName(name, defaultPrefix) { var parts = name.split(/:/), localName, prefix; // no prefix (i.e. only local name) if (parts.length === 1) { localName = name; prefix = defaultPrefix; } else // prefix + local name if (parts.length === 2) { localName = parts[1]; prefix = parts[0]; } else { throw new Error('expected or , got ' + name); } name = (prefix ? prefix + ':' : '') + localName; return { name: name, prefix: prefix, localName: localName }; } /** * A utility to build element descriptors. */ function DescriptorBuilder(nameNs) { this.ns = nameNs; this.name = nameNs.name; this.allTypes = []; this.allTypesByName = {}; this.properties = []; this.propertiesByName = {}; } DescriptorBuilder.prototype.build = function() { return pick(this, [ 'ns', 'name', 'allTypes', 'allTypesByName', 'properties', 'propertiesByName', 'bodyProperty', 'idProperty' ]); }; /** * Add property at given index. * * @param {Object} p * @param {Number} [idx] * @param {Boolean} [validate=true] */ DescriptorBuilder.prototype.addProperty = function(p, idx, validate) { if (typeof idx === 'boolean') { validate = idx; idx = undefined; } this.addNamedProperty(p, validate !== false); var properties = this.properties; if (idx !== undefined) { properties.splice(idx, 0, p); } else { properties.push(p); } }; DescriptorBuilder.prototype.replaceProperty = function(oldProperty, newProperty, replace) { var oldNameNs = oldProperty.ns; var props = this.properties, propertiesByName = this.propertiesByName, rename = oldProperty.name !== newProperty.name; if (oldProperty.isId) { if (!newProperty.isId) { throw new Error( 'property <' + newProperty.ns.name + '> must be id property ' + 'to refine <' + oldProperty.ns.name + '>'); } this.setIdProperty(newProperty, false); } if (oldProperty.isBody) { if (!newProperty.isBody) { throw new Error( 'property <' + newProperty.ns.name + '> must be body property ' + 'to refine <' + oldProperty.ns.name + '>'); } // TODO: Check compatibility this.setBodyProperty(newProperty, false); } // validate existence and get location of old property var idx = props.indexOf(oldProperty); if (idx === -1) { throw new Error('property <' + oldNameNs.name + '> not found in property list'); } // remove old property props.splice(idx, 1); // replacing the named property is intentional // // * validate only if this is a "rename" operation // * add at specific index unless we "replace" // this.addProperty(newProperty, replace ? undefined : idx, rename); // make new property available under old name propertiesByName[oldNameNs.name] = propertiesByName[oldNameNs.localName] = newProperty; }; DescriptorBuilder.prototype.redefineProperty = function(p, targetPropertyName, replace) { var nsPrefix = p.ns.prefix; var parts = targetPropertyName.split('#'); var name = parseName(parts[0], nsPrefix); var attrName = parseName(parts[1], name.prefix).name; var redefinedProperty = this.propertiesByName[attrName]; if (!redefinedProperty) { throw new Error('refined property <' + attrName + '> not found'); } else { this.replaceProperty(redefinedProperty, p, replace); } delete p.redefines; }; DescriptorBuilder.prototype.addNamedProperty = function(p, validate) { var ns = p.ns, propsByName = this.propertiesByName; if (validate) { this.assertNotDefined(p, ns.name); this.assertNotDefined(p, ns.localName); } propsByName[ns.name] = propsByName[ns.localName] = p; }; DescriptorBuilder.prototype.removeNamedProperty = function(p) { var ns = p.ns, propsByName = this.propertiesByName; delete propsByName[ns.name]; delete propsByName[ns.localName]; }; DescriptorBuilder.prototype.setBodyProperty = function(p, validate) { if (validate && this.bodyProperty) { throw new Error( 'body property defined multiple times ' + '(<' + this.bodyProperty.ns.name + '>, <' + p.ns.name + '>)'); } this.bodyProperty = p; }; DescriptorBuilder.prototype.setIdProperty = function(p, validate) { if (validate && this.idProperty) { throw new Error( 'id property defined multiple times ' + '(<' + this.idProperty.ns.name + '>, <' + p.ns.name + '>)'); } this.idProperty = p; }; DescriptorBuilder.prototype.assertNotDefined = function(p, name) { var propertyName = p.name, definedProperty = this.propertiesByName[propertyName]; if (definedProperty) { throw new Error( 'property <' + propertyName + '> already defined; ' + 'override of <' + definedProperty.definedBy.ns.name + '#' + definedProperty.ns.name + '> by ' + '<' + p.definedBy.ns.name + '#' + p.ns.name + '> not allowed without redefines'); } }; DescriptorBuilder.prototype.hasProperty = function(name) { return this.propertiesByName[name]; }; DescriptorBuilder.prototype.addTrait = function(t, inherited) { var typesByName = this.allTypesByName, types = this.allTypes; var typeName = t.name; if (typeName in typesByName) { return; } forEach$1(t.properties, bind(function(p) { // clone property to allow extensions p = assign({}, p, { name: p.ns.localName, inherited: inherited }); Object.defineProperty(p, 'definedBy', { value: t }); var replaces = p.replaces, redefines = p.redefines; // add replace/redefine support if (replaces || redefines) { this.redefineProperty(p, replaces || redefines, replaces); } else { if (p.isBody) { this.setBodyProperty(p); } if (p.isId) { this.setIdProperty(p); } this.addProperty(p); } }, this)); types.push(t); typesByName[typeName] = t; }; /** * A registry of Moddle packages. * * @param {Array} packages * @param {Properties} properties */ function Registry(packages, properties) { this.packageMap = {}; this.typeMap = {}; this.packages = []; this.properties = properties; forEach$1(packages, bind(this.registerPackage, this)); } Registry.prototype.getPackage = function(uriOrPrefix) { return this.packageMap[uriOrPrefix]; }; Registry.prototype.getPackages = function() { return this.packages; }; Registry.prototype.registerPackage = function(pkg) { // copy package pkg = assign({}, pkg); var pkgMap = this.packageMap; ensureAvailable(pkgMap, pkg, 'prefix'); ensureAvailable(pkgMap, pkg, 'uri'); // register types forEach$1(pkg.types, bind(function(descriptor) { this.registerType(descriptor, pkg); }, this)); pkgMap[pkg.uri] = pkgMap[pkg.prefix] = pkg; this.packages.push(pkg); }; /** * Register a type from a specific package with us */ Registry.prototype.registerType = function(type, pkg) { type = assign({}, type, { superClass: (type.superClass || []).slice(), extends: (type.extends || []).slice(), properties: (type.properties || []).slice(), meta: assign((type.meta || {})) }); var ns = parseName(type.name, pkg.prefix), name = ns.name, propertiesByName = {}; // parse properties forEach$1(type.properties, bind(function(p) { // namespace property names var propertyNs = parseName(p.name, ns.prefix), propertyName = propertyNs.name; // namespace property types if (!isBuiltIn(p.type)) { p.type = parseName(p.type, propertyNs.prefix).name; } assign(p, { ns: propertyNs, name: propertyName }); propertiesByName[propertyName] = p; }, this)); // update ns + name assign(type, { ns: ns, name: name, propertiesByName: propertiesByName }); forEach$1(type.extends, bind(function(extendsName) { var extended = this.typeMap[extendsName]; extended.traits = extended.traits || []; extended.traits.push(name); }, this)); // link to package this.definePackage(type, pkg); // register this.typeMap[name] = type; }; /** * Traverse the type hierarchy from bottom to top, * calling iterator with (type, inherited) for all elements in * the inheritance chain. * * @param {Object} nsName * @param {Function} iterator * @param {Boolean} [trait=false] */ Registry.prototype.mapTypes = function(nsName, iterator, trait) { var type = isBuiltIn(nsName.name) ? { name: nsName.name } : this.typeMap[nsName.name]; var self = this; /** * Traverse the selected trait. * * @param {String} cls */ function traverseTrait(cls) { return traverseSuper(cls, true); } /** * Traverse the selected super type or trait * * @param {String} cls * @param {Boolean} [trait=false] */ function traverseSuper(cls, trait) { var parentNs = parseName(cls, isBuiltIn(cls) ? '' : nsName.prefix); self.mapTypes(parentNs, iterator, trait); } if (!type) { throw new Error('unknown type <' + nsName.name + '>'); } forEach$1(type.superClass, trait ? traverseTrait : traverseSuper); // call iterator with (type, inherited=!trait) iterator(type, !trait); forEach$1(type.traits, traverseTrait); }; /** * Returns the effective descriptor for a type. * * @param {String} type the namespaced name (ns:localName) of the type * * @return {Descriptor} the resulting effective descriptor */ Registry.prototype.getEffectiveDescriptor = function(name) { var nsName = parseName(name); var builder = new DescriptorBuilder(nsName); this.mapTypes(nsName, function(type, inherited) { builder.addTrait(type, inherited); }); var descriptor = builder.build(); // define package link this.definePackage(descriptor, descriptor.allTypes[descriptor.allTypes.length - 1].$pkg); return descriptor; }; Registry.prototype.definePackage = function(target, pkg) { this.properties.define(target, '$pkg', { value: pkg }); }; ///////// helpers //////////////////////////// function ensureAvailable(packageMap, pkg, identifierKey) { var value = pkg[identifierKey]; if (value in packageMap) { throw new Error('package with ' + identifierKey + ' <' + value + '> already defined'); } } /** * A utility that gets and sets properties of model elements. * * @param {Model} model */ function Properties(model) { this.model = model; } /** * Sets a named property on the target element. * If the value is undefined, the property gets deleted. * * @param {Object} target * @param {String} name * @param {Object} value */ Properties.prototype.set = function(target, name, value) { var property = this.model.getPropertyDescriptor(target, name); var propertyName = property && property.name; if (isUndefined(value)) { // unset the property, if the specified value is undefined; // delete from $attrs (for extensions) or the target itself if (property) { delete target[propertyName]; } else { delete target.$attrs[name]; } } else { // set the property, defining well defined properties on the fly // or simply updating them in target.$attrs (for extensions) if (property) { if (propertyName in target) { target[propertyName] = value; } else { defineProperty(target, property, value); } } else { target.$attrs[name] = value; } } }; /** * Returns the named property of the given element * * @param {Object} target * @param {String} name * * @return {Object} */ Properties.prototype.get = function(target, name) { var property = this.model.getPropertyDescriptor(target, name); if (!property) { return target.$attrs[name]; } var propertyName = property.name; // check if access to collection property and lazily initialize it if (!target[propertyName] && property.isMany) { defineProperty(target, property, []); } return target[propertyName]; }; /** * Define a property on the target element * * @param {Object} target * @param {String} name * @param {Object} options */ Properties.prototype.define = function(target, name, options) { if (!options.writable) { var value = options.value; // use getters for read-only variables to support ES6 proxies // cf. https://github.com/bpmn-io/internal-docs/issues/386 options = assign({}, options, { get: function() { return value; } }); delete options.value; } Object.defineProperty(target, name, options); }; /** * Define the descriptor for an element */ Properties.prototype.defineDescriptor = function(target, descriptor) { this.define(target, '$descriptor', { value: descriptor }); }; /** * Define the model for an element */ Properties.prototype.defineModel = function(target, model) { this.define(target, '$model', { value: model }); }; function isUndefined(val) { return typeof val === 'undefined'; } function defineProperty(target, property, value) { Object.defineProperty(target, property.name, { enumerable: !property.isReference, writable: true, value: value, configurable: true }); } //// Moddle implementation ///////////////////////////////////////////////// /** * @class Moddle * * A model that can be used to create elements of a specific type. * * @example * * var Moddle = require('moddle'); * * var pkg = { * name: 'mypackage', * prefix: 'my', * types: [ * { name: 'Root' } * ] * }; * * var moddle = new Moddle([pkg]); * * @param {Array} packages the packages to contain */ function Moddle(packages) { this.properties = new Properties(this); this.factory = new Factory(this, this.properties); this.registry = new Registry(packages, this.properties); this.typeCache = {}; } /** * Create an instance of the specified type. * * @method Moddle#create * * @example * * var foo = moddle.create('my:Foo'); * var bar = moddle.create('my:Bar', { id: 'BAR_1' }); * * @param {String|Object} descriptor the type descriptor or name know to the model * @param {Object} attrs a number of attributes to initialize the model instance with * @return {Object} model instance */ Moddle.prototype.create = function(descriptor, attrs) { var Type = this.getType(descriptor); if (!Type) { throw new Error('unknown type <' + descriptor + '>'); } return new Type(attrs); }; /** * Returns the type representing a given descriptor * * @method Moddle#getType * * @example * * var Foo = moddle.getType('my:Foo'); * var foo = new Foo({ 'id' : 'FOO_1' }); * * @param {String|Object} descriptor the type descriptor or name know to the model * @return {Object} the type representing the descriptor */ Moddle.prototype.getType = function(descriptor) { var cache = this.typeCache; var name = isString(descriptor) ? descriptor : descriptor.ns.name; var type = cache[name]; if (!type) { descriptor = this.registry.getEffectiveDescriptor(name); type = cache[name] = this.factory.createType(descriptor); } return type; }; /** * Creates an any-element type to be used within model instances. * * This can be used to create custom elements that lie outside the meta-model. * The created element contains all the meta-data required to serialize it * as part of meta-model elements. * * @method Moddle#createAny * * @example * * var foo = moddle.createAny('vendor:Foo', 'http://vendor', { * value: 'bar' * }); * * var container = moddle.create('my:Container', 'http://my', { * any: [ foo ] * }); * * // go ahead and serialize the stuff * * * @param {String} name the name of the element * @param {String} nsUri the namespace uri of the element * @param {Object} [properties] a map of properties to initialize the instance with * @return {Object} the any type instance */ Moddle.prototype.createAny = function(name, nsUri, properties) { var nameNs = parseName(name); var element = { $type: name, $instanceOf: function(type) { return type === this.$type; } }; var descriptor = { name: name, isGeneric: true, ns: { prefix: nameNs.prefix, localName: nameNs.localName, uri: nsUri } }; this.properties.defineDescriptor(element, descriptor); this.properties.defineModel(element, this); this.properties.define(element, '$parent', { enumerable: false, writable: true }); this.properties.define(element, '$instanceOf', { enumerable: false, writable: true }); forEach$1(properties, function(a, key) { if (isObject(a) && a.value !== undefined) { element[a.name] = a.value; } else { element[key] = a; } }); return element; }; /** * Returns a registered package by uri or prefix * * @return {Object} the package */ Moddle.prototype.getPackage = function(uriOrPrefix) { return this.registry.getPackage(uriOrPrefix); }; /** * Returns a snapshot of all known packages * * @return {Object} the package */ Moddle.prototype.getPackages = function() { return this.registry.getPackages(); }; /** * Returns the descriptor for an element */ Moddle.prototype.getElementDescriptor = function(element) { return element.$descriptor; }; /** * Returns true if the given descriptor or instance * represents the given type. * * May be applied to this, if element is omitted. */ Moddle.prototype.hasType = function(element, type) { if (type === undefined) { type = element; element = this; } var descriptor = element.$model.getElementDescriptor(element); return (type in descriptor.allTypesByName); }; /** * Returns the descriptor of an elements named property */ Moddle.prototype.getPropertyDescriptor = function(element, property) { return this.getElementDescriptor(element).propertiesByName[property]; }; /** * Returns a mapped type's descriptor */ Moddle.prototype.getTypeDescriptor = function(type) { return this.registry.typeMap[type]; }; var fromCharCode = String.fromCharCode; var hasOwnProperty = Object.prototype.hasOwnProperty; var ENTITY_PATTERN = /&#(\d+);|&#x([0-9a-f]+);|&(\w+);/ig; var ENTITY_MAPPING = { 'amp': '&', 'apos': '\'', 'gt': '>', 'lt': '<', 'quot': '"' }; // map UPPERCASE variants of supported special chars Object.keys(ENTITY_MAPPING).forEach(function(k) { ENTITY_MAPPING[k.toUpperCase()] = ENTITY_MAPPING[k]; }); function replaceEntities(_, d, x, z) { // reserved names, i.e.   if (z) { if (hasOwnProperty.call(ENTITY_MAPPING, z)) { return ENTITY_MAPPING[z]; } else { // fall back to original value return '&' + z + ';'; } } // decimal encoded char if (d) { return fromCharCode(d); } // hex encoded char return fromCharCode(parseInt(x, 16)); } /** * A basic entity decoder that can decode a minimal * sub-set of reserved names (&) as well as * hex (ય) and decimal (ӏ) encoded characters. * * @param {string} str * * @return {string} decoded string */ function decodeEntities(s) { if (s.length > 3 && s.indexOf('&') !== -1) { return s.replace(ENTITY_PATTERN, replaceEntities); } return s; } var XSI_URI = 'http://www.w3.org/2001/XMLSchema-instance'; var XSI_PREFIX = 'xsi'; var XSI_TYPE$1 = 'xsi:type'; var NON_WHITESPACE_OUTSIDE_ROOT_NODE = 'non-whitespace outside of root node'; function error$2(msg) { return new Error(msg); } function missingNamespaceForPrefix(prefix) { return 'missing namespace for prefix <' + prefix + '>'; } function getter(getFn) { return { 'get': getFn, 'enumerable': true }; } function cloneNsMatrix(nsMatrix) { var clone = {}, key; for (key in nsMatrix) { clone[key] = nsMatrix[key]; } return clone; } function uriPrefix(prefix) { return prefix + '$uri'; } function buildNsMatrix(nsUriToPrefix) { var nsMatrix = {}, uri, prefix; for (uri in nsUriToPrefix) { prefix = nsUriToPrefix[uri]; nsMatrix[prefix] = prefix; nsMatrix[uriPrefix(prefix)] = uri; } return nsMatrix; } function noopGetContext() { return { 'line': 0, 'column': 0 }; } function throwFunc(err) { throw err; } /** * Creates a new parser with the given options. * * @constructor * * @param {!Object=} options */ function Parser(options) { if (!this) { return new Parser(options); } var proxy = options && options['proxy']; var onText, onOpenTag, onCloseTag, onCDATA, onError = throwFunc, onWarning, onComment, onQuestion, onAttention; var getContext = noopGetContext; /** * Do we need to parse the current elements attributes for namespaces? * * @type {boolean} */ var maybeNS = false; /** * Do we process namespaces at all? * * @type {boolean} */ var isNamespace = false; /** * The caught error returned on parse end * * @type {Error} */ var returnError = null; /** * Should we stop parsing? * * @type {boolean} */ var parseStop = false; /** * A map of { uri: prefix } used by the parser. * * This map will ensure we can normalize prefixes during processing; * for each uri, only one prefix will be exposed to the handlers. * * @type {!Object}} */ var nsUriToPrefix; /** * Handle parse error. * * @param {string|Error} err */ function handleError(err) { if (!(err instanceof Error)) { err = error$2(err); } returnError = err; onError(err, getContext); } /** * Handle parse error. * * @param {string|Error} err */ function handleWarning(err) { if (!onWarning) { return; } if (!(err instanceof Error)) { err = error$2(err); } onWarning(err, getContext); } /** * Register parse listener. * * @param {string} name * @param {Function} cb * * @return {Parser} */ this['on'] = function(name, cb) { if (typeof cb !== 'function') { throw error$2('required args '); } switch (name) { case 'openTag': onOpenTag = cb; break; case 'text': onText = cb; break; case 'closeTag': onCloseTag = cb; break; case 'error': onError = cb; break; case 'warn': onWarning = cb; break; case 'cdata': onCDATA = cb; break; case 'attention': onAttention = cb; break; // case 'question': onQuestion = cb; break; // case 'comment': onComment = cb; break; default: throw error$2('unsupported event: ' + name); } return this; }; /** * Set the namespace to prefix mapping. * * @example * * parser.ns({ * 'http://foo': 'foo', * 'http://bar': 'bar' * }); * * @param {!Object} nsMap * * @return {Parser} */ this['ns'] = function(nsMap) { if (typeof nsMap === 'undefined') { nsMap = {}; } if (typeof nsMap !== 'object') { throw error$2('required args '); } var _nsUriToPrefix = {}, k; for (k in nsMap) { _nsUriToPrefix[k] = nsMap[k]; } // FORCE default mapping for schema instance _nsUriToPrefix[XSI_URI] = XSI_PREFIX; isNamespace = true; nsUriToPrefix = _nsUriToPrefix; return this; }; /** * Parse xml string. * * @param {string} xml * * @return {Error} returnError, if not thrown */ this['parse'] = function(xml) { if (typeof xml !== 'string') { throw error$2('required args '); } returnError = null; parse(xml); getContext = noopGetContext; parseStop = false; return returnError; }; /** * Stop parsing. */ this['stop'] = function() { parseStop = true; }; /** * Parse string, invoking configured listeners on element. * * @param {string} xml */ function parse(xml) { var nsMatrixStack = isNamespace ? [] : null, nsMatrix = isNamespace ? buildNsMatrix(nsUriToPrefix) : null, _nsMatrix, nodeStack = [], anonymousNsCount = 0, tagStart = false, tagEnd = false, i = 0, j = 0, x, y, q, w, v, xmlns, elementName, _elementName, elementProxy ; var attrsString = '', attrsStart = 0, cachedAttrs // false = parsed with errors, null = needs parsing ; /** * Parse attributes on demand and returns the parsed attributes. * * Return semantics: (1) `false` on attribute parse error, * (2) object hash on extracted attrs. * * @return {boolean|Object} */ function getAttrs() { if (cachedAttrs !== null) { return cachedAttrs; } var nsUri, nsUriPrefix, nsName, defaultAlias = isNamespace && nsMatrix['xmlns'], attrList = isNamespace && maybeNS ? [] : null, i = attrsStart, s = attrsString, l = s.length, hasNewMatrix, newalias, value, alias, name, attrs = {}, seenAttrs = {}, skipAttr, w, j; parseAttr: for (; i < l; i++) { skipAttr = false; w = s.charCodeAt(i); if (w === 32 || (w < 14 && w > 8)) { // WHITESPACE={ \f\n\r\t\v} continue; } // wait for non whitespace character if (w < 65 || w > 122 || (w > 90 && w < 97)) { if (w !== 95 && w !== 58) { // char 95"_" 58":" handleWarning('illegal first char attribute name'); skipAttr = true; } } // parse attribute name for (j = i + 1; j < l; j++) { w = s.charCodeAt(j); if ( w > 96 && w < 123 || w > 64 && w < 91 || w > 47 && w < 59 || w === 46 || // '.' w === 45 || // '-' w === 95 // '_' ) { continue; } // unexpected whitespace if (w === 32 || (w < 14 && w > 8)) { // WHITESPACE handleWarning('missing attribute value'); i = j; continue parseAttr; } // expected "=" if (w === 61) { // "=" == 61 break; } handleWarning('illegal attribute name char'); skipAttr = true; } name = s.substring(i, j); if (name === 'xmlns:xmlns') { handleWarning('illegal declaration of xmlns'); skipAttr = true; } w = s.charCodeAt(j + 1); if (w === 34) { // '"' j = s.indexOf('"', i = j + 2); if (j === -1) { j = s.indexOf('\'', i); if (j !== -1) { handleWarning('attribute value quote missmatch'); skipAttr = true; } } } else if (w === 39) { // "'" j = s.indexOf('\'', i = j + 2); if (j === -1) { j = s.indexOf('"', i); if (j !== -1) { handleWarning('attribute value quote missmatch'); skipAttr = true; } } } else { handleWarning('missing attribute value quotes'); skipAttr = true; // skip to next space for (j = j + 1; j < l; j++) { w = s.charCodeAt(j + 1); if (w === 32 || (w < 14 && w > 8)) { // WHITESPACE break; } } } if (j === -1) { handleWarning('missing closing quotes'); j = l; skipAttr = true; } if (!skipAttr) { value = s.substring(i, j); } i = j; // ensure SPACE follows attribute // skip illegal content otherwise // example a="b"c for (; j + 1 < l; j++) { w = s.charCodeAt(j + 1); if (w === 32 || (w < 14 && w > 8)) { // WHITESPACE break; } // FIRST ILLEGAL CHAR if (i === j) { handleWarning('illegal character after attribute end'); skipAttr = true; } } // advance cursor to next attribute i = j + 1; if (skipAttr) { continue parseAttr; } // check attribute re-declaration if (name in seenAttrs) { handleWarning('attribute <' + name + '> already defined'); continue; } seenAttrs[name] = true; if (!isNamespace) { attrs[name] = value; continue; } // try to extract namespace information if (maybeNS) { newalias = ( name === 'xmlns' ? 'xmlns' : (name.charCodeAt(0) === 120 && name.substr(0, 6) === 'xmlns:') ? name.substr(6) : null ); // handle xmlns(:alias) assignment if (newalias !== null) { nsUri = decodeEntities(value); nsUriPrefix = uriPrefix(newalias); alias = nsUriToPrefix[nsUri]; if (!alias) { // no prefix defined or prefix collision if ( (newalias === 'xmlns') || (nsUriPrefix in nsMatrix && nsMatrix[nsUriPrefix] !== nsUri) ) { // alocate free ns prefix do { alias = 'ns' + (anonymousNsCount++); } while (typeof nsMatrix[alias] !== 'undefined'); } else { alias = newalias; } nsUriToPrefix[nsUri] = alias; } if (nsMatrix[newalias] !== alias) { if (!hasNewMatrix) { nsMatrix = cloneNsMatrix(nsMatrix); hasNewMatrix = true; } nsMatrix[newalias] = alias; if (newalias === 'xmlns') { nsMatrix[uriPrefix(alias)] = nsUri; defaultAlias = alias; } nsMatrix[nsUriPrefix] = nsUri; } // expose xmlns(:asd)="..." in attributes attrs[name] = value; continue; } // collect attributes until all namespace // declarations are processed attrList.push(name, value); continue; } /** end if (maybeNs) */ // handle attributes on element without // namespace declarations w = name.indexOf(':'); if (w === -1) { attrs[name] = value; continue; } // normalize ns attribute name if (!(nsName = nsMatrix[name.substring(0, w)])) { handleWarning(missingNamespaceForPrefix(name.substring(0, w))); continue; } name = defaultAlias === nsName ? name.substr(w + 1) : nsName + name.substr(w); // end: normalize ns attribute name // normalize xsi:type ns attribute value if (name === XSI_TYPE$1) { w = value.indexOf(':'); if (w !== -1) { nsName = value.substring(0, w); // handle default prefixes, i.e. xs:String gracefully nsName = nsMatrix[nsName] || nsName; value = nsName + value.substring(w); } else { value = defaultAlias + ':' + value; } } // end: normalize xsi:type ns attribute value attrs[name] = value; } // handle deferred, possibly namespaced attributes if (maybeNS) { // normalize captured attributes for (i = 0, l = attrList.length; i < l; i++) { name = attrList[i++]; value = attrList[i]; w = name.indexOf(':'); if (w !== -1) { // normalize ns attribute name if (!(nsName = nsMatrix[name.substring(0, w)])) { handleWarning(missingNamespaceForPrefix(name.substring(0, w))); continue; } name = defaultAlias === nsName ? name.substr(w + 1) : nsName + name.substr(w); // end: normalize ns attribute name // normalize xsi:type ns attribute value if (name === XSI_TYPE$1) { w = value.indexOf(':'); if (w !== -1) { nsName = value.substring(0, w); // handle default prefixes, i.e. xs:String gracefully nsName = nsMatrix[nsName] || nsName; value = nsName + value.substring(w); } else { value = defaultAlias + ':' + value; } } // end: normalize xsi:type ns attribute value } attrs[name] = value; } // end: normalize captured attributes } return cachedAttrs = attrs; } /** * Extract the parse context { line, column, part } * from the current parser position. * * @return {Object} parse context */ function getParseContext() { var splitsRe = /(\r\n|\r|\n)/g; var line = 0; var column = 0; var startOfLine = 0; var endOfLine = j; var match; var data; while (i >= startOfLine) { match = splitsRe.exec(xml); if (!match) { break; } // end of line = (break idx + break chars) endOfLine = match[0].length + match.index; if (endOfLine > i) { break; } // advance to next line line += 1; startOfLine = endOfLine; } // EOF errors if (i == -1) { column = endOfLine; data = xml.substring(j); } else // start errors if (j === 0) { data = xml.substring(j, i); } // other errors else { column = i - startOfLine; data = (j == -1 ? xml.substring(i) : xml.substring(i, j + 1)); } return { 'data': data, 'line': line, 'column': column }; } getContext = getParseContext; if (proxy) { elementProxy = Object.create({}, { 'name': getter(function() { return elementName; }), 'originalName': getter(function() { return _elementName; }), 'attrs': getter(getAttrs), 'ns': getter(function() { return nsMatrix; }) }); } // actual parse logic while (j !== -1) { if (xml.charCodeAt(j) === 60) { // "<" i = j; } else { i = xml.indexOf('<', j); } // parse end if (i === -1) { if (nodeStack.length) { return handleError('unexpected end of file'); } if (j === 0) { return handleError('missing start tag'); } if (j < xml.length) { if (xml.substring(j).trim()) { handleWarning(NON_WHITESPACE_OUTSIDE_ROOT_NODE); } } return; } // parse text if (j !== i) { if (nodeStack.length) { if (onText) { onText(xml.substring(j, i), decodeEntities, getContext); if (parseStop) { return; } } } else { if (xml.substring(j, i).trim()) { handleWarning(NON_WHITESPACE_OUTSIDE_ROOT_NODE); if (parseStop) { return; } } } } w = xml.charCodeAt(i+1); // parse comments + CDATA if (w === 33) { // "!" q = xml.charCodeAt(i+2); // CDATA section if (q === 91 && xml.substr(i + 3, 6) === 'CDATA[') { // 91 == "[" j = xml.indexOf(']]>', i); if (j === -1) { return handleError('unclosed cdata'); } if (onCDATA) { onCDATA(xml.substring(i + 9, j), getContext); if (parseStop) { return; } } j += 3; continue; } // comment if (q === 45 && xml.charCodeAt(i + 3) === 45) { // 45 == "-" j = xml.indexOf('-->', i); if (j === -1) { return handleError('unclosed comment'); } if (onComment) { onComment(xml.substring(i + 4, j), decodeEntities, getContext); if (parseStop) { return; } } j += 3; continue; } } // parse question if (w === 63) { // "?" j = xml.indexOf('?>', i); if (j === -1) { return handleError('unclosed question'); } if (onQuestion) { onQuestion(xml.substring(i, j + 2), getContext); if (parseStop) { return; } } j += 2; continue; } // find matching closing tag for attention or standard tags // for that we must skip through attribute values // (enclosed in single or double quotes) for (x = i + 1; ; x++) { v = xml.charCodeAt(x); if (isNaN(v)) { j = -1; return handleError('unclosed tag'); } // [10] AttValue ::= '"' ([^<&"] | Reference)* '"' | "'" ([^<&'] | Reference)* "'" // skips the quoted string // (double quotes) does not appear in a literal enclosed by (double quotes) // (single quote) does not appear in a literal enclosed by (single quote) if (v === 34) { // '"' q = xml.indexOf('"', x + 1); x = q !== -1 ? q : x; } else if (v === 39) { // "'" q = xml.indexOf("'", x + 1); x = q !== -1 ? q : x; } else if (v === 62) { // '>' j = x; break; } } // parse attention // previously comment and CDATA have already been parsed if (w === 33) { // "!" if (onAttention) { onAttention(xml.substring(i, j + 1), decodeEntities, getContext); if (parseStop) { return; } } j += 1; continue; } // don't process attributes; // there are none cachedAttrs = {}; // if (xml.charCodeAt(i+1) === 47) { // close tag match x = elementName = nodeStack.pop(); q = i + 2 + x.length; if (xml.substring(i + 2, q) !== x) { return handleError('closing tag mismatch'); } // verify chars in close tag for (; q < j; q++) { w = xml.charCodeAt(q); if (w === 32 || (w > 8 && w < 14)) { // \f\n\r\t\v space continue; } return handleError('close tag'); } } else { if (xml.charCodeAt(j - 1) === 47) { // .../> x = elementName = xml.substring(i + 1, j - 1); tagStart = true; tagEnd = true; } else { x = elementName = xml.substring(i + 1, j); tagStart = true; tagEnd = false; } if (!(w > 96 && w < 123 || w > 64 && w < 91 || w === 95 || w === 58)) { // char 95"_" 58":" return handleError('illegal first char nodeName'); } for (q = 1, y = x.length; q < y; q++) { w = x.charCodeAt(q); if (w > 96 && w < 123 || w > 64 && w < 91 || w > 47 && w < 59 || w === 45 || w === 95 || w == 46) { continue; } if (w === 32 || (w < 14 && w > 8)) { // \f\n\r\t\v space elementName = x.substring(0, q); // maybe there are attributes cachedAttrs = null; break; } return handleError('invalid nodeName'); } if (!tagEnd) { nodeStack.push(elementName); } } if (isNamespace) { _nsMatrix = nsMatrix; if (tagStart) { // remember old namespace // unless we're self-closing if (!tagEnd) { nsMatrixStack.push(_nsMatrix); } if (cachedAttrs === null) { // quick check, whether there may be namespace // declarations on the node; if that is the case // we need to eagerly parse the node attributes if ((maybeNS = x.indexOf('xmlns', q) !== -1)) { attrsStart = q; attrsString = x; getAttrs(); maybeNS = false; } } } _elementName = elementName; w = elementName.indexOf(':'); if (w !== -1) { xmlns = nsMatrix[elementName.substring(0, w)]; // prefix given; namespace must exist if (!xmlns) { return handleError('missing namespace on <' + _elementName + '>'); } elementName = elementName.substr(w + 1); } else { xmlns = nsMatrix['xmlns']; // if no default namespace is defined, // we'll import the element as anonymous. // // it is up to users to correct that to the document defined // targetNamespace, or whatever their undersanding of the // XML spec mandates. } // adjust namespace prefixs as configured if (xmlns) { elementName = xmlns + ':' + elementName; } } if (tagStart) { attrsStart = q; attrsString = x; if (onOpenTag) { if (proxy) { onOpenTag(elementProxy, decodeEntities, tagEnd, getContext); } else { onOpenTag(elementName, getAttrs, decodeEntities, tagEnd, getContext); } if (parseStop) { return; } } } if (tagEnd) { if (onCloseTag) { onCloseTag(proxy ? elementProxy : elementName, decodeEntities, tagStart, getContext); if (parseStop) { return; } } // restore old namespace if (isNamespace) { if (!tagStart) { nsMatrix = nsMatrixStack.pop(); } else { nsMatrix = _nsMatrix; } } } j += 1; } } /** end parse */ } function hasLowerCaseAlias(pkg) { return pkg.xml && pkg.xml.tagAlias === 'lowerCase'; } var DEFAULT_NS_MAP = { 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'xml': 'http://www.w3.org/XML/1998/namespace' }; var XSI_TYPE = 'xsi:type'; function serializeFormat(element) { return element.xml && element.xml.serialize; } function serializeAsType(element) { return serializeFormat(element) === XSI_TYPE; } function serializeAsProperty(element) { return serializeFormat(element) === 'property'; } function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function aliasToName(aliasNs, pkg) { if (!hasLowerCaseAlias(pkg)) { return aliasNs.name; } return aliasNs.prefix + ':' + capitalize(aliasNs.localName); } function prefixedToName(nameNs, pkg) { var name = nameNs.name, localName = nameNs.localName; var typePrefix = pkg.xml && pkg.xml.typePrefix; if (typePrefix && localName.indexOf(typePrefix) === 0) { return nameNs.prefix + ':' + localName.slice(typePrefix.length); } else { return name; } } function normalizeXsiTypeName(name, model) { var nameNs = parseName(name); var pkg = model.getPackage(nameNs.prefix); return prefixedToName(nameNs, pkg); } function error$1(message) { return new Error(message); } /** * Get the moddle descriptor for a given instance or type. * * @param {ModdleElement|Function} element * * @return {Object} the moddle descriptor */ function getModdleDescriptor(element) { return element.$descriptor; } /** * A parse context. * * @class * * @param {Object} options * @param {ElementHandler} options.rootHandler the root handler for parsing a document * @param {boolean} [options.lax=false] whether or not to ignore invalid elements */ function Context(options) { /** * @property {ElementHandler} rootHandler */ /** * @property {Boolean} lax */ assign(this, options); this.elementsById = {}; this.references = []; this.warnings = []; /** * Add an unresolved reference. * * @param {Object} reference */ this.addReference = function(reference) { this.references.push(reference); }; /** * Add a processed element. * * @param {ModdleElement} element */ this.addElement = function(element) { if (!element) { throw error$1('expected element'); } var elementsById = this.elementsById; var descriptor = getModdleDescriptor(element); var idProperty = descriptor.idProperty, id; if (idProperty) { id = element.get(idProperty.name); if (id) { // for QName validation as per http://www.w3.org/TR/REC-xml/#NT-NameChar if (!/^([a-z][\w-.]*:)?[a-z_][\w-.]*$/i.test(id)) { throw new Error('illegal ID <' + id + '>'); } if (elementsById[id]) { throw error$1('duplicate ID <' + id + '>'); } elementsById[id] = element; } } }; /** * Add an import warning. * * @param {Object} warning * @param {String} warning.message * @param {Error} [warning.error] */ this.addWarning = function(warning) { this.warnings.push(warning); }; } function BaseHandler() {} BaseHandler.prototype.handleEnd = function() {}; BaseHandler.prototype.handleText = function() {}; BaseHandler.prototype.handleNode = function() {}; /** * A simple pass through handler that does nothing except for * ignoring all input it receives. * * This is used to ignore unknown elements and * attributes. */ function NoopHandler() { } NoopHandler.prototype = Object.create(BaseHandler.prototype); NoopHandler.prototype.handleNode = function() { return this; }; function BodyHandler() {} BodyHandler.prototype = Object.create(BaseHandler.prototype); BodyHandler.prototype.handleText = function(text) { this.body = (this.body || '') + text; }; function ReferenceHandler(property, context) { this.property = property; this.context = context; } ReferenceHandler.prototype = Object.create(BodyHandler.prototype); ReferenceHandler.prototype.handleNode = function(node) { if (this.element) { throw error$1('expected no sub nodes'); } else { this.element = this.createReference(node); } return this; }; ReferenceHandler.prototype.handleEnd = function() { this.element.id = this.body; }; ReferenceHandler.prototype.createReference = function(node) { return { property: this.property.ns.name, id: '' }; }; function ValueHandler(propertyDesc, element) { this.element = element; this.propertyDesc = propertyDesc; } ValueHandler.prototype = Object.create(BodyHandler.prototype); ValueHandler.prototype.handleEnd = function() { var value = this.body || '', element = this.element, propertyDesc = this.propertyDesc; value = coerceType(propertyDesc.type, value); if (propertyDesc.isMany) { element.get(propertyDesc.name).push(value); } else { element.set(propertyDesc.name, value); } }; function BaseElementHandler() {} BaseElementHandler.prototype = Object.create(BodyHandler.prototype); BaseElementHandler.prototype.handleNode = function(node) { var parser = this, element = this.element; if (!element) { element = this.element = this.createElement(node); this.context.addElement(element); } else { parser = this.handleChild(node); } return parser; }; /** * @class Reader.ElementHandler * */ function ElementHandler(model, typeName, context) { this.model = model; this.type = model.getType(typeName); this.context = context; } ElementHandler.prototype = Object.create(BaseElementHandler.prototype); ElementHandler.prototype.addReference = function(reference) { this.context.addReference(reference); }; ElementHandler.prototype.handleText = function(text) { var element = this.element, descriptor = getModdleDescriptor(element), bodyProperty = descriptor.bodyProperty; if (!bodyProperty) { throw error$1('unexpected body text <' + text + '>'); } BodyHandler.prototype.handleText.call(this, text); }; ElementHandler.prototype.handleEnd = function() { var value = this.body, element = this.element, descriptor = getModdleDescriptor(element), bodyProperty = descriptor.bodyProperty; if (bodyProperty && value !== undefined) { value = coerceType(bodyProperty.type, value); element.set(bodyProperty.name, value); } }; /** * Create an instance of the model from the given node. * * @param {Element} node the xml node */ ElementHandler.prototype.createElement = function(node) { var attributes = node.attributes, Type = this.type, descriptor = getModdleDescriptor(Type), context = this.context, instance = new Type({}), model = this.model, propNameNs; forEach$1(attributes, function(value, name) { var prop = descriptor.propertiesByName[name], values; if (prop && prop.isReference) { if (!prop.isMany) { context.addReference({ element: instance, property: prop.ns.name, id: value }); } else { // IDREFS: parse references as whitespace-separated list values = value.split(' '); forEach$1(values, function(v) { context.addReference({ element: instance, property: prop.ns.name, id: v }); }); } } else { if (prop) { value = coerceType(prop.type, value); } else if (name !== 'xmlns') { propNameNs = parseName(name, descriptor.ns.prefix); // check whether attribute is defined in a well-known namespace // if that is the case we emit a warning to indicate potential misuse if (model.getPackage(propNameNs.prefix)) { context.addWarning({ message: 'unknown attribute <' + name + '>', element: instance, property: name, value: value }); } } instance.set(name, value); } }); return instance; }; ElementHandler.prototype.getPropertyForNode = function(node) { var name = node.name; var nameNs = parseName(name); var type = this.type, model = this.model, descriptor = getModdleDescriptor(type); var propertyName = nameNs.name, property = descriptor.propertiesByName[propertyName], elementTypeName, elementType; // search for properties by name first if (property && !property.isAttr) { if (serializeAsType(property)) { elementTypeName = node.attributes[XSI_TYPE]; // xsi type is optional, if it does not exists the // default type is assumed if (elementTypeName) { // take possible type prefixes from XML // into account, i.e.: xsi:type="t{ActualType}" elementTypeName = normalizeXsiTypeName(elementTypeName, model); elementType = model.getType(elementTypeName); return assign({}, property, { effectiveType: getModdleDescriptor(elementType).name }); } } // search for properties by name first return property; } var pkg = model.getPackage(nameNs.prefix); if (pkg) { elementTypeName = aliasToName(nameNs, pkg); elementType = model.getType(elementTypeName); // search for collection members later property = find(descriptor.properties, function(p) { return !p.isVirtual && !p.isReference && !p.isAttribute && elementType.hasType(p.type); }); if (property) { return assign({}, property, { effectiveType: getModdleDescriptor(elementType).name }); } } else { // parse unknown element (maybe extension) property = find(descriptor.properties, function(p) { return !p.isReference && !p.isAttribute && p.type === 'Element'; }); if (property) { return property; } } throw error$1('unrecognized element <' + nameNs.name + '>'); }; ElementHandler.prototype.toString = function() { return 'ElementDescriptor[' + getModdleDescriptor(this.type).name + ']'; }; ElementHandler.prototype.valueHandler = function(propertyDesc, element) { return new ValueHandler(propertyDesc, element); }; ElementHandler.prototype.referenceHandler = function(propertyDesc) { return new ReferenceHandler(propertyDesc, this.context); }; ElementHandler.prototype.handler = function(type) { if (type === 'Element') { return new GenericElementHandler(this.model, type, this.context); } else { return new ElementHandler(this.model, type, this.context); } }; /** * Handle the child element parsing * * @param {Element} node the xml node */ ElementHandler.prototype.handleChild = function(node) { var propertyDesc, type, element, childHandler; propertyDesc = this.getPropertyForNode(node); element = this.element; type = propertyDesc.effectiveType || propertyDesc.type; if (isSimple(type)) { return this.valueHandler(propertyDesc, element); } if (propertyDesc.isReference) { childHandler = this.referenceHandler(propertyDesc).handleNode(node); } else { childHandler = this.handler(type).handleNode(node); } var newElement = childHandler.element; // child handles may decide to skip elements // by not returning anything if (newElement !== undefined) { if (propertyDesc.isMany) { element.get(propertyDesc.name).push(newElement); } else { element.set(propertyDesc.name, newElement); } if (propertyDesc.isReference) { assign(newElement, { element: element }); this.context.addReference(newElement); } else { // establish child -> parent relationship newElement.$parent = element; } } return childHandler; }; /** * An element handler that performs special validation * to ensure the node it gets initialized with matches * the handlers type (namespace wise). * * @param {Moddle} model * @param {String} typeName * @param {Context} context */ function RootElementHandler(model, typeName, context) { ElementHandler.call(this, model, typeName, context); } RootElementHandler.prototype = Object.create(ElementHandler.prototype); RootElementHandler.prototype.createElement = function(node) { var name = node.name, nameNs = parseName(name), model = this.model, type = this.type, pkg = model.getPackage(nameNs.prefix), typeName = pkg && aliasToName(nameNs, pkg) || name; // verify the correct namespace if we parse // the first element in the handler tree // // this ensures we don't mistakenly import wrong namespace elements if (!type.hasType(typeName)) { throw error$1('unexpected element <' + node.originalName + '>'); } return ElementHandler.prototype.createElement.call(this, node); }; function GenericElementHandler(model, typeName, context) { this.model = model; this.context = context; } GenericElementHandler.prototype = Object.create(BaseElementHandler.prototype); GenericElementHandler.prototype.createElement = function(node) { var name = node.name, ns = parseName(name), prefix = ns.prefix, uri = node.ns[prefix + '$uri'], attributes = node.attributes; return this.model.createAny(name, uri, attributes); }; GenericElementHandler.prototype.handleChild = function(node) { var handler = new GenericElementHandler(this.model, 'Element', this.context).handleNode(node), element = this.element; var newElement = handler.element, children; if (newElement !== undefined) { children = element.$children = element.$children || []; children.push(newElement); // establish child -> parent relationship newElement.$parent = element; } return handler; }; GenericElementHandler.prototype.handleEnd = function() { if (this.body) { this.element.$body = this.body; } }; /** * A reader for a meta-model * * @param {Object} options * @param {Model} options.model used to read xml files * @param {Boolean} options.lax whether to make parse errors warnings */ function Reader(options) { if (options instanceof Moddle) { options = { model: options }; } assign(this, { lax: false }, options); } /** * The fromXML result. * * @typedef {Object} ParseResult * * @property {ModdleElement} rootElement * @property {Array} references * @property {Array} warnings * @property {Object} elementsById - a mapping containing each ID -> ModdleElement */ /** * The fromXML result. * * @typedef {Error} ParseError * * @property {Array} warnings */ /** * Parse the given XML into a moddle document tree. * * @param {String} xml * @param {ElementHandler|Object} options or rootHandler * * @returns {Promise} */ Reader.prototype.fromXML = function(xml, options, done) { var rootHandler = options.rootHandler; if (options instanceof ElementHandler) { // root handler passed via (xml, { rootHandler: ElementHandler }, ...) rootHandler = options; options = {}; } else { if (typeof options === 'string') { // rootHandler passed via (xml, 'someString', ...) rootHandler = this.handler(options); options = {}; } else if (typeof rootHandler === 'string') { // rootHandler passed via (xml, { rootHandler: 'someString' }, ...) rootHandler = this.handler(rootHandler); } } var model = this.model, lax = this.lax; var context = new Context(assign({}, options, { rootHandler: rootHandler })), parser = new Parser({ proxy: true }), stack = createStack(); rootHandler.context = context; // push root handler stack.push(rootHandler); /** * Handle error. * * @param {Error} err * @param {Function} getContext * @param {boolean} lax * * @return {boolean} true if handled */ function handleError(err, getContext, lax) { var ctx = getContext(); var line = ctx.line, column = ctx.column, data = ctx.data; // we receive the full context data here, // for elements trim down the information // to the tag name, only if (data.charAt(0) === '<' && data.indexOf(' ') !== -1) { data = data.slice(0, data.indexOf(' ')) + '>'; } var message = 'unparsable content ' + (data ? data + ' ' : '') + 'detected\n\t' + 'line: ' + line + '\n\t' + 'column: ' + column + '\n\t' + 'nested error: ' + err.message; if (lax) { context.addWarning({ message: message, error: err }); return true; } else { throw error$1(message); } } function handleWarning(err, getContext) { // just like handling errors in mode return handleError(err, getContext, true); } /** * Resolve collected references on parse end. */ function resolveReferences() { var elementsById = context.elementsById; var references = context.references; var i, r; for (i = 0; (r = references[i]); i++) { var element = r.element; var reference = elementsById[r.id]; var property = getModdleDescriptor(element).propertiesByName[r.property]; if (!reference) { context.addWarning({ message: 'unresolved reference <' + r.id + '>', element: r.element, property: r.property, value: r.id }); } if (property.isMany) { var collection = element.get(property.name), idx = collection.indexOf(r); // we replace an existing place holder (idx != -1) or // append to the collection instead if (idx === -1) { idx = collection.length; } if (!reference) { // remove unresolvable reference collection.splice(idx, 1); } else { // add or update reference in collection collection[idx] = reference; } } else { element.set(property.name, reference); } } } function handleClose() { stack.pop().handleEnd(); } var PREAMBLE_START_PATTERN = /^<\?xml /i; var ENCODING_PATTERN = / encoding="([^"]+)"/i; var UTF_8_PATTERN = /^utf-8$/i; function handleQuestion(question) { if (!PREAMBLE_START_PATTERN.test(question)) { return; } var match = ENCODING_PATTERN.exec(question); var encoding = match && match[1]; if (!encoding || UTF_8_PATTERN.test(encoding)) { return; } context.addWarning({ message: 'unsupported document encoding <' + encoding + '>, ' + 'falling back to UTF-8' }); } function handleOpen(node, getContext) { var handler = stack.peek(); try { stack.push(handler.handleNode(node)); } catch (err) { if (handleError(err, getContext, lax)) { stack.push(new NoopHandler()); } } } function handleCData(text, getContext) { try { stack.peek().handleText(text); } catch (err) { handleWarning(err, getContext); } } function handleText(text, getContext) { // strip whitespace only nodes, i.e. before // sections and in between tags if (!text.trim()) { return; } handleCData(text, getContext); } var uriMap = model.getPackages().reduce(function(uriMap, p) { uriMap[p.uri] = p.prefix; return uriMap; }, { 'http://www.w3.org/XML/1998/namespace': 'xml' // add default xml ns }); parser .ns(uriMap) .on('openTag', function(obj, decodeStr, selfClosing, getContext) { // gracefully handle unparsable attributes (attrs=false) var attrs = obj.attrs || {}; var decodedAttrs = Object.keys(attrs).reduce(function(d, key) { var value = decodeStr(attrs[key]); d[key] = value; return d; }, {}); var node = { name: obj.name, originalName: obj.originalName, attributes: decodedAttrs, ns: obj.ns }; handleOpen(node, getContext); }) .on('question', handleQuestion) .on('closeTag', handleClose) .on('cdata', handleCData) .on('text', function(text, decodeEntities, getContext) { handleText(decodeEntities(text), getContext); }) .on('error', handleError) .on('warn', handleWarning); // async XML parsing to make sure the execution environment // (node or brower) is kept responsive and that certain optimization // strategies can kick in. return new Promise(function(resolve, reject) { var err; try { parser.parse(xml); resolveReferences(); } catch (e) { err = e; } var rootElement = rootHandler.element; if (!err && !rootElement) { err = error$1('failed to parse document as <' + rootHandler.type.$descriptor.name + '>'); } var warnings = context.warnings; var references = context.references; var elementsById = context.elementsById; if (err) { err.warnings = warnings; return reject(err); } else { return resolve({ rootElement: rootElement, elementsById: elementsById, references: references, warnings: warnings }); } }); }; Reader.prototype.handler = function(name) { return new RootElementHandler(this.model, name); }; // helpers ////////////////////////// function createStack() { var stack = []; Object.defineProperty(stack, 'peek', { value: function() { return this[this.length - 1]; } }); return stack; } var XML_PREAMBLE = '\n'; var ESCAPE_ATTR_CHARS = /<|>|'|"|&|\n\r|\n/g; var ESCAPE_CHARS = /<|>|&/g; function Namespaces(parent) { var prefixMap = {}; var uriMap = {}; var used = {}; var wellknown = []; var custom = []; // API this.byUri = function(uri) { return uriMap[uri] || ( parent && parent.byUri(uri) ); }; this.add = function(ns, isWellknown) { uriMap[ns.uri] = ns; if (isWellknown) { wellknown.push(ns); } else { custom.push(ns); } this.mapPrefix(ns.prefix, ns.uri); }; this.uriByPrefix = function(prefix) { return prefixMap[prefix || 'xmlns']; }; this.mapPrefix = function(prefix, uri) { prefixMap[prefix || 'xmlns'] = uri; }; this.getNSKey = function(ns) { return (ns.prefix !== undefined) ? (ns.uri + '|' + ns.prefix) : ns.uri; }; this.logUsed = function(ns) { var uri = ns.uri; var nsKey = this.getNSKey(ns); used[nsKey] = this.byUri(uri); // Inform parent recursively about the usage of this NS if (parent) { parent.logUsed(ns); } }; this.getUsed = function(ns) { function isUsed(ns) { var nsKey = self.getNSKey(ns); return used[nsKey]; } var self = this; var allNs = [].concat(wellknown, custom); return allNs.filter(isUsed); }; } function lower(string) { return string.charAt(0).toLowerCase() + string.slice(1); } function nameToAlias(name, pkg) { if (hasLowerCaseAlias(pkg)) { return lower(name); } else { return name; } } function inherits(ctor, superCtor) { ctor.super_ = superCtor; ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); } function nsName(ns) { if (isString(ns)) { return ns; } else { return (ns.prefix ? ns.prefix + ':' : '') + ns.localName; } } function getNsAttrs(namespaces) { return namespaces.getUsed().filter(function(ns) { // do not serialize built in namespace return ns.prefix !== 'xml'; }).map(function(ns) { var name = 'xmlns' + (ns.prefix ? ':' + ns.prefix : ''); return { name: name, value: ns.uri }; }); } function getElementNs(ns, descriptor) { if (descriptor.isGeneric) { return assign({ localName: descriptor.ns.localName }, ns); } else { return assign({ localName: nameToAlias(descriptor.ns.localName, descriptor.$pkg) }, ns); } } function getPropertyNs(ns, descriptor) { return assign({ localName: descriptor.ns.localName }, ns); } function getSerializableProperties(element) { var descriptor = element.$descriptor; return filter(descriptor.properties, function(p) { var name = p.name; if (p.isVirtual) { return false; } // do not serialize defaults if (!has$1(element, name)) { return false; } var value = element[name]; // do not serialize default equals if (value === p.default) { return false; } // do not serialize null properties if (value === null) { return false; } return p.isMany ? value.length : true; }); } var ESCAPE_ATTR_MAP = { '\n': '#10', '\n\r': '#10', '"': '#34', '\'': '#39', '<': '#60', '>': '#62', '&': '#38' }; var ESCAPE_MAP = { '<': 'lt', '>': 'gt', '&': 'amp' }; function escape(str, charPattern, replaceMap) { // ensure we are handling strings here str = isString(str) ? str : '' + str; return str.replace(charPattern, function(s) { return '&' + replaceMap[s] + ';'; }); } /** * Escape a string attribute to not contain any bad values (line breaks, '"', ...) * * @param {String} str the string to escape * @return {String} the escaped string */ function escapeAttr(str) { return escape(str, ESCAPE_ATTR_CHARS, ESCAPE_ATTR_MAP); } function escapeBody(str) { return escape(str, ESCAPE_CHARS, ESCAPE_MAP); } function filterAttributes(props) { return filter(props, function(p) { return p.isAttr; }); } function filterContained(props) { return filter(props, function(p) { return !p.isAttr; }); } function ReferenceSerializer(tagName) { this.tagName = tagName; } ReferenceSerializer.prototype.build = function(element) { this.element = element; return this; }; ReferenceSerializer.prototype.serializeTo = function(writer) { writer .appendIndent() .append('<' + this.tagName + '>' + this.element.id + '') .appendNewLine(); }; function BodySerializer() {} BodySerializer.prototype.serializeValue = BodySerializer.prototype.serializeTo = function(writer) { writer.append( this.escape ? escapeBody(this.value) : this.value ); }; BodySerializer.prototype.build = function(prop, value) { this.value = value; if (prop.type === 'String' && value.search(ESCAPE_CHARS) !== -1) { this.escape = true; } return this; }; function ValueSerializer(tagName) { this.tagName = tagName; } inherits(ValueSerializer, BodySerializer); ValueSerializer.prototype.serializeTo = function(writer) { writer .appendIndent() .append('<' + this.tagName + '>'); this.serializeValue(writer); writer .append('') .appendNewLine(); }; function ElementSerializer(parent, propertyDescriptor) { this.body = []; this.attrs = []; this.parent = parent; this.propertyDescriptor = propertyDescriptor; } ElementSerializer.prototype.build = function(element) { this.element = element; var elementDescriptor = element.$descriptor, propertyDescriptor = this.propertyDescriptor; var otherAttrs, properties; var isGeneric = elementDescriptor.isGeneric; if (isGeneric) { otherAttrs = this.parseGeneric(element); } else { otherAttrs = this.parseNsAttributes(element); } if (propertyDescriptor) { this.ns = this.nsPropertyTagName(propertyDescriptor); } else { this.ns = this.nsTagName(elementDescriptor); } // compute tag name this.tagName = this.addTagName(this.ns); if (!isGeneric) { properties = getSerializableProperties(element); this.parseAttributes(filterAttributes(properties)); this.parseContainments(filterContained(properties)); } this.parseGenericAttributes(element, otherAttrs); return this; }; ElementSerializer.prototype.nsTagName = function(descriptor) { var effectiveNs = this.logNamespaceUsed(descriptor.ns); return getElementNs(effectiveNs, descriptor); }; ElementSerializer.prototype.nsPropertyTagName = function(descriptor) { var effectiveNs = this.logNamespaceUsed(descriptor.ns); return getPropertyNs(effectiveNs, descriptor); }; ElementSerializer.prototype.isLocalNs = function(ns) { return ns.uri === this.ns.uri; }; /** * Get the actual ns attribute name for the given element. * * @param {Object} element * @param {Boolean} [element.inherited=false] * * @return {Object} nsName */ ElementSerializer.prototype.nsAttributeName = function(element) { var ns; if (isString(element)) { ns = parseName(element); } else { ns = element.ns; } // return just local name for inherited attributes if (element.inherited) { return { localName: ns.localName }; } // parse + log effective ns var effectiveNs = this.logNamespaceUsed(ns); // LOG ACTUAL namespace use this.getNamespaces().logUsed(effectiveNs); // strip prefix if same namespace like parent if (this.isLocalNs(effectiveNs)) { return { localName: ns.localName }; } else { return assign({ localName: ns.localName }, effectiveNs); } }; ElementSerializer.prototype.parseGeneric = function(element) { var self = this, body = this.body; var attributes = []; forEach$1(element, function(val, key) { var nonNsAttr; if (key === '$body') { body.push(new BodySerializer().build({ type: 'String' }, val)); } else if (key === '$children') { forEach$1(val, function(child) { body.push(new ElementSerializer(self).build(child)); }); } else if (key.indexOf('$') !== 0) { nonNsAttr = self.parseNsAttribute(element, key, val); if (nonNsAttr) { attributes.push({ name: key, value: val }); } } }); return attributes; }; ElementSerializer.prototype.parseNsAttribute = function(element, name, value) { var model = element.$model; var nameNs = parseName(name); var ns; // parse xmlns:foo="http://foo.bar" if (nameNs.prefix === 'xmlns') { ns = { prefix: nameNs.localName, uri: value }; } // parse xmlns="http://foo.bar" if (!nameNs.prefix && nameNs.localName === 'xmlns') { ns = { uri: value }; } if (!ns) { return { name: name, value: value }; } if (model && model.getPackage(value)) { // register well known namespace this.logNamespace(ns, true, true); } else { // log custom namespace directly as used var actualNs = this.logNamespaceUsed(ns, true); this.getNamespaces().logUsed(actualNs); } }; /** * Parse namespaces and return a list of left over generic attributes * * @param {Object} element * @return {Array} */ ElementSerializer.prototype.parseNsAttributes = function(element, attrs) { var self = this; var genericAttrs = element.$attrs; var attributes = []; // parse namespace attributes first // and log them. push non namespace attributes to a list // and process them later forEach$1(genericAttrs, function(value, name) { var nonNsAttr = self.parseNsAttribute(element, name, value); if (nonNsAttr) { attributes.push(nonNsAttr); } }); return attributes; }; ElementSerializer.prototype.parseGenericAttributes = function(element, attributes) { var self = this; forEach$1(attributes, function(attr) { // do not serialize xsi:type attribute // it is set manually based on the actual implementation type if (attr.name === XSI_TYPE) { return; } try { self.addAttribute(self.nsAttributeName(attr.name), attr.value); } catch (e) { console.warn( 'missing namespace information for ', attr.name, '=', attr.value, 'on', element, e); } }); }; ElementSerializer.prototype.parseContainments = function(properties) { var self = this, body = this.body, element = this.element; forEach$1(properties, function(p) { var value = element.get(p.name), isReference = p.isReference, isMany = p.isMany; if (!isMany) { value = [ value ]; } if (p.isBody) { body.push(new BodySerializer().build(p, value[0])); } else if (isSimple(p.type)) { forEach$1(value, function(v) { body.push(new ValueSerializer(self.addTagName(self.nsPropertyTagName(p))).build(p, v)); }); } else if (isReference) { forEach$1(value, function(v) { body.push(new ReferenceSerializer(self.addTagName(self.nsPropertyTagName(p))).build(v)); }); } else { // allow serialization via type // rather than element name var asType = serializeAsType(p), asProperty = serializeAsProperty(p); forEach$1(value, function(v) { var serializer; if (asType) { serializer = new TypeSerializer(self, p); } else if (asProperty) { serializer = new ElementSerializer(self, p); } else { serializer = new ElementSerializer(self); } body.push(serializer.build(v)); }); } }); }; ElementSerializer.prototype.getNamespaces = function(local) { var namespaces = this.namespaces, parent = this.parent, parentNamespaces; if (!namespaces) { parentNamespaces = parent && parent.getNamespaces(); if (local || !parentNamespaces) { this.namespaces = namespaces = new Namespaces(parentNamespaces); } else { namespaces = parentNamespaces; } } return namespaces; }; ElementSerializer.prototype.logNamespace = function(ns, wellknown, local) { var namespaces = this.getNamespaces(local); var nsUri = ns.uri, nsPrefix = ns.prefix; var existing = namespaces.byUri(nsUri); if (!existing || local) { namespaces.add(ns, wellknown); } namespaces.mapPrefix(nsPrefix, nsUri); return ns; }; ElementSerializer.prototype.logNamespaceUsed = function(ns, local) { var element = this.element, model = element.$model, namespaces = this.getNamespaces(local); // ns may be // // * prefix only // * prefix:uri // * localName only var prefix = ns.prefix, uri = ns.uri, newPrefix, idx, wellknownUri; // handle anonymous namespaces (elementForm=unqualified), cf. #23 if (!prefix && !uri) { return { localName: ns.localName }; } wellknownUri = DEFAULT_NS_MAP[prefix] || model && (model.getPackage(prefix) || {}).uri; uri = uri || wellknownUri || namespaces.uriByPrefix(prefix); if (!uri) { throw new Error('no namespace uri given for prefix <' + prefix + '>'); } ns = namespaces.byUri(uri); if (!ns) { newPrefix = prefix; idx = 1; // find a prefix that is not mapped yet while (namespaces.uriByPrefix(newPrefix)) { newPrefix = prefix + '_' + idx++; } ns = this.logNamespace({ prefix: newPrefix, uri: uri }, wellknownUri === uri); } if (prefix) { namespaces.mapPrefix(prefix, uri); } return ns; }; ElementSerializer.prototype.parseAttributes = function(properties) { var self = this, element = this.element; forEach$1(properties, function(p) { var value = element.get(p.name); if (p.isReference) { if (!p.isMany) { value = value.id; } else { var values = []; forEach$1(value, function(v) { values.push(v.id); }); // IDREFS is a whitespace-separated list of references. value = values.join(' '); } } self.addAttribute(self.nsAttributeName(p), value); }); }; ElementSerializer.prototype.addTagName = function(nsTagName) { var actualNs = this.logNamespaceUsed(nsTagName); this.getNamespaces().logUsed(actualNs); return nsName(nsTagName); }; ElementSerializer.prototype.addAttribute = function(name, value) { var attrs = this.attrs; if (isString(value)) { value = escapeAttr(value); } // de-duplicate attributes // https://github.com/bpmn-io/moddle-xml/issues/66 var idx = findIndex(attrs, function(element) { return ( element.name.localName === name.localName && element.name.uri === name.uri && element.name.prefix === name.prefix ); }); var attr = { name: name, value: value }; if (idx !== -1) { attrs.splice(idx, 1, attr); } else { attrs.push(attr); } }; ElementSerializer.prototype.serializeAttributes = function(writer) { var attrs = this.attrs, namespaces = this.namespaces; if (namespaces) { attrs = getNsAttrs(namespaces).concat(attrs); } forEach$1(attrs, function(a) { writer .append(' ') .append(nsName(a.name)).append('="').append(a.value).append('"'); }); }; ElementSerializer.prototype.serializeTo = function(writer) { var firstBody = this.body[0], indent = firstBody && firstBody.constructor !== BodySerializer; writer .appendIndent() .append('<' + this.tagName); this.serializeAttributes(writer); writer.append(firstBody ? '>' : ' />'); if (firstBody) { if (indent) { writer .appendNewLine() .indent(); } forEach$1(this.body, function(b) { b.serializeTo(writer); }); if (indent) { writer .unindent() .appendIndent(); } writer.append(''); } writer.appendNewLine(); }; /** * A serializer for types that handles serialization of data types */ function TypeSerializer(parent, propertyDescriptor) { ElementSerializer.call(this, parent, propertyDescriptor); } inherits(TypeSerializer, ElementSerializer); TypeSerializer.prototype.parseNsAttributes = function(element) { // extracted attributes var attributes = ElementSerializer.prototype.parseNsAttributes.call(this, element); var descriptor = element.$descriptor; // only serialize xsi:type if necessary if (descriptor.name === this.propertyDescriptor.type) { return attributes; } var typeNs = this.typeNs = this.nsTagName(descriptor); this.getNamespaces().logUsed(this.typeNs); // add xsi:type attribute to represent the elements // actual type var pkg = element.$model.getPackage(typeNs.uri), typePrefix = (pkg.xml && pkg.xml.typePrefix) || ''; this.addAttribute( this.nsAttributeName(XSI_TYPE), (typeNs.prefix ? typeNs.prefix + ':' : '') + typePrefix + descriptor.ns.localName ); return attributes; }; TypeSerializer.prototype.isLocalNs = function(ns) { return ns.uri === (this.typeNs || this.ns).uri; }; function SavingWriter() { this.value = ''; this.write = function(str) { this.value += str; }; } function FormatingWriter(out, format) { var indent = ['']; this.append = function(str) { out.write(str); return this; }; this.appendNewLine = function() { if (format) { out.write('\n'); } return this; }; this.appendIndent = function() { if (format) { out.write(indent.join(' ')); } return this; }; this.indent = function() { indent.push(''); return this; }; this.unindent = function() { indent.pop(); return this; }; } /** * A writer for meta-model backed document trees * * @param {Object} options output options to pass into the writer */ function Writer(options) { options = assign({ format: false, preamble: true }, options || {}); function toXML(tree, writer) { var internalWriter = writer || new SavingWriter(); var formatingWriter = new FormatingWriter(internalWriter, options.format); if (options.preamble) { formatingWriter.append(XML_PREAMBLE); } new ElementSerializer().build(tree).serializeTo(formatingWriter); if (!writer) { return internalWriter.value; } } return { toXML: toXML }; } /** * A sub class of {@link Moddle} with support for import and export of BPMN 2.0 xml files. * * @class BpmnModdle * @extends Moddle * * @param {Object|Array} packages to use for instantiating the model * @param {Object} [options] additional options to pass over */ function BpmnModdle(packages, options) { Moddle.call(this, packages, options); } BpmnModdle.prototype = Object.create(Moddle.prototype); /** * The fromXML result. * * @typedef {Object} ParseResult * * @property {ModdleElement} rootElement * @property {Array} references * @property {Array} warnings * @property {Object} elementsById - a mapping containing each ID -> ModdleElement */ /** * The fromXML error. * * @typedef {Error} ParseError * * @property {Array} warnings */ /** * Instantiates a BPMN model tree from a given xml string. * * @param {String} xmlStr * @param {String} [typeName='bpmn:Definitions'] name of the root element * @param {Object} [options] options to pass to the underlying reader * * @returns {Promise} */ BpmnModdle.prototype.fromXML = function(xmlStr, typeName, options) { if (!isString(typeName)) { options = typeName; typeName = 'bpmn:Definitions'; } var reader = new Reader(assign({ model: this, lax: true }, options)); var rootHandler = reader.handler(typeName); return reader.fromXML(xmlStr, rootHandler); }; /** * The toXML result. * * @typedef {Object} SerializationResult * * @property {String} xml */ /** * Serializes a BPMN 2.0 object tree to XML. * * @param {String} element the root element, typically an instance of `bpmn:Definitions` * @param {Object} [options] to pass to the underlying writer * * @returns {Promise} */ BpmnModdle.prototype.toXML = function(element, options) { var writer = new Writer(options); return new Promise(function(resolve, reject) { try { var result = writer.toXML(element); return resolve({ xml: result }); } catch (err) { return reject(err); } }); }; var name$5 = "BPMN20"; var uri$5 = "http://www.omg.org/spec/BPMN/20100524/MODEL"; var prefix$5 = "bpmn"; var associations$5 = [ ]; var types$5 = [ { name: "Interface", superClass: [ "RootElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "operations", type: "Operation", isMany: true }, { name: "implementationRef", isAttr: true, type: "String" } ] }, { name: "Operation", superClass: [ "BaseElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "inMessageRef", type: "Message", isReference: true }, { name: "outMessageRef", type: "Message", isReference: true }, { name: "errorRef", type: "Error", isMany: true, isReference: true }, { name: "implementationRef", isAttr: true, type: "String" } ] }, { name: "EndPoint", superClass: [ "RootElement" ] }, { name: "Auditing", superClass: [ "BaseElement" ] }, { name: "GlobalTask", superClass: [ "CallableElement" ], properties: [ { name: "resources", type: "ResourceRole", isMany: true } ] }, { name: "Monitoring", superClass: [ "BaseElement" ] }, { name: "Performer", superClass: [ "ResourceRole" ] }, { name: "Process", superClass: [ "FlowElementsContainer", "CallableElement" ], properties: [ { name: "processType", type: "ProcessType", isAttr: true }, { name: "isClosed", isAttr: true, type: "Boolean" }, { name: "auditing", type: "Auditing" }, { name: "monitoring", type: "Monitoring" }, { name: "properties", type: "Property", isMany: true }, { name: "laneSets", isMany: true, replaces: "FlowElementsContainer#laneSets", type: "LaneSet" }, { name: "flowElements", isMany: true, replaces: "FlowElementsContainer#flowElements", type: "FlowElement" }, { name: "artifacts", type: "Artifact", isMany: true }, { name: "resources", type: "ResourceRole", isMany: true }, { name: "correlationSubscriptions", type: "CorrelationSubscription", isMany: true }, { name: "supports", type: "Process", isMany: true, isReference: true }, { name: "definitionalCollaborationRef", type: "Collaboration", isAttr: true, isReference: true }, { name: "isExecutable", isAttr: true, type: "Boolean" } ] }, { name: "LaneSet", superClass: [ "BaseElement" ], properties: [ { name: "lanes", type: "Lane", isMany: true }, { name: "name", isAttr: true, type: "String" } ] }, { name: "Lane", superClass: [ "BaseElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "partitionElementRef", type: "BaseElement", isAttr: true, isReference: true }, { name: "partitionElement", type: "BaseElement" }, { name: "flowNodeRef", type: "FlowNode", isMany: true, isReference: true }, { name: "childLaneSet", type: "LaneSet", xml: { serialize: "xsi:type" } } ] }, { name: "GlobalManualTask", superClass: [ "GlobalTask" ] }, { name: "ManualTask", superClass: [ "Task" ] }, { name: "UserTask", superClass: [ "Task" ], properties: [ { name: "renderings", type: "Rendering", isMany: true }, { name: "implementation", isAttr: true, type: "String" } ] }, { name: "Rendering", superClass: [ "BaseElement" ] }, { name: "HumanPerformer", superClass: [ "Performer" ] }, { name: "PotentialOwner", superClass: [ "HumanPerformer" ] }, { name: "GlobalUserTask", superClass: [ "GlobalTask" ], properties: [ { name: "implementation", isAttr: true, type: "String" }, { name: "renderings", type: "Rendering", isMany: true } ] }, { name: "Gateway", isAbstract: true, superClass: [ "FlowNode" ], properties: [ { name: "gatewayDirection", type: "GatewayDirection", "default": "Unspecified", isAttr: true } ] }, { name: "EventBasedGateway", superClass: [ "Gateway" ], properties: [ { name: "instantiate", "default": false, isAttr: true, type: "Boolean" }, { name: "eventGatewayType", type: "EventBasedGatewayType", isAttr: true, "default": "Exclusive" } ] }, { name: "ComplexGateway", superClass: [ "Gateway" ], properties: [ { name: "activationCondition", type: "Expression", xml: { serialize: "xsi:type" } }, { name: "default", type: "SequenceFlow", isAttr: true, isReference: true } ] }, { name: "ExclusiveGateway", superClass: [ "Gateway" ], properties: [ { name: "default", type: "SequenceFlow", isAttr: true, isReference: true } ] }, { name: "InclusiveGateway", superClass: [ "Gateway" ], properties: [ { name: "default", type: "SequenceFlow", isAttr: true, isReference: true } ] }, { name: "ParallelGateway", superClass: [ "Gateway" ] }, { name: "RootElement", isAbstract: true, superClass: [ "BaseElement" ] }, { name: "Relationship", superClass: [ "BaseElement" ], properties: [ { name: "type", isAttr: true, type: "String" }, { name: "direction", type: "RelationshipDirection", isAttr: true }, { name: "source", isMany: true, isReference: true, type: "Element" }, { name: "target", isMany: true, isReference: true, type: "Element" } ] }, { name: "BaseElement", isAbstract: true, properties: [ { name: "id", isAttr: true, type: "String", isId: true }, { name: "documentation", type: "Documentation", isMany: true }, { name: "extensionDefinitions", type: "ExtensionDefinition", isMany: true, isReference: true }, { name: "extensionElements", type: "ExtensionElements" } ] }, { name: "Extension", properties: [ { name: "mustUnderstand", "default": false, isAttr: true, type: "Boolean" }, { name: "definition", type: "ExtensionDefinition", isAttr: true, isReference: true } ] }, { name: "ExtensionDefinition", properties: [ { name: "name", isAttr: true, type: "String" }, { name: "extensionAttributeDefinitions", type: "ExtensionAttributeDefinition", isMany: true } ] }, { name: "ExtensionAttributeDefinition", properties: [ { name: "name", isAttr: true, type: "String" }, { name: "type", isAttr: true, type: "String" }, { name: "isReference", "default": false, isAttr: true, type: "Boolean" }, { name: "extensionDefinition", type: "ExtensionDefinition", isAttr: true, isReference: true } ] }, { name: "ExtensionElements", properties: [ { name: "valueRef", isAttr: true, isReference: true, type: "Element" }, { name: "values", type: "Element", isMany: true }, { name: "extensionAttributeDefinition", type: "ExtensionAttributeDefinition", isAttr: true, isReference: true } ] }, { name: "Documentation", superClass: [ "BaseElement" ], properties: [ { name: "text", type: "String", isBody: true }, { name: "textFormat", "default": "text/plain", isAttr: true, type: "String" } ] }, { name: "Event", isAbstract: true, superClass: [ "FlowNode", "InteractionNode" ], properties: [ { name: "properties", type: "Property", isMany: true } ] }, { name: "IntermediateCatchEvent", superClass: [ "CatchEvent" ] }, { name: "IntermediateThrowEvent", superClass: [ "ThrowEvent" ] }, { name: "EndEvent", superClass: [ "ThrowEvent" ] }, { name: "StartEvent", superClass: [ "CatchEvent" ], properties: [ { name: "isInterrupting", "default": true, isAttr: true, type: "Boolean" } ] }, { name: "ThrowEvent", isAbstract: true, superClass: [ "Event" ], properties: [ { name: "dataInputs", type: "DataInput", isMany: true }, { name: "dataInputAssociations", type: "DataInputAssociation", isMany: true }, { name: "inputSet", type: "InputSet" }, { name: "eventDefinitions", type: "EventDefinition", isMany: true }, { name: "eventDefinitionRef", type: "EventDefinition", isMany: true, isReference: true } ] }, { name: "CatchEvent", isAbstract: true, superClass: [ "Event" ], properties: [ { name: "parallelMultiple", isAttr: true, type: "Boolean", "default": false }, { name: "dataOutputs", type: "DataOutput", isMany: true }, { name: "dataOutputAssociations", type: "DataOutputAssociation", isMany: true }, { name: "outputSet", type: "OutputSet" }, { name: "eventDefinitions", type: "EventDefinition", isMany: true }, { name: "eventDefinitionRef", type: "EventDefinition", isMany: true, isReference: true } ] }, { name: "BoundaryEvent", superClass: [ "CatchEvent" ], properties: [ { name: "cancelActivity", "default": true, isAttr: true, type: "Boolean" }, { name: "attachedToRef", type: "Activity", isAttr: true, isReference: true } ] }, { name: "EventDefinition", isAbstract: true, superClass: [ "RootElement" ] }, { name: "CancelEventDefinition", superClass: [ "EventDefinition" ] }, { name: "ErrorEventDefinition", superClass: [ "EventDefinition" ], properties: [ { name: "errorRef", type: "Error", isAttr: true, isReference: true } ] }, { name: "TerminateEventDefinition", superClass: [ "EventDefinition" ] }, { name: "EscalationEventDefinition", superClass: [ "EventDefinition" ], properties: [ { name: "escalationRef", type: "Escalation", isAttr: true, isReference: true } ] }, { name: "Escalation", properties: [ { name: "structureRef", type: "ItemDefinition", isAttr: true, isReference: true }, { name: "name", isAttr: true, type: "String" }, { name: "escalationCode", isAttr: true, type: "String" } ], superClass: [ "RootElement" ] }, { name: "CompensateEventDefinition", superClass: [ "EventDefinition" ], properties: [ { name: "waitForCompletion", isAttr: true, type: "Boolean", "default": true }, { name: "activityRef", type: "Activity", isAttr: true, isReference: true } ] }, { name: "TimerEventDefinition", superClass: [ "EventDefinition" ], properties: [ { name: "timeDate", type: "Expression", xml: { serialize: "xsi:type" } }, { name: "timeCycle", type: "Expression", xml: { serialize: "xsi:type" } }, { name: "timeDuration", type: "Expression", xml: { serialize: "xsi:type" } } ] }, { name: "LinkEventDefinition", superClass: [ "EventDefinition" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "target", type: "LinkEventDefinition", isAttr: true, isReference: true }, { name: "source", type: "LinkEventDefinition", isMany: true, isReference: true } ] }, { name: "MessageEventDefinition", superClass: [ "EventDefinition" ], properties: [ { name: "messageRef", type: "Message", isAttr: true, isReference: true }, { name: "operationRef", type: "Operation", isAttr: true, isReference: true } ] }, { name: "ConditionalEventDefinition", superClass: [ "EventDefinition" ], properties: [ { name: "condition", type: "Expression", xml: { serialize: "xsi:type" } } ] }, { name: "SignalEventDefinition", superClass: [ "EventDefinition" ], properties: [ { name: "signalRef", type: "Signal", isAttr: true, isReference: true } ] }, { name: "Signal", superClass: [ "RootElement" ], properties: [ { name: "structureRef", type: "ItemDefinition", isAttr: true, isReference: true }, { name: "name", isAttr: true, type: "String" } ] }, { name: "ImplicitThrowEvent", superClass: [ "ThrowEvent" ] }, { name: "DataState", superClass: [ "BaseElement" ], properties: [ { name: "name", isAttr: true, type: "String" } ] }, { name: "ItemAwareElement", superClass: [ "BaseElement" ], properties: [ { name: "itemSubjectRef", type: "ItemDefinition", isAttr: true, isReference: true }, { name: "dataState", type: "DataState" } ] }, { name: "DataAssociation", superClass: [ "BaseElement" ], properties: [ { name: "sourceRef", type: "ItemAwareElement", isMany: true, isReference: true }, { name: "targetRef", type: "ItemAwareElement", isReference: true }, { name: "transformation", type: "FormalExpression", xml: { serialize: "property" } }, { name: "assignment", type: "Assignment", isMany: true } ] }, { name: "DataInput", superClass: [ "ItemAwareElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "isCollection", "default": false, isAttr: true, type: "Boolean" }, { name: "inputSetRef", type: "InputSet", isMany: true, isVirtual: true, isReference: true }, { name: "inputSetWithOptional", type: "InputSet", isMany: true, isVirtual: true, isReference: true }, { name: "inputSetWithWhileExecuting", type: "InputSet", isMany: true, isVirtual: true, isReference: true } ] }, { name: "DataOutput", superClass: [ "ItemAwareElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "isCollection", "default": false, isAttr: true, type: "Boolean" }, { name: "outputSetRef", type: "OutputSet", isMany: true, isVirtual: true, isReference: true }, { name: "outputSetWithOptional", type: "OutputSet", isMany: true, isVirtual: true, isReference: true }, { name: "outputSetWithWhileExecuting", type: "OutputSet", isMany: true, isVirtual: true, isReference: true } ] }, { name: "InputSet", superClass: [ "BaseElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "dataInputRefs", type: "DataInput", isMany: true, isReference: true }, { name: "optionalInputRefs", type: "DataInput", isMany: true, isReference: true }, { name: "whileExecutingInputRefs", type: "DataInput", isMany: true, isReference: true }, { name: "outputSetRefs", type: "OutputSet", isMany: true, isReference: true } ] }, { name: "OutputSet", superClass: [ "BaseElement" ], properties: [ { name: "dataOutputRefs", type: "DataOutput", isMany: true, isReference: true }, { name: "name", isAttr: true, type: "String" }, { name: "inputSetRefs", type: "InputSet", isMany: true, isReference: true }, { name: "optionalOutputRefs", type: "DataOutput", isMany: true, isReference: true }, { name: "whileExecutingOutputRefs", type: "DataOutput", isMany: true, isReference: true } ] }, { name: "Property", superClass: [ "ItemAwareElement" ], properties: [ { name: "name", isAttr: true, type: "String" } ] }, { name: "DataInputAssociation", superClass: [ "DataAssociation" ] }, { name: "DataOutputAssociation", superClass: [ "DataAssociation" ] }, { name: "InputOutputSpecification", superClass: [ "BaseElement" ], properties: [ { name: "dataInputs", type: "DataInput", isMany: true }, { name: "dataOutputs", type: "DataOutput", isMany: true }, { name: "inputSets", type: "InputSet", isMany: true }, { name: "outputSets", type: "OutputSet", isMany: true } ] }, { name: "DataObject", superClass: [ "FlowElement", "ItemAwareElement" ], properties: [ { name: "isCollection", "default": false, isAttr: true, type: "Boolean" } ] }, { name: "InputOutputBinding", properties: [ { name: "inputDataRef", type: "InputSet", isAttr: true, isReference: true }, { name: "outputDataRef", type: "OutputSet", isAttr: true, isReference: true }, { name: "operationRef", type: "Operation", isAttr: true, isReference: true } ] }, { name: "Assignment", superClass: [ "BaseElement" ], properties: [ { name: "from", type: "Expression", xml: { serialize: "xsi:type" } }, { name: "to", type: "Expression", xml: { serialize: "xsi:type" } } ] }, { name: "DataStore", superClass: [ "RootElement", "ItemAwareElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "capacity", isAttr: true, type: "Integer" }, { name: "isUnlimited", "default": true, isAttr: true, type: "Boolean" } ] }, { name: "DataStoreReference", superClass: [ "ItemAwareElement", "FlowElement" ], properties: [ { name: "dataStoreRef", type: "DataStore", isAttr: true, isReference: true } ] }, { name: "DataObjectReference", superClass: [ "ItemAwareElement", "FlowElement" ], properties: [ { name: "dataObjectRef", type: "DataObject", isAttr: true, isReference: true } ] }, { name: "ConversationLink", superClass: [ "BaseElement" ], properties: [ { name: "sourceRef", type: "InteractionNode", isAttr: true, isReference: true }, { name: "targetRef", type: "InteractionNode", isAttr: true, isReference: true }, { name: "name", isAttr: true, type: "String" } ] }, { name: "ConversationAssociation", superClass: [ "BaseElement" ], properties: [ { name: "innerConversationNodeRef", type: "ConversationNode", isAttr: true, isReference: true }, { name: "outerConversationNodeRef", type: "ConversationNode", isAttr: true, isReference: true } ] }, { name: "CallConversation", superClass: [ "ConversationNode" ], properties: [ { name: "calledCollaborationRef", type: "Collaboration", isAttr: true, isReference: true }, { name: "participantAssociations", type: "ParticipantAssociation", isMany: true } ] }, { name: "Conversation", superClass: [ "ConversationNode" ] }, { name: "SubConversation", superClass: [ "ConversationNode" ], properties: [ { name: "conversationNodes", type: "ConversationNode", isMany: true } ] }, { name: "ConversationNode", isAbstract: true, superClass: [ "InteractionNode", "BaseElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "participantRef", type: "Participant", isMany: true, isReference: true }, { name: "messageFlowRefs", type: "MessageFlow", isMany: true, isReference: true }, { name: "correlationKeys", type: "CorrelationKey", isMany: true } ] }, { name: "GlobalConversation", superClass: [ "Collaboration" ] }, { name: "PartnerEntity", superClass: [ "RootElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "participantRef", type: "Participant", isMany: true, isReference: true } ] }, { name: "PartnerRole", superClass: [ "RootElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "participantRef", type: "Participant", isMany: true, isReference: true } ] }, { name: "CorrelationProperty", superClass: [ "RootElement" ], properties: [ { name: "correlationPropertyRetrievalExpression", type: "CorrelationPropertyRetrievalExpression", isMany: true }, { name: "name", isAttr: true, type: "String" }, { name: "type", type: "ItemDefinition", isAttr: true, isReference: true } ] }, { name: "Error", superClass: [ "RootElement" ], properties: [ { name: "structureRef", type: "ItemDefinition", isAttr: true, isReference: true }, { name: "name", isAttr: true, type: "String" }, { name: "errorCode", isAttr: true, type: "String" } ] }, { name: "CorrelationKey", superClass: [ "BaseElement" ], properties: [ { name: "correlationPropertyRef", type: "CorrelationProperty", isMany: true, isReference: true }, { name: "name", isAttr: true, type: "String" } ] }, { name: "Expression", superClass: [ "BaseElement" ], isAbstract: false, properties: [ { name: "body", isBody: true, type: "String" } ] }, { name: "FormalExpression", superClass: [ "Expression" ], properties: [ { name: "language", isAttr: true, type: "String" }, { name: "evaluatesToTypeRef", type: "ItemDefinition", isAttr: true, isReference: true } ] }, { name: "Message", superClass: [ "RootElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "itemRef", type: "ItemDefinition", isAttr: true, isReference: true } ] }, { name: "ItemDefinition", superClass: [ "RootElement" ], properties: [ { name: "itemKind", type: "ItemKind", isAttr: true }, { name: "structureRef", isAttr: true, type: "String" }, { name: "isCollection", "default": false, isAttr: true, type: "Boolean" }, { name: "import", type: "Import", isAttr: true, isReference: true } ] }, { name: "FlowElement", isAbstract: true, superClass: [ "BaseElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "auditing", type: "Auditing" }, { name: "monitoring", type: "Monitoring" }, { name: "categoryValueRef", type: "CategoryValue", isMany: true, isReference: true } ] }, { name: "SequenceFlow", superClass: [ "FlowElement" ], properties: [ { name: "isImmediate", isAttr: true, type: "Boolean" }, { name: "conditionExpression", type: "Expression", xml: { serialize: "xsi:type" } }, { name: "sourceRef", type: "FlowNode", isAttr: true, isReference: true }, { name: "targetRef", type: "FlowNode", isAttr: true, isReference: true } ] }, { name: "FlowElementsContainer", isAbstract: true, superClass: [ "BaseElement" ], properties: [ { name: "laneSets", type: "LaneSet", isMany: true }, { name: "flowElements", type: "FlowElement", isMany: true } ] }, { name: "CallableElement", isAbstract: true, superClass: [ "RootElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "ioSpecification", type: "InputOutputSpecification", xml: { serialize: "property" } }, { name: "supportedInterfaceRef", type: "Interface", isMany: true, isReference: true }, { name: "ioBinding", type: "InputOutputBinding", isMany: true, xml: { serialize: "property" } } ] }, { name: "FlowNode", isAbstract: true, superClass: [ "FlowElement" ], properties: [ { name: "incoming", type: "SequenceFlow", isMany: true, isReference: true }, { name: "outgoing", type: "SequenceFlow", isMany: true, isReference: true }, { name: "lanes", type: "Lane", isMany: true, isVirtual: true, isReference: true } ] }, { name: "CorrelationPropertyRetrievalExpression", superClass: [ "BaseElement" ], properties: [ { name: "messagePath", type: "FormalExpression" }, { name: "messageRef", type: "Message", isAttr: true, isReference: true } ] }, { name: "CorrelationPropertyBinding", superClass: [ "BaseElement" ], properties: [ { name: "dataPath", type: "FormalExpression" }, { name: "correlationPropertyRef", type: "CorrelationProperty", isAttr: true, isReference: true } ] }, { name: "Resource", superClass: [ "RootElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "resourceParameters", type: "ResourceParameter", isMany: true } ] }, { name: "ResourceParameter", superClass: [ "BaseElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "isRequired", isAttr: true, type: "Boolean" }, { name: "type", type: "ItemDefinition", isAttr: true, isReference: true } ] }, { name: "CorrelationSubscription", superClass: [ "BaseElement" ], properties: [ { name: "correlationKeyRef", type: "CorrelationKey", isAttr: true, isReference: true }, { name: "correlationPropertyBinding", type: "CorrelationPropertyBinding", isMany: true } ] }, { name: "MessageFlow", superClass: [ "BaseElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "sourceRef", type: "InteractionNode", isAttr: true, isReference: true }, { name: "targetRef", type: "InteractionNode", isAttr: true, isReference: true }, { name: "messageRef", type: "Message", isAttr: true, isReference: true } ] }, { name: "MessageFlowAssociation", superClass: [ "BaseElement" ], properties: [ { name: "innerMessageFlowRef", type: "MessageFlow", isAttr: true, isReference: true }, { name: "outerMessageFlowRef", type: "MessageFlow", isAttr: true, isReference: true } ] }, { name: "InteractionNode", isAbstract: true, properties: [ { name: "incomingConversationLinks", type: "ConversationLink", isMany: true, isVirtual: true, isReference: true }, { name: "outgoingConversationLinks", type: "ConversationLink", isMany: true, isVirtual: true, isReference: true } ] }, { name: "Participant", superClass: [ "InteractionNode", "BaseElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "interfaceRef", type: "Interface", isMany: true, isReference: true }, { name: "participantMultiplicity", type: "ParticipantMultiplicity" }, { name: "endPointRefs", type: "EndPoint", isMany: true, isReference: true }, { name: "processRef", type: "Process", isAttr: true, isReference: true } ] }, { name: "ParticipantAssociation", superClass: [ "BaseElement" ], properties: [ { name: "innerParticipantRef", type: "Participant", isAttr: true, isReference: true }, { name: "outerParticipantRef", type: "Participant", isAttr: true, isReference: true } ] }, { name: "ParticipantMultiplicity", properties: [ { name: "minimum", "default": 0, isAttr: true, type: "Integer" }, { name: "maximum", "default": 1, isAttr: true, type: "Integer" } ], superClass: [ "BaseElement" ] }, { name: "Collaboration", superClass: [ "RootElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "isClosed", isAttr: true, type: "Boolean" }, { name: "participants", type: "Participant", isMany: true }, { name: "messageFlows", type: "MessageFlow", isMany: true }, { name: "artifacts", type: "Artifact", isMany: true }, { name: "conversations", type: "ConversationNode", isMany: true }, { name: "conversationAssociations", type: "ConversationAssociation" }, { name: "participantAssociations", type: "ParticipantAssociation", isMany: true }, { name: "messageFlowAssociations", type: "MessageFlowAssociation", isMany: true }, { name: "correlationKeys", type: "CorrelationKey", isMany: true }, { name: "choreographyRef", type: "Choreography", isMany: true, isReference: true }, { name: "conversationLinks", type: "ConversationLink", isMany: true } ] }, { name: "ChoreographyActivity", isAbstract: true, superClass: [ "FlowNode" ], properties: [ { name: "participantRef", type: "Participant", isMany: true, isReference: true }, { name: "initiatingParticipantRef", type: "Participant", isAttr: true, isReference: true }, { name: "correlationKeys", type: "CorrelationKey", isMany: true }, { name: "loopType", type: "ChoreographyLoopType", "default": "None", isAttr: true } ] }, { name: "CallChoreography", superClass: [ "ChoreographyActivity" ], properties: [ { name: "calledChoreographyRef", type: "Choreography", isAttr: true, isReference: true }, { name: "participantAssociations", type: "ParticipantAssociation", isMany: true } ] }, { name: "SubChoreography", superClass: [ "ChoreographyActivity", "FlowElementsContainer" ], properties: [ { name: "artifacts", type: "Artifact", isMany: true } ] }, { name: "ChoreographyTask", superClass: [ "ChoreographyActivity" ], properties: [ { name: "messageFlowRef", type: "MessageFlow", isMany: true, isReference: true } ] }, { name: "Choreography", superClass: [ "Collaboration", "FlowElementsContainer" ] }, { name: "GlobalChoreographyTask", superClass: [ "Choreography" ], properties: [ { name: "initiatingParticipantRef", type: "Participant", isAttr: true, isReference: true } ] }, { name: "TextAnnotation", superClass: [ "Artifact" ], properties: [ { name: "text", type: "String" }, { name: "textFormat", "default": "text/plain", isAttr: true, type: "String" } ] }, { name: "Group", superClass: [ "Artifact" ], properties: [ { name: "categoryValueRef", type: "CategoryValue", isAttr: true, isReference: true } ] }, { name: "Association", superClass: [ "Artifact" ], properties: [ { name: "associationDirection", type: "AssociationDirection", isAttr: true }, { name: "sourceRef", type: "BaseElement", isAttr: true, isReference: true }, { name: "targetRef", type: "BaseElement", isAttr: true, isReference: true } ] }, { name: "Category", superClass: [ "RootElement" ], properties: [ { name: "categoryValue", type: "CategoryValue", isMany: true }, { name: "name", isAttr: true, type: "String" } ] }, { name: "Artifact", isAbstract: true, superClass: [ "BaseElement" ] }, { name: "CategoryValue", superClass: [ "BaseElement" ], properties: [ { name: "categorizedFlowElements", type: "FlowElement", isMany: true, isVirtual: true, isReference: true }, { name: "value", isAttr: true, type: "String" } ] }, { name: "Activity", isAbstract: true, superClass: [ "FlowNode" ], properties: [ { name: "isForCompensation", "default": false, isAttr: true, type: "Boolean" }, { name: "default", type: "SequenceFlow", isAttr: true, isReference: true }, { name: "ioSpecification", type: "InputOutputSpecification", xml: { serialize: "property" } }, { name: "boundaryEventRefs", type: "BoundaryEvent", isMany: true, isReference: true }, { name: "properties", type: "Property", isMany: true }, { name: "dataInputAssociations", type: "DataInputAssociation", isMany: true }, { name: "dataOutputAssociations", type: "DataOutputAssociation", isMany: true }, { name: "startQuantity", "default": 1, isAttr: true, type: "Integer" }, { name: "resources", type: "ResourceRole", isMany: true }, { name: "completionQuantity", "default": 1, isAttr: true, type: "Integer" }, { name: "loopCharacteristics", type: "LoopCharacteristics" } ] }, { name: "ServiceTask", superClass: [ "Task" ], properties: [ { name: "implementation", isAttr: true, type: "String" }, { name: "operationRef", type: "Operation", isAttr: true, isReference: true } ] }, { name: "SubProcess", superClass: [ "Activity", "FlowElementsContainer", "InteractionNode" ], properties: [ { name: "triggeredByEvent", "default": false, isAttr: true, type: "Boolean" }, { name: "artifacts", type: "Artifact", isMany: true } ] }, { name: "LoopCharacteristics", isAbstract: true, superClass: [ "BaseElement" ] }, { name: "MultiInstanceLoopCharacteristics", superClass: [ "LoopCharacteristics" ], properties: [ { name: "isSequential", "default": false, isAttr: true, type: "Boolean" }, { name: "behavior", type: "MultiInstanceBehavior", "default": "All", isAttr: true }, { name: "loopCardinality", type: "Expression", xml: { serialize: "xsi:type" } }, { name: "loopDataInputRef", type: "ItemAwareElement", isReference: true }, { name: "loopDataOutputRef", type: "ItemAwareElement", isReference: true }, { name: "inputDataItem", type: "DataInput", xml: { serialize: "property" } }, { name: "outputDataItem", type: "DataOutput", xml: { serialize: "property" } }, { name: "complexBehaviorDefinition", type: "ComplexBehaviorDefinition", isMany: true }, { name: "completionCondition", type: "Expression", xml: { serialize: "xsi:type" } }, { name: "oneBehaviorEventRef", type: "EventDefinition", isAttr: true, isReference: true }, { name: "noneBehaviorEventRef", type: "EventDefinition", isAttr: true, isReference: true } ] }, { name: "StandardLoopCharacteristics", superClass: [ "LoopCharacteristics" ], properties: [ { name: "testBefore", "default": false, isAttr: true, type: "Boolean" }, { name: "loopCondition", type: "Expression", xml: { serialize: "xsi:type" } }, { name: "loopMaximum", type: "Integer", isAttr: true } ] }, { name: "CallActivity", superClass: [ "Activity", "InteractionNode" ], properties: [ { name: "calledElement", type: "String", isAttr: true } ] }, { name: "Task", superClass: [ "Activity", "InteractionNode" ] }, { name: "SendTask", superClass: [ "Task" ], properties: [ { name: "implementation", isAttr: true, type: "String" }, { name: "operationRef", type: "Operation", isAttr: true, isReference: true }, { name: "messageRef", type: "Message", isAttr: true, isReference: true } ] }, { name: "ReceiveTask", superClass: [ "Task" ], properties: [ { name: "implementation", isAttr: true, type: "String" }, { name: "instantiate", "default": false, isAttr: true, type: "Boolean" }, { name: "operationRef", type: "Operation", isAttr: true, isReference: true }, { name: "messageRef", type: "Message", isAttr: true, isReference: true } ] }, { name: "ScriptTask", superClass: [ "Task" ], properties: [ { name: "scriptFormat", isAttr: true, type: "String" }, { name: "script", type: "String" } ] }, { name: "BusinessRuleTask", superClass: [ "Task" ], properties: [ { name: "implementation", isAttr: true, type: "String" } ] }, { name: "AdHocSubProcess", superClass: [ "SubProcess" ], properties: [ { name: "completionCondition", type: "Expression", xml: { serialize: "xsi:type" } }, { name: "ordering", type: "AdHocOrdering", isAttr: true }, { name: "cancelRemainingInstances", "default": true, isAttr: true, type: "Boolean" } ] }, { name: "Transaction", superClass: [ "SubProcess" ], properties: [ { name: "protocol", isAttr: true, type: "String" }, { name: "method", isAttr: true, type: "String" } ] }, { name: "GlobalScriptTask", superClass: [ "GlobalTask" ], properties: [ { name: "scriptLanguage", isAttr: true, type: "String" }, { name: "script", isAttr: true, type: "String" } ] }, { name: "GlobalBusinessRuleTask", superClass: [ "GlobalTask" ], properties: [ { name: "implementation", isAttr: true, type: "String" } ] }, { name: "ComplexBehaviorDefinition", superClass: [ "BaseElement" ], properties: [ { name: "condition", type: "FormalExpression" }, { name: "event", type: "ImplicitThrowEvent" } ] }, { name: "ResourceRole", superClass: [ "BaseElement" ], properties: [ { name: "resourceRef", type: "Resource", isReference: true }, { name: "resourceParameterBindings", type: "ResourceParameterBinding", isMany: true }, { name: "resourceAssignmentExpression", type: "ResourceAssignmentExpression" }, { name: "name", isAttr: true, type: "String" } ] }, { name: "ResourceParameterBinding", properties: [ { name: "expression", type: "Expression", xml: { serialize: "xsi:type" } }, { name: "parameterRef", type: "ResourceParameter", isAttr: true, isReference: true } ], superClass: [ "BaseElement" ] }, { name: "ResourceAssignmentExpression", properties: [ { name: "expression", type: "Expression", xml: { serialize: "xsi:type" } } ], superClass: [ "BaseElement" ] }, { name: "Import", properties: [ { name: "importType", isAttr: true, type: "String" }, { name: "location", isAttr: true, type: "String" }, { name: "namespace", isAttr: true, type: "String" } ] }, { name: "Definitions", superClass: [ "BaseElement" ], properties: [ { name: "name", isAttr: true, type: "String" }, { name: "targetNamespace", isAttr: true, type: "String" }, { name: "expressionLanguage", "default": "http://www.w3.org/1999/XPath", isAttr: true, type: "String" }, { name: "typeLanguage", "default": "http://www.w3.org/2001/XMLSchema", isAttr: true, type: "String" }, { name: "imports", type: "Import", isMany: true }, { name: "extensions", type: "Extension", isMany: true }, { name: "rootElements", type: "RootElement", isMany: true }, { name: "diagrams", isMany: true, type: "bpmndi:BPMNDiagram" }, { name: "exporter", isAttr: true, type: "String" }, { name: "relationships", type: "Relationship", isMany: true }, { name: "exporterVersion", isAttr: true, type: "String" } ] } ]; var enumerations$3 = [ { name: "ProcessType", literalValues: [ { name: "None" }, { name: "Public" }, { name: "Private" } ] }, { name: "GatewayDirection", literalValues: [ { name: "Unspecified" }, { name: "Converging" }, { name: "Diverging" }, { name: "Mixed" } ] }, { name: "EventBasedGatewayType", literalValues: [ { name: "Parallel" }, { name: "Exclusive" } ] }, { name: "RelationshipDirection", literalValues: [ { name: "None" }, { name: "Forward" }, { name: "Backward" }, { name: "Both" } ] }, { name: "ItemKind", literalValues: [ { name: "Physical" }, { name: "Information" } ] }, { name: "ChoreographyLoopType", literalValues: [ { name: "None" }, { name: "Standard" }, { name: "MultiInstanceSequential" }, { name: "MultiInstanceParallel" } ] }, { name: "AssociationDirection", literalValues: [ { name: "None" }, { name: "One" }, { name: "Both" } ] }, { name: "MultiInstanceBehavior", literalValues: [ { name: "None" }, { name: "One" }, { name: "All" }, { name: "Complex" } ] }, { name: "AdHocOrdering", literalValues: [ { name: "Parallel" }, { name: "Sequential" } ] } ]; var xml$1 = { tagAlias: "lowerCase", typePrefix: "t" }; var BpmnPackage = { name: name$5, uri: uri$5, prefix: prefix$5, associations: associations$5, types: types$5, enumerations: enumerations$3, xml: xml$1 }; var name$4 = "BPMNDI"; var uri$4 = "http://www.omg.org/spec/BPMN/20100524/DI"; var prefix$4 = "bpmndi"; var types$4 = [ { name: "BPMNDiagram", properties: [ { name: "plane", type: "BPMNPlane", redefines: "di:Diagram#rootElement" }, { name: "labelStyle", type: "BPMNLabelStyle", isMany: true } ], superClass: [ "di:Diagram" ] }, { name: "BPMNPlane", properties: [ { name: "bpmnElement", isAttr: true, isReference: true, type: "bpmn:BaseElement", redefines: "di:DiagramElement#modelElement" } ], superClass: [ "di:Plane" ] }, { name: "BPMNShape", properties: [ { name: "bpmnElement", isAttr: true, isReference: true, type: "bpmn:BaseElement", redefines: "di:DiagramElement#modelElement" }, { name: "isHorizontal", isAttr: true, type: "Boolean" }, { name: "isExpanded", isAttr: true, type: "Boolean" }, { name: "isMarkerVisible", isAttr: true, type: "Boolean" }, { name: "label", type: "BPMNLabel" }, { name: "isMessageVisible", isAttr: true, type: "Boolean" }, { name: "participantBandKind", type: "ParticipantBandKind", isAttr: true }, { name: "choreographyActivityShape", type: "BPMNShape", isAttr: true, isReference: true } ], superClass: [ "di:LabeledShape" ] }, { name: "BPMNEdge", properties: [ { name: "label", type: "BPMNLabel" }, { name: "bpmnElement", isAttr: true, isReference: true, type: "bpmn:BaseElement", redefines: "di:DiagramElement#modelElement" }, { name: "sourceElement", isAttr: true, isReference: true, type: "di:DiagramElement", redefines: "di:Edge#source" }, { name: "targetElement", isAttr: true, isReference: true, type: "di:DiagramElement", redefines: "di:Edge#target" }, { name: "messageVisibleKind", type: "MessageVisibleKind", isAttr: true, "default": "initiating" } ], superClass: [ "di:LabeledEdge" ] }, { name: "BPMNLabel", properties: [ { name: "labelStyle", type: "BPMNLabelStyle", isAttr: true, isReference: true, redefines: "di:DiagramElement#style" } ], superClass: [ "di:Label" ] }, { name: "BPMNLabelStyle", properties: [ { name: "font", type: "dc:Font" } ], superClass: [ "di:Style" ] } ]; var enumerations$2 = [ { name: "ParticipantBandKind", literalValues: [ { name: "top_initiating" }, { name: "middle_initiating" }, { name: "bottom_initiating" }, { name: "top_non_initiating" }, { name: "middle_non_initiating" }, { name: "bottom_non_initiating" } ] }, { name: "MessageVisibleKind", literalValues: [ { name: "initiating" }, { name: "non_initiating" } ] } ]; var associations$4 = [ ]; var BpmnDiPackage = { name: name$4, uri: uri$4, prefix: prefix$4, types: types$4, enumerations: enumerations$2, associations: associations$4 }; var name$3 = "DC"; var uri$3 = "http://www.omg.org/spec/DD/20100524/DC"; var prefix$3 = "dc"; var types$3 = [ { name: "Boolean" }, { name: "Integer" }, { name: "Real" }, { name: "String" }, { name: "Font", properties: [ { name: "name", type: "String", isAttr: true }, { name: "size", type: "Real", isAttr: true }, { name: "isBold", type: "Boolean", isAttr: true }, { name: "isItalic", type: "Boolean", isAttr: true }, { name: "isUnderline", type: "Boolean", isAttr: true }, { name: "isStrikeThrough", type: "Boolean", isAttr: true } ] }, { name: "Point", properties: [ { name: "x", type: "Real", "default": "0", isAttr: true }, { name: "y", type: "Real", "default": "0", isAttr: true } ] }, { name: "Bounds", properties: [ { name: "x", type: "Real", "default": "0", isAttr: true }, { name: "y", type: "Real", "default": "0", isAttr: true }, { name: "width", type: "Real", isAttr: true }, { name: "height", type: "Real", isAttr: true } ] } ]; var associations$3 = [ ]; var DcPackage = { name: name$3, uri: uri$3, prefix: prefix$3, types: types$3, associations: associations$3 }; var name$2 = "DI"; var uri$2 = "http://www.omg.org/spec/DD/20100524/DI"; var prefix$2 = "di"; var types$2 = [ { name: "DiagramElement", isAbstract: true, properties: [ { name: "id", isAttr: true, isId: true, type: "String" }, { name: "extension", type: "Extension" }, { name: "owningDiagram", type: "Diagram", isReadOnly: true, isVirtual: true, isReference: true }, { name: "owningElement", type: "DiagramElement", isReadOnly: true, isVirtual: true, isReference: true }, { name: "modelElement", isReadOnly: true, isVirtual: true, isReference: true, type: "Element" }, { name: "style", type: "Style", isReadOnly: true, isVirtual: true, isReference: true }, { name: "ownedElement", type: "DiagramElement", isReadOnly: true, isMany: true, isVirtual: true } ] }, { name: "Node", isAbstract: true, superClass: [ "DiagramElement" ] }, { name: "Edge", isAbstract: true, superClass: [ "DiagramElement" ], properties: [ { name: "source", type: "DiagramElement", isReadOnly: true, isVirtual: true, isReference: true }, { name: "target", type: "DiagramElement", isReadOnly: true, isVirtual: true, isReference: true }, { name: "waypoint", isUnique: false, isMany: true, type: "dc:Point", xml: { serialize: "xsi:type" } } ] }, { name: "Diagram", isAbstract: true, properties: [ { name: "id", isAttr: true, isId: true, type: "String" }, { name: "rootElement", type: "DiagramElement", isReadOnly: true, isVirtual: true }, { name: "name", isAttr: true, type: "String" }, { name: "documentation", isAttr: true, type: "String" }, { name: "resolution", isAttr: true, type: "Real" }, { name: "ownedStyle", type: "Style", isReadOnly: true, isMany: true, isVirtual: true } ] }, { name: "Shape", isAbstract: true, superClass: [ "Node" ], properties: [ { name: "bounds", type: "dc:Bounds" } ] }, { name: "Plane", isAbstract: true, superClass: [ "Node" ], properties: [ { name: "planeElement", type: "DiagramElement", subsettedProperty: "DiagramElement-ownedElement", isMany: true } ] }, { name: "LabeledEdge", isAbstract: true, superClass: [ "Edge" ], properties: [ { name: "ownedLabel", type: "Label", isReadOnly: true, subsettedProperty: "DiagramElement-ownedElement", isMany: true, isVirtual: true } ] }, { name: "LabeledShape", isAbstract: true, superClass: [ "Shape" ], properties: [ { name: "ownedLabel", type: "Label", isReadOnly: true, subsettedProperty: "DiagramElement-ownedElement", isMany: true, isVirtual: true } ] }, { name: "Label", isAbstract: true, superClass: [ "Node" ], properties: [ { name: "bounds", type: "dc:Bounds" } ] }, { name: "Style", isAbstract: true, properties: [ { name: "id", isAttr: true, isId: true, type: "String" } ] }, { name: "Extension", properties: [ { name: "values", isMany: true, type: "Element" } ] } ]; var associations$2 = [ ]; var xml = { tagAlias: "lowerCase" }; var DiPackage = { name: name$2, uri: uri$2, prefix: prefix$2, types: types$2, associations: associations$2, xml: xml }; var name$1 = "bpmn.io colors for BPMN"; var uri$1 = "http://bpmn.io/schema/bpmn/biocolor/1.0"; var prefix$1 = "bioc"; var types$1 = [ { name: "ColoredShape", "extends": [ "bpmndi:BPMNShape" ], properties: [ { name: "stroke", isAttr: true, type: "String" }, { name: "fill", isAttr: true, type: "String" } ] }, { name: "ColoredEdge", "extends": [ "bpmndi:BPMNEdge" ], properties: [ { name: "stroke", isAttr: true, type: "String" }, { name: "fill", isAttr: true, type: "String" } ] } ]; var enumerations$1 = [ ]; var associations$1 = [ ]; var BiocPackage = { name: name$1, uri: uri$1, prefix: prefix$1, types: types$1, enumerations: enumerations$1, associations: associations$1 }; var name = "BPMN in Color"; var uri = "http://www.omg.org/spec/BPMN/non-normative/color/1.0"; var prefix = "color"; var types = [ { name: "ColoredLabel", "extends": [ "bpmndi:BPMNLabel" ], properties: [ { name: "color", isAttr: true, type: "String" } ] }, { name: "ColoredShape", "extends": [ "bpmndi:BPMNShape" ], properties: [ { name: "background-color", isAttr: true, type: "String" }, { name: "border-color", isAttr: true, type: "String" } ] }, { name: "ColoredEdge", "extends": [ "bpmndi:BPMNEdge" ], properties: [ { name: "border-color", isAttr: true, type: "String" } ] } ]; var enumerations = [ ]; var associations = [ ]; var BpmnInColorPackage = { name: name, uri: uri, prefix: prefix, types: types, enumerations: enumerations, associations: associations }; var packages = { bpmn: BpmnPackage, bpmndi: BpmnDiPackage, dc: DcPackage, di: DiPackage, bioc: BiocPackage, color: BpmnInColorPackage }; function simple(additionalPackages, options) { var pks = assign({}, packages, additionalPackages); return new BpmnModdle(pks, options); } function elementToString(e) { if (!e) { return ''; } return '<' + e.$type + (e.id ? ' id="' + e.id : '') + '" />'; } // TODO(nikku): remove with future bpmn-js version /** * Wraps APIs to check: * * 1) If a callback is passed -> Warn users about callback deprecation. * 2) If Promise class is implemented in current environment. * * @private */ function wrapForCompatibility(api) { return function() { if (!window.Promise) { throw new Error('Promises is not supported in this environment. Please polyfill Promise.'); } var argLen = arguments.length; if (argLen >= 1 && isFunction(arguments[argLen - 1])) { var callback = arguments[argLen - 1]; console.warn(new Error( 'Passing callbacks to ' + api.name + ' is deprecated and will be removed in a future major release. ' + 'Please switch to promises: https://bpmn.io/l/moving-to-promises.html' )); var argsWithoutCallback = Array.prototype.slice.call(arguments, 0, -1); api.apply(this, argsWithoutCallback).then(function(result) { var firstKey = Object.keys(result)[0]; // The APIs we are wrapping all resolve a single item depending on the API. // For instance, importXML resolves { warnings } and saveXML returns { xml }. // That's why we can call the callback with the first item of result. return callback(null, result[firstKey]); // Passing a second paramter instead of catch because we don't want to // catch errors thrown by callback(). }, function(err) { return callback(err, err.warnings); }); } else { return api.apply(this, arguments); } }; } // TODO(nikku): remove with future bpmn-js version var DI_ERROR_MESSAGE = 'Tried to access di from the businessObject. The di is available through the diagram element only. For more information, see https://github.com/bpmn-io/bpmn-js/issues/1472'; function ensureCompatDiRef(businessObject) { // bpmnElement can have multiple independent DIs if (!has$1(businessObject, 'di')) { Object.defineProperty(businessObject, 'di', { enumerable: false, get: function() { throw new Error(DI_ERROR_MESSAGE); } }); } } /** * Returns true if an element has the given meta-model type * * @param {ModdleElement} element * @param {string} type * * @return {boolean} */ function is$2(element, type) { return element.$instanceOf(type); } /** * Find a suitable display candidate for definitions where the DI does not * correctly specify one. */ function findDisplayCandidate(definitions) { return find(definitions.rootElements, function(e) { return is$2(e, 'bpmn:Process') || is$2(e, 'bpmn:Collaboration'); }); } function BpmnTreeWalker(handler, translate) { // list of containers already walked var handledElements = {}; // list of elements to handle deferred to ensure // prerequisites are drawn var deferred = []; var diMap = {}; // Helpers ////////////////////// function contextual(fn, ctx) { return function(e) { fn(e, ctx); }; } function handled(element) { handledElements[element.id] = element; } function isHandled(element) { return handledElements[element.id]; } function visit(element, ctx) { var gfx = element.gfx; // avoid multiple rendering of elements if (gfx) { throw new Error( translate('already rendered {element}', { element: elementToString(element) }) ); } // call handler return handler.element(element, diMap[element.id], ctx); } function visitRoot(element, diagram) { return handler.root(element, diMap[element.id], diagram); } function visitIfDi(element, ctx) { try { var gfx = diMap[element.id] && visit(element, ctx); handled(element); return gfx; } catch (e) { logError(e.message, { element: element, error: e }); console.error(translate('failed to import {element}', { element: elementToString(element) })); console.error(e); } } function logError(message, context) { handler.error(message, context); } // DI handling ////////////////////// function registerDi(di) { var bpmnElement = di.bpmnElement; if (bpmnElement) { if (diMap[bpmnElement.id]) { logError( translate('multiple DI elements defined for {element}', { element: elementToString(bpmnElement) }), { element: bpmnElement } ); } else { diMap[bpmnElement.id] = di; ensureCompatDiRef(bpmnElement); } } else { logError( translate('no bpmnElement referenced in {element}', { element: elementToString(di) }), { element: di } ); } } function handleDiagram(diagram) { handlePlane(diagram.plane); } function handlePlane(plane) { registerDi(plane); forEach$1(plane.planeElement, handlePlaneElement); } function handlePlaneElement(planeElement) { registerDi(planeElement); } // Semantic handling ////////////////////// /** * Handle definitions and return the rendered diagram (if any) * * @param {ModdleElement} definitions to walk and import * @param {ModdleElement} [diagram] specific diagram to import and display * * @throws {Error} if no diagram to display could be found */ function handleDefinitions(definitions, diagram) { // make sure we walk the correct bpmnElement var diagrams = definitions.diagrams; if (diagram && diagrams.indexOf(diagram) === -1) { throw new Error(translate('diagram not part of bpmn:Definitions')); } if (!diagram && diagrams && diagrams.length) { diagram = diagrams[0]; } // no diagram -> nothing to import if (!diagram) { throw new Error(translate('no diagram to display')); } // load DI from selected diagram only diMap = {}; handleDiagram(diagram); var plane = diagram.plane; if (!plane) { throw new Error(translate( 'no plane for {element}', { element: elementToString(diagram) } )); } var rootElement = plane.bpmnElement; // ensure we default to a suitable display candidate (process or collaboration), // even if non is specified in DI if (!rootElement) { rootElement = findDisplayCandidate(definitions); if (!rootElement) { throw new Error(translate('no process or collaboration to display')); } else { logError( translate('correcting missing bpmnElement on {plane} to {rootElement}', { plane: elementToString(plane), rootElement: elementToString(rootElement) }) ); // correct DI on the fly plane.bpmnElement = rootElement; registerDi(plane); } } var ctx = visitRoot(rootElement, plane); if (is$2(rootElement, 'bpmn:Process') || is$2(rootElement, 'bpmn:SubProcess')) { handleProcess(rootElement, ctx); } else if (is$2(rootElement, 'bpmn:Collaboration')) { handleCollaboration(rootElement, ctx); // force drawing of everything not yet drawn that is part of the target DI handleUnhandledProcesses(definitions.rootElements, ctx); } else { throw new Error( translate('unsupported bpmnElement for {plane}: {rootElement}', { plane: elementToString(plane), rootElement: elementToString(rootElement) }) ); } // handle all deferred elements handleDeferred(); } function handleDeferred() { var fn; // drain deferred until empty while (deferred.length) { fn = deferred.shift(); fn(); } } function handleProcess(process, context) { handleFlowElementsContainer(process, context); handleIoSpecification(process.ioSpecification, context); handleArtifacts(process.artifacts, context); // log process handled handled(process); } function handleUnhandledProcesses(rootElements, ctx) { // walk through all processes that have not yet been drawn and draw them // if they contain lanes with DI information. // we do this to pass the free-floating lane test cases in the MIWG test suite var processes = filter(rootElements, function(e) { return !isHandled(e) && is$2(e, 'bpmn:Process') && e.laneSets; }); processes.forEach(contextual(handleProcess, ctx)); } function handleMessageFlow(messageFlow, context) { visitIfDi(messageFlow, context); } function handleMessageFlows(messageFlows, context) { forEach$1(messageFlows, contextual(handleMessageFlow, context)); } function handleDataAssociation(association, context) { visitIfDi(association, context); } function handleDataInput(dataInput, context) { visitIfDi(dataInput, context); } function handleDataOutput(dataOutput, context) { visitIfDi(dataOutput, context); } function handleArtifact(artifact, context) { // bpmn:TextAnnotation // bpmn:Group // bpmn:Association visitIfDi(artifact, context); } function handleArtifacts(artifacts, context) { forEach$1(artifacts, function(e) { if (is$2(e, 'bpmn:Association')) { deferred.push(function() { handleArtifact(e, context); }); } else { handleArtifact(e, context); } }); } function handleIoSpecification(ioSpecification, context) { if (!ioSpecification) { return; } forEach$1(ioSpecification.dataInputs, contextual(handleDataInput, context)); forEach$1(ioSpecification.dataOutputs, contextual(handleDataOutput, context)); } function handleSubProcess(subProcess, context) { handleFlowElementsContainer(subProcess, context); handleArtifacts(subProcess.artifacts, context); } function handleFlowNode(flowNode, context) { var childCtx = visitIfDi(flowNode, context); if (is$2(flowNode, 'bpmn:SubProcess')) { handleSubProcess(flowNode, childCtx || context); } if (is$2(flowNode, 'bpmn:Activity')) { handleIoSpecification(flowNode.ioSpecification, context); } // defer handling of associations // affected types: // // * bpmn:Activity // * bpmn:ThrowEvent // * bpmn:CatchEvent // deferred.push(function() { forEach$1(flowNode.dataInputAssociations, contextual(handleDataAssociation, context)); forEach$1(flowNode.dataOutputAssociations, contextual(handleDataAssociation, context)); }); } function handleSequenceFlow(sequenceFlow, context) { visitIfDi(sequenceFlow, context); } function handleDataElement(dataObject, context) { visitIfDi(dataObject, context); } function handleLane(lane, context) { deferred.push(function() { var newContext = visitIfDi(lane, context); if (lane.childLaneSet) { handleLaneSet(lane.childLaneSet, newContext || context); } wireFlowNodeRefs(lane); }); } function handleLaneSet(laneSet, context) { forEach$1(laneSet.lanes, contextual(handleLane, context)); } function handleLaneSets(laneSets, context) { forEach$1(laneSets, contextual(handleLaneSet, context)); } function handleFlowElementsContainer(container, context) { handleFlowElements(container.flowElements, context); if (container.laneSets) { handleLaneSets(container.laneSets, context); } } function handleFlowElements(flowElements, context) { forEach$1(flowElements, function(e) { if (is$2(e, 'bpmn:SequenceFlow')) { deferred.push(function() { handleSequenceFlow(e, context); }); } else if (is$2(e, 'bpmn:BoundaryEvent')) { deferred.unshift(function() { handleFlowNode(e, context); }); } else if (is$2(e, 'bpmn:FlowNode')) { handleFlowNode(e, context); } else if (is$2(e, 'bpmn:DataObject')) ; else if (is$2(e, 'bpmn:DataStoreReference')) { handleDataElement(e, context); } else if (is$2(e, 'bpmn:DataObjectReference')) { handleDataElement(e, context); } else { logError( translate('unrecognized flowElement {element} in context {context}', { element: elementToString(e), context: (context ? elementToString(context.businessObject) : 'null') }), { element: e, context: context } ); } }); } function handleParticipant(participant, context) { var newCtx = visitIfDi(participant, context); var process = participant.processRef; if (process) { handleProcess(process, newCtx || context); } } function handleCollaboration(collaboration, context) { forEach$1(collaboration.participants, contextual(handleParticipant, context)); handleArtifacts(collaboration.artifacts, context); // handle message flows latest in the process deferred.push(function() { handleMessageFlows(collaboration.messageFlows, context); }); } function wireFlowNodeRefs(lane) { // wire the virtual flowNodeRefs <-> relationship forEach$1(lane.flowNodeRef, function(flowNode) { var lanes = flowNode.get('lanes'); if (lanes) { lanes.push(lane); } }); } // API ////////////////////// return { handleDeferred: handleDeferred, handleDefinitions: handleDefinitions, handleSubProcess: handleSubProcess, registerDi: registerDi }; } /** * Is an element of the given BPMN type? * * @param {djs.model.Base|ModdleElement} element * @param {string} type * * @return {boolean} */ function is$1(element, type) { var bo = getBusinessObject(element); return bo && (typeof bo.$instanceOf === 'function') && bo.$instanceOf(type); } /** * Return true if element has any of the given types. * * @param {djs.model.Base} element * @param {Array} types * * @return {boolean} */ function isAny(element, types) { return some(types, function(t) { return is$1(element, t); }); } /** * Return the business object for a given element. * * @param {djs.model.Base|ModdleElement} element * * @return {ModdleElement} */ function getBusinessObject(element) { return (element && element.businessObject) || element; } /** * Return the di object for a given element. * * @param {djs.model.Base} element * * @return {ModdleElement} */ function getDi(element) { return element && element.di; } /** * The importBpmnDiagram result. * * @typedef {Object} ImportBPMNDiagramResult * * @property {Array} warnings */ /** * The importBpmnDiagram error. * * @typedef {Error} ImportBPMNDiagramError * * @property {Array} warnings */ /** * Import the definitions into a diagram. * * Errors and warnings are reported through the specified callback. * * @param {djs.Diagram} diagram * @param {ModdleElement} definitions * @param {ModdleElement} [bpmnDiagram] the diagram to be rendered * (if not provided, the first one will be rendered) * * Returns {Promise} */ function importBpmnDiagram(diagram, definitions, bpmnDiagram) { var importer, eventBus, translate, canvas; var error, warnings = []; /** * Walk the diagram semantically, importing (=drawing) * all elements you encounter. * * @param {ModdleElement} definitions * @param {ModdleElement} bpmnDiagram */ function render(definitions, bpmnDiagram) { var visitor = { root: function(element, di) { return importer.add(element, di); }, element: function(element, di, parentShape) { return importer.add(element, di, parentShape); }, error: function(message, context) { warnings.push({ message: message, context: context }); } }; var walker = new BpmnTreeWalker(visitor, translate); bpmnDiagram = bpmnDiagram || (definitions.diagrams && definitions.diagrams[0]); var diagramsToImport = getDiagramsToImport(definitions, bpmnDiagram); if (!diagramsToImport) { throw new Error(translate('no diagram to display')); } // traverse BPMN 2.0 document model, // starting at definitions forEach$1(diagramsToImport, function(diagram) { walker.handleDefinitions(definitions, diagram); }); var rootId = bpmnDiagram.plane.bpmnElement.id; // we do need to account for different ways we create root elements // each nested imported do have the `_plane` suffix, while // the root is found under the business object ID canvas.setRootElement( canvas.findRoot(rootId + '_plane') || canvas.findRoot(rootId) ); } return new Promise(function(resolve, reject) { try { importer = diagram.get('bpmnImporter'); eventBus = diagram.get('eventBus'); translate = diagram.get('translate'); canvas = diagram.get('canvas'); eventBus.fire('import.render.start', { definitions: definitions }); render(definitions, bpmnDiagram); eventBus.fire('import.render.complete', { error: error, warnings: warnings }); return resolve({ warnings: warnings }); } catch (e) { e.warnings = warnings; return reject(e); } }); } /** * Returns all diagrams in the same hierarchy as the requested diagram. * Includes all parent and sub process diagrams. * * @param {Array} definitions * @param {Object} bpmnDiagram * * @returns {Array} */ function getDiagramsToImport(definitions, bpmnDiagram) { if (!bpmnDiagram) { return; } var bpmnElement = bpmnDiagram.plane.bpmnElement, rootElement = bpmnElement; if (!is$1(bpmnElement, 'bpmn:Process') && !is$1(bpmnElement, 'bpmn:Collaboration')) { rootElement = findRootProcess(bpmnElement); } // in case the process is part of a collaboration, the plane references the // collaboration, not the process var collaboration; if (is$1(rootElement, 'bpmn:Collaboration')) { collaboration = rootElement; } else { collaboration = find(definitions.rootElements, function(element) { if (!is$1(element, 'bpmn:Collaboration')) { return; } return find(element.participants, function(participant) { return participant.processRef === rootElement; }); }); } var rootElements = [ rootElement ]; // all collaboration processes can contain sub-diagrams if (collaboration) { rootElements = map(collaboration.participants, function(participant) { return participant.processRef; }); rootElements.push(collaboration); } var allChildren = selfAndAllFlowElements(rootElements); // if we have multiple diagrams referencing the same element, we // use the first in the file var diagramsToImport = [ bpmnDiagram ]; var handledElements = [ bpmnElement ]; forEach$1(definitions.diagrams, function(diagram) { var businessObject = diagram.plane.bpmnElement; if ( allChildren.indexOf(businessObject) !== -1 && handledElements.indexOf(businessObject) === -1 ) { diagramsToImport.push(diagram); handledElements.push(businessObject); } }); return diagramsToImport; } function selfAndAllFlowElements(elements) { var result = []; forEach$1(elements, function(element) { if (!element) { return; } result.push(element); result = result.concat(selfAndAllFlowElements(element.flowElements)); }); return result; } function findRootProcess(element) { var parent = element; while (parent) { if (is$1(parent, 'bpmn:Process')) { return parent; } parent = parent.$parent; } } /** * This file must not be changed or exchanged. * * @see http://bpmn.io/license for more information. */ // inlined ../../resources/logo.svg var BPMNIO_LOGO_SVG = ''; var BPMNIO_IMG = BPMNIO_LOGO_SVG; var LOGO_STYLES = { verticalAlign: 'middle' }; var LINK_STYLES = { 'color': '#404040' }; var LIGHTBOX_STYLES = { 'zIndex': '1001', 'position': 'fixed', 'top': '0', 'left': '0', 'right': '0', 'bottom': '0' }; var BACKDROP_STYLES = { 'width': '100%', 'height': '100%', 'background': 'rgba(40,40,40,0.2)' }; var NOTICE_STYLES = { 'position': 'absolute', 'left': '50%', 'top': '40%', 'transform': 'translate(-50%)', 'width': '260px', 'padding': '10px', 'background': 'white', 'boxShadow': '0 1px 4px rgba(0,0,0,0.3)', 'fontFamily': 'Helvetica, Arial, sans-serif', 'fontSize': '14px', 'display': 'flex', 'lineHeight': '1.3' }; var LIGHTBOX_MARKUP = '
' + '
' + '
' + '' + BPMNIO_IMG + '' + '' + 'Web-based tooling for BPMN, DMN and CMMN diagrams ' + 'powered by bpmn.io.' + '' + '
' + '
'; var lightbox; function createLightbox() { lightbox = domify(LIGHTBOX_MARKUP); assign$1(lightbox, LIGHTBOX_STYLES); assign$1(query('svg', lightbox), LOGO_STYLES); assign$1(query('.backdrop', lightbox), BACKDROP_STYLES); assign$1(query('.notice', lightbox), NOTICE_STYLES); assign$1(query('.link', lightbox), LINK_STYLES, { 'margin': '15px 20px 15px 10px', 'alignSelf': 'center' }); } function open() { if (!lightbox) { createLightbox(); delegate.bind(lightbox, '.backdrop', 'click', function(event) { document.body.removeChild(lightbox); }); } document.body.appendChild(lightbox); } /** * The code in the area * must not be changed. * * @see http://bpmn.io/license for more information. */ /** * A base viewer for BPMN 2.0 diagrams. * * Have a look at {@link Viewer}, {@link NavigatedViewer} or {@link Modeler} for * bundles that include actual features. * * @param {Object} [options] configuration options to pass to the viewer * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. * @param {string|number} [options.width] the width of the viewer * @param {string|number} [options.height] the height of the viewer * @param {Object} [options.moddleExtensions] extension packages to provide * @param {Array} [options.modules] a list of modules to override the default modules * @param {Array} [options.additionalModules] a list of modules to use with the default modules */ function BaseViewer(options) { options = assign({}, DEFAULT_OPTIONS, options); this._moddle = this._createModdle(options); this._container = this._createContainer(options); /* */ addProjectLogo(this._container); /* */ this._init(this._container, this._moddle, options); } e(BaseViewer, Diagram); /** * The importXML result. * * @typedef {Object} ImportXMLResult * * @property {Array} warnings */ /** * The importXML error. * * @typedef {Error} ImportXMLError * * @property {Array} warnings */ /** * Parse and render a BPMN 2.0 diagram. * * Once finished the viewer reports back the result to the * provided callback function with (err, warnings). * * ## Life-Cycle Events * * During import the viewer will fire life-cycle events: * * * import.parse.start (about to read model from xml) * * import.parse.complete (model read; may have worked or not) * * import.render.start (graphical import start) * * import.render.complete (graphical import finished) * * import.done (everything done) * * You can use these events to hook into the life-cycle. * * @param {string} xml the BPMN 2.0 xml * @param {ModdleElement|string} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered) * * Returns {Promise} */ BaseViewer.prototype.importXML = wrapForCompatibility(function importXML(xml, bpmnDiagram) { var self = this; function ParseCompleteEvent(data) { var event = self.get('eventBus').createEvent(data); // TODO(nikku): remove with future bpmn-js version Object.defineProperty(event, 'context', { enumerable: true, get: function() { console.warn(new Error( 'import.parse.complete is deprecated ' + 'and will be removed in future library versions' )); return { warnings: data.warnings, references: data.references, elementsById: data.elementsById }; } }); return event; } return new Promise(function(resolve, reject) { // hook in pre-parse listeners + // allow xml manipulation xml = self._emit('import.parse.start', { xml: xml }) || xml; self._moddle.fromXML(xml, 'bpmn:Definitions').then(function(result) { var definitions = result.rootElement; var references = result.references; var parseWarnings = result.warnings; var elementsById = result.elementsById; // hook in post parse listeners + // allow definitions manipulation definitions = self._emit('import.parse.complete', ParseCompleteEvent({ error: null, definitions: definitions, elementsById: elementsById, references: references, warnings: parseWarnings })) || definitions; self.importDefinitions(definitions, bpmnDiagram).then(function(result) { var allWarnings = [].concat(parseWarnings, result.warnings || []); self._emit('import.done', { error: null, warnings: allWarnings }); return resolve({ warnings: allWarnings }); }).catch(function(err) { var allWarnings = [].concat(parseWarnings, err.warnings || []); self._emit('import.done', { error: err, warnings: allWarnings }); return reject(addWarningsToError(err, allWarnings)); }); }).catch(function(err) { self._emit('import.parse.complete', { error: err }); err = checkValidationError(err); self._emit('import.done', { error: err, warnings: err.warnings }); return reject(err); }); }); }); /** * The importDefinitions result. * * @typedef {Object} ImportDefinitionsResult * * @property {Array} warnings */ /** * The importDefinitions error. * * @typedef {Error} ImportDefinitionsError * * @property {Array} warnings */ /** * Import parsed definitions and render a BPMN 2.0 diagram. * * Once finished the viewer reports back the result to the * provided callback function with (err, warnings). * * ## Life-Cycle Events * * During import the viewer will fire life-cycle events: * * * import.render.start (graphical import start) * * import.render.complete (graphical import finished) * * You can use these events to hook into the life-cycle. * * @param {ModdleElement} definitions parsed BPMN 2.0 definitions * @param {ModdleElement|string} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered) * * Returns {Promise} */ BaseViewer.prototype.importDefinitions = wrapForCompatibility(function importDefinitions(definitions, bpmnDiagram) { var self = this; return new Promise(function(resolve, reject) { self._setDefinitions(definitions); self.open(bpmnDiagram).then(function(result) { var warnings = result.warnings; return resolve({ warnings: warnings }); }).catch(function(err) { return reject(err); }); }); }); /** * The open result. * * @typedef {Object} OpenResult * * @property {Array} warnings */ /** * The open error. * * @typedef {Error} OpenError * * @property {Array} warnings */ /** * Open diagram of previously imported XML. * * Once finished the viewer reports back the result to the * provided callback function with (err, warnings). * * ## Life-Cycle Events * * During switch the viewer will fire life-cycle events: * * * import.render.start (graphical import start) * * import.render.complete (graphical import finished) * * You can use these events to hook into the life-cycle. * * @param {string|ModdleElement} [bpmnDiagramOrId] id or the diagram to open * * Returns {Promise} */ BaseViewer.prototype.open = wrapForCompatibility(function open(bpmnDiagramOrId) { var definitions = this._definitions; var bpmnDiagram = bpmnDiagramOrId; var self = this; return new Promise(function(resolve, reject) { if (!definitions) { var err1 = new Error('no XML imported'); return reject(addWarningsToError(err1, [])); } if (typeof bpmnDiagramOrId === 'string') { bpmnDiagram = findBPMNDiagram(definitions, bpmnDiagramOrId); if (!bpmnDiagram) { var err2 = new Error('BPMNDiagram <' + bpmnDiagramOrId + '> not found'); return reject(addWarningsToError(err2, [])); } } // clear existing rendered diagram // catch synchronous exceptions during #clear() try { self.clear(); } catch (error) { return reject(addWarningsToError(error, [])); } // perform graphical import importBpmnDiagram(self, definitions, bpmnDiagram).then(function(result) { var warnings = result.warnings; return resolve({ warnings: warnings }); }).catch(function(err) { return reject(err); }); }); }); /** * The saveXML result. * * @typedef {Object} SaveXMLResult * * @property {string} xml */ /** * Export the currently displayed BPMN 2.0 diagram as * a BPMN 2.0 XML document. * * ## Life-Cycle Events * * During XML saving the viewer will fire life-cycle events: * * * saveXML.start (before serialization) * * saveXML.serialized (after xml generation) * * saveXML.done (everything done) * * You can use these events to hook into the life-cycle. * * @param {Object} [options] export options * @param {boolean} [options.format=false] output formatted XML * @param {boolean} [options.preamble=true] output preamble * * Returns {Promise} */ BaseViewer.prototype.saveXML = wrapForCompatibility(function saveXML(options) { options = options || {}; var self = this; var definitions = this._definitions; return new Promise(function(resolve) { if (!definitions) { return resolve({ error: new Error('no definitions loaded') }); } // allow to fiddle around with definitions definitions = self._emit('saveXML.start', { definitions: definitions }) || definitions; self._moddle.toXML(definitions, options).then(function(result) { var xml = result.xml; xml = self._emit('saveXML.serialized', { xml: xml }) || xml; return resolve({ xml: xml }); }); }).catch(function(error) { return { error: error }; }).then(function(result) { self._emit('saveXML.done', result); var error = result.error; if (error) { return Promise.reject(error); } return result; }); }); /** * The saveSVG result. * * @typedef {Object} SaveSVGResult * * @property {string} svg */ /** * Export the currently displayed BPMN 2.0 diagram as * an SVG image. * * ## Life-Cycle Events * * During SVG saving the viewer will fire life-cycle events: * * * saveSVG.start (before serialization) * * saveSVG.done (everything done) * * You can use these events to hook into the life-cycle. * * @param {Object} [options] * * Returns {Promise} */ BaseViewer.prototype.saveSVG = wrapForCompatibility(function saveSVG(options) { var self = this; return new Promise(function(resolve, reject) { self._emit('saveSVG.start'); var svg, err; try { var canvas = self.get('canvas'); var contentNode = canvas.getActiveLayer(), defsNode = query('defs', canvas._svg); var contents = innerSVG(contentNode), defs = defsNode ? '' + innerSVG(defsNode) + '' : ''; var bbox = contentNode.getBBox(); svg = '\n' + '\n' + '\n' + '' + defs + contents + ''; } catch (e) { err = e; } self._emit('saveSVG.done', { error: err, svg: svg }); if (!err) { return resolve({ svg: svg }); } return reject(err); }); }); /** * Get a named diagram service. * * @example * * var elementRegistry = viewer.get('elementRegistry'); * var startEventShape = elementRegistry.get('StartEvent_1'); * * @param {string} name * * @return {Object} diagram service instance * * @method BaseViewer#get */ /** * Invoke a function in the context of this viewer. * * @example * * viewer.invoke(function(elementRegistry) { * var startEventShape = elementRegistry.get('StartEvent_1'); * }); * * @param {Function} fn to be invoked * * @return {Object} the functions return value * * @method BaseViewer#invoke */ BaseViewer.prototype._setDefinitions = function(definitions) { this._definitions = definitions; }; BaseViewer.prototype.getModules = function() { return this._modules; }; /** * Remove all drawn elements from the viewer. * * After calling this method the viewer can still * be reused for opening another diagram. * * @method BaseViewer#clear */ BaseViewer.prototype.clear = function() { if (!this.getDefinitions()) { // no diagram to clear return; } // remove drawn elements Diagram.prototype.clear.call(this); }; /** * Destroy the viewer instance and remove all its * remainders from the document tree. */ BaseViewer.prototype.destroy = function() { // diagram destroy Diagram.prototype.destroy.call(this); // dom detach remove$2(this._container); }; /** * Register an event listener * * Remove a previously added listener via {@link #off(event, callback)}. * * @param {string} event * @param {number} [priority] * @param {Function} callback * @param {Object} [that] */ BaseViewer.prototype.on = function(event, priority, callback, target) { return this.get('eventBus').on(event, priority, callback, target); }; /** * De-register an event listener * * @param {string} event * @param {Function} callback */ BaseViewer.prototype.off = function(event, callback) { this.get('eventBus').off(event, callback); }; BaseViewer.prototype.attachTo = function(parentNode) { if (!parentNode) { throw new Error('parentNode required'); } // ensure we detach from the // previous, old parent this.detach(); // unwrap jQuery if provided if (parentNode.get && parentNode.constructor.prototype.jquery) { parentNode = parentNode.get(0); } if (typeof parentNode === 'string') { parentNode = query(parentNode); } parentNode.appendChild(this._container); this._emit('attach', {}); this.get('canvas').resized(); }; BaseViewer.prototype.getDefinitions = function() { return this._definitions; }; BaseViewer.prototype.detach = function() { var container = this._container, parentNode = container.parentNode; if (!parentNode) { return; } this._emit('detach', {}); parentNode.removeChild(container); }; BaseViewer.prototype._init = function(container, moddle, options) { var baseModules = options.modules || this.getModules(), additionalModules = options.additionalModules || [], staticModules = [ { bpmnjs: [ 'value', this ], moddle: [ 'value', moddle ] } ]; var diagramModules = [].concat(staticModules, baseModules, additionalModules); var diagramOptions = assign(omit(options, [ 'additionalModules' ]), { canvas: assign({}, options.canvas, { container: container }), modules: diagramModules }); // invoke diagram constructor Diagram.call(this, diagramOptions); if (options && options.container) { this.attachTo(options.container); } }; /** * Emit an event on the underlying {@link EventBus} * * @param {string} type * @param {Object} event * * @return {Object} event processing result (if any) */ BaseViewer.prototype._emit = function(type, event) { return this.get('eventBus').fire(type, event); }; BaseViewer.prototype._createContainer = function(options) { var container = domify('
'); assign$1(container, { width: ensureUnit(options.width), height: ensureUnit(options.height), position: options.position }); return container; }; BaseViewer.prototype._createModdle = function(options) { var moddleOptions = assign({}, this._moddleExtensions, options.moddleExtensions); return new simple(moddleOptions); }; BaseViewer.prototype._modules = []; // helpers /////////////// function addWarningsToError(err, warningsAry) { err.warnings = warningsAry; return err; } function checkValidationError(err) { // check if we can help the user by indicating wrong BPMN 2.0 xml // (in case he or the exporting tool did not get that right) var pattern = /unparsable content <([^>]+)> detected([\s\S]*)$/; var match = pattern.exec(err.message); if (match) { err.message = 'unparsable content <' + match[1] + '> detected; ' + 'this may indicate an invalid BPMN 2.0 diagram file' + match[2]; } return err; } var DEFAULT_OPTIONS = { width: '100%', height: '100%', position: 'relative' }; /** * Ensure the passed argument is a proper unit (defaulting to px) */ function ensureUnit(val) { return val + (isNumber(val) ? 'px' : ''); } /** * Find BPMNDiagram in definitions by ID * * @param {ModdleElement} definitions * @param {string} diagramId * * @return {ModdleElement|null} */ function findBPMNDiagram(definitions, diagramId) { if (!diagramId) { return null; } return find(definitions.diagrams, function(element) { return element.id === diagramId; }) || null; } /** * Adds the project logo to the diagram container as * required by the bpmn.io license. * * @see http://bpmn.io/license * * @param {Element} container */ function addProjectLogo(container) { var img = BPMNIO_IMG; var linkMarkup = '' + img + ''; var linkElement = domify(linkMarkup); assign$1(query('svg', linkElement), LOGO_STYLES); assign$1(linkElement, LINK_STYLES, { position: 'absolute', bottom: '15px', right: '15px', zIndex: '100' }); container.appendChild(linkElement); componentEvent.bind(linkElement, 'click', function(event) { open(); event.preventDefault(); }); } /* */ /** * A base modeler for BPMN 2.0 diagrams. * * Have a look at {@link Modeler} for a bundle that includes actual features. * * @param {Object} [options] configuration options to pass to the viewer * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. * @param {string|number} [options.width] the width of the viewer * @param {string|number} [options.height] the height of the viewer * @param {Object} [options.moddleExtensions] extension packages to provide * @param {Array} [options.modules] a list of modules to override the default modules * @param {Array} [options.additionalModules] a list of modules to use with the default modules */ function BaseModeler(options) { BaseViewer.call(this, options); // hook ID collection into the modeler this.on('import.parse.complete', function(event) { if (!event.error) { this._collectIds(event.definitions, event.elementsById); } }, this); this.on('diagram.destroy', function() { this.get('moddle').ids.clear(); }, this); } e(BaseModeler, BaseViewer); /** * Create a moddle instance, attaching ids to it. * * @param {Object} options */ BaseModeler.prototype._createModdle = function(options) { var moddle = BaseViewer.prototype._createModdle.call(this, options); // attach ids to moddle to be able to track // and validated ids in the BPMN 2.0 XML document // tree moddle.ids = new Ids([ 32, 36, 1 ]); return moddle; }; /** * Collect ids processed during parsing of the * definitions object. * * @param {ModdleElement} definitions * @param {Context} context */ BaseModeler.prototype._collectIds = function(definitions, elementsById) { var moddle = definitions.$model, ids = moddle.ids, id; // remove references from previous import ids.clear(); for (id in elementsById) { ids.claim(id, elementsById[id]); } }; function isExpanded(element, di) { if (is$1(element, 'bpmn:CallActivity')) { return false; } if (is$1(element, 'bpmn:SubProcess')) { di = di || getDi(element); if (di && is$1(di, 'bpmndi:BPMNPlane')) { return true; } return di && !!di.isExpanded; } if (is$1(element, 'bpmn:Participant')) { return !!getBusinessObject(element).processRef; } return true; } function isInterrupting(element) { return element && getBusinessObject(element).isInterrupting !== false; } function isEventSubProcess(element) { return element && !!getBusinessObject(element).triggeredByEvent; } function hasEventDefinition$2(element, eventType) { var bo = getBusinessObject(element), hasEventDefinition = false; if (bo.eventDefinitions) { forEach$1(bo.eventDefinitions, function(event) { if (is$1(event, eventType)) { hasEventDefinition = true; } }); } return hasEventDefinition; } function hasErrorEventDefinition(element) { return hasEventDefinition$2(element, 'bpmn:ErrorEventDefinition'); } function hasEscalationEventDefinition(element) { return hasEventDefinition$2(element, 'bpmn:EscalationEventDefinition'); } function hasCompensateEventDefinition(element) { return hasEventDefinition$2(element, 'bpmn:CompensateEventDefinition'); } function getLabelAttr(semantic) { if ( is$1(semantic, 'bpmn:FlowElement') || is$1(semantic, 'bpmn:Participant') || is$1(semantic, 'bpmn:Lane') || is$1(semantic, 'bpmn:SequenceFlow') || is$1(semantic, 'bpmn:MessageFlow') || is$1(semantic, 'bpmn:DataInput') || is$1(semantic, 'bpmn:DataOutput') ) { return 'name'; } if (is$1(semantic, 'bpmn:TextAnnotation')) { return 'text'; } if (is$1(semantic, 'bpmn:Group')) { return 'categoryValueRef'; } } function getCategoryValue(semantic) { var categoryValueRef = semantic['categoryValueRef']; if (!categoryValueRef) { return ''; } return categoryValueRef.value || ''; } function getLabel(element) { var semantic = element.businessObject, attr = getLabelAttr(semantic); if (attr) { if (attr === 'categoryValueRef') { return getCategoryValue(semantic); } return semantic[attr] || ''; } } function setLabel(element, text, isExternal) { var semantic = element.businessObject, attr = getLabelAttr(semantic); if (attr) { if (attr === 'categoryValueRef') { semantic['categoryValueRef'].value = text; } else { semantic[attr] = text; } } return element; } var black = 'hsl(225, 10%, 15%)'; // element utils ////////////////////// /** * Checks if eventDefinition of the given element matches with semantic type. * * @return {boolean} true if element is of the given semantic type */ function isTypedEvent(event, eventDefinitionType, filter) { function matches(definition, filter) { return every(filter, function(val, key) { // we want a == conversion here, to be able to catch // undefined == false and friends /* jshint -W116 */ return definition[key] == val; }); } return some(event.eventDefinitions, function(definition) { return definition.$type === eventDefinitionType && matches(event, filter); }); } function isThrowEvent(event) { return (event.$type === 'bpmn:IntermediateThrowEvent') || (event.$type === 'bpmn:EndEvent'); } function isCollection(element) { var dataObject = element.dataObjectRef; return element.isCollection || (dataObject && dataObject.isCollection); } function getSemantic(element) { return element.businessObject; } // color access ////////////////////// function getFillColor(element, defaultColor) { var di = getDi(element); return di.get('color:background-color') || di.get('bioc:fill') || defaultColor || 'white'; } function getStrokeColor$1(element, defaultColor) { var di = getDi(element); return di.get('color:border-color') || di.get('bioc:stroke') || defaultColor || black; } function getLabelColor(element, defaultColor, defaultStrokeColor) { var di = getDi(element), label = di.get('label'); return label && label.get('color:color') || defaultColor || getStrokeColor$1(element, defaultStrokeColor); } // cropping path customizations ////////////////////// function getCirclePath(shape) { var cx = shape.x + shape.width / 2, cy = shape.y + shape.height / 2, radius = shape.width / 2; var circlePath = [ [ 'M', cx, cy ], [ 'm', 0, -radius ], [ 'a', radius, radius, 0, 1, 1, 0, 2 * radius ], [ 'a', radius, radius, 0, 1, 1, 0, -2 * radius ], [ 'z' ] ]; return componentsToPath(circlePath); } function getRoundRectPath(shape, borderRadius) { var x = shape.x, y = shape.y, width = shape.width, height = shape.height; var roundRectPath = [ [ 'M', x + borderRadius, y ], [ 'l', width - borderRadius * 2, 0 ], [ 'a', borderRadius, borderRadius, 0, 0, 1, borderRadius, borderRadius ], [ 'l', 0, height - borderRadius * 2 ], [ 'a', borderRadius, borderRadius, 0, 0, 1, -borderRadius, borderRadius ], [ 'l', borderRadius * 2 - width, 0 ], [ 'a', borderRadius, borderRadius, 0, 0, 1, -borderRadius, -borderRadius ], [ 'l', 0, borderRadius * 2 - height ], [ 'a', borderRadius, borderRadius, 0, 0, 1, borderRadius, -borderRadius ], [ 'z' ] ]; return componentsToPath(roundRectPath); } function getDiamondPath(shape) { var width = shape.width, height = shape.height, x = shape.x, y = shape.y, halfWidth = width / 2, halfHeight = height / 2; var diamondPath = [ [ 'M', x + halfWidth, y ], [ 'l', halfWidth, halfHeight ], [ 'l', -halfWidth, halfHeight ], [ 'l', -halfWidth, -halfHeight ], [ 'z' ] ]; return componentsToPath(diamondPath); } function getRectPath(shape) { var x = shape.x, y = shape.y, width = shape.width, height = shape.height; var rectPath = [ [ 'M', x, y ], [ 'l', width, 0 ], [ 'l', 0, height ], [ 'l', -width, 0 ], [ 'z' ] ]; return componentsToPath(rectPath); } var RENDERER_IDS = new Ids(); var TASK_BORDER_RADIUS = 10; var INNER_OUTER_DIST = 3; var DEFAULT_FILL_OPACITY = .95, HIGH_FILL_OPACITY = .35; var ELEMENT_LABEL_DISTANCE$1 = 10; function BpmnRenderer( config, eventBus, styles, pathMap, canvas, textRenderer, priority) { BaseRenderer.call(this, eventBus, priority); var defaultFillColor = config && config.defaultFillColor, defaultStrokeColor = config && config.defaultStrokeColor, defaultLabelColor = config && config.defaultLabelColor; var rendererId = RENDERER_IDS.next(); var markers = {}; var computeStyle = styles.computeStyle; function addMarker(id, options) { var attrs = assign({ fill: black, strokeWidth: 1, strokeLinecap: 'round', strokeDasharray: 'none' }, options.attrs); var ref = options.ref || { x: 0, y: 0 }; var scale = options.scale || 1; // fix for safari / chrome / firefox bug not correctly // resetting stroke dash array if (attrs.strokeDasharray === 'none') { attrs.strokeDasharray = [ 10000, 1 ]; } var marker = create$1('marker'); attr(options.element, attrs); append(marker, options.element); attr(marker, { id: id, viewBox: '0 0 20 20', refX: ref.x, refY: ref.y, markerWidth: 20 * scale, markerHeight: 20 * scale, orient: 'auto' }); var defs = query('defs', canvas._svg); if (!defs) { defs = create$1('defs'); append(canvas._svg, defs); } append(defs, marker); markers[id] = marker; } function colorEscape(str) { // only allow characters and numbers return str.replace(/[^0-9a-zA-z]+/g, '_'); } function marker(type, fill, stroke) { var id = type + '-' + colorEscape(fill) + '-' + colorEscape(stroke) + '-' + rendererId; if (!markers[id]) { createMarker(id, type, fill, stroke); } return 'url(#' + id + ')'; } function createMarker(id, type, fill, stroke) { if (type === 'sequenceflow-end') { var sequenceflowEnd = create$1('path'); attr(sequenceflowEnd, { d: 'M 1 5 L 11 10 L 1 15 Z' }); addMarker(id, { element: sequenceflowEnd, ref: { x: 11, y: 10 }, scale: 0.5, attrs: { fill: stroke, stroke: stroke } }); } if (type === 'messageflow-start') { var messageflowStart = create$1('circle'); attr(messageflowStart, { cx: 6, cy: 6, r: 3.5 }); addMarker(id, { element: messageflowStart, attrs: { fill: fill, stroke: stroke }, ref: { x: 6, y: 6 } }); } if (type === 'messageflow-end') { var messageflowEnd = create$1('path'); attr(messageflowEnd, { d: 'm 1 5 l 0 -3 l 7 3 l -7 3 z' }); addMarker(id, { element: messageflowEnd, attrs: { fill: fill, stroke: stroke, strokeLinecap: 'butt' }, ref: { x: 8.5, y: 5 } }); } if (type === 'association-start') { var associationStart = create$1('path'); attr(associationStart, { d: 'M 11 5 L 1 10 L 11 15' }); addMarker(id, { element: associationStart, attrs: { fill: 'none', stroke: stroke, strokeWidth: 1.5 }, ref: { x: 1, y: 10 }, scale: 0.5 }); } if (type === 'association-end') { var associationEnd = create$1('path'); attr(associationEnd, { d: 'M 1 5 L 11 10 L 1 15' }); addMarker(id, { element: associationEnd, attrs: { fill: 'none', stroke: stroke, strokeWidth: 1.5 }, ref: { x: 12, y: 10 }, scale: 0.5 }); } if (type === 'conditional-flow-marker') { var conditionalflowMarker = create$1('path'); attr(conditionalflowMarker, { d: 'M 0 10 L 8 6 L 16 10 L 8 14 Z' }); addMarker(id, { element: conditionalflowMarker, attrs: { fill: fill, stroke: stroke }, ref: { x: -1, y: 10 }, scale: 0.5 }); } if (type === 'conditional-default-flow-marker') { var conditionaldefaultflowMarker = create$1('path'); attr(conditionaldefaultflowMarker, { d: 'M 6 4 L 10 16' }); addMarker(id, { element: conditionaldefaultflowMarker, attrs: { stroke: stroke }, ref: { x: 0, y: 10 }, scale: 0.5 }); } } function drawCircle(parentGfx, width, height, offset, attrs) { if (isObject(offset)) { attrs = offset; offset = 0; } offset = offset || 0; attrs = computeStyle(attrs, { stroke: black, strokeWidth: 2, fill: 'white' }); if (attrs.fill === 'none') { delete attrs.fillOpacity; } var cx = width / 2, cy = height / 2; var circle = create$1('circle'); attr(circle, { cx: cx, cy: cy, r: Math.round((width + height) / 4 - offset) }); attr(circle, attrs); append(parentGfx, circle); return circle; } function drawRect(parentGfx, width, height, r, offset, attrs) { if (isObject(offset)) { attrs = offset; offset = 0; } offset = offset || 0; attrs = computeStyle(attrs, { stroke: black, strokeWidth: 2, fill: 'white' }); var rect = create$1('rect'); attr(rect, { x: offset, y: offset, width: width - offset * 2, height: height - offset * 2, rx: r, ry: r }); attr(rect, attrs); append(parentGfx, rect); return rect; } function drawDiamond(parentGfx, width, height, attrs) { var x_2 = width / 2; var y_2 = height / 2; var points = [ { x: x_2, y: 0 }, { x: width, y: y_2 }, { x: x_2, y: height }, { x: 0, y: y_2 } ]; var pointsString = points.map(function(point) { return point.x + ',' + point.y; }).join(' '); attrs = computeStyle(attrs, { stroke: black, strokeWidth: 2, fill: 'white' }); var polygon = create$1('polygon'); attr(polygon, { points: pointsString }); attr(polygon, attrs); append(parentGfx, polygon); return polygon; } function drawLine(parentGfx, waypoints, attrs) { attrs = computeStyle(attrs, [ 'no-fill' ], { stroke: black, strokeWidth: 2, fill: 'none' }); var line = createLine(waypoints, attrs); append(parentGfx, line); return line; } function drawPath(parentGfx, d, attrs) { attrs = computeStyle(attrs, [ 'no-fill' ], { strokeWidth: 2, stroke: black }); var path = create$1('path'); attr(path, { d: d }); attr(path, attrs); append(parentGfx, path); return path; } function drawMarker(type, parentGfx, path, attrs) { return drawPath(parentGfx, path, assign({ 'data-marker': type }, attrs)); } function renderer(type) { return handlers[type]; } function as(type) { return function(parentGfx, element) { return renderer(type)(parentGfx, element); }; } function renderEventContent(element, parentGfx) { var event = getSemantic(element); var isThrowing = isThrowEvent(event); if (event.eventDefinitions && event.eventDefinitions.length > 1) { if (event.parallelMultiple) { return renderer('bpmn:ParallelMultipleEventDefinition')(parentGfx, element, isThrowing); } else { return renderer('bpmn:MultipleEventDefinition')(parentGfx, element, isThrowing); } } if (isTypedEvent(event, 'bpmn:MessageEventDefinition')) { return renderer('bpmn:MessageEventDefinition')(parentGfx, element, isThrowing); } if (isTypedEvent(event, 'bpmn:TimerEventDefinition')) { return renderer('bpmn:TimerEventDefinition')(parentGfx, element, isThrowing); } if (isTypedEvent(event, 'bpmn:ConditionalEventDefinition')) { return renderer('bpmn:ConditionalEventDefinition')(parentGfx, element); } if (isTypedEvent(event, 'bpmn:SignalEventDefinition')) { return renderer('bpmn:SignalEventDefinition')(parentGfx, element, isThrowing); } if (isTypedEvent(event, 'bpmn:EscalationEventDefinition')) { return renderer('bpmn:EscalationEventDefinition')(parentGfx, element, isThrowing); } if (isTypedEvent(event, 'bpmn:LinkEventDefinition')) { return renderer('bpmn:LinkEventDefinition')(parentGfx, element, isThrowing); } if (isTypedEvent(event, 'bpmn:ErrorEventDefinition')) { return renderer('bpmn:ErrorEventDefinition')(parentGfx, element, isThrowing); } if (isTypedEvent(event, 'bpmn:CancelEventDefinition')) { return renderer('bpmn:CancelEventDefinition')(parentGfx, element, isThrowing); } if (isTypedEvent(event, 'bpmn:CompensateEventDefinition')) { return renderer('bpmn:CompensateEventDefinition')(parentGfx, element, isThrowing); } if (isTypedEvent(event, 'bpmn:TerminateEventDefinition')) { return renderer('bpmn:TerminateEventDefinition')(parentGfx, element, isThrowing); } return null; } function renderLabel(parentGfx, label, options) { options = assign({ size: { width: 100 } }, options); var text = textRenderer.createText(label || '', options); classes(text).add('djs-label'); append(parentGfx, text); return text; } function renderEmbeddedLabel(parentGfx, element, align) { var semantic = getSemantic(element); return renderLabel(parentGfx, semantic.name, { box: element, align: align, padding: 5, style: { fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor) } }); } function renderExternalLabel(parentGfx, element) { var box = { width: 90, height: 30, x: element.width / 2 + element.x, y: element.height / 2 + element.y }; return renderLabel(parentGfx, getLabel(element), { box: box, fitBox: true, style: assign( {}, textRenderer.getExternalStyle(), { fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor) } ) }); } function renderLaneLabel(parentGfx, text, element) { var textBox = renderLabel(parentGfx, text, { box: { height: 30, width: element.height }, align: 'center-middle', style: { fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor) } }); var top = -1 * element.height; transform(textBox, 0, -top, 270); } function createPathFromConnection(connection) { var waypoints = connection.waypoints; var pathData = 'm ' + waypoints[0].x + ',' + waypoints[0].y; for (var i = 1; i < waypoints.length; i++) { pathData += 'L' + waypoints[i].x + ',' + waypoints[i].y + ' '; } return pathData; } var handlers = this.handlers = { 'bpmn:Event': function(parentGfx, element, attrs) { if (!('fillOpacity' in attrs)) { attrs.fillOpacity = DEFAULT_FILL_OPACITY; } return drawCircle(parentGfx, element.width, element.height, attrs); }, 'bpmn:StartEvent': function(parentGfx, element) { var attrs = { fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }; var semantic = getSemantic(element); if (!semantic.isInterrupting) { attrs = { strokeDasharray: '6', strokeLinecap: 'round', fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }; } var circle = renderer('bpmn:Event')(parentGfx, element, attrs); renderEventContent(element, parentGfx); return circle; }, 'bpmn:MessageEventDefinition': function(parentGfx, element, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_MESSAGE', { xScaleFactor: 0.9, yScaleFactor: 0.9, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.235, my: 0.315 } }); var fill = isThrowing ? getStrokeColor$1(element, defaultStrokeColor) : getFillColor(element, defaultFillColor); var stroke = isThrowing ? getFillColor(element, defaultFillColor) : getStrokeColor$1(element, defaultStrokeColor); var messagePath = drawPath(parentGfx, pathData, { strokeWidth: 1, fill: fill, stroke: stroke }); return messagePath; }, 'bpmn:TimerEventDefinition': function(parentGfx, element) { var circle = drawCircle(parentGfx, element.width, element.height, 0.2 * element.height, { strokeWidth: 2, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); var pathData = pathMap.getScaledPath('EVENT_TIMER_WH', { xScaleFactor: 0.75, yScaleFactor: 0.75, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.5, my: 0.5 } }); drawPath(parentGfx, pathData, { strokeWidth: 2, strokeLinecap: 'square', stroke: getStrokeColor$1(element, defaultStrokeColor) }); for (var i = 0;i < 12; i++) { var linePathData = pathMap.getScaledPath('EVENT_TIMER_LINE', { xScaleFactor: 0.75, yScaleFactor: 0.75, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.5, my: 0.5 } }); var width = element.width / 2; var height = element.height / 2; drawPath(parentGfx, linePathData, { strokeWidth: 1, strokeLinecap: 'square', transform: 'rotate(' + (i * 30) + ',' + height + ',' + width + ')', stroke: getStrokeColor$1(element, defaultStrokeColor) }); } return circle; }, 'bpmn:EscalationEventDefinition': function(parentGfx, event, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_ESCALATION', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: event.width, containerHeight: event.height, position: { mx: 0.5, my: 0.2 } }); var fill = isThrowing ? getStrokeColor$1(event, defaultStrokeColor) : 'none'; return drawPath(parentGfx, pathData, { strokeWidth: 1, fill: fill, stroke: getStrokeColor$1(event, defaultStrokeColor) }); }, 'bpmn:ConditionalEventDefinition': function(parentGfx, event) { var pathData = pathMap.getScaledPath('EVENT_CONDITIONAL', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: event.width, containerHeight: event.height, position: { mx: 0.5, my: 0.222 } }); return drawPath(parentGfx, pathData, { strokeWidth: 1, stroke: getStrokeColor$1(event, defaultStrokeColor) }); }, 'bpmn:LinkEventDefinition': function(parentGfx, event, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_LINK', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: event.width, containerHeight: event.height, position: { mx: 0.57, my: 0.263 } }); var fill = isThrowing ? getStrokeColor$1(event, defaultStrokeColor) : 'none'; return drawPath(parentGfx, pathData, { strokeWidth: 1, fill: fill, stroke: getStrokeColor$1(event, defaultStrokeColor) }); }, 'bpmn:ErrorEventDefinition': function(parentGfx, event, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_ERROR', { xScaleFactor: 1.1, yScaleFactor: 1.1, containerWidth: event.width, containerHeight: event.height, position: { mx: 0.2, my: 0.722 } }); var fill = isThrowing ? getStrokeColor$1(event, defaultStrokeColor) : 'none'; return drawPath(parentGfx, pathData, { strokeWidth: 1, fill: fill, stroke: getStrokeColor$1(event, defaultStrokeColor) }); }, 'bpmn:CancelEventDefinition': function(parentGfx, event, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_CANCEL_45', { xScaleFactor: 1.0, yScaleFactor: 1.0, containerWidth: event.width, containerHeight: event.height, position: { mx: 0.638, my: -0.055 } }); var fill = isThrowing ? getStrokeColor$1(event, defaultStrokeColor) : 'none'; var path = drawPath(parentGfx, pathData, { strokeWidth: 1, fill: fill, stroke: getStrokeColor$1(event, defaultStrokeColor) }); rotate(path, 45); return path; }, 'bpmn:CompensateEventDefinition': function(parentGfx, event, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_COMPENSATION', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: event.width, containerHeight: event.height, position: { mx: 0.22, my: 0.5 } }); var fill = isThrowing ? getStrokeColor$1(event, defaultStrokeColor) : 'none'; return drawPath(parentGfx, pathData, { strokeWidth: 1, fill: fill, stroke: getStrokeColor$1(event, defaultStrokeColor) }); }, 'bpmn:SignalEventDefinition': function(parentGfx, event, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_SIGNAL', { xScaleFactor: 0.9, yScaleFactor: 0.9, containerWidth: event.width, containerHeight: event.height, position: { mx: 0.5, my: 0.2 } }); var fill = isThrowing ? getStrokeColor$1(event, defaultStrokeColor) : 'none'; return drawPath(parentGfx, pathData, { strokeWidth: 1, fill: fill, stroke: getStrokeColor$1(event, defaultStrokeColor) }); }, 'bpmn:MultipleEventDefinition': function(parentGfx, event, isThrowing) { var pathData = pathMap.getScaledPath('EVENT_MULTIPLE', { xScaleFactor: 1.1, yScaleFactor: 1.1, containerWidth: event.width, containerHeight: event.height, position: { mx: 0.222, my: 0.36 } }); var fill = isThrowing ? getStrokeColor$1(event, defaultStrokeColor) : 'none'; return drawPath(parentGfx, pathData, { strokeWidth: 1, fill: fill }); }, 'bpmn:ParallelMultipleEventDefinition': function(parentGfx, event) { var pathData = pathMap.getScaledPath('EVENT_PARALLEL_MULTIPLE', { xScaleFactor: 1.2, yScaleFactor: 1.2, containerWidth: event.width, containerHeight: event.height, position: { mx: 0.458, my: 0.194 } }); return drawPath(parentGfx, pathData, { strokeWidth: 1, fill: getStrokeColor$1(event, defaultStrokeColor), stroke: getStrokeColor$1(event, defaultStrokeColor) }); }, 'bpmn:EndEvent': function(parentGfx, element) { var circle = renderer('bpmn:Event')(parentGfx, element, { strokeWidth: 4, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); renderEventContent(element, parentGfx); return circle; }, 'bpmn:TerminateEventDefinition': function(parentGfx, element) { var circle = drawCircle(parentGfx, element.width, element.height, 8, { strokeWidth: 4, fill: getStrokeColor$1(element, defaultStrokeColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); return circle; }, 'bpmn:IntermediateEvent': function(parentGfx, element) { var outer = renderer('bpmn:Event')(parentGfx, element, { strokeWidth: 1, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); /* inner */ drawCircle(parentGfx, element.width, element.height, INNER_OUTER_DIST, { strokeWidth: 1, fill: getFillColor(element, 'none'), stroke: getStrokeColor$1(element, defaultStrokeColor) }); renderEventContent(element, parentGfx); return outer; }, 'bpmn:IntermediateCatchEvent': as('bpmn:IntermediateEvent'), 'bpmn:IntermediateThrowEvent': as('bpmn:IntermediateEvent'), 'bpmn:Activity': function(parentGfx, element, attrs) { attrs = attrs || {}; if (!('fillOpacity' in attrs)) { attrs.fillOpacity = DEFAULT_FILL_OPACITY; } return drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS, attrs); }, 'bpmn:Task': function(parentGfx, element) { var attrs = { fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }; var rect = renderer('bpmn:Activity')(parentGfx, element, attrs); renderEmbeddedLabel(parentGfx, element, 'center-middle'); attachTaskMarkers(parentGfx, element); return rect; }, 'bpmn:ServiceTask': function(parentGfx, element) { var task = renderer('bpmn:Task')(parentGfx, element); var pathDataBG = pathMap.getScaledPath('TASK_TYPE_SERVICE', { abspos: { x: 12, y: 18 } }); /* service bg */ drawPath(parentGfx, pathDataBG, { strokeWidth: 1, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); var fillPathData = pathMap.getScaledPath('TASK_TYPE_SERVICE_FILL', { abspos: { x: 17.2, y: 18 } }); /* service fill */ drawPath(parentGfx, fillPathData, { strokeWidth: 0, fill: getFillColor(element, defaultFillColor) }); var pathData = pathMap.getScaledPath('TASK_TYPE_SERVICE', { abspos: { x: 17, y: 22 } }); /* service */ drawPath(parentGfx, pathData, { strokeWidth: 1, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); return task; }, 'bpmn:UserTask': function(parentGfx, element) { var task = renderer('bpmn:Task')(parentGfx, element); var x = 15; var y = 12; var pathData = pathMap.getScaledPath('TASK_TYPE_USER_1', { abspos: { x: x, y: y } }); /* user path */ drawPath(parentGfx, pathData, { strokeWidth: 0.5, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); var pathData2 = pathMap.getScaledPath('TASK_TYPE_USER_2', { abspos: { x: x, y: y } }); /* user2 path */ drawPath(parentGfx, pathData2, { strokeWidth: 0.5, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); var pathData3 = pathMap.getScaledPath('TASK_TYPE_USER_3', { abspos: { x: x, y: y } }); /* user3 path */ drawPath(parentGfx, pathData3, { strokeWidth: 0.5, fill: getStrokeColor$1(element, defaultStrokeColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); return task; }, 'bpmn:ManualTask': function(parentGfx, element) { var task = renderer('bpmn:Task')(parentGfx, element); var pathData = pathMap.getScaledPath('TASK_TYPE_MANUAL', { abspos: { x: 17, y: 15 } }); /* manual path */ drawPath(parentGfx, pathData, { strokeWidth: 0.5, // 0.25, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); return task; }, 'bpmn:SendTask': function(parentGfx, element) { var task = renderer('bpmn:Task')(parentGfx, element); var pathData = pathMap.getScaledPath('TASK_TYPE_SEND', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: 21, containerHeight: 14, position: { mx: 0.285, my: 0.357 } }); /* send path */ drawPath(parentGfx, pathData, { strokeWidth: 1, fill: getStrokeColor$1(element, defaultStrokeColor), stroke: getFillColor(element, defaultFillColor) }); return task; }, 'bpmn:ReceiveTask' : function(parentGfx, element) { var semantic = getSemantic(element); var task = renderer('bpmn:Task')(parentGfx, element); var pathData; if (semantic.instantiate) { drawCircle(parentGfx, 28, 28, 20 * 0.22, { strokeWidth: 1 }); pathData = pathMap.getScaledPath('TASK_TYPE_INSTANTIATING_SEND', { abspos: { x: 7.77, y: 9.52 } }); } else { pathData = pathMap.getScaledPath('TASK_TYPE_SEND', { xScaleFactor: 0.9, yScaleFactor: 0.9, containerWidth: 21, containerHeight: 14, position: { mx: 0.3, my: 0.4 } }); } /* receive path */ drawPath(parentGfx, pathData, { strokeWidth: 1, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); return task; }, 'bpmn:ScriptTask': function(parentGfx, element) { var task = renderer('bpmn:Task')(parentGfx, element); var pathData = pathMap.getScaledPath('TASK_TYPE_SCRIPT', { abspos: { x: 15, y: 20 } }); /* script path */ drawPath(parentGfx, pathData, { strokeWidth: 1, stroke: getStrokeColor$1(element, defaultStrokeColor) }); return task; }, 'bpmn:BusinessRuleTask': function(parentGfx, element) { var task = renderer('bpmn:Task')(parentGfx, element); var headerPathData = pathMap.getScaledPath('TASK_TYPE_BUSINESS_RULE_HEADER', { abspos: { x: 8, y: 8 } }); var businessHeaderPath = drawPath(parentGfx, headerPathData); attr(businessHeaderPath, { strokeWidth: 1, fill: getFillColor(element, '#aaaaaa'), stroke: getStrokeColor$1(element, defaultStrokeColor) }); var headerData = pathMap.getScaledPath('TASK_TYPE_BUSINESS_RULE_MAIN', { abspos: { x: 8, y: 8 } }); var businessPath = drawPath(parentGfx, headerData); attr(businessPath, { strokeWidth: 1, stroke: getStrokeColor$1(element, defaultStrokeColor) }); return task; }, 'bpmn:SubProcess': function(parentGfx, element, attrs) { attrs = assign({ fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }, attrs); var rect = renderer('bpmn:Activity')(parentGfx, element, attrs); var expanded = isExpanded(element); if (isEventSubProcess(element)) { attr(rect, { strokeDasharray: '1,2' }); } renderEmbeddedLabel(parentGfx, element, expanded ? 'center-top' : 'center-middle'); if (expanded) { attachTaskMarkers(parentGfx, element); } else { attachTaskMarkers(parentGfx, element, [ 'SubProcessMarker' ]); } return rect; }, 'bpmn:AdHocSubProcess': function(parentGfx, element) { return renderer('bpmn:SubProcess')(parentGfx, element); }, 'bpmn:Transaction': function(parentGfx, element) { var outer = renderer('bpmn:SubProcess')(parentGfx, element); var innerAttrs = styles.style([ 'no-fill', 'no-events' ], { stroke: getStrokeColor$1(element, defaultStrokeColor) }); /* inner path */ drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS - 2, INNER_OUTER_DIST, innerAttrs); return outer; }, 'bpmn:CallActivity': function(parentGfx, element) { return renderer('bpmn:SubProcess')(parentGfx, element, { strokeWidth: 5 }); }, 'bpmn:Participant': function(parentGfx, element) { var attrs = { fillOpacity: DEFAULT_FILL_OPACITY, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }; var lane = renderer('bpmn:Lane')(parentGfx, element, attrs); var expandedPool = isExpanded(element); if (expandedPool) { drawLine(parentGfx, [ { x: 30, y: 0 }, { x: 30, y: element.height } ], { stroke: getStrokeColor$1(element, defaultStrokeColor) }); var text = getSemantic(element).name; renderLaneLabel(parentGfx, text, element); } else { // Collapsed pool draw text inline var text2 = getSemantic(element).name; renderLabel(parentGfx, text2, { box: element, align: 'center-middle', style: { fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor) } }); } var participantMultiplicity = !!(getSemantic(element).participantMultiplicity); if (participantMultiplicity) { renderer('ParticipantMultiplicityMarker')(parentGfx, element); } return lane; }, 'bpmn:Lane': function(parentGfx, element, attrs) { var rect = drawRect(parentGfx, element.width, element.height, 0, assign({ fill: getFillColor(element, defaultFillColor), fillOpacity: HIGH_FILL_OPACITY, stroke: getStrokeColor$1(element, defaultStrokeColor) }, attrs)); var semantic = getSemantic(element); if (semantic.$type === 'bpmn:Lane') { var text = semantic.name; renderLaneLabel(parentGfx, text, element); } return rect; }, 'bpmn:InclusiveGateway': function(parentGfx, element) { var diamond = renderer('bpmn:Gateway')(parentGfx, element); /* circle path */ drawCircle(parentGfx, element.width, element.height, element.height * 0.24, { strokeWidth: 2.5, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); return diamond; }, 'bpmn:ExclusiveGateway': function(parentGfx, element) { var diamond = renderer('bpmn:Gateway')(parentGfx, element); var pathData = pathMap.getScaledPath('GATEWAY_EXCLUSIVE', { xScaleFactor: 0.4, yScaleFactor: 0.4, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.32, my: 0.3 } }); if ((getDi(element).isMarkerVisible)) { drawPath(parentGfx, pathData, { strokeWidth: 1, fill: getStrokeColor$1(element, defaultStrokeColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); } return diamond; }, 'bpmn:ComplexGateway': function(parentGfx, element) { var diamond = renderer('bpmn:Gateway')(parentGfx, element); var pathData = pathMap.getScaledPath('GATEWAY_COMPLEX', { xScaleFactor: 0.5, yScaleFactor:0.5, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.46, my: 0.26 } }); /* complex path */ drawPath(parentGfx, pathData, { strokeWidth: 1, fill: getStrokeColor$1(element, defaultStrokeColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); return diamond; }, 'bpmn:ParallelGateway': function(parentGfx, element) { var diamond = renderer('bpmn:Gateway')(parentGfx, element); var pathData = pathMap.getScaledPath('GATEWAY_PARALLEL', { xScaleFactor: 0.6, yScaleFactor:0.6, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.46, my: 0.2 } }); /* parallel path */ drawPath(parentGfx, pathData, { strokeWidth: 1, fill: getStrokeColor$1(element, defaultStrokeColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); return diamond; }, 'bpmn:EventBasedGateway': function(parentGfx, element) { var semantic = getSemantic(element); var diamond = renderer('bpmn:Gateway')(parentGfx, element); /* outer circle path */ drawCircle(parentGfx, element.width, element.height, element.height * 0.20, { strokeWidth: 1, fill: 'none', stroke: getStrokeColor$1(element, defaultStrokeColor) }); var type = semantic.eventGatewayType; var instantiate = !!semantic.instantiate; function drawEvent() { var pathData = pathMap.getScaledPath('GATEWAY_EVENT_BASED', { xScaleFactor: 0.18, yScaleFactor: 0.18, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.36, my: 0.44 } }); var attrs = { strokeWidth: 2, fill: getFillColor(element, 'none'), stroke: getStrokeColor$1(element, defaultStrokeColor) }; /* event path */ drawPath(parentGfx, pathData, attrs); } if (type === 'Parallel') { var pathData = pathMap.getScaledPath('GATEWAY_PARALLEL', { xScaleFactor: 0.4, yScaleFactor:0.4, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.474, my: 0.296 } }); var parallelPath = drawPath(parentGfx, pathData); attr(parallelPath, { strokeWidth: 1, fill: 'none' }); } else if (type === 'Exclusive') { if (!instantiate) { var innerCircle = drawCircle(parentGfx, element.width, element.height, element.height * 0.26); attr(innerCircle, { strokeWidth: 1, fill: 'none', stroke: getStrokeColor$1(element, defaultStrokeColor) }); } drawEvent(); } return diamond; }, 'bpmn:Gateway': function(parentGfx, element) { var attrs = { fill: getFillColor(element, defaultFillColor), fillOpacity: DEFAULT_FILL_OPACITY, stroke: getStrokeColor$1(element, defaultStrokeColor) }; return drawDiamond(parentGfx, element.width, element.height, attrs); }, 'bpmn:SequenceFlow': function(parentGfx, element) { var pathData = createPathFromConnection(element); var fill = getFillColor(element, defaultFillColor), stroke = getStrokeColor$1(element, defaultStrokeColor); var attrs = { strokeLinejoin: 'round', markerEnd: marker('sequenceflow-end', fill, stroke), stroke: getStrokeColor$1(element, defaultStrokeColor) }; var path = drawPath(parentGfx, pathData, attrs); var sequenceFlow = getSemantic(element); var source; if (element.source) { source = element.source.businessObject; // conditional flow marker if (sequenceFlow.conditionExpression && source.$instanceOf('bpmn:Activity')) { attr(path, { markerStart: marker('conditional-flow-marker', fill, stroke) }); } // default marker if (source.default && (source.$instanceOf('bpmn:Gateway') || source.$instanceOf('bpmn:Activity')) && source.default === sequenceFlow) { attr(path, { markerStart: marker('conditional-default-flow-marker', fill, stroke) }); } } return path; }, 'bpmn:Association': function(parentGfx, element, attrs) { var semantic = getSemantic(element); var fill = getFillColor(element, defaultFillColor), stroke = getStrokeColor$1(element, defaultStrokeColor); attrs = assign({ strokeDasharray: '0.5, 5', strokeLinecap: 'round', strokeLinejoin: 'round', stroke: getStrokeColor$1(element, defaultStrokeColor) }, attrs || {}); if (semantic.associationDirection === 'One' || semantic.associationDirection === 'Both') { attrs.markerEnd = marker('association-end', fill, stroke); } if (semantic.associationDirection === 'Both') { attrs.markerStart = marker('association-start', fill, stroke); } return drawLine(parentGfx, element.waypoints, attrs); }, 'bpmn:DataInputAssociation': function(parentGfx, element) { var fill = getFillColor(element, defaultFillColor), stroke = getStrokeColor$1(element, defaultStrokeColor); return renderer('bpmn:Association')(parentGfx, element, { markerEnd: marker('association-end', fill, stroke) }); }, 'bpmn:DataOutputAssociation': function(parentGfx, element) { var fill = getFillColor(element, defaultFillColor), stroke = getStrokeColor$1(element, defaultStrokeColor); return renderer('bpmn:Association')(parentGfx, element, { markerEnd: marker('association-end', fill, stroke) }); }, 'bpmn:MessageFlow': function(parentGfx, element) { var semantic = getSemantic(element), di = getDi(element); var fill = getFillColor(element, defaultFillColor), stroke = getStrokeColor$1(element, defaultStrokeColor); var pathData = createPathFromConnection(element); var attrs = { markerEnd: marker('messageflow-end', fill, stroke), markerStart: marker('messageflow-start', fill, stroke), strokeDasharray: '10, 12', strokeLinecap: 'round', strokeLinejoin: 'round', strokeWidth: '1.5px', stroke: getStrokeColor$1(element, defaultStrokeColor) }; var path = drawPath(parentGfx, pathData, attrs); if (semantic.messageRef) { var midPoint = path.getPointAtLength(path.getTotalLength() / 2); var markerPathData = pathMap.getScaledPath('MESSAGE_FLOW_MARKER', { abspos: { x: midPoint.x, y: midPoint.y } }); var messageAttrs = { strokeWidth: 1 }; if (di.messageVisibleKind === 'initiating') { messageAttrs.fill = 'white'; messageAttrs.stroke = black; } else { messageAttrs.fill = '#888'; messageAttrs.stroke = 'white'; } var message = drawPath(parentGfx, markerPathData, messageAttrs); var labelText = semantic.messageRef.name; var label = renderLabel(parentGfx, labelText, { align: 'center-top', fitBox: true, style: { fill: getStrokeColor$1(element, defaultLabelColor) } }); var messageBounds = message.getBBox(), labelBounds = label.getBBox(); var translateX = midPoint.x - labelBounds.width / 2, translateY = midPoint.y + messageBounds.height / 2 + ELEMENT_LABEL_DISTANCE$1; transform(label, translateX, translateY, 0); } return path; }, 'bpmn:DataObject': function(parentGfx, element) { var pathData = pathMap.getScaledPath('DATA_OBJECT_PATH', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.474, my: 0.296 } }); var elementObject = drawPath(parentGfx, pathData, { fill: getFillColor(element, defaultFillColor), fillOpacity: DEFAULT_FILL_OPACITY, stroke: getStrokeColor$1(element, defaultStrokeColor) }); var semantic = getSemantic(element); if (isCollection(semantic)) { renderDataItemCollection(parentGfx, element); } return elementObject; }, 'bpmn:DataObjectReference': as('bpmn:DataObject'), 'bpmn:DataInput': function(parentGfx, element) { var arrowPathData = pathMap.getRawPath('DATA_ARROW'); // page var elementObject = renderer('bpmn:DataObject')(parentGfx, element); /* input arrow path */ drawPath(parentGfx, arrowPathData, { strokeWidth: 1 }); return elementObject; }, 'bpmn:DataOutput': function(parentGfx, element) { var arrowPathData = pathMap.getRawPath('DATA_ARROW'); // page var elementObject = renderer('bpmn:DataObject')(parentGfx, element); /* output arrow path */ drawPath(parentGfx, arrowPathData, { strokeWidth: 1, fill: black }); return elementObject; }, 'bpmn:DataStoreReference': function(parentGfx, element) { var DATA_STORE_PATH = pathMap.getScaledPath('DATA_STORE', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: 0, my: 0.133 } }); var elementStore = drawPath(parentGfx, DATA_STORE_PATH, { strokeWidth: 2, fill: getFillColor(element, defaultFillColor), fillOpacity: DEFAULT_FILL_OPACITY, stroke: getStrokeColor$1(element, defaultStrokeColor) }); return elementStore; }, 'bpmn:BoundaryEvent': function(parentGfx, element) { var semantic = getSemantic(element), cancel = semantic.cancelActivity; var attrs = { strokeWidth: 1, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }; if (!cancel) { attrs.strokeDasharray = '6'; attrs.strokeLinecap = 'round'; } // apply fillOpacity var outerAttrs = assign({}, attrs, { fillOpacity: 1 }); // apply no-fill var innerAttrs = assign({}, attrs, { fill: 'none' }); var outer = renderer('bpmn:Event')(parentGfx, element, outerAttrs); /* inner path */ drawCircle(parentGfx, element.width, element.height, INNER_OUTER_DIST, innerAttrs); renderEventContent(element, parentGfx); return outer; }, 'bpmn:Group': function(parentGfx, element) { var group = drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS, { stroke: getStrokeColor$1(element, defaultStrokeColor), strokeWidth: 1, strokeDasharray: '8,3,1,3', fill: 'none', pointerEvents: 'none' }); return group; }, 'label': function(parentGfx, element) { return renderExternalLabel(parentGfx, element); }, 'bpmn:TextAnnotation': function(parentGfx, element) { var style = { 'fill': 'none', 'stroke': 'none' }; var textElement = drawRect(parentGfx, element.width, element.height, 0, 0, style); var textPathData = pathMap.getScaledPath('TEXT_ANNOTATION', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.0, my: 0.0 } }); drawPath(parentGfx, textPathData, { stroke: getStrokeColor$1(element, defaultStrokeColor) }); var text = getSemantic(element).text || ''; renderLabel(parentGfx, text, { box: element, align: 'left-top', padding: 5, style: { fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor) } }); return textElement; }, 'ParticipantMultiplicityMarker': function(parentGfx, element) { var markerPath = pathMap.getScaledPath('MARKER_PARALLEL', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: ((element.width / 2) / element.width), my: (element.height - 15) / element.height } }); drawMarker('participant-multiplicity', parentGfx, markerPath, { strokeWidth: 2, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); }, 'SubProcessMarker': function(parentGfx, element) { var markerRect = drawRect(parentGfx, 14, 14, 0, { strokeWidth: 1, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); // Process marker is placed in the middle of the box // therefore fixed values can be used here translate$2(markerRect, element.width / 2 - 7.5, element.height - 20); var markerPath = pathMap.getScaledPath('MARKER_SUB_PROCESS', { xScaleFactor: 1.5, yScaleFactor: 1.5, containerWidth: element.width, containerHeight: element.height, position: { mx: (element.width / 2 - 7.5) / element.width, my: (element.height - 20) / element.height } }); drawMarker('sub-process', parentGfx, markerPath, { fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); }, 'ParallelMarker': function(parentGfx, element, position) { var markerPath = pathMap.getScaledPath('MARKER_PARALLEL', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: ((element.width / 2 + position.parallel) / element.width), my: (element.height - 20) / element.height } }); drawMarker('parallel', parentGfx, markerPath, { fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); }, 'SequentialMarker': function(parentGfx, element, position) { var markerPath = pathMap.getScaledPath('MARKER_SEQUENTIAL', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: ((element.width / 2 + position.seq) / element.width), my: (element.height - 19) / element.height } }); drawMarker('sequential', parentGfx, markerPath, { fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); }, 'CompensationMarker': function(parentGfx, element, position) { var markerMath = pathMap.getScaledPath('MARKER_COMPENSATION', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: ((element.width / 2 + position.compensation) / element.width), my: (element.height - 13) / element.height } }); drawMarker('compensation', parentGfx, markerMath, { strokeWidth: 1, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); }, 'LoopMarker': function(parentGfx, element, position) { var markerPath = pathMap.getScaledPath('MARKER_LOOP', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: ((element.width / 2 + position.loop) / element.width), my: (element.height - 7) / element.height } }); drawMarker('loop', parentGfx, markerPath, { strokeWidth: 1, fill: getFillColor(element, defaultFillColor), stroke: getStrokeColor$1(element, defaultStrokeColor), strokeLinecap: 'round', strokeMiterlimit: 0.5 }); }, 'AdhocMarker': function(parentGfx, element, position) { var markerPath = pathMap.getScaledPath('MARKER_ADHOC', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: ((element.width / 2 + position.adhoc) / element.width), my: (element.height - 15) / element.height } }); drawMarker('adhoc', parentGfx, markerPath, { strokeWidth: 1, fill: getStrokeColor$1(element, defaultStrokeColor), stroke: getStrokeColor$1(element, defaultStrokeColor) }); } }; function attachTaskMarkers(parentGfx, element, taskMarkers) { var obj = getSemantic(element); var subprocess = taskMarkers && taskMarkers.indexOf('SubProcessMarker') !== -1; var position; if (subprocess) { position = { seq: -21, parallel: -22, compensation: -42, loop: -18, adhoc: 10 }; } else { position = { seq: -3, parallel: -6, compensation: -27, loop: 0, adhoc: 10 }; } forEach$1(taskMarkers, function(marker) { renderer(marker)(parentGfx, element, position); }); if (obj.isForCompensation) { renderer('CompensationMarker')(parentGfx, element, position); } if (obj.$type === 'bpmn:AdHocSubProcess') { renderer('AdhocMarker')(parentGfx, element, position); } var loopCharacteristics = obj.loopCharacteristics, isSequential = loopCharacteristics && loopCharacteristics.isSequential; if (loopCharacteristics) { if (isSequential === undefined) { renderer('LoopMarker')(parentGfx, element, position); } if (isSequential === false) { renderer('ParallelMarker')(parentGfx, element, position); } if (isSequential === true) { renderer('SequentialMarker')(parentGfx, element, position); } } } function renderDataItemCollection(parentGfx, element) { var yPosition = (element.height - 18) / element.height; var pathData = pathMap.getScaledPath('DATA_OBJECT_COLLECTION_PATH', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.33, my: yPosition } }); /* collection path */ drawPath(parentGfx, pathData, { strokeWidth: 2 }); } // extension API, use at your own risk this._drawPath = drawPath; this._renderer = renderer; } e(BpmnRenderer, BaseRenderer); BpmnRenderer.$inject = [ 'config.bpmnRenderer', 'eventBus', 'styles', 'pathMap', 'canvas', 'textRenderer' ]; BpmnRenderer.prototype.canRender = function(element) { return is$1(element, 'bpmn:BaseElement'); }; BpmnRenderer.prototype.drawShape = function(parentGfx, element) { var type = element.type; var h = this._renderer(type); /* jshint -W040 */ return h(parentGfx, element); }; BpmnRenderer.prototype.drawConnection = function(parentGfx, element) { var type = element.type; var h = this._renderer(type); /* jshint -W040 */ return h(parentGfx, element); }; BpmnRenderer.prototype.getShapePath = function(element) { if (is$1(element, 'bpmn:Event')) { return getCirclePath(element); } if (is$1(element, 'bpmn:Activity')) { return getRoundRectPath(element, TASK_BORDER_RADIUS); } if (is$1(element, 'bpmn:Gateway')) { return getDiamondPath(element); } return getRectPath(element); }; var DEFAULT_BOX_PADDING = 0; var DEFAULT_LABEL_SIZE$1 = { width: 150, height: 50 }; function parseAlign(align) { var parts = align.split('-'); return { horizontal: parts[0] || 'center', vertical: parts[1] || 'top' }; } function parsePadding(padding) { if (isObject(padding)) { return assign({ top: 0, left: 0, right: 0, bottom: 0 }, padding); } else { return { top: padding, left: padding, right: padding, bottom: padding }; } } function getTextBBox(text, fakeText) { fakeText.textContent = text; var textBBox; try { var bbox, emptyLine = text === ''; // add dummy text, when line is empty to // determine correct height fakeText.textContent = emptyLine ? 'dummy' : text; textBBox = fakeText.getBBox(); // take text rendering related horizontal // padding into account bbox = { width: textBBox.width + textBBox.x * 2, height: textBBox.height }; if (emptyLine) { // correct width bbox.width = 0; } return bbox; } catch (e) { return { width: 0, height: 0 }; } } /** * Layout the next line and return the layouted element. * * Alters the lines passed. * * @param {Array} lines * @return {Object} the line descriptor, an object { width, height, text } */ function layoutNext(lines, maxWidth, fakeText) { var originalLine = lines.shift(), fitLine = originalLine; var textBBox; for (;;) { textBBox = getTextBBox(fitLine, fakeText); textBBox.width = fitLine ? textBBox.width : 0; // try to fit if (fitLine === ' ' || fitLine === '' || textBBox.width < Math.round(maxWidth) || fitLine.length < 2) { return fit(lines, fitLine, originalLine, textBBox); } fitLine = shortenLine(fitLine, textBBox.width, maxWidth); } } function fit(lines, fitLine, originalLine, textBBox) { if (fitLine.length < originalLine.length) { var remainder = originalLine.slice(fitLine.length).trim(); lines.unshift(remainder); } return { width: textBBox.width, height: textBBox.height, text: fitLine }; } var SOFT_BREAK = '\u00AD'; /** * Shortens a line based on spacing and hyphens. * Returns the shortened result on success. * * @param {string} line * @param {number} maxLength the maximum characters of the string * @return {string} the shortened string */ function semanticShorten(line, maxLength) { var parts = line.split(/(\s|-|\u00AD)/g), part, shortenedParts = [], length = 0; // try to shorten via break chars if (parts.length > 1) { while ((part = parts.shift())) { if (part.length + length < maxLength) { shortenedParts.push(part); length += part.length; } else { // remove previous part, too if hyphen does not fit anymore if (part === '-' || part === SOFT_BREAK) { shortenedParts.pop(); } break; } } } var last = shortenedParts[shortenedParts.length - 1]; // translate trailing soft break to actual hyphen if (last && last === SOFT_BREAK) { shortenedParts[shortenedParts.length - 1] = '-'; } return shortenedParts.join(''); } function shortenLine(line, width, maxWidth) { var length = Math.max(line.length * (maxWidth / width), 1); // try to shorten semantically (i.e. based on spaces and hyphens) var shortenedLine = semanticShorten(line, length); if (!shortenedLine) { // force shorten by cutting the long word shortenedLine = line.slice(0, Math.max(Math.round(length - 1), 1)); } return shortenedLine; } function getHelperSvg() { var helperSvg = document.getElementById('helper-svg'); if (!helperSvg) { helperSvg = create$1('svg'); attr(helperSvg, { id: 'helper-svg' }); assign$1(helperSvg, { visibility: 'hidden', position: 'fixed', width: 0, height: 0 }); document.body.appendChild(helperSvg); } return helperSvg; } /** * Creates a new label utility * * @param {Object} config * @param {Dimensions} config.size * @param {number} config.padding * @param {Object} config.style * @param {string} config.align */ function Text(config) { this._config = assign({}, { size: DEFAULT_LABEL_SIZE$1, padding: DEFAULT_BOX_PADDING, style: {}, align: 'center-top' }, config || {}); } /** * Returns the layouted text as an SVG element. * * @param {string} text * @param {Object} options * * @return {SVGElement} */ Text.prototype.createText = function(text, options) { return this.layoutText(text, options).element; }; /** * Returns a labels layouted dimensions. * * @param {string} text to layout * @param {Object} options * * @return {Dimensions} */ Text.prototype.getDimensions = function(text, options) { return this.layoutText(text, options).dimensions; }; /** * Creates and returns a label and its bounding box. * * @method Text#createText * * @param {string} text the text to render on the label * @param {Object} options * @param {string} options.align how to align in the bounding box. * Any of { 'center-middle', 'center-top' }, * defaults to 'center-top'. * @param {string} options.style style to be applied to the text * @param {boolean} options.fitBox indicates if box will be recalculated to * fit text * * @return {Object} { element, dimensions } */ Text.prototype.layoutText = function(text, options) { var box = assign({}, this._config.size, options.box), style = assign({}, this._config.style, options.style), align = parseAlign(options.align || this._config.align), padding = parsePadding(options.padding !== undefined ? options.padding : this._config.padding), fitBox = options.fitBox || false; var lineHeight = getLineHeight(style); // we split text by lines and normalize // {soft break} + {line break} => { line break } var lines = text.split(/\u00AD?\r?\n/), layouted = []; var maxWidth = box.width - padding.left - padding.right; // ensure correct rendering by attaching helper text node to invisible SVG var helperText = create$1('text'); attr(helperText, { x: 0, y: 0 }); attr(helperText, style); var helperSvg = getHelperSvg(); append(helperSvg, helperText); while (lines.length) { layouted.push(layoutNext(lines, maxWidth, helperText)); } if (align.vertical === 'middle') { padding.top = padding.bottom = 0; } var totalHeight = reduce(layouted, function(sum, line, idx) { return sum + (lineHeight || line.height); }, 0) + padding.top + padding.bottom; var maxLineWidth = reduce(layouted, function(sum, line, idx) { return line.width > sum ? line.width : sum; }, 0); // the y position of the next line var y = padding.top; if (align.vertical === 'middle') { y += (box.height - totalHeight) / 2; } // magic number initial offset y -= (lineHeight || layouted[0].height) / 4; var textElement = create$1('text'); attr(textElement, style); // layout each line taking into account that parent // shape might resize to fit text size forEach$1(layouted, function(line) { var x; y += (lineHeight || line.height); switch (align.horizontal) { case 'left': x = padding.left; break; case 'right': x = ((fitBox ? maxLineWidth : maxWidth) - padding.right - line.width); break; default: // aka center x = Math.max((((fitBox ? maxLineWidth : maxWidth) - line.width) / 2 + padding.left), 0); } var tspan = create$1('tspan'); attr(tspan, { x: x, y: y }); tspan.textContent = line.text; append(textElement, tspan); }); remove$1(helperText); var dimensions = { width: maxLineWidth, height: totalHeight }; return { dimensions: dimensions, element: textElement }; }; function getLineHeight(style) { if ('fontSize' in style && 'lineHeight' in style) { return style.lineHeight * parseInt(style.fontSize, 10); } } var DEFAULT_FONT_SIZE = 12; var LINE_HEIGHT_RATIO = 1.2; var MIN_TEXT_ANNOTATION_HEIGHT = 30; function TextRenderer(config) { var defaultStyle = assign({ fontFamily: 'Arial, sans-serif', fontSize: DEFAULT_FONT_SIZE, fontWeight: 'normal', lineHeight: LINE_HEIGHT_RATIO }, config && config.defaultStyle || {}); var fontSize = parseInt(defaultStyle.fontSize, 10) - 1; var externalStyle = assign({}, defaultStyle, { fontSize: fontSize }, config && config.externalStyle || {}); var textUtil = new Text({ style: defaultStyle }); /** * Get the new bounds of an externally rendered, * layouted label. * * @param {Bounds} bounds * @param {string} text * * @return {Bounds} */ this.getExternalLabelBounds = function(bounds, text) { var layoutedDimensions = textUtil.getDimensions(text, { box: { width: 90, height: 30, x: bounds.width / 2 + bounds.x, y: bounds.height / 2 + bounds.y }, style: externalStyle }); // resize label shape to fit label text return { x: Math.round(bounds.x + bounds.width / 2 - layoutedDimensions.width / 2), y: Math.round(bounds.y), width: Math.ceil(layoutedDimensions.width), height: Math.ceil(layoutedDimensions.height) }; }; /** * Get the new bounds of text annotation. * * @param {Bounds} bounds * @param {string} text * * @return {Bounds} */ this.getTextAnnotationBounds = function(bounds, text) { var layoutedDimensions = textUtil.getDimensions(text, { box: bounds, style: defaultStyle, align: 'left-top', padding: 5 }); return { x: bounds.x, y: bounds.y, width: bounds.width, height: Math.max(MIN_TEXT_ANNOTATION_HEIGHT, Math.round(layoutedDimensions.height)) }; }; /** * Create a layouted text element. * * @param {string} text * @param {Object} [options] * * @return {SVGElement} rendered text */ this.createText = function(text, options) { return textUtil.createText(text, options || {}); }; /** * Get default text style. */ this.getDefaultStyle = function() { return defaultStyle; }; /** * Get the external text style. */ this.getExternalStyle = function() { return externalStyle; }; } TextRenderer.$inject = [ 'config.textRenderer' ]; /** * Map containing SVG paths needed by BpmnRenderer. */ function PathMap() { /** * Contains a map of path elements * *

Path definition

* A parameterized path is defined like this: *
     * 'GATEWAY_PARALLEL': {
     *   d: 'm {mx},{my} {e.x0},0 0,{e.x1} {e.x1},0 0,{e.y0} -{e.x1},0 0,{e.y1} ' +
            '-{e.x0},0 0,-{e.y1} -{e.x1},0 0,-{e.y0} {e.x1},0 z',
     *   height: 17.5,
     *   width:  17.5,
     *   heightElements: [2.5, 7.5],
     *   widthElements: [2.5, 7.5]
     * }
     * 
*

It's important to specify a correct height and width for the path as the scaling * is based on the ratio between the specified height and width in this object and the * height and width that is set as scale target (Note x,y coordinates will be scaled with * individual ratios).

*

The 'heightElements' and 'widthElements' array must contain the values that will be scaled. * The scaling is based on the computed ratios. * Coordinates on the y axis should be in the heightElement's array, they will be scaled using * the computed ratio coefficient. * In the parameterized path the scaled values can be accessed through the 'e' object in {} brackets. *

    *
  • The values for the y axis can be accessed in the path string using {e.y0}, {e.y1}, ....
  • *
  • The values for the x axis can be accessed in the path string using {e.x0}, {e.x1}, ....
  • *
* The numbers x0, x1 respectively y0, y1, ... map to the corresponding array index. *

*/ this.pathMap = { 'EVENT_MESSAGE': { d: 'm {mx},{my} l 0,{e.y1} l {e.x1},0 l 0,-{e.y1} z l {e.x0},{e.y0} l {e.x0},-{e.y0}', height: 36, width: 36, heightElements: [ 6, 14 ], widthElements: [ 10.5, 21 ] }, 'EVENT_SIGNAL': { d: 'M {mx},{my} l {e.x0},{e.y0} l -{e.x1},0 Z', height: 36, width: 36, heightElements: [ 18 ], widthElements: [ 10, 20 ] }, 'EVENT_ESCALATION': { d: 'M {mx},{my} l {e.x0},{e.y0} l -{e.x0},-{e.y1} l -{e.x0},{e.y1} Z', height: 36, width: 36, heightElements: [ 20, 7 ], widthElements: [ 8 ] }, 'EVENT_CONDITIONAL': { d: 'M {e.x0},{e.y0} l {e.x1},0 l 0,{e.y2} l -{e.x1},0 Z ' + 'M {e.x2},{e.y3} l {e.x0},0 ' + 'M {e.x2},{e.y4} l {e.x0},0 ' + 'M {e.x2},{e.y5} l {e.x0},0 ' + 'M {e.x2},{e.y6} l {e.x0},0 ' + 'M {e.x2},{e.y7} l {e.x0},0 ' + 'M {e.x2},{e.y8} l {e.x0},0 ', height: 36, width: 36, heightElements: [ 8.5, 14.5, 18, 11.5, 14.5, 17.5, 20.5, 23.5, 26.5 ], widthElements: [ 10.5, 14.5, 12.5 ] }, 'EVENT_LINK': { d: 'm {mx},{my} 0,{e.y0} -{e.x1},0 0,{e.y1} {e.x1},0 0,{e.y0} {e.x0},-{e.y2} -{e.x0},-{e.y2} z', height: 36, width: 36, heightElements: [ 4.4375, 6.75, 7.8125 ], widthElements: [ 9.84375, 13.5 ] }, 'EVENT_ERROR': { d: 'm {mx},{my} {e.x0},-{e.y0} {e.x1},-{e.y1} {e.x2},{e.y2} {e.x3},-{e.y3} -{e.x4},{e.y4} -{e.x5},-{e.y5} z', height: 36, width: 36, heightElements: [ 0.023, 8.737, 8.151, 16.564, 10.591, 8.714 ], widthElements: [ 0.085, 6.672, 6.97, 4.273, 5.337, 6.636 ] }, 'EVENT_CANCEL_45': { d: 'm {mx},{my} -{e.x1},0 0,{e.x0} {e.x1},0 0,{e.y1} {e.x0},0 ' + '0,-{e.y1} {e.x1},0 0,-{e.y0} -{e.x1},0 0,-{e.y1} -{e.x0},0 z', height: 36, width: 36, heightElements: [ 4.75, 8.5 ], widthElements: [ 4.75, 8.5 ] }, 'EVENT_COMPENSATION': { d: 'm {mx},{my} {e.x0},-{e.y0} 0,{e.y1} z m {e.x1},-{e.y2} {e.x2},-{e.y3} 0,{e.y1} -{e.x2},-{e.y3} z', height: 36, width: 36, heightElements: [ 6.5, 13, 0.4, 6.1 ], widthElements: [ 9, 9.3, 8.7 ] }, 'EVENT_TIMER_WH': { d: 'M {mx},{my} l {e.x0},-{e.y0} m -{e.x0},{e.y0} l {e.x1},{e.y1} ', height: 36, width: 36, heightElements: [ 10, 2 ], widthElements: [ 3, 7 ] }, 'EVENT_TIMER_LINE': { d: 'M {mx},{my} ' + 'm {e.x0},{e.y0} l -{e.x1},{e.y1} ', height: 36, width: 36, heightElements: [ 10, 3 ], widthElements: [ 0, 0 ] }, 'EVENT_MULTIPLE': { d:'m {mx},{my} {e.x1},-{e.y0} {e.x1},{e.y0} -{e.x0},{e.y1} -{e.x2},0 z', height: 36, width: 36, heightElements: [ 6.28099, 12.56199 ], widthElements: [ 3.1405, 9.42149, 12.56198 ] }, 'EVENT_PARALLEL_MULTIPLE': { d:'m {mx},{my} {e.x0},0 0,{e.y1} {e.x1},0 0,{e.y0} -{e.x1},0 0,{e.y1} ' + '-{e.x0},0 0,-{e.y1} -{e.x1},0 0,-{e.y0} {e.x1},0 z', height: 36, width: 36, heightElements: [ 2.56228, 7.68683 ], widthElements: [ 2.56228, 7.68683 ] }, 'GATEWAY_EXCLUSIVE': { d:'m {mx},{my} {e.x0},{e.y0} {e.x1},{e.y0} {e.x2},0 {e.x4},{e.y2} ' + '{e.x4},{e.y1} {e.x2},0 {e.x1},{e.y3} {e.x0},{e.y3} ' + '{e.x3},0 {e.x5},{e.y1} {e.x5},{e.y2} {e.x3},0 z', height: 17.5, width: 17.5, heightElements: [ 8.5, 6.5312, -6.5312, -8.5 ], widthElements: [ 6.5, -6.5, 3, -3, 5, -5 ] }, 'GATEWAY_PARALLEL': { d:'m {mx},{my} 0,{e.y1} -{e.x1},0 0,{e.y0} {e.x1},0 0,{e.y1} {e.x0},0 ' + '0,-{e.y1} {e.x1},0 0,-{e.y0} -{e.x1},0 0,-{e.y1} -{e.x0},0 z', height: 30, width: 30, heightElements: [ 5, 12.5 ], widthElements: [ 5, 12.5 ] }, 'GATEWAY_EVENT_BASED': { d:'m {mx},{my} {e.x0},{e.y0} {e.x0},{e.y1} {e.x1},{e.y2} {e.x2},0 z', height: 11, width: 11, heightElements: [ -6, 6, 12, -12 ], widthElements: [ 9, -3, -12 ] }, 'GATEWAY_COMPLEX': { d:'m {mx},{my} 0,{e.y0} -{e.x0},-{e.y1} -{e.x1},{e.y2} {e.x0},{e.y1} -{e.x2},0 0,{e.y3} ' + '{e.x2},0 -{e.x0},{e.y1} l {e.x1},{e.y2} {e.x0},-{e.y1} 0,{e.y0} {e.x3},0 0,-{e.y0} {e.x0},{e.y1} ' + '{e.x1},-{e.y2} -{e.x0},-{e.y1} {e.x2},0 0,-{e.y3} -{e.x2},0 {e.x0},-{e.y1} -{e.x1},-{e.y2} ' + '-{e.x0},{e.y1} 0,-{e.y0} -{e.x3},0 z', height: 17.125, width: 17.125, heightElements: [ 4.875, 3.4375, 2.125, 3 ], widthElements: [ 3.4375, 2.125, 4.875, 3 ] }, 'DATA_OBJECT_PATH': { d:'m 0,0 {e.x1},0 {e.x0},{e.y0} 0,{e.y1} -{e.x2},0 0,-{e.y2} {e.x1},0 0,{e.y0} {e.x0},0', height: 61, width: 51, heightElements: [ 10, 50, 60 ], widthElements: [ 10, 40, 50, 60 ] }, 'DATA_OBJECT_COLLECTION_PATH': { d: 'm{mx},{my} m 3,2 l 0,10 m 3,-10 l 0,10 m 3,-10 l 0,10', height: 10, width: 10, heightElements: [], widthElements: [] }, 'DATA_ARROW': { d:'m 5,9 9,0 0,-3 5,5 -5,5 0,-3 -9,0 z', height: 61, width: 51, heightElements: [], widthElements: [] }, 'DATA_STORE': { d:'m {mx},{my} ' + 'l 0,{e.y2} ' + 'c {e.x0},{e.y1} {e.x1},{e.y1} {e.x2},0 ' + 'l 0,-{e.y2} ' + 'c -{e.x0},-{e.y1} -{e.x1},-{e.y1} -{e.x2},0' + 'c {e.x0},{e.y1} {e.x1},{e.y1} {e.x2},0 ' + 'm -{e.x2},{e.y0}' + 'c {e.x0},{e.y1} {e.x1},{e.y1} {e.x2},0' + 'm -{e.x2},{e.y0}' + 'c {e.x0},{e.y1} {e.x1},{e.y1} {e.x2},0', height: 61, width: 61, heightElements: [ 7, 10, 45 ], widthElements: [ 2, 58, 60 ] }, 'TEXT_ANNOTATION': { d: 'm {mx}, {my} m 10,0 l -10,0 l 0,{e.y0} l 10,0', height: 30, width: 10, heightElements: [ 30 ], widthElements: [ 10 ] }, 'MARKER_SUB_PROCESS': { d: 'm{mx},{my} m 7,2 l 0,10 m -5,-5 l 10,0', height: 10, width: 10, heightElements: [], widthElements: [] }, 'MARKER_PARALLEL': { d: 'm{mx},{my} m 3,2 l 0,10 m 3,-10 l 0,10 m 3,-10 l 0,10', height: 10, width: 10, heightElements: [], widthElements: [] }, 'MARKER_SEQUENTIAL': { d: 'm{mx},{my} m 0,3 l 10,0 m -10,3 l 10,0 m -10,3 l 10,0', height: 10, width: 10, heightElements: [], widthElements: [] }, 'MARKER_COMPENSATION': { d: 'm {mx},{my} 7,-5 0,10 z m 7.1,-0.3 6.9,-4.7 0,10 -6.9,-4.7 z', height: 10, width: 21, heightElements: [], widthElements: [] }, 'MARKER_LOOP': { d: 'm {mx},{my} c 3.526979,0 6.386161,-2.829858 6.386161,-6.320661 0,-3.490806 -2.859182,-6.320661 ' + '-6.386161,-6.320661 -3.526978,0 -6.38616,2.829855 -6.38616,6.320661 0,1.745402 ' + '0.714797,3.325567 1.870463,4.469381 0.577834,0.571908 1.265885,1.034728 2.029916,1.35457 ' + 'l -0.718163,-3.909793 m 0.718163,3.909793 -3.885211,0.802902', height: 13.9, width: 13.7, heightElements: [], widthElements: [] }, 'MARKER_ADHOC': { d: 'm {mx},{my} m 0.84461,2.64411 c 1.05533,-1.23780996 2.64337,-2.07882 4.29653,-1.97997996 2.05163,0.0805 ' + '3.85579,1.15803 5.76082,1.79107 1.06385,0.34139996 2.24454,0.1438 3.18759,-0.43767 0.61743,-0.33642 ' + '1.2775,-0.64078 1.7542,-1.17511 0,0.56023 0,1.12046 0,1.6807 -0.98706,0.96237996 -2.29792,1.62393996 ' + '-3.6918,1.66181996 -1.24459,0.0927 -2.46671,-0.2491 -3.59505,-0.74812 -1.35789,-0.55965 ' + '-2.75133,-1.33436996 -4.27027,-1.18121996 -1.37741,0.14601 -2.41842,1.13685996 -3.44288,1.96782996 z', height: 4, width: 15, heightElements: [], widthElements: [] }, 'TASK_TYPE_SEND': { d: 'm {mx},{my} l 0,{e.y1} l {e.x1},0 l 0,-{e.y1} z l {e.x0},{e.y0} l {e.x0},-{e.y0}', height: 14, width: 21, heightElements: [ 6, 14 ], widthElements: [ 10.5, 21 ] }, 'TASK_TYPE_SCRIPT': { d: 'm {mx},{my} c 9.966553,-6.27276 -8.000926,-7.91932 2.968968,-14.938 l -8.802728,0 ' + 'c -10.969894,7.01868 6.997585,8.66524 -2.968967,14.938 z ' + 'm -7,-12 l 5,0 ' + 'm -4.5,3 l 4.5,0 ' + 'm -3,3 l 5,0' + 'm -4,3 l 5,0', height: 15, width: 12.6, heightElements: [ 6, 14 ], widthElements: [ 10.5, 21 ] }, 'TASK_TYPE_USER_1': { d: 'm {mx},{my} c 0.909,-0.845 1.594,-2.049 1.594,-3.385 0,-2.554 -1.805,-4.62199999 ' + '-4.357,-4.62199999 -2.55199998,0 -4.28799998,2.06799999 -4.28799998,4.62199999 0,1.348 ' + '0.974,2.562 1.89599998,3.405 -0.52899998,0.187 -5.669,2.097 -5.794,4.7560005 v 6.718 ' + 'h 17 v -6.718 c 0,-2.2980005 -5.5279996,-4.5950005 -6.0509996,-4.7760005 z' + 'm -8,6 l 0,5.5 m 11,0 l 0,-5' }, 'TASK_TYPE_USER_2': { d: 'm {mx},{my} m 2.162,1.009 c 0,2.4470005 -2.158,4.4310005 -4.821,4.4310005 ' + '-2.66499998,0 -4.822,-1.981 -4.822,-4.4310005 ' }, 'TASK_TYPE_USER_3': { d: 'm {mx},{my} m -6.9,-3.80 c 0,0 2.25099998,-2.358 4.27399998,-1.177 2.024,1.181 4.221,1.537 ' + '4.124,0.965 -0.098,-0.57 -0.117,-3.79099999 -4.191,-4.13599999 -3.57499998,0.001 ' + '-4.20799998,3.36699999 -4.20699998,4.34799999 z' }, 'TASK_TYPE_MANUAL': { d: 'm {mx},{my} c 0.234,-0.01 5.604,0.008 8.029,0.004 0.808,0 1.271,-0.172 1.417,-0.752 0.227,-0.898 ' + '-0.334,-1.314 -1.338,-1.316 -2.467,-0.01 -7.886,-0.004 -8.108,-0.004 -0.014,-0.079 0.016,-0.533 0,-0.61 ' + '0.195,-0.042 8.507,0.006 9.616,0.002 0.877,-0.007 1.35,-0.438 1.353,-1.208 0.003,-0.768 -0.479,-1.09 ' + '-1.35,-1.091 -2.968,-0.002 -9.619,-0.013 -9.619,-0.013 v -0.591 c 0,0 5.052,-0.016 7.225,-0.016 ' + '0.888,-0.002 1.354,-0.416 1.351,-1.193 -0.006,-0.761 -0.492,-1.196 -1.361,-1.196 -3.473,-0.005 ' + '-10.86,-0.003 -11.0829995,-0.003 -0.022,-0.047 -0.045,-0.094 -0.069,-0.139 0.3939995,-0.319 ' + '2.0409995,-1.626 2.4149995,-2.017 0.469,-0.4870005 0.519,-1.1650005 0.162,-1.6040005 -0.414,-0.511 ' + '-0.973,-0.5 -1.48,-0.236 -1.4609995,0.764 -6.5999995,3.6430005 -7.7329995,4.2710005 -0.9,0.499 ' + '-1.516,1.253 -1.882,2.19 -0.37000002,0.95 -0.17,2.01 -0.166,2.979 0.004,0.718 -0.27300002,1.345 ' + '-0.055,2.063 0.629,2.087 2.425,3.312 4.859,3.318 4.6179995,0.014 9.2379995,-0.139 13.8569995,-0.158 ' + '0.755,-0.004 1.171,-0.301 1.182,-1.033 0.012,-0.754 -0.423,-0.969 -1.183,-0.973 -1.778,-0.01 ' + '-5.824,-0.004 -6.04,-0.004 10e-4,-0.084 0.003,-0.586 10e-4,-0.67 z' }, 'TASK_TYPE_INSTANTIATING_SEND': { d: 'm {mx},{my} l 0,8.4 l 12.6,0 l 0,-8.4 z l 6.3,3.6 l 6.3,-3.6' }, 'TASK_TYPE_SERVICE': { d: 'm {mx},{my} v -1.71335 c 0.352326,-0.0705 0.703932,-0.17838 1.047628,-0.32133 ' + '0.344416,-0.14465 0.665822,-0.32133 0.966377,-0.52145 l 1.19431,1.18005 1.567487,-1.57688 ' + '-1.195028,-1.18014 c 0.403376,-0.61394 0.683079,-1.29908 0.825447,-2.01824 l 1.622133,-0.01 ' + 'v -2.2196 l -1.636514,0.01 c -0.07333,-0.35153 -0.178319,-0.70024 -0.323564,-1.04372 ' + '-0.145244,-0.34406 -0.321407,-0.6644 -0.522735,-0.96217 l 1.131035,-1.13631 -1.583305,-1.56293 ' + '-1.129598,1.13589 c -0.614052,-0.40108 -1.302883,-0.68093 -2.022633,-0.82247 l 0.0093,-1.61852 ' + 'h -2.241173 l 0.0042,1.63124 c -0.353763,0.0736 -0.705369,0.17977 -1.049785,0.32371 -0.344415,0.14437 ' + '-0.665102,0.32092 -0.9635006,0.52046 l -1.1698628,-1.15823 -1.5667691,1.5792 1.1684265,1.15669 ' + 'c -0.4026573,0.61283 -0.68308,1.29797 -0.8247287,2.01713 l -1.6588041,0.003 v 2.22174 ' + 'l 1.6724648,-0.006 c 0.073327,0.35077 0.1797598,0.70243 0.3242851,1.04472 0.1452428,0.34448 ' + '0.3214064,0.6644 0.5227339,0.96066 l -1.1993431,1.19723 1.5840256,1.56011 1.1964668,-1.19348 ' + 'c 0.6140517,0.40346 1.3028827,0.68232 2.0233517,0.82331 l 7.19e-4,1.69892 h 2.226848 z ' + 'm 0.221462,-3.9957 c -1.788948,0.7502 -3.8576,-0.0928 -4.6097055,-1.87438 -0.7521065,-1.78321 ' + '0.090598,-3.84627 1.8802645,-4.59604 1.78823,-0.74936 3.856881,0.0929 4.608987,1.87437 ' + '0.752106,1.78165 -0.0906,3.84612 -1.879546,4.59605 z' }, 'TASK_TYPE_SERVICE_FILL': { d: 'm {mx},{my} c -1.788948,0.7502 -3.8576,-0.0928 -4.6097055,-1.87438 -0.7521065,-1.78321 ' + '0.090598,-3.84627 1.8802645,-4.59604 1.78823,-0.74936 3.856881,0.0929 4.608987,1.87437 ' + '0.752106,1.78165 -0.0906,3.84612 -1.879546,4.59605 z' }, 'TASK_TYPE_BUSINESS_RULE_HEADER': { d: 'm {mx},{my} 0,4 20,0 0,-4 z' }, 'TASK_TYPE_BUSINESS_RULE_MAIN': { d: 'm {mx},{my} 0,12 20,0 0,-12 z' + 'm 0,8 l 20,0 ' + 'm -13,-4 l 0,8' }, 'MESSAGE_FLOW_MARKER': { d: 'm {mx},{my} m -10.5 ,-7 l 0,14 l 21,0 l 0,-14 z l 10.5,6 l 10.5,-6' } }; this.getRawPath = function getRawPath(pathId) { return this.pathMap[pathId].d; }; /** * Scales the path to the given height and width. *

Use case

*

Use case is to scale the content of elements (event, gateways) based * on the element bounding box's size. *

*

Why not transform

*

Scaling a path with transform() will also scale the stroke and IE does not support * the option 'non-scaling-stroke' to prevent this. * Also there are use cases where only some parts of a path should be * scaled.

* * @param {string} pathId The ID of the path. * @param {Object} param

* Example param object scales the path to 60% size of the container (data.width, data.height). *

     *   {
     *     xScaleFactor: 0.6,
     *     yScaleFactor:0.6,
     *     containerWidth: data.width,
     *     containerHeight: data.height,
     *     position: {
     *       mx: 0.46,
     *       my: 0.2,
     *     }
     *   }
     *   
*
    *
  • targetpathwidth = xScaleFactor * containerWidth
  • *
  • targetpathheight = yScaleFactor * containerHeight
  • *
  • Position is used to set the starting coordinate of the path. M is computed: *
      *
    • position.x * containerWidth
    • *
    • position.y * containerHeight
    • *
    * Center of the container
     position: {
         *       mx: 0.5,
         *       my: 0.5,
         *     }
    * Upper left corner of the container *
     position: {
         *       mx: 0.0,
         *       my: 0.0,
         *     }
    *
  • *
*

* */ this.getScaledPath = function getScaledPath(pathId, param) { var rawPath = this.pathMap[pathId]; // positioning // compute the start point of the path var mx, my; if (param.abspos) { mx = param.abspos.x; my = param.abspos.y; } else { mx = param.containerWidth * param.position.mx; my = param.containerHeight * param.position.my; } var coordinates = {}; // map for the scaled coordinates if (param.position) { // path var heightRatio = (param.containerHeight / rawPath.height) * param.yScaleFactor; var widthRatio = (param.containerWidth / rawPath.width) * param.xScaleFactor; // Apply height ratio for (var heightIndex = 0; heightIndex < rawPath.heightElements.length; heightIndex++) { coordinates['y' + heightIndex] = rawPath.heightElements[heightIndex] * heightRatio; } // Apply width ratio for (var widthIndex = 0; widthIndex < rawPath.widthElements.length; widthIndex++) { coordinates['x' + widthIndex] = rawPath.widthElements[widthIndex] * widthRatio; } } // Apply value to raw path var path = format( rawPath.d, { mx: mx, my: my, e: coordinates } ); return path; }; } // helpers ////////////////////// // copied and adjusted from https://github.com/adobe-webplatform/Snap.svg/blob/master/src/svg.js var tokenRegex = /\{([^{}]+)\}/g, objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g; // matches .xxxxx or ["xxxxx"] to run over object properties function replacer(all, key, obj) { var res = obj; key.replace(objNotationRegex, function(all, name, quote, quotedName, isFunc) { name = name || quotedName; if (res) { if (name in res) { res = res[name]; } typeof res == 'function' && isFunc && (res = res()); } }); res = (res == null || res == obj ? all : res) + ''; return res; } function format(str, obj) { return String(str).replace(tokenRegex, function(all, key) { return replacer(all, key, obj); }); } var DrawModule = { __init__: [ 'bpmnRenderer' ], bpmnRenderer: [ 'type', BpmnRenderer ], textRenderer: [ 'type', TextRenderer ], pathMap: [ 'type', PathMap ] }; /** * A simple translation stub to be used for multi-language support * in diagrams. Can be easily replaced with a more sophisticated * solution. * * @example * * // use it inside any diagram component by injecting `translate`. * * function MyService(translate) { * alert(translate('HELLO {you}', { you: 'You!' })); * } * * @param {string} template to interpolate * @param {Object} [replacements] a map with substitutes * * @return {string} the translated string */ function translate$1(template, replacements) { replacements = replacements || {}; return template.replace(/{([^}]+)}/g, function(_, key) { return replacements[key] || '{' + key + '}'; }); } var translate = { translate: [ 'value', translate$1 ] }; var DEFAULT_LABEL_SIZE = { width: 90, height: 20 }; var FLOW_LABEL_INDENT = 15; /** * Returns true if the given semantic has an external label * * @param {BpmnElement} semantic * @return {boolean} true if has label */ function isLabelExternal(semantic) { return is$1(semantic, 'bpmn:Event') || is$1(semantic, 'bpmn:Gateway') || is$1(semantic, 'bpmn:DataStoreReference') || is$1(semantic, 'bpmn:DataObjectReference') || is$1(semantic, 'bpmn:DataInput') || is$1(semantic, 'bpmn:DataOutput') || is$1(semantic, 'bpmn:SequenceFlow') || is$1(semantic, 'bpmn:MessageFlow') || is$1(semantic, 'bpmn:Group'); } /** * Returns true if the given element has an external label * * @param {djs.model.shape} element * @return {boolean} true if has label */ function hasExternalLabel(element) { return isLabel$6(element.label); } /** * Get the position for sequence flow labels * * @param {Array} waypoints * @return {Point} the label position */ function getFlowLabelPosition(waypoints) { // get the waypoints mid var mid = waypoints.length / 2 - 1; var first = waypoints[Math.floor(mid)]; var second = waypoints[Math.ceil(mid + 0.01)]; // get position var position = getWaypointsMid(waypoints); // calculate angle var angle = Math.atan((second.y - first.y) / (second.x - first.x)); var x = position.x, y = position.y; if (Math.abs(angle) < Math.PI / 2) { y -= FLOW_LABEL_INDENT; } else { x += FLOW_LABEL_INDENT; } return { x: x, y: y }; } /** * Get the middle of a number of waypoints * * @param {Array} waypoints * @return {Point} the mid point */ function getWaypointsMid(waypoints) { var mid = waypoints.length / 2 - 1; var first = waypoints[Math.floor(mid)]; var second = waypoints[Math.ceil(mid + 0.01)]; return { x: first.x + (second.x - first.x) / 2, y: first.y + (second.y - first.y) / 2 }; } function getExternalLabelMid(element) { if (element.waypoints) { return getFlowLabelPosition(element.waypoints); } else if (is$1(element, 'bpmn:Group')) { return { x: element.x + element.width / 2, y: element.y + DEFAULT_LABEL_SIZE.height / 2 }; } else { return { x: element.x + element.width / 2, y: element.y + element.height + DEFAULT_LABEL_SIZE.height / 2 }; } } /** * Returns the bounds of an elements label, parsed from the elements DI or * generated from its bounds. * * @param {BpmndDi} di * @param {djs.model.Base} element */ function getExternalLabelBounds(di, element) { var mid, size, bounds, label = di.label; if (label && label.bounds) { bounds = label.bounds; size = { width: Math.max(DEFAULT_LABEL_SIZE.width, bounds.width), height: bounds.height }; mid = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 }; } else { mid = getExternalLabelMid(element); size = DEFAULT_LABEL_SIZE; } return assign({ x: mid.x - size.width / 2, y: mid.y - size.height / 2 }, size); } function isLabel$6(element) { return element && !!element.labelTarget; } /** * @param {ModdleElement} semantic * @param {ModdleElement} di * @param {Object} [attrs=null] * * @return {Object} */ function elementData(semantic, di, attrs) { return assign({ id: semantic.id, type: semantic.$type, businessObject: semantic, di: di }, attrs); } function getWaypoints(di, source, target) { var waypoints = di.waypoint; if (!waypoints || waypoints.length < 2) { return [ getMid(source), getMid(target) ]; } return waypoints.map(function(p) { return { x: p.x, y: p.y }; }); } function notYetDrawn(translate, semantic, refSemantic, property) { return new Error(translate('element {element} referenced by {referenced}#{property} not yet drawn', { element: elementToString(refSemantic), referenced: elementToString(semantic), property: property })); } /** * An importer that adds bpmn elements to the canvas * * @param {EventBus} eventBus * @param {Canvas} canvas * @param {ElementFactory} elementFactory * @param {ElementRegistry} elementRegistry * @param {Function} translate * @param {TextRenderer} textRenderer */ function BpmnImporter( eventBus, canvas, elementFactory, elementRegistry, translate, textRenderer) { this._eventBus = eventBus; this._canvas = canvas; this._elementFactory = elementFactory; this._elementRegistry = elementRegistry; this._translate = translate; this._textRenderer = textRenderer; } BpmnImporter.$inject = [ 'eventBus', 'canvas', 'elementFactory', 'elementRegistry', 'translate', 'textRenderer' ]; /** * Add bpmn element (semantic) to the canvas onto the * specified parent shape. */ BpmnImporter.prototype.add = function(semantic, di, parentElement) { var element, translate = this._translate, hidden; var parentIndex; // ROOT ELEMENT // handle the special case that we deal with a // invisible root element (process, subprocess or collaboration) if (is$1(di, 'bpmndi:BPMNPlane')) { var attrs = is$1(semantic, 'bpmn:SubProcess') ? { id: semantic.id + '_plane' } : {}; // add a virtual element (not being drawn) element = this._elementFactory.createRoot(elementData(semantic, di, attrs)); this._canvas.addRootElement(element); } // SHAPE else if (is$1(di, 'bpmndi:BPMNShape')) { var collapsed = !isExpanded(semantic, di), isFrame = isFrameElement(semantic); hidden = parentElement && (parentElement.hidden || parentElement.collapsed); var bounds = di.bounds; element = this._elementFactory.createShape(elementData(semantic, di, { collapsed: collapsed, hidden: hidden, x: Math.round(bounds.x), y: Math.round(bounds.y), width: Math.round(bounds.width), height: Math.round(bounds.height), isFrame: isFrame })); if (is$1(semantic, 'bpmn:BoundaryEvent')) { this._attachBoundary(semantic, element); } // insert lanes behind other flow nodes (cf. #727) if (is$1(semantic, 'bpmn:Lane')) { parentIndex = 0; } if (is$1(semantic, 'bpmn:DataStoreReference')) { // check whether data store is inside our outside of its semantic parent if (!isPointInsideBBox$1(parentElement, getMid(bounds))) { parentElement = this._canvas.findRoot(parentElement); } } this._canvas.addShape(element, parentElement, parentIndex); } // CONNECTION else if (is$1(di, 'bpmndi:BPMNEdge')) { var source = this._getSource(semantic), target = this._getTarget(semantic); hidden = parentElement && (parentElement.hidden || parentElement.collapsed); element = this._elementFactory.createConnection(elementData(semantic, di, { hidden: hidden, source: source, target: target, waypoints: getWaypoints(di, source, target) })); if (is$1(semantic, 'bpmn:DataAssociation')) { // render always on top; this ensures DataAssociations // are rendered correctly across different "hacks" people // love to model such as cross participant / sub process // associations parentElement = this._canvas.findRoot(parentElement); } this._canvas.addConnection(element, parentElement, parentIndex); } else { throw new Error(translate('unknown di {di} for element {semantic}', { di: elementToString(di), semantic: elementToString(semantic) })); } // (optional) LABEL if (isLabelExternal(semantic) && getLabel(element)) { this.addLabel(semantic, di, element); } this._eventBus.fire('bpmnElement.added', { element: element }); return element; }; /** * Attach the boundary element to the given host * * @param {ModdleElement} boundarySemantic * @param {djs.model.Base} boundaryElement */ BpmnImporter.prototype._attachBoundary = function(boundarySemantic, boundaryElement) { var translate = this._translate; var hostSemantic = boundarySemantic.attachedToRef; if (!hostSemantic) { throw new Error(translate('missing {semantic}#attachedToRef', { semantic: elementToString(boundarySemantic) })); } var host = this._elementRegistry.get(hostSemantic.id), attachers = host && host.attachers; if (!host) { throw notYetDrawn(translate, boundarySemantic, hostSemantic, 'attachedToRef'); } // wire element.host <> host.attachers boundaryElement.host = host; if (!attachers) { host.attachers = attachers = []; } if (attachers.indexOf(boundaryElement) === -1) { attachers.push(boundaryElement); } }; /** * add label for an element */ BpmnImporter.prototype.addLabel = function(semantic, di, element) { var bounds, text, label; bounds = getExternalLabelBounds(di, element); text = getLabel(element); if (text) { // get corrected bounds from actual layouted text bounds = this._textRenderer.getExternalLabelBounds(bounds, text); } label = this._elementFactory.createLabel(elementData(semantic, di, { id: semantic.id + '_label', labelTarget: element, type: 'label', hidden: element.hidden || !getLabel(element), x: Math.round(bounds.x), y: Math.round(bounds.y), width: Math.round(bounds.width), height: Math.round(bounds.height) })); return this._canvas.addShape(label, element.parent); }; /** * Return the drawn connection end based on the given side. * * @throws {Error} if the end is not yet drawn */ BpmnImporter.prototype._getEnd = function(semantic, side) { var element, refSemantic, type = semantic.$type, translate = this._translate; refSemantic = semantic[side + 'Ref']; // handle mysterious isMany DataAssociation#sourceRef if (side === 'source' && type === 'bpmn:DataInputAssociation') { refSemantic = refSemantic && refSemantic[0]; } // fix source / target for DataInputAssociation / DataOutputAssociation if (side === 'source' && type === 'bpmn:DataOutputAssociation' || side === 'target' && type === 'bpmn:DataInputAssociation') { refSemantic = semantic.$parent; } element = refSemantic && this._getElement(refSemantic); if (element) { return element; } if (refSemantic) { throw notYetDrawn(translate, semantic, refSemantic, side + 'Ref'); } else { throw new Error(translate('{semantic}#{side} Ref not specified', { semantic: elementToString(semantic), side: side })); } }; BpmnImporter.prototype._getSource = function(semantic) { return this._getEnd(semantic, 'source'); }; BpmnImporter.prototype._getTarget = function(semantic) { return this._getEnd(semantic, 'target'); }; BpmnImporter.prototype._getElement = function(semantic) { return this._elementRegistry.get(semantic.id); }; // helpers //////////////////// function isPointInsideBBox$1(bbox, point) { var x = point.x, y = point.y; return x >= bbox.x && x <= bbox.x + bbox.width && y >= bbox.y && y <= bbox.y + bbox.height; } function isFrameElement(semantic) { return is$1(semantic, 'bpmn:Group'); } var ImportModule = { __depends__: [ translate ], bpmnImporter: [ 'type', BpmnImporter ] }; var CoreModule = { __depends__: [ DrawModule, ImportModule ] }; function __stopPropagation(event) { if (!event || typeof event.stopPropagation !== 'function') { return; } event.stopPropagation(); } function getOriginal$1(event) { return event.originalEvent || event.srcEvent; } function stopPropagation$1(event, immediate) { __stopPropagation(event); __stopPropagation(getOriginal$1(event)); } function toPoint(event) { if (event.pointers && event.pointers.length) { event = event.pointers[0]; } if (event.touches && event.touches.length) { event = event.touches[0]; } return event ? { x: event.clientX, y: event.clientY } : null; } function isMac() { return (/mac/i).test(navigator.platform); } function isButton(event, button) { return (getOriginal$1(event) || event).button === button; } function isPrimaryButton(event) { // button === 0 -> left áka primary mouse button return isButton(event, 0); } function isAuxiliaryButton(event) { // button === 1 -> auxiliary áka wheel button return isButton(event, 1); } function hasPrimaryModifier(event) { var originalEvent = getOriginal$1(event) || event; if (!isPrimaryButton(event)) { return false; } // Use cmd as primary modifier key for mac OS if (isMac()) { return originalEvent.metaKey; } else { return originalEvent.ctrlKey; } } function hasSecondaryModifier(event) { var originalEvent = getOriginal$1(event) || event; return isPrimaryButton(event) && originalEvent.shiftKey; } function allowAll(event) { return true; } function allowPrimaryAndAuxiliary(event) { return isPrimaryButton(event) || isAuxiliaryButton(event); } var LOW_PRIORITY$q = 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 */ function InteractionEvents(eventBus, elementRegistry, styles) { var self = this; /** * Fire an interaction event. * * @param {string} type local event name, e.g. element.click. * @param {DOMEvent} event native event * @param {djs.model.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} event * @param {djs.model.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 = delegate.bind(node, ELEMENT_SELECTOR, event, handler); } function unregisterEvent(node, event, localEvent) { var handler = mouseHandler(localEvent); if (!handler) { return; } delegate.unbind(node, event, handler.$delegate); } function registerEvents(svg) { forEach$1(bindings, function(val, key) { registerEvent(svg, key, val); }); } function unregisterEvents(svg) { forEach$1(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$q, function(event) { var element = event.element, gfx = event.gfx; eventBus.fire('interactionEvents.updateHit', { element: element, gfx: gfx }); }); eventBus.on('interactionEvents.createHit', LOW_PRIORITY$q, 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 + '>'); } attr(hit, attrs); return hit; } function appendHit(gfx, hit) { append(gfx, hit); } // API /** * Remove hints on the given graphics. * * @param {SVGElement} gfx */ this.removeHits = function(gfx) { var hits = all('.djs-hit', gfx); forEach$1(hits, remove$1); }; /** * Create default hit for the given element. * * @param {djs.model.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} 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 = create$1('rect'); applyStyle(hit, type); attr(hit, attrs); appendHit(gfx, hit); return hit; }; /** * Update default hit of the element. * * @param {djs.model.Base} element * @param {SVGElement} gfx * * @return {SVGElement} updated hit */ this.updateDefaultHit = function(element, gfx) { var hit = query('.djs-hit', gfx); if (!hit) { return; } if (element.waypoints) { updateLine(hit, element.waypoints); } else { attr(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 {djs.model.Base} element * @property {SVGElement} gfx * @property {Event} originalEvent */ /** * An event indicating that the mouse has left an element * * @event element.out * * @type {Object} * @property {djs.model.Base} element * @property {SVGElement} gfx * @property {Event} originalEvent */ /** * An event indicating that the mouse has clicked an element * * @event element.click * * @type {Object} * @property {djs.model.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 {djs.model.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 {djs.model.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 {djs.model.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 {djs.model.Base} element * @property {SVGElement} gfx * @property {Event} originalEvent */ var InteractionEventsModule$1 = { __init__: [ 'interactionEvents' ], interactionEvents: [ 'type', InteractionEvents ] }; var LOW_PRIORITY$p = 500; /** * @class * * A plugin that adds an outline to shapes and connections that may be activated and styled * via CSS classes. * * @param {EventBus} eventBus * @param {Styles} styles * @param {ElementRegistry} elementRegistry */ function Outline(eventBus, styles, elementRegistry) { this.offset = 6; var OUTLINE_STYLE = styles.cls('djs-outline', [ 'no-fill' ]); var self = this; function createOutline(gfx, bounds) { var outline = create$1('rect'); attr(outline, assign({ x: 10, y: 10, rx: 3, width: 100, height: 100 }, OUTLINE_STYLE)); append(gfx, outline); return outline; } // A low priortity is necessary, because outlines of labels have to be updated // after the label bounds have been updated in the renderer. eventBus.on([ 'shape.added', 'shape.changed' ], LOW_PRIORITY$p, function(event) { var element = event.element, gfx = event.gfx; var outline = query('.djs-outline', gfx); if (!outline) { outline = createOutline(gfx); } self.updateShapeOutline(outline, element); }); eventBus.on([ 'connection.added', 'connection.changed' ], function(event) { var element = event.element, gfx = event.gfx; var outline = query('.djs-outline', gfx); if (!outline) { outline = createOutline(gfx); } self.updateConnectionOutline(outline, element); }); } /** * Updates the outline of a shape respecting the dimension of the * element and an outline offset. * * @param {SVGElement} outline * @param {djs.model.Base} element */ Outline.prototype.updateShapeOutline = function(outline, element) { attr(outline, { x: -this.offset, y: -this.offset, width: element.width + this.offset * 2, height: element.height + this.offset * 2 }); }; /** * Updates the outline of a connection respecting the bounding box of * the connection and an outline offset. * * @param {SVGElement} outline * @param {djs.model.Base} element */ Outline.prototype.updateConnectionOutline = function(outline, connection) { var bbox = getBBox(connection); attr(outline, { x: bbox.x - this.offset, y: bbox.y - this.offset, width: bbox.width + this.offset * 2, height: bbox.height + this.offset * 2 }); }; Outline.$inject = [ 'eventBus', 'styles', 'elementRegistry' ]; var OutlineModule = { __init__: [ 'outline' ], outline: [ 'type', Outline ] }; /** * A service that offers the current selection in a diagram. * Offers the api to control the selection, too. * * @class * * @param {EventBus} eventBus the event bus */ function Selection(eventBus, canvas) { this._eventBus = eventBus; this._canvas = canvas; this._selectedElements = []; var self = this; eventBus.on([ 'shape.remove', 'connection.remove' ], function(e) { var element = e.element; self.deselect(element); }); eventBus.on([ 'diagram.clear', 'root.set' ], function(e) { self.select(null); }); } Selection.$inject = [ 'eventBus', 'canvas' ]; Selection.prototype.deselect = function(element) { var selectedElements = this._selectedElements; var idx = selectedElements.indexOf(element); if (idx !== -1) { var oldSelection = selectedElements.slice(); selectedElements.splice(idx, 1); this._eventBus.fire('selection.changed', { oldSelection: oldSelection, newSelection: selectedElements }); } }; Selection.prototype.get = function() { return this._selectedElements; }; Selection.prototype.isSelected = function(element) { return this._selectedElements.indexOf(element) !== -1; }; /** * This method selects one or more elements on the diagram. * * By passing an additional add parameter you can decide whether or not the element(s) * should be added to the already existing selection or not. * * @method Selection#select * * @param {Object|Object[]} elements element or array of elements to be selected * @param {boolean} [add] whether the element(s) should be appended to the current selection, defaults to false */ Selection.prototype.select = function(elements, add) { var selectedElements = this._selectedElements, oldSelection = selectedElements.slice(); if (!isArray$3(elements)) { elements = elements ? [ elements ] : []; } var canvas = this._canvas; var rootElement = canvas.getRootElement(); elements = elements.filter(function(element) { var elementRoot = canvas.findRoot(element); return rootElement === elementRoot; }); // selection may be cleared by passing an empty array or null // to the method if (add) { forEach$1(elements, function(element) { if (selectedElements.indexOf(element) !== -1) { // already selected return; } else { selectedElements.push(element); } }); } else { this._selectedElements = selectedElements = elements.slice(); } this._eventBus.fire('selection.changed', { oldSelection: oldSelection, newSelection: selectedElements }); }; var MARKER_HOVER = 'hover', MARKER_SELECTED = 'selected'; var SELECTION_OUTLINE_PADDING = 6; /** * A plugin that adds a visible selection UI to shapes and connections * by appending the hover and selected classes to them. * * @class * * Makes elements selectable, too. * * @param {Canvas} canvas * @param {EventBus} eventBus */ function SelectionVisuals(canvas, eventBus, selection) { this._canvas = canvas; var self = this; this._multiSelectionBox = null; function addMarker(e, cls) { canvas.addMarker(e, cls); } function removeMarker(e, cls) { canvas.removeMarker(e, cls); } eventBus.on('element.hover', function(event) { addMarker(event.element, MARKER_HOVER); }); eventBus.on('element.out', function(event) { removeMarker(event.element, MARKER_HOVER); }); eventBus.on('selection.changed', function(event) { function deselect(s) { removeMarker(s, MARKER_SELECTED); } function select(s) { addMarker(s, MARKER_SELECTED); } var oldSelection = event.oldSelection, newSelection = event.newSelection; forEach$1(oldSelection, function(e) { if (newSelection.indexOf(e) === -1) { deselect(e); } }); forEach$1(newSelection, function(e) { if (oldSelection.indexOf(e) === -1) { select(e); } }); self._updateSelectionOutline(newSelection); }); eventBus.on('element.changed', function(event) { if (selection.isSelected(event.element)) { self._updateSelectionOutline(selection.get()); } }); } SelectionVisuals.$inject = [ 'canvas', 'eventBus', 'selection' ]; SelectionVisuals.prototype._updateSelectionOutline = function(selection) { var layer = this._canvas.getLayer('selectionOutline'); clear(layer); var enabled = selection.length > 1; var container = this._canvas.getContainer(); classes(container)[enabled ? 'add' : 'remove']('djs-multi-select'); if (!enabled) { return; } var bBox = addSelectionOutlinePadding(getBBox(selection)); var rect = create$1('rect'); attr(rect, assign({ rx: 3 }, bBox)); classes(rect).add('djs-selection-outline'); append(layer, rect); }; // helpers ////////// function addSelectionOutlinePadding(bBox) { return { x: bBox.x - SELECTION_OUTLINE_PADDING, y: bBox.y - SELECTION_OUTLINE_PADDING, width: bBox.width + SELECTION_OUTLINE_PADDING * 2, height: bBox.height + SELECTION_OUTLINE_PADDING * 2 }; } function SelectionBehavior(eventBus, selection, canvas, elementRegistry) { // Select elements on create eventBus.on('create.end', 500, function(event) { var context = event.context, canExecute = context.canExecute, elements = context.elements, hints = context.hints || {}, autoSelect = hints.autoSelect; if (canExecute) { if (autoSelect === false) { // Select no elements return; } if (isArray$3(autoSelect)) { selection.select(autoSelect); } else { // Select all elements by default selection.select(elements.filter(isShown)); } } }); // Select connection targets on connect eventBus.on('connect.end', 500, function(event) { var context = event.context, connection = context.connection; if (connection) { selection.select(connection); } }); // Select shapes on move eventBus.on('shape.move.end', 500, function(event) { var previousSelection = event.previousSelection || []; var shape = elementRegistry.get(event.context.shape.id); // Always select main shape on move var isSelected = find(previousSelection, function(selectedShape) { return shape.id === selectedShape.id; }); if (!isSelected) { selection.select(shape); } }); // Select elements on click eventBus.on('element.click', function(event) { if (!isPrimaryButton(event)) { return; } var element = event.element; if (element === canvas.getRootElement()) { element = null; } var isSelected = selection.isSelected(element), isMultiSelect = selection.get().length > 1; // Add to selection if CTRL or SHIFT pressed var add = hasPrimaryModifier(event) || hasSecondaryModifier(event); if (isSelected && isMultiSelect) { if (add) { // Deselect element return selection.deselect(element); } else { // Select element only return selection.select(element); } } else if (!isSelected) { // Select element selection.select(element, add); } else { // Deselect element selection.deselect(element); } }); } SelectionBehavior.$inject = [ 'eventBus', 'selection', 'canvas', 'elementRegistry' ]; function isShown(element) { return !element.hidden; } var SelectionModule = { __init__: [ 'selectionVisuals', 'selectionBehavior' ], __depends__: [ InteractionEventsModule$1, OutlineModule ], selection: [ 'type', Selection ], selectionVisuals: [ 'type', SelectionVisuals ], selectionBehavior: [ 'type', SelectionBehavior ] }; /** * Util that provides unique IDs. * * @class djs.util.IdGenerator * @constructor * @memberOf djs.util * * The ids can be customized via a given prefix and contain a random value to avoid collisions. * * @param {string} prefix a prefix to prepend to generated ids (for better readability) */ function IdGenerator(prefix) { this._counter = 0; this._prefix = (prefix ? prefix + '-' : '') + Math.floor(Math.random() * 1000000000) + '-'; } /** * Returns a next unique ID. * * @method djs.util.IdGenerator#next * * @returns {string} the id */ IdGenerator.prototype.next = function() { return this._prefix + (++this._counter); }; // document wide unique overlay ids var ids$1 = new IdGenerator('ov'); var LOW_PRIORITY$o = 500; /** * A service that allows users to attach overlays to diagram elements. * * The overlay service will take care of overlay positioning during updates. * * @example * * // add a pink badge on the top left of the shape * overlays.add(someShape, { * position: { * top: -5, * left: -5 * }, * html: '
0
' * }); * * // or add via shape id * * overlays.add('some-element-id', { * position: { * top: -5, * left: -5 * } * html: '
0
' * }); * * // or add with optional type * * overlays.add(someShape, 'badge', { * position: { * top: -5, * left: -5 * } * html: '
0
' * }); * * * // remove an overlay * * var id = overlays.add(...); * overlays.remove(id); * * * You may configure overlay defaults during tool by providing a `config` module * with `overlays.defaults` as an entry: * * { * overlays: { * defaults: { * show: { * minZoom: 0.7, * maxZoom: 5.0 * }, * scale: { * min: 1 * } * } * } * * @param {Object} config * @param {EventBus} eventBus * @param {Canvas} canvas * @param {ElementRegistry} elementRegistry */ function Overlays(config, eventBus, canvas, elementRegistry) { this._eventBus = eventBus; this._canvas = canvas; this._elementRegistry = elementRegistry; this._ids = ids$1; this._overlayDefaults = assign({ // no show constraints show: null, // always scale scale: true }, config && config.defaults); /** * Mapping overlayId -> overlay */ this._overlays = {}; /** * Mapping elementId -> overlay container */ this._overlayContainers = []; // root html element for all overlays this._overlayRoot = createRoot$1(canvas.getContainer()); this._init(); } Overlays.$inject = [ 'config.overlays', 'eventBus', 'canvas', 'elementRegistry' ]; /** * Returns the overlay with the specified id or a list of overlays * for an element with a given type. * * @example * * // return the single overlay with the given id * overlays.get('some-id'); * * // return all overlays for the shape * overlays.get({ element: someShape }); * * // return all overlays on shape with type 'badge' * overlays.get({ element: someShape, type: 'badge' }); * * // shape can also be specified as id * overlays.get({ element: 'element-id', type: 'badge' }); * * * @param {Object} search * @param {string} [search.id] * @param {string|djs.model.Base} [search.element] * @param {string} [search.type] * * @return {Object|Array} the overlay(s) */ Overlays.prototype.get = function(search) { if (isString(search)) { search = { id: search }; } if (isString(search.element)) { search.element = this._elementRegistry.get(search.element); } if (search.element) { var container = this._getOverlayContainer(search.element, true); // return a list of overlays when searching by element (+type) if (container) { return search.type ? filter(container.overlays, matchPattern({ type: search.type })) : container.overlays.slice(); } else { return []; } } else if (search.type) { return filter(this._overlays, matchPattern({ type: search.type })); } else { // return single element when searching by id return search.id ? this._overlays[search.id] : null; } }; /** * Adds a HTML overlay to an element. * * @param {string|djs.model.Base} element attach overlay to this shape * @param {string} [type] optional type to assign to the overlay * @param {Object} overlay the overlay configuration * * @param {string|DOMElement} overlay.html html element to use as an overlay * @param {Object} [overlay.show] show configuration * @param {number} [overlay.show.minZoom] minimal zoom level to show the overlay * @param {number} [overlay.show.maxZoom] maximum zoom level to show the overlay * @param {Object} overlay.position where to attach the overlay * @param {number} [overlay.position.left] relative to element bbox left attachment * @param {number} [overlay.position.top] relative to element bbox top attachment * @param {number} [overlay.position.bottom] relative to element bbox bottom attachment * @param {number} [overlay.position.right] relative to element bbox right attachment * @param {boolean|Object} [overlay.scale=true] false to preserve the same size regardless of * diagram zoom * @param {number} [overlay.scale.min] * @param {number} [overlay.scale.max] * * @return {string} id that may be used to reference the overlay for update or removal */ Overlays.prototype.add = function(element, type, overlay) { if (isObject(type)) { overlay = type; type = null; } if (!element.id) { element = this._elementRegistry.get(element); } if (!overlay.position) { throw new Error('must specifiy overlay position'); } if (!overlay.html) { throw new Error('must specifiy overlay html'); } if (!element) { throw new Error('invalid element specified'); } var id = this._ids.next(); overlay = assign({}, this._overlayDefaults, overlay, { id: id, type: type, element: element, html: overlay.html }); this._addOverlay(overlay); return id; }; /** * Remove an overlay with the given id or all overlays matching the given filter. * * @see Overlays#get for filter options. * * @param {string|object} [filter] */ Overlays.prototype.remove = function(filter) { var overlays = this.get(filter) || []; if (!isArray$3(overlays)) { overlays = [ overlays ]; } var self = this; forEach$1(overlays, function(overlay) { var container = self._getOverlayContainer(overlay.element, true); if (overlay) { remove$2(overlay.html); remove$2(overlay.htmlContainer); delete overlay.htmlContainer; delete overlay.element; delete self._overlays[overlay.id]; } if (container) { var idx = container.overlays.indexOf(overlay); if (idx !== -1) { container.overlays.splice(idx, 1); } } }); }; Overlays.prototype.show = function() { setVisible$1(this._overlayRoot); }; Overlays.prototype.hide = function() { setVisible$1(this._overlayRoot, false); }; Overlays.prototype.clear = function() { this._overlays = {}; this._overlayContainers = []; clear$1(this._overlayRoot); }; Overlays.prototype._updateOverlayContainer = function(container) { var element = container.element, html = container.html; // update container left,top according to the elements x,y coordinates // this ensures we can attach child elements relative to this container var x = element.x, y = element.y; if (element.waypoints) { var bbox = getBBox(element); x = bbox.x; y = bbox.y; } setPosition$1(html, x, y); attr$1(container.html, 'data-container-id', element.id); }; Overlays.prototype._updateOverlay = function(overlay) { var position = overlay.position, htmlContainer = overlay.htmlContainer, element = overlay.element; // update overlay html relative to shape because // it is already positioned on the element // update relative var left = position.left, top = position.top; if (position.right !== undefined) { var width; if (element.waypoints) { width = getBBox(element).width; } else { width = element.width; } left = position.right * -1 + width; } if (position.bottom !== undefined) { var height; if (element.waypoints) { height = getBBox(element).height; } else { height = element.height; } top = position.bottom * -1 + height; } setPosition$1(htmlContainer, left || 0, top || 0); this._updateOverlayVisibilty(overlay, this._canvas.viewbox()); }; Overlays.prototype._createOverlayContainer = function(element) { var html = domify('
'); assign$1(html, { position: 'absolute' }); this._overlayRoot.appendChild(html); var container = { html: html, element: element, overlays: [] }; this._updateOverlayContainer(container); this._overlayContainers.push(container); return container; }; Overlays.prototype._updateRoot = function(viewbox) { var scale = viewbox.scale || 1; var matrix = 'matrix(' + [ scale, 0, 0, scale, -1 * viewbox.x * scale, -1 * viewbox.y * scale ].join(',') + ')'; setTransform$1(this._overlayRoot, matrix); }; Overlays.prototype._getOverlayContainer = function(element, raw) { var container = find(this._overlayContainers, function(c) { return c.element === element; }); if (!container && !raw) { return this._createOverlayContainer(element); } return container; }; Overlays.prototype._addOverlay = function(overlay) { var id = overlay.id, element = overlay.element, html = overlay.html, htmlContainer, overlayContainer; // unwrap jquery (for those who need it) if (html.get && html.constructor.prototype.jquery) { html = html.get(0); } // create proper html elements from // overlay HTML strings if (isString(html)) { html = domify(html); } overlayContainer = this._getOverlayContainer(element); htmlContainer = domify('
'); assign$1(htmlContainer, { position: 'absolute' }); htmlContainer.appendChild(html); if (overlay.type) { classes$1(htmlContainer).add('djs-overlay-' + overlay.type); } var elementRoot = this._canvas.findRoot(element); var activeRoot = this._canvas.getRootElement(); setVisible$1(htmlContainer, elementRoot === activeRoot); overlay.htmlContainer = htmlContainer; overlayContainer.overlays.push(overlay); overlayContainer.html.appendChild(htmlContainer); this._overlays[id] = overlay; this._updateOverlay(overlay); this._updateOverlayVisibilty(overlay, this._canvas.viewbox()); }; Overlays.prototype._updateOverlayVisibilty = function(overlay, viewbox) { var show = overlay.show, rootElement = this._canvas.findRoot(overlay.element), minZoom = show && show.minZoom, maxZoom = show && show.maxZoom, htmlContainer = overlay.htmlContainer, activeRootElement = this._canvas.getRootElement(), visible = true; if (rootElement !== activeRootElement) { visible = false; } else if (show) { if ( (isDefined(minZoom) && minZoom > viewbox.scale) || (isDefined(maxZoom) && maxZoom < viewbox.scale) ) { visible = false; } } setVisible$1(htmlContainer, visible); this._updateOverlayScale(overlay, viewbox); }; Overlays.prototype._updateOverlayScale = function(overlay, viewbox) { var shouldScale = overlay.scale, minScale, maxScale, htmlContainer = overlay.htmlContainer; var scale, transform = ''; if (shouldScale !== true) { if (shouldScale === false) { minScale = 1; maxScale = 1; } else { minScale = shouldScale.min; maxScale = shouldScale.max; } if (isDefined(minScale) && viewbox.scale < minScale) { scale = (1 / viewbox.scale || 1) * minScale; } if (isDefined(maxScale) && viewbox.scale > maxScale) { scale = (1 / viewbox.scale || 1) * maxScale; } } if (isDefined(scale)) { transform = 'scale(' + scale + ',' + scale + ')'; } setTransform$1(htmlContainer, transform); }; Overlays.prototype._updateOverlaysVisibilty = function(viewbox) { var self = this; forEach$1(this._overlays, function(overlay) { self._updateOverlayVisibilty(overlay, viewbox); }); }; Overlays.prototype._init = function() { var eventBus = this._eventBus; var self = this; // scroll/zoom integration function updateViewbox(viewbox) { self._updateRoot(viewbox); self._updateOverlaysVisibilty(viewbox); self.show(); } eventBus.on('canvas.viewbox.changing', function(event) { self.hide(); }); eventBus.on('canvas.viewbox.changed', function(event) { updateViewbox(event.viewbox); }); // remove integration eventBus.on([ 'shape.remove', 'connection.remove' ], function(e) { var element = e.element; var overlays = self.get({ element: element }); forEach$1(overlays, function(o) { self.remove(o.id); }); var container = self._getOverlayContainer(element); if (container) { remove$2(container.html); var i = self._overlayContainers.indexOf(container); if (i !== -1) { self._overlayContainers.splice(i, 1); } } }); // move integration eventBus.on('element.changed', LOW_PRIORITY$o, function(e) { var element = e.element; var container = self._getOverlayContainer(element, true); if (container) { forEach$1(container.overlays, function(overlay) { self._updateOverlay(overlay); }); self._updateOverlayContainer(container); } }); // marker integration, simply add them on the overlays as classes, too. eventBus.on('element.marker.update', function(e) { var container = self._getOverlayContainer(e.element, true); if (container) { classes$1(container.html)[e.add ? 'add' : 'remove'](e.marker); } }); eventBus.on('root.set', function() { self._updateOverlaysVisibilty(self._canvas.viewbox()); }); // clear overlays with diagram eventBus.on('diagram.clear', this.clear, this); }; // helpers ///////////////////////////// function createRoot$1(parentNode) { var root = domify( '
' ); assign$1(root, { position: 'absolute', width: 0, height: 0 }); parentNode.insertBefore(root, parentNode.firstChild); return root; } function setPosition$1(el, x, y) { assign$1(el, { left: x + 'px', top: y + 'px' }); } /** * Set element visible * * @param {DOMElement} el * @param {boolean} [visible=true] */ function setVisible$1(el, visible) { el.style.display = visible === false ? 'none' : ''; } function setTransform$1(el, transform) { el.style['transform-origin'] = 'top left'; [ '', '-ms-', '-webkit-' ].forEach(function(prefix) { el.style[prefix + 'transform'] = transform; }); } var OverlaysModule = { __init__: [ 'overlays' ], overlays: [ 'type', Overlays ] }; /** * Adds change support to the diagram, including * *
    *
  • redrawing shapes and connections on change
  • *
* * @param {EventBus} eventBus * @param {Canvas} canvas * @param {ElementRegistry} elementRegistry * @param {GraphicsFactory} graphicsFactory */ function ChangeSupport( eventBus, canvas, elementRegistry, graphicsFactory) { // redraw shapes / connections on change eventBus.on('element.changed', function(event) { var element = event.element; // element might have been deleted and replaced by new element with same ID // thus check for parent of element except for root element if (element.parent || element === canvas.getRootElement()) { event.gfx = elementRegistry.getGraphics(element); } // shape + gfx may have been deleted if (!event.gfx) { return; } eventBus.fire(getType(element) + '.changed', event); }); eventBus.on('elements.changed', function(event) { var elements = event.elements; elements.forEach(function(e) { eventBus.fire('element.changed', { element: e }); }); graphicsFactory.updateContainments(elements); }); eventBus.on('shape.changed', function(event) { graphicsFactory.update('shape', event.element, event.gfx); }); eventBus.on('connection.changed', function(event) { graphicsFactory.update('connection', event.element, event.gfx); }); } ChangeSupport.$inject = [ 'eventBus', 'canvas', 'elementRegistry', 'graphicsFactory' ]; var ChangeSupportModule = { __init__: [ 'changeSupport' ], changeSupport: [ 'type', ChangeSupport ] }; var DEFAULT_PRIORITY$4 = 1000; /** * A utility that can be used to plug-in into the command execution for * extension and/or validation. * * @param {EventBus} eventBus * * @example * * import inherits from 'inherits-browser'; * * import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; * * function CommandLogger(eventBus) { * CommandInterceptor.call(this, eventBus); * * this.preExecute(function(event) { * console.log('command pre-execute', event); * }); * } * * inherits(CommandLogger, CommandInterceptor); * */ function CommandInterceptor(eventBus) { this._eventBus = eventBus; } CommandInterceptor.$inject = [ 'eventBus' ]; function unwrapEvent(fn, that) { return function(event) { return fn.call(that || null, event.context, event.command, event); }; } /** * Register an interceptor for a command execution * * @param {string|Array} [events] list of commands to register on * @param {string} [hook] command hook, i.e. preExecute, executed to listen on * @param {number} [priority] the priority on which to hook into the execution * @param {Function} handlerFn interceptor to be invoked with (event) * @param {boolean} unwrap if true, unwrap the event and pass (context, command, event) to the * listener instead * @param {Object} [that] Pass context (`this`) to the handler function */ CommandInterceptor.prototype.on = function(events, hook, priority, handlerFn, unwrap, that) { if (isFunction(hook) || isNumber(hook)) { that = unwrap; unwrap = handlerFn; handlerFn = priority; priority = hook; hook = null; } if (isFunction(priority)) { that = unwrap; unwrap = handlerFn; handlerFn = priority; priority = DEFAULT_PRIORITY$4; } if (isObject(unwrap)) { that = unwrap; unwrap = false; } if (!isFunction(handlerFn)) { throw new Error('handlerFn must be a function'); } if (!isArray$3(events)) { events = [ events ]; } var eventBus = this._eventBus; forEach$1(events, function(event) { // concat commandStack(.event)?(.hook)? var fullEvent = [ 'commandStack', event, hook ].filter(function(e) { return e; }).join('.'); eventBus.on(fullEvent, priority, unwrap ? unwrapEvent(handlerFn, that) : handlerFn, that); }); }; var hooks = [ 'canExecute', 'preExecute', 'preExecuted', 'execute', 'executed', 'postExecute', 'postExecuted', 'revert', 'reverted' ]; /* * Install hook shortcuts * * This will generate the CommandInterceptor#(preExecute|...|reverted) methods * which will in term forward to CommandInterceptor#on. */ forEach$1(hooks, function(hook) { /** * {canExecute|preExecute|preExecuted|execute|executed|postExecute|postExecuted|revert|reverted} * * A named hook for plugging into the command execution * * @param {string|Array} [events] list of commands to register on * @param {number} [priority] the priority on which to hook into the execution * @param {Function} handlerFn interceptor to be invoked with (event) * @param {boolean} [unwrap=false] if true, unwrap the event and pass (context, command, event) to the * listener instead * @param {Object} [that] Pass context (`this`) to the handler function */ CommandInterceptor.prototype[hook] = function(events, priority, handlerFn, unwrap, that) { if (isFunction(events) || isNumber(events)) { that = unwrap; unwrap = handlerFn; handlerFn = priority; priority = events; events = null; } this.on(events, hook, priority, handlerFn, unwrap, that); }; }); /** * A modeling behavior that ensures we set the correct root element * as we undo and redo commands. * * @param {Canvas} canvas * @param {didi.Injector} injector */ function RootElementsBehavior(canvas, injector) { injector.invoke(CommandInterceptor, this); this.executed(function(event) { var context = event.context; if (context.rootElement) { canvas.setRootElement(context.rootElement); } else { context.rootElement = canvas.getRootElement(); } }); this.revert(function(event) { var context = event.context; if (context.rootElement) { canvas.setRootElement(context.rootElement); } }); } e(RootElementsBehavior, CommandInterceptor); RootElementsBehavior.$inject = [ 'canvas', 'injector' ]; var RootElementsModule = { __init__: [ 'rootElementsBehavior' ], rootElementsBehavior: [ 'type', RootElementsBehavior ] }; var css_escape = {exports: {}}; /*! https://mths.be/cssescape v1.5.1 by @mathias | MIT license */ (function (module, exports) { (function(root, factory) { // https://github.com/umdjs/umd/blob/master/returnExports.js { // For Node.js. module.exports = factory(root); } }(typeof commonjsGlobal != 'undefined' ? commonjsGlobal : commonjsGlobal, function(root) { if (root.CSS && root.CSS.escape) { return root.CSS.escape; } // https://drafts.csswg.org/cssom/#serialize-an-identifier var cssEscape = function(value) { if (arguments.length == 0) { throw new TypeError('`CSS.escape` requires an argument.'); } var string = String(value); var length = string.length; var index = -1; var codeUnit; var result = ''; var firstCodeUnit = string.charCodeAt(0); while (++index < length) { codeUnit = string.charCodeAt(index); // Note: there’s no need to special-case astral symbols, surrogate // pairs, or lone surrogates. // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER // (U+FFFD). if (codeUnit == 0x0000) { result += '\uFFFD'; continue; } if ( // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is // U+007F, […] (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F || // If the character is the first character and is in the range [0-9] // (U+0030 to U+0039), […] (index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || // If the character is the second character and is in the range [0-9] // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] ( index == 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && firstCodeUnit == 0x002D ) ) { // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point result += '\\' + codeUnit.toString(16) + ' '; continue; } if ( // If the character is the first character and is a `-` (U+002D), and // there is no second character, […] index == 0 && length == 1 && codeUnit == 0x002D ) { result += '\\' + string.charAt(index); continue; } // If the character is not handled by one of the above rules and is // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to // U+005A), or [a-z] (U+0061 to U+007A), […] if ( codeUnit >= 0x0080 || codeUnit == 0x002D || codeUnit == 0x005F || codeUnit >= 0x0030 && codeUnit <= 0x0039 || codeUnit >= 0x0041 && codeUnit <= 0x005A || codeUnit >= 0x0061 && codeUnit <= 0x007A ) { // the character itself result += string.charAt(index); continue; } // Otherwise, the escaped character. // https://drafts.csswg.org/cssom/#escape-a-character result += '\\' + string.charAt(index); } return result; }; if (!root.CSS) { root.CSS = {}; } root.CSS.escape = cssEscape; return cssEscape; })); } (css_escape)); var cssEscape = css_escape.exports; var HTML_ESCAPE_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' }; function escapeHTML(str) { str = '' + str; return str && str.replace(/[&<>"']/g, function(match) { return HTML_ESCAPE_MAP[match]; }); } var planeSuffix = '_plane'; /** * Get primary shape ID for a plane. * * @param {djs.model.Base|ModdleElement} element * * @returns {String} */ function getShapeIdFromPlane(element) { var id = element.id; return removePlaneSuffix(id); } /** * Get plane ID for a primary shape. * * @param {djs.model.Base|ModdleElement} element * * @returns {String} */ function getPlaneIdFromShape(element) { var id = element.id; if (is$1(element, 'bpmn:SubProcess')) { return addPlaneSuffix(id); } return id; } /** * Get plane ID for primary shape ID. * * @param {String} id * * @returns {String} */ function toPlaneId(id) { return addPlaneSuffix(id); } /** * Check wether element is plane. * * @param {djs.model.Base|ModdleElement} element * * @returns {Boolean} */ function isPlane(element) { var di = getDi(element); return is$1(di, 'bpmndi:BPMNPlane'); } function addPlaneSuffix(id) { return id + planeSuffix; } function removePlaneSuffix(id) { return id.replace(new RegExp(planeSuffix + '$'), ''); } var OPEN_CLASS = 'bjs-breadcrumbs-shown'; /** * Adds overlays that allow switching planes on collapsed subprocesses. * * @param {eventBus} eventBus * @param {elementRegistry} elementRegistry * @param {overlays} overlays * @param {canvas} canvas */ function DrilldownBreadcrumbs(eventBus, elementRegistry, overlays, canvas) { var breadcrumbs = domify('
    '); var container = canvas.getContainer(); var containerClasses = classes$1(container); container.appendChild(breadcrumbs); var boParents = []; // update breadcrumbs if name or ID of the primary shape changes eventBus.on('element.changed', function(e) { var shape = e.element, bo = getBusinessObject(shape); var isPresent = find(boParents, function(el) { return el === bo; }); if (!isPresent) { return; } updateBreadcrumbs(); }); /** * Updates the displayed breadcrumbs. If no element is provided, only the * labels are updated. * * @param {djs.model.Base} [element] */ function updateBreadcrumbs(element) { if (element) { boParents = getBoParentChain(element); } var path = boParents.map(function(parent) { var title = escapeHTML(parent.name || parent.id); var link = domify('
  • ' + title + '
  • '); var parentPlane = canvas.findRoot(getPlaneIdFromShape(parent)) || canvas.findRoot(parent.id); // when the root is a collaboration, the process does not have a corresponding // element in the elementRegisty. Instead, we search for the corresponding participant if (!parentPlane && is$1(parent, 'bpmn:Process')) { var participant = elementRegistry.find(function(element) { var bo = getBusinessObject(element); return bo && bo.processRef && bo.processRef === parent; }); parentPlane = canvas.findRoot(participant.id); } link.addEventListener('click', function() { canvas.setRootElement(parentPlane); }); return link; }); breadcrumbs.innerHTML = ''; // show breadcrumbs and expose state to .djs-container var visible = path.length > 1; containerClasses.toggle(OPEN_CLASS, visible); path.forEach(function(el) { breadcrumbs.appendChild(el); }); } eventBus.on('root.set', function(event) { updateBreadcrumbs(event.element); }); } DrilldownBreadcrumbs.$inject = [ 'eventBus', 'elementRegistry', 'overlays', 'canvas' ]; // helpers ////////// /** * Returns the parents for the element using the business object chain, * starting with the root element. * * @param {djs.model.Shape} child * * @returns {Array} parents */ function getBoParentChain(child) { var bo = getBusinessObject(child); var parents = []; for (var element = bo; element; element = element.$parent) { if (is$1(element, 'bpmn:SubProcess') || is$1(element, 'bpmn:Process')) { parents.push(element); } } return parents.reverse(); } /** * Move collapsed subprocesses into view when drilling down. * * Zoom and scroll are saved in a session. * * @param {eventBus} eventBus * @param {canvas} canvas */ function DrilldownCentering(eventBus, canvas) { var currentRoot = null; var positionMap = new Map(); eventBus.on('root.set', function(event) { var newRoot = event.element; var currentViewbox = canvas.viewbox(); var storedViewbox = positionMap.get(newRoot); positionMap.set(currentRoot, { x: currentViewbox.x, y: currentViewbox.y, zoom: currentViewbox.scale }); currentRoot = newRoot; // current root was replaced with a collaboration, we don't update the viewbox if (is$1(newRoot, 'bpmn:Collaboration') && !storedViewbox) { return; } storedViewbox = storedViewbox || { x: 0, y: 0, zoom: 1 }; var dx = (currentViewbox.x - storedViewbox.x) * currentViewbox.scale, dy = (currentViewbox.y - storedViewbox.y) * currentViewbox.scale; if (dx !== 0 || dy !== 0) { canvas.scroll({ dx: dx, dy: dy }); } if (storedViewbox.zoom !== currentViewbox.scale) { canvas.zoom(storedViewbox.zoom, { x: 0, y: 0 }); } }); eventBus.on('diagram.clear', function() { positionMap.clear(); currentRoot = null; }); } DrilldownCentering.$inject = [ 'eventBus', 'canvas' ]; /** * ES5 Map implementation. Works. */ function Map() { this._entries = []; this.set = function(key, value) { var found = false; for (var k in this._entries) { if (this._entries[k][0] === key) { this._entries[k][1] = value; found = true; break; } } if (!found) { this._entries.push([ key, value ]); } }; this.get = function(key) { for (var k in this._entries) { if (this._entries[k][0] === key) { return this._entries[k][1]; } } return null; }; this.clear = function() { this._entries.length = 0; }; this.remove = function(key) { var idx = -1; for (var k in this._entries) { if (this._entries[k][0] === key) { idx = k; break; } } if (idx !== -1) { this._entries.splice(idx, 1); } }; } var DEFAULT_POSITION$1 = { x: 180, y: 160 }; /** * Hook into `import.render.start` and create new planes for diagrams with * collapsed subprocesses and all dis on the same plane. * * @param {eventBus} eventBus * @param {moddle} moddle */ function SubprocessCompatibility(eventBus, moddle) { this._eventBus = eventBus; this._moddle = moddle; var self = this; eventBus.on('import.render.start', 1500, function(e, context) { self.handleImport(context.definitions); }); } SubprocessCompatibility.prototype.handleImport = function(definitions) { if (!definitions.diagrams) { return; } var self = this; this._definitions = definitions; this._processToDiagramMap = {}; definitions.diagrams.forEach(function(diagram) { if (!diagram.plane || !diagram.plane.bpmnElement) { return; } self._processToDiagramMap[diagram.plane.bpmnElement.id] = diagram; }); var newDiagrams = []; definitions.diagrams.forEach(function(diagram) { var createdDiagrams = self.createNewDiagrams(diagram.plane); Array.prototype.push.apply(newDiagrams, createdDiagrams); }); newDiagrams.forEach(function(diagram) { self.movePlaneElementsToOrigin(diagram.plane); }); }; /** * Moves all DI elements from collapsed subprocesses to a new plane. * * @param {Object} plane * @return {Array} new diagrams created for the collapsed subprocesses */ SubprocessCompatibility.prototype.createNewDiagrams = function(plane) { var self = this; var collapsedElements = []; var elementsToMove = []; plane.get('planeElement').forEach(function(diElement) { var bo = diElement.bpmnElement; if (!bo) { return; } var parent = bo.$parent; if (is$1(bo, 'bpmn:SubProcess') && !diElement.isExpanded) { collapsedElements.push(bo); } if (shouldMoveToPlane(bo, plane)) { // don't change the array while we iterate over it elementsToMove.push({ diElement: diElement, parent: parent }); } }); var newDiagrams = []; // create new planes for all collapsed subprocesses, even when they are empty collapsedElements.forEach(function(element) { if (!self._processToDiagramMap[element.id]) { var diagram = self.createDiagram(element); self._processToDiagramMap[element.id] = diagram; newDiagrams.push(diagram); } }); elementsToMove.forEach(function(element) { var diElement = element.diElement; var parent = element.parent; // parent is expanded, get nearest collapsed parent while (parent && collapsedElements.indexOf(parent) === -1) { parent = parent.$parent; } // false positive, all parents are expanded if (!parent) { return; } var diagram = self._processToDiagramMap[parent.id]; self.moveToDiPlane(diElement, diagram.plane); }); return newDiagrams; }; SubprocessCompatibility.prototype.movePlaneElementsToOrigin = function(plane) { var elements = plane.get('planeElement'); // get bounding box of all elements var planeBounds = getPlaneBounds(plane); var offset = { x: planeBounds.x - DEFAULT_POSITION$1.x, y: planeBounds.y - DEFAULT_POSITION$1.y }; elements.forEach(function(diElement) { if (diElement.waypoint) { diElement.waypoint.forEach(function(waypoint) { waypoint.x = waypoint.x - offset.x; waypoint.y = waypoint.y - offset.y; }); } else if (diElement.bounds) { diElement.bounds.x = diElement.bounds.x - offset.x; diElement.bounds.y = diElement.bounds.y - offset.y; } }); }; SubprocessCompatibility.prototype.moveToDiPlane = function(diElement, newPlane) { var containingDiagram = findRootDiagram(diElement); // remove DI from old Plane and add it to the new one var parentPlaneElement = containingDiagram.plane.get('planeElement'); parentPlaneElement.splice(parentPlaneElement.indexOf(diElement), 1); newPlane.get('planeElement').push(diElement); }; SubprocessCompatibility.prototype.createDiagram = function(bo) { var plane = this._moddle.create('bpmndi:BPMNPlane', { bpmnElement: bo }); var diagram = this._moddle.create('bpmndi:BPMNDiagram', { plane: plane }); plane.$parent = diagram; plane.bpmnElement = bo; diagram.$parent = this._definitions; this._definitions.diagrams.push(diagram); return diagram; }; SubprocessCompatibility.$inject = [ 'eventBus', 'moddle' ]; // helpers ////////////////////////// function findRootDiagram(element) { if (is$1(element, 'bpmndi:BPMNDiagram')) { return element; } else { return findRootDiagram(element.$parent); } } function getPlaneBounds(plane) { var planeTrbl = { top: Infinity, right: -Infinity, bottom: -Infinity, left: Infinity }; plane.planeElement.forEach(function(element) { if (!element.bounds) { return; } var trbl = asTRBL(element.bounds); planeTrbl.top = Math.min(trbl.top, planeTrbl.top); planeTrbl.left = Math.min(trbl.left, planeTrbl.left); }); return asBounds(planeTrbl); } function shouldMoveToPlane(bo, plane) { var parent = bo.$parent; // don't move elements that are already on the plane if (!is$1(parent, 'bpmn:SubProcess') || parent === plane.bpmnElement) { return false; } // dataAssociations are children of the subprocess but rendered on process level // cf. https://github.com/bpmn-io/bpmn-js/issues/1619 if (isAny(bo, [ 'bpmn:DataInputAssociation', 'bpmn:DataOutputAssociation' ])) { return false; } return true; } var LOW_PRIORITY$n = 250; var ARROW_DOWN_SVG = ''; var EMPTY_MARKER = 'bjs-drilldown-empty'; function DrilldownOverlayBehavior( canvas, eventBus, elementRegistry, overlays ) { CommandInterceptor.call(this, eventBus); this._canvas = canvas; this._eventBus = eventBus; this._elementRegistry = elementRegistry; this._overlays = overlays; var self = this; this.executed('shape.toggleCollapse', LOW_PRIORITY$n, function(context) { var shape = context.shape; // Add overlay to the collapsed shape if (self.canDrillDown(shape)) { self.addOverlay(shape); } else { self.removeOverlay(shape); } }, true); this.reverted('shape.toggleCollapse', LOW_PRIORITY$n, function(context) { var shape = context.shape; // Add overlay to the collapsed shape if (self.canDrillDown(shape)) { self.addOverlay(shape); } else { self.removeOverlay(shape); } }, true); this.executed([ 'shape.create', 'shape.move', 'shape.delete' ], LOW_PRIORITY$n, function(context) { var oldParent = context.oldParent, newParent = context.newParent || context.parent, shape = context.shape; // Add overlay to the collapsed shape if (self.canDrillDown(shape)) { self.addOverlay(shape); } self.updateDrilldownOverlay(oldParent); self.updateDrilldownOverlay(newParent); self.updateDrilldownOverlay(shape); }, true); this.reverted([ 'shape.create', 'shape.move', 'shape.delete' ], LOW_PRIORITY$n, function(context) { var oldParent = context.oldParent, newParent = context.newParent || context.parent, shape = context.shape; // Add overlay to the collapsed shape if (self.canDrillDown(shape)) { self.addOverlay(shape); } self.updateDrilldownOverlay(oldParent); self.updateDrilldownOverlay(newParent); self.updateDrilldownOverlay(shape); }, true); eventBus.on('import.render.complete', function() { elementRegistry.filter(function(e) { return self.canDrillDown(e); }).map(function(el) { self.addOverlay(el); }); }); } e(DrilldownOverlayBehavior, CommandInterceptor); DrilldownOverlayBehavior.prototype.updateDrilldownOverlay = function(shape) { var canvas = this._canvas; if (!shape) { return; } var root = canvas.findRoot(shape); if (root) { this.updateOverlayVisibility(root); } }; DrilldownOverlayBehavior.prototype.canDrillDown = function(element) { var canvas = this._canvas; return is$1(element, 'bpmn:SubProcess') && canvas.findRoot(getPlaneIdFromShape(element)); }; /** * Updates visibility of the drilldown overlay. If the plane has no elements, * the drilldown will be only shown when the element is selected. * * @param {djs.model.Shape|djs.model.Root} element collapsed shape or root element */ DrilldownOverlayBehavior.prototype.updateOverlayVisibility = function(element) { var overlays = this._overlays; var bo = element.businessObject; var overlay = overlays.get({ element: bo.id, type: 'drilldown' })[0]; if (!overlay) { return; } var hasContent = bo && bo.flowElements && bo.flowElements.length; classes$1(overlay.html).toggle(EMPTY_MARKER, !hasContent); }; /** * Attaches a drilldown button to the given element. We assume that the plane has * the same id as the element. * * @param {djs.model.Shape} element collapsed shape */ DrilldownOverlayBehavior.prototype.addOverlay = function(element) { var canvas = this._canvas; var overlays = this._overlays; var existingOverlays = overlays.get({ element: element, type: 'drilldown' }); if (existingOverlays.length) { this.removeOverlay(element); } var button = domify(''); button.addEventListener('click', function() { canvas.setRootElement(canvas.findRoot(getPlaneIdFromShape(element))); }); overlays.add(element, 'drilldown', { position: { bottom: -7, right: -8 }, html: button }); this.updateOverlayVisibility(element); }; DrilldownOverlayBehavior.prototype.removeOverlay = function(element) { var overlays = this._overlays; overlays.remove({ element: element, type: 'drilldown' }); }; DrilldownOverlayBehavior.$inject = [ 'canvas', 'eventBus', 'elementRegistry', 'overlays' ]; var DrilldownModdule = { __depends__: [ OverlaysModule, ChangeSupportModule, RootElementsModule ], __init__: [ 'drilldownBreadcrumbs', 'drilldownOverlayBehavior', 'drilldownCentering', 'subprocessCompatibility' ], drilldownBreadcrumbs: [ 'type', DrilldownBreadcrumbs ], drilldownCentering: [ 'type', DrilldownCentering ], drilldownOverlayBehavior: [ 'type', DrilldownOverlayBehavior ], subprocessCompatibility: [ 'type', SubprocessCompatibility ] }; /** * A viewer for BPMN 2.0 diagrams. * * Have a look at {@link NavigatedViewer} or {@link Modeler} for bundles that include * additional features. * * * ## Extending the Viewer * * In order to extend the viewer pass extension modules to bootstrap via the * `additionalModules` option. An extension module is an object that exposes * named services. * * The following example depicts the integration of a simple * logging component that integrates with interaction events: * * * ```javascript * * // logging component * function InteractionLogger(eventBus) { * eventBus.on('element.hover', function(event) { * console.log() * }) * } * * InteractionLogger.$inject = [ 'eventBus' ]; // minification save * * // extension module * var extensionModule = { * __init__: [ 'interactionLogger' ], * interactionLogger: [ 'type', InteractionLogger ] * }; * * // extend the viewer * var bpmnViewer = new Viewer({ additionalModules: [ extensionModule ] }); * bpmnViewer.importXML(...); * ``` * * @param {Object} [options] configuration options to pass to the viewer * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. * @param {string|number} [options.width] the width of the viewer * @param {string|number} [options.height] the height of the viewer * @param {Object} [options.moddleExtensions] extension packages to provide * @param {Array} [options.modules] a list of modules to override the default modules * @param {Array} [options.additionalModules] a list of modules to use with the default modules */ function Viewer(options) { BaseViewer.call(this, options); } e(Viewer, BaseViewer); // modules the viewer is composed of Viewer.prototype._modules = [ CoreModule, translate, SelectionModule, OverlaysModule, DrilldownModdule ]; // default moddle extensions the viewer is composed of Viewer.prototype._moddleExtensions = {}; var KEYCODE_C = 67; var KEYCODE_V = 86; var KEYCODE_Y = 89; var KEYCODE_Z = 90; var KEYS_COPY = [ 'c', 'C', KEYCODE_C ]; var KEYS_PASTE = [ 'v', 'V', KEYCODE_V ]; var KEYS_REDO = [ 'y', 'Y', KEYCODE_Y ]; var KEYS_UNDO = [ 'z', 'Z', KEYCODE_Z ]; /** * Returns true if event was triggered with any modifier * @param {KeyboardEvent} event */ function hasModifier(event) { return (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey); } /** * @param {KeyboardEvent} event */ function isCmd(event) { // ensure we don't react to AltGr // (mapped to CTRL + ALT) if (event.altKey) { return false; } return event.ctrlKey || event.metaKey; } /** * Checks if key pressed is one of provided keys. * * @param {string|Array} keys * @param {KeyboardEvent} event */ function isKey(keys, event) { keys = isArray$3(keys) ? keys : [ keys ]; return keys.indexOf(event.key) !== -1 || keys.indexOf(event.keyCode) !== -1; } /** * @param {KeyboardEvent} event */ function isShift(event) { return event.shiftKey; } function isCopy(event) { return isCmd(event) && isKey(KEYS_COPY, event); } function isPaste(event) { return isCmd(event) && isKey(KEYS_PASTE, event); } function isUndo(event) { return isCmd(event) && !isShift(event) && isKey(KEYS_UNDO, event); } function isRedo(event) { return isCmd(event) && ( isKey(KEYS_REDO, event) || ( isKey(KEYS_UNDO, event) && isShift(event) ) ); } var KEYDOWN_EVENT = 'keyboard.keydown', KEYUP_EVENT = 'keyboard.keyup'; var HANDLE_MODIFIER_ATTRIBUTE = 'input-handle-modified-keys'; var DEFAULT_PRIORITY$3 = 1000; /** * A keyboard abstraction that may be activated and * deactivated by users at will, consuming key events * and triggering diagram actions. * * For keys pressed down, keyboard fires `keyboard.keydown` event. * The event context contains one field which is `KeyboardEvent` event. * * The implementation fires the following key events that allow * other components to hook into key handling: * * - keyboard.bind * - keyboard.unbind * - keyboard.init * - keyboard.destroy * * All events contain one field which is node. * * A default binding for the keyboard may be specified via the * `keyboard.bindTo` configuration option. * * @param {Config} config * @param {EventBus} eventBus */ function Keyboard(config, eventBus) { var self = this; this._config = config || {}; this._eventBus = eventBus; this._keydownHandler = this._keydownHandler.bind(this); this._keyupHandler = this._keyupHandler.bind(this); // properly clean dom registrations eventBus.on('diagram.destroy', function() { self._fire('destroy'); self.unbind(); }); eventBus.on('diagram.init', function() { self._fire('init'); }); eventBus.on('attach', function() { if (config && config.bindTo) { self.bind(config.bindTo); } }); eventBus.on('detach', function() { self.unbind(); }); } Keyboard.$inject = [ 'config.keyboard', 'eventBus' ]; Keyboard.prototype._keydownHandler = function(event) { this._keyHandler(event, KEYDOWN_EVENT); }; Keyboard.prototype._keyupHandler = function(event) { this._keyHandler(event, KEYUP_EVENT); }; Keyboard.prototype._keyHandler = function(event, type) { var eventBusResult; if (this._isEventIgnored(event)) { return; } var context = { keyEvent: event }; eventBusResult = this._eventBus.fire(type || KEYDOWN_EVENT, context); if (eventBusResult) { event.preventDefault(); } }; Keyboard.prototype._isEventIgnored = function(event) { return isInput(event.target) && this._isModifiedKeyIgnored(event); }; Keyboard.prototype._isModifiedKeyIgnored = function(event) { if (!isCmd(event)) { return true; } var allowedModifiers = this._getAllowedModifiers(event.target); return allowedModifiers.indexOf(event.key) === -1; }; Keyboard.prototype._getAllowedModifiers = function(element) { var modifierContainer = closest(element, '[' + HANDLE_MODIFIER_ATTRIBUTE + ']', true); if (!modifierContainer || (this._node && !this._node.contains(modifierContainer))) { return []; } return modifierContainer.getAttribute(HANDLE_MODIFIER_ATTRIBUTE).split(','); }; Keyboard.prototype.bind = function(node) { // make sure that the keyboard is only bound once to the DOM this.unbind(); this._node = node; // bind key events componentEvent.bind(node, 'keydown', this._keydownHandler, true); componentEvent.bind(node, 'keyup', this._keyupHandler, true); this._fire('bind'); }; Keyboard.prototype.getBinding = function() { return this._node; }; Keyboard.prototype.unbind = function() { var node = this._node; if (node) { this._fire('unbind'); // unbind key events componentEvent.unbind(node, 'keydown', this._keydownHandler, true); componentEvent.unbind(node, 'keyup', this._keyupHandler, true); } this._node = null; }; Keyboard.prototype._fire = function(event) { this._eventBus.fire('keyboard.' + event, { node: this._node }); }; /** * Add a listener function that is notified with `KeyboardEvent` whenever * the keyboard is bound and the user presses a key. If no priority is * provided, the default value of 1000 is used. * * @param {number} [priority] * @param {Function} listener * @param {string} type */ Keyboard.prototype.addListener = function(priority, listener, type) { if (isFunction(priority)) { type = listener; listener = priority; priority = DEFAULT_PRIORITY$3; } this._eventBus.on(type || KEYDOWN_EVENT, priority, listener); }; Keyboard.prototype.removeListener = function(listener, type) { this._eventBus.off(type || KEYDOWN_EVENT, listener); }; Keyboard.prototype.hasModifier = hasModifier; Keyboard.prototype.isCmd = isCmd; Keyboard.prototype.isShift = isShift; Keyboard.prototype.isKey = isKey; // helpers /////// function isInput(target) { return target && (matchesSelector(target, 'input, textarea') || target.contentEditable === 'true'); } var LOW_PRIORITY$m = 500; /** * Adds default keyboard bindings. * * This does not pull in any features will bind only actions that * have previously been registered against the editorActions component. * * @param {EventBus} eventBus * @param {Keyboard} keyboard */ function KeyboardBindings(eventBus, keyboard) { var self = this; eventBus.on('editorActions.init', LOW_PRIORITY$m, function(event) { var editorActions = event.editorActions; self.registerBindings(keyboard, editorActions); }); } KeyboardBindings.$inject = [ 'eventBus', 'keyboard' ]; /** * Register available keyboard bindings. * * @param {Keyboard} keyboard * @param {EditorActions} editorActions */ KeyboardBindings.prototype.registerBindings = function(keyboard, editorActions) { /** * Add keyboard binding if respective editor action * is registered. * * @param {string} action name * @param {Function} fn that implements the key binding */ function addListener(action, fn) { if (editorActions.isRegistered(action)) { keyboard.addListener(fn); } } // undo // (CTRL|CMD) + Z addListener('undo', function(context) { var event = context.keyEvent; if (isUndo(event)) { editorActions.trigger('undo'); return true; } }); // redo // CTRL + Y // CMD + SHIFT + Z addListener('redo', function(context) { var event = context.keyEvent; if (isRedo(event)) { editorActions.trigger('redo'); return true; } }); // copy // CTRL/CMD + C addListener('copy', function(context) { var event = context.keyEvent; if (isCopy(event)) { editorActions.trigger('copy'); return true; } }); // paste // CTRL/CMD + V addListener('paste', function(context) { var event = context.keyEvent; if (isPaste(event)) { editorActions.trigger('paste'); return true; } }); // zoom in one step // CTRL/CMD + + addListener('stepZoom', function(context) { var event = context.keyEvent; // quirk: it has to be triggered by `=` as well to work on international keyboard layout // cf: https://github.com/bpmn-io/bpmn-js/issues/1362#issuecomment-722989754 if (isKey([ '+', 'Add', '=' ], event) && isCmd(event)) { editorActions.trigger('stepZoom', { value: 1 }); return true; } }); // zoom out one step // CTRL + - addListener('stepZoom', function(context) { var event = context.keyEvent; if (isKey([ '-', 'Subtract' ], event) && isCmd(event)) { editorActions.trigger('stepZoom', { value: -1 }); return true; } }); // zoom to the default level // CTRL + 0 addListener('zoom', function(context) { var event = context.keyEvent; if (isKey('0', event) && isCmd(event)) { editorActions.trigger('zoom', { value: 1 }); return true; } }); // delete selected element // DEL addListener('removeSelection', function(context) { var event = context.keyEvent; if (isKey([ 'Backspace', 'Delete', 'Del' ], event)) { editorActions.trigger('removeSelection'); return true; } }); }; var KeyboardModule$1 = { __init__: [ 'keyboard', 'keyboardBindings' ], keyboard: [ 'type', Keyboard ], keyboardBindings: [ 'type', KeyboardBindings ] }; var DEFAULT_CONFIG$1 = { moveSpeed: 50, moveSpeedAccelerated: 200 }; /** * A feature that allows users to move the canvas using the keyboard. * * @param {Object} config * @param {number} [config.moveSpeed=50] * @param {number} [config.moveSpeedAccelerated=200] * @param {Keyboard} keyboard * @param {Canvas} canvas */ function KeyboardMove( config, keyboard, canvas ) { var self = this; this._config = assign({}, DEFAULT_CONFIG$1, config || {}); keyboard.addListener(arrowsListener); function arrowsListener(context) { var event = context.keyEvent, config = self._config; if (!keyboard.isCmd(event)) { return; } if (keyboard.isKey([ 'ArrowLeft', 'Left', 'ArrowUp', 'Up', 'ArrowDown', 'Down', 'ArrowRight', 'Right' ], event)) { var speed = ( keyboard.isShift(event) ? config.moveSpeedAccelerated : config.moveSpeed ); var direction; switch (event.key) { case 'ArrowLeft': case 'Left': direction = 'left'; break; case 'ArrowUp': case 'Up': direction = 'up'; break; case 'ArrowRight': case 'Right': direction = 'right'; break; case 'ArrowDown': case 'Down': direction = 'down'; break; } self.moveCanvas({ speed: speed, direction: direction }); return true; } } this.moveCanvas = function(opts) { var dx = 0, dy = 0, speed = opts.speed; var actualSpeed = speed / Math.min(Math.sqrt(canvas.viewbox().scale), 1); switch (opts.direction) { case 'left': // Left dx = actualSpeed; break; case 'up': // Up dy = actualSpeed; break; case 'right': // Right dx = -actualSpeed; break; case 'down': // Down dy = -actualSpeed; break; } canvas.scroll({ dx: dx, dy: dy }); }; } KeyboardMove.$inject = [ 'config.keyboardMove', 'keyboard', 'canvas' ]; var KeyboardMoveModule = { __depends__: [ KeyboardModule$1 ], __init__: [ 'keyboardMove' ], keyboardMove: [ 'type', KeyboardMove ] }; var CURSOR_CLS_PATTERN = /^djs-cursor-.*$/; function set(mode) { var classes = classes$1(document.body); classes.removeMatching(CURSOR_CLS_PATTERN); if (mode) { classes.add('djs-cursor-' + mode); } } function unset() { set(null); } var TRAP_PRIORITY = 5000; /** * Installs a click trap that prevents a ghost click following a dragging operation. * * @return {Function} a function to immediately remove the installed trap. */ function install(eventBus, eventName) { eventName = eventName || 'element.click'; function trap() { return false; } eventBus.once(eventName, TRAP_PRIORITY, trap); return function() { eventBus.off(eventName, trap); }; } function center(bounds) { return { x: bounds.x + (bounds.width / 2), y: bounds.y + (bounds.height / 2) }; } function delta(a, b) { return { x: a.x - b.x, y: a.y - b.y }; } var THRESHOLD$1 = 15; /** * Move the canvas via mouse. * * @param {EventBus} eventBus * @param {Canvas} canvas */ function MoveCanvas(eventBus, canvas) { var context; // listen for move on element mouse down; // allow others to hook into the event before us though // (dragging / element moving will do this) eventBus.on('element.mousedown', 500, function(e) { return handleStart(e.originalEvent); }); function handleMove(event) { var start = context.start, button = context.button, position = toPoint(event), delta$1 = delta(position, start); if (!context.dragging && length(delta$1) > THRESHOLD$1) { context.dragging = true; if (button === 0) { install(eventBus); } set('grab'); } if (context.dragging) { var lastPosition = context.last || context.start; delta$1 = delta(position, lastPosition); canvas.scroll({ dx: delta$1.x, dy: delta$1.y }); context.last = position; } // prevent select event.preventDefault(); } function handleEnd(event) { componentEvent.unbind(document, 'mousemove', handleMove); componentEvent.unbind(document, 'mouseup', handleEnd); context = null; unset(); } function handleStart(event) { // event is already handled by '.djs-draggable' if (closest(event.target, '.djs-draggable')) { return; } var button = event.button; // reject right mouse button or modifier key if (button >= 2 || event.ctrlKey || event.shiftKey || event.altKey) { return; } context = { button: button, start: toPoint(event) }; componentEvent.bind(document, 'mousemove', handleMove); componentEvent.bind(document, 'mouseup', handleEnd); // we've handled the event return true; } this.isActive = function() { return !!context; }; } MoveCanvas.$inject = [ 'eventBus', 'canvas' ]; // helpers /////// function length(point) { return Math.sqrt(Math.pow(point.x, 2) + Math.pow(point.y, 2)); } var MoveCanvasModule = { __init__: [ 'moveCanvas' ], moveCanvas: [ 'type', MoveCanvas ] }; /** * Get the logarithm of x with base 10 * @param {Integer} value */ function log10(x) { return Math.log(x) / Math.log(10); } /** * Get step size for given range and number of steps. * * @param {Object} range * @param {number} range.min * @param {number} range.max */ function getStepSize(range, steps) { var minLinearRange = log10(range.min), maxLinearRange = log10(range.max); var absoluteLinearRange = Math.abs(minLinearRange) + Math.abs(maxLinearRange); return absoluteLinearRange / steps; } function cap(range, scale) { return Math.max(range.min, Math.min(range.max, scale)); } var sign = Math.sign || function(n) { return n >= 0 ? 1 : -1; }; var RANGE = { min: 0.2, max: 4 }, NUM_STEPS = 10; var DELTA_THRESHOLD = 0.1; var DEFAULT_SCALE = 0.75; /** * An implementation of zooming and scrolling within the * {@link Canvas} via the mouse wheel. * * Mouse wheel zooming / scrolling may be disabled using * the {@link toggle(enabled)} method. * * @param {Object} [config] * @param {boolean} [config.enabled=true] default enabled state * @param {number} [config.scale=.75] scroll sensivity * @param {EventBus} eventBus * @param {Canvas} canvas */ function ZoomScroll(config, eventBus, canvas) { config = config || {}; this._enabled = false; this._canvas = canvas; this._container = canvas._container; this._handleWheel = bind(this._handleWheel, this); this._totalDelta = 0; this._scale = config.scale || DEFAULT_SCALE; var self = this; eventBus.on('canvas.init', function(e) { self._init(config.enabled !== false); }); } ZoomScroll.$inject = [ 'config.zoomScroll', 'eventBus', 'canvas' ]; ZoomScroll.prototype.scroll = function scroll(delta) { this._canvas.scroll(delta); }; ZoomScroll.prototype.reset = function reset() { this._canvas.zoom('fit-viewport'); }; /** * Zoom depending on delta. * * @param {number} delta * @param {Object} position */ ZoomScroll.prototype.zoom = function zoom(delta, position) { // zoom with half the step size of stepZoom var stepSize = getStepSize(RANGE, NUM_STEPS * 2); // add until threshold reached this._totalDelta += delta; if (Math.abs(this._totalDelta) > DELTA_THRESHOLD) { this._zoom(delta, position, stepSize); // reset this._totalDelta = 0; } }; ZoomScroll.prototype._handleWheel = function handleWheel(event) { // event is already handled by '.djs-scrollable' if (closest(event.target, '.djs-scrollable', true)) { return; } var element = this._container; event.preventDefault(); // pinch to zoom is mapped to wheel + ctrlKey = true // in modern browsers (!) var isZoom = event.ctrlKey; var isHorizontalScroll = event.shiftKey; var factor = -1 * this._scale, delta; if (isZoom) { factor *= event.deltaMode === 0 ? 0.020 : 0.32; } else { factor *= event.deltaMode === 0 ? 1.0 : 16.0; } if (isZoom) { var elementRect = element.getBoundingClientRect(); var offset = { x: event.clientX - elementRect.left, y: event.clientY - elementRect.top }; delta = ( Math.sqrt( Math.pow(event.deltaY, 2) + Math.pow(event.deltaX, 2) ) * sign(event.deltaY) * factor ); // zoom in relative to diagram {x,y} coordinates this.zoom(delta, offset); } else { if (isHorizontalScroll) { delta = { dx: factor * event.deltaY, dy: 0 }; } else { delta = { dx: factor * event.deltaX, dy: factor * event.deltaY }; } this.scroll(delta); } }; /** * Zoom with fixed step size. * * @param {number} delta - Zoom delta (1 for zooming in, -1 for out). * @param {Object} position */ ZoomScroll.prototype.stepZoom = function stepZoom(delta, position) { var stepSize = getStepSize(RANGE, NUM_STEPS); this._zoom(delta, position, stepSize); }; /** * Zoom in/out given a step size. * * @param {number} delta * @param {Object} position * @param {number} stepSize */ ZoomScroll.prototype._zoom = function(delta, position, stepSize) { var canvas = this._canvas; var direction = delta > 0 ? 1 : -1; var currentLinearZoomLevel = log10(canvas.zoom()); // snap to a proximate zoom step var newLinearZoomLevel = Math.round(currentLinearZoomLevel / stepSize) * stepSize; // increase or decrease one zoom step in the given direction newLinearZoomLevel += stepSize * direction; // calculate the absolute logarithmic zoom level based on the linear zoom level // (e.g. 2 for an absolute x2 zoom) var newLogZoomLevel = Math.pow(10, newLinearZoomLevel); canvas.zoom(cap(RANGE, newLogZoomLevel), position); }; /** * Toggle the zoom scroll ability via mouse wheel. * * @param {boolean} [newEnabled] new enabled state */ ZoomScroll.prototype.toggle = function toggle(newEnabled) { var element = this._container; var handleWheel = this._handleWheel; var oldEnabled = this._enabled; if (typeof newEnabled === 'undefined') { newEnabled = !oldEnabled; } // only react on actual changes if (oldEnabled !== newEnabled) { // add or remove wheel listener based on // changed enabled state componentEvent[newEnabled ? 'bind' : 'unbind'](element, 'wheel', handleWheel, false); } this._enabled = newEnabled; return newEnabled; }; ZoomScroll.prototype._init = function(newEnabled) { this.toggle(newEnabled); }; var ZoomScrollModule = { __init__: [ 'zoomScroll' ], zoomScroll: [ 'type', ZoomScroll ] }; /** * A viewer that includes mouse navigation facilities * * @param {Object} options */ function NavigatedViewer(options) { Viewer.call(this, options); } e(NavigatedViewer, Viewer); NavigatedViewer.prototype._navigationModules = [ KeyboardMoveModule, MoveCanvasModule, ZoomScrollModule ]; NavigatedViewer.prototype._modules = [].concat( Viewer.prototype._modules, NavigatedViewer.prototype._navigationModules ); var hammer = {exports: {}}; /*! Hammer.JS - v2.0.7 - 2016-04-22 * http://hammerjs.github.io/ * * Copyright (c) 2016 Jorik Tangelder; * Licensed under the MIT license */ (function (module) { (function(window, document, exportName, undefined$1) { var VENDOR_PREFIXES = ['', 'webkit', 'Moz', 'MS', 'ms', 'o']; var TEST_ELEMENT = document.createElement('div'); var TYPE_FUNCTION = 'function'; var round = Math.round; var abs = Math.abs; var now = Date.now; /** * set a timeout with a given scope * @param {Function} fn * @param {Number} timeout * @param {Object} context * @returns {number} */ function setTimeoutContext(fn, timeout, context) { return setTimeout(bindFn(fn, context), timeout); } /** * if the argument is an array, we want to execute the fn on each entry * if it aint an array we don't want to do a thing. * this is used by all the methods that accept a single and array argument. * @param {*|Array} arg * @param {String} fn * @param {Object} [context] * @returns {Boolean} */ function invokeArrayArg(arg, fn, context) { if (Array.isArray(arg)) { each(arg, context[fn], context); return true; } return false; } /** * walk objects and arrays * @param {Object} obj * @param {Function} iterator * @param {Object} context */ function each(obj, iterator, context) { var i; if (!obj) { return; } if (obj.forEach) { obj.forEach(iterator, context); } else if (obj.length !== undefined$1) { i = 0; while (i < obj.length) { iterator.call(context, obj[i], i, obj); i++; } } else { for (i in obj) { obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); } } } /** * wrap a method with a deprecation warning and stack trace * @param {Function} method * @param {String} name * @param {String} message * @returns {Function} A new function wrapping the supplied method. */ function deprecate(method, name, message) { var deprecationMessage = 'DEPRECATED METHOD: ' + name + '\n' + message + ' AT \n'; return function() { var e = new Error('get-stack-trace'); var stack = e && e.stack ? e.stack.replace(/^[^\(]+?[\n$]/gm, '') .replace(/^\s+at\s+/gm, '') .replace(/^Object.\s*\(/gm, '{anonymous}()@') : 'Unknown Stack Trace'; var log = window.console && (window.console.warn || window.console.log); if (log) { log.call(window.console, deprecationMessage, stack); } return method.apply(this, arguments); }; } /** * extend object. * means that properties in dest will be overwritten by the ones in src. * @param {Object} target * @param {...Object} objects_to_assign * @returns {Object} target */ var assign; if (typeof Object.assign !== 'function') { assign = function assign(target) { if (target === undefined$1 || target === null) { throw new TypeError('Cannot convert undefined or null to object'); } var output = Object(target); for (var index = 1; index < arguments.length; index++) { var source = arguments[index]; if (source !== undefined$1 && source !== null) { for (var nextKey in source) { if (source.hasOwnProperty(nextKey)) { output[nextKey] = source[nextKey]; } } } } return output; }; } else { assign = Object.assign; } /** * extend object. * means that properties in dest will be overwritten by the ones in src. * @param {Object} dest * @param {Object} src * @param {Boolean} [merge=false] * @returns {Object} dest */ var extend = deprecate(function extend(dest, src, merge) { var keys = Object.keys(src); var i = 0; while (i < keys.length) { if (!merge || (merge && dest[keys[i]] === undefined$1)) { dest[keys[i]] = src[keys[i]]; } i++; } return dest; }, 'extend', 'Use `assign`.'); /** * merge the values from src in the dest. * means that properties that exist in dest will not be overwritten by src * @param {Object} dest * @param {Object} src * @returns {Object} dest */ var merge = deprecate(function merge(dest, src) { return extend(dest, src, true); }, 'merge', 'Use `assign`.'); /** * simple class inheritance * @param {Function} child * @param {Function} base * @param {Object} [properties] */ function inherit(child, base, properties) { var baseP = base.prototype, childP; childP = child.prototype = Object.create(baseP); childP.constructor = child; childP._super = baseP; if (properties) { assign(childP, properties); } } /** * simple function bind * @param {Function} fn * @param {Object} context * @returns {Function} */ function bindFn(fn, context) { return function boundFn() { return fn.apply(context, arguments); }; } /** * let a boolean value also be a function that must return a boolean * this first item in args will be used as the context * @param {Boolean|Function} val * @param {Array} [args] * @returns {Boolean} */ function boolOrFn(val, args) { if (typeof val == TYPE_FUNCTION) { return val.apply(args ? args[0] || undefined$1 : undefined$1, args); } return val; } /** * use the val2 when val1 is undefined * @param {*} val1 * @param {*} val2 * @returns {*} */ function ifUndefined(val1, val2) { return (val1 === undefined$1) ? val2 : val1; } /** * addEventListener with multiple events at once * @param {EventTarget} target * @param {String} types * @param {Function} handler */ function addEventListeners(target, types, handler) { each(splitStr(types), function(type) { target.addEventListener(type, handler, false); }); } /** * removeEventListener with multiple events at once * @param {EventTarget} target * @param {String} types * @param {Function} handler */ function removeEventListeners(target, types, handler) { each(splitStr(types), function(type) { target.removeEventListener(type, handler, false); }); } /** * find if a node is in the given parent * @method hasParent * @param {HTMLElement} node * @param {HTMLElement} parent * @return {Boolean} found */ function hasParent(node, parent) { while (node) { if (node == parent) { return true; } node = node.parentNode; } return false; } /** * small indexOf wrapper * @param {String} str * @param {String} find * @returns {Boolean} found */ function inStr(str, find) { return str.indexOf(find) > -1; } /** * split string on whitespace * @param {String} str * @returns {Array} words */ function splitStr(str) { return str.trim().split(/\s+/g); } /** * find if a array contains the object using indexOf or a simple polyFill * @param {Array} src * @param {String} find * @param {String} [findByKey] * @return {Boolean|Number} false when not found, or the index */ function inArray(src, find, findByKey) { if (src.indexOf && !findByKey) { return src.indexOf(find); } else { var i = 0; while (i < src.length) { if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { return i; } i++; } return -1; } } /** * convert array-like objects to real arrays * @param {Object} obj * @returns {Array} */ function toArray(obj) { return Array.prototype.slice.call(obj, 0); } /** * unique array with objects based on a key (like 'id') or just by the array's value * @param {Array} src [{id:1},{id:2},{id:1}] * @param {String} [key] * @param {Boolean} [sort=False] * @returns {Array} [{id:1},{id:2}] */ function uniqueArray(src, key, sort) { var results = []; var values = []; var i = 0; while (i < src.length) { var val = key ? src[i][key] : src[i]; if (inArray(values, val) < 0) { results.push(src[i]); } values[i] = val; i++; } if (sort) { if (!key) { results = results.sort(); } else { results = results.sort(function sortUniqueArray(a, b) { return a[key] > b[key]; }); } } return results; } /** * get the prefixed property * @param {Object} obj * @param {String} property * @returns {String|Undefined} prefixed */ function prefixed(obj, property) { var prefix, prop; var camelProp = property[0].toUpperCase() + property.slice(1); var i = 0; while (i < VENDOR_PREFIXES.length) { prefix = VENDOR_PREFIXES[i]; prop = (prefix) ? prefix + camelProp : property; if (prop in obj) { return prop; } i++; } return undefined$1; } /** * get a unique id * @returns {number} uniqueId */ var _uniqueId = 1; function uniqueId() { return _uniqueId++; } /** * get the window object of an element * @param {HTMLElement} element * @returns {DocumentView|Window} */ function getWindowForElement(element) { var doc = element.ownerDocument || element; return (doc.defaultView || doc.parentWindow || window); } var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; var SUPPORT_TOUCH = ('ontouchstart' in window); var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined$1; var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); var INPUT_TYPE_TOUCH = 'touch'; var INPUT_TYPE_PEN = 'pen'; var INPUT_TYPE_MOUSE = 'mouse'; var INPUT_TYPE_KINECT = 'kinect'; var COMPUTE_INTERVAL = 25; var INPUT_START = 1; var INPUT_MOVE = 2; var INPUT_END = 4; var INPUT_CANCEL = 8; var DIRECTION_NONE = 1; var DIRECTION_LEFT = 2; var DIRECTION_RIGHT = 4; var DIRECTION_UP = 8; var DIRECTION_DOWN = 16; var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; var PROPS_XY = ['x', 'y']; var PROPS_CLIENT_XY = ['clientX', 'clientY']; /** * create new input type manager * @param {Manager} manager * @param {Function} callback * @returns {Input} * @constructor */ function Input(manager, callback) { var self = this; this.manager = manager; this.callback = callback; this.element = manager.element; this.target = manager.options.inputTarget; // smaller wrapper around the handler, for the scope and the enabled state of the manager, // so when disabled the input events are completely bypassed. this.domHandler = function(ev) { if (boolOrFn(manager.options.enable, [manager])) { self.handler(ev); } }; this.init(); } Input.prototype = { /** * should handle the inputEvent data and trigger the callback * @virtual */ handler: function() { }, /** * bind the events */ init: function() { this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); }, /** * unbind the events */ destroy: function() { this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); } }; /** * create new input type manager * called by the Manager constructor * @param {Hammer} manager * @returns {Input} */ function createInputInstance(manager) { var Type; var inputClass = manager.options.inputClass; if (inputClass) { Type = inputClass; } else if (SUPPORT_POINTER_EVENTS) { Type = PointerEventInput; } else if (SUPPORT_ONLY_TOUCH) { Type = TouchInput; } else if (!SUPPORT_TOUCH) { Type = MouseInput; } else { Type = TouchMouseInput; } return new (Type)(manager, inputHandler); } /** * handle input events * @param {Manager} manager * @param {String} eventType * @param {Object} input */ function inputHandler(manager, eventType, input) { var pointersLen = input.pointers.length; var changedPointersLen = input.changedPointers.length; var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); input.isFirst = !!isFirst; input.isFinal = !!isFinal; if (isFirst) { manager.session = {}; } // source event is the normalized value of the domEvents // like 'touchstart, mouseup, pointerdown' input.eventType = eventType; // compute scale, rotation etc computeInputData(manager, input); // emit secret event manager.emit('hammer.input', input); manager.recognize(input); manager.session.prevInput = input; } /** * extend the data with some usable properties like scale, rotate, velocity etc * @param {Object} manager * @param {Object} input */ function computeInputData(manager, input) { var session = manager.session; var pointers = input.pointers; var pointersLength = pointers.length; // store the first input to calculate the distance and direction if (!session.firstInput) { session.firstInput = simpleCloneInputData(input); } // to compute scale and rotation we need to store the multiple touches if (pointersLength > 1 && !session.firstMultiple) { session.firstMultiple = simpleCloneInputData(input); } else if (pointersLength === 1) { session.firstMultiple = false; } var firstInput = session.firstInput; var firstMultiple = session.firstMultiple; var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; var center = input.center = getCenter(pointers); input.timeStamp = now(); input.deltaTime = input.timeStamp - firstInput.timeStamp; input.angle = getAngle(offsetCenter, center); input.distance = getDistance(offsetCenter, center); computeDeltaXY(session, input); input.offsetDirection = getDirection(input.deltaX, input.deltaY); var overallVelocity = getVelocity(input.deltaTime, input.deltaX, input.deltaY); input.overallVelocityX = overallVelocity.x; input.overallVelocityY = overallVelocity.y; input.overallVelocity = (abs(overallVelocity.x) > abs(overallVelocity.y)) ? overallVelocity.x : overallVelocity.y; input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; input.maxPointers = !session.prevInput ? input.pointers.length : ((input.pointers.length > session.prevInput.maxPointers) ? input.pointers.length : session.prevInput.maxPointers); computeIntervalInputData(session, input); // find the correct target var target = manager.element; if (hasParent(input.srcEvent.target, target)) { target = input.srcEvent.target; } input.target = target; } function computeDeltaXY(session, input) { var center = input.center; var offset = session.offsetDelta || {}; var prevDelta = session.prevDelta || {}; var prevInput = session.prevInput || {}; if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { prevDelta = session.prevDelta = { x: prevInput.deltaX || 0, y: prevInput.deltaY || 0 }; offset = session.offsetDelta = { x: center.x, y: center.y }; } input.deltaX = prevDelta.x + (center.x - offset.x); input.deltaY = prevDelta.y + (center.y - offset.y); } /** * velocity is calculated every x ms * @param {Object} session * @param {Object} input */ function computeIntervalInputData(session, input) { var last = session.lastInterval || input, deltaTime = input.timeStamp - last.timeStamp, velocity, velocityX, velocityY, direction; if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined$1)) { var deltaX = input.deltaX - last.deltaX; var deltaY = input.deltaY - last.deltaY; var v = getVelocity(deltaTime, deltaX, deltaY); velocityX = v.x; velocityY = v.y; velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; direction = getDirection(deltaX, deltaY); session.lastInterval = input; } else { // use latest velocity info if it doesn't overtake a minimum period velocity = last.velocity; velocityX = last.velocityX; velocityY = last.velocityY; direction = last.direction; } input.velocity = velocity; input.velocityX = velocityX; input.velocityY = velocityY; input.direction = direction; } /** * create a simple clone from the input used for storage of firstInput and firstMultiple * @param {Object} input * @returns {Object} clonedInputData */ function simpleCloneInputData(input) { // make a simple copy of the pointers because we will get a reference if we don't // we only need clientXY for the calculations var pointers = []; var i = 0; while (i < input.pointers.length) { pointers[i] = { clientX: round(input.pointers[i].clientX), clientY: round(input.pointers[i].clientY) }; i++; } return { timeStamp: now(), pointers: pointers, center: getCenter(pointers), deltaX: input.deltaX, deltaY: input.deltaY }; } /** * get the center of all the pointers * @param {Array} pointers * @return {Object} center contains `x` and `y` properties */ function getCenter(pointers) { var pointersLength = pointers.length; // no need to loop when only one touch if (pointersLength === 1) { return { x: round(pointers[0].clientX), y: round(pointers[0].clientY) }; } var x = 0, y = 0, i = 0; while (i < pointersLength) { x += pointers[i].clientX; y += pointers[i].clientY; i++; } return { x: round(x / pointersLength), y: round(y / pointersLength) }; } /** * calculate the velocity between two points. unit is in px per ms. * @param {Number} deltaTime * @param {Number} x * @param {Number} y * @return {Object} velocity `x` and `y` */ function getVelocity(deltaTime, x, y) { return { x: x / deltaTime || 0, y: y / deltaTime || 0 }; } /** * get the direction between two points * @param {Number} x * @param {Number} y * @return {Number} direction */ function getDirection(x, y) { if (x === y) { return DIRECTION_NONE; } if (abs(x) >= abs(y)) { return x < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; } return y < 0 ? DIRECTION_UP : DIRECTION_DOWN; } /** * calculate the absolute distance between two points * @param {Object} p1 {x, y} * @param {Object} p2 {x, y} * @param {Array} [props] containing x and y keys * @return {Number} distance */ function getDistance(p1, p2, props) { if (!props) { props = PROPS_XY; } var x = p2[props[0]] - p1[props[0]], y = p2[props[1]] - p1[props[1]]; return Math.sqrt((x * x) + (y * y)); } /** * calculate the angle between two coordinates * @param {Object} p1 * @param {Object} p2 * @param {Array} [props] containing x and y keys * @return {Number} angle */ function getAngle(p1, p2, props) { if (!props) { props = PROPS_XY; } var x = p2[props[0]] - p1[props[0]], y = p2[props[1]] - p1[props[1]]; return Math.atan2(y, x) * 180 / Math.PI; } /** * calculate the rotation degrees between two pointersets * @param {Array} start array of pointers * @param {Array} end array of pointers * @return {Number} rotation */ function getRotation(start, end) { return getAngle(end[1], end[0], PROPS_CLIENT_XY) + getAngle(start[1], start[0], PROPS_CLIENT_XY); } /** * calculate the scale factor between two pointersets * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out * @param {Array} start array of pointers * @param {Array} end array of pointers * @return {Number} scale */ function getScale(start, end) { return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); } var MOUSE_INPUT_MAP = { mousedown: INPUT_START, mousemove: INPUT_MOVE, mouseup: INPUT_END }; var MOUSE_ELEMENT_EVENTS = 'mousedown'; var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; /** * Mouse events input * @constructor * @extends Input */ function MouseInput() { this.evEl = MOUSE_ELEMENT_EVENTS; this.evWin = MOUSE_WINDOW_EVENTS; this.pressed = false; // mousedown state Input.apply(this, arguments); } inherit(MouseInput, Input, { /** * handle mouse events * @param {Object} ev */ handler: function MEhandler(ev) { var eventType = MOUSE_INPUT_MAP[ev.type]; // on start we want to have the left mouse button down if (eventType & INPUT_START && ev.button === 0) { this.pressed = true; } if (eventType & INPUT_MOVE && ev.which !== 1) { eventType = INPUT_END; } // mouse must be down if (!this.pressed) { return; } if (eventType & INPUT_END) { this.pressed = false; } this.callback(this.manager, eventType, { pointers: [ev], changedPointers: [ev], pointerType: INPUT_TYPE_MOUSE, srcEvent: ev }); } }); var POINTER_INPUT_MAP = { pointerdown: INPUT_START, pointermove: INPUT_MOVE, pointerup: INPUT_END, pointercancel: INPUT_CANCEL, pointerout: INPUT_CANCEL }; // in IE10 the pointer types is defined as an enum var IE10_POINTER_TYPE_ENUM = { 2: INPUT_TYPE_TOUCH, 3: INPUT_TYPE_PEN, 4: INPUT_TYPE_MOUSE, 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 }; var POINTER_ELEMENT_EVENTS = 'pointerdown'; var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; // IE10 has prefixed support, and case-sensitive if (window.MSPointerEvent && !window.PointerEvent) { POINTER_ELEMENT_EVENTS = 'MSPointerDown'; POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; } /** * Pointer events input * @constructor * @extends Input */ function PointerEventInput() { this.evEl = POINTER_ELEMENT_EVENTS; this.evWin = POINTER_WINDOW_EVENTS; Input.apply(this, arguments); this.store = (this.manager.session.pointerEvents = []); } inherit(PointerEventInput, Input, { /** * handle mouse events * @param {Object} ev */ handler: function PEhandler(ev) { var store = this.store; var removePointer = false; var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; var isTouch = (pointerType == INPUT_TYPE_TOUCH); // get index of the event in the store var storeIndex = inArray(store, ev.pointerId, 'pointerId'); // start and mouse must be down if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { if (storeIndex < 0) { store.push(ev); storeIndex = store.length - 1; } } else if (eventType & (INPUT_END | INPUT_CANCEL)) { removePointer = true; } // it not found, so the pointer hasn't been down (so it's probably a hover) if (storeIndex < 0) { return; } // update the event in the store store[storeIndex] = ev; this.callback(this.manager, eventType, { pointers: store, changedPointers: [ev], pointerType: pointerType, srcEvent: ev }); if (removePointer) { // remove from the store store.splice(storeIndex, 1); } } }); var SINGLE_TOUCH_INPUT_MAP = { touchstart: INPUT_START, touchmove: INPUT_MOVE, touchend: INPUT_END, touchcancel: INPUT_CANCEL }; var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; /** * Touch events input * @constructor * @extends Input */ function SingleTouchInput() { this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; this.started = false; Input.apply(this, arguments); } inherit(SingleTouchInput, Input, { handler: function TEhandler(ev) { var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; // should we handle the touch events? if (type === INPUT_START) { this.started = true; } if (!this.started) { return; } var touches = normalizeSingleTouches.call(this, ev, type); // when done, reset the started state if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { this.started = false; } this.callback(this.manager, type, { pointers: touches[0], changedPointers: touches[1], pointerType: INPUT_TYPE_TOUCH, srcEvent: ev }); } }); /** * @this {TouchInput} * @param {Object} ev * @param {Number} type flag * @returns {undefined|Array} [all, changed] */ function normalizeSingleTouches(ev, type) { var all = toArray(ev.touches); var changed = toArray(ev.changedTouches); if (type & (INPUT_END | INPUT_CANCEL)) { all = uniqueArray(all.concat(changed), 'identifier', true); } return [all, changed]; } var TOUCH_INPUT_MAP = { touchstart: INPUT_START, touchmove: INPUT_MOVE, touchend: INPUT_END, touchcancel: INPUT_CANCEL }; var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; /** * Multi-user touch events input * @constructor * @extends Input */ function TouchInput() { this.evTarget = TOUCH_TARGET_EVENTS; this.targetIds = {}; Input.apply(this, arguments); } inherit(TouchInput, Input, { handler: function MTEhandler(ev) { var type = TOUCH_INPUT_MAP[ev.type]; var touches = getTouches.call(this, ev, type); if (!touches) { return; } this.callback(this.manager, type, { pointers: touches[0], changedPointers: touches[1], pointerType: INPUT_TYPE_TOUCH, srcEvent: ev }); } }); /** * @this {TouchInput} * @param {Object} ev * @param {Number} type flag * @returns {undefined|Array} [all, changed] */ function getTouches(ev, type) { var allTouches = toArray(ev.touches); var targetIds = this.targetIds; // when there is only one touch, the process can be simplified if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { targetIds[allTouches[0].identifier] = true; return [allTouches, allTouches]; } var i, targetTouches, changedTouches = toArray(ev.changedTouches), changedTargetTouches = [], target = this.target; // get target touches from touches targetTouches = allTouches.filter(function(touch) { return hasParent(touch.target, target); }); // collect touches if (type === INPUT_START) { i = 0; while (i < targetTouches.length) { targetIds[targetTouches[i].identifier] = true; i++; } } // filter changed touches to only contain touches that exist in the collected target ids i = 0; while (i < changedTouches.length) { if (targetIds[changedTouches[i].identifier]) { changedTargetTouches.push(changedTouches[i]); } // cleanup removed touches if (type & (INPUT_END | INPUT_CANCEL)) { delete targetIds[changedTouches[i].identifier]; } i++; } if (!changedTargetTouches.length) { return; } return [ // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), changedTargetTouches ]; } /** * Combined touch and mouse input * * Touch has a higher priority then mouse, and while touching no mouse events are allowed. * This because touch devices also emit mouse events while doing a touch. * * @constructor * @extends Input */ var DEDUP_TIMEOUT = 2500; var DEDUP_DISTANCE = 25; function TouchMouseInput() { Input.apply(this, arguments); var handler = bindFn(this.handler, this); this.touch = new TouchInput(this.manager, handler); this.mouse = new MouseInput(this.manager, handler); this.primaryTouch = null; this.lastTouches = []; } inherit(TouchMouseInput, Input, { /** * handle mouse and touch events * @param {Hammer} manager * @param {String} inputEvent * @param {Object} inputData */ handler: function TMEhandler(manager, inputEvent, inputData) { var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); if (isMouse && inputData.sourceCapabilities && inputData.sourceCapabilities.firesTouchEvents) { return; } // when we're in a touch event, record touches to de-dupe synthetic mouse event if (isTouch) { recordTouches.call(this, inputEvent, inputData); } else if (isMouse && isSyntheticEvent.call(this, inputData)) { return; } this.callback(manager, inputEvent, inputData); }, /** * remove the event listeners */ destroy: function destroy() { this.touch.destroy(); this.mouse.destroy(); } }); function recordTouches(eventType, eventData) { if (eventType & INPUT_START) { this.primaryTouch = eventData.changedPointers[0].identifier; setLastTouch.call(this, eventData); } else if (eventType & (INPUT_END | INPUT_CANCEL)) { setLastTouch.call(this, eventData); } } function setLastTouch(eventData) { var touch = eventData.changedPointers[0]; if (touch.identifier === this.primaryTouch) { var lastTouch = {x: touch.clientX, y: touch.clientY}; this.lastTouches.push(lastTouch); var lts = this.lastTouches; var removeLastTouch = function() { var i = lts.indexOf(lastTouch); if (i > -1) { lts.splice(i, 1); } }; setTimeout(removeLastTouch, DEDUP_TIMEOUT); } } function isSyntheticEvent(eventData) { var x = eventData.srcEvent.clientX, y = eventData.srcEvent.clientY; for (var i = 0; i < this.lastTouches.length; i++) { var t = this.lastTouches[i]; var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y); if (dx <= DEDUP_DISTANCE && dy <= DEDUP_DISTANCE) { return true; } } return false; } var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined$1; // magical touchAction value var TOUCH_ACTION_COMPUTE = 'compute'; var TOUCH_ACTION_AUTO = 'auto'; var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented var TOUCH_ACTION_NONE = 'none'; var TOUCH_ACTION_PAN_X = 'pan-x'; var TOUCH_ACTION_PAN_Y = 'pan-y'; var TOUCH_ACTION_MAP = getTouchActionProps(); /** * Touch Action * sets the touchAction property or uses the js alternative * @param {Manager} manager * @param {String} value * @constructor */ function TouchAction(manager, value) { this.manager = manager; this.set(value); } TouchAction.prototype = { /** * set the touchAction value on the element or enable the polyfill * @param {String} value */ set: function(value) { // find out the touch-action by the event handlers if (value == TOUCH_ACTION_COMPUTE) { value = this.compute(); } if (NATIVE_TOUCH_ACTION && this.manager.element.style && TOUCH_ACTION_MAP[value]) { this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; } this.actions = value.toLowerCase().trim(); }, /** * just re-set the touchAction value */ update: function() { this.set(this.manager.options.touchAction); }, /** * compute the value for the touchAction property based on the recognizer's settings * @returns {String} value */ compute: function() { var actions = []; each(this.manager.recognizers, function(recognizer) { if (boolOrFn(recognizer.options.enable, [recognizer])) { actions = actions.concat(recognizer.getTouchAction()); } }); return cleanTouchActions(actions.join(' ')); }, /** * this method is called on each input cycle and provides the preventing of the browser behavior * @param {Object} input */ preventDefaults: function(input) { var srcEvent = input.srcEvent; var direction = input.offsetDirection; // if the touch action did prevented once this session if (this.manager.session.prevented) { srcEvent.preventDefault(); return; } var actions = this.actions; var hasNone = inStr(actions, TOUCH_ACTION_NONE) && !TOUCH_ACTION_MAP[TOUCH_ACTION_NONE]; var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_Y]; var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_X]; if (hasNone) { //do not prevent defaults if this is a tap gesture var isTapPointer = input.pointers.length === 1; var isTapMovement = input.distance < 2; var isTapTouchTime = input.deltaTime < 250; if (isTapPointer && isTapMovement && isTapTouchTime) { return; } } if (hasPanX && hasPanY) { // `pan-x pan-y` means browser handles all scrolling/panning, do not prevent return; } if (hasNone || (hasPanY && direction & DIRECTION_HORIZONTAL) || (hasPanX && direction & DIRECTION_VERTICAL)) { return this.preventSrc(srcEvent); } }, /** * call preventDefault to prevent the browser's default behavior (scrolling in most cases) * @param {Object} srcEvent */ preventSrc: function(srcEvent) { this.manager.session.prevented = true; srcEvent.preventDefault(); } }; /** * when the touchActions are collected they are not a valid value, so we need to clean things up. * * @param {String} actions * @returns {*} */ function cleanTouchActions(actions) { // none if (inStr(actions, TOUCH_ACTION_NONE)) { return TOUCH_ACTION_NONE; } var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); // if both pan-x and pan-y are set (different recognizers // for different directions, e.g. horizontal pan but vertical swipe?) // we need none (as otherwise with pan-x pan-y combined none of these // recognizers will work, since the browser would handle all panning if (hasPanX && hasPanY) { return TOUCH_ACTION_NONE; } // pan-x OR pan-y if (hasPanX || hasPanY) { return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; } // manipulation if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { return TOUCH_ACTION_MANIPULATION; } return TOUCH_ACTION_AUTO; } function getTouchActionProps() { if (!NATIVE_TOUCH_ACTION) { return false; } var touchMap = {}; var cssSupports = window.CSS && window.CSS.supports; ['auto', 'manipulation', 'pan-y', 'pan-x', 'pan-x pan-y', 'none'].forEach(function(val) { // If css.supports is not supported but there is native touch-action assume it supports // all values. This is the case for IE 10 and 11. touchMap[val] = cssSupports ? window.CSS.supports('touch-action', val) : true; }); return touchMap; } /** * Recognizer flow explained; * * All recognizers have the initial state of POSSIBLE when a input session starts. * The definition of a input session is from the first input until the last input, with all it's movement in it. * * Example session for mouse-input: mousedown -> mousemove -> mouseup * * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed * which determines with state it should be. * * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to * POSSIBLE to give it another change on the next cycle. * * Possible * | * +-----+---------------+ * | | * +-----+-----+ | * | | | * Failed Cancelled | * +-------+------+ * | | * Recognized Began * | * Changed * | * Ended/Recognized */ var STATE_POSSIBLE = 1; var STATE_BEGAN = 2; var STATE_CHANGED = 4; var STATE_ENDED = 8; var STATE_RECOGNIZED = STATE_ENDED; var STATE_CANCELLED = 16; var STATE_FAILED = 32; /** * Recognizer * Every recognizer needs to extend from this class. * @constructor * @param {Object} options */ function Recognizer(options) { this.options = assign({}, this.defaults, options || {}); this.id = uniqueId(); this.manager = null; // default is enable true this.options.enable = ifUndefined(this.options.enable, true); this.state = STATE_POSSIBLE; this.simultaneous = {}; this.requireFail = []; } Recognizer.prototype = { /** * @virtual * @type {Object} */ defaults: {}, /** * set options * @param {Object} options * @return {Recognizer} */ set: function(options) { assign(this.options, options); // also update the touchAction, in case something changed about the directions/enabled state this.manager && this.manager.touchAction.update(); return this; }, /** * recognize simultaneous with an other recognizer. * @param {Recognizer} otherRecognizer * @returns {Recognizer} this */ recognizeWith: function(otherRecognizer) { if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { return this; } var simultaneous = this.simultaneous; otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); if (!simultaneous[otherRecognizer.id]) { simultaneous[otherRecognizer.id] = otherRecognizer; otherRecognizer.recognizeWith(this); } return this; }, /** * drop the simultaneous link. it doesnt remove the link on the other recognizer. * @param {Recognizer} otherRecognizer * @returns {Recognizer} this */ dropRecognizeWith: function(otherRecognizer) { if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { return this; } otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); delete this.simultaneous[otherRecognizer.id]; return this; }, /** * recognizer can only run when an other is failing * @param {Recognizer} otherRecognizer * @returns {Recognizer} this */ requireFailure: function(otherRecognizer) { if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { return this; } var requireFail = this.requireFail; otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); if (inArray(requireFail, otherRecognizer) === -1) { requireFail.push(otherRecognizer); otherRecognizer.requireFailure(this); } return this; }, /** * drop the requireFailure link. it does not remove the link on the other recognizer. * @param {Recognizer} otherRecognizer * @returns {Recognizer} this */ dropRequireFailure: function(otherRecognizer) { if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { return this; } otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); var index = inArray(this.requireFail, otherRecognizer); if (index > -1) { this.requireFail.splice(index, 1); } return this; }, /** * has require failures boolean * @returns {boolean} */ hasRequireFailures: function() { return this.requireFail.length > 0; }, /** * if the recognizer can recognize simultaneous with an other recognizer * @param {Recognizer} otherRecognizer * @returns {Boolean} */ canRecognizeWith: function(otherRecognizer) { return !!this.simultaneous[otherRecognizer.id]; }, /** * You should use `tryEmit` instead of `emit` directly to check * that all the needed recognizers has failed before emitting. * @param {Object} input */ emit: function(input) { var self = this; var state = this.state; function emit(event) { self.manager.emit(event, input); } // 'panstart' and 'panmove' if (state < STATE_ENDED) { emit(self.options.event + stateStr(state)); } emit(self.options.event); // simple 'eventName' events if (input.additionalEvent) { // additional event(panleft, panright, pinchin, pinchout...) emit(input.additionalEvent); } // panend and pancancel if (state >= STATE_ENDED) { emit(self.options.event + stateStr(state)); } }, /** * Check that all the require failure recognizers has failed, * if true, it emits a gesture event, * otherwise, setup the state to FAILED. * @param {Object} input */ tryEmit: function(input) { if (this.canEmit()) { return this.emit(input); } // it's failing anyway this.state = STATE_FAILED; }, /** * can we emit? * @returns {boolean} */ canEmit: function() { var i = 0; while (i < this.requireFail.length) { if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { return false; } i++; } return true; }, /** * update the recognizer * @param {Object} inputData */ recognize: function(inputData) { // make a new copy of the inputData // so we can change the inputData without messing up the other recognizers var inputDataClone = assign({}, inputData); // is is enabled and allow recognizing? if (!boolOrFn(this.options.enable, [this, inputDataClone])) { this.reset(); this.state = STATE_FAILED; return; } // reset when we've reached the end if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { this.state = STATE_POSSIBLE; } this.state = this.process(inputDataClone); // the recognizer has recognized a gesture // so trigger an event if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { this.tryEmit(inputDataClone); } }, /** * return the state of the recognizer * the actual recognizing happens in this method * @virtual * @param {Object} inputData * @returns {Const} STATE */ process: function(inputData) { }, // jshint ignore:line /** * return the preferred touch-action * @virtual * @returns {Array} */ getTouchAction: function() { }, /** * called when the gesture isn't allowed to recognize * like when another is being recognized or it is disabled * @virtual */ reset: function() { } }; /** * get a usable string, used as event postfix * @param {Const} state * @returns {String} state */ function stateStr(state) { if (state & STATE_CANCELLED) { return 'cancel'; } else if (state & STATE_ENDED) { return 'end'; } else if (state & STATE_CHANGED) { return 'move'; } else if (state & STATE_BEGAN) { return 'start'; } return ''; } /** * direction cons to string * @param {Const} direction * @returns {String} */ function directionStr(direction) { if (direction == DIRECTION_DOWN) { return 'down'; } else if (direction == DIRECTION_UP) { return 'up'; } else if (direction == DIRECTION_LEFT) { return 'left'; } else if (direction == DIRECTION_RIGHT) { return 'right'; } return ''; } /** * get a recognizer by name if it is bound to a manager * @param {Recognizer|String} otherRecognizer * @param {Recognizer} recognizer * @returns {Recognizer} */ function getRecognizerByNameIfManager(otherRecognizer, recognizer) { var manager = recognizer.manager; if (manager) { return manager.get(otherRecognizer); } return otherRecognizer; } /** * This recognizer is just used as a base for the simple attribute recognizers. * @constructor * @extends Recognizer */ function AttrRecognizer() { Recognizer.apply(this, arguments); } inherit(AttrRecognizer, Recognizer, { /** * @namespace * @memberof AttrRecognizer */ defaults: { /** * @type {Number} * @default 1 */ pointers: 1 }, /** * Used to check if it the recognizer receives valid input, like input.distance > 10. * @memberof AttrRecognizer * @param {Object} input * @returns {Boolean} recognized */ attrTest: function(input) { var optionPointers = this.options.pointers; return optionPointers === 0 || input.pointers.length === optionPointers; }, /** * Process the input and return the state for the recognizer * @memberof AttrRecognizer * @param {Object} input * @returns {*} State */ process: function(input) { var state = this.state; var eventType = input.eventType; var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); var isValid = this.attrTest(input); // on cancel input and we've recognized before, return STATE_CANCELLED if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { return state | STATE_CANCELLED; } else if (isRecognized || isValid) { if (eventType & INPUT_END) { return state | STATE_ENDED; } else if (!(state & STATE_BEGAN)) { return STATE_BEGAN; } return state | STATE_CHANGED; } return STATE_FAILED; } }); /** * Pan * Recognized when the pointer is down and moved in the allowed direction. * @constructor * @extends AttrRecognizer */ function PanRecognizer() { AttrRecognizer.apply(this, arguments); this.pX = null; this.pY = null; } inherit(PanRecognizer, AttrRecognizer, { /** * @namespace * @memberof PanRecognizer */ defaults: { event: 'pan', threshold: 10, pointers: 1, direction: DIRECTION_ALL }, getTouchAction: function() { var direction = this.options.direction; var actions = []; if (direction & DIRECTION_HORIZONTAL) { actions.push(TOUCH_ACTION_PAN_Y); } if (direction & DIRECTION_VERTICAL) { actions.push(TOUCH_ACTION_PAN_X); } return actions; }, directionTest: function(input) { var options = this.options; var hasMoved = true; var distance = input.distance; var direction = input.direction; var x = input.deltaX; var y = input.deltaY; // lock to axis? if (!(direction & options.direction)) { if (options.direction & DIRECTION_HORIZONTAL) { direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; hasMoved = x != this.pX; distance = Math.abs(input.deltaX); } else { direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; hasMoved = y != this.pY; distance = Math.abs(input.deltaY); } } input.direction = direction; return hasMoved && distance > options.threshold && direction & options.direction; }, attrTest: function(input) { return AttrRecognizer.prototype.attrTest.call(this, input) && (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); }, emit: function(input) { this.pX = input.deltaX; this.pY = input.deltaY; var direction = directionStr(input.direction); if (direction) { input.additionalEvent = this.options.event + direction; } this._super.emit.call(this, input); } }); /** * Pinch * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). * @constructor * @extends AttrRecognizer */ function PinchRecognizer() { AttrRecognizer.apply(this, arguments); } inherit(PinchRecognizer, AttrRecognizer, { /** * @namespace * @memberof PinchRecognizer */ defaults: { event: 'pinch', threshold: 0, pointers: 2 }, getTouchAction: function() { return [TOUCH_ACTION_NONE]; }, attrTest: function(input) { return this._super.attrTest.call(this, input) && (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); }, emit: function(input) { if (input.scale !== 1) { var inOut = input.scale < 1 ? 'in' : 'out'; input.additionalEvent = this.options.event + inOut; } this._super.emit.call(this, input); } }); /** * Press * Recognized when the pointer is down for x ms without any movement. * @constructor * @extends Recognizer */ function PressRecognizer() { Recognizer.apply(this, arguments); this._timer = null; this._input = null; } inherit(PressRecognizer, Recognizer, { /** * @namespace * @memberof PressRecognizer */ defaults: { event: 'press', pointers: 1, time: 251, // minimal time of the pointer to be pressed threshold: 9 // a minimal movement is ok, but keep it low }, getTouchAction: function() { return [TOUCH_ACTION_AUTO]; }, process: function(input) { var options = this.options; var validPointers = input.pointers.length === options.pointers; var validMovement = input.distance < options.threshold; var validTime = input.deltaTime > options.time; this._input = input; // we only allow little movement // and we've reached an end event, so a tap is possible if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { this.reset(); } else if (input.eventType & INPUT_START) { this.reset(); this._timer = setTimeoutContext(function() { this.state = STATE_RECOGNIZED; this.tryEmit(); }, options.time, this); } else if (input.eventType & INPUT_END) { return STATE_RECOGNIZED; } return STATE_FAILED; }, reset: function() { clearTimeout(this._timer); }, emit: function(input) { if (this.state !== STATE_RECOGNIZED) { return; } if (input && (input.eventType & INPUT_END)) { this.manager.emit(this.options.event + 'up', input); } else { this._input.timeStamp = now(); this.manager.emit(this.options.event, this._input); } } }); /** * Rotate * Recognized when two or more pointer are moving in a circular motion. * @constructor * @extends AttrRecognizer */ function RotateRecognizer() { AttrRecognizer.apply(this, arguments); } inherit(RotateRecognizer, AttrRecognizer, { /** * @namespace * @memberof RotateRecognizer */ defaults: { event: 'rotate', threshold: 0, pointers: 2 }, getTouchAction: function() { return [TOUCH_ACTION_NONE]; }, attrTest: function(input) { return this._super.attrTest.call(this, input) && (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); } }); /** * Swipe * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. * @constructor * @extends AttrRecognizer */ function SwipeRecognizer() { AttrRecognizer.apply(this, arguments); } inherit(SwipeRecognizer, AttrRecognizer, { /** * @namespace * @memberof SwipeRecognizer */ defaults: { event: 'swipe', threshold: 10, velocity: 0.3, direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, pointers: 1 }, getTouchAction: function() { return PanRecognizer.prototype.getTouchAction.call(this); }, attrTest: function(input) { var direction = this.options.direction; var velocity; if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { velocity = input.overallVelocity; } else if (direction & DIRECTION_HORIZONTAL) { velocity = input.overallVelocityX; } else if (direction & DIRECTION_VERTICAL) { velocity = input.overallVelocityY; } return this._super.attrTest.call(this, input) && direction & input.offsetDirection && input.distance > this.options.threshold && input.maxPointers == this.options.pointers && abs(velocity) > this.options.velocity && input.eventType & INPUT_END; }, emit: function(input) { var direction = directionStr(input.offsetDirection); if (direction) { this.manager.emit(this.options.event + direction, input); } this.manager.emit(this.options.event, input); } }); /** * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur * between the given interval and position. The delay option can be used to recognize multi-taps without firing * a single tap. * * The eventData from the emitted event contains the property `tapCount`, which contains the amount of * multi-taps being recognized. * @constructor * @extends Recognizer */ function TapRecognizer() { Recognizer.apply(this, arguments); // previous time and center, // used for tap counting this.pTime = false; this.pCenter = false; this._timer = null; this._input = null; this.count = 0; } inherit(TapRecognizer, Recognizer, { /** * @namespace * @memberof PinchRecognizer */ defaults: { event: 'tap', pointers: 1, taps: 1, interval: 300, // max time between the multi-tap taps time: 250, // max time of the pointer to be down (like finger on the screen) threshold: 9, // a minimal movement is ok, but keep it low posThreshold: 10 // a multi-tap can be a bit off the initial position }, getTouchAction: function() { return [TOUCH_ACTION_MANIPULATION]; }, process: function(input) { var options = this.options; var validPointers = input.pointers.length === options.pointers; var validMovement = input.distance < options.threshold; var validTouchTime = input.deltaTime < options.time; this.reset(); if ((input.eventType & INPUT_START) && (this.count === 0)) { return this.failTimeout(); } // we only allow little movement // and we've reached an end event, so a tap is possible if (validMovement && validTouchTime && validPointers) { if (input.eventType != INPUT_END) { return this.failTimeout(); } var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; this.pTime = input.timeStamp; this.pCenter = input.center; if (!validMultiTap || !validInterval) { this.count = 1; } else { this.count += 1; } this._input = input; // if tap count matches we have recognized it, // else it has began recognizing... var tapCount = this.count % options.taps; if (tapCount === 0) { // no failing requirements, immediately trigger the tap event // or wait as long as the multitap interval to trigger if (!this.hasRequireFailures()) { return STATE_RECOGNIZED; } else { this._timer = setTimeoutContext(function() { this.state = STATE_RECOGNIZED; this.tryEmit(); }, options.interval, this); return STATE_BEGAN; } } } return STATE_FAILED; }, failTimeout: function() { this._timer = setTimeoutContext(function() { this.state = STATE_FAILED; }, this.options.interval, this); return STATE_FAILED; }, reset: function() { clearTimeout(this._timer); }, emit: function() { if (this.state == STATE_RECOGNIZED) { this._input.tapCount = this.count; this.manager.emit(this.options.event, this._input); } } }); /** * Simple way to create a manager with a default set of recognizers. * @param {HTMLElement} element * @param {Object} [options] * @constructor */ function Hammer(element, options) { options = options || {}; options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); return new Manager(element, options); } /** * @const {string} */ Hammer.VERSION = '2.0.7'; /** * default settings * @namespace */ Hammer.defaults = { /** * set if DOM events are being triggered. * But this is slower and unused by simple implementations, so disabled by default. * @type {Boolean} * @default false */ domEvents: false, /** * The value for the touchAction property/fallback. * When set to `compute` it will magically set the correct value based on the added recognizers. * @type {String} * @default compute */ touchAction: TOUCH_ACTION_COMPUTE, /** * @type {Boolean} * @default true */ enable: true, /** * EXPERIMENTAL FEATURE -- can be removed/changed * Change the parent input target element. * If Null, then it is being set the to main element. * @type {Null|EventTarget} * @default null */ inputTarget: null, /** * force an input class * @type {Null|Function} * @default null */ inputClass: null, /** * Default recognizer setup when calling `Hammer()` * When creating a new Manager these will be skipped. * @type {Array} */ preset: [ // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] [RotateRecognizer, {enable: false}], [PinchRecognizer, {enable: false}, ['rotate']], [SwipeRecognizer, {direction: DIRECTION_HORIZONTAL}], [PanRecognizer, {direction: DIRECTION_HORIZONTAL}, ['swipe']], [TapRecognizer], [TapRecognizer, {event: 'doubletap', taps: 2}, ['tap']], [PressRecognizer] ], /** * Some CSS properties can be used to improve the working of Hammer. * Add them to this method and they will be set when creating a new Manager. * @namespace */ cssProps: { /** * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. * @type {String} * @default 'none' */ userSelect: 'none', /** * Disable the Windows Phone grippers when pressing an element. * @type {String} * @default 'none' */ touchSelect: 'none', /** * Disables the default callout shown when you touch and hold a touch target. * On iOS, when you touch and hold a touch target such as a link, Safari displays * a callout containing information about the link. This property allows you to disable that callout. * @type {String} * @default 'none' */ touchCallout: 'none', /** * Specifies whether zooming is enabled. Used by IE10> * @type {String} * @default 'none' */ contentZooming: 'none', /** * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. * @type {String} * @default 'none' */ userDrag: 'none', /** * Overrides the highlight color shown when the user taps a link or a JavaScript * clickable element in iOS. This property obeys the alpha value, if specified. * @type {String} * @default 'rgba(0,0,0,0)' */ tapHighlightColor: 'rgba(0,0,0,0)' } }; var STOP = 1; var FORCED_STOP = 2; /** * Manager * @param {HTMLElement} element * @param {Object} [options] * @constructor */ function Manager(element, options) { this.options = assign({}, Hammer.defaults, options || {}); this.options.inputTarget = this.options.inputTarget || element; this.handlers = {}; this.session = {}; this.recognizers = []; this.oldCssProps = {}; this.element = element; this.input = createInputInstance(this); this.touchAction = new TouchAction(this, this.options.touchAction); toggleCssProps(this, true); each(this.options.recognizers, function(item) { var recognizer = this.add(new (item[0])(item[1])); item[2] && recognizer.recognizeWith(item[2]); item[3] && recognizer.requireFailure(item[3]); }, this); } Manager.prototype = { /** * set options * @param {Object} options * @returns {Manager} */ set: function(options) { assign(this.options, options); // Options that need a little more setup if (options.touchAction) { this.touchAction.update(); } if (options.inputTarget) { // Clean up existing event listeners and reinitialize this.input.destroy(); this.input.target = options.inputTarget; this.input.init(); } return this; }, /** * stop recognizing for this session. * This session will be discarded, when a new [input]start event is fired. * When forced, the recognizer cycle is stopped immediately. * @param {Boolean} [force] */ stop: function(force) { this.session.stopped = force ? FORCED_STOP : STOP; }, /** * run the recognizers! * called by the inputHandler function on every movement of the pointers (touches) * it walks through all the recognizers and tries to detect the gesture that is being made * @param {Object} inputData */ recognize: function(inputData) { var session = this.session; if (session.stopped) { return; } // run the touch-action polyfill this.touchAction.preventDefaults(inputData); var recognizer; var recognizers = this.recognizers; // this holds the recognizer that is being recognized. // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED // if no recognizer is detecting a thing, it is set to `null` var curRecognizer = session.curRecognizer; // reset when the last recognizer is recognized // or when we're in a new session if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { curRecognizer = session.curRecognizer = null; } var i = 0; while (i < recognizers.length) { recognizer = recognizers[i]; // find out if we are allowed try to recognize the input for this one. // 1. allow if the session is NOT forced stopped (see the .stop() method) // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one // that is being recognized. // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. // this can be setup with the `recognizeWith()` method on the recognizer. if (session.stopped !== FORCED_STOP && ( // 1 !curRecognizer || recognizer == curRecognizer || // 2 recognizer.canRecognizeWith(curRecognizer))) { // 3 recognizer.recognize(inputData); } else { recognizer.reset(); } // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the // current active recognizer. but only if we don't already have an active recognizer if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { curRecognizer = session.curRecognizer = recognizer; } i++; } }, /** * get a recognizer by its event name. * @param {Recognizer|String} recognizer * @returns {Recognizer|Null} */ get: function(recognizer) { if (recognizer instanceof Recognizer) { return recognizer; } var recognizers = this.recognizers; for (var i = 0; i < recognizers.length; i++) { if (recognizers[i].options.event == recognizer) { return recognizers[i]; } } return null; }, /** * add a recognizer to the manager * existing recognizers with the same event name will be removed * @param {Recognizer} recognizer * @returns {Recognizer|Manager} */ add: function(recognizer) { if (invokeArrayArg(recognizer, 'add', this)) { return this; } // remove existing var existing = this.get(recognizer.options.event); if (existing) { this.remove(existing); } this.recognizers.push(recognizer); recognizer.manager = this; this.touchAction.update(); return recognizer; }, /** * remove a recognizer by name or instance * @param {Recognizer|String} recognizer * @returns {Manager} */ remove: function(recognizer) { if (invokeArrayArg(recognizer, 'remove', this)) { return this; } recognizer = this.get(recognizer); // let's make sure this recognizer exists if (recognizer) { var recognizers = this.recognizers; var index = inArray(recognizers, recognizer); if (index !== -1) { recognizers.splice(index, 1); this.touchAction.update(); } } return this; }, /** * bind event * @param {String} events * @param {Function} handler * @returns {EventEmitter} this */ on: function(events, handler) { if (events === undefined$1) { return; } if (handler === undefined$1) { return; } var handlers = this.handlers; each(splitStr(events), function(event) { handlers[event] = handlers[event] || []; handlers[event].push(handler); }); return this; }, /** * unbind event, leave emit blank to remove all handlers * @param {String} events * @param {Function} [handler] * @returns {EventEmitter} this */ off: function(events, handler) { if (events === undefined$1) { return; } var handlers = this.handlers; each(splitStr(events), function(event) { if (!handler) { delete handlers[event]; } else { handlers[event] && handlers[event].splice(inArray(handlers[event], handler), 1); } }); return this; }, /** * emit event to the listeners * @param {String} event * @param {Object} data */ emit: function(event, data) { // we also want to trigger dom events if (this.options.domEvents) { triggerDomEvent(event, data); } // no handlers, so skip it all var handlers = this.handlers[event] && this.handlers[event].slice(); if (!handlers || !handlers.length) { return; } data.type = event; data.preventDefault = function() { data.srcEvent.preventDefault(); }; var i = 0; while (i < handlers.length) { handlers[i](data); i++; } }, /** * destroy the manager and unbinds all events * it doesn't unbind dom events, that is the user own responsibility */ destroy: function() { this.element && toggleCssProps(this, false); this.handlers = {}; this.session = {}; this.input.destroy(); this.element = null; } }; /** * add/remove the css properties as defined in manager.options.cssProps * @param {Manager} manager * @param {Boolean} add */ function toggleCssProps(manager, add) { var element = manager.element; if (!element.style) { return; } var prop; each(manager.options.cssProps, function(value, name) { prop = prefixed(element.style, name); if (add) { manager.oldCssProps[prop] = element.style[prop]; element.style[prop] = value; } else { element.style[prop] = manager.oldCssProps[prop] || ''; } }); if (!add) { manager.oldCssProps = {}; } } /** * trigger dom event * @param {String} event * @param {Object} data */ function triggerDomEvent(event, data) { var gestureEvent = document.createEvent('Event'); gestureEvent.initEvent(event, true, true); gestureEvent.gesture = data; data.target.dispatchEvent(gestureEvent); } assign(Hammer, { INPUT_START: INPUT_START, INPUT_MOVE: INPUT_MOVE, INPUT_END: INPUT_END, INPUT_CANCEL: INPUT_CANCEL, STATE_POSSIBLE: STATE_POSSIBLE, STATE_BEGAN: STATE_BEGAN, STATE_CHANGED: STATE_CHANGED, STATE_ENDED: STATE_ENDED, STATE_RECOGNIZED: STATE_RECOGNIZED, STATE_CANCELLED: STATE_CANCELLED, STATE_FAILED: STATE_FAILED, DIRECTION_NONE: DIRECTION_NONE, DIRECTION_LEFT: DIRECTION_LEFT, DIRECTION_RIGHT: DIRECTION_RIGHT, DIRECTION_UP: DIRECTION_UP, DIRECTION_DOWN: DIRECTION_DOWN, DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, DIRECTION_VERTICAL: DIRECTION_VERTICAL, DIRECTION_ALL: DIRECTION_ALL, Manager: Manager, Input: Input, TouchAction: TouchAction, TouchInput: TouchInput, MouseInput: MouseInput, PointerEventInput: PointerEventInput, TouchMouseInput: TouchMouseInput, SingleTouchInput: SingleTouchInput, Recognizer: Recognizer, AttrRecognizer: AttrRecognizer, Tap: TapRecognizer, Pan: PanRecognizer, Swipe: SwipeRecognizer, Pinch: PinchRecognizer, Rotate: RotateRecognizer, Press: PressRecognizer, on: addEventListeners, off: removeEventListeners, each: each, merge: merge, extend: extend, assign: assign, inherit: inherit, bindFn: bindFn, prefixed: prefixed }); // this prevents errors when Hammer is loaded in the presence of an AMD // style loader but by script tag, not by the loader. var freeGlobal = (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {})); // jshint ignore:line freeGlobal.Hammer = Hammer; if (typeof undefined$1 === 'function' && undefined$1.amd) { undefined$1(function() { return Hammer; }); } else if (module.exports) { module.exports = Hammer; } else { window[exportName] = Hammer; } })(window, document, 'Hammer'); } (hammer)); var Hammer = hammer.exports; var MIN_ZOOM = 0.2, MAX_ZOOM = 4; var mouseEvents = [ 'mousedown', 'mouseup', 'mouseover', 'mouseout', 'click', 'dblclick' ]; function get(service, injector) { return injector.get(service, false); } function stopEvent(event) { event.preventDefault(); if (typeof event.stopPropagation === 'function') { event.stopPropagation(); } else if (event.srcEvent && typeof event.srcEvent.stopPropagation === 'function') { // iPhone & iPad event.srcEvent.stopPropagation(); } if (typeof event.stopImmediatePropagation === 'function') { event.stopImmediatePropagation(); } } function createTouchRecognizer(node) { function stopMouse(event) { forEach$1(mouseEvents, function(e) { componentEvent.bind(node, e, stopEvent, true); }); } function allowMouse(event) { setTimeout(function() { forEach$1(mouseEvents, function(e) { componentEvent.unbind(node, e, stopEvent, true); }); }, 500); } componentEvent.bind(node, 'touchstart', stopMouse, true); componentEvent.bind(node, 'touchend', allowMouse, true); componentEvent.bind(node, 'touchcancel', allowMouse, true); // A touch event recognizer that handles // touch events only (we know, we can already handle // mouse events out of the box) var recognizer = new Hammer.Manager(node, { inputClass: Hammer.TouchInput, recognizers: [], domEvents: true }); var tap = new Hammer.Tap(); var pan = new Hammer.Pan({ threshold: 10 }); var press = new Hammer.Press(); var pinch = new Hammer.Pinch(); var doubleTap = new Hammer.Tap({ event: 'doubletap', taps: 2 }); pinch.requireFailure(pan); pinch.requireFailure(press); recognizer.add([ pan, press, pinch, doubleTap, tap ]); recognizer.reset = function(force) { var recognizers = this.recognizers, session = this.session; if (session.stopped) { return; } recognizer.stop(force); setTimeout(function() { var i, r; for (i = 0; (r = recognizers[i]); i++) { r.reset(); r.state = 8; // FAILED STATE } session.curRecognizer = null; }, 0); }; recognizer.on('hammer.input', function(event) { if (event.srcEvent.defaultPrevented) { recognizer.reset(true); } }); return recognizer; } /** * A plugin that provides touch events for elements. * * @param {EventBus} eventBus * @param {InteractionEvents} interactionEvents */ function TouchInteractionEvents( injector, canvas, eventBus, elementRegistry, interactionEvents) { // optional integrations var dragging = get('dragging', injector), move = get('move', injector), contextPad = get('contextPad', injector), palette = get('palette', injector); // the touch recognizer var recognizer; function handler(type, buttonType) { return function(event) { var gfx = getGfx(event.target), element = gfx && elementRegistry.get(gfx); // translate into an actual mouse click event if (buttonType) { event.srcEvent.button = buttonType; } return interactionEvents.fire(type, event, element); }; } function getGfx(target) { var node = closest(target, 'svg, .djs-element', true); return node; } function initEvents(svg) { // touch recognizer recognizer = createTouchRecognizer(svg); function startGrabCanvas(event) { var lx = 0, ly = 0; function update(e) { var dx = e.deltaX - lx, dy = e.deltaY - ly; canvas.scroll({ dx: dx, dy: dy }); lx = e.deltaX; ly = e.deltaY; } function end(e) { recognizer.off('panmove', update); recognizer.off('panend', end); recognizer.off('pancancel', end); } recognizer.on('panmove', update); recognizer.on('panend', end); recognizer.on('pancancel', end); } function startGrab(event) { var gfx = getGfx(event.target), element = gfx && elementRegistry.get(gfx); // recognizer if (move && canvas.getRootElement() !== element) { return move.start(event, element, true); } else { startGrabCanvas(); } } function startZoom(e) { var zoom = canvas.zoom(), mid = e.center; function update(e) { var ratio = 1 - (1 - e.scale) / 1.50, newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, ratio * zoom)); canvas.zoom(newZoom, mid); stopEvent(e); } function end(e) { recognizer.off('pinchmove', update); recognizer.off('pinchend', end); recognizer.off('pinchcancel', end); recognizer.reset(true); } recognizer.on('pinchmove', update); recognizer.on('pinchend', end); recognizer.on('pinchcancel', end); } recognizer.on('tap', handler('element.click')); recognizer.on('doubletap', handler('element.dblclick', 1)); recognizer.on('panstart', startGrab); recognizer.on('press', startGrab); recognizer.on('pinchstart', startZoom); } if (dragging) { // simulate hover during dragging eventBus.on('drag.move', function(event) { var originalEvent = event.originalEvent; if (!originalEvent || originalEvent instanceof MouseEvent) { return; } var position = toPoint(originalEvent); // this gets really expensive ... var node = document.elementFromPoint(position.x, position.y), gfx = getGfx(node), element = gfx && elementRegistry.get(gfx); if (element !== event.hover) { if (event.hover) { dragging.out(event); } if (element) { dragging.hover({ element: element, gfx: gfx }); event.hover = element; event.hoverGfx = gfx; } } }); } if (contextPad) { eventBus.on('contextPad.create', function(event) { var node = event.pad.html; // touch recognizer var padRecognizer = createTouchRecognizer(node); padRecognizer.on('panstart', function(event) { contextPad.trigger('dragstart', event, true); }); padRecognizer.on('press', function(event) { contextPad.trigger('dragstart', event, true); }); padRecognizer.on('tap', function(event) { contextPad.trigger('click', event); }); }); } if (palette) { eventBus.on('palette.create', function(event) { var node = event.container; // touch recognizer var padRecognizer = createTouchRecognizer(node); padRecognizer.on('panstart', function(event) { palette.trigger('dragstart', event, true); }); padRecognizer.on('press', function(event) { palette.trigger('dragstart', event, true); }); padRecognizer.on('tap', function(event) { palette.trigger('click', event); }); }); } eventBus.on('canvas.init', function(event) { initEvents(event.svg); }); } TouchInteractionEvents.$inject = [ 'injector', 'canvas', 'eventBus', 'elementRegistry', 'interactionEvents', 'touchFix' ]; function TouchFix(canvas, eventBus) { var self = this; eventBus.on('canvas.init', function(e) { self.addBBoxMarker(e.svg); }); } TouchFix.$inject = [ 'canvas', 'eventBus' ]; /** * Safari mobile (iOS 7) does not fire touchstart event in element * if there is no shape between 0,0 and viewport elements origin. * * So touchstart event is only fired when the element was hit. * Putting an element over and below the 'viewport' fixes that behavior. */ TouchFix.prototype.addBBoxMarker = function(svg) { var markerStyle = { fill: 'none', class: 'outer-bound-marker' }; var rect1 = create$1('rect'); attr(rect1, { x: -10000, y: 10000, width: 10, height: 10 }); attr(rect1, markerStyle); append(svg, rect1); var rect2 = create$1('rect'); attr(rect2, { x: 10000, y: 10000, width: 10, height: 10 }); attr(rect2, markerStyle); append(svg, rect2); }; var TouchModule$1 = { __depends__: [ InteractionEventsModule$1 ], __init__: [ 'touchInteractionEvents' ], touchInteractionEvents: [ 'type', TouchInteractionEvents ], touchFix: [ 'type', TouchFix ] }; var TouchModule = { __depends__: [ TouchModule$1 ] }; function last(arr) { return arr && arr[arr.length - 1]; } function sortTopOrMiddle(element) { return element.y; } function sortLeftOrCenter(element) { return element.x; } /** * Sorting functions for different types of alignment * * @type {Object} * * @return {Function} */ var ALIGNMENT_SORTING = { left: sortLeftOrCenter, center: sortLeftOrCenter, right: function(element) { return element.x + element.width; }, top: sortTopOrMiddle, middle: sortTopOrMiddle, bottom: function(element) { return element.y + element.height; } }; function AlignElements$1(modeling, rules) { this._modeling = modeling; this._rules = rules; } AlignElements$1.$inject = [ 'modeling', 'rules' ]; /** * Get the relevant "axis" and "dimension" related to the current type of alignment * * @param {string} type left|right|center|top|bottom|middle * * @return {Object} { axis, dimension } */ AlignElements$1.prototype._getOrientationDetails = function(type) { var vertical = [ 'top', 'bottom', 'middle' ], axis = 'x', dimension = 'width'; if (vertical.indexOf(type) !== -1) { axis = 'y'; dimension = 'height'; } return { axis: axis, dimension: dimension }; }; AlignElements$1.prototype._isType = function(type, types) { return types.indexOf(type) !== -1; }; /** * Get a point on the relevant axis where elements should align to * * @param {string} type left|right|center|top|bottom|middle * @param {Array} sortedElements * * @return {Object} */ AlignElements$1.prototype._alignmentPosition = function(type, sortedElements) { var orientation = this._getOrientationDetails(type), axis = orientation.axis, dimension = orientation.dimension, alignment = {}, centers = {}, hasSharedCenters = false, centeredElements, firstElement, lastElement; function getMiddleOrTop(first, last) { return Math.round((first[axis] + last[axis] + last[dimension]) / 2); } if (this._isType(type, [ 'left', 'top' ])) { alignment[type] = sortedElements[0][axis]; } else if (this._isType(type, [ 'right', 'bottom' ])) { lastElement = last(sortedElements); alignment[type] = lastElement[axis] + lastElement[dimension]; } else if (this._isType(type, [ 'center', 'middle' ])) { // check if there is a center shared by more than one shape // if not, just take the middle of the range forEach$1(sortedElements, function(element) { var center = element[axis] + Math.round(element[dimension] / 2); if (centers[center]) { centers[center].elements.push(element); } else { centers[center] = { elements: [ element ], center: center }; } }); centeredElements = sortBy(centers, function(center) { if (center.elements.length > 1) { hasSharedCenters = true; } return center.elements.length; }); if (hasSharedCenters) { alignment[type] = last(centeredElements).center; return alignment; } firstElement = sortedElements[0]; sortedElements = sortBy(sortedElements, function(element) { return element[axis] + element[dimension]; }); lastElement = last(sortedElements); alignment[type] = getMiddleOrTop(firstElement, lastElement); } return alignment; }; /** * Executes the alignment of a selection of elements * * @param {Array} elements * @param {string} type left|right|center|top|bottom|middle */ AlignElements$1.prototype.trigger = function(elements, type) { var modeling = this._modeling, allowed; // filter out elements which cannot be aligned var filteredElements = filter(elements, function(element) { return !(element.waypoints || element.host || element.labelTarget); }); // filter out elements via rules allowed = this._rules.allowed('elements.align', { elements: filteredElements }); if (isArray$3(allowed)) { filteredElements = allowed; } if (filteredElements.length < 2 || !allowed) { return; } var sortFn = ALIGNMENT_SORTING[type]; var sortedElements = sortBy(filteredElements, sortFn); var alignment = this._alignmentPosition(type, sortedElements); modeling.alignElements(sortedElements, alignment); }; var AlignElementsModule$1 = { __init__: [ 'alignElements' ], alignElements: [ 'type', AlignElements$1 ] }; var entrySelector = '.entry'; var DEFAULT_PRIORITY$2 = 1000; var CONTEXT_PAD_PADDING = 12; /** * @typedef {djs.model.Base|djs.model.Base[]} ContextPadTarget */ /** * A context pad that displays element specific, contextual actions next * to a diagram element. * * @param {Canvas} canvas * @param {Object} config * @param {boolean|Object} [config.scale={ min: 1.0, max: 1.5 }] * @param {number} [config.scale.min] * @param {number} [config.scale.max] * @param {EventBus} eventBus * @param {Overlays} overlays */ function ContextPad(canvas, config, eventBus, overlays) { this._canvas = canvas; this._eventBus = eventBus; this._overlays = overlays; var scale = isDefined(config && config.scale) ? config.scale : { min: 1, max: 1.5 }; this._overlaysConfig = { scale: scale }; this._current = null; this._init(); } ContextPad.$inject = [ 'canvas', 'config.contextPad', 'eventBus', 'overlays' ]; /** * Registers events needed for interaction with other components. */ ContextPad.prototype._init = function() { var self = this; this._eventBus.on('selection.changed', function(event) { var selection = event.newSelection; var target = selection.length ? selection.length === 1 ? selection[0] : selection : null; if (target) { self.open(target, true); } else { self.close(); } }); this._eventBus.on('elements.changed', function(event) { var elements = event.elements, current = self._current; if (!current) { return; } var currentTarget = current.target; var currentChanged = some( isArray$3(currentTarget) ? currentTarget : [ currentTarget ], function(element) { return includes$8(elements, element); } ); // re-open if elements in current selection changed if (currentChanged) { self.open(currentTarget, true); } }); }; /** * Register context pad provider. * * @param {number} [priority=1000] * @param {ContextPadProvider} provider * * @example * const contextPadProvider = { * getContextPadEntries: function(element) { * return function(entries) { * return { * ...entries, * 'entry-1': { * label: 'My Entry', * action: function() { alert("I have been clicked!"); } * } * }; * } * }, * * getMultiElementContextPadEntries: function(elements) { * // ... * } * }; * * contextPad.registerProvider(800, contextPadProvider); */ ContextPad.prototype.registerProvider = function(priority, provider) { if (!provider) { provider = priority; priority = DEFAULT_PRIORITY$2; } this._eventBus.on('contextPad.getProviders', priority, function(event) { event.providers.push(provider); }); }; /** * Get context pad entries for given elements. * * @param {ContextPadTarget} target * * @return {ContextPadEntryDescriptor[]} list of entries */ ContextPad.prototype.getEntries = function(target) { var providers = this._getProviders(); var provideFn = isArray$3(target) ? 'getMultiElementContextPadEntries' : 'getContextPadEntries'; var entries = {}; // loop through all providers and their entries. // group entries by id so that overriding an entry is possible forEach$1(providers, function(provider) { if (!isFunction(provider[provideFn])) { return; } var entriesOrUpdater = provider[provideFn](target); if (isFunction(entriesOrUpdater)) { entries = entriesOrUpdater(entries); } else { forEach$1(entriesOrUpdater, function(entry, id) { entries[id] = entry; }); } }); return entries; }; /** * Trigger context pad action. * * @param {string} action * @param {Event} event * @param {boolean} [autoActivate=false] */ ContextPad.prototype.trigger = function(action, event, autoActivate) { var target = this._current.target, entries = this._current.entries, entry, handler, originalEvent, button = event.delegateTarget || event.target; if (!button) { return event.preventDefault(); } entry = entries[attr$1(button, 'data-action')]; handler = entry.action; originalEvent = event.originalEvent || event; // simple action (via callback function) if (isFunction(handler)) { if (action === 'click') { return handler(originalEvent, target, autoActivate); } } else { if (handler[action]) { return handler[action](originalEvent, target, autoActivate); } } // silence other actions event.preventDefault(); }; /** * Open the context pad for given elements. * * @param {ContextPadTarget} target * @param {boolean} [force=false] - Force re-opening context pad. */ ContextPad.prototype.open = function(target, force) { if (!force && this.isOpen(target)) { return; } this.close(); this._updateAndOpen(target); }; ContextPad.prototype._getProviders = function() { var event = this._eventBus.createEvent({ type: 'contextPad.getProviders', providers: [] }); this._eventBus.fire(event); return event.providers; }; /** * @param {ContextPadTarget} target */ ContextPad.prototype._updateAndOpen = function(target) { var entries = this.getEntries(target), pad = this.getPad(target), html = pad.html, image; forEach$1(entries, function(entry, id) { var grouping = entry.group || 'default', control = domify(entry.html || '
    '), container; attr$1(control, 'data-action', id); container = query('[data-group=' + cssEscape(grouping) + ']', html); if (!container) { container = domify('
    '); attr$1(container, 'data-group', grouping); html.appendChild(container); } container.appendChild(control); if (entry.className) { addClasses$1(control, entry.className); } if (entry.title) { attr$1(control, 'title', entry.title); } if (entry.imageUrl) { image = domify(''); attr$1(image, 'src', entry.imageUrl); image.style.width = '100%'; image.style.height = '100%'; control.appendChild(image); } }); classes$1(html).add('open'); this._current = { target: target, entries: entries, pad: pad }; this._eventBus.fire('contextPad.open', { current: this._current }); }; /** * @param {ContextPadTarget} target * * @return {Overlay} */ ContextPad.prototype.getPad = function(target) { if (this.isOpen()) { return this._current.pad; } var self = this; var overlays = this._overlays; var html = domify('
    '); var position = this._getPosition(target); var overlaysConfig = assign({ html: html }, this._overlaysConfig, position); delegate.bind(html, entrySelector, 'click', function(event) { self.trigger('click', event); }); delegate.bind(html, entrySelector, 'dragstart', function(event) { self.trigger('dragstart', event); }); // stop propagation of mouse events componentEvent.bind(html, 'mousedown', function(event) { event.stopPropagation(); }); var activeRootElement = this._canvas.getRootElement(); this._overlayId = overlays.add(activeRootElement, 'context-pad', overlaysConfig); var pad = overlays.get(this._overlayId); this._eventBus.fire('contextPad.create', { target: target, pad: pad }); return pad; }; /** * Close the context pad */ ContextPad.prototype.close = function() { if (!this.isOpen()) { return; } this._overlays.remove(this._overlayId); this._overlayId = null; this._eventBus.fire('contextPad.close', { current: this._current }); this._current = null; }; /** * Check if pad is open. * * If target is provided, check if it is opened * for the given target (single or multiple elements). * * @param {ContextPadTarget} [target] * @return {boolean} */ ContextPad.prototype.isOpen = function(target) { var current = this._current; if (!current) { return false; } // basic no-args is open check if (!target) { return true; } var currentTarget = current.target; // strict handling of single vs. multi-selection if (isArray$3(target) !== isArray$3(currentTarget)) { return false; } if (isArray$3(target)) { return ( target.length === currentTarget.length && every(target, function(element) { return includes$8(currentTarget, element); }) ); } else { return currentTarget === target; } }; /** * Get contex pad position. * * @param {ContextPadTarget} target * @return {Bounds} */ ContextPad.prototype._getPosition = function(target) { var elements = isArray$3(target) ? target : [ target ]; var bBox = getBBox(elements); return { position: { left: bBox.x + bBox.width + CONTEXT_PAD_PADDING, top: bBox.y - CONTEXT_PAD_PADDING / 2 } }; }; // helpers ////////// function addClasses$1(element, classNames) { var classes = classes$1(element); classNames = isArray$3(classNames) ? classNames : classNames.split(/\s+/g); classNames.forEach(function(cls) { classes.add(cls); }); } /** * @param {any[]} array * @param {any} item * * @return {boolean} */ function includes$8(array, item) { return array.indexOf(item) !== -1; } var ContextPadModule$1 = { __depends__: [ InteractionEventsModule$1, OverlaysModule ], contextPad: [ 'type', ContextPad ] }; var DATA_REF = 'data-id'; var CLOSE_EVENTS = [ 'contextPad.close', 'canvas.viewbox.changing', 'commandStack.changed' ]; var DEFAULT_PRIORITY$1 = 1000; /** * A popup menu that can be used to display a list of actions anywhere in the canvas. * * @param {Object} config * @param {boolean|Object} [config.scale={ min: 1.0, max: 1.5 }] * @param {number} [config.scale.min] * @param {number} [config.scale.max] * @param {EventBus} eventBus * @param {Canvas} canvas * * @class * @constructor */ function PopupMenu(config, eventBus, canvas) { var scale = isDefined(config && config.scale) ? config.scale : { min: 1, max: 1.5 }; this._config = { scale: scale }; this._eventBus = eventBus; this._canvas = canvas; this._providers = {}; this._current = {}; } PopupMenu.$inject = [ 'config.popupMenu', 'eventBus', 'canvas' ]; /** * Registers a popup menu provider * * @param {string} id * @param {number} [priority=1000] * @param {Object} provider * * @example * const popupMenuProvider = { * getPopupMenuEntries: function(element) { * return { * 'entry-1': { * label: 'My Entry', * action: function() { alert("I have been clicked!"); } * } * } * } * }; * * popupMenu.registerProvider('myMenuID', popupMenuProvider); */ PopupMenu.prototype.registerProvider = function(id, priority, provider) { if (!provider) { provider = priority; priority = DEFAULT_PRIORITY$1; } this._eventBus.on('popupMenu.getProviders.' + id, priority, function(event) { event.providers.push(provider); }); }; /** * Determine if the popup menu has entries. * * @return {boolean} true if empty */ PopupMenu.prototype.isEmpty = function(element, providerId) { if (!element) { throw new Error('element parameter is missing'); } if (!providerId) { throw new Error('providerId parameter is missing'); } var providers = this._getProviders(providerId); if (!providers) { return true; } var entries = this._getEntries(element, providers), headerEntries = this._getHeaderEntries(element, providers); var hasEntries = size(entries) > 0, hasHeaderEntries = headerEntries && size(headerEntries) > 0; return !hasEntries && !hasHeaderEntries; }; /** * Create entries and open popup menu at given position * * @param {Object} element * @param {string} id provider id * @param {Object} position * * @return {Object} popup menu instance */ PopupMenu.prototype.open = function(element, id, position) { var providers = this._getProviders(id); if (!element) { throw new Error('Element is missing'); } if (!providers || !providers.length) { throw new Error('No registered providers for: ' + id); } if (!position) { throw new Error('the position argument is missing'); } if (this.isOpen()) { this.close(); } this._emit('open'); var current = this._current = { className: id, element: element, position: position }; var entries = this._getEntries(element, providers), headerEntries = this._getHeaderEntries(element, providers); current.entries = assign({}, entries, headerEntries); current.container = this._createContainer(id); if (size(headerEntries)) { current.container.appendChild( this._createEntries(headerEntries, 'djs-popup-header') ); } if (size(entries)) { current.container.appendChild( this._createEntries(entries, 'djs-popup-body') ); } var canvas = this._canvas, parent = canvas.getContainer(); this._attachContainer(current.container, parent, position.cursor); this._bindAutoClose(); }; /** * Removes the popup menu and unbinds the event handlers. */ PopupMenu.prototype.close = function() { if (!this.isOpen()) { return; } this._emit('close'); this._unbindAutoClose(); remove$2(this._current.container); this._current.container = null; }; /** * Determine if an open popup menu exist. * * @return {boolean} true if open */ PopupMenu.prototype.isOpen = function() { return !!this._current.container; }; /** * Trigger an action associated with an entry. * * @param {Object} event * * @return the result of the action callback, if any */ PopupMenu.prototype.trigger = function(event) { // silence other actions event.preventDefault(); var element = event.delegateTarget || event.target, entryId = attr$1(element, DATA_REF); var entry = this._getEntry(entryId); if (entry.action) { return entry.action.call(null, event, entry); } }; PopupMenu.prototype._getProviders = function(id) { var event = this._eventBus.createEvent({ type: 'popupMenu.getProviders.' + id, providers: [] }); this._eventBus.fire(event); return event.providers; }; PopupMenu.prototype._getEntries = function(element, providers) { var entries = {}; forEach$1(providers, function(provider) { // handle legacy method if (!provider.getPopupMenuEntries) { forEach$1(provider.getEntries(element), function(entry) { var id = entry.id; if (!id) { throw new Error('every entry must have the id property set'); } entries[id] = omit(entry, [ 'id' ]); }); return; } var entriesOrUpdater = provider.getPopupMenuEntries(element); if (isFunction(entriesOrUpdater)) { entries = entriesOrUpdater(entries); } else { forEach$1(entriesOrUpdater, function(entry, id) { entries[id] = entry; }); } }); return entries; }; PopupMenu.prototype._getHeaderEntries = function(element, providers) { var entries = {}; forEach$1(providers, function(provider) { // handle legacy method if (!provider.getPopupMenuHeaderEntries) { if (!provider.getHeaderEntries) { return; } forEach$1(provider.getHeaderEntries(element), function(entry) { var id = entry.id; if (!id) { throw new Error('every entry must have the id property set'); } entries[id] = omit(entry, [ 'id' ]); }); return; } var entriesOrUpdater = provider.getPopupMenuHeaderEntries(element); if (isFunction(entriesOrUpdater)) { entries = entriesOrUpdater(entries); } else { forEach$1(entriesOrUpdater, function(entry, id) { entries[id] = entry; }); } }); return entries; }; /** * Gets an entry instance (either entry or headerEntry) by id. * * @param {string} entryId * * @return {Object} entry instance */ PopupMenu.prototype._getEntry = function(entryId) { var entry = this._current.entries[entryId]; if (!entry) { throw new Error('entry not found'); } return entry; }; PopupMenu.prototype._emit = function(eventName) { this._eventBus.fire('popupMenu.' + eventName); }; /** * Creates the popup menu container. * * @return {Object} a DOM container */ PopupMenu.prototype._createContainer = function(id) { var container = domify('
    '), position = this._current.position, className = this._current.className; assign$1(container, { position: 'absolute', left: position.x + 'px', top: position.y + 'px', visibility: 'hidden' }); classes$1(container).add(className); attr$1(container, 'data-popup', id); return container; }; /** * Attaches the container to the DOM. * * @param {Object} container * @param {Object} parent */ PopupMenu.prototype._attachContainer = function(container, parent, cursor) { var self = this; // Event handler delegate.bind(container, '.entry' ,'click', function(event) { self.trigger(event); }); this._updateScale(container); // Attach to DOM parent.appendChild(container); if (cursor) { this._assureIsInbounds(container, cursor); } // display after position adjustment to avoid flickering assign$1(container, { visibility: 'visible' }); }; /** * Updates popup style.transform with respect to the config and zoom level. * * @method _updateScale * * @param {Object} container */ PopupMenu.prototype._updateScale = function(container) { var zoom = this._canvas.zoom(); var scaleConfig = this._config.scale, minScale, maxScale, scale = zoom; if (scaleConfig !== true) { if (scaleConfig === false) { minScale = 1; maxScale = 1; } else { minScale = scaleConfig.min; maxScale = scaleConfig.max; } if (isDefined(minScale) && zoom < minScale) { scale = minScale; } if (isDefined(maxScale) && zoom > maxScale) { scale = maxScale; } } setTransform(container, 'scale(' + scale + ')'); }; /** * Make sure that the menu is always fully shown * * @method function * * @param {Object} container * @param {Position} cursor {x, y} */ PopupMenu.prototype._assureIsInbounds = function(container, cursor) { var canvas = this._canvas, clientRect = canvas._container.getBoundingClientRect(); var containerX = container.offsetLeft, containerY = container.offsetTop, containerWidth = container.scrollWidth, containerHeight = container.scrollHeight, overAxis = {}, left, top; var cursorPosition = { x: cursor.x - clientRect.left, y: cursor.y - clientRect.top }; if (containerX + containerWidth > clientRect.width) { overAxis.x = true; } if (containerY + containerHeight > clientRect.height) { overAxis.y = true; } if (overAxis.x && overAxis.y) { left = cursorPosition.x - containerWidth + 'px'; top = cursorPosition.y - containerHeight + 'px'; } else if (overAxis.x) { left = cursorPosition.x - containerWidth + 'px'; top = cursorPosition.y + 'px'; } else if (overAxis.y && cursorPosition.y < containerHeight) { left = cursorPosition.x + 'px'; top = 10 + 'px'; } else if (overAxis.y) { left = cursorPosition.x + 'px'; top = cursorPosition.y - containerHeight + 'px'; } assign$1(container, { left: left, top: top }, { 'zIndex': 1000 }); }; /** * Creates a list of entries and returns them as a DOM container. * * @param {Array} entries an array of entry objects * @param {string} className the class name of the entry container * * @return {Object} a DOM container */ PopupMenu.prototype._createEntries = function(entries, className) { var entriesContainer = domify('
    '), self = this; classes$1(entriesContainer).add(className); forEach$1(entries, function(entry, id) { var entryContainer = self._createEntry(entry, id), grouping = entry.group || 'default', groupContainer = query('[data-group=' + cssEscape(grouping) + ']', entriesContainer); if (!groupContainer) { groupContainer = domify('
    '); attr$1(groupContainer, 'data-group', grouping); entriesContainer.appendChild(groupContainer); } groupContainer.appendChild(entryContainer); }); return entriesContainer; }; /** * Creates a single entry and returns it as a DOM container. * * @param {Object} entry * * @return {Object} a DOM container */ PopupMenu.prototype._createEntry = function(entry, id) { var entryContainer = domify('
    '), entryClasses = classes$1(entryContainer); entryClasses.add('entry'); if (entry.className) { entry.className.split(' ').forEach(function(className) { entryClasses.add(className); }); } attr$1(entryContainer, DATA_REF, id); if (entry.label) { var label = domify(''); label.textContent = entry.label; entryContainer.appendChild(label); } if (entry.imageUrl) { var image = domify(''); attr$1(image, 'src', entry.imageUrl); entryContainer.appendChild(image); } if (entry.active === true) { entryClasses.add('active'); } if (entry.disabled === true) { entryClasses.add('disabled'); } if (entry.title) { entryContainer.title = entry.title; } return entryContainer; }; /** * Set up listener to close popup automatically on certain events. */ PopupMenu.prototype._bindAutoClose = function() { this._eventBus.once(CLOSE_EVENTS, this.close, this); }; /** * Remove the auto-closing listener. */ PopupMenu.prototype._unbindAutoClose = function() { this._eventBus.off(CLOSE_EVENTS, this.close, this); }; // helpers ///////////////////////////// function setTransform(element, transform) { element.style['transform-origin'] = 'top left'; [ '', '-ms-', '-webkit-' ].forEach(function(prefix) { element.style[prefix + 'transform'] = transform; }); } var PopupMenuModule$1 = { __init__: [ 'popupMenu' ], popupMenu: [ 'type', PopupMenu ] }; /** * To change the icons, modify the SVGs in `./resources`, execute `npx svgo -f resources --datauri enc -o dist`, * and then replace respective icons with the optimized data URIs in `./dist`. */ var icons$1 = { align: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%202000%202000%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M200%20150v1700%22%2F%3E%3Crect%20x%3D%22500%22%20y%3D%22150%22%20width%3D%221300%22%20height%3D%22700%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%22500%22%20y%3D%221150%22%20width%3D%22700%22%20height%3D%22700%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E', bottom: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M150%201650h1500%22%2F%3E%3Crect%20x%3D%22150%22%20y%3D%22350%22%20width%3D%22600%22%20height%3D%221300%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%221050%22%20y%3D%22850%22%20width%3D%22600%22%20height%3D%22800%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E', center: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M900%20150v1500%22%2F%3E%3Crect%20x%3D%22250%22%20y%3D%22150%22%20width%3D%221300%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%22500%22%20y%3D%221050%22%20width%3D%22800%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E', left: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M100%20150v1500%22%2F%3E%3Crect%20x%3D%22100%22%20y%3D%22150%22%20width%3D%221300%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%22100%22%20y%3D%221050%22%20width%3D%22800%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E', right: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M1650%20150v1500%22%2F%3E%3Crect%20x%3D%22350%22%20y%3D%22150%22%20width%3D%221300%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%22850%22%20y%3D%221050%22%20width%3D%22800%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E', top: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M150%20150h1500%22%2F%3E%3Crect%20x%3D%22150%22%20y%3D%22150%22%20width%3D%22600%22%20height%3D%221300%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%221050%22%20y%3D%22150%22%20width%3D%22600%22%20height%3D%22800%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E', middle: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22stroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linecap%3Around%22%20d%3D%22M150%20900h1500%22%2F%3E%3Crect%20x%3D%22150%22%20y%3D%22250%22%20width%3D%22600%22%20height%3D%221300%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%221050%22%20y%3D%22500%22%20width%3D%22600%22%20height%3D%22800%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E' }; var LOW_PRIORITY$l = 900; /** * A provider for align elements context pad button */ function AlignElementsContextPadProvider(contextPad, popupMenu, translate, canvas) { contextPad.registerProvider(LOW_PRIORITY$l, this); this._contextPad = contextPad; this._popupMenu = popupMenu; this._translate = translate; this._canvas = canvas; } AlignElementsContextPadProvider.$inject = [ 'contextPad', 'popupMenu', 'translate', 'canvas' ]; AlignElementsContextPadProvider.prototype.getMultiElementContextPadEntries = function(elements) { var actions = {}; if (this._isAllowed(elements)) { assign(actions, this._getEntries(elements)); } return actions; }; AlignElementsContextPadProvider.prototype._isAllowed = function(elements) { return !this._popupMenu.isEmpty(elements, 'align-elements'); }; AlignElementsContextPadProvider.prototype._getEntries = function(elements) { var self = this; return { 'align-elements': { group: 'align-elements', title: self._translate('Align elements'), imageUrl: icons$1['align'], action: { click: function(event, elements) { var position = self._getMenuPosition(elements); assign(position, { cursor: { x: event.x, y: event.y } }); self._popupMenu.open(elements, 'align-elements', position); } } } }; }; AlignElementsContextPadProvider.prototype._getMenuPosition = function(elements) { var Y_OFFSET = 5; var diagramContainer = this._canvas.getContainer(), pad = this._contextPad.getPad(elements).html; var diagramRect = diagramContainer.getBoundingClientRect(), padRect = pad.getBoundingClientRect(); var top = padRect.top - diagramRect.top; var left = padRect.left - diagramRect.left; var pos = { x: left, y: top + padRect.height + Y_OFFSET }; return pos; }; var ALIGNMENT_OPTIONS = [ 'left', 'center', 'right', 'top', 'middle', 'bottom' ]; /** * A provider for align elements popup menu. */ function AlignElementsMenuProvider(popupMenu, alignElements, translate, rules) { this._alignElements = alignElements; this._translate = translate; this._popupMenu = popupMenu; this._rules = rules; popupMenu.registerProvider('align-elements', this); } AlignElementsMenuProvider.$inject = [ 'popupMenu', 'alignElements', 'translate', 'rules' ]; AlignElementsMenuProvider.prototype.getPopupMenuEntries = function(elements) { var entries = {}; if (this._isAllowed(elements)) { assign(entries, this._getEntries(elements)); } return entries; }; AlignElementsMenuProvider.prototype._isAllowed = function(elements) { return this._rules.allowed('elements.align', { elements: elements }); }; AlignElementsMenuProvider.prototype._getEntries = function(elements) { var alignElements = this._alignElements, translate = this._translate, popupMenu = this._popupMenu; var entries = {}; forEach$1(ALIGNMENT_OPTIONS, function(alignment) { entries[ 'align-elements-' + alignment ] = { group: 'align', title: translate('Align elements ' + alignment), className: 'bjs-align-elements-menu-entry', imageUrl: icons$1[alignment], action: function(event, entry) { alignElements.trigger(elements, alignment); popupMenu.close(); } }; }); return entries; }; /** * A basic provider that may be extended to implement modeling rules. * * Extensions should implement the init method to actually add their custom * modeling checks. Checks may be added via the #addRule(action, fn) method. * * @param {EventBus} eventBus */ function RuleProvider(eventBus) { CommandInterceptor.call(this, eventBus); this.init(); } RuleProvider.$inject = [ 'eventBus' ]; e(RuleProvider, CommandInterceptor); /** * Adds a modeling rule for the given action, implemented through * a callback function. * * The function will receive the modeling specific action context * to perform its check. It must return `false` to disallow the * action from happening or `true` to allow the action. * * A rule provider may pass over the evaluation to lower priority * rules by returning return nothing (or undefined). * * @example * * ResizableRules.prototype.init = function() { * * \/** * * Return `true`, `false` or nothing to denote * * _allowed_, _not allowed_ and _continue evaluating_. * *\/ * this.addRule('shape.resize', function(context) { * * var shape = context.shape; * * if (!context.newBounds) { * // check general resizability * if (!shape.resizable) { * return false; * } * * // not returning anything (read: undefined) * // will continue the evaluation of other rules * // (with lower priority) * return; * } else { * // element must have minimum size of 10*10 points * return context.newBounds.width > 10 && context.newBounds.height > 10; * } * }); * }; * * @param {string|Array} actions the identifier for the modeling action to check * @param {number} [priority] the priority at which this rule is being applied * @param {Function} fn the callback function that performs the actual check */ RuleProvider.prototype.addRule = function(actions, priority, fn) { var self = this; if (typeof actions === 'string') { actions = [ actions ]; } actions.forEach(function(action) { self.canExecute(action, priority, function(context, action, event) { return fn(context); }, true); }); }; /** * Implement this method to add new rules during provider initialization. */ RuleProvider.prototype.init = function() {}; /** * Rule provider for alignment of BPMN elements. */ function BpmnAlignElements(eventBus) { RuleProvider.call(this, eventBus); } BpmnAlignElements.$inject = [ 'eventBus' ]; e(BpmnAlignElements, RuleProvider); BpmnAlignElements.prototype.init = function() { this.addRule('elements.align', function(context) { var elements = context.elements; // filter out elements which cannot be aligned var filteredElements = filter(elements, function(element) { return !(element.waypoints || element.host || element.labelTarget); }); // filter out elements which are children of any of the selected elements filteredElements = getParents$1(filteredElements); if (filteredElements.length < 2) { return false; } return filteredElements; }); }; var AlignElementsModule = { __depends__: [ AlignElementsModule$1, ContextPadModule$1, PopupMenuModule$1 ], __init__: [ 'alignElementsContextPadProvider', 'alignElementsMenuProvider', 'bpmnAlignElements' ], alignElementsContextPadProvider: [ 'type', AlignElementsContextPadProvider ], alignElementsMenuProvider: [ 'type', AlignElementsMenuProvider ], bpmnAlignElements: [ 'type', BpmnAlignElements ] }; // padding to detect element placement var PLACEMENT_DETECTION_PAD = 10; var DEFAULT_DISTANCE = 50; var DEFAULT_MAX_DISTANCE = 250; /** * Get free position starting from given position. * * @param {djs.model.Shape} source * @param {djs.model.Shape} element * @param {Point} position * @param {Function} getNextPosition * * @return {Point} */ function findFreePosition(source, element, position, getNextPosition) { var connectedAtPosition; while ((connectedAtPosition = getConnectedAtPosition(source, position, element))) { position = getNextPosition(element, position, connectedAtPosition); } return position; } /** * Returns function that returns next position. * * @param {Object} nextPositionDirection * @param {Object} [nextPositionDirection.x] * @param {Object} [nextPositionDirection.y] * * @returns {Function} */ function generateGetNextPosition(nextPositionDirection) { return function(element, previousPosition, connectedAtPosition) { var nextPosition = { x: previousPosition.x, y: previousPosition.y }; [ 'x', 'y' ].forEach(function(axis) { var nextPositionDirectionForAxis = nextPositionDirection[ axis ]; if (!nextPositionDirectionForAxis) { return; } var dimension = axis === 'x' ? 'width' : 'height'; var margin = nextPositionDirectionForAxis.margin, minDistance = nextPositionDirectionForAxis.minDistance; if (margin < 0) { nextPosition[ axis ] = Math.min( connectedAtPosition[ axis ] + margin - element[ dimension ] / 2, previousPosition[ axis ] - minDistance + margin ); } else { nextPosition[ axis ] = Math.max( connectedAtPosition[ axis ] + connectedAtPosition[ dimension ] + margin + element[ dimension ] / 2, previousPosition[ axis ] + minDistance + margin ); } }); return nextPosition; }; } /** * Return target at given position, if defined. * * This takes connected elements from host and attachers * into account, too. */ function getConnectedAtPosition(source, position, element) { var bounds = { x: position.x - (element.width / 2), y: position.y - (element.height / 2), width: element.width, height: element.height }; var closure = getAutoPlaceClosure(source); return find(closure, function(target) { if (target === element) { return false; } var orientation = getOrientation(target, bounds, PLACEMENT_DETECTION_PAD); return orientation === 'intersect'; }); } /** * Compute optimal distance between source and target based on existing connections to and from source. * Assumes left-to-right and top-to-down modeling. * * @param {djs.model.Shape} source * @param {Object} [hints] * @param {number} [hints.defaultDistance] * @param {string} [hints.direction] * @param {Function} [hints.filter] * @param {Function} [hints.getWeight] * @param {number} [hints.maxDistance] * @param {string} [hints.reference] * * @return {number} */ function getConnectedDistance(source, hints) { if (!hints) { hints = {}; } // targets > sources by default function getDefaultWeight(connection) { return connection.source === source ? 1 : -1; } var defaultDistance = hints.defaultDistance || DEFAULT_DISTANCE, direction = hints.direction || 'e', filter = hints.filter, getWeight = hints.getWeight || getDefaultWeight, maxDistance = hints.maxDistance || DEFAULT_MAX_DISTANCE, reference = hints.reference || 'start'; if (!filter) { filter = noneFilter; } function getDistance(a, b) { if (direction === 'n') { if (reference === 'start') { return asTRBL(a).top - asTRBL(b).bottom; } else if (reference === 'center') { return asTRBL(a).top - getMid(b).y; } else { return asTRBL(a).top - asTRBL(b).top; } } else if (direction === 'w') { if (reference === 'start') { return asTRBL(a).left - asTRBL(b).right; } else if (reference === 'center') { return asTRBL(a).left - getMid(b).x; } else { return asTRBL(a).left - asTRBL(b).left; } } else if (direction === 's') { if (reference === 'start') { return asTRBL(b).top - asTRBL(a).bottom; } else if (reference === 'center') { return getMid(b).y - asTRBL(a).bottom; } else { return asTRBL(b).bottom - asTRBL(a).bottom; } } else { if (reference === 'start') { return asTRBL(b).left - asTRBL(a).right; } else if (reference === 'center') { return getMid(b).x - asTRBL(a).right; } else { return asTRBL(b).right - asTRBL(a).right; } } } var sourcesDistances = source.incoming .filter(filter) .map(function(connection) { var weight = getWeight(connection); var distance = weight < 0 ? getDistance(connection.source, source) : getDistance(source, connection.source); return { id: connection.source.id, distance: distance, weight: weight }; }); var targetsDistances = source.outgoing .filter(filter) .map(function(connection) { var weight = getWeight(connection); var distance = weight > 0 ? getDistance(source, connection.target) : getDistance(connection.target, source); return { id: connection.target.id, distance: distance, weight: weight }; }); var distances = sourcesDistances.concat(targetsDistances).reduce(function(accumulator, currentValue) { accumulator[ currentValue.id + '__weight_' + currentValue.weight ] = currentValue; return accumulator; }, {}); var distancesGrouped = reduce(distances, function(accumulator, currentValue) { var distance = currentValue.distance, weight = currentValue.weight; if (distance < 0 || distance > maxDistance) { return accumulator; } if (!accumulator[ String(distance) ]) { accumulator[ String(distance) ] = 0; } accumulator[ String(distance) ] += 1 * weight; if (!accumulator.distance || accumulator[ accumulator.distance ] < accumulator[ String(distance) ]) { accumulator.distance = distance; } return accumulator; }, {}); return distancesGrouped.distance || defaultDistance; } /** * Returns all connected elements around the given source. * * This includes: * * - connected elements * - host connected elements * - attachers connected elements * * @param {djs.model.Shape} source * * @return {Array} */ function getAutoPlaceClosure(source) { var allConnected = getConnected(source); if (source.host) { allConnected = allConnected.concat(getConnected(source.host)); } if (source.attachers) { allConnected = allConnected.concat(source.attachers.reduce(function(shapes, attacher) { return shapes.concat(getConnected(attacher)); }, [])); } return allConnected; } function getConnected(element) { return getTargets(element).concat(getSources(element)); } function getSources(shape) { return shape.incoming.map(function(connection) { return connection.source; }); } function getTargets(shape) { return shape.outgoing.map(function(connection) { return connection.target; }); } function noneFilter() { return true; } var LOW_PRIORITY$k = 100; /** * A service that places elements connected to existing ones * to an appropriate position in an _automated_ fashion. * * @param {EventBus} eventBus * @param {Modeling} modeling */ function AutoPlace$1(eventBus, modeling, canvas) { eventBus.on('autoPlace', LOW_PRIORITY$k, function(context) { var shape = context.shape, source = context.source; return getNewShapePosition$1(source, shape); }); eventBus.on('autoPlace.end', function(event) { canvas.scrollToElement(event.shape); }); /** * Append shape to source at appropriate position. * * @param {djs.model.Shape} source * @param {djs.model.Shape} shape * * @return {djs.model.Shape} appended shape */ this.append = function(source, shape, hints) { eventBus.fire('autoPlace.start', { source: source, shape: shape }); // allow others to provide the position var position = eventBus.fire('autoPlace', { source: source, shape: shape }); var newShape = modeling.appendShape(source, shape, position, source.parent, hints); eventBus.fire('autoPlace.end', { source: source, shape: newShape }); return newShape; }; } AutoPlace$1.$inject = [ 'eventBus', 'modeling', 'canvas' ]; // helpers ////////// /** * Find the new position for the target element to * connect to source. * * @param {djs.model.Shape} source * @param {djs.model.Shape} element * @param {Object} [hints] * @param {Object} [hints.defaultDistance] * * @returns {Point} */ function getNewShapePosition$1(source, element, hints) { if (!hints) { hints = {}; } var distance = hints.defaultDistance || DEFAULT_DISTANCE; var sourceMid = getMid(source), sourceTrbl = asTRBL(source); // simply put element right next to source return { x: sourceTrbl.right + distance + element.width / 2, y: sourceMid.y }; } /** * Select element after auto placement. * * @param {EventBus} eventBus * @param {Selection} selection */ function AutoPlaceSelectionBehavior(eventBus, selection) { eventBus.on('autoPlace.end', 500, function(e) { selection.select(e.shape); }); } AutoPlaceSelectionBehavior.$inject = [ 'eventBus', 'selection' ]; var AutoPlaceModule$1 = { __init__: [ 'autoPlaceSelectionBehavior' ], autoPlace: [ 'type', AutoPlace$1 ], autoPlaceSelectionBehavior: [ 'type', AutoPlaceSelectionBehavior ] }; /** * Return the parent of the element with any of the given types. * * @param {djs.model.Base} element * @param {string|Array} anyType * * @return {djs.model.Base} */ function getParent(element, anyType) { if (typeof anyType === 'string') { anyType = [ anyType ]; } while ((element = element.parent)) { if (isAny(element, anyType)) { return element; } } return null; } /** * Find the new position for the target element to * connect to source. * * @param {djs.model.Shape} source * @param {djs.model.Shape} element * * @return {Point} */ function getNewShapePosition(source, element) { if (is$1(element, 'bpmn:TextAnnotation')) { return getTextAnnotationPosition(source, element); } if (isAny(element, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ])) { return getDataElementPosition(source, element); } if (is$1(element, 'bpmn:FlowNode')) { return getFlowNodePosition(source, element); } } /** * Always try to place element right of source; * compute actual distance from previous nodes in flow. */ function getFlowNodePosition(source, element) { var sourceTrbl = asTRBL(source); var sourceMid = getMid(source); var horizontalDistance = getConnectedDistance(source, { filter: function(connection) { return is$1(connection, 'bpmn:SequenceFlow'); } }); var margin = 30, minDistance = 80, orientation = 'left'; if (is$1(source, 'bpmn:BoundaryEvent')) { orientation = getOrientation(source, source.host, -25); if (orientation.indexOf('top') !== -1) { margin *= -1; } } var position = { x: sourceTrbl.right + horizontalDistance + element.width / 2, y: sourceMid.y + getVerticalDistance(orientation, minDistance) }; var nextPositionDirection = { y: { margin: margin, minDistance: minDistance } }; return findFreePosition(source, element, position, generateGetNextPosition(nextPositionDirection)); } function getVerticalDistance(orientation, minDistance) { if (orientation.indexOf('top') != -1) { return -1 * minDistance; } else if (orientation.indexOf('bottom') != -1) { return minDistance; } else { return 0; } } /** * Always try to place text annotations top right of source. */ function getTextAnnotationPosition(source, element) { var sourceTrbl = asTRBL(source); var position = { x: sourceTrbl.right + element.width / 2, y: sourceTrbl.top - 50 - element.height / 2 }; if (isConnection$e(source)) { position = getMid(source); position.x += 100; position.y -= 50; } var nextPositionDirection = { y: { margin: -30, minDistance: 20 } }; return findFreePosition(source, element, position, generateGetNextPosition(nextPositionDirection)); } /** * Always put element bottom right of source. */ function getDataElementPosition(source, element) { var sourceTrbl = asTRBL(source); var position = { x: sourceTrbl.right - 10 + element.width / 2, y: sourceTrbl.bottom + 40 + element.width / 2 }; var nextPositionDirection = { x: { margin: 30, minDistance: 30 } }; return findFreePosition(source, element, position, generateGetNextPosition(nextPositionDirection)); } function isConnection$e(element) { return !!element.waypoints; } /** * BPMN auto-place behavior. * * @param {EventBus} eventBus */ function AutoPlace(eventBus) { eventBus.on('autoPlace', function(context) { var shape = context.shape, source = context.source; return getNewShapePosition(source, shape); }); } AutoPlace.$inject = [ 'eventBus' ]; var AutoPlaceModule = { __depends__: [ AutoPlaceModule$1 ], __init__: [ 'bpmnAutoPlace' ], bpmnAutoPlace: [ 'type', AutoPlace ] }; /** * An auto resize component that takes care of expanding a parent element * if child elements are created or moved close the parents edge. * * @param {EventBus} eventBus * @param {ElementRegistry} elementRegistry * @param {Modeling} modeling * @param {Rules} rules */ function AutoResize(eventBus, elementRegistry, modeling, rules) { CommandInterceptor.call(this, eventBus); this._elementRegistry = elementRegistry; this._modeling = modeling; this._rules = rules; var self = this; this.postExecuted([ 'shape.create' ], function(event) { var context = event.context, hints = context.hints || {}, shape = context.shape, parent = context.parent || context.newParent; if (hints.autoResize === false) { return; } self._expand([ shape ], parent); }); this.postExecuted([ 'elements.move' ], function(event) { var context = event.context, elements = flatten(values(context.closure.topLevel)), hints = context.hints; var autoResize = hints ? hints.autoResize : true; if (autoResize === false) { return; } var expandings = groupBy(elements, function(element) { return element.parent.id; }); forEach$1(expandings, function(elements, parentId) { // optionally filter elements to be considered when resizing if (isArray$3(autoResize)) { elements = elements.filter(function(element) { return find(autoResize, matchPattern({ id: element.id })); }); } self._expand(elements, parentId); }); }); this.postExecuted([ 'shape.toggleCollapse' ], function(event) { var context = event.context, hints = context.hints, shape = context.shape; if (hints && hints.autoResize === false) { return; } if (shape.collapsed) { return; } self._expand(shape.children || [], shape); }); this.postExecuted([ 'shape.resize' ], function(event) { var context = event.context, hints = context.hints, shape = context.shape, parent = shape.parent; if (hints && hints.autoResize === false) { return; } if (parent) { self._expand([ shape ], parent); } }); } AutoResize.$inject = [ 'eventBus', 'elementRegistry', 'modeling', 'rules' ]; e(AutoResize, CommandInterceptor); /** * Calculate the new bounds of the target shape, given * a number of elements have been moved or added into the parent. * * This method considers the current size, the added elements as well as * the provided padding for the new bounds. * * @param {Array} elements * @param {djs.model.Shape} target */ AutoResize.prototype._getOptimalBounds = function(elements, target) { var offset = this.getOffset(target), padding = this.getPadding(target); var elementsTrbl = asTRBL(getBBox(elements)), targetTrbl = asTRBL(target); var newTrbl = {}; if (elementsTrbl.top - targetTrbl.top < padding.top) { newTrbl.top = elementsTrbl.top - offset.top; } if (elementsTrbl.left - targetTrbl.left < padding.left) { newTrbl.left = elementsTrbl.left - offset.left; } if (targetTrbl.right - elementsTrbl.right < padding.right) { newTrbl.right = elementsTrbl.right + offset.right; } if (targetTrbl.bottom - elementsTrbl.bottom < padding.bottom) { newTrbl.bottom = elementsTrbl.bottom + offset.bottom; } return asBounds(assign({}, targetTrbl, newTrbl)); }; /** * Expand the target shape respecting rules, offset and padding * * @param {Array} elements * @param {djs.model.Shape|string} target|targetId */ AutoResize.prototype._expand = function(elements, target) { if (typeof target === 'string') { target = this._elementRegistry.get(target); } var allowed = this._rules.allowed('element.autoResize', { elements: elements, target: target }); if (!allowed) { return; } // calculate the new bounds var newBounds = this._getOptimalBounds(elements, target); if (!boundsChanged$1(newBounds, target)) { return; } var resizeDirections = getResizeDirections(pick(target, [ 'x', 'y', 'width', 'height' ]), newBounds); // resize the parent shape this.resize(target, newBounds, { autoResize: resizeDirections }); var parent = target.parent; // recursively expand parent elements if (parent) { this._expand([ target ], parent); } }; /** * Get the amount to expand the given shape in each direction. * * @param {djs.model.Shape} shape * * @return {TRBL} */ AutoResize.prototype.getOffset = function(shape) { return { top: 60, bottom: 60, left: 100, right: 100 }; }; /** * Get the activation threshold for each side for which * resize triggers. * * @param {djs.model.Shape} shape * * @return {TRBL} */ AutoResize.prototype.getPadding = function(shape) { return { top: 2, bottom: 2, left: 15, right: 15 }; }; /** * Perform the actual resize operation. * * @param {djs.model.Shape} shape * @param {Bounds} newBounds * @param {Object} [hints] * @param {string} [hints.autoResize] */ AutoResize.prototype.resize = function(shape, newBounds, hints) { this._modeling.resizeShape(shape, newBounds, null, hints); }; function boundsChanged$1(newBounds, oldBounds) { return ( newBounds.x !== oldBounds.x || newBounds.y !== oldBounds.y || newBounds.width !== oldBounds.width || newBounds.height !== oldBounds.height ); } /** * Get directions of resize as {n|w|s|e} e.g. "nw". * * @param {Bounds} oldBounds * @param {Bounds} newBounds * * @returns {string} Resize directions as {n|w|s|e}. */ function getResizeDirections(oldBounds, newBounds) { var directions = ''; oldBounds = asTRBL(oldBounds); newBounds = asTRBL(newBounds); if (oldBounds.top > newBounds.top) { directions = directions.concat('n'); } if (oldBounds.right < newBounds.right) { directions = directions.concat('w'); } if (oldBounds.bottom < newBounds.bottom) { directions = directions.concat('s'); } if (oldBounds.left > newBounds.left) { directions = directions.concat('e'); } return directions; } /** * Sub class of the AutoResize module which implements a BPMN * specific resize function. */ function BpmnAutoResize(injector) { injector.invoke(AutoResize, this); } BpmnAutoResize.$inject = [ 'injector' ]; e(BpmnAutoResize, AutoResize); /** * Resize shapes and lanes. * * @param {djs.model.Shape} target * @param {Bounds} newBounds * @param {Object} hints */ BpmnAutoResize.prototype.resize = function(target, newBounds, hints) { if (is$1(target, 'bpmn:Participant')) { this._modeling.resizeLane(target, newBounds, null, hints); } else { this._modeling.resizeShape(target, newBounds, null, hints); } }; /** * This is a base rule provider for the element.autoResize rule. */ function AutoResizeProvider(eventBus) { RuleProvider.call(this, eventBus); var self = this; this.addRule('element.autoResize', function(context) { return self.canResize(context.elements, context.target); }); } AutoResizeProvider.$inject = [ 'eventBus' ]; e(AutoResizeProvider, RuleProvider); /** * Needs to be implemented by sub classes to allow actual auto resize * * @param {Array} elements * @param {djs.model.Shape} target * * @return {boolean} */ AutoResizeProvider.prototype.canResize = function(elements, target) { return false; }; /** * This module is a provider for automatically resizing parent BPMN elements */ function BpmnAutoResizeProvider(eventBus, modeling) { AutoResizeProvider.call(this, eventBus); this._modeling = modeling; } e(BpmnAutoResizeProvider, AutoResizeProvider); BpmnAutoResizeProvider.$inject = [ 'eventBus', 'modeling' ]; /** * Check if the given target can be expanded * * @param {djs.model.Shape} target * * @return {boolean} */ BpmnAutoResizeProvider.prototype.canResize = function(elements, target) { // do not resize plane elements: // root elements, collapsed sub-processes if (is$1(target.di, 'bpmndi:BPMNPlane')) { return false; } if (!is$1(target, 'bpmn:Participant') && !is$1(target, 'bpmn:Lane') && !(is$1(target, 'bpmn:SubProcess'))) { return false; } var canResize = true; forEach$1(elements, function(element) { if (is$1(element, 'bpmn:Lane') || element.labelTarget) { canResize = false; return; } }); return canResize; }; var AutoResizeModule = { __init__: [ 'bpmnAutoResize', 'bpmnAutoResizeProvider' ], bpmnAutoResize: [ 'type', BpmnAutoResize ], bpmnAutoResizeProvider: [ 'type', BpmnAutoResizeProvider ] }; var HIGH_PRIORITY$j = 1500; /** * Browsers may swallow certain events (hover, out ...) if users are to * fast with the mouse. * * @see http://stackoverflow.com/questions/7448468/why-cant-i-reliably-capture-a-mouseout-event * * The fix implemented in this component ensure that we * * 1) have a hover state after a successful drag.move event * 2) have an out event when dragging leaves an element * * @param {ElementRegistry} elementRegistry * @param {EventBus} eventBus * @param {Injector} injector */ function HoverFix(elementRegistry, eventBus, injector) { var self = this; var dragging = injector.get('dragging', false); /** * Make sure we are god damn hovering! * * @param {Event} dragging event */ function ensureHover(event) { if (event.hover) { return; } var originalEvent = event.originalEvent; var gfx = self._findTargetGfx(originalEvent); var element = gfx && elementRegistry.get(gfx); if (gfx && element) { // 1) cancel current mousemove event.stopPropagation(); // 2) emit fake hover for new target dragging.hover({ element: element, gfx: gfx }); // 3) re-trigger move event dragging.move(originalEvent); } } if (dragging) { /** * We wait for a specific sequence of events before * emitting a fake drag.hover event. * * Event Sequence: * * drag.start * drag.move >> ensure we are hovering */ eventBus.on('drag.start', function(event) { eventBus.once('drag.move', HIGH_PRIORITY$j, function(event) { ensureHover(event); }); }); } /** * We make sure that element.out is always fired, even if the * browser swallows an element.out event. * * Event sequence: * * element.hover * (element.out >> sometimes swallowed) * element.hover >> ensure we fired element.out */ (function() { var hoverGfx; var hover; eventBus.on('element.hover', function(event) { // (1) remember current hover element hoverGfx = event.gfx; hover = event.element; }); eventBus.on('element.hover', HIGH_PRIORITY$j, function(event) { // (3) am I on an element still? if (hover) { // (4) that is a problem, gotta "simulate the out" eventBus.fire('element.out', { element: hover, gfx: hoverGfx }); } }); eventBus.on('element.out', function() { // (2) unset hover state if we correctly outed us *GG* hoverGfx = null; hover = null; }); })(); this._findTargetGfx = function(event) { var position, target; if (!(event instanceof MouseEvent)) { return; } position = toPoint(event); // damn expensive operation, ouch! target = document.elementFromPoint(position.x, position.y); return getGfx(target); }; } HoverFix.$inject = [ 'elementRegistry', 'eventBus', 'injector' ]; // helpers ///////////////////// function getGfx(target) { return closest(target, 'svg, .djs-element', true); } var HoverFixModule = { __init__: [ 'hoverFix' ], hoverFix: [ 'type', HoverFix ], }; var round$a = Math.round; var DRAG_ACTIVE_CLS = 'djs-drag-active'; function preventDefault$1(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 if elements still exist * * 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" * } * } * }); * }); * } */ function Dragging(eventBus, canvas, selection, elementRegistry) { 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); } function restoreSelection(previousSelection) { var existingSelection = previousSelection.filter(function(element) { return elementRegistry.get(element.id); }); existingSelection.length && selection.select(existingSelection); } // event listeners function move(event, activate) { var payload = context.payload, displacement = context.displacement; var globalStart = context.globalStart, globalCurrent = toPoint(event), globalDelta = delta(globalCurrent, globalStart); var localStart = context.localStart, localCurrent = toLocalPoint(globalCurrent), localDelta = delta(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$a(localStart.x + displacement.x), y: round$a(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) { set(context.cursor); } // indicate dragging via marker on root element canvas.addMarker(canvas.getRootElement(), DRAG_ACTIVE_CLS); } stopPropagation$1(event); if (context.active) { // update payload with actual coordinates assign(payload, { x: round$a(localCurrent.x + displacement.x), y: round$a(localCurrent.y + displacement.y), dx: round$a(localDelta.x), dy: round$a(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$1(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$1(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 = install(eventBus); // remove trap after minimal delay setTimeout(untrap, 400); // prevent default action (click) preventDefault$1(event); } end(event); } function trapTouch(event) { move(event); } // update the drag events hover (djs.model.Base) and hoverGfx (Snap) // 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 unset(); if (context.trapClick) { endDrag = trapClickAndEnd; } else { endDrag = end; } // reset dom listeners componentEvent.unbind(document, 'mousemove', move); componentEvent.unbind(document, 'dragstart', preventDefault$1); componentEvent.unbind(document, 'selectstart', preventDefault$1); componentEvent.unbind(document, 'mousedown', endDrag, true); componentEvent.unbind(document, 'mouseup', endDrag, true); componentEvent.unbind(document, 'keyup', checkCancel); componentEvent.unbind(document, 'touchstart', trapTouch, true); componentEvent.unbind(document, 'touchcancel', cancel, true); componentEvent.unbind(document, 'touchmove', move, true); componentEvent.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) { restoreSelection(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$1(event) || event; globalStart = toPoint(event); stopPropagation$1(event); // prevent default browser dragging behavior if (originalEvent.type === 'dragstart') { preventDefault$1(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: delta(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) { componentEvent.bind(document, 'touchstart', trapTouch, true); componentEvent.bind(document, 'touchcancel', cancel, true); componentEvent.bind(document, 'touchmove', move, true); componentEvent.bind(document, 'touchend', end, true); } else { // assume we use the mouse to interact per default componentEvent.bind(document, 'mousemove', move); // prevent default browser drag and text selection behavior componentEvent.bind(document, 'dragstart', preventDefault$1); componentEvent.bind(document, 'selectstart', preventDefault$1); componentEvent.bind(document, 'mousedown', endDrag, true); componentEvent.bind(document, 'mouseup', endDrag, true); } componentEvent.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', 'elementRegistry' ]; var DraggingModule = { __depends__: [ HoverFixModule, SelectionModule, ], dragging: [ 'type', Dragging ], }; /** * Initiates canvas scrolling if current cursor point is close to a border. * Cancelled when current point moves back inside the scrolling borders * or cancelled manually. * * Default options : * scrollThresholdIn: [ 20, 20, 20, 20 ], * scrollThresholdOut: [ 0, 0, 0, 0 ], * scrollRepeatTimeout: 15, * scrollStep: 10 * * Threshold order: * [ left, top, right, bottom ] */ function AutoScroll(config, eventBus, canvas) { this._canvas = canvas; this._opts = assign({ scrollThresholdIn: [ 20, 20, 20, 20 ], scrollThresholdOut: [ 0, 0, 0, 0 ], scrollRepeatTimeout: 15, scrollStep: 10 }, config); var self = this; eventBus.on('drag.move', function(e) { var point = self._toBorderPoint(e); self.startScroll(point); }); eventBus.on([ 'drag.cleanup' ], function() { self.stopScroll(); }); } AutoScroll.$inject = [ 'config.autoScroll', 'eventBus', 'canvas' ]; /** * Starts scrolling loop. * Point is given in global scale in canvas container box plane. * * @param {Object} point { x: X, y: Y } */ AutoScroll.prototype.startScroll = function(point) { var canvas = this._canvas; var opts = this._opts; var self = this; var clientRect = canvas.getContainer().getBoundingClientRect(); var diff = [ point.x, point.y, clientRect.width - point.x, clientRect.height - point.y ]; this.stopScroll(); var dx = 0, dy = 0; for (var i = 0; i < 4; i++) { if (between(diff[i], opts.scrollThresholdOut[i], opts.scrollThresholdIn[i])) { if (i === 0) { dx = opts.scrollStep; } else if (i == 1) { dy = opts.scrollStep; } else if (i == 2) { dx = -opts.scrollStep; } else if (i == 3) { dy = -opts.scrollStep; } } } if (dx !== 0 || dy !== 0) { canvas.scroll({ dx: dx, dy: dy }); this._scrolling = setTimeout(function() { self.startScroll(point); }, opts.scrollRepeatTimeout); } }; function between(val, start, end) { if (start < val && val < end) { return true; } return false; } /** * Stops scrolling loop. */ AutoScroll.prototype.stopScroll = function() { clearTimeout(this._scrolling); }; /** * Overrides defaults options. * * @param {Object} options */ AutoScroll.prototype.setOptions = function(options) { this._opts = assign({}, this._opts, options); }; /** * Converts event to a point in canvas container plane in global scale. * * @param {Event} event * @return {Point} */ AutoScroll.prototype._toBorderPoint = function(event) { var clientRect = this._canvas._container.getBoundingClientRect(); var globalPosition = toPoint(event.originalEvent); return { x: globalPosition.x - clientRect.left, y: globalPosition.y - clientRect.top }; }; var AutoScrollModule = { __depends__: [ DraggingModule, ], __init__: [ 'autoScroll' ], autoScroll: [ 'type', AutoScroll ] }; /** * A service that provides rules for certain diagram actions. * * The default implementation will hook into the {@link CommandStack} * to perform the actual rule evaluation. Make sure to provide the * `commandStack` service with this module if you plan to use it. * * Together with this implementation you may use the {@link RuleProvider} * to implement your own rule checkers. * * This module is ment to be easily replaced, thus the tiny foot print. * * @param {Injector} injector */ function Rules(injector) { this._commandStack = injector.get('commandStack', false); } Rules.$inject = [ 'injector' ]; /** * Returns whether or not a given modeling action can be executed * in the specified context. * * This implementation will respond with allow unless anyone * objects. * * @param {string} action the action to be checked * @param {Object} [context] the context to check the action in * * @return {boolean} returns true, false or null depending on whether the * operation is allowed, not allowed or should be ignored. */ Rules.prototype.allowed = function(action, context) { var allowed = true; var commandStack = this._commandStack; if (commandStack) { allowed = commandStack.canExecute(action, context); } // map undefined to true, i.e. no rules return allowed === undefined ? true : allowed; }; var RulesModule$1 = { __init__: [ 'rules' ], rules: [ 'type', Rules ] }; var round$9 = Math.round, max$6 = Math.max; function circlePath(center, r) { var x = center.x, y = center.y; return [ [ 'M', x, y ], [ 'm', 0, -r ], [ 'a', r, r, 0, 1, 1, 0, 2 * r ], [ 'a', r, r, 0, 1, 1, 0, -2 * r ], [ 'z' ] ]; } function linePath(points) { var segments = []; points.forEach(function(p, idx) { segments.push([ idx === 0 ? 'M' : 'L', p.x, p.y ]); }); return segments; } var INTERSECTION_THRESHOLD$1 = 10; function getBendpointIntersection(waypoints, reference) { var i, w; for (i = 0; (w = waypoints[i]); i++) { if (pointDistance(w, reference) <= INTERSECTION_THRESHOLD$1) { return { point: waypoints[i], bendpoint: true, index: i }; } } return null; } function getPathIntersection(waypoints, reference) { var intersections = intersect(circlePath(reference, INTERSECTION_THRESHOLD$1), linePath(waypoints)); var a = intersections[0], b = intersections[intersections.length - 1], idx; if (!a) { // no intersection return null; } if (a !== b) { if (a.segment2 !== b.segment2) { // we use the bendpoint in between both segments // as the intersection point idx = max$6(a.segment2, b.segment2) - 1; return { point: waypoints[idx], bendpoint: true, index: idx }; } return { point: { x: (round$9(a.x + b.x) / 2), y: (round$9(a.y + b.y) / 2) }, index: a.segment2 }; } return { point: { x: round$9(a.x), y: round$9(a.y) }, index: a.segment2 }; } /** * Returns the closest point on the connection towards a given reference point. * * @param {Array} waypoints * @param {Point} reference * * @return {Object} intersection data (segment, point) */ function getApproxIntersection(waypoints, reference) { return getBendpointIntersection(waypoints, reference) || getPathIntersection(waypoints, reference); } /** * Returns the length of a vector * * @param {Vector} * @return {Float} */ function vectorLength(v) { return Math.sqrt(Math.pow(v.x, 2) + Math.pow(v.y, 2)); } /** * Calculates the angle between a line a the yAxis * * @param {Array} * @return {Float} */ function getAngle(line) { // return value is between 0, 180 and -180, -0 // @janstuemmel: maybe replace return a/b with b/a return Math.atan((line[1].y - line[0].y) / (line[1].x - line[0].x)); } /** * Rotates a vector by a given angle * * @param {Vector} * @param {Float} Angle in radians * @return {Vector} */ function rotateVector(vector, angle) { return (!angle) ? vector : { x: Math.cos(angle) * vector.x - Math.sin(angle) * vector.y, y: Math.sin(angle) * vector.x + Math.cos(angle) * vector.y }; } /** * Solves a 2D equation system * a + r*b = c, where a,b,c are 2D vectors * * @param {Vector} * @param {Vector} * @param {Vector} * @return {Float} */ function solveLambaSystem(a, b, c) { // the 2d system var system = [ { n: a[0] - c[0], lambda: b[0] }, { n: a[1] - c[1], lambda: b[1] } ]; // solve var n = system[0].n * b[0] + system[1].n * b[1], l = system[0].lambda * b[0] + system[1].lambda * b[1]; return -n / l; } /** * Position of perpendicular foot * * @param {Point} * @param [ {Point}, {Point} ] line defined through two points * @return {Point} the perpendicular foot position */ function perpendicularFoot(point, line) { var a = line[0], b = line[1]; // relative position of b from a var bd = { x: b.x - a.x, y: b.y - a.y }; // solve equation system to the parametrized vectors param real value var r = solveLambaSystem([ a.x, a.y ], [ bd.x, bd.y ], [ point.x, point.y ]); return { x: a.x + r * bd.x, y: a.y + r * bd.y }; } /** * Calculates the distance between a point and a line * * @param {Point} * @param [ {Point}, {Point} ] line defined through two points * @return {Float} distance */ function getDistancePointLine(point, line) { var pfPoint = perpendicularFoot(point, line); // distance vector var connectionVector = { x: pfPoint.x - point.x, y: pfPoint.y - point.y }; return vectorLength(connectionVector); } /** * Calculates the distance between two points * * @param {Point} * @param {Point} * @return {Float} distance */ function getDistancePointPoint(point1, point2) { return vectorLength({ x: point1.x - point2.x, y: point1.y - point2.y }); } var BENDPOINT_CLS = 'djs-bendpoint'; var SEGMENT_DRAGGER_CLS = 'djs-segment-dragger'; function toCanvasCoordinates(canvas, event) { var position = toPoint(event), clientRect = canvas._container.getBoundingClientRect(), offset; // canvas relative position offset = { x: clientRect.left, y: clientRect.top }; // update actual event payload with canvas relative measures var viewbox = canvas.viewbox(); return { x: viewbox.x + (position.x - offset.x) / viewbox.scale, y: viewbox.y + (position.y - offset.y) / viewbox.scale }; } function getConnectionIntersection(canvas, waypoints, event) { var localPosition = toCanvasCoordinates(canvas, event), intersection = getApproxIntersection(waypoints, localPosition); return intersection; } function addBendpoint(parentGfx, cls) { var groupGfx = create$1('g'); classes(groupGfx).add(BENDPOINT_CLS); append(parentGfx, groupGfx); var visual = create$1('circle'); attr(visual, { cx: 0, cy: 0, r: 4 }); classes(visual).add('djs-visual'); append(groupGfx, visual); var hit = create$1('circle'); attr(hit, { cx: 0, cy: 0, r: 10 }); classes(hit).add('djs-hit'); append(groupGfx, hit); if (cls) { classes(groupGfx).add(cls); } return groupGfx; } function createParallelDragger(parentGfx, segmentStart, segmentEnd, alignment) { var draggerGfx = create$1('g'); append(parentGfx, draggerGfx); var width = 18, height = 6, padding = 11, hitWidth = calculateHitWidth(segmentStart, segmentEnd, alignment), hitHeight = height + padding; var visual = create$1('rect'); attr(visual, { x: -width / 2, y: -height / 2, width: width, height: height }); classes(visual).add('djs-visual'); append(draggerGfx, visual); var hit = create$1('rect'); attr(hit, { x: -hitWidth / 2, y: -hitHeight / 2, width: hitWidth, height: hitHeight }); classes(hit).add('djs-hit'); append(draggerGfx, hit); rotate(draggerGfx, alignment === 'v' ? 90 : 0); return draggerGfx; } function addSegmentDragger(parentGfx, segmentStart, segmentEnd) { var groupGfx = create$1('g'), mid = getMidPoint(segmentStart, segmentEnd), alignment = pointsAligned(segmentStart, segmentEnd); append(parentGfx, groupGfx); createParallelDragger(groupGfx, segmentStart, segmentEnd, alignment); classes(groupGfx).add(SEGMENT_DRAGGER_CLS); classes(groupGfx).add(alignment === 'h' ? 'horizontal' : 'vertical'); translate$2(groupGfx, mid.x, mid.y); return groupGfx; } /** * Calculates region for segment move which is 2/3 of the full segment length * @param {number} segmentLength * * @return {number} */ function calculateSegmentMoveRegion(segmentLength) { return Math.abs(Math.round(segmentLength * 2 / 3)); } /** * Returns the point with the closest distance that is on the connection path. * * @param {Point} position * @param {djs.Base.Connection} connection * @returns {Point} */ function getClosestPointOnConnection(position, connection) { var segment = getClosestSegment(position, connection); return perpendicularFoot(position, segment); } // helper ////////// function calculateHitWidth(segmentStart, segmentEnd, alignment) { var segmentLengthXAxis = segmentEnd.x - segmentStart.x, segmentLengthYAxis = segmentEnd.y - segmentStart.y; return alignment === 'h' ? calculateSegmentMoveRegion(segmentLengthXAxis) : calculateSegmentMoveRegion(segmentLengthYAxis); } function getClosestSegment(position, connection) { var waypoints = connection.waypoints; var minDistance = Infinity, segmentIndex; for (var i = 0; i < waypoints.length - 1; i++) { var start = waypoints[i], end = waypoints[i + 1], distance = getDistancePointLine(position, [ start, end ]); if (distance < minDistance) { minDistance = distance; segmentIndex = i; } } return [ waypoints[segmentIndex], waypoints[segmentIndex + 1] ]; } /** * A service that adds editable bendpoints to connections. */ function Bendpoints( eventBus, canvas, interactionEvents, bendpointMove, connectionSegmentMove) { /** * Returns true if intersection point is inside middle region of segment, adjusted by * optional threshold */ function isIntersectionMiddle(intersection, waypoints, treshold) { var idx = intersection.index, p = intersection.point, p0, p1, mid, aligned, xDelta, yDelta; if (idx <= 0 || intersection.bendpoint) { return false; } p0 = waypoints[idx - 1]; p1 = waypoints[idx]; mid = getMidPoint(p0, p1), aligned = pointsAligned(p0, p1); xDelta = Math.abs(p.x - mid.x); yDelta = Math.abs(p.y - mid.y); return aligned && xDelta <= treshold && yDelta <= treshold; } /** * Calculates the threshold from a connection's middle which fits the two-third-region */ function calculateIntersectionThreshold(connection, intersection) { var waypoints = connection.waypoints, relevantSegment, alignment, segmentLength, threshold; if (intersection.index <= 0 || intersection.bendpoint) { return null; } // segment relative to connection intersection relevantSegment = { start: waypoints[intersection.index - 1], end: waypoints[intersection.index] }; alignment = pointsAligned(relevantSegment.start, relevantSegment.end); if (!alignment) { return null; } if (alignment === 'h') { segmentLength = relevantSegment.end.x - relevantSegment.start.x; } else { segmentLength = relevantSegment.end.y - relevantSegment.start.y; } // calculate threshold relative to 2/3 of segment length threshold = calculateSegmentMoveRegion(segmentLength) / 2; return threshold; } function activateBendpointMove(event, connection) { var waypoints = connection.waypoints, intersection = getConnectionIntersection(canvas, waypoints, event), threshold; if (!intersection) { return; } threshold = calculateIntersectionThreshold(connection, intersection); if (isIntersectionMiddle(intersection, waypoints, threshold)) { connectionSegmentMove.start(event, connection, intersection.index); } else { bendpointMove.start(event, connection, intersection.index, !intersection.bendpoint); } // we've handled the event return true; } function bindInteractionEvents(node, eventName, element) { componentEvent.bind(node, eventName, function(event) { interactionEvents.triggerMouseEvent(eventName, event, element); event.stopPropagation(); }); } function getBendpointsContainer(element, create) { var layer = canvas.getLayer('overlays'), gfx = query('.djs-bendpoints[data-element-id="' + cssEscape(element.id) + '"]', layer); if (!gfx && create) { gfx = create$1('g'); attr(gfx, { 'data-element-id': element.id }); classes(gfx).add('djs-bendpoints'); append(layer, gfx); bindInteractionEvents(gfx, 'mousedown', element); bindInteractionEvents(gfx, 'click', element); bindInteractionEvents(gfx, 'dblclick', element); } return gfx; } function getSegmentDragger(idx, parentGfx) { return query( '.djs-segment-dragger[data-segment-idx="' + idx + '"]', parentGfx ); } function createBendpoints(gfx, connection) { connection.waypoints.forEach(function(p, idx) { var bendpoint = addBendpoint(gfx); append(gfx, bendpoint); translate$2(bendpoint, p.x, p.y); }); // add floating bendpoint addBendpoint(gfx, 'floating'); } function createSegmentDraggers(gfx, connection) { var waypoints = connection.waypoints; var segmentStart, segmentEnd, segmentDraggerGfx; for (var i = 1; i < waypoints.length; i++) { segmentStart = waypoints[i - 1]; segmentEnd = waypoints[i]; if (pointsAligned(segmentStart, segmentEnd)) { segmentDraggerGfx = addSegmentDragger(gfx, segmentStart, segmentEnd); attr(segmentDraggerGfx, { 'data-segment-idx': i }); bindInteractionEvents(segmentDraggerGfx, 'mousemove', connection); } } } function clearBendpoints(gfx) { forEach$1(all('.' + BENDPOINT_CLS, gfx), function(node) { remove$1(node); }); } function clearSegmentDraggers(gfx) { forEach$1(all('.' + SEGMENT_DRAGGER_CLS, gfx), function(node) { remove$1(node); }); } function addHandles(connection) { var gfx = getBendpointsContainer(connection); if (!gfx) { gfx = getBendpointsContainer(connection, true); createBendpoints(gfx, connection); createSegmentDraggers(gfx, connection); } return gfx; } function updateHandles(connection) { var gfx = getBendpointsContainer(connection); if (gfx) { clearSegmentDraggers(gfx); clearBendpoints(gfx); createSegmentDraggers(gfx, connection); createBendpoints(gfx, connection); } } function updateFloatingBendpointPosition(parentGfx, intersection) { var floating = query('.floating', parentGfx), point = intersection.point; if (!floating) { return; } translate$2(floating, point.x, point.y); } function updateSegmentDraggerPosition(parentGfx, intersection, waypoints) { var draggerGfx = getSegmentDragger(intersection.index, parentGfx), segmentStart = waypoints[intersection.index - 1], segmentEnd = waypoints[intersection.index], point = intersection.point, mid = getMidPoint(segmentStart, segmentEnd), alignment = pointsAligned(segmentStart, segmentEnd), draggerVisual, relativePosition; if (!draggerGfx) { return; } draggerVisual = getDraggerVisual(draggerGfx); relativePosition = { x: point.x - mid.x, y: point.y - mid.y }; if (alignment === 'v') { // rotate position relativePosition = { x: relativePosition.y, y: relativePosition.x }; } translate$2(draggerVisual, relativePosition.x, relativePosition.y); } eventBus.on('connection.changed', function(event) { updateHandles(event.element); }); eventBus.on('connection.remove', function(event) { var gfx = getBendpointsContainer(event.element); if (gfx) { remove$1(gfx); } }); eventBus.on('element.marker.update', function(event) { var element = event.element, bendpointsGfx; if (!element.waypoints) { return; } bendpointsGfx = addHandles(element); if (event.add) { classes(bendpointsGfx).add(event.marker); } else { classes(bendpointsGfx).remove(event.marker); } }); eventBus.on('element.mousemove', function(event) { var element = event.element, waypoints = element.waypoints, bendpointsGfx, intersection; if (waypoints) { bendpointsGfx = getBendpointsContainer(element, true); intersection = getConnectionIntersection(canvas, waypoints, event.originalEvent); if (!intersection) { return; } updateFloatingBendpointPosition(bendpointsGfx, intersection); if (!intersection.bendpoint) { updateSegmentDraggerPosition(bendpointsGfx, intersection, waypoints); } } }); eventBus.on('element.mousedown', function(event) { if (!isPrimaryButton(event)) { return; } var originalEvent = event.originalEvent, element = event.element; if (!element.waypoints) { return; } return activateBendpointMove(originalEvent, element); }); eventBus.on('selection.changed', function(event) { var newSelection = event.newSelection, primary = newSelection[0]; if (primary && primary.waypoints) { addHandles(primary); } }); eventBus.on('element.hover', function(event) { var element = event.element; if (element.waypoints) { addHandles(element); interactionEvents.registerEvent(event.gfx, 'mousemove', 'element.mousemove'); } }); eventBus.on('element.out', function(event) { interactionEvents.unregisterEvent(event.gfx, 'mousemove', 'element.mousemove'); }); // update bendpoint container data attribute on element ID change eventBus.on('element.updateId', function(context) { var element = context.element, newId = context.newId; if (element.waypoints) { var bendpointContainer = getBendpointsContainer(element); if (bendpointContainer) { attr(bendpointContainer, { 'data-element-id': newId }); } } }); // API this.addHandles = addHandles; this.updateHandles = updateHandles; this.getBendpointsContainer = getBendpointsContainer; this.getSegmentDragger = getSegmentDragger; } Bendpoints.$inject = [ 'eventBus', 'canvas', 'interactionEvents', 'bendpointMove', 'connectionSegmentMove' ]; // helper ///////////// function getDraggerVisual(draggerGfx) { return query('.djs-visual', draggerGfx); } var round$8 = Math.round; var RECONNECT_START$1 = 'reconnectStart', RECONNECT_END$1 = 'reconnectEnd', UPDATE_WAYPOINTS$1 = 'updateWaypoints'; /** * Move bendpoints through drag and drop to add/remove bendpoints or reconnect connection. */ function BendpointMove(injector, eventBus, canvas, dragging, rules, modeling) { this._injector = injector; this.start = function(event, connection, bendpointIndex, insert) { var gfx = canvas.getGraphics(connection), source = connection.source, target = connection.target, waypoints = connection.waypoints, type; if (!insert && bendpointIndex === 0) { type = RECONNECT_START$1; } else if (!insert && bendpointIndex === waypoints.length - 1) { type = RECONNECT_END$1; } else { type = UPDATE_WAYPOINTS$1; } var command = type === UPDATE_WAYPOINTS$1 ? 'connection.updateWaypoints' : 'connection.reconnect'; var allowed = rules.allowed(command, { connection: connection, source: source, target: target }); if (allowed === false) { allowed = rules.allowed(command, { connection: connection, source: target, target: source }); } if (allowed === false) { return; } dragging.init(event, 'bendpoint.move', { data: { connection: connection, connectionGfx: gfx, context: { allowed: allowed, bendpointIndex: bendpointIndex, connection: connection, source: source, target: target, insert: insert, type: type } } }); }; eventBus.on('bendpoint.move.hover', function(event) { var context = event.context, connection = context.connection, source = connection.source, target = connection.target, hover = event.hover, type = context.type; // cache hover state context.hover = hover; var allowed; if (!hover) { return; } var command = type === UPDATE_WAYPOINTS$1 ? 'connection.updateWaypoints' : 'connection.reconnect'; allowed = context.allowed = rules.allowed(command, { connection: connection, source: type === RECONNECT_START$1 ? hover : source, target: type === RECONNECT_END$1 ? hover : target }); if (allowed) { context.source = type === RECONNECT_START$1 ? hover : source; context.target = type === RECONNECT_END$1 ? hover : target; return; } if (allowed === false) { allowed = context.allowed = rules.allowed(command, { connection: connection, source: type === RECONNECT_END$1 ? hover : target, target: type === RECONNECT_START$1 ? hover : source }); } if (allowed) { context.source = type === RECONNECT_END$1 ? hover : target; context.target = type === RECONNECT_START$1 ? hover : source; } }); eventBus.on([ 'bendpoint.move.out', 'bendpoint.move.cleanup' ], function(event) { var context = event.context, type = context.type; context.hover = null; context.source = null; context.target = null; if (type !== UPDATE_WAYPOINTS$1) { context.allowed = false; } }); eventBus.on('bendpoint.move.end', function(event) { var context = event.context, allowed = context.allowed, bendpointIndex = context.bendpointIndex, connection = context.connection, insert = context.insert, newWaypoints = connection.waypoints.slice(), source = context.source, target = context.target, type = context.type, hints = context.hints || {}; // ensure integer values (important if zoom level was > 1 during move) var docking = { x: round$8(event.x), y: round$8(event.y) }; if (!allowed) { return false; } if (type === UPDATE_WAYPOINTS$1) { if (insert) { // insert new bendpoint newWaypoints.splice(bendpointIndex, 0, docking); } else { // swap previous waypoint with moved one newWaypoints[bendpointIndex] = docking; } // pass hints about actual moved bendpoint // useful for connection/label layout hints.bendpointMove = { insert: insert, bendpointIndex: bendpointIndex }; newWaypoints = this.cropWaypoints(connection, newWaypoints); modeling.updateWaypoints(connection, filterRedundantWaypoints(newWaypoints), hints); } else { if (type === RECONNECT_START$1) { hints.docking = 'source'; if (isReverse$2(context)) { hints.docking = 'target'; hints.newWaypoints = newWaypoints.reverse(); } } else if (type === RECONNECT_END$1) { hints.docking = 'target'; if (isReverse$2(context)) { hints.docking = 'source'; hints.newWaypoints = newWaypoints.reverse(); } } modeling.reconnect(connection, source, target, docking, hints); } }, this); } BendpointMove.$inject = [ 'injector', 'eventBus', 'canvas', 'dragging', 'rules', 'modeling' ]; BendpointMove.prototype.cropWaypoints = function(connection, newWaypoints) { var connectionDocking = this._injector.get('connectionDocking', false); if (!connectionDocking) { return newWaypoints; } var waypoints = connection.waypoints; connection.waypoints = newWaypoints; connection.waypoints = connectionDocking.getCroppedWaypoints(connection); newWaypoints = connection.waypoints; connection.waypoints = waypoints; return newWaypoints; }; // helpers ////////// function isReverse$2(context) { var hover = context.hover, source = context.source, target = context.target, type = context.type; if (type === RECONNECT_START$1) { return hover && target && hover === target && source !== target; } if (type === RECONNECT_END$1) { return hover && source && hover === source && source !== target; } } var RECONNECT_START = 'reconnectStart', RECONNECT_END = 'reconnectEnd', UPDATE_WAYPOINTS = 'updateWaypoints'; var MARKER_OK$4 = 'connect-ok', MARKER_NOT_OK$4 = 'connect-not-ok', MARKER_CONNECT_HOVER$1 = 'connect-hover', MARKER_CONNECT_UPDATING$1 = 'djs-updating', MARKER_ELEMENT_HIDDEN = 'djs-element-hidden'; var HIGH_PRIORITY$i = 1100; /** * Preview connection while moving bendpoints. */ function BendpointMovePreview(bendpointMove, injector, eventBus, canvas) { this._injector = injector; var connectionPreview = injector.get('connectionPreview', false); eventBus.on('bendpoint.move.start', function(event) { var context = event.context, bendpointIndex = context.bendpointIndex, connection = context.connection, insert = context.insert, waypoints = connection.waypoints, newWaypoints = waypoints.slice(); context.waypoints = waypoints; if (insert) { // insert placeholder for new bendpoint newWaypoints.splice(bendpointIndex, 0, { x: event.x, y: event.y }); } connection.waypoints = newWaypoints; // add dragger gfx var draggerGfx = context.draggerGfx = addBendpoint(canvas.getLayer('overlays')); classes(draggerGfx).add('djs-dragging'); canvas.addMarker(connection, MARKER_ELEMENT_HIDDEN); canvas.addMarker(connection, MARKER_CONNECT_UPDATING$1); }); eventBus.on('bendpoint.move.hover', function(event) { var context = event.context, allowed = context.allowed, hover = context.hover, type = context.type; if (hover) { canvas.addMarker(hover, MARKER_CONNECT_HOVER$1); if (type === UPDATE_WAYPOINTS) { return; } if (allowed) { canvas.removeMarker(hover, MARKER_NOT_OK$4); canvas.addMarker(hover, MARKER_OK$4); } else if (allowed === false) { canvas.removeMarker(hover, MARKER_OK$4); canvas.addMarker(hover, MARKER_NOT_OK$4); } } }); eventBus.on([ 'bendpoint.move.out', 'bendpoint.move.cleanup' ], HIGH_PRIORITY$i, function(event) { var context = event.context, hover = context.hover, target = context.target; if (hover) { canvas.removeMarker(hover, MARKER_CONNECT_HOVER$1); canvas.removeMarker(hover, target ? MARKER_OK$4 : MARKER_NOT_OK$4); } }); eventBus.on('bendpoint.move.move', function(event) { var context = event.context, allowed = context.allowed, bendpointIndex = context.bendpointIndex, draggerGfx = context.draggerGfx, hover = context.hover, type = context.type, connection = context.connection, source = connection.source, target = connection.target, newWaypoints = connection.waypoints.slice(), bendpoint = { x: event.x, y: event.y }, hints = context.hints || {}, drawPreviewHints = {}; if (connectionPreview) { if (hints.connectionStart) { drawPreviewHints.connectionStart = hints.connectionStart; } if (hints.connectionEnd) { drawPreviewHints.connectionEnd = hints.connectionEnd; } if (type === RECONNECT_START) { if (isReverse$2(context)) { drawPreviewHints.connectionEnd = drawPreviewHints.connectionEnd || bendpoint; drawPreviewHints.source = target; drawPreviewHints.target = hover || source; newWaypoints = newWaypoints.reverse(); } else { drawPreviewHints.connectionStart = drawPreviewHints.connectionStart || bendpoint; drawPreviewHints.source = hover || source; drawPreviewHints.target = target; } } else if (type === RECONNECT_END) { if (isReverse$2(context)) { drawPreviewHints.connectionStart = drawPreviewHints.connectionStart || bendpoint; drawPreviewHints.source = hover || target; drawPreviewHints.target = source; newWaypoints = newWaypoints.reverse(); } else { drawPreviewHints.connectionEnd = drawPreviewHints.connectionEnd || bendpoint; drawPreviewHints.source = source; drawPreviewHints.target = hover || target; } } else { drawPreviewHints.noCropping = true; drawPreviewHints.noLayout = true; newWaypoints[ bendpointIndex ] = bendpoint; } if (type === UPDATE_WAYPOINTS) { newWaypoints = bendpointMove.cropWaypoints(connection, newWaypoints); } drawPreviewHints.waypoints = newWaypoints; connectionPreview.drawPreview(context, allowed, drawPreviewHints); } translate$2(draggerGfx, event.x, event.y); }, this); eventBus.on([ 'bendpoint.move.end', 'bendpoint.move.cancel' ], HIGH_PRIORITY$i, function(event) { var context = event.context, connection = context.connection, draggerGfx = context.draggerGfx, hover = context.hover, target = context.target, waypoints = context.waypoints; connection.waypoints = waypoints; // remove dragger gfx remove$1(draggerGfx); canvas.removeMarker(connection, MARKER_CONNECT_UPDATING$1); canvas.removeMarker(connection, MARKER_ELEMENT_HIDDEN); if (hover) { canvas.removeMarker(hover, MARKER_OK$4); canvas.removeMarker(hover, target ? MARKER_OK$4 : MARKER_NOT_OK$4); } if (connectionPreview) { connectionPreview.cleanUp(context); } }); } BendpointMovePreview.$inject = [ 'bendpointMove', 'injector', 'eventBus', 'canvas' ]; var MARKER_CONNECT_HOVER = 'connect-hover', MARKER_CONNECT_UPDATING = 'djs-updating'; function axisAdd(point, axis, delta) { return axisSet(point, axis, point[axis] + delta); } function axisSet(point, axis, value) { return { x: (axis === 'x' ? value : point.x), y: (axis === 'y' ? value : point.y) }; } function axisFenced(position, segmentStart, segmentEnd, axis) { var maxValue = Math.max(segmentStart[axis], segmentEnd[axis]), minValue = Math.min(segmentStart[axis], segmentEnd[axis]); var padding = 20; var fencedValue = Math.min(Math.max(minValue + padding, position[axis]), maxValue - padding); return axisSet(segmentStart, axis, fencedValue); } function flipAxis(axis) { return axis === 'x' ? 'y' : 'x'; } /** * Get the docking point on the given element. * * Compute a reasonable docking, if non exists. * * @param {Point} point * @param {djs.model.Shape} referenceElement * @param {string} moveAxis (x|y) * * @return {Point} */ function getDocking$2(point, referenceElement, moveAxis) { var referenceMid, inverseAxis; if (point.original) { return point.original; } else { referenceMid = getMid(referenceElement); inverseAxis = flipAxis(moveAxis); return axisSet(point, inverseAxis, referenceMid[inverseAxis]); } } /** * A component that implements moving of bendpoints */ function ConnectionSegmentMove( injector, eventBus, canvas, dragging, graphicsFactory, modeling) { // optional connection docking integration var connectionDocking = injector.get('connectionDocking', false); // API this.start = function(event, connection, idx) { var context, gfx = canvas.getGraphics(connection), segmentStartIndex = idx - 1, segmentEndIndex = idx, waypoints = connection.waypoints, segmentStart = waypoints[segmentStartIndex], segmentEnd = waypoints[segmentEndIndex], intersection = getConnectionIntersection(canvas, waypoints, event), direction, axis, dragPosition; direction = pointsAligned(segmentStart, segmentEnd); // do not move diagonal connection if (!direction) { return; } // the axis where we are going to move things axis = direction === 'v' ? 'x' : 'y'; if (segmentStartIndex === 0) { segmentStart = getDocking$2(segmentStart, connection.source, axis); } if (segmentEndIndex === waypoints.length - 1) { segmentEnd = getDocking$2(segmentEnd, connection.target, axis); } if (intersection) { dragPosition = intersection.point; } else { // set to segment center as default dragPosition = { x: (segmentStart.x + segmentEnd.x) / 2, y: (segmentStart.y + segmentEnd.y) / 2 }; } context = { connection: connection, segmentStartIndex: segmentStartIndex, segmentEndIndex: segmentEndIndex, segmentStart: segmentStart, segmentEnd: segmentEnd, axis: axis, dragPosition: dragPosition }; dragging.init(event, dragPosition, 'connectionSegment.move', { cursor: axis === 'x' ? 'resize-ew' : 'resize-ns', data: { connection: connection, connectionGfx: gfx, context: context } }); }; /** * Crop connection if connection cropping is provided. * * @param {Connection} connection * @param {Array} newWaypoints * * @return {Array} cropped connection waypoints */ function cropConnection(connection, newWaypoints) { // crop connection, if docking service is provided only if (!connectionDocking) { return newWaypoints; } var oldWaypoints = connection.waypoints, croppedWaypoints; // temporary set new waypoints connection.waypoints = newWaypoints; croppedWaypoints = connectionDocking.getCroppedWaypoints(connection); // restore old waypoints connection.waypoints = oldWaypoints; return croppedWaypoints; } // DRAGGING IMPLEMENTATION function redrawConnection(data) { graphicsFactory.update('connection', data.connection, data.connectionGfx); } function updateDragger(context, segmentOffset, event) { var newWaypoints = context.newWaypoints, segmentStartIndex = context.segmentStartIndex + segmentOffset, segmentStart = newWaypoints[segmentStartIndex], segmentEndIndex = context.segmentEndIndex + segmentOffset, segmentEnd = newWaypoints[segmentEndIndex], axis = flipAxis(context.axis); // make sure the dragger does not move // outside the connection var draggerPosition = axisFenced(event, segmentStart, segmentEnd, axis); // update dragger translate$2(context.draggerGfx, draggerPosition.x, draggerPosition.y); } /** * Filter waypoints for redundant ones (i.e. on the same axis). * Returns the filtered waypoints and the offset related to the segment move. * * @param {Array} waypoints * @param {Integer} segmentStartIndex of moved segment start * * @return {Object} { filteredWaypoints, segmentOffset } */ function filterRedundantWaypoints(waypoints, segmentStartIndex) { var segmentOffset = 0; var filteredWaypoints = waypoints.filter(function(r, idx) { if (pointsOnLine(waypoints[idx - 1], waypoints[idx + 1], r)) { // remove point and increment offset segmentOffset = idx <= segmentStartIndex ? segmentOffset - 1 : segmentOffset; return false; } // dont remove point return true; }); return { waypoints: filteredWaypoints, segmentOffset: segmentOffset }; } eventBus.on('connectionSegment.move.start', function(event) { var context = event.context, connection = event.connection, layer = canvas.getLayer('overlays'); context.originalWaypoints = connection.waypoints.slice(); // add dragger gfx context.draggerGfx = addSegmentDragger(layer, context.segmentStart, context.segmentEnd); classes(context.draggerGfx).add('djs-dragging'); canvas.addMarker(connection, MARKER_CONNECT_UPDATING); }); eventBus.on('connectionSegment.move.move', function(event) { var context = event.context, connection = context.connection, segmentStartIndex = context.segmentStartIndex, segmentEndIndex = context.segmentEndIndex, segmentStart = context.segmentStart, segmentEnd = context.segmentEnd, axis = context.axis; var newWaypoints = context.originalWaypoints.slice(), newSegmentStart = axisAdd(segmentStart, axis, event['d' + axis]), newSegmentEnd = axisAdd(segmentEnd, axis, event['d' + axis]); // original waypoint count and added / removed // from start waypoint delta. We use the later // to retrieve the updated segmentStartIndex / segmentEndIndex var waypointCount = newWaypoints.length, segmentOffset = 0; // move segment start / end by axis delta newWaypoints[segmentStartIndex] = newSegmentStart; newWaypoints[segmentEndIndex] = newSegmentEnd; var sourceToSegmentOrientation, targetToSegmentOrientation; // handle first segment if (segmentStartIndex < 2) { sourceToSegmentOrientation = getOrientation(connection.source, newSegmentStart); // first bendpoint, remove first segment if intersecting if (segmentStartIndex === 1) { if (sourceToSegmentOrientation === 'intersect') { newWaypoints.shift(); newWaypoints[0] = newSegmentStart; segmentOffset--; } } // docking point, add segment if not intersecting anymore else { if (sourceToSegmentOrientation !== 'intersect') { newWaypoints.unshift(segmentStart); segmentOffset++; } } } // handle last segment if (segmentEndIndex > waypointCount - 3) { targetToSegmentOrientation = getOrientation(connection.target, newSegmentEnd); // last bendpoint, remove last segment if intersecting if (segmentEndIndex === waypointCount - 2) { if (targetToSegmentOrientation === 'intersect') { newWaypoints.pop(); newWaypoints[newWaypoints.length - 1] = newSegmentEnd; } } // last bendpoint, remove last segment if intersecting else { if (targetToSegmentOrientation !== 'intersect') { newWaypoints.push(segmentEnd); } } } // update connection waypoints context.newWaypoints = connection.waypoints = cropConnection(connection, newWaypoints); // update dragger position updateDragger(context, segmentOffset, event); // save segmentOffset in context context.newSegmentStartIndex = segmentStartIndex + segmentOffset; // redraw connection redrawConnection(event); }); eventBus.on('connectionSegment.move.hover', function(event) { event.context.hover = event.hover; canvas.addMarker(event.hover, MARKER_CONNECT_HOVER); }); eventBus.on([ 'connectionSegment.move.out', 'connectionSegment.move.cleanup' ], function(event) { // remove connect marker // if it was added var hover = event.context.hover; if (hover) { canvas.removeMarker(hover, MARKER_CONNECT_HOVER); } }); eventBus.on('connectionSegment.move.cleanup', function(event) { var context = event.context, connection = context.connection; // remove dragger gfx if (context.draggerGfx) { remove$1(context.draggerGfx); } canvas.removeMarker(connection, MARKER_CONNECT_UPDATING); }); eventBus.on([ 'connectionSegment.move.cancel', 'connectionSegment.move.end' ], function(event) { var context = event.context, connection = context.connection; connection.waypoints = context.originalWaypoints; redrawConnection(event); }); eventBus.on('connectionSegment.move.end', function(event) { var context = event.context, connection = context.connection, newWaypoints = context.newWaypoints, newSegmentStartIndex = context.newSegmentStartIndex; // ensure we have actual pixel values bendpoint // coordinates (important when zoom level was > 1 during move) newWaypoints = newWaypoints.map(function(p) { return { original: p.original, x: Math.round(p.x), y: Math.round(p.y) }; }); // apply filter redunant waypoints var filtered = filterRedundantWaypoints(newWaypoints, newSegmentStartIndex); // get filtered waypoints var filteredWaypoints = filtered.waypoints, croppedWaypoints = cropConnection(connection, filteredWaypoints), segmentOffset = filtered.segmentOffset; var hints = { segmentMove: { segmentStartIndex: context.segmentStartIndex, newSegmentStartIndex: newSegmentStartIndex + segmentOffset } }; modeling.updateWaypoints(connection, croppedWaypoints, hints); }); } ConnectionSegmentMove.$inject = [ 'injector', 'eventBus', 'canvas', 'dragging', 'graphicsFactory', 'modeling' ]; var abs$6 = Math.abs, round$7 = Math.round; /** * Snap value to a collection of reference values. * * @param {number} value * @param {Array} values * @param {number} [tolerance=10] * * @return {number} the value we snapped to or null, if none snapped */ function snapTo(value, values, tolerance) { tolerance = tolerance === undefined ? 10 : tolerance; var idx, snapValue; for (idx = 0; idx < values.length; idx++) { snapValue = values[idx]; if (abs$6(snapValue - value) <= tolerance) { return snapValue; } } } function topLeft(bounds) { return { x: bounds.x, y: bounds.y }; } function bottomRight(bounds) { return { x: bounds.x + bounds.width, y: bounds.y + bounds.height }; } function mid$2(bounds, defaultValue) { if (!bounds || isNaN(bounds.x) || isNaN(bounds.y)) { return defaultValue; } return { x: round$7(bounds.x + bounds.width / 2), y: round$7(bounds.y + bounds.height / 2) }; } /** * Retrieve the snap state of the given event. * * @param {Event} event * @param {string} axis * * @return {boolean} the snapped state * */ function isSnapped(event, axis) { var snapped = event.snapped; if (!snapped) { return false; } if (typeof axis === 'string') { return snapped[axis]; } return snapped.x && snapped.y; } /** * Set the given event as snapped. * * This method may change the x and/or y position of the shape * from the given event! * * @param {Event} event * @param {string} axis * @param {number|boolean} value * * @return {number} old value */ function setSnapped(event, axis, value) { if (typeof axis !== 'string') { throw new Error('axis must be in [x, y]'); } if (typeof value !== 'number' && value !== false) { throw new Error('value must be Number or false'); } var delta, previousValue = event[axis]; var snapped = event.snapped = (event.snapped || {}); if (value === false) { snapped[axis] = false; } else { snapped[axis] = true; delta = value - previousValue; event[axis] += delta; event['d' + axis] += delta; } return previousValue; } /** * Get children of a shape. * * @param {djs.model.Shape} parent * * @returns {Array} */ function getChildren(parent) { return parent.children || []; } var abs$5 = Math.abs, round$6 = Math.round; var TOLERANCE = 10; function BendpointSnapping(eventBus) { function snapTo(values, value) { if (isArray$3(values)) { var i = values.length; while (i--) if (abs$5(values[i] - value) <= TOLERANCE) { return values[i]; } } else { values = +values; var rem = value % values; if (rem < TOLERANCE) { return value - rem; } if (rem > values - TOLERANCE) { return value - rem + values; } } return value; } function getSnapPoint(element, event) { if (element.waypoints) { return getClosestPointOnConnection(event, element); } if (element.width) { return { x: round$6(element.width / 2 + element.x), y: round$6(element.height / 2 + element.y) }; } } // connection segment snapping ////////////////////// function getConnectionSegmentSnaps(event) { var context = event.context, snapPoints = context.snapPoints, connection = context.connection, waypoints = connection.waypoints, segmentStart = context.segmentStart, segmentStartIndex = context.segmentStartIndex, segmentEnd = context.segmentEnd, segmentEndIndex = context.segmentEndIndex, axis = context.axis; if (snapPoints) { return snapPoints; } var referenceWaypoints = [ waypoints[segmentStartIndex - 1], segmentStart, segmentEnd, waypoints[segmentEndIndex + 1] ]; if (segmentStartIndex < 2) { referenceWaypoints.unshift(getSnapPoint(connection.source, event)); } if (segmentEndIndex > waypoints.length - 3) { referenceWaypoints.unshift(getSnapPoint(connection.target, event)); } context.snapPoints = snapPoints = { horizontal: [] , vertical: [] }; forEach$1(referenceWaypoints, function(p) { // we snap on existing bendpoints only, // not placeholders that are inserted during add if (p) { p = p.original || p; if (axis === 'y') { snapPoints.horizontal.push(p.y); } if (axis === 'x') { snapPoints.vertical.push(p.x); } } }); return snapPoints; } eventBus.on('connectionSegment.move.move', 1500, function(event) { var snapPoints = getConnectionSegmentSnaps(event), x = event.x, y = event.y, sx, sy; if (!snapPoints) { return; } // snap sx = snapTo(snapPoints.vertical, x); sy = snapTo(snapPoints.horizontal, y); // correction x/y var cx = (x - sx), cy = (y - sy); // update delta assign(event, { dx: event.dx - cx, dy: event.dy - cy, x: sx, y: sy }); // only set snapped if actually snapped if (cx || snapPoints.vertical.indexOf(x) !== -1) { setSnapped(event, 'x', sx); } if (cy || snapPoints.horizontal.indexOf(y) !== -1) { setSnapped(event, 'y', sy); } }); // bendpoint snapping ////////////////////// function getBendpointSnaps(context) { var snapPoints = context.snapPoints, waypoints = context.connection.waypoints, bendpointIndex = context.bendpointIndex; if (snapPoints) { return snapPoints; } var referenceWaypoints = [ waypoints[bendpointIndex - 1], waypoints[bendpointIndex + 1] ]; context.snapPoints = snapPoints = { horizontal: [] , vertical: [] }; forEach$1(referenceWaypoints, function(p) { // we snap on existing bendpoints only, // not placeholders that are inserted during add if (p) { p = p.original || p; snapPoints.horizontal.push(p.y); snapPoints.vertical.push(p.x); } }); return snapPoints; } // Snap Endpoint of new connection eventBus.on([ 'connect.hover', 'connect.move', 'connect.end' ], 1500, function(event) { var context = event.context, hover = context.hover, hoverMid = hover && getSnapPoint(hover, event); // only snap on connections, elements can have multiple connect endpoints if (!isConnection$d(hover) || !hoverMid || !hoverMid.x || !hoverMid.y) { return; } setSnapped(event, 'x', hoverMid.x); setSnapped(event, 'y', hoverMid.y); }); eventBus.on([ 'bendpoint.move.move', 'bendpoint.move.end' ], 1500, function(event) { var context = event.context, snapPoints = getBendpointSnaps(context), hover = context.hover, hoverMid = hover && getSnapPoint(hover, event), x = event.x, y = event.y, sx, sy; if (!snapPoints) { return; } // snap to hover mid sx = snapTo(hoverMid ? snapPoints.vertical.concat([ hoverMid.x ]) : snapPoints.vertical, x); sy = snapTo(hoverMid ? snapPoints.horizontal.concat([ hoverMid.y ]) : snapPoints.horizontal, y); // correction x/y var cx = (x - sx), cy = (y - sy); // update delta assign(event, { dx: event.dx - cx, dy: event.dy - cy, x: event.x - cx, y: event.y - cy }); // only set snapped if actually snapped if (cx || snapPoints.vertical.indexOf(x) !== -1) { setSnapped(event, 'x', sx); } if (cy || snapPoints.horizontal.indexOf(y) !== -1) { setSnapped(event, 'y', sy); } }); } BendpointSnapping.$inject = [ 'eventBus' ]; // helpers ////////////////////// function isConnection$d(element) { return element && !!element.waypoints; } var BendpointsModule = { __depends__: [ DraggingModule, RulesModule$1 ], __init__: [ 'bendpoints', 'bendpointSnapping', 'bendpointMovePreview' ], bendpoints: [ 'type', Bendpoints ], bendpointMove: [ 'type', BendpointMove ], bendpointMovePreview: [ 'type', BendpointMovePreview ], connectionSegmentMove: [ 'type', ConnectionSegmentMove ], bendpointSnapping: [ 'type', BendpointSnapping ] }; function Connect(eventBus, dragging, modeling, rules) { // rules function canConnect(source, target) { return rules.allowed('connection.create', { source: source, target: target }); } function canConnectReverse(source, target) { return canConnect(target, source); } // event handlers eventBus.on('connect.hover', function(event) { var context = event.context, start = context.start, hover = event.hover, canExecute; // cache hover state context.hover = hover; canExecute = context.canExecute = canConnect(start, hover); // ignore hover if (isNil(canExecute)) { return; } if (canExecute !== false) { context.source = start; context.target = hover; return; } canExecute = context.canExecute = canConnectReverse(start, hover); // ignore hover if (isNil(canExecute)) { return; } if (canExecute !== false) { context.source = hover; context.target = start; } }); eventBus.on([ 'connect.out', 'connect.cleanup' ], function(event) { var context = event.context; context.hover = null; context.source = null; context.target = null; context.canExecute = false; }); eventBus.on('connect.end', function(event) { var context = event.context, canExecute = context.canExecute, connectionStart = context.connectionStart, connectionEnd = { x: event.x, y: event.y }, source = context.source, target = context.target; if (!canExecute) { return false; } var attrs = null, hints = { connectionStart: isReverse$1(context) ? connectionEnd : connectionStart, connectionEnd: isReverse$1(context) ? connectionStart : connectionEnd }; if (isObject(canExecute)) { attrs = canExecute; } context.connection = modeling.connect(source, target, attrs, hints); }); // API /** * Start connect operation. * * @param {DOMEvent} event * @param {djs.model.Base} start * @param {Point} [connectionStart] * @param {boolean} [autoActivate=false] */ this.start = function(event, start, connectionStart, autoActivate) { if (!isObject(connectionStart)) { autoActivate = connectionStart; connectionStart = getMid(start); } dragging.init(event, 'connect', { autoActivate: autoActivate, data: { shape: start, context: { start: start, connectionStart: connectionStart } } }); }; } Connect.$inject = [ 'eventBus', 'dragging', 'modeling', 'rules' ]; // helpers ////////// function isReverse$1(context) { var hover = context.hover, source = context.source, target = context.target; return hover && source && hover === source && source !== target; } var HIGH_PRIORITY$h = 1100, LOW_PRIORITY$j = 900; var MARKER_OK$3 = 'connect-ok', MARKER_NOT_OK$3 = 'connect-not-ok'; /** * Shows connection preview during connect. * * @param {didi.Injector} injector * @param {EventBus} eventBus * @param {Canvas} canvas */ function ConnectPreview(injector, eventBus, canvas) { var connectionPreview = injector.get('connectionPreview', false); connectionPreview && eventBus.on('connect.move', function(event) { var context = event.context, canConnect = context.canExecute, hover = context.hover, source = context.source, start = context.start, startPosition = context.startPosition, target = context.target, connectionStart = context.connectionStart || startPosition, connectionEnd = context.connectionEnd || { x: event.x, y: event.y }, previewStart = connectionStart, previewEnd = connectionEnd; if (isReverse$1(context)) { previewStart = connectionEnd; previewEnd = connectionStart; } connectionPreview.drawPreview(context, canConnect, { source: source || start, target: target || hover, connectionStart: previewStart, connectionEnd: previewEnd }); }); eventBus.on('connect.hover', LOW_PRIORITY$j, function(event) { var context = event.context, hover = event.hover, canExecute = context.canExecute; // ignore hover if (canExecute === null) { return; } canvas.addMarker(hover, canExecute ? MARKER_OK$3 : MARKER_NOT_OK$3); }); eventBus.on([ 'connect.out', 'connect.cleanup' ], HIGH_PRIORITY$h, function(event) { var hover = event.hover; if (hover) { canvas.removeMarker(hover, MARKER_OK$3); canvas.removeMarker(hover, MARKER_NOT_OK$3); } }); connectionPreview && eventBus.on('connect.cleanup', function(event) { connectionPreview.cleanUp(event.context); }); } ConnectPreview.$inject = [ 'injector', 'eventBus', 'canvas' ]; var ConnectModule = { __depends__: [ SelectionModule, RulesModule$1, DraggingModule ], __init__: [ 'connectPreview' ], connect: [ 'type', Connect ], connectPreview: [ 'type', ConnectPreview ] }; var MARKER_CONNECTION_PREVIEW = 'djs-connection-preview'; /** * Draws connection preview. Optionally, this can use layouter and connection docking to draw * better looking previews. * * @param {didi.Injector} injector * @param {Canvas} canvas * @param {GraphicsFactory} graphicsFactory * @param {ElementFactory} elementFactory */ function ConnectionPreview( injector, canvas, graphicsFactory, elementFactory ) { this._canvas = canvas; this._graphicsFactory = graphicsFactory; this._elementFactory = elementFactory; // optional components this._connectionDocking = injector.get('connectionDocking', false); this._layouter = injector.get('layouter', false); } ConnectionPreview.$inject = [ 'injector', 'canvas', 'graphicsFactory', 'elementFactory' ]; /** * Draw connection preview. * * Provide at least one of and to create a preview. * In the clean up stage, call `connectionPreview#cleanUp` with the context to remove preview. * * @param {Object} context * @param {Object|boolean} canConnect * @param {Object} hints * @param {djs.model.shape} [hints.source] source element * @param {djs.model.shape} [hints.target] target element * @param {Point} [hints.connectionStart] connection preview start * @param {Point} [hints.connectionEnd] connection preview end * @param {Array} [hints.waypoints] provided waypoints for preview * @param {boolean} [hints.noLayout] true if preview should not be laid out * @param {boolean} [hints.noCropping] true if preview should not be cropped * @param {boolean} [hints.noNoop] true if simple connection should not be drawn */ ConnectionPreview.prototype.drawPreview = function(context, canConnect, hints) { hints = hints || {}; var connectionPreviewGfx = context.connectionPreviewGfx, getConnection = context.getConnection, source = hints.source, target = hints.target, waypoints = hints.waypoints, connectionStart = hints.connectionStart, connectionEnd = hints.connectionEnd, noLayout = hints.noLayout, noCropping = hints.noCropping, noNoop = hints.noNoop, connection; var self = this; if (!connectionPreviewGfx) { connectionPreviewGfx = context.connectionPreviewGfx = this.createConnectionPreviewGfx(); } clear(connectionPreviewGfx); if (!getConnection) { getConnection = context.getConnection = cacheReturnValues(function(canConnect, source, target) { return self.getConnection(canConnect, source, target); }); } if (canConnect) { connection = getConnection(canConnect, source, target); } if (!connection) { !noNoop && this.drawNoopPreview(connectionPreviewGfx, hints); return; } connection.waypoints = waypoints || []; // optional layout if (this._layouter && !noLayout) { connection.waypoints = this._layouter.layoutConnection(connection, { source: source, target: target, connectionStart: connectionStart, connectionEnd: connectionEnd, waypoints: hints.waypoints || connection.waypoints }); } // fallback if no waypoints were provided nor created with layouter if (!connection.waypoints || !connection.waypoints.length) { connection.waypoints = [ source ? getMid(source) : connectionStart, target ? getMid(target) : connectionEnd ]; } // optional cropping if (this._connectionDocking && (source || target) && !noCropping) { connection.waypoints = this._connectionDocking.getCroppedWaypoints(connection, source, target); } this._graphicsFactory.drawConnection(connectionPreviewGfx, connection); }; /** * Draw simple connection between source and target or provided points. * * @param {SVGElement} connectionPreviewGfx container for the connection * @param {Object} hints * @param {djs.model.shape} [hints.source] source element * @param {djs.model.shape} [hints.target] target element * @param {Point} [hints.connectionStart] required if source is not provided * @param {Point} [hints.connectionEnd] required if target is not provided */ ConnectionPreview.prototype.drawNoopPreview = function(connectionPreviewGfx, hints) { var source = hints.source, target = hints.target, start = hints.connectionStart || getMid(source), end = hints.connectionEnd || getMid(target); var waypoints = this.cropWaypoints(start, end, source, target); var connection = this.createNoopConnection(waypoints[0], waypoints[1]); append(connectionPreviewGfx, connection); }; /** * Return cropped waypoints. * * @param {Point} start * @param {Point} end * @param {djs.model.shape} source * @param {djs.model.shape} target * * @returns {Array} */ ConnectionPreview.prototype.cropWaypoints = function(start, end, source, target) { var graphicsFactory = this._graphicsFactory, sourcePath = source && graphicsFactory.getShapePath(source), targetPath = target && graphicsFactory.getShapePath(target), connectionPath = graphicsFactory.getConnectionPath({ waypoints: [ start, end ] }); start = (source && getElementLineIntersection(sourcePath, connectionPath, true)) || start; end = (target && getElementLineIntersection(targetPath, connectionPath, false)) || end; return [ start, end ]; }; /** * Remove connection preview container if it exists. * * @param {Object} [context] * @param {SVGElement} [context.connectionPreviewGfx] preview container */ ConnectionPreview.prototype.cleanUp = function(context) { if (context && context.connectionPreviewGfx) { remove$1(context.connectionPreviewGfx); } }; /** * Get connection that connects source and target. * * @param {Object|boolean} canConnect * * @returns {djs.model.connection} */ ConnectionPreview.prototype.getConnection = function(canConnect) { var attrs = ensureConnectionAttrs(canConnect); return this._elementFactory.createConnection(attrs); }; /** * Add and return preview graphics. * * @returns {SVGElement} */ ConnectionPreview.prototype.createConnectionPreviewGfx = function() { var gfx = create$1('g'); attr(gfx, { pointerEvents: 'none' }); classes(gfx).add(MARKER_CONNECTION_PREVIEW); append(this._canvas.getActiveLayer(), gfx); return gfx; }; /** * Create and return simple connection. * * @param {Point} start * @param {Point} end * * @returns {SVGElement} */ ConnectionPreview.prototype.createNoopConnection = function(start, end) { var connection = create$1('polyline'); attr(connection, { 'stroke': '#333', 'strokeDasharray': [ 1 ], 'strokeWidth': 2, 'pointer-events': 'none' }); attr(connection, { 'points': [ start.x, start.y, end.x, end.y ] }); return connection; }; // helpers ////////// /** * Returns function that returns cached return values referenced by stringified first argument. * * @param {Function} fn * * @return {Function} */ function cacheReturnValues(fn) { var returnValues = {}; /** * Return cached return value referenced by stringified first argument. * * @returns {*} */ return function(firstArgument) { var key = JSON.stringify(firstArgument); var returnValue = returnValues[key]; if (!returnValue) { returnValue = returnValues[key] = fn.apply(null, arguments); } return returnValue; }; } /** * Ensure connection attributes is object. * * @param {Object|boolean} canConnect * * @returns {Object} */ function ensureConnectionAttrs(canConnect) { if (isObject(canConnect)) { return canConnect; } else { return {}; } } var ConnectionPreviewModule = { __init__: [ 'connectionPreview' ], connectionPreview: [ 'type', ConnectionPreview ] }; var min$3 = Math.min, max$5 = Math.max; function preventDefault(e) { e.preventDefault(); } function stopPropagation(e) { e.stopPropagation(); } function isTextNode(node) { return node.nodeType === Node.TEXT_NODE; } function toArray(nodeList) { return [].slice.call(nodeList); } /** * Initializes a container for a content editable div. * * Structure: * * container * parent * content * resize-handle * * @param {object} options * @param {DOMElement} options.container The DOM element to append the contentContainer to * @param {Function} options.keyHandler Handler for key events * @param {Function} options.resizeHandler Handler for resize events */ function TextBox(options) { this.container = options.container; this.parent = domify( '
    ' + '
    ' + '
    ' ); this.content = query('[contenteditable]', this.parent); this.keyHandler = options.keyHandler || function() {}; this.resizeHandler = options.resizeHandler || function() {}; this.autoResize = bind(this.autoResize, this); this.handlePaste = bind(this.handlePaste, this); } /** * Create a text box with the given position, size, style and text content * * @param {Object} bounds * @param {Number} bounds.x absolute x position * @param {Number} bounds.y absolute y position * @param {Number} [bounds.width] fixed width value * @param {Number} [bounds.height] fixed height value * @param {Number} [bounds.maxWidth] maximum width value * @param {Number} [bounds.maxHeight] maximum height value * @param {Number} [bounds.minWidth] minimum width value * @param {Number} [bounds.minHeight] minimum height value * @param {Object} [style] * @param {String} value text content * * @return {DOMElement} The created content DOM element */ TextBox.prototype.create = function(bounds, style, value, options) { var self = this; var parent = this.parent, content = this.content, container = this.container; options = this.options = options || {}; style = this.style = style || {}; var parentStyle = pick(style, [ 'width', 'height', 'maxWidth', 'maxHeight', 'minWidth', 'minHeight', 'left', 'top', 'backgroundColor', 'position', 'overflow', 'border', 'wordWrap', 'textAlign', 'outline', 'transform' ]); assign(parent.style, { width: bounds.width + 'px', height: bounds.height + 'px', maxWidth: bounds.maxWidth + 'px', maxHeight: bounds.maxHeight + 'px', minWidth: bounds.minWidth + 'px', minHeight: bounds.minHeight + 'px', left: bounds.x + 'px', top: bounds.y + 'px', backgroundColor: '#ffffff', position: 'absolute', overflow: 'visible', border: '1px solid #ccc', boxSizing: 'border-box', wordWrap: 'normal', textAlign: 'center', outline: 'none' }, parentStyle); var contentStyle = pick(style, [ 'fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft' ]); assign(content.style, { boxSizing: 'border-box', width: '100%', outline: 'none', wordWrap: 'break-word' }, contentStyle); if (options.centerVertically) { assign(content.style, { position: 'absolute', top: '50%', transform: 'translate(0, -50%)' }, contentStyle); } content.innerText = value; componentEvent.bind(content, 'keydown', this.keyHandler); componentEvent.bind(content, 'mousedown', stopPropagation); componentEvent.bind(content, 'paste', self.handlePaste); if (options.autoResize) { componentEvent.bind(content, 'input', this.autoResize); } if (options.resizable) { this.resizable(style); } container.appendChild(parent); // set selection to end of text this.setSelection(content.lastChild, content.lastChild && content.lastChild.length); return parent; }; /** * Intercept paste events to remove formatting from pasted text. */ TextBox.prototype.handlePaste = function(e) { var options = this.options, style = this.style; e.preventDefault(); var text; if (e.clipboardData) { // Chrome, Firefox, Safari text = e.clipboardData.getData('text/plain'); } else { // Internet Explorer text = window.clipboardData.getData('Text'); } this.insertText(text); if (options.autoResize) { var hasResized = this.autoResize(style); if (hasResized) { this.resizeHandler(hasResized); } } }; TextBox.prototype.insertText = function(text) { text = normalizeEndOfLineSequences(text); // insertText command not supported by Internet Explorer var success = document.execCommand('insertText', false, text); if (success) { return; } this._insertTextIE(text); }; TextBox.prototype._insertTextIE = function(text) { // Internet Explorer var range = this.getSelection(), startContainer = range.startContainer, endContainer = range.endContainer, startOffset = range.startOffset, endOffset = range.endOffset, commonAncestorContainer = range.commonAncestorContainer; var childNodesArray = toArray(commonAncestorContainer.childNodes); var container, offset; if (isTextNode(commonAncestorContainer)) { var containerTextContent = startContainer.textContent; startContainer.textContent = containerTextContent.substring(0, startOffset) + text + containerTextContent.substring(endOffset); container = startContainer; offset = startOffset + text.length; } else if (startContainer === this.content && endContainer === this.content) { var textNode = document.createTextNode(text); this.content.insertBefore(textNode, childNodesArray[startOffset]); container = textNode; offset = textNode.textContent.length; } else { var startContainerChildIndex = childNodesArray.indexOf(startContainer), endContainerChildIndex = childNodesArray.indexOf(endContainer); childNodesArray.forEach(function(childNode, index) { if (index === startContainerChildIndex) { childNode.textContent = startContainer.textContent.substring(0, startOffset) + text + endContainer.textContent.substring(endOffset); } else if (index > startContainerChildIndex && index <= endContainerChildIndex) { remove$2(childNode); } }); container = startContainer; offset = startOffset + text.length; } if (container && offset !== undefined) { // is necessary in Internet Explorer setTimeout(function() { self.setSelection(container, offset); }); } }; /** * Automatically resize element vertically to fit its content. */ TextBox.prototype.autoResize = function() { var parent = this.parent, content = this.content; var fontSize = parseInt(this.style.fontSize) || 12; if (content.scrollHeight > parent.offsetHeight || content.scrollHeight < parent.offsetHeight - fontSize) { var bounds = parent.getBoundingClientRect(); var height = content.scrollHeight; parent.style.height = height + 'px'; this.resizeHandler({ width: bounds.width, height: bounds.height, dx: 0, dy: height - bounds.height }); } }; /** * Make an element resizable by adding a resize handle. */ TextBox.prototype.resizable = function() { var self = this; var parent = this.parent, resizeHandle = this.resizeHandle; var minWidth = parseInt(this.style.minWidth) || 0, minHeight = parseInt(this.style.minHeight) || 0, maxWidth = parseInt(this.style.maxWidth) || Infinity, maxHeight = parseInt(this.style.maxHeight) || Infinity; if (!resizeHandle) { resizeHandle = this.resizeHandle = domify( '
    ' ); var startX, startY, startWidth, startHeight; var onMouseDown = function(e) { preventDefault(e); stopPropagation(e); startX = e.clientX; startY = e.clientY; var bounds = parent.getBoundingClientRect(); startWidth = bounds.width; startHeight = bounds.height; componentEvent.bind(document, 'mousemove', onMouseMove); componentEvent.bind(document, 'mouseup', onMouseUp); }; var onMouseMove = function(e) { preventDefault(e); stopPropagation(e); var newWidth = min$3(max$5(startWidth + e.clientX - startX, minWidth), maxWidth); var newHeight = min$3(max$5(startHeight + e.clientY - startY, minHeight), maxHeight); parent.style.width = newWidth + 'px'; parent.style.height = newHeight + 'px'; self.resizeHandler({ width: startWidth, height: startHeight, dx: e.clientX - startX, dy: e.clientY - startY }); }; var onMouseUp = function(e) { preventDefault(e); stopPropagation(e); componentEvent.unbind(document,'mousemove', onMouseMove, false); componentEvent.unbind(document, 'mouseup', onMouseUp, false); }; componentEvent.bind(resizeHandle, 'mousedown', onMouseDown); } assign(resizeHandle.style, { position: 'absolute', bottom: '0px', right: '0px', cursor: 'nwse-resize', width: '0', height: '0', borderTop: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid transparent', borderRight: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid #ccc', borderBottom: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid #ccc', borderLeft: (parseInt(this.style.fontSize) / 4 || 3) + 'px solid transparent' }); parent.appendChild(resizeHandle); }; /** * Clear content and style of the textbox, unbind listeners and * reset CSS style. */ TextBox.prototype.destroy = function() { var parent = this.parent, content = this.content, resizeHandle = this.resizeHandle; // clear content content.innerText = ''; // clear styles parent.removeAttribute('style'); content.removeAttribute('style'); componentEvent.unbind(content, 'keydown', this.keyHandler); componentEvent.unbind(content, 'mousedown', stopPropagation); componentEvent.unbind(content, 'input', this.autoResize); componentEvent.unbind(content, 'paste', this.handlePaste); if (resizeHandle) { resizeHandle.removeAttribute('style'); remove$2(resizeHandle); } remove$2(parent); }; TextBox.prototype.getValue = function() { return this.content.innerText.trim(); }; TextBox.prototype.getSelection = function() { var selection = window.getSelection(), range = selection.getRangeAt(0); return range; }; TextBox.prototype.setSelection = function(container, offset) { var range = document.createRange(); if (container === null) { range.selectNodeContents(this.content); } else { range.setStart(container, offset); range.setEnd(container, offset); } var selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); }; // helpers ////////// function normalizeEndOfLineSequences(string) { return string.replace(/\r\n|\r|\n/g, '\n'); } /** * A direct editing component that allows users * to edit an elements text directly in the diagram * * @param {EventBus} eventBus the event bus */ function DirectEditing(eventBus, canvas) { this._eventBus = eventBus; this._providers = []; this._textbox = new TextBox({ container: canvas.getContainer(), keyHandler: bind(this._handleKey, this), resizeHandler: bind(this._handleResize, this) }); } DirectEditing.$inject = [ 'eventBus', 'canvas' ]; /** * Register a direct editing provider * @param {Object} provider the provider, must expose an #activate(element) method that returns * an activation context ({ bounds: {x, y, width, height }, text }) if * direct editing is available for the given element. * Additionally the provider must expose a #update(element, value) method * to receive direct editing updates. */ DirectEditing.prototype.registerProvider = function(provider) { this._providers.push(provider); }; /** * Returns true if direct editing is currently active * * @param {djs.model.Base} [element] * * @return {boolean} */ DirectEditing.prototype.isActive = function(element) { return !!(this._active && (!element || this._active.element === element)); }; /** * Cancel direct editing, if it is currently active */ DirectEditing.prototype.cancel = function() { if (!this._active) { return; } this._fire('cancel'); this.close(); }; DirectEditing.prototype._fire = function(event, context) { this._eventBus.fire('directEditing.' + event, context || { active: this._active }); }; DirectEditing.prototype.close = function() { this._textbox.destroy(); this._fire('deactivate'); this._active = null; this.resizable = undefined; }; DirectEditing.prototype.complete = function() { var active = this._active; if (!active) { return; } var containerBounds, previousBounds = active.context.bounds, newBounds = this.$textbox.getBoundingClientRect(), newText = this.getValue(), previousText = active.context.text; if ( newText !== previousText || newBounds.height !== previousBounds.height || newBounds.width !== previousBounds.width ) { containerBounds = this._textbox.container.getBoundingClientRect(); active.provider.update(active.element, newText, active.context.text, { x: newBounds.left - containerBounds.left, y: newBounds.top - containerBounds.top, width: newBounds.width, height: newBounds.height }); } this._fire('complete'); this.close(); }; DirectEditing.prototype.getValue = function() { return this._textbox.getValue(); }; DirectEditing.prototype._handleKey = function(e) { // stop bubble e.stopPropagation(); var key = e.keyCode || e.charCode; // ESC if (key === 27) { e.preventDefault(); return this.cancel(); } // Enter if (key === 13 && !e.shiftKey) { e.preventDefault(); return this.complete(); } }; DirectEditing.prototype._handleResize = function(event) { this._fire('resize', event); }; /** * Activate direct editing on the given element * * @param {Object} ElementDescriptor the descriptor for a shape or connection * @return {Boolean} true if the activation was possible */ DirectEditing.prototype.activate = function(element) { if (this.isActive()) { this.cancel(); } // the direct editing context var context; var provider = find(this._providers, function(p) { return (context = p.activate(element)) ? p : null; }); // check if activation took place if (context) { this.$textbox = this._textbox.create( context.bounds, context.style, context.text, context.options ); this._active = { element: element, context: context, provider: provider }; if (context.options && context.options.resizable) { this.resizable = true; } this._fire('activate'); } return !!context; }; var DirectEditingModule = { __depends__: [ InteractionEventsModule$1 ], __init__: [ 'directEditing' ], directEditing: [ 'type', DirectEditing ] }; var MARKER_TYPES = [ 'marker-start', 'marker-mid', 'marker-end' ]; var NODES_CAN_HAVE_MARKER = [ 'circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect' ]; /** * Adds support for previews of moving/resizing elements. */ function PreviewSupport(elementRegistry, eventBus, canvas, styles) { this._elementRegistry = elementRegistry; this._canvas = canvas; this._styles = styles; this._clonedMarkers = {}; var self = this; eventBus.on('drag.cleanup', function() { forEach$1(self._clonedMarkers, function(clonedMarker) { remove$1(clonedMarker); }); self._clonedMarkers = {}; }); } PreviewSupport.$inject = [ 'elementRegistry', 'eventBus', 'canvas', 'styles' ]; /** * Returns graphics of an element. * * @param {djs.model.Base} element * * @return {SVGElement} */ PreviewSupport.prototype.getGfx = function(element) { return this._elementRegistry.getGraphics(element); }; /** * Adds a move preview of a given shape to a given svg group. * * @param {djs.model.Base} element * @param {SVGElement} group * @param {SVGElement} [gfx] * * @return {SVGElement} dragger */ PreviewSupport.prototype.addDragger = function(element, group, gfx) { gfx = gfx || this.getGfx(element); var dragger = clone$1(gfx); var bbox = gfx.getBoundingClientRect(); this._cloneMarkers(getVisual(dragger)); attr(dragger, this._styles.cls('djs-dragger', [], { x: bbox.top, y: bbox.left })); append(group, dragger); return dragger; }; /** * Adds a resize preview of a given shape to a given svg group. * * @param {djs.model.Base} element * @param {SVGElement} group * * @return {SVGElement} frame */ PreviewSupport.prototype.addFrame = function(shape, group) { var frame = create$1('rect', { class: 'djs-resize-overlay', width: shape.width, height: shape.height, x: shape.x, y: shape.y }); append(group, frame); return frame; }; /** * Clone all markers referenced by a node and its child nodes. * * @param {SVGElement} gfx */ PreviewSupport.prototype._cloneMarkers = function(gfx) { var self = this; if (gfx.childNodes) { // TODO: use forEach once we drop PhantomJS for (var i = 0; i < gfx.childNodes.length; i++) { // recursively clone markers of child nodes self._cloneMarkers(gfx.childNodes[ i ]); } } if (!canHaveMarker(gfx)) { return; } MARKER_TYPES.forEach(function(markerType) { if (attr(gfx, markerType)) { var marker = getMarker(gfx, markerType, self._canvas.getContainer()); self._cloneMarker(gfx, marker, markerType); } }); }; /** * Clone marker referenced by an element. * * @param {SVGElement} gfx * @param {SVGElement} marker * @param {string} markerType */ PreviewSupport.prototype._cloneMarker = function(gfx, marker, markerType) { var markerId = marker.id; var clonedMarker = this._clonedMarkers[ markerId ]; if (!clonedMarker) { clonedMarker = clone$1(marker); var clonedMarkerId = markerId + '-clone'; clonedMarker.id = clonedMarkerId; classes(clonedMarker) .add('djs-dragger') .add('djs-dragger-marker'); this._clonedMarkers[ markerId ] = clonedMarker; var defs = query('defs', this._canvas._svg); if (!defs) { defs = create$1('defs'); append(this._canvas._svg, defs); } append(defs, clonedMarker); } var reference = idToReference(this._clonedMarkers[ markerId ].id); attr(gfx, markerType, reference); }; // helpers ////////// /** * Get marker of given type referenced by node. * * @param {Node} node * @param {string} markerType * @param {Node} [parentNode] * * @param {Node} */ function getMarker(node, markerType, parentNode) { var id = referenceToId(attr(node, markerType)); return query('marker#' + id, parentNode || document); } /** * Get ID of fragment within current document from its functional IRI reference. * References may use single or double quotes. * * @param {string} reference * * @returns {string} */ function referenceToId(reference) { return reference.match(/url\(['"]?#([^'"]*)['"]?\)/)[1]; } /** * Get functional IRI reference for given ID of fragment within current document. * * @param {string} id * * @returns {string} */ function idToReference(id) { return 'url(#' + id + ')'; } /** * Check wether node type can have marker attributes. * * @param {Node} node * * @returns {boolean} */ function canHaveMarker(node) { return NODES_CAN_HAVE_MARKER.indexOf(node.nodeName) !== -1; } var PreviewSupportModule = { __init__: [ 'previewSupport' ], previewSupport: [ 'type', PreviewSupport ] }; var MARKER_OK$2 = 'drop-ok', MARKER_NOT_OK$2 = 'drop-not-ok', MARKER_ATTACH$2 = 'attach-ok', MARKER_NEW_PARENT$1 = 'new-parent'; var PREFIX = 'create'; var HIGH_PRIORITY$g = 2000; /** * Create new elements through drag and drop. * * @param {Canvas} canvas * @param {Dragging} dragging * @param {EventBus} eventBus * @param {Modeling} modeling * @param {Rules} rules */ function Create( canvas, dragging, eventBus, modeling, rules ) { // rules ////////// /** * Check wether elements can be created. * * @param {Array} elements * @param {djs.model.Base} target * @param {Point} position * @param {djs.model.Base} [source] * * @returns {boolean|null|Object} */ function canCreate(elements, target, position, source, hints) { if (!target) { return false; } // ignore child elements and external labels elements = filter(elements, function(element) { var labelTarget = element.labelTarget; return !element.parent && !(isLabel$5(element) && elements.indexOf(labelTarget) !== -1); }); var shape = find(elements, function(element) { return !isConnection$c(element); }); var attach = false, connect = false, create = false; // (1) attaching single shapes if (isSingleShape(elements)) { attach = rules.allowed('shape.attach', { position: position, shape: shape, target: target }); } if (!attach) { // (2) creating elements if (isSingleShape(elements)) { create = rules.allowed('shape.create', { position: position, shape: shape, source: source, target: target }); } else { create = rules.allowed('elements.create', { elements: elements, position: position, target: target }); } } var connectionTarget = hints.connectionTarget; // (3) appending single shapes if (create || attach) { if (shape && source) { connect = rules.allowed('connection.create', { source: connectionTarget === source ? shape : source, target: connectionTarget === source ? source : shape, hints: { targetParent: target, targetAttach: attach } }); } return { attach: attach, connect: connect }; } // ignore wether or not elements can be created if (create === null || attach === null) { return null; } return false; } function setMarker(element, marker) { [ MARKER_ATTACH$2, MARKER_OK$2, MARKER_NOT_OK$2, MARKER_NEW_PARENT$1 ].forEach(function(m) { if (m === marker) { canvas.addMarker(element, m); } else { canvas.removeMarker(element, m); } }); } // event handling ////////// eventBus.on([ 'create.move', 'create.hover' ], function(event) { var context = event.context, elements = context.elements, hover = event.hover, source = context.source, hints = context.hints || {}; if (!hover) { context.canExecute = false; context.target = null; return; } ensureConstraints$2(event); var position = { x: event.x, y: event.y }; var canExecute = context.canExecute = hover && canCreate(elements, hover, position, source, hints); if (hover && canExecute !== null) { context.target = hover; if (canExecute && canExecute.attach) { setMarker(hover, MARKER_ATTACH$2); } else { setMarker(hover, canExecute ? MARKER_NEW_PARENT$1 : MARKER_NOT_OK$2); } } }); eventBus.on([ 'create.end', 'create.out', 'create.cleanup' ], function(event) { var hover = event.hover; if (hover) { setMarker(hover, null); } }); eventBus.on('create.end', function(event) { var context = event.context, source = context.source, shape = context.shape, elements = context.elements, target = context.target, canExecute = context.canExecute, attach = canExecute && canExecute.attach, connect = canExecute && canExecute.connect, hints = context.hints || {}; if (canExecute === false || !target) { return false; } ensureConstraints$2(event); var position = { x: event.x, y: event.y }; if (connect) { shape = modeling.appendShape(source, shape, position, target, { attach: attach, connection: connect === true ? {} : connect, connectionTarget: hints.connectionTarget }); } else { elements = modeling.createElements(elements, position, target, assign({}, hints, { attach: attach })); // update shape shape = find(elements, function(element) { return !isConnection$c(element); }); } // update elements and shape assign(context, { elements: elements, shape: shape }); assign(event, { elements: elements, shape: shape }); }); function cancel() { var context = dragging.context(); if (context && context.prefix === PREFIX) { dragging.cancel(); } } // cancel on that is not result of eventBus.on('create.init', function() { eventBus.on('elements.changed', cancel); eventBus.once([ 'create.cancel', 'create.end' ], HIGH_PRIORITY$g, function() { eventBus.off('elements.changed', cancel); }); }); // API ////////// this.start = function(event, elements, context) { if (!isArray$3(elements)) { elements = [ elements ]; } var shape = find(elements, function(element) { return !isConnection$c(element); }); if (!shape) { // at least one shape is required return; } context = assign({ elements: elements, hints: {}, shape: shape }, context || {}); // make sure each element has x and y forEach$1(elements, function(element) { if (!isNumber(element.x)) { element.x = 0; } if (!isNumber(element.y)) { element.y = 0; } }); var visibleElements = filter(elements, function(element) { return !element.hidden; }); var bbox = getBBox(visibleElements); // center elements around cursor forEach$1(elements, function(element) { if (isConnection$c(element)) { element.waypoints = map(element.waypoints, function(waypoint) { return { x: waypoint.x - bbox.x - bbox.width / 2, y: waypoint.y - bbox.y - bbox.height / 2 }; }); } assign(element, { x: element.x - bbox.x - bbox.width / 2, y: element.y - bbox.y - bbox.height / 2 }); }); dragging.init(event, PREFIX, { cursor: 'grabbing', autoActivate: true, data: { shape: shape, elements: elements, context: context } }); }; } Create.$inject = [ 'canvas', 'dragging', 'eventBus', 'modeling', 'rules' ]; // helpers ////////// function ensureConstraints$2(event) { var context = event.context, createConstraints = context.createConstraints; if (!createConstraints) { return; } if (createConstraints.left) { event.x = Math.max(event.x, createConstraints.left); } if (createConstraints.right) { event.x = Math.min(event.x, createConstraints.right); } if (createConstraints.top) { event.y = Math.max(event.y, createConstraints.top); } if (createConstraints.bottom) { event.y = Math.min(event.y, createConstraints.bottom); } } function isConnection$c(element) { return !!element.waypoints; } function isSingleShape(elements) { return elements && elements.length === 1 && !isConnection$c(elements[0]); } function isLabel$5(element) { return !!element.labelTarget; } var LOW_PRIORITY$i = 750; function CreatePreview( canvas, eventBus, graphicsFactory, previewSupport, styles ) { function createDragGroup(elements) { var dragGroup = create$1('g'); attr(dragGroup, styles.cls('djs-drag-group', [ 'no-events' ])); var childrenGfx = create$1('g'); elements.forEach(function(element) { // create graphics var gfx; if (element.hidden) { return; } if (element.waypoints) { gfx = graphicsFactory._createContainer('connection', childrenGfx); graphicsFactory.drawConnection(getVisual(gfx), element); } else { gfx = graphicsFactory._createContainer('shape', childrenGfx); graphicsFactory.drawShape(getVisual(gfx), element); translate$2(gfx, element.x, element.y); } // add preview previewSupport.addDragger(element, dragGroup, gfx); }); return dragGroup; } eventBus.on('create.move', LOW_PRIORITY$i, function(event) { var hover = event.hover, context = event.context, elements = context.elements, dragGroup = context.dragGroup; // lazily create previews if (!dragGroup) { dragGroup = context.dragGroup = createDragGroup(elements); } var activeLayer; if (hover) { if (!dragGroup.parentNode) { activeLayer = canvas.getActiveLayer(); append(activeLayer, dragGroup); } translate$2(dragGroup, event.x, event.y); } else { remove$1(dragGroup); } }); eventBus.on('create.cleanup', function(event) { var context = event.context, dragGroup = context.dragGroup; if (dragGroup) { remove$1(dragGroup); } }); } CreatePreview.$inject = [ 'canvas', 'eventBus', 'graphicsFactory', 'previewSupport', 'styles' ]; var CreateModule = { __depends__: [ DraggingModule, PreviewSupportModule, RulesModule$1, SelectionModule ], __init__: [ 'create', 'createPreview' ], create: [ 'type', Create ], createPreview: [ 'type', CreatePreview ] }; /** * A clip board stub */ function Clipboard() {} Clipboard.prototype.get = function() { return this._data; }; Clipboard.prototype.set = function(data) { this._data = data; }; Clipboard.prototype.clear = function() { var data = this._data; delete this._data; return data; }; Clipboard.prototype.isEmpty = function() { return !this._data; }; var ClipboardModule = { clipboard: [ 'type', Clipboard ] }; function Mouse(eventBus) { var self = this; this._lastMoveEvent = null; function setLastMoveEvent(mousemoveEvent) { self._lastMoveEvent = mousemoveEvent; } eventBus.on('canvas.init', function(context) { var svg = self._svg = context.svg; svg.addEventListener('mousemove', setLastMoveEvent); }); eventBus.on('canvas.destroy', function() { self._lastMouseEvent = null; self._svg.removeEventListener('mousemove', setLastMoveEvent); }); } Mouse.$inject = [ 'eventBus' ]; Mouse.prototype.getLastMoveEvent = function() { return this._lastMoveEvent || createMoveEvent(0, 0); }; // helpers ////////// function createMoveEvent(x, y) { var event = document.createEvent('MouseEvent'); var screenX = x, screenY = y, clientX = x, clientY = y; if (event.initMouseEvent) { event.initMouseEvent( 'mousemove', true, true, window, 0, screenX, screenY, clientX, clientY, false, false, false, false, 0, null ); } return event; } var MouseModule = { __init__: [ 'mouse' ], mouse: [ 'type', Mouse ] }; /** * @typedef {Function} listener * * @param {Object} context * @param {Array} context.elements * * @returns {Array|boolean} - Return elements to be copied or false to disallow * copying. */ /** * @typedef {Function} listener * * @param {Object} context * @param {Object} context.descriptor * @param {djs.model.Base} context.element * @param {Array} context.elements */ /** * @typedef {Function} listener * * @param {Object} context * @param {djs.model.Base} context.element * @param {Array} context.children - Add children to be added to tree. */ /** * @typedef {Function} listener * * @param {Object} context * @param {Object} context.elements * @param {Object} context.tree */ /** * @typedef {Function} listener * * @param {Object} context * @param {Object} context.cache - Already created elements. * @param {Object} context.descriptor */ /** * @typedef {Function} listener * * @param {Object} context * @param {Object} context.hints - Add hints before pasting. */ /** * Copy and paste elements. * * @param {Canvas} canvas * @param {Create} create * @param {Clipboard} clipboard * @param {ElementFactory} elementFactory * @param {EventBus} eventBus * @param {Modeling} modeling * @param {Mouse} mouse * @param {Rules} rules */ function CopyPaste( canvas, create, clipboard, elementFactory, eventBus, modeling, mouse, rules ) { this._canvas = canvas; this._create = create; this._clipboard = clipboard; this._elementFactory = elementFactory; this._eventBus = eventBus; this._modeling = modeling; this._mouse = mouse; this._rules = rules; eventBus.on('copyPaste.copyElement', function(context) { var descriptor = context.descriptor, element = context.element, elements = context.elements; // default priority (priority = 1) descriptor.priority = 1; descriptor.id = element.id; var parentCopied = find(elements, function(e) { return e === element.parent; }); // do NOT reference parent if parent wasn't copied if (parentCopied) { descriptor.parent = element.parent.id; } // attachers (priority = 2) if (isAttacher$1(element)) { descriptor.priority = 2; descriptor.host = element.host.id; } // connections (priority = 3) if (isConnection$b(element)) { descriptor.priority = 3; descriptor.source = element.source.id; descriptor.target = element.target.id; descriptor.waypoints = copyWaypoints$1(element); } // labels (priority = 4) if (isLabel$4(element)) { descriptor.priority = 4; descriptor.labelTarget = element.labelTarget.id; } forEach$1([ 'x', 'y', 'width', 'height' ], function(property) { if (isNumber(element[ property ])) { descriptor[ property ] = element[ property ]; } }); descriptor.hidden = element.hidden; descriptor.collapsed = element.collapsed; }); eventBus.on('copyPaste.pasteElements', function(context) { var hints = context.hints; assign(hints, { createElementsBehavior: false }); }); } CopyPaste.$inject = [ 'canvas', 'create', 'clipboard', 'elementFactory', 'eventBus', 'modeling', 'mouse', 'rules' ]; /** * Copy elements. * * @param {Array} elements * * @returns {Object} */ CopyPaste.prototype.copy = function(elements) { var allowed, tree; if (!isArray$3(elements)) { elements = elements ? [ elements ] : []; } allowed = this._eventBus.fire('copyPaste.canCopyElements', { elements: elements }); if (allowed === false) { tree = {}; } else { tree = this.createTree(isArray$3(allowed) ? allowed : elements); } // we set an empty tree, selection of elements // to copy was empty. this._clipboard.set(tree); this._eventBus.fire('copyPaste.elementsCopied', { elements: elements, tree: tree }); return tree; }; /** * Paste elements. * * @param {Object} [context] * @param {djs.model.base} [context.element] - Parent. * @param {Point} [context.point] - Position. * @param {Object} [context.hints] - Hints. */ CopyPaste.prototype.paste = function(context) { var tree = this._clipboard.get(); if (this._clipboard.isEmpty()) { return; } var hints = context && context.hints || {}; this._eventBus.fire('copyPaste.pasteElements', { hints: hints }); var elements = this._createElements(tree); // paste directly if (context && context.element && context.point) { return this._paste(elements, context.element, context.point, hints); } this._create.start(this._mouse.getLastMoveEvent(), elements, { hints: hints || {} }); }; /** * Paste elements directly. * * @param {Array} elements * @param {djs.model.base} target * @param {Point} position * @param {Object} [hints] */ CopyPaste.prototype._paste = function(elements, target, position, hints) { // make sure each element has x and y forEach$1(elements, function(element) { if (!isNumber(element.x)) { element.x = 0; } if (!isNumber(element.y)) { element.y = 0; } }); var bbox = getBBox(elements); // center elements around cursor forEach$1(elements, function(element) { if (isConnection$b(element)) { element.waypoints = map(element.waypoints, function(waypoint) { return { x: waypoint.x - bbox.x - bbox.width / 2, y: waypoint.y - bbox.y - bbox.height / 2 }; }); } assign(element, { x: element.x - bbox.x - bbox.width / 2, y: element.y - bbox.y - bbox.height / 2 }); }); return this._modeling.createElements(elements, position, target, assign({}, hints)); }; /** * Create elements from tree. */ CopyPaste.prototype._createElements = function(tree) { var self = this; var eventBus = this._eventBus; var cache = {}; var elements = []; forEach$1(tree, function(branch, depth) { // sort by priority branch = sortBy(branch, 'priority'); forEach$1(branch, function(descriptor) { // remove priority var attrs = assign({}, omit(descriptor, [ 'priority' ])); if (cache[ descriptor.parent ]) { attrs.parent = cache[ descriptor.parent ]; } else { delete attrs.parent; } eventBus.fire('copyPaste.pasteElement', { cache: cache, descriptor: attrs }); var element; if (isConnection$b(attrs)) { attrs.source = cache[ descriptor.source ]; attrs.target = cache[ descriptor.target ]; element = cache[ descriptor.id ] = self.createConnection(attrs); elements.push(element); return; } if (isLabel$4(attrs)) { attrs.labelTarget = cache[ attrs.labelTarget ]; element = cache[ descriptor.id ] = self.createLabel(attrs); elements.push(element); return; } if (attrs.host) { attrs.host = cache[ attrs.host ]; } element = cache[ descriptor.id ] = self.createShape(attrs); elements.push(element); }); }); return elements; }; CopyPaste.prototype.createConnection = function(attrs) { var connection = this._elementFactory.createConnection(omit(attrs, [ 'id' ])); return connection; }; CopyPaste.prototype.createLabel = function(attrs) { var label = this._elementFactory.createLabel(omit(attrs, [ 'id' ])); return label; }; CopyPaste.prototype.createShape = function(attrs) { var shape = this._elementFactory.createShape(omit(attrs, [ 'id' ])); return shape; }; /** * Check wether element has relations to other elements e.g. attachers, labels and connections. * * @param {Object} element * @param {Array} elements * * @returns {boolean} */ CopyPaste.prototype.hasRelations = function(element, elements) { var labelTarget, source, target; if (isConnection$b(element)) { source = find(elements, matchPattern({ id: element.source.id })); target = find(elements, matchPattern({ id: element.target.id })); if (!source || !target) { return false; } } if (isLabel$4(element)) { labelTarget = find(elements, matchPattern({ id: element.labelTarget.id })); if (!labelTarget) { return false; } } return true; }; /** * Create a tree-like structure from elements. * * @example * tree: { * 0: [ * { id: 'Shape_1', priority: 1, ... }, * { id: 'Shape_2', priority: 1, ... }, * { id: 'Connection_1', source: 'Shape_1', target: 'Shape_2', priority: 3, ... }, * ... * ], * 1: [ * { id: 'Shape_3', parent: 'Shape1', priority: 1, ... }, * ... * ] * }; * * @param {Array} elements * * @return {Object} */ CopyPaste.prototype.createTree = function(elements) { var rules = this._rules, self = this; var tree = {}, elementsData = []; var parents = getParents$1(elements); function canCopy(element, elements) { return rules.allowed('element.copy', { element: element, elements: elements }); } function addElementData(element, depth) { // (1) check wether element has already been added var foundElementData = find(elementsData, function(elementsData) { return element === elementsData.element; }); // (2) add element if not already added if (!foundElementData) { elementsData.push({ element: element, depth: depth }); return; } // (3) update depth if (foundElementData.depth < depth) { elementsData = removeElementData(foundElementData, elementsData); elementsData.push({ element: foundElementData.element, depth: depth }); } } function removeElementData(elementData, elementsData) { var index = elementsData.indexOf(elementData); if (index !== -1) { elementsData.splice(index, 1); } return elementsData; } // (1) add elements eachElement(parents, function(element, _index, depth) { // do NOT add external labels directly if (isLabel$4(element)) { return; } // always copy external labels forEach$1(element.labels, function(label) { addElementData(label, depth); }); function addRelatedElements(elements) { elements && elements.length && forEach$1(elements, function(element) { // add external labels forEach$1(element.labels, function(label) { addElementData(label, depth); }); addElementData(element, depth); }); } forEach$1([ element.attachers, element.incoming, element.outgoing ], addRelatedElements); addElementData(element, depth); var children = []; if (element.children) { children = element.children.slice(); } // allow others to add children to tree self._eventBus.fire('copyPaste.createTree', { element: element, children: children }); return children; }); elements = map(elementsData, function(elementData) { return elementData.element; }); // (2) copy elements elementsData = map(elementsData, function(elementData) { elementData.descriptor = {}; self._eventBus.fire('copyPaste.copyElement', { descriptor: elementData.descriptor, element: elementData.element, elements: elements }); return elementData; }); // (3) sort elements by priority elementsData = sortBy(elementsData, function(elementData) { return elementData.descriptor.priority; }); elements = map(elementsData, function(elementData) { return elementData.element; }); // (4) create tree forEach$1(elementsData, function(elementData) { var depth = elementData.depth; if (!self.hasRelations(elementData.element, elements)) { removeElement(elementData.element, elements); return; } if (!canCopy(elementData.element, elements)) { removeElement(elementData.element, elements); return; } if (!tree[depth]) { tree[depth] = []; } tree[depth].push(elementData.descriptor); }); return tree; }; // helpers ////////// function isAttacher$1(element) { return !!element.host; } function isConnection$b(element) { return !!element.waypoints; } function isLabel$4(element) { return !!element.labelTarget; } function copyWaypoints$1(element) { return map(element.waypoints, function(waypoint) { waypoint = copyWaypoint$1(waypoint); if (waypoint.original) { waypoint.original = copyWaypoint$1(waypoint.original); } return waypoint; }); } function copyWaypoint$1(waypoint) { return assign({}, waypoint); } function removeElement(element, elements) { var index = elements.indexOf(element); if (index === -1) { return elements; } return elements.splice(index, 1); } var CopyPasteModule$1 = { __depends__: [ ClipboardModule, CreateModule, MouseModule, RulesModule$1 ], __init__: [ 'copyPaste' ], copyPaste: [ 'type', CopyPaste ] }; function copyProperties$1(source, target, properties) { if (!isArray$3(properties)) { properties = [ properties ]; } forEach$1(properties, function(property) { if (!isUndefined$2(source[property])) { target[property] = source[property]; } }); } var LOW_PRIORITY$h = 750; function BpmnCopyPaste(bpmnFactory, eventBus, moddleCopy) { function copy(bo, clone) { var targetBo = bpmnFactory.create(bo.$type); return moddleCopy.copyElement(bo, targetBo, null, clone); } eventBus.on('copyPaste.copyElement', LOW_PRIORITY$h, function(context) { var descriptor = context.descriptor, element = context.element, businessObject = getBusinessObject(element); // do not copy business object + di for labels; // will be pulled from the referenced label target if (isLabel$3(element)) { return descriptor; } var businessObjectCopy = descriptor.businessObject = copy(businessObject, true); var diCopy = descriptor.di = copy(getDi(element), true); diCopy.bpmnElement = businessObjectCopy; copyProperties$1(businessObjectCopy, descriptor, 'name'); copyProperties$1(diCopy, descriptor, 'isExpanded'); // default sequence flow if (businessObject.default) { descriptor.default = businessObject.default.id; } }); var referencesKey = '-bpmn-js-refs'; function getReferences(cache) { return (cache[referencesKey] = cache[referencesKey] || {}); } function setReferences(cache, references) { cache[referencesKey] = references; } function resolveReferences(descriptor, cache, references) { var businessObject = getBusinessObject(descriptor); // default sequence flows if (descriptor.default) { // relationship cannot be resolved immediately references[ descriptor.default ] = { element: businessObject, property: 'default' }; } // boundary events if (descriptor.host) { // relationship can be resolved immediately getBusinessObject(descriptor).attachedToRef = getBusinessObject(cache[ descriptor.host ]); } return omit(references, reduce(references, function(array, reference, key) { var element = reference.element, property = reference.property; if (key === descriptor.id) { element[ property ] = businessObject; array.push(descriptor.id); } return array; }, [])); } eventBus.on('copyPaste.pasteElement', function(context) { var cache = context.cache, descriptor = context.descriptor, businessObject = descriptor.businessObject, di = descriptor.di; // wire existing di + businessObject for external label if (isLabel$3(descriptor)) { descriptor.businessObject = getBusinessObject(cache[ descriptor.labelTarget ]); descriptor.di = getDi(cache[ descriptor.labelTarget ]); return; } businessObject = descriptor.businessObject = copy(businessObject); di = descriptor.di = copy(di); di.bpmnElement = businessObject; copyProperties$1(descriptor, businessObject, [ 'isExpanded', 'name' ]); descriptor.type = businessObject.$type; }); // copy + paste processRef with participant eventBus.on('copyPaste.copyElement', LOW_PRIORITY$h, function(context) { var descriptor = context.descriptor, element = context.element; if (!is$1(element, 'bpmn:Participant')) { return; } var participantBo = getBusinessObject(element); if (participantBo.processRef) { descriptor.processRef = copy(participantBo.processRef, true); } }); eventBus.on('copyPaste.pasteElement', function(context) { var descriptor = context.descriptor, processRef = descriptor.processRef; if (processRef) { descriptor.processRef = copy(processRef); } }); // resolve references eventBus.on('copyPaste.pasteElement', LOW_PRIORITY$h, function(context) { var cache = context.cache, descriptor = context.descriptor; // resolve references e.g. default sequence flow setReferences( cache, resolveReferences(descriptor, cache, getReferences(cache)) ); }); } BpmnCopyPaste.$inject = [ 'bpmnFactory', 'eventBus', 'moddleCopy' ]; // helpers ////////// function isLabel$3(element) { return !!element.labelTarget; } var DISALLOWED_PROPERTIES = [ 'artifacts', 'dataInputAssociations', 'dataOutputAssociations', 'default', 'flowElements', 'lanes', 'incoming', 'outgoing', 'categoryValue' ]; /** * @typedef {Function} listener * * @param {Object} context * @param {Array} context.propertyNames * @param {ModdleElement} context.sourceElement * @param {ModdleElement} context.targetElement * * @returns {Array|boolean} - Return properties to be copied or false to disallow * copying. */ /** * @typedef {Function} listener * * @param {Object} context * @param {ModdleElement} context.parent * @param {*} context.property * @param {string} context.propertyName * * @returns {*|boolean} - Return copied property or false to disallow * copying. */ /** * @typedef {Function} listener * * @param {Object} context * @param {ModdleElement} context.parent * @param {*} context.property * @param {string} context.propertyName * * @returns {boolean} - Return false to disallow * setting copied property. */ /** * Utility for copying model properties from source element to target element. * * @param {EventBus} eventBus * @param {BpmnFactory} bpmnFactory * @param {BpmnModdle} moddle */ function ModdleCopy(eventBus, bpmnFactory, moddle) { this._bpmnFactory = bpmnFactory; this._eventBus = eventBus; this._moddle = moddle; // copy extension elements last eventBus.on('moddleCopy.canCopyProperties', function(context) { var propertyNames = context.propertyNames; if (!propertyNames || !propertyNames.length) { return; } return sortBy(propertyNames, function(propertyName) { return propertyName === 'extensionElements'; }); }); // default check whether property can be copied eventBus.on('moddleCopy.canCopyProperty', function(context) { var parent = context.parent, parentDescriptor = isObject(parent) && parent.$descriptor, propertyName = context.propertyName; if (propertyName && DISALLOWED_PROPERTIES.indexOf(propertyName) !== -1) { // disallow copying property return false; } if (propertyName && parentDescriptor && !find(parentDescriptor.properties, matchPattern({ name: propertyName }))) { // disallow copying property return false; } }); // do NOT allow to copy empty extension elements eventBus.on('moddleCopy.canSetCopiedProperty', function(context) { var property = context.property; if (is(property, 'bpmn:ExtensionElements') && (!property.values || !property.values.length)) { // disallow setting copied property return false; } }); } ModdleCopy.$inject = [ 'eventBus', 'bpmnFactory', 'moddle' ]; /** * Copy model properties of source element to target element. * * @param {ModdleElement} sourceElement * @param {ModdleElement} targetElement * @param {Array} [propertyNames] * @param {boolean} clone * * @param {ModdleElement} */ ModdleCopy.prototype.copyElement = function(sourceElement, targetElement, propertyNames, clone) { var self = this; if (propertyNames && !isArray$3(propertyNames)) { propertyNames = [ propertyNames ]; } propertyNames = propertyNames || getPropertyNames(sourceElement.$descriptor); var canCopyProperties = this._eventBus.fire('moddleCopy.canCopyProperties', { propertyNames: propertyNames, sourceElement: sourceElement, targetElement: targetElement, clone: clone }); if (canCopyProperties === false) { return targetElement; } if (isArray$3(canCopyProperties)) { propertyNames = canCopyProperties; } // copy properties forEach$1(propertyNames, function(propertyName) { var sourceProperty; if (has$1(sourceElement, propertyName)) { sourceProperty = sourceElement.get(propertyName); } var copiedProperty = self.copyProperty(sourceProperty, targetElement, propertyName, clone); if (!isDefined(copiedProperty)) { return; } var canSetProperty = self._eventBus.fire('moddleCopy.canSetCopiedProperty', { parent: targetElement, property: copiedProperty, propertyName: propertyName }); if (canSetProperty === false) { return; } // TODO(nikku): unclaim old IDs if ID property is copied over // this._moddle.getPropertyDescriptor(parent, propertyName) targetElement.set(propertyName, copiedProperty); }); return targetElement; }; /** * Copy model property. * * @param {*} property * @param {ModdleElement} parent * @param {string} propertyName * @param {boolean} clone * * @returns {*} */ ModdleCopy.prototype.copyProperty = function(property, parent, propertyName, clone) { var self = this; // allow others to copy property var copiedProperty = this._eventBus.fire('moddleCopy.canCopyProperty', { parent: parent, property: property, propertyName: propertyName, clone: clone }); // return if copying is NOT allowed if (copiedProperty === false) { return; } if (copiedProperty) { if (isObject(copiedProperty) && copiedProperty.$type && !copiedProperty.$parent) { copiedProperty.$parent = parent; } return copiedProperty; } var propertyDescriptor = this._moddle.getPropertyDescriptor(parent, propertyName); // do NOT copy references if (propertyDescriptor.isReference) { return; } // copy id if (propertyDescriptor.isId) { return property && this._copyId(property, parent, clone); } // copy arrays if (isArray$3(property)) { return reduce(property, function(childProperties, childProperty) { // recursion copiedProperty = self.copyProperty(childProperty, parent, propertyName, clone); // copying might NOT be allowed if (copiedProperty) { return childProperties.concat(copiedProperty); } return childProperties; }, []); } // copy model elements if (isObject(property) && property.$type) { if (this._moddle.getElementDescriptor(property).isGeneric) { return; } copiedProperty = self._bpmnFactory.create(property.$type); copiedProperty.$parent = parent; // recursion copiedProperty = self.copyElement(property, copiedProperty, null, clone); return copiedProperty; } // copy primitive properties return property; }; ModdleCopy.prototype._copyId = function(id, element, clone) { if (clone) { return id; } // disallow if already taken if (this._moddle.ids.assigned(id)) { return; } else { this._moddle.ids.claim(id, element); return id; } }; // helpers ////////// function getPropertyNames(descriptor, keepDefaultProperties) { return reduce(descriptor.properties, function(properties, property) { if (keepDefaultProperties && property.default) { return properties; } return properties.concat(property.name); }, []); } function is(element, type) { return element && (typeof element.$instanceOf === 'function') && element.$instanceOf(type); } var CopyPasteModule = { __depends__: [ CopyPasteModule$1 ], __init__: [ 'bpmnCopyPaste', 'moddleCopy' ], bpmnCopyPaste: [ 'type', BpmnCopyPaste ], moddleCopy: [ 'type', ModdleCopy ] }; var round$5 = Math.round; /** * Service that allow replacing of elements. */ function Replace(modeling) { this._modeling = modeling; } Replace.$inject = [ 'modeling' ]; /** * @param {Element} oldElement - Element to be replaced * @param {Object} newElementData - Containing information about the new element, * for example the new bounds and type. * @param {Object} options - Custom options that will be attached to the context. It can be used to inject data * that is needed in the command chain. For example it could be used in * eventbus.on('commandStack.shape.replace.postExecute') to change shape attributes after * shape creation. */ Replace.prototype.replaceElement = function(oldElement, newElementData, options) { if (oldElement.waypoints) { // TODO(nikku): we do not replace connections, yet return null; } var modeling = this._modeling; var width = newElementData.width || oldElement.width, height = newElementData.height || oldElement.height, x = newElementData.x || oldElement.x, y = newElementData.y || oldElement.y, centerX = round$5(x + width / 2), centerY = round$5(y + height / 2); // modeling API requires center coordinates, // account for that when handling shape bounds return modeling.replaceShape( oldElement, assign( {}, newElementData, { x: centerX, y: centerY, width: width, height: height } ), options ); }; var ReplaceModule$1 = { __init__: [ 'replace' ], replace: [ 'type', Replace ] }; function copyProperties(source, target, properties) { if (!isArray$3(properties)) { properties = [ properties ]; } forEach$1(properties, function(property) { if (!isUndefined$2(source[property])) { target[property] = source[property]; } }); } var CUSTOM_PROPERTIES = [ 'cancelActivity', 'instantiate', 'eventGatewayType', 'triggeredByEvent', 'isInterrupting' ]; /** * Check if element should be collapsed or expanded. */ function shouldToggleCollapsed(element, targetElement) { var oldCollapsed = ( element && has$1(element, 'collapsed') ? element.collapsed : !isExpanded(element) ); var targetCollapsed; if (targetElement && (has$1(targetElement, 'collapsed') || has$1(targetElement, 'isExpanded'))) { // property is explicitly set so use it targetCollapsed = ( has$1(targetElement, 'collapsed') ? targetElement.collapsed : !targetElement.isExpanded ); } else { // keep old state targetCollapsed = oldCollapsed; } if (oldCollapsed !== targetCollapsed) { return true; } return false; } /** * This module takes care of replacing BPMN elements */ function BpmnReplace( bpmnFactory, elementFactory, moddleCopy, modeling, replace, rules, selection ) { /** * Prepares a new business object for the replacement element * and triggers the replace operation. * * @param {djs.model.Base} element * @param {Object} target * @param {Object} [hints] * * @return {djs.model.Base} the newly created element */ function replaceElement(element, target, hints) { hints = hints || {}; var type = target.type, oldBusinessObject = element.businessObject; if (isSubProcess(oldBusinessObject) && type === 'bpmn:SubProcess') { if (shouldToggleCollapsed(element, target)) { // expanding or collapsing process modeling.toggleCollapse(element); return element; } } var newBusinessObject = bpmnFactory.create(type); var newElement = { type: type, businessObject: newBusinessObject, }; newElement.di = {}; // colors will be set to DI copyProperties(element.di, newElement.di, [ 'fill', 'stroke', 'background-color', 'border-color', 'color' ]); var elementProps = getPropertyNames(oldBusinessObject.$descriptor), newElementProps = getPropertyNames(newBusinessObject.$descriptor, true), copyProps = intersection(elementProps, newElementProps); // initialize special properties defined in target definition assign(newBusinessObject, pick(target, CUSTOM_PROPERTIES)); var properties = filter(copyProps, function(propertyName) { // copying event definitions, unless we replace if (propertyName === 'eventDefinitions') { return hasEventDefinition$1(element, target.eventDefinitionType); } // retain loop characteristics if the target element // is not an event sub process if (propertyName === 'loopCharacteristics') { return !isEventSubProcess(newBusinessObject); } // so the applied properties from 'target' don't get lost if (has$1(newBusinessObject, propertyName)) { return false; } if (propertyName === 'processRef' && target.isExpanded === false) { return false; } if (propertyName === 'triggeredByEvent') { return false; } return true; }); newBusinessObject = moddleCopy.copyElement( oldBusinessObject, newBusinessObject, properties ); // initialize custom BPMN extensions if (target.eventDefinitionType) { // only initialize with new eventDefinition // if we did not set an event definition yet, // i.e. because we copied it if (!hasEventDefinition$1(newBusinessObject, target.eventDefinitionType)) { newElement.eventDefinitionType = target.eventDefinitionType; newElement.eventDefinitionAttrs = target.eventDefinitionAttrs; } } if (is$1(oldBusinessObject, 'bpmn:Activity')) { if (isSubProcess(oldBusinessObject)) { // no toggeling, so keep old state newElement.isExpanded = isExpanded(element); } // else if property is explicitly set, use it else if (target && has$1(target, 'isExpanded')) { newElement.isExpanded = target.isExpanded; // assign default size of new expanded element var defaultSize = elementFactory.getDefaultSize(newBusinessObject, { isExpanded: newElement.isExpanded }); newElement.width = defaultSize.width; newElement.height = defaultSize.height; // keep element centered newElement.x = element.x - (newElement.width - element.width) / 2; newElement.y = element.y - (newElement.height - element.height) / 2; } // TODO: need also to respect min/max Size // copy size, from an expanded subprocess to an expanded alternative subprocess // except bpmn:Task, because Task is always expanded if ((isExpanded(element) && !is$1(oldBusinessObject, 'bpmn:Task')) && newElement.isExpanded) { newElement.width = element.width; newElement.height = element.height; } } // remove children if not expanding sub process if (isSubProcess(oldBusinessObject) && !isSubProcess(newBusinessObject)) { hints.moveChildren = false; } // transform collapsed/expanded pools if (is$1(oldBusinessObject, 'bpmn:Participant')) { // create expanded pool if (target.isExpanded === true) { newBusinessObject.processRef = bpmnFactory.create('bpmn:Process'); } else { // remove children when transforming to collapsed pool hints.moveChildren = false; } // apply same width and default height newElement.width = element.width; newElement.height = elementFactory.getDefaultSize(newElement).height; } if (!rules.allowed('shape.resize', { shape: newBusinessObject })) { newElement.height = elementFactory.getDefaultSize(newElement).height; newElement.width = elementFactory.getDefaultSize(newElement).width; } newBusinessObject.name = oldBusinessObject.name; // retain default flow's reference between inclusive <-> exclusive gateways and activities if ( isAny(oldBusinessObject, [ 'bpmn:ExclusiveGateway', 'bpmn:InclusiveGateway', 'bpmn:Activity' ]) && isAny(newBusinessObject, [ 'bpmn:ExclusiveGateway', 'bpmn:InclusiveGateway', 'bpmn:Activity' ]) ) { newBusinessObject.default = oldBusinessObject.default; } if ( target.host && !is$1(oldBusinessObject, 'bpmn:BoundaryEvent') && is$1(newBusinessObject, 'bpmn:BoundaryEvent') ) { newElement.host = target.host; } // The DataStoreReference element is 14px wider than the DataObjectReference element // This ensures that they stay centered on the x axis when replaced if ( newElement.type === 'bpmn:DataStoreReference' || newElement.type === 'bpmn:DataObjectReference' ) { newElement.x = element.x + (element.width - newElement.width) / 2; } newElement = replace.replaceElement(element, newElement, hints); if (hints.select !== false) { selection.select(newElement); } return newElement; } this.replaceElement = replaceElement; } BpmnReplace.$inject = [ 'bpmnFactory', 'elementFactory', 'moddleCopy', 'modeling', 'replace', 'rules', 'selection' ]; function isSubProcess(bo) { return is$1(bo, 'bpmn:SubProcess'); } function hasEventDefinition$1(element, type) { var bo = getBusinessObject(element); return type && bo.get('eventDefinitions').some(function(definition) { return is$1(definition, type); }); } /** * Compute intersection between two arrays. */ function intersection(a1, a2) { return a1.filter(function(el) { return a2.indexOf(el) !== -1; }); } var ReplaceModule = { __depends__: [ CopyPasteModule, ReplaceModule$1, SelectionModule ], bpmnReplace: [ 'type', BpmnReplace ] }; /** * Returns true, if an element is from a different type * than a target definition. Takes into account the type, * event definition type and triggeredByEvent property. * * @param {djs.model.Base} element * * @return {boolean} */ function isDifferentType(element) { return function(entry) { var target = entry.target; var businessObject = getBusinessObject(element), eventDefinition = businessObject.eventDefinitions && businessObject.eventDefinitions[0]; var isTypeEqual = businessObject.$type === target.type; var isEventDefinitionEqual = ( (eventDefinition && eventDefinition.$type) === target.eventDefinitionType ); var isTriggeredByEventEqual = ( businessObject.triggeredByEvent === target.triggeredByEvent ); var isExpandedEqual = ( target.isExpanded === undefined || target.isExpanded === isExpanded(element) ); return !isTypeEqual || !isEventDefinitionEqual || !isTriggeredByEventEqual || !isExpandedEqual; }; } var START_EVENT = [ { label: 'Start Event', actionName: 'replace-with-none-start', className: 'bpmn-icon-start-event-none', target: { type: 'bpmn:StartEvent' } }, { label: 'Intermediate Throw Event', actionName: 'replace-with-none-intermediate-throwing', className: 'bpmn-icon-intermediate-event-none', target: { type: 'bpmn:IntermediateThrowEvent' } }, { label: 'End Event', actionName: 'replace-with-none-end', className: 'bpmn-icon-end-event-none', target: { type: 'bpmn:EndEvent' } }, { label: 'Message Start Event', actionName: 'replace-with-message-start', className: 'bpmn-icon-start-event-message', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:MessageEventDefinition' } }, { label: 'Timer Start Event', actionName: 'replace-with-timer-start', className: 'bpmn-icon-start-event-timer', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:TimerEventDefinition' } }, { label: 'Conditional Start Event', actionName: 'replace-with-conditional-start', className: 'bpmn-icon-start-event-condition', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:ConditionalEventDefinition' } }, { label: 'Signal Start Event', actionName: 'replace-with-signal-start', className: 'bpmn-icon-start-event-signal', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:SignalEventDefinition' } } ]; var START_EVENT_SUB_PROCESS = [ { label: 'Start Event', actionName: 'replace-with-none-start', className: 'bpmn-icon-start-event-none', target: { type: 'bpmn:StartEvent' } }, { label: 'Intermediate Throw Event', actionName: 'replace-with-none-intermediate-throwing', className: 'bpmn-icon-intermediate-event-none', target: { type: 'bpmn:IntermediateThrowEvent' } }, { label: 'End Event', actionName: 'replace-with-none-end', className: 'bpmn-icon-end-event-none', target: { type: 'bpmn:EndEvent' } } ]; var INTERMEDIATE_EVENT = [ { label: 'Start Event', actionName: 'replace-with-none-start', className: 'bpmn-icon-start-event-none', target: { type: 'bpmn:StartEvent' } }, { label: 'Intermediate Throw Event', actionName: 'replace-with-none-intermediate-throw', className: 'bpmn-icon-intermediate-event-none', target: { type: 'bpmn:IntermediateThrowEvent' } }, { label: 'End Event', actionName: 'replace-with-none-end', className: 'bpmn-icon-end-event-none', target: { type: 'bpmn:EndEvent' } }, { label: 'Message Intermediate Catch Event', actionName: 'replace-with-message-intermediate-catch', className: 'bpmn-icon-intermediate-event-catch-message', target: { type: 'bpmn:IntermediateCatchEvent', eventDefinitionType: 'bpmn:MessageEventDefinition' } }, { label: 'Message Intermediate Throw Event', actionName: 'replace-with-message-intermediate-throw', className: 'bpmn-icon-intermediate-event-throw-message', target: { type: 'bpmn:IntermediateThrowEvent', eventDefinitionType: 'bpmn:MessageEventDefinition' } }, { label: 'Timer Intermediate Catch Event', actionName: 'replace-with-timer-intermediate-catch', className: 'bpmn-icon-intermediate-event-catch-timer', target: { type: 'bpmn:IntermediateCatchEvent', eventDefinitionType: 'bpmn:TimerEventDefinition' } }, { label: 'Escalation Intermediate Throw Event', actionName: 'replace-with-escalation-intermediate-throw', className: 'bpmn-icon-intermediate-event-throw-escalation', target: { type: 'bpmn:IntermediateThrowEvent', eventDefinitionType: 'bpmn:EscalationEventDefinition' } }, { label: 'Conditional Intermediate Catch Event', actionName: 'replace-with-conditional-intermediate-catch', className: 'bpmn-icon-intermediate-event-catch-condition', target: { type: 'bpmn:IntermediateCatchEvent', eventDefinitionType: 'bpmn:ConditionalEventDefinition' } }, { label: 'Link Intermediate Catch Event', actionName: 'replace-with-link-intermediate-catch', className: 'bpmn-icon-intermediate-event-catch-link', target: { type: 'bpmn:IntermediateCatchEvent', eventDefinitionType: 'bpmn:LinkEventDefinition', eventDefinitionAttrs: { name: '' } } }, { label: 'Link Intermediate Throw Event', actionName: 'replace-with-link-intermediate-throw', className: 'bpmn-icon-intermediate-event-throw-link', target: { type: 'bpmn:IntermediateThrowEvent', eventDefinitionType: 'bpmn:LinkEventDefinition', eventDefinitionAttrs: { name: '' } } }, { label: 'Compensation Intermediate Throw Event', actionName: 'replace-with-compensation-intermediate-throw', className: 'bpmn-icon-intermediate-event-throw-compensation', target: { type: 'bpmn:IntermediateThrowEvent', eventDefinitionType: 'bpmn:CompensateEventDefinition' } }, { label: 'Signal Intermediate Catch Event', actionName: 'replace-with-signal-intermediate-catch', className: 'bpmn-icon-intermediate-event-catch-signal', target: { type: 'bpmn:IntermediateCatchEvent', eventDefinitionType: 'bpmn:SignalEventDefinition' } }, { label: 'Signal Intermediate Throw Event', actionName: 'replace-with-signal-intermediate-throw', className: 'bpmn-icon-intermediate-event-throw-signal', target: { type: 'bpmn:IntermediateThrowEvent', eventDefinitionType: 'bpmn:SignalEventDefinition' } } ]; var END_EVENT = [ { label: 'Start Event', actionName: 'replace-with-none-start', className: 'bpmn-icon-start-event-none', target: { type: 'bpmn:StartEvent' } }, { label: 'Intermediate Throw Event', actionName: 'replace-with-none-intermediate-throw', className: 'bpmn-icon-intermediate-event-none', target: { type: 'bpmn:IntermediateThrowEvent' } }, { label: 'End Event', actionName: 'replace-with-none-end', className: 'bpmn-icon-end-event-none', target: { type: 'bpmn:EndEvent' } }, { label: 'Message End Event', actionName: 'replace-with-message-end', className: 'bpmn-icon-end-event-message', target: { type: 'bpmn:EndEvent', eventDefinitionType: 'bpmn:MessageEventDefinition' } }, { label: 'Escalation End Event', actionName: 'replace-with-escalation-end', className: 'bpmn-icon-end-event-escalation', target: { type: 'bpmn:EndEvent', eventDefinitionType: 'bpmn:EscalationEventDefinition' } }, { label: 'Error End Event', actionName: 'replace-with-error-end', className: 'bpmn-icon-end-event-error', target: { type: 'bpmn:EndEvent', eventDefinitionType: 'bpmn:ErrorEventDefinition' } }, { label: 'Cancel End Event', actionName: 'replace-with-cancel-end', className: 'bpmn-icon-end-event-cancel', target: { type: 'bpmn:EndEvent', eventDefinitionType: 'bpmn:CancelEventDefinition' } }, { label: 'Compensation End Event', actionName: 'replace-with-compensation-end', className: 'bpmn-icon-end-event-compensation', target: { type: 'bpmn:EndEvent', eventDefinitionType: 'bpmn:CompensateEventDefinition' } }, { label: 'Signal End Event', actionName: 'replace-with-signal-end', className: 'bpmn-icon-end-event-signal', target: { type: 'bpmn:EndEvent', eventDefinitionType: 'bpmn:SignalEventDefinition' } }, { label: 'Terminate End Event', actionName: 'replace-with-terminate-end', className: 'bpmn-icon-end-event-terminate', target: { type: 'bpmn:EndEvent', eventDefinitionType: 'bpmn:TerminateEventDefinition' } } ]; var GATEWAY = [ { label: 'Exclusive Gateway', actionName: 'replace-with-exclusive-gateway', className: 'bpmn-icon-gateway-xor', target: { type: 'bpmn:ExclusiveGateway' } }, { label: 'Parallel Gateway', actionName: 'replace-with-parallel-gateway', className: 'bpmn-icon-gateway-parallel', target: { type: 'bpmn:ParallelGateway' } }, { label: 'Inclusive Gateway', actionName: 'replace-with-inclusive-gateway', className: 'bpmn-icon-gateway-or', target: { type: 'bpmn:InclusiveGateway' } }, { label: 'Complex Gateway', actionName: 'replace-with-complex-gateway', className: 'bpmn-icon-gateway-complex', target: { type: 'bpmn:ComplexGateway' } }, { label: 'Event based Gateway', actionName: 'replace-with-event-based-gateway', className: 'bpmn-icon-gateway-eventbased', target: { type: 'bpmn:EventBasedGateway', instantiate: false, eventGatewayType: 'Exclusive' } } // Gateways deactivated until https://github.com/bpmn-io/bpmn-js/issues/194 // { // label: 'Event based instantiating Gateway', // actionName: 'replace-with-exclusive-event-based-gateway', // className: 'bpmn-icon-exclusive-event-based', // target: { // type: 'bpmn:EventBasedGateway' // }, // options: { // businessObject: { instantiate: true, eventGatewayType: 'Exclusive' } // } // }, // { // label: 'Parallel Event based instantiating Gateway', // actionName: 'replace-with-parallel-event-based-instantiate-gateway', // className: 'bpmn-icon-parallel-event-based-instantiate-gateway', // target: { // type: 'bpmn:EventBasedGateway' // }, // options: { // businessObject: { instantiate: true, eventGatewayType: 'Parallel' } // } // } ]; var SUBPROCESS_EXPANDED = [ { label: 'Transaction', actionName: 'replace-with-transaction', className: 'bpmn-icon-transaction', target: { type: 'bpmn:Transaction', isExpanded: true } }, { label: 'Event Sub Process', actionName: 'replace-with-event-subprocess', className: 'bpmn-icon-event-subprocess-expanded', target: { type: 'bpmn:SubProcess', triggeredByEvent: true, isExpanded: true } }, { label: 'Sub Process (collapsed)', actionName: 'replace-with-collapsed-subprocess', className: 'bpmn-icon-subprocess-collapsed', target: { type: 'bpmn:SubProcess', isExpanded: false } } ]; var TRANSACTION = [ { label: 'Sub Process', actionName: 'replace-with-subprocess', className: 'bpmn-icon-subprocess-expanded', target: { type: 'bpmn:SubProcess', isExpanded: true } }, { label: 'Event Sub Process', actionName: 'replace-with-event-subprocess', className: 'bpmn-icon-event-subprocess-expanded', target: { type: 'bpmn:SubProcess', triggeredByEvent: true, isExpanded: true } } ]; var EVENT_SUB_PROCESS = [ { label: 'Sub Process', actionName: 'replace-with-subprocess', className: 'bpmn-icon-subprocess-expanded', target: { type: 'bpmn:SubProcess', isExpanded: true } }, { label: 'Transaction', actionName: 'replace-with-transaction', className: 'bpmn-icon-transaction', target: { type: 'bpmn:Transaction', isExpanded: true } } ]; var TASK = [ { label: 'Task', actionName: 'replace-with-task', className: 'bpmn-icon-task', target: { type: 'bpmn:Task' } }, { label: 'Send Task', actionName: 'replace-with-send-task', className: 'bpmn-icon-send', target: { type: 'bpmn:SendTask' } }, { label: 'Receive Task', actionName: 'replace-with-receive-task', className: 'bpmn-icon-receive', target: { type: 'bpmn:ReceiveTask' } }, { label: 'User Task', actionName: 'replace-with-user-task', className: 'bpmn-icon-user', target: { type: 'bpmn:UserTask' } }, { label: 'Manual Task', actionName: 'replace-with-manual-task', className: 'bpmn-icon-manual', target: { type: 'bpmn:ManualTask' } }, { label: 'Business Rule Task', actionName: 'replace-with-rule-task', className: 'bpmn-icon-business-rule', target: { type: 'bpmn:BusinessRuleTask' } }, { label: 'Service Task', actionName: 'replace-with-service-task', className: 'bpmn-icon-service', target: { type: 'bpmn:ServiceTask' } }, { label: 'Script Task', actionName: 'replace-with-script-task', className: 'bpmn-icon-script', target: { type: 'bpmn:ScriptTask' } }, { label: 'Call Activity', actionName: 'replace-with-call-activity', className: 'bpmn-icon-call-activity', target: { type: 'bpmn:CallActivity' } }, { label: 'Sub Process (collapsed)', actionName: 'replace-with-collapsed-subprocess', className: 'bpmn-icon-subprocess-collapsed', target: { type: 'bpmn:SubProcess', isExpanded: false } }, { label: 'Sub Process (expanded)', actionName: 'replace-with-expanded-subprocess', className: 'bpmn-icon-subprocess-expanded', target: { type: 'bpmn:SubProcess', isExpanded: true } } ]; var DATA_OBJECT_REFERENCE = [ { label: 'Data Store Reference', actionName: 'replace-with-data-store-reference', className: 'bpmn-icon-data-store', target: { type: 'bpmn:DataStoreReference' } } ]; var DATA_STORE_REFERENCE = [ { label: 'Data Object Reference', actionName: 'replace-with-data-object-reference', className: 'bpmn-icon-data-object', target: { type: 'bpmn:DataObjectReference' } } ]; var BOUNDARY_EVENT = [ { label: 'Message Boundary Event', actionName: 'replace-with-message-boundary', className: 'bpmn-icon-intermediate-event-catch-message', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:MessageEventDefinition' } }, { label: 'Timer Boundary Event', actionName: 'replace-with-timer-boundary', className: 'bpmn-icon-intermediate-event-catch-timer', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:TimerEventDefinition' } }, { label: 'Escalation Boundary Event', actionName: 'replace-with-escalation-boundary', className: 'bpmn-icon-intermediate-event-catch-escalation', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:EscalationEventDefinition' } }, { label: 'Conditional Boundary Event', actionName: 'replace-with-conditional-boundary', className: 'bpmn-icon-intermediate-event-catch-condition', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:ConditionalEventDefinition' } }, { label: 'Error Boundary Event', actionName: 'replace-with-error-boundary', className: 'bpmn-icon-intermediate-event-catch-error', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:ErrorEventDefinition' } }, { label: 'Cancel Boundary Event', actionName: 'replace-with-cancel-boundary', className: 'bpmn-icon-intermediate-event-catch-cancel', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:CancelEventDefinition' } }, { label: 'Signal Boundary Event', actionName: 'replace-with-signal-boundary', className: 'bpmn-icon-intermediate-event-catch-signal', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:SignalEventDefinition' } }, { label: 'Compensation Boundary Event', actionName: 'replace-with-compensation-boundary', className: 'bpmn-icon-intermediate-event-catch-compensation', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:CompensateEventDefinition' } }, { label: 'Message Boundary Event (non-interrupting)', actionName: 'replace-with-non-interrupting-message-boundary', className: 'bpmn-icon-intermediate-event-catch-non-interrupting-message', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:MessageEventDefinition', cancelActivity: false } }, { label: 'Timer Boundary Event (non-interrupting)', actionName: 'replace-with-non-interrupting-timer-boundary', className: 'bpmn-icon-intermediate-event-catch-non-interrupting-timer', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:TimerEventDefinition', cancelActivity: false } }, { label: 'Escalation Boundary Event (non-interrupting)', actionName: 'replace-with-non-interrupting-escalation-boundary', className: 'bpmn-icon-intermediate-event-catch-non-interrupting-escalation', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:EscalationEventDefinition', cancelActivity: false } }, { label: 'Conditional Boundary Event (non-interrupting)', actionName: 'replace-with-non-interrupting-conditional-boundary', className: 'bpmn-icon-intermediate-event-catch-non-interrupting-condition', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:ConditionalEventDefinition', cancelActivity: false } }, { label: 'Signal Boundary Event (non-interrupting)', actionName: 'replace-with-non-interrupting-signal-boundary', className: 'bpmn-icon-intermediate-event-catch-non-interrupting-signal', target: { type: 'bpmn:BoundaryEvent', eventDefinitionType: 'bpmn:SignalEventDefinition', cancelActivity: false } } ]; var EVENT_SUB_PROCESS_START_EVENT = [ { label: 'Message Start Event', actionName: 'replace-with-message-start', className: 'bpmn-icon-start-event-message', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:MessageEventDefinition' } }, { label: 'Timer Start Event', actionName: 'replace-with-timer-start', className: 'bpmn-icon-start-event-timer', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:TimerEventDefinition' } }, { label: 'Conditional Start Event', actionName: 'replace-with-conditional-start', className: 'bpmn-icon-start-event-condition', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:ConditionalEventDefinition' } }, { label: 'Signal Start Event', actionName: 'replace-with-signal-start', className: 'bpmn-icon-start-event-signal', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:SignalEventDefinition' } }, { label: 'Error Start Event', actionName: 'replace-with-error-start', className: 'bpmn-icon-start-event-error', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:ErrorEventDefinition' } }, { label: 'Escalation Start Event', actionName: 'replace-with-escalation-start', className: 'bpmn-icon-start-event-escalation', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:EscalationEventDefinition' } }, { label: 'Compensation Start Event', actionName: 'replace-with-compensation-start', className: 'bpmn-icon-start-event-compensation', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:CompensateEventDefinition' } }, { label: 'Message Start Event (non-interrupting)', actionName: 'replace-with-non-interrupting-message-start', className: 'bpmn-icon-start-event-non-interrupting-message', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:MessageEventDefinition', isInterrupting: false } }, { label: 'Timer Start Event (non-interrupting)', actionName: 'replace-with-non-interrupting-timer-start', className: 'bpmn-icon-start-event-non-interrupting-timer', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:TimerEventDefinition', isInterrupting: false } }, { label: 'Conditional Start Event (non-interrupting)', actionName: 'replace-with-non-interrupting-conditional-start', className: 'bpmn-icon-start-event-non-interrupting-condition', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:ConditionalEventDefinition', isInterrupting: false } }, { label: 'Signal Start Event (non-interrupting)', actionName: 'replace-with-non-interrupting-signal-start', className: 'bpmn-icon-start-event-non-interrupting-signal', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:SignalEventDefinition', isInterrupting: false } }, { label: 'Escalation Start Event (non-interrupting)', actionName: 'replace-with-non-interrupting-escalation-start', className: 'bpmn-icon-start-event-non-interrupting-escalation', target: { type: 'bpmn:StartEvent', eventDefinitionType: 'bpmn:EscalationEventDefinition', isInterrupting: false } } ]; var SEQUENCE_FLOW = [ { label: 'Sequence Flow', actionName: 'replace-with-sequence-flow', className: 'bpmn-icon-connection' }, { label: 'Default Flow', actionName: 'replace-with-default-flow', className: 'bpmn-icon-default-flow' }, { label: 'Conditional Flow', actionName: 'replace-with-conditional-flow', className: 'bpmn-icon-conditional-flow' } ]; var PARTICIPANT = [ { label: 'Expanded Pool', actionName: 'replace-with-expanded-pool', className: 'bpmn-icon-participant', target: { type: 'bpmn:Participant', isExpanded: true } }, { label: function(element) { var label = 'Empty Pool'; if (element.children && element.children.length) { label += ' (removes content)'; } return label; }, actionName: 'replace-with-collapsed-pool', // TODO(@janstuemmel): maybe design new icon className: 'bpmn-icon-lane', target: { type: 'bpmn:Participant', isExpanded: false } } ]; /** * This module is an element agnostic replace menu provider for the popup menu. */ function ReplaceMenuProvider( bpmnFactory, popupMenu, modeling, moddle, bpmnReplace, rules, translate) { this._bpmnFactory = bpmnFactory; this._popupMenu = popupMenu; this._modeling = modeling; this._moddle = moddle; this._bpmnReplace = bpmnReplace; this._rules = rules; this._translate = translate; this.register(); } ReplaceMenuProvider.$inject = [ 'bpmnFactory', 'popupMenu', 'modeling', 'moddle', 'bpmnReplace', 'rules', 'translate' ]; /** * Register replace menu provider in the popup menu */ ReplaceMenuProvider.prototype.register = function() { this._popupMenu.registerProvider('bpmn-replace', this); }; /** * Get all entries from replaceOptions for the given element and apply filters * on them. Get for example only elements, which are different from the current one. * * @param {djs.model.Base} element * * @return {Array} a list of menu entry items */ ReplaceMenuProvider.prototype.getEntries = function(element) { var businessObject = element.businessObject; var rules = this._rules; var entries; if (!rules.allowed('shape.replace', { element: element })) { return []; } var differentType = isDifferentType(element); if (is$1(businessObject, 'bpmn:DataObjectReference')) { return this._createEntries(element, DATA_OBJECT_REFERENCE); } if (is$1(businessObject, 'bpmn:DataStoreReference') && !is$1(element.parent, 'bpmn:Collaboration')) { return this._createEntries(element, DATA_STORE_REFERENCE); } // start events outside sub processes if (is$1(businessObject, 'bpmn:StartEvent') && !is$1(businessObject.$parent, 'bpmn:SubProcess')) { entries = filter(START_EVENT, differentType); return this._createEntries(element, entries); } // expanded/collapsed pools if (is$1(businessObject, 'bpmn:Participant')) { entries = filter(PARTICIPANT, function(entry) { return isExpanded(element) !== entry.target.isExpanded; }); return this._createEntries(element, entries); } // start events inside event sub processes if (is$1(businessObject, 'bpmn:StartEvent') && isEventSubProcess(businessObject.$parent)) { entries = filter(EVENT_SUB_PROCESS_START_EVENT, function(entry) { var target = entry.target; var isInterrupting = target.isInterrupting !== false; var isInterruptingEqual = getBusinessObject(element).isInterrupting === isInterrupting; // filters elements which types and event definition are equal but have have different interrupting types return differentType(entry) || !differentType(entry) && !isInterruptingEqual; }); return this._createEntries(element, entries); } // start events inside sub processes if (is$1(businessObject, 'bpmn:StartEvent') && !isEventSubProcess(businessObject.$parent) && is$1(businessObject.$parent, 'bpmn:SubProcess')) { entries = filter(START_EVENT_SUB_PROCESS, differentType); return this._createEntries(element, entries); } // end events if (is$1(businessObject, 'bpmn:EndEvent')) { entries = filter(END_EVENT, function(entry) { var target = entry.target; // hide cancel end events outside transactions if (target.eventDefinitionType == 'bpmn:CancelEventDefinition' && !is$1(businessObject.$parent, 'bpmn:Transaction')) { return false; } return differentType(entry); }); return this._createEntries(element, entries); } // boundary events if (is$1(businessObject, 'bpmn:BoundaryEvent')) { entries = filter(BOUNDARY_EVENT, function(entry) { var target = entry.target; if (target.eventDefinitionType == 'bpmn:CancelEventDefinition' && !is$1(businessObject.attachedToRef, 'bpmn:Transaction')) { return false; } var cancelActivity = target.cancelActivity !== false; var isCancelActivityEqual = businessObject.cancelActivity == cancelActivity; return differentType(entry) || !differentType(entry) && !isCancelActivityEqual; }); return this._createEntries(element, entries); } // intermediate events if (is$1(businessObject, 'bpmn:IntermediateCatchEvent') || is$1(businessObject, 'bpmn:IntermediateThrowEvent')) { entries = filter(INTERMEDIATE_EVENT, differentType); return this._createEntries(element, entries); } // gateways if (is$1(businessObject, 'bpmn:Gateway')) { entries = filter(GATEWAY, differentType); return this._createEntries(element, entries); } // transactions if (is$1(businessObject, 'bpmn:Transaction')) { entries = filter(TRANSACTION, differentType); return this._createEntries(element, entries); } // expanded event sub processes if (isEventSubProcess(businessObject) && isExpanded(element)) { entries = filter(EVENT_SUB_PROCESS, differentType); return this._createEntries(element, entries); } // expanded sub processes if (is$1(businessObject, 'bpmn:SubProcess') && isExpanded(element)) { entries = filter(SUBPROCESS_EXPANDED, differentType); return this._createEntries(element, entries); } // collapsed ad hoc sub processes if (is$1(businessObject, 'bpmn:AdHocSubProcess') && !isExpanded(element)) { entries = filter(TASK, function(entry) { var target = entry.target; var isTargetSubProcess = target.type === 'bpmn:SubProcess'; var isTargetExpanded = target.isExpanded === true; return isDifferentType(element) && (!isTargetSubProcess || isTargetExpanded); }); return this._createEntries(element, entries); } // sequence flows if (is$1(businessObject, 'bpmn:SequenceFlow')) { return this._createSequenceFlowEntries(element, SEQUENCE_FLOW); } // flow nodes if (is$1(businessObject, 'bpmn:FlowNode')) { entries = filter(TASK, differentType); // collapsed SubProcess can not be replaced with itself if (is$1(businessObject, 'bpmn:SubProcess') && !isExpanded(element)) { entries = filter(entries, function(entry) { return entry.label !== 'Sub Process (collapsed)'; }); } return this._createEntries(element, entries); } return []; }; /** * Get a list of header items for the given element. This includes buttons * for multi instance markers and for the ad hoc marker. * * @param {djs.model.Base} element * * @return {Array} a list of menu entry items */ ReplaceMenuProvider.prototype.getHeaderEntries = function(element) { var headerEntries = []; if (is$1(element, 'bpmn:Activity') && !isEventSubProcess(element)) { headerEntries = headerEntries.concat(this._getLoopEntries(element)); } if (is$1(element, 'bpmn:DataObjectReference')) { headerEntries = headerEntries.concat(this._getDataObjectIsCollection(element)); } if (is$1(element, 'bpmn:Participant')) { headerEntries = headerEntries.concat(this._getParticipantMultiplicity(element)); } if (is$1(element, 'bpmn:SubProcess') && !is$1(element, 'bpmn:Transaction') && !isEventSubProcess(element)) { headerEntries.push(this._getAdHocEntry(element)); } return headerEntries; }; /** * Creates an array of menu entry objects for a given element and filters the replaceOptions * according to a filter function. * * @param {djs.model.Base} element * @param {Object} replaceOptions * * @return {Array} a list of menu items */ ReplaceMenuProvider.prototype._createEntries = function(element, replaceOptions) { var menuEntries = []; var self = this; forEach$1(replaceOptions, function(definition) { var entry = self._createMenuEntry(definition, element); menuEntries.push(entry); }); return menuEntries; }; /** * Creates an array of menu entry objects for a given sequence flow. * * @param {djs.model.Base} element * @param {Object} replaceOptions * @return {Array} a list of menu items */ ReplaceMenuProvider.prototype._createSequenceFlowEntries = function(element, replaceOptions) { var businessObject = getBusinessObject(element); var menuEntries = []; var modeling = this._modeling, moddle = this._moddle; var self = this; forEach$1(replaceOptions, function(entry) { switch (entry.actionName) { case 'replace-with-default-flow': if (businessObject.sourceRef.default !== businessObject && (is$1(businessObject.sourceRef, 'bpmn:ExclusiveGateway') || is$1(businessObject.sourceRef, 'bpmn:InclusiveGateway') || is$1(businessObject.sourceRef, 'bpmn:ComplexGateway') || is$1(businessObject.sourceRef, 'bpmn:Activity'))) { menuEntries.push(self._createMenuEntry(entry, element, function() { modeling.updateProperties(element.source, { default: businessObject }); })); } break; case 'replace-with-conditional-flow': if (!businessObject.conditionExpression && is$1(businessObject.sourceRef, 'bpmn:Activity')) { menuEntries.push(self._createMenuEntry(entry, element, function() { var conditionExpression = moddle.create('bpmn:FormalExpression', { body: '' }); modeling.updateProperties(element, { conditionExpression: conditionExpression }); })); } break; default: // default flows if (is$1(businessObject.sourceRef, 'bpmn:Activity') && businessObject.conditionExpression) { return menuEntries.push(self._createMenuEntry(entry, element, function() { modeling.updateProperties(element, { conditionExpression: undefined }); })); } // conditional flows if ((is$1(businessObject.sourceRef, 'bpmn:ExclusiveGateway') || is$1(businessObject.sourceRef, 'bpmn:InclusiveGateway') || is$1(businessObject.sourceRef, 'bpmn:ComplexGateway') || is$1(businessObject.sourceRef, 'bpmn:Activity')) && businessObject.sourceRef.default === businessObject) { return menuEntries.push(self._createMenuEntry(entry, element, function() { modeling.updateProperties(element.source, { default: undefined }); })); } } }); return menuEntries; }; /** * Creates and returns a single menu entry item. * * @param {Object} definition a single replace options definition object * @param {djs.model.Base} element * @param {Function} [action] an action callback function which gets called when * the menu entry is being triggered. * * @return {Object} menu entry item */ ReplaceMenuProvider.prototype._createMenuEntry = function(definition, element, action) { var translate = this._translate; var replaceElement = this._bpmnReplace.replaceElement; var replaceAction = function() { return replaceElement(element, definition.target); }; var label = definition.label; if (label && typeof label === 'function') { label = label(element); } action = action || replaceAction; var menuEntry = { label: translate(label), className: definition.className, id: definition.actionName, action: action }; return menuEntry; }; /** * Get a list of menu items containing buttons for multi instance markers * * @param {djs.model.Base} element * * @return {Array} a list of menu items */ ReplaceMenuProvider.prototype._getLoopEntries = function(element) { var self = this; var translate = this._translate; function toggleLoopEntry(event, entry) { var newLoopCharacteristics = getBusinessObject(element).loopCharacteristics; if (entry.active) { newLoopCharacteristics = undefined; } else { if (isUndefined$2(entry.options.isSequential) || !newLoopCharacteristics || !is$1(newLoopCharacteristics, entry.options.loopCharacteristics)) { newLoopCharacteristics = self._moddle.create(entry.options.loopCharacteristics); } newLoopCharacteristics.isSequential = entry.options.isSequential; } self._modeling.updateProperties(element, { loopCharacteristics: newLoopCharacteristics }); } var businessObject = getBusinessObject(element), loopCharacteristics = businessObject.loopCharacteristics; var isSequential, isLoop, isParallel; if (loopCharacteristics) { isSequential = loopCharacteristics.isSequential; isLoop = loopCharacteristics.isSequential === undefined; isParallel = loopCharacteristics.isSequential !== undefined && !loopCharacteristics.isSequential; } var loopEntries = [ { id: 'toggle-parallel-mi', className: 'bpmn-icon-parallel-mi-marker', title: translate('Parallel Multi Instance'), active: isParallel, action: toggleLoopEntry, options: { loopCharacteristics: 'bpmn:MultiInstanceLoopCharacteristics', isSequential: false } }, { id: 'toggle-sequential-mi', className: 'bpmn-icon-sequential-mi-marker', title: translate('Sequential Multi Instance'), active: isSequential, action: toggleLoopEntry, options: { loopCharacteristics: 'bpmn:MultiInstanceLoopCharacteristics', isSequential: true } }, { id: 'toggle-loop', className: 'bpmn-icon-loop-marker', title: translate('Loop'), active: isLoop, action: toggleLoopEntry, options: { loopCharacteristics: 'bpmn:StandardLoopCharacteristics' } } ]; return loopEntries; }; /** * Get a list of menu items containing a button for the collection marker * * @param {djs.model.Base} element * * @return {Array} a list of menu items */ ReplaceMenuProvider.prototype._getDataObjectIsCollection = function(element) { var self = this; var translate = this._translate; function toggleIsCollection(event, entry) { self._modeling.updateModdleProperties( element, dataObject, { isCollection: !entry.active }); } var dataObject = element.businessObject.dataObjectRef, isCollection = dataObject.isCollection; var dataObjectEntries = [ { id: 'toggle-is-collection', className: 'bpmn-icon-parallel-mi-marker', title: translate('Collection'), active: isCollection, action: toggleIsCollection, } ]; return dataObjectEntries; }; /** * Get a list of menu items containing a button for the participant multiplicity marker * * @param {djs.model.Base} element * * @return {Array} a list of menu items */ ReplaceMenuProvider.prototype._getParticipantMultiplicity = function(element) { var self = this; var bpmnFactory = this._bpmnFactory; var translate = this._translate; function toggleParticipantMultiplicity(event, entry) { var isActive = entry.active; var participantMultiplicity; if (!isActive) { participantMultiplicity = bpmnFactory.create('bpmn:ParticipantMultiplicity'); } self._modeling.updateProperties( element, { participantMultiplicity: participantMultiplicity }); } var participantMultiplicity = element.businessObject.participantMultiplicity; var participantEntries = [ { id: 'toggle-participant-multiplicity', className: 'bpmn-icon-parallel-mi-marker', title: translate('Participant Multiplicity'), active: !!participantMultiplicity, action: toggleParticipantMultiplicity, } ]; return participantEntries; }; /** * Get the menu items containing a button for the ad hoc marker * * @param {djs.model.Base} element * * @return {Object} a menu item */ ReplaceMenuProvider.prototype._getAdHocEntry = function(element) { var translate = this._translate; var businessObject = getBusinessObject(element); var isAdHoc = is$1(businessObject, 'bpmn:AdHocSubProcess'); var replaceElement = this._bpmnReplace.replaceElement; var adHocEntry = { id: 'toggle-adhoc', className: 'bpmn-icon-ad-hoc-marker', title: translate('Ad-hoc'), active: isAdHoc, action: function(event, entry) { if (isAdHoc) { return replaceElement(element, { type: 'bpmn:SubProcess' }, { autoResize: false, layoutConnection: false }); } else { return replaceElement(element, { type: 'bpmn:AdHocSubProcess' }, { autoResize: false, layoutConnection: false }); } } }; return adHocEntry; }; var PopupMenuModule = { __depends__: [ PopupMenuModule$1, ReplaceModule ], __init__: [ 'replaceMenuProvider' ], replaceMenuProvider: [ 'type', ReplaceMenuProvider ] }; var max$4 = Math.max, min$2 = Math.min; var DEFAULT_CHILD_BOX_PADDING = 20; /** * Substract a TRBL from another * * @param {TRBL} trblA * @param {TRBL} trblB * * @return {TRBL} */ function substractTRBL(trblA, trblB) { return { top: trblA.top - trblB.top, right: trblA.right - trblB.right, bottom: trblA.bottom - trblB.bottom, left: trblA.left - trblB.left }; } /** * Resize the given bounds by the specified delta from a given anchor point. * * @param {Bounds} bounds the bounding box that should be resized * @param {string} direction in which the element is resized (nw, ne, se, sw) * @param {Point} delta of the resize operation * * @return {Bounds} resized bounding box */ function resizeBounds$1(bounds, direction, delta) { var dx = delta.x, dy = delta.y; var newBounds = { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }; if (direction.indexOf('n') !== -1) { newBounds.y = bounds.y + dy; newBounds.height = bounds.height - dy; } else if (direction.indexOf('s') !== -1) { newBounds.height = bounds.height + dy; } if (direction.indexOf('e') !== -1) { newBounds.width = bounds.width + dx; } else if (direction.indexOf('w') !== -1) { newBounds.x = bounds.x + dx; newBounds.width = bounds.width - dx; } return newBounds; } /** * Resize the given bounds by applying the passed * { top, right, bottom, left } delta. * * @param {Bounds} bounds * @param {TRBL} trblResize * * @return {Bounds} */ function resizeTRBL(bounds, resize) { return { x: bounds.x + (resize.left || 0), y: bounds.y + (resize.top || 0), width: bounds.width - (resize.left || 0) + (resize.right || 0), height: bounds.height - (resize.top || 0) + (resize.bottom || 0) }; } function applyConstraints(attr, trbl, resizeConstraints) { var value = trbl[attr], minValue = resizeConstraints.min && resizeConstraints.min[attr], maxValue = resizeConstraints.max && resizeConstraints.max[attr]; if (isNumber(minValue)) { value = (/top|left/.test(attr) ? min$2 : max$4)(value, minValue); } if (isNumber(maxValue)) { value = (/top|left/.test(attr) ? max$4 : min$2)(value, maxValue); } return value; } function ensureConstraints$1(currentBounds, resizeConstraints) { if (!resizeConstraints) { return currentBounds; } var currentTrbl = asTRBL(currentBounds); return asBounds({ top: applyConstraints('top', currentTrbl, resizeConstraints), right: applyConstraints('right', currentTrbl, resizeConstraints), bottom: applyConstraints('bottom', currentTrbl, resizeConstraints), left: applyConstraints('left', currentTrbl, resizeConstraints) }); } function getMinResizeBounds(direction, currentBounds, minDimensions, childrenBounds) { var currentBox = asTRBL(currentBounds); var minBox = { top: /n/.test(direction) ? currentBox.bottom - minDimensions.height : currentBox.top, left: /w/.test(direction) ? currentBox.right - minDimensions.width : currentBox.left, bottom: /s/.test(direction) ? currentBox.top + minDimensions.height : currentBox.bottom, right: /e/.test(direction) ? currentBox.left + minDimensions.width : currentBox.right }; var childrenBox = childrenBounds ? asTRBL(childrenBounds) : minBox; var combinedBox = { top: min$2(minBox.top, childrenBox.top), left: min$2(minBox.left, childrenBox.left), bottom: max$4(minBox.bottom, childrenBox.bottom), right: max$4(minBox.right, childrenBox.right) }; return asBounds(combinedBox); } function asPadding(mayBePadding, defaultValue) { if (typeof mayBePadding !== 'undefined') { return mayBePadding; } else { return DEFAULT_CHILD_BOX_PADDING; } } function addPadding$1(bbox, padding) { var left, right, top, bottom; if (typeof padding === 'object') { left = asPadding(padding.left); right = asPadding(padding.right); top = asPadding(padding.top); bottom = asPadding(padding.bottom); } else { left = right = top = bottom = asPadding(padding); } return { x: bbox.x - left, y: bbox.y - top, width: bbox.width + left + right, height: bbox.height + top + bottom }; } /** * Is the given element part of the resize * targets min boundary box? * * This is the default implementation which excludes * connections and labels. * * @param {djs.model.Base} element */ function isBBoxChild(element) { // exclude connections if (element.waypoints) { return false; } // exclude labels if (element.type === 'label') { return false; } return true; } /** * Return children bounding computed from a shapes children * or a list of prefiltered children. * * @param {djs.model.Shape|Array} shapeOrChildren * @param {number|Object} padding * * @return {Bounds} */ function computeChildrenBBox(shapeOrChildren, padding) { var elements; // compute based on shape if (shapeOrChildren.length === undefined) { // grab all the children that are part of the // parents children box elements = filter(shapeOrChildren.children, isBBoxChild); } else { elements = shapeOrChildren; } if (elements.length) { return addPadding$1(getBBox(elements), padding); } } var abs$4 = Math.abs; function getTRBLResize(oldBounds, newBounds) { return substractTRBL(asTRBL(newBounds), asTRBL(oldBounds)); } var LANE_PARENTS = [ 'bpmn:Participant', 'bpmn:Process', 'bpmn:SubProcess' ]; var LANE_INDENTATION = 30; /** * Collect all lane shapes in the given paren * * @param {djs.model.Shape} shape * @param {Array} [collectedShapes] * * @return {Array} */ function collectLanes(shape, collectedShapes) { collectedShapes = collectedShapes || []; shape.children.filter(function(s) { if (is$1(s, 'bpmn:Lane')) { collectLanes(s, collectedShapes); collectedShapes.push(s); } }); return collectedShapes; } /** * Return the lane children of the given element. * * @param {djs.model.Shape} shape * * @return {Array} */ function getChildLanes(shape) { return shape.children.filter(function(c) { return is$1(c, 'bpmn:Lane'); }); } /** * Return the root element containing the given lane shape * * @param {djs.model.Shape} shape * * @return {djs.model.Shape} */ function getLanesRoot(shape) { return getParent(shape, LANE_PARENTS) || shape; } /** * Compute the required resize operations for lanes * adjacent to the given shape, assuming it will be * resized to the given new bounds. * * @param {djs.model.Shape} shape * @param {Bounds} newBounds * * @return {Array} */ function computeLanesResize(shape, newBounds) { var rootElement = getLanesRoot(shape); var initialShapes = is$1(rootElement, 'bpmn:Process') ? [] : [ rootElement ]; var allLanes = collectLanes(rootElement, initialShapes), shapeTrbl = asTRBL(shape), shapeNewTrbl = asTRBL(newBounds), trblResize = getTRBLResize(shape, newBounds), resizeNeeded = []; allLanes.forEach(function(other) { if (other === shape) { return; } var topResize = 0, rightResize = trblResize.right, bottomResize = 0, leftResize = trblResize.left; var otherTrbl = asTRBL(other); if (trblResize.top) { if (abs$4(otherTrbl.bottom - shapeTrbl.top) < 10) { bottomResize = shapeNewTrbl.top - otherTrbl.bottom; } if (abs$4(otherTrbl.top - shapeTrbl.top) < 5) { topResize = shapeNewTrbl.top - otherTrbl.top; } } if (trblResize.bottom) { if (abs$4(otherTrbl.top - shapeTrbl.bottom) < 10) { topResize = shapeNewTrbl.bottom - otherTrbl.top; } if (abs$4(otherTrbl.bottom - shapeTrbl.bottom) < 5) { bottomResize = shapeNewTrbl.bottom - otherTrbl.bottom; } } if (topResize || rightResize || bottomResize || leftResize) { resizeNeeded.push({ shape: other, newBounds: resizeTRBL(other, { top: topResize, right: rightResize, bottom: bottomResize, left: leftResize }) }); } }); return resizeNeeded; } /** * A provider for BPMN 2.0 elements context pad */ function ContextPadProvider( config, injector, eventBus, contextPad, modeling, elementFactory, connect, create, popupMenu, canvas, rules, translate) { config = config || {}; contextPad.registerProvider(this); this._contextPad = contextPad; this._modeling = modeling; this._elementFactory = elementFactory; this._connect = connect; this._create = create; this._popupMenu = popupMenu; this._canvas = canvas; this._rules = rules; this._translate = translate; if (config.autoPlace !== false) { this._autoPlace = injector.get('autoPlace', false); } eventBus.on('create.end', 250, function(event) { var context = event.context, shape = context.shape; if (!hasPrimaryModifier(event) || !contextPad.isOpen(shape)) { return; } var entries = contextPad.getEntries(shape); if (entries.replace) { entries.replace.action.click(event, shape); } }); } ContextPadProvider.$inject = [ 'config.contextPad', 'injector', 'eventBus', 'contextPad', 'modeling', 'elementFactory', 'connect', 'create', 'popupMenu', 'canvas', 'rules', 'translate' ]; ContextPadProvider.prototype.getMultiElementContextPadEntries = function(elements) { var modeling = this._modeling; var actions = {}; if (this._isDeleteAllowed(elements)) { assign(actions, { 'delete': { group: 'edit', className: 'bpmn-icon-trash', title: this._translate('Remove'), action: { click: function(event, elements) { modeling.removeElements(elements.slice()); } } } }); } return actions; }; /** * @param {djs.model.Base[]} elements * @return {boolean} */ ContextPadProvider.prototype._isDeleteAllowed = function(elements) { var baseAllowed = this._rules.allowed('elements.delete', { elements: elements }); if (isArray$3(baseAllowed)) { return every(baseAllowed, function(element) { return includes$7(baseAllowed, element); }); } return baseAllowed; }; ContextPadProvider.prototype.getContextPadEntries = function(element) { var contextPad = this._contextPad, modeling = this._modeling, elementFactory = this._elementFactory, connect = this._connect, create = this._create, popupMenu = this._popupMenu, canvas = this._canvas, rules = this._rules, autoPlace = this._autoPlace, translate = this._translate; var actions = {}; if (element.type === 'label') { return actions; } var businessObject = element.businessObject; function startConnect(event, element) { connect.start(event, element); } function removeElement(e, element) { modeling.removeElements([ element ]); } function getReplaceMenuPosition(element) { var Y_OFFSET = 5; var diagramContainer = canvas.getContainer(), pad = contextPad.getPad(element).html; var diagramRect = diagramContainer.getBoundingClientRect(), padRect = pad.getBoundingClientRect(); var top = padRect.top - diagramRect.top; var left = padRect.left - diagramRect.left; var pos = { x: left, y: top + padRect.height + Y_OFFSET }; return pos; } /** * Create an append action * * @param {string} type * @param {string} className * @param {string} [title] * @param {Object} [options] * * @return {Object} descriptor */ function appendAction(type, className, title, options) { if (typeof title !== 'string') { options = title; title = translate('Append {type}', { type: type.replace(/^bpmn:/, '') }); } function appendStart(event, element) { var shape = elementFactory.createShape(assign({ type: type }, options)); create.start(event, shape, { source: element }); } var append = autoPlace ? function(event, element) { var shape = elementFactory.createShape(assign({ type: type }, options)); autoPlace.append(element, shape); } : appendStart; return { group: 'model', className: className, title: title, action: { dragstart: appendStart, click: append } }; } function splitLaneHandler(count) { return function(event, element) { // actual split modeling.splitLane(element, count); // refresh context pad after split to // get rid of split icons contextPad.open(element, true); }; } if (isAny(businessObject, [ 'bpmn:Lane', 'bpmn:Participant' ]) && isExpanded(element)) { var childLanes = getChildLanes(element); assign(actions, { 'lane-insert-above': { group: 'lane-insert-above', className: 'bpmn-icon-lane-insert-above', title: translate('Add Lane above'), action: { click: function(event, element) { modeling.addLane(element, 'top'); } } } }); if (childLanes.length < 2) { if (element.height >= 120) { assign(actions, { 'lane-divide-two': { group: 'lane-divide', className: 'bpmn-icon-lane-divide-two', title: translate('Divide into two Lanes'), action: { click: splitLaneHandler(2) } } }); } if (element.height >= 180) { assign(actions, { 'lane-divide-three': { group: 'lane-divide', className: 'bpmn-icon-lane-divide-three', title: translate('Divide into three Lanes'), action: { click: splitLaneHandler(3) } } }); } } assign(actions, { 'lane-insert-below': { group: 'lane-insert-below', className: 'bpmn-icon-lane-insert-below', title: translate('Add Lane below'), action: { click: function(event, element) { modeling.addLane(element, 'bottom'); } } } }); } if (is$1(businessObject, 'bpmn:FlowNode')) { if (is$1(businessObject, 'bpmn:EventBasedGateway')) { assign(actions, { 'append.receive-task': appendAction( 'bpmn:ReceiveTask', 'bpmn-icon-receive-task', translate('Append ReceiveTask') ), 'append.message-intermediate-event': appendAction( 'bpmn:IntermediateCatchEvent', 'bpmn-icon-intermediate-event-catch-message', translate('Append MessageIntermediateCatchEvent'), { eventDefinitionType: 'bpmn:MessageEventDefinition' } ), 'append.timer-intermediate-event': appendAction( 'bpmn:IntermediateCatchEvent', 'bpmn-icon-intermediate-event-catch-timer', translate('Append TimerIntermediateCatchEvent'), { eventDefinitionType: 'bpmn:TimerEventDefinition' } ), 'append.condition-intermediate-event': appendAction( 'bpmn:IntermediateCatchEvent', 'bpmn-icon-intermediate-event-catch-condition', translate('Append ConditionIntermediateCatchEvent'), { eventDefinitionType: 'bpmn:ConditionalEventDefinition' } ), 'append.signal-intermediate-event': appendAction( 'bpmn:IntermediateCatchEvent', 'bpmn-icon-intermediate-event-catch-signal', translate('Append SignalIntermediateCatchEvent'), { eventDefinitionType: 'bpmn:SignalEventDefinition' } ) }); } else if (isEventType(businessObject, 'bpmn:BoundaryEvent', 'bpmn:CompensateEventDefinition')) { assign(actions, { 'append.compensation-activity': appendAction( 'bpmn:Task', 'bpmn-icon-task', translate('Append compensation activity'), { isForCompensation: true } ) }); } else if (!is$1(businessObject, 'bpmn:EndEvent') && !businessObject.isForCompensation && !isEventType(businessObject, 'bpmn:IntermediateThrowEvent', 'bpmn:LinkEventDefinition') && !isEventSubProcess(businessObject)) { assign(actions, { 'append.end-event': appendAction( 'bpmn:EndEvent', 'bpmn-icon-end-event-none', translate('Append EndEvent') ), 'append.gateway': appendAction( 'bpmn:ExclusiveGateway', 'bpmn-icon-gateway-none', translate('Append Gateway') ), 'append.append-task': appendAction( 'bpmn:Task', 'bpmn-icon-task', translate('Append Task') ), 'append.intermediate-event': appendAction( 'bpmn:IntermediateThrowEvent', 'bpmn-icon-intermediate-event-none', translate('Append Intermediate/Boundary Event') ) }); } } if (!popupMenu.isEmpty(element, 'bpmn-replace')) { // Replace menu entry assign(actions, { 'replace': { group: 'edit', className: 'bpmn-icon-screw-wrench', title: translate('Change type'), action: { click: function(event, element) { var position = assign(getReplaceMenuPosition(element), { cursor: { x: event.x, y: event.y } }); popupMenu.open(element, 'bpmn-replace', position); } } } }); } if (is$1(businessObject, 'bpmn:SequenceFlow')) { assign(actions, { 'append.text-annotation': appendAction( 'bpmn:TextAnnotation', 'bpmn-icon-text-annotation' ) }); } if ( isAny(businessObject, [ 'bpmn:FlowNode', 'bpmn:InteractionNode', 'bpmn:DataObjectReference', 'bpmn:DataStoreReference', ]) ) { assign(actions, { 'append.text-annotation': appendAction( 'bpmn:TextAnnotation', 'bpmn-icon-text-annotation' ), 'connect': { group: 'connect', className: 'bpmn-icon-connection-multi', title: translate( 'Connect using ' + (businessObject.isForCompensation ? '' : 'Sequence/MessageFlow or ') + 'Association' ), action: { click: startConnect, dragstart: startConnect, }, }, }); } if (is$1(businessObject, 'bpmn:TextAnnotation')) { assign(actions, { 'connect': { group: 'connect', className: 'bpmn-icon-connection-multi', title: translate('Connect using Association'), action: { click: startConnect, dragstart: startConnect, }, }, }); } if (isAny(businessObject, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ])) { assign(actions, { 'connect': { group: 'connect', className: 'bpmn-icon-connection-multi', title: translate('Connect using DataInputAssociation'), action: { click: startConnect, dragstart: startConnect } } }); } if (is$1(businessObject, 'bpmn:Group')) { assign(actions, { 'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation') }); } // delete element entry, only show if allowed by rules var deleteAllowed = rules.allowed('elements.delete', { elements: [ element ] }); if (isArray$3(deleteAllowed)) { // was the element returned as a deletion candidate? deleteAllowed = deleteAllowed[0] === element; } if (deleteAllowed) { assign(actions, { 'delete': { group: 'edit', className: 'bpmn-icon-trash', title: translate('Remove'), action: { click: removeElement } } }); } return actions; }; // helpers ///////// function isEventType(eventBo, type, definition) { var isType = eventBo.$instanceOf(type); var isDefinition = false; var definitions = eventBo.eventDefinitions || []; forEach$1(definitions, function(def) { if (def.$type === definition) { isDefinition = true; } }); return isType && isDefinition; } function includes$7(array, item) { return array.indexOf(item) !== -1; } var ContextPadModule = { __depends__: [ DirectEditingModule, ContextPadModule$1, SelectionModule, ConnectModule, CreateModule, PopupMenuModule ], __init__: [ 'contextPadProvider' ], contextPadProvider: [ 'type', ContextPadProvider ] }; var AXIS_DIMENSIONS = { horizontal: [ 'x', 'width' ], vertical: [ 'y', 'height' ] }; var THRESHOLD = 5; /** * Groups and filters elements and then trigger even distribution. */ function DistributeElements$1(modeling, rules) { this._modeling = modeling; this._filters = []; this.registerFilter(function(elements) { var allowed = rules.allowed('elements.distribute', { elements: elements }); if (isArray$3(allowed)) { return allowed; } return allowed ? elements : []; }); } DistributeElements$1.$inject = [ 'modeling', 'rules' ]; /** * Registers filter functions that allow external parties to filter * out certain elements. * * @param {Function} filterFn */ DistributeElements$1.prototype.registerFilter = function(filterFn) { if (typeof filterFn !== 'function') { throw new Error('the filter has to be a function'); } this._filters.push(filterFn); }; /** * Distributes the elements with a given orientation * * @param {Array} elements * @param {string} orientation */ DistributeElements$1.prototype.trigger = function(elements, orientation) { var modeling = this._modeling; var groups, distributableElements; if (elements.length < 3) { return; } this._setOrientation(orientation); distributableElements = this._filterElements(elements); groups = this._createGroups(distributableElements); // nothing to distribute if (groups.length <= 2) { return; } modeling.distributeElements(groups, this._axis, this._dimension); return groups; }; /** * Filters the elements with provided filters by external parties * * @param {Array[Elements]} elements * * @return {Array[Elements]} */ DistributeElements$1.prototype._filterElements = function(elements) { var filters = this._filters, axis = this._axis, dimension = this._dimension, distributableElements = [].concat(elements); if (!filters.length) { return elements; } forEach$1(filters, function(filterFn) { distributableElements = filterFn(distributableElements, axis, dimension); }); return distributableElements; }; /** * Create range (min, max) groups. Also tries to group elements * together that share the same range. * * @example * var distributableElements = [ * { * range: { * min: 100, * max: 200 * }, * elements: [ { id: 'shape1', .. }] * } * ] * * @param {Array} elements * * @return {Array[Objects]} */ DistributeElements$1.prototype._createGroups = function(elements) { var rangeGroups = [], self = this, axis = this._axis, dimension = this._dimension; if (!axis) { throw new Error('must have a defined "axis" and "dimension"'); } // sort by 'left->right' or 'top->bottom' var sortedElements = sortBy(elements, axis); forEach$1(sortedElements, function(element, idx) { var elementRange = self._findRange(element, axis, dimension), range; var previous = rangeGroups[rangeGroups.length - 1]; if (previous && self._hasIntersection(previous.range, elementRange)) { rangeGroups[rangeGroups.length - 1].elements.push(element); } else { range = { range: elementRange, elements: [ element ] }; rangeGroups.push(range); } }); return rangeGroups; }; /** * Maps a direction to the according axis and dimension * * @param {string} direction 'horizontal' or 'vertical' */ DistributeElements$1.prototype._setOrientation = function(direction) { var orientation = AXIS_DIMENSIONS[direction]; this._axis = orientation[0]; this._dimension = orientation[1]; }; /** * Checks if the two ranges intercept each other * * @param {Object} rangeA {min, max} * @param {Object} rangeB {min, max} * * @return {boolean} */ DistributeElements$1.prototype._hasIntersection = function(rangeA, rangeB) { return Math.max(rangeA.min, rangeA.max) >= Math.min(rangeB.min, rangeB.max) && Math.min(rangeA.min, rangeA.max) <= Math.max(rangeB.min, rangeB.max); }; /** * Returns the min and max values for an element * * @param {Bounds} element * @param {string} axis * @param {string} dimension * * @return {{ min: number, max: number }} */ DistributeElements$1.prototype._findRange = function(element) { var axis = element[this._axis], dimension = element[this._dimension]; return { min: axis + THRESHOLD, max: axis + dimension - THRESHOLD }; }; var DistributeElementsModule$1 = { __init__: [ 'distributeElements' ], distributeElements: [ 'type', DistributeElements$1 ] }; /** * Registers element exclude filters for elements that * currently do not support distribution. */ function BpmnDistributeElements(distributeElements, eventBus, rules) { RuleProvider.call(this, eventBus); } BpmnDistributeElements.$inject = [ 'distributeElements', 'eventBus', 'rules' ]; e(BpmnDistributeElements, RuleProvider); BpmnDistributeElements.prototype.init = function() { this.addRule('elements.distribute', function(context) { var elements = context.elements; elements = filter(elements, function(element) { var cannotDistribute = isAny(element, [ 'bpmn:Association', 'bpmn:BoundaryEvent', 'bpmn:DataInputAssociation', 'bpmn:DataOutputAssociation', 'bpmn:Lane', 'bpmn:MessageFlow', 'bpmn:SequenceFlow', 'bpmn:TextAnnotation' ]); return !(element.labelTarget || cannotDistribute); }); // filter out elements which are children of any of the selected elements elements = getParents$1(elements); if (elements.length < 3) { return false; } return elements; }); }; /** * To change the icons, modify the SVGs in `./resources`, execute `npx svgo -f resources --datauri enc -o dist`, * and then replace respective icons with the optimized data URIs in `./dist`. */ var icons = { horizontal: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linejoin%3Around%22%20d%3D%22M450%20400V150h900v250%22%2F%3E%3Crect%20x%3D%22150%22%20y%3D%22450%22%20width%3D%22600%22%20height%3D%221200%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%221050%22%20y%3D%22450%22%20width%3D%22600%22%20height%3D%22800%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E', vertical: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201800%201800%22%3E%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bstroke-linejoin%3Around%22%20d%3D%22M400%201350H150V450h250%22%2F%3E%3Crect%20x%3D%22450%22%20y%3D%22150%22%20width%3D%221200%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3Anone%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%22%2F%3E%3Crect%20x%3D%22450%22%20y%3D%221050%22%20width%3D%22800%22%20height%3D%22600%22%20rx%3D%221%22%20style%3D%22fill%3AcurrentColor%3Bstroke%3AcurrentColor%3Bstroke-width%3A100%3Bopacity%3A.5%22%2F%3E%3C%2Fsvg%3E', }; var LOW_PRIORITY$g = 900; /** * A provider for distribute elements popup menu. */ function DistributeElementsMenuProvider( popupMenu, distributeElements, translate, rules) { this._distributeElements = distributeElements; this._translate = translate; this._popupMenu = popupMenu; this._rules = rules; popupMenu.registerProvider('align-elements', LOW_PRIORITY$g, this); } DistributeElementsMenuProvider.$inject = [ 'popupMenu', 'distributeElements', 'translate', 'rules' ]; DistributeElementsMenuProvider.prototype.getPopupMenuEntries = function(elements) { var entries = {}; if (this._isAllowed(elements)) { assign(entries, this._getEntries(elements)); } return entries; }; DistributeElementsMenuProvider.prototype._isAllowed = function(elements) { return this._rules.allowed('elements.distribute', { elements: elements }); }; DistributeElementsMenuProvider.prototype._getEntries = function(elements) { var distributeElements = this._distributeElements, translate = this._translate, popupMenu = this._popupMenu; var entries = { 'distribute-elements-horizontal': { group: 'distribute', title: translate('Distribute elements horizontally'), className: 'bjs-align-elements-menu-entry', imageUrl: icons['horizontal'], action: function(event, entry) { distributeElements.trigger(elements, 'horizontal'); popupMenu.close(); } }, 'distribute-elements-vertical': { group: 'distribute', title: translate('Distribute elements vertically'), imageUrl: icons['vertical'], action: function(event, entry) { distributeElements.trigger(elements, 'vertical'); popupMenu.close(); } }, }; return entries; }; var DistributeElementsModule = { __depends__: [ PopupMenuModule$1, DistributeElementsModule$1 ], __init__: [ 'bpmnDistributeElements', 'distributeElementsMenuProvider' ], bpmnDistributeElements: [ 'type', BpmnDistributeElements ], distributeElementsMenuProvider: [ 'type', DistributeElementsMenuProvider ] }; var NOT_REGISTERED_ERROR = 'is not a registered action', IS_REGISTERED_ERROR = 'is already registered'; /** * An interface that provides access to modeling actions by decoupling * the one who requests the action to be triggered and the trigger itself. * * It's possible to add new actions by registering them with ´registerAction´ * and likewise unregister existing ones with ´unregisterAction´. * * * ## Life-Cycle and configuration * * The editor actions will wait for diagram initialization before * registering default actions _and_ firing an `editorActions.init` event. * * Interested parties may listen to the `editorActions.init` event with * low priority to check, which actions got registered. Other components * may use the event to register their own actions via `registerAction`. * * @param {EventBus} eventBus * @param {Injector} injector */ function EditorActions(eventBus, injector) { // initialize actions this._actions = {}; var self = this; eventBus.on('diagram.init', function() { // all diagram modules got loaded; check which ones // are available and register the respective default actions self._registerDefaultActions(injector); // ask interested parties to register available editor // actions on diagram initialization eventBus.fire('editorActions.init', { editorActions: self }); }); } EditorActions.$inject = [ 'eventBus', 'injector' ]; /** * Register default actions. * * @param {Injector} injector */ EditorActions.prototype._registerDefaultActions = function(injector) { // (1) retrieve optional components to integrate with var commandStack = injector.get('commandStack', false); var modeling = injector.get('modeling', false); var selection = injector.get('selection', false); var zoomScroll = injector.get('zoomScroll', false); var copyPaste = injector.get('copyPaste', false); var canvas = injector.get('canvas', false); var rules = injector.get('rules', false); var keyboardMove = injector.get('keyboardMove', false); var keyboardMoveSelection = injector.get('keyboardMoveSelection', false); // (2) check components and register actions if (commandStack) { this.register('undo', function() { commandStack.undo(); }); this.register('redo', function() { commandStack.redo(); }); } if (copyPaste && selection) { this.register('copy', function() { var selectedElements = selection.get(); if (selectedElements.length) { return copyPaste.copy(selectedElements); } }); } if (copyPaste) { this.register('paste', function() { copyPaste.paste(); }); } if (zoomScroll) { this.register('stepZoom', function(opts) { zoomScroll.stepZoom(opts.value); }); } if (canvas) { this.register('zoom', function(opts) { canvas.zoom(opts.value); }); } if (modeling && selection && rules) { this.register('removeSelection', function() { var selectedElements = selection.get(); if (!selectedElements.length) { return; } var allowed = rules.allowed('elements.delete', { elements: selectedElements }), removableElements; if (allowed === false) { return; } else if (isArray$3(allowed)) { removableElements = allowed; } else { removableElements = selectedElements; } if (removableElements.length) { modeling.removeElements(removableElements.slice()); } }); } if (keyboardMove) { this.register('moveCanvas', function(opts) { keyboardMove.moveCanvas(opts); }); } if (keyboardMoveSelection) { this.register('moveSelection', function(opts) { keyboardMoveSelection.moveSelection(opts.direction, opts.accelerated); }); } }; /** * Triggers a registered action * * @param {string} action * @param {Object} opts * * @return {Unknown} Returns what the registered listener returns */ EditorActions.prototype.trigger = function(action, opts) { if (!this._actions[action]) { throw error(action, NOT_REGISTERED_ERROR); } return this._actions[action](opts); }; /** * Registers a collections of actions. * The key of the object will be the name of the action. * * @example * ´´´ * var actions = { * spaceTool: function() { * spaceTool.activateSelection(); * }, * lassoTool: function() { * lassoTool.activateSelection(); * } * ]; * * editorActions.register(actions); * * editorActions.isRegistered('spaceTool'); // true * ´´´ * * @param {Object} actions */ EditorActions.prototype.register = function(actions, listener) { var self = this; if (typeof actions === 'string') { return this._registerAction(actions, listener); } forEach$1(actions, function(listener, action) { self._registerAction(action, listener); }); }; /** * Registers a listener to an action key * * @param {string} action * @param {Function} listener */ EditorActions.prototype._registerAction = function(action, listener) { if (this.isRegistered(action)) { throw error(action, IS_REGISTERED_ERROR); } this._actions[action] = listener; }; /** * Unregister an existing action * * @param {string} action */ EditorActions.prototype.unregister = function(action) { if (!this.isRegistered(action)) { throw error(action, NOT_REGISTERED_ERROR); } this._actions[action] = undefined; }; /** * Returns the number of actions that are currently registered * * @return {number} */ EditorActions.prototype.getActions = function() { return Object.keys(this._actions); }; /** * Checks wether the given action is registered * * @param {string} action * * @return {boolean} */ EditorActions.prototype.isRegistered = function(action) { return !!this._actions[action]; }; function error(action, message) { return new Error(action + ' ' + message); } var EditorActionsModule$1 = { __init__: [ 'editorActions' ], editorActions: [ 'type', EditorActions ] }; /** * Registers and executes BPMN specific editor actions. * * @param {Injector} injector */ function BpmnEditorActions(injector) { injector.invoke(EditorActions, this); } e(BpmnEditorActions, EditorActions); BpmnEditorActions.$inject = [ 'injector' ]; /** * Register default actions. * * @param {Injector} injector */ BpmnEditorActions.prototype._registerDefaultActions = function(injector) { // (0) invoke super method EditorActions.prototype._registerDefaultActions.call(this, injector); // (1) retrieve optional components to integrate with var canvas = injector.get('canvas', false); var elementRegistry = injector.get('elementRegistry', false); var selection = injector.get('selection', false); var spaceTool = injector.get('spaceTool', false); var lassoTool = injector.get('lassoTool', false); var handTool = injector.get('handTool', false); var globalConnect = injector.get('globalConnect', false); var distributeElements = injector.get('distributeElements', false); var alignElements = injector.get('alignElements', false); var directEditing = injector.get('directEditing', false); var searchPad = injector.get('searchPad', false); var modeling = injector.get('modeling', false); // (2) check components and register actions if (canvas && elementRegistry && selection) { this._registerAction('selectElements', function() { // select all elements except for the invisible // root element var rootElement = canvas.getRootElement(); var elements = elementRegistry.filter(function(element) { return element !== rootElement; }); selection.select(elements); return elements; }); } if (spaceTool) { this._registerAction('spaceTool', function() { spaceTool.toggle(); }); } if (lassoTool) { this._registerAction('lassoTool', function() { lassoTool.toggle(); }); } if (handTool) { this._registerAction('handTool', function() { handTool.toggle(); }); } if (globalConnect) { this._registerAction('globalConnectTool', function() { globalConnect.toggle(); }); } if (selection && distributeElements) { this._registerAction('distributeElements', function(opts) { var currentSelection = selection.get(), type = opts.type; if (currentSelection.length) { distributeElements.trigger(currentSelection, type); } }); } if (selection && alignElements) { this._registerAction('alignElements', function(opts) { var currentSelection = selection.get(), aligneableElements = [], type = opts.type; if (currentSelection.length) { aligneableElements = filter(currentSelection, function(element) { return !is$1(element, 'bpmn:Lane'); }); alignElements.trigger(aligneableElements, type); } }); } if (selection && modeling) { this._registerAction('setColor', function(opts) { var currentSelection = selection.get(); if (currentSelection.length) { modeling.setColor(currentSelection, opts); } }); } if (selection && directEditing) { this._registerAction('directEditing', function() { var currentSelection = selection.get(); if (currentSelection.length) { directEditing.activate(currentSelection[0]); } }); } if (searchPad) { this._registerAction('find', function() { searchPad.toggle(); }); } if (canvas && modeling) { this._registerAction('moveToOrigin', function() { var rootElement = canvas.getRootElement(), boundingBox, elements; if (is$1(rootElement, 'bpmn:Collaboration')) { elements = elementRegistry.filter(function(element) { return is$1(element.parent, 'bpmn:Collaboration'); }); } else { elements = elementRegistry.filter(function(element) { return element !== rootElement && !is$1(element.parent, 'bpmn:SubProcess'); }); } boundingBox = getBBox(elements); modeling.moveElements( elements, { x: -boundingBox.x, y: -boundingBox.y }, rootElement ); }); } }; var EditorActionsModule = { __depends__: [ EditorActionsModule$1 ], editorActions: [ 'type', BpmnEditorActions ] }; function BpmnGridSnapping(eventBus) { eventBus.on([ 'create.init', 'shape.move.init' ], function(event) { var context = event.context, shape = event.shape; if (isAny(shape, [ 'bpmn:Participant', 'bpmn:SubProcess', 'bpmn:TextAnnotation' ])) { if (!context.gridSnappingContext) { context.gridSnappingContext = {}; } context.gridSnappingContext.snapLocation = 'top-left'; } }); } BpmnGridSnapping.$inject = [ 'eventBus' ]; var SPACING = 10; function quantize(value, quantum, fn) { if (!fn) { fn = 'round'; } return Math[ fn ](value / quantum) * quantum; } var LOWER_PRIORITY$1 = 1200; var LOW_PRIORITY$f = 800; /** * Basic grid snapping that covers connecting, creating, moving, resizing shapes, moving bendpoints * and connection segments. */ function GridSnapping(elementRegistry, eventBus, config) { var active = !config || config.active !== false; this._eventBus = eventBus; var self = this; eventBus.on('diagram.init', LOW_PRIORITY$f, function() { self.setActive(active); }); eventBus.on([ 'create.move', 'create.end', 'bendpoint.move.move', 'bendpoint.move.end', 'connect.move', 'connect.end', 'connectionSegment.move.move', 'connectionSegment.move.end', 'resize.move', 'resize.end', 'shape.move.move', 'shape.move.end' ], LOWER_PRIORITY$1, function(event) { var originalEvent = event.originalEvent; if (!self.active || (originalEvent && isCmd(originalEvent))) { return; } var context = event.context, gridSnappingContext = context.gridSnappingContext; if (!gridSnappingContext) { gridSnappingContext = context.gridSnappingContext = {}; } [ 'x', 'y' ].forEach(function(axis) { var options = {}; // allow snapping with offset var snapOffset = getSnapOffset(event, axis, elementRegistry); if (snapOffset) { options.offset = snapOffset; } // allow snapping with min and max var snapConstraints = getSnapConstraints(event, axis); if (snapConstraints) { assign(options, snapConstraints); } if (!isSnapped(event, axis)) { self.snapEvent(event, axis, options); } }); }); } /** * Snap an events x or y with optional min, max and offset. * * @param {Object} event * @param {string} axis * @param {number} [options.min] * @param {number} [options.max] * @param {number} [options.offset] */ GridSnapping.prototype.snapEvent = function(event, axis, options) { var snappedValue = this.snapValue(event[ axis ], options); setSnapped(event, axis, snappedValue); }; /** * Expose grid spacing for third parties (i.e. extensions). * * @return {number} spacing of grid dots */ GridSnapping.prototype.getGridSpacing = function() { return SPACING; }; /** * Snap value with optional min, max and offset. * * @param {number} value * @param {Object} options * @param {number} [options.min] * @param {number} [options.max] * @param {number} [options.offset] */ GridSnapping.prototype.snapValue = function(value, options) { var offset = 0; if (options && options.offset) { offset = options.offset; } value += offset; value = quantize(value, SPACING); var min, max; if (options && options.min) { min = options.min; if (isNumber(min)) { min = quantize(min + offset, SPACING, 'ceil'); value = Math.max(value, min); } } if (options && options.max) { max = options.max; if (isNumber(max)) { max = quantize(max + offset, SPACING, 'floor'); value = Math.min(value, max); } } value -= offset; return value; }; GridSnapping.prototype.isActive = function() { return this.active; }; GridSnapping.prototype.setActive = function(active) { this.active = active; this._eventBus.fire('gridSnapping.toggle', { active: active }); }; GridSnapping.prototype.toggleActive = function() { this.setActive(!this.active); }; GridSnapping.$inject = [ 'elementRegistry', 'eventBus', 'config.gridSnapping' ]; // helpers ////////// /** * Get minimum and maximum snap constraints. * Constraints are cached. * * @param {Object} event * @param {Object} event.context * @param {string} axis * * @returns {boolean|Object} */ function getSnapConstraints(event, axis) { var context = event.context, createConstraints = context.createConstraints, resizeConstraints = context.resizeConstraints || {}, gridSnappingContext = context.gridSnappingContext, snapConstraints = gridSnappingContext.snapConstraints; // cache snap constraints if (snapConstraints && snapConstraints[ axis ]) { return snapConstraints[ axis ]; } if (!snapConstraints) { snapConstraints = gridSnappingContext.snapConstraints = {}; } if (!snapConstraints[ axis ]) { snapConstraints[ axis ] = {}; } var direction = context.direction; // create if (createConstraints) { if (isHorizontal$3(axis)) { snapConstraints.x.min = createConstraints.left; snapConstraints.x.max = createConstraints.right; } else { snapConstraints.y.min = createConstraints.top; snapConstraints.y.max = createConstraints.bottom; } } // resize var minResizeConstraints = resizeConstraints.min, maxResizeConstraints = resizeConstraints.max; if (minResizeConstraints) { if (isHorizontal$3(axis)) { if (isWest(direction)) { snapConstraints.x.max = minResizeConstraints.left; } else { snapConstraints.x.min = minResizeConstraints.right; } } else { if (isNorth(direction)) { snapConstraints.y.max = minResizeConstraints.top; } else { snapConstraints.y.min = minResizeConstraints.bottom; } } } if (maxResizeConstraints) { if (isHorizontal$3(axis)) { if (isWest(direction)) { snapConstraints.x.min = maxResizeConstraints.left; } else { snapConstraints.x.max = maxResizeConstraints.right; } } else { if (isNorth(direction)) { snapConstraints.y.min = maxResizeConstraints.top; } else { snapConstraints.y.max = maxResizeConstraints.bottom; } } } return snapConstraints[ axis ]; } /** * Get snap offset. * Offset is cached. * * @param {Object} event * @param {string} axis * @param {ElementRegistry} elementRegistry * * @returns {number} */ function getSnapOffset(event, axis, elementRegistry) { var context = event.context, shape = event.shape, gridSnappingContext = context.gridSnappingContext, snapLocation = gridSnappingContext.snapLocation, snapOffset = gridSnappingContext.snapOffset; // cache snap offset if (snapOffset && isNumber(snapOffset[ axis ])) { return snapOffset[ axis ]; } if (!snapOffset) { snapOffset = gridSnappingContext.snapOffset = {}; } if (!isNumber(snapOffset[ axis ])) { snapOffset[ axis ] = 0; } if (!shape) { return snapOffset[ axis ]; } if (!elementRegistry.get(shape.id)) { if (isHorizontal$3(axis)) { snapOffset[ axis ] += shape[ axis ] + shape.width / 2; } else { snapOffset[ axis ] += shape[ axis ] + shape.height / 2; } } if (!snapLocation) { return snapOffset[ axis ]; } if (axis === 'x') { if (/left/.test(snapLocation)) { snapOffset[ axis ] -= shape.width / 2; } else if (/right/.test(snapLocation)) { snapOffset[ axis ] += shape.width / 2; } } else { if (/top/.test(snapLocation)) { snapOffset[ axis ] -= shape.height / 2; } else if (/bottom/.test(snapLocation)) { snapOffset[ axis ] += shape.height / 2; } } return snapOffset[ axis ]; } function isHorizontal$3(axis) { return axis === 'x'; } function isNorth(direction) { return direction.indexOf('n') !== -1; } function isWest(direction) { return direction.indexOf('w') !== -1; } /** * Integrates resizing with grid snapping. */ function ResizeBehavior$1(eventBus, gridSnapping) { CommandInterceptor.call(this, eventBus); this._gridSnapping = gridSnapping; var self = this; this.preExecute('shape.resize', function(event) { var context = event.context, hints = context.hints || {}, autoResize = hints.autoResize; if (!autoResize) { return; } var shape = context.shape, newBounds = context.newBounds; if (isString(autoResize)) { context.newBounds = self.snapComplex(newBounds, autoResize); } else { context.newBounds = self.snapSimple(shape, newBounds); } }); } ResizeBehavior$1.$inject = [ 'eventBus', 'gridSnapping', 'modeling' ]; e(ResizeBehavior$1, CommandInterceptor); /** * Snap width and height in relation to center. * * @param {djs.model.shape} shape * @param {Bounds} newBounds * * @returns {Bounds} Snapped bounds. */ ResizeBehavior$1.prototype.snapSimple = function(shape, newBounds) { var gridSnapping = this._gridSnapping; newBounds.width = gridSnapping.snapValue(newBounds.width, { min: newBounds.width }); newBounds.height = gridSnapping.snapValue(newBounds.height, { min: newBounds.height }); newBounds.x = shape.x + (shape.width / 2) - (newBounds.width / 2); newBounds.y = shape.y + (shape.height / 2) - (newBounds.height / 2); return newBounds; }; /** * Snap x, y, width and height according to given directions. * * @param {Bounds} newBounds * @param {string} directions - Directions as {n|w|s|e}. * * @returns {Bounds} Snapped bounds. */ ResizeBehavior$1.prototype.snapComplex = function(newBounds, directions) { if (/w|e/.test(directions)) { newBounds = this.snapHorizontally(newBounds, directions); } if (/n|s/.test(directions)) { newBounds = this.snapVertically(newBounds, directions); } return newBounds; }; /** * Snap in one or both directions horizontally. * * @param {Bounds} newBounds * @param {string} directions - Directions as {n|w|s|e}. * * @returns {Bounds} Snapped bounds. */ ResizeBehavior$1.prototype.snapHorizontally = function(newBounds, directions) { var gridSnapping = this._gridSnapping, west = /w/.test(directions), east = /e/.test(directions); var snappedNewBounds = {}; snappedNewBounds.width = gridSnapping.snapValue(newBounds.width, { min: newBounds.width }); if (east) { // handle if (west) { snappedNewBounds.x = gridSnapping.snapValue(newBounds.x, { max: newBounds.x }); snappedNewBounds.width += gridSnapping.snapValue(newBounds.x - snappedNewBounds.x, { min: newBounds.x - snappedNewBounds.x }); } // handle else { newBounds.x = newBounds.x + newBounds.width - snappedNewBounds.width; } } // assign snapped x and width assign(newBounds, snappedNewBounds); return newBounds; }; /** * Snap in one or both directions vertically. * * @param {Bounds} newBounds * @param {string} directions - Directions as {n|w|s|e}. * * @returns {Bounds} Snapped bounds. */ ResizeBehavior$1.prototype.snapVertically = function(newBounds, directions) { var gridSnapping = this._gridSnapping, north = /n/.test(directions), south = /s/.test(directions); var snappedNewBounds = {}; snappedNewBounds.height = gridSnapping.snapValue(newBounds.height, { min: newBounds.height }); if (north) { // handle if (south) { snappedNewBounds.y = gridSnapping.snapValue(newBounds.y, { max: newBounds.y }); snappedNewBounds.height += gridSnapping.snapValue(newBounds.y - snappedNewBounds.y, { min: newBounds.y - snappedNewBounds.y }); } // handle else { newBounds.y = newBounds.y + newBounds.height - snappedNewBounds.height; } } // assign snapped y and height assign(newBounds, snappedNewBounds); return newBounds; }; var HIGH_PRIORITY$f = 2000; /** * Integrates space tool with grid snapping. */ function SpaceToolBehavior$1(eventBus, gridSnapping) { eventBus.on([ 'spaceTool.move', 'spaceTool.end' ], HIGH_PRIORITY$f, function(event) { var context = event.context; if (!context.initialized) { return; } var axis = context.axis; var snapped; if (axis === 'x') { // snap delta x to multiple of 10 snapped = gridSnapping.snapValue(event.dx); event.x = event.x + snapped - event.dx; event.dx = snapped; } else { // snap delta y to multiple of 10 snapped = gridSnapping.snapValue(event.dy); event.y = event.y + snapped - event.dy; event.dy = snapped; } }); } SpaceToolBehavior$1.$inject = [ 'eventBus', 'gridSnapping' ]; var GridSnappingBehaviorModule$1 = { __init__: [ 'gridSnappingResizeBehavior', 'gridSnappingSpaceToolBehavior' ], gridSnappingResizeBehavior: [ 'type', ResizeBehavior$1 ], gridSnappingSpaceToolBehavior: [ 'type', SpaceToolBehavior$1 ] }; var GridSnappingModule$1 = { __depends__: [ GridSnappingBehaviorModule$1 ], __init__: [ 'gridSnapping' ], gridSnapping: [ 'type', GridSnapping ] }; var HIGH_PRIORITY$e = 2000; function GridSnappingAutoPlaceBehavior(eventBus, gridSnapping) { eventBus.on('autoPlace', HIGH_PRIORITY$e, function(context) { var source = context.source, sourceMid = getMid(source), shape = context.shape; var position = getNewShapePosition(source, shape); [ 'x', 'y' ].forEach(function(axis) { var options = {}; // do not snap if x/y equal if (position[ axis ] === sourceMid[ axis ]) { return; } if (position[ axis ] > sourceMid[ axis ]) { options.min = position[ axis ]; } else { options.max = position[ axis ]; } if (is$1(shape, 'bpmn:TextAnnotation')) { if (isHorizontal$2(axis)) { options.offset = -shape.width / 2; } else { options.offset = -shape.height / 2; } } position[ axis ] = gridSnapping.snapValue(position[ axis ], options); }); // must be returned to be considered by auto place return position; }); } GridSnappingAutoPlaceBehavior.$inject = [ 'eventBus', 'gridSnapping' ]; // helpers ////////// function isHorizontal$2(axis) { return axis === 'x'; } var HIGHER_PRIORITY$4 = 1750; function GridSnappingParticipantBehavior(canvas, eventBus, gridSnapping) { eventBus.on([ 'create.start', 'shape.move.start' ], HIGHER_PRIORITY$4, function(event) { var context = event.context, shape = context.shape, rootElement = canvas.getRootElement(); if (!is$1(shape, 'bpmn:Participant') || !is$1(rootElement, 'bpmn:Process') || !rootElement.children.length) { return; } var createConstraints = context.createConstraints; if (!createConstraints) { return; } shape.width = gridSnapping.snapValue(shape.width, { min: shape.width }); shape.height = gridSnapping.snapValue(shape.height, { min: shape.height }); }); } GridSnappingParticipantBehavior.$inject = [ 'canvas', 'eventBus', 'gridSnapping' ]; var HIGH_PRIORITY$d = 3000; /** * Snaps connections with Manhattan layout. */ function GridSnappingLayoutConnectionBehavior(eventBus, gridSnapping, modeling) { CommandInterceptor.call(this, eventBus); this._gridSnapping = gridSnapping; var self = this; this.postExecuted([ 'connection.create', 'connection.layout' ], HIGH_PRIORITY$d, function(event) { var context = event.context, connection = context.connection, hints = context.hints || {}, waypoints = connection.waypoints; if (hints.connectionStart || hints.connectionEnd || hints.createElementsBehavior === false) { return; } if (!hasMiddleSegments(waypoints)) { return; } modeling.updateWaypoints(connection, self.snapMiddleSegments(waypoints)); }); } GridSnappingLayoutConnectionBehavior.$inject = [ 'eventBus', 'gridSnapping', 'modeling' ]; e(GridSnappingLayoutConnectionBehavior, CommandInterceptor); /** * Snap middle segments of a given connection. * * @param {Array} waypoints * * @returns {Array} */ GridSnappingLayoutConnectionBehavior.prototype.snapMiddleSegments = function(waypoints) { var gridSnapping = this._gridSnapping, snapped; waypoints = waypoints.slice(); for (var i = 1; i < waypoints.length - 2; i++) { snapped = snapSegment(gridSnapping, waypoints[i], waypoints[i + 1]); waypoints[i] = snapped[0]; waypoints[i + 1] = snapped[1]; } return waypoints; }; // helpers ////////// /** * Check whether a connection has a middle segments. * * @param {Array} waypoints * * @returns {boolean} */ function hasMiddleSegments(waypoints) { return waypoints.length > 3; } /** * Check whether an alignment is horizontal. * * @param {string} aligned * * @returns {boolean} */ function horizontallyAligned(aligned) { return aligned === 'h'; } /** * Check whether an alignment is vertical. * * @param {string} aligned * * @returns {boolean} */ function verticallyAligned(aligned) { return aligned === 'v'; } /** * Get middle segments from a given connection. * * @param {Array} waypoints * * @returns {Array} */ function snapSegment(gridSnapping, segmentStart, segmentEnd) { var aligned = pointsAligned(segmentStart, segmentEnd); var snapped = {}; if (horizontallyAligned(aligned)) { // snap horizontally snapped.y = gridSnapping.snapValue(segmentStart.y); } if (verticallyAligned(aligned)) { // snap vertically snapped.x = gridSnapping.snapValue(segmentStart.x); } if ('x' in snapped || 'y' in snapped) { segmentStart = assign({}, segmentStart, snapped); segmentEnd = assign({}, segmentEnd, snapped); } return [ segmentStart, segmentEnd ]; } var GridSnappingBehaviorModule = { __init__: [ 'gridSnappingAutoPlaceBehavior', 'gridSnappingParticipantBehavior', 'gridSnappingLayoutConnectionBehavior', ], gridSnappingAutoPlaceBehavior: [ 'type', GridSnappingAutoPlaceBehavior ], gridSnappingParticipantBehavior: [ 'type', GridSnappingParticipantBehavior ], gridSnappingLayoutConnectionBehavior: [ 'type', GridSnappingLayoutConnectionBehavior ] }; var GridSnappingModule = { __depends__: [ GridSnappingModule$1, GridSnappingBehaviorModule ], __init__: [ 'bpmnGridSnapping' ], bpmnGridSnapping: [ 'type', BpmnGridSnapping ] }; var LABEL_WIDTH = 30, LABEL_HEIGHT = 30; /** * BPMN-specific hit zones and interaction fixes. * * @param {EventBus} eventBus * @param {InteractionEvents} interactionEvents */ function BpmnInteractionEvents(eventBus, interactionEvents) { this._interactionEvents = interactionEvents; var self = this; eventBus.on([ 'interactionEvents.createHit', 'interactionEvents.updateHit' ], function(context) { var element = context.element, gfx = context.gfx; if (is$1(element, 'bpmn:Lane')) { return self.createParticipantHit(element, gfx); } else if (is$1(element, 'bpmn:Participant')) { if (isExpanded(element)) { return self.createParticipantHit(element, gfx); } else { return self.createDefaultHit(element, gfx); } } else if (is$1(element, 'bpmn:SubProcess')) { if (isExpanded(element)) { return self.createSubProcessHit(element, gfx); } else { return self.createDefaultHit(element, gfx); } } }); } BpmnInteractionEvents.$inject = [ 'eventBus', 'interactionEvents' ]; BpmnInteractionEvents.prototype.createDefaultHit = function(element, gfx) { this._interactionEvents.removeHits(gfx); this._interactionEvents.createDefaultHit(element, gfx); // indicate that we created a hit return true; }; BpmnInteractionEvents.prototype.createParticipantHit = function(element, gfx) { // remove existing hits this._interactionEvents.removeHits(gfx); // add body hit this._interactionEvents.createBoxHit(gfx, 'no-move', { width: element.width, height: element.height }); // add outline hit this._interactionEvents.createBoxHit(gfx, 'click-stroke', { width: element.width, height: element.height }); // add label hit this._interactionEvents.createBoxHit(gfx, 'all', { width: LABEL_WIDTH, height: element.height }); // indicate that we created a hit return true; }; BpmnInteractionEvents.prototype.createSubProcessHit = function(element, gfx) { // remove existing hits this._interactionEvents.removeHits(gfx); // add body hit this._interactionEvents.createBoxHit(gfx, 'no-move', { width: element.width, height: element.height }); // add outline hit this._interactionEvents.createBoxHit(gfx, 'click-stroke', { width: element.width, height: element.height }); // add label hit this._interactionEvents.createBoxHit(gfx, 'all', { width: element.width, height: LABEL_HEIGHT }); // indicate that we created a hit return true; }; var InteractionEventsModule = { __init__: [ 'bpmnInteractionEvents' ], bpmnInteractionEvents: [ 'type', BpmnInteractionEvents ] }; /** * BPMN 2.0 specific keyboard bindings. * * @param {Injector} injector */ function BpmnKeyboardBindings(injector) { injector.invoke(KeyboardBindings, this); } e(BpmnKeyboardBindings, KeyboardBindings); BpmnKeyboardBindings.$inject = [ 'injector' ]; /** * Register available keyboard bindings. * * @param {Keyboard} keyboard * @param {EditorActions} editorActions */ BpmnKeyboardBindings.prototype.registerBindings = function(keyboard, editorActions) { // inherit default bindings KeyboardBindings.prototype.registerBindings.call(this, keyboard, editorActions); /** * Add keyboard binding if respective editor action * is registered. * * @param {string} action name * @param {Function} fn that implements the key binding */ function addListener(action, fn) { if (editorActions.isRegistered(action)) { keyboard.addListener(fn); } } // select all elements // CTRL + A addListener('selectElements', function(context) { var event = context.keyEvent; if (keyboard.isKey([ 'a', 'A' ], event) && keyboard.isCmd(event)) { editorActions.trigger('selectElements'); return true; } }); // search labels // CTRL + F addListener('find', function(context) { var event = context.keyEvent; if (keyboard.isKey([ 'f', 'F' ], event) && keyboard.isCmd(event)) { editorActions.trigger('find'); return true; } }); // activate space tool // S addListener('spaceTool', function(context) { var event = context.keyEvent; if (keyboard.hasModifier(event)) { return; } if (keyboard.isKey([ 's', 'S' ], event)) { editorActions.trigger('spaceTool'); return true; } }); // activate lasso tool // L addListener('lassoTool', function(context) { var event = context.keyEvent; if (keyboard.hasModifier(event)) { return; } if (keyboard.isKey([ 'l', 'L' ], event)) { editorActions.trigger('lassoTool'); return true; } }); // activate hand tool // H addListener('handTool', function(context) { var event = context.keyEvent; if (keyboard.hasModifier(event)) { return; } if (keyboard.isKey([ 'h', 'H' ], event)) { editorActions.trigger('handTool'); return true; } }); // activate global connect tool // C addListener('globalConnectTool', function(context) { var event = context.keyEvent; if (keyboard.hasModifier(event)) { return; } if (keyboard.isKey([ 'c', 'C' ], event)) { editorActions.trigger('globalConnectTool'); return true; } }); // activate direct editing // E addListener('directEditing', function(context) { var event = context.keyEvent; if (keyboard.hasModifier(event)) { return; } if (keyboard.isKey([ 'e', 'E' ], event)) { editorActions.trigger('directEditing'); return true; } }); }; var KeyboardModule = { __depends__: [ KeyboardModule$1 ], __init__: [ 'keyboardBindings' ], keyboardBindings: [ 'type', BpmnKeyboardBindings ] }; var DEFAULT_CONFIG = { moveSpeed: 1, moveSpeedAccelerated: 10 }; var HIGHER_PRIORITY$3 = 1500; var LEFT = 'left'; var UP = 'up'; var RIGHT = 'right'; var DOWN = 'down'; var KEY_TO_DIRECTION = { ArrowLeft: LEFT, Left: LEFT, ArrowUp: UP, Up: UP, ArrowRight: RIGHT, Right: RIGHT, ArrowDown: DOWN, Down: DOWN }; var DIRECTIONS_DELTA = { left: function(speed) { return { x: -speed, y: 0 }; }, up: function(speed) { return { x: 0, y: -speed }; }, right: function(speed) { return { x: speed, y: 0 }; }, down: function(speed) { return { x: 0, y: speed }; } }; /** * Enables to move selection with keyboard arrows. * Use with Shift for modified speed (default=1, with Shift=10). * Pressed Cmd/Ctrl turns the feature off. * * @param {Object} config * @param {number} [config.moveSpeed=1] * @param {number} [config.moveSpeedAccelerated=10] * @param {Keyboard} keyboard * @param {Modeling} modeling * @param {Selection} selection */ function KeyboardMoveSelection( config, keyboard, modeling, rules, selection ) { var self = this; this._config = assign({}, DEFAULT_CONFIG, config || {}); keyboard.addListener(HIGHER_PRIORITY$3, function(event) { var keyEvent = event.keyEvent; var direction = KEY_TO_DIRECTION[keyEvent.key]; if (!direction) { return; } if (keyboard.isCmd(keyEvent)) { return; } var accelerated = keyboard.isShift(keyEvent); self.moveSelection(direction, accelerated); return true; }); /** * Move selected elements in the given direction, * optionally specifying accelerated movement. * * @param {string} direction * @param {boolean} [accelerated=false] */ this.moveSelection = function(direction, accelerated) { var selectedElements = selection.get(); if (!selectedElements.length) { return; } var speed = this._config[ accelerated ? 'moveSpeedAccelerated' : 'moveSpeed' ]; var delta = DIRECTIONS_DELTA[direction](speed); var canMove = rules.allowed('elements.move', { shapes: selectedElements }); if (canMove) { modeling.moveElements(selectedElements, delta); } }; } KeyboardMoveSelection.$inject = [ 'config.keyboardMoveSelection', 'keyboard', 'modeling', 'rules', 'selection' ]; var KeyboardMoveSelectionModule = { __depends__: [ KeyboardModule$1, SelectionModule ], __init__: [ 'keyboardMoveSelection' ], keyboardMoveSelection: [ 'type', KeyboardMoveSelection ] }; var DEFAULT_MIN_WIDTH = 10; /** * A component that provides resizing of shapes on the canvas. * * The following components are part of shape resize: * * * adding resize handles, * * creating a visual during resize * * checking resize rules * * committing a change once finished * * * ## Customizing * * It's possible to customize the resizing behaviour by intercepting 'resize.start' * and providing the following parameters through the 'context': * * * minDimensions ({ width, height }): minimum shape dimensions * * * childrenBoxPadding ({ left, top, bottom, right } || number): * gap between the minimum bounding box and the container * * f.ex: * * ```javascript * eventBus.on('resize.start', 1500, function(event) { * var context = event.context, * * context.minDimensions = { width: 140, height: 120 }; * * // Passing general padding * context.childrenBoxPadding = 30; * * // Passing padding to a specific side * context.childrenBoxPadding.left = 20; * }); * ``` */ function Resize(eventBus, rules, modeling, dragging) { this._dragging = dragging; this._rules = rules; var self = this; /** * Handle resize move by specified delta. * * @param {Object} context * @param {Point} delta */ function handleMove(context, delta) { var shape = context.shape, direction = context.direction, resizeConstraints = context.resizeConstraints, newBounds; context.delta = delta; newBounds = resizeBounds$1(shape, direction, delta); // ensure constraints during resize context.newBounds = ensureConstraints$1(newBounds, resizeConstraints); // update + cache executable state context.canExecute = self.canResize(context); } /** * Handle resize start. * * @param {Object} context */ function handleStart(context) { var resizeConstraints = context.resizeConstraints, // evaluate minBounds for backwards compatibility minBounds = context.minBounds; if (resizeConstraints !== undefined) { return; } if (minBounds === undefined) { minBounds = self.computeMinResizeBox(context); } context.resizeConstraints = { min: asTRBL(minBounds) }; } /** * Handle resize end. * * @param {Object} context */ function handleEnd(context) { var shape = context.shape, canExecute = context.canExecute, newBounds = context.newBounds; if (canExecute) { // ensure we have actual pixel values for new bounds // (important when zoom level was > 1 during move) newBounds = roundBounds(newBounds); if (!boundsChanged(shape, newBounds)) { // no resize necessary return; } // perform the actual resize modeling.resizeShape(shape, newBounds); } } eventBus.on('resize.start', function(event) { handleStart(event.context); }); eventBus.on('resize.move', function(event) { var delta = { x: event.dx, y: event.dy }; handleMove(event.context, delta); }); eventBus.on('resize.end', function(event) { handleEnd(event.context); }); } Resize.prototype.canResize = function(context) { var rules = this._rules; var ctx = pick(context, [ 'newBounds', 'shape', 'delta', 'direction' ]); return rules.allowed('shape.resize', ctx); }; /** * Activate a resize operation. * * You may specify additional contextual information and must specify a * resize direction during activation of the resize event. * * @param {MouseEvent} event * @param {djs.model.Shape} shape * @param {Object|string} contextOrDirection */ Resize.prototype.activate = function(event, shape, contextOrDirection) { var dragging = this._dragging, context, direction; if (typeof contextOrDirection === 'string') { contextOrDirection = { direction: contextOrDirection }; } context = assign({ shape: shape }, contextOrDirection); direction = context.direction; if (!direction) { throw new Error('must provide a direction (n|w|s|e|nw|se|ne|sw)'); } dragging.init(event, getReferencePoint$1(shape, direction), 'resize', { autoActivate: true, cursor: getCursor(direction), data: { shape: shape, context: context } }); }; Resize.prototype.computeMinResizeBox = function(context) { var shape = context.shape, direction = context.direction, minDimensions, childrenBounds; minDimensions = context.minDimensions || { width: DEFAULT_MIN_WIDTH, height: DEFAULT_MIN_WIDTH }; // get children bounds childrenBounds = computeChildrenBBox(shape, context.childrenBoxPadding); // get correct minimum bounds from given resize direction // basically ensures that the minBounds is max(childrenBounds, minDimensions) return getMinResizeBounds(direction, shape, minDimensions, childrenBounds); }; Resize.$inject = [ 'eventBus', 'rules', 'modeling', 'dragging' ]; // helpers ////////// function boundsChanged(shape, newBounds) { return shape.x !== newBounds.x || shape.y !== newBounds.y || shape.width !== newBounds.width || shape.height !== newBounds.height; } function getReferencePoint$1(shape, direction) { var mid = getMid(shape), trbl = asTRBL(shape); var referencePoint = { x: mid.x, y: mid.y }; if (direction.indexOf('n') !== -1) { referencePoint.y = trbl.top; } else if (direction.indexOf('s') !== -1) { referencePoint.y = trbl.bottom; } if (direction.indexOf('e') !== -1) { referencePoint.x = trbl.right; } else if (direction.indexOf('w') !== -1) { referencePoint.x = trbl.left; } return referencePoint; } function getCursor(direction) { var prefix = 'resize-'; if (direction === 'n' || direction === 's') { return prefix + 'ns'; } else if (direction === 'e' || direction === 'w') { return prefix + 'ew'; } else if (direction === 'nw' || direction === 'se') { return prefix + 'nwse'; } else { return prefix + 'nesw'; } } var MARKER_RESIZING$1 = 'djs-resizing', MARKER_RESIZE_NOT_OK = 'resize-not-ok'; var LOW_PRIORITY$e = 500; /** * Provides previews for resizing shapes when resizing. * * @param {EventBus} eventBus * @param {Canvas} canvas * @param {PreviewSupport} previewSupport */ function ResizePreview(eventBus, canvas, previewSupport) { /** * Update resizer frame. * * @param {Object} context */ function updateFrame(context) { var shape = context.shape, bounds = context.newBounds, frame = context.frame; if (!frame) { frame = context.frame = previewSupport.addFrame(shape, canvas.getActiveLayer()); canvas.addMarker(shape, MARKER_RESIZING$1); } if (bounds.width > 5) { attr(frame, { x: bounds.x, width: bounds.width }); } if (bounds.height > 5) { attr(frame, { y: bounds.y, height: bounds.height }); } if (context.canExecute) { classes(frame).remove(MARKER_RESIZE_NOT_OK); } else { classes(frame).add(MARKER_RESIZE_NOT_OK); } } /** * Remove resizer frame. * * @param {Object} context */ function removeFrame(context) { var shape = context.shape, frame = context.frame; if (frame) { remove$1(context.frame); } canvas.removeMarker(shape, MARKER_RESIZING$1); } // add and update previews eventBus.on('resize.move', LOW_PRIORITY$e, function(event) { updateFrame(event.context); }); // remove previews eventBus.on('resize.cleanup', function(event) { removeFrame(event.context); }); } ResizePreview.$inject = [ 'eventBus', 'canvas', 'previewSupport' ]; var HANDLE_OFFSET = -6, HANDLE_SIZE = 8, HANDLE_HIT_SIZE = 20; var CLS_RESIZER = 'djs-resizer'; var directions = [ 'n', 'w', 's', 'e', 'nw', 'ne', 'se', 'sw' ]; /** * This component is responsible for adding resize handles. * * @param {EventBus} eventBus * @param {Canvas} canvas * @param {Selection} selection * @param {Resize} resize */ function ResizeHandles(eventBus, canvas, selection, resize) { this._resize = resize; this._canvas = canvas; var self = this; eventBus.on('selection.changed', function(e) { var newSelection = e.newSelection; // remove old selection markers self.removeResizers(); // add new selection markers ONLY if single selection if (newSelection.length === 1) { forEach$1(newSelection, bind(self.addResizer, self)); } }); eventBus.on('shape.changed', function(e) { var shape = e.element; if (selection.isSelected(shape)) { self.removeResizers(); self.addResizer(shape); } }); } ResizeHandles.prototype.makeDraggable = function(element, gfx, direction) { var resize = this._resize; function startResize(event) { // only trigger on left mouse button if (isPrimaryButton(event)) { resize.activate(event, element, direction); } } componentEvent.bind(gfx, 'mousedown', startResize); componentEvent.bind(gfx, 'touchstart', startResize); }; ResizeHandles.prototype._createResizer = function(element, x, y, direction) { var resizersParent = this._getResizersParent(); var offset = getHandleOffset(direction); var group = create$1('g'); classes(group).add(CLS_RESIZER); classes(group).add(CLS_RESIZER + '-' + element.id); classes(group).add(CLS_RESIZER + '-' + direction); append(resizersParent, group); var visual = create$1('rect'); attr(visual, { x: -HANDLE_SIZE / 2 + offset.x, y: -HANDLE_SIZE / 2 + offset.y, width: HANDLE_SIZE, height: HANDLE_SIZE }); classes(visual).add(CLS_RESIZER + '-visual'); append(group, visual); var hit = create$1('rect'); attr(hit, { x: -HANDLE_HIT_SIZE / 2 + offset.x, y: -HANDLE_HIT_SIZE / 2 + offset.y, width: HANDLE_HIT_SIZE, height: HANDLE_HIT_SIZE }); classes(hit).add(CLS_RESIZER + '-hit'); append(group, hit); transform(group, x, y); return group; }; ResizeHandles.prototype.createResizer = function(element, direction) { var point = getReferencePoint$1(element, direction); var resizer = this._createResizer(element, point.x, point.y, direction); this.makeDraggable(element, resizer, direction); }; // resize handles implementation /////////////////////////////// /** * Add resizers for a given element. * * @param {djs.model.Element} element */ ResizeHandles.prototype.addResizer = function(element) { var self = this; if (isConnection$a(element) || !this._resize.canResize({ shape: element })) { return; } forEach$1(directions, function(direction) { self.createResizer(element, direction); }); }; /** * Remove all resizers */ ResizeHandles.prototype.removeResizers = function() { var resizersParent = this._getResizersParent(); clear(resizersParent); }; ResizeHandles.prototype._getResizersParent = function() { return this._canvas.getLayer('resizers'); }; ResizeHandles.$inject = [ 'eventBus', 'canvas', 'selection', 'resize' ]; // helpers ////////// function getHandleOffset(direction) { var offset = { x: 0, y: 0 }; if (direction.indexOf('e') !== -1) { offset.x = -HANDLE_OFFSET; } else if (direction.indexOf('w') !== -1) { offset.x = HANDLE_OFFSET; } if (direction.indexOf('s') !== -1) { offset.y = -HANDLE_OFFSET; } else if (direction.indexOf('n') !== -1) { offset.y = HANDLE_OFFSET; } return offset; } function isConnection$a(element) { return !!element.waypoints; } var ResizeModule = { __depends__: [ RulesModule$1, DraggingModule, PreviewSupportModule ], __init__: [ 'resize', 'resizePreview', 'resizeHandles' ], resize: [ 'type', Resize ], resizePreview: [ 'type', ResizePreview ], resizeHandles: [ 'type', ResizeHandles ] }; var HIGH_PRIORITY$c = 2000; function LabelEditingProvider( eventBus, bpmnFactory, canvas, directEditing, modeling, resizeHandles, textRenderer) { this._bpmnFactory = bpmnFactory; this._canvas = canvas; this._modeling = modeling; this._textRenderer = textRenderer; directEditing.registerProvider(this); // listen to dblclick on non-root elements eventBus.on('element.dblclick', function(event) { activateDirectEdit(event.element, true); }); // complete on followup canvas operation eventBus.on([ 'autoPlace.start', 'canvas.viewbox.changing', 'drag.init', 'element.mousedown', 'popupMenu.open', 'root.set', 'selection.changed' ], function(event) { if (directEditing.isActive()) { directEditing.complete(); } }); eventBus.on([ 'shape.remove', 'connection.remove' ], HIGH_PRIORITY$c, function(event) { if (directEditing.isActive(event.element)) { directEditing.cancel(); } }); // cancel on command stack changes eventBus.on([ 'commandStack.changed' ], function(e) { if (directEditing.isActive()) { directEditing.cancel(); } }); eventBus.on('directEditing.activate', function(event) { resizeHandles.removeResizers(); }); eventBus.on('create.end', 500, function(event) { var context = event.context, element = context.shape, canExecute = event.context.canExecute, isTouch = event.isTouch; // TODO(nikku): we need to find a way to support the // direct editing on mobile devices; right now this will // break for desworkflowediting on mobile devices // as it breaks the user interaction workflow // TODO(nre): we should temporarily focus the edited element // here and release the focused viewport after the direct edit // operation is finished if (isTouch) { return; } if (!canExecute) { return; } if (context.hints && context.hints.createElementsBehavior === false) { return; } activateDirectEdit(element); }); eventBus.on('autoPlace.end', 500, function(event) { activateDirectEdit(event.shape); }); function activateDirectEdit(element, force) { if (force || isAny(element, [ 'bpmn:Task', 'bpmn:TextAnnotation' ]) || isCollapsedSubProcess(element)) { directEditing.activate(element); } } } LabelEditingProvider.$inject = [ 'eventBus', 'bpmnFactory', 'canvas', 'directEditing', 'modeling', 'resizeHandles', 'textRenderer' ]; /** * Activate direct editing for activities and text annotations. * * @param {djs.model.Base} element * * @return {Object} an object with properties bounds (position and size), text and options */ LabelEditingProvider.prototype.activate = function(element) { // text var text = getLabel(element); if (text === undefined) { return; } var context = { text: text }; // bounds var bounds = this.getEditingBBox(element); assign(context, bounds); var options = {}; // tasks if ( isAny(element, [ 'bpmn:Task', 'bpmn:Participant', 'bpmn:Lane', 'bpmn:CallActivity' ]) || isCollapsedSubProcess(element) ) { assign(options, { centerVertically: true }); } // external labels if (isLabelExternal(element)) { assign(options, { autoResize: true }); } // text annotations if (is$1(element, 'bpmn:TextAnnotation')) { assign(options, { resizable: true, autoResize: true }); } assign(context, { options: options }); return context; }; /** * Get the editing bounding box based on the element's size and position * * @param {djs.model.Base} element * * @return {Object} an object containing information about position * and size (fixed or minimum and/or maximum) */ LabelEditingProvider.prototype.getEditingBBox = function(element) { var canvas = this._canvas; var target = element.label || element; var bbox = canvas.getAbsoluteBBox(target); var mid = { x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2 }; // default position var bounds = { x: bbox.x, y: bbox.y }; var zoom = canvas.zoom(); var defaultStyle = this._textRenderer.getDefaultStyle(), externalStyle = this._textRenderer.getExternalStyle(); // take zoom into account var externalFontSize = externalStyle.fontSize * zoom, externalLineHeight = externalStyle.lineHeight, defaultFontSize = defaultStyle.fontSize * zoom, defaultLineHeight = defaultStyle.lineHeight; var style = { fontFamily: this._textRenderer.getDefaultStyle().fontFamily, fontWeight: this._textRenderer.getDefaultStyle().fontWeight }; // adjust for expanded pools AND lanes if (is$1(element, 'bpmn:Lane') || isExpandedPool(element)) { assign(bounds, { width: bbox.height, height: 30 * zoom, x: bbox.x - bbox.height / 2 + (15 * zoom), y: mid.y - (30 * zoom) / 2 }); assign(style, { fontSize: defaultFontSize + 'px', lineHeight: defaultLineHeight, paddingTop: (7 * zoom) + 'px', paddingBottom: (7 * zoom) + 'px', paddingLeft: (5 * zoom) + 'px', paddingRight: (5 * zoom) + 'px', transform: 'rotate(-90deg)' }); } // internal labels for tasks and collapsed call activities, // sub processes and participants if (isAny(element, [ 'bpmn:Task', 'bpmn:CallActivity' ]) || isCollapsedPool(element) || isCollapsedSubProcess(element)) { assign(bounds, { width: bbox.width, height: bbox.height }); assign(style, { fontSize: defaultFontSize + 'px', lineHeight: defaultLineHeight, paddingTop: (7 * zoom) + 'px', paddingBottom: (7 * zoom) + 'px', paddingLeft: (5 * zoom) + 'px', paddingRight: (5 * zoom) + 'px' }); } // internal labels for expanded sub processes if (isExpandedSubProcess$1(element)) { assign(bounds, { width: bbox.width, x: bbox.x }); assign(style, { fontSize: defaultFontSize + 'px', lineHeight: defaultLineHeight, paddingTop: (7 * zoom) + 'px', paddingBottom: (7 * zoom) + 'px', paddingLeft: (5 * zoom) + 'px', paddingRight: (5 * zoom) + 'px' }); } var width = 90 * zoom, paddingTop = 7 * zoom, paddingBottom = 4 * zoom; // external labels for events, data elements, gateways, groups and connections if (target.labelTarget) { assign(bounds, { width: width, height: bbox.height + paddingTop + paddingBottom, x: mid.x - width / 2, y: bbox.y - paddingTop }); assign(style, { fontSize: externalFontSize + 'px', lineHeight: externalLineHeight, paddingTop: paddingTop + 'px', paddingBottom: paddingBottom + 'px' }); } // external label not yet created if (isLabelExternal(target) && !hasExternalLabel(target) && !isLabel$6(target)) { var externalLabelMid = getExternalLabelMid(element); var absoluteBBox = canvas.getAbsoluteBBox({ x: externalLabelMid.x, y: externalLabelMid.y, width: 0, height: 0 }); var height = externalFontSize + paddingTop + paddingBottom; assign(bounds, { width: width, height: height, x: absoluteBBox.x - width / 2, y: absoluteBBox.y - height / 2 }); assign(style, { fontSize: externalFontSize + 'px', lineHeight: externalLineHeight, paddingTop: paddingTop + 'px', paddingBottom: paddingBottom + 'px' }); } // text annotations if (is$1(element, 'bpmn:TextAnnotation')) { assign(bounds, { width: bbox.width, height: bbox.height, minWidth: 30 * zoom, minHeight: 10 * zoom }); assign(style, { textAlign: 'left', paddingTop: (5 * zoom) + 'px', paddingBottom: (7 * zoom) + 'px', paddingLeft: (7 * zoom) + 'px', paddingRight: (5 * zoom) + 'px', fontSize: defaultFontSize + 'px', lineHeight: defaultLineHeight }); } return { bounds: bounds, style: style }; }; LabelEditingProvider.prototype.update = function( element, newLabel, activeContextText, bounds) { var newBounds, bbox; if (is$1(element, 'bpmn:TextAnnotation')) { bbox = this._canvas.getAbsoluteBBox(element); newBounds = { x: element.x, y: element.y, width: element.width / bbox.width * bounds.width, height: element.height / bbox.height * bounds.height }; } if (isEmptyText$1(newLabel)) { newLabel = null; } this._modeling.updateLabel(element, newLabel, newBounds); }; // helpers ////////////////////// function isCollapsedSubProcess(element) { return is$1(element, 'bpmn:SubProcess') && !isExpanded(element); } function isExpandedSubProcess$1(element) { return is$1(element, 'bpmn:SubProcess') && isExpanded(element); } function isCollapsedPool(element) { return is$1(element, 'bpmn:Participant') && !isExpanded(element); } function isExpandedPool(element) { return is$1(element, 'bpmn:Participant') && isExpanded(element); } function isEmptyText$1(label) { return !label || !label.trim(); } var MARKER_HIDDEN = 'djs-element-hidden', MARKER_LABEL_HIDDEN = 'djs-label-hidden'; function LabelEditingPreview( eventBus, canvas, elementRegistry, pathMap) { var self = this; var defaultLayer = canvas.getDefaultLayer(); var element, absoluteElementBBox, gfx; eventBus.on('directEditing.activate', function(context) { var activeProvider = context.active; element = activeProvider.element.label || activeProvider.element; // text annotation if (is$1(element, 'bpmn:TextAnnotation')) { absoluteElementBBox = canvas.getAbsoluteBBox(element); gfx = create$1('g'); var textPathData = pathMap.getScaledPath('TEXT_ANNOTATION', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: element.height, position: { mx: 0.0, my: 0.0 } }); var path = self.path = create$1('path'); attr(path, { d: textPathData, strokeWidth: 2, stroke: getStrokeColor(element) }); append(gfx, path); append(defaultLayer, gfx); translate$2(gfx, element.x, element.y); } if (is$1(element, 'bpmn:TextAnnotation') || element.labelTarget) { canvas.addMarker(element, MARKER_HIDDEN); } else if (is$1(element, 'bpmn:Task') || is$1(element, 'bpmn:CallActivity') || is$1(element, 'bpmn:SubProcess') || is$1(element, 'bpmn:Participant')) { canvas.addMarker(element, MARKER_LABEL_HIDDEN); } }); eventBus.on('directEditing.resize', function(context) { // text annotation if (is$1(element, 'bpmn:TextAnnotation')) { var height = context.height, dy = context.dy; var newElementHeight = Math.max(element.height / absoluteElementBBox.height * (height + dy), 0); var textPathData = pathMap.getScaledPath('TEXT_ANNOTATION', { xScaleFactor: 1, yScaleFactor: 1, containerWidth: element.width, containerHeight: newElementHeight, position: { mx: 0.0, my: 0.0 } }); attr(self.path, { d: textPathData }); } }); eventBus.on([ 'directEditing.complete', 'directEditing.cancel' ], function(context) { var activeProvider = context.active; if (activeProvider) { canvas.removeMarker(activeProvider.element.label || activeProvider.element, MARKER_HIDDEN); canvas.removeMarker(element, MARKER_LABEL_HIDDEN); } element = undefined; absoluteElementBBox = undefined; if (gfx) { remove$1(gfx); gfx = undefined; } }); } LabelEditingPreview.$inject = [ 'eventBus', 'canvas', 'elementRegistry', 'pathMap' ]; // helpers /////////////////// function getStrokeColor(element, defaultColor) { var di = getDi(element); return di.get('stroke') || defaultColor || 'black'; } var LabelEditingModule = { __depends__: [ ChangeSupportModule, ResizeModule, DirectEditingModule ], __init__: [ 'labelEditingProvider', 'labelEditingPreview' ], labelEditingProvider: [ 'type', LabelEditingProvider ], labelEditingPreview: [ 'type', LabelEditingPreview ] }; var ALIGNMENTS = [ 'top', 'bottom', 'left', 'right' ]; var ELEMENT_LABEL_DISTANCE = 10; /** * A component that makes sure that external labels are added * together with respective elements and properly updated (DI wise) * during move. * * @param {EventBus} eventBus * @param {Modeling} modeling */ function AdaptiveLabelPositioningBehavior(eventBus, modeling) { CommandInterceptor.call(this, eventBus); this.postExecuted([ 'connection.create', 'connection.layout', 'connection.updateWaypoints' ], function(event) { var context = event.context, connection = context.connection, source = connection.source, target = connection.target, hints = context.hints || {}; if (hints.createElementsBehavior !== false) { checkLabelAdjustment(source); checkLabelAdjustment(target); } }); this.postExecuted([ 'label.create' ], function(event) { var context = event.context, shape = context.shape, hints = context.hints || {}; if (hints.createElementsBehavior !== false) { checkLabelAdjustment(shape.labelTarget); } }); this.postExecuted([ 'elements.create' ], function(event) { var context = event.context, elements = context.elements, hints = context.hints || {}; if (hints.createElementsBehavior !== false) { elements.forEach(function(element) { checkLabelAdjustment(element); }); } }); function checkLabelAdjustment(element) { // skip non-existing labels if (!hasExternalLabel(element)) { return; } var optimalPosition = getOptimalPosition(element); // no optimal position found if (!optimalPosition) { return; } adjustLabelPosition(element, optimalPosition); } function adjustLabelPosition(element, orientation) { var elementMid = getMid(element), label = element.label, labelMid = getMid(label); // ignore labels that are being created if (!label.parent) { return; } var elementTrbl = asTRBL(element); var newLabelMid; switch (orientation) { case 'top': newLabelMid = { x: elementMid.x, y: elementTrbl.top - ELEMENT_LABEL_DISTANCE - label.height / 2 }; break; case 'left': newLabelMid = { x: elementTrbl.left - ELEMENT_LABEL_DISTANCE - label.width / 2, y: elementMid.y }; break; case 'bottom': newLabelMid = { x: elementMid.x, y: elementTrbl.bottom + ELEMENT_LABEL_DISTANCE + label.height / 2 }; break; case 'right': newLabelMid = { x: elementTrbl.right + ELEMENT_LABEL_DISTANCE + label.width / 2, y: elementMid.y }; break; } var delta$1 = delta(newLabelMid, labelMid); modeling.moveShape(label, delta$1); } } e(AdaptiveLabelPositioningBehavior, CommandInterceptor); AdaptiveLabelPositioningBehavior.$inject = [ 'eventBus', 'modeling' ]; // helpers ////////////////////// /** * Return alignments which are taken by a boundary's host element * * @param {Shape} element * * @return {Array} */ function getTakenHostAlignments(element) { var hostElement = element.host, elementMid = getMid(element), hostOrientation = getOrientation(elementMid, hostElement); var freeAlignments; // check whether there is a multi-orientation, e.g. 'top-left' if (hostOrientation.indexOf('-') >= 0) { freeAlignments = hostOrientation.split('-'); } else { freeAlignments = [ hostOrientation ]; } var takenAlignments = ALIGNMENTS.filter(function(alignment) { return freeAlignments.indexOf(alignment) === -1; }); return takenAlignments; } /** * Return alignments which are taken by related connections * * @param {Shape} element * * @return {Array} */ function getTakenConnectionAlignments(element) { var elementMid = getMid(element); var takenAlignments = [].concat( element.incoming.map(function(c) { return c.waypoints[c.waypoints.length - 2 ]; }), element.outgoing.map(function(c) { return c.waypoints[1]; }) ).map(function(point) { return getApproximateOrientation(elementMid, point); }); return takenAlignments; } /** * Return the optimal label position around an element * or _undefined_, if none was found. * * @param {Shape} element * * @return {string} positioning identifier */ function getOptimalPosition(element) { var labelMid = getMid(element.label); var elementMid = getMid(element); var labelOrientation = getApproximateOrientation(elementMid, labelMid); if (!isAligned(labelOrientation)) { return; } var takenAlignments = getTakenConnectionAlignments(element); if (element.host) { var takenHostAlignments = getTakenHostAlignments(element); takenAlignments = takenAlignments.concat(takenHostAlignments); } var freeAlignments = ALIGNMENTS.filter(function(alignment) { return takenAlignments.indexOf(alignment) === -1; }); // NOTHING TO DO; label already aligned a.O.K. if (freeAlignments.indexOf(labelOrientation) !== -1) { return; } return freeAlignments[0]; } function getApproximateOrientation(p0, p1) { return getOrientation(p1, p0, 5); } function isAligned(orientation) { return ALIGNMENTS.indexOf(orientation) !== -1; } function AppendBehavior(eventBus, elementFactory, bpmnRules) { CommandInterceptor.call(this, eventBus); // assign correct shape position unless already set this.preExecute('shape.append', function(context) { var source = context.source, shape = context.shape; if (!context.position) { if (is$1(shape, 'bpmn:TextAnnotation')) { context.position = { x: source.x + source.width / 2 + 75, y: source.y - (50) - shape.height / 2 }; } else { context.position = { x: source.x + source.width + 80 + shape.width / 2, y: source.y + source.height / 2 }; } } }, true); } e(AppendBehavior, CommandInterceptor); AppendBehavior.$inject = [ 'eventBus', 'elementFactory', 'bpmnRules' ]; function AssociationBehavior(injector, modeling) { injector.invoke(CommandInterceptor, this); this.postExecute('shape.move', function(context) { var newParent = context.newParent, shape = context.shape; var associations = filter(shape.incoming.concat(shape.outgoing), function(connection) { return is$1(connection, 'bpmn:Association'); }); forEach$1(associations, function(association) { modeling.moveConnection(association, { x: 0, y: 0 }, newParent); }); }, true); } e(AssociationBehavior, CommandInterceptor); AssociationBehavior.$inject = [ 'injector', 'modeling' ]; var LOW_PRIORITY$d = 500; /** * Replace intermediate event with boundary event when creating or moving results in attached event. */ function AttachEventBehavior(bpmnReplace, injector) { injector.invoke(CommandInterceptor, this); this._bpmnReplace = bpmnReplace; var self = this; this.postExecuted('elements.create', LOW_PRIORITY$d, function(context) { var elements = context.elements; elements = elements.filter(function(shape) { var host = shape.host; return shouldReplace$1(shape, host); }); if (elements.length !== 1) { return; } elements.map(function(element) { return elements.indexOf(element); }).forEach(function(index) { var host = elements[ index ]; context.elements[ index ] = self.replaceShape(elements[ index ], host); }); }, true); this.preExecute('elements.move', LOW_PRIORITY$d, function(context) { var shapes = context.shapes, host = context.newHost; if (shapes.length !== 1) { return; } var shape = shapes[0]; if (shouldReplace$1(shape, host)) { context.shapes = [ self.replaceShape(shape, host) ]; } }, true); } AttachEventBehavior.$inject = [ 'bpmnReplace', 'injector' ]; e(AttachEventBehavior, CommandInterceptor); AttachEventBehavior.prototype.replaceShape = function(shape, host) { var eventDefinition = getEventDefinition$1(shape); var boundaryEvent = { type: 'bpmn:BoundaryEvent', host: host }; if (eventDefinition) { boundaryEvent.eventDefinitionType = eventDefinition.$type; } return this._bpmnReplace.replaceElement(shape, boundaryEvent, { layoutConnection: false }); }; // helpers ////////// function getEventDefinition$1(element) { var businessObject = getBusinessObject(element), eventDefinitions = businessObject.eventDefinitions; return eventDefinitions && eventDefinitions[0]; } function shouldReplace$1(shape, host) { return !isLabel$6(shape) && isAny(shape, [ 'bpmn:IntermediateThrowEvent', 'bpmn:IntermediateCatchEvent' ]) && !!host; } /** * BPMN specific boundary event behavior */ function BoundaryEventBehavior(eventBus, modeling) { CommandInterceptor.call(this, eventBus); function getBoundaryEvents(element) { return filter(element.attachers, function(attacher) { return is$1(attacher, 'bpmn:BoundaryEvent'); }); } // remove after connecting to event-based gateway this.postExecute('connection.create', function(event) { var source = event.context.source, target = event.context.target, boundaryEvents = getBoundaryEvents(target); if ( is$1(source, 'bpmn:EventBasedGateway') && is$1(target, 'bpmn:ReceiveTask') && boundaryEvents.length > 0 ) { modeling.removeElements(boundaryEvents); } }); // remove after replacing connected gateway with event-based gateway this.postExecute('connection.reconnect', function(event) { var oldSource = event.context.oldSource, newSource = event.context.newSource; if (is$1(oldSource, 'bpmn:Gateway') && is$1(newSource, 'bpmn:EventBasedGateway')) { forEach$1(newSource.outgoing, function(connection) { var target = connection.target, attachedboundaryEvents = getBoundaryEvents(target); if (is$1(target, 'bpmn:ReceiveTask') && attachedboundaryEvents.length > 0) { modeling.removeElements(attachedboundaryEvents); } }); } }); } BoundaryEventBehavior.$inject = [ 'eventBus', 'modeling' ]; e(BoundaryEventBehavior, CommandInterceptor); function CreateBehavior(injector) { injector.invoke(CommandInterceptor, this); this.preExecute('shape.create', 1500, function(event) { var context = event.context, parent = context.parent, shape = context.shape; if (is$1(parent, 'bpmn:Lane') && !is$1(shape, 'bpmn:Lane')) { context.parent = getParent(parent, 'bpmn:Participant'); } }); } CreateBehavior.$inject = [ 'injector' ]; e(CreateBehavior, CommandInterceptor); /** * BPMN specific create data object behavior */ function CreateDataObjectBehavior(eventBus, bpmnFactory, moddle) { CommandInterceptor.call(this, eventBus); this.preExecute('shape.create', function(event) { var context = event.context, shape = context.shape; if (is$1(shape, 'bpmn:DataObjectReference') && shape.type !== 'label') { // create a DataObject every time a DataObjectReference is created var dataObject = bpmnFactory.create('bpmn:DataObject'); // set the reference to the DataObject shape.businessObject.dataObjectRef = dataObject; } }); } CreateDataObjectBehavior.$inject = [ 'eventBus', 'bpmnFactory', 'moddle' ]; e(CreateDataObjectBehavior, CommandInterceptor); var HORIZONTAL_PARTICIPANT_PADDING = 20, VERTICAL_PARTICIPANT_PADDING = 20; var PARTICIPANT_BORDER_WIDTH = 30; var HIGH_PRIORITY$b = 2000; /** * BPMN-specific behavior for creating participants. */ function CreateParticipantBehavior(canvas, eventBus, modeling) { CommandInterceptor.call(this, eventBus); // fit participant eventBus.on([ 'create.start', 'shape.move.start' ], HIGH_PRIORITY$b, function(event) { var context = event.context, shape = context.shape, rootElement = canvas.getRootElement(); if (!is$1(shape, 'bpmn:Participant') || !is$1(rootElement, 'bpmn:Process') || !rootElement.children.length) { return; } // ignore connections, groups and labels var children = rootElement.children.filter(function(element) { return !is$1(element, 'bpmn:Group') && !isLabel$6(element) && !isConnection$9(element); }); // ensure for available children to calculate bounds if (!children.length) { return; } var childrenBBox = getBBox(children); var participantBounds = getParticipantBounds(shape, childrenBBox); // assign width and height assign(shape, participantBounds); // assign create constraints context.createConstraints = getParticipantCreateConstraints(shape, childrenBBox); }); // force hovering process when creating first participant eventBus.on('create.start', HIGH_PRIORITY$b, function(event) { var context = event.context, shape = context.shape, rootElement = canvas.getRootElement(), rootElementGfx = canvas.getGraphics(rootElement); function ensureHoveringProcess(event) { event.element = rootElement; event.gfx = rootElementGfx; } if (is$1(shape, 'bpmn:Participant') && is$1(rootElement, 'bpmn:Process')) { eventBus.on('element.hover', HIGH_PRIORITY$b, ensureHoveringProcess); eventBus.once('create.cleanup', function() { eventBus.off('element.hover', ensureHoveringProcess); }); } }); // turn process into collaboration when creating first participant function getOrCreateCollaboration() { var rootElement = canvas.getRootElement(); if (is$1(rootElement, 'bpmn:Collaboration')) { return rootElement; } return modeling.makeCollaboration(); } // when creating mutliple elements through `elements.create` parent must be set to collaboration // and passed to `shape.create` as hint this.preExecute('elements.create', HIGH_PRIORITY$b, function(context) { var elements = context.elements, parent = context.parent, participant = findParticipant(elements), hints; if (participant && is$1(parent, 'bpmn:Process')) { context.parent = getOrCreateCollaboration(); hints = context.hints = context.hints || {}; hints.participant = participant; hints.process = parent; hints.processRef = getBusinessObject(participant).get('processRef'); } }, true); // when creating single shape through `shape.create` parent must be set to collaboration // unless it was already set through `elements.create` this.preExecute('shape.create', function(context) { var parent = context.parent, shape = context.shape; if (is$1(shape, 'bpmn:Participant') && is$1(parent, 'bpmn:Process')) { context.parent = getOrCreateCollaboration(); context.process = parent; context.processRef = getBusinessObject(shape).get('processRef'); } }, true); // #execute necessary because #preExecute not called on CommandStack#redo this.execute('shape.create', function(context) { var hints = context.hints || {}, process = context.process || hints.process, shape = context.shape, participant = hints.participant; // both shape.create and elements.create must be handled if (process && (!participant || shape === participant)) { // monkey-patch process ref getBusinessObject(shape).set('processRef', getBusinessObject(process)); } }, true); this.revert('shape.create', function(context) { var hints = context.hints || {}, process = context.process || hints.process, processRef = context.processRef || hints.processRef, shape = context.shape, participant = hints.participant; // both shape.create and elements.create must be handled if (process && (!participant || shape === participant)) { // monkey-patch process ref getBusinessObject(shape).set('processRef', processRef); } }, true); this.postExecute('shape.create', function(context) { var hints = context.hints || {}, process = context.process || context.hints.process, shape = context.shape, participant = hints.participant; if (process) { var children = process.children.slice(); // both shape.create and elements.create must be handled if (!participant) { modeling.moveElements(children, { x: 0, y: 0 }, shape); } else if (shape === participant) { modeling.moveElements(children, { x: 0, y: 0 }, participant); } } }, true); } CreateParticipantBehavior.$inject = [ 'canvas', 'eventBus', 'modeling' ]; e(CreateParticipantBehavior, CommandInterceptor); // helpers ////////// function getParticipantBounds(shape, childrenBBox) { childrenBBox = { width: childrenBBox.width + HORIZONTAL_PARTICIPANT_PADDING * 2 + PARTICIPANT_BORDER_WIDTH, height: childrenBBox.height + VERTICAL_PARTICIPANT_PADDING * 2 }; var width = Math.max(shape.width, childrenBBox.width), height = Math.max(shape.height, childrenBBox.height); return { x: -width / 2, y: -height / 2, width: width, height: height }; } function getParticipantCreateConstraints(shape, childrenBBox) { childrenBBox = asTRBL(childrenBBox); return { bottom: childrenBBox.top + shape.height / 2 - VERTICAL_PARTICIPANT_PADDING, left: childrenBBox.right - shape.width / 2 + HORIZONTAL_PARTICIPANT_PADDING, top: childrenBBox.bottom - shape.height / 2 + VERTICAL_PARTICIPANT_PADDING, right: childrenBBox.left + shape.width / 2 - HORIZONTAL_PARTICIPANT_PADDING - PARTICIPANT_BORDER_WIDTH }; } function isConnection$9(element) { return !!element.waypoints; } function findParticipant(elements) { return find(elements, function(element) { return is$1(element, 'bpmn:Participant'); }); } var TARGET_REF_PLACEHOLDER_NAME = '__targetRef_placeholder'; /** * This behavior makes sure we always set a fake * DataInputAssociation#targetRef as demanded by the BPMN 2.0 * XSD schema. * * The reference is set to a bpmn:Property{ name: '__targetRef_placeholder' } * which is created on the fly and cleaned up afterwards if not needed * anymore. * * @param {EventBus} eventBus * @param {BpmnFactory} bpmnFactory */ function DataInputAssociationBehavior(eventBus, bpmnFactory) { CommandInterceptor.call(this, eventBus); this.executed([ 'connection.create', 'connection.delete', 'connection.move', 'connection.reconnect' ], ifDataInputAssociation(fixTargetRef)); this.reverted([ 'connection.create', 'connection.delete', 'connection.move', 'connection.reconnect' ], ifDataInputAssociation(fixTargetRef)); function usesTargetRef(element, targetRef, removedConnection) { var inputAssociations = element.get('dataInputAssociations'); return find(inputAssociations, function(association) { return association !== removedConnection && association.targetRef === targetRef; }); } function getTargetRef(element, create) { var properties = element.get('properties'); var targetRefProp = find(properties, function(p) { return p.name === TARGET_REF_PLACEHOLDER_NAME; }); if (!targetRefProp && create) { targetRefProp = bpmnFactory.create('bpmn:Property', { name: TARGET_REF_PLACEHOLDER_NAME }); add(properties, targetRefProp); } return targetRefProp; } function cleanupTargetRef(element, connection) { var targetRefProp = getTargetRef(element); if (!targetRefProp) { return; } if (!usesTargetRef(element, targetRefProp, connection)) { remove(element.get('properties'), targetRefProp); } } /** * Make sure targetRef is set to a valid property or * `null` if the connection is detached. * * @param {Event} event */ function fixTargetRef(event) { var context = event.context, connection = context.connection, connectionBo = connection.businessObject, target = connection.target, targetBo = target && target.businessObject, newTarget = context.newTarget, newTargetBo = newTarget && newTarget.businessObject, oldTarget = context.oldTarget || context.target, oldTargetBo = oldTarget && oldTarget.businessObject; var dataAssociation = connection.businessObject, targetRefProp; if (oldTargetBo && oldTargetBo !== targetBo) { cleanupTargetRef(oldTargetBo, connectionBo); } if (newTargetBo && newTargetBo !== targetBo) { cleanupTargetRef(newTargetBo, connectionBo); } if (targetBo) { targetRefProp = getTargetRef(targetBo, true); dataAssociation.targetRef = targetRefProp; } else { dataAssociation.targetRef = null; } } } DataInputAssociationBehavior.$inject = [ 'eventBus', 'bpmnFactory' ]; e(DataInputAssociationBehavior, CommandInterceptor); /** * Only call the given function when the event * touches a bpmn:DataInputAssociation. * * @param {Function} fn * @return {Function} */ function ifDataInputAssociation(fn) { return function(event) { var context = event.context, connection = context.connection; if (is$1(connection, 'bpmn:DataInputAssociation')) { return fn(event); } }; } function UpdateSemanticParentHandler(bpmnUpdater) { this._bpmnUpdater = bpmnUpdater; } UpdateSemanticParentHandler.$inject = [ 'bpmnUpdater' ]; UpdateSemanticParentHandler.prototype.execute = function(context) { var dataStoreBo = context.dataStoreBo, dataStoreDi = context.dataStoreDi, newSemanticParent = context.newSemanticParent, newDiParent = context.newDiParent; context.oldSemanticParent = dataStoreBo.$parent; context.oldDiParent = dataStoreDi.$parent; // update semantic parent this._bpmnUpdater.updateSemanticParent(dataStoreBo, newSemanticParent); // update DI parent this._bpmnUpdater.updateDiParent(dataStoreDi, newDiParent); }; UpdateSemanticParentHandler.prototype.revert = function(context) { var dataStoreBo = context.dataStoreBo, dataStoreDi = context.dataStoreDi, oldSemanticParent = context.oldSemanticParent, oldDiParent = context.oldDiParent; // update semantic parent this._bpmnUpdater.updateSemanticParent(dataStoreBo, oldSemanticParent); // update DI parent this._bpmnUpdater.updateDiParent(dataStoreDi, oldDiParent); }; /** * BPMN specific data store behavior */ function DataStoreBehavior( canvas, commandStack, elementRegistry, eventBus) { CommandInterceptor.call(this, eventBus); commandStack.registerHandler('dataStore.updateContainment', UpdateSemanticParentHandler); function getFirstParticipantWithProcessRef() { return elementRegistry.filter(function(element) { return is$1(element, 'bpmn:Participant') && getBusinessObject(element).processRef; })[0]; } function getDataStores(element) { return element.children.filter(function(child) { return is$1(child, 'bpmn:DataStoreReference') && !child.labelTarget; }); } function updateDataStoreParent(dataStore, newDataStoreParent) { var dataStoreBo = dataStore.businessObject || dataStore; newDataStoreParent = newDataStoreParent || getFirstParticipantWithProcessRef(); if (newDataStoreParent) { var newDataStoreParentBo = newDataStoreParent.businessObject || newDataStoreParent; commandStack.execute('dataStore.updateContainment', { dataStoreBo: dataStoreBo, dataStoreDi: getDi(dataStore), newSemanticParent: newDataStoreParentBo.processRef || newDataStoreParentBo, newDiParent: getDi(newDataStoreParent) }); } } // disable auto-resize for data stores this.preExecute('shape.create', function(event) { var context = event.context, shape = context.shape; if (is$1(shape, 'bpmn:DataStoreReference') && shape.type !== 'label') { if (!context.hints) { context.hints = {}; } // prevent auto resizing context.hints.autoResize = false; } }); // disable auto-resize for data stores this.preExecute('elements.move', function(event) { var context = event.context, shapes = context.shapes; var dataStoreReferences = shapes.filter(function(shape) { return is$1(shape, 'bpmn:DataStoreReference'); }); if (dataStoreReferences.length) { if (!context.hints) { context.hints = {}; } // prevent auto resizing for data store references context.hints.autoResize = shapes.filter(function(shape) { return !is$1(shape, 'bpmn:DataStoreReference'); }); } }); // update parent on data store created this.postExecute('shape.create', function(event) { var context = event.context, shape = context.shape, parent = shape.parent; if (is$1(shape, 'bpmn:DataStoreReference') && shape.type !== 'label' && is$1(parent, 'bpmn:Collaboration')) { updateDataStoreParent(shape); } }); // update parent on data store moved this.postExecute('shape.move', function(event) { var context = event.context, shape = context.shape, oldParent = context.oldParent, parent = shape.parent; if (is$1(oldParent, 'bpmn:Collaboration')) { // do nothing if not necessary return; } if (is$1(shape, 'bpmn:DataStoreReference') && shape.type !== 'label' && is$1(parent, 'bpmn:Collaboration')) { var participant = is$1(oldParent, 'bpmn:Participant') ? oldParent : getAncestor(oldParent, 'bpmn:Participant'); updateDataStoreParent(shape, participant); } }); // update data store parents on participant or subprocess deleted this.postExecute('shape.delete', function(event) { var context = event.context, shape = context.shape, rootElement = canvas.getRootElement(); if (isAny(shape, [ 'bpmn:Participant', 'bpmn:SubProcess' ]) && is$1(rootElement, 'bpmn:Collaboration')) { getDataStores(rootElement) .filter(function(dataStore) { return isDescendant(dataStore, shape); }) .forEach(function(dataStore) { updateDataStoreParent(dataStore); }); } }); // update data store parents on collaboration -> process this.postExecute('canvas.updateRoot', function(event) { var context = event.context, oldRoot = context.oldRoot, newRoot = context.newRoot; var dataStores = getDataStores(oldRoot); dataStores.forEach(function(dataStore) { if (is$1(newRoot, 'bpmn:Process')) { updateDataStoreParent(dataStore, newRoot); } }); }); } DataStoreBehavior.$inject = [ 'canvas', 'commandStack', 'elementRegistry', 'eventBus', ]; e(DataStoreBehavior, CommandInterceptor); // helpers ////////// function isDescendant(descendant, ancestor) { var descendantBo = descendant.businessObject || descendant, ancestorBo = ancestor.businessObject || ancestor; while (descendantBo.$parent) { if (descendantBo.$parent === ancestorBo.processRef || ancestorBo) { return true; } descendantBo = descendantBo.$parent; } return false; } function getAncestor(element, type) { while (element.parent) { if (is$1(element.parent, type)) { return element.parent; } element = element.parent; } } var LOW_PRIORITY$c = 500; /** * BPMN specific delete lane behavior */ function DeleteLaneBehavior(eventBus, modeling, spaceTool) { CommandInterceptor.call(this, eventBus); function compensateLaneDelete(shape, oldParent) { var siblings = getChildLanes(oldParent); var topAffected = []; var bottomAffected = []; eachElement(siblings, function(element) { if (element.y > shape.y) { bottomAffected.push(element); } else { topAffected.push(element); } return element.children; }); if (!siblings.length) { return; } var offset; if (bottomAffected.length && topAffected.length) { offset = shape.height / 2; } else { offset = shape.height; } var topAdjustments, bottomAdjustments; if (topAffected.length) { topAdjustments = spaceTool.calculateAdjustments( topAffected, 'y', offset, shape.y - 10); spaceTool.makeSpace( topAdjustments.movingShapes, topAdjustments.resizingShapes, { x: 0, y: offset }, 's'); } if (bottomAffected.length) { bottomAdjustments = spaceTool.calculateAdjustments( bottomAffected, 'y', -offset, shape.y + shape.height + 10); spaceTool.makeSpace( bottomAdjustments.movingShapes, bottomAdjustments.resizingShapes, { x: 0, y: -offset }, 'n'); } } /** * Adjust sizes of other lanes after lane deletion */ this.postExecuted('shape.delete', LOW_PRIORITY$c, function(event) { var context = event.context, hints = context.hints, shape = context.shape, oldParent = context.oldParent; // only compensate lane deletes if (!is$1(shape, 'bpmn:Lane')) { return; } // compensate root deletes only if (hints && hints.nested) { return; } compensateLaneDelete(shape, oldParent); }); } DeleteLaneBehavior.$inject = [ 'eventBus', 'modeling', 'spaceTool' ]; e(DeleteLaneBehavior, CommandInterceptor); var LOW_PRIORITY$b = 500; /** * Replace boundary event with intermediate event when creating or moving results in detached event. */ function DetachEventBehavior(bpmnReplace, injector) { injector.invoke(CommandInterceptor, this); this._bpmnReplace = bpmnReplace; var self = this; this.postExecuted('elements.create', LOW_PRIORITY$b, function(context) { var elements = context.elements; elements.filter(function(shape) { var host = shape.host; return shouldReplace(shape, host); }).map(function(shape) { return elements.indexOf(shape); }).forEach(function(index) { context.elements[ index ] = self.replaceShape(elements[ index ]); }); }, true); this.preExecute('elements.move', LOW_PRIORITY$b, function(context) { var shapes = context.shapes, newHost = context.newHost; shapes.forEach(function(shape, index) { var host = shape.host; if (shouldReplace(shape, includes$6(shapes, host) ? host : newHost)) { shapes[ index ] = self.replaceShape(shape); } }); }, true); } DetachEventBehavior.$inject = [ 'bpmnReplace', 'injector' ]; e(DetachEventBehavior, CommandInterceptor); DetachEventBehavior.prototype.replaceShape = function(shape) { var eventDefinition = getEventDefinition(shape), intermediateEvent; if (eventDefinition) { intermediateEvent = { type: 'bpmn:IntermediateCatchEvent', eventDefinitionType: eventDefinition.$type }; } else { intermediateEvent = { type: 'bpmn:IntermediateThrowEvent' }; } return this._bpmnReplace.replaceElement(shape, intermediateEvent, { layoutConnection: false }); }; // helpers ////////// function getEventDefinition(element) { var businessObject = getBusinessObject(element), eventDefinitions = businessObject.eventDefinitions; return eventDefinitions && eventDefinitions[0]; } function shouldReplace(shape, host) { return !isLabel$6(shape) && is$1(shape, 'bpmn:BoundaryEvent') && !host; } function includes$6(array, item) { return array.indexOf(item) !== -1; } function DropOnFlowBehavior(eventBus, bpmnRules, modeling) { CommandInterceptor.call(this, eventBus); /** * Reconnect start / end of a connection after * dropping an element on a flow. */ function insertShape(shape, targetFlow, positionOrBounds) { var waypoints = targetFlow.waypoints, waypointsBefore, waypointsAfter, dockingPoint, source, target, incomingConnection, outgoingConnection, oldOutgoing = shape.outgoing.slice(), oldIncoming = shape.incoming.slice(); var mid; if (isNumber(positionOrBounds.width)) { mid = getMid(positionOrBounds); } else { mid = positionOrBounds; } var intersection = getApproxIntersection(waypoints, mid); if (intersection) { waypointsBefore = waypoints.slice(0, intersection.index); waypointsAfter = waypoints.slice(intersection.index + (intersection.bendpoint ? 1 : 0)); // due to inaccuracy intersection might have been found if (!waypointsBefore.length || !waypointsAfter.length) { return; } dockingPoint = intersection.bendpoint ? waypoints[intersection.index] : mid; // if last waypointBefore is inside shape's bounds, ignore docking point if (waypointsBefore.length === 1 || !isPointInsideBBox(shape, waypointsBefore[waypointsBefore.length - 1])) { waypointsBefore.push(copy(dockingPoint)); } // if first waypointAfter is inside shape's bounds, ignore docking point if (waypointsAfter.length === 1 || !isPointInsideBBox(shape, waypointsAfter[0])) { waypointsAfter.unshift(copy(dockingPoint)); } } source = targetFlow.source; target = targetFlow.target; if (bpmnRules.canConnect(source, shape, targetFlow)) { // reconnect source -> inserted shape modeling.reconnectEnd(targetFlow, shape, waypointsBefore || mid); incomingConnection = targetFlow; } if (bpmnRules.canConnect(shape, target, targetFlow)) { if (!incomingConnection) { // reconnect inserted shape -> end modeling.reconnectStart(targetFlow, shape, waypointsAfter || mid); outgoingConnection = targetFlow; } else { outgoingConnection = modeling.connect( shape, target, { type: targetFlow.type, waypoints: waypointsAfter } ); } } var duplicateConnections = [].concat( incomingConnection && filter(oldIncoming, function(connection) { return connection.source === incomingConnection.source; }) || [], outgoingConnection && filter(oldOutgoing, function(connection) { return connection.target === outgoingConnection.target; }) || [] ); if (duplicateConnections.length) { modeling.removeElements(duplicateConnections); } } this.preExecute('elements.move', function(context) { var newParent = context.newParent, shapes = context.shapes, delta = context.delta, shape = shapes[0]; if (!shape || !newParent) { return; } // if the new parent is a connection, // change it to the new parent's parent if (newParent && newParent.waypoints) { context.newParent = newParent = newParent.parent; } var shapeMid = getMid(shape); var newShapeMid = { x: shapeMid.x + delta.x, y: shapeMid.y + delta.y }; // find a connection which intersects with the // element's mid point var connection = find(newParent.children, function(element) { var canInsert = bpmnRules.canInsert(shapes, element); return canInsert && getApproxIntersection(element.waypoints, newShapeMid); }); if (connection) { context.targetFlow = connection; context.position = newShapeMid; } }, true); this.postExecuted('elements.move', function(context) { var shapes = context.shapes, targetFlow = context.targetFlow, position = context.position; if (targetFlow) { insertShape(shapes[0], targetFlow, position); } }, true); this.preExecute('shape.create', function(context) { var parent = context.parent, shape = context.shape; if (bpmnRules.canInsert(shape, parent)) { context.targetFlow = parent; context.parent = parent.parent; } }, true); this.postExecuted('shape.create', function(context) { var shape = context.shape, targetFlow = context.targetFlow, positionOrBounds = context.position; if (targetFlow) { insertShape(shape, targetFlow, positionOrBounds); } }, true); } e(DropOnFlowBehavior, CommandInterceptor); DropOnFlowBehavior.$inject = [ 'eventBus', 'bpmnRules', 'modeling' ]; // helpers ///////////////////// function isPointInsideBBox(bbox, point) { var x = point.x, y = point.y; return x >= bbox.x && x <= bbox.x + bbox.width && y >= bbox.y && y <= bbox.y + bbox.height; } function copy(obj) { return assign({}, obj); } function EventBasedGatewayBehavior(eventBus, modeling) { CommandInterceptor.call(this, eventBus); /** * Remove existing sequence flows of event-based target before connecting * from event-based gateway. */ this.preExecuted('connection.create', function(event) { var context = event.context, source = context.source, target = context.target, existingIncomingConnections = target.incoming.slice(); if (context.hints && context.hints.createElementsBehavior === false) { return; } if ( is$1(source, 'bpmn:EventBasedGateway') && target.incoming.length ) { existingIncomingConnections.filter(isSequenceFlow) .forEach(function(sequenceFlow) { modeling.removeConnection(sequenceFlow); }); } }); /** * After replacing shape with event-based gateway, remove incoming sequence * flows of event-based targets which do not belong to event-based gateway * source. */ this.preExecuted('shape.replace', function(event) { var newShape = event.context.newShape, newShapeTargets, newShapeTargetsIncomingSequenceFlows; if (!is$1(newShape, 'bpmn:EventBasedGateway')) { return; } newShapeTargets = newShape.outgoing.filter(isSequenceFlow) .map(function(sequenceFlow) { return sequenceFlow.target; }); newShapeTargetsIncomingSequenceFlows = newShapeTargets.reduce(function(sequenceFlows, target) { var incomingSequenceFlows = target.incoming.filter(isSequenceFlow); return sequenceFlows.concat(incomingSequenceFlows); }, []); newShapeTargetsIncomingSequenceFlows.forEach(function(sequenceFlow) { if (sequenceFlow.source !== newShape) { modeling.removeConnection(sequenceFlow); } }); }); } EventBasedGatewayBehavior.$inject = [ 'eventBus', 'modeling' ]; e(EventBasedGatewayBehavior, CommandInterceptor); // helpers ////////////////////// function isSequenceFlow(connection) { return is$1(connection, 'bpmn:SequenceFlow'); } var HIGH_PRIORITY$a = 1500; var HIGHEST_PRIORITY = 2000; /** * Correct hover targets in certain situations to improve diagram interaction. * * @param {ElementRegistry} elementRegistry * @param {EventBus} eventBus * @param {Canvas} canvas */ function FixHoverBehavior(elementRegistry, eventBus, canvas) { eventBus.on([ 'create.hover', 'create.move', 'create.out', 'create.end', 'shape.move.hover', 'shape.move.move', 'shape.move.out', 'shape.move.end' ], HIGH_PRIORITY$a, function(event) { var context = event.context, shape = context.shape || event.shape, hover = event.hover; // ensure elements are not dropped onto a bpmn:Lane but onto // the underlying bpmn:Participant if (is$1(hover, 'bpmn:Lane') && !isAny(shape, [ 'bpmn:Lane', 'bpmn:Participant' ])) { event.hover = getLanesRoot(hover); event.hoverGfx = elementRegistry.getGraphics(event.hover); } var rootElement = canvas.getRootElement(); // ensure bpmn:Group and label elements are dropped // always onto the root if (hover !== rootElement && (shape.labelTarget || is$1(shape, 'bpmn:Group'))) { event.hover = rootElement; event.hoverGfx = elementRegistry.getGraphics(event.hover); } }); eventBus.on([ 'connect.hover', 'connect.out', 'connect.end', 'connect.cleanup', 'global-connect.hover', 'global-connect.out', 'global-connect.end', 'global-connect.cleanup' ], HIGH_PRIORITY$a, function(event) { var hover = event.hover; // ensure connections start/end on bpmn:Participant, // not the underlying bpmn:Lane if (is$1(hover, 'bpmn:Lane')) { event.hover = getLanesRoot(hover) || hover; event.hoverGfx = elementRegistry.getGraphics(event.hover); } }); eventBus.on([ 'bendpoint.move.hover' ], HIGH_PRIORITY$a, function(event) { var context = event.context, hover = event.hover, type = context.type; // ensure reconnect start/end on bpmn:Participant, // not the underlying bpmn:Lane if (is$1(hover, 'bpmn:Lane') && /reconnect/.test(type)) { event.hover = getLanesRoot(hover) || hover; event.hoverGfx = elementRegistry.getGraphics(event.hover); } }); eventBus.on([ 'connect.start' ], HIGH_PRIORITY$a, function(event) { var context = event.context, start = context.start; // ensure connect start on bpmn:Participant, // not the underlying bpmn:Lane if (is$1(start, 'bpmn:Lane')) { context.start = getLanesRoot(start) || start; } }); // allow movement of participants from lanes eventBus.on('shape.move.start', HIGHEST_PRIORITY, function(event) { var shape = event.shape; if (is$1(shape, 'bpmn:Lane')) { event.shape = getLanesRoot(shape) || shape; } }); } FixHoverBehavior.$inject = [ 'elementRegistry', 'eventBus', 'canvas' ]; /** * Creates a new bpmn:CategoryValue inside a new bpmn:Category * * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} categoryValue. */ function createCategory(bpmnFactory) { return bpmnFactory.create('bpmn:Category'); } /** * Creates a new bpmn:CategoryValue inside a new bpmn:Category * * @param {BpmnFactory} bpmnFactory * * @return {ModdleElement} categoryValue. */ function createCategoryValue(bpmnFactory) { return bpmnFactory.create('bpmn:CategoryValue'); } /** * Adds category value to definitions * * @param {ModdleElement} categoryValue * @param {ModdleElement} category * @param {ModdleElement} definitions * * @return {ModdleElement} categoryValue */ function linkCategoryValue(categoryValue, category, definitions) { add(category.get('categoryValue'), categoryValue); categoryValue.$parent = category; add(definitions.get('rootElements'), category); category.$parent = definitions; return categoryValue; } /** * Unlink category value from parent * * @param {ModdleElement} categoryValue * * @return {ModdleElement} categoryValue */ function unlinkCategoryValue(categoryValue) { var category = categoryValue.$parent; if (category) { remove(category.get('categoryValue'), categoryValue); categoryValue.$parent = null; } return categoryValue; } /** * Unlink category from parent * * @param {ModdleElement} category * @param {ModdleElement} definitions * * @return {ModdleElement} categoryValue */ function unlinkCategory(category) { var definitions = category.$parent; if (definitions) { remove(definitions.get('rootElements'), category); category.$parent = null; } return category; } var LOWER_PRIORITY = 770; /** * BPMN specific Group behavior */ function GroupBehavior( bpmnFactory, bpmnjs, elementRegistry, eventBus, injector, moddleCopy ) { injector.invoke(CommandInterceptor, this); /** * Returns all group element in the current registry * * @return {Array} a list of group shapes */ function getGroupElements() { return elementRegistry.filter(function(e) { return is$1(e, 'bpmn:Group'); }); } /** * Returns true if given category is referenced in one of the given elements * * @param { djs.model.Element[] } elements * @param { ModdleElement } category * * @return { boolean } */ function isReferencedCategory(elements, category) { return elements.some(function(element) { var businessObject = getBusinessObject(element); var _category = businessObject.categoryValueRef && businessObject.categoryValueRef.$parent; return _category === category; }); } /** * Returns true if given categoryValue is referenced in one of the given elements * * @param { djs.model.Element[] } elements * @param { ModdleElement } categoryValue * * @return { boolean } */ function isReferencedCategoryValue(elements, categoryValue) { return elements.some(function(element) { var businessObject = getBusinessObject(element); return businessObject.categoryValueRef === categoryValue; }); } /** * Remove category value unless it is still referenced * * @param {ModdleElement} categoryValue * @param {ModdleElement} category * @param {ModdleElement} businessObject */ function removeCategoryValue(categoryValue, category, businessObject) { var groups = getGroupElements().filter(function(element) { return element.businessObject !== businessObject; }); if (category && !isReferencedCategory(groups, category)) { unlinkCategory(category); } if (categoryValue && !isReferencedCategoryValue(groups, categoryValue)) { unlinkCategoryValue(categoryValue); } } /** * Add category value * * @param {ModdleElement} categoryValue * @param {ModdleElement} category */ function addCategoryValue(categoryValue, category) { return linkCategoryValue(categoryValue, category, bpmnjs.getDefinitions()); } function setCategoryValue(element, context) { var businessObject = getBusinessObject(element), categoryValue = businessObject.categoryValueRef; if (!categoryValue) { categoryValue = businessObject.categoryValueRef = context.categoryValue = ( context.categoryValue || createCategoryValue(bpmnFactory) ); } var category = categoryValue.$parent; if (!category) { category = categoryValue.$parent = context.category = ( context.category || createCategory(bpmnFactory) ); } addCategoryValue(categoryValue, category, bpmnjs.getDefinitions()); } function unsetCategoryValue(element, context) { var category = context.category, categoryValue = context.categoryValue, businessObject = getBusinessObject(element); if (categoryValue) { businessObject.categoryValueRef = null; removeCategoryValue(categoryValue, category, businessObject); } else { removeCategoryValue(null, businessObject.categoryValueRef.$parent, businessObject); } } // ensure category + value exist before label editing this.execute('label.create', function(event) { var context = event.context, labelTarget = context.labelTarget; if (!is$1(labelTarget, 'bpmn:Group')) { return; } setCategoryValue(labelTarget, context); }); this.revert('label.create', function(event) { var context = event.context, labelTarget = context.labelTarget; if (!is$1(labelTarget, 'bpmn:Group')) { return; } unsetCategoryValue(labelTarget, context); }); // remove referenced category + value when group was deleted this.execute('shape.delete', function(event) { var context = event.context, shape = context.shape, businessObject = getBusinessObject(shape); if (!is$1(shape, 'bpmn:Group') || shape.labelTarget) { return; } var categoryValue = context.categoryValue = businessObject.categoryValueRef, category; if (categoryValue) { category = context.category = categoryValue.$parent; removeCategoryValue(categoryValue, category, businessObject); businessObject.categoryValueRef = null; } }); this.reverted('shape.delete', function(event) { var context = event.context, shape = context.shape; if (!is$1(shape, 'bpmn:Group') || shape.labelTarget) { return; } var category = context.category, categoryValue = context.categoryValue, businessObject = getBusinessObject(shape); if (categoryValue) { businessObject.categoryValueRef = categoryValue; addCategoryValue(categoryValue, category); } }); // create new category + value when group was created this.execute('shape.create', function(event) { var context = event.context, shape = context.shape; if (!is$1(shape, 'bpmn:Group') || shape.labelTarget) { return; } if (getBusinessObject(shape).categoryValueRef) { setCategoryValue(shape, context); } }); this.reverted('shape.create', function(event) { var context = event.context, shape = context.shape; if (!is$1(shape, 'bpmn:Group') || shape.labelTarget) { return; } if (getBusinessObject(shape).categoryValueRef) { unsetCategoryValue(shape, context); } }); // copy + paste categoryValueRef with group function copy(bo, clone) { var targetBo = bpmnFactory.create(bo.$type); return moddleCopy.copyElement(bo, targetBo, null, clone); } eventBus.on('copyPaste.copyElement', LOWER_PRIORITY, function(context) { var descriptor = context.descriptor, element = context.element; if (!is$1(element, 'bpmn:Group') || element.labelTarget) { return; } var groupBo = getBusinessObject(element); if (groupBo.categoryValueRef) { var categoryValue = groupBo.categoryValueRef; descriptor.categoryValue = copy(categoryValue, true); if (categoryValue.$parent) { descriptor.category = copy(categoryValue.$parent, true); } } }); eventBus.on('copyPaste.pasteElement', LOWER_PRIORITY, function(context) { var descriptor = context.descriptor, businessObject = descriptor.businessObject, categoryValue = descriptor.categoryValue, category = descriptor.category; if (categoryValue) { categoryValue = businessObject.categoryValueRef = copy(categoryValue); } if (category) { categoryValue.$parent = copy(category); } delete descriptor.category; delete descriptor.categoryValue; }); } GroupBehavior.$inject = [ 'bpmnFactory', 'bpmnjs', 'elementRegistry', 'eventBus', 'injector', 'moddleCopy' ]; e(GroupBehavior, CommandInterceptor); /** * Returns the intersection between two line segments a and b. * * @param {Point} l1s * @param {Point} l1e * @param {Point} l2s * @param {Point} l2e * * @return {Point} */ function lineIntersect(l1s, l1e, l2s, l2e) { // if the lines intersect, the result contains the x and y of the // intersection (treating the lines as infinite) and booleans for // whether line segment 1 or line segment 2 contain the point var denominator, a, b, c, numerator; denominator = ((l2e.y - l2s.y) * (l1e.x - l1s.x)) - ((l2e.x - l2s.x) * (l1e.y - l1s.y)); if (denominator == 0) { return null; } a = l1s.y - l2s.y; b = l1s.x - l2s.x; numerator = ((l2e.x - l2s.x) * a) - ((l2e.y - l2s.y) * b); c = numerator / denominator; // if we cast these lines infinitely in // both directions, they intersect here return { x: Math.round(l1s.x + (c * (l1e.x - l1s.x))), y: Math.round(l1s.y + (c * (l1e.y - l1s.y))) }; } /** * Fix broken dockings after DI imports. * * @param {EventBus} eventBus */ function ImportDockingFix(eventBus) { function adjustDocking(startPoint, nextPoint, elementMid) { var elementTop = { x: elementMid.x, y: elementMid.y - 50 }; var elementLeft = { x: elementMid.x - 50, y: elementMid.y }; var verticalIntersect = lineIntersect(startPoint, nextPoint, elementMid, elementTop), horizontalIntersect = lineIntersect(startPoint, nextPoint, elementMid, elementLeft); // original is horizontal or vertical center cross intersection var centerIntersect; if (verticalIntersect && horizontalIntersect) { if (getDistance$1(verticalIntersect, elementMid) > getDistance$1(horizontalIntersect, elementMid)) { centerIntersect = horizontalIntersect; } else { centerIntersect = verticalIntersect; } } else { centerIntersect = verticalIntersect || horizontalIntersect; } startPoint.original = centerIntersect; } function fixDockings(connection) { var waypoints = connection.waypoints; adjustDocking( waypoints[0], waypoints[1], getMid(connection.source) ); adjustDocking( waypoints[waypoints.length - 1], waypoints[waypoints.length - 2], getMid(connection.target) ); } eventBus.on('bpmnElement.added', function(e) { var element = e.element; if (element.waypoints) { fixDockings(element); } }); } ImportDockingFix.$inject = [ 'eventBus' ]; // helpers ////////////////////// function getDistance$1(p1, p2) { return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); } /** * A component that makes sure that each created or updated * Pool and Lane is assigned an isHorizontal property set to true. * * @param {EventBus} eventBus */ function IsHorizontalFix(eventBus) { CommandInterceptor.call(this, eventBus); var elementTypesToUpdate = [ 'bpmn:Participant', 'bpmn:Lane' ]; this.executed([ 'shape.move', 'shape.create', 'shape.resize' ], function(event) { var shape = event.context.shape, bo = getBusinessObject(shape), di = getDi(shape); if (isAny(bo, elementTypesToUpdate) && !di.get('isHorizontal')) { // set attribute directly to avoid modeling#updateProperty side effects di.set('isHorizontal', true); } }); } IsHorizontalFix.$inject = [ 'eventBus' ]; e(IsHorizontalFix, CommandInterceptor); var sqrt = Math.sqrt, min$1 = Math.min, max$3 = Math.max, abs$3 = Math.abs; /** * Calculate the square (power to two) of a number. * * @param {number} n * * @return {number} */ function sq(n) { return Math.pow(n, 2); } /** * Get distance between two points. * * @param {Point} p1 * @param {Point} p2 * * @return {number} */ function getDistance(p1, p2) { return sqrt(sq(p1.x - p2.x) + sq(p1.y - p2.y)); } /** * Return the attachment of the given point on the specified line. * * The attachment is either a bendpoint (attached to the given point) * or segment (attached to a location on a line segment) attachment: * * ```javascript * var pointAttachment = { * type: 'bendpoint', * bendpointIndex: 3, * position: { x: 10, y: 10 } // the attach point on the line * }; * * var segmentAttachment = { * type: 'segment', * segmentIndex: 2, * relativeLocation: 0.31, // attach point location between 0 (at start) and 1 (at end) * position: { x: 10, y: 10 } // the attach point on the line * }; * ``` * * @param {Point} point * @param {Array} line * * @return {Object} attachment */ function getAttachment(point, line) { var idx = 0, segmentStart, segmentEnd, segmentStartDistance, segmentEndDistance, attachmentPosition, minDistance, intersections, attachment, attachmentDistance, closestAttachmentDistance, closestAttachment; for (idx = 0; idx < line.length - 1; idx++) { segmentStart = line[idx]; segmentEnd = line[idx + 1]; if (pointsEqual(segmentStart, segmentEnd)) { intersections = [ segmentStart ]; } else { segmentStartDistance = getDistance(point, segmentStart); segmentEndDistance = getDistance(point, segmentEnd); minDistance = min$1(segmentStartDistance, segmentEndDistance); intersections = getCircleSegmentIntersections(segmentStart, segmentEnd, point, minDistance); } if (intersections.length < 1) { throw new Error('expected between [1, 2] circle -> line intersections'); } // one intersection -> bendpoint attachment if (intersections.length === 1) { attachment = { type: 'bendpoint', position: intersections[0], segmentIndex: idx, bendpointIndex: pointsEqual(segmentStart, intersections[0]) ? idx : idx + 1 }; } // two intersections -> segment attachment if (intersections.length === 2) { attachmentPosition = mid$1(intersections[0], intersections[1]); attachment = { type: 'segment', position: attachmentPosition, segmentIndex: idx, relativeLocation: getDistance(segmentStart, attachmentPosition) / getDistance(segmentStart, segmentEnd) }; } attachmentDistance = getDistance(attachment.position, point); if (!closestAttachment || closestAttachmentDistance > attachmentDistance) { closestAttachment = attachment; closestAttachmentDistance = attachmentDistance; } } return closestAttachment; } /** * Gets the intersection between a circle and a line segment. * * @param {Point} s1 segment start * @param {Point} s2 segment end * @param {Point} cc circle center * @param {number} cr circle radius * * @return {Array} intersections */ function getCircleSegmentIntersections(s1, s2, cc, cr) { var baX = s2.x - s1.x; var baY = s2.y - s1.y; var caX = cc.x - s1.x; var caY = cc.y - s1.y; var a = baX * baX + baY * baY; var bBy2 = baX * caX + baY * caY; var c = caX * caX + caY * caY - cr * cr; var pBy2 = bBy2 / a; var q = c / a; var disc = pBy2 * pBy2 - q; // check against negative value to work around // negative, very close to zero results (-4e-15) // being produced in some environments if (disc < 0 && disc > -0.000001) { disc = 0; } if (disc < 0) { return []; } // if disc == 0 ... dealt with later var tmpSqrt = sqrt(disc); var abScalingFactor1 = -pBy2 + tmpSqrt; var abScalingFactor2 = -pBy2 - tmpSqrt; var i1 = { x: s1.x - baX * abScalingFactor1, y: s1.y - baY * abScalingFactor1 }; if (disc === 0) { // abScalingFactor1 == abScalingFactor2 return [ i1 ]; } var i2 = { x: s1.x - baX * abScalingFactor2, y: s1.y - baY * abScalingFactor2 }; // return only points on line segment return [ i1, i2 ].filter(function(p) { return isPointInSegment(p, s1, s2); }); } function isPointInSegment(p, segmentStart, segmentEnd) { return ( fenced(p.x, segmentStart.x, segmentEnd.x) && fenced(p.y, segmentStart.y, segmentEnd.y) ); } function fenced(n, rangeStart, rangeEnd) { // use matching threshold to work around // precision errors in intersection computation return ( n >= min$1(rangeStart, rangeEnd) - EQUAL_THRESHOLD && n <= max$3(rangeStart, rangeEnd) + EQUAL_THRESHOLD ); } /** * Calculate mid of two points. * * @param {Point} p1 * @param {Point} p2 * * @return {Point} */ function mid$1(p1, p2) { return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }; } var EQUAL_THRESHOLD = 0.1; function pointsEqual(p1, p2) { return ( abs$3(p1.x - p2.x) <= EQUAL_THRESHOLD && abs$3(p1.y - p2.y) <= EQUAL_THRESHOLD ); } function findNewLineStartIndex(oldWaypoints, newWaypoints, attachment, hints) { var index = attachment.segmentIndex; var offset = newWaypoints.length - oldWaypoints.length; // segmentMove happened if (hints.segmentMove) { var oldSegmentStartIndex = hints.segmentMove.segmentStartIndex, newSegmentStartIndex = hints.segmentMove.newSegmentStartIndex; // if point was on moved segment return new segment index if (index === oldSegmentStartIndex) { return newSegmentStartIndex; } // point is after new segment index if (index >= newSegmentStartIndex) { return (index + offset < newSegmentStartIndex) ? newSegmentStartIndex : index + offset; } // if point is before new segment index return index; } // bendpointMove happened if (hints.bendpointMove) { var insert = hints.bendpointMove.insert, bendpointIndex = hints.bendpointMove.bendpointIndex, newIndex; // waypoints length didnt change if (offset === 0) { return index; } // point behind new/removed bendpoint if (index >= bendpointIndex) { newIndex = insert ? index + 1 : index - 1; } // point before new/removed bendpoint if (index < bendpointIndex) { newIndex = index; // decide point should take right or left segment if (insert && attachment.type !== 'bendpoint' && bendpointIndex - 1 === index) { var rel = relativePositionMidWaypoint(newWaypoints, bendpointIndex); if (rel < attachment.relativeLocation) { newIndex++; } } } return newIndex; } // start/end changed if (offset === 0) { return index; } if (hints.connectionStart && index === 0) { return 0; } if (hints.connectionEnd && index === oldWaypoints.length - 2) { return newWaypoints.length - 2; } // if nothing fits, take the middle segment return Math.floor((newWaypoints.length - 2) / 2); } /** * Calculate the required adjustment (move delta) for the given point * after the connection waypoints got updated. * * @param {Point} position * @param {Array} newWaypoints * @param {Array} oldWaypoints * @param {Object} hints * * @return {Object} result * @return {Point} result.point * @return {Point} result.delta */ function getAnchorPointAdjustment(position, newWaypoints, oldWaypoints, hints) { var dx = 0, dy = 0; var oldPosition = { point: position, delta: { x: 0, y: 0 } }; // get closest attachment var attachment = getAttachment(position, oldWaypoints), oldLabelLineIndex = attachment.segmentIndex, newLabelLineIndex = findNewLineStartIndex(oldWaypoints, newWaypoints, attachment, hints); // should never happen // TODO(@janstuemmel): throw an error here when connectionSegmentMove is refactored if (newLabelLineIndex < 0 || newLabelLineIndex > newWaypoints.length - 2 || newLabelLineIndex === null) { return oldPosition; } var oldLabelLine = getLine(oldWaypoints, oldLabelLineIndex), newLabelLine = getLine(newWaypoints, newLabelLineIndex), oldFoot = attachment.position; var relativeFootPosition = getRelativeFootPosition(oldLabelLine, oldFoot), angleDelta = getAngleDelta(oldLabelLine, newLabelLine); // special rule if label on bendpoint if (attachment.type === 'bendpoint') { var offset = newWaypoints.length - oldWaypoints.length, oldBendpointIndex = attachment.bendpointIndex, oldBendpoint = oldWaypoints[oldBendpointIndex]; // bendpoint position hasn't changed, return same position if (newWaypoints.indexOf(oldBendpoint) !== -1) { return oldPosition; } // new bendpoint and old bendpoint have same index, then just return the offset if (offset === 0) { var newBendpoint = newWaypoints[oldBendpointIndex]; dx = newBendpoint.x - attachment.position.x, dy = newBendpoint.y - attachment.position.y; return { delta: { x: dx, y: dy }, point: { x: position.x + dx, y: position.y + dy } }; } // if bendpoints get removed if (offset < 0 && oldBendpointIndex !== 0 && oldBendpointIndex < oldWaypoints.length - 1) { relativeFootPosition = relativePositionMidWaypoint(oldWaypoints, oldBendpointIndex); } } var newFoot = { x: (newLabelLine[1].x - newLabelLine[0].x) * relativeFootPosition + newLabelLine[0].x, y: (newLabelLine[1].y - newLabelLine[0].y) * relativeFootPosition + newLabelLine[0].y }; // the rotated vector to label var newLabelVector = rotateVector({ x: position.x - oldFoot.x, y: position.y - oldFoot.y }, angleDelta); // the new relative position dx = newFoot.x + newLabelVector.x - position.x; dy = newFoot.y + newLabelVector.y - position.y; return { point: roundPoint(newFoot), delta: roundPoint({ x: dx, y: dy }) }; } // HELPERS ////////////////////// function relativePositionMidWaypoint(waypoints, idx) { var distanceSegment1 = getDistancePointPoint(waypoints[idx - 1], waypoints[idx]), distanceSegment2 = getDistancePointPoint(waypoints[idx], waypoints[idx + 1]); var relativePosition = distanceSegment1 / (distanceSegment1 + distanceSegment2); return relativePosition; } function getAngleDelta(l1, l2) { var a1 = getAngle(l1), a2 = getAngle(l2); return a2 - a1; } function getLine(waypoints, idx) { return [ waypoints[idx], waypoints[idx + 1] ]; } function getRelativeFootPosition(line, foot) { var length = getDistancePointPoint(line[0], line[1]), lengthToFoot = getDistancePointPoint(line[0], foot); return length === 0 ? 0 : lengthToFoot / length; } /** * Calculate the required adjustment (move delta) for the given label * after the connection waypoints got updated. * * @param {djs.model.Label} label * @param {Array} newWaypoints * @param {Array} oldWaypoints * @param {Object} hints * * @return {Point} delta */ function getLabelAdjustment(label, newWaypoints, oldWaypoints, hints) { var labelPosition = getLabelMid(label); return getAnchorPointAdjustment(labelPosition, newWaypoints, oldWaypoints, hints).delta; } // HELPERS ////////////////////// function getLabelMid(label) { return { x: label.x + label.width / 2, y: label.y + label.height / 2 }; } /** * Calculates the absolute point relative to the new element's position * * @param {point} point [absolute] * @param {bounds} oldBounds * @param {bounds} newBounds * * @return {point} point [absolute] */ function getNewAttachPoint(point, oldBounds, newBounds) { var oldCenter = center(oldBounds), newCenter = center(newBounds), oldDelta = delta(point, oldCenter); var newDelta = { x: oldDelta.x * (newBounds.width / oldBounds.width), y: oldDelta.y * (newBounds.height / oldBounds.height) }; return roundPoint({ x: newCenter.x + newDelta.x, y: newCenter.y + newDelta.y }); } /** * Calculates the shape's delta relative to a new position * of a certain element's bounds * * @param {djs.model.Shape} point [absolute] * @param {bounds} oldBounds * @param {bounds} newBounds * * @return {delta} delta */ function getNewAttachShapeDelta(shape, oldBounds, newBounds) { var shapeCenter = center(shape), oldCenter = center(oldBounds), newCenter = center(newBounds), shapeDelta = delta(shape, shapeCenter), oldCenterDelta = delta(shapeCenter, oldCenter), stickyPositionDelta = getStickyPositionDelta(shapeCenter, oldBounds, newBounds); if (stickyPositionDelta) { return stickyPositionDelta; } var newCenterDelta = { x: oldCenterDelta.x * (newBounds.width / oldBounds.width), y: oldCenterDelta.y * (newBounds.height / oldBounds.height) }; var newShapeCenter = { x: newCenter.x + newCenterDelta.x, y: newCenter.y + newCenterDelta.y }; return roundPoint({ x: newShapeCenter.x + shapeDelta.x - shape.x, y: newShapeCenter.y + shapeDelta.y - shape.y }); } function getStickyPositionDelta(oldShapeCenter, oldBounds, newBounds) { var oldTRBL = asTRBL(oldBounds), newTRBL = asTRBL(newBounds); if (isMoved(oldTRBL, newTRBL)) { return null; } var oldOrientation = getOrientation(oldBounds, oldShapeCenter), stickyPositionDelta, newShapeCenter, newOrientation; if (oldOrientation === 'top') { stickyPositionDelta = { x: 0, y: newTRBL.bottom - oldTRBL.bottom }; } else if (oldOrientation === 'bottom') { stickyPositionDelta = { x: 0, y: newTRBL.top - oldTRBL.top }; } else if (oldOrientation === 'right') { stickyPositionDelta = { x: newTRBL.left - oldTRBL.left, y: 0 }; } else if (oldOrientation === 'left') { stickyPositionDelta = { x: newTRBL.right - oldTRBL.right, y: 0 }; } else { // fallback to proportional movement for corner-placed attachments return null; } newShapeCenter = { x: oldShapeCenter.x + stickyPositionDelta.x, y: oldShapeCenter.y + stickyPositionDelta.y }; newOrientation = getOrientation(newBounds, newShapeCenter); if (newOrientation !== oldOrientation) { // fallback to proportional movement if orientation would otherwise change return null; } return stickyPositionDelta; } function isMoved(oldTRBL, newTRBL) { return isHorizontallyMoved(oldTRBL, newTRBL) || isVerticallyMoved(oldTRBL, newTRBL); } function isHorizontallyMoved(oldTRBL, newTRBL) { return oldTRBL.right !== newTRBL.right && oldTRBL.left !== newTRBL.left; } function isVerticallyMoved(oldTRBL, newTRBL) { return oldTRBL.top !== newTRBL.top && oldTRBL.bottom !== newTRBL.bottom; } var DEFAULT_LABEL_DIMENSIONS = { width: 90, height: 20 }; var NAME_PROPERTY = 'name'; var TEXT_PROPERTY = 'text'; /** * A component that makes sure that external labels are added * together with respective elements and properly updated (DI wise) * during move. * * @param {EventBus} eventBus * @param {Modeling} modeling * @param {BpmnFactory} bpmnFactory * @param {TextRenderer} textRenderer */ function LabelBehavior( eventBus, modeling, bpmnFactory, textRenderer) { CommandInterceptor.call(this, eventBus); // update label if name property was updated this.postExecute('element.updateProperties', function(e) { var context = e.context, element = context.element, properties = context.properties; if (NAME_PROPERTY in properties) { modeling.updateLabel(element, properties[NAME_PROPERTY]); } if (TEXT_PROPERTY in properties && is$1(element, 'bpmn:TextAnnotation')) { var newBounds = textRenderer.getTextAnnotationBounds( { x: element.x, y: element.y, width: element.width, height: element.height }, properties[TEXT_PROPERTY] || '' ); modeling.updateLabel(element, properties.text, newBounds); } }); // create label shape after shape/connection was created this.postExecute([ 'shape.create', 'connection.create' ], function(e) { var context = e.context, hints = context.hints || {}; if (hints.createElementsBehavior === false) { return; } var element = context.shape || context.connection, businessObject = element.businessObject; if (isLabel$6(element) || !isLabelExternal(element)) { return; } // only create label if attribute available if (!getLabel(element)) { return; } var labelCenter = getExternalLabelMid(element); // we don't care about x and y var labelDimensions = textRenderer.getExternalLabelBounds( DEFAULT_LABEL_DIMENSIONS, getLabel(element) ); modeling.createLabel(element, labelCenter, { id: businessObject.id + '_label', businessObject: businessObject, width: labelDimensions.width, height: labelDimensions.height }); }); // update label after label shape was deleted this.postExecute('shape.delete', function(event) { var context = event.context, labelTarget = context.labelTarget, hints = context.hints || {}; // check if label if (labelTarget && hints.unsetLabel !== false) { modeling.updateLabel(labelTarget, null, null, { removeShape: false }); } }); // update di information on label creation this.postExecute([ 'label.create' ], function(event) { var context = event.context, element = context.shape, labelTarget = context.labelTarget, di; // we want to trigger on real labels only if (!labelTarget) { return; } // we want to trigger on BPMN elements only if (!is$1(labelTarget, 'bpmn:BaseElement')) { return; } di = getDi(labelTarget); if (!di.label) { di.label = bpmnFactory.create('bpmndi:BPMNLabel', { bounds: bpmnFactory.create('dc:Bounds') }); element.di = di; } assign(di.label.bounds, { x: element.x, y: element.y, width: element.width, height: element.height }); }); function getVisibleLabelAdjustment(event) { var context = event.context, connection = context.connection, label = connection.label, hints = assign({}, context.hints), newWaypoints = context.newWaypoints || connection.waypoints, oldWaypoints = context.oldWaypoints; if (typeof hints.startChanged === 'undefined') { hints.startChanged = !!hints.connectionStart; } if (typeof hints.endChanged === 'undefined') { hints.endChanged = !!hints.connectionEnd; } return getLabelAdjustment(label, newWaypoints, oldWaypoints, hints); } this.postExecute([ 'connection.layout', 'connection.updateWaypoints' ], function(event) { var context = event.context, hints = context.hints || {}; if (hints.labelBehavior === false) { return; } var connection = context.connection, label = connection.label, labelAdjustment; // handle missing label as well as the case // that the label parent does not exist (yet), // because it is being pasted / created via multi element create // // Cf. https://github.com/bpmn-io/bpmn-js/pull/1227 if (!label || !label.parent) { return; } labelAdjustment = getVisibleLabelAdjustment(event); modeling.moveShape(label, labelAdjustment); }); // keep label position on shape replace this.postExecute([ 'shape.replace' ], function(event) { var context = event.context, newShape = context.newShape, oldShape = context.oldShape; var businessObject = getBusinessObject(newShape); if (businessObject && isLabelExternal(businessObject) && oldShape.label && newShape.label) { newShape.label.x = oldShape.label.x; newShape.label.y = oldShape.label.y; } }); // move external label after resizing this.postExecute('shape.resize', function(event) { var context = event.context, shape = context.shape, newBounds = context.newBounds, oldBounds = context.oldBounds; if (hasExternalLabel(shape)) { var label = shape.label, labelMid = getMid(label), edges = asEdges(oldBounds); // get nearest border point to label as reference point var referencePoint = getReferencePoint(labelMid, edges); var delta = getReferencePointDelta(referencePoint, oldBounds, newBounds); modeling.moveShape(label, delta); } }); } e(LabelBehavior, CommandInterceptor); LabelBehavior.$inject = [ 'eventBus', 'modeling', 'bpmnFactory', 'textRenderer' ]; // helpers ////////////////////// /** * Calculates a reference point delta relative to a new position * of a certain element's bounds * * @param {Point} point * @param {Bounds} oldBounds * @param {Bounds} newBounds * * @return {Delta} delta */ function getReferencePointDelta(referencePoint, oldBounds, newBounds) { var newReferencePoint = getNewAttachPoint(referencePoint, oldBounds, newBounds); return roundPoint(delta(newReferencePoint, referencePoint)); } /** * Generates the nearest point (reference point) for a given point * onto given set of lines * * @param {Array} lines * @param {Point} point * * @param {Point} */ function getReferencePoint(point, lines) { if (!lines.length) { return; } var nearestLine = getNearestLine(point, lines); return perpendicularFoot(point, nearestLine); } /** * Convert the given bounds to a lines array containing all edges * * @param {Bounds|Point} bounds * * @return Array */ function asEdges(bounds) { return [ [ // top { x: bounds.x, y: bounds.y }, { x: bounds.x + (bounds.width || 0), y: bounds.y } ], [ // right { x: bounds.x + (bounds.width || 0), y: bounds.y }, { x: bounds.x + (bounds.width || 0), y: bounds.y + (bounds.height || 0) } ], [ // bottom { x: bounds.x, y: bounds.y + (bounds.height || 0) }, { x: bounds.x + (bounds.width || 0), y: bounds.y + (bounds.height || 0) } ], [ // left { x: bounds.x, y: bounds.y }, { x: bounds.x, y: bounds.y + (bounds.height || 0) } ] ]; } /** * Returns the nearest line for a given point by distance * @param {Point} point * @param Array lines * * @return Array */ function getNearestLine(point, lines) { var distances = lines.map(function(l) { return { line: l, distance: getDistancePointLine(point, l) }; }); var sorted = sortBy(distances, 'distance'); return sorted[0].line; } /** * Calculate the new point after the connection waypoints got updated. * * @param {djs.model.Label} label * @param {Array} newWaypoints * @param {Array} oldWaypoints * @param {Object} hints * * @return {Point} point */ function getConnectionAdjustment(position, newWaypoints, oldWaypoints, hints) { return getAnchorPointAdjustment(position, newWaypoints, oldWaypoints, hints).point; } /** * A component that makes sure that Associations connected to Connections * are updated together with the Connection. * * @param {EventBus} eventBus * @param {Modeling} modeling */ function LayoutConnectionBehavior( eventBus, modeling) { CommandInterceptor.call(this, eventBus); function getnewAnchorPoint(event, point) { var context = event.context, connection = context.connection, hints = assign({}, context.hints), newWaypoints = context.newWaypoints || connection.waypoints, oldWaypoints = context.oldWaypoints; if (typeof hints.startChanged === 'undefined') { hints.startChanged = !!hints.connectionStart; } if (typeof hints.endChanged === 'undefined') { hints.endChanged = !!hints.connectionEnd; } return getConnectionAdjustment(point, newWaypoints, oldWaypoints, hints); } this.postExecute([ 'connection.layout', 'connection.updateWaypoints' ], function(event) { var context = event.context; var connection = context.connection, outgoing = connection.outgoing, incoming = connection.incoming; incoming.forEach(function(connection) { var endPoint = connection.waypoints[connection.waypoints.length - 1]; var newEndpoint = getnewAnchorPoint(event, endPoint); var newWaypoints = [].concat(connection.waypoints.slice(0, -1), [ newEndpoint ]); modeling.updateWaypoints(connection, newWaypoints); }); outgoing.forEach(function(connection) { var startpoint = connection.waypoints[0]; var newStartpoint = getnewAnchorPoint(event, startpoint); var newWaypoints = [].concat([ newStartpoint ], connection.waypoints.slice(1)); modeling.updateWaypoints(connection, newWaypoints); }); }); this.postExecute([ 'connection.move' ], function(event) { var context = event.context; var connection = context.connection, outgoing = connection.outgoing, incoming = connection.incoming, delta = context.delta; incoming.forEach(function(connection) { var endPoint = connection.waypoints[connection.waypoints.length - 1]; var newEndpoint = { x: endPoint.x + delta.x, y: endPoint.y + delta.y }; var newWaypoints = [].concat(connection.waypoints.slice(0, -1), [ newEndpoint ]); modeling.updateWaypoints(connection, newWaypoints); }); outgoing.forEach(function(connection) { var startpoint = connection.waypoints[0]; var newStartpoint = { x: startpoint.x + delta.x, y: startpoint.y + delta.y }; var newWaypoints = [].concat([ newStartpoint ], connection.waypoints.slice(1)); modeling.updateWaypoints(connection, newWaypoints); }); }); } e(LayoutConnectionBehavior, CommandInterceptor); LayoutConnectionBehavior.$inject = [ 'eventBus', 'modeling' ]; function getResizedSourceAnchor(connection, shape, oldBounds) { var waypoints = safeGetWaypoints(connection), waypointsInsideNewBounds = getWaypointsInsideBounds(waypoints, shape), oldAnchor = waypoints[0]; // new anchor is the last waypoint enclosed be resized source if (waypointsInsideNewBounds.length) { return waypointsInsideNewBounds[ waypointsInsideNewBounds.length - 1 ]; } return getNewAttachPoint(oldAnchor.original || oldAnchor, oldBounds, shape); } function getResizedTargetAnchor(connection, shape, oldBounds) { var waypoints = safeGetWaypoints(connection), waypointsInsideNewBounds = getWaypointsInsideBounds(waypoints, shape), oldAnchor = waypoints[waypoints.length - 1]; // new anchor is the first waypoint enclosed be resized target if (waypointsInsideNewBounds.length) { return waypointsInsideNewBounds[ 0 ]; } return getNewAttachPoint(oldAnchor.original || oldAnchor, oldBounds, shape); } function getMovedSourceAnchor(connection, source, moveDelta) { var waypoints = safeGetWaypoints(connection), oldBounds = subtract(source, moveDelta), oldAnchor = waypoints[ 0 ]; return getNewAttachPoint(oldAnchor.original || oldAnchor, oldBounds, source); } function getMovedTargetAnchor(connection, target, moveDelta) { var waypoints = safeGetWaypoints(connection), oldBounds = subtract(target, moveDelta), oldAnchor = waypoints[ waypoints.length - 1 ]; return getNewAttachPoint(oldAnchor.original || oldAnchor, oldBounds, target); } // helpers ////////////////////// function subtract(bounds, delta) { return { x: bounds.x - delta.x, y: bounds.y - delta.y, width: bounds.width, height: bounds.height }; } /** * Return waypoints of given connection; throw if non exists (should not happen!!). * * @param {Connection} connection * * @return {Array} */ function safeGetWaypoints(connection) { var waypoints = connection.waypoints; if (!waypoints.length) { throw new Error('connection#' + connection.id + ': no waypoints'); } return waypoints; } function getWaypointsInsideBounds(waypoints, bounds) { var originalWaypoints = map(waypoints, getOriginal); return filter(originalWaypoints, function(waypoint) { return isInsideBounds(waypoint, bounds); }); } /** * Checks if point is inside bounds, incl. edges. * * @param {Point} point * @param {Bounds} bounds */ function isInsideBounds(point, bounds) { return getOrientation(bounds, point, 1) === 'intersect'; } function getOriginal(point) { return point.original || point; } /** * BPMN-specific message flow behavior. */ function MessageFlowBehavior(eventBus, modeling) { CommandInterceptor.call(this, eventBus); this.postExecute('shape.replace', function(context) { var oldShape = context.oldShape, newShape = context.newShape; if (!isParticipantCollapse(oldShape, newShape)) { return; } var messageFlows = getMessageFlows(oldShape); messageFlows.incoming.forEach(function(incoming) { var anchor = getResizedTargetAnchor(incoming, newShape, oldShape); modeling.reconnectEnd(incoming, newShape, anchor); }); messageFlows.outgoing.forEach(function(outgoing) { var anchor = getResizedSourceAnchor(outgoing, newShape, oldShape); modeling.reconnectStart(outgoing, newShape, anchor); }); }, true); } MessageFlowBehavior.$inject = [ 'eventBus', 'modeling' ]; e(MessageFlowBehavior, CommandInterceptor); // helpers ////////// function isParticipantCollapse(oldShape, newShape) { return is$1(oldShape, 'bpmn:Participant') && isExpanded(oldShape) && is$1(newShape, 'bpmn:Participant') && !isExpanded(newShape); } function getMessageFlows(parent) { var elements = selfAndAllChildren([ parent ], false); var incoming = [], outgoing = []; elements.forEach(function(element) { if (element === parent) { return; } element.incoming.forEach(function(connection) { if (is$1(connection, 'bpmn:MessageFlow')) { incoming.push(connection); } }); element.outgoing.forEach(function(connection) { if (is$1(connection, 'bpmn:MessageFlow')) { outgoing.push(connection); } }); }, []); return { incoming: incoming, outgoing: outgoing }; } var COLLAB_ERR_MSG = 'flow elements must be children of pools/participants'; function ModelingFeedback(eventBus, tooltips, translate) { function showError(position, message, timeout) { tooltips.add({ position: { x: position.x + 5, y: position.y + 5 }, type: 'error', timeout: timeout || 2000, html: '
    ' + message + '
    ' }); } eventBus.on([ 'shape.move.rejected', 'create.rejected' ], function(event) { var context = event.context, shape = context.shape, target = context.target; if (is$1(target, 'bpmn:Collaboration') && is$1(shape, 'bpmn:FlowNode')) { showError(event, translate(COLLAB_ERR_MSG)); } }); } ModelingFeedback.$inject = [ 'eventBus', 'tooltips', 'translate' ]; /** * BPMN specific behavior ensuring that bpmndi:Label's dc:Bounds are removed * when shape is resized. */ function RemoveEmbeddedLabelBoundsBehavior(eventBus, modeling) { CommandInterceptor.call(this, eventBus); this.preExecute('shape.resize', function(context) { var shape = context.shape; var di = getDi(shape), label = di && di.get('label'), bounds = label && label.get('bounds'); if (bounds) { modeling.updateModdleProperties(shape, label, { bounds: undefined }); } }, true); } e(RemoveEmbeddedLabelBoundsBehavior, CommandInterceptor); RemoveEmbeddedLabelBoundsBehavior.$inject = [ 'eventBus', 'modeling' ]; function RemoveElementBehavior(eventBus, bpmnRules, modeling) { CommandInterceptor.call(this, eventBus); /** * Combine sequence flows when deleting an element * if there is one incoming and one outgoing * sequence flow */ this.preExecute('shape.delete', function(e) { var shape = e.context.shape; // only handle [a] -> [shape] -> [b] patterns if (shape.incoming.length !== 1 || shape.outgoing.length !== 1) { return; } var inConnection = shape.incoming[0], outConnection = shape.outgoing[0]; // only handle sequence flows if (!is$1(inConnection, 'bpmn:SequenceFlow') || !is$1(outConnection, 'bpmn:SequenceFlow')) { return; } if (bpmnRules.canConnect(inConnection.source, outConnection.target, inConnection)) { // compute new, combined waypoints var newWaypoints = getNewWaypoints(inConnection.waypoints, outConnection.waypoints); modeling.reconnectEnd(inConnection, outConnection.target, newWaypoints); } }); } e(RemoveElementBehavior, CommandInterceptor); RemoveElementBehavior.$inject = [ 'eventBus', 'bpmnRules', 'modeling' ]; // helpers ////////////////////// function getDocking$1(point) { return point.original || point; } function getNewWaypoints(inWaypoints, outWaypoints) { var intersection = lineIntersect( getDocking$1(inWaypoints[inWaypoints.length - 2]), getDocking$1(inWaypoints[inWaypoints.length - 1]), getDocking$1(outWaypoints[1]), getDocking$1(outWaypoints[0])); if (intersection) { return [].concat( inWaypoints.slice(0, inWaypoints.length - 1), [ intersection ], outWaypoints.slice(1)); } else { return [ getDocking$1(inWaypoints[0]), getDocking$1(outWaypoints[outWaypoints.length - 1]) ]; } } /** * BPMN specific remove behavior */ function RemoveParticipantBehavior(eventBus, modeling) { CommandInterceptor.call(this, eventBus); /** * morph collaboration diagram into process diagram * after the last participant has been removed */ this.preExecute('shape.delete', function(context) { var shape = context.shape, parent = shape.parent; // activate the behavior if the shape to be removed // is a participant if (is$1(shape, 'bpmn:Participant')) { context.collaborationRoot = parent; } }, true); this.postExecute('shape.delete', function(context) { var collaborationRoot = context.collaborationRoot; if (collaborationRoot && !collaborationRoot.businessObject.participants.length) { // replace empty collaboration with process diagram modeling.makeProcess(); } }, true); } RemoveParticipantBehavior.$inject = [ 'eventBus', 'modeling' ]; e(RemoveParticipantBehavior, CommandInterceptor); function ReplaceConnectionBehavior(eventBus, modeling, bpmnRules, injector) { CommandInterceptor.call(this, eventBus); var dragging = injector.get('dragging', false); function fixConnection(connection) { var source = connection.source, target = connection.target, parent = connection.parent; // do not do anything if connection // is already deleted (may happen due to other // behaviors plugged-in before) if (!parent) { return; } var replacementType, remove; /** * Check if incoming or outgoing connections * can stay or could be substituted with an * appropriate replacement. * * This holds true for SequenceFlow <> MessageFlow. */ if (is$1(connection, 'bpmn:SequenceFlow')) { if (!bpmnRules.canConnectSequenceFlow(source, target)) { remove = true; } if (bpmnRules.canConnectMessageFlow(source, target)) { replacementType = 'bpmn:MessageFlow'; } } // transform message flows into sequence flows, if possible if (is$1(connection, 'bpmn:MessageFlow')) { if (!bpmnRules.canConnectMessageFlow(source, target)) { remove = true; } if (bpmnRules.canConnectSequenceFlow(source, target)) { replacementType = 'bpmn:SequenceFlow'; } } if (is$1(connection, 'bpmn:Association') && !bpmnRules.canConnectAssociation(source, target)) { remove = true; } // remove invalid connection, // unless it has been removed already if (remove) { modeling.removeConnection(connection); } // replace SequenceFlow <> MessageFlow if (replacementType) { modeling.connect(source, target, { type: replacementType, waypoints: connection.waypoints.slice() }); } } function replaceReconnectedConnection(event) { var context = event.context, connection = context.connection, source = context.newSource || connection.source, target = context.newTarget || connection.target, allowed, replacement; allowed = bpmnRules.canConnect(source, target); if (!allowed || allowed.type === connection.type) { return; } replacement = modeling.connect(source, target, { type: allowed.type, waypoints: connection.waypoints.slice() }); // remove old connection modeling.removeConnection(connection); // replace connection in context to reconnect end/start context.connection = replacement; if (dragging) { cleanDraggingSelection(connection, replacement); } } // monkey-patch selection saved in dragging in order to re-select it when operation is finished function cleanDraggingSelection(oldConnection, newConnection) { var context = dragging.context(), previousSelection = context && context.payload.previousSelection, index; // do nothing if not dragging or no selection was present if (!previousSelection || !previousSelection.length) { return; } index = previousSelection.indexOf(oldConnection); if (index === -1) { return; } previousSelection.splice(index, 1, newConnection); } // lifecycle hooks this.postExecuted('elements.move', function(context) { var closure = context.closure, allConnections = closure.allConnections; forEach$1(allConnections, fixConnection); }, true); this.preExecute('connection.reconnect', replaceReconnectedConnection); this.postExecuted('element.updateProperties', function(event) { var context = event.context, properties = context.properties, element = context.element, businessObject = element.businessObject, connection; // remove condition on change to default if (properties.default) { connection = find( element.outgoing, matchPattern({ id: element.businessObject.default.id }) ); if (connection) { modeling.updateProperties(connection, { conditionExpression: undefined }); } } // remove default from source on change to conditional if (properties.conditionExpression && businessObject.sourceRef.default === businessObject) { modeling.updateProperties(element.source, { default: undefined }); } }); } e(ReplaceConnectionBehavior, CommandInterceptor); ReplaceConnectionBehavior.$inject = [ 'eventBus', 'modeling', 'bpmnRules', 'injector' ]; /** * BPMN-specific replace behavior. */ function ReplaceElementBehaviour( bpmnReplace, bpmnRules, elementRegistry, injector, modeling, selection ) { injector.invoke(CommandInterceptor, this); this._bpmnReplace = bpmnReplace; this._elementRegistry = elementRegistry; this._selection = selection; // replace elements on create, e.g. during copy-paste this.postExecuted([ 'elements.create' ], 500, function(event) { var context = event.context, target = context.parent, elements = context.elements; var elementReplacements = reduce(elements, function(replacements, element) { var canReplace = bpmnRules.canReplace([ element ], element.host || element.parent || target); return canReplace ? replacements.concat(canReplace.replacements) : replacements; }, []); if (elementReplacements.length) { this.replaceElements(elements, elementReplacements); } }, this); // replace elements on move this.postExecuted([ 'elements.move' ], 500, function(event) { var context = event.context, target = context.newParent, newHost = context.newHost, elements = []; forEach$1(context.closure.topLevel, function(topLevelElements) { if (isEventSubProcess(topLevelElements)) { elements = elements.concat(topLevelElements.children); } else { elements = elements.concat(topLevelElements); } }); // set target to host if attaching if (elements.length === 1 && newHost) { target = newHost; } var canReplace = bpmnRules.canReplace(elements, target); if (canReplace) { this.replaceElements(elements, canReplace.replacements, newHost); } }, this); // update attachments on host replace this.postExecute([ 'shape.replace' ], 1500, function(e) { var context = e.context, oldShape = context.oldShape, newShape = context.newShape, attachers = oldShape.attachers, canReplace; if (attachers && attachers.length) { canReplace = bpmnRules.canReplace(attachers, newShape); this.replaceElements(attachers, canReplace.replacements); } }, this); // keep ID on shape replace this.postExecuted([ 'shape.replace' ], 1500, function(e) { var context = e.context, oldShape = context.oldShape, newShape = context.newShape; modeling.unclaimId(oldShape.businessObject.id, oldShape.businessObject); modeling.updateProperties(newShape, { id: oldShape.id }); }); } e(ReplaceElementBehaviour, CommandInterceptor); ReplaceElementBehaviour.prototype.replaceElements = function(elements, newElements) { var elementRegistry = this._elementRegistry, bpmnReplace = this._bpmnReplace, selection = this._selection; forEach$1(newElements, function(replacement) { var newElement = { type: replacement.newElementType }; var oldElement = elementRegistry.get(replacement.oldElementId); var idx = elements.indexOf(oldElement); elements[idx] = bpmnReplace.replaceElement(oldElement, newElement, { select: false }); }); if (newElements) { selection.select(elements); } }; ReplaceElementBehaviour.$inject = [ 'bpmnReplace', 'bpmnRules', 'elementRegistry', 'injector', 'modeling', 'selection' ]; var HIGH_PRIORITY$9 = 1500; var GROUP_MIN_DIMENSIONS = { width: 140, height: 120 }; var LANE_MIN_DIMENSIONS = { width: 300, height: 60 }; var PARTICIPANT_MIN_DIMENSIONS = { width: 300, height: 150 }; var SUB_PROCESS_MIN_DIMENSIONS = { width: 140, height: 120 }; var TEXT_ANNOTATION_MIN_DIMENSIONS = { width: 50, height: 30 }; /** * Set minimum bounds/resize constraints on resize. * * @param {EventBus} eventBus */ function ResizeBehavior(eventBus) { eventBus.on('resize.start', HIGH_PRIORITY$9, function(event) { var context = event.context, shape = context.shape, direction = context.direction, balanced = context.balanced; if (is$1(shape, 'bpmn:Lane') || is$1(shape, 'bpmn:Participant')) { context.resizeConstraints = getParticipantResizeConstraints(shape, direction, balanced); } if (is$1(shape, 'bpmn:Participant')) { context.minDimensions = PARTICIPANT_MIN_DIMENSIONS; } if (is$1(shape, 'bpmn:SubProcess') && isExpanded(shape)) { context.minDimensions = SUB_PROCESS_MIN_DIMENSIONS; } if (is$1(shape, 'bpmn:TextAnnotation')) { context.minDimensions = TEXT_ANNOTATION_MIN_DIMENSIONS; } }); } ResizeBehavior.$inject = [ 'eventBus' ]; var abs$2 = Math.abs, min = Math.min, max$2 = Math.max; function addToTrbl(trbl, attr, value, choice) { var current = trbl[attr]; // make sure to set the value if it does not exist // or apply the correct value by comparing against // choice(value, currentValue) trbl[attr] = current === undefined ? value : choice(value, current); } function addMin(trbl, attr, value) { return addToTrbl(trbl, attr, value, min); } function addMax(trbl, attr, value) { return addToTrbl(trbl, attr, value, max$2); } var LANE_RIGHT_PADDING = 20, LANE_LEFT_PADDING = 50, LANE_TOP_PADDING = 20, LANE_BOTTOM_PADDING = 20; function getParticipantResizeConstraints(laneShape, resizeDirection, balanced) { var lanesRoot = getLanesRoot(laneShape); var isFirst = true, isLast = true; // max top/bottom size for lanes var allLanes = collectLanes(lanesRoot, [ lanesRoot ]); var laneTrbl = asTRBL(laneShape); var maxTrbl = {}, minTrbl = {}; if (/e/.test(resizeDirection)) { minTrbl.right = laneTrbl.left + LANE_MIN_DIMENSIONS.width; } else if (/w/.test(resizeDirection)) { minTrbl.left = laneTrbl.right - LANE_MIN_DIMENSIONS.width; } allLanes.forEach(function(other) { var otherTrbl = asTRBL(other); if (/n/.test(resizeDirection)) { if (otherTrbl.top < (laneTrbl.top - 10)) { isFirst = false; } // max top size (based on next element) if (balanced && abs$2(laneTrbl.top - otherTrbl.bottom) < 10) { addMax(maxTrbl, 'top', otherTrbl.top + LANE_MIN_DIMENSIONS.height); } // min top size (based on self or nested element) if (abs$2(laneTrbl.top - otherTrbl.top) < 5) { addMin(minTrbl, 'top', otherTrbl.bottom - LANE_MIN_DIMENSIONS.height); } } if (/s/.test(resizeDirection)) { if (otherTrbl.bottom > (laneTrbl.bottom + 10)) { isLast = false; } // max bottom size (based on previous element) if (balanced && abs$2(laneTrbl.bottom - otherTrbl.top) < 10) { addMin(maxTrbl, 'bottom', otherTrbl.bottom - LANE_MIN_DIMENSIONS.height); } // min bottom size (based on self or nested element) if (abs$2(laneTrbl.bottom - otherTrbl.bottom) < 5) { addMax(minTrbl, 'bottom', otherTrbl.top + LANE_MIN_DIMENSIONS.height); } } }); // max top/bottom/left/right size based on flow nodes var flowElements = lanesRoot.children.filter(function(s) { return !s.hidden && !s.waypoints && (is$1(s, 'bpmn:FlowElement') || is$1(s, 'bpmn:Artifact')); }); flowElements.forEach(function(flowElement) { var flowElementTrbl = asTRBL(flowElement); if (isFirst && /n/.test(resizeDirection)) { addMin(minTrbl, 'top', flowElementTrbl.top - LANE_TOP_PADDING); } if (/e/.test(resizeDirection)) { addMax(minTrbl, 'right', flowElementTrbl.right + LANE_RIGHT_PADDING); } if (isLast && /s/.test(resizeDirection)) { addMax(minTrbl, 'bottom', flowElementTrbl.bottom + LANE_BOTTOM_PADDING); } if (/w/.test(resizeDirection)) { addMin(minTrbl, 'left', flowElementTrbl.left - LANE_LEFT_PADDING); } }); return { min: minTrbl, max: maxTrbl }; } var SLIGHTLY_HIGHER_PRIORITY = 1001; /** * Invoke {@link Modeling#resizeLane} instead of * {@link Modeling#resizeShape} when resizing a Lane * or Participant shape. */ function ResizeLaneBehavior(eventBus, modeling) { eventBus.on('resize.start', SLIGHTLY_HIGHER_PRIORITY + 500, function(event) { var context = event.context, shape = context.shape; if (is$1(shape, 'bpmn:Lane') || is$1(shape, 'bpmn:Participant')) { // should we resize the opposite lane(s) in // order to compensate for the resize operation? context.balanced = !hasPrimaryModifier(event); } }); /** * Intercept resize end and call resize lane function instead. */ eventBus.on('resize.end', SLIGHTLY_HIGHER_PRIORITY, function(event) { var context = event.context, shape = context.shape, canExecute = context.canExecute, newBounds = context.newBounds; if (is$1(shape, 'bpmn:Lane') || is$1(shape, 'bpmn:Participant')) { if (canExecute) { // ensure we have actual pixel values for new bounds // (important when zoom level was > 1 during move) newBounds = roundBounds(newBounds); // perform the actual resize modeling.resizeLane(shape, newBounds, context.balanced); } // stop propagation return false; } }); } ResizeLaneBehavior.$inject = [ 'eventBus', 'modeling' ]; var LOW_PRIORITY$a = 500; /** * Add referenced root elements (error, escalation, message, signal) if they don't exist. * Copy referenced root elements on copy & paste. */ function RootElementReferenceBehavior( bpmnjs, eventBus, injector, moddleCopy, bpmnFactory ) { injector.invoke(CommandInterceptor, this); function canHaveRootElementReference(element) { return isAny(element, [ 'bpmn:ReceiveTask', 'bpmn:SendTask' ]) || hasAnyEventDefinition(element, [ 'bpmn:ErrorEventDefinition', 'bpmn:EscalationEventDefinition', 'bpmn:MessageEventDefinition', 'bpmn:SignalEventDefinition' ]); } function hasRootElement(rootElement) { var definitions = bpmnjs.getDefinitions(), rootElements = definitions.get('rootElements'); return !!find(rootElements, matchPattern({ id: rootElement.id })); } function getRootElementReferencePropertyName(eventDefinition) { if (is$1(eventDefinition, 'bpmn:ErrorEventDefinition')) { return 'errorRef'; } else if (is$1(eventDefinition, 'bpmn:EscalationEventDefinition')) { return 'escalationRef'; } else if (is$1(eventDefinition, 'bpmn:MessageEventDefinition')) { return 'messageRef'; } else if (is$1(eventDefinition, 'bpmn:SignalEventDefinition')) { return 'signalRef'; } } function getRootElement(businessObject) { if (isAny(businessObject, [ 'bpmn:ReceiveTask', 'bpmn:SendTask' ])) { return businessObject.get('messageRef'); } var eventDefinitions = businessObject.get('eventDefinitions'), eventDefinition = eventDefinitions[ 0 ]; return eventDefinition.get(getRootElementReferencePropertyName(eventDefinition)); } function setRootElement(businessObject, rootElement) { if (isAny(businessObject, [ 'bpmn:ReceiveTask', 'bpmn:SendTask' ])) { return businessObject.set('messageRef', rootElement); } var eventDefinitions = businessObject.get('eventDefinitions'), eventDefinition = eventDefinitions[ 0 ]; return eventDefinition.set(getRootElementReferencePropertyName(eventDefinition), rootElement); } // create shape this.executed('shape.create', function(context) { var shape = context.shape; if (!canHaveRootElementReference(shape)) { return; } var businessObject = getBusinessObject(shape), rootElement = getRootElement(businessObject), rootElements; if (rootElement && !hasRootElement(rootElement)) { rootElements = bpmnjs.getDefinitions().get('rootElements'); // add root element add(rootElements, rootElement); context.addedRootElement = rootElement; } }, true); this.reverted('shape.create', function(context) { var addedRootElement = context.addedRootElement; if (!addedRootElement) { return; } var rootElements = bpmnjs.getDefinitions().get('rootElements'); // remove root element remove(rootElements, addedRootElement); }, true); eventBus.on('copyPaste.copyElement', function(context) { var descriptor = context.descriptor, element = context.element; if (element.labelTarget || !canHaveRootElementReference(element)) { return; } var businessObject = getBusinessObject(element), rootElement = getRootElement(businessObject); if (rootElement) { // TODO(nikku): clone on copy descriptor.referencedRootElement = rootElement; } }); eventBus.on('copyPaste.pasteElement', LOW_PRIORITY$a, function(context) { var descriptor = context.descriptor, businessObject = descriptor.businessObject, referencedRootElement = descriptor.referencedRootElement; if (!referencedRootElement) { return; } if (!hasRootElement(referencedRootElement)) { referencedRootElement = moddleCopy.copyElement( referencedRootElement, bpmnFactory.create(referencedRootElement.$type) ); } setRootElement(businessObject, referencedRootElement); delete descriptor.referencedRootElement; }); } RootElementReferenceBehavior.$inject = [ 'bpmnjs', 'eventBus', 'injector', 'moddleCopy', 'bpmnFactory' ]; e(RootElementReferenceBehavior, CommandInterceptor); // helpers ////////// function hasAnyEventDefinition(element, types) { if (!isArray$3(types)) { types = [ types ]; } return some(types, function(type) { return hasEventDefinition$2(element, type); }); } var max$1 = Math.max; function SpaceToolBehavior(eventBus) { eventBus.on('spaceTool.getMinDimensions', function(context) { var shapes = context.shapes, axis = context.axis, start = context.start, minDimensions = {}; forEach$1(shapes, function(shape) { var id = shape.id; if (is$1(shape, 'bpmn:Participant')) { if (isHorizontal$1(axis)) { minDimensions[ id ] = PARTICIPANT_MIN_DIMENSIONS; } else { minDimensions[ id ] = { width: PARTICIPANT_MIN_DIMENSIONS.width, height: getParticipantMinHeight(shape, start) }; } } if (is$1(shape, 'bpmn:SubProcess') && isExpanded(shape)) { minDimensions[ id ] = SUB_PROCESS_MIN_DIMENSIONS; } if (is$1(shape, 'bpmn:TextAnnotation')) { minDimensions[ id ] = TEXT_ANNOTATION_MIN_DIMENSIONS; } if (is$1(shape, 'bpmn:Group')) { minDimensions[ id ] = GROUP_MIN_DIMENSIONS; } }); return minDimensions; }); } SpaceToolBehavior.$inject = [ 'eventBus' ]; // helpers ////////// function isHorizontal$1(axis) { return axis === 'x'; } /** * Get minimum height for participant taking lanes into account. * * @param {} participant * @param {number} start * * @returns {Object} */ function getParticipantMinHeight(participant, start) { var lanesMinHeight; if (!hasChildLanes(participant)) { return PARTICIPANT_MIN_DIMENSIONS.height; } lanesMinHeight = getLanesMinHeight(participant, start); return max$1(PARTICIPANT_MIN_DIMENSIONS.height, lanesMinHeight); } function hasChildLanes(element) { return !!getChildLanes(element).length; } function getLanesMinHeight(participant, resizeStart) { var lanes = getChildLanes(participant), resizedLane; // find the nested lane which is currently resized resizedLane = findResizedLane(lanes, resizeStart); // resized lane cannot shrink below the minimum height // but remaining lanes' dimensions are kept intact return participant.height - resizedLane.height + LANE_MIN_DIMENSIONS.height; } /** * Find nested lane which is currently resized. * * @param {Array} lanes * @param {number} resizeStart */ function findResizedLane(lanes, resizeStart) { var i, lane, childLanes; for (i = 0; i < lanes.length; i++) { lane = lanes[i]; // resizing current lane or a lane nested if (resizeStart >= lane.y && resizeStart <= lane.y + lane.height) { childLanes = getChildLanes(lane); // a nested lane is resized if (childLanes.length) { return findResizedLane(childLanes, resizeStart); } // current lane is the resized one return lane; } } } var LOW_PRIORITY$9 = 400; var HIGH_PRIORITY$8 = 600; var DEFAULT_POSITION = { x: 180, y: 160 }; /** * Creates bpmndi:BPMNPlane elements and canvas planes when collapsed subprocesses are created. * * * @param {Canvas} canvas * @param {EventBus} eventBus * @param {Modeling} modeling * @param {ElementFactory} elementFactory * @param {BpmnFactory} bpmnFactory * @param {Bpmnjs} bpmnjs * @param {ElementRegistry} elementRegistry */ function SubProcessPlaneBehavior( canvas, eventBus, modeling, elementFactory, bpmnFactory, bpmnjs, elementRegistry) { CommandInterceptor.call(this, eventBus); this._canvas = canvas; this._eventBus = eventBus; this._modeling = modeling; this._elementFactory = elementFactory; this._bpmnFactory = bpmnFactory; this._bpmnjs = bpmnjs; this._elementRegistry = elementRegistry; var self = this; function isCollapsedSubProcess(element) { return is$1(element, 'bpmn:SubProcess') && !isExpanded(element); } function createRoot(context) { var shape = context.shape, rootElement = context.newRootElement; var businessObject = getBusinessObject(shape); rootElement = self._addDiagram(rootElement || businessObject); context.newRootElement = canvas.addRootElement(rootElement); } function removeRoot(context) { var shape = context.shape; var businessObject = getBusinessObject(shape); self._removeDiagram(businessObject); var rootElement = context.newRootElement = elementRegistry.get(getPlaneIdFromShape(businessObject)); canvas.removeRootElement(rootElement); } // add plane elements for newly created sub-processes // this ensures we can actually drill down into the element this.executed('shape.create', function(context) { var shape = context.shape; if (!isCollapsedSubProcess(shape)) { return; } createRoot(context); }, true); this.postExecuted('shape.create', function(context) { var shape = context.shape, rootElement = context.newRootElement; if (!rootElement || !shape.children) { return; } self._showRecursively(shape.children); self._moveChildrenToShape(shape, rootElement); }, true); this.reverted('shape.create', function(context) { var shape = context.shape; if (!isCollapsedSubProcess(shape)) { return; } removeRoot(context); }, true); this.preExecuted('shape.delete', function(context) { var shape = context.shape; if (!isCollapsedSubProcess(shape)) { return; } var attachedRoot = elementRegistry.get(getPlaneIdFromShape(shape)); if (!attachedRoot) { return; } modeling.removeElements(attachedRoot.children.slice()); }, true); this.executed('shape.delete', function(context) { var shape = context.shape; if (!isCollapsedSubProcess(shape)) { return; } removeRoot(context); }, true); this.reverted('shape.delete', function(context) { var shape = context.shape; if (!isCollapsedSubProcess(shape)) { return; } createRoot(context); }, true); this.preExecuted('shape.replace', function(context) { var oldShape = context.oldShape; var newShape = context.newShape; if (!isCollapsedSubProcess(oldShape) || !isCollapsedSubProcess(newShape)) { return; } // old plane could have content, // we remove it so it is not recursively deleted from 'shape.delete' context.oldRoot = canvas.removeRootElement(getPlaneIdFromShape(oldShape)); }, true); this.postExecuted('shape.replace', function(context) { var newShape = context.newShape, source = context.oldRoot, target = canvas.findRoot(getPlaneIdFromShape(newShape)); if (!source || !target) { return; } var elements = source.children; modeling.moveElements(elements, { x: 0, y: 0 }, target); }, true); // rename primary elements when the secondary element changes // this ensures rootElement.id = element.id + '_plane' this.executed('element.updateProperties', function(context) { var shape = context.element; if (!is$1(shape, 'bpmn:SubProcess')) { return; } var properties = context.properties; var oldProperties = context.oldProperties; var oldId = oldProperties.id, newId = properties.id; if (oldId === newId) { return; } if (isPlane(shape)) { elementRegistry.updateId(shape, toPlaneId(newId)); elementRegistry.updateId(oldId, newId); return; } var planeElement = elementRegistry.get(toPlaneId(oldId)); if (!planeElement) { return; } elementRegistry.updateId(toPlaneId(oldId), toPlaneId(newId)); }, true); this.reverted('element.updateProperties', function(context) { var shape = context.element; if (!is$1(shape, 'bpmn:SubProcess')) { return; } var properties = context.properties; var oldProperties = context.oldProperties; var oldId = oldProperties.id, newId = properties.id; if (oldId === newId) { return; } if (isPlane(shape)) { elementRegistry.updateId(shape, toPlaneId(oldId)); elementRegistry.updateId(newId, oldId); return; } var planeElement = elementRegistry.get(toPlaneId(newId)); if (!planeElement) { return; } elementRegistry.updateId(planeElement, toPlaneId(oldId)); }, true); // re-throw element.changed to re-render primary shape if associated plane has // changed (e.g. bpmn:name property has changed) eventBus.on('element.changed', function(context) { var element = context.element; if (!isPlane(element)) { return; } var plane = element; var primaryShape = elementRegistry.get(getShapeIdFromPlane(plane)); // do not re-throw if no associated primary shape (e.g. bpmn:Process) if (!primaryShape || primaryShape === plane) { return; } eventBus.fire('element.changed', { element: primaryShape }); }); // create/remove plane for the subprocess this.executed('shape.toggleCollapse', LOW_PRIORITY$9, function(context) { var shape = context.shape; if (!is$1(shape, 'bpmn:SubProcess')) { return; } if (!isExpanded(shape)) { createRoot(context); self._showRecursively(shape.children); } else { removeRoot(context); } }, true); // create/remove plane for the subprocess this.reverted('shape.toggleCollapse', LOW_PRIORITY$9, function(context) { var shape = context.shape; if (!is$1(shape, 'bpmn:SubProcess')) { return; } if (!isExpanded(shape)) { createRoot(context); self._showRecursively(shape.children); } else { removeRoot(context); } }, true); // move elements between planes this.postExecuted('shape.toggleCollapse', HIGH_PRIORITY$8, function(context) { var shape = context.shape; if (!is$1(shape, 'bpmn:SubProcess')) { return; } var rootElement = context.newRootElement; if (!rootElement) { return; } if (!isExpanded(shape)) { // collapsed self._moveChildrenToShape(shape, rootElement); } else { self._moveChildrenToShape(rootElement, shape); } }, true); // copy-paste /////////// // add elements in plane to tree eventBus.on('copyPaste.createTree', function(context) { var element = context.element, children = context.children; if (!isCollapsedSubProcess(element)) { return; } var id = getPlaneIdFromShape(element); var parent = elementRegistry.get(id); if (parent) { // do not copy invisible root element children.push.apply(children, parent.children); } }); // set plane children as direct children of collapsed shape eventBus.on('copyPaste.copyElement', function(context) { var descriptor = context.descriptor, element = context.element, elements = context.elements; var parent = element.parent; var isPlane = is$1(getDi(parent), 'bpmndi:BPMNPlane'); if (!isPlane) { return; } var parentId = getShapeIdFromPlane(parent); var referencedShape = find(elements, function(element) { return element.id === parentId; }); if (!referencedShape) { return; } descriptor.parent = referencedShape.id; }); // hide children during pasting eventBus.on('copyPaste.pasteElement', function(context) { var descriptor = context.descriptor; if (!descriptor.parent) { return; } if (isCollapsedSubProcess(descriptor.parent) || descriptor.parent.hidden) { descriptor.hidden = true; } }); } e(SubProcessPlaneBehavior, CommandInterceptor); /** * Moves the child elements from source to target. * * If the target is a plane, the children are moved to the top left corner. * Otherwise, the center of the target is used. * * @param {Object|djs.model.Base} source * @param {Object|djs.model.Base} target */ SubProcessPlaneBehavior.prototype._moveChildrenToShape = function(source, target) { var modeling = this._modeling; var children = source.children; var offset; if (!children) { return; } // add external labels that weren't children of sub process children = children.concat(children.reduce(function(labels, child) { if (child.label && child.label.parent !== source) { return labels.concat(child.label); } return labels; }, [])); // only change plane if there are no visible children, but don't move them var visibleChildren = children.filter(function(child) { return !child.hidden; }); if (!visibleChildren.length) { modeling.moveElements(children, { x: 0, y: 0 }, target, { autoResize: false }); return; } var childrenBounds = getBBox(visibleChildren); // target is a plane if (!target.x) { offset = { x: DEFAULT_POSITION.x - childrenBounds.x, y: DEFAULT_POSITION.y - childrenBounds.y }; } // source is a plane else { // move relative to the center of the shape var targetMid = getMid(target); var childrenMid = getMid(childrenBounds); offset = { x: targetMid.x - childrenMid.x, y: targetMid.y - childrenMid.y }; } modeling.moveElements(children, offset, target, { autoResize: false }); }; /** * Sets `hidden` property on all children of the given shape. * * @param {Array} elements * @param {Boolean} [hidden] * @returns {Array} all child elements */ SubProcessPlaneBehavior.prototype._showRecursively = function(elements, hidden) { var self = this; var result = []; elements.forEach(function(element) { element.hidden = !!hidden; result = result.concat(element); if (element.children) { result = result.concat( self._showRecursively(element.children, element.collapsed || hidden) ); } }); return result; }; /** * Adds a given rootElement to the bpmnDi diagrams. * * @param {Object} rootElement * @returns {Object} planeElement */ SubProcessPlaneBehavior.prototype._addDiagram = function(planeElement) { var bpmnjs = this._bpmnjs; var diagrams = bpmnjs.getDefinitions().diagrams; if (!planeElement.businessObject) { planeElement = this._createNewDiagram(planeElement); } diagrams.push(planeElement.di.$parent); return planeElement; }; /** * Creates a new plane element for the given sub process. * * @param {Object} bpmnElement * * @return {Object} new diagram element */ SubProcessPlaneBehavior.prototype._createNewDiagram = function(bpmnElement) { var bpmnFactory = this._bpmnFactory; var elementFactory = this._elementFactory; var diPlane = bpmnFactory.create('bpmndi:BPMNPlane', { bpmnElement: bpmnElement }); var diDiagram = bpmnFactory.create('bpmndi:BPMNDiagram', { plane: diPlane }); diPlane.$parent = diDiagram; // add a virtual element (not being drawn), // a copy cat of our BpmnImporter code var planeElement = elementFactory.createRoot({ id: getPlaneIdFromShape(bpmnElement), type: bpmnElement.$type, di: diPlane, businessObject: bpmnElement, collapsed: true }); return planeElement; }; /** * Removes the diagram for a given root element * * @param {Object} rootElement * @returns {Object} removed bpmndi:BPMNDiagram */ SubProcessPlaneBehavior.prototype._removeDiagram = function(rootElement) { var bpmnjs = this._bpmnjs; var diagrams = bpmnjs.getDefinitions().diagrams; var removedDiagram = find(diagrams, function(diagram) { return diagram.plane.bpmnElement.id === rootElement.id; }); diagrams.splice(diagrams.indexOf(removedDiagram), 1); return removedDiagram; }; SubProcessPlaneBehavior.$inject = [ 'canvas', 'eventBus', 'modeling', 'elementFactory', 'bpmnFactory', 'bpmnjs', 'elementRegistry' ]; /** * Add start event replacing element with expanded sub process. * * @param {Injector} injector * @param {Modeling} modeling */ function SubProcessStartEventBehavior(injector, modeling) { injector.invoke(CommandInterceptor, this); this.postExecuted('shape.replace', function(event) { var oldShape = event.context.oldShape, newShape = event.context.newShape; if ( !is$1(newShape, 'bpmn:SubProcess') || ! (is$1(oldShape, 'bpmn:Task') || is$1(oldShape, 'bpmn:CallActivity')) || !isExpanded(newShape) ) { return; } var position = getStartEventPosition(newShape); modeling.createShape({ type: 'bpmn:StartEvent' }, position, newShape); }); } SubProcessStartEventBehavior.$inject = [ 'injector', 'modeling' ]; e(SubProcessStartEventBehavior, CommandInterceptor); // helpers ////////// function getStartEventPosition(shape) { return { x: shape.x + shape.width / 6, y: shape.y + shape.height / 2 }; } function ToggleCollapseConnectionBehaviour( eventBus, modeling ) { CommandInterceptor.call(this, eventBus); this.postExecuted('shape.toggleCollapse', 1500, function(context) { // var shape = context.shape; var shape = context.shape; // only change connections when collapsing if (isExpanded(shape)) { return; } var allChildren = selfAndAllChildren(shape); allChildren.forEach(function(child) { // Ensure that the connection array is not modified during iteration var incomingConnections = child.incoming.slice(), outgoingConnections = child.outgoing.slice(); forEach$1(incomingConnections, function(c) { handleConnection(c, true); }); forEach$1(outgoingConnections, function(c) { handleConnection(c, false); }); }); function handleConnection(c, incoming) { if (allChildren.indexOf(c.source) !== -1 && allChildren.indexOf(c.target) !== -1) { return; } if (incoming) { modeling.reconnectEnd(c, shape, getMid(shape)); } else { modeling.reconnectStart(c, shape, getMid(shape)); } } }, true); } e(ToggleCollapseConnectionBehaviour, CommandInterceptor); ToggleCollapseConnectionBehaviour.$inject = [ 'eventBus', 'modeling', ]; var LOW_PRIORITY$8 = 500; function ToggleElementCollapseBehaviour( eventBus, elementFactory, modeling, resize) { CommandInterceptor.call(this, eventBus); function hideEmptyLabels(children) { if (children.length) { children.forEach(function(child) { if (child.type === 'label' && !child.businessObject.name) { child.hidden = true; } }); } } function expandedBounds(shape, defaultSize) { var children = shape.children, newBounds = defaultSize, visibleElements, visibleBBox; visibleElements = filterVisible(children).concat([ shape ]); visibleBBox = computeChildrenBBox(visibleElements); if (visibleBBox) { // center to visibleBBox with max(defaultSize, childrenBounds) newBounds.width = Math.max(visibleBBox.width, newBounds.width); newBounds.height = Math.max(visibleBBox.height, newBounds.height); newBounds.x = visibleBBox.x + (visibleBBox.width - newBounds.width) / 2; newBounds.y = visibleBBox.y + (visibleBBox.height - newBounds.height) / 2; } else { // center to collapsed shape with defaultSize newBounds.x = shape.x + (shape.width - newBounds.width) / 2; newBounds.y = shape.y + (shape.height - newBounds.height) / 2; } return newBounds; } function collapsedBounds(shape, defaultSize) { return { x: shape.x + (shape.width - defaultSize.width) / 2, y: shape.y + (shape.height - defaultSize.height) / 2, width: defaultSize.width, height: defaultSize.height }; } this.executed([ 'shape.toggleCollapse' ], LOW_PRIORITY$8, function(e) { var context = e.context, shape = context.shape; if (!is$1(shape, 'bpmn:SubProcess')) { return; } if (!shape.collapsed) { // all children got made visible through djs, hide empty labels hideEmptyLabels(shape.children); // remove collapsed marker getDi(shape).isExpanded = true; } else { // place collapsed marker getDi(shape).isExpanded = false; } }); this.reverted([ 'shape.toggleCollapse' ], LOW_PRIORITY$8, function(e) { var context = e.context; var shape = context.shape; // revert removing/placing collapsed marker if (!shape.collapsed) { getDi(shape).isExpanded = true; } else { getDi(shape).isExpanded = false; } }); this.postExecuted([ 'shape.toggleCollapse' ], LOW_PRIORITY$8, function(e) { var shape = e.context.shape, defaultSize = elementFactory.getDefaultSize(shape), newBounds; if (shape.collapsed) { // resize to default size of collapsed shapes newBounds = collapsedBounds(shape, defaultSize); } else { // resize to bounds of max(visible children, defaultSize) newBounds = expandedBounds(shape, defaultSize); } modeling.resizeShape(shape, newBounds, null, { autoResize: shape.collapsed ? false : 'nwse' }); }); } e(ToggleElementCollapseBehaviour, CommandInterceptor); ToggleElementCollapseBehaviour.$inject = [ 'eventBus', 'elementFactory', 'modeling' ]; // helpers ////////////////////// function filterVisible(elements) { return elements.filter(function(e) { return !e.hidden; }); } /** * Unclaims model IDs on element deletion. * * @param {Canvas} canvas * @param {Injector} injector * @param {Moddle} moddle * @param {Modeling} modeling */ function UnclaimIdBehavior(canvas, injector, moddle, modeling) { injector.invoke(CommandInterceptor, this); this.preExecute('shape.delete', function(event) { var context = event.context, shape = context.shape, shapeBo = shape.businessObject; if (isLabel$6(shape)) { return; } if (is$1(shape, 'bpmn:Participant') && isExpanded(shape)) { moddle.ids.unclaim(shapeBo.processRef.id); } modeling.unclaimId(shapeBo.id, shapeBo); }); this.preExecute('connection.delete', function(event) { var context = event.context, connection = context.connection, connectionBo = connection.businessObject; modeling.unclaimId(connectionBo.id, connectionBo); }); this.preExecute('canvas.updateRoot', function() { var rootElement = canvas.getRootElement(), rootElementBo = rootElement.businessObject; if (is$1(rootElement, 'bpmn:Collaboration')) { moddle.ids.unclaim(rootElementBo.id); } }); } e(UnclaimIdBehavior, CommandInterceptor); UnclaimIdBehavior.$inject = [ 'canvas', 'injector', 'moddle', 'modeling' ]; /** * A behavior that unsets the Default property of * sequence flow source on element delete, if the * removed element is the Gateway or Task's default flow. * * @param {EventBus} eventBus * @param {Modeling} modeling */ function DeleteSequenceFlowBehavior(eventBus, modeling) { CommandInterceptor.call(this, eventBus); this.preExecute('connection.delete', function(event) { var context = event.context, connection = context.connection, source = connection.source; if (isDefaultFlow(connection, source)) { modeling.updateProperties(source, { 'default': null }); } }); } e(DeleteSequenceFlowBehavior, CommandInterceptor); DeleteSequenceFlowBehavior.$inject = [ 'eventBus', 'modeling' ]; // helpers ////////////////////// function isDefaultFlow(connection, source) { if (!is$1(connection, 'bpmn:SequenceFlow')) { return false; } var sourceBo = getBusinessObject(source), sequenceFlow = getBusinessObject(connection); return sourceBo.get('default') === sequenceFlow; } var LOW_PRIORITY$7 = 500, HIGH_PRIORITY$7 = 5000; /** * BPMN specific delete lane behavior */ function UpdateFlowNodeRefsBehavior(eventBus, modeling, translate) { CommandInterceptor.call(this, eventBus); /** * Ok, this is it: * * We have to update the Lane#flowNodeRefs _and_ * FlowNode#lanes with every FlowNode move/resize and * Lane move/resize. * * We want to group that stuff to recompute containments * as efficient as possible. * * Yea! */ // the update context var context; function initContext() { context = context || new UpdateContext(); context.enter(); return context; } function getContext() { if (!context) { throw new Error(translate('out of bounds release')); } return context; } function releaseContext() { if (!context) { throw new Error(translate('out of bounds release')); } var triggerUpdate = context.leave(); if (triggerUpdate) { modeling.updateLaneRefs(context.flowNodes, context.lanes); context = null; } return triggerUpdate; } var laneRefUpdateEvents = [ 'spaceTool', 'lane.add', 'lane.resize', 'lane.split', 'elements.create', 'elements.delete', 'elements.move', 'shape.create', 'shape.delete', 'shape.move', 'shape.resize' ]; // listen to a lot of stuff to group lane updates this.preExecute(laneRefUpdateEvents, HIGH_PRIORITY$7, function(event) { initContext(); }); this.postExecuted(laneRefUpdateEvents, LOW_PRIORITY$7, function(event) { releaseContext(); }); // Mark flow nodes + lanes that need an update this.preExecute([ 'shape.create', 'shape.move', 'shape.delete', 'shape.resize' ], function(event) { var context = event.context, shape = context.shape; var updateContext = getContext(); // no need to update labels if (shape.labelTarget) { return; } if (is$1(shape, 'bpmn:Lane')) { updateContext.addLane(shape); } if (is$1(shape, 'bpmn:FlowNode')) { updateContext.addFlowNode(shape); } }); } UpdateFlowNodeRefsBehavior.$inject = [ 'eventBus', 'modeling' , 'translate' ]; e(UpdateFlowNodeRefsBehavior, CommandInterceptor); function UpdateContext() { this.flowNodes = []; this.lanes = []; this.counter = 0; this.addLane = function(lane) { this.lanes.push(lane); }; this.addFlowNode = function(flowNode) { this.flowNodes.push(flowNode); }; this.enter = function() { this.counter++; }; this.leave = function() { this.counter--; return !this.counter; }; } var BehaviorModule = { __init__: [ 'adaptiveLabelPositioningBehavior', 'appendBehavior', 'associationBehavior', 'attachEventBehavior', 'boundaryEventBehavior', 'createBehavior', 'createDataObjectBehavior', 'createParticipantBehavior', 'dataInputAssociationBehavior', 'dataStoreBehavior', 'deleteLaneBehavior', 'detachEventBehavior', 'dropOnFlowBehavior', 'eventBasedGatewayBehavior', 'fixHoverBehavior', 'groupBehavior', 'importDockingFix', 'isHorizontalFix', 'labelBehavior', 'layoutConnectionBehavior', 'messageFlowBehavior', 'modelingFeedback', 'removeElementBehavior', 'removeEmbeddedLabelBoundsBehavior', 'removeParticipantBehavior', 'replaceConnectionBehavior', 'replaceElementBehaviour', 'resizeBehavior', 'resizeLaneBehavior', 'rootElementReferenceBehavior', 'spaceToolBehavior', 'subProcessPlaneBehavior', 'subProcessStartEventBehavior', 'toggleCollapseConnectionBehaviour', 'toggleElementCollapseBehaviour', 'unclaimIdBehavior', 'updateFlowNodeRefsBehavior', 'unsetDefaultFlowBehavior' ], adaptiveLabelPositioningBehavior: [ 'type', AdaptiveLabelPositioningBehavior ], appendBehavior: [ 'type', AppendBehavior ], associationBehavior: [ 'type', AssociationBehavior ], attachEventBehavior: [ 'type', AttachEventBehavior ], boundaryEventBehavior: [ 'type', BoundaryEventBehavior ], createBehavior: [ 'type', CreateBehavior ], createDataObjectBehavior: [ 'type', CreateDataObjectBehavior ], createParticipantBehavior: [ 'type', CreateParticipantBehavior ], dataInputAssociationBehavior: [ 'type', DataInputAssociationBehavior ], dataStoreBehavior: [ 'type', DataStoreBehavior ], deleteLaneBehavior: [ 'type', DeleteLaneBehavior ], detachEventBehavior: [ 'type', DetachEventBehavior ], dropOnFlowBehavior: [ 'type', DropOnFlowBehavior ], eventBasedGatewayBehavior: [ 'type', EventBasedGatewayBehavior ], fixHoverBehavior: [ 'type', FixHoverBehavior ], groupBehavior: [ 'type', GroupBehavior ], importDockingFix: [ 'type', ImportDockingFix ], isHorizontalFix: [ 'type', IsHorizontalFix ], labelBehavior: [ 'type', LabelBehavior ], layoutConnectionBehavior: [ 'type', LayoutConnectionBehavior ], messageFlowBehavior: [ 'type', MessageFlowBehavior ], modelingFeedback: [ 'type', ModelingFeedback ], removeElementBehavior: [ 'type', RemoveElementBehavior ], removeEmbeddedLabelBoundsBehavior: [ 'type', RemoveEmbeddedLabelBoundsBehavior ], removeParticipantBehavior: [ 'type', RemoveParticipantBehavior ], replaceConnectionBehavior: [ 'type', ReplaceConnectionBehavior ], replaceElementBehaviour: [ 'type', ReplaceElementBehaviour ], resizeBehavior: [ 'type', ResizeBehavior ], resizeLaneBehavior: [ 'type', ResizeLaneBehavior ], rootElementReferenceBehavior: [ 'type', RootElementReferenceBehavior ], spaceToolBehavior: [ 'type', SpaceToolBehavior ], subProcessPlaneBehavior: [ 'type', SubProcessPlaneBehavior ], subProcessStartEventBehavior: [ 'type', SubProcessStartEventBehavior ], toggleCollapseConnectionBehaviour: [ 'type', ToggleCollapseConnectionBehaviour ], toggleElementCollapseBehaviour : [ 'type', ToggleElementCollapseBehaviour ], unclaimIdBehavior: [ 'type', UnclaimIdBehavior ], unsetDefaultFlowBehavior: [ 'type', DeleteSequenceFlowBehavior ], updateFlowNodeRefsBehavior: [ 'type', UpdateFlowNodeRefsBehavior ] }; function getBoundaryAttachment(position, targetBounds) { var orientation = getOrientation(position, targetBounds, -15); if (orientation !== 'intersect') { return orientation; } else { return null; } } /** * BPMN specific modeling rule */ function BpmnRules(eventBus) { RuleProvider.call(this, eventBus); } e(BpmnRules, RuleProvider); BpmnRules.$inject = [ 'eventBus' ]; BpmnRules.prototype.init = function() { this.addRule('connection.start', function(context) { var source = context.source; return canStartConnection(source); }); this.addRule('connection.create', function(context) { var source = context.source, target = context.target, hints = context.hints || {}, targetParent = hints.targetParent, targetAttach = hints.targetAttach; // don't allow incoming connections on // newly created boundary events // to boundary events if (targetAttach) { return false; } // temporarily set target parent for scoping // checks to work if (targetParent) { target.parent = targetParent; } try { return canConnect(source, target); } finally { // unset temporary target parent if (targetParent) { target.parent = null; } } }); this.addRule('connection.reconnect', function(context) { var connection = context.connection, source = context.source, target = context.target; return canConnect(source, target, connection); }); this.addRule('connection.updateWaypoints', function(context) { return { type: context.connection.type }; }); this.addRule('shape.resize', function(context) { var shape = context.shape, newBounds = context.newBounds; return canResize(shape, newBounds); }); this.addRule('elements.create', function(context) { var elements = context.elements, position = context.position, target = context.target; if (isConnection$8(target) && !canInsert(elements, target)) { return false; } return every(elements, function(element) { if (isConnection$8(element)) { return canConnect(element.source, element.target, element); } if (element.host) { return canAttach(element, element.host, null, position); } return canCreate(element, target, null); }); }); this.addRule('elements.move', function(context) { var target = context.target, shapes = context.shapes, position = context.position; return canAttach(shapes, target, null, position) || canReplace(shapes, target, position) || canMove(shapes, target) || canInsert(shapes, target); }); this.addRule('shape.create', function(context) { return canCreate( context.shape, context.target, context.source, context.position ); }); this.addRule('shape.attach', function(context) { return canAttach( context.shape, context.target, null, context.position ); }); this.addRule('element.copy', function(context) { var element = context.element, elements = context.elements; return canCopy(elements, element); }); }; BpmnRules.prototype.canConnectMessageFlow = canConnectMessageFlow; BpmnRules.prototype.canConnectSequenceFlow = canConnectSequenceFlow; BpmnRules.prototype.canConnectDataAssociation = canConnectDataAssociation; BpmnRules.prototype.canConnectAssociation = canConnectAssociation; BpmnRules.prototype.canMove = canMove; BpmnRules.prototype.canAttach = canAttach; BpmnRules.prototype.canReplace = canReplace; BpmnRules.prototype.canDrop = canDrop; BpmnRules.prototype.canInsert = canInsert; BpmnRules.prototype.canCreate = canCreate; BpmnRules.prototype.canConnect = canConnect; BpmnRules.prototype.canResize = canResize; BpmnRules.prototype.canCopy = canCopy; /** * Utility functions for rule checking */ /** * Checks if given element can be used for starting connection. * * @param {Element} source * @return {boolean} */ function canStartConnection(element) { if (nonExistingOrLabel(element)) { return null; } return isAny(element, [ 'bpmn:FlowNode', 'bpmn:InteractionNode', 'bpmn:DataObjectReference', 'bpmn:DataStoreReference', 'bpmn:Group', 'bpmn:TextAnnotation' ]); } function nonExistingOrLabel(element) { return !element || isLabel$6(element); } function isSame$1(a, b) { return a === b; } function getOrganizationalParent(element) { do { if (is$1(element, 'bpmn:Process')) { return getBusinessObject(element); } if (is$1(element, 'bpmn:Participant')) { return ( getBusinessObject(element).processRef || getBusinessObject(element) ); } } while ((element = element.parent)); } function isTextAnnotation(element) { return is$1(element, 'bpmn:TextAnnotation'); } function isGroup(element) { return is$1(element, 'bpmn:Group') && !element.labelTarget; } function isCompensationBoundary(element) { return is$1(element, 'bpmn:BoundaryEvent') && hasEventDefinition(element, 'bpmn:CompensateEventDefinition'); } function isForCompensation(e) { return getBusinessObject(e).isForCompensation; } function isSameOrganization(a, b) { var parentA = getOrganizationalParent(a), parentB = getOrganizationalParent(b); return parentA === parentB; } function isMessageFlowSource(element) { return ( is$1(element, 'bpmn:InteractionNode') && !is$1(element, 'bpmn:BoundaryEvent') && ( !is$1(element, 'bpmn:Event') || ( is$1(element, 'bpmn:ThrowEvent') && hasEventDefinitionOrNone(element, 'bpmn:MessageEventDefinition') ) ) ); } function isMessageFlowTarget(element) { return ( is$1(element, 'bpmn:InteractionNode') && !isForCompensation(element) && ( !is$1(element, 'bpmn:Event') || ( is$1(element, 'bpmn:CatchEvent') && hasEventDefinitionOrNone(element, 'bpmn:MessageEventDefinition') ) ) && !( is$1(element, 'bpmn:BoundaryEvent') && !hasEventDefinition(element, 'bpmn:MessageEventDefinition') ) ); } function getScopeParent(element) { var parent = element; while ((parent = parent.parent)) { if (is$1(parent, 'bpmn:FlowElementsContainer')) { return getBusinessObject(parent); } if (is$1(parent, 'bpmn:Participant')) { return getBusinessObject(parent).processRef; } } return null; } function isSameScope(a, b) { var scopeParentA = getScopeParent(a), scopeParentB = getScopeParent(b); return scopeParentA === scopeParentB; } function hasEventDefinition(element, eventDefinition) { var bo = getBusinessObject(element); return !!find(bo.eventDefinitions || [], function(definition) { return is$1(definition, eventDefinition); }); } function hasEventDefinitionOrNone(element, eventDefinition) { var bo = getBusinessObject(element); return (bo.eventDefinitions || []).every(function(definition) { return is$1(definition, eventDefinition); }); } function isSequenceFlowSource(element) { return ( is$1(element, 'bpmn:FlowNode') && !is$1(element, 'bpmn:EndEvent') && !isEventSubProcess(element) && !(is$1(element, 'bpmn:IntermediateThrowEvent') && hasEventDefinition(element, 'bpmn:LinkEventDefinition') ) && !isCompensationBoundary(element) && !isForCompensation(element) ); } function isSequenceFlowTarget(element) { return ( is$1(element, 'bpmn:FlowNode') && !is$1(element, 'bpmn:StartEvent') && !is$1(element, 'bpmn:BoundaryEvent') && !isEventSubProcess(element) && !(is$1(element, 'bpmn:IntermediateCatchEvent') && hasEventDefinition(element, 'bpmn:LinkEventDefinition') ) && !isForCompensation(element) ); } function isEventBasedTarget(element) { return ( is$1(element, 'bpmn:ReceiveTask') || ( is$1(element, 'bpmn:IntermediateCatchEvent') && ( hasEventDefinition(element, 'bpmn:MessageEventDefinition') || hasEventDefinition(element, 'bpmn:TimerEventDefinition') || hasEventDefinition(element, 'bpmn:ConditionalEventDefinition') || hasEventDefinition(element, 'bpmn:SignalEventDefinition') ) ) ); } function isConnection$8(element) { return element.waypoints; } function getParents(element) { var parents = []; while (element) { element = element.parent; if (element) { parents.push(element); } } return parents; } function isParent(possibleParent, element) { var allParents = getParents(element); return allParents.indexOf(possibleParent) !== -1; } function canConnect(source, target, connection) { if (nonExistingOrLabel(source) || nonExistingOrLabel(target)) { return null; } if (!is$1(connection, 'bpmn:DataAssociation')) { if (canConnectMessageFlow(source, target)) { return { type: 'bpmn:MessageFlow' }; } if (canConnectSequenceFlow(source, target)) { return { type: 'bpmn:SequenceFlow' }; } } var connectDataAssociation = canConnectDataAssociation(source, target); if (connectDataAssociation) { return connectDataAssociation; } if (isCompensationBoundary(source) && isForCompensation(target)) { return { type: 'bpmn:Association', associationDirection: 'One' }; } if (canConnectAssociation(source, target)) { return { type: 'bpmn:Association' }; } return false; } /** * Can an element be dropped into the target element * * @return {boolean} */ function canDrop(element, target, position) { // can move labels and groups everywhere if (isLabel$6(element) || isGroup(element)) { return true; } // disallow to create elements on collapsed pools if (is$1(target, 'bpmn:Participant') && !isExpanded(target)) { return false; } // allow to create new participants on // existing collaboration and process diagrams if (is$1(element, 'bpmn:Participant')) { return is$1(target, 'bpmn:Process') || is$1(target, 'bpmn:Collaboration'); } // allow moving DataInput / DataOutput within its original container only if (isAny(element, [ 'bpmn:DataInput', 'bpmn:DataOutput' ])) { if (element.parent) { return target === element.parent; } } // allow creating lanes on participants and other lanes only if (is$1(element, 'bpmn:Lane')) { return is$1(target, 'bpmn:Participant') || is$1(target, 'bpmn:Lane'); } // disallow dropping boundary events which cannot replace with intermediate event if (is$1(element, 'bpmn:BoundaryEvent') && !isDroppableBoundaryEvent(element)) { return false; } // drop flow elements onto flow element containers // and participants if (is$1(element, 'bpmn:FlowElement') && !is$1(element, 'bpmn:DataStoreReference')) { if (is$1(target, 'bpmn:FlowElementsContainer')) { return isExpanded(target); } return isAny(target, [ 'bpmn:Participant', 'bpmn:Lane' ]); } // disallow dropping data store reference if there is no process to append to if (is$1(element, 'bpmn:DataStoreReference') && is$1(target, 'bpmn:Collaboration')) { return some(getBusinessObject(target).get('participants'), function(participant) { return !!participant.get('processRef'); }); } // account for the fact that data associations are always // rendered and moved to top (Process or Collaboration level) // // artifacts may be placed wherever, too if (isAny(element, [ 'bpmn:Artifact', 'bpmn:DataAssociation', 'bpmn:DataStoreReference' ])) { return isAny(target, [ 'bpmn:Collaboration', 'bpmn:Lane', 'bpmn:Participant', 'bpmn:Process', 'bpmn:SubProcess' ]); } if (is$1(element, 'bpmn:MessageFlow')) { return is$1(target, 'bpmn:Collaboration') || element.source.parent == target || element.target.parent == target; } return false; } function isDroppableBoundaryEvent(event) { return getBusinessObject(event).cancelActivity && ( hasNoEventDefinition(event) || hasCommonBoundaryIntermediateEventDefinition(event) ); } function isBoundaryEvent(element) { return !isLabel$6(element) && is$1(element, 'bpmn:BoundaryEvent'); } function isLane(element) { return is$1(element, 'bpmn:Lane'); } /** * We treat IntermediateThrowEvents as boundary events during create, * this must be reflected in the rules. */ function isBoundaryCandidate(element) { if (isBoundaryEvent(element)) { return true; } if (is$1(element, 'bpmn:IntermediateThrowEvent') && hasNoEventDefinition(element)) { return true; } return ( is$1(element, 'bpmn:IntermediateCatchEvent') && hasCommonBoundaryIntermediateEventDefinition(element) ); } function hasNoEventDefinition(element) { var bo = getBusinessObject(element); return bo && !(bo.eventDefinitions && bo.eventDefinitions.length); } function hasCommonBoundaryIntermediateEventDefinition(element) { return hasOneOfEventDefinitions(element, [ 'bpmn:MessageEventDefinition', 'bpmn:TimerEventDefinition', 'bpmn:SignalEventDefinition', 'bpmn:ConditionalEventDefinition' ]); } function hasOneOfEventDefinitions(element, eventDefinitions) { return eventDefinitions.some(function(definition) { return hasEventDefinition(element, definition); }); } function isReceiveTaskAfterEventBasedGateway(element) { return ( is$1(element, 'bpmn:ReceiveTask') && find(element.incoming, function(incoming) { return is$1(incoming.source, 'bpmn:EventBasedGateway'); }) ); } function canAttach(elements, target, source, position) { if (!Array.isArray(elements)) { elements = [ elements ]; } // only (re-)attach one element at a time if (elements.length !== 1) { return false; } var element = elements[0]; // do not attach labels if (isLabel$6(element)) { return false; } // only handle boundary events if (!isBoundaryCandidate(element)) { return false; } // disallow drop on event sub processes if (isEventSubProcess(target)) { return false; } // only allow drop on non compensation activities if (!is$1(target, 'bpmn:Activity') || isForCompensation(target)) { return false; } // only attach to subprocess border if (position && !getBoundaryAttachment(position, target)) { return false; } // do not attach on receive tasks after event based gateways if (isReceiveTaskAfterEventBasedGateway(target)) { return false; } return 'attach'; } /** * Defines how to replace elements for a given target. * * Returns an array containing all elements which will be replaced. * * @example * * [{ id: 'IntermediateEvent_2', * type: 'bpmn:StartEvent' * }, * { id: 'IntermediateEvent_5', * type: 'bpmn:EndEvent' * }] * * @param {Array} elements * @param {Object} target * * @return {Object} an object containing all elements which have to be replaced */ function canReplace(elements, target, position) { if (!target) { return false; } var canExecute = { replacements: [] }; forEach$1(elements, function(element) { if (!isEventSubProcess(target)) { if (is$1(element, 'bpmn:StartEvent') && element.type !== 'label' && canDrop(element, target)) { // replace a non-interrupting start event by a blank interrupting start event // when the target is not an event sub process if (!isInterrupting(element)) { canExecute.replacements.push({ oldElementId: element.id, newElementType: 'bpmn:StartEvent' }); } // replace an error/escalation/compensate start event by a blank interrupting start event // when the target is not an event sub process if (hasErrorEventDefinition(element) || hasEscalationEventDefinition(element) || hasCompensateEventDefinition(element)) { canExecute.replacements.push({ oldElementId: element.id, newElementType: 'bpmn:StartEvent' }); } // replace a typed start event by a blank interrupting start event // when the target is a sub process but not an event sub process if (hasOneOfEventDefinitions(element, [ 'bpmn:MessageEventDefinition', 'bpmn:TimerEventDefinition', 'bpmn:SignalEventDefinition', 'bpmn:ConditionalEventDefinition' ]) && is$1(target, 'bpmn:SubProcess')) { canExecute.replacements.push({ oldElementId: element.id, newElementType: 'bpmn:StartEvent' }); } } } if (!is$1(target, 'bpmn:Transaction')) { if (hasEventDefinition(element, 'bpmn:CancelEventDefinition') && element.type !== 'label') { if (is$1(element, 'bpmn:EndEvent') && canDrop(element, target)) { canExecute.replacements.push({ oldElementId: element.id, newElementType: 'bpmn:EndEvent' }); } if (is$1(element, 'bpmn:BoundaryEvent') && canAttach(element, target, null, position)) { canExecute.replacements.push({ oldElementId: element.id, newElementType: 'bpmn:BoundaryEvent' }); } } } }); return canExecute.replacements.length ? canExecute : false; } function canMove(elements, target) { // do not move selection containing lanes if (some(elements, isLane)) { return false; } // allow default move check to start move operation if (!target) { return true; } return elements.every(function(element) { return canDrop(element, target); }); } function canCreate(shape, target, source, position) { if (!target) { return false; } if (isLabel$6(shape) || isGroup(shape)) { return true; } if (isSame$1(source, target)) { return false; } // ensure we do not drop the element // into source if (source && isParent(source, target)) { return false; } return canDrop(shape, target) || canInsert(shape, target); } function canResize(shape, newBounds) { if (is$1(shape, 'bpmn:SubProcess')) { return ( isExpanded(shape) && ( !newBounds || (newBounds.width >= 100 && newBounds.height >= 80) ) ); } if (is$1(shape, 'bpmn:Lane')) { return !newBounds || (newBounds.width >= 130 && newBounds.height >= 60); } if (is$1(shape, 'bpmn:Participant')) { return !newBounds || (newBounds.width >= 250 && newBounds.height >= 50); } if (isTextAnnotation(shape)) { return true; } if (isGroup(shape)) { return true; } return false; } /** * Check, whether one side of the relationship * is a text annotation. */ function isOneTextAnnotation(source, target) { var sourceTextAnnotation = isTextAnnotation(source), targetTextAnnotation = isTextAnnotation(target); return ( (sourceTextAnnotation || targetTextAnnotation) && (sourceTextAnnotation !== targetTextAnnotation) ); } function canConnectAssociation(source, target) { // compensation boundary events are exception if (isCompensationBoundary(source) && isForCompensation(target)) { return true; } // don't connect parent <-> child if (isParent(target, source) || isParent(source, target)) { return false; } // allow connection of associations between and if (isOneTextAnnotation(source, target)) { return true; } // can connect associations where we can connect // data associations, too (!) return !!canConnectDataAssociation(source, target); } function canConnectMessageFlow(source, target) { // during connect user might move mouse out of canvas // https://github.com/bpmn-io/bpmn-js/issues/1033 if (getRootElement(source) && !getRootElement(target)) { return false; } return ( isMessageFlowSource(source) && isMessageFlowTarget(target) && !isSameOrganization(source, target) ); } function canConnectSequenceFlow(source, target) { if ( isEventBasedTarget(target) && target.incoming.length > 0 && areOutgoingEventBasedGatewayConnections(target.incoming) && !is$1(source, 'bpmn:EventBasedGateway') ) { return false; } return isSequenceFlowSource(source) && isSequenceFlowTarget(target) && isSameScope(source, target) && !(is$1(source, 'bpmn:EventBasedGateway') && !isEventBasedTarget(target)); } function canConnectDataAssociation(source, target) { if (isAny(source, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ]) && isAny(target, [ 'bpmn:Activity', 'bpmn:ThrowEvent' ])) { return { type: 'bpmn:DataInputAssociation' }; } if (isAny(target, [ 'bpmn:DataObjectReference', 'bpmn:DataStoreReference' ]) && isAny(source, [ 'bpmn:Activity', 'bpmn:CatchEvent' ])) { return { type: 'bpmn:DataOutputAssociation' }; } return false; } function canInsert(shape, flow, position) { if (!flow) { return false; } if (Array.isArray(shape)) { if (shape.length !== 1) { return false; } shape = shape[0]; } if (flow.source === shape || flow.target === shape) { return false; } // return true if we can drop on the // underlying flow parent // // at this point we are not really able to talk // about connection rules (yet) return ( isAny(flow, [ 'bpmn:SequenceFlow', 'bpmn:MessageFlow' ]) && !isLabel$6(flow) && is$1(shape, 'bpmn:FlowNode') && !is$1(shape, 'bpmn:BoundaryEvent') && canDrop(shape, flow.parent)); } function includes$5(elements, element) { return (elements && element) && elements.indexOf(element) !== -1; } function canCopy(elements, element) { if (isLabel$6(element)) { return true; } if (is$1(element, 'bpmn:Lane') && !includes$5(elements, element.parent)) { return false; } return true; } function isOutgoingEventBasedGatewayConnection(connection) { if (connection && connection.source) { return is$1(connection.source, 'bpmn:EventBasedGateway'); } } function areOutgoingEventBasedGatewayConnections(connections) { connections = connections || []; return connections.some(isOutgoingEventBasedGatewayConnection); } function getRootElement(element) { return getParent(element, 'bpmn:Process') || getParent(element, 'bpmn:Collaboration'); } var RulesModule = { __depends__: [ RulesModule$1 ], __init__: [ 'bpmnRules' ], bpmnRules: [ 'type', BpmnRules ] }; var HIGH_PRIORITY$6 = 2000; function BpmnDiOrdering(eventBus, canvas) { eventBus.on('saveXML.start', HIGH_PRIORITY$6, orderDi); function orderDi() { var rootElements = canvas.getRootElements(); forEach$1(rootElements, function(root) { var rootDi = getDi(root), elements, diElements; elements = selfAndAllChildren([ root ], false); // only bpmndi:Shape and bpmndi:Edge can be direct children of bpmndi:Plane elements = filter(elements, function(element) { return element !== root && !element.labelTarget; }); diElements = map(elements, getDi); rootDi.set('planeElement', diElements); }); } } BpmnDiOrdering.$inject = [ 'eventBus', 'canvas' ]; var DiOrderingModule = { __init__: [ 'bpmnDiOrdering' ], bpmnDiOrdering: [ 'type', BpmnDiOrdering ] }; /** * An abstract provider that allows modelers to implement a custom * ordering of diagram elements on the canvas. * * It makes sure that the order is always preserved during element * creation and move operations. * * In order to use this behavior, inherit from it and override * the method {@link OrderingProvider#getOrdering}. * * @example * * ```javascript * function CustomOrderingProvider(eventBus) { * OrderingProvider.call(this, eventBus); * * this.getOrdering = function(element, newParent) { * // always insert elements at the front * // when moving * return { * index: 0, * parent: newParent * }; * }; * } * ``` * * @param {EventBus} eventBus */ function OrderingProvider(eventBus) { CommandInterceptor.call(this, eventBus); var self = this; this.preExecute([ 'shape.create', 'connection.create' ], function(event) { var context = event.context, element = context.shape || context.connection, parent = context.parent; var ordering = self.getOrdering(element, parent); if (ordering) { if (ordering.parent !== undefined) { context.parent = ordering.parent; } context.parentIndex = ordering.index; } }); this.preExecute([ 'shape.move', 'connection.move' ], function(event) { var context = event.context, element = context.shape || context.connection, parent = context.newParent || element.parent; var ordering = self.getOrdering(element, parent); if (ordering) { if (ordering.parent !== undefined) { context.newParent = ordering.parent; } context.newParentIndex = ordering.index; } }); } /** * Return a custom ordering of the element, both in terms * of parent element and index in the new parent. * * Implementors of this method must return an object with * `parent` _and_ `index` in it. * * @param {djs.model.Base} element * @param {djs.model.Shape} newParent * * @return {Object} ordering descriptor */ OrderingProvider.prototype.getOrdering = function(element, newParent) { return null; }; e(OrderingProvider, CommandInterceptor); /** * a simple ordering provider that makes sure: * * (0) labels and groups are rendered always on top * (1) elements are ordered by a {level} property */ function BpmnOrderingProvider(eventBus, canvas, translate) { OrderingProvider.call(this, eventBus); var orders = [ { type: 'bpmn:SubProcess', order: { level: 6 } }, // handle SequenceFlow(s) like message flows and render them always on top { type: 'bpmn:SequenceFlow', order: { level: 9, containers: [ 'bpmn:Participant', 'bpmn:FlowElementsContainer' ] } }, // handle DataAssociation(s) like message flows and render them always on top { type: 'bpmn:DataAssociation', order: { level: 9, containers: [ 'bpmn:Collaboration', 'bpmn:FlowElementsContainer' ] } }, { type: 'bpmn:MessageFlow', order: { level: 9, containers: [ 'bpmn:Collaboration' ] } }, { type: 'bpmn:Association', order: { level: 6, containers: [ 'bpmn:Participant', 'bpmn:FlowElementsContainer', 'bpmn:Collaboration' ] } }, { type: 'bpmn:BoundaryEvent', order: { level: 8 } }, { type: 'bpmn:Group', order: { level: 10, containers: [ 'bpmn:Collaboration', 'bpmn:FlowElementsContainer' ] } }, { type: 'bpmn:FlowElement', order: { level: 5 } }, { type: 'bpmn:Participant', order: { level: -2 } }, { type: 'bpmn:Lane', order: { level: -1 } } ]; function computeOrder(element) { if (element.labelTarget) { return { level: 10 }; } var entry = find(orders, function(o) { return isAny(element, [ o.type ]); }); return entry && entry.order || { level: 1 }; } function getOrder(element) { var order = element.order; if (!order) { element.order = order = computeOrder(element); } if (!order) { throw new Error('no order for <' + element.id + '>'); } return order; } function findActualParent(element, newParent, containers) { var actualParent = newParent; while (actualParent) { if (isAny(actualParent, containers)) { break; } actualParent = actualParent.parent; } if (!actualParent) { throw new Error('no parent for <' + element.id + '> in <' + (newParent && newParent.id) + '>'); } return actualParent; } this.getOrdering = function(element, newParent) { // render labels always on top if (element.labelTarget) { return { parent: canvas.findRoot(newParent) || canvas.getRootElement(), index: -1 }; } var elementOrder = getOrder(element); if (elementOrder.containers) { newParent = findActualParent(element, newParent, elementOrder.containers); } var currentIndex = newParent.children.indexOf(element); var insertIndex = findIndex(newParent.children, function(child) { // do not compare with labels, they are created // in the wrong order (right after elements) during import and // mess up the positioning. if (!element.labelTarget && child.labelTarget) { return false; } return elementOrder.level < getOrder(child).level; }); // if the element is already in the child list at // a smaller index, we need to adjust the insert index. // this takes into account that the element is being removed // before being re-inserted if (insertIndex !== -1) { if (currentIndex !== -1 && currentIndex < insertIndex) { insertIndex -= 1; } } return { index: insertIndex, parent: newParent }; }; } BpmnOrderingProvider.$inject = [ 'eventBus', 'canvas', 'translate' ]; e(BpmnOrderingProvider, OrderingProvider); var OrderingModule = { __depends__: [ translate ], __init__: [ 'bpmnOrderingProvider' ], bpmnOrderingProvider: [ 'type', BpmnOrderingProvider ] }; /** * A service that offers un- and redoable execution of commands. * * The command stack is responsible for executing modeling actions * in a un- and redoable manner. To do this it delegates the actual * command execution to {@link CommandHandler}s. * * Command handlers provide {@link CommandHandler#execute(ctx)} and * {@link CommandHandler#revert(ctx)} methods to un- and redo a command * identified by a command context. * * * ## Life-Cycle events * * In the process the command stack fires a number of life-cycle events * that other components to participate in the command execution. * * * preExecute * * preExecuted * * execute * * executed * * postExecute * * postExecuted * * revert * * reverted * * A special event is used for validating, whether a command can be * performed prior to its execution. * * * canExecute * * Each of the events is fired as `commandStack.{eventName}` and * `commandStack.{commandName}.{eventName}`, respectively. This gives * components fine grained control on where to hook into. * * The event object fired transports `command`, the name of the * command and `context`, the command context. * * * ## Creating Command Handlers * * Command handlers should provide the {@link CommandHandler#execute(ctx)} * and {@link CommandHandler#revert(ctx)} methods to implement * redoing and undoing of a command. * * A command handler _must_ ensure undo is performed properly in order * not to break the undo chain. It must also return the shapes that * got changed during the `execute` and `revert` operations. * * Command handlers may execute other modeling operations (and thus * commands) in their `preExecute` and `postExecute` phases. The command * stack will properly group all commands together into a logical unit * that may be re- and undone atomically. * * Command handlers must not execute other commands from within their * core implementation (`execute`, `revert`). * * * ## Change Tracking * * During the execution of the CommandStack it will keep track of all * elements that have been touched during the command's execution. * * At the end of the CommandStack execution it will notify interested * components via an 'elements.changed' event with all the dirty * elements. * * The event can be picked up by components that are interested in the fact * that elements have been changed. One use case for this is updating * their graphical representation after moving / resizing or deletion. * * @see CommandHandler * * @param {EventBus} eventBus * @param {Injector} injector */ function CommandStack(eventBus, injector) { /** * A map of all registered command handlers. * * @type {Object} */ this._handlerMap = {}; /** * A stack containing all re/undoable actions on the diagram * * @type {Array} */ this._stack = []; /** * The current index on the stack * * @type {number} */ this._stackIdx = -1; /** * Current active commandStack execution * * @type {Object} * @property {Object[]} actions * @property {Object[]} dirty * @property { 'undo' | 'redo' | 'clear' | 'execute' | null } trigger the cause of the current excecution */ this._currentExecution = { actions: [], dirty: [], trigger: null }; this._injector = injector; this._eventBus = eventBus; this._uid = 1; eventBus.on([ 'diagram.destroy', 'diagram.clear' ], function() { this.clear(false); }, this); } CommandStack.$inject = [ 'eventBus', 'injector' ]; /** * Execute a command * * @param {string} command the command to execute * @param {Object} context the environment to execute the command in */ CommandStack.prototype.execute = function(command, context) { if (!command) { throw new Error('command required'); } this._currentExecution.trigger = 'execute'; var action = { command: command, context: context }; this._pushAction(action); this._internalExecute(action); this._popAction(action); }; /** * Ask whether a given command can be executed. * * Implementors may hook into the mechanism on two ways: * * * in event listeners: * * Users may prevent the execution via an event listener. * It must prevent the default action for `commandStack.(.)canExecute` events. * * * in command handlers: * * If the method {@link CommandHandler#canExecute} is implemented in a handler * it will be called to figure out whether the execution is allowed. * * @param {string} command the command to execute * @param {Object} context the environment to execute the command in * * @return {boolean} true if the command can be executed */ CommandStack.prototype.canExecute = function(command, context) { var action = { command: command, context: context }; var handler = this._getHandler(command); var result = this._fire(command, 'canExecute', action); // handler#canExecute will only be called if no listener // decided on a result already if (result === undefined) { if (!handler) { return false; } if (handler.canExecute) { result = handler.canExecute(context); } } return result; }; /** * Clear the command stack, erasing all undo / redo history */ CommandStack.prototype.clear = function(emit) { this._stack.length = 0; this._stackIdx = -1; if (emit !== false) { this._fire('changed', { trigger: 'clear' }); } }; /** * Undo last command(s) */ CommandStack.prototype.undo = function() { var action = this._getUndoAction(), next; if (action) { this._currentExecution.trigger = 'undo'; this._pushAction(action); while (action) { this._internalUndo(action); next = this._getUndoAction(); if (!next || next.id !== action.id) { break; } action = next; } this._popAction(); } }; /** * Redo last command(s) */ CommandStack.prototype.redo = function() { var action = this._getRedoAction(), next; if (action) { this._currentExecution.trigger = 'redo'; this._pushAction(action); while (action) { this._internalExecute(action, true); next = this._getRedoAction(); if (!next || next.id !== action.id) { break; } action = next; } this._popAction(); } }; /** * Register a handler instance with the command stack * * @param {string} command * @param {CommandHandler} handler */ CommandStack.prototype.register = function(command, handler) { this._setHandler(command, handler); }; /** * Register a handler type with the command stack * by instantiating it and injecting its dependencies. * * @param {string} command * @param {Function} a constructor for a {@link CommandHandler} */ CommandStack.prototype.registerHandler = function(command, handlerCls) { if (!command || !handlerCls) { throw new Error('command and handlerCls must be defined'); } var handler = this._injector.instantiate(handlerCls); this.register(command, handler); }; CommandStack.prototype.canUndo = function() { return !!this._getUndoAction(); }; CommandStack.prototype.canRedo = function() { return !!this._getRedoAction(); }; // stack access ////////////////////// CommandStack.prototype._getRedoAction = function() { return this._stack[this._stackIdx + 1]; }; CommandStack.prototype._getUndoAction = function() { return this._stack[this._stackIdx]; }; // internal functionality ////////////////////// CommandStack.prototype._internalUndo = function(action) { var self = this; var command = action.command, context = action.context; var handler = this._getHandler(command); // guard against illegal nested command stack invocations this._atomicDo(function() { self._fire(command, 'revert', action); if (handler.revert) { self._markDirty(handler.revert(context)); } self._revertedAction(action); self._fire(command, 'reverted', action); }); }; CommandStack.prototype._fire = function(command, qualifier, event) { if (arguments.length < 3) { event = qualifier; qualifier = null; } var names = qualifier ? [ command + '.' + qualifier, qualifier ] : [ command ], i, name, result; event = this._eventBus.createEvent(event); for (i = 0; (name = names[i]); i++) { result = this._eventBus.fire('commandStack.' + name, event); if (event.cancelBubble) { break; } } return result; }; CommandStack.prototype._createId = function() { return this._uid++; }; CommandStack.prototype._atomicDo = function(fn) { var execution = this._currentExecution; execution.atomic = true; try { fn(); } finally { execution.atomic = false; } }; CommandStack.prototype._internalExecute = function(action, redo) { var self = this; var command = action.command, context = action.context; var handler = this._getHandler(command); if (!handler) { throw new Error('no command handler registered for <' + command + '>'); } this._pushAction(action); if (!redo) { this._fire(command, 'preExecute', action); if (handler.preExecute) { handler.preExecute(context); } this._fire(command, 'preExecuted', action); } // guard against illegal nested command stack invocations this._atomicDo(function() { self._fire(command, 'execute', action); if (handler.execute) { // actual execute + mark return results as dirty self._markDirty(handler.execute(context)); } // log to stack self._executedAction(action, redo); self._fire(command, 'executed', action); }); if (!redo) { this._fire(command, 'postExecute', action); if (handler.postExecute) { handler.postExecute(context); } this._fire(command, 'postExecuted', action); } this._popAction(action); }; CommandStack.prototype._pushAction = function(action) { var execution = this._currentExecution, actions = execution.actions; var baseAction = actions[0]; if (execution.atomic) { throw new Error('illegal invocation in or phase (action: ' + action.command + ')'); } if (!action.id) { action.id = (baseAction && baseAction.id) || this._createId(); } actions.push(action); }; CommandStack.prototype._popAction = function() { var execution = this._currentExecution, trigger = execution.trigger, actions = execution.actions, dirty = execution.dirty; actions.pop(); if (!actions.length) { this._eventBus.fire('elements.changed', { elements: uniqueBy('id', dirty.reverse()) }); dirty.length = 0; this._fire('changed', { trigger: trigger }); execution.trigger = null; } }; CommandStack.prototype._markDirty = function(elements) { var execution = this._currentExecution; if (!elements) { return; } elements = isArray$3(elements) ? elements : [ elements ]; execution.dirty = execution.dirty.concat(elements); }; CommandStack.prototype._executedAction = function(action, redo) { var stackIdx = ++this._stackIdx; if (!redo) { this._stack.splice(stackIdx, this._stack.length, action); } }; CommandStack.prototype._revertedAction = function(action) { this._stackIdx--; }; CommandStack.prototype._getHandler = function(command) { return this._handlerMap[command]; }; CommandStack.prototype._setHandler = function(command, handler) { if (!command || !handler) { throw new Error('command and handler required'); } if (this._handlerMap[command]) { throw new Error('overriding handler for command <' + command + '>'); } this._handlerMap[command] = handler; }; var CommandModule = { commandStack: [ 'type', CommandStack ] }; // document wide unique tooltip ids var ids = new IdGenerator('tt'); function createRoot(parentNode) { var root = domify( '
    ' ); assign$1(root, { position: 'absolute', width: '0', height: '0' }); parentNode.insertBefore(root, parentNode.firstChild); return root; } function setPosition(el, x, y) { assign$1(el, { left: x + 'px', top: y + 'px' }); } function setVisible(el, visible) { el.style.display = visible === false ? 'none' : ''; } var tooltipClass = 'djs-tooltip', tooltipSelector = '.' + tooltipClass; /** * A service that allows users to render tool tips on the diagram. * * The tooltip service will take care of updating the tooltip positioning * during navigation + zooming. * * @example * * ```javascript * * // add a pink badge on the top left of the shape * tooltips.add({ * position: { * x: 50, * y: 100 * }, * html: '
    0
    ' * }); * * // or with optional life span * tooltips.add({ * position: { * top: -5, * left: -5 * }, * html: '
    0
    ', * ttl: 2000 * }); * * // remove a tool tip * var id = tooltips.add(...); * tooltips.remove(id); * ``` * * @param {EventBus} eventBus * @param {Canvas} canvas */ function Tooltips(eventBus, canvas) { this._eventBus = eventBus; this._canvas = canvas; this._ids = ids; this._tooltipDefaults = { show: { minZoom: 0.7, maxZoom: 5.0 } }; /** * Mapping tooltipId -> tooltip */ this._tooltips = {}; // root html element for all tooltips this._tooltipRoot = createRoot(canvas.getContainer()); var self = this; delegate.bind(this._tooltipRoot, tooltipSelector, 'mousedown', function(event) { event.stopPropagation(); }); delegate.bind(this._tooltipRoot, tooltipSelector, 'mouseover', function(event) { self.trigger('mouseover', event); }); delegate.bind(this._tooltipRoot, tooltipSelector, 'mouseout', function(event) { self.trigger('mouseout', event); }); this._init(); } Tooltips.$inject = [ 'eventBus', 'canvas' ]; /** * Adds a HTML tooltip to the diagram * * @param {Object} tooltip the tooltip configuration * * @param {string|DOMElement} tooltip.html html element to use as an tooltip * @param {Object} [tooltip.show] show configuration * @param {number} [tooltip.show.minZoom] minimal zoom level to show the tooltip * @param {number} [tooltip.show.maxZoom] maximum zoom level to show the tooltip * @param {Object} tooltip.position where to attach the tooltip * @param {number} [tooltip.position.left] relative to element bbox left attachment * @param {number} [tooltip.position.top] relative to element bbox top attachment * @param {number} [tooltip.position.bottom] relative to element bbox bottom attachment * @param {number} [tooltip.position.right] relative to element bbox right attachment * @param {number} [tooltip.timeout=-1] * * @return {string} id that may be used to reference the tooltip for update or removal */ Tooltips.prototype.add = function(tooltip) { if (!tooltip.position) { throw new Error('must specifiy tooltip position'); } if (!tooltip.html) { throw new Error('must specifiy tooltip html'); } var id = this._ids.next(); tooltip = assign({}, this._tooltipDefaults, tooltip, { id: id }); this._addTooltip(tooltip); if (tooltip.timeout) { this.setTimeout(tooltip); } return id; }; Tooltips.prototype.trigger = function(action, event) { var node = event.delegateTarget || event.target; var tooltip = this.get(attr$1(node, 'data-tooltip-id')); if (!tooltip) { return; } if (action === 'mouseover' && tooltip.timeout) { this.clearTimeout(tooltip); } if (action === 'mouseout' && tooltip.timeout) { // cut timeout after mouse out tooltip.timeout = 1000; this.setTimeout(tooltip); } }; /** * Get a tooltip with the given id * * @param {string} id */ Tooltips.prototype.get = function(id) { if (typeof id !== 'string') { id = id.id; } return this._tooltips[id]; }; Tooltips.prototype.clearTimeout = function(tooltip) { tooltip = this.get(tooltip); if (!tooltip) { return; } var removeTimer = tooltip.removeTimer; if (removeTimer) { clearTimeout(removeTimer); tooltip.removeTimer = null; } }; Tooltips.prototype.setTimeout = function(tooltip) { tooltip = this.get(tooltip); if (!tooltip) { return; } this.clearTimeout(tooltip); var self = this; tooltip.removeTimer = setTimeout(function() { self.remove(tooltip); }, tooltip.timeout); }; /** * Remove an tooltip with the given id * * @param {string} id */ Tooltips.prototype.remove = function(id) { var tooltip = this.get(id); if (tooltip) { remove$2(tooltip.html); remove$2(tooltip.htmlContainer); delete tooltip.htmlContainer; delete this._tooltips[tooltip.id]; } }; Tooltips.prototype.show = function() { setVisible(this._tooltipRoot); }; Tooltips.prototype.hide = function() { setVisible(this._tooltipRoot, false); }; Tooltips.prototype._updateRoot = function(viewbox) { var a = viewbox.scale || 1; var d = viewbox.scale || 1; var matrix = 'matrix(' + a + ',0,0,' + d + ',' + (-1 * viewbox.x * a) + ',' + (-1 * viewbox.y * d) + ')'; this._tooltipRoot.style.transform = matrix; this._tooltipRoot.style['-ms-transform'] = matrix; }; Tooltips.prototype._addTooltip = function(tooltip) { var id = tooltip.id, html = tooltip.html, htmlContainer, tooltipRoot = this._tooltipRoot; // unwrap jquery (for those who need it) if (html.get && html.constructor.prototype.jquery) { html = html.get(0); } // create proper html elements from // tooltip HTML strings if (isString(html)) { html = domify(html); } htmlContainer = domify('
    '); assign$1(htmlContainer, { position: 'absolute' }); htmlContainer.appendChild(html); if (tooltip.type) { classes$1(htmlContainer).add('djs-tooltip-' + tooltip.type); } if (tooltip.className) { classes$1(htmlContainer).add(tooltip.className); } tooltip.htmlContainer = htmlContainer; tooltipRoot.appendChild(htmlContainer); this._tooltips[id] = tooltip; this._updateTooltip(tooltip); }; Tooltips.prototype._updateTooltip = function(tooltip) { var position = tooltip.position, htmlContainer = tooltip.htmlContainer; // update overlay html based on tooltip x, y setPosition(htmlContainer, position.x, position.y); }; Tooltips.prototype._updateTooltipVisibilty = function(viewbox) { forEach$1(this._tooltips, function(tooltip) { var show = tooltip.show, htmlContainer = tooltip.htmlContainer, visible = true; if (show) { if (show.minZoom > viewbox.scale || show.maxZoom < viewbox.scale) { visible = false; } setVisible(htmlContainer, visible); } }); }; Tooltips.prototype._init = function() { var self = this; // scroll/zoom integration function updateViewbox(viewbox) { self._updateRoot(viewbox); self._updateTooltipVisibilty(viewbox); self.show(); } this._eventBus.on('canvas.viewbox.changing', function(event) { self.hide(); }); this._eventBus.on('canvas.viewbox.changed', function(event) { updateViewbox(event.viewbox); }); }; var TooltipsModule = { __init__: [ 'tooltips' ], tooltips: [ 'type', Tooltips ] }; /** * Remove from the beginning of a collection until it is empty. * * This is a null-safe operation that ensures elements * are being removed from the given collection until the * collection is empty. * * The implementation deals with the fact that a remove operation * may touch, i.e. remove multiple elements in the collection * at a time. * * @param {Array} [collection] * @param {Function} removeFn * * @return {Array} the cleared collection */ function saveClear(collection, removeFn) { if (typeof removeFn !== 'function') { throw new Error('removeFn iterator must be a function'); } if (!collection) { return; } var e; while ((e = collection[0])) { removeFn(e); } return collection; } var LOW_PRIORITY$6 = 250, HIGH_PRIORITY$5 = 1400; /** * A handler that makes sure labels are properly moved with * their label targets. * * @param {didi.Injector} injector * @param {EventBus} eventBus * @param {Modeling} modeling */ function LabelSupport(injector, eventBus, modeling) { CommandInterceptor.call(this, eventBus); var movePreview = injector.get('movePreview', false); // remove labels from the collection that are being // moved with other elements anyway eventBus.on('shape.move.start', HIGH_PRIORITY$5, function(e) { var context = e.context, shapes = context.shapes, validatedShapes = context.validatedShapes; context.shapes = removeLabels(shapes); context.validatedShapes = removeLabels(validatedShapes); }); // add labels to visual's group movePreview && eventBus.on('shape.move.start', LOW_PRIORITY$6, function(e) { var context = e.context, shapes = context.shapes; var labels = []; forEach$1(shapes, function(element) { forEach$1(element.labels, function(label) { if (!label.hidden && context.shapes.indexOf(label) === -1) { labels.push(label); } if (element.labelTarget) { labels.push(element); } }); }); forEach$1(labels, function(label) { movePreview.makeDraggable(context, label, true); }); }); // add all labels to move closure this.preExecuted('elements.move', HIGH_PRIORITY$5, function(e) { var context = e.context, closure = context.closure, enclosedElements = closure.enclosedElements; var enclosedLabels = []; // find labels that are not part of // move closure yet and add them forEach$1(enclosedElements, function(element) { forEach$1(element.labels, function(label) { if (!enclosedElements[label.id]) { enclosedLabels.push(label); } }); }); closure.addAll(enclosedLabels); }); this.preExecute([ 'connection.delete', 'shape.delete' ], function(e) { var context = e.context, element = context.connection || context.shape; saveClear(element.labels, function(label) { modeling.removeShape(label, { nested: true }); }); }); this.execute('shape.delete', function(e) { var context = e.context, shape = context.shape, labelTarget = shape.labelTarget; // unset labelTarget if (labelTarget) { context.labelTargetIndex = indexOf(labelTarget.labels, shape); context.labelTarget = labelTarget; shape.labelTarget = null; } }); this.revert('shape.delete', function(e) { var context = e.context, shape = context.shape, labelTarget = context.labelTarget, labelTargetIndex = context.labelTargetIndex; // restore labelTarget if (labelTarget) { add(labelTarget.labels, shape, labelTargetIndex); shape.labelTarget = labelTarget; } }); } e(LabelSupport, CommandInterceptor); LabelSupport.$inject = [ 'injector', 'eventBus', 'modeling' ]; /** * Return a filtered list of elements that do not * contain attached elements with hosts being part * of the selection. * * @param {Array} elements * * @return {Array} filtered */ function removeLabels(elements) { return filter(elements, function(element) { // filter out labels that are move together // with their label targets return elements.indexOf(element.labelTarget) === -1; }); } var LabelSupportModule = { __init__: [ 'labelSupport' ], labelSupport: [ 'type', LabelSupport ] }; var LOW_PRIORITY$5 = 251, HIGH_PRIORITY$4 = 1401; var MARKER_ATTACH$1 = 'attach-ok'; /** * Adds the notion of attached elements to the modeler. * * Optionally depends on `diagram-js/lib/features/move` to render * the attached elements during move preview. * * Optionally depends on `diagram-js/lib/features/label-support` * to render attached labels during move preview. * * @param {didi.Injector} injector * @param {EventBus} eventBus * @param {Canvas} canvas * @param {Rules} rules * @param {Modeling} modeling */ function AttachSupport(injector, eventBus, canvas, rules, modeling) { CommandInterceptor.call(this, eventBus); var movePreview = injector.get('movePreview', false); // remove all the attached elements from the shapes to be validated // add all the attached shapes to the overall list of moved shapes eventBus.on('shape.move.start', HIGH_PRIORITY$4, function(e) { var context = e.context, shapes = context.shapes, validatedShapes = context.validatedShapes; context.shapes = addAttached(shapes); context.validatedShapes = removeAttached(validatedShapes); }); // add attachers to the visual's group movePreview && eventBus.on('shape.move.start', LOW_PRIORITY$5, function(e) { var context = e.context, shapes = context.shapes, attachers = getAttachers(shapes); forEach$1(attachers, function(attacher) { movePreview.makeDraggable(context, attacher, true); forEach$1(attacher.labels, function(label) { movePreview.makeDraggable(context, label, true); }); }); }); // add attach-ok marker to current host movePreview && eventBus.on('shape.move.start', function(event) { var context = event.context, shapes = context.shapes; if (shapes.length !== 1) { return; } var shape = shapes[0]; var host = shape.host; if (host) { canvas.addMarker(host, MARKER_ATTACH$1); eventBus.once([ 'shape.move.out', 'shape.move.cleanup' ], function() { canvas.removeMarker(host, MARKER_ATTACH$1); }); } }); // add all attachers to move closure this.preExecuted('elements.move', HIGH_PRIORITY$4, function(e) { var context = e.context, closure = context.closure, shapes = context.shapes, attachers = getAttachers(shapes); forEach$1(attachers, function(attacher) { closure.add(attacher, closure.topLevel[attacher.host.id]); }); }); // perform the attaching after shapes are done moving this.postExecuted('elements.move', function(e) { var context = e.context, shapes = context.shapes, newHost = context.newHost, attachers; // only single elements can be attached // multiply elements can be detached if (newHost && shapes.length !== 1) { return; } if (newHost) { attachers = shapes; } else { // find attachers moved without host attachers = filter(shapes, function(shape) { var host = shape.host; return isAttacher(shape) && !includes$4(shapes, host); }); } forEach$1(attachers, function(attacher) { modeling.updateAttachment(attacher, newHost); }); }); // ensure invalid attachment connections are removed this.postExecuted('elements.move', function(e) { var shapes = e.context.shapes; forEach$1(shapes, function(shape) { forEach$1(shape.attachers, function(attacher) { // remove invalid outgoing connections forEach$1(attacher.outgoing.slice(), function(connection) { var allowed = rules.allowed('connection.reconnect', { connection: connection, source: connection.source, target: connection.target }); if (!allowed) { modeling.removeConnection(connection); } }); // remove invalid incoming connections forEach$1(attacher.incoming.slice(), function(connection) { var allowed = rules.allowed('connection.reconnect', { connection: connection, source: connection.source, target: connection.target }); if (!allowed) { modeling.removeConnection(connection); } }); }); }); }); this.postExecute('shape.create', function(e) { var context = e.context, shape = context.shape, host = context.host; if (host) { modeling.updateAttachment(shape, host); } }); // update attachments if the host is replaced this.postExecute('shape.replace', function(e) { var context = e.context, oldShape = context.oldShape, newShape = context.newShape; // move the attachers to the new host saveClear(oldShape.attachers, function(attacher) { var allowed = rules.allowed('elements.move', { target: newShape, shapes: [ attacher ] }); if (allowed === 'attach') { modeling.updateAttachment(attacher, newShape); } else { modeling.removeShape(attacher); } }); // move attachers if new host has different size if (newShape.attachers.length) { forEach$1(newShape.attachers, function(attacher) { var delta = getNewAttachShapeDelta(attacher, oldShape, newShape); modeling.moveShape(attacher, delta, attacher.parent); }); } }); // move shape on host resize this.postExecute('shape.resize', function(event) { var context = event.context, shape = context.shape, oldBounds = context.oldBounds, newBounds = context.newBounds, attachers = shape.attachers, hints = context.hints || {}; if (hints.attachSupport === false) { return; } forEach$1(attachers, function(attacher) { var delta = getNewAttachShapeDelta(attacher, oldBounds, newBounds); modeling.moveShape(attacher, delta, attacher.parent); forEach$1(attacher.labels, function(label) { modeling.moveShape(label, delta, label.parent); }); }); }); // remove attachments this.preExecute('shape.delete', function(event) { var shape = event.context.shape; saveClear(shape.attachers, function(attacher) { modeling.removeShape(attacher); }); if (shape.host) { modeling.updateAttachment(shape, null); } }); } e(AttachSupport, CommandInterceptor); AttachSupport.$inject = [ 'injector', 'eventBus', 'canvas', 'rules', 'modeling' ]; /** * Return attachers of the given shapes * * @param {Array} shapes * @return {Array} */ function getAttachers(shapes) { return flatten(map(shapes, function(s) { return s.attachers || []; })); } /** * Return a combined list of elements and * attachers. * * @param {Array} elements * @return {Array} filtered */ function addAttached(elements) { var attachers = getAttachers(elements); return unionBy('id', elements, attachers); } /** * Return a filtered list of elements that do not * contain attached elements with hosts being part * of the selection. * * @param {Array} elements * * @return {Array} filtered */ function removeAttached(elements) { var ids = groupBy(elements, 'id'); return filter(elements, function(element) { while (element) { // host in selection if (element.host && ids[element.host.id]) { return false; } element = element.parent; } return true; }); } function isAttacher(shape) { return !!shape.host; } function includes$4(array, item) { return array.indexOf(item) !== -1; } var AttachSupportModule = { __depends__: [ RulesModule$1 ], __init__: [ 'attachSupport' ], attachSupport: [ 'type', AttachSupport ] }; var LOW_PRIORITY$4 = 250; /** * The tool manager acts as middle-man between the available tool's and the Palette, * it takes care of making sure that the correct active state is set. * * @param {Object} eventBus * @param {Object} dragging */ function ToolManager(eventBus, dragging) { this._eventBus = eventBus; this._dragging = dragging; this._tools = []; this._active = null; } ToolManager.$inject = [ 'eventBus', 'dragging' ]; ToolManager.prototype.registerTool = function(name, events) { var tools = this._tools; if (!events) { throw new Error('A tool has to be registered with it\'s "events"'); } tools.push(name); this.bindEvents(name, events); }; ToolManager.prototype.isActive = function(tool) { return tool && this._active === tool; }; ToolManager.prototype.length = function(tool) { return this._tools.length; }; ToolManager.prototype.setActive = function(tool) { var eventBus = this._eventBus; if (this._active !== tool) { this._active = tool; eventBus.fire('tool-manager.update', { tool: tool }); } }; ToolManager.prototype.bindEvents = function(name, events) { var eventBus = this._eventBus, dragging = this._dragging; var eventsToRegister = []; eventBus.on(events.tool + '.init', function(event) { var context = event.context; // Active tools that want to reactivate themselves must do this explicitly if (!context.reactivate && this.isActive(name)) { this.setActive(null); dragging.cancel(); return; } this.setActive(name); }, this); // Todo[ricardo]: add test cases forEach$1(events, function(event) { eventsToRegister.push(event + '.ended'); eventsToRegister.push(event + '.canceled'); }); eventBus.on(eventsToRegister, LOW_PRIORITY$4, function(event) { // We defer the de-activation of the tool to the .activate phase, // so we're able to check if we want to toggle off the current // active tool or switch to a new one if (!this._active) { return; } if (isPaletteClick(event)) { return; } this.setActive(null); }, this); }; // helpers /////////////// /** * Check if a given event is a palette click event. * * @param {EventBus.Event} event * * @return {boolean} */ function isPaletteClick(event) { var target = event.originalEvent && event.originalEvent.target; return target && closest(target, '.group[data-group="tools"]'); } var ToolManagerModule = { __depends__: [ DraggingModule ], __init__: [ 'toolManager' ], toolManager: [ 'type', ToolManager ] }; /** * Return direction given axis and delta. * * @param {string} axis * @param {number} delta * * @return {string} */ function getDirection(axis, delta) { if (axis === 'x') { if (delta > 0) { return 'e'; } if (delta < 0) { return 'w'; } } if (axis === 'y') { if (delta > 0) { return 's'; } if (delta < 0) { return 'n'; } } return null; } /** * Returns connections whose waypoints are to be updated. Waypoints are to be updated if start * or end is to be moved or resized. * * @param {Array} */ function getWaypointsUpdatingConnections(movingShapes, resizingShapes) { var waypointsUpdatingConnections = []; forEach$1(movingShapes.concat(resizingShapes), function(shape) { var incoming = shape.incoming, outgoing = shape.outgoing; forEach$1(incoming.concat(outgoing), function(connection) { var source = connection.source, target = connection.target; if (includes$3(movingShapes, source) || includes$3(movingShapes, target) || includes$3(resizingShapes, source) || includes$3(resizingShapes, target)) { if (!includes$3(waypointsUpdatingConnections, connection)) { waypointsUpdatingConnections.push(connection); } } }); }); return waypointsUpdatingConnections; } function includes$3(array, item) { return array.indexOf(item) !== -1; } /** * Resize bounds. * * @param {Object} bounds * @param {number} bounds.x * @param {number} bounds.y * @param {number} bounds.width * @param {number} bounds.height * @param {string} direction * @param {Object} delta * @param {number} delta.x * @param {number} delta.y * * @return {Object} */ function resizeBounds(bounds, direction, delta) { var x = bounds.x, y = bounds.y, width = bounds.width, height = bounds.height, dx = delta.x, dy = delta.y; switch (direction) { case 'n': return { x: x, y: y + dy, width: width, height: height - dy }; case 's': return { x: x, y: y, width: width, height: height + dy }; case 'w': return { x: x + dx, y: y, width: width - dx, height: height }; case 'e': return { x: x, y: y, width: width + dx, height: height }; default: throw new Error('unknown direction: ' + direction); } } var abs$1 = Math.abs, round$4 = Math.round; var AXIS_TO_DIMENSION = { x: 'width', y: 'height' }; var CURSOR_CROSSHAIR = 'crosshair'; var DIRECTION_TO_TRBL = { n: 'top', w: 'left', s: 'bottom', e: 'right' }; var HIGH_PRIORITY$3 = 1500; var DIRECTION_TO_OPPOSITE = { n: 's', w: 'e', s: 'n', e: 'w' }; var PADDING = 20; /** * Add or remove space by moving and resizing elements. * * @param {Canvas} canvas * @param {Dragging} dragging * @param {EventBus} eventBus * @param {Modeling} modeling * @param {Rules} rules * @param {ToolManager} toolManager * @param {Mouse} mouse */ function SpaceTool( canvas, dragging, eventBus, modeling, rules, toolManager, mouse) { this._canvas = canvas; this._dragging = dragging; this._eventBus = eventBus; this._modeling = modeling; this._rules = rules; this._toolManager = toolManager; this._mouse = mouse; var self = this; toolManager.registerTool('space', { tool: 'spaceTool.selection', dragging: 'spaceTool' }); eventBus.on('spaceTool.selection.end', function(event) { eventBus.once('spaceTool.selection.ended', function() { self.activateMakeSpace(event.originalEvent); }); }); eventBus.on('spaceTool.move', HIGH_PRIORITY$3 , function(event) { var context = event.context, initialized = context.initialized; if (!initialized) { initialized = context.initialized = self.init(event, context); } if (initialized) { ensureConstraints(event); } }); eventBus.on('spaceTool.end', function(event) { var context = event.context, axis = context.axis, direction = context.direction, movingShapes = context.movingShapes, resizingShapes = context.resizingShapes, start = context.start; if (!context.initialized) { return; } ensureConstraints(event); var delta = { x: 0, y: 0 }; delta[ axis ] = round$4(event[ 'd' + axis ]); self.makeSpace(movingShapes, resizingShapes, delta, direction, start); eventBus.once('spaceTool.ended', function(event) { // activate space tool selection after make space self.activateSelection(event.originalEvent, true, true); }); }); } SpaceTool.$inject = [ 'canvas', 'dragging', 'eventBus', 'modeling', 'rules', 'toolManager', 'mouse' ]; /** * Activate space tool selection. * * @param {Object} event * @param {boolean} autoActivate */ SpaceTool.prototype.activateSelection = function(event, autoActivate, reactivate) { this._dragging.init(event, 'spaceTool.selection', { autoActivate: autoActivate, cursor: CURSOR_CROSSHAIR, data: { context: { reactivate: reactivate } }, trapClick: false }); }; /** * Activate space tool make space. * * @param {MouseEvent} event */ SpaceTool.prototype.activateMakeSpace = function(event) { this._dragging.init(event, 'spaceTool', { autoActivate: true, cursor: CURSOR_CROSSHAIR, data: { context: {} } }); }; /** * Make space. * * @param {Array} movingShapes * @param {Array} resizingShapes * @param {Object} delta * @param {number} delta.x * @param {number} delta.y * @param {string} direction * @param {number} start */ SpaceTool.prototype.makeSpace = function(movingShapes, resizingShapes, delta, direction, start) { return this._modeling.createSpace(movingShapes, resizingShapes, delta, direction, start); }; /** * Initialize make space and return true if that was successful. * * @param {Object} event * @param {Object} context * * @return {boolean} */ SpaceTool.prototype.init = function(event, context) { var axis = abs$1(event.dx) > abs$1(event.dy) ? 'x' : 'y', delta = event[ 'd' + axis ], start = event[ axis ] - delta; if (abs$1(delta) < 5) { return false; } // invert delta to remove space when moving left if (delta < 0) { delta *= -1; } // invert delta to add/remove space when removing/adding space if modifier key is pressed if (hasPrimaryModifier(event)) { delta *= -1; } var direction = getDirection(axis, delta); var root = this._canvas.getRootElement(); var children = selfAndAllChildren(root, true); var elements = this.calculateAdjustments(children, axis, delta, start); var minDimensions = this._eventBus.fire('spaceTool.getMinDimensions', { axis: axis, direction: direction, shapes: elements.resizingShapes, start: start }); var spaceToolConstraints = getSpaceToolConstraints(elements, axis, direction, start, minDimensions); assign( context, elements, { axis: axis, direction: direction, spaceToolConstraints: spaceToolConstraints, start: start } ); set('resize-' + (axis === 'x' ? 'ew' : 'ns')); return true; }; /** * Get elements to be moved and resized. * * @param {Array} elements * @param {string} axis * @param {number} delta * @param {number} start * * @return {Object} */ SpaceTool.prototype.calculateAdjustments = function(elements, axis, delta, start) { var rules = this._rules; var movingShapes = [], resizingShapes = []; forEach$1(elements, function(element) { if (!element.parent || isConnection$7(element)) { return; } var shapeStart = element[ axis ], shapeEnd = shapeStart + element[ AXIS_TO_DIMENSION[ axis ] ]; // shape to be moved if ((delta > 0 && shapeStart > start) || (delta < 0 && shapeEnd < start)) { return movingShapes.push(element); } // shape to be resized if (shapeStart < start && shapeEnd > start && rules.allowed('shape.resize', { shape: element }) ) { return resizingShapes.push(element); } }); return { movingShapes: movingShapes, resizingShapes: resizingShapes }; }; SpaceTool.prototype.toggle = function() { if (this.isActive()) { return this._dragging.cancel(); } var mouseEvent = this._mouse.getLastMoveEvent(); this.activateSelection(mouseEvent, !!mouseEvent); }; SpaceTool.prototype.isActive = function() { var context = this._dragging.context(); return context && /^spaceTool/.test(context.prefix); }; // helpers ////////// function addPadding(trbl) { return { top: trbl.top - PADDING, right: trbl.right + PADDING, bottom: trbl.bottom + PADDING, left: trbl.left - PADDING }; } function ensureConstraints(event) { var context = event.context, spaceToolConstraints = context.spaceToolConstraints; if (!spaceToolConstraints) { return; } var x, y; if (isNumber(spaceToolConstraints.left)) { x = Math.max(event.x, spaceToolConstraints.left); event.dx = event.dx + x - event.x; event.x = x; } if (isNumber(spaceToolConstraints.right)) { x = Math.min(event.x, spaceToolConstraints.right); event.dx = event.dx + x - event.x; event.x = x; } if (isNumber(spaceToolConstraints.top)) { y = Math.max(event.y, spaceToolConstraints.top); event.dy = event.dy + y - event.y; event.y = y; } if (isNumber(spaceToolConstraints.bottom)) { y = Math.min(event.y, spaceToolConstraints.bottom); event.dy = event.dy + y - event.y; event.y = y; } } function getSpaceToolConstraints(elements, axis, direction, start, minDimensions) { var movingShapes = elements.movingShapes, resizingShapes = elements.resizingShapes; if (!resizingShapes.length) { return; } var spaceToolConstraints = {}, min, max; forEach$1(resizingShapes, function(resizingShape) { var resizingShapeBBox = asTRBL(resizingShape); // find children that are not moving or resizing var nonMovingResizingChildren = filter(resizingShape.children, function(child) { return !isConnection$7(child) && !isLabel$2(child) && !includes$2(movingShapes, child) && !includes$2(resizingShapes, child); }); // find children that are moving var movingChildren = filter(resizingShape.children, function(child) { return !isConnection$7(child) && !isLabel$2(child) && includes$2(movingShapes, child); }); var minOrMax, nonMovingResizingChildrenBBox, movingChildrenBBox; if (nonMovingResizingChildren.length) { nonMovingResizingChildrenBBox = addPadding(asTRBL(getBBox(nonMovingResizingChildren))); minOrMax = start - resizingShapeBBox[ DIRECTION_TO_TRBL[ direction ] ] + nonMovingResizingChildrenBBox[ DIRECTION_TO_TRBL[ direction ] ]; if (direction === 'n') { spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax; } else if (direction === 'w') { spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax; } else if (direction === 's') { spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax; } else if (direction === 'e') { spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax; } } if (movingChildren.length) { movingChildrenBBox = addPadding(asTRBL(getBBox(movingChildren))); minOrMax = start - movingChildrenBBox[ DIRECTION_TO_TRBL[ DIRECTION_TO_OPPOSITE[ direction ] ] ] + resizingShapeBBox[ DIRECTION_TO_TRBL[ DIRECTION_TO_OPPOSITE[ direction ] ] ]; if (direction === 'n') { spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax; } else if (direction === 'w') { spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax; } else if (direction === 's') { spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax; } else if (direction === 'e') { spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax; } } var resizingShapeMinDimensions = minDimensions && minDimensions[ resizingShape.id ]; if (resizingShapeMinDimensions) { if (direction === 'n') { minOrMax = start + resizingShape[ AXIS_TO_DIMENSION [ axis ] ] - resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ]; spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax; } else if (direction === 'w') { minOrMax = start + resizingShape[ AXIS_TO_DIMENSION [ axis ] ] - resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ]; spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax; } else if (direction === 's') { minOrMax = start - resizingShape[ AXIS_TO_DIMENSION [ axis ] ] + resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ]; spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax; } else if (direction === 'e') { minOrMax = start - resizingShape[ AXIS_TO_DIMENSION [ axis ] ] + resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ]; spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax; } } }); return spaceToolConstraints; } function includes$2(array, item) { return array.indexOf(item) !== -1; } function isConnection$7(element) { return !!element.waypoints; } function isLabel$2(element) { return !!element.labelTarget; } var MARKER_DRAGGING$1 = 'djs-dragging', MARKER_RESIZING = 'djs-resizing'; var LOW_PRIORITY$3 = 250; var max = Math.max; /** * Provides previews for selecting/moving/resizing shapes when creating/removing space. * * @param {EventBus} eventBus * @param {ElementRegistry} elementRegistry * @param {Canvas} canvas * @param {Styles} styles */ function SpaceToolPreview( eventBus, elementRegistry, canvas, styles, previewSupport) { function addPreviewGfx(collection, dragGroup) { forEach$1(collection, function(element) { previewSupport.addDragger(element, dragGroup); canvas.addMarker(element, MARKER_DRAGGING$1); }); } // add crosshair eventBus.on('spaceTool.selection.start', function(event) { var space = canvas.getLayer('space'), context = event.context; var orientation = { x: 'M 0,-10000 L 0,10000', y: 'M -10000,0 L 10000,0' }; var crosshairGroup = create$1('g'); attr(crosshairGroup, styles.cls('djs-crosshair-group', [ 'no-events' ])); append(space, crosshairGroup); // horizontal path var pathX = create$1('path'); attr(pathX, 'd', orientation.x); classes(pathX).add('djs-crosshair'); append(crosshairGroup, pathX); // vertical path var pathY = create$1('path'); attr(pathY, 'd', orientation.y); classes(pathY).add('djs-crosshair'); append(crosshairGroup, pathY); context.crosshairGroup = crosshairGroup; }); // update crosshair eventBus.on('spaceTool.selection.move', function(event) { var crosshairGroup = event.context.crosshairGroup; translate$2(crosshairGroup, event.x, event.y); }); // remove crosshair eventBus.on('spaceTool.selection.cleanup', function(event) { var context = event.context, crosshairGroup = context.crosshairGroup; if (crosshairGroup) { remove$1(crosshairGroup); } }); // add and update move/resize previews eventBus.on('spaceTool.move', LOW_PRIORITY$3, function(event) { var context = event.context, line = context.line, axis = context.axis, movingShapes = context.movingShapes, resizingShapes = context.resizingShapes; if (!context.initialized) { return; } if (!context.dragGroup) { var spaceLayer = canvas.getLayer('space'); line = create$1('path'); attr(line, 'd', 'M0,0 L0,0'); classes(line).add('djs-crosshair'); append(spaceLayer, line); context.line = line; var dragGroup = create$1('g'); attr(dragGroup, styles.cls('djs-drag-group', [ 'no-events' ])); append(canvas.getActiveLayer(), dragGroup); // shapes addPreviewGfx(movingShapes, dragGroup); // connections var movingConnections = context.movingConnections = elementRegistry.filter(function(element) { var sourceIsMoving = false; forEach$1(movingShapes, function(shape) { forEach$1(shape.outgoing, function(connection) { if (element === connection) { sourceIsMoving = true; } }); }); var targetIsMoving = false; forEach$1(movingShapes, function(shape) { forEach$1(shape.incoming, function(connection) { if (element === connection) { targetIsMoving = true; } }); }); var sourceIsResizing = false; forEach$1(resizingShapes, function(shape) { forEach$1(shape.outgoing, function(connection) { if (element === connection) { sourceIsResizing = true; } }); }); var targetIsResizing = false; forEach$1(resizingShapes, function(shape) { forEach$1(shape.incoming, function(connection) { if (element === connection) { targetIsResizing = true; } }); }); return isConnection$6(element) && (sourceIsMoving || sourceIsResizing) && (targetIsMoving || targetIsResizing); }); addPreviewGfx(movingConnections, dragGroup); context.dragGroup = dragGroup; } if (!context.frameGroup) { var frameGroup = create$1('g'); attr(frameGroup, styles.cls('djs-frame-group', [ 'no-events' ])); append(canvas.getActiveLayer(), frameGroup); var frames = []; forEach$1(resizingShapes, function(shape) { var frame = previewSupport.addFrame(shape, frameGroup); var initialBounds = frame.getBBox(); frames.push({ element: frame, initialBounds: initialBounds }); canvas.addMarker(shape, MARKER_RESIZING); }); context.frameGroup = frameGroup; context.frames = frames; } var orientation = { x: 'M' + event.x + ', -10000 L' + event.x + ', 10000', y: 'M -10000, ' + event.y + ' L 10000, ' + event.y }; attr(line, { d: orientation[ axis ] }); var opposite = { x: 'y', y: 'x' }; var delta = { x: event.dx, y: event.dy }; delta[ opposite[ context.axis ] ] = 0; // update move previews translate$2(context.dragGroup, delta.x, delta.y); // update resize previews forEach$1(context.frames, function(frame) { var element = frame.element, initialBounds = frame.initialBounds, width, height; if (context.direction === 'e') { attr(element, { width: max(initialBounds.width + delta.x, 5) }); } else { width = max(initialBounds.width - delta.x, 5); attr(element, { width: width, x: initialBounds.x + initialBounds.width - width }); } if (context.direction === 's') { attr(element, { height: max(initialBounds.height + delta.y, 5) }); } else { height = max(initialBounds.height - delta.y, 5); attr(element, { height: height, y: initialBounds.y + initialBounds.height - height }); } }); }); // remove move/resize previews eventBus.on('spaceTool.cleanup', function(event) { var context = event.context, movingShapes = context.movingShapes, movingConnections = context.movingConnections, resizingShapes = context.resizingShapes, line = context.line, dragGroup = context.dragGroup, frameGroup = context.frameGroup; // moving shapes forEach$1(movingShapes, function(shape) { canvas.removeMarker(shape, MARKER_DRAGGING$1); }); // moving connections forEach$1(movingConnections, function(connection) { canvas.removeMarker(connection, MARKER_DRAGGING$1); }); if (dragGroup) { remove$1(line); remove$1(dragGroup); } forEach$1(resizingShapes, function(shape) { canvas.removeMarker(shape, MARKER_RESIZING); }); if (frameGroup) { remove$1(frameGroup); } }); } SpaceToolPreview.$inject = [ 'eventBus', 'elementRegistry', 'canvas', 'styles', 'previewSupport' ]; // helpers ////////////////////// /** * Checks if an element is a connection. */ function isConnection$6(element) { return element.waypoints; } var SpaceToolModule = { __init__: [ 'spaceToolPreview' ], __depends__: [ DraggingModule, RulesModule$1, ToolManagerModule, PreviewSupportModule, MouseModule ], spaceTool: [ 'type', SpaceTool ], spaceToolPreview: [ 'type', SpaceToolPreview ] }; function BpmnFactory(moddle) { this._model = moddle; } BpmnFactory.$inject = [ 'moddle' ]; BpmnFactory.prototype._needsId = function(element) { return isAny(element, [ 'bpmn:RootElement', 'bpmn:FlowElement', 'bpmn:MessageFlow', 'bpmn:DataAssociation', 'bpmn:Artifact', 'bpmn:Participant', 'bpmn:Lane', 'bpmn:LaneSet', 'bpmn:Process', 'bpmn:Collaboration', 'bpmndi:BPMNShape', 'bpmndi:BPMNEdge', 'bpmndi:BPMNDiagram', 'bpmndi:BPMNPlane', 'bpmn:Property', 'bpmn:CategoryValue' ]); }; BpmnFactory.prototype._ensureId = function(element) { if (element.id) { this._model.ids.claim(element.id, element); return; } // generate semantic ids for elements // bpmn:SequenceFlow -> SequenceFlow_ID var prefix; if (is$1(element, 'bpmn:Activity')) { prefix = 'Activity'; } else if (is$1(element, 'bpmn:Event')) { prefix = 'Event'; } else if (is$1(element, 'bpmn:Gateway')) { prefix = 'Gateway'; } else if (isAny(element, [ 'bpmn:SequenceFlow', 'bpmn:MessageFlow' ])) { prefix = 'Flow'; } else { prefix = (element.$type || '').replace(/^[^:]*:/g, ''); } prefix += '_'; if (!element.id && this._needsId(element)) { element.id = this._model.ids.nextPrefixed(prefix, element); } }; BpmnFactory.prototype.create = function(type, attrs) { var element = this._model.create(type, attrs || {}); this._ensureId(element); return element; }; BpmnFactory.prototype.createDiLabel = function() { return this.create('bpmndi:BPMNLabel', { bounds: this.createDiBounds() }); }; BpmnFactory.prototype.createDiShape = function(semantic, attrs) { return this.create('bpmndi:BPMNShape', assign({ bpmnElement: semantic, bounds: this.createDiBounds() }, attrs)); }; BpmnFactory.prototype.createDiBounds = function(bounds) { return this.create('dc:Bounds', bounds); }; BpmnFactory.prototype.createDiWaypoints = function(waypoints) { var self = this; return map(waypoints, function(pos) { return self.createDiWaypoint(pos); }); }; BpmnFactory.prototype.createDiWaypoint = function(point) { return this.create('dc:Point', pick(point, [ 'x', 'y' ])); }; BpmnFactory.prototype.createDiEdge = function(semantic, attrs) { return this.create('bpmndi:BPMNEdge', assign({ bpmnElement: semantic, waypoint: this.createDiWaypoints([]) }, attrs)); }; BpmnFactory.prototype.createDiPlane = function(semantic, attrs) { return this.create('bpmndi:BPMNPlane', assign({ bpmnElement: semantic }, attrs)); }; /** * A handler responsible for updating the underlying BPMN 2.0 XML + DI * once changes on the diagram happen */ function BpmnUpdater( eventBus, bpmnFactory, connectionDocking, translate) { CommandInterceptor.call(this, eventBus); this._bpmnFactory = bpmnFactory; this._translate = translate; var self = this; // connection cropping ////////////////////// // crop connection ends during create/update function cropConnection(e) { var context = e.context, hints = context.hints || {}, connection; if (!context.cropped && hints.createElementsBehavior !== false) { connection = context.connection; connection.waypoints = connectionDocking.getCroppedWaypoints(connection); context.cropped = true; } } this.executed([ 'connection.layout', 'connection.create' ], cropConnection); this.reverted([ 'connection.layout' ], function(e) { delete e.context.cropped; }); // BPMN + DI update ////////////////////// // update parent function updateParent(e) { var context = e.context; self.updateParent(context.shape || context.connection, context.oldParent); } function reverseUpdateParent(e) { var context = e.context; var element = context.shape || context.connection, // oldParent is the (old) new parent, because we are undoing oldParent = context.parent || context.newParent; self.updateParent(element, oldParent); } this.executed([ 'shape.move', 'shape.create', 'shape.delete', 'connection.create', 'connection.move', 'connection.delete' ], ifBpmn(updateParent)); this.reverted([ 'shape.move', 'shape.create', 'shape.delete', 'connection.create', 'connection.move', 'connection.delete' ], ifBpmn(reverseUpdateParent)); /* * ## Updating Parent * * When morphing a Process into a Collaboration or vice-versa, * make sure that both the *semantic* and *di* parent of each element * is updated. * */ function updateRoot(event) { var context = event.context, oldRoot = context.oldRoot, children = oldRoot.children; forEach$1(children, function(child) { if (is$1(child, 'bpmn:BaseElement')) { self.updateParent(child); } }); } this.executed([ 'canvas.updateRoot' ], updateRoot); this.reverted([ 'canvas.updateRoot' ], updateRoot); // update bounds function updateBounds(e) { var shape = e.context.shape; if (!is$1(shape, 'bpmn:BaseElement')) { return; } self.updateBounds(shape); } this.executed([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(function(event) { // exclude labels because they're handled separately during shape.changed if (event.context.shape.type === 'label') { return; } updateBounds(event); })); this.reverted([ 'shape.move', 'shape.create', 'shape.resize' ], ifBpmn(function(event) { // exclude labels because they're handled separately during shape.changed if (event.context.shape.type === 'label') { return; } updateBounds(event); })); // Handle labels separately. This is necessary, because the label bounds have to be updated // every time its shape changes, not only on move, create and resize. eventBus.on('shape.changed', function(event) { if (event.element.type === 'label') { updateBounds({ context: { shape: event.element } }); } }); // attach / detach connection function updateConnection(e) { self.updateConnection(e.context); } this.executed([ 'connection.create', 'connection.move', 'connection.delete', 'connection.reconnect' ], ifBpmn(updateConnection)); this.reverted([ 'connection.create', 'connection.move', 'connection.delete', 'connection.reconnect' ], ifBpmn(updateConnection)); // update waypoints function updateConnectionWaypoints(e) { self.updateConnectionWaypoints(e.context.connection); } this.executed([ 'connection.layout', 'connection.move', 'connection.updateWaypoints', ], ifBpmn(updateConnectionWaypoints)); this.reverted([ 'connection.layout', 'connection.move', 'connection.updateWaypoints', ], ifBpmn(updateConnectionWaypoints)); // update conditional/default flows this.executed('connection.reconnect', ifBpmn(function(event) { var context = event.context, connection = context.connection, oldSource = context.oldSource, newSource = context.newSource, connectionBo = getBusinessObject(connection), oldSourceBo = getBusinessObject(oldSource), newSourceBo = getBusinessObject(newSource); // remove condition from connection on reconnect to new source // if new source can NOT have condional sequence flow if (connectionBo.conditionExpression && !isAny(newSourceBo, [ 'bpmn:Activity', 'bpmn:ExclusiveGateway', 'bpmn:InclusiveGateway' ])) { context.oldConditionExpression = connectionBo.conditionExpression; delete connectionBo.conditionExpression; } // remove default from old source flow on reconnect to new source // if source changed if (oldSource !== newSource && oldSourceBo.default === connectionBo) { context.oldDefault = oldSourceBo.default; delete oldSourceBo.default; } })); this.reverted('connection.reconnect', ifBpmn(function(event) { var context = event.context, connection = context.connection, oldSource = context.oldSource, newSource = context.newSource, connectionBo = getBusinessObject(connection), oldSourceBo = getBusinessObject(oldSource), newSourceBo = getBusinessObject(newSource); // add condition to connection on revert reconnect to new source if (context.oldConditionExpression) { connectionBo.conditionExpression = context.oldConditionExpression; } // add default to old source on revert reconnect to new source if (context.oldDefault) { oldSourceBo.default = context.oldDefault; delete newSourceBo.default; } })); // update attachments function updateAttachment(e) { self.updateAttachment(e.context); } this.executed([ 'element.updateAttachment' ], ifBpmn(updateAttachment)); this.reverted([ 'element.updateAttachment' ], ifBpmn(updateAttachment)); } e(BpmnUpdater, CommandInterceptor); BpmnUpdater.$inject = [ 'eventBus', 'bpmnFactory', 'connectionDocking', 'translate' ]; // implementation ////////////////////// BpmnUpdater.prototype.updateAttachment = function(context) { var shape = context.shape, businessObject = shape.businessObject, host = shape.host; businessObject.attachedToRef = host && host.businessObject; }; BpmnUpdater.prototype.updateParent = function(element, oldParent) { // do not update BPMN 2.0 label parent if (element instanceof Label) { return; } // data stores in collaborations are handled separately by DataStoreBehavior if (is$1(element, 'bpmn:DataStoreReference') && element.parent && is$1(element.parent, 'bpmn:Collaboration')) { return; } var parentShape = element.parent; var businessObject = element.businessObject, di = getDi(element), parentBusinessObject = parentShape && parentShape.businessObject, parentDi = getDi(parentShape); if (is$1(element, 'bpmn:FlowNode')) { this.updateFlowNodeRefs(businessObject, parentBusinessObject, oldParent && oldParent.businessObject); } if (is$1(element, 'bpmn:DataOutputAssociation')) { if (element.source) { parentBusinessObject = element.source.businessObject; } else { parentBusinessObject = null; } } if (is$1(element, 'bpmn:DataInputAssociation')) { if (element.target) { parentBusinessObject = element.target.businessObject; } else { parentBusinessObject = null; } } this.updateSemanticParent(businessObject, parentBusinessObject); if (is$1(element, 'bpmn:DataObjectReference') && businessObject.dataObjectRef) { this.updateSemanticParent(businessObject.dataObjectRef, parentBusinessObject); } this.updateDiParent(di, parentDi); }; BpmnUpdater.prototype.updateBounds = function(shape) { var di = getDi(shape), embeddedLabelBounds = getEmbeddedLabelBounds(shape); // update embedded label bounds if possible if (embeddedLabelBounds) { var embeddedLabelBoundsDelta = delta(embeddedLabelBounds, di.get('bounds')); assign(embeddedLabelBounds, { x: shape.x + embeddedLabelBoundsDelta.x, y: shape.y + embeddedLabelBoundsDelta.y }); } var target = (shape instanceof Label) ? this._getLabel(di) : di; var bounds = target.bounds; if (!bounds) { bounds = this._bpmnFactory.createDiBounds(); target.set('bounds', bounds); } assign(bounds, { x: shape.x, y: shape.y, width: shape.width, height: shape.height }); }; BpmnUpdater.prototype.updateFlowNodeRefs = function(businessObject, newContainment, oldContainment) { if (oldContainment === newContainment) { return; } var oldRefs, newRefs; if (is$1 (oldContainment, 'bpmn:Lane')) { oldRefs = oldContainment.get('flowNodeRef'); remove(oldRefs, businessObject); } if (is$1(newContainment, 'bpmn:Lane')) { newRefs = newContainment.get('flowNodeRef'); add(newRefs, businessObject); } }; // update existing sourceElement and targetElement di information BpmnUpdater.prototype.updateDiConnection = function(connection, newSource, newTarget) { var connectionDi = getDi(connection), newSourceDi = getDi(newSource), newTargetDi = getDi(newTarget); if (connectionDi.sourceElement && connectionDi.sourceElement.bpmnElement !== getBusinessObject(newSource)) { connectionDi.sourceElement = newSource && newSourceDi; } if (connectionDi.targetElement && connectionDi.targetElement.bpmnElement !== getBusinessObject(newTarget)) { connectionDi.targetElement = newTarget && newTargetDi; } }; BpmnUpdater.prototype.updateDiParent = function(di, parentDi) { if (parentDi && !is$1(parentDi, 'bpmndi:BPMNPlane')) { parentDi = parentDi.$parent; } if (di.$parent === parentDi) { return; } var planeElements = (parentDi || di.$parent).get('planeElement'); if (parentDi) { planeElements.push(di); di.$parent = parentDi; } else { remove(planeElements, di); di.$parent = null; } }; function getDefinitions(element) { while (element && !is$1(element, 'bpmn:Definitions')) { element = element.$parent; } return element; } BpmnUpdater.prototype.getLaneSet = function(container) { var laneSet, laneSets; // bpmn:Lane if (is$1(container, 'bpmn:Lane')) { laneSet = container.childLaneSet; if (!laneSet) { laneSet = this._bpmnFactory.create('bpmn:LaneSet'); container.childLaneSet = laneSet; laneSet.$parent = container; } return laneSet; } // bpmn:Participant if (is$1(container, 'bpmn:Participant')) { container = container.processRef; } // bpmn:FlowElementsContainer laneSets = container.get('laneSets'); laneSet = laneSets[0]; if (!laneSet) { laneSet = this._bpmnFactory.create('bpmn:LaneSet'); laneSet.$parent = container; laneSets.push(laneSet); } return laneSet; }; BpmnUpdater.prototype.updateSemanticParent = function(businessObject, newParent, visualParent) { var containment, translate = this._translate; if (businessObject.$parent === newParent) { return; } if (is$1(businessObject, 'bpmn:DataInput') || is$1(businessObject, 'bpmn:DataOutput')) { if (is$1(newParent, 'bpmn:Participant') && 'processRef' in newParent) { newParent = newParent.processRef; } // already in correct ioSpecification if ('ioSpecification' in newParent && newParent.ioSpecification === businessObject.$parent) { return; } } if (is$1(businessObject, 'bpmn:Lane')) { if (newParent) { newParent = this.getLaneSet(newParent); } containment = 'lanes'; } else if (is$1(businessObject, 'bpmn:FlowElement')) { if (newParent) { if (is$1(newParent, 'bpmn:Participant')) { newParent = newParent.processRef; } else if (is$1(newParent, 'bpmn:Lane')) { do { // unwrap Lane -> LaneSet -> (Lane | FlowElementsContainer) newParent = newParent.$parent.$parent; } while (is$1(newParent, 'bpmn:Lane')); } } containment = 'flowElements'; } else if (is$1(businessObject, 'bpmn:Artifact')) { while (newParent && !is$1(newParent, 'bpmn:Process') && !is$1(newParent, 'bpmn:SubProcess') && !is$1(newParent, 'bpmn:Collaboration')) { if (is$1(newParent, 'bpmn:Participant')) { newParent = newParent.processRef; break; } else { newParent = newParent.$parent; } } containment = 'artifacts'; } else if (is$1(businessObject, 'bpmn:MessageFlow')) { containment = 'messageFlows'; } else if (is$1(businessObject, 'bpmn:Participant')) { containment = 'participants'; // make sure the participants process is properly attached / detached // from the XML document var process = businessObject.processRef, definitions; if (process) { definitions = getDefinitions(businessObject.$parent || newParent); if (businessObject.$parent) { remove(definitions.get('rootElements'), process); process.$parent = null; } if (newParent) { add(definitions.get('rootElements'), process); process.$parent = definitions; } } } else if (is$1(businessObject, 'bpmn:DataOutputAssociation')) { containment = 'dataOutputAssociations'; } else if (is$1(businessObject, 'bpmn:DataInputAssociation')) { containment = 'dataInputAssociations'; } if (!containment) { throw new Error(translate( 'no parent for {element} in {parent}', { element: businessObject.id, parent: newParent.id } )); } var children; if (businessObject.$parent) { // remove from old parent children = businessObject.$parent.get(containment); remove(children, businessObject); } if (!newParent) { businessObject.$parent = null; } else { // add to new parent children = newParent.get(containment); children.push(businessObject); businessObject.$parent = newParent; } if (visualParent) { var diChildren = visualParent.get(containment); remove(children, businessObject); if (newParent) { if (!diChildren) { diChildren = []; newParent.set(containment, diChildren); } diChildren.push(businessObject); } } }; BpmnUpdater.prototype.updateConnectionWaypoints = function(connection) { var di = getDi(connection); di.set('waypoint', this._bpmnFactory.createDiWaypoints(connection.waypoints)); }; BpmnUpdater.prototype.updateConnection = function(context) { var connection = context.connection, businessObject = getBusinessObject(connection), newSource = connection.source, newSourceBo = getBusinessObject(newSource), newTarget = connection.target, newTargetBo = getBusinessObject(connection.target), visualParent; if (!is$1(businessObject, 'bpmn:DataAssociation')) { var inverseSet = is$1(businessObject, 'bpmn:SequenceFlow'); if (businessObject.sourceRef !== newSourceBo) { if (inverseSet) { remove(businessObject.sourceRef && businessObject.sourceRef.get('outgoing'), businessObject); if (newSourceBo && newSourceBo.get('outgoing')) { newSourceBo.get('outgoing').push(businessObject); } } businessObject.sourceRef = newSourceBo; } if (businessObject.targetRef !== newTargetBo) { if (inverseSet) { remove(businessObject.targetRef && businessObject.targetRef.get('incoming'), businessObject); if (newTargetBo && newTargetBo.get('incoming')) { newTargetBo.get('incoming').push(businessObject); } } businessObject.targetRef = newTargetBo; } } else if (is$1(businessObject, 'bpmn:DataInputAssociation')) { // handle obnoxious isMsome sourceRef businessObject.get('sourceRef')[0] = newSourceBo; visualParent = context.parent || context.newParent || newTargetBo; this.updateSemanticParent(businessObject, newTargetBo, visualParent); } else if (is$1(businessObject, 'bpmn:DataOutputAssociation')) { visualParent = context.parent || context.newParent || newSourceBo; this.updateSemanticParent(businessObject, newSourceBo, visualParent); // targetRef = new target businessObject.targetRef = newTargetBo; } this.updateConnectionWaypoints(connection); this.updateDiConnection(connection, newSource, newTarget); }; // helpers ////////////////////// BpmnUpdater.prototype._getLabel = function(di) { if (!di.label) { di.label = this._bpmnFactory.createDiLabel(); } return di.label; }; /** * Make sure the event listener is only called * if the touched element is a BPMN element. * * @param {Function} fn * @return {Function} guarded function */ function ifBpmn(fn) { return function(event) { var context = event.context, element = context.shape || context.connection; if (is$1(element, 'bpmn:BaseElement')) { fn(event); } }; } /** * Return dc:Bounds of bpmndi:BPMNLabel if exists. * * @param {djs.model.shape} shape * * @returns {Object|undefined} */ function getEmbeddedLabelBounds(shape) { if (!is$1(shape, 'bpmn:Activity')) { return; } var di = getDi(shape); if (!di) { return; } var label = di.get('label'); if (!label) { return; } return label.get('bounds'); } /** * A bpmn-aware factory for diagram-js shapes */ function ElementFactory(bpmnFactory, moddle, translate) { ElementFactory$1.call(this); this._bpmnFactory = bpmnFactory; this._moddle = moddle; this._translate = translate; } e(ElementFactory, ElementFactory$1); ElementFactory.$inject = [ 'bpmnFactory', 'moddle', 'translate' ]; ElementFactory.prototype.baseCreate = ElementFactory$1.prototype.create; ElementFactory.prototype.create = function(elementType, attrs) { // no special magic for labels, // we assume their businessObjects have already been created // and wired via attrs if (elementType === 'label') { var di = attrs.di || this._bpmnFactory.createDiLabel(); return this.baseCreate(elementType, assign({ type: 'label', di: di }, DEFAULT_LABEL_SIZE, attrs)); } return this.createBpmnElement(elementType, attrs); }; ElementFactory.prototype.createBpmnElement = function(elementType, attrs) { var size, translate = this._translate; attrs = assign({}, attrs || {}); var businessObject = attrs.businessObject, di = attrs.di; if (!businessObject) { if (!attrs.type) { throw new Error(translate('no shape type specified')); } businessObject = this._bpmnFactory.create(attrs.type); ensureCompatDiRef(businessObject); } if (!isModdleDi(di)) { var diAttrs = assign( {}, di || {}, { id: businessObject.id + '_di' } ); if (elementType === 'root') { di = this._bpmnFactory.createDiPlane(businessObject, diAttrs); } else if (elementType === 'connection') { di = this._bpmnFactory.createDiEdge(businessObject, diAttrs); } else { di = this._bpmnFactory.createDiShape(businessObject, diAttrs); } } if (is$1(businessObject, 'bpmn:Group')) { attrs = assign({ isFrame: true }, attrs); } attrs = applyAttributes(businessObject, attrs, [ 'processRef', 'isInterrupting', 'associationDirection', 'isForCompensation' ]); if (attrs.isExpanded) { attrs = applyAttribute(di, attrs, 'isExpanded'); } if (is$1(businessObject, 'bpmn:SubProcess')) { attrs.collapsed = !isExpanded(businessObject, di); } if (is$1(businessObject, 'bpmn:ExclusiveGateway')) { di.isMarkerVisible = true; } var eventDefinitions, newEventDefinition; if (attrs.eventDefinitionType) { eventDefinitions = businessObject.get('eventDefinitions') || []; newEventDefinition = this._bpmnFactory.create(attrs.eventDefinitionType, attrs.eventDefinitionAttrs); if (attrs.eventDefinitionType === 'bpmn:ConditionalEventDefinition') { newEventDefinition.condition = this._bpmnFactory.create('bpmn:FormalExpression'); } eventDefinitions.push(newEventDefinition); newEventDefinition.$parent = businessObject; businessObject.eventDefinitions = eventDefinitions; delete attrs.eventDefinitionType; } size = this.getDefaultSize(businessObject, di); attrs = assign({ id: businessObject.id }, size, attrs, { businessObject: businessObject, di: di }); return this.baseCreate(elementType, attrs); }; ElementFactory.prototype.getDefaultSize = function(element, di) { var bo = getBusinessObject(element); di = di || getDi(element); if (is$1(bo, 'bpmn:SubProcess')) { if (isExpanded(bo, di)) { return { width: 350, height: 200 }; } else { return { width: 100, height: 80 }; } } if (is$1(bo, 'bpmn:Task')) { return { width: 100, height: 80 }; } if (is$1(bo, 'bpmn:Gateway')) { return { width: 50, height: 50 }; } if (is$1(bo, 'bpmn:Event')) { return { width: 36, height: 36 }; } if (is$1(bo, 'bpmn:Participant')) { if (isExpanded(bo, di)) { return { width: 600, height: 250 }; } else { return { width: 400, height: 60 }; } } if (is$1(bo, 'bpmn:Lane')) { return { width: 400, height: 100 }; } if (is$1(bo, 'bpmn:DataObjectReference')) { return { width: 36, height: 50 }; } if (is$1(bo, 'bpmn:DataStoreReference')) { return { width: 50, height: 50 }; } if (is$1(bo, 'bpmn:TextAnnotation')) { return { width: 100, height: 30 }; } if (is$1(bo, 'bpmn:Group')) { return { width: 300, height: 300 }; } return { width: 100, height: 80 }; }; /** * Create participant. * * @param {boolean|Object} [attrs] attrs * * @returns {djs.model.Shape} */ ElementFactory.prototype.createParticipantShape = function(attrs) { if (!isObject(attrs)) { attrs = { isExpanded: attrs }; } attrs = assign({ type: 'bpmn:Participant' }, attrs || {}); // participants are expanded by default if (attrs.isExpanded !== false) { attrs.processRef = this._bpmnFactory.create('bpmn:Process'); } return this.createShape(attrs); }; // helpers ////////////////////// /** * Apply attributes from a map to the given element, * remove attribute from the map on application. * * @param {Base} element * @param {Object} attrs (in/out map of attributes) * @param {Array} attributeNames name of attributes to apply * * @return {Object} changed attrs */ function applyAttributes(element, attrs, attributeNames) { forEach$1(attributeNames, function(property) { attrs = applyAttribute(element, attrs, property); }); return attrs; } /** * Apply named property to element and drain it from the attrs * collection. * * @param {Base} element * @param {Object} attrs (in/out map of attributes) * @param {string} attributeName to apply * * @return {Object} changed attrs */ function applyAttribute(element, attrs, attributeName) { if (attrs[attributeName] === undefined) { return attrs; } element[attributeName] = attrs[attributeName]; return omit(attrs, [ attributeName ]); } function isModdleDi(element) { return isAny(element, [ 'bpmndi:BPMNShape', 'bpmndi:BPMNEdge', 'bpmndi:BPMNDiagram', 'bpmndi:BPMNPlane', ]); } /** * A handler that align elements in a certain way. * */ function AlignElements(modeling, canvas) { this._modeling = modeling; this._canvas = canvas; } AlignElements.$inject = [ 'modeling', 'canvas' ]; AlignElements.prototype.preExecute = function(context) { var modeling = this._modeling; var elements = context.elements, alignment = context.alignment; forEach$1(elements, function(element) { var delta = { x: 0, y: 0 }; if (alignment.left) { delta.x = alignment.left - element.x; } else if (alignment.right) { delta.x = (alignment.right - element.width) - element.x; } else if (alignment.center) { delta.x = (alignment.center - Math.round(element.width / 2)) - element.x; } else if (alignment.top) { delta.y = alignment.top - element.y; } else if (alignment.bottom) { delta.y = (alignment.bottom - element.height) - element.y; } else if (alignment.middle) { delta.y = (alignment.middle - Math.round(element.height / 2)) - element.y; } modeling.moveElements([ element ], delta, element.parent); }); }; AlignElements.prototype.postExecute = function(context) { }; /** * A handler that implements reversible appending of shapes * to a source shape. * * @param {canvas} Canvas * @param {elementFactory} ElementFactory * @param {modeling} Modeling */ function AppendShapeHandler(modeling) { this._modeling = modeling; } AppendShapeHandler.$inject = [ 'modeling' ]; // api ////////////////////// /** * Creates a new shape * * @param {Object} context * @param {ElementDescriptor} context.shape the new shape * @param {ElementDescriptor} context.source the source object * @param {ElementDescriptor} context.parent the parent object * @param {Point} context.position position of the new element */ AppendShapeHandler.prototype.preExecute = function(context) { var source = context.source; if (!source) { throw new Error('source required'); } var target = context.target || source.parent, shape = context.shape, hints = context.hints || {}; shape = context.shape = this._modeling.createShape( shape, context.position, target, { attach: hints.attach }); context.shape = shape; }; AppendShapeHandler.prototype.postExecute = function(context) { var hints = context.hints || {}; if (!existsConnection(context.source, context.shape)) { // create connection if (hints.connectionTarget === context.source) { this._modeling.connect(context.shape, context.source, context.connection); } else { this._modeling.connect(context.source, context.shape, context.connection); } } }; function existsConnection(source, target) { return some(source.outgoing, function(c) { return c.target === target; }); } function CreateConnectionHandler(canvas, layouter) { this._canvas = canvas; this._layouter = layouter; } CreateConnectionHandler.$inject = [ 'canvas', 'layouter' ]; // api ////////////////////// /** * Appends a shape to a target shape * * @param {Object} context * @param {djs.element.Base} context.source the source object * @param {djs.element.Base} context.target the parent object * @param {Point} context.position position of the new element */ CreateConnectionHandler.prototype.execute = function(context) { var connection = context.connection, source = context.source, target = context.target, parent = context.parent, parentIndex = context.parentIndex, hints = context.hints; if (!source || !target) { throw new Error('source and target required'); } if (!parent) { throw new Error('parent required'); } connection.source = source; connection.target = target; if (!connection.waypoints) { connection.waypoints = this._layouter.layoutConnection(connection, hints); } // add connection this._canvas.addConnection(connection, parent, parentIndex); return connection; }; CreateConnectionHandler.prototype.revert = function(context) { var connection = context.connection; this._canvas.removeConnection(connection); connection.source = null; connection.target = null; return connection; }; var round$3 = Math.round; function CreateElementsHandler(modeling) { this._modeling = modeling; } CreateElementsHandler.$inject = [ 'modeling' ]; CreateElementsHandler.prototype.preExecute = function(context) { var elements = context.elements, parent = context.parent, parentIndex = context.parentIndex, position = context.position, hints = context.hints; var modeling = this._modeling; // make sure each element has x and y forEach$1(elements, function(element) { if (!isNumber(element.x)) { element.x = 0; } if (!isNumber(element.y)) { element.y = 0; } }); var visibleElements = filter(elements, function(element) { return !element.hidden; }); var bbox = getBBox(visibleElements); // center elements around position forEach$1(elements, function(element) { if (isConnection$5(element)) { element.waypoints = map(element.waypoints, function(waypoint) { return { x: round$3(waypoint.x - bbox.x - bbox.width / 2 + position.x), y: round$3(waypoint.y - bbox.y - bbox.height / 2 + position.y) }; }); } assign(element, { x: round$3(element.x - bbox.x - bbox.width / 2 + position.x), y: round$3(element.y - bbox.y - bbox.height / 2 + position.y) }); }); var parents = getParents$1(elements); var cache = {}; forEach$1(elements, function(element) { if (isConnection$5(element)) { cache[ element.id ] = isNumber(parentIndex) ? modeling.createConnection( cache[ element.source.id ], cache[ element.target.id ], parentIndex, element, element.parent || parent, hints ) : modeling.createConnection( cache[ element.source.id ], cache[ element.target.id ], element, element.parent || parent, hints ); return; } var createShapeHints = assign({}, hints); if (parents.indexOf(element) === -1) { createShapeHints.autoResize = false; } cache[ element.id ] = isNumber(parentIndex) ? modeling.createShape( element, pick(element, [ 'x', 'y', 'width', 'height' ]), element.parent || parent, parentIndex, createShapeHints ) : modeling.createShape( element, pick(element, [ 'x', 'y', 'width', 'height' ]), element.parent || parent, createShapeHints ); }); context.elements = values(cache); }; // helpers ////////// function isConnection$5(element) { return !!element.waypoints; } var round$2 = Math.round; /** * A handler that implements reversible addition of shapes. * * @param {canvas} Canvas */ function CreateShapeHandler(canvas) { this._canvas = canvas; } CreateShapeHandler.$inject = [ 'canvas' ]; // api ////////////////////// /** * Appends a shape to a target shape * * @param {Object} context * @param {djs.model.Base} context.parent the parent object * @param {Point} context.position position of the new element */ CreateShapeHandler.prototype.execute = function(context) { var shape = context.shape, positionOrBounds = context.position, parent = context.parent, parentIndex = context.parentIndex; if (!parent) { throw new Error('parent required'); } if (!positionOrBounds) { throw new Error('position required'); } // (1) add at event center position _or_ at given bounds if (positionOrBounds.width !== undefined) { assign(shape, positionOrBounds); } else { assign(shape, { x: positionOrBounds.x - round$2(shape.width / 2), y: positionOrBounds.y - round$2(shape.height / 2) }); } // (2) add to canvas this._canvas.addShape(shape, parent, parentIndex); return shape; }; /** * Undo append by removing the shape */ CreateShapeHandler.prototype.revert = function(context) { var shape = context.shape; // (3) remove form canvas this._canvas.removeShape(shape); return shape; }; /** * A handler that attaches a label to a given target shape. * * @param {Canvas} canvas */ function CreateLabelHandler(canvas) { CreateShapeHandler.call(this, canvas); } e(CreateLabelHandler, CreateShapeHandler); CreateLabelHandler.$inject = [ 'canvas' ]; // api ////////////////////// var originalExecute = CreateShapeHandler.prototype.execute; /** * Appends a label to a target shape. * * @method CreateLabelHandler#execute * * @param {Object} context * @param {ElementDescriptor} context.target the element the label is attached to * @param {ElementDescriptor} context.parent the parent object * @param {Point} context.position position of the new element */ CreateLabelHandler.prototype.execute = function(context) { var label = context.shape; ensureValidDimensions(label); label.labelTarget = context.labelTarget; return originalExecute.call(this, context); }; var originalRevert = CreateShapeHandler.prototype.revert; /** * Undo append by removing the shape */ CreateLabelHandler.prototype.revert = function(context) { context.shape.labelTarget = null; return originalRevert.call(this, context); }; // helpers ////////////////////// function ensureValidDimensions(label) { // make sure a label has valid { width, height } dimensions [ 'width', 'height' ].forEach(function(prop) { if (typeof label[prop] === 'undefined') { label[prop] = 0; } }); } /** * A handler that implements reversible deletion of Connections. */ function DeleteConnectionHandler(canvas, modeling) { this._canvas = canvas; this._modeling = modeling; } DeleteConnectionHandler.$inject = [ 'canvas', 'modeling' ]; /** * - Remove connections */ DeleteConnectionHandler.prototype.preExecute = function(context) { var modeling = this._modeling; var connection = context.connection; // remove connections saveClear(connection.incoming, function(connection) { // To make sure that the connection isn't removed twice // For example if a container is removed modeling.removeConnection(connection, { nested: true }); }); saveClear(connection.outgoing, function(connection) { modeling.removeConnection(connection, { nested: true }); }); }; DeleteConnectionHandler.prototype.execute = function(context) { var connection = context.connection, parent = connection.parent; context.parent = parent; // remember containment context.parentIndex = indexOf(parent.children, connection); context.source = connection.source; context.target = connection.target; this._canvas.removeConnection(connection); connection.source = null; connection.target = null; return connection; }; /** * Command revert implementation. */ DeleteConnectionHandler.prototype.revert = function(context) { var connection = context.connection, parent = context.parent, parentIndex = context.parentIndex; connection.source = context.source; connection.target = context.target; // restore containment add(parent.children, connection, parentIndex); this._canvas.addConnection(connection, parent); return connection; }; function DeleteElementsHandler(modeling, elementRegistry) { this._modeling = modeling; this._elementRegistry = elementRegistry; } DeleteElementsHandler.$inject = [ 'modeling', 'elementRegistry' ]; DeleteElementsHandler.prototype.postExecute = function(context) { var modeling = this._modeling, elementRegistry = this._elementRegistry, elements = context.elements; forEach$1(elements, function(element) { // element may have been removed with previous // remove operations already (e.g. in case of nesting) if (!elementRegistry.get(element.id)) { return; } if (element.waypoints) { modeling.removeConnection(element); } else { modeling.removeShape(element); } }); }; /** * A handler that implements reversible deletion of shapes. * */ function DeleteShapeHandler(canvas, modeling) { this._canvas = canvas; this._modeling = modeling; } DeleteShapeHandler.$inject = [ 'canvas', 'modeling' ]; /** * - Remove connections * - Remove all direct children */ DeleteShapeHandler.prototype.preExecute = function(context) { var modeling = this._modeling; var shape = context.shape; // remove connections saveClear(shape.incoming, function(connection) { // To make sure that the connection isn't removed twice // For example if a container is removed modeling.removeConnection(connection, { nested: true }); }); saveClear(shape.outgoing, function(connection) { modeling.removeConnection(connection, { nested: true }); }); // remove child shapes and connections saveClear(shape.children, function(child) { if (isConnection$4(child)) { modeling.removeConnection(child, { nested: true }); } else { modeling.removeShape(child, { nested: true }); } }); }; /** * Remove shape and remember the parent */ DeleteShapeHandler.prototype.execute = function(context) { var canvas = this._canvas; var shape = context.shape, oldParent = shape.parent; context.oldParent = oldParent; // remove containment context.oldParentIndex = indexOf(oldParent.children, shape); // remove shape canvas.removeShape(shape); return shape; }; /** * Command revert implementation */ DeleteShapeHandler.prototype.revert = function(context) { var canvas = this._canvas; var shape = context.shape, oldParent = context.oldParent, oldParentIndex = context.oldParentIndex; // restore containment add(oldParent.children, shape, oldParentIndex); canvas.addShape(shape, oldParent); return shape; }; function isConnection$4(element) { return element.waypoints; } /** * A handler that distributes elements evenly. */ function DistributeElements(modeling) { this._modeling = modeling; } DistributeElements.$inject = [ 'modeling' ]; var OFF_AXIS = { x: 'y', y: 'x' }; DistributeElements.prototype.preExecute = function(context) { var modeling = this._modeling; var groups = context.groups, axis = context.axis, dimension = context.dimension; function updateRange(group, element) { group.range.min = Math.min(element[axis], group.range.min); group.range.max = Math.max(element[axis] + element[dimension], group.range.max); } function center(element) { return element[axis] + element[dimension] / 2; } function lastIdx(arr) { return arr.length - 1; } function rangeDiff(range) { return range.max - range.min; } function centerElement(refCenter, element) { var delta = { y: 0 }; delta[axis] = refCenter - center(element); if (delta[axis]) { delta[OFF_AXIS[axis]] = 0; modeling.moveElements([ element ], delta, element.parent); } } var firstGroup = groups[0], lastGroupIdx = lastIdx(groups), lastGroup = groups[ lastGroupIdx ]; var margin, spaceInBetween, groupsSize = 0; // the size of each range forEach$1(groups, function(group, idx) { var sortedElements, refElem, refCenter; if (group.elements.length < 2) { if (idx && idx !== groups.length - 1) { updateRange(group, group.elements[0]); groupsSize += rangeDiff(group.range); } return; } sortedElements = sortBy(group.elements, axis); refElem = sortedElements[0]; if (idx === lastGroupIdx) { refElem = sortedElements[lastIdx(sortedElements)]; } refCenter = center(refElem); // wanna update the ranges after the shapes have been centered group.range = null; forEach$1(sortedElements, function(element) { centerElement(refCenter, element); if (group.range === null) { group.range = { min: element[axis], max: element[axis] + element[dimension] }; return; } // update group's range after centering the range elements updateRange(group, element); }); if (idx && idx !== groups.length - 1) { groupsSize += rangeDiff(group.range); } }); spaceInBetween = Math.abs(lastGroup.range.min - firstGroup.range.max); margin = Math.round((spaceInBetween - groupsSize) / (groups.length - 1)); if (margin < groups.length - 1) { return; } forEach$1(groups, function(group, groupIdx) { var delta = {}, prevGroup; if (group === firstGroup || group === lastGroup) { return; } prevGroup = groups[groupIdx - 1]; group.range.max = 0; forEach$1(group.elements, function(element, idx) { delta[OFF_AXIS[axis]] = 0; delta[axis] = (prevGroup.range.max - element[axis]) + margin; if (group.range.min !== element[axis]) { delta[axis] += element[axis] - group.range.min; } if (delta[axis]) { modeling.moveElements([ element ], delta, element.parent); } group.range.max = Math.max(element[axis] + element[dimension], idx ? group.range.max : 0); }); }); }; DistributeElements.prototype.postExecute = function(context) { }; /** * A handler that implements reversible moving of shapes. */ function LayoutConnectionHandler(layouter, canvas) { this._layouter = layouter; this._canvas = canvas; } LayoutConnectionHandler.$inject = [ 'layouter', 'canvas' ]; LayoutConnectionHandler.prototype.execute = function(context) { var connection = context.connection; var oldWaypoints = connection.waypoints; assign(context, { oldWaypoints: oldWaypoints }); connection.waypoints = this._layouter.layoutConnection(connection, context.hints); return connection; }; LayoutConnectionHandler.prototype.revert = function(context) { var connection = context.connection; connection.waypoints = context.oldWaypoints; return connection; }; /** * A handler that implements reversible moving of connections. * * The handler differs from the layout connection handler in a sense * that it preserves the connection layout. */ function MoveConnectionHandler() { } MoveConnectionHandler.prototype.execute = function(context) { var connection = context.connection, delta = context.delta; var newParent = context.newParent || connection.parent, newParentIndex = context.newParentIndex, oldParent = connection.parent; // save old parent in context context.oldParent = oldParent; context.oldParentIndex = remove(oldParent.children, connection); // add to new parent at position add(newParent.children, connection, newParentIndex); // update parent connection.parent = newParent; // update waypoint positions forEach$1(connection.waypoints, function(p) { p.x += delta.x; p.y += delta.y; if (p.original) { p.original.x += delta.x; p.original.y += delta.y; } }); return connection; }; MoveConnectionHandler.prototype.revert = function(context) { var connection = context.connection, newParent = connection.parent, oldParent = context.oldParent, oldParentIndex = context.oldParentIndex, delta = context.delta; // remove from newParent remove(newParent.children, connection); // restore previous location in old parent add(oldParent.children, connection, oldParentIndex); // restore parent connection.parent = oldParent; // revert to old waypoint positions forEach$1(connection.waypoints, function(p) { p.x -= delta.x; p.y -= delta.y; if (p.original) { p.original.x -= delta.x; p.original.y -= delta.y; } }); return connection; }; function MoveClosure() { this.allShapes = {}; this.allConnections = {}; this.enclosedElements = {}; this.enclosedConnections = {}; this.topLevel = {}; } MoveClosure.prototype.add = function(element, isTopLevel) { return this.addAll([ element ], isTopLevel); }; MoveClosure.prototype.addAll = function(elements, isTopLevel) { var newClosure = getClosure(elements, !!isTopLevel, this); assign(this, newClosure); return this; }; /** * A helper that is able to carry out serialized move * operations on multiple elements. * * @param {Modeling} modeling */ function MoveHelper(modeling) { this._modeling = modeling; } /** * Move the specified elements and all children by the given delta. * * This moves all enclosed connections, too and layouts all affected * external connections. * * @param {Array} elements * @param {Point} delta * @param {djs.model.Base} newParent applied to the first level of shapes * * @return {Array} list of touched elements */ MoveHelper.prototype.moveRecursive = function(elements, delta, newParent) { if (!elements) { return []; } else { return this.moveClosure(this.getClosure(elements), delta, newParent); } }; /** * Move the given closure of elmements. * * @param {Object} closure * @param {Point} delta * @param {djs.model.Base} [newParent] * @param {djs.model.Base} [newHost] */ MoveHelper.prototype.moveClosure = function(closure, delta, newParent, newHost, primaryShape) { var modeling = this._modeling; var allShapes = closure.allShapes, allConnections = closure.allConnections, enclosedConnections = closure.enclosedConnections, topLevel = closure.topLevel, keepParent = false; if (primaryShape && primaryShape.parent === newParent) { keepParent = true; } // move all shapes forEach$1(allShapes, function(shape) { // move the element according to the given delta modeling.moveShape(shape, delta, topLevel[shape.id] && !keepParent && newParent, { recurse: false, layout: false }); }); // move all child connections / layout external connections forEach$1(allConnections, function(c) { var sourceMoved = !!allShapes[c.source.id], targetMoved = !!allShapes[c.target.id]; if (enclosedConnections[c.id] && sourceMoved && targetMoved) { modeling.moveConnection(c, delta, topLevel[c.id] && !keepParent && newParent); } else { modeling.layoutConnection(c, { connectionStart: sourceMoved && getMovedSourceAnchor(c, c.source, delta), connectionEnd: targetMoved && getMovedTargetAnchor(c, c.target, delta) }); } }); }; /** * Returns the closure for the selected elements * * @param {Array} elements * @return {MoveClosure} closure */ MoveHelper.prototype.getClosure = function(elements) { return new MoveClosure().addAll(elements, true); }; /** * A handler that implements reversible moving of shapes. */ function MoveElementsHandler(modeling) { this._helper = new MoveHelper(modeling); } MoveElementsHandler.$inject = [ 'modeling' ]; MoveElementsHandler.prototype.preExecute = function(context) { context.closure = this._helper.getClosure(context.shapes); }; MoveElementsHandler.prototype.postExecute = function(context) { var hints = context.hints, primaryShape; if (hints && hints.primaryShape) { primaryShape = hints.primaryShape; hints.oldParent = primaryShape.parent; } this._helper.moveClosure( context.closure, context.delta, context.newParent, context.newHost, primaryShape ); }; /** * A handler that implements reversible moving of shapes. */ function MoveShapeHandler(modeling) { this._modeling = modeling; this._helper = new MoveHelper(modeling); } MoveShapeHandler.$inject = [ 'modeling' ]; MoveShapeHandler.prototype.execute = function(context) { var shape = context.shape, delta = context.delta, newParent = context.newParent || shape.parent, newParentIndex = context.newParentIndex, oldParent = shape.parent; context.oldBounds = pick(shape, [ 'x', 'y', 'width', 'height' ]); // save old parent in context context.oldParent = oldParent; context.oldParentIndex = remove(oldParent.children, shape); // add to new parent at position add(newParent.children, shape, newParentIndex); // update shape parent + position assign(shape, { parent: newParent, x: shape.x + delta.x, y: shape.y + delta.y }); return shape; }; MoveShapeHandler.prototype.postExecute = function(context) { var shape = context.shape, delta = context.delta, hints = context.hints; var modeling = this._modeling; if (hints.layout !== false) { forEach$1(shape.incoming, function(c) { modeling.layoutConnection(c, { connectionEnd: getMovedTargetAnchor(c, shape, delta) }); }); forEach$1(shape.outgoing, function(c) { modeling.layoutConnection(c, { connectionStart: getMovedSourceAnchor(c, shape, delta) }); }); } if (hints.recurse !== false) { this.moveChildren(context); } }; MoveShapeHandler.prototype.revert = function(context) { var shape = context.shape, oldParent = context.oldParent, oldParentIndex = context.oldParentIndex, delta = context.delta; // restore previous location in old parent add(oldParent.children, shape, oldParentIndex); // revert to old position and parent assign(shape, { parent: oldParent, x: shape.x - delta.x, y: shape.y - delta.y }); return shape; }; MoveShapeHandler.prototype.moveChildren = function(context) { var delta = context.delta, shape = context.shape; this._helper.moveRecursive(shape.children, delta, null); }; MoveShapeHandler.prototype.getNewParent = function(context) { return context.newParent || context.shape.parent; }; /** * Reconnect connection handler */ function ReconnectConnectionHandler(modeling) { this._modeling = modeling; } ReconnectConnectionHandler.$inject = [ 'modeling' ]; ReconnectConnectionHandler.prototype.execute = function(context) { var newSource = context.newSource, newTarget = context.newTarget, connection = context.connection, dockingOrPoints = context.dockingOrPoints; if (!newSource && !newTarget) { throw new Error('newSource or newTarget required'); } if (isArray$3(dockingOrPoints)) { context.oldWaypoints = connection.waypoints; connection.waypoints = dockingOrPoints; } if (newSource) { context.oldSource = connection.source; connection.source = newSource; } if (newTarget) { context.oldTarget = connection.target; connection.target = newTarget; } return connection; }; ReconnectConnectionHandler.prototype.postExecute = function(context) { var connection = context.connection, newSource = context.newSource, newTarget = context.newTarget, dockingOrPoints = context.dockingOrPoints, hints = context.hints || {}; var layoutConnectionHints = {}; if (hints.connectionStart) { layoutConnectionHints.connectionStart = hints.connectionStart; } if (hints.connectionEnd) { layoutConnectionHints.connectionEnd = hints.connectionEnd; } if (hints.layoutConnection === false) { return; } if (newSource && (!newTarget || hints.docking === 'source')) { layoutConnectionHints.connectionStart = layoutConnectionHints.connectionStart || getDocking(isArray$3(dockingOrPoints) ? dockingOrPoints[ 0 ] : dockingOrPoints); } if (newTarget && (!newSource || hints.docking === 'target')) { layoutConnectionHints.connectionEnd = layoutConnectionHints.connectionEnd || getDocking(isArray$3(dockingOrPoints) ? dockingOrPoints[ dockingOrPoints.length - 1 ] : dockingOrPoints); } if (hints.newWaypoints) { layoutConnectionHints.waypoints = hints.newWaypoints; } this._modeling.layoutConnection(connection, layoutConnectionHints); }; ReconnectConnectionHandler.prototype.revert = function(context) { var oldSource = context.oldSource, oldTarget = context.oldTarget, oldWaypoints = context.oldWaypoints, connection = context.connection; if (oldSource) { connection.source = oldSource; } if (oldTarget) { connection.target = oldTarget; } if (oldWaypoints) { connection.waypoints = oldWaypoints; } return connection; }; // helpers ////////// function getDocking(point) { return point.original || point; } /** * Replace shape by adding new shape and removing old shape. Incoming and outgoing connections will * be kept if possible. * * @class * @constructor * * @param {Modeling} modeling * @param {Rules} rules */ function ReplaceShapeHandler(modeling, rules) { this._modeling = modeling; this._rules = rules; } ReplaceShapeHandler.$inject = [ 'modeling', 'rules' ]; /** * Add new shape. * * @param {Object} context * @param {djs.model.Shape} context.oldShape * @param {Object} context.newData * @param {string} context.newData.type * @param {number} context.newData.x * @param {number} context.newData.y * @param {Object} [hints] */ ReplaceShapeHandler.prototype.preExecute = function(context) { var self = this, modeling = this._modeling, rules = this._rules; var oldShape = context.oldShape, newData = context.newData, hints = context.hints || {}, newShape; function canReconnect(source, target, connection) { return rules.allowed('connection.reconnect', { connection: connection, source: source, target: target }); } // (1) add new shape at given position var position = { x: newData.x, y: newData.y }; var oldBounds = { x: oldShape.x, y: oldShape.y, width: oldShape.width, height: oldShape.height }; newShape = context.newShape = context.newShape || self.createShape(newData, position, oldShape.parent, hints); // (2) update host if (oldShape.host) { modeling.updateAttachment(newShape, oldShape.host); } // (3) adopt all children from old shape var children; if (hints.moveChildren !== false) { children = oldShape.children.slice(); modeling.moveElements(children, { x: 0, y: 0 }, newShape, hints); } // (4) reconnect connections to new shape if possible var incoming = oldShape.incoming.slice(), outgoing = oldShape.outgoing.slice(); forEach$1(incoming, function(connection) { var source = connection.source, allowed = canReconnect(source, newShape, connection); if (allowed) { self.reconnectEnd( connection, newShape, getResizedTargetAnchor(connection, newShape, oldBounds), hints ); } }); forEach$1(outgoing, function(connection) { var target = connection.target, allowed = canReconnect(newShape, target, connection); if (allowed) { self.reconnectStart( connection, newShape, getResizedSourceAnchor(connection, newShape, oldBounds), hints ); } }); }; /** * Remove old shape. */ ReplaceShapeHandler.prototype.postExecute = function(context) { var oldShape = context.oldShape; this._modeling.removeShape(oldShape); }; ReplaceShapeHandler.prototype.execute = function(context) {}; ReplaceShapeHandler.prototype.revert = function(context) {}; ReplaceShapeHandler.prototype.createShape = function(shape, position, target, hints) { return this._modeling.createShape(shape, position, target, hints); }; ReplaceShapeHandler.prototype.reconnectStart = function(connection, newSource, dockingPoint, hints) { this._modeling.reconnectStart(connection, newSource, dockingPoint, hints); }; ReplaceShapeHandler.prototype.reconnectEnd = function(connection, newTarget, dockingPoint, hints) { this._modeling.reconnectEnd(connection, newTarget, dockingPoint, hints); }; /** * A handler that implements reversible resizing of shapes. * * @param {Modeling} modeling */ function ResizeShapeHandler(modeling) { this._modeling = modeling; } ResizeShapeHandler.$inject = [ 'modeling' ]; /** * { * shape: {....} * newBounds: { * width: 20, * height: 40, * x: 5, * y: 10 * } * * } */ ResizeShapeHandler.prototype.execute = function(context) { var shape = context.shape, newBounds = context.newBounds, minBounds = context.minBounds; if (newBounds.x === undefined || newBounds.y === undefined || newBounds.width === undefined || newBounds.height === undefined) { throw new Error('newBounds must have {x, y, width, height} properties'); } if (minBounds && (newBounds.width < minBounds.width || newBounds.height < minBounds.height)) { throw new Error('width and height cannot be less than minimum height and width'); } else if (!minBounds && newBounds.width < 10 || newBounds.height < 10) { throw new Error('width and height cannot be less than 10px'); } // save old bbox in context context.oldBounds = { width: shape.width, height: shape.height, x: shape.x, y: shape.y }; // update shape assign(shape, { width: newBounds.width, height: newBounds.height, x: newBounds.x, y: newBounds.y }); return shape; }; ResizeShapeHandler.prototype.postExecute = function(context) { var modeling = this._modeling; var shape = context.shape, oldBounds = context.oldBounds, hints = context.hints || {}; if (hints.layout === false) { return; } forEach$1(shape.incoming, function(c) { modeling.layoutConnection(c, { connectionEnd: getResizedTargetAnchor(c, shape, oldBounds) }); }); forEach$1(shape.outgoing, function(c) { modeling.layoutConnection(c, { connectionStart: getResizedSourceAnchor(c, shape, oldBounds) }); }); }; ResizeShapeHandler.prototype.revert = function(context) { var shape = context.shape, oldBounds = context.oldBounds; // restore previous bbox assign(shape, { width: oldBounds.width, height: oldBounds.height, x: oldBounds.x, y: oldBounds.y }); return shape; }; /** * Add or remove space by moving and resizing shapes and updating connection waypoints. */ function SpaceToolHandler(modeling) { this._modeling = modeling; } SpaceToolHandler.$inject = [ 'modeling' ]; SpaceToolHandler.prototype.preExecute = function(context) { var delta = context.delta, direction = context.direction, movingShapes = context.movingShapes, resizingShapes = context.resizingShapes, start = context.start, oldBounds = {}; // (1) move shapes this.moveShapes(movingShapes, delta); // (2a) save old bounds of resized shapes forEach$1(resizingShapes, function(shape) { oldBounds[shape.id] = getBounds(shape); }); // (2b) resize shapes this.resizeShapes(resizingShapes, delta, direction); // (3) update connection waypoints this.updateConnectionWaypoints( getWaypointsUpdatingConnections(movingShapes, resizingShapes), delta, direction, start, movingShapes, resizingShapes, oldBounds ); }; SpaceToolHandler.prototype.execute = function() {}; SpaceToolHandler.prototype.revert = function() {}; SpaceToolHandler.prototype.moveShapes = function(shapes, delta) { var self = this; forEach$1(shapes, function(element) { self._modeling.moveShape(element, delta, null, { autoResize: false, layout: false, recurse: false }); }); }; SpaceToolHandler.prototype.resizeShapes = function(shapes, delta, direction) { var self = this; forEach$1(shapes, function(shape) { var newBounds = resizeBounds(shape, direction, delta); self._modeling.resizeShape(shape, newBounds, null, { attachSupport: false, autoResize: false, layout: false }); }); }; /** * Update connections waypoints according to the rules: * 1. Both source and target are moved/resized => move waypoints by the delta * 2. Only one of source and target is moved/resized => re-layout connection with moved start/end */ SpaceToolHandler.prototype.updateConnectionWaypoints = function( connections, delta, direction, start, movingShapes, resizingShapes, oldBounds ) { var self = this, affectedShapes = movingShapes.concat(resizingShapes); forEach$1(connections, function(connection) { var source = connection.source, target = connection.target, waypoints = copyWaypoints(connection), axis = getAxisFromDirection(direction), layoutHints = { labelBehavior: false }; if (includes$1(affectedShapes, source) && includes$1(affectedShapes, target)) { // move waypoints waypoints = map(waypoints, function(waypoint) { if (shouldMoveWaypoint(waypoint, start, direction)) { // move waypoint waypoint[ axis ] = waypoint[ axis ] + delta[ axis ]; } if (waypoint.original && shouldMoveWaypoint(waypoint.original, start, direction)) { // move waypoint original waypoint.original[ axis ] = waypoint.original[ axis ] + delta[ axis ]; } return waypoint; }); self._modeling.updateWaypoints(connection, waypoints, { labelBehavior: false }); } else if (includes$1(affectedShapes, source) || includes$1(affectedShapes, target)) { // re-layout connection with moved start/end if (includes$1(movingShapes, source)) { layoutHints.connectionStart = getMovedSourceAnchor(connection, source, delta); } else if (includes$1(movingShapes, target)) { layoutHints.connectionEnd = getMovedTargetAnchor(connection, target, delta); } else if (includes$1(resizingShapes, source)) { layoutHints.connectionStart = getResizedSourceAnchor( connection, source, oldBounds[source.id] ); } else if (includes$1(resizingShapes, target)) { layoutHints.connectionEnd = getResizedTargetAnchor( connection, target, oldBounds[target.id] ); } self._modeling.layoutConnection(connection, layoutHints); } }); }; // helpers ////////// function copyWaypoint(waypoint) { return assign({}, waypoint); } function copyWaypoints(connection) { return map(connection.waypoints, function(waypoint) { waypoint = copyWaypoint(waypoint); if (waypoint.original) { waypoint.original = copyWaypoint(waypoint.original); } return waypoint; }); } function getAxisFromDirection(direction) { switch (direction) { case 'n': return 'y'; case 'w': return 'x'; case 's': return 'y'; case 'e': return 'x'; } } function shouldMoveWaypoint(waypoint, start, direction) { var relevantAxis = getAxisFromDirection(direction); if (/e|s/.test(direction)) { return waypoint[ relevantAxis ] > start; } else if (/n|w/.test(direction)) { return waypoint[ relevantAxis ] < start; } } function includes$1(array, item) { return array.indexOf(item) !== -1; } function getBounds(shape) { return { x: shape.x, y: shape.y, height: shape.height, width: shape.width }; } /** * A handler that toggles the collapsed state of an element * and the visibility of all its children. * * @param {Modeling} modeling */ function ToggleShapeCollapseHandler(modeling) { this._modeling = modeling; } ToggleShapeCollapseHandler.$inject = [ 'modeling' ]; ToggleShapeCollapseHandler.prototype.execute = function(context) { var shape = context.shape, children = shape.children; // recursively remember previous visibility of children context.oldChildrenVisibility = getElementsVisibilityRecursive(children); // toggle state shape.collapsed = !shape.collapsed; // recursively hide/show children var result = setHiddenRecursive(children, shape.collapsed); return [ shape ].concat(result); }; ToggleShapeCollapseHandler.prototype.revert = function(context) { var shape = context.shape, oldChildrenVisibility = context.oldChildrenVisibility; var children = shape.children; // recursively set old visability of children var result = restoreVisibilityRecursive(children, oldChildrenVisibility); // retoggle state shape.collapsed = !shape.collapsed; return [ shape ].concat(result); }; // helpers ////////////////////// /** * Return a map { elementId -> hiddenState}. * * @param {Array} elements * * @return {Object} */ function getElementsVisibilityRecursive(elements) { var result = {}; forEach$1(elements, function(element) { result[element.id] = element.hidden; if (element.children) { result = assign({}, result, getElementsVisibilityRecursive(element.children)); } }); return result; } function setHiddenRecursive(elements, newHidden) { var result = []; forEach$1(elements, function(element) { element.hidden = newHidden; result = result.concat(element); if (element.children) { result = result.concat(setHiddenRecursive(element.children, element.collapsed || newHidden)); } }); return result; } function restoreVisibilityRecursive(elements, lastState) { var result = []; forEach$1(elements, function(element) { element.hidden = lastState[element.id]; result = result.concat(element); if (element.children) { result = result.concat(restoreVisibilityRecursive(element.children, lastState)); } }); return result; } /** * A handler that implements reversible attaching/detaching of shapes. */ function UpdateAttachmentHandler(modeling) { this._modeling = modeling; } UpdateAttachmentHandler.$inject = [ 'modeling' ]; UpdateAttachmentHandler.prototype.execute = function(context) { var shape = context.shape, newHost = context.newHost, oldHost = shape.host; // (0) detach from old host context.oldHost = oldHost; context.attacherIdx = removeAttacher(oldHost, shape); // (1) attach to new host addAttacher(newHost, shape); // (2) update host shape.host = newHost; return shape; }; UpdateAttachmentHandler.prototype.revert = function(context) { var shape = context.shape, newHost = context.newHost, oldHost = context.oldHost, attacherIdx = context.attacherIdx; // (2) update host shape.host = oldHost; // (1) attach to new host removeAttacher(newHost, shape); // (0) detach from old host addAttacher(oldHost, shape, attacherIdx); return shape; }; function removeAttacher(host, attacher) { // remove attacher from host return remove(host && host.attachers, attacher); } function addAttacher(host, attacher, idx) { if (!host) { return; } var attachers = host.attachers; if (!attachers) { host.attachers = attachers = []; } add(attachers, attacher, idx); } function UpdateWaypointsHandler() { } UpdateWaypointsHandler.prototype.execute = function(context) { var connection = context.connection, newWaypoints = context.newWaypoints; context.oldWaypoints = connection.waypoints; connection.waypoints = newWaypoints; return connection; }; UpdateWaypointsHandler.prototype.revert = function(context) { var connection = context.connection, oldWaypoints = context.oldWaypoints; connection.waypoints = oldWaypoints; return connection; }; /** * The basic modeling entry point. * * @param {EventBus} eventBus * @param {ElementFactory} elementFactory * @param {CommandStack} commandStack */ function Modeling$1(eventBus, elementFactory, commandStack) { this._eventBus = eventBus; this._elementFactory = elementFactory; this._commandStack = commandStack; var self = this; eventBus.on('diagram.init', function() { // register modeling handlers self.registerHandlers(commandStack); }); } Modeling$1.$inject = [ 'eventBus', 'elementFactory', 'commandStack' ]; Modeling$1.prototype.getHandlers = function() { return { 'shape.append': AppendShapeHandler, 'shape.create': CreateShapeHandler, 'shape.delete': DeleteShapeHandler, 'shape.move': MoveShapeHandler, 'shape.resize': ResizeShapeHandler, 'shape.replace': ReplaceShapeHandler, 'shape.toggleCollapse': ToggleShapeCollapseHandler, 'spaceTool': SpaceToolHandler, 'label.create': CreateLabelHandler, 'connection.create': CreateConnectionHandler, 'connection.delete': DeleteConnectionHandler, 'connection.move': MoveConnectionHandler, 'connection.layout': LayoutConnectionHandler, 'connection.updateWaypoints': UpdateWaypointsHandler, 'connection.reconnect': ReconnectConnectionHandler, 'elements.create': CreateElementsHandler, 'elements.move': MoveElementsHandler, 'elements.delete': DeleteElementsHandler, 'elements.distribute': DistributeElements, 'elements.align': AlignElements, 'element.updateAttachment': UpdateAttachmentHandler }; }; /** * Register handlers with the command stack * * @param {CommandStack} commandStack */ Modeling$1.prototype.registerHandlers = function(commandStack) { forEach$1(this.getHandlers(), function(handler, id) { commandStack.registerHandler(id, handler); }); }; // modeling helpers ////////////////////// Modeling$1.prototype.moveShape = function(shape, delta, newParent, newParentIndex, hints) { if (typeof newParentIndex === 'object') { hints = newParentIndex; newParentIndex = null; } var context = { shape: shape, delta: delta, newParent: newParent, newParentIndex: newParentIndex, hints: hints || {} }; this._commandStack.execute('shape.move', context); }; /** * Update the attachment of the given shape. * * @param {djs.mode.Base} shape * @param {djs.model.Base} [newHost] */ Modeling$1.prototype.updateAttachment = function(shape, newHost) { var context = { shape: shape, newHost: newHost }; this._commandStack.execute('element.updateAttachment', context); }; /** * Move a number of shapes to a new target, either setting it as * the new parent or attaching it. * * @param {Array} shapes * @param {Point} delta * @param {djs.model.Base} [target] * @param {Object} [hints] * @param {boolean} [hints.attach=false] */ Modeling$1.prototype.moveElements = function(shapes, delta, target, hints) { hints = hints || {}; var attach = hints.attach; var newParent = target, newHost; if (attach === true) { newHost = target; newParent = target.parent; } else if (attach === false) { newHost = null; } var context = { shapes: shapes, delta: delta, newParent: newParent, newHost: newHost, hints: hints }; this._commandStack.execute('elements.move', context); }; Modeling$1.prototype.moveConnection = function(connection, delta, newParent, newParentIndex, hints) { if (typeof newParentIndex === 'object') { hints = newParentIndex; newParentIndex = undefined; } var context = { connection: connection, delta: delta, newParent: newParent, newParentIndex: newParentIndex, hints: hints || {} }; this._commandStack.execute('connection.move', context); }; Modeling$1.prototype.layoutConnection = function(connection, hints) { var context = { connection: connection, hints: hints || {} }; this._commandStack.execute('connection.layout', context); }; /** * Create connection. * * @param {djs.model.Base} source * @param {djs.model.Base} target * @param {number} [parentIndex] * @param {Object|djs.model.Connection} connection * @param {djs.model.Base} parent * @param {Object} hints * * @return {djs.model.Connection} the created connection. */ Modeling$1.prototype.createConnection = function(source, target, parentIndex, connection, parent, hints) { if (typeof parentIndex === 'object') { hints = parent; parent = connection; connection = parentIndex; parentIndex = undefined; } connection = this._create('connection', connection); var context = { source: source, target: target, parent: parent, parentIndex: parentIndex, connection: connection, hints: hints }; this._commandStack.execute('connection.create', context); return context.connection; }; /** * Create a shape at the specified position. * * @param {djs.model.Shape|Object} shape * @param {Point} position * @param {djs.model.Shape|djs.model.Root} target * @param {number} [parentIndex] position in parents children list * @param {Object} [hints] * @param {boolean} [hints.attach] whether to attach to target or become a child * * @return {djs.model.Shape} the created shape */ Modeling$1.prototype.createShape = function(shape, position, target, parentIndex, hints) { if (typeof parentIndex !== 'number') { hints = parentIndex; parentIndex = undefined; } hints = hints || {}; var attach = hints.attach, parent, host; shape = this._create('shape', shape); if (attach) { parent = target.parent; host = target; } else { parent = target; } var context = { position: position, shape: shape, parent: parent, parentIndex: parentIndex, host: host, hints: hints }; this._commandStack.execute('shape.create', context); return context.shape; }; Modeling$1.prototype.createElements = function(elements, position, parent, parentIndex, hints) { if (!isArray$3(elements)) { elements = [ elements ]; } if (typeof parentIndex !== 'number') { hints = parentIndex; parentIndex = undefined; } hints = hints || {}; var context = { position: position, elements: elements, parent: parent, parentIndex: parentIndex, hints: hints }; this._commandStack.execute('elements.create', context); return context.elements; }; Modeling$1.prototype.createLabel = function(labelTarget, position, label, parent) { label = this._create('label', label); var context = { labelTarget: labelTarget, position: position, parent: parent || labelTarget.parent, shape: label }; this._commandStack.execute('label.create', context); return context.shape; }; /** * Append shape to given source, drawing a connection * between source and the newly created shape. * * @param {djs.model.Shape} source * @param {djs.model.Shape|Object} shape * @param {Point} position * @param {djs.model.Shape} target * @param {Object} [hints] * @param {boolean} [hints.attach] * @param {djs.model.Connection|Object} [hints.connection] * @param {djs.model.Base} [hints.connectionParent] * * @return {djs.model.Shape} the newly created shape */ Modeling$1.prototype.appendShape = function(source, shape, position, target, hints) { hints = hints || {}; shape = this._create('shape', shape); var context = { source: source, position: position, target: target, shape: shape, connection: hints.connection, connectionParent: hints.connectionParent, hints: hints }; this._commandStack.execute('shape.append', context); return context.shape; }; Modeling$1.prototype.removeElements = function(elements) { var context = { elements: elements }; this._commandStack.execute('elements.delete', context); }; Modeling$1.prototype.distributeElements = function(groups, axis, dimension) { var context = { groups: groups, axis: axis, dimension: dimension }; this._commandStack.execute('elements.distribute', context); }; Modeling$1.prototype.removeShape = function(shape, hints) { var context = { shape: shape, hints: hints || {} }; this._commandStack.execute('shape.delete', context); }; Modeling$1.prototype.removeConnection = function(connection, hints) { var context = { connection: connection, hints: hints || {} }; this._commandStack.execute('connection.delete', context); }; Modeling$1.prototype.replaceShape = function(oldShape, newShape, hints) { var context = { oldShape: oldShape, newData: newShape, hints: hints || {} }; this._commandStack.execute('shape.replace', context); return context.newShape; }; Modeling$1.prototype.alignElements = function(elements, alignment) { var context = { elements: elements, alignment: alignment }; this._commandStack.execute('elements.align', context); }; Modeling$1.prototype.resizeShape = function(shape, newBounds, minBounds, hints) { var context = { shape: shape, newBounds: newBounds, minBounds: minBounds, hints: hints }; this._commandStack.execute('shape.resize', context); }; Modeling$1.prototype.createSpace = function(movingShapes, resizingShapes, delta, direction, start) { var context = { delta: delta, direction: direction, movingShapes: movingShapes, resizingShapes: resizingShapes, start: start }; this._commandStack.execute('spaceTool', context); }; Modeling$1.prototype.updateWaypoints = function(connection, newWaypoints, hints) { var context = { connection: connection, newWaypoints: newWaypoints, hints: hints || {} }; this._commandStack.execute('connection.updateWaypoints', context); }; Modeling$1.prototype.reconnect = function(connection, source, target, dockingOrPoints, hints) { var context = { connection: connection, newSource: source, newTarget: target, dockingOrPoints: dockingOrPoints, hints: hints || {} }; this._commandStack.execute('connection.reconnect', context); }; Modeling$1.prototype.reconnectStart = function(connection, newSource, dockingOrPoints, hints) { if (!hints) { hints = {}; } this.reconnect(connection, newSource, connection.target, dockingOrPoints, assign(hints, { docking: 'source' })); }; Modeling$1.prototype.reconnectEnd = function(connection, newTarget, dockingOrPoints, hints) { if (!hints) { hints = {}; } this.reconnect(connection, connection.source, newTarget, dockingOrPoints, assign(hints, { docking: 'target' })); }; Modeling$1.prototype.connect = function(source, target, attrs, hints) { return this.createConnection(source, target, attrs || {}, source.parent, hints); }; Modeling$1.prototype._create = function(type, attrs) { if (attrs instanceof Base$1) { return attrs; } else { return this._elementFactory.create(type, attrs); } }; Modeling$1.prototype.toggleCollapse = function(shape, hints) { var context = { shape: shape, hints: hints || {} }; this._commandStack.execute('shape.toggleCollapse', context); }; function UpdateModdlePropertiesHandler(elementRegistry) { this._elementRegistry = elementRegistry; } UpdateModdlePropertiesHandler.$inject = [ 'elementRegistry' ]; UpdateModdlePropertiesHandler.prototype.execute = function(context) { var element = context.element, moddleElement = context.moddleElement, properties = context.properties; if (!moddleElement) { throw new Error(' required'); } // TODO(nikku): we need to ensure that ID properties // are properly registered / unregistered via // this._moddle.ids.assigned(id) var changed = context.changed || this.getVisualReferences(moddleElement).concat(element); var oldProperties = context.oldProperties || getModdleProperties(moddleElement, keys(properties)); setModdleProperties(moddleElement, properties); context.oldProperties = oldProperties; context.changed = changed; return changed; }; UpdateModdlePropertiesHandler.prototype.revert = function(context) { var oldProperties = context.oldProperties, moddleElement = context.moddleElement, changed = context.changed; setModdleProperties(moddleElement, oldProperties); return changed; }; /** * Return visual references of given moddle element within the diagram. * * @param {ModdleElement} moddleElement * * @return {Array} */ UpdateModdlePropertiesHandler.prototype.getVisualReferences = function(moddleElement) { var elementRegistry = this._elementRegistry; if (is$1(moddleElement, 'bpmn:DataObject')) { return getAllDataObjectReferences(moddleElement, elementRegistry); } return []; }; // helpers ///////////////// function getModdleProperties(moddleElement, propertyNames) { return reduce(propertyNames, function(result, key) { result[key] = moddleElement.get(key); return result; }, {}); } function setModdleProperties(moddleElement, properties) { forEach$1(properties, function(value, key) { moddleElement.set(key, value); }); } function getAllDataObjectReferences(dataObject, elementRegistry) { return elementRegistry.filter(function(element) { return ( is$1(element, 'bpmn:DataObjectReference') && getBusinessObject(element).dataObjectRef === dataObject ); }); } var DEFAULT_FLOW = 'default', ID = 'id', DI = 'di'; var NULL_DIMENSIONS$1 = { width: 0, height: 0 }; /** * A handler that implements a BPMN 2.0 property update. * * This should be used to set simple properties on elements with * an underlying BPMN business object. * * Use respective diagram-js provided handlers if you would * like to perform automated modeling. */ function UpdatePropertiesHandler( elementRegistry, moddle, translate, modeling, textRenderer) { this._elementRegistry = elementRegistry; this._moddle = moddle; this._translate = translate; this._modeling = modeling; this._textRenderer = textRenderer; } UpdatePropertiesHandler.$inject = [ 'elementRegistry', 'moddle', 'translate', 'modeling', 'textRenderer' ]; // api ////////////////////// /** * Updates a BPMN element with a list of new properties * * @param {Object} context * @param {djs.model.Base} context.element the element to update * @param {Object} context.properties a list of properties to set on the element's * businessObject (the BPMN model element) * * @return {Array} the updated element */ UpdatePropertiesHandler.prototype.execute = function(context) { var element = context.element, changed = [ element ], translate = this._translate; if (!element) { throw new Error(translate('element required')); } var elementRegistry = this._elementRegistry, ids = this._moddle.ids; var businessObject = element.businessObject, properties = unwrapBusinessObjects(context.properties), oldProperties = context.oldProperties || getProperties(element, properties); if (isIdChange(properties, businessObject)) { ids.unclaim(businessObject[ID]); elementRegistry.updateId(element, properties[ID]); ids.claim(properties[ID], businessObject); } // correctly indicate visual changes on default flow updates if (DEFAULT_FLOW in properties) { if (properties[DEFAULT_FLOW]) { changed.push(elementRegistry.get(properties[DEFAULT_FLOW].id)); } if (businessObject[DEFAULT_FLOW]) { changed.push(elementRegistry.get(businessObject[DEFAULT_FLOW].id)); } } // update properties setProperties(element, properties); // store old values context.oldProperties = oldProperties; context.changed = changed; // indicate changed on objects affected by the update return changed; }; UpdatePropertiesHandler.prototype.postExecute = function(context) { var element = context.element, label = element.label; var text = label && getBusinessObject(label).name; if (!text) { return; } // get layouted text bounds and resize external // external label accordingly var newLabelBounds = this._textRenderer.getExternalLabelBounds(label, text); this._modeling.resizeShape(label, newLabelBounds, NULL_DIMENSIONS$1); }; /** * Reverts the update on a BPMN elements properties. * * @param {Object} context * * @return {djs.model.Base} the updated element */ UpdatePropertiesHandler.prototype.revert = function(context) { var element = context.element, properties = context.properties, oldProperties = context.oldProperties, businessObject = element.businessObject, elementRegistry = this._elementRegistry, ids = this._moddle.ids; // update properties setProperties(element, oldProperties); if (isIdChange(properties, businessObject)) { ids.unclaim(properties[ID]); elementRegistry.updateId(element, oldProperties[ID]); ids.claim(oldProperties[ID], businessObject); } return context.changed; }; function isIdChange(properties, businessObject) { return ID in properties && properties[ID] !== businessObject[ID]; } function getProperties(element, properties) { var propertyNames = keys(properties), businessObject = element.businessObject, di = getDi(element); return reduce(propertyNames, function(result, key) { // handle DI separately if (key !== DI) { result[key] = businessObject.get(key); } else { result[key] = getDiProperties(di, keys(properties.di)); } return result; }, {}); } function getDiProperties(di, propertyNames) { return reduce(propertyNames, function(result, key) { result[key] = di && di.get(key); return result; }, {}); } function setProperties(element, properties) { var businessObject = element.businessObject, di = getDi(element); forEach$1(properties, function(value, key) { if (key !== DI) { businessObject.set(key, value); } else { // only update, if di exists if (di) { setDiProperties(di, value); } } }); } function setDiProperties(di, properties) { forEach$1(properties, function(value, key) { di.set(key, value); }); } var referencePropertyNames = [ 'default' ]; /** * Make sure we unwrap the actual business object * behind diagram element that may have been * passed as arguments. * * @param {Object} properties * * @return {Object} unwrappedProps */ function unwrapBusinessObjects(properties) { var unwrappedProps = assign({}, properties); referencePropertyNames.forEach(function(name) { if (name in properties) { unwrappedProps[name] = getBusinessObject(unwrappedProps[name]); } }); return unwrappedProps; } function UpdateCanvasRootHandler(canvas, modeling) { this._canvas = canvas; this._modeling = modeling; } UpdateCanvasRootHandler.$inject = [ 'canvas', 'modeling' ]; UpdateCanvasRootHandler.prototype.execute = function(context) { var canvas = this._canvas; var newRoot = context.newRoot, newRootBusinessObject = newRoot.businessObject, oldRoot = canvas.getRootElement(), oldRootBusinessObject = oldRoot.businessObject, bpmnDefinitions = oldRootBusinessObject.$parent, diPlane = getDi(oldRoot); // (1) replace process old <> new root canvas.setRootElement(newRoot); canvas.removeRootElement(oldRoot); // (2) update root elements add(bpmnDefinitions.rootElements, newRootBusinessObject); newRootBusinessObject.$parent = bpmnDefinitions; remove(bpmnDefinitions.rootElements, oldRootBusinessObject); oldRootBusinessObject.$parent = null; // (3) wire di oldRoot.di = null; diPlane.bpmnElement = newRootBusinessObject; newRoot.di = diPlane; context.oldRoot = oldRoot; // TODO(nikku): return changed elements? // return [ newRoot, oldRoot ]; }; UpdateCanvasRootHandler.prototype.revert = function(context) { var canvas = this._canvas; var newRoot = context.newRoot, newRootBusinessObject = newRoot.businessObject, oldRoot = context.oldRoot, oldRootBusinessObject = oldRoot.businessObject, bpmnDefinitions = newRootBusinessObject.$parent, diPlane = getDi(newRoot); // (1) replace process old <> new root canvas.setRootElement(oldRoot); canvas.removeRootElement(newRoot); // (2) update root elements remove(bpmnDefinitions.rootElements, newRootBusinessObject); newRootBusinessObject.$parent = null; add(bpmnDefinitions.rootElements, oldRootBusinessObject); oldRootBusinessObject.$parent = bpmnDefinitions; // (3) wire di newRoot.di = null; diPlane.bpmnElement = oldRootBusinessObject; oldRoot.di = diPlane; // TODO(nikku): return changed elements? // return [ newRoot, oldRoot ]; }; /** * A handler that allows us to add a new lane * above or below an existing one. * * @param {Modeling} modeling * @param {SpaceTool} spaceTool */ function AddLaneHandler(modeling, spaceTool) { this._modeling = modeling; this._spaceTool = spaceTool; } AddLaneHandler.$inject = [ 'modeling', 'spaceTool' ]; AddLaneHandler.prototype.preExecute = function(context) { var spaceTool = this._spaceTool, modeling = this._modeling; var shape = context.shape, location = context.location; var lanesRoot = getLanesRoot(shape); var isRoot = lanesRoot === shape, laneParent = isRoot ? shape : shape.parent; var existingChildLanes = getChildLanes(laneParent); // (0) add a lane if we currently got none and are adding to root if (!existingChildLanes.length) { modeling.createShape({ type: 'bpmn:Lane' }, { x: shape.x + LANE_INDENTATION, y: shape.y, width: shape.width - LANE_INDENTATION, height: shape.height }, laneParent); } // (1) collect affected elements to create necessary space var allAffected = []; eachElement(lanesRoot, function(element) { allAffected.push(element); // handle element labels in the diagram root if (element.label) { allAffected.push(element.label); } if (element === shape) { return []; } return filter(element.children, function(c) { return c !== shape; }); }); var offset = location === 'top' ? -120 : 120, lanePosition = location === 'top' ? shape.y : shape.y + shape.height, spacePos = lanePosition + (location === 'top' ? 10 : -10), direction = location === 'top' ? 'n' : 's'; var adjustments = spaceTool.calculateAdjustments(allAffected, 'y', offset, spacePos); spaceTool.makeSpace( adjustments.movingShapes, adjustments.resizingShapes, { x: 0, y: offset }, direction, spacePos ); // (2) create new lane at open space context.newLane = modeling.createShape({ type: 'bpmn:Lane' }, { x: shape.x + (isRoot ? LANE_INDENTATION : 0), y: lanePosition - (location === 'top' ? 120 : 0), width: shape.width - (isRoot ? LANE_INDENTATION : 0), height: 120 }, laneParent); }; /** * A handler that splits a lane into a number of sub-lanes, * creating new sub lanes, if necessary. * * @param {Modeling} modeling */ function SplitLaneHandler(modeling, translate) { this._modeling = modeling; this._translate = translate; } SplitLaneHandler.$inject = [ 'modeling', 'translate' ]; SplitLaneHandler.prototype.preExecute = function(context) { var modeling = this._modeling, translate = this._translate; var shape = context.shape, newLanesCount = context.count; var childLanes = getChildLanes(shape), existingLanesCount = childLanes.length; if (existingLanesCount > newLanesCount) { throw new Error(translate('more than {count} child lanes', { count: newLanesCount })); } var newLanesHeight = Math.round(shape.height / newLanesCount); // Iterate from top to bottom in child lane order, // resizing existing lanes and creating new ones // so that they split the parent proportionally. // // Due to rounding related errors, the bottom lane // needs to take up all the remaining space. var laneY, laneHeight, laneBounds, newLaneAttrs, idx; for (idx = 0; idx < newLanesCount; idx++) { laneY = shape.y + idx * newLanesHeight; // if bottom lane if (idx === newLanesCount - 1) { laneHeight = shape.height - (newLanesHeight * idx); } else { laneHeight = newLanesHeight; } laneBounds = { x: shape.x + LANE_INDENTATION, y: laneY, width: shape.width - LANE_INDENTATION, height: laneHeight }; if (idx < existingLanesCount) { // resize existing lane modeling.resizeShape(childLanes[idx], laneBounds); } else { // create a new lane at position newLaneAttrs = { type: 'bpmn:Lane' }; modeling.createShape(newLaneAttrs, laneBounds, shape); } } }; /** * A handler that resizes a lane. * * @param {Modeling} modeling */ function ResizeLaneHandler(modeling, spaceTool) { this._modeling = modeling; this._spaceTool = spaceTool; } ResizeLaneHandler.$inject = [ 'modeling', 'spaceTool' ]; ResizeLaneHandler.prototype.preExecute = function(context) { var shape = context.shape, newBounds = context.newBounds, balanced = context.balanced; if (balanced !== false) { this.resizeBalanced(shape, newBounds); } else { this.resizeSpace(shape, newBounds); } }; /** * Resize balanced, adjusting next / previous lane sizes. * * @param {djs.model.Shape} shape * @param {Bounds} newBounds */ ResizeLaneHandler.prototype.resizeBalanced = function(shape, newBounds) { var modeling = this._modeling; var resizeNeeded = computeLanesResize(shape, newBounds); // resize the lane modeling.resizeShape(shape, newBounds); // resize other lanes as needed resizeNeeded.forEach(function(r) { modeling.resizeShape(r.shape, r.newBounds); }); }; /** * Resize, making actual space and moving below / above elements. * * @param {djs.model.Shape} shape * @param {Bounds} newBounds */ ResizeLaneHandler.prototype.resizeSpace = function(shape, newBounds) { var spaceTool = this._spaceTool; var shapeTrbl = asTRBL(shape), newTrbl = asTRBL(newBounds); var trblDiff = substractTRBL(newTrbl, shapeTrbl); var lanesRoot = getLanesRoot(shape); var allAffected = [], allLanes = []; eachElement(lanesRoot, function(element) { allAffected.push(element); if (is$1(element, 'bpmn:Lane') || is$1(element, 'bpmn:Participant')) { allLanes.push(element); } return element.children; }); var change, spacePos, direction, offset, adjustments; if (trblDiff.bottom || trblDiff.top) { change = trblDiff.bottom || trblDiff.top; spacePos = shape.y + (trblDiff.bottom ? shape.height : 0) + (trblDiff.bottom ? -10 : 10); direction = trblDiff.bottom ? 's' : 'n'; offset = trblDiff.top > 0 || trblDiff.bottom < 0 ? -change : change; adjustments = spaceTool.calculateAdjustments(allAffected, 'y', offset, spacePos); spaceTool.makeSpace(adjustments.movingShapes, adjustments.resizingShapes, { x: 0, y: change }, direction); } if (trblDiff.left || trblDiff.right) { change = trblDiff.right || trblDiff.left; spacePos = shape.x + (trblDiff.right ? shape.width : 0) + (trblDiff.right ? -10 : 100); direction = trblDiff.right ? 'e' : 'w'; offset = trblDiff.left > 0 || trblDiff.right < 0 ? -change : change; adjustments = spaceTool.calculateAdjustments(allLanes, 'x', offset, spacePos); spaceTool.makeSpace(adjustments.movingShapes, adjustments.resizingShapes, { x: change, y: 0 }, direction); } }; var FLOW_NODE_REFS_ATTR = 'flowNodeRef', LANES_ATTR = 'lanes'; /** * A handler that updates lane refs on changed elements */ function UpdateFlowNodeRefsHandler(elementRegistry) { this._elementRegistry = elementRegistry; } UpdateFlowNodeRefsHandler.$inject = [ 'elementRegistry' ]; UpdateFlowNodeRefsHandler.prototype.computeUpdates = function(flowNodeShapes, laneShapes) { var handledNodes = []; var updates = []; var participantCache = {}; var allFlowNodeShapes = []; function isInLaneShape(element, laneShape) { var laneTrbl = asTRBL(laneShape); var elementMid = { x: element.x + element.width / 2, y: element.y + element.height / 2 }; return elementMid.x > laneTrbl.left && elementMid.x < laneTrbl.right && elementMid.y > laneTrbl.top && elementMid.y < laneTrbl.bottom; } function addFlowNodeShape(flowNodeShape) { if (handledNodes.indexOf(flowNodeShape) === -1) { allFlowNodeShapes.push(flowNodeShape); handledNodes.push(flowNodeShape); } } function getAllLaneShapes(flowNodeShape) { var root = getLanesRoot(flowNodeShape); if (!participantCache[root.id]) { participantCache[root.id] = collectLanes(root); } return participantCache[root.id]; } function getNewLanes(flowNodeShape) { if (!flowNodeShape.parent) { return []; } var allLaneShapes = getAllLaneShapes(flowNodeShape); return allLaneShapes.filter(function(l) { return isInLaneShape(flowNodeShape, l); }).map(function(shape) { return shape.businessObject; }); } laneShapes.forEach(function(laneShape) { var root = getLanesRoot(laneShape); if (!root || handledNodes.indexOf(root) !== -1) { return; } var children = root.children.filter(function(c) { return is$1(c, 'bpmn:FlowNode'); }); children.forEach(addFlowNodeShape); handledNodes.push(root); }); flowNodeShapes.forEach(addFlowNodeShape); allFlowNodeShapes.forEach(function(flowNodeShape) { var flowNode = flowNodeShape.businessObject; var lanes = flowNode.get(LANES_ATTR), remove = lanes.slice(), add = getNewLanes(flowNodeShape); updates.push({ flowNode: flowNode, remove: remove, add: add }); }); laneShapes.forEach(function(laneShape) { var lane = laneShape.businessObject; // lane got removed XX-) if (!laneShape.parent) { lane.get(FLOW_NODE_REFS_ATTR).forEach(function(flowNode) { updates.push({ flowNode: flowNode, remove: [ lane ], add: [] }); }); } }); return updates; }; UpdateFlowNodeRefsHandler.prototype.execute = function(context) { var updates = context.updates; if (!updates) { updates = context.updates = this.computeUpdates(context.flowNodeShapes, context.laneShapes); } updates.forEach(function(update) { var flowNode = update.flowNode, lanes = flowNode.get(LANES_ATTR); // unwire old update.remove.forEach(function(oldLane) { remove(lanes, oldLane); remove(oldLane.get(FLOW_NODE_REFS_ATTR), flowNode); }); // wire new update.add.forEach(function(newLane) { add(lanes, newLane); add(newLane.get(FLOW_NODE_REFS_ATTR), flowNode); }); }); // TODO(nikku): return changed elements // return [ ... ]; }; UpdateFlowNodeRefsHandler.prototype.revert = function(context) { var updates = context.updates; updates.forEach(function(update) { var flowNode = update.flowNode, lanes = flowNode.get(LANES_ATTR); // unwire new update.add.forEach(function(newLane) { remove(lanes, newLane); remove(newLane.get(FLOW_NODE_REFS_ATTR), flowNode); }); // wire old update.remove.forEach(function(oldLane) { add(lanes, oldLane); add(oldLane.get(FLOW_NODE_REFS_ATTR), flowNode); }); }); // TODO(nikku): return changed elements // return [ ... ]; }; function IdClaimHandler(moddle) { this._moddle = moddle; } IdClaimHandler.$inject = [ 'moddle' ]; IdClaimHandler.prototype.execute = function(context) { var ids = this._moddle.ids, id = context.id, element = context.element, claiming = context.claiming; if (claiming) { ids.claim(id, element); } else { ids.unclaim(id); } }; /** * Command revert implementation. */ IdClaimHandler.prototype.revert = function(context) { var ids = this._moddle.ids, id = context.id, element = context.element, claiming = context.claiming; if (claiming) { ids.unclaim(id); } else { ids.claim(id, element); } }; var DEFAULT_COLORS = { fill: undefined, stroke: undefined }; function SetColorHandler(commandStack) { this._commandStack = commandStack; this._normalizeColor = function(color) { // Remove color for falsy values. if (!color) { return undefined; } if (isString(color)) { var hexColor = colorToHex(color); if (hexColor) { return hexColor; } } throw new Error('invalid color value: ' + color); }; } SetColorHandler.$inject = [ 'commandStack' ]; SetColorHandler.prototype.postExecute = function(context) { var elements = context.elements, colors = context.colors || DEFAULT_COLORS; var self = this; var di = {}; if ('fill' in colors) { assign(di, { 'background-color': this._normalizeColor(colors.fill) }); } if ('stroke' in colors) { assign(di, { 'border-color': this._normalizeColor(colors.stroke) }); } forEach$1(elements, function(element) { var assignedDi = isConnection$3(element) ? pick(di, [ 'border-color' ]) : di; // TODO @barmac: remove once we drop bpmn.io properties ensureLegacySupport(assignedDi); if (isLabel$6(element)) { // set label colors as bpmndi:BPMNLabel#color self._commandStack.execute('element.updateModdleProperties', { element: element, moddleElement: getDi(element).label, properties: { color: di['border-color'] } }); } else { // set colors bpmndi:BPMNEdge or bpmndi:BPMNShape self._commandStack.execute('element.updateProperties', { element: element, properties: { di: assignedDi } }); } }); }; /** * Convert color from rgb(a)/hsl to hex. Returns `null` for unknown color names and for colors * with alpha less than 1.0. This depends on `` serialization of the `context.fillStyle`. * Cf. https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-fillstyle * * @example * ```js * var color = 'fuchsia'; * console.log(colorToHex(color)); * // "#ff00ff" * color = 'rgba(1,2,3,0.4)'; * console.log(colorToHex(color)); * // null * ``` * * @param {string} color * @returns {string|null} */ function colorToHex(color) { var context = document.createElement('canvas').getContext('2d'); // (0) Start with transparent to account for browser default values. context.fillStyle = 'transparent'; // (1) Assign color so that it's serialized. context.fillStyle = color; // (2) Return null for non-hex serialization result. return /^#[0-9a-fA-F]{6}$/.test(context.fillStyle) ? context.fillStyle : null; } function isConnection$3(element) { return !!element.waypoints; } /** * Add legacy properties if required. * @param {{ 'border-color': string?, 'background-color': string? }} di */ function ensureLegacySupport(di) { if ('border-color' in di) { di.stroke = di['border-color']; } if ('background-color' in di) { di.fill = di['background-color']; } } var NULL_DIMENSIONS = { width: 0, height: 0 }; /** * A handler that updates the text of a BPMN element. */ function UpdateLabelHandler(modeling, textRenderer, bpmnFactory) { /** * Creates an empty `diLabel` attribute for embedded labels. * * @param {djs.model.Base} element * @param {string} text */ function ensureInternalLabelDi(element, text) { if (isLabelExternal(element)) { return; } var di = getDi(element); if (text && !di.label) { di.label = bpmnFactory.create('bpmndi:BPMNLabel'); } if (!text && di.label) { delete di.label; } } /** * Set the label and return the changed elements. * * Element parameter can be label itself or connection (i.e. sequence flow). * * @param {djs.model.Base} element * @param {string} text */ function setText(element, text) { // external label if present var label = element.label || element; var labelTarget = element.labelTarget || element; setLabel(label, text); ensureInternalLabelDi(element, text); return [ label, labelTarget ]; } function preExecute(ctx) { var element = ctx.element, businessObject = element.businessObject, newLabel = ctx.newLabel; if (!isLabel$6(element) && isLabelExternal(element) && !hasExternalLabel(element) && !isEmptyText(newLabel)) { // create label var paddingTop = 7; var labelCenter = getExternalLabelMid(element); labelCenter = { x: labelCenter.x, y: labelCenter.y + paddingTop }; modeling.createLabel(element, labelCenter, { id: businessObject.id + '_label', businessObject: businessObject, di: element.di }); } } function execute(ctx) { ctx.oldLabel = getLabel(ctx.element); return setText(ctx.element, ctx.newLabel); } function revert(ctx) { return setText(ctx.element, ctx.oldLabel); } function postExecute(ctx) { var element = ctx.element, label = element.label || element, newLabel = ctx.newLabel, newBounds = ctx.newBounds, hints = ctx.hints || {}; // ignore internal labels for elements except text annotations if (!isLabel$6(label) && !is$1(label, 'bpmn:TextAnnotation')) { return; } if (isLabel$6(label) && isEmptyText(newLabel)) { if (hints.removeShape !== false) { modeling.removeShape(label, { unsetLabel: false }); } return; } var text = getLabel(label); // resize element based on label _or_ pre-defined bounds if (typeof newBounds === 'undefined') { newBounds = textRenderer.getExternalLabelBounds(label, text); } // setting newBounds to false or _null_ will // disable the postExecute resize operation if (newBounds) { modeling.resizeShape(label, newBounds, NULL_DIMENSIONS); } } // API this.preExecute = preExecute; this.execute = execute; this.revert = revert; this.postExecute = postExecute; } UpdateLabelHandler.$inject = [ 'modeling', 'textRenderer', 'bpmnFactory' ]; // helpers /////////////////////// function isEmptyText(label) { return !label || !label.trim(); } /** * BPMN 2.0 modeling features activator * * @param {EventBus} eventBus * @param {ElementFactory} elementFactory * @param {CommandStack} commandStack * @param {BpmnRules} bpmnRules */ function Modeling( eventBus, elementFactory, commandStack, bpmnRules) { Modeling$1.call(this, eventBus, elementFactory, commandStack); this._bpmnRules = bpmnRules; } e(Modeling, Modeling$1); Modeling.$inject = [ 'eventBus', 'elementFactory', 'commandStack', 'bpmnRules' ]; Modeling.prototype.getHandlers = function() { var handlers = Modeling$1.prototype.getHandlers.call(this); handlers['element.updateModdleProperties'] = UpdateModdlePropertiesHandler; handlers['element.updateProperties'] = UpdatePropertiesHandler; handlers['canvas.updateRoot'] = UpdateCanvasRootHandler; handlers['lane.add'] = AddLaneHandler; handlers['lane.resize'] = ResizeLaneHandler; handlers['lane.split'] = SplitLaneHandler; handlers['lane.updateRefs'] = UpdateFlowNodeRefsHandler; handlers['id.updateClaim'] = IdClaimHandler; handlers['element.setColor'] = SetColorHandler; handlers['element.updateLabel'] = UpdateLabelHandler; return handlers; }; Modeling.prototype.updateLabel = function(element, newLabel, newBounds, hints) { this._commandStack.execute('element.updateLabel', { element: element, newLabel: newLabel, newBounds: newBounds, hints: hints || {} }); }; Modeling.prototype.connect = function(source, target, attrs, hints) { var bpmnRules = this._bpmnRules; if (!attrs) { attrs = bpmnRules.canConnect(source, target); } if (!attrs) { return; } return this.createConnection(source, target, attrs, source.parent, hints); }; Modeling.prototype.updateModdleProperties = function(element, moddleElement, properties) { this._commandStack.execute('element.updateModdleProperties', { element: element, moddleElement: moddleElement, properties: properties }); }; Modeling.prototype.updateProperties = function(element, properties) { this._commandStack.execute('element.updateProperties', { element: element, properties: properties }); }; Modeling.prototype.resizeLane = function(laneShape, newBounds, balanced) { this._commandStack.execute('lane.resize', { shape: laneShape, newBounds: newBounds, balanced: balanced }); }; Modeling.prototype.addLane = function(targetLaneShape, location) { var context = { shape: targetLaneShape, location: location }; this._commandStack.execute('lane.add', context); return context.newLane; }; Modeling.prototype.splitLane = function(targetLane, count) { this._commandStack.execute('lane.split', { shape: targetLane, count: count }); }; /** * Transform the current diagram into a collaboration. * * @return {djs.model.Root} the new root element */ Modeling.prototype.makeCollaboration = function() { var collaborationElement = this._create('root', { type: 'bpmn:Collaboration' }); var context = { newRoot: collaborationElement }; this._commandStack.execute('canvas.updateRoot', context); return collaborationElement; }; Modeling.prototype.updateLaneRefs = function(flowNodeShapes, laneShapes) { this._commandStack.execute('lane.updateRefs', { flowNodeShapes: flowNodeShapes, laneShapes: laneShapes }); }; /** * Transform the current diagram into a process. * * @return {djs.model.Root} the new root element */ Modeling.prototype.makeProcess = function() { var processElement = this._create('root', { type: 'bpmn:Process' }); var context = { newRoot: processElement }; this._commandStack.execute('canvas.updateRoot', context); }; Modeling.prototype.claimId = function(id, moddleElement) { this._commandStack.execute('id.updateClaim', { id: id, element: moddleElement, claiming: true }); }; Modeling.prototype.unclaimId = function(id, moddleElement) { this._commandStack.execute('id.updateClaim', { id: id, element: moddleElement }); }; Modeling.prototype.setColor = function(elements, colors) { if (!elements.length) { elements = [ elements ]; } this._commandStack.execute('element.setColor', { elements: elements, colors: colors }); }; /** * A base connection layouter implementation * that layouts the connection by directly connecting * mid(source) + mid(target). */ function BaseLayouter() {} /** * Return the new layouted waypoints for the given connection. * * The connection passed is still unchanged; you may figure out about * the new connection start / end via the layout hints provided. * * @param {djs.model.Connection} connection * @param {Object} [hints] * @param {Point} [hints.connectionStart] * @param {Point} [hints.connectionEnd] * @param {Point} [hints.source] * @param {Point} [hints.target] * * @return {Array} the layouted connection waypoints */ BaseLayouter.prototype.layoutConnection = function(connection, hints) { hints = hints || {}; return [ hints.connectionStart || getMid(hints.source || connection.source), hints.connectionEnd || getMid(hints.target || connection.target) ]; }; var MIN_SEGMENT_LENGTH = 20, POINT_ORIENTATION_PADDING = 5; var round$1 = Math.round; var INTERSECTION_THRESHOLD = 20, ORIENTATION_THRESHOLD = { 'h:h': 20, 'v:v': 20, 'h:v': -10, 'v:h': -10 }; function needsTurn(orientation, startDirection) { return !{ t: /top/, r: /right/, b: /bottom/, l: /left/, h: /./, v: /./ }[startDirection].test(orientation); } function canLayoutStraight(direction, targetOrientation) { return { t: /top/, r: /right/, b: /bottom/, l: /left/, h: /left|right/, v: /top|bottom/ }[direction].test(targetOrientation); } function getSegmentBendpoints(a, b, directions) { var orientation = getOrientation(b, a, POINT_ORIENTATION_PADDING); var startDirection = directions.split(':')[0]; var xmid = round$1((b.x - a.x) / 2 + a.x), ymid = round$1((b.y - a.y) / 2 + a.y); var segmentEnd, segmentDirections; var layoutStraight = canLayoutStraight(startDirection, orientation), layoutHorizontal = /h|r|l/.test(startDirection), layoutTurn = false; var turnNextDirections = false; if (layoutStraight) { segmentEnd = layoutHorizontal ? { x: xmid, y: a.y } : { x: a.x, y: ymid }; segmentDirections = layoutHorizontal ? 'h:h' : 'v:v'; } else { layoutTurn = needsTurn(orientation, startDirection); segmentDirections = layoutHorizontal ? 'h:v' : 'v:h'; if (layoutTurn) { if (layoutHorizontal) { turnNextDirections = ymid === a.y; segmentEnd = { x: a.x + MIN_SEGMENT_LENGTH * (/l/.test(startDirection) ? -1 : 1), y: turnNextDirections ? ymid + MIN_SEGMENT_LENGTH : ymid }; } else { turnNextDirections = xmid === a.x; segmentEnd = { x: turnNextDirections ? xmid + MIN_SEGMENT_LENGTH : xmid, y: a.y + MIN_SEGMENT_LENGTH * (/t/.test(startDirection) ? -1 : 1) }; } } else { segmentEnd = { x: xmid, y: ymid }; } } return { waypoints: getBendpoints(a, segmentEnd, segmentDirections).concat(segmentEnd), directions: segmentDirections, turnNextDirections: turnNextDirections }; } function getStartSegment(a, b, directions) { return getSegmentBendpoints(a, b, directions); } function getEndSegment(a, b, directions) { var invertedSegment = getSegmentBendpoints(b, a, invertDirections(directions)); return { waypoints: invertedSegment.waypoints.slice().reverse(), directions: invertDirections(invertedSegment.directions), turnNextDirections: invertedSegment.turnNextDirections }; } function getMidSegment(startSegment, endSegment) { var startDirection = startSegment.directions.split(':')[1], endDirection = endSegment.directions.split(':')[0]; if (startSegment.turnNextDirections) { startDirection = startDirection == 'h' ? 'v' : 'h'; } if (endSegment.turnNextDirections) { endDirection = endDirection == 'h' ? 'v' : 'h'; } var directions = startDirection + ':' + endDirection; var bendpoints = getBendpoints( startSegment.waypoints[startSegment.waypoints.length - 1], endSegment.waypoints[0], directions ); return { waypoints: bendpoints, directions: directions }; } function invertDirections(directions) { return directions.split(':').reverse().join(':'); } /** * Handle simple layouts with maximum two bendpoints. */ function getSimpleBendpoints(a, b, directions) { var xmid = round$1((b.x - a.x) / 2 + a.x), ymid = round$1((b.y - a.y) / 2 + a.y); // one point, right or left from a if (directions === 'h:v') { return [ { x: b.x, y: a.y } ]; } // one point, above or below a if (directions === 'v:h') { return [ { x: a.x, y: b.y } ]; } // vertical segment between a and b if (directions === 'h:h') { return [ { x: xmid, y: a.y }, { x: xmid, y: b.y } ]; } // horizontal segment between a and b if (directions === 'v:v') { return [ { x: a.x, y: ymid }, { x: b.x, y: ymid } ]; } throw new Error('invalid directions: can only handle varians of [hv]:[hv]'); } /** * Returns the mid points for a manhattan connection between two points. * * @example h:h (horizontal:horizontal) * * [a]----[x] * | * [x]----[b] * * @example h:v (horizontal:vertical) * * [a]----[x] * | * [b] * * @example h:r (horizontal:right) * * [a]----[x] * | * [b]-[x] * * @param {Point} a * @param {Point} b * @param {string} directions * * @return {Array} */ function getBendpoints(a, b, directions) { directions = directions || 'h:h'; if (!isValidDirections(directions)) { throw new Error( 'unknown directions: <' + directions + '>: ' + 'must be specified as : ' + 'with start/end in { h,v,t,r,b,l }' ); } // compute explicit directions, involving trbl dockings // using a three segmented layouting algorithm if (isExplicitDirections(directions)) { var startSegment = getStartSegment(a, b, directions), endSegment = getEndSegment(a, b, directions), midSegment = getMidSegment(startSegment, endSegment); return [].concat( startSegment.waypoints, midSegment.waypoints, endSegment.waypoints ); } // handle simple [hv]:[hv] cases that can be easily computed return getSimpleBendpoints(a, b, directions); } /** * Create a connection between the two points according * to the manhattan layout (only horizontal and vertical) edges. * * @param {Point} a * @param {Point} b * * @param {string} [directions='h:h'] specifies manhattan directions for each point as {adirection}:{bdirection}. A directionfor a point is either `h` (horizontal) or `v` (vertical) * * @return {Array} */ function connectPoints(a, b, directions) { var points = getBendpoints(a, b, directions); points.unshift(a); points.push(b); return withoutRedundantPoints(points); } /** * Connect two rectangles using a manhattan layouted connection. * * @param {Bounds} source source rectangle * @param {Bounds} target target rectangle * @param {Point} [start] source docking * @param {Point} [end] target docking * * @param {Object} [hints] * @param {string} [hints.preserveDocking=source] preserve docking on selected side * @param {Array} [hints.preferredLayouts] * @param {Point|boolean} [hints.connectionStart] whether the start changed * @param {Point|boolean} [hints.connectionEnd] whether the end changed * * @return {Array} connection points */ function connectRectangles(source, target, start, end, hints) { var preferredLayouts = hints && hints.preferredLayouts || []; var preferredLayout = without(preferredLayouts, 'straight')[0] || 'h:h'; var threshold = ORIENTATION_THRESHOLD[preferredLayout] || 0; var orientation = getOrientation(source, target, threshold); var directions = getDirections(orientation, preferredLayout); start = start || getMid(source); end = end || getMid(target); var directionSplit = directions.split(':'); // compute actual docking points for start / end // this ensures we properly layout only parts of the // connection that lies in between the two rectangles var startDocking = getDockingPoint(start, source, directionSplit[0], invertOrientation(orientation)), endDocking = getDockingPoint(end, target, directionSplit[1], orientation); return connectPoints(startDocking, endDocking, directions); } /** * Repair the connection between two rectangles, of which one has been updated. * * @param {Bounds} source * @param {Bounds} target * @param {Point} [start] * @param {Point} [end] * @param {Array} [waypoints] * @param {Object} [hints] * @param {Array} [hints.preferredLayouts] list of preferred layouts * @param {boolean} [hints.connectionStart] * @param {boolean} [hints.connectionEnd] * * @return {Array} repaired waypoints */ function repairConnection(source, target, start, end, waypoints, hints) { if (isArray$3(start)) { waypoints = start; hints = end; start = getMid(source); end = getMid(target); } hints = assign({ preferredLayouts: [] }, hints); waypoints = waypoints || []; var preferredLayouts = hints.preferredLayouts, preferStraight = preferredLayouts.indexOf('straight') !== -1, repairedWaypoints; // just layout non-existing or simple connections // attempt to render straight lines, if required // attempt to layout a straight line repairedWaypoints = preferStraight && tryLayoutStraight(source, target, start, end, hints); if (repairedWaypoints) { return repairedWaypoints; } // try to layout from end repairedWaypoints = hints.connectionEnd && tryRepairConnectionEnd(target, source, end, waypoints); if (repairedWaypoints) { return repairedWaypoints; } // try to layout from start repairedWaypoints = hints.connectionStart && tryRepairConnectionStart(source, target, start, waypoints); if (repairedWaypoints) { return repairedWaypoints; } // or whether nothing seems to have changed if (!hints.connectionStart && !hints.connectionEnd && waypoints && waypoints.length) { return waypoints; } // simply reconnect if nothing else worked return connectRectangles(source, target, start, end, hints); } function inRange(a, start, end) { return a >= start && a <= end; } function isInRange(axis, a, b) { var size = { x: 'width', y: 'height' }; return inRange(a[axis], b[axis], b[axis] + b[size[axis]]); } /** * Layout a straight connection * * @param {Bounds} source * @param {Bounds} target * @param {Point} start * @param {Point} end * @param {Object} [hints] * * @return {Array|null} waypoints if straight layout worked */ function tryLayoutStraight(source, target, start, end, hints) { var axis = {}, primaryAxis, orientation; orientation = getOrientation(source, target); // only layout a straight connection if shapes are // horizontally or vertically aligned if (!/^(top|bottom|left|right)$/.test(orientation)) { return null; } if (/top|bottom/.test(orientation)) { primaryAxis = 'x'; } if (/left|right/.test(orientation)) { primaryAxis = 'y'; } if (hints.preserveDocking === 'target') { if (!isInRange(primaryAxis, end, source)) { return null; } axis[primaryAxis] = end[primaryAxis]; return [ { x: axis.x !== undefined ? axis.x : start.x, y: axis.y !== undefined ? axis.y : start.y, original: { x: axis.x !== undefined ? axis.x : start.x, y: axis.y !== undefined ? axis.y : start.y } }, { x: end.x, y: end.y } ]; } else { if (!isInRange(primaryAxis, start, target)) { return null; } axis[primaryAxis] = start[primaryAxis]; return [ { x: start.x, y: start.y }, { x: axis.x !== undefined ? axis.x : end.x, y: axis.y !== undefined ? axis.y : end.y, original: { x: axis.x !== undefined ? axis.x : end.x, y: axis.y !== undefined ? axis.y : end.y } } ]; } } /** * Repair a connection from start. * * @param {Bounds} moved * @param {Bounds} other * @param {Point} newDocking * @param {Array} points originalPoints from moved to other * * @return {Array|null} the repaired points between the two rectangles */ function tryRepairConnectionStart(moved, other, newDocking, points) { return _tryRepairConnectionSide(moved, other, newDocking, points); } /** * Repair a connection from end. * * @param {Bounds} moved * @param {Bounds} other * @param {Point} newDocking * @param {Array} points originalPoints from moved to other * * @return {Array|null} the repaired points between the two rectangles */ function tryRepairConnectionEnd(moved, other, newDocking, points) { var waypoints = points.slice().reverse(); waypoints = _tryRepairConnectionSide(moved, other, newDocking, waypoints); return waypoints ? waypoints.reverse() : null; } /** * Repair a connection from one side that moved. * * @param {Bounds} moved * @param {Bounds} other * @param {Point} newDocking * @param {Array} points originalPoints from moved to other * * @return {Array} the repaired points between the two rectangles */ function _tryRepairConnectionSide(moved, other, newDocking, points) { function needsRelayout(points) { if (points.length < 3) { return true; } if (points.length > 4) { return false; } // relayout if two points overlap // this is most likely due to return !!find(points, function(p, idx) { var q = points[idx - 1]; return q && pointDistance(p, q) < 3; }); } function repairBendpoint(candidate, oldPeer, newPeer) { var alignment = pointsAligned(oldPeer, candidate); switch (alignment) { case 'v': // repair horizontal alignment return { x: newPeer.x, y: candidate.y }; case 'h': // repair vertical alignment return { x: candidate.x, y: newPeer.y }; } return { x: candidate.x, y: candidate. y }; } function removeOverlapping(points, a, b) { var i; for (i = points.length - 2; i !== 0; i--) { // intersects (?) break, remove all bendpoints up to this one and relayout if (pointInRect(points[i], a, INTERSECTION_THRESHOLD) || pointInRect(points[i], b, INTERSECTION_THRESHOLD)) { // return sliced old connection return points.slice(i); } } return points; } // (0) only repair what has layoutable bendpoints // (1) if only one bendpoint and on shape moved onto other shapes axis // (horizontally / vertically), relayout if (needsRelayout(points)) { return null; } var oldDocking = points[0], newPoints = points.slice(), slicedPoints; // (2) repair only last line segment and only if it was layouted before newPoints[0] = newDocking; newPoints[1] = repairBendpoint(newPoints[1], oldDocking, newDocking); // (3) if shape intersects with any bendpoint after repair, // remove all segments up to this bendpoint and repair from there slicedPoints = removeOverlapping(newPoints, moved, other); if (slicedPoints !== newPoints) { newPoints = _tryRepairConnectionSide(moved, other, newDocking, slicedPoints); } // (4) do NOT repair if repaired bendpoints are aligned if (newPoints && pointsAligned(newPoints)) { return null; } return newPoints; } /** * Returns the manhattan directions connecting two rectangles * with the given orientation. * * Will always return the default layout, if it is specific * regarding sides already (trbl). * * @example * * getDirections('top'); // -> 'v:v' * getDirections('intersect'); // -> 't:t' * * getDirections('top-right', 'v:h'); // -> 'v:h' * getDirections('top-right', 'h:h'); // -> 'h:h' * * * @param {string} orientation * @param {string} defaultLayout * * @return {string} */ function getDirections(orientation, defaultLayout) { // don't override specific trbl directions if (isExplicitDirections(defaultLayout)) { return defaultLayout; } switch (orientation) { case 'intersect': return 't:t'; case 'top': case 'bottom': return 'v:v'; case 'left': case 'right': return 'h:h'; // 'top-left' // 'top-right' // 'bottom-left' // 'bottom-right' default: return defaultLayout; } } function isValidDirections(directions) { return directions && /^h|v|t|r|b|l:h|v|t|r|b|l$/.test(directions); } function isExplicitDirections(directions) { return directions && /t|r|b|l/.test(directions); } function invertOrientation(orientation) { return { 'top': 'bottom', 'bottom': 'top', 'left': 'right', 'right': 'left', 'top-left': 'bottom-right', 'bottom-right': 'top-left', 'top-right': 'bottom-left', 'bottom-left': 'top-right', }[orientation]; } function getDockingPoint(point, rectangle, dockingDirection, targetOrientation) { // ensure we end up with a specific docking direction // based on the targetOrientation, if is being passed if (dockingDirection === 'h') { dockingDirection = /left/.test(targetOrientation) ? 'l' : 'r'; } if (dockingDirection === 'v') { dockingDirection = /top/.test(targetOrientation) ? 't' : 'b'; } if (dockingDirection === 't') { return { original: point, x: point.x, y: rectangle.y }; } if (dockingDirection === 'r') { return { original: point, x: rectangle.x + rectangle.width, y: point.y }; } if (dockingDirection === 'b') { return { original: point, x: point.x, y: rectangle.y + rectangle.height }; } if (dockingDirection === 'l') { return { original: point, x: rectangle.x, y: point.y }; } throw new Error('unexpected dockingDirection: <' + dockingDirection + '>'); } /** * Return list of waypoints with redundant ones filtered out. * * @example * * Original points: * * [x] ----- [x] ------ [x] * | * [x] ----- [x] - [x] * * Filtered: * * [x] ---------------- [x] * | * [x] ----------- [x] * * @param {Array} waypoints * * @return {Array} */ function withoutRedundantPoints(waypoints) { return waypoints.reduce(function(points, p, idx) { var previous = points[points.length - 1], next = waypoints[idx + 1]; if (!pointsOnLine(previous, next, p, 0)) { points.push(p); } return points; }, []); } var ATTACH_ORIENTATION_PADDING = -10, BOUNDARY_TO_HOST_THRESHOLD$1 = 40; var oppositeOrientationMapping = { 'top': 'bottom', 'top-right': 'bottom-left', 'top-left': 'bottom-right', 'right': 'left', 'bottom': 'top', 'bottom-right': 'top-left', 'bottom-left': 'top-right', 'left': 'right' }; var orientationDirectionMapping = { top: 't', right: 'r', bottom: 'b', left: 'l' }; function BpmnLayouter() {} e(BpmnLayouter, BaseLayouter); BpmnLayouter.prototype.layoutConnection = function(connection, hints) { if (!hints) { hints = {}; } var source = hints.source || connection.source, target = hints.target || connection.target, waypoints = hints.waypoints || connection.waypoints, connectionStart = hints.connectionStart, connectionEnd = hints.connectionEnd; var manhattanOptions, updatedWaypoints; if (!connectionStart) { connectionStart = getConnectionDocking(waypoints && waypoints[ 0 ], source); } if (!connectionEnd) { connectionEnd = getConnectionDocking(waypoints && waypoints[ waypoints.length - 1 ], target); } // TODO(nikku): support vertical modeling // and invert preferredLayouts accordingly if (is$1(connection, 'bpmn:Association') || is$1(connection, 'bpmn:DataAssociation')) { if (waypoints && !isCompensationAssociation(source, target)) { return [].concat([ connectionStart ], waypoints.slice(1, -1), [ connectionEnd ]); } } if (is$1(connection, 'bpmn:MessageFlow')) { manhattanOptions = getMessageFlowManhattanOptions(source, target); } else if (is$1(connection, 'bpmn:SequenceFlow') || isCompensationAssociation(source, target)) { // layout all connection between flow elements h:h, except for // (1) outgoing of boundary events -> layout based on attach orientation and target orientation // (2) incoming/outgoing of gateways -> v:h for outgoing, h:v for incoming // (3) loops if (source === target) { manhattanOptions = { preferredLayouts: getLoopPreferredLayout(source, connection) }; } else if (is$1(source, 'bpmn:BoundaryEvent')) { manhattanOptions = { preferredLayouts: getBoundaryEventPreferredLayouts(source, target, connectionEnd) }; } else if (isExpandedSubProcess(source) || isExpandedSubProcess(target)) { manhattanOptions = getSubProcessManhattanOptions(source); } else if (is$1(source, 'bpmn:Gateway')) { manhattanOptions = { preferredLayouts: [ 'v:h' ] }; } else if (is$1(target, 'bpmn:Gateway')) { manhattanOptions = { preferredLayouts: [ 'h:v' ] }; } else { manhattanOptions = { preferredLayouts: [ 'h:h' ] }; } } if (manhattanOptions) { manhattanOptions = assign(manhattanOptions, hints); updatedWaypoints = withoutRedundantPoints(repairConnection( source, target, connectionStart, connectionEnd, waypoints, manhattanOptions )); } return updatedWaypoints || [ connectionStart, connectionEnd ]; }; // helpers ////////// function getAttachOrientation(attachedElement) { var hostElement = attachedElement.host; return getOrientation(getMid(attachedElement), hostElement, ATTACH_ORIENTATION_PADDING); } function getMessageFlowManhattanOptions(source, target) { return { preferredLayouts: [ 'straight', 'v:v' ], preserveDocking: getMessageFlowPreserveDocking(source, target) }; } function getMessageFlowPreserveDocking(source, target) { // (1) docking element connected to participant has precedence if (is$1(target, 'bpmn:Participant')) { return 'source'; } if (is$1(source, 'bpmn:Participant')) { return 'target'; } // (2) docking element connected to expanded sub-process has precedence if (isExpandedSubProcess(target)) { return 'source'; } if (isExpandedSubProcess(source)) { return 'target'; } // (3) docking event has precedence if (is$1(target, 'bpmn:Event')) { return 'target'; } if (is$1(source, 'bpmn:Event')) { return 'source'; } return null; } function getSubProcessManhattanOptions(source) { return { preferredLayouts: [ 'straight', 'h:h' ], preserveDocking: getSubProcessPreserveDocking(source) }; } function getSubProcessPreserveDocking(source) { return isExpandedSubProcess(source) ? 'target' : 'source'; } function getConnectionDocking(point, shape) { return point ? (point.original || point) : getMid(shape); } function isCompensationAssociation(source, target) { return is$1(target, 'bpmn:Activity') && is$1(source, 'bpmn:BoundaryEvent') && target.businessObject.isForCompensation; } function isExpandedSubProcess(element) { return is$1(element, 'bpmn:SubProcess') && isExpanded(element); } function isSame(a, b) { return a === b; } function isAnyOrientation(orientation, orientations) { return orientations.indexOf(orientation) !== -1; } function getHorizontalOrientation(orientation) { var matches = /right|left/.exec(orientation); return matches && matches[0]; } function getVerticalOrientation(orientation) { var matches = /top|bottom/.exec(orientation); return matches && matches[0]; } function isOppositeOrientation(a, b) { return oppositeOrientationMapping[a] === b; } function isOppositeHorizontalOrientation(a, b) { var horizontalOrientation = getHorizontalOrientation(a); var oppositeHorizontalOrientation = oppositeOrientationMapping[horizontalOrientation]; return b.indexOf(oppositeHorizontalOrientation) !== -1; } function isOppositeVerticalOrientation(a, b) { var verticalOrientation = getVerticalOrientation(a); var oppositeVerticalOrientation = oppositeOrientationMapping[verticalOrientation]; return b.indexOf(oppositeVerticalOrientation) !== -1; } function isHorizontalOrientation(orientation) { return orientation === 'right' || orientation === 'left'; } function getLoopPreferredLayout(source, connection) { var waypoints = connection.waypoints; var orientation = waypoints && waypoints.length && getOrientation(waypoints[0], source); if (orientation === 'top') { return [ 't:r' ]; } else if (orientation === 'right') { return [ 'r:b' ]; } else if (orientation === 'left') { return [ 'l:t' ]; } return [ 'b:l' ]; } function getBoundaryEventPreferredLayouts(source, target, end) { var sourceMid = getMid(source), targetMid = getMid(target), attachOrientation = getAttachOrientation(source), sourceLayout, targetLayout; var isLoop = isSame(source.host, target); var attachedToSide = isAnyOrientation(attachOrientation, [ 'top', 'right', 'bottom', 'left' ]); var targetOrientation = getOrientation(targetMid, sourceMid, { x: source.width / 2 + target.width / 2, y: source.height / 2 + target.height / 2 }); if (isLoop) { return getBoundaryEventLoopLayout(attachOrientation, attachedToSide, source, target, end); } // source layout sourceLayout = getBoundaryEventSourceLayout(attachOrientation, targetOrientation, attachedToSide); // target layout targetLayout = getBoundaryEventTargetLayout(attachOrientation, targetOrientation, attachedToSide); return [ sourceLayout + ':' + targetLayout ]; } function getBoundaryEventLoopLayout(attachOrientation, attachedToSide, source, target, end) { var orientation = attachedToSide ? attachOrientation : getVerticalOrientation(attachOrientation), sourceLayout = orientationDirectionMapping[ orientation ], targetLayout; if (attachedToSide) { if (isHorizontalOrientation(attachOrientation)) { targetLayout = shouldConnectToSameSide('y', source, target, end) ? 'h' : 'b'; } else { targetLayout = shouldConnectToSameSide('x', source, target, end) ? 'v' : 'l'; } } else { targetLayout = 'v'; } return [ sourceLayout + ':' + targetLayout ]; } function shouldConnectToSameSide(axis, source, target, end) { var threshold = BOUNDARY_TO_HOST_THRESHOLD$1; return !( areCloseOnAxis(axis, end, target, threshold) || areCloseOnAxis(axis, end, { x: target.x + target.width, y: target.y + target.height }, threshold) || areCloseOnAxis(axis, end, getMid(source), threshold) ); } function areCloseOnAxis(axis, a, b, threshold) { return Math.abs(a[ axis ] - b[ axis ]) < threshold; } function getBoundaryEventSourceLayout(attachOrientation, targetOrientation, attachedToSide) { // attached to either top, right, bottom or left side if (attachedToSide) { return orientationDirectionMapping[ attachOrientation ]; } // attached to either top-right, top-left, bottom-right or bottom-left corner // same vertical or opposite horizontal orientation if (isSame( getVerticalOrientation(attachOrientation), getVerticalOrientation(targetOrientation) ) || isOppositeOrientation( getHorizontalOrientation(attachOrientation), getHorizontalOrientation(targetOrientation) )) { return orientationDirectionMapping[ getVerticalOrientation(attachOrientation) ]; } // fallback return orientationDirectionMapping[ getHorizontalOrientation(attachOrientation) ]; } function getBoundaryEventTargetLayout(attachOrientation, targetOrientation, attachedToSide) { // attached to either top, right, bottom or left side if (attachedToSide) { if (isHorizontalOrientation(attachOrientation)) { // orientation is right or left // opposite horizontal orientation or same orientation if ( isOppositeHorizontalOrientation(attachOrientation, targetOrientation) || isSame(attachOrientation, targetOrientation) ) { return 'h'; } // fallback return 'v'; } else { // orientation is top or bottom // opposite vertical orientation or same orientation if ( isOppositeVerticalOrientation(attachOrientation, targetOrientation) || isSame(attachOrientation, targetOrientation) ) { return 'v'; } // fallback return 'h'; } } // attached to either top-right, top-left, bottom-right or bottom-left corner // orientation is right, left // or same vertical orientation but also right or left if (isHorizontalOrientation(targetOrientation) || (isSame(getVerticalOrientation(attachOrientation), getVerticalOrientation(targetOrientation)) && getHorizontalOrientation(targetOrientation))) { return 'h'; } else { return 'v'; } } function dockingToPoint(docking) { // use the dockings actual point and // retain the original docking return assign({ original: docking.point.original || docking.point }, docking.actual); } /** * A {@link ConnectionDocking} that crops connection waypoints based on * the path(s) of the connection source and target. * * @param {djs.core.ElementRegistry} elementRegistry */ function CroppingConnectionDocking(elementRegistry, graphicsFactory) { this._elementRegistry = elementRegistry; this._graphicsFactory = graphicsFactory; } CroppingConnectionDocking.$inject = [ 'elementRegistry', 'graphicsFactory' ]; /** * @inheritDoc ConnectionDocking#getCroppedWaypoints */ CroppingConnectionDocking.prototype.getCroppedWaypoints = function(connection, source, target) { source = source || connection.source; target = target || connection.target; var sourceDocking = this.getDockingPoint(connection, source, true), targetDocking = this.getDockingPoint(connection, target); var croppedWaypoints = connection.waypoints.slice(sourceDocking.idx + 1, targetDocking.idx); croppedWaypoints.unshift(dockingToPoint(sourceDocking)); croppedWaypoints.push(dockingToPoint(targetDocking)); return croppedWaypoints; }; /** * Return the connection docking point on the specified shape * * @inheritDoc ConnectionDocking#getDockingPoint */ CroppingConnectionDocking.prototype.getDockingPoint = function(connection, shape, dockStart) { var waypoints = connection.waypoints, dockingIdx, dockingPoint, croppedPoint; dockingIdx = dockStart ? 0 : waypoints.length - 1; dockingPoint = waypoints[dockingIdx]; croppedPoint = this._getIntersection(shape, connection, dockStart); return { point: dockingPoint, actual: croppedPoint || dockingPoint, idx: dockingIdx }; }; // helpers ////////////////////// CroppingConnectionDocking.prototype._getIntersection = function(shape, connection, takeFirst) { var shapePath = this._getShapePath(shape), connectionPath = this._getConnectionPath(connection); return getElementLineIntersection(shapePath, connectionPath, takeFirst); }; CroppingConnectionDocking.prototype._getConnectionPath = function(connection) { return this._graphicsFactory.getConnectionPath(connection); }; CroppingConnectionDocking.prototype._getShapePath = function(shape) { return this._graphicsFactory.getShapePath(shape); }; CroppingConnectionDocking.prototype._getGfx = function(element) { return this._elementRegistry.getGraphics(element); }; var ModelingModule = { __init__: [ 'modeling', 'bpmnUpdater' ], __depends__: [ BehaviorModule, RulesModule, DiOrderingModule, OrderingModule, ReplaceModule, CommandModule, TooltipsModule, LabelSupportModule, AttachSupportModule, SelectionModule, ChangeSupportModule, SpaceToolModule ], bpmnFactory: [ 'type', BpmnFactory ], bpmnUpdater: [ 'type', BpmnUpdater ], elementFactory: [ 'type', ElementFactory ], modeling: [ 'type', Modeling ], layouter: [ 'type', BpmnLayouter ], connectionDocking: [ 'type', CroppingConnectionDocking ] }; var LOW_PRIORITY$2 = 500, MEDIUM_PRIORITY = 1250, HIGH_PRIORITY$2 = 1500; var round = Math.round; function mid(element) { return { x: element.x + round(element.width / 2), y: element.y + round(element.height / 2) }; } /** * A plugin that makes shapes draggable / droppable. * * @param {EventBus} eventBus * @param {Dragging} dragging * @param {Modeling} modeling * @param {Selection} selection * @param {Rules} rules */ function MoveEvents( eventBus, dragging, modeling, selection, rules) { // rules function canMove(shapes, delta, position, target) { return rules.allowed('elements.move', { shapes: shapes, delta: delta, position: position, target: target }); } // move events // assign a high priority to this handler to setup the environment // others may hook up later, e.g. at default priority and modify // the move environment. // // This sets up the context with // // * shape: the primary shape being moved // * shapes: a list of shapes to be moved // * validatedShapes: a list of shapes that are being checked // against the rules before and during move // eventBus.on('shape.move.start', HIGH_PRIORITY$2, function(event) { var context = event.context, shape = event.shape, shapes = selection.get().slice(); // move only single shape if the dragged element // is not part of the current selection if (shapes.indexOf(shape) === -1) { shapes = [ shape ]; } // ensure we remove nested elements in the collection // and add attachers for a proper dragger shapes = removeNested(shapes); // attach shapes to drag context assign(context, { shapes: shapes, validatedShapes: shapes, shape: shape }); }); // assign a high priority to this handler to setup the environment // others may hook up later, e.g. at default priority and modify // the move environment // eventBus.on('shape.move.start', MEDIUM_PRIORITY, function(event) { var context = event.context, validatedShapes = context.validatedShapes, canExecute; canExecute = context.canExecute = canMove(validatedShapes); // check if we can move the elements if (!canExecute) { return false; } }); // assign a low priority to this handler // to let others modify the move event before we update // the context // eventBus.on('shape.move.move', LOW_PRIORITY$2, function(event) { var context = event.context, validatedShapes = context.validatedShapes, hover = event.hover, delta = { x: event.dx, y: event.dy }, position = { x: event.x, y: event.y }, canExecute; // check if we can move the elements canExecute = canMove(validatedShapes, delta, position, hover); context.delta = delta; context.canExecute = canExecute; // simply ignore move over if (canExecute === null) { context.target = null; return; } context.target = hover; }); eventBus.on('shape.move.end', function(event) { var context = event.context; var delta = context.delta, canExecute = context.canExecute, isAttach = canExecute === 'attach', shapes = context.shapes; if (canExecute === false) { return false; } // ensure we have actual pixel values deltas // (important when zoom level was > 1 during move) delta.x = round(delta.x); delta.y = round(delta.y); if (delta.x === 0 && delta.y === 0) { // didn't move return; } modeling.moveElements(shapes, delta, context.target, { primaryShape: context.shape, attach: isAttach }); }); // move activation eventBus.on('element.mousedown', function(event) { if (!isPrimaryButton(event)) { return; } var originalEvent = getOriginal$1(event); if (!originalEvent) { throw new Error('must supply DOM mousedown event'); } return start(originalEvent, event.element); }); /** * Start move. * * @param {MouseEvent} event * @param {djs.model.Shape} shape * @param {boolean} [activate] * @param {Object} [context] */ function start(event, element, activate, context) { if (isObject(activate)) { context = activate; activate = false; } // do not move connections or the root element if (element.waypoints || !element.parent) { return; } // ignore non-draggable hits if (classes(event.target).has('djs-hit-no-move')) { return; } var referencePoint = mid(element); dragging.init(event, referencePoint, 'shape.move', { cursor: 'grabbing', autoActivate: activate, data: { shape: element, context: context || {} } }); // we've handled the event return true; } // API this.start = start; } MoveEvents.$inject = [ 'eventBus', 'dragging', 'modeling', 'selection', 'rules' ]; /** * Return a filtered list of elements that do not contain * those nested into others. * * @param {Array} elements * * @return {Array} filtered */ function removeNested(elements) { var ids = groupBy(elements, 'id'); return filter(elements, function(element) { while ((element = element.parent)) { // parent in selection if (ids[element.id]) { return false; } } return true; }); } var LOW_PRIORITY$1 = 499; var MARKER_DRAGGING = 'djs-dragging', MARKER_OK$1 = 'drop-ok', MARKER_NOT_OK$1 = 'drop-not-ok', MARKER_NEW_PARENT = 'new-parent', MARKER_ATTACH = 'attach-ok'; /** * Provides previews for moving shapes when moving. * * @param {EventBus} eventBus * @param {ElementRegistry} elementRegistry * @param {Canvas} canvas * @param {Styles} styles */ function MovePreview( eventBus, canvas, styles, previewSupport) { function getVisualDragShapes(shapes) { var elements = getAllDraggedElements(shapes); var filteredElements = removeEdges(elements); return filteredElements; } function getAllDraggedElements(shapes) { var allShapes = selfAndAllChildren(shapes, true); var allConnections = map(allShapes, function(shape) { return (shape.incoming || []).concat(shape.outgoing || []); }); return flatten(allShapes.concat(allConnections)); } /** * Sets drop marker on an element. */ function setMarker(element, marker) { [ MARKER_ATTACH, MARKER_OK$1, MARKER_NOT_OK$1, MARKER_NEW_PARENT ].forEach(function(m) { if (m === marker) { canvas.addMarker(element, m); } else { canvas.removeMarker(element, m); } }); } /** * Make an element draggable. * * @param {Object} context * @param {djs.model.Base} element * @param {boolean} addMarker */ function makeDraggable(context, element, addMarker) { previewSupport.addDragger(element, context.dragGroup); if (addMarker) { canvas.addMarker(element, MARKER_DRAGGING); } if (context.allDraggedElements) { context.allDraggedElements.push(element); } else { context.allDraggedElements = [ element ]; } } // assign a low priority to this handler // to let others modify the move context before // we draw things eventBus.on('shape.move.start', LOW_PRIORITY$1, function(event) { var context = event.context, dragShapes = context.shapes, allDraggedElements = context.allDraggedElements; var visuallyDraggedShapes = getVisualDragShapes(dragShapes); if (!context.dragGroup) { var dragGroup = create$1('g'); attr(dragGroup, styles.cls('djs-drag-group', [ 'no-events' ])); var activeLayer = canvas.getActiveLayer(); append(activeLayer, dragGroup); context.dragGroup = dragGroup; } // add previews visuallyDraggedShapes.forEach(function(shape) { previewSupport.addDragger(shape, context.dragGroup); }); // cache all dragged elements / gfx // so that we can quickly undo their state changes later if (!allDraggedElements) { allDraggedElements = getAllDraggedElements(dragShapes); } else { allDraggedElements = flatten([ allDraggedElements, getAllDraggedElements(dragShapes) ]); } // add dragging marker forEach$1(allDraggedElements, function(e) { canvas.addMarker(e, MARKER_DRAGGING); }); context.allDraggedElements = allDraggedElements; // determine, if any of the dragged elements have different parents context.differentParents = haveDifferentParents(dragShapes); }); // update previews eventBus.on('shape.move.move', LOW_PRIORITY$1, function(event) { var context = event.context, dragGroup = context.dragGroup, target = context.target, parent = context.shape.parent, canExecute = context.canExecute; if (target) { if (canExecute === 'attach') { setMarker(target, MARKER_ATTACH); } else if (context.canExecute && target && target.id !== parent.id) { setMarker(target, MARKER_NEW_PARENT); } else { setMarker(target, context.canExecute ? MARKER_OK$1 : MARKER_NOT_OK$1); } } translate$2(dragGroup, event.dx, event.dy); }); eventBus.on([ 'shape.move.out', 'shape.move.cleanup' ], function(event) { var context = event.context, target = context.target; if (target) { setMarker(target, null); } }); // remove previews eventBus.on('shape.move.cleanup', function(event) { var context = event.context, allDraggedElements = context.allDraggedElements, dragGroup = context.dragGroup; // remove dragging marker forEach$1(allDraggedElements, function(e) { canvas.removeMarker(e, MARKER_DRAGGING); }); if (dragGroup) { remove$1(dragGroup); } }); // API ////////////////////// /** * Make an element draggable. * * @param {Object} context * @param {djs.model.Base} element * @param {boolean} addMarker */ this.makeDraggable = makeDraggable; } MovePreview.$inject = [ 'eventBus', 'canvas', 'styles', 'previewSupport' ]; // helpers ////////////////////// /** * returns elements minus all connections * where source or target is not elements */ function removeEdges(elements) { var filteredElements = filter(elements, function(element) { if (!isConnection$2(element)) { return true; } else { return ( find(elements, matchPattern({ id: element.source.id })) && find(elements, matchPattern({ id: element.target.id })) ); } }); return filteredElements; } function haveDifferentParents(elements) { return size(groupBy(elements, function(e) { return e.parent && e.parent.id; })) !== 1; } /** * Checks if an element is a connection. */ function isConnection$2(element) { return element.waypoints; } var MoveModule = { __depends__: [ InteractionEventsModule$1, SelectionModule, OutlineModule, RulesModule$1, DraggingModule, PreviewSupportModule ], __init__: [ 'move', 'movePreview' ], move: [ 'type', MoveEvents ], movePreview: [ 'type', MovePreview ] }; var TOGGLE_SELECTOR = '.djs-palette-toggle', ENTRY_SELECTOR = '.entry', ELEMENT_SELECTOR = TOGGLE_SELECTOR + ', ' + ENTRY_SELECTOR; var PALETTE_PREFIX = 'djs-palette-', PALETTE_SHOWN_CLS = 'shown', PALETTE_OPEN_CLS = 'open', PALETTE_TWO_COLUMN_CLS = 'two-column'; var DEFAULT_PRIORITY = 1000; /** * A palette containing modeling elements. */ function Palette(eventBus, canvas) { this._eventBus = eventBus; this._canvas = canvas; var self = this; eventBus.on('tool-manager.update', function(event) { var tool = event.tool; self.updateToolHighlight(tool); }); eventBus.on('i18n.changed', function() { self._update(); }); eventBus.on('diagram.init', function() { self._diagramInitialized = true; self._rebuild(); }); } Palette.$inject = [ 'eventBus', 'canvas' ]; /** * Register a provider with the palette * * @param {number} [priority=1000] * @param {PaletteProvider} provider * * @example * const paletteProvider = { * getPaletteEntries: function() { * return function(entries) { * return { * ...entries, * 'entry-1': { * label: 'My Entry', * action: function() { alert("I have been clicked!"); } * } * }; * } * } * }; * * palette.registerProvider(800, paletteProvider); */ Palette.prototype.registerProvider = function(priority, provider) { if (!provider) { provider = priority; priority = DEFAULT_PRIORITY; } this._eventBus.on('palette.getProviders', priority, function(event) { event.providers.push(provider); }); this._rebuild(); }; /** * Returns the palette entries * * @return {Object} map of entries */ Palette.prototype.getEntries = function() { var providers = this._getProviders(); return providers.reduce(addPaletteEntries, {}); }; Palette.prototype._rebuild = function() { if (!this._diagramInitialized) { return; } var providers = this._getProviders(); if (!providers.length) { return; } if (!this._container) { this._init(); } this._update(); }; /** * Initialize */ Palette.prototype._init = function() { var self = this; var eventBus = this._eventBus; var parentContainer = this._getParentContainer(); var container = this._container = domify(Palette.HTML_MARKUP); parentContainer.appendChild(container); classes$1(parentContainer).add(PALETTE_PREFIX + PALETTE_SHOWN_CLS); delegate.bind(container, ELEMENT_SELECTOR, 'click', function(event) { var target = event.delegateTarget; if (matchesSelector(target, TOGGLE_SELECTOR)) { return self.toggle(); } self.trigger('click', event); }); // prevent drag propagation componentEvent.bind(container, 'mousedown', function(event) { event.stopPropagation(); }); // prevent drag propagation delegate.bind(container, ENTRY_SELECTOR, 'dragstart', function(event) { self.trigger('dragstart', event); }); eventBus.on('canvas.resized', this._layoutChanged, this); eventBus.fire('palette.create', { container: container }); }; Palette.prototype._getProviders = function(id) { var event = this._eventBus.createEvent({ type: 'palette.getProviders', providers: [] }); this._eventBus.fire(event); return event.providers; }; /** * Update palette state. * * @param {Object} [state] { open, twoColumn } */ Palette.prototype._toggleState = function(state) { state = state || {}; var parent = this._getParentContainer(), container = this._container; var eventBus = this._eventBus; var twoColumn; var cls = classes$1(container), parentCls = classes$1(parent); if ('twoColumn' in state) { twoColumn = state.twoColumn; } else { twoColumn = this._needsCollapse(parent.clientHeight, this._entries || {}); } // always update two column cls.toggle(PALETTE_TWO_COLUMN_CLS, twoColumn); parentCls.toggle(PALETTE_PREFIX + PALETTE_TWO_COLUMN_CLS, twoColumn); if ('open' in state) { cls.toggle(PALETTE_OPEN_CLS, state.open); parentCls.toggle(PALETTE_PREFIX + PALETTE_OPEN_CLS, state.open); } eventBus.fire('palette.changed', { twoColumn: twoColumn, open: this.isOpen() }); }; Palette.prototype._update = function() { var entriesContainer = query('.djs-palette-entries', this._container), entries = this._entries = this.getEntries(); clear$1(entriesContainer); forEach$1(entries, function(entry, id) { var grouping = entry.group || 'default'; var container = query('[data-group=' + cssEscape(grouping) + ']', entriesContainer); if (!container) { container = domify('
    '); attr$1(container, 'data-group', grouping); entriesContainer.appendChild(container); } var html = entry.html || ( entry.separator ? '
    ' : '
    '); var control = domify(html); container.appendChild(control); if (!entry.separator) { attr$1(control, 'data-action', id); if (entry.title) { attr$1(control, 'title', entry.title); } if (entry.className) { addClasses(control, entry.className); } if (entry.imageUrl) { var image = domify(''); attr$1(image, 'src', entry.imageUrl); control.appendChild(image); } } }); // open after update this.open(); }; /** * Trigger an action available on the palette * * @param {string} action * @param {Event} event */ Palette.prototype.trigger = function(action, event, autoActivate) { var entries = this._entries, entry, handler, originalEvent, button = event.delegateTarget || event.target; if (!button) { return event.preventDefault(); } entry = entries[attr$1(button, 'data-action')]; // when user clicks on the palette and not on an action if (!entry) { return; } handler = entry.action; originalEvent = event.originalEvent || event; // simple action (via callback function) if (isFunction(handler)) { if (action === 'click') { handler(originalEvent, autoActivate); } } else { if (handler[action]) { handler[action](originalEvent, autoActivate); } } // silence other actions event.preventDefault(); }; Palette.prototype._layoutChanged = function() { this._toggleState({}); }; /** * Do we need to collapse to two columns? * * @param {number} availableHeight * @param {Object} entries * * @return {boolean} */ Palette.prototype._needsCollapse = function(availableHeight, entries) { // top margin + bottom toggle + bottom margin // implementors must override this method if they // change the palette styles var margin = 20 + 10 + 20; var entriesHeight = Object.keys(entries).length * 46; return availableHeight < entriesHeight + margin; }; /** * Close the palette */ Palette.prototype.close = function() { this._toggleState({ open: false, twoColumn: false }); }; /** * Open the palette */ Palette.prototype.open = function() { this._toggleState({ open: true }); }; Palette.prototype.toggle = function(open) { if (this.isOpen()) { this.close(); } else { this.open(); } }; Palette.prototype.isActiveTool = function(tool) { return tool && this._activeTool === tool; }; Palette.prototype.updateToolHighlight = function(name) { var entriesContainer, toolsContainer; if (!this._toolsContainer) { entriesContainer = query('.djs-palette-entries', this._container); this._toolsContainer = query('[data-group=tools]', entriesContainer); } toolsContainer = this._toolsContainer; forEach$1(toolsContainer.children, function(tool) { var actionName = tool.getAttribute('data-action'); if (!actionName) { return; } var toolClasses = classes$1(tool); actionName = actionName.replace('-tool', ''); if (toolClasses.contains('entry') && actionName === name) { toolClasses.add('highlighted-entry'); } else { toolClasses.remove('highlighted-entry'); } }); }; /** * Return true if the palette is opened. * * @example * * palette.open(); * * if (palette.isOpen()) { * // yes, we are open * } * * @return {boolean} true if palette is opened */ Palette.prototype.isOpen = function() { return classes$1(this._container).has(PALETTE_OPEN_CLS); }; /** * Get container the palette lives in. * * @return {Element} */ Palette.prototype._getParentContainer = function() { return this._canvas.getContainer(); }; /* markup definition */ Palette.HTML_MARKUP = '
    ' + '
    ' + '
    ' + '
    '; // helpers ////////////////////// function addClasses(element, classNames) { var classes = classes$1(element); var actualClassNames = isArray$3(classNames) ? classNames : classNames.split(/\s+/g); actualClassNames.forEach(function(cls) { classes.add(cls); }); } function addPaletteEntries(entries, provider) { var entriesOrUpdater = provider.getPaletteEntries(); if (isFunction(entriesOrUpdater)) { return entriesOrUpdater(entries); } forEach$1(entriesOrUpdater, function(entry, id) { entries[id] = entry; }); return entries; } var PaletteModule$1 = { __init__: [ 'palette' ], palette: [ 'type', Palette ] }; var LASSO_TOOL_CURSOR = 'crosshair'; function LassoTool( eventBus, canvas, dragging, elementRegistry, selection, toolManager, mouse) { this._selection = selection; this._dragging = dragging; this._mouse = mouse; var self = this; // lasso visuals implementation /** * A helper that realizes the selection box visual */ var visuals = { create: function(context) { var container = canvas.getActiveLayer(), frame; frame = context.frame = create$1('rect'); attr(frame, { class: 'djs-lasso-overlay', width: 1, height: 1, x: 0, y: 0 }); append(container, frame); }, update: function(context) { var frame = context.frame, bbox = context.bbox; attr(frame, { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }); }, remove: function(context) { if (context.frame) { remove$1(context.frame); } } }; toolManager.registerTool('lasso', { tool: 'lasso.selection', dragging: 'lasso' }); eventBus.on('lasso.selection.end', function(event) { var target = event.originalEvent.target; // only reactive on diagram click // on some occasions, event.hover is not set and we have to check if the target is an svg if (!event.hover && !(target instanceof SVGElement)) { return; } eventBus.once('lasso.selection.ended', function() { self.activateLasso(event.originalEvent, true); }); }); // lasso interaction implementation eventBus.on('lasso.end', function(event) { var bbox = toBBox(event); var elements = elementRegistry.filter(function(element) { return element; }); self.select(elements, bbox); }); eventBus.on('lasso.start', function(event) { var context = event.context; context.bbox = toBBox(event); visuals.create(context); }); eventBus.on('lasso.move', function(event) { var context = event.context; context.bbox = toBBox(event); visuals.update(context); }); eventBus.on('lasso.cleanup', function(event) { var context = event.context; visuals.remove(context); }); // event integration eventBus.on('element.mousedown', 1500, function(event) { if (!hasSecondaryModifier(event)) { return; } self.activateLasso(event.originalEvent); // we've handled the event return true; }); } LassoTool.$inject = [ 'eventBus', 'canvas', 'dragging', 'elementRegistry', 'selection', 'toolManager', 'mouse' ]; LassoTool.prototype.activateLasso = function(event, autoActivate) { this._dragging.init(event, 'lasso', { autoActivate: autoActivate, cursor: LASSO_TOOL_CURSOR, data: { context: {} } }); }; LassoTool.prototype.activateSelection = function(event, autoActivate) { this._dragging.init(event, 'lasso.selection', { trapClick: false, autoActivate: autoActivate, cursor: LASSO_TOOL_CURSOR, data: { context: {} } }); }; LassoTool.prototype.select = function(elements, bbox) { var selectedElements = getEnclosedElements(elements, bbox); this._selection.select(values(selectedElements)); }; LassoTool.prototype.toggle = function() { if (this.isActive()) { return this._dragging.cancel(); } var mouseEvent = this._mouse.getLastMoveEvent(); this.activateSelection(mouseEvent, !!mouseEvent); }; LassoTool.prototype.isActive = function() { var context = this._dragging.context(); return context && /^lasso/.test(context.prefix); }; function toBBox(event) { var start = { x: event.x - event.dx, y: event.y - event.dy }; var end = { x: event.x, y: event.y }; var bbox; if ((start.x <= end.x && start.y < end.y) || (start.x < end.x && start.y <= end.y)) { bbox = { x: start.x, y: start.y, width: end.x - start.x, height: end.y - start.y }; } else if ((start.x >= end.x && start.y < end.y) || (start.x > end.x && start.y <= end.y)) { bbox = { x: end.x, y: start.y, width: start.x - end.x, height: end.y - start.y }; } else if ((start.x <= end.x && start.y > end.y) || (start.x < end.x && start.y >= end.y)) { bbox = { x: start.x, y: end.y, width: end.x - start.x, height: start.y - end.y }; } else if ((start.x >= end.x && start.y > end.y) || (start.x > end.x && start.y >= end.y)) { bbox = { x: end.x, y: end.y, width: start.x - end.x, height: start.y - end.y }; } else { bbox = { x: end.x, y: end.y, width: 0, height: 0 }; } return bbox; } var LassoToolModule = { __depends__: [ ToolManagerModule, MouseModule ], __init__: [ 'lassoTool' ], lassoTool: [ 'type', LassoTool ] }; var HIGH_PRIORITY$1 = 1500; var HAND_CURSOR = 'grab'; function HandTool( eventBus, canvas, dragging, injector, toolManager, mouse) { this._dragging = dragging; this._mouse = mouse; var self = this, keyboard = injector.get('keyboard', false); toolManager.registerTool('hand', { tool: 'hand', dragging: 'hand.move' }); eventBus.on('element.mousedown', HIGH_PRIORITY$1, function(event) { if (!hasPrimaryModifier(event)) { return; } self.activateMove(event.originalEvent, true); return false; }); keyboard && keyboard.addListener(HIGH_PRIORITY$1, function(e) { if (!isSpace(e.keyEvent) || self.isActive()) { return; } var mouseEvent = self._mouse.getLastMoveEvent(); self.activateMove(mouseEvent, !!mouseEvent); }, 'keyboard.keydown'); keyboard && keyboard.addListener(HIGH_PRIORITY$1, function(e) { if (!isSpace(e.keyEvent) || !self.isActive()) { return; } self.toggle(); }, 'keyboard.keyup'); eventBus.on('hand.end', function(event) { var target = event.originalEvent.target; // only reactive on diagram click // on some occasions, event.hover is not set and we have to check if the target is an svg if (!event.hover && !(target instanceof SVGElement)) { return false; } eventBus.once('hand.ended', function() { self.activateMove(event.originalEvent, { reactivate: true }); }); }); eventBus.on('hand.move.move', function(event) { var scale = canvas.viewbox().scale; canvas.scroll({ dx: event.dx * scale, dy: event.dy * scale }); }); eventBus.on('hand.move.end', function(event) { var context = event.context, reactivate = context.reactivate; // Don't reactivate if the user is using the keyboard keybinding if (!hasPrimaryModifier(event) && reactivate) { eventBus.once('hand.move.ended', function(event) { self.activateHand(event.originalEvent, true, true); }); } return false; }); } HandTool.$inject = [ 'eventBus', 'canvas', 'dragging', 'injector', 'toolManager', 'mouse' ]; HandTool.prototype.activateMove = function(event, autoActivate, context) { if (typeof autoActivate === 'object') { context = autoActivate; autoActivate = false; } this._dragging.init(event, 'hand.move', { autoActivate: autoActivate, cursor: HAND_CURSOR, data: { context: context || {} } }); }; HandTool.prototype.activateHand = function(event, autoActivate, reactivate) { this._dragging.init(event, 'hand', { trapClick: false, autoActivate: autoActivate, cursor: HAND_CURSOR, data: { context: { reactivate: reactivate } } }); }; HandTool.prototype.toggle = function() { if (this.isActive()) { return this._dragging.cancel(); } var mouseEvent = this._mouse.getLastMoveEvent(); this.activateHand(mouseEvent, !!mouseEvent); }; HandTool.prototype.isActive = function() { var context = this._dragging.context(); if (context) { return /^(hand|hand\.move)$/.test(context.prefix); } return false; }; // helpers ////////// function isSpace(keyEvent) { return isKey(' ', keyEvent); } var HandToolModule = { __depends__: [ ToolManagerModule, MouseModule ], __init__: [ 'handTool' ], handTool: [ 'type', HandTool ] }; var MARKER_OK = 'connect-ok', MARKER_NOT_OK = 'connect-not-ok'; /** * @class * @constructor * * @param {EventBus} eventBus * @param {Dragging} dragging * @param {Connect} connect * @param {Canvas} canvas * @param {ToolManager} toolManager * @param {Rules} rules * @param {Mouse} mouse */ function GlobalConnect( eventBus, dragging, connect, canvas, toolManager, rules, mouse) { var self = this; this._dragging = dragging; this._rules = rules; this._mouse = mouse; toolManager.registerTool('global-connect', { tool: 'global-connect', dragging: 'global-connect.drag' }); eventBus.on('global-connect.hover', function(event) { var context = event.context, startTarget = event.hover; var canStartConnect = context.canStartConnect = self.canStartConnect(startTarget); // simply ignore hover if (canStartConnect === null) { return; } context.startTarget = startTarget; canvas.addMarker(startTarget, canStartConnect ? MARKER_OK : MARKER_NOT_OK); }); eventBus.on([ 'global-connect.out', 'global-connect.cleanup' ], function(event) { var startTarget = event.context.startTarget, canStartConnect = event.context.canStartConnect; if (startTarget) { canvas.removeMarker(startTarget, canStartConnect ? MARKER_OK : MARKER_NOT_OK); } }); eventBus.on([ 'global-connect.ended' ], function(event) { var context = event.context, startTarget = context.startTarget, startPosition = { x: event.x, y: event.y }; var canStartConnect = self.canStartConnect(startTarget); if (!canStartConnect) { return; } eventBus.once('element.out', function() { eventBus.once([ 'connect.ended', 'connect.canceled' ], function() { eventBus.fire('global-connect.drag.ended'); }); connect.start(null, startTarget, startPosition); }); return false; }); } GlobalConnect.$inject = [ 'eventBus', 'dragging', 'connect', 'canvas', 'toolManager', 'rules', 'mouse' ]; /** * Initiates tool activity. */ GlobalConnect.prototype.start = function(event, autoActivate) { this._dragging.init(event, 'global-connect', { autoActivate: autoActivate, trapClick: false, data: { context: {} } }); }; GlobalConnect.prototype.toggle = function() { if (this.isActive()) { return this._dragging.cancel(); } var mouseEvent = this._mouse.getLastMoveEvent(); return this.start(mouseEvent, !!mouseEvent); }; GlobalConnect.prototype.isActive = function() { var context = this._dragging.context(); return context && /^global-connect/.test(context.prefix); }; /** * Check if source shape can initiate connection. * * @param {Shape} startTarget * @return {boolean} */ GlobalConnect.prototype.canStartConnect = function(startTarget) { return this._rules.allowed('connection.start', { source: startTarget }); }; var GlobalConnectModule = { __depends__: [ ConnectModule, RulesModule$1, DraggingModule, ToolManagerModule, MouseModule ], globalConnect: [ 'type', GlobalConnect ] }; /** * A palette provider for BPMN 2.0 elements. */ function PaletteProvider( palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect, translate) { this._palette = palette; this._create = create; this._elementFactory = elementFactory; this._spaceTool = spaceTool; this._lassoTool = lassoTool; this._handTool = handTool; this._globalConnect = globalConnect; this._translate = translate; palette.registerProvider(this); } PaletteProvider.$inject = [ 'palette', 'create', 'elementFactory', 'spaceTool', 'lassoTool', 'handTool', 'globalConnect', 'translate' ]; PaletteProvider.prototype.getPaletteEntries = function(element) { var actions = {}, create = this._create, elementFactory = this._elementFactory, spaceTool = this._spaceTool, lassoTool = this._lassoTool, handTool = this._handTool, globalConnect = this._globalConnect, translate = this._translate; function createAction(type, group, className, title, options) { function createListener(event) { var shape = elementFactory.createShape(assign({ type: type }, options)); if (options) { var di = getDi(shape); di.isExpanded = options.isExpanded; } create.start(event, shape); } var shortType = type.replace(/^bpmn:/, ''); return { group: group, className: className, title: title || translate('Create {type}', { type: shortType }), action: { dragstart: createListener, click: createListener } }; } function createSubprocess(event) { var subProcess = elementFactory.createShape({ type: 'bpmn:SubProcess', x: 0, y: 0, isExpanded: true }); var startEvent = elementFactory.createShape({ type: 'bpmn:StartEvent', x: 40, y: 82, parent: subProcess }); create.start(event, [ subProcess, startEvent ], { hints: { autoSelect: [ subProcess ] } }); } function createParticipant(event) { create.start(event, elementFactory.createParticipantShape()); } assign(actions, { 'hand-tool': { group: 'tools', className: 'bpmn-icon-hand-tool', title: translate('Activate the hand tool'), action: { click: function(event) { handTool.activateHand(event); } } }, 'lasso-tool': { group: 'tools', className: 'bpmn-icon-lasso-tool', title: translate('Activate the lasso tool'), action: { click: function(event) { lassoTool.activateSelection(event); } } }, 'space-tool': { group: 'tools', className: 'bpmn-icon-space-tool', title: translate('Activate the create/remove space tool'), action: { click: function(event) { spaceTool.activateSelection(event); } } }, 'global-connect-tool': { group: 'tools', className: 'bpmn-icon-connection-multi', title: translate('Activate the global connect tool'), action: { click: function(event) { globalConnect.start(event); } } }, 'tool-separator': { group: 'tools', separator: true }, 'create.start-event': createAction( 'bpmn:StartEvent', 'event', 'bpmn-icon-start-event-none', translate('Create StartEvent') ), 'create.intermediate-event': createAction( 'bpmn:IntermediateThrowEvent', 'event', 'bpmn-icon-intermediate-event-none', translate('Create Intermediate/Boundary Event') ), 'create.end-event': createAction( 'bpmn:EndEvent', 'event', 'bpmn-icon-end-event-none', translate('Create EndEvent') ), 'create.exclusive-gateway': createAction( 'bpmn:ExclusiveGateway', 'gateway', 'bpmn-icon-gateway-none', translate('Create Gateway') ), 'create.task': createAction( 'bpmn:Task', 'activity', 'bpmn-icon-task', translate('Create Task') ), 'create.data-object': createAction( 'bpmn:DataObjectReference', 'data-object', 'bpmn-icon-data-object', translate('Create DataObjectReference') ), 'create.data-store': createAction( 'bpmn:DataStoreReference', 'data-store', 'bpmn-icon-data-store', translate('Create DataStoreReference') ), 'create.subprocess-expanded': { group: 'activity', className: 'bpmn-icon-subprocess-expanded', title: translate('Create expanded SubProcess'), action: { dragstart: createSubprocess, click: createSubprocess } }, 'create.participant-expanded': { group: 'collaboration', className: 'bpmn-icon-participant', title: translate('Create Pool/Participant'), action: { dragstart: createParticipant, click: createParticipant } }, 'create.group': createAction( 'bpmn:Group', 'artifact', 'bpmn-icon-group', translate('Create Group') ), }); return actions; }; var PaletteModule = { __depends__: [ PaletteModule$1, CreateModule, SpaceToolModule, LassoToolModule, HandToolModule, GlobalConnectModule, translate ], __init__: [ 'paletteProvider' ], paletteProvider: [ 'type', PaletteProvider ] }; var LOW_PRIORITY = 250; function BpmnReplacePreview( eventBus, elementRegistry, elementFactory, canvas, previewSupport) { CommandInterceptor.call(this, eventBus); /** * Replace the visuals of all elements in the context which can be replaced * * @param {Object} context */ function replaceVisual(context) { var replacements = context.canExecute.replacements; forEach$1(replacements, function(replacement) { var id = replacement.oldElementId; var newElement = { type: replacement.newElementType }; // if the visual of the element is already replaced if (context.visualReplacements[id]) { return; } var element = elementRegistry.get(id); assign(newElement, { x: element.x, y: element.y }); // create a temporary shape var tempShape = elementFactory.createShape(newElement); canvas.addShape(tempShape, element.parent); // select the original SVG element related to the element and hide it var gfx = query('[data-element-id="' + cssEscape(element.id) + '"]', context.dragGroup); if (gfx) { attr(gfx, { display: 'none' }); } // clone the gfx of the temporary shape and add it to the drag group var dragger = previewSupport.addDragger(tempShape, context.dragGroup); context.visualReplacements[id] = dragger; canvas.removeShape(tempShape); }); } /** * Restore the original visuals of the previously replaced elements * * @param {Object} context */ function restoreVisual(context) { var visualReplacements = context.visualReplacements; forEach$1(visualReplacements, function(dragger, id) { var originalGfx = query('[data-element-id="' + cssEscape(id) + '"]', context.dragGroup); if (originalGfx) { attr(originalGfx, { display: 'inline' }); } dragger.remove(); if (visualReplacements[id]) { delete visualReplacements[id]; } }); } eventBus.on('shape.move.move', LOW_PRIORITY, function(event) { var context = event.context, canExecute = context.canExecute; if (!context.visualReplacements) { context.visualReplacements = {}; } if (canExecute && canExecute.replacements) { replaceVisual(context); } else { restoreVisual(context); } }); } BpmnReplacePreview.$inject = [ 'eventBus', 'elementRegistry', 'elementFactory', 'canvas', 'previewSupport' ]; e(BpmnReplacePreview, CommandInterceptor); var ReplacePreviewModule = { __depends__: [ PreviewSupportModule ], __init__: [ 'bpmnReplacePreview' ], bpmnReplacePreview: [ 'type', BpmnReplacePreview ] }; var HIGHER_PRIORITY$2 = 1250; var BOUNDARY_TO_HOST_THRESHOLD = 40; var TARGET_BOUNDS_PADDING = 20, TASK_BOUNDS_PADDING = 10; var TARGET_CENTER_PADDING = 20; var AXES = [ 'x', 'y' ]; var abs = Math.abs; /** * Snap during connect. * * @param {EventBus} eventBus */ function BpmnConnectSnapping(eventBus) { eventBus.on([ 'connect.hover', 'connect.move', 'connect.end', ], HIGHER_PRIORITY$2, function(event) { var context = event.context, canExecute = context.canExecute, start = context.start, hover = context.hover, source = context.source, target = context.target; // do NOT snap on CMD if (event.originalEvent && isCmd(event.originalEvent)) { return; } if (!context.initialConnectionStart) { context.initialConnectionStart = context.connectionStart; } // snap hover if (canExecute && hover) { snapToShape(event, hover, getTargetBoundsPadding(hover)); } if (hover && isAnyType(canExecute, [ 'bpmn:Association', 'bpmn:DataInputAssociation', 'bpmn:DataOutputAssociation', 'bpmn:SequenceFlow' ])) { context.connectionStart = mid$2(start); // snap hover if (isAny(hover, [ 'bpmn:Event', 'bpmn:Gateway' ])) { snapToPosition(event, mid$2(hover)); } // snap hover if (isAny(hover, [ 'bpmn:Task', 'bpmn:SubProcess' ])) { snapToTargetMid(event, hover); } // snap source and target if (is$1(source, 'bpmn:BoundaryEvent') && target === source.host) { snapBoundaryEventLoop(event); } } else if (isType(canExecute, 'bpmn:MessageFlow')) { if (is$1(start, 'bpmn:Event')) { // snap start context.connectionStart = mid$2(start); } if (is$1(hover, 'bpmn:Event')) { // snap hover snapToPosition(event, mid$2(hover)); } } else { // un-snap source context.connectionStart = context.initialConnectionStart; } }); } BpmnConnectSnapping.$inject = [ 'eventBus' ]; // helpers ////////// // snap to target if event in target function snapToShape(event, target, padding) { AXES.forEach(function(axis) { var dimensionForAxis = getDimensionForAxis(axis, target); if (event[ axis ] < target[ axis ] + padding) { setSnapped(event, axis, target[ axis ] + padding); } else if (event[ axis ] > target[ axis ] + dimensionForAxis - padding) { setSnapped(event, axis, target[ axis ] + dimensionForAxis - padding); } }); } // snap to target mid if event in target mid function snapToTargetMid(event, target) { var targetMid = mid$2(target); AXES.forEach(function(axis) { if (isMid(event, target, axis)) { setSnapped(event, axis, targetMid[ axis ]); } }); } // snap to prevent loop overlapping boundary event function snapBoundaryEventLoop(event) { var context = event.context, source = context.source, target = context.target; if (isReverse(context)) { return; } var sourceMid = mid$2(source), orientation = getOrientation(sourceMid, target, -10), axes = []; if (/top|bottom/.test(orientation)) { axes.push('x'); } if (/left|right/.test(orientation)) { axes.push('y'); } axes.forEach(function(axis) { var coordinate = event[ axis ], newCoordinate; if (abs(coordinate - sourceMid[ axis ]) < BOUNDARY_TO_HOST_THRESHOLD) { if (coordinate > sourceMid[ axis ]) { newCoordinate = sourceMid[ axis ] + BOUNDARY_TO_HOST_THRESHOLD; } else { newCoordinate = sourceMid[ axis ] - BOUNDARY_TO_HOST_THRESHOLD; } setSnapped(event, axis, newCoordinate); } }); } function snapToPosition(event, position) { setSnapped(event, 'x', position.x); setSnapped(event, 'y', position.y); } function isType(attrs, type) { return attrs && attrs.type === type; } function isAnyType(attrs, types) { return some(types, function(type) { return isType(attrs, type); }); } function getDimensionForAxis(axis, element) { return axis === 'x' ? element.width : element.height; } function getTargetBoundsPadding(target) { if (is$1(target, 'bpmn:Task')) { return TASK_BOUNDS_PADDING; } else { return TARGET_BOUNDS_PADDING; } } function isMid(event, target, axis) { return event[ axis ] > target[ axis ] + TARGET_CENTER_PADDING && event[ axis ] < target[ axis ] + getDimensionForAxis(axis, target) - TARGET_CENTER_PADDING; } function isReverse(context) { var hover = context.hover, source = context.source; return hover && source && hover === source; } /** * A snap context, containing the (possibly incomplete) * mappings of drop targets (to identify the snapping) * to computed snap points. */ function SnapContext() { /** * Map mapping drop targets to * a list of possible snappings. * * @type {Object} */ this._targets = {}; /** * Map initial positioning of element * regarding various snap directions. * * @type {Object} */ this._snapOrigins = {}; /** * List of snap locations * * @type {Array} */ this._snapLocations = []; /** * Map> of default snapping locations * * @type {Object} */ this._defaultSnaps = {}; } SnapContext.prototype.getSnapOrigin = function(snapLocation) { return this._snapOrigins[snapLocation]; }; SnapContext.prototype.setSnapOrigin = function(snapLocation, initialValue) { this._snapOrigins[snapLocation] = initialValue; if (this._snapLocations.indexOf(snapLocation) === -1) { this._snapLocations.push(snapLocation); } }; SnapContext.prototype.addDefaultSnap = function(type, point) { var snapValues = this._defaultSnaps[type]; if (!snapValues) { snapValues = this._defaultSnaps[type] = []; } snapValues.push(point); }; /** * Return a number of initialized snaps, i.e. snap locations such as * top-left, mid, bottom-right and so forth. * * @return {Array} snapLocations */ SnapContext.prototype.getSnapLocations = function() { return this._snapLocations; }; /** * Set the snap locations for this context. * * The order of locations determines precedence. * * @param {Array} snapLocations */ SnapContext.prototype.setSnapLocations = function(snapLocations) { this._snapLocations = snapLocations; }; /** * Get snap points for a given target * * @param {Element|string} target */ SnapContext.prototype.pointsForTarget = function(target) { var targetId = target.id || target; var snapPoints = this._targets[targetId]; if (!snapPoints) { snapPoints = this._targets[targetId] = new SnapPoints(); snapPoints.initDefaults(this._defaultSnaps); } return snapPoints; }; /** * Creates the snap points and initializes them with the * given default values. * * @param {Object>} [defaultPoints] */ function SnapPoints(defaultSnaps) { /** * Map>> mapping snap locations, * i.e. top-left, bottom-right, center to actual snap values. * * @type {Object} */ this._snapValues = {}; } SnapPoints.prototype.add = function(snapLocation, point) { var snapValues = this._snapValues[snapLocation]; if (!snapValues) { snapValues = this._snapValues[snapLocation] = { x: [], y: [] }; } if (snapValues.x.indexOf(point.x) === -1) { snapValues.x.push(point.x); } if (snapValues.y.indexOf(point.y) === -1) { snapValues.y.push(point.y); } }; SnapPoints.prototype.snap = function(point, snapLocation, axis, tolerance) { var snappingValues = this._snapValues[snapLocation]; return snappingValues && snapTo(point[axis], snappingValues[axis], tolerance); }; /** * Initialize a number of default snapping points. * * @param {Object} defaultSnaps */ SnapPoints.prototype.initDefaults = function(defaultSnaps) { var self = this; forEach$1(defaultSnaps || {}, function(snapPoints, snapLocation) { forEach$1(snapPoints, function(point) { self.add(snapLocation, point); }); }); }; var HIGHER_PRIORITY$1 = 1250; /** * Snap during create and move. * * @param {EventBus} elementRegistry * @param {EventBus} eventBus * @param {Snapping} snapping */ function CreateMoveSnapping(elementRegistry, eventBus, snapping) { var self = this; this._elementRegistry = elementRegistry; eventBus.on([ 'create.start', 'shape.move.start' ], function(event) { self.initSnap(event); }); eventBus.on([ 'create.move', 'create.end', 'shape.move.move', 'shape.move.end' ], HIGHER_PRIORITY$1, function(event) { var context = event.context, shape = context.shape, snapContext = context.snapContext, target = context.target; if (event.originalEvent && isCmd(event.originalEvent)) { return; } if (isSnapped(event) || !target) { return; } var snapPoints = snapContext.pointsForTarget(target); if (!snapPoints.initialized) { snapPoints = self.addSnapTargetPoints(snapPoints, shape, target); snapPoints.initialized = true; } snapping.snap(event, snapPoints); }); eventBus.on([ 'create.cleanup', 'shape.move.cleanup' ], function() { snapping.hide(); }); } CreateMoveSnapping.$inject = [ 'elementRegistry', 'eventBus', 'snapping' ]; CreateMoveSnapping.prototype.initSnap = function(event) { var elementRegistry = this._elementRegistry; var context = event.context, shape = context.shape, snapContext = context.snapContext; if (!snapContext) { snapContext = context.snapContext = new SnapContext(); } var shapeMid; if (elementRegistry.get(shape.id)) { // move shapeMid = mid$2(shape, event); } else { // create shapeMid = { x: event.x + mid$2(shape).x, y: event.y + mid$2(shape).y }; } var shapeTopLeft = { x: shapeMid.x - shape.width / 2, y: shapeMid.y - shape.height / 2 }, shapeBottomRight = { x: shapeMid.x + shape.width / 2, y: shapeMid.y + shape.height / 2 }; snapContext.setSnapOrigin('mid', { x: shapeMid.x - event.x, y: shapeMid.y - event.y }); // snap labels to mid only if (isLabel$1(shape)) { return snapContext; } snapContext.setSnapOrigin('top-left', { x: shapeTopLeft.x - event.x, y: shapeTopLeft.y - event.y }); snapContext.setSnapOrigin('bottom-right', { x: shapeBottomRight.x - event.x, y: shapeBottomRight.y - event.y }); return snapContext; }; CreateMoveSnapping.prototype.addSnapTargetPoints = function(snapPoints, shape, target) { var snapTargets = this.getSnapTargets(shape, target); forEach$1(snapTargets, function(snapTarget) { // handle labels if (isLabel$1(snapTarget)) { if (isLabel$1(shape)) { snapPoints.add('mid', mid$2(snapTarget)); } return; } // handle connections if (isConnection$1(snapTarget)) { // ignore single segment connections if (snapTarget.waypoints.length < 3) { return; } // ignore first and last waypoint var waypoints = snapTarget.waypoints.slice(1, -1); forEach$1(waypoints, function(waypoint) { snapPoints.add('mid', waypoint); }); return; } // handle shapes snapPoints.add('mid', mid$2(snapTarget)); }); if (!isNumber(shape.x) || !isNumber(shape.y)) { return snapPoints; } // snap to original position when moving if (this._elementRegistry.get(shape.id)) { snapPoints.add('mid', mid$2(shape)); } return snapPoints; }; CreateMoveSnapping.prototype.getSnapTargets = function(shape, target) { return getChildren(target).filter(function(child) { return !isHidden$1(child); }); }; // helpers ////////// function isConnection$1(element) { return !!element.waypoints; } function isHidden$1(element) { return !!element.hidden; } function isLabel$1(element) { return !!element.labelTarget; } var HIGH_PRIORITY = 1500; /** * Snap during create and move. * * @param {EventBus} eventBus * @param {Injector} injector */ function BpmnCreateMoveSnapping(eventBus, injector) { injector.invoke(CreateMoveSnapping, this); // creating first participant eventBus.on([ 'create.move', 'create.end' ], HIGH_PRIORITY, setSnappedIfConstrained); // snap boundary events eventBus.on([ 'create.move', 'create.end', 'shape.move.move', 'shape.move.end' ], HIGH_PRIORITY, function(event) { var context = event.context, canExecute = context.canExecute, target = context.target; var canAttach = canExecute && (canExecute === 'attach' || canExecute.attach); if (canAttach && !isSnapped(event)) { snapBoundaryEvent(event, target); } }); } e(BpmnCreateMoveSnapping, CreateMoveSnapping); BpmnCreateMoveSnapping.$inject = [ 'eventBus', 'injector' ]; BpmnCreateMoveSnapping.prototype.initSnap = function(event) { var snapContext = CreateMoveSnapping.prototype.initSnap.call(this, event); var shape = event.shape; var isMove = !!this._elementRegistry.get(shape.id); // snap to docking points forEach$1(shape.outgoing, function(connection) { var docking = connection.waypoints[0]; docking = docking.original || docking; snapContext.setSnapOrigin(connection.id + '-docking', getDockingSnapOrigin(docking, isMove, event)); }); forEach$1(shape.incoming, function(connection) { var docking = connection.waypoints[connection.waypoints.length - 1]; docking = docking.original || docking; snapContext.setSnapOrigin(connection.id + '-docking', getDockingSnapOrigin(docking, isMove, event)); }); if (is$1(shape, 'bpmn:Participant')) { // snap to borders with higher priority snapContext.setSnapLocations([ 'top-left', 'bottom-right', 'mid' ]); } return snapContext; }; BpmnCreateMoveSnapping.prototype.addSnapTargetPoints = function(snapPoints, shape, target) { CreateMoveSnapping.prototype.addSnapTargetPoints.call(this, snapPoints, shape, target); var snapTargets = this.getSnapTargets(shape, target); forEach$1(snapTargets, function(snapTarget) { // handle TRBL alignment // // * with container elements // * with text annotations if (isContainer(snapTarget) || areAll([ shape, snapTarget ], 'bpmn:TextAnnotation')) { snapPoints.add('top-left', topLeft(snapTarget)); snapPoints.add('bottom-right', bottomRight(snapTarget)); } }); var elementRegistry = this._elementRegistry; // snap to docking points if not create mode forEach$1(shape.incoming, function(connection) { if (elementRegistry.get(shape.id)) { if (!includes(snapTargets, connection.source)) { snapPoints.add('mid', getMid(connection.source)); } var docking = connection.waypoints[0]; snapPoints.add(connection.id + '-docking', docking.original || docking); } }); forEach$1(shape.outgoing, function(connection) { if (elementRegistry.get(shape.id)) { if (!includes(snapTargets, connection.target)) { snapPoints.add('mid', getMid(connection.target)); } var docking = connection.waypoints[ connection.waypoints.length - 1 ]; snapPoints.add(connection.id + '-docking', docking.original || docking); } }); // add sequence flow parents as snap targets if (is$1(target, 'bpmn:SequenceFlow')) { snapPoints = this.addSnapTargetPoints(snapPoints, shape, target.parent); } return snapPoints; }; BpmnCreateMoveSnapping.prototype.getSnapTargets = function(shape, target) { return CreateMoveSnapping.prototype.getSnapTargets.call(this, shape, target) .filter(function(snapTarget) { // do not snap to lanes return !is$1(snapTarget, 'bpmn:Lane'); }); }; // helpers ////////// function snapBoundaryEvent(event, target) { var targetTRBL = asTRBL(target); var direction = getBoundaryAttachment(event, target); var context = event.context, shape = context.shape; var offset; if (shape.parent) { offset = { x: 0, y: 0 }; } else { offset = getMid(shape); } if (/top/.test(direction)) { setSnapped(event, 'y', targetTRBL.top - offset.y); } else if (/bottom/.test(direction)) { setSnapped(event, 'y', targetTRBL.bottom - offset.y); } if (/left/.test(direction)) { setSnapped(event, 'x', targetTRBL.left - offset.x); } else if (/right/.test(direction)) { setSnapped(event, 'x', targetTRBL.right - offset.x); } } function areAll(elements, type) { return elements.every(function(el) { return is$1(el, type); }); } function isContainer(element) { if (is$1(element, 'bpmn:SubProcess') && isExpanded(element)) { return true; } return is$1(element, 'bpmn:Participant'); } function setSnappedIfConstrained(event) { var context = event.context, createConstraints = context.createConstraints; if (!createConstraints) { return; } var top = createConstraints.top, right = createConstraints.right, bottom = createConstraints.bottom, left = createConstraints.left; if ((left && left >= event.x) || (right && right <= event.x)) { setSnapped(event, 'x', event.x); } if ((top && top >= event.y) || (bottom && bottom <= event.y)) { setSnapped(event, 'y', event.y); } } function includes(array, value) { return array.indexOf(value) !== -1; } function getDockingSnapOrigin(docking, isMove, event) { return isMove ? ( { x: docking.x - event.x, y: docking.y - event.y } ) : { x: docking.x, y: docking.y }; } var HIGHER_PRIORITY = 1250; /** * Snap during resize. * * @param {EventBus} eventBus * @param {Snapping} snapping */ function ResizeSnapping(eventBus, snapping) { var self = this; eventBus.on([ 'resize.start' ], function(event) { self.initSnap(event); }); eventBus.on([ 'resize.move', 'resize.end', ], HIGHER_PRIORITY, function(event) { var context = event.context, shape = context.shape, parent = shape.parent, direction = context.direction, snapContext = context.snapContext; if (event.originalEvent && isCmd(event.originalEvent)) { return; } if (isSnapped(event)) { return; } var snapPoints = snapContext.pointsForTarget(parent); if (!snapPoints.initialized) { snapPoints = self.addSnapTargetPoints(snapPoints, shape, parent, direction); snapPoints.initialized = true; } if (isHorizontal(direction)) { setSnapped(event, 'x', event.x); } if (isVertical(direction)) { setSnapped(event, 'y', event.y); } snapping.snap(event, snapPoints); }); eventBus.on([ 'resize.cleanup' ], function() { snapping.hide(); }); } ResizeSnapping.prototype.initSnap = function(event) { var context = event.context, shape = context.shape, direction = context.direction, snapContext = context.snapContext; if (!snapContext) { snapContext = context.snapContext = new SnapContext(); } var snapOrigin = getSnapOrigin(shape, direction); snapContext.setSnapOrigin('corner', { x: snapOrigin.x - event.x, y: snapOrigin.y - event.y }); return snapContext; }; ResizeSnapping.prototype.addSnapTargetPoints = function(snapPoints, shape, target, direction) { var snapTargets = this.getSnapTargets(shape, target); forEach$1(snapTargets, function(snapTarget) { snapPoints.add('corner', bottomRight(snapTarget)); snapPoints.add('corner', topLeft(snapTarget)); }); snapPoints.add('corner', getSnapOrigin(shape, direction)); return snapPoints; }; ResizeSnapping.$inject = [ 'eventBus', 'snapping' ]; ResizeSnapping.prototype.getSnapTargets = function(shape, target) { return getChildren(target).filter(function(child) { return !isAttached(child, shape) && !isConnection(child) && !isHidden(child) && !isLabel(child); }); }; // helpers ////////// function getSnapOrigin(shape, direction) { var mid = getMid(shape), trbl = asTRBL(shape); var snapOrigin = { x: mid.x, y: mid.y }; if (direction.indexOf('n') !== -1) { snapOrigin.y = trbl.top; } else if (direction.indexOf('s') !== -1) { snapOrigin.y = trbl.bottom; } if (direction.indexOf('e') !== -1) { snapOrigin.x = trbl.right; } else if (direction.indexOf('w') !== -1) { snapOrigin.x = trbl.left; } return snapOrigin; } function isAttached(element, host) { return element.host === host; } function isConnection(element) { return !!element.waypoints; } function isHidden(element) { return !!element.hidden; } function isLabel(element) { return !!element.labelTarget; } function isHorizontal(direction) { return direction === 'n' || direction === 's'; } function isVertical(direction) { return direction === 'e' || direction === 'w'; } var SNAP_TOLERANCE = 7; var SNAP_LINE_HIDE_DELAY = 1000; /** * Generic snapping feature. * * @param {EventBus} eventBus * @param {Canvas} canvas */ function Snapping(canvas) { this._canvas = canvas; // delay hide by 1000 seconds since last snap this._asyncHide = debounce(bind(this.hide, this), SNAP_LINE_HIDE_DELAY); } Snapping.$inject = [ 'canvas' ]; /** * Snap an event to given snap points. * * @param {Event} event * @param {SnapPoints} snapPoints */ Snapping.prototype.snap = function(event, snapPoints) { var context = event.context, snapContext = context.snapContext, snapLocations = snapContext.getSnapLocations(); var snapping = { x: isSnapped(event, 'x'), y: isSnapped(event, 'y') }; forEach$1(snapLocations, function(location) { var snapOrigin = snapContext.getSnapOrigin(location); var snapCurrent = { x: event.x + snapOrigin.x, y: event.y + snapOrigin.y }; // snap both axis if not snapped already forEach$1([ 'x', 'y' ], function(axis) { var locationSnapping; if (!snapping[axis]) { locationSnapping = snapPoints.snap(snapCurrent, location, axis, SNAP_TOLERANCE); if (locationSnapping !== undefined) { snapping[axis] = { value: locationSnapping, originValue: locationSnapping - snapOrigin[axis] }; } } }); // no need to continue snapping if (snapping.x && snapping.y) { return false; } }); // show snap lines this.showSnapLine('vertical', snapping.x && snapping.x.value); this.showSnapLine('horizontal', snapping.y && snapping.y.value); // snap event forEach$1([ 'x', 'y' ], function(axis) { var axisSnapping = snapping[axis]; if (isObject(axisSnapping)) { setSnapped(event, axis, axisSnapping.originValue); } }); }; Snapping.prototype._createLine = function(orientation) { var root = this._canvas.getLayer('snap'); var line = create$1('path'); attr(line, { d: 'M0,0 L0,0' }); classes(line).add('djs-snap-line'); append(root, line); return { update: function(position) { if (!isNumber(position)) { attr(line, { display: 'none' }); } else { if (orientation === 'horizontal') { attr(line, { d: 'M-100000,' + position + ' L+100000,' + position, display: '' }); } else { attr(line, { d: 'M ' + position + ',-100000 L ' + position + ', +100000', display: '' }); } } } }; }; Snapping.prototype._createSnapLines = function() { this._snapLines = { horizontal: this._createLine('horizontal'), vertical: this._createLine('vertical') }; }; Snapping.prototype.showSnapLine = function(orientation, position) { var line = this.getSnapLine(orientation); if (line) { line.update(position); } this._asyncHide(); }; Snapping.prototype.getSnapLine = function(orientation) { if (!this._snapLines) { this._createSnapLines(); } return this._snapLines[orientation]; }; Snapping.prototype.hide = function() { forEach$1(this._snapLines, function(snapLine) { snapLine.update(); }); }; var SnappingModule$1 = { __init__: [ 'createMoveSnapping', 'resizeSnapping', 'snapping' ], createMoveSnapping: [ 'type', CreateMoveSnapping ], resizeSnapping: [ 'type', ResizeSnapping ], snapping: [ 'type', Snapping ] }; var SnappingModule = { __depends__: [ SnappingModule$1 ], __init__: [ 'connectSnapping', 'createMoveSnapping' ], connectSnapping: [ 'type', BpmnConnectSnapping ], createMoveSnapping: [ 'type', BpmnCreateMoveSnapping ] }; /** * Provides searching infrastructure */ function SearchPad(canvas, eventBus, overlays, selection) { this._open = false; this._results = []; this._eventMaps = []; this._canvas = canvas; this._eventBus = eventBus; this._overlays = overlays; this._selection = selection; // setup elements this._container = domify(SearchPad.BOX_HTML); this._searchInput = query(SearchPad.INPUT_SELECTOR, this._container); this._resultsContainer = query(SearchPad.RESULTS_CONTAINER_SELECTOR, this._container); // attach search pad this._canvas.getContainer().appendChild(this._container); // cleanup on destroy eventBus.on([ 'canvas.destroy', 'diagram.destroy' ], this.close, this); } SearchPad.$inject = [ 'canvas', 'eventBus', 'overlays', 'selection' ]; /** * Binds and keeps track of all event listereners */ SearchPad.prototype._bindEvents = function() { var self = this; function listen(el, selector, type, fn) { self._eventMaps.push({ el: el, type: type, listener: delegate.bind(el, selector, type, fn) }); } // close search on clicking anywhere outside listen(document, 'html', 'click', function(e) { self.close(); }); // stop event from propagating and closing search // focus on input listen(this._container, SearchPad.INPUT_SELECTOR, 'click', function(e) { e.stopPropagation(); e.delegateTarget.focus(); }); // preselect result on hover listen(this._container, SearchPad.RESULT_SELECTOR, 'mouseover', function(e) { e.stopPropagation(); self._scrollToNode(e.delegateTarget); self._preselect(e.delegateTarget); }); // selects desired result on mouse click listen(this._container, SearchPad.RESULT_SELECTOR, 'click', function(e) { e.stopPropagation(); self._select(e.delegateTarget); }); // prevent cursor in input from going left and right when using up/down to // navigate results listen(this._container, SearchPad.INPUT_SELECTOR, 'keydown', function(e) { // up if (e.keyCode === 38) { e.preventDefault(); } // down if (e.keyCode === 40) { e.preventDefault(); } }); // handle keyboard input listen(this._container, SearchPad.INPUT_SELECTOR, 'keyup', function(e) { // escape if (e.keyCode === 27) { return self.close(); } // enter if (e.keyCode === 13) { var selected = self._getCurrentResult(); return selected ? self._select(selected) : self.close(); } // up if (e.keyCode === 38) { return self._scrollToDirection(true); } // down if (e.keyCode === 40) { return self._scrollToDirection(); } // left && right // do not search while navigating text input if (e.keyCode === 37 || e.keyCode === 39) { return; } // anything else self._search(e.delegateTarget.value); }); }; /** * Unbinds all previously established listeners */ SearchPad.prototype._unbindEvents = function() { this._eventMaps.forEach(function(m) { delegate.unbind(m.el, m.type, m.listener); }); }; /** * Performs a search for the given pattern. * * @param {string} pattern */ SearchPad.prototype._search = function(pattern) { var self = this; this._clearResults(); // do not search on empty query if (!pattern || pattern === '') { return; } var searchResults = this._searchProvider.find(pattern); if (!searchResults.length) { return; } // append new results searchResults.forEach(function(result) { var id = result.element.id; var node = self._createResultNode(result, id); self._results[id] = { element: result.element, node: node }; }); // preselect first result var node = query(SearchPad.RESULT_SELECTOR, this._resultsContainer); this._scrollToNode(node); this._preselect(node); }; /** * Navigate to the previous/next result. Defaults to next result. * @param {boolean} previous */ SearchPad.prototype._scrollToDirection = function(previous) { var selected = this._getCurrentResult(); if (!selected) { return; } var node = previous ? selected.previousElementSibling : selected.nextElementSibling; if (node) { this._scrollToNode(node); this._preselect(node); } }; /** * Scroll to the node if it is not visible. * * @param {Element} node */ SearchPad.prototype._scrollToNode = function(node) { if (!node || node === this._getCurrentResult()) { return; } var nodeOffset = node.offsetTop; var containerScroll = this._resultsContainer.scrollTop; var bottomScroll = nodeOffset - this._resultsContainer.clientHeight + node.clientHeight; if (nodeOffset < containerScroll) { this._resultsContainer.scrollTop = nodeOffset; } else if (containerScroll < bottomScroll) { this._resultsContainer.scrollTop = bottomScroll; } }; /** * Clears all results data. */ SearchPad.prototype._clearResults = function() { clear$1(this._resultsContainer); this._results = []; this._resetOverlay(); this._eventBus.fire('searchPad.cleared'); }; /** * Get currently selected result. * * @return {Element} */ SearchPad.prototype._getCurrentResult = function() { return query(SearchPad.RESULT_SELECTED_SELECTOR, this._resultsContainer); }; /** * Create result DOM element within results container * that corresponds to a search result. * * 'result' : one of the elements returned by SearchProvider * 'id' : id attribute value to assign to the new DOM node * return : created DOM element * * @param {SearchResult} result * @param {string} id * @return {Element} */ SearchPad.prototype._createResultNode = function(result, id) { var node = domify(SearchPad.RESULT_HTML); // create only if available if (result.primaryTokens.length > 0) { createInnerTextNode(node, result.primaryTokens, SearchPad.RESULT_PRIMARY_HTML); } // secondary tokens (represent element ID) are allways available createInnerTextNode(node, result.secondaryTokens, SearchPad.RESULT_SECONDARY_HTML); attr$1(node, SearchPad.RESULT_ID_ATTRIBUTE, id); this._resultsContainer.appendChild(node); return node; }; /** * Register search element provider. * * SearchProvider.find - provides search function over own elements * (pattern) => [{ text: , element: }, ...] * * @param {SearchProvider} provider */ SearchPad.prototype.registerProvider = function(provider) { this._searchProvider = provider; }; /** * Open search pad. */ SearchPad.prototype.open = function() { if (!this._searchProvider) { throw new Error('no search provider registered'); } if (this.isOpen()) { return; } this._bindEvents(); this._open = true; classes$1(this._container).add('open'); this._searchInput.focus(); this._eventBus.fire('searchPad.opened'); }; /** * Close search pad. */ SearchPad.prototype.close = function() { if (!this.isOpen()) { return; } this._unbindEvents(); this._open = false; classes$1(this._container).remove('open'); this._clearResults(); this._searchInput.value = ''; this._searchInput.blur(); this._resetOverlay(); this._eventBus.fire('searchPad.closed'); }; /** * Toggles search pad on/off. */ SearchPad.prototype.toggle = function() { this.isOpen() ? this.close() : this.open(); }; /** * Report state of search pad. */ SearchPad.prototype.isOpen = function() { return this._open; }; /** * Preselect result entry. * * @param {Element} element */ SearchPad.prototype._preselect = function(node) { var selectedNode = this._getCurrentResult(); // already selected if (node === selectedNode) { return; } // removing preselection from current node if (selectedNode) { classes$1(selectedNode).remove(SearchPad.RESULT_SELECTED_CLASS); } var id = attr$1(node, SearchPad.RESULT_ID_ATTRIBUTE); var element = this._results[id].element; classes$1(node).add(SearchPad.RESULT_SELECTED_CLASS); this._resetOverlay(element); this._canvas.scrollToElement(element, { top: 400 }); this._selection.select(element); this._eventBus.fire('searchPad.preselected', element); }; /** * Select result node. * * @param {Element} element */ SearchPad.prototype._select = function(node) { var id = attr$1(node, SearchPad.RESULT_ID_ATTRIBUTE); var element = this._results[id].element; this.close(); this._resetOverlay(); this._canvas.scrollToElement(element, { top: 400 }); this._selection.select(element); this._eventBus.fire('searchPad.selected', element); }; /** * Reset overlay removes and, optionally, set * overlay to a new element. * * @param {Element} element */ SearchPad.prototype._resetOverlay = function(element) { if (this._overlayId) { this._overlays.remove(this._overlayId); } if (element) { var box = getBBox(element); var overlay = constructOverlay(box); this._overlayId = this._overlays.add(element, overlay); } }; /** * Construct overlay object for the given bounding box. * * @param {BoundingBox} box * @return {Object} */ function constructOverlay(box) { var offset = 6; var w = box.width + offset * 2; var h = box.height + offset * 2; var styles = { width: w + 'px', height: h + 'px' }; var html = domify('
    '); assign$1(html, styles); return { position: { bottom: h - offset, right: w - offset }, show: true, html: html }; } /** * Creates and appends child node from result tokens and HTML template. * * @param {Element} node * @param {Array} tokens * @param {string} template */ function createInnerTextNode(parentNode, tokens, template) { var text = createHtmlText(tokens); var childNode = domify(template); childNode.innerHTML = text; parentNode.appendChild(childNode); } /** * Create internal HTML markup from result tokens. * Caters for highlighting pattern matched tokens. * * @param {Array} tokens * @return {string} */ function createHtmlText(tokens) { var htmlText = ''; tokens.forEach(function(t) { if (t.matched) { htmlText += '' + escapeHTML(t.matched) + ''; } else { htmlText += escapeHTML(t.normal); } }); return htmlText !== '' ? htmlText : null; } /** * CONSTANTS */ SearchPad.CONTAINER_SELECTOR = '.djs-search-container'; SearchPad.INPUT_SELECTOR = '.djs-search-input input'; SearchPad.RESULTS_CONTAINER_SELECTOR = '.djs-search-results'; SearchPad.RESULT_SELECTOR = '.djs-search-result'; SearchPad.RESULT_SELECTED_CLASS = 'djs-search-result-selected'; SearchPad.RESULT_SELECTED_SELECTOR = '.' + SearchPad.RESULT_SELECTED_CLASS; SearchPad.RESULT_ID_ATTRIBUTE = 'data-result-id'; SearchPad.RESULT_HIGHLIGHT_CLASS = 'djs-search-highlight'; SearchPad.OVERLAY_CLASS = 'djs-search-overlay'; SearchPad.BOX_HTML = '
    ' + '
    ' + '' + '
    ' + '
    ' + '
    '; SearchPad.RESULT_HTML = '
    '; SearchPad.RESULT_PRIMARY_HTML = '
    '; SearchPad.RESULT_SECONDARY_HTML = '

    '; var SearchPadModule = { __depends__: [ OverlaysModule, SelectionModule ], searchPad: [ 'type', SearchPad ] }; /** * Provides ability to search through BPMN elements */ function BpmnSearchProvider(elementRegistry, searchPad, canvas) { this._elementRegistry = elementRegistry; this._canvas = canvas; searchPad.registerProvider(this); } BpmnSearchProvider.$inject = [ 'elementRegistry', 'searchPad', 'canvas' ]; /** * Finds all elements that match given pattern * * : * { * primaryTokens: >, * secondaryTokens: >, * element: * } * * : * { * normal|matched: * } * * @param {string} pattern * @return {Array} */ BpmnSearchProvider.prototype.find = function(pattern) { var rootElement = this._canvas.getRootElement(); var elements = this._elementRegistry.filter(function(element) { if (element.labelTarget) { return false; } return true; }); // do not include root element elements = filter(elements, function(element) { return element !== rootElement; }); elements = map(elements, function(element) { return { primaryTokens: matchAndSplit(getLabel(element), pattern), secondaryTokens: matchAndSplit(element.id, pattern), element: element }; }); // exclude non-matched elements elements = filter(elements, function(element) { return hasMatched(element.primaryTokens) || hasMatched(element.secondaryTokens); }); elements = sortBy(elements, function(element) { return getLabel(element.element) + element.element.id; }); return elements; }; function hasMatched(tokens) { var matched = filter(tokens, function(t) { return !!t.matched; }); return matched.length > 0; } function matchAndSplit(text, pattern) { var tokens = [], originalText = text; if (!text) { return tokens; } text = text.toLowerCase(); pattern = pattern.toLowerCase(); var i = text.indexOf(pattern); if (i > -1) { if (i !== 0) { tokens.push({ normal: originalText.substr(0, i) }); } tokens.push({ matched: originalText.substr(i, pattern.length) }); if (pattern.length + i < text.length) { tokens.push({ normal: originalText.substr(pattern.length + i, text.length) }); } } else { tokens.push({ normal: originalText }); } return tokens; } var SearchModule = { __depends__: [ SearchPadModule ], __init__: [ 'bpmnSearch' ], bpmnSearch: [ 'type', BpmnSearchProvider ] }; var initialDiagram = '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; /** * A modeler for BPMN 2.0 diagrams. * * * ## Extending the Modeler * * In order to extend the viewer pass extension modules to bootstrap via the * `additionalModules` option. An extension module is an object that exposes * named services. * * The following example depicts the integration of a simple * logging component that integrates with interaction events: * * * ```javascript * * // logging component * function InteractionLogger(eventBus) { * eventBus.on('element.hover', function(event) { * console.log() * }) * } * * InteractionLogger.$inject = [ 'eventBus' ]; // minification save * * // extension module * var extensionModule = { * __init__: [ 'interactionLogger' ], * interactionLogger: [ 'type', InteractionLogger ] * }; * * // extend the viewer * var bpmnModeler = new Modeler({ additionalModules: [ extensionModule ] }); * bpmnModeler.importXML(...); * ``` * * * ## Customizing / Replacing Components * * You can replace individual diagram components by redefining them in override modules. * This works for all components, including those defined in the core. * * Pass in override modules via the `options.additionalModules` flag like this: * * ```javascript * function CustomContextPadProvider(contextPad) { * * contextPad.registerProvider(this); * * this.getContextPadEntries = function(element) { * // no entries, effectively disable the context pad * return {}; * }; * } * * CustomContextPadProvider.$inject = [ 'contextPad' ]; * * var overrideModule = { * contextPadProvider: [ 'type', CustomContextPadProvider ] * }; * * var bpmnModeler = new Modeler({ additionalModules: [ overrideModule ]}); * ``` * * @param {Object} [options] configuration options to pass to the viewer * @param {DOMElement} [options.container] the container to render the viewer in, defaults to body. * @param {string|number} [options.width] the width of the viewer * @param {string|number} [options.height] the height of the viewer * @param {Object} [options.moddleExtensions] extension packages to provide * @param {Array} [options.modules] a list of modules to override the default modules * @param {Array} [options.additionalModules] a list of modules to use with the default modules */ function Modeler(options) { BaseModeler.call(this, options); } e(Modeler, BaseModeler); Modeler.Viewer = Viewer; Modeler.NavigatedViewer = NavigatedViewer; /** * The createDiagram result. * * @typedef {Object} CreateDiagramResult * * @property {Array} warnings */ /** * The createDiagram error. * * @typedef {Error} CreateDiagramError * * @property {Array} warnings */ /** * Create a new diagram to start modeling. * * Returns {Promise} */ Modeler.prototype.createDiagram = wrapForCompatibility(function createDiagram() { return this.importXML(initialDiagram); }); Modeler.prototype._interactionModules = [ // non-modeling components KeyboardMoveModule, MoveCanvasModule, TouchModule, ZoomScrollModule ]; Modeler.prototype._modelingModules = [ // modeling components AlignElementsModule, AutoPlaceModule, AutoScrollModule, AutoResizeModule, BendpointsModule, ConnectModule, ConnectionPreviewModule, ContextPadModule, CopyPasteModule, CreateModule, DistributeElementsModule, EditorActionsModule, GridSnappingModule, InteractionEventsModule, KeyboardModule, KeyboardMoveSelectionModule, LabelEditingModule, ModelingModule, MoveModule, PaletteModule, ReplacePreviewModule, ResizeModule, SnappingModule, SearchModule ]; // modules the modeler is composed of // // - viewer modules // - interaction modules // - modeling modules Modeler.prototype._modules = [].concat( Viewer.prototype._modules, Modeler.prototype._interactionModules, Modeler.prototype._modelingModules ); return Modeler; }));