PanAndZoomLayout.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: Storytelling (C) Copyright IBM Corp. 2015, 2019
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. */
  7. define(['gemini/dashboard/layout/authoring/views/PageCollectionView', 'jquery', 'underscore', 'baglass/core-client/js/core-client/utils/Utils', '../../../lib/@ba-ui-toolkit/ba-graphics/dist/illustrations-js/visualization-widget_128'], function (BaseClass, $, _, Utils, visualizationIcon) {
  8. var PanAndZoomLayout = BaseClass.extend({
  9. init: function init(options) {
  10. PanAndZoomLayout.inherited('init', this, arguments);
  11. this.dashboardApi = options.dashboardApi;
  12. this._dndManager = this.dashboardApi.getDashboardCoreSvc('.DndManager');
  13. this.startOverview = {
  14. id: 'start_overview_' + this.model.id,
  15. $el: this.$el.find('#' + 'start_overview_' + this.model.id)
  16. };
  17. this.endOverview = {
  18. id: 'end_overview_' + this.model.id,
  19. $el: this.$el.find('#' + 'end_overview_' + this.model.id)
  20. };
  21. this.specializeConsumeView(['currentSceneChanged', 'onKeyDown', 'onLayoutReady', 'onPlaybackNext', 'onPlaybackPrevious']);
  22. this._updateSceneIndexes();
  23. this._scaleOverviewElements();
  24. // save the value so we can unregister later
  25. this._overviewDragEventCallback = this._onOverviewSceneDrag.bind(this);
  26. this.$el.find('.overviewBlockerCell').hammer().on('dragstart', this._overviewDragEventCallback);
  27. // This is here because on init of this class the consumeView's internal handling of scenes might not
  28. // be loaded yet. When "onLayoutReady" is called (see below) we will call _registerScene on all model
  29. // items as we do here.
  30. // This block is when we switch between consumption and authoring modes.
  31. if (this.consumeView._getScenes().length !== 0) {
  32. _.each(this.model.items, this._registerScene.bind(this));
  33. }
  34. },
  35. destroy: function destroy() {
  36. _.each(this.model.items, this._deRegisterScene.bind(this));
  37. this.$el.find('.overviewBlockerCell').hammer().off('dragstart', this._overviewDragEventCallback);
  38. PanAndZoomLayout.inherited('destroy', this, arguments);
  39. },
  40. /**
  41. * Called when a new scene is added.
  42. *
  43. * @param {Object} layoutView The scene layout model that is being added/moved
  44. * @param {String} [insertBefore] The id value of the scene model to use as the next sibling
  45. * @param {Object} [payload] Payload of previously executed events
  46. *
  47. * @returns {Promise}
  48. */
  49. add: function add(layoutView, insertBefore, payload) {
  50. if (!this.consumeView._hasScene(layoutView.model)) {
  51. this._addNewScene(layoutView, insertBefore, payload);
  52. } else {
  53. this._reorderScenes(layoutView, insertBefore);
  54. }
  55. return this.consumeView._selectScene(layoutView.model.id);
  56. },
  57. /**
  58. * Called when a view is removed
  59. *
  60. * @param node
  61. */
  62. removeChild: function removeChild(layoutView, payload) {
  63. payload = this._checkPayloadData(payload);
  64. var model = layoutView.model;
  65. this._deRegisterScene(model);
  66. this.consumeView._impress.removeStep(this.consumeView._getViewId(model));
  67. // Remove the content nodes
  68. layoutView.$el.find('.overviewBlockerCell').hammer().off('dragstart', this._overviewDragEventCallback);
  69. layoutView.$el.parent().remove();
  70. this.consumeView._removeScene(model);
  71. // in the case of undo/redo we have already updated the models.
  72. if (payload.sender !== 'UndoRedoController') {
  73. this._shiftFollowingScenes(model.data.positionIndex, -1, payload);
  74. }
  75. this._updateOverviewsLocation();
  76. PanAndZoomLayout.inherited('remove', this, arguments);
  77. this._updateSceneIndexes();
  78. },
  79. /**
  80. * A specializeConsumeView method
  81. * When creating a new story we need to wait for the layout to be ready (which means the DOM is constructed)
  82. * before we call _registerScene on the model items.
  83. */
  84. onLayoutReady: function onLayoutReady() {
  85. var _this = this;
  86. return this.overridden.onLayoutReady().then(function () {
  87. _.each(_this.model.items, _this._registerScene.bind(_this));
  88. });
  89. },
  90. /**
  91. * A specializeConsumeView method
  92. */
  93. currentSceneChanged: function currentSceneChanged() {
  94. // call consume view
  95. this.overridden.currentSceneChanged();
  96. this._scaleOverviewElements();
  97. },
  98. /**
  99. * A specializeConsumeView method
  100. */
  101. onPlaybackNext: function onPlaybackNext() {
  102. return this;
  103. },
  104. /**
  105. * A specializeConsumeView method
  106. */
  107. onPlaybackPrevious: function onPlaybackPrevious() {
  108. return this;
  109. },
  110. /**
  111. * A specializeConsumeView method
  112. */
  113. onKeyDown: function onKeyDown(event) {
  114. // for now we only deal with keys on the overview.
  115. if (!this.$el.hasClass('overview')) {
  116. this.overridden.onKeyDown(event);
  117. return;
  118. }
  119. var keyCode = event.keyCode || event.charCode;
  120. if (keyCode === 13 /* enter */ || keyCode === 32 /* space */) {
  121. var $target = $(event.target).closest('.step.pageTabContent');
  122. var targetId = $target[0].dataset.modelId;
  123. var $selected = this.$el.find('.swapSelected.step.pageTabContent').removeClass('swapSelected');
  124. if ($selected.length === 1) {
  125. var selectedId = $selected[0].dataset.modelId;
  126. // if we have something already selected we swap or remove the selection.
  127. // but we already reselected the item so we only deal with the swap
  128. if (targetId && targetId !== selectedId) {
  129. var scene1 = this.consumeView._getSceneById(selectedId);
  130. var scene2 = this.consumeView._getSceneById(targetId);
  131. this._swapScenes(scene1, scene2);
  132. }
  133. } else {
  134. // nothing is selected so add the class to target
  135. $target.addClass('swapSelected');
  136. }
  137. } else {
  138. this.overridden.onKeyDown(event);
  139. }
  140. },
  141. _addNewScene: function _addNewScene(layoutView, insertBefore, payload) {
  142. var sceneIndex = this.consumeView.model.items.length - 1;
  143. var model = layoutView.model;
  144. payload = this._checkPayloadData(payload);
  145. // in the case of undo/redo we have already updated the model.
  146. if (payload.sender !== 'UndoRedoController') {
  147. // if model.data is set and we are here then we are a duplicating a scene
  148. if (model.data) {
  149. this._shiftFollowingScenes(model.data.positionIndex, 1, payload);
  150. // put the duplicated scene after the one it was copied from
  151. //and now that we shifted the pattern there is an empty spot there
  152. sceneIndex = model.data.positionIndex + 1;
  153. }
  154. this.model.updateModel({
  155. updateArray: [{
  156. id: model.id,
  157. data: {
  158. positionIndex: sceneIndex
  159. }
  160. }]
  161. }, payload.sender, payload.data);
  162. }
  163. var template = this.htmlTemplate.getItemTemplate(this.model.type, '', model);
  164. var sceneContent = this.htmlTemplate.replaceLayoutValues(template, model);
  165. var $sceneContent = $(sceneContent);
  166. $sceneContent.append(layoutView.domNode);
  167. $sceneContent.find('.overviewBlockerCell').hammer().on('dragstart', this._overviewDragEventCallback);
  168. // This is not optimal, but in case of undo/redo we might not navigate to the new scene. So we have to do this here.
  169. $sceneContent.removeClass('hiddenScene');
  170. // impress adds a nested div child to impress section
  171. $sceneContent.insertBefore(this.endOverview.$el);
  172. this.consumeView._impress.addStep('#' + $sceneContent[0].id, this.endOverview.id);
  173. this.consumeView._addScene(model);
  174. if (insertBefore) {
  175. this._reorderScenes(layoutView, insertBefore);
  176. }
  177. this.consumeView._updateEndpoints();
  178. this._updateOverviewsLocation();
  179. this._registerScene(model);
  180. },
  181. _reorderScenes: function _reorderScenes(layoutView, insertBefore) {
  182. var model = layoutView.model;
  183. var scene = this.consumeView._getScene(model);
  184. scene.$el.detach().append(layoutView.domNode);
  185. var $before = this.endOverview.$el;
  186. if (insertBefore) {
  187. var beforeScene = this.consumeView._getSceneById(insertBefore);
  188. $before = beforeScene.$el;
  189. }
  190. scene.$el.insertBefore($before);
  191. this.consumeView._updateSceneList(model, insertBefore);
  192. var sceneId = scene.$el[0].id;
  193. this.consumeView._impress.removeStep(sceneId);
  194. this.consumeView._impress.addStep('#' + sceneId, $before[0].id);
  195. this.layoutController.eventRouter.trigger('scene:reorder', { model: model, insertBefore: insertBefore });
  196. this._updateSceneIndexes();
  197. this._scaleOverviewElements();
  198. },
  199. _shiftFollowingScenes: function _shiftFollowingScenes(fromPosition, amount, payload) {
  200. var scenes = this.consumeView._getScenes();
  201. var dataUpdateArray = [];
  202. _.each(scenes, function (scene) {
  203. var model = scene.getLayoutView().model;
  204. if (model.data.positionIndex > fromPosition) {
  205. var positionIndex = model.data.positionIndex + amount;
  206. dataUpdateArray.push({
  207. id: model.id,
  208. data: {
  209. positionIndex: positionIndex
  210. }
  211. });
  212. }
  213. }.bind(this));
  214. this.model.updateModel({ updateArray: dataUpdateArray }, payload.sender, payload.data);
  215. },
  216. _registerScene: function _registerScene(sceneModel) {
  217. this._addDropZone(sceneModel);
  218. this.consumeView._updateSceneLabel({ model: sceneModel });
  219. sceneModel.on('change:data', this._onSceneModelChanged, this);
  220. sceneModel.on('change:title', this.consumeView._updateSceneLabel, this);
  221. },
  222. _deRegisterScene: function _deRegisterScene(sceneModel) {
  223. this._removeDropZone(sceneModel);
  224. sceneModel.off('change:data', this._onSceneModelChanged, this);
  225. sceneModel.off('change:title', this.consumeView._updateSceneLabel, this);
  226. },
  227. _onSceneModelChanged: function _onSceneModelChanged(payload) {
  228. var model = this.model.findModel(payload.modelId);
  229. var scene = this.consumeView._getScene(model);
  230. if (scene && model) {
  231. this.consumeView._updateSceneSize(scene, this.consumeView._getViewPort());
  232. this._scaleOverviewElements();
  233. }
  234. },
  235. _updateOverviewsLocation: function _updateOverviewsLocation() {
  236. var info = this.consumeView._getOverviewLocation(this.consumeView._getViewPort());
  237. this.consumeView._updateElementData(this.startOverview.$el, info);
  238. this.consumeView._updateElementData(this.endOverview.$el, info);
  239. this._scaleOverviewElements();
  240. },
  241. /**
  242. * Go through all of the scenes on the canvas and update the index number that's shown
  243. * on the start/end overviews.
  244. */
  245. _updateSceneIndexes: function _updateSceneIndexes() {
  246. var items = this.model.items;
  247. this.$el.find('.sceneOrder').each(function (i) {
  248. this.setAttribute('data-index', i + 1);
  249. this.setAttribute('data-order', items[i].data.positionIndex + 1);
  250. });
  251. },
  252. /**
  253. * Scale border width of scenes based on data-scale should be called any time user
  254. * moves from overview to a scene
  255. */
  256. _scaleSceneBorderWidth: function _scaleSceneBorderWidth() {
  257. // if a scene (not an overview) is currently selected, shrink the border width
  258. // so that outline doesn't appear too bulky when zoomed in
  259. // OR if a story only has one scene, border width should be same whether viewing
  260. // overview or the scene
  261. var defaultBorderWidth = 5;
  262. var currentSceneIndex = this.consumeView._getSceneIndexById(this.consumeView.sceneId);
  263. var isOnScene = currentSceneIndex >= 0;
  264. var numScenes = this.consumeView._scenes.length;
  265. if (isOnScene || numScenes === 1) {
  266. defaultBorderWidth = 2;
  267. }
  268. this.$el.find('.pageTabContent').each(function (index, sceneDiv) {
  269. var visualPositionIndex = this.model.items[index].data.positionIndex;
  270. var scale = this.consumeView.getSceneScale(visualPositionIndex) || 1;
  271. var newBorderWidth = defaultBorderWidth / scale;
  272. $(sceneDiv).find(this.consumeView._dropZoneSelector).each(function (index, dropZoneEl) {
  273. dropZoneEl.style.borderWidth = newBorderWidth + 'px';
  274. });
  275. }.bind(this));
  276. },
  277. /**
  278. * scale box and font size of scene #'s in overview based on data-scale
  279. */
  280. _scaleOverviewElements: function _scaleOverviewElements() {
  281. // default sizes in pixels for the width and height of the sceneOrder div, and font
  282. var defaultBoxSize = 80;
  283. var defaultFontSize = 48;
  284. this.$el.find('.sceneOrder').each(function (index, sceneOrderDiv) {
  285. var visualPositionIndex = this.model.items[index].data.positionIndex;
  286. var scale = this.consumeView.getSceneScale(visualPositionIndex) || 1;
  287. var newBoxSize = defaultBoxSize / scale;
  288. var newFontSize = defaultFontSize / scale;
  289. sceneOrderDiv.style.width = newBoxSize + 'px';
  290. sceneOrderDiv.style.height = newBoxSize + 'px';
  291. sceneOrderDiv.style.fontSize = newFontSize + 'px';
  292. }.bind(this));
  293. this._scaleSceneBorderWidth();
  294. },
  295. _removeDropZone: function _removeDropZone(targetModel) {
  296. if (targetModel._dropZone) {
  297. targetModel._dropZone.remove();
  298. }
  299. },
  300. _addDropZone: function _addDropZone(targetModel) {
  301. var targetScene = this.consumeView._getScene(targetModel);
  302. if (targetScene) {
  303. targetModel._dropZone = this._dndManager.addDropTarget(targetScene.$el[0], {
  304. accepts: function accepts(dragObject) {
  305. return dragObject.type === 'scene';
  306. },
  307. onDrop: function (dragObject) {
  308. targetScene.$el.removeClass('activeDropZone');
  309. this._swapScenes(targetScene, dragObject.data.scene);
  310. }.bind(this),
  311. onDragEnter: function onDragEnter() {
  312. targetScene.$el.addClass('activeDropZone');
  313. },
  314. onDragLeave: function onDragLeave() {
  315. targetScene.$el.removeClass('activeDropZone');
  316. }
  317. });
  318. }
  319. },
  320. _swapScenes: function _swapScenes(scene1, scene2) {
  321. var model1 = scene1.getLayoutView().model;
  322. var model2 = scene2.getLayoutView().model;
  323. var payloadData = {
  324. undoRedoTransactionId: _.uniqueId('PanAndZoomLayoutTransaction')
  325. };
  326. var model1UpdateArray = _.map(model1.items, function (model) {
  327. return {
  328. id: model.id,
  329. parentId: model2.id
  330. };
  331. });
  332. var model2UpdateArray = _.map(model2.items, function (model) {
  333. return {
  334. id: model.id,
  335. parentId: model1.id
  336. };
  337. });
  338. var model1TitleOld = model1.get('title');
  339. model1.updateModel({ updateArray: model1UpdateArray }, this, payloadData);
  340. model1.set({ title: model2.get('title') }, { payloadData: payloadData });
  341. model2.updateModel({ updateArray: model2UpdateArray }, this, payloadData);
  342. model2.set({ title: model1TitleOld }, { payloadData: payloadData });
  343. // moving the content resets the styling we applied, so we need to redo it
  344. this._scaleOverviewElements();
  345. this.layoutController.eventRouter.trigger('scene:swap', {
  346. scenes: [scene1, scene2]
  347. });
  348. },
  349. _onOverviewSceneDrag: function _onOverviewSceneDrag(event) {
  350. var sceneId = $(event.target).closest('.overview .step.pageTabContent')[0].dataset.modelId;
  351. var scene = this.consumeView._getSceneById(sceneId);
  352. if (scene) {
  353. var pageContainer = scene.getLayoutView().$el[0];
  354. var bounds = pageContainer.getBoundingClientRect();
  355. var $avatar = $(document.createElement('div')).css({
  356. left: bounds.left,
  357. top: bounds.top,
  358. width: bounds.width,
  359. height: bounds.height,
  360. // dragging the avatar from the center instead of a corner looks better
  361. transform: 'translate( -50%, -50%)'
  362. }).addClass('overviewSceneMoveAvatar');
  363. Utils.setIcon($avatar, visualizationIcon.default.id);
  364. this._dndManager.startDrag({
  365. event: event,
  366. type: 'scene',
  367. data: {
  368. scene: scene
  369. },
  370. avatar: $avatar[0],
  371. callerCallbacks: {
  372. onDragStart: function onDragStart() {
  373. scene.$el.addClass('sourceDropZone');
  374. },
  375. onDragDone: function onDragDone() {
  376. scene.$el.removeClass('sourceDropZone');
  377. }
  378. }
  379. });
  380. }
  381. },
  382. _checkPayloadData: function _checkPayloadData(payload) {
  383. payload = payload || {};
  384. payload.sender = payload.sender || this;
  385. payload.data = payload.data || { undoRedoTransactionId: _.uniqueId('PanAndZoomLayoutTransaction') };
  386. return payload;
  387. }
  388. });
  389. return PanAndZoomLayout;
  390. });
  391. //# sourceMappingURL=PanAndZoomLayout.js.map