'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2013, 2020 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. * * DataPlayerView */ define(['jquery', 'underscore', '../../../util/DashboardFormatter', '../VisView', '../../../widgets/livewidget/nls/StringResources', 'text!./DataPlayerView.html', '../../../util/EventUtils'], function ($, _, Formatter, VisView, StringResources, template, EventUtils) { 'use strict'; var DataPlayerView = null; DataPlayerView = VisView.extend({ // constants templateString: template, playerSpeed: 1600, tickMinDistance: 44, maxLabelLayerCount: 2, verticalPositionScaleFactor: 2 / 3, barOverflow: 60, // references to JQuery selected elements $_sliderNode: null, $_sliderBar: null, $_sliderContainer: null, $_playPauseButton: null, _dataItemAPI: null, _playTimeout: null, _playIndex: -1, _currentSelectionIndex: -1, _mouseStartX: 0, _mouseOrgStartX: 0, _nodeVirtualPosition: 0, _snapThreshold: 15, _dragging: false, $tick: $('
'), $label: $(''), events: { 'clicktap .sliderLabel': 'onLabelClick', 'keydown .sliderLabel': 'onLabelKeypress', 'click .sliderTickForHorizontal': 'onTickClick', 'tap .sliderTickForHorizontal': 'onTickClick', 'touchstart .sliderNode': 'onSliderNodeMouseDown', 'touchend .slider': 'onSliderMouseUp', 'mousedown .sliderNode': 'onSliderNodeMouseDown', 'mouseup .slider': 'onSliderMouseUp', 'clicktap .playOrPause': 'onPlayOrPauseClick', 'mousedown .playOrPause': 'preventSelection', 'keydown .playOrPause': 'onPlayOrPauseKeypress' }, getRenderer: function getRenderer() { return 'dashboard-analytics/visualizations/renderer/dataplayer/DataPlayerRenderer'; }, init: function init() { DataPlayerView.inherited('init', this, arguments); if (!this.visModel) { throw 'Invalid VisModel reference.'; } var iconsFeature = this.dashboardApi.getFeature('Icons'); var playIcon = iconsFeature.getIcon('playIcon'); var pauseIcon = iconsFeature.getIcon('pauseIcon'); $(this.el).find('.playOrPause').append(''); this.el.style.position = 'relative'; // main list of player entries this.playerEntries = []; }, remove: function remove() { // clear timer this._clearPlayTimer(); this.ownerWidget.dashboardApi.off('open:sharePanel', this._onSharePanelOpen, this); this.ownerWidget.dashboardApi.off('close:sharePanel', this._onSharePanelClose, this); // call overridden super class DataPlayerView.inherited('remove', this, arguments); }, /** * RENDERING SEQUENCE: * For dataPlayer, whenDataReady will resolve when a column query for the column of interest * is completed. * @returns a promise which resolves when the column query for the data completes. */ whenDataReady: function whenDataReady() { var _this = this; if (!this.isMappingComplete() || this.hasUnavailableMetadataColumns() || this.hasMissingFilters()) { return Promise.resolve({ data: {} }); } this._dataItemAPI = this.visualization.getSlots().getSlotList()[0].getDataItemList()[0]; var queryExecution = this.content.getFeature('DataQueryExecution'); return queryExecution.executeQueries().then(function (retData) { if (_this.filterIndicator) { _this.filterIndicator.update(); } return { data: retData }; }); }, getDescription: function getDescription() { // Append the F12 key instruction to the description var description = DataPlayerView.inherited('getDescription', this, arguments); return StringResources.get('WidgetLabelWithDescripion', { label: description, description: StringResources.get('f12KeyDescription') }); }, /** * @param {object} renderInfo - the renderInfo as passed from the render sequence. * @returns a promise which is resolved when the render is complete */ render: function render(renderInfo) { if (!this.visModel || this.visModel.getRenderer() !== this.getRenderer()) { return Promise.resolve(this); } this.$_playPauseButton = $(this.visEl).find('.playOrPause'); if (!this.isMappingComplete() || this.hasMissingFilters() || this.hasUnavailableMetadataColumns()) { this._getSliderContainer().empty(); this.renderIconView(); this.resizeToWidget(renderInfo); // render complete so fade in $(this.visEl).animate({ opacity: 1 }); this.$_playPauseButton.hide(); return Promise.resolve(this); } this.removeIconView(); this.resizeToWidget(renderInfo); this.onResultsReady(renderInfo.data.getResult()); this.$_playPauseButton.attr('aria-label', StringResources.get('playButtonLabel')); this.$_playPauseButton.show(); this.ownerWidget.dashboardApi.on('open:sharePanel', this._onSharePanelOpen, this); this.ownerWidget.dashboardApi.on('close:sharePanel', this._onSharePanelClose, this); //NOTE: renderComplete is called from the base class visView. return DataPlayerView.inherited('render', this, arguments); }, onExitContainer: function onExitContainer() { this.$_playPauseButton.attr('tabindex', '-1'); this.$el.find('.sliderLabel.selected').attr('tabindex', '-1'); }, /** * Handle the on container entered event */ onEnterContainer: function onEnterContainer() { this.$_playPauseButton.attr('tabindex', '0'); this.$el.find('.sliderLabel.selected').attr('tabindex', '0'); this.$_playPauseButton.focus(); }, onLabelKeypress: function onLabelKeypress(evt) { var $target = $(evt.currentTarget); var key = evt.keyCode; if (key === 37 || key === 38) { //left this._moveToItem($target, $target.prevAll('.sliderLabel').first()); evt.stopPropagation(); } else if (key === 39 || key === 40) { //right this._moveToItem($target, $target.nextAll('.sliderLabel').first()); evt.stopPropagation(); } }, preventSelection: function preventSelection(evt) { evt.stopPropagation(); }, _moveToItem: function _moveToItem($current, $next) { $current.attr('tabindex', '-1'); $next.click().attr('tabindex', '0').focus(); }, getFilterIndicatorSpec: function getFilterIndicatorSpec() { return { localFilters: true }; }, onResultsReady: function onResultsReady(resultData) { // clear up the content var dataItems = []; // data player has only one data item var rowSize = resultData.getResultItemList()[0].getRowCount(); for (var i = 0; i < rowSize; i++) { dataItems.push(resultData.getValue(i, 0)[0]); } this._renderSlider(dataItems); }, /** * Event handler for clicking or tapping on a slider tick. * @param event: Event object **/ onTickClick: function onTickClick(event) { // get clicked item and set filter this._setFilterByEventTarget(event, 'sliderTickForHorizontal', false); // prevent click after a tap if (event.gesture) { event.gesture.preventDefault(); } }, /** * Event handler for clicking or tapping on a slider label. * @param event: Event object **/ onLabelClick: function onLabelClick(event) { // get clicked item and either select or deselect this._setFilterByEventTarget(event, 'sliderLabel', true); }, onPlayOrPauseKeypress: function onPlayOrPauseKeypress(event) { if (event.keyCode === 13 || event.keyCode === 32) { this.onPlayOrPauseClick(event); } }, /** * Event handler for clicking or tapping on the play or pause button. It will * toggle the button between play and pause. * @param event: Event object **/ onPlayOrPauseClick: function onPlayOrPauseClick(event) { // toggle playing mode if (this._isPlaying()) { this._pause(); } else { this._play(); } event.stopPropagation(); }, _onSharePanelOpen: function _onSharePanelOpen() { if (this._isPlaying()) { this._pause(); this.shareState = { wasPlaying: true }; } }, _onSharePanelClose: function _onSharePanelClose() { if (this.shareState && this.shareState.wasPlaying) { this.shareState = null; this._play(); } }, /** * Event handler for the mouse or touch down on the slider node. This function * tracks the start position and starts the dragging logic. * @param event: Event object **/ onSliderNodeMouseDown: function onSliderNodeMouseDown(event) { // stop playing (set flag for mouse up event to know playing was just terminated or not) this._wasJustPlaying = false; if (this._isPlaying()) { this._pause(); this._wasJustPlaying = true; } // record mouse start position and virtual node position (need virtual position because of snap) this._mouseStartX = this._getPageX(event); this._mouseOrgStartX = this._mouseStartX; this._nodeVirtualPosition = this.$_sliderNode.position().left + this.$_sliderContainer.scrollLeft(); // setup for dragging this.$_sliderContainer.on('mousemove touchmove', this.onSliderMouseMove.bind(this)); this._dragging = true; // stop the event from being passed down to underlying elements event.stopPropagation(); // prevent other browser events (like real mouse down if this is a touch event) event.preventDefault(); }, /** * Event handler for the mouse or touch up on the slider node and slider area. * This function processes the end of drag as well as determines if a deselect should happen. * @param event: event object **/ onSliderMouseUp: function onSliderMouseUp(event) { // stop dragging as necessary if (this._dragging) { // turn off dragging this._dragging = false; // cancel the mouse move this.$_sliderContainer.off('mousemove touchmove'); // check if has moved if (this._mouseOrgStartX !== this._mouseStartX) { // get nearest tick datavalue var snap = this._snapToTick(this._nodeVirtualPosition, true); // set filter this._setFilter(snap.index); } else { // clear the selection if not playing if (!this._wasJustPlaying) { this._clearSelection(); } } // prevent other browser events (like real mouse up if this is a touch event) event.preventDefault(); } }, /** * Event handler for the mouse or touch move. This occurs on the slider area. * It moves the slider in the case of dragging and will snap to the nearest tick. * @param event: event object **/ onSliderMouseMove: function onSliderMouseMove(event) { // calculate mouse delta X var pageX = this._getPageX(event); var deltaX = pageX - this._mouseStartX; // update mouse start position this._mouseStartX = pageX; // calculate new left positon var newLeft = this._nodeVirtualPosition + deltaX; // record position this._nodeVirtualPosition = newLeft; // snap to tick as necessary var snap = this._snapToTick(newLeft, false); // move the slider with the mouse but locked to track this.$_sliderNode.css('left', snap.left + 'px'); // set the filter if a snap occurred if (snap.left !== newLeft) { this._selectLabel(snap.index); this._setFilter(snap.index); } // prevent other browser events (like real mouse down if this is a touch event) event.preventDefault(); }, /** * Listener function for the filter change event. This overrides the base one. * This function gets the current filter selection and moves the slider to the * corresponding location. **/ onChangeFilter: function onChangeFilter() { //Call base class to process page context changes, requery etc. return DataPlayerView.inherited('onChangeFilter', this, arguments).then(function () { // watch for null column or dragging if (!this._dataItemAPI || this._dragging) { return; } // get the single filter selection var filter = this.getController().getSelectedValues(this._dataItemAPI.getColumnId()); // get entry index bases on filter var dataItemIndex = -1; if (filter && filter.length > 0) { dataItemIndex = this._getEntryIndex(filter[0]); } // no need to reselect if already selected (in the case of multiple change events fired back to back) if (dataItemIndex !== this._currentSelectionIndex) { // store the new selection this._currentSelectionIndex = dataItemIndex; // position slider by value this._positionSlider(this._currentSelectionIndex, true, true); } }.bind(this)); }, /** * This function handles the rendering of the slider elements. It calculates * placement of ticks and labels while hidden and then animates a fade in to * show the control. It tracks data entries in a global array. * @param dataItems: Array of data results to populate the slider **/ _renderSlider: function _renderSlider(dataItems) { // init and clear slider container this.$_sliderContainer = this._getSliderContainer(); this.$_sliderContainer.attr('aria-label', StringResources.get('dataPlayerValueListLabel')); this.$_sliderContainer.empty(); // sanity check for data if (!(dataItems && dataItems.length > 1)) { return; } // get container size var w = this.$_sliderContainer[0].clientWidth; var h = this.$_sliderContainer[0].clientHeight; // init the play button this._initPlayPauseButton(h); // calculate min length var barMinLength = this.tickMinDistance * (dataItems.length - 1); // bar length includes overflow front and end var barLength = Math.max(barMinLength, w - this.barOverflow * 2); // calculate the actual tick spacing var tickSpacing = barLength / (dataItems.length - 1); // position from top var barTop = Math.round(h * this.verticalPositionScaleFactor); // create the slider bar and position it this._createSliderBar(barTop, this.barOverflow / 2, barLength); // create the current position indicator this._createSliderNode(); // calculate the baseline for the labels var labelBaseline = barTop - this.$_sliderNode[0].clientHeight / 2; // init vars for label positioning var aoLabels = []; // loop process results into player entries this.playerEntries.length = 0; var tickSizeCache = {}; for (var i = 0; i < dataItems.length; i++) { // create new player entry var playerEntry = { index: i, value: dataItems[i], tick: this._createTick(i, barTop, tickSpacing, tickSizeCache) }; // create new label aoLabels.push(this._createLabel(dataItems[i].label, i, playerEntry.tick.left + playerEntry.tick.width / 2, labelBaseline, i === 0 || i === dataItems.length - 1)); // append entry object to collection this.playerEntries.push(playerEntry); } // reset scrollleft to the start. Some browsers seem to remember this setting and will scroll // to the place last left which is not necessarily correct this.$_sliderContainer.animate({ scrollLeft: 0 }); // position labels (collision avoidance) this._positionLabels(aoLabels); // position slider silently to default on first tick or first filtered value var filter = this.getController().getSelectedValues(this._dataItemAPI.getColumnId()); var dataIndex = -1; var showNode = false; if (filter && filter.length > 0) { dataIndex = this._getEntryIndex(filter[0]); dataIndex = dataIndex !== -1 ? dataIndex : 0; showNode = true; } this._currentSelectionIndex = dataIndex; this._positionSlider(dataIndex, false, showNode); // render complete so fade in $(this.visEl).animate({ opacity: 1 }); }, /** * This is a helper function that looks up and returns the data entry for * the passed in value. * @param dataItemValue: String - value of the data entry to look up * *@return object: Object - Data entry object **/ _getPlayerEntry: function _getPlayerEntry(dataItemValue) { // init var entry = null; // search array for entry var searchResults = $.grep(this.playerEntries, function (entry) { return entry.value.value === dataItemValue; }); // if found then continue if (searchResults.length > 0) { // get entry entry = searchResults[0]; } // return return entry; }, /** * This is a helper function that looks up and returns the index for a passed in value * @param dataItemValue: String - value of the data entry to look up * *@return Integer: The index of the entry or -1 by if not found. **/ _getEntryIndex: function _getEntryIndex(dataItemValue) { // init var index = -1; var entry = this._getPlayerEntry(dataItemValue); // return index if (entry) { index = entry.index; } // return return index; }, /** * This is a helper function to calculate the slider position based on the * given data index. It looks up the corresponding data entry and returns * the position object for the slider node. * @param dataItemValue: String - value of the data entry to look up * @return object: Object - position object for the slider node **/ _calcSliderPosition: function _calcSliderPosition(dataItemIndex) { // init var oSliderPosition = {}; // get entry var entry = this.playerEntries[dataItemIndex]; if (entry) { oSliderPosition = this._getTickTopLeftPosition(entry); } // return return oSliderPosition; }, _getTickTopLeftPosition: function _getTickTopLeftPosition(entry) { var position = {}; // set slider top (need slight adjustment for visual alignment of circle) position.top = Math.round(entry.tick.top + entry.tick.height / 2 - this.$_sliderNode[0].clientHeight / 2); // set slider left position.left = Math.round(entry.tick.left + entry.tick.width / 2 - this.$_sliderNode[0].clientWidth / 2); return position; }, /** * This function moves the slider to the tick for the corresponding passed in * data index. It can optionally be animated and made visible. This function * also sets the play index counter to the next tick down the line. * @param dataItemIndex: Integer - index of the data entry to look up * @param animate: Boolean - flag to indicate if the movement is * to be animated or not * @param showNode: Boolean - flag to indicate if the slider node * should be made visible **/ _positionSlider: function _positionSlider(dataItemIndex, animate, showNode) { // if no dataItemIndex then no selection if (dataItemIndex < 0) { this.$_sliderNode.css('opacity', 0); this.$_sliderNode.css('display', 'none'); this._selectLabel(-1); return; } // get scrollLeftPosition and container width var scrollLeftPos = this.$_sliderContainer.scrollLeft(); var sliderViewPortWidth = this.$_sliderContainer[0].clientWidth; // get slider position var oSliderPosition = this._calcSliderPosition(dataItemIndex); // set top this.$_sliderNode.css('top', oSliderPosition.top + 'px'); // scroll the div back if the node goes off the front or back of scroll if (oSliderPosition.left >= scrollLeftPos + sliderViewPortWidth - 10 || oSliderPosition.left < scrollLeftPos) { this.$_sliderContainer.animate({ scrollLeft: oSliderPosition.left - this.barOverflow }); } var styles = { left: oSliderPosition.left }; // ensure the node is visible if (showNode) { styles.opacity = 1; styles.display = 'block'; } // now move to new position if (animate) { this.$_sliderNode.animate(styles, 200); } else { this.$_sliderNode.css(styles); } // select the label if (showNode) { this._selectLabel(dataItemIndex); } // update the play index to the next one down the line (so that play starts there) this._playIndex = (dataItemIndex + 1) % this.playerEntries.length; }, /** * This is a helper function to highlight the label that corresponds to the * passed in data index. This function will de-select allowToggle other labels. * @param dataItemIndex: Integer - value of the data entry to look up **/ _selectLabel: function _selectLabel(dataItemIndex) { // remove all selections var $selected = $(this.visEl).find('.sliderLabel.selected'); var tabindex = $selected.attr('tabindex'); $selected.removeClass('selected'); $selected.attr('tabindex', -1); $selected.attr('aria-selected', 'false'); // select if index given if (dataItemIndex >= 0) { $(this.visEl).find('.sliderLabel[data-item-index="' + dataItemIndex + '"]').addClass('selected').attr('aria-selected', 'true').attr('tabindex', tabindex); } }, /** * This is a helper function to position the labels passed in as an array. * The array of labels contains initial position information. Labels are * recursively processed and layered so that they do not collide with each other. * If a label does not fit, then it is moved to a new layer above the other labels. * @param aoLabels: Array - list of label objects inclusing positon info **/ _positionLabels: function _positionLabels(aoLabels) { // check for empty if (aoLabels.length === 0) { return; } // create the label layers array (labels can be layered for collision avoidance) var aaLabelLayers = [aoLabels]; // loop and process the layers array until all layers have been processed // layers will get added as collisions are detected for (var i = 0; i < aaLabelLayers.length; i++) { // process the last layer in the array this._processLabelLayers(aaLabelLayers); // now that the layer has been processed, positon the labels in it for (var j = 0; j < aaLabelLayers[i].length; j++) { var oLabel = aaLabelLayers[i][j]; oLabel.$el.css('top', Math.max(-8, oLabel.labelBaseline - oLabel.height * (i + 1)) + 'px'); oLabel.$el.css('left', oLabel.left + 'px'); } // if the layer count exceeds the max then rotate the labels if (aaLabelLayers.length > this.maxLabelLayerCount) { // collapse all labels into a single array var aoRotateLabels = []; for (var k = 0; k < aaLabelLayers.length; k++) { $.merge(aoRotateLabels, aaLabelLayers[k]); } // rotate labels this._rotateLabels(aoRotateLabels); // no need to continue processing layers break; } } }, /** * This is a helper function that processes a single layer of labels which is * taken from the first elemenrt of the array passed in. If collisions are * detected between elements of this array then those elements are pushed into * a new array list that is then appended to the passed in array for further * processing. * @param aaLabelLayers: Array - array of arrays to be processed. Only the * first element of the array is processed. **/ _processLabelLayers: function _processLabelLayers(aaLabelLayers) { // init next layer if needed var aNewLabelLayer = []; var iPrevLabelEnd = -99999999; // process the last layer in the array var aoLabelLayer = aaLabelLayers[aaLabelLayers.length - 1]; for (var i = 0; i < aoLabelLayer.length; i++) { // does this label's left collide with the previous end if (aoLabelLayer[i].left < iPrevLabelEnd) { // move this label to the next layer aNewLabelLayer = aNewLabelLayer.concat(aoLabelLayer.splice(i, 1)); // array was altered so next element has replaced this one. i--; } else { // just record the ending point and move along iPrevLabelEnd = aoLabelLayer[i].right; } } // check if any labels were added to the new array and append as necessary if (aNewLabelLayer.length > 0) { aaLabelLayers.push(aNewLabelLayer); } }, /** * This is a helper function that takes an array of labels and positions and rotates them. * @param aoRotateLabels: Array - list of labels to rotate and position **/ _rotateLabels: function _rotateLabels(aoRotateLabels) { if (aoRotateLabels.length < 1) { return; } var widgetWidget = this.$_sliderContainer[0].scrollWidth || this.$_sliderContainer[0].clientWidth; // The top is the same for all the labels, so calculate it once var top = Math.max(-8, aoRotateLabels[0].labelBaseline - aoRotateLabels[0].height); // Since the labels are shown at a 45 degree angle, use Pythagorean theorem to calculate the maximum length var maxWidth = Math.max(25, Math.floor(Math.sqrt(top * top + top * top))); // loop and process all labels for (var i = 0; i < aoRotateLabels.length; i++) { // position the label and rotate (restore width for possoibly truncated labels) var $oLabel = aoRotateLabels[i].$el; $oLabel.css('top', top + 'px'); $oLabel.css('left', aoRotateLabels[i].tickLeft + 'px'); // If ever we don't have enough room left over to do a 45 degree triangle (top), then recalculate the max width. This hanppens // at the end of the data player, the last few labels would extend past the right edge and cause scrollbars if (widgetWidget - aoRotateLabels[i].tickLeft < top) { var roomLeft = widgetWidget - aoRotateLabels[i].tickLeft; $oLabel.css('width', Math.floor(Math.sqrt(roomLeft * roomLeft + roomLeft * roomLeft)) + 'px'); } else { $oLabel.css('width', maxWidth + 'px'); } $oLabel.addClass('rotated'); } }, /** * This function takes the given index and sets a filter accordingly. * It will clear all existing filters before setting the new one. * @param index: Integer - index of value to look up **/ _setFilter: function _setFilter(index) { // Clear any tooltips or other toolbars when we change the data to avoid stale info in our view this.ownerWidget.dashboardApi.triggerDashboardEvent('widget:hideToolbar'); var value = this.playerEntries[index]; this.getController().select({ itemIds: [this._dataItemAPI.getColumnId()], tuple: [EventUtils.toDeprecatedPayload(value.value)], command: 'update', slotsToClear: this.visualization.getSlots().getMappedSlotList() }); }, /** * This is a helper function that sets a filter by event and target. * @param event: Event - event object * @param targetName: String - target to filter on * @param allowToggle: Boolean - flag to indicate if the filter should * toggle if already set **/ _setFilterByEventTarget: function _setFilterByEventTarget(event, targetName, allowToggle) { // get clicked item and either select or deselect var el = this.getTarget(event.target, targetName); var index = el.getAttribute('data-item-index'); if (index) { // make sure the index is an integer index = parseInt(index, 10); // select if not selected if (index !== this._currentSelectionIndex) { // immediately show label as selected this._selectLabel(index); // set filer this._setFilter(index); } else { // deselect if (allowToggle && !this._isPlaying()) { this._clearSelection(); } } } }, /** * This function initiates play mode. **/ _play: function _play() { // unpause this.$_playPauseButton.removeClass('paused'); // start playing this._playIteration(); }, /** * This function stops playback. **/ _pause: function _pause() { // clear timer this._clearPlayTimer(); // pause this.$_playPauseButton.addClass('paused'); }, /** * Helper function to determine if control is currently playing. * @return Boolean - True if playing, false if not. **/ _isPlaying: function _isPlaying() { return !this.$_playPauseButton.hasClass('paused'); }, /** * This function conducts the playing operation and gets executed on a timer. **/ _playIteration: function _playIteration() { // clear timer this._clearPlayTimer(); // sanity check for data if (!(this._dataItemAPI && this.playerEntries && this.playerEntries.length)) { return; } // when slider position is not set, start playing with index 0 instead of -1; if (this._playIndex === -1) { this._playIndex = 0; } // set global filter this._setFilter(this._playIndex, true); // trigger next iteration this._playTimeout = window.setTimeout(this._playIteration.bind(this), this.playerSpeed); }, /** * Helper function to access the slider container. **/ _getSliderContainer: function _getSliderContainer() { return $(this.visEl).find('.slider'); }, /** * Helper function to reset the timer. **/ _clearPlayTimer: function _clearPlayTimer() { if (this._playTimeout) { window.clearTimeout(this._playTimeout); this._playTimout = null; } }, /** * This function clears all filters and selections. **/ _clearSelection: function _clearSelection() { if (this._currentSelectionIndex !== -1) { this.getController().select({ itemIds: this._dataItemAPI.getColumnId(), tuple: EventUtils.toDeprecatedPayload(this.playerEntries[this._currentSelectionIndex].value), command: 'remove', slotsToClear: this.visualization.getSlots().getMappedSlotList() }); } // clear the slider this._positionSlider(-1); // clear current selection this._selectLabel(-1); this._currentSelectionIndex = -1; }, /** * This function takes in the left position of the slider and determines if * it is in range to snap to a tick. It will return a new left position. * @param leftPosition: Integer - the left position of the slider node * @param splitThreshold: Boolean - flag to indicate if the threshold is * pre-defined or split between ticks. * @return Object: Integer position and tick index value **/ _snapToTick: function _snapToTick(leftPosition, splitThreshold) { // init var centeredLeftPos = leftPosition + this.$_sliderNode[0].clientWidth / 2; var newLeft = leftPosition; var index = -1; // clear current selection so that it will snap correctly this._currentSelectionIndex = -1; // threshold is either pre-defined or is split halfway between ticks var threshold = this._snapThreshold; if (splitThreshold) { threshold = null; } // loop through collect and determine either snap to tick, limit to start, or limit to end for (var i = 0; i < this.playerEntries.length; i++) { // set threshold as necessary if (!threshold) { threshold = this.playerEntries[i].tick.spacing / 2; } // get tick center var tickCenterLeft = this.playerEntries[i].tick.left + this.playerEntries[i].tick.width / 2; // get snap left and snap right var snapLeft = centeredLeftPos <= tickCenterLeft + threshold; var snapRight = centeredLeftPos >= tickCenterLeft - threshold; // check if first or last entry and if snap is needed var isFirstAndSnap = i === 0 && snapLeft; var isLastAndSnap = i === this.playerEntries.length - 1 && snapRight; // check limits and snap to tick if (isFirstAndSnap || isLastAndSnap || snapLeft && snapRight) { // calculate new left & record value newLeft = this._calcSliderPosition(i).left; index = i; break; } } // return return { left: Math.round(newLeft), index: index }; }, /** * Helper function to create the slider bar and append it to the DOM. * @param barTop: Integer - top position * @param barLeft: Integer - left position * @param barLength: Integer - length **/ _createSliderBar: function _createSliderBar(barTop, barLeft, barLength) { this.$_sliderBar = $(''); this.$_sliderContainer.append(this.$_sliderBar); this.$_sliderBar.css('top', barTop + 'px'); this.$_sliderBar.css('left', barLeft + 'px'); this.$_sliderBar.css('width', barLength + 'px'); }, /** * Helper function to create the slider node and append it to the DOM. It is * drawn using SVG instead of the icon font because of precise pixel level * positioning differences across browsers. **/ _createSliderNode: function _createSliderNode() { this.$_sliderNode = $(''); this.$_sliderContainer.append(this.$_sliderNode); }, /** * Helper function to position the play/pause button. * @param containerHeight: Integer - container height **/ _initPlayPauseButton: function _initPlayPauseButton(containerHeight) { // get button this.$_playPauseButton = $(this.visEl).find('.playOrPause'); // position this.$_playPauseButton.css('top', Math.round(containerHeight * this.verticalPositionScaleFactor - 28) + 'px'); }, /** * This function creates an individual tick and appends it to the DOM. It * returns a tick object to be used in the global entries list. * @param index: Integer - position index (zero based) * @param barTop: Integer - top position of bar * @param tickSpacing: Integer - distance between ticks * @param sizeCache Object - simple cache for width and hight of ticks * based on constant width and constant height * @return object: Tick object **/ _createTick: function _createTick(index, barTop, tickSpacing, sizeCache) { // create new tick var $tick = this.$tick.clone(); this.$_sliderNode.before($tick); var tickClientWidth; if (sizeCache.w) { tickClientWidth = sizeCache.w; } else { tickClientWidth = $tick[0].clientWidth; sizeCache.w = tickClientWidth; } var tickClientHeight; if (sizeCache.h) { tickClientHeight = sizeCache.h; } else { tickClientHeight = $tick[0].clientHeight; sizeCache.h = tickClientHeight; } // set further attributes $tick.attr('data-item-index', index); $tick.css('height', tickClientHeight + 'px'); var tickTop = barTop - Math.round(tickClientHeight / 2); var tickLeft = Math.round(this.barOverflow / 2 + tickSpacing * index - tickClientWidth / 2); $tick.css('top', tickTop + 'px'); $tick.css('left', tickLeft + 'px'); // return tick object return { top: tickTop, left: tickLeft, width: tickClientWidth, height: tickClientHeight, spacing: tickSpacing }; }, /** * This function creates a label and appends it to the DOM. It returns a * tick object to be used in positioning. * @param value: String - data value * @param index: Integer - position index (zero based) * @param tickLeft: Integer - left position * @param labelBaseline: Integer - vertical bottom of labels * @param isFirstOrLastLabel: Boolean - flag to indicate if first of last * label to be processed. * @return object: Label object **/ _createLabel: function _createLabel(value, index, tickLeft, labelBaseline, isFirstOrLastLabel) { // format the text for the label var label = Formatter.format(value, this._dataItemAPI.getFormat()); // create the label object var $label = this.$label.clone(); $label.attr('title', label); $label.attr('role', 'option'); $label.attr('tabindex', '-1'); $label.text(label); this.$_sliderNode.before($label); $label.attr('data-item-index', index); // calculate label width and truncate the first and last labels as necessary var labelWidth = $label[0].clientWidth; var actualWidth = labelWidth; if (isFirstOrLastLabel) { var truncLength = this.barOverflow * 2 - 10; // the 10 is a fudge factor if (labelWidth > truncLength) { labelWidth = Math.round(truncLength); $label.css('width', labelWidth + 'px'); } } // calculate the rest of the label info and store in array to be used for positioning var labelHeight = $label[0].clientHeight; var labelLeft = Math.round(tickLeft - labelWidth / 2); var labelRight = labelLeft + labelWidth; if (labelLeft < 0) { //if the left is off the screen, set it to 0, but add the difference to the right. labelRight += 0 - labelLeft; labelLeft = 0; } return { $el: $label, left: labelLeft, right: labelRight, width: labelWidth, actualWidth: actualWidth, height: labelHeight, labelBaseline: labelBaseline, tickLeft: tickLeft }; }, /** * Helper function to get the pageX position between a touch event and regular mouse event. * @param event: event object **/ _getPageX: function _getPageX(event) { return event.type.substr(0, 5) !== 'touch' ? event.pageX : event.originalEvent.touches[0].pageX; } }); return DataPlayerView; }); //# sourceMappingURL=DataPlayerView.js.map