diff options
Diffstat (limited to 'javascript/videojs/src/js/slider/slider.js')
| -rw-r--r-- | javascript/videojs/src/js/slider/slider.js | 392 |
1 files changed, 392 insertions, 0 deletions
diff --git a/javascript/videojs/src/js/slider/slider.js b/javascript/videojs/src/js/slider/slider.js new file mode 100644 index 0000000..8762863 --- /dev/null +++ b/javascript/videojs/src/js/slider/slider.js @@ -0,0 +1,392 @@ +/** + * @file slider.js + */ +import Component from '../component.js'; +import * as Dom from '../utils/dom.js'; +import {IS_CHROME} from '../utils/browser.js'; +import {clamp} from '../utils/num.js'; + +/** @import Player from '../player' */ + +/** + * The base functionality for a slider. Can be vertical or horizontal. + * For instance the volume bar or the seek bar on a video is a slider. + * + * @extends Component + */ +class Slider extends Component { + + /** + * Create an instance of this class + * + * @param {Player} player + * The `Player` that this class should be attached to. + * + * @param {Object} [options] + * The key/value store of player options. + */ + constructor(player, options) { + super(player, options); + + this.handleMouseDown_ = (e) => this.handleMouseDown(e); + this.handleMouseUp_ = (e) => this.handleMouseUp(e); + this.handleKeyDown_ = (e) => this.handleKeyDown(e); + this.handleClick_ = (e) => this.handleClick(e); + this.handleMouseMove_ = (e) => this.handleMouseMove(e); + this.update_ = (e) => this.update(e); + + // Set property names to bar to match with the child Slider class is looking for + this.bar = this.getChild(this.options_.barName); + + // Set a horizontal or vertical class on the slider depending on the slider type + this.vertical(!!this.options_.vertical); + + this.enable(); + } + + /** + * Are controls are currently enabled for this slider or not. + * + * @return {boolean} + * true if controls are enabled, false otherwise + */ + enabled() { + return this.enabled_; + } + + /** + * Enable controls for this slider if they are disabled + */ + enable() { + if (this.enabled()) { + return; + } + + this.on('mousedown', this.handleMouseDown_); + this.on('touchstart', this.handleMouseDown_); + this.on('keydown', this.handleKeyDown_); + this.on('click', this.handleClick_); + + // TODO: deprecated, controlsvisible does not seem to be fired + this.on(this.player_, 'controlsvisible', this.update); + + if (this.playerEvent) { + this.on(this.player_, this.playerEvent, this.update); + } + + this.removeClass('disabled'); + this.setAttribute('tabindex', 0); + + this.enabled_ = true; + } + + /** + * Disable controls for this slider if they are enabled + */ + disable() { + if (!this.enabled()) { + return; + } + const doc = this.bar.el_.ownerDocument; + + this.off('mousedown', this.handleMouseDown_); + this.off('touchstart', this.handleMouseDown_); + this.off('keydown', this.handleKeyDown_); + this.off('click', this.handleClick_); + this.off(this.player_, 'controlsvisible', this.update_); + this.off(doc, 'mousemove', this.handleMouseMove_); + this.off(doc, 'mouseup', this.handleMouseUp_); + this.off(doc, 'touchmove', this.handleMouseMove_); + this.off(doc, 'touchend', this.handleMouseUp_); + this.removeAttribute('tabindex'); + + this.addClass('disabled'); + + if (this.playerEvent) { + this.off(this.player_, this.playerEvent, this.update); + } + this.enabled_ = false; + } + + /** + * Create the `Slider`s DOM element. + * + * @param {string} type + * Type of element to create. + * + * @param {Object} [props={}] + * List of properties in Object form. + * + * @param {Object} [attributes={}] + * list of attributes in Object form. + * + * @return {Element} + * The element that gets created. + */ + createEl(type, props = {}, attributes = {}) { + // Add the slider element class to all sub classes + props.className = props.className + ' vjs-slider'; + props = Object.assign({ + tabIndex: 0 + }, props); + + attributes = Object.assign({ + 'role': 'slider', + 'aria-valuenow': 0, + 'aria-valuemin': 0, + 'aria-valuemax': 100 + }, attributes); + + return super.createEl(type, props, attributes); + } + + /** + * Handle `mousedown` or `touchstart` events on the `Slider`. + * + * @param {MouseEvent} event + * `mousedown` or `touchstart` event that triggered this function + * + * @listens mousedown + * @listens touchstart + * @fires Slider#slideractive + */ + handleMouseDown(event) { + const doc = this.bar.el_.ownerDocument; + + if (event.type === 'mousedown') { + event.preventDefault(); + } + // Do not call preventDefault() on touchstart in Chrome + // to avoid console warnings. Use a 'touch-action: none' style + // instead to prevent unintended scrolling. + // https://developers.google.com/web/updates/2017/01/scrolling-intervention + if (event.type === 'touchstart' && !IS_CHROME) { + event.preventDefault(); + } + Dom.blockTextSelection(); + + this.addClass('vjs-sliding'); + /** + * Triggered when the slider is in an active state + * + * @event Slider#slideractive + * @type {MouseEvent} + */ + this.trigger('slideractive'); + + this.on(doc, 'mousemove', this.handleMouseMove_); + this.on(doc, 'mouseup', this.handleMouseUp_); + this.on(doc, 'touchmove', this.handleMouseMove_); + this.on(doc, 'touchend', this.handleMouseUp_); + + this.handleMouseMove(event, true); + } + + /** + * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`. + * The `mousemove` and `touchmove` events will only only trigger this function during + * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and + * {@link Slider#handleMouseUp}. + * + * @param {MouseEvent} event + * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered + * this function + * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false. + * + * @listens mousemove + * @listens touchmove + */ + handleMouseMove(event) {} + + /** + * Handle `mouseup` or `touchend` events on the `Slider`. + * + * @param {MouseEvent} event + * `mouseup` or `touchend` event that triggered this function. + * + * @listens touchend + * @listens mouseup + * @fires Slider#sliderinactive + */ + handleMouseUp(event) { + const doc = this.bar.el_.ownerDocument; + + Dom.unblockTextSelection(); + + this.removeClass('vjs-sliding'); + /** + * Triggered when the slider is no longer in an active state. + * + * @event Slider#sliderinactive + * @type {Event} + */ + this.trigger('sliderinactive'); + + this.off(doc, 'mousemove', this.handleMouseMove_); + this.off(doc, 'mouseup', this.handleMouseUp_); + this.off(doc, 'touchmove', this.handleMouseMove_); + this.off(doc, 'touchend', this.handleMouseUp_); + + this.update(); + } + + /** + * Update the progress bar of the `Slider`. + * + * @return {number} + * The percentage of progress the progress bar represents as a + * number from 0 to 1. + */ + update() { + // In VolumeBar init we have a setTimeout for update that pops and update + // to the end of the execution stack. The player is destroyed before then + // update will cause an error + // If there's no bar... + if (!this.el_ || !this.bar) { + return; + } + + // clamp progress between 0 and 1 + // and only round to four decimal places, as we round to two below + const progress = this.getProgress(); + + if (progress === this.progress_) { + return progress; + } + + this.progress_ = progress; + + this.requestNamedAnimationFrame('Slider#update', () => { + // Set the new bar width or height + const sizeKey = this.vertical() ? 'height' : 'width'; + + // Convert to a percentage for css value + this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%'; + }); + + return progress; + } + + /** + * Get the percentage of the bar that should be filled + * but clamped and rounded. + * + * @return {number} + * percentage filled that the slider is + */ + getProgress() { + return Number(clamp(this.getPercent(), 0, 1).toFixed(4)); + } + + /** + * Calculate distance for slider + * + * @param {Event} event + * The event that caused this function to run. + * + * @return {number} + * The current position of the Slider. + * - position.x for vertical `Slider`s + * - position.y for horizontal `Slider`s + */ + calculateDistance(event) { + const position = Dom.getPointerPosition(this.el_, event); + + if (this.vertical()) { + return position.y; + } + return position.x; + } + + /** + * Handle a `keydown` event on the `Slider`. Watches for left, right, up, and down + * arrow keys. This function will only be called when the slider has focus. See + * {@link Slider#handleFocus} and {@link Slider#handleBlur}. + * + * @param {KeyboardEvent} event + * the `keydown` event that caused this function to run. + * + * @listens keydown + */ + handleKeyDown(event) { + const spatialNavOptions = this.options_.playerOptions.spatialNavigation; + const spatialNavEnabled = spatialNavOptions && spatialNavOptions.enabled; + const horizontalSeek = spatialNavOptions && spatialNavOptions.horizontalSeek; + + if (spatialNavEnabled) { + if ((horizontalSeek && event.key === 'ArrowLeft') || + (!horizontalSeek && event.key === 'ArrowDown')) { + event.preventDefault(); + event.stopPropagation(); + this.stepBack(); + } else if ((horizontalSeek && event.key === 'ArrowRight') || + (!horizontalSeek && event.key === 'ArrowUp')) { + event.preventDefault(); + event.stopPropagation(); + this.stepForward(); + } else { + if (this.pendingSeekTime()) { + this.pendingSeekTime(null); + this.userSeek_(this.player_.currentTime()); + } + super.handleKeyDown(event); + } + + // Left and Down Arrows + } else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') { + event.preventDefault(); + event.stopPropagation(); + this.stepBack(); + + // Up and Right Arrows + } else if (event.key === 'ArrowUp' || event.key === 'ArrowRight') { + event.preventDefault(); + event.stopPropagation(); + this.stepForward(); + } else { + + // Pass keydown handling up for unsupported keys + super.handleKeyDown(event); + } + } + + /** + * Listener for click events on slider, used to prevent clicks + * from bubbling up to parent elements like button menus. + * + * @param {Object} event + * Event that caused this object to run + */ + handleClick(event) { + event.stopPropagation(); + event.preventDefault(); + } + + /** + * Get/set if slider is horizontal for vertical + * + * @param {boolean} [bool] + * - true if slider is vertical, + * - false is horizontal + * + * @return {boolean} + * - true if slider is vertical, and getting + * - false if the slider is horizontal, and getting + */ + vertical(bool) { + if (bool === undefined) { + return this.vertical_ || false; + } + + this.vertical_ = !!bool; + + if (this.vertical_) { + this.addClass('vjs-slider-vertical'); + } else { + this.addClass('vjs-slider-horizontal'); + } + } +} + +Component.registerComponent('Slider', Slider); +export default Slider; |
