SpaceTool.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. import {
  2. assign,
  3. filter,
  4. forEach,
  5. isNumber
  6. } from 'min-dash';
  7. import {
  8. asTRBL,
  9. getMid
  10. } from '../../layout/LayoutUtil';
  11. import { getBBox } from '../../util/Elements';
  12. import { getDirection } from './SpaceUtil';
  13. import { hasPrimaryModifier } from '../../util/Mouse';
  14. import { set as setCursor } from '../../util/Cursor';
  15. import { selfAndAllChildren } from '../../util/Elements';
  16. var abs = Math.abs,
  17. round = Math.round;
  18. var AXIS_TO_DIMENSION = {
  19. x: 'width',
  20. y: 'height'
  21. };
  22. var CURSOR_CROSSHAIR = 'crosshair';
  23. var DIRECTION_TO_TRBL = {
  24. n: 'top',
  25. w: 'left',
  26. s: 'bottom',
  27. e: 'right'
  28. };
  29. var HIGH_PRIORITY = 1500;
  30. var DIRECTION_TO_OPPOSITE = {
  31. n: 's',
  32. w: 'e',
  33. s: 'n',
  34. e: 'w'
  35. };
  36. var PADDING = 20;
  37. /**
  38. * Add or remove space by moving and resizing elements.
  39. *
  40. * @param {Canvas} canvas
  41. * @param {Dragging} dragging
  42. * @param {EventBus} eventBus
  43. * @param {Modeling} modeling
  44. * @param {Rules} rules
  45. * @param {ToolManager} toolManager
  46. * @param {Mouse} mouse
  47. */
  48. export default function SpaceTool(
  49. canvas, dragging, eventBus,
  50. modeling, rules, toolManager,
  51. mouse) {
  52. this._canvas = canvas;
  53. this._dragging = dragging;
  54. this._eventBus = eventBus;
  55. this._modeling = modeling;
  56. this._rules = rules;
  57. this._toolManager = toolManager;
  58. this._mouse = mouse;
  59. var self = this;
  60. toolManager.registerTool('space', {
  61. tool: 'spaceTool.selection',
  62. dragging: 'spaceTool'
  63. });
  64. eventBus.on('spaceTool.selection.end', function(event) {
  65. eventBus.once('spaceTool.selection.ended', function() {
  66. self.activateMakeSpace(event.originalEvent);
  67. });
  68. });
  69. eventBus.on('spaceTool.move', HIGH_PRIORITY , function(event) {
  70. var context = event.context,
  71. initialized = context.initialized;
  72. if (!initialized) {
  73. initialized = context.initialized = self.init(event, context);
  74. }
  75. if (initialized) {
  76. ensureConstraints(event);
  77. }
  78. });
  79. eventBus.on('spaceTool.end', function(event) {
  80. var context = event.context,
  81. axis = context.axis,
  82. direction = context.direction,
  83. movingShapes = context.movingShapes,
  84. resizingShapes = context.resizingShapes,
  85. start = context.start;
  86. if (!context.initialized) {
  87. return;
  88. }
  89. ensureConstraints(event);
  90. var delta = {
  91. x: 0,
  92. y: 0
  93. };
  94. delta[ axis ] = round(event[ 'd' + axis ]);
  95. self.makeSpace(movingShapes, resizingShapes, delta, direction, start);
  96. eventBus.once('spaceTool.ended', function(event) {
  97. // activate space tool selection after make space
  98. self.activateSelection(event.originalEvent, true, true);
  99. });
  100. });
  101. }
  102. SpaceTool.$inject = [
  103. 'canvas',
  104. 'dragging',
  105. 'eventBus',
  106. 'modeling',
  107. 'rules',
  108. 'toolManager',
  109. 'mouse'
  110. ];
  111. /**
  112. * Activate space tool selection.
  113. *
  114. * @param {Object} event
  115. * @param {boolean} autoActivate
  116. */
  117. SpaceTool.prototype.activateSelection = function(event, autoActivate, reactivate) {
  118. this._dragging.init(event, 'spaceTool.selection', {
  119. autoActivate: autoActivate,
  120. cursor: CURSOR_CROSSHAIR,
  121. data: {
  122. context: {
  123. reactivate: reactivate
  124. }
  125. },
  126. trapClick: false
  127. });
  128. };
  129. /**
  130. * Activate space tool make space.
  131. *
  132. * @param {MouseEvent} event
  133. */
  134. SpaceTool.prototype.activateMakeSpace = function(event) {
  135. this._dragging.init(event, 'spaceTool', {
  136. autoActivate: true,
  137. cursor: CURSOR_CROSSHAIR,
  138. data: {
  139. context: {}
  140. }
  141. });
  142. };
  143. /**
  144. * Make space.
  145. *
  146. * @param {Array<djs.model.Shape>} movingShapes
  147. * @param {Array<djs.model.Shape>} resizingShapes
  148. * @param {Object} delta
  149. * @param {number} delta.x
  150. * @param {number} delta.y
  151. * @param {string} direction
  152. * @param {number} start
  153. */
  154. SpaceTool.prototype.makeSpace = function(movingShapes, resizingShapes, delta, direction, start) {
  155. return this._modeling.createSpace(movingShapes, resizingShapes, delta, direction, start);
  156. };
  157. /**
  158. * Initialize make space and return true if that was successful.
  159. *
  160. * @param {Object} event
  161. * @param {Object} context
  162. *
  163. * @return {boolean}
  164. */
  165. SpaceTool.prototype.init = function(event, context) {
  166. var axis = abs(event.dx) > abs(event.dy) ? 'x' : 'y',
  167. delta = event[ 'd' + axis ],
  168. start = event[ axis ] - delta;
  169. if (abs(delta) < 5) {
  170. return false;
  171. }
  172. // invert delta to remove space when moving left
  173. if (delta < 0) {
  174. delta *= -1;
  175. }
  176. // invert delta to add/remove space when removing/adding space if modifier key is pressed
  177. if (hasPrimaryModifier(event)) {
  178. delta *= -1;
  179. }
  180. var direction = getDirection(axis, delta);
  181. var root = this._canvas.getRootElement();
  182. var children = selfAndAllChildren(root, true);
  183. var elements = this.calculateAdjustments(children, axis, delta, start);
  184. var minDimensions = this._eventBus.fire('spaceTool.getMinDimensions', {
  185. axis: axis,
  186. direction: direction,
  187. shapes: elements.resizingShapes,
  188. start: start
  189. });
  190. var spaceToolConstraints = getSpaceToolConstraints(elements, axis, direction, start, minDimensions);
  191. assign(
  192. context,
  193. elements,
  194. {
  195. axis: axis,
  196. direction: direction,
  197. spaceToolConstraints: spaceToolConstraints,
  198. start: start
  199. }
  200. );
  201. setCursor('resize-' + (axis === 'x' ? 'ew' : 'ns'));
  202. return true;
  203. };
  204. /**
  205. * Get elements to be moved and resized.
  206. *
  207. * @param {Array<djs.model.Shape>} elements
  208. * @param {string} axis
  209. * @param {number} delta
  210. * @param {number} start
  211. *
  212. * @return {Object}
  213. */
  214. SpaceTool.prototype.calculateAdjustments = function(elements, axis, delta, start) {
  215. var rules = this._rules;
  216. var movingShapes = [],
  217. resizingShapes = [];
  218. var attachers = [],
  219. connections = [];
  220. function moveShape(shape) {
  221. if (!movingShapes.includes(shape)) {
  222. movingShapes.push(shape);
  223. }
  224. var label = shape.label;
  225. // move external label if its label target is moving
  226. if (label && !movingShapes.includes(label)) {
  227. movingShapes.push(label);
  228. }
  229. }
  230. function resizeShape(shape) {
  231. if (!resizingShapes.includes(shape)) {
  232. resizingShapes.push(shape);
  233. }
  234. }
  235. forEach(elements, function(element) {
  236. if (!element.parent || isLabel(element)) {
  237. return;
  238. }
  239. // handle connections separately
  240. if (isConnection(element)) {
  241. connections.push(element);
  242. return;
  243. }
  244. var shapeStart = element[ axis ],
  245. shapeEnd = shapeStart + element[ AXIS_TO_DIMENSION[ axis ] ];
  246. // handle attachers separately
  247. if (isAttacher(element)
  248. && ((delta > 0 && getMid(element)[ axis ] > start)
  249. || (delta < 0 && getMid(element)[ axis ] < start))) {
  250. attachers.push(element);
  251. return;
  252. }
  253. // move shape if its start is after space tool
  254. if ((delta > 0 && shapeStart > start)
  255. || (delta < 0 && shapeEnd < start)) {
  256. moveShape(element);
  257. return;
  258. }
  259. // resize shape if it's resizable and its start is before and its end is after space tool
  260. if (shapeStart < start
  261. && shapeEnd > start
  262. && rules.allowed('shape.resize', { shape: element })
  263. ) {
  264. resizeShape(element);
  265. return;
  266. }
  267. });
  268. // move attacher if its host is moving
  269. forEach(movingShapes, function(shape) {
  270. var attachers = shape.attachers;
  271. if (attachers) {
  272. forEach(attachers, function(attacher) {
  273. moveShape(attacher);
  274. });
  275. }
  276. });
  277. var allShapes = movingShapes.concat(resizingShapes);
  278. // move attacher if its mid is after space tool and its host is moving or resizing
  279. forEach(attachers, function(attacher) {
  280. var host = attacher.host;
  281. if (includes(allShapes, host)) {
  282. moveShape(attacher);
  283. }
  284. });
  285. allShapes = movingShapes.concat(resizingShapes);
  286. // move external label if its label target's (connection) source and target are moving
  287. forEach(connections, function(connection) {
  288. var source = connection.source,
  289. target = connection.target,
  290. label = connection.label;
  291. if (includes(allShapes, source)
  292. && includes(allShapes, target)
  293. && label) {
  294. moveShape(label);
  295. }
  296. });
  297. return {
  298. movingShapes: movingShapes,
  299. resizingShapes: resizingShapes
  300. };
  301. };
  302. SpaceTool.prototype.toggle = function() {
  303. if (this.isActive()) {
  304. return this._dragging.cancel();
  305. }
  306. var mouseEvent = this._mouse.getLastMoveEvent();
  307. this.activateSelection(mouseEvent, !!mouseEvent);
  308. };
  309. SpaceTool.prototype.isActive = function() {
  310. var context = this._dragging.context();
  311. if (context) {
  312. return /^spaceTool/.test(context.prefix);
  313. }
  314. return false;
  315. };
  316. // helpers //////////
  317. function addPadding(trbl) {
  318. return {
  319. top: trbl.top - PADDING,
  320. right: trbl.right + PADDING,
  321. bottom: trbl.bottom + PADDING,
  322. left: trbl.left - PADDING
  323. };
  324. }
  325. function ensureConstraints(event) {
  326. var context = event.context,
  327. spaceToolConstraints = context.spaceToolConstraints;
  328. if (!spaceToolConstraints) {
  329. return;
  330. }
  331. var x, y;
  332. if (isNumber(spaceToolConstraints.left)) {
  333. x = Math.max(event.x, spaceToolConstraints.left);
  334. event.dx = event.dx + x - event.x;
  335. event.x = x;
  336. }
  337. if (isNumber(spaceToolConstraints.right)) {
  338. x = Math.min(event.x, spaceToolConstraints.right);
  339. event.dx = event.dx + x - event.x;
  340. event.x = x;
  341. }
  342. if (isNumber(spaceToolConstraints.top)) {
  343. y = Math.max(event.y, spaceToolConstraints.top);
  344. event.dy = event.dy + y - event.y;
  345. event.y = y;
  346. }
  347. if (isNumber(spaceToolConstraints.bottom)) {
  348. y = Math.min(event.y, spaceToolConstraints.bottom);
  349. event.dy = event.dy + y - event.y;
  350. event.y = y;
  351. }
  352. }
  353. function getSpaceToolConstraints(elements, axis, direction, start, minDimensions) {
  354. var movingShapes = elements.movingShapes,
  355. resizingShapes = elements.resizingShapes;
  356. if (!resizingShapes.length) {
  357. return;
  358. }
  359. var spaceToolConstraints = {},
  360. min,
  361. max;
  362. forEach(resizingShapes, function(resizingShape) {
  363. var attachers = resizingShape.attachers,
  364. children = resizingShape.children;
  365. var resizingShapeBBox = asTRBL(resizingShape);
  366. // find children that are not moving or resizing
  367. var nonMovingResizingChildren = filter(children, function(child) {
  368. return !isConnection(child) &&
  369. !isLabel(child) &&
  370. !includes(movingShapes, child) &&
  371. !includes(resizingShapes, child);
  372. });
  373. // find children that are moving
  374. var movingChildren = filter(children, function(child) {
  375. return !isConnection(child) && !isLabel(child) && includes(movingShapes, child);
  376. });
  377. var minOrMax,
  378. nonMovingResizingChildrenBBox,
  379. movingChildrenBBox,
  380. movingAttachers = [],
  381. nonMovingAttachers = [],
  382. movingAttachersBBox,
  383. movingAttachersConstraint,
  384. nonMovingAttachersBBox,
  385. nonMovingAttachersConstraint;
  386. if (nonMovingResizingChildren.length) {
  387. nonMovingResizingChildrenBBox = addPadding(asTRBL(getBBox(nonMovingResizingChildren)));
  388. minOrMax = start -
  389. resizingShapeBBox[ DIRECTION_TO_TRBL[ direction ] ] +
  390. nonMovingResizingChildrenBBox[ DIRECTION_TO_TRBL[ direction ] ];
  391. if (direction === 'n') {
  392. spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
  393. } else if (direction === 'w') {
  394. spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
  395. } else if (direction === 's') {
  396. spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
  397. } else if (direction === 'e') {
  398. spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
  399. }
  400. }
  401. if (movingChildren.length) {
  402. movingChildrenBBox = addPadding(asTRBL(getBBox(movingChildren)));
  403. minOrMax = start -
  404. movingChildrenBBox[ DIRECTION_TO_TRBL[ DIRECTION_TO_OPPOSITE[ direction ] ] ] +
  405. resizingShapeBBox[ DIRECTION_TO_TRBL[ DIRECTION_TO_OPPOSITE[ direction ] ] ];
  406. if (direction === 'n') {
  407. spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
  408. } else if (direction === 'w') {
  409. spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
  410. } else if (direction === 's') {
  411. spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
  412. } else if (direction === 'e') {
  413. spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
  414. }
  415. }
  416. if (attachers && attachers.length) {
  417. attachers.forEach(function(attacher) {
  418. if (includes(movingShapes, attacher)) {
  419. movingAttachers.push(attacher);
  420. } else {
  421. nonMovingAttachers.push(attacher);
  422. }
  423. });
  424. if (movingAttachers.length) {
  425. movingAttachersBBox = asTRBL(getBBox(movingAttachers.map(getMid)));
  426. movingAttachersConstraint = resizingShapeBBox[ DIRECTION_TO_TRBL[ DIRECTION_TO_OPPOSITE[ direction ] ] ]
  427. - (movingAttachersBBox[ DIRECTION_TO_TRBL[ DIRECTION_TO_OPPOSITE[ direction ] ] ] - start);
  428. }
  429. if (nonMovingAttachers.length) {
  430. nonMovingAttachersBBox = asTRBL(getBBox(nonMovingAttachers.map(getMid)));
  431. nonMovingAttachersConstraint = nonMovingAttachersBBox[ DIRECTION_TO_TRBL[ direction ] ]
  432. - (resizingShapeBBox[ DIRECTION_TO_TRBL[ direction ] ] - start);
  433. }
  434. if (direction === 'n') {
  435. minOrMax = Math.min(movingAttachersConstraint || Infinity, nonMovingAttachersConstraint || Infinity);
  436. spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
  437. } else if (direction === 'w') {
  438. minOrMax = Math.min(movingAttachersConstraint || Infinity, nonMovingAttachersConstraint || Infinity);
  439. spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
  440. } else if (direction === 's') {
  441. minOrMax = Math.max(movingAttachersConstraint || -Infinity, nonMovingAttachersConstraint || -Infinity);
  442. spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
  443. } else if (direction === 'e') {
  444. minOrMax = Math.max(movingAttachersConstraint || -Infinity, nonMovingAttachersConstraint || -Infinity);
  445. spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
  446. }
  447. }
  448. var resizingShapeMinDimensions = minDimensions && minDimensions[ resizingShape.id ];
  449. if (resizingShapeMinDimensions) {
  450. if (direction === 'n') {
  451. minOrMax = start +
  452. resizingShape[ AXIS_TO_DIMENSION [ axis ] ] -
  453. resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ];
  454. spaceToolConstraints.bottom = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
  455. } else if (direction === 'w') {
  456. minOrMax = start +
  457. resizingShape[ AXIS_TO_DIMENSION [ axis ] ] -
  458. resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ];
  459. spaceToolConstraints.right = max = isNumber(max) ? Math.min(max, minOrMax) : minOrMax;
  460. } else if (direction === 's') {
  461. minOrMax = start -
  462. resizingShape[ AXIS_TO_DIMENSION [ axis ] ] +
  463. resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ];
  464. spaceToolConstraints.top = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
  465. } else if (direction === 'e') {
  466. minOrMax = start -
  467. resizingShape[ AXIS_TO_DIMENSION [ axis ] ] +
  468. resizingShapeMinDimensions[ AXIS_TO_DIMENSION[ axis ] ];
  469. spaceToolConstraints.left = min = isNumber(min) ? Math.max(min, minOrMax) : minOrMax;
  470. }
  471. }
  472. });
  473. return spaceToolConstraints;
  474. }
  475. function includes(array, item) {
  476. return array.indexOf(item) !== -1;
  477. }
  478. function isAttacher(element) {
  479. return !!element.host;
  480. }
  481. function isConnection(element) {
  482. return !!element.waypoints;
  483. }
  484. function isLabel(element) {
  485. return !!element.labelTarget;
  486. }