CopyPaste.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. import {
  2. isArray,
  3. forEach,
  4. map,
  5. matchPattern,
  6. find,
  7. findIndex,
  8. sortBy,
  9. reduce
  10. } from 'min-dash';
  11. import { getBBox } from '../../util/Elements';
  12. import {
  13. center,
  14. delta as posDelta
  15. } from '../../util/PositionUtil';
  16. import {
  17. getTopLevel
  18. } from '../../util/CopyPasteUtil';
  19. import {
  20. eachElement
  21. } from '../../util/Elements';
  22. export default function CopyPaste(
  23. eventBus, modeling, elementFactory,
  24. rules, clipboard, canvas) {
  25. this._eventBus = eventBus;
  26. this._modeling = modeling;
  27. this._elementFactory = elementFactory;
  28. this._rules = rules;
  29. this._canvas = canvas;
  30. this._clipboard = clipboard;
  31. this._descriptors = [];
  32. // Element creation priorities:
  33. // - 1: Independent shapes
  34. // - 2: Attached shapes
  35. // - 3: Connections
  36. // - 4: labels
  37. this.registerDescriptor(function(element, descriptor) {
  38. // Base priority
  39. descriptor.priority = 1;
  40. descriptor.id = element.id;
  41. if (element.parent) {
  42. descriptor.parent = element.parent.id;
  43. }
  44. if (element.labelTarget) {
  45. // Labels priority
  46. descriptor.priority = 4;
  47. descriptor.labelTarget = element.labelTarget.id;
  48. }
  49. if (element.host) {
  50. // Attached shapes priority
  51. descriptor.priority = 2;
  52. descriptor.host = element.host.id;
  53. }
  54. if (typeof element.x === 'number') {
  55. descriptor.x = element.x;
  56. descriptor.y = element.y;
  57. }
  58. if (element.width) {
  59. descriptor.width = element.width;
  60. descriptor.height = element.height;
  61. }
  62. if (element.waypoints) {
  63. // Connections priority
  64. descriptor.priority = 3;
  65. descriptor.waypoints = [];
  66. forEach(element.waypoints, function(waypoint) {
  67. var wp = {
  68. x: waypoint.x,
  69. y: waypoint.y
  70. };
  71. if (waypoint.original) {
  72. wp.original = {
  73. x: waypoint.original.x,
  74. y: waypoint.original.y
  75. };
  76. }
  77. descriptor.waypoints.push(wp);
  78. });
  79. }
  80. if (element.source && element.target) {
  81. descriptor.source = element.source.id;
  82. descriptor.target = element.target.id;
  83. }
  84. return descriptor;
  85. });
  86. }
  87. CopyPaste.$inject = [
  88. 'eventBus',
  89. 'modeling',
  90. 'elementFactory',
  91. 'rules',
  92. 'clipboard',
  93. 'canvas'
  94. ];
  95. /**
  96. * Copy a number of elements.
  97. *
  98. * @param {djs.model.Base} selectedElements
  99. *
  100. * @return {Object} the copied tree
  101. */
  102. CopyPaste.prototype.copy = function(selectedElements) {
  103. var clipboard = this._clipboard,
  104. tree, bbox;
  105. if (!isArray(selectedElements)) {
  106. selectedElements = selectedElements ? [ selectedElements ] : [];
  107. }
  108. if (!selectedElements.length) {
  109. return;
  110. }
  111. tree = this.createTree(selectedElements);
  112. bbox = this._bbox = center(getBBox(tree.allShapes));
  113. // not needed after computing the center position of the copied elements
  114. delete tree.allShapes;
  115. forEach(tree, function(elements) {
  116. forEach(elements, function(element) {
  117. var delta, labelTarget;
  118. // set label's relative position to their label target
  119. if (element.labelTarget) {
  120. labelTarget = find(elements, matchPattern({ id: element.labelTarget }));
  121. // just grab the delta from the first waypoint
  122. if (labelTarget.waypoints) {
  123. delta = posDelta(element, labelTarget.waypoints[0]);
  124. } else {
  125. delta = posDelta(element, labelTarget);
  126. }
  127. } else
  128. if (element.priority === 3) {
  129. // connections have priority 3
  130. delta = [];
  131. forEach(element.waypoints, function(waypoint) {
  132. var waypointDelta = posDelta(waypoint, bbox);
  133. delta.push(waypointDelta);
  134. });
  135. } else {
  136. delta = posDelta(element, bbox);
  137. }
  138. element.delta = delta;
  139. });
  140. });
  141. this._eventBus.fire('elements.copy', { context: { tree: tree } });
  142. // if tree is empty, means that nothing can be or is allowed to be copied
  143. if (Object.keys(tree).length === 0) {
  144. clipboard.clear();
  145. } else {
  146. clipboard.set(tree);
  147. }
  148. this._eventBus.fire('elements.copied', { context: { tree: tree } });
  149. return tree;
  150. };
  151. // Allow pasting under the cursor
  152. CopyPaste.prototype.paste = function(context) {
  153. var clipboard = this._clipboard,
  154. modeling = this._modeling,
  155. eventBus = this._eventBus,
  156. rules = this._rules;
  157. var tree = clipboard.get(),
  158. topParent = context.element,
  159. position = context.point,
  160. newTree, canPaste;
  161. if (clipboard.isEmpty()) {
  162. return;
  163. }
  164. newTree = reduce(tree, function(pasteTree, elements, depthStr) {
  165. var depth = parseInt(depthStr, 10);
  166. if (isNaN(depth)) {
  167. return pasteTree;
  168. }
  169. pasteTree[depth] = elements;
  170. return pasteTree;
  171. }, {});
  172. canPaste = rules.allowed('elements.paste', {
  173. tree: newTree,
  174. target: topParent
  175. });
  176. if (!canPaste) {
  177. eventBus.fire('elements.paste.rejected', {
  178. context: {
  179. tree: newTree,
  180. position: position,
  181. target: topParent
  182. }
  183. });
  184. return;
  185. }
  186. modeling.pasteElements(newTree, topParent, position);
  187. };
  188. CopyPaste.prototype._computeDelta = function(elements, element) {
  189. var bbox = this._bbox,
  190. delta = {};
  191. // set label's relative position to their label target
  192. if (element.labelTarget) {
  193. return posDelta(element, element.labelTarget);
  194. }
  195. // connections have prority 3
  196. if (element.priority === 3) {
  197. delta = [];
  198. forEach(element.waypoints, function(waypoint) {
  199. var waypointDelta = posDelta(waypoint, bbox);
  200. delta.push(waypointDelta);
  201. });
  202. } else {
  203. delta = posDelta(element, bbox);
  204. }
  205. return delta;
  206. };
  207. /**
  208. * Checks if the element in question has a relations to other elements.
  209. * Possible dependants: connections, labels, attachers
  210. *
  211. * @param {Array} elements
  212. * @param {Object} element
  213. *
  214. * @return {Boolean}
  215. */
  216. CopyPaste.prototype.hasRelations = function(elements, element) {
  217. var source, target, labelTarget;
  218. if (element.waypoints) {
  219. source = find(elements, matchPattern({ id: element.source.id }));
  220. target = find(elements, matchPattern({ id: element.target.id }));
  221. if (!source || !target) {
  222. return false;
  223. }
  224. }
  225. if (element.labelTarget) {
  226. labelTarget = find(elements, matchPattern({ id: element.labelTarget.id }));
  227. if (!labelTarget) {
  228. return false;
  229. }
  230. }
  231. return true;
  232. };
  233. CopyPaste.prototype.registerDescriptor = function(descriptor) {
  234. if (typeof descriptor !== 'function') {
  235. throw new Error('the descriptor must be a function');
  236. }
  237. if (this._descriptors.indexOf(descriptor) !== -1) {
  238. throw new Error('this descriptor is already registered');
  239. }
  240. this._descriptors.push(descriptor);
  241. };
  242. CopyPaste.prototype._executeDescriptors = function(data) {
  243. if (!data.descriptor) {
  244. data.descriptor = {};
  245. }
  246. forEach(this._descriptors, function(descriptor) {
  247. data.descriptor = descriptor(data.element, data.descriptor);
  248. });
  249. return data;
  250. };
  251. /**
  252. * Creates a tree like structure from an arbitrary collection of elements
  253. *
  254. * @example
  255. * tree: {
  256. * 0: [
  257. * { id: 'shape_12da', priority: 1, ... },
  258. * { id: 'shape_01bj', priority: 1, ... },
  259. * { id: 'connection_79fa', source: 'shape_12da', target: 'shape_01bj', priority: 3, ... },
  260. * ],
  261. * 1: [ ... ]
  262. * };
  263. *
  264. * @param {Array} elements
  265. * @return {Object}
  266. */
  267. CopyPaste.prototype.createTree = function(elements) {
  268. var rules = this._rules,
  269. self = this;
  270. var tree = {},
  271. includedElements = [],
  272. _elements;
  273. var topLevel = getTopLevel(elements);
  274. tree.allShapes = [];
  275. function canCopy(collection, element) {
  276. return rules.allowed('element.copy', {
  277. collection: collection,
  278. element: element
  279. });
  280. }
  281. function includeElement(data) {
  282. var idx = findIndex(includedElements, matchPattern({ element: data.element })),
  283. element;
  284. if (idx !== -1) {
  285. element = includedElements[idx];
  286. } else {
  287. return includedElements.push(data);
  288. }
  289. // makes sure that it has the correct depth
  290. if (element.depth < data.depth) {
  291. includedElements.splice(idx, 1);
  292. includedElements.push(data);
  293. }
  294. }
  295. eachElement(topLevel, function(element, i, depth) {
  296. var nestedChildren = element.children;
  297. // don't add labels directly
  298. if (element.labelTarget) {
  299. return;
  300. }
  301. function getNested(lists) {
  302. forEach(lists, function(list) {
  303. if (list && list.length) {
  304. forEach(list, function(elem) {
  305. forEach(elem.labels, function(label) {
  306. includeElement({
  307. element: label,
  308. depth: depth
  309. });
  310. });
  311. includeElement({
  312. element: elem,
  313. depth: depth
  314. });
  315. });
  316. }
  317. });
  318. }
  319. // fetch element's labels
  320. forEach(element.labels, function(label) {
  321. includeElement({
  322. element: label,
  323. depth: depth
  324. });
  325. });
  326. getNested([ element.attachers, element.incoming, element.outgoing ]);
  327. includeElement({
  328. element: element,
  329. depth: depth
  330. });
  331. if (nestedChildren) {
  332. return nestedChildren;
  333. }
  334. });
  335. includedElements = map(includedElements, function(data) {
  336. // this is where other registered descriptors hook in
  337. return self._executeDescriptors(data);
  338. });
  339. // order the elements to check if the ones dependant on others (by relationship)
  340. // can be copied. f.ex: label needs it's label target
  341. includedElements = sortBy(includedElements, function(data) {
  342. return data.descriptor.priority;
  343. });
  344. _elements = map(includedElements, function(data) {
  345. return data.element;
  346. });
  347. forEach(includedElements, function(data) {
  348. var depth = data.depth;
  349. if (!self.hasRelations(tree.allShapes, data.element)) {
  350. return;
  351. }
  352. if (!canCopy(_elements, data.element)) {
  353. return;
  354. }
  355. tree.allShapes.push(data.element);
  356. // create depth branches
  357. if (!tree[depth]) {
  358. tree[depth] = [];
  359. }
  360. tree[depth].push(data.descriptor);
  361. });
  362. return tree;
  363. };