CopyPaste.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. import {
  2. assign,
  3. find,
  4. forEach,
  5. isArray,
  6. isNumber,
  7. map,
  8. matchPattern,
  9. omit,
  10. sortBy
  11. } from 'min-dash';
  12. import {
  13. getBBox,
  14. getParents
  15. } from '../../util/Elements';
  16. import { eachElement } from '../../util/Elements';
  17. /**
  18. * @typedef {Function} <copyPaste.canCopyElements> listener
  19. *
  20. * @param {Object} context
  21. * @param {Array<djs.model.Base>} context.elements
  22. *
  23. * @returns {Array<djs.model.Base>|boolean} - Return elements to be copied or false to disallow
  24. * copying.
  25. */
  26. /**
  27. * @typedef {Function} <copyPaste.copyElement> listener
  28. *
  29. * @param {Object} context
  30. * @param {Object} context.descriptor
  31. * @param {djs.model.Base} context.element
  32. * @param {Array<djs.model.Base>} context.elements
  33. */
  34. /**
  35. * @typedef {Function} <copyPaste.createTree> listener
  36. *
  37. * @param {Object} context
  38. * @param {djs.model.Base} context.element
  39. * @param {Array<djs.model.Base>} context.children - Add children to be added to tree.
  40. */
  41. /**
  42. * @typedef {Function} <copyPaste.elementsCopied> listener
  43. *
  44. * @param {Object} context
  45. * @param {Object} context.elements
  46. * @param {Object} context.tree
  47. */
  48. /**
  49. * @typedef {Function} <copyPaste.pasteElement> listener
  50. *
  51. * @param {Object} context
  52. * @param {Object} context.cache - Already created elements.
  53. * @param {Object} context.descriptor
  54. */
  55. /**
  56. * @typedef {Function} <copyPaste.pasteElements> listener
  57. *
  58. * @param {Object} context
  59. * @param {Object} context.hints - Add hints before pasting.
  60. */
  61. /**
  62. * Copy and paste elements.
  63. *
  64. * @param {Canvas} canvas
  65. * @param {Create} create
  66. * @param {Clipboard} clipboard
  67. * @param {ElementFactory} elementFactory
  68. * @param {EventBus} eventBus
  69. * @param {Modeling} modeling
  70. * @param {Mouse} mouse
  71. * @param {Rules} rules
  72. */
  73. export default function CopyPaste(
  74. canvas,
  75. create,
  76. clipboard,
  77. elementFactory,
  78. eventBus,
  79. modeling,
  80. mouse,
  81. rules
  82. ) {
  83. this._canvas = canvas;
  84. this._create = create;
  85. this._clipboard = clipboard;
  86. this._elementFactory = elementFactory;
  87. this._eventBus = eventBus;
  88. this._modeling = modeling;
  89. this._mouse = mouse;
  90. this._rules = rules;
  91. eventBus.on('copyPaste.copyElement', function(context) {
  92. var descriptor = context.descriptor,
  93. element = context.element,
  94. elements = context.elements;
  95. // default priority (priority = 1)
  96. descriptor.priority = 1;
  97. descriptor.id = element.id;
  98. var parentCopied = find(elements, function(e) {
  99. return e === element.parent;
  100. });
  101. // do NOT reference parent if parent wasn't copied
  102. if (parentCopied) {
  103. descriptor.parent = element.parent.id;
  104. }
  105. // attachers (priority = 2)
  106. if (isAttacher(element)) {
  107. descriptor.priority = 2;
  108. descriptor.host = element.host.id;
  109. }
  110. // connections (priority = 3)
  111. if (isConnection(element)) {
  112. descriptor.priority = 3;
  113. descriptor.source = element.source.id;
  114. descriptor.target = element.target.id;
  115. descriptor.waypoints = copyWaypoints(element);
  116. }
  117. // labels (priority = 4)
  118. if (isLabel(element)) {
  119. descriptor.priority = 4;
  120. descriptor.labelTarget = element.labelTarget.id;
  121. }
  122. forEach([ 'x', 'y', 'width', 'height' ], function(property) {
  123. if (isNumber(element[ property ])) {
  124. descriptor[ property ] = element[ property ];
  125. }
  126. });
  127. descriptor.hidden = element.hidden;
  128. descriptor.collapsed = element.collapsed;
  129. });
  130. eventBus.on('copyPaste.pasteElements', function(context) {
  131. var hints = context.hints;
  132. assign(hints, {
  133. createElementsBehavior: false
  134. });
  135. });
  136. }
  137. CopyPaste.$inject = [
  138. 'canvas',
  139. 'create',
  140. 'clipboard',
  141. 'elementFactory',
  142. 'eventBus',
  143. 'modeling',
  144. 'mouse',
  145. 'rules'
  146. ];
  147. /**
  148. * Copy elements.
  149. *
  150. * @param {Array<djs.model.Base>} elements
  151. *
  152. * @returns {Object}
  153. */
  154. CopyPaste.prototype.copy = function(elements) {
  155. var allowed,
  156. tree;
  157. if (!isArray(elements)) {
  158. elements = elements ? [ elements ] : [];
  159. }
  160. allowed = this._eventBus.fire('copyPaste.canCopyElements', {
  161. elements: elements
  162. });
  163. if (allowed === false) {
  164. tree = {};
  165. } else {
  166. tree = this.createTree(isArray(allowed) ? allowed : elements);
  167. }
  168. // we set an empty tree, selection of elements
  169. // to copy was empty.
  170. this._clipboard.set(tree);
  171. this._eventBus.fire('copyPaste.elementsCopied', {
  172. elements: elements,
  173. tree: tree
  174. });
  175. return tree;
  176. };
  177. /**
  178. * Paste elements.
  179. *
  180. * @param {Object} [context]
  181. * @param {djs.model.base} [context.element] - Parent.
  182. * @param {Point} [context.point] - Position.
  183. * @param {Object} [context.hints] - Hints.
  184. */
  185. CopyPaste.prototype.paste = function(context) {
  186. var tree = this._clipboard.get();
  187. if (this._clipboard.isEmpty()) {
  188. return;
  189. }
  190. var hints = context && context.hints || {};
  191. this._eventBus.fire('copyPaste.pasteElements', {
  192. hints: hints
  193. });
  194. var elements = this._createElements(tree);
  195. // paste directly
  196. if (context && context.element && context.point) {
  197. return this._paste(elements, context.element, context.point, hints);
  198. }
  199. this._create.start(this._mouse.getLastMoveEvent(), elements, {
  200. hints: hints || {}
  201. });
  202. };
  203. /**
  204. * Paste elements directly.
  205. *
  206. * @param {Array<djs.model.Base>} elements
  207. * @param {djs.model.base} target
  208. * @param {Point} position
  209. * @param {Object} [hints]
  210. */
  211. CopyPaste.prototype._paste = function(elements, target, position, hints) {
  212. // make sure each element has x and y
  213. forEach(elements, function(element) {
  214. if (!isNumber(element.x)) {
  215. element.x = 0;
  216. }
  217. if (!isNumber(element.y)) {
  218. element.y = 0;
  219. }
  220. });
  221. var bbox = getBBox(elements);
  222. // center elements around cursor
  223. forEach(elements, function(element) {
  224. if (isConnection(element)) {
  225. element.waypoints = map(element.waypoints, function(waypoint) {
  226. return {
  227. x: waypoint.x - bbox.x - bbox.width / 2,
  228. y: waypoint.y - bbox.y - bbox.height / 2
  229. };
  230. });
  231. }
  232. assign(element, {
  233. x: element.x - bbox.x - bbox.width / 2,
  234. y: element.y - bbox.y - bbox.height / 2
  235. });
  236. });
  237. return this._modeling.createElements(elements, position, target, assign({}, hints));
  238. };
  239. /**
  240. * Create elements from tree.
  241. */
  242. CopyPaste.prototype._createElements = function(tree) {
  243. var self = this;
  244. var eventBus = this._eventBus;
  245. var cache = {};
  246. var elements = [];
  247. forEach(tree, function(branch, depth) {
  248. depth = parseInt(depth, 10);
  249. // sort by priority
  250. branch = sortBy(branch, 'priority');
  251. forEach(branch, function(descriptor) {
  252. // remove priority
  253. var attrs = assign({}, omit(descriptor, [ 'priority' ]));
  254. if (cache[ descriptor.parent ]) {
  255. attrs.parent = cache[ descriptor.parent ];
  256. } else {
  257. delete attrs.parent;
  258. }
  259. eventBus.fire('copyPaste.pasteElement', {
  260. cache: cache,
  261. descriptor: attrs
  262. });
  263. var element;
  264. if (isConnection(attrs)) {
  265. attrs.source = cache[ descriptor.source ];
  266. attrs.target = cache[ descriptor.target ];
  267. element = cache[ descriptor.id ] = self.createConnection(attrs);
  268. elements.push(element);
  269. return;
  270. }
  271. if (isLabel(attrs)) {
  272. attrs.labelTarget = cache[ attrs.labelTarget ];
  273. element = cache[ descriptor.id ] = self.createLabel(attrs);
  274. elements.push(element);
  275. return;
  276. }
  277. if (attrs.host) {
  278. attrs.host = cache[ attrs.host ];
  279. }
  280. element = cache[ descriptor.id ] = self.createShape(attrs);
  281. elements.push(element);
  282. });
  283. });
  284. return elements;
  285. };
  286. CopyPaste.prototype.createConnection = function(attrs) {
  287. var connection = this._elementFactory.createConnection(omit(attrs, [ 'id' ]));
  288. return connection;
  289. };
  290. CopyPaste.prototype.createLabel = function(attrs) {
  291. var label = this._elementFactory.createLabel(omit(attrs, [ 'id' ]));
  292. return label;
  293. };
  294. CopyPaste.prototype.createShape = function(attrs) {
  295. var shape = this._elementFactory.createShape(omit(attrs, [ 'id' ]));
  296. return shape;
  297. };
  298. /**
  299. * Check wether element has relations to other elements e.g. attachers, labels and connections.
  300. *
  301. * @param {Object} element
  302. * @param {Array<djs.model.Base>} elements
  303. *
  304. * @returns {boolean}
  305. */
  306. CopyPaste.prototype.hasRelations = function(element, elements) {
  307. var labelTarget,
  308. source,
  309. target;
  310. if (isConnection(element)) {
  311. source = find(elements, matchPattern({ id: element.source.id }));
  312. target = find(elements, matchPattern({ id: element.target.id }));
  313. if (!source || !target) {
  314. return false;
  315. }
  316. }
  317. if (isLabel(element)) {
  318. labelTarget = find(elements, matchPattern({ id: element.labelTarget.id }));
  319. if (!labelTarget) {
  320. return false;
  321. }
  322. }
  323. return true;
  324. };
  325. /**
  326. * Create a tree-like structure from elements.
  327. *
  328. * @example
  329. * tree: {
  330. * 0: [
  331. * { id: 'Shape_1', priority: 1, ... },
  332. * { id: 'Shape_2', priority: 1, ... },
  333. * { id: 'Connection_1', source: 'Shape_1', target: 'Shape_2', priority: 3, ... },
  334. * ...
  335. * ],
  336. * 1: [
  337. * { id: 'Shape_3', parent: 'Shape1', priority: 1, ... },
  338. * ...
  339. * ]
  340. * };
  341. *
  342. * @param {Array<djs.model.base>} elements
  343. *
  344. * @return {Object}
  345. */
  346. CopyPaste.prototype.createTree = function(elements) {
  347. var rules = this._rules,
  348. self = this;
  349. var tree = {},
  350. elementsData = [];
  351. var parents = getParents(elements);
  352. function canCopy(element, elements) {
  353. return rules.allowed('element.copy', {
  354. element: element,
  355. elements: elements
  356. });
  357. }
  358. function addElementData(element, depth) {
  359. // (1) check wether element has already been added
  360. var foundElementData = find(elementsData, function(elementsData) {
  361. return element === elementsData.element;
  362. });
  363. // (2) add element if not already added
  364. if (!foundElementData) {
  365. elementsData.push({
  366. element: element,
  367. depth: depth
  368. });
  369. return;
  370. }
  371. // (3) update depth
  372. if (foundElementData.depth < depth) {
  373. elementsData = removeElementData(foundElementData, elementsData);
  374. elementsData.push({
  375. element: foundElementData.element,
  376. depth: depth
  377. });
  378. }
  379. }
  380. function removeElementData(elementData, elementsData) {
  381. var index = elementsData.indexOf(elementData);
  382. if (index !== -1) {
  383. elementsData.splice(index, 1);
  384. }
  385. return elementsData;
  386. }
  387. // (1) add elements
  388. eachElement(parents, function(element, _index, depth) {
  389. // do NOT add external labels directly
  390. if (isLabel(element)) {
  391. return;
  392. }
  393. // always copy external labels
  394. forEach(element.labels, function(label) {
  395. addElementData(label, depth);
  396. });
  397. function addRelatedElements(elements) {
  398. elements && elements.length && forEach(elements, function(element) {
  399. // add external labels
  400. forEach(element.labels, function(label) {
  401. addElementData(label, depth);
  402. });
  403. addElementData(element, depth);
  404. });
  405. }
  406. forEach([ element.attachers, element.incoming, element.outgoing ], addRelatedElements);
  407. addElementData(element, depth);
  408. var children = [];
  409. if (element.children) {
  410. children = element.children.slice();
  411. }
  412. // allow others to add children to tree
  413. self._eventBus.fire('copyPaste.createTree', {
  414. element: element,
  415. children: children
  416. });
  417. return children;
  418. });
  419. elements = map(elementsData, function(elementData) {
  420. return elementData.element;
  421. });
  422. // (2) copy elements
  423. elementsData = map(elementsData, function(elementData) {
  424. elementData.descriptor = {};
  425. self._eventBus.fire('copyPaste.copyElement', {
  426. descriptor: elementData.descriptor,
  427. element: elementData.element,
  428. elements: elements
  429. });
  430. return elementData;
  431. });
  432. // (3) sort elements by priority
  433. elementsData = sortBy(elementsData, function(elementData) {
  434. return elementData.descriptor.priority;
  435. });
  436. elements = map(elementsData, function(elementData) {
  437. return elementData.element;
  438. });
  439. // (4) create tree
  440. forEach(elementsData, function(elementData) {
  441. var depth = elementData.depth;
  442. if (!self.hasRelations(elementData.element, elements)) {
  443. removeElement(elementData.element, elements);
  444. return;
  445. }
  446. if (!canCopy(elementData.element, elements)) {
  447. removeElement(elementData.element, elements);
  448. return;
  449. }
  450. if (!tree[depth]) {
  451. tree[depth] = [];
  452. }
  453. tree[depth].push(elementData.descriptor);
  454. });
  455. return tree;
  456. };
  457. // helpers //////////
  458. function isAttacher(element) {
  459. return !!element.host;
  460. }
  461. function isConnection(element) {
  462. return !!element.waypoints;
  463. }
  464. function isLabel(element) {
  465. return !!element.labelTarget;
  466. }
  467. function copyWaypoints(element) {
  468. return map(element.waypoints, function(waypoint) {
  469. waypoint = copyWaypoint(waypoint);
  470. if (waypoint.original) {
  471. waypoint.original = copyWaypoint(waypoint.original);
  472. }
  473. return waypoint;
  474. });
  475. }
  476. function copyWaypoint(waypoint) {
  477. return assign({}, waypoint);
  478. }
  479. function removeElement(element, elements) {
  480. var index = elements.indexOf(element);
  481. if (index === -1) {
  482. return elements;
  483. }
  484. return elements.splice(index, 1);
  485. }