TimelineModel.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: Storytelling
  5. * (C) Copyright IBM Corp. 2014, 2020
  6. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  7. */
  8. define(['gemini/lib/@waca/dashboard-common/dist/core/Model', './TimelineEpisodes', 'underscore'], function (Model, TimelineEpisodes, _) {
  9. var TimelineModel = Model.extend({
  10. nestedCollections: { episodes: TimelineEpisodes },
  11. whitelistAttrs: ['id', 'story', 'title', 'episodes', 'playThrough', 'kioskMode', 'navigateMarkers', 'refreshData'],
  12. init: function init(spec, options) {
  13. var _this = this;
  14. TimelineModel.inherited('init', this, arguments);
  15. if (!this.episodes) {
  16. this.set({
  17. 'episodes': []
  18. });
  19. }
  20. this.boardModel = options.boardModel;
  21. this.dashboardApi = options.dashboardApi;
  22. this.dashboardApi.getCanvasWhenReady().then(function (canvas) {
  23. canvas.on('add:content:child', _this._addContent, _this);
  24. canvas.on('remove:content:child', _this._removeContent, _this);
  25. });
  26. this.boardModel.on('addWidget', this._addWidget, this);
  27. this.boardModel.on('pre:removeWidget', this._removeWidget, this);
  28. this.boardModel.on('addFragment', this._addFragment, this);
  29. //at this point the board model does not trigger removeFragment. It only calls removeLayouts.
  30. this.boardModel.on('addLayouts', this._addLayouts, this);
  31. this.boardModel.on('pre:removeLayouts', this._removeLayouts, this);
  32. this.boardModel.on('duplicateLayout', this._duplicateLayout, this);
  33. },
  34. /**
  35. * @param {Object} options: - Object that includes:
  36. * options.currentEndTime - the current end time of widgets that should be modified.
  37. * options.newEndTime - the new endtime that should be applied to the matching widgets
  38. * options.subset - id's of the subset of widgets that should be considered.
  39. * @param {Object} [sender] - sender context.
  40. * @param {Object} [payloadData] - extra data. Currently used for undo/redo transaction id.
  41. */
  42. stretchEndingEpisodes: function stretchEndingEpisodes(options, sender, payloadData) {
  43. if (!options || !options.currentEndTime || !options.newEndTime || !options.subset) {
  44. return;
  45. }
  46. _.each(options.subset, function (id) {
  47. var episode = this.episodes.get(id);
  48. if (episode) {
  49. var act = episode.getExitAct();
  50. if (act && act.timer >= options.currentEndTime) {
  51. act.set({ timer: options.newEndTime }, { sender: sender, payloadData: payloadData });
  52. }
  53. }
  54. }.bind(this));
  55. },
  56. /*
  57. * add an episode that may me missing some information.
  58. * Once it's added we notify the TimelineController so it can fill in the missing information.
  59. */
  60. addEpisodeFragment: function addEpisodeFragment(model, options) {
  61. var addedModel = this.episodes.add(model, options);
  62. this.trigger('timeline:episodeFragmentAdded', {
  63. id: addedModel.id,
  64. options: options
  65. });
  66. if (options.insertBefore) {
  67. this.episodes.reorder(addedModel.id, options.insertBefore, options);
  68. }
  69. },
  70. _getSceneDuration: function _getSceneDuration(widgetIds) {
  71. return _.reduce(widgetIds, function (workingDuration, id) {
  72. var ep = this.episodes.get(id);
  73. // only widgets that we have in the timeline contribute to the duration.
  74. // I.e not groups or anything else.
  75. if (ep) {
  76. return Math.max(workingDuration, ep.getExitAct().timer);
  77. } else {
  78. return workingDuration;
  79. }
  80. }.bind(this), 0);
  81. },
  82. getWidgetTransitionMap: function getWidgetTransitionMap(scene1, scene2) {
  83. if (!scene1 || !scene2) {
  84. return null;
  85. }
  86. var rootLayout = this.boardModel.layout;
  87. var scene1Widgets = rootLayout.listWidgets([scene1.id]);
  88. var scene2Widgets = rootLayout.listWidgets([scene2.id]);
  89. // are the scenes not empty
  90. if (!scene1Widgets.length || !scene2Widgets.length) {
  91. return null;
  92. }
  93. // adjacency test
  94. var nextSiblingId = scene1.model.getNextSiblingId();
  95. if (nextSiblingId && nextSiblingId !== scene2.id) {
  96. return null;
  97. }
  98. // remove from scene 1 all the widgets that don't go till the end;
  99. var scene1Duration = this._getSceneDuration(scene1Widgets);
  100. scene1Widgets = _.filter(scene1Widgets, function (id) {
  101. var ep = this.episodes.get(id);
  102. return ep ? ep.touchesEnd(scene1Duration) : true;
  103. }.bind(this));
  104. // remove from scene 2 all the widgets that don't start at the beginning;
  105. scene2Widgets = _.filter(scene2Widgets, function (id) {
  106. var ep = this.episodes.get(id);
  107. return ep ? ep.touchesStart() : true;
  108. }.bind(this));
  109. var transitionMap = {
  110. forward: {},
  111. backward: {}
  112. };
  113. // loop trough each widgets and find all the ones in scene2
  114. _.each(scene1Widgets, function (scene1WidgetId) {
  115. var scene1Widget = rootLayout.findModel(scene1WidgetId);
  116. _.each(scene2Widgets, function (scene2WidgetId) {
  117. var scene2Widget = rootLayout.findModel(scene2WidgetId);
  118. if (scene1Widget.id === scene2Widget.from || scene1Widget.from === scene2Widget.id) {
  119. var list = rootLayout.getLinkedLayoutTree(scene1Widget, scene2Widget);
  120. _.each(list, function (widgets) {
  121. transitionMap.forward[widgets[0].id] = widgets[1].id;
  122. transitionMap.backward[widgets[1].id] = widgets[0].id;
  123. });
  124. }
  125. }.bind(this));
  126. }.bind(this));
  127. if (!Object.keys(transitionMap.backward).length) {
  128. return null;
  129. }
  130. return transitionMap;
  131. },
  132. _addContent: function _addContent(payload) {
  133. // the use of the context.undoRedo in here is temporary and should be considered internal
  134. // please do not use this mechanism. The usage will be changed to properly handle these events when
  135. // we switch completely to content API events
  136. var isUndoRedo = !!payload.context.undoRedo;
  137. if (!isUndoRedo) {
  138. return;
  139. }
  140. var options = payload.info.value;
  141. var id = payload.info.newContentId;
  142. var payloadData = {
  143. skipUndoRedo: isUndoRedo,
  144. undoRedoTransactionId: payload.transactionToken && payload.transactionToken.transactionId
  145. };
  146. this.addEpisodeFragment({
  147. type: 'widget',
  148. id: id
  149. }, {
  150. payloadData: payloadData,
  151. insertBefore: options.insertBefore
  152. });
  153. },
  154. _removeContent: function _removeContent(payload) {
  155. // the use of the context.undoRedo in here is temporary and should be considered internal
  156. // please do not use this mechanism. The usage will be changed to properly handle these events when
  157. // we switch completely to content API events
  158. var isUndoRedo = !!payload.context.undoRedo;
  159. if (!isUndoRedo) {
  160. return;
  161. }
  162. var id = payload.info.value;
  163. // dashboard is setting options we don't want
  164. // in the end we only want the ID, so we just grab that
  165. // we should revisit when we move to use the content API
  166. var data = {
  167. undoRedoTransactionId: payload.transactionToken && payload.transactionToken.transactionId,
  168. skipUndoRedo: isUndoRedo
  169. };
  170. this._removeEpisode(id, data);
  171. },
  172. _addWidget: function _addWidget(payload) {
  173. var options = payload.value.parameter;
  174. var sender = payload.sender;
  175. var payloadData = payload.data;
  176. if (this._isUndoRedoController(sender)) {
  177. return;
  178. }
  179. if (payloadData.replace) {
  180. var _payload$idMap;
  181. payload.idMap = (_payload$idMap = {}, _payload$idMap[options.insertBefore] = options.model.id, _payload$idMap);
  182. this._duplicateLayout(payload);
  183. this.episodes.reorder(options.model.id, options.insertBefore, { sender: sender, payloadData: payloadData });
  184. } else {
  185. this.addEpisodeFragment({
  186. type: 'widget',
  187. id: options.model.id
  188. }, {
  189. sender: sender,
  190. payloadData: payloadData,
  191. insertBefore: options.insertBefore
  192. });
  193. }
  194. },
  195. _removeWidget: function _removeWidget(payload) {
  196. var id = payload.id;
  197. var sender = payload.sender;
  198. // dashboard is setting options we don't want
  199. // in the end we only want the ID, so we just grab that
  200. // we should revisit when we move to use the content API
  201. var payloadData = {
  202. undoRedoTransactionId: payload.data && payload.data.undoRedoTransactionId
  203. };
  204. if (this._isUndoRedoController(sender)) {
  205. return;
  206. }
  207. this._removeEpisode(id, payloadData, sender);
  208. },
  209. _removeEpisode: function _removeEpisode(id, data, sender) {
  210. var episode = this.episodes.get(id);
  211. if (episode) {
  212. var index = this.episodes.indexOf(episode);
  213. var beforeID = this.episodes.models[index + 1] ? this.episodes.models[index + 1].id : null;
  214. // Added beforeID to make sure the postion doesn't change
  215. this.episodes.reorder(id, beforeID, {
  216. sender: sender,
  217. payloadData: data,
  218. forceEvent: true
  219. });
  220. this.episodes.remove(episode, {
  221. sender: sender,
  222. payloadData: data
  223. });
  224. }
  225. },
  226. _addFragment: function _addFragment(payload) {
  227. var options = payload.value.parameter;
  228. var sender = payload.sender;
  229. var payloadData = payload.data;
  230. if (this._isUndoRedoController(sender)) {
  231. return;
  232. }
  233. var fragSpec = options.model;
  234. // the boardmodel has updated all the widgets and layouts by now, so the
  235. // ID's in the fragspec are the 'new' ones that where generated.
  236. // in this case we need a map from new to old, so we inverse the map here.
  237. var widgetIdMap = _.invert(options.widgetIdMap);
  238. _.each(fragSpec.widgets, function (widgetModel) {
  239. var newId = widgetModel.id;
  240. var oldId = widgetIdMap[newId];
  241. var episodeJSON = _.find(fragSpec.episodes, function (obj) {
  242. return obj.id === oldId;
  243. }) || {};
  244. // strip out act id in order to get new act id
  245. _.each(episodeJSON.acts, function (act) {
  246. delete act.id;
  247. });
  248. _.extend(episodeJSON, { id: newId, type: 'widget' });
  249. this.addEpisodeFragment(episodeJSON, {
  250. sender: sender,
  251. payloadData: payloadData
  252. });
  253. }.bind(this));
  254. },
  255. _addLayouts: function _addLayouts(payload) {
  256. var _this2 = this;
  257. var options = payload.value.parameter;
  258. if (this._isUndoRedoController(payload.sender)) {
  259. return;
  260. }
  261. _.each(options.widgetSpecMap, function (widget) {
  262. _this2.addEpisodeFragment({
  263. id: widget.id, type: 'widget'
  264. }, {
  265. sender: payload.sender,
  266. payloadData: payload.data
  267. });
  268. });
  269. },
  270. _removeLayouts: function _removeLayouts(payload) {
  271. var _this3 = this;
  272. var sender = payload.sender;
  273. // dashboard is setting options we don't want
  274. // in the end we only want the ID, so we just grab that
  275. // we should revisit when we move to use the content API
  276. var payloadData = {
  277. undoRedoTransactionId: payload.data && payload.data.undoRedoTransactionId,
  278. skipUndoRedo: payload.data && payload.data.skipUndoRedo
  279. };
  280. if (this._isUndoRedoController(sender)) {
  281. return;
  282. }
  283. var widgetIds = this.boardModel.layout.listWidgets(payload.idArray);
  284. widgetIds.forEach(function (id) {
  285. var episode = _this3.episodes.get(id);
  286. var index = _this3.episodes.indexOf(episode);
  287. var beforeID = _this3.episodes.models[index + 1] ? _this3.episodes.models[index + 1].id : null;
  288. // Added beforeID to make sure the postion doesn't change
  289. _this3.episodes.reorder(id, beforeID, {
  290. sender: sender,
  291. payloadData: payloadData,
  292. forceEvent: true
  293. });
  294. _this3.episodes.remove(episode, {
  295. sender: sender,
  296. payloadData: payloadData
  297. });
  298. });
  299. },
  300. _duplicateLayout: function _duplicateLayout(payload) {
  301. var _this4 = this;
  302. var idMap = payload.idMap;
  303. var sender = payload.sender;
  304. var payloadData = payload.data;
  305. if (this._isUndoRedoController(sender)) {
  306. return;
  307. }
  308. var clones = [];
  309. this.episodes.each(function (episode) {
  310. if (idMap[episode.id]) {
  311. // strip out act id in order to re-generate a new act id on clone
  312. var episodeJSON = episode.toJSON();
  313. if (episodeJSON.acts) {
  314. episodeJSON.acts.forEach(function (act) {
  315. delete act.id;
  316. });
  317. }
  318. var clone = new _this4.episodes.modelClass(episodeJSON);
  319. clone.replaceIds(idMap);
  320. clones.push(clone);
  321. }
  322. });
  323. if (clones.length) {
  324. // preserve timeline order and save to the same undoRedoTransactionId as duplicateLayout
  325. this._reorderTimelines(this.episodes, clones, sender, payloadData);
  326. this.episodes.add(clones, {
  327. sender: sender,
  328. payloadData: payloadData,
  329. merge: true
  330. });
  331. }
  332. },
  333. _reorderTimelines: function _reorderTimelines(episodes, clones, sender, payloadData) {
  334. for (var i = clones.length - 1; i > 0; i--) {
  335. episodes.reorder(clones[i - 1].id, clones[i].id, {
  336. sender: sender,
  337. payloadData: payloadData
  338. });
  339. }
  340. },
  341. // the board model manually handles undo/redo and re-triggers events.
  342. // In the case of undo redo we have already done the work and we let the model deal with it.
  343. _isUndoRedoController: function _isUndoRedoController(sender) {
  344. return sender === 'UndoRedoController';
  345. }
  346. });
  347. /**
  348. * @static
  349. * @param {JSON} timeline timeline json object from the model
  350. * @param {Array} widgets array of widget objects
  351. * @param {Object} options collection options used when adding episodes
  352. *
  353. * @return {TimelineModel}
  354. */
  355. TimelineModel.widgetsToEpisodes = function (timeline, widgets, options) {
  356. var episodes = new TimelineEpisodes(timeline.episodes);
  357. widgets.forEach(function (widget) {
  358. episodes.add({
  359. type: 'widget',
  360. id: widget.id
  361. }, options);
  362. });
  363. timeline.episodes = episodes.toJSON();
  364. };
  365. return TimelineModel;
  366. });
  367. //# sourceMappingURL=TimelineModel.js.map