Move.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2013, 2021
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. */
  7. define(['./BaseStylePropertyAction', '../../../../lib/@waca/core-client/js/core-client/utils/Deferred', 'jquery', 'doT', '../EventHelper', '../../../../lib/@waca/dashboard-common/dist/ui/interaction/Utils', '../../../../lib/@waca/core-client/js/core-client/ui/KeyCodes', '../../../../app/util/ScreenReaderUtil', '../../../../app/nls/StringResources', 'text!./MoveEdges.html', './LayoutIndicator', './GuidelineManager', '../../../util/PxPercentUtil', '../../../../lib/@waca/core-client/js/core-client/i18n/Formatter', '../../../../lib/@waca/dashboard-common/dist/utils/EventChainLocal', 'underscore', 'bspopover'], function (BaseClass, Deferred, $, dot, eventHelper, utils, KeyCodes, ScreenReaderUtil, stringResources, MoveTemplate, LayoutIndicator, GuidelineManager, PxPercentUtil, Formatter, EventChainLocal, _) {
  8. var Move = null;
  9. Move = BaseClass.extend({
  10. _savedSelectedNodes: null,
  11. init: function init(controller) {
  12. var _this = this;
  13. Move.inherited('init', this, arguments);
  14. this.controller = controller;
  15. this.isDragging = false;
  16. this._isTouch = false;
  17. this.dropInfo = {};
  18. this._guidelineManager = new GuidelineManager({ 'layoutController': controller.layoutController });
  19. this._ScreenReader = new ScreenReaderUtil();
  20. this.whenIsReadyDfd = new Deferred();
  21. this._createAvatarContainer();
  22. this._layoutIndicator = new LayoutIndicator({ 'node': this._$avatarContainer });
  23. this._initialize();
  24. // Formating the coordinates is a pretty big perf hit while moving, so throttle it
  25. this._updateLayoutIndicatorContent = _.throttle(function (evt, dragBoxSize, nodeInfo, revisedOffsets, dragContext) {
  26. var node = nodeInfo.node;
  27. if (node && node._layout && node._layout.layoutController) {
  28. var dropTargetNode = dragContext.dropTargetNode;
  29. var containerNode = node._layout.layoutController.getLayoutContentContainerForDropTarget(dropTargetNode);
  30. var coordinates = _this._convertCoordinates(dragBoxSize, evt.clientX - dragBoxSize.offsetx - revisedOffsets.x, evt.clientY - dragBoxSize.offsety - revisedOffsets.y, containerNode);
  31. var formatedDragCoordinates = _this._formatPosition(node, coordinates, containerNode);
  32. _this._layoutIndicator.updateContent('x: ' + formatedDragCoordinates.left + ' y: ' + formatedDragCoordinates.top);
  33. }
  34. }, 250);
  35. },
  36. /**
  37. * Creates an avatar container. This container will be shared across dashboards, so only need to create it once.
  38. */
  39. _createAvatarContainer: function _createAvatarContainer() {
  40. // Look for an existing avatar div
  41. this._$avatarContainer = $('body').children('.avatarContainer.avatarContainerNotDragging');
  42. if (this._$avatarContainer.length === 0) {
  43. this._$avatarContainer = $('<div class="avatarContainer avatarContainerNotDragging"></div>');
  44. $('body').append(this._$avatarContainer);
  45. }
  46. },
  47. /**
  48. * Get the jQuery object for the avatar node
  49. * @returns jquery object of the avatar node
  50. */
  51. getAvatarContainer: function getAvatarContainer() {
  52. return this._$avatarContainer;
  53. },
  54. whenIsReady: function whenIsReady() {
  55. return this.whenIsReadyDfd.promise;
  56. },
  57. _initialize: function _initialize() {
  58. this._dndManager = this.controller.dashboardApi.getFeature('DashboardDnd.internal');
  59. this.whenIsReadyDfd.resolve();
  60. },
  61. destroy: function destroy() {
  62. if (this._dndManager) {
  63. this._dndManager.resetDragging();
  64. this._dndManager = null;
  65. }
  66. this.resetDraggingState();
  67. this._ScreenReader.destroy();
  68. if (this._iKeyPressTimer) {
  69. clearTimeout(this._iKeyPressTimer);
  70. }
  71. this._$avatarContainer.empty();
  72. },
  73. /**
  74. * Setup the event handlers that will start the dragging operation
  75. *
  76. */
  77. setupStartEventHandlers: function setupStartEventHandlers() {
  78. var moveStart = this.dragStartMouseEvent.bind(this);
  79. var moveStartTouch = this.initiateDragging.bind(this);
  80. var dragStart = this.onDragStart.bind(this);
  81. var n;
  82. for (var i = 0; i < this.selectedNodes.length; i++) {
  83. n = this.selectedNodes[i];
  84. n._events = [];
  85. n._events.push(eventHelper.on(n, 'touchstart', moveStartTouch));
  86. n._events.push(eventHelper.on(n, 'mousedown', moveStart));
  87. n._events.push(eventHelper.on(n, 'dragstart', dragStart));
  88. }
  89. },
  90. onDragStart: function onDragStart() {
  91. // cancel drag events so they don't interfere with the move.
  92. return false;
  93. },
  94. /**
  95. * Clear the event handlers used to start the dragging operation
  96. *
  97. */
  98. clearStartEventHandlers: function clearStartEventHandlers() {
  99. var n;
  100. for (var i = 0; i < this.selectedNodes.length; i++) {
  101. n = this.selectedNodes[i];
  102. if (n._events) {
  103. utils.removeEvents(n._events);
  104. n._events = null;
  105. }
  106. }
  107. },
  108. /**
  109. * Called by the interaction manager when we have new selection
  110. *
  111. * @param nodes -
  112. * selected nodes
  113. * @param evt -
  114. * the event used to select the last node (optional)
  115. */
  116. newSelection: function newSelection(nodes, evt) {
  117. this._resetSelectionState();
  118. this.selectedNodes = nodes;
  119. this._savedSelectedNode = nodes;
  120. this.dropInfo.nodeInfoList = [];
  121. for (var i = 0; i < nodes.length; i++) {
  122. this.dropInfo.nodeInfoList.push({ node: nodes[i] });
  123. }
  124. this.setupStartEventHandlers();
  125. var eventTypeInit = evt && (evt.type === 'mousedown' || evt.type === 'hold' || evt.type === 'touchstart');
  126. if (eventTypeInit && this.selectedNodes.length > 0) {
  127. // allow the user to move while the selection mousedown/hold event is still in effect")
  128. this.initiateDragging(evt);
  129. }
  130. // set up keyboard navigation
  131. if (nodes.length > 0) {
  132. this.controller.$el.on('keydown.interactionMoveKey', this.arrowKeyPress.bind(this));
  133. } else {
  134. this.controller.$el.off('keydown.interactionMoveKey');
  135. }
  136. this.addMoveHandle(nodes);
  137. },
  138. addMoveHandle: function addMoveHandle(nodes) {
  139. var iconsFeature = this.controller.dashboardApi.getFeature('Icons');
  140. var moveIcon = iconsFeature.getIcon('move');
  141. var html = dot.template(MoveTemplate)({
  142. moveIcon: moveIcon.id
  143. });
  144. $(nodes).append(html).find('.moveHandle').attr('title', stringResources.get('moveHandle'));
  145. },
  146. saveNodeInfo: function saveNodeInfo() {
  147. for (var i = 0, iLen = this.dropInfo.nodeInfoList.length; i < iLen; i++) {
  148. this.setNodePositionInfo(this.dropInfo.nodeInfoList[i]);
  149. }
  150. },
  151. /**
  152. * Handle the keyboard move events
  153. */
  154. _iKeyPressTimer: null,
  155. /**
  156. * @param {Event} evt
  157. * Note: It's a valid event for ArrowKeyPress if and only if:
  158. * 1. the key is one of the arrow keys
  159. * 2. shift key hasn't been pressed
  160. * 3. the target doesn't have 'inlineText' class
  161. * 4. the target's parent don't in focus mode
  162. * 5. the event doesn't have a 'preventMoveAction' property
  163. * 6. the event target is not inside tab controls
  164. * 7. the event target is inside a canvas (see https://jsw.ibm.com/browse/CADBC-502)
  165. */
  166. _isValidEventForArrowKeyPress: function _isValidEventForArrowKeyPress(evt) {
  167. var $target = $(evt.target);
  168. var isArrowKey = evt.keyCode === KeyCodes.LEFT_ARROW || evt.keyCode === KeyCodes.RIGHT_ARROW || evt.keyCode === KeyCodes.UP_ARROW || evt.keyCode === KeyCodes.DOWN_ARROW;
  169. if (isArrowKey) {
  170. var inTabControls = $target.closest('.ba-common-tabList').length !== 0;
  171. var eventChainLocal = new EventChainLocal(evt);
  172. var isFromCanvas = $('.dashboardFrameCentre').has(evt.target).length > 0;
  173. return !(evt.shiftKey || $target.hasClass('inlineText') || $target.parents().hasClass('widgetFocus') || eventChainLocal.getProperty('preventMoveAction') || inTabControls) && isFromCanvas;
  174. } else {
  175. return false;
  176. }
  177. },
  178. /**
  179. * @param {Event} evt
  180. * Note: init move delta and adjust according to which arrow key is pressed
  181. */
  182. _calculateInitDeltasForArrowKeyPress: function _calculateInitDeltasForArrowKeyPress(evt) {
  183. var iDeltaTop = 0;
  184. var iDeltaLeft = 0;
  185. var sMessage = '';
  186. var farEdge = false;
  187. switch (evt.keyCode) {
  188. case KeyCodes.LEFT_ARROW:
  189. iDeltaLeft = -1;
  190. sMessage = stringResources.get('srWidgetMoveLeft');
  191. break;
  192. case KeyCodes.UP_ARROW:
  193. iDeltaTop = -1;
  194. sMessage = stringResources.get('srWidgetMoveUp');
  195. break;
  196. case KeyCodes.RIGHT_ARROW:
  197. iDeltaLeft = 1;
  198. sMessage = stringResources.get('srWidgetMoveRight');
  199. farEdge = true;
  200. break;
  201. case KeyCodes.DOWN_ARROW:
  202. iDeltaTop = 1;
  203. sMessage = stringResources.get('srWidgetMoveDown');
  204. farEdge = true;
  205. break;
  206. }
  207. return { iDeltaTop: iDeltaTop, iDeltaLeft: iDeltaLeft, farEdge: farEdge, sMessage: sMessage };
  208. },
  209. /**
  210. * @param node : the node need to be moved
  211. * @param initDeltas
  212. * @param nodeInfo
  213. * Note: calculate the position update for a node when pressing an arrow key.
  214. */
  215. _calculatePositionUpdateForNodeWhenArrowKeyPress: function _calculatePositionUpdateForNodeWhenArrowKeyPress(node, initDeltas, nodeInfo) {
  216. var iDeltaTop = initDeltas.iDeltaTop,
  217. iDeltaLeft = initDeltas.iDeltaLeft,
  218. farEdge = initDeltas.farEdge;
  219. var position = {
  220. left: parseInt($(node).css('left')),
  221. top: parseInt($(node).css('top'))
  222. };
  223. var newDeltaLeft = 0;
  224. var newDeltaTop = 0;
  225. var dragbox = { 'top': position.top, 'left': position.left, 'right': position.left + node.clientWidth, 'bottom': position.top + node.clientHeight };
  226. // TODO -- should not call getLayoutContentContainer - we need to detect the parent of the node being moved..
  227. var containerNode = nodeInfo.node._layout.layoutController.getLayoutContentContainer();
  228. this._guidelineManager.getReady(containerNode, dragbox, true);
  229. var newSnapCoords = this._getNewCoordinates(dragbox);
  230. var revisedOffsets = this.getRevisedOffsets(newSnapCoords);
  231. if (this._guidelineManager.gridGuidelines && this._guidelineManager.gridGuidelines.snapGrid) {
  232. if (farEdge) {
  233. revisedOffsets.x = this._guidelineManager.gridGuidelines.gridSize - revisedOffsets.x;
  234. revisedOffsets.y = this._guidelineManager.gridGuidelines.gridSize - revisedOffsets.y;
  235. }
  236. /** If the revisedOffset is less than one we jump to the next snap point. this is
  237. because we do not support half pixels in our model. */
  238. if (revisedOffsets.x <= 1) {
  239. revisedOffsets.x = 0;
  240. }
  241. if (revisedOffsets.y <= 1) {
  242. revisedOffsets.y = 0;
  243. }
  244. revisedOffsets.x = revisedOffsets.x === null || revisedOffsets.x === 0 ? this._guidelineManager.gridGuidelines.gridSize : revisedOffsets.x;
  245. revisedOffsets.y = revisedOffsets.y === null || revisedOffsets.y === 0 ? this._guidelineManager.gridGuidelines.gridSize : revisedOffsets.y;
  246. newDeltaLeft = iDeltaLeft * revisedOffsets.x;
  247. newDeltaTop = iDeltaTop * revisedOffsets.y;
  248. } else {
  249. newDeltaLeft = iDeltaLeft;
  250. newDeltaTop = iDeltaTop;
  251. }
  252. nodeInfo.dropPosition = {
  253. x: position.left + newDeltaLeft,
  254. y: position.top + newDeltaTop,
  255. before: nodeInfo.oldValues.nextSibling
  256. };
  257. var positionUpdate = nodeInfo.node._layout.parentLayout.getWidgetPositionUpdate(nodeInfo);
  258. return positionUpdate;
  259. },
  260. /**
  261. * @param positionUpdateArray
  262. * @param sMessage
  263. * Note: update the model after all nodes had been moved the their destinations.
  264. */
  265. _updateModelAfterArrowKeyPress: function _updateModelAfterArrowKeyPress(positionUpdateArray, sMessage) {
  266. // delay the update of the model to account for multiple keypress events in a row
  267. clearTimeout(this._iKeyPressTimer);
  268. this._iKeyPressTimer = setTimeout(function () {
  269. this.emit('move:end', { 'currentSelection': this.selectedNodes });
  270. this.updateModel(positionUpdateArray);
  271. this.refreshProperties();
  272. this._ScreenReader.callOut(sMessage);
  273. }.bind(this), 500);
  274. },
  275. /**
  276. * @param {Event} evt
  277. * Note: move all nodes when pressing an arrow key.
  278. */
  279. _moveNodesForArrowKeyPress: function _moveNodesForArrowKeyPress(evt) {
  280. var positionUpdateArray = [];
  281. var moves = [];
  282. var initDeltas = this._calculateInitDeltasForArrowKeyPress(evt);
  283. for (var i = 0, iLen = this.dropInfo.nodeInfoList.length; i < iLen; i++) {
  284. var nodeInfo = this.dropInfo.nodeInfoList[i];
  285. if (nodeInfo.node._layout) {
  286. var node = this.dropInfo.nodeInfoList[i].node;
  287. var positionUpdate = this._calculatePositionUpdateForNodeWhenArrowKeyPress(node, initDeltas, nodeInfo);
  288. positionUpdateArray.push(positionUpdate);
  289. moves.push({
  290. node: node,
  291. newPosition: {
  292. top: parseInt(positionUpdate.style.top, 10),
  293. left: parseInt(positionUpdate.style.left, 10)
  294. }
  295. });
  296. }
  297. }
  298. this.moveNodesToNewPositions(moves);
  299. return { positionUpdateArray: positionUpdateArray, sMessage: initDeltas.sMessage };
  300. },
  301. /**
  302. * @param moves
  303. * Note: the structure of each element of moves:
  304. * {
  305. * node: a node need to be moved,
  306. * newPosition: {
  307. * top: this value will be set to node.style.top,
  308. * left: this value will be set to node.style.left
  309. * }
  310. * }
  311. */
  312. moveNodesToNewPositions: function moveNodesToNewPositions(moves) {
  313. for (var i = 0; i < moves.length; i++) {
  314. var move = moves[i];
  315. var node = move.node;
  316. var newPosition = move.newPosition;
  317. utils.setTopLeft(node, newPosition.top, newPosition.left);
  318. }
  319. },
  320. /**
  321. * @param {Event} evt
  322. * Note: being called everytime when a key is pressed. But only process the event when it can pass the validation.
  323. */
  324. arrowKeyPress: function arrowKeyPress(evt) {
  325. if (this._isValidEventForArrowKeyPress(evt)) {
  326. evt.preventDefault();
  327. // broadcast that a move has started
  328. this.emit('move:start', { 'currentSelection': this.selectedNodes });
  329. this.saveNodeInfo();
  330. var _moveNodesForArrowKey = this._moveNodesForArrowKeyPress(evt),
  331. positionUpdateArray = _moveNodesForArrowKey.positionUpdateArray,
  332. sMessage = _moveNodesForArrowKey.sMessage;
  333. this._updateModelAfterArrowKeyPress(positionUpdateArray, sMessage);
  334. }
  335. },
  336. _resetSelectionState: function _resetSelectionState() {
  337. if (this.selectedNodes) {
  338. // If we are dragging and the selection changes, we just need to cancel the drag.
  339. if (this._dndManager) {
  340. this._dndManager.resetDragging();
  341. }
  342. this.dropInfo = {};
  343. this.clearStartEventHandlers();
  344. }
  345. },
  346. /**
  347. * Called to stop the dragging operation
  348. */
  349. dragStopEvent: function dragStopEvent(evt, dropContext) {
  350. if (this.isDragging) {
  351. this._showingLayoutIndicator = false;
  352. this.handleDrop(dropContext);
  353. this.resetDraggingState();
  354. this._$avatarContainer[0].className = 'avatarContainer avatarContainerNotDragging';
  355. // While moving, the target will lose focus. We restore the focus once we are done.
  356. this.$lastFocus.focus();
  357. //trigger event stopMove
  358. this.controller.eventRouter.trigger('widget:stopMove');
  359. this.emit('move:end', { 'currentSelection': this.selectedNodes });
  360. this.refreshProperties();
  361. this._guidelineManager.hideAll();
  362. this._guidelineManager.finish();
  363. this._layoutIndicator.remove();
  364. }
  365. },
  366. resetDraggingState: function resetDraggingState() {
  367. this.isDragging = false;
  368. $(this.selectedNodes).removeClass('nodeDragging');
  369. this.dropInfo.operation = null;
  370. },
  371. handleDrop: function handleDrop() /*dropContext*/{
  372. // To be overriden by sub-classes
  373. },
  374. /**
  375. * Event handler that will initiate the dragging operation on mouse down
  376. *
  377. * @param evt
  378. * @returns {Boolean} - indicate whether we want to stop the propagation of the event
  379. */
  380. dragStartMouseEvent: function dragStartMouseEvent(evt) {
  381. if (this._isTouch) {
  382. // We have a touch event, we skip mouse events.
  383. return false;
  384. }
  385. if (evt.type === 'mousedown' && ((evt.buttons || evt.which) !== 1 || evt.ctrlKey)) {
  386. return;
  387. }
  388. this.initiateDragging(evt);
  389. },
  390. /**
  391. * Helper method that will setup all the handlers needed by the dragging operation This method is called when we want to start a dragging operation
  392. *
  393. * @param evt
  394. */
  395. initiateDragging: function initiateDragging(evt, nodeMove) {
  396. // ignore drag unless it came from the moveHandle or the margin of a data widget
  397. var wasDraggedFromMoveHandle = $(evt.target).closest('.moveHandle').length === 1;
  398. var wasDraggedFromWidgetMargin = $(evt.target).closest('.widgetContent').length === 0;
  399. if (!wasDraggedFromMoveHandle && !wasDraggedFromWidgetMargin) {
  400. return true;
  401. }
  402. evt.preventDefault();
  403. this.saveNodeInfo();
  404. this._isTouch = evt.isTouch;
  405. this.$lastFocus = $(':focus');
  406. // get the current drop zone that the node belongs to
  407. // We need to pass this to the Dnd manager in case we are dropping in a drop zone that support scrolling.
  408. // The mouse might not be over the drop zone or any other drop zone and we should still be able to move the widget within its current drop zone
  409. var node = null;
  410. var currentDropZone = null;
  411. if (!nodeMove) {
  412. node = this._getTargetWidgetNode(evt);
  413. } else {
  414. node = nodeMove;
  415. }
  416. if ($(node).hasClass('moveDisabled') || !node) {
  417. //If the node implements its own drag and drop behaviour externally (ie: vizControlWidget),
  418. //it must disable widget drag move by setting class 'moveDisabled'.
  419. return;
  420. }
  421. currentDropZone = node._layout ? node._layout.parentLayout.domNode : null;
  422. this._dndManager.startDrag({
  423. event: evt,
  424. type: this.getDragDataType(),
  425. data: this.dropInfo,
  426. avatar: null,
  427. callerCallbacks: {
  428. onMove: this.dragMoveEvent.bind(this),
  429. onDragDone: this.dragStopEvent.bind(this)
  430. },
  431. moveXThreshold: 3,
  432. moveYThreshold: 3,
  433. currentDropZoneNode: currentDropZone
  434. });
  435. },
  436. _getTargetWidgetNode: function _getTargetWidgetNode(e) {
  437. var $target = $(e.target);
  438. if (!$target.is('.widget')) {
  439. var $targetParentWidget = $target.parents('.widget');
  440. if ($targetParentWidget.length) {
  441. $target = $targetParentWidget;
  442. } else {
  443. $target = $target.parents('.pagegroup');
  444. }
  445. }
  446. return $target[0];
  447. },
  448. getDragDataType: function getDragDataType() {
  449. return null;
  450. },
  451. /**
  452. * Event handler called when the dragged items are being moved
  453. *
  454. * @param evt
  455. * @returns {Boolean} indicates if we want to stop the propagation of the event
  456. */
  457. dragMoveEvent: function dragMoveEvent(evt, dragContext) {
  458. var _this2 = this;
  459. eventHelper.fixEvent(evt);
  460. var i = void 0,
  461. iLen = void 0;
  462. if (!this.isDragging) {
  463. this._$avatarContainer[0].className = 'avatarContainer avatarContainerDragging';
  464. this.emit('move:start', { 'currentSelection': this.selectedNodes });
  465. this.prepareDropInfo(this.dropInfo);
  466. this.isDragging = true;
  467. var selectedNodes = [],
  468. nodes = [];
  469. for (i = 0, iLen = this.dropInfo.nodeInfoList.length; i < iLen; i++) {
  470. var nodeInfo = this.dropInfo.nodeInfoList[i];
  471. selectedNodes.push(nodeInfo.node);
  472. // this is the actual of the widget in the viewport
  473. var nodePosition = nodeInfo.node.getBoundingClientRect();
  474. this.prepareNodeForMove(nodeInfo.node);
  475. nodes.push(nodeInfo.node);
  476. var parentNodePosition = nodeInfo.node.parentNode.getBoundingClientRect();
  477. // move to clientX/clientY, then save the offsetX/offsetY of the click to take into account when we move the element.
  478. var coords = utils.getRotatedCoordinates(nodeInfo.node.parentNode, evt.clientX, evt.clientY, parentNodePosition);
  479. $(nodeInfo.node).addClass('nodeDragging');
  480. utils.setTopLeft(nodeInfo.node, coords.y, coords.x);
  481. var rotateOffset = {
  482. top: $(nodeInfo.node).position().top - nodeInfo.node.offsetTop,
  483. left: $(nodeInfo.node).position().left - nodeInfo.node.offsetLeft
  484. };
  485. // Get the offset of the mouse down position and the node we're moving
  486. nodeInfo.offsety = dragContext.dragObject.startPosition.y - nodePosition.top + rotateOffset.top;
  487. nodeInfo.offsetx = dragContext.dragObject.startPosition.x - nodePosition.left + rotateOffset.left;
  488. nodeInfo.parentNodeRect = parentNodePosition;
  489. nodeInfo.position = nodePosition;
  490. }
  491. // get the size of the drag box
  492. this.dropInfo.dragBoxSize = this._calculateBoxSize(this.dropInfo.nodeInfoList);
  493. // initialize the drag box
  494. if (dragContext.dragObject && dragContext.dropTargetNode) {
  495. dragContext.dropTargetNodeOffset = $(dragContext.dropTargetNode).offset();
  496. var dragBox = this._convertCoordinates(this.dropInfo.dragBoxSize, evt.clientX - this.dropInfo.dragBoxSize.offsetx, evt.clientY - this.dropInfo.dragBoxSize.offsety, dragContext.dropTargetNode, dragContext.dropTargetNodeOffset);
  497. dragContext.dragObject.dragBox = dragBox;
  498. dragContext.dragObject.dragBox.ids = this.getIds(this.dropInfo.nodeInfoList).ids;
  499. this._guidelineManager.getReady(dragContext.dropTargetNode, dragContext.dragObject);
  500. this.postNodeForMove(nodes);
  501. }
  502. this.controller.eventRouter.trigger('widget:startMove', { selectedNodes: selectedNodes });
  503. $('.popover').popover('hide');
  504. if (this.dropInfo.nodeInfoList.length > 0) {
  505. this._showingLayoutIndicator = false;
  506. // Don't start showing the drag coordinates right away since it's a pretty big perf hit on IE.
  507. // Let the move start doing its thing before we show the coordinates
  508. setTimeout(function () {
  509. _this2._layoutIndicator.render();
  510. _this2._layoutIndicator.show();
  511. _this2._showingLayoutIndicator = true;
  512. }, 150);
  513. }
  514. }
  515. // get the revised offsets, if applicable
  516. var revisedOffsets = this._preMoveNode(this.dropInfo.dragBoxSize, dragContext, evt, this.dropInfo.nodeInfoList[0]);
  517. // calculate the position of each node
  518. for (i = 0, iLen = this.dropInfo.nodeInfoList.length; i < iLen; i++) {
  519. this.moveNode(this.dropInfo.nodeInfoList[i], evt, dragContext, revisedOffsets);
  520. }
  521. return false;
  522. },
  523. /**
  524. * Calculate the dimensions for a conceptual box which covers all
  525. * selected shapes which need to be dragged on the canvas
  526. *
  527. * @param nodes
  528. * List of selected nodes
  529. *
  530. * @return boxDimensions
  531. * Object containing the dimensions of the drag box, containing all selected widgets
  532. */
  533. _calculateBoxSize: function _calculateBoxSize(nodes) {
  534. var boxDimensions = {
  535. top: Number.MAX_SAFE_INTEGER,
  536. left: Number.MAX_SAFE_INTEGER,
  537. bottom: Number.MIN_SAFE_INTEGER,
  538. right: Number.MIN_SAFE_INTEGER,
  539. offsety: 0,
  540. offsetx: 0,
  541. width: 0,
  542. height: 0
  543. };
  544. var i = void 0,
  545. iLen = void 0;
  546. for (i = 0, iLen = nodes.length; i < iLen; i++) {
  547. var node = nodes[i];
  548. if (node.position.top < boxDimensions.top) {
  549. boxDimensions.top = node.position.top;
  550. boxDimensions.offsety = node.offsety;
  551. }
  552. if (node.position.left < boxDimensions.left) {
  553. boxDimensions.left = node.position.left;
  554. boxDimensions.offsetx = node.offsetx;
  555. }
  556. if (node.position.bottom > boxDimensions.bottom) {
  557. boxDimensions.bottom = node.position.bottom;
  558. }
  559. if (node.position.right > boxDimensions.right) {
  560. boxDimensions.right = node.position.right;
  561. }
  562. }
  563. boxDimensions.width = boxDimensions.right - boxDimensions.left;
  564. boxDimensions.height = boxDimensions.bottom - boxDimensions.top;
  565. return boxDimensions;
  566. },
  567. /**
  568. * Perform some necessary calculations before calling moveNode(), also
  569. * return a set of revised offsets if applicable
  570. *
  571. * @param dragBoxSize
  572. * @param dragContext
  573. * @param evt
  574. * @param nodeInfo
  575. *
  576. * @return revisedOffsets
  577. * Either x and y will be valid numbers or NULLs
  578. * When null used in calculation, it is converted into number 0. So we can safely return NULL
  579. * and use it in our calculations to determine whether we need to snap to grid or not.
  580. */
  581. _preMoveNode: function _preMoveNode(dragBoxSize, dragContext, evt, nodeInfo) {
  582. var revisedOffsets = { x: 0, y: 0 };
  583. if (dragContext.dropTargetNode && nodeInfo.node._layout) {
  584. dragContext.dropTargetNodeOffset = $(dragContext.dropTargetNode).offset();
  585. var dragBox = this._convertCoordinates(dragBoxSize, evt.clientX - dragBoxSize.offsetx, evt.clientY - dragBoxSize.offsety, dragContext.dropTargetNode, dragContext.dropTargetNodeOffset);
  586. var revisedCoordinates = this._getNewCoordinates(dragBox);
  587. revisedOffsets = this.getRevisedOffsets(revisedCoordinates);
  588. }
  589. if (this._showingLayoutIndicator && nodeInfo.node._layout) {
  590. this._layoutIndicator.updatePosition({ 'top': evt.clientY, 'left': evt.clientX });
  591. this._updateLayoutIndicatorContent(evt, dragBoxSize, nodeInfo, revisedOffsets, dragContext);
  592. }
  593. return revisedOffsets;
  594. },
  595. /**
  596. * Prepare the drop info object that will be delivered to the drop zone.
  597. * This method is called once when a drag starts
  598. */
  599. prepareDropInfo: function prepareDropInfo() /*dropInfo*/{
  600. // To be overriden by sub-classes
  601. },
  602. /**
  603. * Prepare the nodes to be dragged. This method is called once for every selected node
  604. * @param {object} n - the dom node being dragged
  605. */
  606. prepareNodeForMove: function prepareNodeForMove(n) {
  607. return n;
  608. },
  609. /**
  610. * Invoke after all nodes are prepared for move
  611. *
  612. * @param {array} nodes - collection of nodes
  613. */
  614. postNodeForMove: function postNodeForMove() /*nodes*/{
  615. // To be overriden by sub-classes
  616. },
  617. /**
  618. * Retrieve collection of selected Dom nodes
  619. *
  620. * @return {array} collection of Dom nodes
  621. */
  622. getSelectedDomNodes: function getSelectedDomNodes() {
  623. var i, iLen;
  624. var nodeInfo,
  625. nodes = [];
  626. for (i = 0, iLen = this.dropInfo.nodeInfoList.length; i < iLen; i++) {
  627. nodeInfo = this.dropInfo.nodeInfoList[i];
  628. nodes.push(nodeInfo.node);
  629. }
  630. return nodes;
  631. },
  632. /**
  633. * Move selected nodes to a drop zone
  634. *
  635. * @param nodeInfo
  636. * @param evt
  637. * @param dragContext
  638. * @param revisedOffsets
  639. */
  640. moveNode: function moveNode(nodeInfo, evt, dragContext, revisedOffsets) {
  641. var node = nodeInfo.node;
  642. var coords = {};
  643. // revisedOffsets will always be a valid object, the values of x and y might be a number or null
  644. // if revisedOffsets x and y are numbers, that means snap to grid is on
  645. // if revisedOffsets x and y are nulls, that means snap to grid is not on (null converted to number is 0)
  646. coords = utils.getRotatedCoordinates(node.parentNode, evt.clientX - nodeInfo.offsetx - revisedOffsets.x, evt.clientY - nodeInfo.offsety - revisedOffsets.y, nodeInfo.parentNodeRect);
  647. utils.setTopLeft(node, coords.y, coords.x);
  648. this.handleMove(nodeInfo, coords, dragContext);
  649. },
  650. _formatPosition: function _formatPosition(node, dragBox, containerNode) {
  651. if (node._layout) {
  652. var layoutPositioning = node._layout.model.getValueFromSelfOrParent('layoutPositioning');
  653. if (layoutPositioning === 'relative') {
  654. var dragBoxModel = { top: dragBox.top + 'px', left: dragBox.left + 'px' };
  655. var pageSize = { width: $(containerNode).width(), height: $(containerNode).height() };
  656. PxPercentUtil.changePixelPropertiesToPercent(dragBoxModel, pageSize, 1);
  657. dragBox.top = Formatter.formatNumber(dragBoxModel.top.replace(/%/g, '')) + ' %';
  658. dragBox.left = Formatter.formatNumber(dragBoxModel.left.replace(/%/g, '')) + ' %';
  659. } else {
  660. dragBox.top = Formatter.formatNumber(dragBox.top) + ' ' + stringResources.get('pixelUnit') + ' ';
  661. dragBox.left = Formatter.formatNumber(dragBox.left) + ' ' + stringResources.get('pixelUnit') + ' ';
  662. }
  663. }
  664. return dragBox;
  665. },
  666. getIds: function getIds(nodeInfoList) {
  667. var dragBox = nodeInfoList.reduce(function (box, current) {
  668. box.ids.push(current.node.id);
  669. return box;
  670. }, {
  671. ids: []
  672. });
  673. return dragBox;
  674. },
  675. getRevisedOffsets: function getRevisedOffsets(revisedCoordinates) {
  676. var revisedOffset = {};
  677. if (revisedCoordinates.left === null) {
  678. revisedOffset.x = revisedCoordinates.right;
  679. } else if (revisedCoordinates.right === null) {
  680. revisedOffset.x = revisedCoordinates.left;
  681. } else {
  682. revisedOffset.x = Math.abs(revisedCoordinates.left) < Math.abs(revisedCoordinates.right) ? revisedCoordinates.left : revisedCoordinates.right;
  683. }
  684. if (revisedCoordinates.top === null) {
  685. revisedOffset.y = revisedCoordinates.bottom;
  686. } else if (revisedCoordinates.bottom === null) {
  687. revisedOffset.y = revisedCoordinates.top;
  688. } else {
  689. revisedOffset.y = Math.abs(revisedCoordinates.bottom) < Math.abs(revisedCoordinates.top) ? revisedCoordinates.bottom : revisedCoordinates.top;
  690. }
  691. return revisedOffset;
  692. },
  693. _convertCoordinates: function _convertCoordinates(dragBoxSize, x, y, dropTargetNode, dropTargetNodeOffset) {
  694. var dragBox = {};
  695. if (dropTargetNode) {
  696. var rect = dropTargetNodeOffset || $(dropTargetNode).offset();
  697. var xNew = Math.round(x - rect.left + dropTargetNode.scrollLeft);
  698. var yNew = Math.round(y - rect.top + dropTargetNode.scrollTop);
  699. dragBox = {
  700. top: yNew,
  701. right: xNew + dragBoxSize.width,
  702. bottom: yNew + dragBoxSize.height,
  703. left: xNew,
  704. center_x: xNew + dragBoxSize.width / 2,
  705. center_y: yNew + dragBoxSize.height / 2
  706. };
  707. }
  708. return dragBox;
  709. },
  710. _getNewCoordinates: function _getNewCoordinates(dragBox) {
  711. this._guidelineManager.hideAll();
  712. return this._guidelineManager.getSnapCoordinates(dragBox);
  713. },
  714. handleMove: function handleMove() /*nodeInfo, coords, dragContext*/{
  715. // To be overriden by sub-classes
  716. },
  717. /**
  718. * Save the node original position. To be used when we want to restore the original position
  719. *
  720. * @param node
  721. */
  722. setNodePositionInfo: function setNodePositionInfo(nodeInfo) {
  723. var node = nodeInfo.node;
  724. nodeInfo.oldValues = {
  725. 'top': node.style.top,
  726. 'left': node.style.left,
  727. 'height': node.style.height,
  728. 'width': node.style.width,
  729. 'parent': node.parentElement,
  730. 'nextSibling': node.nextSibling
  731. };
  732. while (nodeInfo.oldValues.nextSibling && this.selectedNodes.indexOf(nodeInfo.oldValues.nextSibling) > -1) {
  733. nodeInfo.oldValues.nextSibling = nodeInfo.oldValues.nextSibling.nextSibling;
  734. }
  735. },
  736. moveByProperty: function moveByProperty(style, value, units) {
  737. var sanitizedValue = value;
  738. var sizeProperty = style === 'left' ? 'width' : 'height';
  739. var parentSize = this._getParentDim(sizeProperty, this.propertyNodes);
  740. var offset = this._getWidgetOffset(sizeProperty, this.propertyNodes);
  741. offset = offset + this._getWidgetSize(sizeProperty, this.propertyNodes); // correct the offset since move moves the left top edge and doesnt count widget size
  742. if (units === 'px') {
  743. var minValueFn = style === 'left' ? this.propertyNodes[0]._layout.parentLayout.getMinimumLeft : this.propertyNodes[0]._layout.parentLayout.getMinimumTop;
  744. sanitizedValue = Math.max(minValueFn(), sanitizedValue);
  745. sanitizedValue = Math.min(parentSize - offset, sanitizedValue);
  746. } else {
  747. //% size
  748. sanitizedValue = this._sanitizePercentSizeOnPage(sanitizedValue, sizeProperty);
  749. }
  750. var propertyValue = sanitizedValue + units;
  751. var positionUpdateArray = [];
  752. this.propertyNodes.forEach(function (node) {
  753. var styleUpdate = {};
  754. styleUpdate[style] = propertyValue;
  755. positionUpdateArray.push({
  756. style: styleUpdate,
  757. id: node._layout.model.id
  758. });
  759. });
  760. this.updateModel(positionUpdateArray);
  761. return sanitizedValue;
  762. },
  763. _processChange: function _processChange(id, numericValue, units) {
  764. return this.moveByProperty(id === 'left' ? 'left' : 'top', numericValue, units);
  765. },
  766. /* TODO: This has been replaced by API work. Remove the unused functions, and the base class if no longer needed. */
  767. getProperties: function getProperties() {
  768. Move.inherited('getProperties', this, arguments);
  769. var unit = this.getUnitsFromValue(this.propertyNodes[0]._layout.model.style.left);
  770. var propValues = [{
  771. units: unit,
  772. getProp: function getProp(node) {
  773. return node._layout.model.style.left;
  774. }
  775. }, {
  776. units: unit,
  777. getProp: function getProp(node) {
  778. return node._layout.model.style.top;
  779. }
  780. }];
  781. this._getValuesForProperties(propValues);
  782. var properties = [{
  783. 'type': 'SectionLabel',
  784. 'label': stringResources.get('propPositionLabel'),
  785. 'tabName': stringResources.get('tabName_general'),
  786. 'sectionName': stringResources.get('sectionName_layout'),
  787. 'sectionOpened': true,
  788. order: 20
  789. }, {
  790. 'type': 'Split',
  791. 'name': 'moveWidgetSplit',
  792. 'id': 'moveWidgetSplit',
  793. 'tabName': stringResources.get('tabName_general'),
  794. 'sectionName': stringResources.get('sectionName_layout'),
  795. order: 21,
  796. 'items': [{
  797. align: 'left',
  798. items: [{
  799. 'type': 'InputLabel',
  800. 'label': stringResources.get('propPositionXAxis'),
  801. 'name': 'left',
  802. 'id': 'left',
  803. 'tabName': stringResources.get('tabName_general'),
  804. 'sectionName': stringResources.get('sectionName_layout'),
  805. 'readOnly': false,
  806. 'value': propValues[0].value,
  807. 'onChangeValueHold': propValues[0].value,
  808. 'multiline': true,
  809. 'handleReturnKey': true,
  810. 'units': unit,
  811. 'onChange': this.wrapChangeProperty()
  812. }]
  813. }, {
  814. align: 'right',
  815. items: [{
  816. 'type': 'InputLabel',
  817. 'label': stringResources.get('propPositionYAxis'),
  818. 'name': 'top',
  819. 'id': 'top',
  820. 'tabName': stringResources.get('tabName_general'),
  821. 'sectionName': stringResources.get('sectionName_layout'),
  822. 'readOnly': false,
  823. 'value': propValues[1].value,
  824. 'onChangeValueHold': propValues[1].value,
  825. 'multiline': true,
  826. 'handleReturnKey': true,
  827. 'units': unit,
  828. 'onChange': this.wrapChangeProperty()
  829. }]
  830. }]
  831. }];
  832. if (this.showStyleProperties(this.propertyNodes[0])) {
  833. return properties;
  834. } else {
  835. return [];
  836. }
  837. }
  838. });
  839. return Move;
  840. });
  841. //# sourceMappingURL=Move.js.map