You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
60829 lines
1.4 MiB
60829 lines
1.4 MiB
/*! |
|
* 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 <this>. |
|
* |
|
* @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 = ' <link/><table></table><a href="/a">a</a><input type="checkbox"/>'; |
|
// 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, '<fieldset>', '</fieldset>'], |
|
tr: [2, '<table><tbody>', '</tbody></table>'], |
|
col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'], |
|
// 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<div>', '</div>'] : [0, '', ''] |
|
}; |
|
|
|
map$1.td = |
|
map$1.th = [3, '<table><tbody><tr>', '</tr></tbody></table>']; |
|
|
|
map$1.option = |
|
map$1.optgroup = [1, '<select multiple="multiple">', '</select>']; |
|
|
|
map$1.thead = |
|
map$1.tbody = |
|
map$1.colgroup = |
|
map$1.caption = |
|
map$1.tfoot = [1, '<table>', '</table>']; |
|
|
|
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, '<svg xmlns="http://www.w3.org/2000/svg" version="1.1">','</svg>']; |
|
|
|
/** |
|
* Parse `html` and return a DOM Node instance, which could be a TextNode, |
|
* HTML DOM Node of some kind (<div> 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 xmlns="' + ns.svg + '"'; |
|
|
|
function parse(svg) { |
|
|
|
var unwrap = false; |
|
|
|
// ensure we import a valid svg document |
|
if (svg.substring(0, 4) === '<svg') { |
|
if (svg.indexOf(ns.svg) === -1) { |
|
svg = SVG_START + svg.substring(4); |
|
} |
|
} else { |
|
// namespace svg |
|
svg = SVG_START + '>' + svg + '</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('</', node.tagName, '>'); |
|
} else { |
|
output.push('/>'); |
|
} |
|
break; |
|
|
|
// COMMENT |
|
case 8: |
|
output.push('<!--', escape$1(node.nodeValue, TEXT_ENTITIES), '-->'); |
|
break; |
|
|
|
// CDATA |
|
case 4: |
|
output.push('<![CDATA[', node.nodeValue, ']]>'); |
|
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 <svg> 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<SVGTransform|SVGMatrix>} [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<djs.model.base>} elements |
|
* |
|
* @returns {Array<djs.model.Base>} |
|
*/ |
|
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<Object>} 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<Object>} 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<djs.model.Base>} 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<djs.model.Base>} 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<djs.model.Base>} elements to query |
|
* @param {boolean} allowDuplicates to allow duplicates in the result set |
|
* |
|
* @return {Array<djs.model.Base>} 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<djs.model.Base>} 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>|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<djs.model.Shape>} elements List of Elements to search through |
|
* @param {djs.model.Shape} bbox the enclosing bbox. |
|
* |
|
* @return {Array<djs.model.Shape>} 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<string>} 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<string>} 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<Object>} [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<Object>} 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<Object>} 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>|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<PathDef>} path1 |
|
* @param {String|Array<PathDef>} path2 |
|
* @param {Boolean} [justCount=false] |
|
* |
|
* @return {Array<Intersection>|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 <div> around the svg element with the respective size |
|
// this way we can always get the correct container size |
|
// (this is impossible for <svg> 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 <svg> element that is wrapped into a <div>. |
|
* 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) |
|
|
|
* <div class="djs-container" style="width: {desired-width}, height: {desired-height}"> |
|
* <svg width="100%" height="100%"> |
|
* ... |
|
* </svg> |
|
* </div> |
|
*/ |
|
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; // <g class="... some-marker"> ... </g> |
|
* |
|
* @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 <svg> |
|
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 <svg> |
|
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<djs.model.Base>} |
|
*/ |
|
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<djs.model.Base>} |
|
*/ |
|
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); // <g ...> |
|
* |
|
* elementRegistry.getGraphics(rootElement, true); // <svg ...> |
|
* |
|
* |
|
* @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<Object>} collection |
|
* @param {Refs} refs instance |
|
* @param {Object} property represented by the collection |
|
* @param {Object} target object the collection is attached to |
|
* |
|
* @return {RefsCollection<Object>} 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. |
|
* |
|
* <p> |
|
* When bound to an object using {@link Refs#bind} the references |
|
* get activated and ensure that add and remove operations are applied |
|
* reversely, too. |
|
* </p> |
|
* |
|
* <p> |
|
* 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. |
|
* </p> |
|
* |
|
* @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<Connection> |
|
*/ |
|
outgoingRefs.bind(this, 'outgoing'); |
|
|
|
/** |
|
* The list of incoming connections |
|
* |
|
* @name Base#incoming |
|
* @type Array<Connection> |
|
*/ |
|
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<Base> |
|
*/ |
|
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<string>} 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<string>} 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<Object>} 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<SVGElement>} gfx |
|
* |
|
* @return {Snap<SVGElement>} |
|
*/ |
|
function getVisual(gfx) { |
|
return gfx.childNodes[0]; |
|
} |
|
|
|
/** |
|
* Returns the children for a given diagram element. |
|
* |
|
* @param {Snap<SVGElement>} gfx |
|
* @return {Snap<SVGElement>} |
|
*/ |
|
function getChildren$1(gfx) { |
|
return gfx.parentNode.childNodes[1]; |
|
} |
|
|
|
/**
|
|
* @param {<SVGElement>} 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 <g class="djs-visual" /> 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: |
|
* |
|
* <g class="djs-group"> |
|
* |
|
* <!-- the gfx --> |
|
* <g class="djs-element djs-(shape|connection|frame)"> |
|
* <g class="djs-visual"> |
|
* <!-- the renderer draws in here --> |
|
* </g> |
|
* |
|
* <!-- extensions (overlays, click box, ...) goes here |
|
* </g> |
|
* |
|
* <!-- the gfx child nodes --> |
|
* <g class="djs-children"></g> |
|
* </g> |
|
* |
|
* @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<Module>} 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<Module> to the constructor. |
|
* |
|
* @class djs.Diagram |
|
* @memberOf djs |
|
* @constructor |
|
* |
|
* @example |
|
* |
|
* <caption>Creating a plug-in that logs whenever a shape is added to the canvas.</caption> |
|
* |
|
* // 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<Module>} [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 <prefix:localName> or <localName>, 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<Package>} 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<Package>} 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<string, ?>=} 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<string, string>}} |
|
*/ |
|
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 <name, cb>'); |
|
} |
|
|
|
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; // <!XXXXX zzzz="eeee"> |
|
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<string, string>} nsMap |
|
* |
|
* @return {Parser} |
|
*/ |
|
this['ns'] = function(nsMap) { |
|
|
|
if (typeof nsMap === 'undefined') { |
|
nsMap = {}; |
|
} |
|
|
|
if (typeof nsMap !== 'object') { |
|
throw error$2('required args <nsMap={}>'); |
|
} |
|
|
|
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 <xml=string>'); |
|
} |
|
|
|
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) { // </... |
|
if (w === 47) { // </... |
|
tagStart = false; |
|
tagEnd = true; |
|
|
|
if (!nodeStack.length) { |
|
return handleError('missing open tag'); |
|
} |
|
|
|
// verify open <-> 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<Object>} references |
|
* @property {Array<Error>} warnings |
|
* @property {Object} elementsById - a mapping containing each ID -> ModdleElement |
|
*/ |
|
|
|
/** |
|
* The fromXML result. |
|
* |
|
* @typedef {Error} ParseError |
|
* |
|
* @property {Array<Error>} warnings |
|
*/ |
|
|
|
/** |
|
* Parse the given XML into a moddle document tree. |
|
* |
|
* @param {String} xml |
|
* @param {ElementHandler|Object} options or rootHandler |
|
* |
|
* @returns {Promise<ParseResult, ParseError>} |
|
*/ |
|
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 <lax=true> 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 |
|
// <!CDATA[ ... ]> 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 = '<?xml version="1.0" encoding="UTF-8"?>\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 <xml> 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 + '</' + this.tagName + '>') |
|
.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('</' + this.tagName + '>') |
|
.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<Object>} |
|
*/ |
|
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('</' + this.tagName + '>'); |
|
} |
|
|
|
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<Object>} references |
|
* @property {Array<Error>} warnings |
|
* @property {Object} elementsById - a mapping containing each ID -> ModdleElement |
|
*/ |
|
|
|
/** |
|
* The fromXML error. |
|
* |
|
* @typedef {Error} ParseError |
|
* |
|
* @property {Array<Error>} 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<ParseResult, ParseError>} |
|
*/ |
|
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<SerializationResult, Error>} |
|
*/ |
|
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 '<null>'; |
|
} |
|
|
|
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<string>} 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<string>} warnings |
|
*/ |
|
|
|
/** |
|
* The importBpmnDiagram error. |
|
* |
|
* @typedef {Error} ImportBPMNDiagramError |
|
* |
|
* @property {Array<string>} warnings |
|
*/ |
|
|
|
/** |
|
* Import the definitions into a diagram. |
|
* |
|
* Errors and warnings are reported through the specified callback. |
|
* |
|
* @param {djs.Diagram} diagram |
|
* @param {ModdleElement<Definitions>} definitions |
|
* @param {ModdleElement<BPMNDiagram>} [bpmnDiagram] the diagram to be rendered |
|
* (if not provided, the first one will be rendered) |
|
* |
|
* Returns {Promise<ImportBPMNDiagramResult, ImportBPMNDiagramError>} |
|
*/ |
|
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>} definitions |
|
* @param {ModdleElement<BPMNDiagram>} 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 <root> do have the `_plane` suffix, while |
|
// the root <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<Object>} |
|
*/ |
|
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 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14.02 5.57" width="53" height="21"><path fill="currentColor" d="M1.88.92v.14c0 .41-.13.68-.4.8.33.14.46.44.46.86v.33c0 .61-.33.95-.95.95H0V0h.95c.65 0 .93.3.93.92zM.63.57v1.06h.24c.24 0 .38-.1.38-.43V.98c0-.28-.1-.4-.32-.4zm0 1.63v1.22h.36c.2 0 .32-.1.32-.39v-.35c0-.37-.12-.48-.4-.48H.63zM4.18.99v.52c0 .64-.31.98-.94.98h-.3V4h-.62V0h.92c.63 0 .94.35.94.99zM2.94.57v1.35h.3c.2 0 .3-.09.3-.37v-.6c0-.29-.1-.38-.3-.38h-.3zm2.89 2.27L6.25 0h.88v4h-.6V1.12L6.1 3.99h-.6l-.46-2.82v2.82h-.55V0h.87zM8.14 1.1V4h-.56V0h.79L9 2.4V0h.56v4h-.64zm2.49 2.29v.6h-.6v-.6zM12.12 1c0-.63.33-1 .95-1 .61 0 .95.37.95 1v2.04c0 .64-.34 1-.95 1-.62 0-.95-.37-.95-1zm.62 2.08c0 .28.13.39.33.39s.32-.1.32-.4V.98c0-.29-.12-.4-.32-.4s-.33.11-.33.4z"/><path fill="currentColor" d="M0 4.53h14.02v1.04H0zM11.08 0h.63v.62h-.63zm.63 4V1h-.63v2.98z"/></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 = |
|
'<div class="bjs-powered-by-lightbox">' + |
|
'<div class="backdrop"></div>' + |
|
'<div class="notice">' + |
|
'<a href="https://bpmn.io" target="_blank" rel="noopener" class="link">' + |
|
BPMNIO_IMG + |
|
'</a>' + |
|
'<span>' + |
|
'Web-based tooling for BPMN, DMN and CMMN diagrams ' + |
|
'powered by <a href="https://bpmn.io" target="_blank" rel="noopener">bpmn.io</a>.' + |
|
'</span>' + |
|
'</div>' + |
|
'</div>'; |
|
|
|
|
|
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 <project-logo></project-logo> 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<didi.Module>} [options.modules] a list of modules to override the default modules |
|
* @param {Array<didi.Module>} [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); |
|
|
|
/* <project-logo> */ |
|
|
|
addProjectLogo(this._container); |
|
|
|
/* </project-logo> */ |
|
|
|
this._init(this._container, this._moddle, options); |
|
} |
|
|
|
e(BaseViewer, Diagram); |
|
|
|
/** |
|
* The importXML result. |
|
* |
|
* @typedef {Object} ImportXMLResult |
|
* |
|
* @property {Array<string>} warnings |
|
*/ |
|
|
|
/** |
|
* The importXML error. |
|
* |
|
* @typedef {Error} ImportXMLError |
|
* |
|
* @property {Array<string>} 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<BPMNDiagram>|string} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered) |
|
* |
|
* Returns {Promise<ImportXMLResult, ImportXMLError>} |
|
*/ |
|
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 <context> 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<string>} warnings |
|
*/ |
|
|
|
/** |
|
* The importDefinitions error. |
|
* |
|
* @typedef {Error} ImportDefinitionsError |
|
* |
|
* @property {Array<string>} 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>} definitions parsed BPMN 2.0 definitions |
|
* @param {ModdleElement<BPMNDiagram>|string} [bpmnDiagram] BPMN diagram or id of diagram to render (if not provided, the first one will be rendered) |
|
* |
|
* Returns {Promise<ImportDefinitionsResult, ImportDefinitionsError>} |
|
*/ |
|
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<string>} warnings |
|
*/ |
|
|
|
/** |
|
* The open error. |
|
* |
|
* @typedef {Error} OpenError |
|
* |
|
* @property {Array<string>} 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<BPMNDiagram>} [bpmnDiagramOrId] id or the diagram to open |
|
* |
|
* Returns {Promise<OpenResult, OpenError>} |
|
*/ |
|
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<SaveXMLResult, Error>} |
|
*/ |
|
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<SaveSVGResult, Error>} |
|
*/ |
|
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 ? '<defs>' + innerSVG(defsNode) + '</defs>' : ''; |
|
|
|
var bbox = contentNode.getBBox(); |
|
|
|
svg = |
|
'<?xml version="1.0" encoding="utf-8"?>\n' + |
|
'<!-- created with bpmn-js / http://bpmn.io -->\n' + |
|
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' + |
|
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' + |
|
'width="' + bbox.width + '" height="' + bbox.height + '" ' + |
|
'viewBox="' + bbox.x + ' ' + bbox.y + ' ' + bbox.width + ' ' + bbox.height + '" version="1.1">' + |
|
defs + contents + |
|
'</svg>'; |
|
} 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('<div class="bjs-container"></div>'); |
|
|
|
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>} definitions |
|
* @param {string} diagramId |
|
* |
|
* @return {ModdleElement<BPMNDiagram>|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 = |
|
'<a href="http://bpmn.io" ' + |
|
'target="_blank" ' + |
|
'class="bjs-powered-by" ' + |
|
'title="Powered by bpmn.io" ' + |
|
'>' + |
|
img + |
|
'</a>'; |
|
|
|
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(); |
|
}); |
|
} |
|
|
|
/* </project-logo> */ |
|
|
|
/** |
|
* 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<didi.Module>} [options.modules] a list of modules to override the default modules |
|
* @param {Array<didi.Module>} [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<string>} 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 |
|
* |
|
* <h1>Path definition</h1> |
|
* A parameterized path is defined like this: |
|
* <pre> |
|
* '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] |
|
* } |
|
* </pre> |
|
* <p>It's important to specify a correct <b>height and width</b> 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).</p> |
|
* <p>The '<b>heightElements</b>' and '<b>widthElements</b>' 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 <b>heightElement</b>'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. |
|
* <ul> |
|
* <li>The values for the y axis can be accessed in the path string using {e.y0}, {e.y1}, ....</li> |
|
* <li>The values for the x axis can be accessed in the path string using {e.x0}, {e.x1}, ....</li> |
|
* </ul> |
|
* The numbers x0, x1 respectively y0, y1, ... map to the corresponding array index. |
|
* </p> |
|
*/ |
|
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. |
|
* <h1>Use case</h1> |
|
* <p>Use case is to scale the content of elements (event, gateways) based |
|
* on the element bounding box's size. |
|
* </p> |
|
* <h1>Why not transform</h1> |
|
* <p>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.</p> |
|
* |
|
* @param {string} pathId The ID of the path. |
|
* @param {Object} param <p> |
|
* Example param object scales the path to 60% size of the container (data.width, data.height). |
|
* <pre> |
|
* { |
|
* xScaleFactor: 0.6, |
|
* yScaleFactor:0.6, |
|
* containerWidth: data.width, |
|
* containerHeight: data.height, |
|
* position: { |
|
* mx: 0.46, |
|
* my: 0.2, |
|
* } |
|
* } |
|
* </pre> |
|
* <ul> |
|
* <li>targetpathwidth = xScaleFactor * containerWidth</li> |
|
* <li>targetpathheight = yScaleFactor * containerHeight</li> |
|
* <li>Position is used to set the starting coordinate of the path. M is computed: |
|
* <ul> |
|
* <li>position.x * containerWidth</li> |
|
* <li>position.y * containerHeight</li> |
|
* </ul> |
|
* Center of the container <pre> position: { |
|
* mx: 0.5, |
|
* my: 0.5, |
|
* }</pre> |
|
* Upper left corner of the container |
|
* <pre> position: { |
|
* mx: 0.0, |
|
* my: 0.0, |
|
* }</pre> |
|
* </li> |
|
* </ul> |
|
* </p> |
|
* |
|
*/ |
|
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<Point>} 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<Point>} 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<Point>} waypoints |
|
* |
|
* @return {SVGElement} |
|
*/ |
|
this.createWaypointsHit = function(gfx, waypoints) { |
|
|
|
var hit = createLine(waypoints); |
|
|
|
applyStyle(hit, 'stroke'); |
|
|
|
appendHit(gfx, hit); |
|
|
|
return hit; |
|
}; |
|
|
|
/** |
|
* Create hits for a box. |
|
* |
|
* @param {SVGElement} gfx |
|
* @param {string} hitType |
|
* @param {Object} attrs |
|
* |
|
* @return {SVGElement} |
|
*/ |
|
this.createBoxHit = function(gfx, type, attrs) { |
|
|
|
attrs = assign({ |
|
x: 0, |
|
y: 0 |
|
}, attrs); |
|
|
|
var hit = 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 <code>hover</code> and <code>selected</code> 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: '<div style="width: 10px; background: fuchsia; color: white;">0</div>' |
|
* }); |
|
* |
|
* // or add via shape id |
|
* |
|
* overlays.add('some-element-id', { |
|
* position: { |
|
* top: -5, |
|
* left: -5 |
|
* } |
|
* html: '<div style="width: 10px; background: fuchsia; color: white;">0</div>' |
|
* }); |
|
* |
|
* // or add with optional type |
|
* |
|
* overlays.add(someShape, 'badge', { |
|
* position: { |
|
* top: -5, |
|
* left: -5 |
|
* } |
|
* html: '<div style="width: 10px; background: fuchsia; color: white;">0</div>' |
|
* }); |
|
* |
|
* |
|
* // 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<Object>} 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('<div class="djs-overlays" />'); |
|
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('<div class="djs-overlay" data-overlay-id="' + id + '">'); |
|
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( |
|
'<div class="djs-overlay-container" />' |
|
); |
|
|
|
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 |
|
* |
|
* <ul> |
|
* <li>redrawing shapes and connections on change</li> |
|
* </ul> |
|
* |
|
* @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<string>} [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<string>} [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('<ul class="bjs-breadcrumbs"></ul>'); |
|
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('<li><span class="bjs-crumb"><a title="' + title + '">' + title + '</a></span></li>'); |
|
|
|
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<djs.model.Shape>} 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 = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M4.81801948,3.50735931 L10.4996894,9.1896894 L10.5,4 L12,4 L12,12 L4,12 L4,10.5 L9.6896894,10.4996894 L3.75735931,4.56801948 C3.46446609,4.27512627 3.46446609,3.80025253 3.75735931,3.50735931 C4.05025253,3.21446609 4.52512627,3.21446609 4.81801948,3.50735931 Z"/></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 class="bjs-drilldown">' + ARROW_DOWN_SVG + '</button>'); |
|
|
|
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<didi.Module>} [options.modules] a list of modules to override the default modules |
|
* @param {Array<didi.Module>} [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<string>} 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.<anonymous>\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 <SVG> element |
|
* if there is no shape between 0,0 and viewport elements origin. |
|
* |
|
* So touchstart event is only fired when the <g class="viewport"> 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 || '<div class="entry" draggable="true"></div>'), |
|
container; |
|
|
|
attr$1(control, 'data-action', id); |
|
|
|
container = query('[data-group=' + cssEscape(grouping) + ']', html); |
|
if (!container) { |
|
container = domify('<div class="group"></div>'); |
|
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('<img>'); |
|
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('<div class="djs-context-pad"></div>'); |
|
|
|
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('<div class="djs-popup">'), |
|
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<Object>} 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('<div>'), |
|
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('<div class="group"></div>'); |
|
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('<div>'), |
|
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('<span>'); |
|
label.textContent = entry.label; |
|
entryContainer.appendChild(label); |
|
} |
|
|
|
if (entry.imageUrl) { |
|
var image = domify('<img>'); |
|
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 <code>undefined</code>). |
|
* |
|
* @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<string>} 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<djs.model.Shape>} |
|
*/ |
|
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<string>} 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<djs.model.Shape>} 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<djs.model.Shape>} 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<djs.model.Shape>} 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<SVGElement>) |
|
// properties during hover and out and fire {prefix}.hover and {prefix}.out properties |
|
// respectively |
|
|
|
function hover(event) { |
|
var payload = context.payload; |
|
|
|
payload.hoverGfx = event.gfx; |
|
payload.hover = event.element; |
|
|
|
fire('hover'); |
|
} |
|
|
|
function out(event) { |
|
fire('out'); |
|
|
|
var payload = context.payload; |
|
|
|
payload.hoverGfx = null; |
|
payload.hover = null; |
|
} |
|
|
|
|
|
// life-cycle methods |
|
|
|
function cancel(restore) { |
|
var previousContext; |
|
|
|
if (!context) { |
|
return; |
|
} |
|
|
|
var wasActive = context.active; |
|
|
|
if (wasActive) { |
|
fire('cancel'); |
|
} |
|
|
|
previousContext = cleanup(restore); |
|
|
|
if (wasActive) { |
|
|
|
// last event to be fired when all drag operations are done |
|
// at this point in time no drag operation is in progress anymore |
|
fire('canceled', previousContext); |
|
} |
|
} |
|
|
|
function cleanup(restore) { |
|
var previousContext, |
|
endDrag; |
|
|
|
fire('cleanup'); |
|
|
|
// reset cursor |
|
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<Point>} 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<Point>} newWaypoints |
|
* |
|
* @return {Array<Point>} 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<Point>} 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<number>} 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<djs.model.Shape|djs.model.Connection>} |
|
*/ |
|
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 <source, connectionStart> and <target, connectionEnd> 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<Point>} [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( |
|
'<div class="djs-direct-editing-parent">' + |
|
'<div class="djs-direct-editing-content" contenteditable="true"></div>' + |
|
'</div>' |
|
); |
|
|
|
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( |
|
'<div class="djs-direct-editing-resize-handle"></div>' |
|
); |
|
|
|
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<djs.model.Base>} 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 <elements.changed> that is not result of <drag.end> |
|
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} <copyPaste.canCopyElements> listener |
|
* |
|
* @param {Object} context |
|
* @param {Array<djs.model.Base>} context.elements |
|
* |
|
* @returns {Array<djs.model.Base>|boolean} - Return elements to be copied or false to disallow |
|
* copying. |
|
*/ |
|
|
|
/** |
|
* @typedef {Function} <copyPaste.copyElement> listener |
|
* |
|
* @param {Object} context |
|
* @param {Object} context.descriptor |
|
* @param {djs.model.Base} context.element |
|
* @param {Array<djs.model.Base>} context.elements |
|
*/ |
|
|
|
/** |
|
* @typedef {Function} <copyPaste.createTree> listener |
|
* |
|
* @param {Object} context |
|
* @param {djs.model.Base} context.element |
|
* @param {Array<djs.model.Base>} context.children - Add children to be added to tree. |
|
*/ |
|
|
|
/** |
|
* @typedef {Function} <copyPaste.elementsCopied> listener |
|
* |
|
* @param {Object} context |
|
* @param {Object} context.elements |
|
* @param {Object} context.tree |
|
*/ |
|
|
|
/** |
|
* @typedef {Function} <copyPaste.pasteElement> listener |
|
* |
|
* @param {Object} context |
|
* @param {Object} context.cache - Already created elements. |
|
* @param {Object} context.descriptor |
|
*/ |
|
|
|
/** |
|
* @typedef {Function} <copyPaste.pasteElements> 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<djs.model.Base>} 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<djs.model.Base>} 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<djs.model.Base>} 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<djs.model.base>} 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} <moddleCopy.canCopyProperties> listener |
|
* |
|
* @param {Object} context |
|
* @param {Array<string>} context.propertyNames |
|
* @param {ModdleElement} context.sourceElement |
|
* @param {ModdleElement} context.targetElement |
|
* |
|
* @returns {Array<string>|boolean} - Return properties to be copied or false to disallow |
|
* copying. |
|
*/ |
|
|
|
/** |
|
* @typedef {Function} <moddleCopy.canCopyProperty> 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} <moddleCopy.canSetCopiedProperty> 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<string>} [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<Object>} 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<Object>} 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<Object>} 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<Object>} 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<Object>} 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<Object>} 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<Object>} 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<djs.model.Shape>} 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<djs.model.Base>} [collectedShapes] |
|
* |
|
* @return {Array<djs.model.Base>} |
|
*/ |
|
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<djs.model.Shape>} |
|
*/ |
|
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<Object>} |
|
*/ |
|
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 <we>
|
|
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 <e>
|
|
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 <ns>
|
|
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 <n>
|
|
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<Point>} waypoints |
|
* |
|
* @returns {Array<Point>} |
|
*/ |
|
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<string>} |
|
*/ |
|
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<string>} |
|
*/ |
|
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<djs.model.shape>} 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<Point>} 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<Point>} 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<Point>} newWaypoints |
|
* @param {Array<Point>} 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<Point>} newWaypoints |
|
* @param {Array<Point>} 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<Point, Point>} 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<Point> |
|
*/ |
|
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<Point> lines |
|
* |
|
* @return Array<Point> |
|
*/ |
|
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<Point>} newWaypoints |
|
* @param {Array<Point>} 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<Point>} |
|
*/ |
|
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: '<div>' + message + '</div>' |
|
}); |
|
} |
|
|
|
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 {<djs.model.Shape>} 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<djs.model.Shape>} 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 <!TextAnnotation> and <TextAnnotation> |
|
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<Object>} |
|
*/ |
|
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.(<command>.)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 <execute> or <revert> 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( |
|
'<div class="djs-tooltip-container" />' |
|
); |
|
|
|
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: '<div style="width: 10px; background: fuchsia; color: white;">0</div>' |
|
* }); |
|
* |
|
* // or with optional life span |
|
* tooltips.add({ |
|
* position: { |
|
* top: -5, |
|
* left: -5 |
|
* }, |
|
* html: '<div style="width: 10px; background: fuchsia; color: white;">0</div>', |
|
* 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('<div data-tooltip-id="' + id + '" class="' + tooltipClass + '">'); |
|
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<Object>} [collection] |
|
* @param {Function} removeFn |
|
* |
|
* @return {Array<Object>} 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<djs.model.Base>} elements |
|
* |
|
* @return {Array<djs.model.Base>} 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<djs.model.Base>} shapes |
|
* @return {Array<djs.model.Base>} |
|
*/ |
|
function getAttachers(shapes) { |
|
return flatten(map(shapes, function(s) { |
|
return s.attachers || []; |
|
})); |
|
} |
|
|
|
/** |
|
* Return a combined list of elements and |
|
* attachers. |
|
* |
|
* @param {Array<djs.model.Base>} elements |
|
* @return {Array<djs.model.Base>} 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<djs.model.Base>} elements |
|
* |
|
* @return {Array<djs.model.Base>} 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<djs.model.Shape} movingShapes |
|
* @param {Array<djs.model.Shape} resizingShapes |
|
* |
|
* @returns {Array<djs.model.Connection>} |
|
*/ |
|
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<djs.model.Shape>} movingShapes |
|
* @param {Array<djs.model.Shape>} 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<djs.model.Shape>} 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<string>} 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<djs.model.Base>} elements |
|
* @param {Point} delta |
|
* @param {djs.model.Base} newParent applied to the first level of shapes |
|
* |
|
* @return {Array<djs.model.Base>} 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<djs.model.Base>} 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<djs.model.Shape>} 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<djs.mode.Base>} 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('<moddleElement> 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<djs.model.Element>} |
|
*/ |
|
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<djs.model.Base>} 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 `<canvas>` 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<Point>} 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<Point>} |
|
*/ |
|
function getBendpoints(a, b, directions) { |
|
directions = directions || 'h:h'; |
|
|
|
if (!isValidDirections(directions)) { |
|
throw new Error( |
|
'unknown directions: <' + directions + '>: ' + |
|
'must be specified as <start>:<end> ' + |
|
'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<Point>} |
|
*/ |
|
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<string>} [hints.preferredLayouts] |
|
* @param {Point|boolean} [hints.connectionStart] whether the start changed |
|
* @param {Point|boolean} [hints.connectionEnd] whether the end changed |
|
* |
|
* @return {Array<Point>} 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<Point>} [waypoints] |
|
* @param {Object} [hints] |
|
* @param {Array<string>} [hints.preferredLayouts] list of preferred layouts |
|
* @param {boolean} [hints.connectionStart] |
|
* @param {boolean} [hints.connectionEnd] |
|
* |
|
* @return {Array<Point>} 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<Point>|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<Point>} points originalPoints from moved to other |
|
* |
|
* @return {Array<Point>|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<Point>} points originalPoints from moved to other |
|
* |
|
* @return {Array<Point>|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<Point>} points originalPoints from moved to other |
|
* |
|
* @return {Array<Point>} 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 <h|v> 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<Point>} waypoints |
|
* |
|
* @return {Array<Point>} |
|
*/ |
|
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<djs.model.Base>} elements |
|
* |
|
* @return {Array<djs.model.Base>} 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<string, PaletteEntryDescriptor>} 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('<div class="group"></div>'); |
|
attr$1(container, 'data-group', grouping); |
|
|
|
entriesContainer.appendChild(container); |
|
} |
|
|
|
var html = entry.html || ( |
|
entry.separator ? |
|
'<hr class="separator" />' : |
|
'<div class="entry" draggable="true"></div>'); |
|
|
|
|
|
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('<img>'); |
|
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 = |
|
'<div class="djs-palette">' + |
|
'<div class="djs-palette-entries"></div>' + |
|
'<div class="djs-palette-toggle"></div>' + |
|
'</div>'; |
|
|
|
|
|
// 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<String, SnapPoints> mapping drop targets to |
|
* a list of possible snappings. |
|
* |
|
* @type {Object} |
|
*/ |
|
this._targets = {}; |
|
|
|
/** |
|
* Map<String, Point> initial positioning of element |
|
* regarding various snap directions. |
|
* |
|
* @type {Object} |
|
*/ |
|
this._snapOrigins = {}; |
|
|
|
/** |
|
* List of snap locations |
|
* |
|
* @type {Array<string>} |
|
*/ |
|
this._snapLocations = []; |
|
|
|
/** |
|
* Map<String, Array<Point>> 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<string>} snapLocations |
|
*/ |
|
SnapContext.prototype.getSnapLocations = function() { |
|
return this._snapLocations; |
|
}; |
|
|
|
/** |
|
* Set the snap locations for this context. |
|
* |
|
* The order of locations determines precedence. |
|
* |
|
* @param {Array<string>} 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<string, Array<Point>>} [defaultPoints] |
|
*/ |
|
function SnapPoints(defaultSnaps) { |
|
|
|
/** |
|
* Map<String, Map<(x|y), Array<number>>> 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: <String>, element: <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('<div class="' + SearchPad.OVERLAY_CLASS + '"></div>'); |
|
|
|
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<Object>} 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<Object>} tokens |
|
* @return {string} |
|
*/ |
|
function createHtmlText(tokens) { |
|
var htmlText = ''; |
|
|
|
tokens.forEach(function(t) { |
|
if (t.matched) { |
|
htmlText += '<strong class="' + SearchPad.RESULT_HIGHLIGHT_CLASS + '">' + escapeHTML(t.matched) + '</strong>'; |
|
} 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 = |
|
'<div class="djs-search-container djs-draggable djs-scrollable">' + |
|
'<div class="djs-search-input">' + |
|
'<input type="text"/>' + |
|
'</div>' + |
|
'<div class="djs-search-results"></div>' + |
|
'</div>'; |
|
|
|
SearchPad.RESULT_HTML = |
|
'<div class="djs-search-result"></div>'; |
|
|
|
SearchPad.RESULT_PRIMARY_HTML = |
|
'<div class="djs-search-result-primary"></div>'; |
|
|
|
SearchPad.RESULT_SECONDARY_HTML = |
|
'<p class="djs-search-result-secondary"></p>'; |
|
|
|
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 |
|
* |
|
* <Result> : |
|
* { |
|
* primaryTokens: <Array<Token>>, |
|
* secondaryTokens: <Array<Token>>, |
|
* element: <Element> |
|
* } |
|
* |
|
* <Token> : |
|
* { |
|
* normal|matched: <string> |
|
* } |
|
* |
|
* @param {string} pattern |
|
* @return {Array<Result>} |
|
*/ |
|
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 = |
|
'<?xml version="1.0" encoding="UTF-8"?>' + |
|
'<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + |
|
'xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" ' + |
|
'xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" ' + |
|
'xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" ' + |
|
'targetNamespace="http://bpmn.io/schema/bpmn" ' + |
|
'id="Definitions_1">' + |
|
'<bpmn:process id="Process_1" isExecutable="false">' + |
|
'<bpmn:startEvent id="StartEvent_1"/>' + |
|
'</bpmn:process>' + |
|
'<bpmndi:BPMNDiagram id="BPMNDiagram_1">' + |
|
'<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">' + |
|
'<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">' + |
|
'<dc:Bounds height="36.0" width="36.0" x="173.0" y="102.0"/>' + |
|
'</bpmndi:BPMNShape>' + |
|
'</bpmndi:BPMNPlane>' + |
|
'</bpmndi:BPMNDiagram>' + |
|
'</bpmn:definitions>'; |
|
|
|
|
|
/** |
|
* 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<didi.Module>} [options.modules] a list of modules to override the default modules |
|
* @param {Array<didi.Module>} [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<string>} warnings |
|
*/ |
|
|
|
/** |
|
* The createDiagram error. |
|
* |
|
* @typedef {Error} CreateDiagramError |
|
* |
|
* @property {Array<string>} warnings |
|
*/ |
|
|
|
/** |
|
* Create a new diagram to start modeling. |
|
* |
|
* Returns {Promise<CreateDiagramResult, CreateDiagramError>} |
|
*/ |
|
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; |
|
|
|
}));
|