TimelineTimeIndicatorView.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2018
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. */
  7. define(['baglass/core-client/js/core-client/ui/core/View', 'jquery', 'underscore', 'gemini/app/util/ScreenReaderUtil', 'storytelling/nls/StringResources', 'text!./templates/TimeIndicator.html'], function (View, $, _, ScreenReaderUtil, stringResources, Template) {
  8. var TimelineTimeIndicatorView = View.extend({
  9. templateString: Template,
  10. _allowDrag: true,
  11. _isDragging: false,
  12. _dragInfo: null,
  13. _seekRefreshInterval: 200,
  14. _seekRefreshTimer: -1,
  15. _scrollEventHandler: null,
  16. lastScroll: 0,
  17. init: function init(options) {
  18. TimelineTimeIndicatorView.inherited('init', this, arguments);
  19. this.controller = options.controller;
  20. this.controller.on('time:update', this.onTimeUpdated, this);
  21. this.scaleManager = options.scaleManager;
  22. this.scaleManager.on('scale:change', this.onScaleChanged, this);
  23. this._ScreenReader = new ScreenReaderUtil();
  24. this._callOut = _.debounce(this._updateAriaLabel.bind(this), options.callOutDelay || 600);
  25. this._indicatorOnly = options.indicatorOnly === true;
  26. if (this._indicatorOnly && typeof options.progressBarCallback === 'function') {
  27. this.progressBarCallback = options.progressBarCallback;
  28. }
  29. },
  30. remove: function remove() {
  31. if (this.$handle) {
  32. this.$handle.off('focus touchstart touchend mousedown mouseup keydown');
  33. this.$handle.hammer().off('dragstart dragleft dragright dragend');
  34. this.$handle = null;
  35. }
  36. if (this.controller) {
  37. this.controller.off('time:update', this.onTimeUpdated, this);
  38. }
  39. if (this.scaleManager) {
  40. this.scaleManager.off('scale:change', this.onScaleChanged, this);
  41. }
  42. TimelineTimeIndicatorView.inherited('remove', this, arguments);
  43. },
  44. /**
  45. * Renders the film strip
  46. *
  47. * @returns
  48. */
  49. render: function render() {
  50. var sHtml = this.dotTemplate({
  51. allInfo: !this._indicatorOnly,
  52. handleLabel: stringResources.get('timelinePositionIndicator', { position: this.getCurrentTime() })
  53. });
  54. this.$el.html(sHtml);
  55. this.$handle = this.$el.find('div.handle');
  56. if (!this._indicatorOnly) {
  57. this.$handleValue = this.$handle.find('div:nth-of-type(1)');
  58. this.$handleArrow = this.$handle.find('div:nth-of-type(2)');
  59. }
  60. this.$handle.on('focus', this.handleTimelinePositionIndicatorMove.bind(this)).on('touchstart', this.onTouchStart.bind(this)).on('touchend', this.onTouchEnd.bind(this)).on('mousedown', this.onTouchStart.bind(this)).on('mouseup', this.onTouchEnd.bind(this)).on('keydown', this._onKeyDown.bind(this));
  61. this.$handle.hammer().on('dragstart', this.onDragStart.bind(this)).on('dragleft', this.onDragLeftRight.bind(this)).on('dragright', this.onDragLeftRight.bind(this)).on('dragend', this.onDragEnd.bind(this));
  62. // Set the initial position.
  63. this._moveIndicatorWithTime(this.controller.getCurrentTime());
  64. return this;
  65. },
  66. setFocus: function setFocus() {
  67. this.$handle.focus();
  68. },
  69. getCurrentTime: function getCurrentTime() {
  70. return this.controller.getTimeLabel(this.controller.getCurrentTime(), true);
  71. },
  72. getEndTime: function getEndTime() {
  73. return this.controller.getTimeLabel(this.controller.getDuration(), true);
  74. },
  75. onScroll: function onScroll(event) {
  76. this.lastScroll = event.scrollLeft;
  77. this.refresh();
  78. },
  79. refresh: function refresh() {
  80. this._moveIndicatorWithTime(this.controller.getCursorTime());
  81. },
  82. /*
  83. * View events
  84. */
  85. onTouchStart: function onTouchStart() {
  86. this.$el.addClass('dragging');
  87. this.wasPlaying = this.controller.isPlaying();
  88. // Pause when interacting with the indicator.
  89. if (this.wasPlaying) {
  90. this.controller.pause();
  91. }
  92. },
  93. onTouchEnd: function onTouchEnd() {
  94. this.$el.removeClass('dragging');
  95. // Start playing if we were originally playing before the drag.
  96. if (this.wasPlaying) {
  97. this.wasPlaying = false;
  98. this.controller.play();
  99. }
  100. },
  101. onDragStart: function onDragStart(event) {
  102. event.gesture.preventDefault();
  103. this.$el.addClass('dragging');
  104. this._setDragStartInfo();
  105. },
  106. onDragLeftRight: function onDragLeftRight(event) {
  107. event.gesture.preventDefault();
  108. var position = this._getBoundedPosition(event.gesture.deltaX);
  109. this._moveIndicatorWithPosition(position);
  110. },
  111. onDragEnd: function onDragEnd(event) {
  112. event.gesture.preventDefault();
  113. this._clearInterval();
  114. this.$el.removeClass('dragging');
  115. this._isDragging = false;
  116. var position = this._getBoundedPosition(event.gesture.deltaX);
  117. this._moveIndicatorWithPosition(position);
  118. this._updateTimeFromDrag();
  119. this._dragInfo = null;
  120. },
  121. _onKeyDown: function _onKeyDown(evt) {
  122. if (this._shouldScrub(evt)) {
  123. // Arrow key with no shift key
  124. this._onArrowKeyPress(evt);
  125. } else if (this._shouldJumpToNextMarker(evt)) {
  126. // Shift + (Left or Up) when parent view is the timeline or navigate markers is enabled
  127. this.controller.jumpToNextMarker();
  128. this.handlePositionMoveRight();
  129. evt.stopPropagation();
  130. } else if (this._shouldJumpToPreviousMarker(evt)) {
  131. // Shift + (Right or Down) when parent view is the timeline or navigate markers is enabled
  132. this.controller.jumpToPreviousMarker();
  133. this.handlePositionMoveLeft();
  134. evt.stopPropagation();
  135. } else if (this._shouldJumpToStartOfScene(evt)) {
  136. // Shift + (Left or Up) when parent view is the progress bar
  137. this.controller.setCurrentTime(0);
  138. this._updateAriaLabel('sceneStart');
  139. evt.stopPropagation();
  140. } else if (this._shouldJumpToEndOfScene(evt)) {
  141. // Shift + (Right or Down) when parent view is the progress bar
  142. this.controller.setCurrentTime(this.controller.getDuration());
  143. this._updateAriaLabel('sceneEnd');
  144. evt.stopPropagation();
  145. } else if (this._shouldPerformTogglePlayPause(evt)) {
  146. // Spacebar when parent view is the progress bar or the timeline
  147. this.controller.eventRouter.trigger('playback:togglePlayPause');
  148. evt.stopPropagation();
  149. }
  150. },
  151. // Left or Right with no shift key
  152. _shouldScrub: function _shouldScrub(evt) {
  153. return !evt.shiftKey && [37, 38, 39, 40].indexOf(evt.keyCode) !== -1;
  154. },
  155. // Shift + (Left or Up) when parent view is the timeline
  156. _shouldJumpToPreviousMarker: function _shouldJumpToPreviousMarker(evt) {
  157. var isTimeline = this.$el.hasClass('timelinePosition');
  158. return (isTimeline || this.controller.isNavigateMarkers()) && evt.shiftKey && [37, 38].indexOf(evt.keyCode) !== -1;
  159. },
  160. // Shift + (Right or Down) when parent view is the timeline
  161. _shouldJumpToNextMarker: function _shouldJumpToNextMarker(evt) {
  162. var isTimeline = this.$el.hasClass('timelinePosition');
  163. return (isTimeline || this.controller.isNavigateMarkers()) && evt.shiftKey && [39, 40].indexOf(evt.keyCode) !== -1;
  164. },
  165. // Shift + (Left or Up) when parent view is the progress bar
  166. _shouldJumpToStartOfScene: function _shouldJumpToStartOfScene(evt) {
  167. var isProgressBar = this.$el.hasClass('progressBarPosition');
  168. return isProgressBar && evt.shiftKey && [37, 38].indexOf(evt.keyCode) !== -1;
  169. },
  170. // Shift + (Right or Down) when parent view is the progress bar
  171. _shouldJumpToEndOfScene: function _shouldJumpToEndOfScene(evt) {
  172. var isProgressBar = this.$el.hasClass('progressBarPosition');
  173. return isProgressBar && evt.shiftKey && [39, 40].indexOf(evt.keyCode) !== -1;
  174. },
  175. // Space with parent view timeline or progress bar
  176. _shouldPerformTogglePlayPause: function _shouldPerformTogglePlayPause(evt) {
  177. return evt.keyCode === 32;
  178. },
  179. _onArrowKeyPress: function _onArrowKeyPress(evt) {
  180. var $target = $(evt.currentTarget);
  181. // Set as selected
  182. $target.focus();
  183. // The minimum we can move is 1 tick.
  184. // we are free to scale it with shift/ctrl later.
  185. var moveByDelta = this.controller.getTickDuration();
  186. switch (evt.keyCode) {
  187. // left key
  188. case 37:
  189. case 38:
  190. moveByDelta *= -1;
  191. break;
  192. // right key
  193. case 39:
  194. case 40:
  195. break;
  196. default:
  197. return;
  198. }
  199. evt.stopPropagation();
  200. evt.preventDefault();
  201. this.controller.setCurrentTime(this.controller.getCurrentTime() + moveByDelta);
  202. if (moveByDelta < 0) {
  203. this._callOut('timelinePositionIndicatorMoveLeftTo');
  204. } else {
  205. this._callOut('timelinePositionIndicatorMoveRightTo');
  206. }
  207. this._dragInfo = null;
  208. },
  209. /*
  210. * Controller events.
  211. */
  212. onScaleChanged: function onScaleChanged() {
  213. this._moveIndicatorWithTime(this.controller.getCurrentTime());
  214. },
  215. onTimeUpdated: function onTimeUpdated(event) {
  216. if (!this._isDragging) {
  217. this._moveIndicatorWithTime(event.currentTime);
  218. }
  219. },
  220. /*
  221. * Helpers
  222. */
  223. // Public Helpers
  224. handleTimelinePositionIndicatorMove: function handleTimelinePositionIndicatorMove() {
  225. this._callOut('timelinePositionIndicator');
  226. },
  227. handlePositionMoveLeft: function handlePositionMoveLeft() {
  228. var time = this.getCurrentTime();
  229. var translationKey = time === '0:00.0' ? 'sceneStart' : 'timelinePositionIndicatorMoveLeftTo';
  230. this._callOut(translationKey);
  231. },
  232. handlePositionMoveRight: function handlePositionMoveRight() {
  233. var translationKey = this.controller.isAtEndOfScene() ? 'sceneEnd' : 'timelinePositionIndicatorMoveRightTo';
  234. this._callOut(translationKey);
  235. },
  236. // Private Helpers
  237. _setDragStartInfo: function _setDragStartInfo() {
  238. this._isDragging = true;
  239. this._dragInfo = {};
  240. this._dragInfo.initialScroll = this.lastScroll;
  241. this._dragInfo.left = this.$el.position().left + this.lastScroll;
  242. this._dragInfo.currentLeft = this._dragInfo.left;
  243. this._clearInterval();
  244. this._seekRefreshTimer = setInterval(this._updateTimeFromDrag.bind(this), this._seekRefreshInterval);
  245. },
  246. _updateTimeFromDrag: function _updateTimeFromDrag() {
  247. // Calculate the time based on the position of the indicator
  248. var time = this.scaleManager.convertPositionToTime(this._dragInfo.currentLeft);
  249. this.controller.setCurrentTime(time);
  250. },
  251. _moveIndicatorWithTime: function _moveIndicatorWithTime(endTime) {
  252. var position = this.scaleManager.convertTimeToPosition(endTime);
  253. this._moveIndicatorWithPosition(position);
  254. },
  255. _moveIndicatorWithPosition: function _moveIndicatorWithPosition(position) {
  256. if (!this.$handle) {
  257. return;
  258. }
  259. if (!this._indicatorOnly) {
  260. //round to the nearest tick. This is the minimum amount we can deal with.
  261. var time = this.scaleManager.convertPositionToTime(position);
  262. var tick = this.controller.getTickDuration();
  263. time = Math.round(time / tick) * tick;
  264. position = this.scaleManager.convertTimeToPosition(time);
  265. }
  266. var leftVal = void 0;
  267. var percent = void 0;
  268. if (this._indicatorOnly) {
  269. var maxPos = this._getMaxPosition();
  270. if (maxPos === 0) {
  271. percent = 0;
  272. } else {
  273. percent = position / maxPos * 100;
  274. }
  275. leftVal = 'calc(' + percent + '% - ' + this.lastScroll + 'px)';
  276. } else {
  277. leftVal = position - this.lastScroll + 'px';
  278. }
  279. this.$el.css({
  280. 'left': leftVal
  281. });
  282. this._moveHelper(position);
  283. if (this._indicatorOnly && this.progressBarCallback) {
  284. this.progressBarCallback(percent);
  285. }
  286. },
  287. _moveHelper: function _moveHelper(position) {
  288. var halfWidth = this.$handle.outerWidth(false) / 2;
  289. var margin = position - halfWidth < 0 ? position : halfWidth;
  290. var marginLeft = Math.max(0, margin - 7);
  291. var borderLeftWidth = Math.max(0, Math.min(7, margin));
  292. // Get the cursor time.
  293. var time = this.scaleManager.convertPositionToTime(position);
  294. // _moveHelper is expensive (the two css() calls below) and gets called A LOT, so if nothing is changed, return immediately
  295. if (margin === this._prevMargin && marginLeft === this._preMarginLeft && borderLeftWidth === this._prevBorderLeftWidth && time === this._prevTime) {
  296. return;
  297. }
  298. this._preMarginLeft = marginLeft;
  299. this._prevMargin = margin;
  300. this._prevBorderLeftWidth = borderLeftWidth;
  301. this._prevTime = time;
  302. if (!this._indicatorOnly) {
  303. this.$handle.css('margin-left', -margin + 'px');
  304. this.$handleArrow.css({
  305. 'margin-left': marginLeft + 'px',
  306. 'border-left-width': borderLeftWidth + 'px'
  307. });
  308. // Update the cursor label.
  309. this._updateTimeLabel(time);
  310. }
  311. if (this._dragInfo) {
  312. this._dragInfo.currentLeft = position;
  313. }
  314. // Update the controller.
  315. this.controller.setCursorTime(time);
  316. },
  317. _getMaxPosition: function _getMaxPosition() {
  318. return this.scaleManager.convertTimeToPosition(this.controller.getDuration());
  319. },
  320. _getCursorPosition: function _getCursorPosition(delta) {
  321. return this._dragInfo.left + delta + this.lastScroll - this._dragInfo.initialScroll;
  322. },
  323. _getBoundedPosition: function _getBoundedPosition(delta) {
  324. return Math.max(0, Math.min(this._getCursorPosition(delta), this._getMaxPosition()));
  325. },
  326. _updateTimeLabel: function _updateTimeLabel(time) {
  327. this.$handleValue.text(this.controller.getTimeLabel(time, 1), true);
  328. },
  329. _updateAriaLabel: function _updateAriaLabel(msg) {
  330. if (this.$handle) {
  331. var sMessage = stringResources.get(msg, { position: this.getCurrentTime() });
  332. this.$handle.attr({ 'aria-label': sMessage });
  333. this._ScreenReader.callOut(sMessage);
  334. }
  335. },
  336. _clearInterval: function _clearInterval() {
  337. if (this._seekRefreshTimer >= 0) {
  338. clearInterval(this._seekRefreshTimer);
  339. this._seekRefreshTimer = -1;
  340. }
  341. }
  342. });
  343. return TimelineTimeIndicatorView;
  344. });
  345. //# sourceMappingURL=TimelineTimeIndicatorView.js.map