DataPlayerView.js 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2013, 2020
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. *
  7. * DataPlayerView
  8. */
  9. define(['jquery', 'underscore', '../../../util/DashboardFormatter', '../VisView', '../../../widgets/livewidget/nls/StringResources', 'text!./DataPlayerView.html', '../../../util/EventUtils'], function ($, _, Formatter, VisView, StringResources, template, EventUtils) {
  10. 'use strict';
  11. var DataPlayerView = null;
  12. DataPlayerView = VisView.extend({
  13. // constants
  14. templateString: template,
  15. playerSpeed: 1600,
  16. tickMinDistance: 44,
  17. maxLabelLayerCount: 2,
  18. verticalPositionScaleFactor: 2 / 3,
  19. barOverflow: 60,
  20. // references to JQuery selected elements
  21. $_sliderNode: null,
  22. $_sliderBar: null,
  23. $_sliderContainer: null,
  24. $_playPauseButton: null,
  25. _dataItemAPI: null,
  26. _playTimeout: null,
  27. _playIndex: -1,
  28. _currentSelectionIndex: -1,
  29. _mouseStartX: 0,
  30. _mouseOrgStartX: 0,
  31. _nodeVirtualPosition: 0,
  32. _snapThreshold: 15,
  33. _dragging: false,
  34. $tick: $('<div class="sliderTickForHorizontal"><div class="tick"></div></div>'),
  35. $label: $('<div class="sliderLabel" title=""></div>'),
  36. events: {
  37. 'clicktap .sliderLabel': 'onLabelClick',
  38. 'keydown .sliderLabel': 'onLabelKeypress',
  39. 'click .sliderTickForHorizontal': 'onTickClick',
  40. 'tap .sliderTickForHorizontal': 'onTickClick',
  41. 'touchstart .sliderNode': 'onSliderNodeMouseDown',
  42. 'touchend .slider': 'onSliderMouseUp',
  43. 'mousedown .sliderNode': 'onSliderNodeMouseDown',
  44. 'mouseup .slider': 'onSliderMouseUp',
  45. 'clicktap .playOrPause': 'onPlayOrPauseClick',
  46. 'mousedown .playOrPause': 'preventSelection',
  47. 'keydown .playOrPause': 'onPlayOrPauseKeypress'
  48. },
  49. getRenderer: function getRenderer() {
  50. return 'dashboard-analytics/visualizations/renderer/dataplayer/DataPlayerRenderer';
  51. },
  52. init: function init() {
  53. DataPlayerView.inherited('init', this, arguments);
  54. if (!this.visModel) {
  55. throw 'Invalid VisModel reference.';
  56. }
  57. var iconsFeature = this.dashboardApi.getFeature('Icons');
  58. var playIcon = iconsFeature.getIcon('playIcon');
  59. var pauseIcon = iconsFeature.getIcon('pauseIcon');
  60. $(this.el).find('.playOrPause').append('<svg class="play"><use xlink:href="#' + playIcon.id + '"/></svg><svg class="pause"><use xlink:href="#' + pauseIcon.id + '"/></svg>');
  61. this.el.style.position = 'relative';
  62. // main list of player entries
  63. this.playerEntries = [];
  64. },
  65. remove: function remove() {
  66. // clear timer
  67. this._clearPlayTimer();
  68. this.ownerWidget.dashboardApi.off('open:sharePanel', this._onSharePanelOpen, this);
  69. this.ownerWidget.dashboardApi.off('close:sharePanel', this._onSharePanelClose, this);
  70. // call overridden super class
  71. DataPlayerView.inherited('remove', this, arguments);
  72. },
  73. /**
  74. * RENDERING SEQUENCE:
  75. * For dataPlayer, whenDataReady will resolve when a column query for the column of interest
  76. * is completed.
  77. * @returns a promise which resolves when the column query for the data completes.
  78. */
  79. whenDataReady: function whenDataReady() {
  80. var _this = this;
  81. if (!this.isMappingComplete() || this.hasUnavailableMetadataColumns() || this.hasMissingFilters()) {
  82. return Promise.resolve({
  83. data: {}
  84. });
  85. }
  86. this._dataItemAPI = this.visualization.getSlots().getSlotList()[0].getDataItemList()[0];
  87. var queryExecution = this.content.getFeature('DataQueryExecution');
  88. return queryExecution.executeQueries().then(function (retData) {
  89. if (_this.filterIndicator) {
  90. _this.filterIndicator.update();
  91. }
  92. return {
  93. data: retData
  94. };
  95. });
  96. },
  97. getDescription: function getDescription() {
  98. // Append the F12 key instruction to the description
  99. var description = DataPlayerView.inherited('getDescription', this, arguments);
  100. return StringResources.get('WidgetLabelWithDescripion', {
  101. label: description,
  102. description: StringResources.get('f12KeyDescription')
  103. });
  104. },
  105. /**
  106. * @param {object} renderInfo - the renderInfo as passed from the render sequence.
  107. * @returns a promise which is resolved when the render is complete
  108. */
  109. render: function render(renderInfo) {
  110. if (!this.visModel || this.visModel.getRenderer() !== this.getRenderer()) {
  111. return Promise.resolve(this);
  112. }
  113. this.$_playPauseButton = $(this.visEl).find('.playOrPause');
  114. if (!this.isMappingComplete() || this.hasMissingFilters() || this.hasUnavailableMetadataColumns()) {
  115. this._getSliderContainer().empty();
  116. this.renderIconView();
  117. this.resizeToWidget(renderInfo);
  118. // render complete so fade in
  119. $(this.visEl).animate({
  120. opacity: 1
  121. });
  122. this.$_playPauseButton.hide();
  123. return Promise.resolve(this);
  124. }
  125. this.removeIconView();
  126. this.resizeToWidget(renderInfo);
  127. this.onResultsReady(renderInfo.data.getResult());
  128. this.$_playPauseButton.attr('aria-label', StringResources.get('playButtonLabel'));
  129. this.$_playPauseButton.show();
  130. this.ownerWidget.dashboardApi.on('open:sharePanel', this._onSharePanelOpen, this);
  131. this.ownerWidget.dashboardApi.on('close:sharePanel', this._onSharePanelClose, this);
  132. //NOTE: renderComplete is called from the base class visView.
  133. return DataPlayerView.inherited('render', this, arguments);
  134. },
  135. onExitContainer: function onExitContainer() {
  136. this.$_playPauseButton.attr('tabindex', '-1');
  137. this.$el.find('.sliderLabel.selected').attr('tabindex', '-1');
  138. },
  139. /**
  140. * Handle the on container entered event
  141. */
  142. onEnterContainer: function onEnterContainer() {
  143. this.$_playPauseButton.attr('tabindex', '0');
  144. this.$el.find('.sliderLabel.selected').attr('tabindex', '0');
  145. this.$_playPauseButton.focus();
  146. },
  147. onLabelKeypress: function onLabelKeypress(evt) {
  148. var $target = $(evt.currentTarget);
  149. var key = evt.keyCode;
  150. if (key === 37 || key === 38) {
  151. //left
  152. this._moveToItem($target, $target.prevAll('.sliderLabel').first());
  153. evt.stopPropagation();
  154. } else if (key === 39 || key === 40) {
  155. //right
  156. this._moveToItem($target, $target.nextAll('.sliderLabel').first());
  157. evt.stopPropagation();
  158. }
  159. },
  160. preventSelection: function preventSelection(evt) {
  161. evt.stopPropagation();
  162. },
  163. _moveToItem: function _moveToItem($current, $next) {
  164. $current.attr('tabindex', '-1');
  165. $next.click().attr('tabindex', '0').focus();
  166. },
  167. getFilterIndicatorSpec: function getFilterIndicatorSpec() {
  168. return {
  169. localFilters: true
  170. };
  171. },
  172. onResultsReady: function onResultsReady(resultData) {
  173. // clear up the content
  174. var dataItems = [];
  175. // data player has only one data item
  176. var rowSize = resultData.getResultItemList()[0].getRowCount();
  177. for (var i = 0; i < rowSize; i++) {
  178. dataItems.push(resultData.getValue(i, 0)[0]);
  179. }
  180. this._renderSlider(dataItems);
  181. },
  182. /**
  183. * Event handler for clicking or tapping on a slider tick.
  184. * @param event: Event object
  185. **/
  186. onTickClick: function onTickClick(event) {
  187. // get clicked item and set filter
  188. this._setFilterByEventTarget(event, 'sliderTickForHorizontal', false);
  189. // prevent click after a tap
  190. if (event.gesture) {
  191. event.gesture.preventDefault();
  192. }
  193. },
  194. /**
  195. * Event handler for clicking or tapping on a slider label.
  196. * @param event: Event object
  197. **/
  198. onLabelClick: function onLabelClick(event) {
  199. // get clicked item and either select or deselect
  200. this._setFilterByEventTarget(event, 'sliderLabel', true);
  201. },
  202. onPlayOrPauseKeypress: function onPlayOrPauseKeypress(event) {
  203. if (event.keyCode === 13 || event.keyCode === 32) {
  204. this.onPlayOrPauseClick(event);
  205. }
  206. },
  207. /**
  208. * Event handler for clicking or tapping on the play or pause button. It will
  209. * toggle the button between play and pause.
  210. * @param event: Event object
  211. **/
  212. onPlayOrPauseClick: function onPlayOrPauseClick(event) {
  213. // toggle playing mode
  214. if (this._isPlaying()) {
  215. this._pause();
  216. } else {
  217. this._play();
  218. }
  219. event.stopPropagation();
  220. },
  221. _onSharePanelOpen: function _onSharePanelOpen() {
  222. if (this._isPlaying()) {
  223. this._pause();
  224. this.shareState = { wasPlaying: true };
  225. }
  226. },
  227. _onSharePanelClose: function _onSharePanelClose() {
  228. if (this.shareState && this.shareState.wasPlaying) {
  229. this.shareState = null;
  230. this._play();
  231. }
  232. },
  233. /**
  234. * Event handler for the mouse or touch down on the slider node. This function
  235. * tracks the start position and starts the dragging logic.
  236. * @param event: Event object
  237. **/
  238. onSliderNodeMouseDown: function onSliderNodeMouseDown(event) {
  239. // stop playing (set flag for mouse up event to know playing was just terminated or not)
  240. this._wasJustPlaying = false;
  241. if (this._isPlaying()) {
  242. this._pause();
  243. this._wasJustPlaying = true;
  244. }
  245. // record mouse start position and virtual node position (need virtual position because of snap)
  246. this._mouseStartX = this._getPageX(event);
  247. this._mouseOrgStartX = this._mouseStartX;
  248. this._nodeVirtualPosition = this.$_sliderNode.position().left + this.$_sliderContainer.scrollLeft();
  249. // setup for dragging
  250. this.$_sliderContainer.on('mousemove touchmove', this.onSliderMouseMove.bind(this));
  251. this._dragging = true;
  252. // stop the event from being passed down to underlying elements
  253. event.stopPropagation();
  254. // prevent other browser events (like real mouse down if this is a touch event)
  255. event.preventDefault();
  256. },
  257. /**
  258. * Event handler for the mouse or touch up on the slider node and slider area.
  259. * This function processes the end of drag as well as determines if a deselect should happen.
  260. * @param event: event object
  261. **/
  262. onSliderMouseUp: function onSliderMouseUp(event) {
  263. // stop dragging as necessary
  264. if (this._dragging) {
  265. // turn off dragging
  266. this._dragging = false;
  267. // cancel the mouse move
  268. this.$_sliderContainer.off('mousemove touchmove');
  269. // check if has moved
  270. if (this._mouseOrgStartX !== this._mouseStartX) {
  271. // get nearest tick datavalue
  272. var snap = this._snapToTick(this._nodeVirtualPosition, true);
  273. // set filter
  274. this._setFilter(snap.index);
  275. } else {
  276. // clear the selection if not playing
  277. if (!this._wasJustPlaying) {
  278. this._clearSelection();
  279. }
  280. }
  281. // prevent other browser events (like real mouse up if this is a touch event)
  282. event.preventDefault();
  283. }
  284. },
  285. /**
  286. * Event handler for the mouse or touch move. This occurs on the slider area.
  287. * It moves the slider in the case of dragging and will snap to the nearest tick.
  288. * @param event: event object
  289. **/
  290. onSliderMouseMove: function onSliderMouseMove(event) {
  291. // calculate mouse delta X
  292. var pageX = this._getPageX(event);
  293. var deltaX = pageX - this._mouseStartX;
  294. // update mouse start position
  295. this._mouseStartX = pageX;
  296. // calculate new left positon
  297. var newLeft = this._nodeVirtualPosition + deltaX;
  298. // record position
  299. this._nodeVirtualPosition = newLeft;
  300. // snap to tick as necessary
  301. var snap = this._snapToTick(newLeft, false);
  302. // move the slider with the mouse but locked to track
  303. this.$_sliderNode.css('left', snap.left + 'px');
  304. // set the filter if a snap occurred
  305. if (snap.left !== newLeft) {
  306. this._selectLabel(snap.index);
  307. this._setFilter(snap.index);
  308. }
  309. // prevent other browser events (like real mouse down if this is a touch event)
  310. event.preventDefault();
  311. },
  312. /**
  313. * Listener function for the filter change event. This overrides the base one.
  314. * This function gets the current filter selection and moves the slider to the
  315. * corresponding location.
  316. **/
  317. onChangeFilter: function onChangeFilter() {
  318. //Call base class to process page context changes, requery etc.
  319. return DataPlayerView.inherited('onChangeFilter', this, arguments).then(function () {
  320. // watch for null column or dragging
  321. if (!this._dataItemAPI || this._dragging) {
  322. return;
  323. }
  324. // get the single filter selection
  325. var filter = this.getController().getSelectedValues(this._dataItemAPI.getColumnId());
  326. // get entry index bases on filter
  327. var dataItemIndex = -1;
  328. if (filter && filter.length > 0) {
  329. dataItemIndex = this._getEntryIndex(filter[0]);
  330. }
  331. // no need to reselect if already selected (in the case of multiple change events fired back to back)
  332. if (dataItemIndex !== this._currentSelectionIndex) {
  333. // store the new selection
  334. this._currentSelectionIndex = dataItemIndex;
  335. // position slider by value
  336. this._positionSlider(this._currentSelectionIndex, true, true);
  337. }
  338. }.bind(this));
  339. },
  340. /**
  341. * This function handles the rendering of the slider elements. It calculates
  342. * placement of ticks and labels while hidden and then animates a fade in to
  343. * show the control. It tracks data entries in a global array.
  344. * @param dataItems: Array of data results to populate the slider
  345. **/
  346. _renderSlider: function _renderSlider(dataItems) {
  347. // init and clear slider container
  348. this.$_sliderContainer = this._getSliderContainer();
  349. this.$_sliderContainer.attr('aria-label', StringResources.get('dataPlayerValueListLabel'));
  350. this.$_sliderContainer.empty();
  351. // sanity check for data
  352. if (!(dataItems && dataItems.length > 1)) {
  353. return;
  354. }
  355. // get container size
  356. var w = this.$_sliderContainer[0].clientWidth;
  357. var h = this.$_sliderContainer[0].clientHeight;
  358. // init the play button
  359. this._initPlayPauseButton(h);
  360. // calculate min length
  361. var barMinLength = this.tickMinDistance * (dataItems.length - 1);
  362. // bar length includes overflow front and end
  363. var barLength = Math.max(barMinLength, w - this.barOverflow * 2);
  364. // calculate the actual tick spacing
  365. var tickSpacing = barLength / (dataItems.length - 1);
  366. // position from top
  367. var barTop = Math.round(h * this.verticalPositionScaleFactor);
  368. // create the slider bar and position it
  369. this._createSliderBar(barTop, this.barOverflow / 2, barLength);
  370. // create the current position indicator
  371. this._createSliderNode();
  372. // calculate the baseline for the labels
  373. var labelBaseline = barTop - this.$_sliderNode[0].clientHeight / 2;
  374. // init vars for label positioning
  375. var aoLabels = [];
  376. // loop process results into player entries
  377. this.playerEntries.length = 0;
  378. var tickSizeCache = {};
  379. for (var i = 0; i < dataItems.length; i++) {
  380. // create new player entry
  381. var playerEntry = {
  382. index: i,
  383. value: dataItems[i],
  384. tick: this._createTick(i, barTop, tickSpacing, tickSizeCache)
  385. };
  386. // create new label
  387. aoLabels.push(this._createLabel(dataItems[i].label, i, playerEntry.tick.left + playerEntry.tick.width / 2, labelBaseline, i === 0 || i === dataItems.length - 1));
  388. // append entry object to collection
  389. this.playerEntries.push(playerEntry);
  390. }
  391. // reset scrollleft to the start. Some browsers seem to remember this setting and will scroll
  392. // to the place last left which is not necessarily correct
  393. this.$_sliderContainer.animate({
  394. scrollLeft: 0
  395. });
  396. // position labels (collision avoidance)
  397. this._positionLabels(aoLabels);
  398. // position slider silently to default on first tick or first filtered value
  399. var filter = this.getController().getSelectedValues(this._dataItemAPI.getColumnId());
  400. var dataIndex = -1;
  401. var showNode = false;
  402. if (filter && filter.length > 0) {
  403. dataIndex = this._getEntryIndex(filter[0]);
  404. dataIndex = dataIndex !== -1 ? dataIndex : 0;
  405. showNode = true;
  406. }
  407. this._currentSelectionIndex = dataIndex;
  408. this._positionSlider(dataIndex, false, showNode);
  409. // render complete so fade in
  410. $(this.visEl).animate({
  411. opacity: 1
  412. });
  413. },
  414. /**
  415. * This is a helper function that looks up and returns the data entry for
  416. * the passed in value.
  417. * @param dataItemValue: String - value of the data entry to look up
  418. * *@return object: Object - Data entry object
  419. **/
  420. _getPlayerEntry: function _getPlayerEntry(dataItemValue) {
  421. // init
  422. var entry = null;
  423. // search array for entry
  424. var searchResults = $.grep(this.playerEntries, function (entry) {
  425. return entry.value.value === dataItemValue;
  426. });
  427. // if found then continue
  428. if (searchResults.length > 0) {
  429. // get entry
  430. entry = searchResults[0];
  431. }
  432. // return
  433. return entry;
  434. },
  435. /**
  436. * This is a helper function that looks up and returns the index for a passed in value
  437. * @param dataItemValue: String - value of the data entry to look up
  438. * *@return Integer: The index of the entry or -1 by if not found.
  439. **/
  440. _getEntryIndex: function _getEntryIndex(dataItemValue) {
  441. // init
  442. var index = -1;
  443. var entry = this._getPlayerEntry(dataItemValue);
  444. // return index
  445. if (entry) {
  446. index = entry.index;
  447. }
  448. // return
  449. return index;
  450. },
  451. /**
  452. * This is a helper function to calculate the slider position based on the
  453. * given data index. It looks up the corresponding data entry and returns
  454. * the position object for the slider node.
  455. * @param dataItemValue: String - value of the data entry to look up
  456. * @return object: Object - position object for the slider node
  457. **/
  458. _calcSliderPosition: function _calcSliderPosition(dataItemIndex) {
  459. // init
  460. var oSliderPosition = {};
  461. // get entry
  462. var entry = this.playerEntries[dataItemIndex];
  463. if (entry) {
  464. oSliderPosition = this._getTickTopLeftPosition(entry);
  465. }
  466. // return
  467. return oSliderPosition;
  468. },
  469. _getTickTopLeftPosition: function _getTickTopLeftPosition(entry) {
  470. var position = {};
  471. // set slider top (need slight adjustment for visual alignment of circle)
  472. position.top = Math.round(entry.tick.top + entry.tick.height / 2 - this.$_sliderNode[0].clientHeight / 2);
  473. // set slider left
  474. position.left = Math.round(entry.tick.left + entry.tick.width / 2 - this.$_sliderNode[0].clientWidth / 2);
  475. return position;
  476. },
  477. /**
  478. * This function moves the slider to the tick for the corresponding passed in
  479. * data index. It can optionally be animated and made visible. This function
  480. * also sets the play index counter to the next tick down the line.
  481. * @param dataItemIndex: Integer - index of the data entry to look up
  482. * @param animate: Boolean - flag to indicate if the movement is
  483. * to be animated or not
  484. * @param showNode: Boolean - flag to indicate if the slider node
  485. * should be made visible
  486. **/
  487. _positionSlider: function _positionSlider(dataItemIndex, animate, showNode) {
  488. // if no dataItemIndex then no selection
  489. if (dataItemIndex < 0) {
  490. this.$_sliderNode.css('opacity', 0);
  491. this.$_sliderNode.css('display', 'none');
  492. this._selectLabel(-1);
  493. return;
  494. }
  495. // get scrollLeftPosition and container width
  496. var scrollLeftPos = this.$_sliderContainer.scrollLeft();
  497. var sliderViewPortWidth = this.$_sliderContainer[0].clientWidth;
  498. // get slider position
  499. var oSliderPosition = this._calcSliderPosition(dataItemIndex);
  500. // set top
  501. this.$_sliderNode.css('top', oSliderPosition.top + 'px');
  502. // scroll the div back if the node goes off the front or back of scroll
  503. if (oSliderPosition.left >= scrollLeftPos + sliderViewPortWidth - 10 || oSliderPosition.left < scrollLeftPos) {
  504. this.$_sliderContainer.animate({
  505. scrollLeft: oSliderPosition.left - this.barOverflow
  506. });
  507. }
  508. var styles = {
  509. left: oSliderPosition.left
  510. };
  511. // ensure the node is visible
  512. if (showNode) {
  513. styles.opacity = 1;
  514. styles.display = 'block';
  515. }
  516. // now move to new position
  517. if (animate) {
  518. this.$_sliderNode.animate(styles, 200);
  519. } else {
  520. this.$_sliderNode.css(styles);
  521. }
  522. // select the label
  523. if (showNode) {
  524. this._selectLabel(dataItemIndex);
  525. }
  526. // update the play index to the next one down the line (so that play starts there)
  527. this._playIndex = (dataItemIndex + 1) % this.playerEntries.length;
  528. },
  529. /**
  530. * This is a helper function to highlight the label that corresponds to the
  531. * passed in data index. This function will de-select allowToggle other labels.
  532. * @param dataItemIndex: Integer - value of the data entry to look up
  533. **/
  534. _selectLabel: function _selectLabel(dataItemIndex) {
  535. // remove all selections
  536. var $selected = $(this.visEl).find('.sliderLabel.selected');
  537. var tabindex = $selected.attr('tabindex');
  538. $selected.removeClass('selected');
  539. $selected.attr('tabindex', -1);
  540. $selected.attr('aria-selected', 'false');
  541. // select if index given
  542. if (dataItemIndex >= 0) {
  543. $(this.visEl).find('.sliderLabel[data-item-index="' + dataItemIndex + '"]').addClass('selected').attr('aria-selected', 'true').attr('tabindex', tabindex);
  544. }
  545. },
  546. /**
  547. * This is a helper function to position the labels passed in as an array.
  548. * The array of labels contains initial position information. Labels are
  549. * recursively processed and layered so that they do not collide with each other.
  550. * If a label does not fit, then it is moved to a new layer above the other labels.
  551. * @param aoLabels: Array - list of label objects inclusing positon info
  552. **/
  553. _positionLabels: function _positionLabels(aoLabels) {
  554. // check for empty
  555. if (aoLabels.length === 0) {
  556. return;
  557. }
  558. // create the label layers array (labels can be layered for collision avoidance)
  559. var aaLabelLayers = [aoLabels];
  560. // loop and process the layers array until all layers have been processed
  561. // layers will get added as collisions are detected
  562. for (var i = 0; i < aaLabelLayers.length; i++) {
  563. // process the last layer in the array
  564. this._processLabelLayers(aaLabelLayers);
  565. // now that the layer has been processed, positon the labels in it
  566. for (var j = 0; j < aaLabelLayers[i].length; j++) {
  567. var oLabel = aaLabelLayers[i][j];
  568. oLabel.$el.css('top', Math.max(-8, oLabel.labelBaseline - oLabel.height * (i + 1)) + 'px');
  569. oLabel.$el.css('left', oLabel.left + 'px');
  570. }
  571. // if the layer count exceeds the max then rotate the labels
  572. if (aaLabelLayers.length > this.maxLabelLayerCount) {
  573. // collapse all labels into a single array
  574. var aoRotateLabels = [];
  575. for (var k = 0; k < aaLabelLayers.length; k++) {
  576. $.merge(aoRotateLabels, aaLabelLayers[k]);
  577. }
  578. // rotate labels
  579. this._rotateLabels(aoRotateLabels);
  580. // no need to continue processing layers
  581. break;
  582. }
  583. }
  584. },
  585. /**
  586. * This is a helper function that processes a single layer of labels which is
  587. * taken from the first elemenrt of the array passed in. If collisions are
  588. * detected between elements of this array then those elements are pushed into
  589. * a new array list that is then appended to the passed in array for further
  590. * processing.
  591. * @param aaLabelLayers: Array - array of arrays to be processed. Only the
  592. * first element of the array is processed.
  593. **/
  594. _processLabelLayers: function _processLabelLayers(aaLabelLayers) {
  595. // init next layer if needed
  596. var aNewLabelLayer = [];
  597. var iPrevLabelEnd = -99999999;
  598. // process the last layer in the array
  599. var aoLabelLayer = aaLabelLayers[aaLabelLayers.length - 1];
  600. for (var i = 0; i < aoLabelLayer.length; i++) {
  601. // does this label's left collide with the previous end
  602. if (aoLabelLayer[i].left < iPrevLabelEnd) {
  603. // move this label to the next layer
  604. aNewLabelLayer = aNewLabelLayer.concat(aoLabelLayer.splice(i, 1));
  605. // array was altered so next element has replaced this one.
  606. i--;
  607. } else {
  608. // just record the ending point and move along
  609. iPrevLabelEnd = aoLabelLayer[i].right;
  610. }
  611. }
  612. // check if any labels were added to the new array and append as necessary
  613. if (aNewLabelLayer.length > 0) {
  614. aaLabelLayers.push(aNewLabelLayer);
  615. }
  616. },
  617. /**
  618. * This is a helper function that takes an array of labels and positions and rotates them.
  619. * @param aoRotateLabels: Array - list of labels to rotate and position
  620. **/
  621. _rotateLabels: function _rotateLabels(aoRotateLabels) {
  622. if (aoRotateLabels.length < 1) {
  623. return;
  624. }
  625. var widgetWidget = this.$_sliderContainer[0].scrollWidth || this.$_sliderContainer[0].clientWidth;
  626. // The top is the same for all the labels, so calculate it once
  627. var top = Math.max(-8, aoRotateLabels[0].labelBaseline - aoRotateLabels[0].height);
  628. // Since the labels are shown at a 45 degree angle, use Pythagorean theorem to calculate the maximum length
  629. var maxWidth = Math.max(25, Math.floor(Math.sqrt(top * top + top * top)));
  630. // loop and process all labels
  631. for (var i = 0; i < aoRotateLabels.length; i++) {
  632. // position the label and rotate (restore width for possoibly truncated labels)
  633. var $oLabel = aoRotateLabels[i].$el;
  634. $oLabel.css('top', top + 'px');
  635. $oLabel.css('left', aoRotateLabels[i].tickLeft + 'px');
  636. // If ever we don't have enough room left over to do a 45 degree triangle (top), then recalculate the max width. This hanppens
  637. // at the end of the data player, the last few labels would extend past the right edge and cause scrollbars
  638. if (widgetWidget - aoRotateLabels[i].tickLeft < top) {
  639. var roomLeft = widgetWidget - aoRotateLabels[i].tickLeft;
  640. $oLabel.css('width', Math.floor(Math.sqrt(roomLeft * roomLeft + roomLeft * roomLeft)) + 'px');
  641. } else {
  642. $oLabel.css('width', maxWidth + 'px');
  643. }
  644. $oLabel.addClass('rotated');
  645. }
  646. },
  647. /**
  648. * This function takes the given index and sets a filter accordingly.
  649. * It will clear all existing filters before setting the new one.
  650. * @param index: Integer - index of value to look up
  651. **/
  652. _setFilter: function _setFilter(index) {
  653. // Clear any tooltips or other toolbars when we change the data to avoid stale info in our view
  654. this.ownerWidget.dashboardApi.triggerDashboardEvent('widget:hideToolbar');
  655. var value = this.playerEntries[index];
  656. this.getController().select({
  657. itemIds: [this._dataItemAPI.getColumnId()],
  658. tuple: [EventUtils.toDeprecatedPayload(value.value)],
  659. command: 'update',
  660. slotsToClear: this.visualization.getSlots().getMappedSlotList()
  661. });
  662. },
  663. /**
  664. * This is a helper function that sets a filter by event and target.
  665. * @param event: Event - event object
  666. * @param targetName: String - target to filter on
  667. * @param allowToggle: Boolean - flag to indicate if the filter should
  668. * toggle if already set
  669. **/
  670. _setFilterByEventTarget: function _setFilterByEventTarget(event, targetName, allowToggle) {
  671. // get clicked item and either select or deselect
  672. var el = this.getTarget(event.target, targetName);
  673. var index = el.getAttribute('data-item-index');
  674. if (index) {
  675. // make sure the index is an integer
  676. index = parseInt(index, 10);
  677. // select if not selected
  678. if (index !== this._currentSelectionIndex) {
  679. // immediately show label as selected
  680. this._selectLabel(index);
  681. // set filer
  682. this._setFilter(index);
  683. } else {
  684. // deselect
  685. if (allowToggle && !this._isPlaying()) {
  686. this._clearSelection();
  687. }
  688. }
  689. }
  690. },
  691. /**
  692. * This function initiates play mode.
  693. **/
  694. _play: function _play() {
  695. // unpause
  696. this.$_playPauseButton.removeClass('paused');
  697. // start playing
  698. this._playIteration();
  699. },
  700. /**
  701. * This function stops playback.
  702. **/
  703. _pause: function _pause() {
  704. // clear timer
  705. this._clearPlayTimer();
  706. // pause
  707. this.$_playPauseButton.addClass('paused');
  708. },
  709. /**
  710. * Helper function to determine if control is currently playing.
  711. * @return Boolean - True if playing, false if not.
  712. **/
  713. _isPlaying: function _isPlaying() {
  714. return !this.$_playPauseButton.hasClass('paused');
  715. },
  716. /**
  717. * This function conducts the playing operation and gets executed on a timer.
  718. **/
  719. _playIteration: function _playIteration() {
  720. // clear timer
  721. this._clearPlayTimer();
  722. // sanity check for data
  723. if (!(this._dataItemAPI && this.playerEntries && this.playerEntries.length)) {
  724. return;
  725. }
  726. // when slider position is not set, start playing with index 0 instead of -1;
  727. if (this._playIndex === -1) {
  728. this._playIndex = 0;
  729. }
  730. // set global filter
  731. this._setFilter(this._playIndex, true);
  732. // trigger next iteration
  733. this._playTimeout = window.setTimeout(this._playIteration.bind(this), this.playerSpeed);
  734. },
  735. /**
  736. * Helper function to access the slider container.
  737. **/
  738. _getSliderContainer: function _getSliderContainer() {
  739. return $(this.visEl).find('.slider');
  740. },
  741. /**
  742. * Helper function to reset the timer.
  743. **/
  744. _clearPlayTimer: function _clearPlayTimer() {
  745. if (this._playTimeout) {
  746. window.clearTimeout(this._playTimeout);
  747. this._playTimout = null;
  748. }
  749. },
  750. /**
  751. * This function clears all filters and selections.
  752. **/
  753. _clearSelection: function _clearSelection() {
  754. if (this._currentSelectionIndex !== -1) {
  755. this.getController().select({
  756. itemIds: this._dataItemAPI.getColumnId(),
  757. tuple: EventUtils.toDeprecatedPayload(this.playerEntries[this._currentSelectionIndex].value),
  758. command: 'remove',
  759. slotsToClear: this.visualization.getSlots().getMappedSlotList()
  760. });
  761. }
  762. // clear the slider
  763. this._positionSlider(-1);
  764. // clear current selection
  765. this._selectLabel(-1);
  766. this._currentSelectionIndex = -1;
  767. },
  768. /**
  769. * This function takes in the left position of the slider and determines if
  770. * it is in range to snap to a tick. It will return a new left position.
  771. * @param leftPosition: Integer - the left position of the slider node
  772. * @param splitThreshold: Boolean - flag to indicate if the threshold is
  773. * pre-defined or split between ticks.
  774. * @return Object: Integer position and tick index value
  775. **/
  776. _snapToTick: function _snapToTick(leftPosition, splitThreshold) {
  777. // init
  778. var centeredLeftPos = leftPosition + this.$_sliderNode[0].clientWidth / 2;
  779. var newLeft = leftPosition;
  780. var index = -1;
  781. // clear current selection so that it will snap correctly
  782. this._currentSelectionIndex = -1;
  783. // threshold is either pre-defined or is split halfway between ticks
  784. var threshold = this._snapThreshold;
  785. if (splitThreshold) {
  786. threshold = null;
  787. }
  788. // loop through collect and determine either snap to tick, limit to start, or limit to end
  789. for (var i = 0; i < this.playerEntries.length; i++) {
  790. // set threshold as necessary
  791. if (!threshold) {
  792. threshold = this.playerEntries[i].tick.spacing / 2;
  793. }
  794. // get tick center
  795. var tickCenterLeft = this.playerEntries[i].tick.left + this.playerEntries[i].tick.width / 2;
  796. // get snap left and snap right
  797. var snapLeft = centeredLeftPos <= tickCenterLeft + threshold;
  798. var snapRight = centeredLeftPos >= tickCenterLeft - threshold;
  799. // check if first or last entry and if snap is needed
  800. var isFirstAndSnap = i === 0 && snapLeft;
  801. var isLastAndSnap = i === this.playerEntries.length - 1 && snapRight;
  802. // check limits and snap to tick
  803. if (isFirstAndSnap || isLastAndSnap || snapLeft && snapRight) {
  804. // calculate new left & record value
  805. newLeft = this._calcSliderPosition(i).left;
  806. index = i;
  807. break;
  808. }
  809. }
  810. // return
  811. return {
  812. left: Math.round(newLeft),
  813. index: index
  814. };
  815. },
  816. /**
  817. * Helper function to create the slider bar and append it to the DOM.
  818. * @param barTop: Integer - top position
  819. * @param barLeft: Integer - left position
  820. * @param barLength: Integer - length
  821. **/
  822. _createSliderBar: function _createSliderBar(barTop, barLeft, barLength) {
  823. this.$_sliderBar = $('<div class="sliderBarHorizontal"></div>');
  824. this.$_sliderContainer.append(this.$_sliderBar);
  825. this.$_sliderBar.css('top', barTop + 'px');
  826. this.$_sliderBar.css('left', barLeft + 'px');
  827. this.$_sliderBar.css('width', barLength + 'px');
  828. },
  829. /**
  830. * Helper function to create the slider node and append it to the DOM. It is
  831. * drawn using SVG instead of the icon font because of precise pixel level
  832. * positioning differences across browsers.
  833. **/
  834. _createSliderNode: function _createSliderNode() {
  835. this.$_sliderNode = $('<div class="sliderNode"><svg class="sliderSymbol" height="18" width="18"><circle cx="10" cy="10" r="8" fill-opacity="1"/></svg></div>');
  836. this.$_sliderContainer.append(this.$_sliderNode);
  837. },
  838. /**
  839. * Helper function to position the play/pause button.
  840. * @param containerHeight: Integer - container height
  841. **/
  842. _initPlayPauseButton: function _initPlayPauseButton(containerHeight) {
  843. // get button
  844. this.$_playPauseButton = $(this.visEl).find('.playOrPause');
  845. // position
  846. this.$_playPauseButton.css('top', Math.round(containerHeight * this.verticalPositionScaleFactor - 28) + 'px');
  847. },
  848. /**
  849. * This function creates an individual tick and appends it to the DOM. It
  850. * returns a tick object to be used in the global entries list.
  851. * @param index: Integer - position index (zero based)
  852. * @param barTop: Integer - top position of bar
  853. * @param tickSpacing: Integer - distance between ticks
  854. * @param sizeCache Object - simple cache for width and hight of ticks
  855. * based on constant width and constant height
  856. * @return object: Tick object
  857. **/
  858. _createTick: function _createTick(index, barTop, tickSpacing, sizeCache) {
  859. // create new tick
  860. var $tick = this.$tick.clone();
  861. this.$_sliderNode.before($tick);
  862. var tickClientWidth;
  863. if (sizeCache.w) {
  864. tickClientWidth = sizeCache.w;
  865. } else {
  866. tickClientWidth = $tick[0].clientWidth;
  867. sizeCache.w = tickClientWidth;
  868. }
  869. var tickClientHeight;
  870. if (sizeCache.h) {
  871. tickClientHeight = sizeCache.h;
  872. } else {
  873. tickClientHeight = $tick[0].clientHeight;
  874. sizeCache.h = tickClientHeight;
  875. }
  876. // set further attributes
  877. $tick.attr('data-item-index', index);
  878. $tick.css('height', tickClientHeight + 'px');
  879. var tickTop = barTop - Math.round(tickClientHeight / 2);
  880. var tickLeft = Math.round(this.barOverflow / 2 + tickSpacing * index - tickClientWidth / 2);
  881. $tick.css('top', tickTop + 'px');
  882. $tick.css('left', tickLeft + 'px');
  883. // return tick object
  884. return {
  885. top: tickTop,
  886. left: tickLeft,
  887. width: tickClientWidth,
  888. height: tickClientHeight,
  889. spacing: tickSpacing
  890. };
  891. },
  892. /**
  893. * This function creates a label and appends it to the DOM. It returns a
  894. * tick object to be used in positioning.
  895. * @param value: String - data value
  896. * @param index: Integer - position index (zero based)
  897. * @param tickLeft: Integer - left position
  898. * @param labelBaseline: Integer - vertical bottom of labels
  899. * @param isFirstOrLastLabel: Boolean - flag to indicate if first of last
  900. * label to be processed.
  901. * @return object: Label object
  902. **/
  903. _createLabel: function _createLabel(value, index, tickLeft, labelBaseline, isFirstOrLastLabel) {
  904. // format the text for the label
  905. var label = Formatter.format(value, this._dataItemAPI.getFormat());
  906. // create the label object
  907. var $label = this.$label.clone();
  908. $label.attr('title', label);
  909. $label.attr('role', 'option');
  910. $label.attr('tabindex', '-1');
  911. $label.text(label);
  912. this.$_sliderNode.before($label);
  913. $label.attr('data-item-index', index);
  914. // calculate label width and truncate the first and last labels as necessary
  915. var labelWidth = $label[0].clientWidth;
  916. var actualWidth = labelWidth;
  917. if (isFirstOrLastLabel) {
  918. var truncLength = this.barOverflow * 2 - 10; // the 10 is a fudge factor
  919. if (labelWidth > truncLength) {
  920. labelWidth = Math.round(truncLength);
  921. $label.css('width', labelWidth + 'px');
  922. }
  923. }
  924. // calculate the rest of the label info and store in array to be used for positioning
  925. var labelHeight = $label[0].clientHeight;
  926. var labelLeft = Math.round(tickLeft - labelWidth / 2);
  927. var labelRight = labelLeft + labelWidth;
  928. if (labelLeft < 0) {
  929. //if the left is off the screen, set it to 0, but add the difference to the right.
  930. labelRight += 0 - labelLeft;
  931. labelLeft = 0;
  932. }
  933. return {
  934. $el: $label,
  935. left: labelLeft,
  936. right: labelRight,
  937. width: labelWidth,
  938. actualWidth: actualWidth,
  939. height: labelHeight,
  940. labelBaseline: labelBaseline,
  941. tickLeft: tickLeft
  942. };
  943. },
  944. /**
  945. * Helper function to get the pageX position between a touch event and regular mouse event.
  946. * @param event: event object
  947. **/
  948. _getPageX: function _getPageX(event) {
  949. return event.type.substr(0, 5) !== 'touch' ? event.pageX : event.originalEvent.touches[0].pageX;
  950. }
  951. });
  952. return DataPlayerView;
  953. });
  954. //# sourceMappingURL=DataPlayerView.js.map