LayoutModel.js 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2020
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. */
  7. define(['../../lib/@waca/dashboard-common/dist/core/Model', './content/ContentModel', './ModelUtils', 'underscore', 'jquery'], function (Model, ContentModel, ModelUtils, _, $) {
  8. /**
  9. *
  10. * Public model operations:
  11. *
  12. * Function name: add
  13. * Description: Adds a model as a child to the layout model
  14. * Triggered event name: 'add:item'
  15. * Event listeners: Layout view object - Needed to created the proper html for the new view.
  16. * Called by:
  17. * - Board model: when a new widget is added.
  18. * - Layout model - updateModel operation
  19. *
  20. *
  21. * Function name: remove
  22. * Description: Removes a model from the layout model
  23. * Triggered event name: 'remove:item'
  24. * * Event listeners: Layout view object - Needed to the remove the html node associated with the deleted
  25. * Called by:
  26. * - Board model: when a new widget is deleted.
  27. * - Layout model - updateModel operation
  28. *
  29. *
  30. * Function name: updateStyle
  31. * Description: Updates the style property in the model. This operation will do a mixin with current value.
  32. * Triggered event name: 'change:style'
  33. * Event listeners: Layout view object - Needed to update the css style of the model node
  34. * Called by:
  35. * - Layout model - updateModel operation
  36. *
  37. * Function name: updateData
  38. * Description: Updates the data property in the model.
  39. * Triggered event name: 'change:data'
  40. * Event listeners: Layout view object - Needed to update the impress positioning of the view
  41. * Called by:
  42. * - Layout model - updateModel operation
  43. *
  44. * Function name: updateParent
  45. * Description: Updates the parent of the model.
  46. * Triggered event name: 'change:parent'
  47. * Event listeners: Layout view object - Needed to update the view node parent to match the new parent model
  48. * Called by:
  49. * - Layout model - updateModel operation
  50. *
  51. *
  52. * Function name: updateModel
  53. * Description: An operation that updates a model. This operation can add,update and remove models in one operation
  54. * Triggered event name: 'op:updateModel'
  55. * Event listeners: Board model - Needed to propagate the event to the auto save and undo/redo controller.
  56. * Called by:
  57. * - Layout interaction controller - when doing a move operation
  58. *
  59. *
  60. */
  61. var LayoutModel = Model.extend({
  62. localizedProps: ['title'],
  63. /**
  64. * Initialize this model from a layout spec, using the json under 'layout' from a board specification.
  65. * @param {object} layout spec
  66. * @param {object} The board models that owns this layout
  67. */
  68. init: function init(spec, boardModel, logger) {
  69. LayoutModel.inherited('init', this, arguments);
  70. this.boardModel = boardModel;
  71. this.whitelistAttrs = ['id', 'from', 'css', 'items', 'style', 'type', 'title', 'templateName', 'relatedLayouts', 'clones', 'fillColor', 'disableScrollDrop', 'showGrid', 'snapGrid', 'snapObjects', 'tabTextColor', 'tabSelectedLineColor', 'tabBackgroundColor', 'pageSize', 'hideTab', 'tabPosition', 'tabIconPosition', 'tabIcon', 'tabIconColor', 'layoutPositioning', 'fitPage', 'content'];
  72. this.colorProperties = ['fillColor', 'tabTextColor', 'tabSelectedLineColor', 'tabBackgroundColor', 'tabIconColor'];
  73. // eslint-disable-next-line no-undef
  74. this.selected = new Set();
  75. if (this.boardModel.layoutExtensions) {
  76. var modelExtensions = this.boardModel.layoutExtensions;
  77. Object.keys(modelExtensions || {}).forEach(function (property) {
  78. this.whitelistAttrs.push(property);
  79. if (modelExtensions[property] && typeof modelExtensions[property].init === 'function') {
  80. modelExtensions[property].init(this, spec, boardModel, logger);
  81. }
  82. }, this);
  83. }
  84. // Notify the board model when there is a layout change to the items so that the event is propagated to the auto save and the undo/redo controller
  85. this.on('op', this.boardModel.onLayoutChange, this.boardModel);
  86. this.on('change:title', this.boardModel.onLayoutChange, this.boardModel);
  87. // TODO: for change:css, we should move away from monitoring/setting the css property in the model and set specify properties
  88. this.on('change:css', this.boardModel.onLayoutChange, this.boardModel);
  89. this.on('change:fillColor', this.boardModel.onLayoutChange, this.boardModel);
  90. this.on('change:type', this.boardModel.onLayoutChange, this.boardModel);
  91. this.on('change:showGrid', this.boardModel.onLayoutChange, this.boardModel);
  92. this.on('change:snapGrid', this.boardModel.onLayoutChange, this.boardModel);
  93. this.on('change:snapObjects', this.boardModel.onLayoutChange, this.boardModel);
  94. this.on('change:tabTextColor', this.boardModel.onLayoutChange, this.boardModel);
  95. this.on('change:tabSelectedLineColor', this.boardModel.onLayoutChange, this.boardModel);
  96. this.on('change:tabBackgroundColor', this.boardModel.onLayoutChange, this.boardModel);
  97. this.on('change:pageSize', this.boardModel.onLayoutChange, this.boardModel);
  98. this.on('change:hideTab', this.boardModel.onLayoutChange, this.boardModel);
  99. this.on('change:tabPosition', this.boardModel.onLayoutChange, this.boardModel);
  100. this.on('change:tabIconPosition', this.boardModel.onLayoutChange, this.boardModel);
  101. this.on('change:tabIcon', this.boardModel.onLayoutChange, this.boardModel);
  102. this.on('change:tabIconColor', this.boardModel.onLayoutChange, this.boardModel);
  103. this.on('change:layoutPositioning', this.boardModel.onLayoutChange, this.boardModel);
  104. this.on('change:fitPage', this.boardModel.onLayoutChange, this.boardModel);
  105. this._initializeContentModel();
  106. this.logger = logger;
  107. var idMap = {};
  108. if (spec.items) {
  109. this.items = [];
  110. for (var i = 0, iLen = spec.items.length; i < iLen; i++) {
  111. var item = new LayoutModel(spec.items[i], this.boardModel, logger);
  112. if (!idMap[item.id]) {
  113. this.items.push(item);
  114. idMap[item.id] = true;
  115. } else {
  116. this.logger.error('Found duplicate layout id "' + item.id + '" on initialization of layout "' + this.id + '".');
  117. }
  118. }
  119. }
  120. return this;
  121. },
  122. getValueFromSelfOrParent: function getValueFromSelfOrParent(prop) {
  123. var value = this.get(prop);
  124. if (value === undefined) {
  125. var parent = this.getParent();
  126. if (parent) {
  127. value = parent.getValueFromSelfOrParent(prop);
  128. }
  129. }
  130. return value;
  131. },
  132. cloneLayout: function cloneLayout(idMap) {
  133. var clone = new LayoutModel(this.toJSON(), this.boardModel);
  134. if (!this.clones) {
  135. this.clones = 0;
  136. }
  137. this.clones++;
  138. clone.clones = 0;
  139. clone.replaceIds(idMap);
  140. clone.replaceRelatedLayouts(idMap);
  141. clone.incrementTitleCount(this.clones);
  142. return clone;
  143. },
  144. /**
  145. * Find a model based on its id recursively.
  146. * @param {String} sId
  147. */
  148. findModel: function findModel(sId) {
  149. var m = null;
  150. if (this.id === sId) {
  151. m = this;
  152. }
  153. if (!m && this.items) {
  154. for (var i = 0; !m && i < this.items.length; i++) {
  155. m = this.items[i].findModel(sId);
  156. }
  157. }
  158. return m;
  159. },
  160. /**
  161. * Return an array of the widget IDs contained in the layouts with the specified IDs.
  162. *
  163. * @param aIDs Layout id array
  164. */
  165. listWidgets: function listWidgets(aIDs) {
  166. var widgetIdList = [];
  167. var sId;
  168. for (var i = 0; i < aIDs.length; i++) {
  169. sId = aIDs[i];
  170. var layout = this.findModel(sId);
  171. if (!layout) {
  172. continue;
  173. }
  174. if (layout.type === 'widget') {
  175. widgetIdList.push(layout.id);
  176. } else if (layout.items) {
  177. for (var j = 0; j < layout.items.length; j++) {
  178. widgetIdList.push.apply(widgetIdList, layout.items[j].listWidgets([layout.items[j].id]));
  179. }
  180. }
  181. }
  182. return widgetIdList;
  183. },
  184. /**
  185. * Add a layout model as a child.
  186. * @param {object} add options
  187. * options.model - model json
  188. * options.insertBefore - insert location
  189. * @param sender - scope or identifier for the object doing the update
  190. * @param payloadData - Additional data that will be included in the event payload.
  191. *
  192. * @returns - Triggered event payload if available
  193. */
  194. add: function add(options, sender, payloadData) {
  195. payloadData = this.checkPayloadData(payloadData);
  196. var parentId = options.parentId ? options.parentId : this.id;
  197. var parent = this.findModel(parentId);
  198. //TODO: This needs to be more generic and not specific to widget
  199. var isSubContent = parent && parent.type === 'widget';
  200. var modelJSON = options.model;
  201. if (!modelJSON) {
  202. return null;
  203. }
  204. var payload = this._insertItem({ model: modelJSON, insertBeforeId: options.insertBefore }, sender, payloadData);
  205. var addedModel = payload.value.parameter.model;
  206. if (isSubContent) {
  207. return this._createPayload('add:item', { op: 'add', parameter: { model: addedModel, insertBefore: options.insertBefore } }, { op: 'remove', parameter: addedModel.id }, sender, payloadData);
  208. }
  209. return this._triggerEvent('add:item', { op: 'add', parameter: { model: addedModel, insertBefore: options.insertBefore } }, { op: 'remove', parameter: addedModel.id }, sender, payloadData);
  210. },
  211. /**
  212. * Add multiple layout models
  213. * @param {object} modelArray An array of add options
  214. * options.parentId - parent id. If not available, the model will be added to the current model.
  215. * options.model - model json
  216. * options.insertBefore - insert location
  217. * @param sender - scope or identifier for the object doing the update
  218. * @param payloadData - Additional data that will be included in the event payload.
  219. *
  220. * @returns - Triggered event payload if available
  221. */
  222. addArray: function addArray(modelArray, sender, payloadData) {
  223. payloadData = this.checkPayloadData(payloadData);
  224. var modelIdArray = [];
  225. var addedModels = [];
  226. var addPayload, options, parent, parentId;
  227. for (var i = 0; i < modelArray.length; i++) {
  228. options = modelArray[i];
  229. parentId = options.parentId ? options.parentId : this.id;
  230. parent = this.findModel(parentId);
  231. addPayload = parent.add(modelArray[i], sender, payloadData);
  232. if (addPayload && addPayload.value && addPayload.prevValue) {
  233. addedModels.push(_.extend({ parentId: parentId }, addPayload.value.parameter));
  234. modelIdArray.unshift(addPayload.prevValue.parameter);
  235. }
  236. }
  237. var payload = null;
  238. if (modelIdArray.length > 0) {
  239. payload = this._triggerEvent('add:items', { op: 'addArray', parameter: addedModels }, { op: 'removeArray', parameter: modelIdArray }, sender, payloadData);
  240. }
  241. return payload;
  242. },
  243. /**
  244. * Remove a child from this Model.
  245. * @param {string} model id to remove.
  246. * @param sender - scope or identifier for the object doing the update
  247. * @param payloadData - Additional data that will be included in the event payload.
  248. *
  249. * @returns - Triggered event payload if available
  250. */
  251. remove: function remove(id, sender, payloadData) {
  252. payloadData = this.checkPayloadData(payloadData);
  253. this._triggerEvent('pre:remove:item', { parameter: id }, null, sender, payloadData);
  254. //remove from current parent
  255. var payload = null;
  256. var oldSiblingId = this.findModel(id).getNextSiblingId();
  257. var model = this._removeItem(id);
  258. if (model) {
  259. payload = this._triggerEvent('remove:item', { op: 'remove', parameter: id }, { op: 'add', parameter: { model: model.toJSON(), insertBefore: oldSiblingId } }, sender, payloadData);
  260. model.off();
  261. }
  262. return payload;
  263. },
  264. /**
  265. * Remove multiple children from the model.
  266. * @param {string} Array of model ids to remove.
  267. * @param sender - scope or identifier for the object doing the update
  268. * @param payloadData - Additional data that will be included in the event payload.
  269. *
  270. * @returns - Triggered event payload if available
  271. */
  272. removeArray: function removeArray(idArray, sender, payloadData) {
  273. payloadData = this.checkPayloadData(payloadData);
  274. var models = [];
  275. var removePayload;
  276. var id, parent;
  277. for (var i = 0; i < idArray.length; i++) {
  278. id = idArray[i];
  279. parent = this.findParentModel(id);
  280. if (parent) {
  281. removePayload = parent.remove(idArray[i], sender, payloadData);
  282. if (removePayload) {
  283. models.unshift(_.extend({ parentId: parent.id }, removePayload.prevValue.parameter));
  284. }
  285. }
  286. }
  287. var payload = null;
  288. if (models.length > 0) {
  289. payload = this._triggerEvent('remove:items', { op: 'removeArray', parameter: idArray }, { op: 'addArray', parameter: models }, sender, payloadData);
  290. }
  291. return payload;
  292. },
  293. /**
  294. * Update the style property of a model. This function will merge the provided styles with the existing ones
  295. * @param style - object containing the styles that we would like to update
  296. * @param sender - scope or identifier for the object doing the update
  297. * @param payloadData - Additional data that will be included in the event payload.
  298. *
  299. * @returns - Triggered event payload if available
  300. */
  301. updateStyle: function updateStyle(style, sender, payloadData) {
  302. payloadData = this.checkPayloadData(payloadData);
  303. var payload = null;
  304. if (style) {
  305. if (!this.style) {
  306. this.style = {};
  307. }
  308. var prev = this._getStyles(style);
  309. payload = this._triggerEvent('prechange:style', { style: style }, null, sender, payloadData);
  310. _.extend(this.style, style);
  311. payload = this._triggerEvent(LayoutModel.CHANGE_STYLE_EVENT_NAME, { op: 'updateStyle', parameter: style }, { op: 'updateStyle', parameter: prev }, sender, payloadData);
  312. }
  313. return payload;
  314. },
  315. /**
  316. * Update the filters of a model given id
  317. * @param id
  318. * @param filters
  319. * @param sender - scope or identifier for the object doing the update
  320. * @param payloadData - Additional data that will be included in the event payload.
  321. *
  322. * @returns - Triggered event payload if available
  323. */
  324. updateFilters: function updateFilters(id, filters) {
  325. var changes = {
  326. prevValue: {},
  327. value: {}
  328. };
  329. var widgetModel = this.boardModel.getWidgetModel(id);
  330. if (widgetModel && widgetModel.filters) {
  331. var modelFilters = widgetModel.filters;
  332. widgetModel.filters = filters;
  333. changes.prevValue.filters = modelFilters;
  334. changes.value.filters = filters;
  335. }
  336. return changes;
  337. },
  338. /**
  339. * Update the parent of the model
  340. *
  341. * @param options
  342. * options.parentId - new parent id
  343. * options.insertBefore - Id of the next sibling. If not provided, the model will be appended at the end.
  344. * @param sender - scope or identifier for the object doing the update
  345. * @param payloadData - Additional data that will be included in the event payload.
  346. *
  347. * @returns - Triggered event payload if available
  348. */
  349. updateParent: function updateParent(options, sender, payloadData) {
  350. payloadData = this.checkPayloadData(payloadData);
  351. var payload = null;
  352. var newParent = this.boardModel.layout.findModel(options.parentId);
  353. if (newParent) {
  354. //remove from current parent
  355. var oldParent = this.getParent();
  356. var oldSiblingId = null;
  357. if (oldParent) {
  358. oldSiblingId = this.getNextSiblingId();
  359. oldParent._removeItem(this.id);
  360. }
  361. newParent._insertItem({ model: this, insertBeforeId: options.insertBefore }, sender, payloadData);
  362. payload = this._triggerEvent('change:parent', {
  363. op: 'updateParent',
  364. parameter: options
  365. }, {
  366. op: 'updateParent',
  367. parameter: {
  368. parentId: oldParent ? oldParent.id : null,
  369. insertBefore: oldSiblingId
  370. }
  371. }, sender, payloadData);
  372. }
  373. return payload;
  374. },
  375. /**
  376. * Apply changes to the model using the update spec. This method can do add, remove and update in one operation.
  377. *
  378. * The order of the execution is the following:
  379. *
  380. * - Add new models
  381. * - Update models
  382. * - Remove models
  383. *
  384. *
  385. *
  386. * @param {object} options that describe the operation
  387. * options.addArray - Array of options needed to add the model.
  388. * {
  389. * model: {}, // model to add
  390. * parentId: 'parentId' // If not available, the model will be added to the current model.
  391. * }
  392. *
  393. * options.removeArray - Array of model IDs to remove
  394. * *
  395. * options.updateArray - Options used by the move operation.
  396. * {
  397. * id: 'model id',
  398. * parentId:'parent id' // If not available, then the model will be moved to the first model that is being added in this operation
  399. * insertBefore: 'someId', // Id of the model where we need to insert
  400. * style :{
  401. * top:'10px',
  402. * left:'10px'
  403. * }
  404. * }
  405. * @param sender - scope or identifier for the object doing the update
  406. * @param payloadData - Additional data that will be included in the event payload.
  407. *
  408. * @returns - Triggered event payload if available
  409. */
  410. updateModel: function updateModel(options, sender, payloadData) {
  411. payloadData = this.checkPayloadData(payloadData);
  412. var payload;
  413. var addPayload = null;
  414. var payloadValue = {};
  415. var payloadPreValue = {};
  416. if (options.addArray) {
  417. addPayload = this.addArray(options.addArray, sender, payloadData);
  418. if (addPayload) {
  419. payloadValue.addArray = addPayload.value.parameter;
  420. payloadPreValue.removeArray = addPayload.prevValue.parameter;
  421. }
  422. }
  423. if (options.updateArray) {
  424. var addedModels = addPayload ? addPayload.value.parameter : null;
  425. var updatePayload = this._updateModelsProperties(options.updateArray, sender, addedModels, payloadData);
  426. if (updatePayload) {
  427. if (JSON.stringify(updatePayload.value) !== JSON.stringify(updatePayload.prevValue)) {
  428. payloadValue.updateArray = updatePayload.value;
  429. payloadPreValue.updateArray = updatePayload.prevValue;
  430. }
  431. }
  432. }
  433. if (options.removeArray) {
  434. var removePayload = this.removeArray(options.removeArray, sender, payloadData);
  435. if (removePayload) {
  436. payloadValue.removeArray = removePayload.value.parameter;
  437. payloadPreValue.addArray = removePayload.prevValue.parameter;
  438. }
  439. }
  440. // Maintain any payload data that the caller included in the options.
  441. if (options.payloadData) {
  442. payloadValue.info = options.payloadData;
  443. payloadPreValue.info = options.payloadData;
  444. }
  445. if (!_.isEmpty(payloadValue)) {
  446. payload = this._triggerEvent('op:updateModel', {
  447. op: 'updateModel',
  448. parameter: payloadValue
  449. }, {
  450. op: 'updateModel',
  451. parameter: payloadPreValue
  452. }, sender, payloadData);
  453. }
  454. return payload;
  455. },
  456. /**
  457. * @deprecate should use CanvasAPI.moveContent() instead.
  458. */
  459. moveModel: function moveModel(options, sender, payloadData) {
  460. payloadData = this.checkPayloadData(payloadData);
  461. var payload = null;
  462. var payloadValue = {};
  463. var payloadPreValue = {};
  464. if (options.removeArray) {
  465. var removePayload = this.removeArray(options.removeArray, sender, payloadData);
  466. if (removePayload) {
  467. payloadValue.removeArray = removePayload.value.parameter;
  468. payloadPreValue.addArray = removePayload.prevValue.parameter;
  469. }
  470. }
  471. if (options.addArray) {
  472. var addPayload = this.addArray(options.addArray, sender, payloadData);
  473. if (addPayload) {
  474. payloadValue.addArray = addPayload.value.parameter;
  475. payloadPreValue.removeArray = addPayload.prevValue.parameter;
  476. }
  477. }
  478. if (options.updateArray) {
  479. var updatePayload = this._updateModelsProperties(options.updateArray, sender, payloadData);
  480. if (updatePayload) {
  481. payloadValue.updateArray = updatePayload.value;
  482. payloadPreValue.updateArray = updatePayload.prevValue;
  483. }
  484. }
  485. // Maintain any payload data that the caller included in the options.
  486. if (options.payloadData) {
  487. payloadValue.info = options.payloadData;
  488. payloadPreValue.info = options.payloadData;
  489. }
  490. payload = this._triggerEvent('op:moveModel', {
  491. op: 'moveModel',
  492. parameter: payloadValue
  493. }, {
  494. op: 'moveModel',
  495. parameter: payloadPreValue
  496. }, sender, payloadData);
  497. return payload;
  498. },
  499. checkPayloadData: function checkPayloadData(payloadData) {
  500. var data = payloadData;
  501. if (!data) {
  502. var undoRedoTransactionId = _.uniqueId('layoutModelTransaction');
  503. data = { undoRedoTransactionId: undoRedoTransactionId };
  504. }
  505. return data;
  506. },
  507. /**
  508. * Helper function that updated the parent and the style of a model. Returns an object with value before and after the change.
  509. * @param model
  510. * @param options
  511. * @param sender
  512. * @param addedModels - a list of models that were added in the same operation
  513. * @param payloadData - Additional data that will be included in the event payload.
  514. */
  515. _updateModelProperties: function _updateModelProperties(model, options, sender, addedModels, payloadData) {
  516. var changes = {
  517. prevValue: { id: model.id },
  518. value: { id: model.id }
  519. };
  520. if (options.parentId) {
  521. var parentId = options.parentId;
  522. // For now, only support adding to the first added model
  523. if (parentId === '$addArray[0]' && addedModels && addedModels.length > 0) {
  524. parentId = addedModels[0].model.id;
  525. }
  526. // Update the parent
  527. var updateParent = model.updateParent({
  528. parentId: parentId,
  529. insertBefore: options.insertBefore
  530. }, sender, payloadData);
  531. if (updateParent !== null) {
  532. changes.prevValue = _.extend(changes.prevValue, updateParent.prevValue.parameter);
  533. changes.value = _.extend(changes.value, updateParent.value.parameter);
  534. }
  535. }
  536. // Update the style submodel
  537. var updateStyle = model.updateStyle(options.style, sender, payloadData);
  538. if (updateStyle) {
  539. changes.prevValue.style = updateStyle.prevValue.parameter;
  540. changes.value.style = updateStyle.value.parameter;
  541. }
  542. // Update the filters submodel
  543. if (options.filters) {
  544. var updateFilters = model.updateFilters(options.id, options.filters);
  545. if (updateFilters) {
  546. changes.prevValue.filters = updateFilters.prevValue.filters;
  547. changes.value.filters = updateFilters.value.filters;
  548. }
  549. }
  550. if (this.boardModel.layoutExtensions) {
  551. var modelExtensions = this.boardModel.layoutExtensions;
  552. Object.keys(modelExtensions || {}).forEach(function (property) {
  553. var update = model._callExtensionMethod({
  554. property: property,
  555. value: options[property]
  556. }, sender, payloadData);
  557. if (update) {
  558. changes.prevValue[property] = update.prevValue.parameter.value;
  559. changes.value[property] = update.value.parameter.value;
  560. }
  561. }, this);
  562. }
  563. return changes;
  564. },
  565. /**
  566. * Helper to call extension update. Handles triggering an event and undo redo automatically
  567. * @param options {object} :
  568. * {
  569. * property: name of the property
  570. * value: value of the property
  571. * }
  572. * @param sender sender of the call
  573. * @param payloadData - Additional data that will be included in the event payload.
  574. */
  575. _callExtensionMethod: function _callExtensionMethod(options, sender, payloadData) {
  576. var modelExtension = this.boardModel.layoutExtensions[options.property];
  577. if (modelExtension && typeof modelExtension.update === 'function') {
  578. var payload = modelExtension.update(this, options.value);
  579. if (payload) {
  580. return this._triggerEvent(payload.event, {
  581. op: '_callExtensionMethod',
  582. parameter: {
  583. property: options.property,
  584. value: payload.value
  585. }
  586. }, {
  587. op: '_callExtensionMethod',
  588. parameter: {
  589. property: options.property,
  590. value: payload.previousValue
  591. }
  592. }, sender, payloadData);
  593. }
  594. }
  595. return null;
  596. },
  597. /**
  598. * Helper function that updates an list of models.
  599. */
  600. _updateModelsProperties: function _updateModelsProperties(updateArray, sender, addedModels, payloadData) {
  601. var changes = { value: [], prevValue: [] };
  602. var info;
  603. for (var i = 0; i < updateArray.length; i++) {
  604. info = updateArray[i];
  605. var child = this.findModel(info.id);
  606. if (child) {
  607. var change = this._updateModelProperties(child, info, sender, addedModels, payloadData);
  608. changes.value.push(change.value);
  609. changes.prevValue.unshift(change.prevValue);
  610. }
  611. }
  612. return changes;
  613. },
  614. isSelected: function isSelected(id) {
  615. return this.selected.has(id);
  616. },
  617. select: function select(id) {
  618. var model = this.findModel(id);
  619. if (model) {
  620. this.selected.add(id);
  621. }
  622. },
  623. deselect: function deselect(id) {
  624. this.selected.delete(id);
  625. // Recursively deselect all the way up to the root.
  626. var parent = this.getParent();
  627. parent && parent.deselect(id);
  628. },
  629. getSelectedChildLayouts: function getSelectedChildLayouts() {
  630. var _this = this;
  631. var items = [];
  632. var addModel = function addModel(id) {
  633. var model = _this.findModel(id);
  634. if (model) {
  635. items.push(model);
  636. }
  637. return model;
  638. };
  639. // Find the selected layouts at this level.
  640. this.selected.forEach(addModel);
  641. if (this.type === 'group') {
  642. var parent = this.getParent();
  643. // If this layout is selected and a 'group', then add the subitems as they are implicitly selected.
  644. if (parent && parent.isSelected(this.id)) {
  645. var addNestedItems = function addNestedItems(item) {
  646. addModel(item.id);
  647. item.item && item.items.forEach(function (item) {
  648. return addNestedItems(item);
  649. });
  650. };
  651. this.items.forEach(function (item) {
  652. return addNestedItems(item);
  653. });
  654. return items;
  655. }
  656. }
  657. // Recursively iterate through children.
  658. if (this.items) {
  659. this.items.forEach(function (item) {
  660. var selected = item.getSelectedChildLayouts();
  661. if (selected) {
  662. items = items.concat(selected);
  663. }
  664. });
  665. }
  666. return items;
  667. },
  668. getSelectedWidgets: function getSelectedWidgets() {
  669. return this.getSelectedChildLayouts().filter(function (item) {
  670. return item.type === 'widget';
  671. });
  672. },
  673. getWidgetByIndex: function getWidgetByIndex(index) {
  674. if (index < 0) {
  675. return;
  676. }
  677. function _getWidgetByIndex(item, index, cursor) {
  678. if (item.type === 'widget' && index === cursor.index++) {
  679. return item;
  680. }
  681. var items = item.items;
  682. if (items) {
  683. var length = items.length;
  684. // Cannot use _.find here. Noticed some weird behaviour using it in a recursive
  685. // setting. It would return the parent of the matched item (sharing env or something?).
  686. for (var i = 0; i < length; i++) {
  687. var subItem = items[i];
  688. var widget = _getWidgetByIndex(subItem, index, cursor);
  689. if (widget) {
  690. return widget;
  691. }
  692. }
  693. }
  694. }
  695. return _getWidgetByIndex(this, index, { index: 0 });
  696. },
  697. /**
  698. * Get the next sibling id
  699. * @returns
  700. */
  701. getNextSiblingId: function getNextSiblingId() {
  702. return this._getSiblingId(1);
  703. },
  704. /**
  705. * Get the previous sibling id
  706. * @returns
  707. */
  708. getPreviousSiblingId: function getPreviousSiblingId() {
  709. return this._getSiblingId(-1);
  710. },
  711. _getSiblingId: function _getSiblingId(step) {
  712. var parent = this.getParent();
  713. var next = null;
  714. if (parent) {
  715. var idx = parent.items.indexOf(this) + step;
  716. if (idx >= 0 && idx < parent.items.length) {
  717. next = parent.items[idx].id;
  718. }
  719. }
  720. return next;
  721. },
  722. getParent: function getParent() {
  723. return this.boardModel.layout.findParentModel(this.id);
  724. },
  725. getTopLayoutModel: function getTopLayoutModel() {
  726. return this.boardModel.layout;
  727. },
  728. /**
  729. * Get the style map using the names in the given map
  730. *
  731. * @param names - map with names
  732. * @returns {___anonymous8145_8146}
  733. */
  734. _getStyles: function _getStyles(names) {
  735. var styles = {};
  736. for (var name in names) {
  737. if (names.hasOwnProperty(name)) {
  738. styles[name] = this.style && this.style[name] ? this.style[name] : '';
  739. }
  740. }
  741. return styles;
  742. },
  743. /**
  744. * Util for incrementing a style value ( % or px ) by 5% or 25px
  745. * Moved from BoardModel to become common
  746. *
  747. * @param value - value to be incremented
  748. * @returns the incremented value
  749. */
  750. incrementStyleValue: function incrementStyleValue(value) {
  751. if (value) {
  752. if (value[value.length - 1] === '%') {
  753. return parseFloat(value) + 5 + '%';
  754. } else if (value[value.length - 1] === 'x') {
  755. return parseFloat(value) + 25 + 'px';
  756. } else {
  757. return value;
  758. }
  759. }
  760. return value;
  761. },
  762. /**
  763. * Util for decrementing a style value ( % or px ) by 5% or 25px
  764. * Moved from BoardModel to become common
  765. *
  766. * @param value - value to be incremented
  767. * @returns the incremented value
  768. */
  769. decrementStyleValue: function decrementStyleValue(value) {
  770. if (value) {
  771. if (value[value.length - 1] === '%') {
  772. return parseFloat(value) - 5 + '%';
  773. } else if (value[value.length - 1] === 'x') {
  774. return parseFloat(value) - 25 + 'px';
  775. } else {
  776. return value;
  777. }
  778. }
  779. return value;
  780. },
  781. /**
  782. * Find a parent model based on the child id recursively.
  783. * @param {String} sId
  784. */
  785. findParentModel: function findParentModel(id) {
  786. var index = this._getItemIndex(id);
  787. if (index !== -1) {
  788. return this;
  789. }
  790. var parent = null;
  791. if (this.items) {
  792. for (var i = 0; !parent && i < this.items.length; i++) {
  793. parent = this.items[i].findParentModel(id);
  794. }
  795. }
  796. return parent;
  797. },
  798. /**
  799. * A private method to check if the layout type is a container layout
  800. *
  801. * @returns {boolean} true if type is container; false otherwise
  802. */
  803. _isContainerType: function _isContainerType() {
  804. if (_.contains(['container'], this.type)) {
  805. return true;
  806. }
  807. return false;
  808. },
  809. /**
  810. * This method will return an object containing a list of drop zones found from the layout it is called from.
  811. * This can be every drop zone in a dashboard or story (all tabs or all scenes) or it can be a single tab/scene's
  812. * drop zones depending on how high up the model the search is done from.
  813. *
  814. * @returns undefined|{Object} Return early if not template layout. Otherwise return an object with an array of IDs
  815. * and the parent ID of the drop zones
  816. */
  817. findDropZones: function findDropZones() {
  818. if (!this._isContainerType()) {
  819. return;
  820. }
  821. var dropZones = this.findDescendantsWithType(['templateDropZone', 'templateIndicator']);
  822. var parentId = this.findParentModel(dropZones[0].id).id;
  823. var dropZoneIds = [];
  824. for (var i = 0; i < dropZones.length; i++) {
  825. dropZoneIds.push(dropZones[i].id);
  826. }
  827. return {
  828. ids: dropZoneIds,
  829. parentId: parentId
  830. };
  831. },
  832. /**
  833. * Return the list of all the descendant layouts with the given type
  834. * Stops descending on found model type (does not search its children)
  835. *
  836. * @param type - type(s) of layout to search for (single value or array of values)
  837. */
  838. findDescendantsWithType: function findDescendantsWithType(types) {
  839. var findTypes = Array.isArray(types) ? types : [types];
  840. var descendants = [];
  841. if (this.items) {
  842. for (var _iterator = this.items, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) {
  843. var _ref;
  844. if (_isArray) {
  845. if (_i >= _iterator.length) break;
  846. _ref = _iterator[_i++];
  847. } else {
  848. _i = _iterator.next();
  849. if (_i.done) break;
  850. _ref = _i.value;
  851. }
  852. var item = _ref;
  853. if (findTypes.indexOf(item.type) !== -1) {
  854. descendants.push(item);
  855. } else {
  856. descendants.push.apply(descendants, item.findDescendantsWithType(types));
  857. }
  858. }
  859. }
  860. return descendants;
  861. },
  862. /**
  863. * Return the parent layouts that are groups
  864. * the returned object are the actual layouts and not the IDs
  865. *
  866. * @param layout1 the first layout.
  867. * @param layout2 the second layout.
  868. * @return [
  869. * [widget1, widget2],
  870. * [parent1, parent2],
  871. * [grandParent1, grandParent2],
  872. * ... and so on ...
  873. * ]
  874. */
  875. getLinkedLayoutTree: function getLinkedLayoutTree(layout1, layout2) {
  876. var result = [];
  877. return this._getLinkedLayoutTreeWorker(layout1, layout2, result);
  878. },
  879. /* recurse up the family tree until we find a parent that is not a "linkable" container type
  880. */
  881. _getLinkedLayoutTreeWorker: function _getLinkedLayoutTreeWorker(layout1, layout2, result) {
  882. result.push([layout1, layout2]);
  883. var oflinkableType = function oflinkableType(layout) {
  884. return layout.type === 'group';
  885. };
  886. var parent1 = layout1.getParent();
  887. var parent2 = layout2.getParent();
  888. if (!oflinkableType(parent1) && !oflinkableType(parent2)) {
  889. // both parents are not groups so the family tree could be the same.
  890. // and if we made it this far then they are the same.
  891. return result;
  892. }
  893. if (!oflinkableType(parent1) || !oflinkableType(parent2)) {
  894. // only 1 parent is not a group so definitely different family trees
  895. return null;
  896. }
  897. return this._getLinkedLayoutTreeWorker(parent1, parent2, result);
  898. },
  899. /**
  900. * Given any layout item, find its top level parent item (the item in the layout's root level item list that contains it).
  901. * @param id - the id of the model for a widget, templateDropZone etc.
  902. * @returns the parent object (ie tab) for this model id
  903. */
  904. findTopLevelParentItem: function findTopLevelParentItem(id) {
  905. var itemFound = null;
  906. _.each(this.items, function (item) {
  907. if (item.id === id) {
  908. itemFound = this;
  909. } else if (this.findChildItem(item.items, id) !== null) {
  910. itemFound = item;
  911. }
  912. }.bind(this));
  913. return itemFound;
  914. },
  915. /**
  916. * Recursively search all layout items and their child items lists for an id which matches
  917. * the specified id
  918. * @param items - the items list whose subtree of items lists are to be searched
  919. * @param id - the id to search for.
  920. * @returns the item if found or null if this id does not exist in this items list or any child lists.
  921. */
  922. findChildItem: function findChildItem(items, id) {
  923. if (items) {
  924. for (var i = 0; i < items.length; ++i) {
  925. var item = items[i];
  926. if (item.id === id) {
  927. // found it!
  928. return item;
  929. } else if (item.items) {
  930. item = this.findChildItem(item.items, id);
  931. if (item) {
  932. // found it!
  933. return item;
  934. }
  935. // otherwise continue with the remaining siblings
  936. }
  937. }
  938. }
  939. return null;
  940. },
  941. /**
  942. * Insert an item in the item array. If insertBeforeId is not provided, the item will be inserted at the end
  943. * @param options
  944. * options.model
  945. * options.insertBeforeId
  946. *
  947. * @param sender
  948. * @param payloadData
  949. */
  950. _insertItem: function _insertItem(options, sender, payloadData) {
  951. var item = options.model;
  952. var insertBeforeId = options.insertBeforeId;
  953. if (!(item instanceof LayoutModel)) {
  954. // Clone the model so that the internal model values are not affected by the value in the options parameters
  955. item = new LayoutModel($.extend(true, {}, item), this.boardModel);
  956. }
  957. if (!this.items) {
  958. this.items = [];
  959. }
  960. var duplicateIndex = this._getItemIndex(item.id);
  961. if (duplicateIndex !== -1) {
  962. // this should never occur since it would cause a layout with a duplicate ID to be added.
  963. // this works around the issue in the hope that we can find the broken code eventually.
  964. this.logger.error('Found duplicate layout id "' + item.id + '" at index "' + duplicateIndex + '" in layout "' + this.id + '".');
  965. // this is not added to the undo/redo stack since undo of an insert should be a delete.
  966. // we are just working around the error condition of inserting a duplicate layout.
  967. this.items.splice(duplicateIndex, 1);
  968. }
  969. // Find insert position
  970. var idx = this._getItemIndex(insertBeforeId);
  971. // Add to the items array
  972. if (idx !== -1) {
  973. this.items.splice(idx, 0, item);
  974. } else {
  975. this.items.push(item);
  976. }
  977. return this._triggerEvent('insert:item', { op: '_insertItem', parameter: { model: item.toJSON(), insertBeforeId: insertBeforeId } }, { op: '_removeItem', parameter: item.id }, sender, payloadData);
  978. },
  979. /**
  980. * Find the index of the child in the item array
  981. * @param {Object} item with an id attribute
  982. */
  983. _getItemIndex: function _getItemIndex(id) {
  984. var idx = -1;
  985. if (this.items) {
  986. var model = null;
  987. for (var i = 0; i < this.items.length; i++) {
  988. model = this.items[i];
  989. if (model.id === id) {
  990. idx = i;
  991. break;
  992. }
  993. }
  994. }
  995. return idx;
  996. },
  997. /**
  998. * Remove the child model with the given id
  999. * @param id
  1000. * @returns
  1001. */
  1002. _removeItem: function _removeItem(id) {
  1003. var model = null;
  1004. var idx = this._getItemIndex(id);
  1005. if (idx >= 0) {
  1006. model = this.items[idx];
  1007. this.items.splice(idx, 1);
  1008. }
  1009. return model;
  1010. },
  1011. /*
  1012. * Used while cloning a 'LayoutModel'; Replaces any 'related layout' ids with those found in map.
  1013. */
  1014. replaceRelatedLayouts: function replaceRelatedLayouts(map) {
  1015. if (this.relatedLayouts && this.relatedLayouts.length > 0) {
  1016. var updated = '';
  1017. if (this.relatedLayouts[0] === '|') {
  1018. var related = this.relatedLayouts.split('|');
  1019. updated += '|';
  1020. _.each(related, function (rel) {
  1021. if (rel.length > 0) {
  1022. updated += (map[rel] ? map[rel] : rel) + '|';
  1023. }
  1024. });
  1025. } else {
  1026. updated = map[this.relatedLayouts] ? map[this.relatedLayouts] : this.relatedLayouts;
  1027. }
  1028. this.relatedLayouts = updated;
  1029. }
  1030. _.each(this.items, function (item) {
  1031. item.replaceRelatedLayouts(map);
  1032. });
  1033. },
  1034. incrementTitleCount: function incrementTitleCount(count) {
  1035. if (this.title && this.title.length > 0) {
  1036. this.title = this.title + ' (' + count + ')';
  1037. }
  1038. if (this.title && this.title.translationTable) {
  1039. this.title.translationTable = _.extend({}, this.title.translationTable);
  1040. Object.keys(this.title.translationTable).forEach(function (key) {
  1041. this.title.translationTable[key] = this.title.translationTable[key] + '-' + count;
  1042. }.bind(this));
  1043. }
  1044. },
  1045. _createPayload: function _createPayload(eventName, value, prevValue, sender, payloadData) {
  1046. var payload = {
  1047. modelId: this.id,
  1048. value: value,
  1049. sender: sender ? sender : this
  1050. };
  1051. if (payloadData) {
  1052. payload.data = payloadData;
  1053. }
  1054. // the event supports undo/redo
  1055. if (prevValue) {
  1056. payload.prevValue = prevValue;
  1057. // Use the applyFn of the top level when we the operation is not a change operation on the current model.
  1058. // if we delete the model and then undo, we don't want to use the applyFn of a deleted model.
  1059. var layout = eventName.indexOf('change:') === 0 ? this : this.boardModel.layout;
  1060. var applyFn = layout.applyFn.bind(layout);
  1061. payload.senderContext = {
  1062. applyFn: function (undoRedoParam, undoRedoSender) {
  1063. applyFn(undoRedoParam, undoRedoSender, payloadData);
  1064. }.bind(this)
  1065. };
  1066. }
  1067. return payload;
  1068. },
  1069. /**
  1070. * Helper function that will trigger an event
  1071. * @param eventName
  1072. * @param value
  1073. * @param prevValue
  1074. * @param sender
  1075. * @param payloadData - Additional data that will be included in the event payload.
  1076. */
  1077. _triggerEvent: function _triggerEvent(eventName, value, prevValue, sender, payloadData) {
  1078. var payload = this._createPayload(eventName, value, prevValue, sender, payloadData);
  1079. this.trigger(eventName, payload);
  1080. return payload;
  1081. },
  1082. /**
  1083. * function used for undo/redo.
  1084. * @param value
  1085. * @param sender
  1086. * @param payloadData - Additional data that will be included in the event payload.
  1087. */
  1088. applyFn: function applyFn(value, sender, payloadData) {
  1089. var _this2 = this;
  1090. if (value && value.op && typeof this[value.op] === 'function') {
  1091. var args = [value.parameter];
  1092. args.push(sender);
  1093. args.push(payloadData);
  1094. this[value.op].apply(this, args);
  1095. } else {
  1096. var layoutContexts = this.boardModel && this.boardModel.layout && this.boardModel.layout.items || [];
  1097. var matchingContext = layoutContexts.find(function (item) {
  1098. return item.id === _this2.id;
  1099. });
  1100. LayoutModel.inherited('applyFn', matchingContext || this, arguments);
  1101. }
  1102. },
  1103. /**
  1104. * Determine the layout positioning for the given model.
  1105. * Walk up the parent tree until found.
  1106. * @param {boolean} includeParents - Search parents if not defined on this model
  1107. * @return {string} Either 'absolute' or 'relative'
  1108. */
  1109. getLayoutPositioning: function getLayoutPositioning(includeParents) {
  1110. if (this.type === 'genericPage') {
  1111. return this.layoutPositioning || 'relative';
  1112. } else if (includeParents) {
  1113. var parent = this.getParent();
  1114. if (parent) {
  1115. return parent.getLayoutPositioning();
  1116. }
  1117. return undefined;
  1118. } else {
  1119. return undefined;
  1120. }
  1121. },
  1122. /**
  1123. * Creates a ContentModel by using properties and features specs.
  1124. *
  1125. * If the features spec contains a "Models_internal" use that
  1126. * If the features spec contains a "Models.internal" use that
  1127. * If using one of them than instantiate a new widget based on that spec
  1128. *
  1129. * TODO: this functon is referenced in Explore, need to clean that up
  1130. */
  1131. _initializeContentModel: function _initializeContentModel() {
  1132. if (this.features) {
  1133. var featureName = void 0;
  1134. if (this.features['Models.internal']) {
  1135. featureName = 'Models.internal';
  1136. } else if (this.features['Models_internal']) {
  1137. featureName = 'Models_internal';
  1138. }
  1139. if (featureName) {
  1140. this.boardModel.createLegacyWidgetModel(this.features[featureName]);
  1141. }
  1142. }
  1143. this.content = ModelUtils.initializeContentModel(this);
  1144. },
  1145. /**
  1146. * @return {ContentModel} @see ContentModel.js
  1147. */
  1148. getContentModel: function getContentModel() {
  1149. return this.content;
  1150. },
  1151. getUsedCustomColors: function getUsedCustomColors(customColors) {
  1152. var usedColors = LayoutModel.inherited('getUsedCustomColors', this, arguments);
  1153. if (this.items && this.items.length) {
  1154. this.items.forEach(function (item) {
  1155. usedColors = usedColors.concat(item.getUsedCustomColors(customColors));
  1156. });
  1157. }
  1158. return _.uniq(usedColors, false);
  1159. }
  1160. });
  1161. LayoutModel.CHANGE_STYLE_EVENT_NAME = 'change:style';
  1162. return LayoutModel;
  1163. });
  1164. //# sourceMappingURL=LayoutModel.js.map