WidgetBase.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155
  1. 'use strict';
  2. /*
  3. *+------------------------------------------------------------------------+
  4. *| Licensed Materials - Property of IBM
  5. *| IBM Cognos Products: Dashboard
  6. *| (C) Copyright IBM Corp. 2014, 2020
  7. *|
  8. *| US Government Users Restricted Rights - Use, duplication or disclosure
  9. *| restricted by GSA ADP Schedule Contract with IBM Corp.
  10. *+------------------------------------------------------------------------+
  11. */
  12. define(['../lib/@waca/core-client/js/core-client/ui/core/Events', '../lib/@waca/core-client/js/core-client/utils/Deferred', '../api/Error', 'underscore', 'jquery', '../utils/HtmlXSSUtils', '../DynamicFileLoader', '../api/impl/Widget', '../utils/ContentUtil', './ExpandedController', './FocusView'], function (Events, Deferred, ApiError, _, $, HtmlCleanser, DynamicFileLoader, WidgetAPIImpl, ContentUtil, ExpandedController, FocusView) {
  13. /**
  14. * A widget will normally size itself, but when it has an error, default dimensions are specified so the error message is readable
  15. */
  16. var ERROR_MESSAGE_MIN_DIMENSIONS = {
  17. width: 300,
  18. height: 300
  19. };
  20. /**
  21. * Base Widget
  22. *
  23. * Base class for Gemini widgets. This class encapsulates the interactions with the dashboard infrastructure.
  24. *
  25. * Widgets would have these life cycle functions
  26. * render : render the initial view
  27. * onContainerReady : called when container is ready. container provides a Widget Model object in this call. Widgets
  28. * can react to model changes by adding listeners as well as update the model so canvas container gets notified
  29. * destroy : cleanup on destruction
  30. *
  31. */
  32. var WidgetBase = Events.extend({
  33. expanded: false,
  34. // constructor
  35. init: function init(options) {
  36. WidgetBase.inherited('init', this, arguments);
  37. this.dashboardApi = options.dashboardApi;
  38. var usePreferredSizeConfig = [undefined, true, 'true'].indexOf(this.dashboardApi.getAppConfig('usePreferredSize')) !== -1;
  39. var usePreferredSizeModel = options.initialConfigJSON && options.initialConfigJSON.usePreferredSize;
  40. this.usePreferredSize = usePreferredSizeModel === false ? false : usePreferredSizeConfig;
  41. this.whenContainerIsReady = new Deferred();
  42. this.id = options.id;
  43. this.canvas = options.canvas;
  44. this.content = options.content;
  45. this._stateAPI = this.content.getFeature('state.internal');
  46. this._stateAPI.onChangeError(this.onStateChangeError.bind(this));
  47. this.initialConfigJSON = options.initialConfigJSON;
  48. this.el = options.el;
  49. this.$el = $(this.el);
  50. this.$widgetContainer = $(options.widgetContainer);
  51. this.eventRouter = options.eventRouter;
  52. this.services = options.services;
  53. if (options.registry) {
  54. this.properties = options.registry.properties;
  55. }
  56. var dashboardInternalFeature = this.dashboardApi.getFeature('internal');
  57. if (dashboardInternalFeature) {
  58. this.eventGroups = dashboardInternalFeature.getBoardModel().eventGroups;
  59. }
  60. this.colorsService = this.dashboardApi.getFeature('Colors');
  61. // Can we register the events after render?
  62. this.registerEvents(options.eventRouter);
  63. this.contributionSpec = options.registry;
  64. this.logger = this.dashboardApi.getGlassCoreSvc('.Logger');
  65. this.errorView = options.errorView;
  66. this.propertiesUtil = options.propertiesUtil;
  67. // Register the widget api as a deprecated content feature
  68. // Don't pass the widget iteself otherwise it will be destroyed twice (by the fetureLoader and the widget loader)
  69. options.contentFeatureLoader.registerDeprecatedFeature(this.id, 'WidgetAPI.deprecated', {
  70. getAPI: this.getAPI.bind(this)
  71. });
  72. this._expandModeContainerSelector = '.boardPageView:visible > .pageViewContent > .dashboardFrame > .dashboardFrameCentre';
  73. },
  74. getDashboardApi: function getDashboardApi() {
  75. return this.dashboardApi;
  76. },
  77. /**
  78. * Subclasses can call this method to extend the widget API with additional methods
  79. * @deprecated Visualization API should exposed in the getFeature response
  80. */
  81. _extendAPI: function _extendAPI(extensions) {
  82. var widgetAPI = this.getAPI();
  83. for (var name in extensions) {
  84. if (typeof extensions[name] === 'function') {
  85. widgetAPI[name] = extensions[name];
  86. }
  87. }
  88. },
  89. /**
  90. * For now, only live widgets support features
  91. */
  92. getFeature: function getFeature() /*name*/{
  93. return null;
  94. },
  95. /**
  96. * For now, only live widgets support features
  97. */
  98. setFeatureEnabled: function setFeatureEnabled() /*name, isEnabled*/{},
  99. getId: function getId() {
  100. return this.id;
  101. },
  102. /*
  103. * Return the widget API object
  104. */
  105. getAPI: function getAPI() {
  106. if (!this.widgetAPI) {
  107. this.widgetAPI = new WidgetAPIImpl(this).getAPI();
  108. }
  109. return this.widgetAPI;
  110. },
  111. /**
  112. * Helper to register board events handlers for infrastructure events
  113. * Derived Widget classes would override this function to add their handlers
  114. *
  115. */
  116. registerEvents: function registerEvents() {
  117. this.dashboardApi.on('widget:stopMove', this.onStopMove, this);
  118. this.dashboardApi.on('widget:startMove', this.onStartMove, this);
  119. this.dashboardApi.on('widget:onDetailErrors', this.addErrorDetailsHandler, this);
  120. if (this.colorsService) {
  121. this.colorsService.on('colorSet:changed', this.onDashboardColorSetChanged, this);
  122. }
  123. },
  124. unregisterEvents: function unregisterEvents() {
  125. this.dashboardApi.off('widget:stopMove', this.onStopMove, this);
  126. this.dashboardApi.off('widget:startMove', this.onStartMove, this);
  127. this.dashboardApi.off('widget:onDetailErrors', this.addErrorDetailsHandler, this);
  128. if (this.colorsService) {
  129. this.colorsService.off('colorSet:changed', this.onDashboardColorSetChanged, this);
  130. }
  131. },
  132. registerEventGroup: function registerEventGroup(transactionId) {
  133. transactionId = transactionId || _.uniqueId('_addToDefaultGroup_');
  134. // check if the widget is already assigned to an eventGroup
  135. if (this.eventGroups) {
  136. var group = this.eventGroups.findGroup(this.id);
  137. if (!group || group.getPageId() !== this.getContainerPageId()) {
  138. // obtain the default event group of the current page
  139. group = this.eventGroups.getDefaultGroup(this.getContainerPageId(), {
  140. payloadData: {
  141. undoRedoTransactionId: transactionId
  142. }
  143. });
  144. // assign to the default event group
  145. this.eventGroups.addToGroup(group.id, [this.id], {
  146. payloadData: {
  147. undoRedoTransactionId: transactionId
  148. }
  149. });
  150. return true;
  151. }
  152. }
  153. return false;
  154. },
  155. /**
  156. * Helper to register Widget Chrome event handlers
  157. * Derived Widget classes would override this function to add their handlers
  158. *
  159. * @param eventRouter - event router shared between 'widget chrome' and widget
  160. */
  161. registerWidgetChromeEvents: function registerWidgetChromeEvents(eventRouter) {
  162. //TODO: derived classes override this to setup handlers on view events or to fire requests to view
  163. if (eventRouter) {
  164. eventRouter.on('widget:onResize', this.resize, this);
  165. eventRouter.on('widget:onShow', this.onShow, this);
  166. eventRouter.on('widget:onHide', this.onHide, this);
  167. eventRouter.on('widget:onMaximize', this.onMaximize, this);
  168. eventRouter.on('widget:onRestore', this.onRestore, this);
  169. eventRouter.on('widget:onTitleChange', this.onTitleChange, this);
  170. eventRouter.on('widgetchrome:selected', this.onChromeSelected, this);
  171. eventRouter.on('widgetchrome:deselected', this.onChromeDeselected, this);
  172. eventRouter.on('widget:onAuthoringMode', this.onAuthoringMode, this);
  173. eventRouter.on('widget:onConsumeMode', this.onConsumeMode, this);
  174. eventRouter.on('widget:onEventGroupMode', this.onEventGroupMode, this);
  175. eventRouter.on('widget:onEnterContainer', this.onEnterContainer, this);
  176. eventRouter.on('widget:onExitContainer', this.onExitContainer, this);
  177. eventRouter.on('layout:fillColorChange', this.onPagefillColorChange, this);
  178. }
  179. },
  180. /**
  181. * Helper to unregister Widget Chrome event handlers
  182. * Derived Widget classes would override this function to add their handlers
  183. *
  184. * @param eventRouter - event router shared between 'widget chrome' and widget
  185. */
  186. unregisterWidgetChromeEvents: function unregisterWidgetChromeEvents(eventRouter) {
  187. //TODO: derived classes override this to setup handlers on view events or to fire requests to view
  188. if (eventRouter) {
  189. eventRouter.off('widget:onResize', this.resize, this);
  190. eventRouter.off('widget:onShow', this.onShow, this);
  191. eventRouter.off('widget:onHide', this.onHide, this);
  192. eventRouter.off('widget:onMaximize', this.onMaximize, this);
  193. eventRouter.off('widget:onRestore', this.onRestore, this);
  194. eventRouter.off('widget:onTitleChange', this.onTitleChange, this);
  195. eventRouter.off('widgetchrome:selected', this.onChromeSelected, this);
  196. eventRouter.off('widgetchrome:deselected', this.onChromeDeselected, this);
  197. eventRouter.off('widget:onAuthoringMode', this.onAuthoringMode, this);
  198. eventRouter.off('widget:onConsumeMode', this.onConsumeMode, this);
  199. eventRouter.off('widget:onEventGroupMode', this.onEventGroupMode, this);
  200. eventRouter.off('widget:onEnterContainer', this.onEnterContainer, this);
  201. eventRouter.off('widget:onExitContainer', this.onExitContainer, this);
  202. eventRouter.off('layout:fillColorChange', this.onPagefillColorChange, this);
  203. }
  204. },
  205. _registerModelEvents: function _registerModelEvents() {
  206. this.model.on('change', this._onModelChange, this);
  207. this.model.on('change:fillColor', this.applyFillColor, this);
  208. this.model.on('change:borderColor', this.applyBorderColor, this);
  209. this._modelEventsRegistered = true;
  210. },
  211. _unregisterModelEvents: function _unregisterModelEvents() {
  212. if (this._modelEventsRegistered) {
  213. this.model.off('change', this._onModelChange, this);
  214. this.model.off('change:fillColor', this.applyFillColor, this);
  215. this.model.off('change:borderColor', this.applyBorderColor, this);
  216. this._modelEventsRegistered = false;
  217. }
  218. },
  219. onStartMove: function onStartMove() {},
  220. onStopMove: function onStopMove() {},
  221. onPagefillColorChange: function onPagefillColorChange() /*payload*/{
  222. // To be overriden by sub classes
  223. },
  224. onDashboardColorSetChanged: function onDashboardColorSetChanged() /*payload*/{
  225. this.colorsService.makeSureColorIsValidInModel({
  226. model: this.model,
  227. propertyName: 'fillColor'
  228. });
  229. this.colorsService.makeSureColorIsValidInModel({
  230. model: this.model,
  231. propertyName: 'borderColor'
  232. });
  233. },
  234. getDefaultValue: function getDefaultValue(property) {
  235. var propSelected = _.find(this.properties, function (prop) {
  236. return prop.id === property;
  237. });
  238. if (propSelected) {
  239. return propSelected.defaultValue;
  240. }
  241. return;
  242. },
  243. /**
  244. * Widget Life cycle handler - gets called when container is ready
  245. * Canvas provides WidgetModel obj to widget, so that changes to it can be tracked at the Widget as well as canvas side
  246. */
  247. onContainerReady: function onContainerReady(containerContext) {
  248. var model = containerContext.model,
  249. widgetChromeEventRouter = containerContext.widgetChromeEventRouter,
  250. isAuthoringMode = containerContext.isAuthoringMode,
  251. _containerContext$add = containerContext.additionalWidgetData,
  252. additionalWidgetData = _containerContext$add === undefined ? {} : _containerContext$add,
  253. layoutAPI = containerContext.layoutAPI;
  254. this.model = model;
  255. this.widgetChromeEventRouter = widgetChromeEventRouter;
  256. this.isAuthoringMode = isAuthoringMode;
  257. this.addPayloadData = additionalWidgetData.addPayloadData;
  258. this.layoutAPI = layoutAPI;
  259. if (this.model) {
  260. this._registerModelEvents();
  261. this.addWhiteListAttrs('fillColor', 'borderColor', 'animationEntrance', 'animationExit');
  262. this.addColorProperties(['fillColor', 'borderColor']);
  263. if (this.model.localizedProps && this.model.localizedProps.length) {
  264. this.dashboardApi.getFeature('TranslationService').registerView({ view: this, model: this.model });
  265. }
  266. }
  267. this.registerWidgetChromeEvents(this.widgetChromeEventRouter);
  268. this.whenContainerIsReady.resolve();
  269. },
  270. /**
  271. * Get layout API
  272. *
  273. * @return {LayoutAPI} layout API
  274. */
  275. getLayoutAPI: function getLayoutAPI() {
  276. return this.layoutAPI;
  277. },
  278. onAuthoringMode: function onAuthoringMode() {
  279. this.isAuthoringMode = true;
  280. this.isEventGroupMode = false;
  281. },
  282. onConsumeMode: function onConsumeMode() {
  283. this.isAuthoringMode = false;
  284. this.isEventGroupMode = false;
  285. },
  286. onEventGroupMode: function onEventGroupMode() {
  287. this.isAuthoringMode = true;
  288. this.isEventGroupMode = true;
  289. this._setEventGroupOverlayContent(this.eventGroups.findGroup(this.id));
  290. },
  291. getEventGroupId: function getEventGroupId() {
  292. return this.eventRouter ? this.eventRouter.channelId : undefined;
  293. },
  294. setEventRouter: function setEventRouter(newEventRouter, options) {
  295. if (!newEventRouter && this.eventRouter) {
  296. // unregister from the current event router
  297. this.unregisterEvents(this.eventRouter);
  298. this.onRemoveCurrentEventRouter(options);
  299. this.unregistered = true;
  300. } else if (this.unregistered || newEventRouter.channelId !== this.eventRouter.channelId) {
  301. // unregister first and then...
  302. if (!this.unregistered) {
  303. this.unregisterEvents(this.eventRouter);
  304. this.onRemoveCurrentEventRouter(options);
  305. }
  306. // register with the new event router
  307. this.eventRouter = newEventRouter;
  308. if (this.eventRouter) {
  309. this.registerEvents(this.eventRouter);
  310. this.onNewEventRouter(options);
  311. this.unregistered = false;
  312. }
  313. }
  314. this._setEventGroupOverlayContent();
  315. },
  316. onRemoveCurrentEventRouter: function onRemoveCurrentEventRouter() /*options*/{
  317. // To be implemented by concrete classes
  318. },
  319. onNewEventRouter: function onNewEventRouter() /*options*/{
  320. // To be implemented by concrete classes
  321. },
  322. _setEventGroupOverlayContent: function _setEventGroupOverlayContent() {
  323. var group = this.eventGroups.findGroup(this.id);
  324. var node = this.getWidgetStyleNode();
  325. var overlayContent = node.find('.eventGroupOverlayContent');
  326. if (group && overlayContent) {
  327. overlayContent.text(group.getGroupIndex());
  328. }
  329. },
  330. /**
  331. * Event handler to be overridden by extended classes.
  332. * Intended to be used to handle navigation within a widget
  333. */
  334. onEnterContainer: function onEnterContainer() {
  335. /* meant to be overridden */
  336. },
  337. /**
  338. * Event handler to be overridden by extended classes.
  339. * Intended to be used to exit the navigation within a widget
  340. */
  341. onExitContainer: function onExitContainer() {
  342. /* meant to be overridden */
  343. },
  344. /**
  345. * Widget Life cycle handler - gets called when board spec gets changed e.g. when undo/redo happens
  346. * Derived classes override this function
  347. *
  348. * @param options The updated model
  349. */
  350. _onModelChange: function _onModelChange(options) {
  351. // call register properties to refresh the properties UI on undo/redo
  352. // We re-register only if we have previously registered.
  353. if (options && options.sender === 'UndoRedoController') {
  354. this.refreshPropertiesPane();
  355. }
  356. },
  357. /**
  358. * Widget Life cycle handler - gets called when widget is destroyed
  359. * Derived classes override this function
  360. */
  361. destroy: function destroy() {
  362. this._unregisterModelEvents();
  363. if (this._expanded) {
  364. this._expanded.remove();
  365. }
  366. this.unregisterEvents(this.eventRouter);
  367. this.unregisterWidgetChromeEvents(this.widgetChromeEventRouter);
  368. if (this.model) {
  369. if (this.model.localizedProps && this.model.localizedProps.length) {
  370. this.dashboardApi.getFeature('TranslationService').deregisterView(this.model.id);
  371. }
  372. this.model.contentReferences = [];
  373. }
  374. this._expanded = null;
  375. this.eventRouter = null;
  376. },
  377. /**
  378. * Returns property from model
  379. *
  380. * @param The property name to get
  381. */
  382. get: function get(propertyName) {
  383. return this.model ? this.model[propertyName] : undefined;
  384. },
  385. /**
  386. * Sets a hash of attributes on this widget's model and notifies the canvas of the change.
  387. *
  388. * @param The properties to set on the model
  389. * @param Model set options, see Model.js for details
  390. */
  391. set: function set(attributes, options) {
  392. if (this.model && attributes) {
  393. options = options || {};
  394. options = _.defaults(options, {
  395. sender: this.id
  396. });
  397. this.model.set(attributes, options);
  398. }
  399. },
  400. /**
  401. * Trigger an event through the canvas infrastructure eventing mechanism
  402. *
  403. * @param eventName The name of the event to trigger
  404. * @param event The event
  405. */
  406. triggerExternalEvent: function triggerExternalEvent(eventName, event) {
  407. if (this.eventRouter) {
  408. this.eventRouter.trigger(eventName, event);
  409. }
  410. },
  411. /**
  412. * @param options
  413. */
  414. onTitleChange: function onTitleChange() /*options*/{
  415. //handle title update
  416. },
  417. /*
  418. * Called when properties are changed on the properties pane for this widget. Overridden by derived classes
  419. */
  420. onPropertyUpdate: function onPropertyUpdate(propChange) {
  421. var prop = {};
  422. prop[propChange.category] = propChange.item;
  423. var data = null;
  424. if (propChange.transactionId) {
  425. data = {
  426. undoRedoTransactionId: propChange.transactionId,
  427. transactionToken: propChange.transactionToken
  428. };
  429. }
  430. if (propChange.category === 'fillColor' || propChange.category === 'borderColor') {
  431. this.colorsService.prepareForColorModelChange(prop, propChange.category);
  432. }
  433. this.set(prop, {
  434. silent: false,
  435. payloadData: data
  436. });
  437. },
  438. /**
  439. * Notify the layout to update the widget label
  440. */
  441. updateDescription: function updateDescription(label) {
  442. if (this.widgetChromeEventRouter) {
  443. this.widgetChromeEventRouter.trigger('widget:updateDescription', {
  444. value: label
  445. });
  446. }
  447. },
  448. /**
  449. * Notify the layout to update the widget aria-label
  450. */
  451. updateWidgetArialabel: function updateWidgetArialabel(label) {
  452. if (this.widgetChromeEventRouter) {
  453. this.widgetChromeEventRouter.trigger('widget:updateWidgetArialabel', {
  454. value: label
  455. });
  456. }
  457. },
  458. /**
  459. * Notify the layout to clear the widget aria-label
  460. */
  461. clearWidgetArialabel: function clearWidgetArialabel() {
  462. if (this.widgetChromeEventRouter) {
  463. this.widgetChromeEventRouter.trigger('widget:clearWidgetArialabel');
  464. }
  465. },
  466. setLayoutProperties: function setLayoutProperties(properties) {
  467. if (this.widgetChromeEventRouter) {
  468. this.widgetChromeEventRouter.trigger('widget:setLayoutProperties', properties);
  469. }
  470. },
  471. /**
  472. * @param icon - object - A jquery object referencing an element to add
  473. * @param name - string - A string representing the name of the icon
  474. * @param location - string|undefined - A string value of 'footer' to add an icon to the widget footer. undefined to keep backwards
  475. * compatability for adding icon to header
  476. */
  477. addIcon: function addIcon(icon, name) {
  478. if (this.widgetChromeEventRouter) {
  479. var location = void 0;
  480. var $header = this._isMaximized && this._expanded && this._expanded.containerElement && this._expanded.containerElement.find('.widgetHeader');
  481. if ($header && $header.length) {
  482. location = $header.find('.widgetIcons');
  483. if (location.length === 0) {
  484. location = $header;
  485. }
  486. }
  487. this.widgetChromeEventRouter.trigger('widget:addIcon', {
  488. widgetIcon: icon,
  489. name: name,
  490. location: location
  491. });
  492. }
  493. },
  494. /**
  495. * Update the widget container to reflect the common style properties in the model
  496. */
  497. applyCommonProperties: function applyCommonProperties() {
  498. if (this.model) {
  499. this.applyFillColor({
  500. value: this.model.fillColor
  501. });
  502. this.applyBorderColor({
  503. value: this.model.borderColor
  504. });
  505. var showTitle = this.model.showTitle;
  506. if (!showTitle) {
  507. showTitle = this.getDefaultValue('showTitle');
  508. }
  509. this.showTitle({
  510. value: showTitle
  511. });
  512. }
  513. },
  514. showTitle: function showTitle(data) {
  515. if (!data.value) {
  516. var $parent = this.$el.parent();
  517. if ($parent.hasClass('widget')) {
  518. $parent.find('.textArea').addClass('hidden');
  519. }
  520. }
  521. },
  522. changeTitleType: function changeTitleType(data) {
  523. if (data.value === 'smart') {
  524. var widgetSpec = this.model.toJSON();
  525. widgetSpec.name = '';
  526. var value = this.dashboardApi.getDashboardCoreSvc('.SmartNamingSvc').getWidgetName(widgetSpec);
  527. this.$el.parent().find('.textArea').text(value);
  528. }
  529. },
  530. /**
  531. * Apply the fill color value to the view node
  532. * @param data
  533. */
  534. applyFillColor: function applyFillColor(data) {
  535. this.applyColor(data, 'fill');
  536. },
  537. /**
  538. * Apply the border color value to the view node
  539. * @param data
  540. */
  541. applyBorderColor: function applyBorderColor(data) {
  542. this.applyColor(data, 'border');
  543. },
  544. /**
  545. * Apply color (border/fill) value to the view node
  546. * @param data
  547. */
  548. applyColor: function applyColor(data, type) {
  549. var $node = this.getWidgetStyleNode();
  550. $node.each(function (index, node) {
  551. // We set the classname this way so that it works for svg elements. JQuery add/removeClass does not work with svg elements.
  552. // clear previous fill color
  553. var re = new RegExp('\\s*\\b' + type + '-[^\\s]*\\b', 'g');
  554. var className = node.getAttribute('class') || '';
  555. className = className.replace(re, '');
  556. if (data.value) {
  557. className += ' ' + this.getThemeColorClassName(data.value, type);
  558. }
  559. node.setAttribute('class', className);
  560. }.bind(this));
  561. },
  562. getWidgetStyleNode: function getWidgetStyleNode() {
  563. return this.$el.closest('.widget');
  564. },
  565. getThemeColorClassName: function getThemeColorClassName(color, type) {
  566. return type + '-' + color;
  567. },
  568. resize: function resize() {
  569. // handle resize update
  570. },
  571. onShow: function onShow() {},
  572. onHide: function onHide() {},
  573. /**
  574. * To be overridden by extended classes.
  575. * Intended to be used to re-render a widget when it is revealed
  576. */
  577. reveal: function reveal() {},
  578. onChromeSelected: function onChromeSelected() {
  579. //update state info
  580. this.chromeSelected = true;
  581. this.triggerExternalEvent('widget:selected', {
  582. sender: this.model.id,
  583. payloadData: this
  584. });
  585. },
  586. onChromeDeselected: function onChromeDeselected() {
  587. //update state info
  588. this.chromeSelected = false;
  589. this.triggerExternalEvent('widget:deselected', {
  590. sender: this.model.id
  591. });
  592. this.triggerExternalEvent('properties:deregister', {
  593. sender: this.model.id
  594. }); //TODO do we need sender ?
  595. },
  596. showWarning: function showWarning(msg, params) {
  597. this.showError(msg, params, 'warning');
  598. },
  599. _updateErrorContainer: function _updateErrorContainer() /*msg, errorContainer*/{},
  600. _updateErrorType: function _updateErrorType(msg, errorType) {
  601. return errorType ? errorType : 'error';
  602. },
  603. /**
  604. * Check whether the widget is displaying an error
  605. *
  606. * @return {boolean} TRUE is widget has error and FALSE if not
  607. */
  608. hasError: function hasError() {
  609. //TODO: Add a property on the widget model to expose the error state instead of
  610. //using the DOM.
  611. return this.$el.has('.errorContainer').length > 0;
  612. },
  613. /**
  614. * Check whether the widget has warning
  615. *
  616. * @return {boolean} TRUE is widget has warning and FALSE if not
  617. */
  618. hasWarning: function hasWarning() {
  619. //TODO: Add a property on the widget model to expose the warning state instead of
  620. //using the DOM.
  621. return this.$el.has('.warningContainer').length > 0;
  622. },
  623. getError: function getError() {
  624. return this.errorMessage;
  625. },
  626. onStateChangeError: function onStateChangeError(error) {
  627. if (error) {
  628. this.renderError(error.getMessage(), error.getParams(), error.getType());
  629. } else {
  630. this._clearError();
  631. }
  632. },
  633. renderError: function renderError(msg, params, type) {
  634. // Always keep track of the last query response that caused an error
  635. if (params && params.errorInfo) {
  636. this._lastErrorInfo = params.errorInfo;
  637. }
  638. type = this._updateErrorType(msg, type);
  639. var hasError = this.hasError();
  640. if (this.el) {
  641. var $errorContainer = this.errorView.renderContainer({
  642. id: this.id + 'Title',
  643. type: type,
  644. msg: {
  645. str: msg,
  646. params: params
  647. }
  648. });
  649. this._updateErrorContainer(msg, $errorContainer);
  650. this.$el.find('.errorContainer').remove();
  651. // Only cause the widget to resize on the first error we show or it'll keep shrinking on every subsequent error
  652. if (this.widgetChromeEventRouter && !hasError) {
  653. var errorContainerStyle = {
  654. 'min-height': Math.max(ERROR_MESSAGE_MIN_DIMENSIONS.height, this.$el.innerHeight()),
  655. 'min-width': ERROR_MESSAGE_MIN_DIMENSIONS.width
  656. };
  657. this.widgetChromeEventRouter.trigger('widget:showError', errorContainerStyle, this);
  658. }
  659. // Replace the text in helper node for Jaws reading.
  660. var msgText = this.errorView.makeNlsMessage({
  661. str: msg,
  662. params: params
  663. });
  664. this.errorMessage = msgText;
  665. this.updateWidgetArialabel(msgText);
  666. this.$el.append($errorContainer);
  667. this.addErrorDetailsHandler();
  668. if (this.renderComplete) {
  669. //If a renderComplete function is available, call it as we have rendered what we can and anything waiting on this widget to render shouldn't be blocked.
  670. this.renderComplete();
  671. }
  672. }
  673. },
  674. showError: function showError(msg, params, type) {
  675. var error = new ApiError({ 'msg': msg, 'params': params }, { 'type': type });
  676. this._stateAPI.setError(error);
  677. },
  678. /**
  679. * Add onClick handler to the error div if details are enabled and we have information about an http request that failed
  680. */
  681. addErrorDetailsHandler: function addErrorDetailsHandler() {
  682. if (window.dashboardErrorDetailsEnabled === true && this._lastErrorInfo && this.$el.has('.errorContainer').length > 0) {
  683. this.$el.find('.dashboardMessageBox').onClick(this._showErrorDetails.bind(this));
  684. }
  685. },
  686. _showErrorDetails: function _showErrorDetails() {
  687. if (this._lastErrorInfo) {
  688. return DynamicFileLoader.load(['ui/dialogs/MessageBox']).then(function (Modules) {
  689. var MessageBox = Modules[0];
  690. var detailsObj = {
  691. httpResponse: {
  692. status: this._lastErrorInfo.status,
  693. details: this._lastErrorInfo.responseJSON ? this._lastErrorInfo.responseJSON : {}
  694. },
  695. httpRequest: {
  696. querySpec: this._lastErrorInfo.querySpec ? this._lastErrorInfo.querySpec : {}
  697. },
  698. widgetSpec: this.model
  699. };
  700. var msgBox = new MessageBox('selectableInfo', 'Error details', JSON.stringify(detailsObj, null, 4));
  701. msgBox.open();
  702. }.bind(this));
  703. }
  704. return Promise.resolve();
  705. },
  706. clearError: function clearError() {
  707. this._stateAPI.clearError();
  708. },
  709. /**
  710. * Clears inline error message
  711. */
  712. _clearError: function _clearError() {
  713. this._lastQueryErrorInfo = null;
  714. this.errorMessage = null;
  715. if (this.hasError()) {
  716. if (this.widgetChromeEventRouter) {
  717. var payload = {};
  718. this.widgetChromeEventRouter.trigger('widget:clearError', payload);
  719. }
  720. this.$el.find('.errorContainer').remove();
  721. }
  722. },
  723. clearMoreDataIndicator: function clearMoreDataIndicator() {
  724. if (this.widgetChromeEventRouter) {
  725. this.widgetChromeEventRouter.trigger('widget:clearMoreDataIndicator');
  726. }
  727. },
  728. addWhiteListAttrs: function addWhiteListAttrs() {
  729. var args = Array.prototype.slice.call(arguments, 0);
  730. if (this.model && args.length > 0) {
  731. if (this.model.whitelistAttrs) {
  732. this.model.whitelistAttrs = _.uniq(this.model.whitelistAttrs.concat(args));
  733. } else {
  734. this.model.whitelistAttrs = args;
  735. }
  736. }
  737. },
  738. addColorProperties: function addColorProperties(colorProperties) {
  739. if (this.model) {
  740. if (this.model.colorProperties) {
  741. this.model.colorProperties = this.model.colorProperties.concat(colorProperties);
  742. } else {
  743. this.model.colorProperties = colorProperties;
  744. }
  745. }
  746. },
  747. addContentReferences: function addContentReferences(references) {
  748. if (this.model) {
  749. this.model.contentReferences = this.model.contentReferences.concat(references);
  750. }
  751. },
  752. removeWhiteListAttrs: function removeWhiteListAttrs() {
  753. if (this.model && this.model.whitelistAttrs) {
  754. var args = Array.prototype.slice.call(arguments, 0);
  755. this.model.whitelistAttrs = _.difference(this.model.whitelistAttrs, args);
  756. }
  757. },
  758. //when we 'expand' a widget, we still want to allow access to data strip and properties panels
  759. //so, a blocker is placed behind the widget over the canvas container
  760. //this selector identifies the container - should probably use a better/more decoupled way
  761. getExpandStartingPosition: function getExpandStartingPosition() {
  762. //when we 'expand' a widget, there is animation to zoom it to the bigger size (and back)
  763. //coordinates for the animation use screen coordinates, while the widget being zoomed uses relative position within the
  764. //chrome and also within the preview div
  765. //if we use the offset coordinates of this.$el, the starting position will get offset a bit
  766. //hence this adjustment to make it line up properly
  767. var screenVsRelativeOffset = $(this._expandModeContainerSelector).offset().top;
  768. var bounds = this.$el.offset();
  769. bounds.top -= screenVsRelativeOffset;
  770. bounds.width = this.$el.width();
  771. bounds.height = this.$el.height();
  772. screenVsRelativeOffset = $(this._expandModeContainerSelector).offset().left;
  773. bounds.left -= screenVsRelativeOffset;
  774. return bounds;
  775. },
  776. getExpandViewContent: function getExpandViewContent() {
  777. return $('<span class="collapseIcon" title="collapse"></span>');
  778. },
  779. isWidgetMaximized: function isWidgetMaximized() {
  780. return this._isMaximized === true;
  781. },
  782. // TODO: Focus mode needs to be a feature
  783. onMaximize: function onMaximize() {
  784. if (this._isMaximized || !this.isMaximizeSupported) {
  785. return;
  786. }
  787. this._isMaximized = true;
  788. this.triggerExternalEvent('widget:maximize', {
  789. id: this.id
  790. });
  791. if (this._expanded) {
  792. this._expanded.remove();
  793. }
  794. //save this before getExpandStartingPosition() gets called
  795. this._restoreToParent = this.$el.parent();
  796. this._expanded = new FocusView({
  797. owner: this,
  798. content: this.content,
  799. data: this.el,
  800. containerElement: $(this._expandModeContainerSelector),
  801. launchPoint: this.getWidgetStyleNode()[0],
  802. dashboardState: this.dashboardApi.getFeature('DashboardState')
  803. });
  804. return this._expanded.render();
  805. },
  806. toggleExpanded: function toggleExpanded(toggle) {
  807. if (toggle === undefined) {
  808. toggle = !this.expanded;
  809. }
  810. if (toggle !== this.expanded) {
  811. this.expanded = toggle;
  812. return this._getExpandedController().toggle(toggle);
  813. }
  814. return Promise.resolve();
  815. },
  816. _getExpandedController: function _getExpandedController() {
  817. if (!this._expandedController) {
  818. this._expandedController = new ExpandedController({
  819. widget: this
  820. });
  821. }
  822. return this._expandedController;
  823. },
  824. whenRenderComplete: function whenRenderComplete() {
  825. this._stateAPI.setStatus(this._stateAPI.STATUS.RENDERED);
  826. // The base implementation does not have a render. Once constructed, we assume that it is rendered.
  827. return Promise.resolve();
  828. },
  829. /**
  830. * This method is called when restoring a widget from maximized back to its parent container.
  831. * It returns a promise when its parent exist and undefined otherwise
  832. * @return Promise or undefined
  833. */
  834. onRestore: function onRestore() {
  835. this._isMaximized = false;
  836. this.triggerExternalEvent('widget:restore', {
  837. id: this.id
  838. });
  839. if (this._restoreToParent) {
  840. return this.getVisBounds().then(function (bounds) {
  841. //set dimensions so this.resize() works (when expanded, this.$el would have been resized to a bigger size)
  842. this.$el.css({
  843. width: bounds.width,
  844. height: bounds.height
  845. });
  846. this._restoreToParent.append(this.$el);
  847. this._restoreToParent = null;
  848. this.applyCommonProperties();
  849. this.resize();
  850. }.bind(this));
  851. }
  852. },
  853. /*
  854. * Returns an array of toolbar items to be displayed. Overridden by derived classes
  855. */
  856. getContextToolbarItems: function getContextToolbarItems() {
  857. return [];
  858. },
  859. /**
  860. * returns containerPageId that the widget belongs to
  861. */
  862. getContainerPageId: function getContainerPageId() {
  863. var type = this.dashboardApi.getAppConfig('pageContainerType');
  864. var pageContent = ContentUtil.getPageContent(this.content, type);
  865. if (pageContent) {
  866. return pageContent.getId();
  867. }
  868. return null;
  869. },
  870. /**
  871. * Reset the widget size to the preferred size.
  872. *
  873. */
  874. resizeToPreferredSize: function resizeToPreferredSize() {
  875. // to be implemented by concrete widgets
  876. },
  877. /**
  878. * Trigger the setPreferred size event to notify listeners (e.g the widget layout view)
  879. * of the preferred size.
  880. *
  881. * @param preferredSize This is the visualization's preferred size gotten from its definition
  882. * @param options Object that contains other information for the layout view such as transaction id
  883. */
  884. setPreferredSize: function setPreferredSize(preferredSize, options) {
  885. if (!this.usePreferredSize) {
  886. return;
  887. }
  888. var payload = {
  889. preferredSize: preferredSize,
  890. options: options
  891. };
  892. if (this.widgetChromeEventRouter) {
  893. this.widgetChromeEventRouter.trigger('widget:setPreferredSize', payload, this);
  894. }
  895. },
  896. /**
  897. * Get the appropriate visualization size from the layout view. This is an asynchronous function.
  898. * @returns {Promise} A Deferred's promise object that gets resolved with the vis bounds/size
  899. */
  900. getVisBounds: function getVisBounds() {
  901. var dfd = new Deferred();
  902. // The vis bounds depends on whether data widget is been rendered in focus mode,
  903. // intent view or otherwise.
  904. if (!this.widgetChromeEventRouter || this._isMaximized && this._expanded) {
  905. dfd.resolve({
  906. top: 0,
  907. left: 0,
  908. width: this.$el.innerWidth(),
  909. height: this.$el.innerHeight()
  910. });
  911. } else if (this._isIntentViewMode()) {
  912. // Widget is being rendered in intent results dialog
  913. var nPreviewContainer = this.$el.parents('.intent-results-preview');
  914. dfd.resolve({
  915. top: 0,
  916. left: 0,
  917. width: nPreviewContainer.innerWidth(),
  918. height: nPreviewContainer.innerHeight()
  919. });
  920. } else {
  921. // TODO: never pass a deferred/promise through an event!
  922. //trigger event to layoutview and attach oDeffered object as payload
  923. this.widgetChromeEventRouter.trigger('widget:getSize', {
  924. deferred: dfd
  925. }, this);
  926. }
  927. return dfd.promise;
  928. },
  929. /**
  930. * Determine if the widget is been rendered in an intent view dialog
  931. */
  932. _isIntentViewMode: function _isIntentViewMode() {
  933. //TODO (STORY 53043) This is a hacky approach. Needs rework.
  934. return this.$el.parents('.intent-results-preview').length > 0;
  935. },
  936. /**
  937. * Get the type of of the widget
  938. *
  939. * @return {string} type of the widget
  940. */
  941. getType: function getType() {
  942. return this.model.type;
  943. },
  944. _filterActiveAutoBinProperty: function _filterActiveAutoBinProperty(properties) {
  945. var _this = this;
  946. return _.filter(properties, function (property) {
  947. if (['autoBin.toggle', 'autoBin.count'].includes(property.id)) {
  948. return _this.visModelManager.getDefinition().binningConfig && _this.visModelManager.getDefinition().binningConfig.auto === true;
  949. }
  950. return true;
  951. });
  952. },
  953. onPropertyChange: function onPropertyChange(propertyName, value) {
  954. this.onPropertyUpdate(this.propertiesUtil.buildPropChangePayload(propertyName, value));
  955. },
  956. refreshPropertiesPane: function refreshPropertiesPane(options) {
  957. if (this.model) {
  958. this.triggerExternalEvent('properties:refreshPane', _.extend({
  959. 'sender': this.model.id
  960. }, options));
  961. }
  962. },
  963. isValidHtmlContent: function isValidHtmlContent(widgetContent) {
  964. return HtmlCleanser.isValidHtmlContent(widgetContent);
  965. },
  966. /**
  967. * check if the value of a property matched expected
  968. * @param {String} propertyName
  969. * @param {object} expectedValue
  970. * @return {boolean} true if it matches
  971. */
  972. doesVisPropertyMatchExpected: function doesVisPropertyMatchExpected(propertyName, expectedValue) {
  973. if (this._currVis && this._currVis.doesVisPropertyMatchExpected) {
  974. return this._currVis.doesVisPropertyMatchExpected(propertyName, expectedValue);
  975. }
  976. },
  977. /**
  978. * check if contextual grid is supported for this widget
  979. * to be overriden by children class
  980. * @return {Promise} promise resolves with true if enabled
  981. */
  982. isContextualGridEnabled: function isContextualGridEnabled() {
  983. return Promise.resolve(false);
  984. },
  985. _invokeLifeCycleHandlers: function _invokeLifeCycleHandlers(name, payload) {
  986. var _this2 = this;
  987. return this.dashboardApi.getDashboardSvc('.LifeCycleManager').then(function (LifeCycleManager) {
  988. return LifeCycleManager.invokeLifeCycleHandlers(name, payload);
  989. }).catch(function (e) {
  990. _this2.logger.error(e);
  991. });
  992. },
  993. /**
  994. * Render widget
  995. *
  996. * @return {Promise} promise resolved with current $el
  997. */
  998. render: function render() {
  999. this._stateAPI.setStatus(this._stateAPI.STATUS.RENDERING);
  1000. return Promise.resolve(this.$el);
  1001. }
  1002. });
  1003. return WidgetBase;
  1004. });
  1005. //# sourceMappingURL=WidgetBase.js.map