TimelineSliderContentView.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: Storytelling
  5. * (C) Copyright IBM Corp. 2014, 2020
  6. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  7. */
  8. define(['baglass/core-client/js/core-client/ui/core/View', 'jquery', 'underscore', 'gemini/app/util/ScreenReaderUtil', 'baglass/core-client/js/core-client/utils/ContentFormatter', 'text!./templates/TimelineSliderContentView.html', 'baglass/core-client/js/core-client/utils/dom-utils', 'baglass/core-client/js/core-client/utils/Utils', '../lib/@ba-ui-toolkit/ba-graphics/dist/icons-js/resize-panel_16'], function (View, $, _, ScreenReaderUtil, ContentFormatter, template, DomUtils, Utils, resizePanelIcon) {
  9. var TimelineSliderContentView = View.extend({
  10. templateString: template,
  11. //should match _endBufferTime in TimeQueue
  12. _endBufferTime: 200,
  13. dragValue: null,
  14. value: null,
  15. min: 0,
  16. max: 0,
  17. scale: 1,
  18. title: null,
  19. notifyChangedInterval: 200,
  20. draggingElement: null,
  21. events: {},
  22. init: function init(options) {
  23. TimelineSliderContentView.inherited('init', this, arguments);
  24. this.id = options.id;
  25. this.min = options.min || 0;
  26. this.max = options.max || 0;
  27. this.value = options.value;
  28. this.scale = options.scale || 1;
  29. this.title = options.title;
  30. this.dndManager = options.dndManager;
  31. this.cssClassSelector = options.cssClassSelector;
  32. this.stringResources = options.services.getSvcSync('.StringResources');
  33. this.timelineController = options.timelineController;
  34. // convert to seconds
  35. this._minimumDuration = this._endBufferTime / 1000;
  36. this._ScreenReader = new ScreenReaderUtil();
  37. },
  38. render: function render() {
  39. var leftHandleLabel = this.title ? this.stringResources.get('timelineLeftHandle', { name: this.title }) : this.stringResources.get('timelineLeftHandleNoTitle');
  40. var rightHandleLabel = this.title ? this.stringResources.get('timelineRightHandle', { name: this.title }) : this.stringResources.get('timelineRightHandleNoTitle');
  41. var sHtml = this.dotTemplate({
  42. id: this.id,
  43. leftHandleLabel: leftHandleLabel,
  44. rightHandleLabel: rightHandleLabel
  45. });
  46. this.$el.html(sHtml);
  47. this.$highlightsEl = this.$el.find('.highlights-container');
  48. this.$selection = this.$el.find('.slider-selection');
  49. this.$handles = this.$selection.find('.handle');
  50. this.$content = this.$selection.find('.content');
  51. this.$content = this.$el.find('.content');
  52. this.$title = this.$content.find('.title');
  53. Utils.setIcon(this.$el.find('.svgHandleIcon'), resizePanelIcon.default.id);
  54. var isTouch = this._isTouchMode();
  55. this._toggleDragEvents(!isTouch);
  56. if (isTouch) {
  57. this._addHoldEvents();
  58. } else {
  59. this._addKeyDownEvents();
  60. }
  61. this._addClickEvents();
  62. this._update();
  63. this._updateTitle();
  64. return this;
  65. },
  66. setTitle: function setTitle(title) {
  67. this.title = title;
  68. this._updateTitle();
  69. },
  70. /** Sets the title and middle-shortens it if the title is too long for the container. */
  71. middleShortenTitle: function middleShortenTitle(title) {
  72. this.setTitle(title);
  73. ContentFormatter.middleShortenString(this.$title[0]);
  74. },
  75. setScale: function setScale(scale) {
  76. this.scale = scale;
  77. this._update();
  78. },
  79. setValue: function setValue(value, preventNotify) {
  80. this.value = value;
  81. if (this.$content) {
  82. this._update();
  83. if (!preventNotify) {
  84. this._notifyValueChanged();
  85. }
  86. }
  87. },
  88. getValue: function getValue() {
  89. return this.value;
  90. },
  91. getDragValue: function getDragValue() {
  92. return this.dragValue;
  93. },
  94. setMax: function setMax(max) {
  95. this.max = max;
  96. },
  97. toggleSelected: function toggleSelected(toggle) {
  98. this.$el.toggleClass('selected', toggle);
  99. if (this._isTouchMode()) {
  100. this._toggleDragEvents(this.isSelected());
  101. }
  102. },
  103. isSelected: function isSelected() {
  104. return this.$el.hasClass('selected');
  105. },
  106. move: function move(value) {
  107. this._move(value[0], value[1] - value[0]);
  108. },
  109. /*
  110. * View events.
  111. */
  112. onHold: function onHold(event) {
  113. this._select(event);
  114. },
  115. onClick: function onClick(event) {
  116. this._select(event);
  117. },
  118. /*
  119. * Helpers.
  120. */
  121. _startArrowKeyMove: function _startArrowKeyMove() {
  122. this.dragValue = this.value;
  123. this._notifyValueChangeBegin();
  124. },
  125. _select: function _select(event) {
  126. this.toggleSelected(true);
  127. this.trigger('dragSlider:select', event);
  128. },
  129. _move: function _move(start, length, visualOnly) {
  130. // on playback we move forward by tick duration each time.
  131. // when play on click is enabled it shows in certain cases.
  132. // to resolve this display issue we round down to the nearest tick.
  133. // (We need the value in seconds)
  134. var tick = this.timelineController.getTickDuration() / 1000;
  135. var round = function round(value) {
  136. return Math.round(value / tick) * tick;
  137. };
  138. start = round(start);
  139. length = round(length);
  140. // Boundary check the values.
  141. if (start < 0) {
  142. start = 0;
  143. }
  144. if (length < this._minimumDuration) {
  145. length = this._minimumDuration;
  146. }
  147. var properties = {
  148. left: start * this.scale + 'px',
  149. width: length * this.scale + 'px'
  150. };
  151. this.$selection.css(properties);
  152. var value = [start, start + length];
  153. if (!visualOnly) {
  154. this.value = value;
  155. }
  156. var timelineMax = this._roundValueForScreenReader(this.$el.width() / this.scale);
  157. this.$handles.eq(0).attr('aria-valuenow', this._roundValueForScreenReader(value[0]));
  158. this.$handles.eq(0).attr('aria-valuemin', 0);
  159. this.$handles.eq(0).attr('aria-valuemax', this._roundValueForScreenReader(value[1] - this._minimumDuration));
  160. this.$handles.eq(1).attr('aria-valuenow', this._roundValueForScreenReader(value[1]));
  161. this.$handles.eq(1).attr('aria-valuemin', this._roundValueForScreenReader(value[0] + this._minimumDuration));
  162. this.$handles.eq(1).attr('aria-valuemax', timelineMax);
  163. this.$content.attr('aria-valuenow', this._roundValueForScreenReader((value[0] + value[1]) / 2));
  164. this.$content.attr('aria-valuemin', 0);
  165. this.$content.attr('aria-valuemax', timelineMax);
  166. return value;
  167. },
  168. _update: function _update() {
  169. this.move(this.value);
  170. },
  171. _updateTitle: function _updateTitle() {
  172. var leftHandleLabel = this.title ? this.stringResources.get('timelineLeftHandle', { name: this.title }) : this.stringResources.get('timelineLeftHandleNoTitle');
  173. var rightHandleLabel = this.title ? this.stringResources.get('timelineRightHandle', { name: this.title }) : this.stringResources.get('timelineRightHandleNoTitle');
  174. this.$title.text(this.title);
  175. this.$handles.eq(0).attr('aria-label', leftHandleLabel);
  176. this.$handles.eq(1).attr('aria-label', rightHandleLabel);
  177. },
  178. _notifyValueChangeBegin: function _notifyValueChangeBegin() {
  179. this.trigger('value:willChange');
  180. },
  181. _notifyValueChanged: function _notifyValueChanged(extraOptions) {
  182. this.trigger('value:changed', _.extend({
  183. value: this.value,
  184. min: this.min,
  185. max: this.max
  186. }, extraOptions));
  187. },
  188. _addHoldEvents: function _addHoldEvents() {
  189. var handler = this.onHold.bind(this);
  190. this.$handles.hammer().on('hold', handler);
  191. this.$content.hammer().on('hold', handler);
  192. },
  193. _addClickEvents: function _addClickEvents() {
  194. var mouseDownHandler = function () {
  195. this.isDragging = false;
  196. }.bind(this);
  197. var clickHandler = function (event) {
  198. if (!this.isDragging) {
  199. this.onClick(event);
  200. }
  201. }.bind(this);
  202. this.$handles.on('mousedown', mouseDownHandler);
  203. this.$content.on('mousedown', mouseDownHandler);
  204. this.$handles.on('click', clickHandler);
  205. this.$content.on('click', clickHandler);
  206. },
  207. _addKeyDownEvents: function _addKeyDownEvents() {
  208. this.$handles.on('keydown', this._onKeyPress.bind(this));
  209. this.$content.on('keydown', this._onKeyPress.bind(this));
  210. },
  211. _toggleDragEvents: function _toggleDragEvents(toggle) {
  212. this._toggleDragEvent(this.$handles.eq(0), toggle, this._leftHandleDragHandler);
  213. this._toggleDragEvent(this.$handles.eq(1), toggle, this._rightHandleDragHandler);
  214. this._toggleDragEvent(this.$content, toggle, this._contentDragHandler);
  215. },
  216. _toggleDragEvent: function _toggleDragEvent($node, enable, handler) {
  217. if (enable) {
  218. $node.hammer().on('dragstart', this._dragHandler.bind(this, handler));
  219. } else {
  220. $node.hammer().off('dragstart');
  221. }
  222. },
  223. _dragHandler: function _dragHandler(wrapped, event) {
  224. this.trigger('dragSlider:dragStarted');
  225. var extraOptions = {
  226. payloadData: {
  227. undoRedoTransactionId: _.uniqueId('dragSlider')
  228. }
  229. };
  230. var config = {
  231. type: 'timelineSlider',
  232. data: {
  233. slider: this
  234. },
  235. restrictToXAxis: true,
  236. avatar: null,
  237. event: event,
  238. callerCallbacks: {
  239. onDragStart: function (event, options) {
  240. $('body').addClass('overflowHidden');
  241. this.dragValue = this.value;
  242. if (wrapped.onDragStart) {
  243. wrapped.onDragStart.bind(this)(event, options);
  244. }
  245. this._notifyValueChangeBegin();
  246. }.bind(this),
  247. onMove: function (event, options) {
  248. if (wrapped.onMove) {
  249. wrapped.onMove.bind(this)(event, options);
  250. }
  251. }.bind(this),
  252. onDragDone: function (event, options) {
  253. if (wrapped.onDragDone) {
  254. wrapped.onDragDone.bind(this)(event, _.extend({}, options, extraOptions));
  255. }
  256. if (this.dragValue) {
  257. this._move(this.dragValue[0], this.dragValue[1] - this.dragValue[0]);
  258. this.dragValue = null;
  259. this._notifyValueChanged(extraOptions);
  260. }
  261. this.draggingElement = null;
  262. $('body').removeClass('overflowHidden');
  263. this._select(event);
  264. }.bind(this)
  265. }
  266. };
  267. if (wrapped.getConfig) {
  268. var wrapperConfig = wrapped.getConfig.bind(this)(event);
  269. _.extend(config, wrapperConfig);
  270. }
  271. this.dndManager.startDrag(config);
  272. },
  273. _leftHandleDragHandler: {
  274. onDragStart: function onDragStart() {
  275. this.draggingElement = 'leftHandle';
  276. this.isDragging = true;
  277. },
  278. onMove: function onMove(event, options) {
  279. var deltaX = options.dragObject.position.x - options.dragObject.startPosition.x;
  280. var x = deltaX / this.scale;
  281. var start = x + this.value[0];
  282. var length = this.value[1] - this.value[0] - x;
  283. if (start < 0) {
  284. length += start;
  285. } else if (start + this._minimumDuration > this.value[1]) {
  286. start = this.value[1] - this._minimumDuration;
  287. }
  288. this.dragValue = this._move(start, length, true);
  289. }
  290. },
  291. _rightHandleDragHandler: {
  292. onDragStart: function onDragStart() {
  293. this.draggingElement = 'rightHandle';
  294. this.isDragging = true;
  295. },
  296. onMove: function onMove(event, options) {
  297. var deltaX = options.dragObject.position.x - options.dragObject.startPosition.x;
  298. this.dragValue = this._move(this.value[0], deltaX / this.scale + (this.value[1] - this.value[0]), true);
  299. }
  300. },
  301. _contentDragHandler: {
  302. getConfig: function getConfig(event) {
  303. var $avatar = $('<div>').addClass('dragAvatar timelineContent bringToFront selected').hide();
  304. $('body').append($avatar);
  305. $avatar.append(this.$selection.clone());
  306. if (this.cssClassSelector) {
  307. $avatar.append(this.$el.find('.' + this.cssClassSelector).clone());
  308. }
  309. var offset = this.$selection.offset();
  310. var eventPos = DomUtils.getEventPos(event);
  311. var avatarYOffset = offset.top - eventPos.pageY;
  312. var avatarXOffset = offset.left - eventPos.pageX - this.value[0] * this.scale;
  313. return {
  314. type: 'dragSliderContent',
  315. dragLockToAxis: true,
  316. restrictToXAxis: false,
  317. restrictToYAxis: false,
  318. moveXThreshold: 20,
  319. moveYThreshold: 20,
  320. data: {
  321. slider: this,
  322. avatar: $avatar
  323. },
  324. avatar: $avatar[0],
  325. avatarXOffset: avatarXOffset,
  326. avatarYOffset: avatarYOffset
  327. };
  328. },
  329. onDragStart: function onDragStart(event, options) {
  330. this.draggingElement = 'content';
  331. // show the avatar and hide the real slider
  332. options.dragObject.data.avatar.show();
  333. if (this.cssClassSelector) {
  334. this.$cssClassSelector = this.$el.find('.' + this.cssClassSelector);
  335. this.$cssClassSelector.hide();
  336. }
  337. this.$selection.hide();
  338. this.isDragging = true;
  339. },
  340. onMove: function onMove(event, options) {
  341. var deltaX = options.dragObject.position.x - options.dragObject.startPosition.x;
  342. this.dragValue = this._move(deltaX / this.scale + this.value[0], this.value[1] - this.value[0], true);
  343. },
  344. onDragDone: function onDragDone(event, options) {
  345. // restore the slider and hide the avatar
  346. this.$selection.show();
  347. if (this.cssClassSelector) {
  348. this.$cssClassSelector.show();
  349. }
  350. options.dragObject.data.avatar.hide();
  351. this.trigger('dragSlider:isDropped', _.extend({ id: this.id }, options));
  352. }
  353. },
  354. _onKeyPress: function _onKeyPress(evt) {
  355. var $target = $(evt.currentTarget);
  356. var isLeftHandle = $target.hasClass('lefthandle');
  357. var isRightHandle = $target.hasClass('righthandle');
  358. // Select the selection with white space or return key
  359. if (evt.keyCode === 13 || evt.keyCode === 32) {
  360. this._select(evt);
  361. $target.toggleClass('active', isLeftHandle || isRightHandle);
  362. $target.focus();
  363. return;
  364. }
  365. var messageOptions = {
  366. title: this.title
  367. };
  368. // Set moveby amount and message for move
  369. var xMoveByDelta = 0;
  370. var yMoveByDelta = 0;
  371. var sMessage = '';
  372. switch (evt.keyCode) {
  373. case 9:
  374. // Tab
  375. $target.toggleClass('active', false);
  376. return;
  377. case 37:
  378. // Left arrow
  379. xMoveByDelta = -0.1;
  380. break;
  381. case 39:
  382. // Right arrow
  383. xMoveByDelta = 0.1;
  384. break;
  385. case 38:
  386. // up arrow
  387. if (isLeftHandle || isRightHandle) {
  388. //handles can't be moved up
  389. evt.stopPropagation();
  390. evt.preventDefault();
  391. return;
  392. }
  393. yMoveByDelta = -1;
  394. break;
  395. case 40:
  396. //down arrow
  397. if (isLeftHandle || isRightHandle) {
  398. //handles can't be moved down
  399. evt.stopPropagation();
  400. evt.preventDefault();
  401. return;
  402. }
  403. yMoveByDelta = 1;
  404. break;
  405. default:
  406. return;
  407. }
  408. evt.stopPropagation();
  409. evt.preventDefault();
  410. $target.toggleClass('active', isLeftHandle || isRightHandle);
  411. // Start arrow key move
  412. this._startArrowKeyMove();
  413. if (isLeftHandle) {
  414. var start = xMoveByDelta + this.value[0];
  415. var length = this.value[1] - this.value[0] - xMoveByDelta;
  416. if (start < 0) {
  417. length += start;
  418. } else if (start > this.value[1]) {
  419. start = this.value[1];
  420. }
  421. this.dragValue = this._move(start, length, true);
  422. } else if (isRightHandle) {
  423. this.dragValue = this._move(this.value[0], xMoveByDelta + (this.value[1] - this.value[0]), true);
  424. } else {
  425. if (xMoveByDelta != 0) {
  426. this.dragValue = this._move(xMoveByDelta + this.value[0], this.value[1] - this.value[0], true);
  427. } else {
  428. var options = {
  429. id: this.id,
  430. payloadData: {
  431. undoRedoTransactionId: _.uniqueId('dragSlider')
  432. }
  433. };
  434. var event = yMoveByDelta > 0 ? 'dragSlider:movingDown' : 'dragSlider:movingUp';
  435. this.trigger(event, options);
  436. this.trigger('dragSlider:isDropped', options);
  437. }
  438. }
  439. // End move update
  440. if (this.dragValue) {
  441. messageOptions.newStartTime = this._roundValueForScreenReader(this.dragValue[0]);
  442. messageOptions.newEndTime = this._roundValueForScreenReader(this.dragValue[1]);
  443. this._move(this.dragValue[0], this.dragValue[1] - this.dragValue[0]);
  444. this.dragValue = null;
  445. this._notifyValueChanged();
  446. }
  447. if (isLeftHandle) {
  448. messageOptions.resourceName = 'timelineMoveWidgetStartTime';
  449. } else if (isRightHandle) {
  450. messageOptions.resourceName = 'timelineMoveWidgetEndTime';
  451. } else if (yMoveByDelta < 0) {
  452. messageOptions.resourceName = 'timelineMoveWidgetUp';
  453. } else if (yMoveByDelta > 0) {
  454. messageOptions.resourceName = 'timelineMoveWidgetDown';
  455. } else {
  456. messageOptions.resourceName = 'timelineMoveWidget';
  457. }
  458. // update the move
  459. this._update();
  460. sMessage = this.stringResources.get(messageOptions.resourceName, messageOptions);
  461. // update screenreader input
  462. this._ScreenReader.callOut(sMessage);
  463. },
  464. _roundValueForScreenReader: function _roundValueForScreenReader(value) {
  465. return Math.round(value * 100) / 100;
  466. },
  467. _isTouchMode: function _isTouchMode() {
  468. return 'ontouchstart' in document.documentElement;
  469. }
  470. });
  471. return TimelineSliderContentView;
  472. });
  473. //# sourceMappingURL=TimelineSliderContentView.js.map