TemplateDropZone.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2013, 2020
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. */
  7. define(['jquery', 'underscore', './LayoutBaseView', '../../../../lib/@waca/dashboard-common/dist/ui/interaction/Utils', '../../../../app/nls/StringResources', '../../../../lib/@waca/core-client/js/core-client/utils/Deferred', '../../LayoutHelper', '../../../glass/util/InstrumentationUtil'], function ($, _, BaseLayout, utils, stringResources, Deferred, LayoutHelper, InstrumentationUtil) {
  8. var PageLayout = null;
  9. PageLayout = BaseLayout.extend({
  10. init: function init(options) {
  11. PageLayout.inherited('init', this, arguments);
  12. this.services = options.services;
  13. this.specializeConsumeView(['setPreferredLocation', 'isLayoutRelatedToDropZone', 'addRelatedModel', 'removeRelatedModel']);
  14. this.$el.parent().addClass('templateDropZoneContainer');
  15. this.updateRelatedContentState();
  16. this.whenIsReadyDfd = new Deferred();
  17. this.initializeDropZones();
  18. },
  19. initializeDropZones: function initializeDropZones() {
  20. this._dndManager = this.dashboardApi.getFeature('DashboardDnd.internal');
  21. if (!this.centerDrop) {
  22. this.centerDrop = $('<div class="centerDrop"><div class="dropIcon"><svg class="svgIcon" role="img" focusable="false"><use style="pointer-events: none; " xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#dashboard-fullscreen"></use></svg></div><div class="dropLabel">' + stringResources.get('dropZoneLabel') + '</div></div>');
  23. this.centerDrop.hide();
  24. this.$el.append(this.centerDrop);
  25. }
  26. var $centerDropZone = this.centerDrop.find('.dropIcon');
  27. this._dndManager.addDropTarget($centerDropZone[0], {
  28. accepts: this.accepts.bind(this),
  29. onDrop: this.onCentreDrop.bind(this),
  30. onDragEnter: this.onCentreDragEnter.bind(this),
  31. onDragLeave: this.onCentreDragLeave.bind(this),
  32. priority: -100 /* Indicate that any other drop zone takes priority */
  33. });
  34. // Set the layout node as a drop zone. This drop zone will not accept anything
  35. // but it is configured to receive the enter/leave/move events. This is used to show the maximize square target
  36. this._dndManager.addDropTarget(this.domNode, {
  37. accepts: function accepts() {
  38. return false;
  39. },
  40. onDragEnter: this.onDragZoneEnter.bind(this),
  41. onDragLeave: this.onDragZoneLeave.bind(this),
  42. onDrop: this.onDropZoneDrop.bind(this),
  43. receiveEventsWhenNotAccepting: true,
  44. /* absolute pages are -100 priority.
  45. * We set the drop zone priority to be higher (i.e. -99)
  46. * so that it takes priority in case the drop zone is the same size as the parent absolute page
  47. * We also need to set it to negative to indicate that any other drop zone takes priority */
  48. priority: -99
  49. });
  50. this._setupGlassDroppables($centerDropZone);
  51. this.whenIsReadyDfd.resolve();
  52. },
  53. whenIsReady: function whenIsReady() {
  54. return this.whenIsReadyDfd.promise;
  55. },
  56. _setupGlassDroppables: function _setupGlassDroppables($centerDropZone) {
  57. if (this.$el.glassDroppableV2) {
  58. //we are in the glass, accept pin drops
  59. var thisObj = this;
  60. //center zone accepts drops (maximizes to area in template)
  61. $centerDropZone.glassDroppableV2({
  62. onDrop: this._onPinDrop.bind(this),
  63. onEnter: this.onGlassCentreDragEnter.bind(this),
  64. onLeave: this.onCentreDragLeave.bind(this),
  65. allowOnDropPropagation: false
  66. });
  67. //this.$el tracks onEnter, onLeave so that it can show the center drop zone
  68. this.$el.glassDroppableV2({
  69. onEnter: function onEnter() {
  70. if (this.id === thisObj.$el[0].id) {
  71. thisObj.onDragZoneEnter();
  72. }
  73. },
  74. onLeave: function onLeave() {
  75. if (this.id === thisObj.$el[0].id) {
  76. thisObj.onDragZoneLeave();
  77. }
  78. },
  79. onDrop: function onDrop() {
  80. thisObj.onDropZoneDrop();
  81. return true;
  82. },
  83. allowOnDropPropagation: true
  84. });
  85. }
  86. },
  87. _showCenterDrop: function _showCenterDrop(data, isMovingOneWidget) {
  88. var excludeId = null;
  89. if (isMovingOneWidget) {
  90. var layout = data.nodeInfoList[0].node._layout;
  91. if (layout) {
  92. excludeId = layout.model.id;
  93. }
  94. }
  95. // We will not show the center drop if we already have a maximized widget
  96. // But if we are moving the widget that is already maximized, then we display the center.
  97. if (!this.hasMaximizedWidget() || excludeId && this.isWidgetMaximized(excludeId)) {
  98. this.centerDrop.show();
  99. }
  100. },
  101. onDragZoneEnter: function onDragZoneEnter(dragObject) {
  102. this.inDropZone = true;
  103. var data = dragObject && dragObject.data || {};
  104. var isNewDrop = !data.nodeInfoList;
  105. var isMovingOneWidget = data.nodeInfoList && data.nodeInfoList.length === 1;
  106. // Show the center drop when we are dropping a new widget or moving one widget.
  107. if (isNewDrop || isMovingOneWidget) {
  108. this._showCenterDrop(data, isMovingOneWidget);
  109. var dropTarget = this._dndManager.getDropTargetFromNode(this.centerDrop.find('.dropIcon')[0]);
  110. this._dndManager.reassessDropTarget(dropTarget);
  111. }
  112. },
  113. onDragZoneLeave: function onDragZoneLeave() {
  114. if (!this.centerDrop.hasClass('active')) {
  115. this.centerDrop.hide();
  116. }
  117. this.inDropZone = false;
  118. },
  119. onDropZoneDrop: function onDropZoneDrop() {
  120. this.deactivateAndhideCenterDropZone();
  121. this.inDropZone = false;
  122. },
  123. deactivateAndhideCenterDropZone: function deactivateAndhideCenterDropZone() {
  124. if (this.centerDrop) {
  125. this.centerDrop.removeClass('active');
  126. this.centerDrop.hide();
  127. }
  128. },
  129. destroy: function destroy() {
  130. if (this._dndManager) {
  131. this._dndManager.removeDropTarget(this.centerDrop.find('.dropIcon')[0]);
  132. this._dndManager.removeDropTarget(this.domNode);
  133. this._dndManager = null;
  134. }
  135. this.centerDrop.remove();
  136. this.centerDrop = null;
  137. if (this.$el.parent().find('.pagetemplateDropZone') <= 1) {
  138. this.$el.parent().removeClass('templateDropZoneContainer');
  139. }
  140. if ($.glassdnd && $.glassdnd.cancelDroppable) {
  141. $.glassdnd.cancelDroppable(this.$el);
  142. }
  143. PageLayout.inherited('destroy', this, arguments);
  144. },
  145. /**
  146. * Called by the DnD manager to check if this drop zone accept the dragged object
  147. * We only accept object with type widget and pin
  148. * @returns {Boolean}
  149. */
  150. accepts: function accepts(dragObject) {
  151. var canvasDnD = this.dashboardApi.getFeature('CanvasDnD');
  152. return canvasDnD.accepts(dragObject, {
  153. fromTemplate: true
  154. });
  155. },
  156. /**
  157. * Called by the DnD manager when we enter and move inside the centre of drop zone (only if the drop zone accepts the object)
  158. *
  159. */
  160. onCentreDragEnter: function onCentreDragEnter() {
  161. this.centerDrop.addClass('active');
  162. },
  163. /**
  164. * Called by the DnD manager when we enter and move inside the centre of drop zone (only if the drop zone accepts the object)
  165. *
  166. */
  167. onGlassCentreDragEnter: function onGlassCentreDragEnter() {
  168. this.centerDrop.show();
  169. this.centerDrop.addClass('active');
  170. },
  171. /**
  172. * Called by the drag&drop manager when we leave the centre drop zone
  173. *
  174. */
  175. onCentreDragLeave: function onCentreDragLeave() {
  176. this.centerDrop.removeClass('active');
  177. if (!this.inDropZone) {
  178. this.centerDrop.hide();
  179. }
  180. },
  181. /**
  182. * Called by the drag&drop manager when a drop happens at the centre
  183. *
  184. * @param dragObject
  185. * @param targetNode
  186. */
  187. onCentreDrop: function onCentreDrop(dragObject) {
  188. var _this = this;
  189. var promise = Promise.resolve();
  190. if (dragObject.data.operation === 'move') {
  191. promise = Promise.resolve(this._moveDrop(dragObject));
  192. } else if (dragObject.type === 'pin' && dragObject.data.operation === 'new') {
  193. promise = Promise.resolve(this._onPinDrop(dragObject));
  194. } else if (dragObject.data.operation === 'new' || dragObject.type === 'MODEL_ITEM' || dragObject.type === 'GRID_HEADER_ITEM') {
  195. promise = this._newDrop(dragObject);
  196. }
  197. return promise.then(function () {
  198. return _this.deactivateAndhideCenterDropZone();
  199. });
  200. },
  201. _moveDrop: function _moveDrop(dragObject) {
  202. var updateArray = [];
  203. var nodeInfo, nodeModel, options;
  204. for (var i = 0; i < dragObject.data.nodeInfoList.length; i++) {
  205. nodeInfo = dragObject.data.nodeInfoList[i];
  206. nodeModel = nodeInfo.node._layout.model;
  207. options = {
  208. style: {},
  209. parentId: this.model.getParent().id,
  210. id: nodeModel.id
  211. };
  212. this.setPreferredLayoutProperties(options);
  213. options.insertBefore = this.getWidgetIdForInsertBefore();
  214. updateArray.push(options);
  215. }
  216. var transactionApi = this.dashboardApi.getFeature('Transaction');
  217. var transactionToken = transactionApi.startTransaction();
  218. var nodeIds = updateArray.map(function (update) {
  219. return update.id;
  220. });
  221. var validate = false; // for move drop, no need for validation in layoutPropertiesProvider
  222. this.dashboardApi.getCanvas().moveContent(this.model.getParent().id, nodeIds, transactionToken);
  223. this.updateModel(updateArray, transactionToken, validate);
  224. transactionApi.endTransaction(transactionToken);
  225. },
  226. _newDrop: function _newDrop(dragObject) {
  227. var _this2 = this;
  228. return this._getModelToAddFromDragObject(dragObject).then(function (newModel) {
  229. if (newModel) {
  230. var widgetSpec = {
  231. model: newModel,
  232. parentId: _this2.id,
  233. layoutProperties: dragObject.data.layoutProperties || {}
  234. };
  235. InstrumentationUtil.trackWidget('created', _this2.dashboardApi, widgetSpec.model);
  236. _this2.setPreferredLocation(widgetSpec);
  237. _this2._addWidget(widgetSpec, dragObject.isTouch);
  238. }
  239. });
  240. },
  241. /**
  242. * a helper function that handles pin dropping
  243. *
  244. * @private
  245. *
  246. * @param {object} dragObject - The object to be dropped
  247. */
  248. _onPinDrop: function _onPinDrop(dragObject) {
  249. var pinSpec = dragObject.data.pinSpec;
  250. var isTouch = dragObject.isTouch;
  251. //gemini widget
  252. if (pinSpec.contentType === 'boardFragment') {
  253. this.setPreferredLayoutProperties(pinSpec.content.layout);
  254. }
  255. pinSpec.parentId = this.id;
  256. this.setMaximizedStateParentLocation(pinSpec);
  257. this._processWidgetSpecForPin(pinSpec, isTouch);
  258. this.deactivateAndhideCenterDropZone();
  259. },
  260. setPreferredLocation: function setPreferredLocation(options) {
  261. this.setPreferredLayoutProperties(options.layoutProperties);
  262. this.setMaximizedStateParentLocation(options);
  263. },
  264. setMaximizedStateParentLocation: function setMaximizedStateParentLocation(options) {
  265. options.parentId = this.model.getParent().id;
  266. options.insertBefore = this.getWidgetIdForInsertBefore();
  267. },
  268. setPreferredLayoutProperties: function setPreferredLayoutProperties(props) {
  269. if (!props.style) {
  270. props.style = {};
  271. }
  272. if (this.$el.is(':visible')) {
  273. //for selected tab/scene
  274. var contentOffset = this.$el.offset();
  275. var parentOffset = this.$el.parent().offset();
  276. props.style.top = Math.round(contentOffset.top - parentOffset.top);
  277. props.style.left = Math.round(contentOffset.left - parentOffset.left);
  278. props.style.height = this.$el.outerHeight();
  279. props.style.width = this.$el.outerWidth();
  280. this.moveToFitBoundaries(this.$el.parent().height(), this.$el.parent().width(), props.style);
  281. LayoutHelper.styleIntToPx(props.style);
  282. } else {
  283. //in the case of tab/scene hidden (for moving widgets from one tab/scene to another)
  284. var templateDropZone = this.$el[0];
  285. var style = this._calculateStylePercentage(templateDropZone.style);
  286. props.style = style;
  287. }
  288. },
  289. /**
  290. * Fix the old behavor where we always insert in a drop zone and place the object behind all existing objects.
  291. * The new logic will find the first object that is smaller than the dropzone (with a small amount of forgiveness) and insert before it.
  292. */
  293. getWidgetIdForInsertBefore: function getWidgetIdForInsertBefore() {
  294. var zoneHeight = this.$el.height();
  295. var zoneWidth = this.$el.width();
  296. var widgets = this.$el.siblings(':not(.pagetemplateDropZone)');
  297. var widgetNode = _.find(widgets, function (node) {
  298. if (node._layout) {
  299. var $node = $(node);
  300. return $node.outerHeight() < zoneHeight - 5 || $node.outerWidth() < zoneWidth - 5;
  301. }
  302. return false;
  303. });
  304. return widgetNode ? widgetNode._layout.model.id : null;
  305. },
  306. isLayoutRelatedToDropZone: function isLayoutRelatedToDropZone(node) {
  307. var props = {};
  308. this.setPreferredLayoutProperties(props);
  309. var isRelated = false;
  310. var topIsNear, leftIsNear, heightIsNear, widthIsNear;
  311. if (this.$el.is(':visible')) {
  312. //for selected tab/scene
  313. var position = utils.position(node);
  314. var size = utils.widgetSize(node);
  315. topIsNear = this.isNear(Math.round(position.top), props.style.top, 5);
  316. leftIsNear = this.isNear(Math.round(position.left), props.style.left, 5);
  317. heightIsNear = this.isNear(size.height, props.style.height, 10);
  318. widthIsNear = this.isNear(size.width, props.style.width, 10);
  319. } else {
  320. //in the case of tab/scene hidden (for moving widgets from one tab/scene to another)
  321. topIsNear = this.isNear(node.style.top, props.style.top, 10);
  322. leftIsNear = this.isNear(node.style.left, props.style.left, 10);
  323. heightIsNear = this.isNear(node.style.height, props.style.height, 10);
  324. widthIsNear = this.isNear(node.style.width, props.style.width, 10);
  325. }
  326. isRelated = topIsNear && leftIsNear && heightIsNear && widthIsNear;
  327. return isRelated;
  328. },
  329. /**
  330. * set a model as a related model to this drop zone. Related models are usually the ones snapped to this drop zone
  331. */
  332. addRelatedModel: function addRelatedModel(id, payloadData) {
  333. var relatedWidgets = this.model.relatedLayouts || '|';
  334. if (relatedWidgets.indexOf('|' + id + '|') === -1) {
  335. relatedWidgets += id + '|';
  336. }
  337. this.model.set({
  338. relatedLayouts: relatedWidgets
  339. }, {
  340. payloadData: payloadData.data
  341. });
  342. this.updateRelatedContentState();
  343. },
  344. hasMaximizedWidget: function hasMaximizedWidget() {
  345. return !!this.model.relatedLayouts;
  346. },
  347. isWidgetMaximized: function isWidgetMaximized(id) {
  348. var relatedWidgets = this.model.relatedLayouts || '|';
  349. return relatedWidgets.indexOf('|' + id + '|') !== -1;
  350. },
  351. /**
  352. * Remove a model as a related model to this drop zone. Related models are usually the ones snapped to this drop zone
  353. */
  354. removeRelatedModel: function removeRelatedModel(id, payloadData) {
  355. var relatedWidgets = this.model.relatedLayouts;
  356. if (relatedWidgets && relatedWidgets.indexOf('|' + id + '|') !== -1) {
  357. relatedWidgets = relatedWidgets.replace('|' + id + '|', '|');
  358. if (relatedWidgets === '|') {
  359. relatedWidgets = '';
  360. }
  361. this.model.set({
  362. relatedLayouts: relatedWidgets
  363. }, {
  364. payloadData: payloadData.data
  365. });
  366. }
  367. this.updateRelatedContentState();
  368. },
  369. /**
  370. * Add a css class to indicate of this drop zone has related layouts. This will be used to find an empty spot when adding widgets to the page
  371. */
  372. updateRelatedContentState: function updateRelatedContentState() {
  373. var relatedWidgets = this.model.relatedLayouts;
  374. if (!relatedWidgets) {
  375. this.$el.addClass('empty');
  376. } else {
  377. this.$el.removeClass('empty');
  378. }
  379. },
  380. isNear: function isNear(i, s, variance) {
  381. return Math.abs(parseInt(i, 10) - parseInt(s, 10)) <= variance;
  382. },
  383. _calculateStylePercentage: function _calculateStylePercentage(style) {
  384. var newStyle = {};
  385. newStyle.top = style.top;
  386. newStyle.left = style.left;
  387. newStyle.width = 100 - parseInt(style.right, 10) - parseInt(style.left, 10) + '%';
  388. newStyle.height = 100 - parseInt(style.bottom, 10) - parseInt(style.top, 10) + '%';
  389. return newStyle;
  390. }
  391. });
  392. return PageLayout;
  393. });
  394. //# sourceMappingURL=TemplateDropZone.js.map