diff options
Diffstat (limited to 'javascript/videojs/src/js/control-bar/volume-control')
7 files changed, 682 insertions, 0 deletions
diff --git a/javascript/videojs/src/js/control-bar/volume-control/check-mute-support.js b/javascript/videojs/src/js/control-bar/volume-control/check-mute-support.js new file mode 100644 index 0000000..8665883 --- /dev/null +++ b/javascript/videojs/src/js/control-bar/volume-control/check-mute-support.js @@ -0,0 +1,31 @@ +/** @import Component from '../../component' */ +/** @import Player from '../../player' */ + +/** + * Check if muting volume is supported and if it isn't hide the mute toggle + * button. + * + * @param {Component} self + * A reference to the mute toggle button + * + * @param {Player} player + * A reference to the player + * + * @private + */ +const checkMuteSupport = function(self, player) { + // hide mute toggle button if it's not supported by the current tech + if (player.tech_ && !player.tech_.featuresMuteControl) { + self.addClass('vjs-hidden'); + } + + self.on(player, 'loadstart', function() { + if (!player.tech_.featuresMuteControl) { + self.addClass('vjs-hidden'); + } else { + self.removeClass('vjs-hidden'); + } + }); +}; + +export default checkMuteSupport; diff --git a/javascript/videojs/src/js/control-bar/volume-control/check-volume-support.js b/javascript/videojs/src/js/control-bar/volume-control/check-volume-support.js new file mode 100644 index 0000000..88f287b --- /dev/null +++ b/javascript/videojs/src/js/control-bar/volume-control/check-volume-support.js @@ -0,0 +1,31 @@ +/** @import Component from '../../component' */ +/** @import Player from '../../player' */ + +/** + * Check if volume control is supported and if it isn't hide the + * `Component` that was passed using the `vjs-hidden` class. + * + * @param {Component} self + * The component that should be hidden if volume is unsupported + * + * @param {Player} player + * A reference to the player + * + * @private + */ +const checkVolumeSupport = function(self, player) { + // hide volume controls when they're not supported by the current tech + if (player.tech_ && !player.tech_.featuresVolumeControl) { + self.addClass('vjs-hidden'); + } + + self.on(player, 'loadstart', function() { + if (!player.tech_.featuresVolumeControl) { + self.addClass('vjs-hidden'); + } else { + self.removeClass('vjs-hidden'); + } + }); +}; + +export default checkVolumeSupport; diff --git a/javascript/videojs/src/js/control-bar/volume-control/mouse-volume-level-display.js b/javascript/videojs/src/js/control-bar/volume-control/mouse-volume-level-display.js new file mode 100644 index 0000000..467d7b5 --- /dev/null +++ b/javascript/videojs/src/js/control-bar/volume-control/mouse-volume-level-display.js @@ -0,0 +1,89 @@ +/** + * @file mouse-volume-level-display.js + */ +import Component from '../../component.js'; +import * as Fn from '../../utils/fn.js'; + +/** @import Player from '../../player' */ + +import './volume-level-tooltip'; + +/** + * The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the + * {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip} + * indicating the volume level which is represented by a given point in the + * {@link VolumeBar}. + * + * @extends Component + */ +class MouseVolumeLevelDisplay extends Component { + + /** + * Creates an instance of this class. + * + * @param {Player} player + * The {@link 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.update = Fn.throttle(Fn.bind_(this, this.update), Fn.UPDATE_REFRESH_INTERVAL); + } + + /** + * Create the DOM element for this class. + * + * @return {Element} + * The element that was created. + */ + createEl() { + return super.createEl('div', { + className: 'vjs-mouse-display' + }); + } + + /** + * Enquires updates to its own DOM as well as the DOM of its + * {@link VolumeLevelTooltip} child. + * + * @param {Object} rangeBarRect + * The `ClientRect` for the {@link VolumeBar} element. + * + * @param {number} rangeBarPoint + * A number from 0 to 1, representing a horizontal/vertical reference point + * from the left edge of the {@link VolumeBar} + * + * @param {boolean} vertical + * Referees to the Volume control position + * in the control bar{@link VolumeControl} + * + */ + update(rangeBarRect, rangeBarPoint, vertical) { + const volume = 100 * rangeBarPoint; + + this.getChild('volumeLevelTooltip').updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, () => { + if (vertical) { + this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`; + } else { + this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`; + } + }); + } +} + +/** + * Default options for `MouseVolumeLevelDisplay` + * + * @type {Object} + * @private + */ +MouseVolumeLevelDisplay.prototype.options_ = { + children: [ + 'volumeLevelTooltip' + ] +}; + +Component.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay); +export default MouseVolumeLevelDisplay; diff --git a/javascript/videojs/src/js/control-bar/volume-control/volume-bar.js b/javascript/videojs/src/js/control-bar/volume-control/volume-bar.js new file mode 100644 index 0000000..4e2a02c --- /dev/null +++ b/javascript/videojs/src/js/control-bar/volume-control/volume-bar.js @@ -0,0 +1,212 @@ +/** + * @file volume-bar.js + */ +import Slider from '../../slider/slider.js'; +import Component from '../../component.js'; +import * as Dom from '../../utils/dom.js'; +import {clamp} from '../../utils/num.js'; +import {IS_IOS, IS_ANDROID} from '../../utils/browser.js'; + +/** @import Player from '../../player' */ + +// Required children +import './volume-level.js'; +import './mouse-volume-level-display.js'; + +/** + * The bar that contains the volume level and can be clicked on to adjust the level + * + * @extends Slider + */ +class VolumeBar extends Slider { + + /** + * Creates 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.on('slideractive', (e) => this.updateLastVolume_(e)); + this.on(player, 'volumechange', (e) => this.updateARIAAttributes(e)); + player.ready(() => this.updateARIAAttributes()); + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + createEl() { + return super.createEl('div', { + className: 'vjs-volume-bar vjs-slider-bar' + }, { + 'aria-label': this.localize('Volume Level'), + 'aria-live': 'polite' + }); + } + + /** + * Handle mouse down on volume bar + * + * @param {Event} event + * The `mousedown` event that caused this to run. + * + * @listens mousedown + */ + handleMouseDown(event) { + if (!Dom.isSingleLeftClick(event)) { + return; + } + + super.handleMouseDown(event); + } + + /** + * Handle movement events on the {@link VolumeMenuButton}. + * + * @param {Event} event + * The event that caused this function to run. + * + * @listens mousemove + */ + handleMouseMove(event) { + const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay'); + + if (mouseVolumeLevelDisplay) { + const volumeBarEl = this.el(); + const volumeBarRect = Dom.getBoundingClientRect(volumeBarEl); + const vertical = this.vertical(); + let volumeBarPoint = Dom.getPointerPosition(volumeBarEl, event); + + volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x; + // The default skin has a gap on either side of the `VolumeBar`. This means + // that it's possible to trigger this behavior outside the boundaries of + // the `VolumeBar`. This ensures we stay within it at all times. + volumeBarPoint = clamp(volumeBarPoint, 0, 1); + + mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical); + } + + if (!Dom.isSingleLeftClick(event)) { + return; + } + + this.checkMuted(); + this.player_.volume(this.calculateDistance(event)); + } + + /** + * If the player is muted unmute it. + */ + checkMuted() { + if (this.player_.muted()) { + this.player_.muted(false); + } + } + + /** + * Get percent of volume level + * + * @return {number} + * Volume level percent as a decimal number. + */ + getPercent() { + if (this.player_.muted()) { + return 0; + } + return this.player_.volume(); + } + + /** + * Increase volume level for keyboard users + */ + stepForward() { + this.checkMuted(); + this.player_.volume(this.player_.volume() + 0.1); + } + + /** + * Decrease volume level for keyboard users + */ + stepBack() { + this.checkMuted(); + this.player_.volume(this.player_.volume() - 0.1); + } + + /** + * Update ARIA accessibility attributes + * + * @param {Event} [event] + * The `volumechange` event that caused this function to run. + * + * @listens Player#volumechange + */ + updateARIAAttributes(event) { + const ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_(); + + this.el_.setAttribute('aria-valuenow', ariaValue); + this.el_.setAttribute('aria-valuetext', ariaValue + '%'); + } + + /** + * Returns the current value of the player volume as a percentage + * + * @private + */ + volumeAsPercentage_() { + return Math.round(this.player_.volume() * 100); + } + + /** + * When user starts dragging the VolumeBar, store the volume and listen for + * the end of the drag. When the drag ends, if the volume was set to zero, + * set lastVolume to the stored volume. + * + * @listens slideractive + * @private + */ + updateLastVolume_() { + const volumeBeforeDrag = this.player_.volume(); + + this.one('sliderinactive', () => { + if (this.player_.volume() === 0) { + this.player_.lastVolume_(volumeBeforeDrag); + } + }); + } + +} + +/** + * Default options for the `VolumeBar` + * + * @type {Object} + * @private + */ +VolumeBar.prototype.options_ = { + children: [ + 'volumeLevel' + ], + barName: 'volumeLevel' +}; + +// MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices +if (!IS_IOS && !IS_ANDROID) { + VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay'); +} + +/** + * Call the update event for this Slider when this event happens on the player. + * + * @type {string} + */ +VolumeBar.prototype.playerEvent = 'volumechange'; + +Component.registerComponent('VolumeBar', VolumeBar); +export default VolumeBar; diff --git a/javascript/videojs/src/js/control-bar/volume-control/volume-control.js b/javascript/videojs/src/js/control-bar/volume-control/volume-control.js new file mode 100644 index 0000000..723e9c3 --- /dev/null +++ b/javascript/videojs/src/js/control-bar/volume-control/volume-control.js @@ -0,0 +1,148 @@ +/** + * @file volume-control.js + */ +import Component from '../../component.js'; +import checkVolumeSupport from './check-volume-support'; +import {isPlain} from '../../utils/obj'; +import {throttle, bind_, UPDATE_REFRESH_INTERVAL} from '../../utils/fn.js'; + +/** @import Player from '../../player' */ + +// Required children +import './volume-bar.js'; + +/** + * The component for controlling the volume level + * + * @extends Component + */ +class VolumeControl extends Component { + + /** + * Creates 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 = {}) { + options.vertical = options.vertical || false; + + // Pass the vertical option down to the VolumeBar if + // the VolumeBar is turned on. + if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) { + options.volumeBar = options.volumeBar || {}; + options.volumeBar.vertical = options.vertical; + } + + super(player, options); + + // hide this control if volume support is missing + checkVolumeSupport(this, player); + + this.throttledHandleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL); + this.handleMouseUpHandler_ = (e) => this.handleMouseUp(e); + + this.on('mousedown', (e) => this.handleMouseDown(e)); + this.on('touchstart', (e) => this.handleMouseDown(e)); + this.on('mousemove', (e) => this.handleMouseMove(e)); + + // while the slider is active (the mouse has been pressed down and + // is dragging) or in focus we do not want to hide the VolumeBar + this.on(this.volumeBar, ['focus', 'slideractive'], () => { + this.volumeBar.addClass('vjs-slider-active'); + this.addClass('vjs-slider-active'); + this.trigger('slideractive'); + }); + + this.on(this.volumeBar, ['blur', 'sliderinactive'], () => { + this.volumeBar.removeClass('vjs-slider-active'); + this.removeClass('vjs-slider-active'); + this.trigger('sliderinactive'); + }); + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + createEl() { + let orientationClass = 'vjs-volume-horizontal'; + + if (this.options_.vertical) { + orientationClass = 'vjs-volume-vertical'; + } + + return super.createEl('div', { + className: `vjs-volume-control vjs-control ${orientationClass}` + }); + } + + /** + * Handle `mousedown` or `touchstart` events on the `VolumeControl`. + * + * @param {Event} event + * `mousedown` or `touchstart` event that triggered this function + * + * @listens mousedown + * @listens touchstart + */ + handleMouseDown(event) { + const doc = this.el_.ownerDocument; + + this.on(doc, 'mousemove', this.throttledHandleMouseMove); + this.on(doc, 'touchmove', this.throttledHandleMouseMove); + this.on(doc, 'mouseup', this.handleMouseUpHandler_); + this.on(doc, 'touchend', this.handleMouseUpHandler_); + } + + /** + * Handle `mouseup` or `touchend` events on the `VolumeControl`. + * + * @param {Event} event + * `mouseup` or `touchend` event that triggered this function. + * + * @listens touchend + * @listens mouseup + */ + handleMouseUp(event) { + const doc = this.el_.ownerDocument; + + this.off(doc, 'mousemove', this.throttledHandleMouseMove); + this.off(doc, 'touchmove', this.throttledHandleMouseMove); + this.off(doc, 'mouseup', this.handleMouseUpHandler_); + this.off(doc, 'touchend', this.handleMouseUpHandler_); + } + + /** + * Handle `mousedown` or `touchstart` events on the `VolumeControl`. + * + * @param {Event} event + * `mousedown` or `touchstart` event that triggered this function + * + * @listens mousedown + * @listens touchstart + */ + handleMouseMove(event) { + this.volumeBar.handleMouseMove(event); + } +} + +/** + * Default options for the `VolumeControl` + * + * @type {Object} + * @private + */ +VolumeControl.prototype.options_ = { + children: [ + 'volumeBar' + ] +}; + +Component.registerComponent('VolumeControl', VolumeControl); +export default VolumeControl; diff --git a/javascript/videojs/src/js/control-bar/volume-control/volume-level-tooltip.js b/javascript/videojs/src/js/control-bar/volume-control/volume-level-tooltip.js new file mode 100644 index 0000000..9df9843 --- /dev/null +++ b/javascript/videojs/src/js/control-bar/volume-control/volume-level-tooltip.js @@ -0,0 +1,135 @@ +/** + * @file volume-level-tooltip.js + */ +import Component from '../../component'; +import * as Dom from '../../utils/dom.js'; +import * as Fn from '../../utils/fn.js'; + +/** @import Player from '../../player' */ + +/** + * Volume level tooltips display a volume above or side by side the volume bar. + * + * @extends Component + */ +class VolumeLevelTooltip extends Component { + + /** + * Creates an instance of this class. + * + * @param {Player} player + * The {@link 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.update = Fn.throttle(Fn.bind_(this, this.update), Fn.UPDATE_REFRESH_INTERVAL); + } + + /** + * Create the volume tooltip DOM element + * + * @return {Element} + * The element that was created. + */ + createEl() { + return super.createEl('div', { + className: 'vjs-volume-tooltip' + }, { + 'aria-hidden': 'true' + }); + } + + /** + * Updates the position of the tooltip relative to the `VolumeBar` and + * its content text. + * + * @param {Object} rangeBarRect + * The `ClientRect` for the {@link VolumeBar} element. + * + * @param {number} rangeBarPoint + * A number from 0 to 1, representing a horizontal/vertical reference point + * from the left edge of the {@link VolumeBar} + * + * @param {boolean} vertical + * Referees to the Volume control position + * in the control bar{@link VolumeControl} + * + */ + update(rangeBarRect, rangeBarPoint, vertical, content) { + if (!vertical) { + const tooltipRect = Dom.getBoundingClientRect(this.el_); + const playerRect = Dom.getBoundingClientRect(this.player_.el()); + const volumeBarPointPx = rangeBarRect.width * rangeBarPoint; + + if (!playerRect || !tooltipRect) { + return; + } + + const spaceLeftOfPoint = (rangeBarRect.left - playerRect.left) + volumeBarPointPx; + const spaceRightOfPoint = (rangeBarRect.width - volumeBarPointPx) + + (playerRect.right - rangeBarRect.right); + let pullTooltipBy = tooltipRect.width / 2; + + if (spaceLeftOfPoint < pullTooltipBy) { + pullTooltipBy += pullTooltipBy - spaceLeftOfPoint; + } else if (spaceRightOfPoint < pullTooltipBy) { + pullTooltipBy = spaceRightOfPoint; + } + + if (pullTooltipBy < 0) { + pullTooltipBy = 0; + } else if (pullTooltipBy > tooltipRect.width) { + pullTooltipBy = tooltipRect.width; + } + + this.el_.style.right = `-${pullTooltipBy}px`; + } + this.write(`${content}%`); + } + + /** + * Write the volume to the tooltip DOM element. + * + * @param {string} content + * The formatted volume for the tooltip. + */ + write(content) { + Dom.textContent(this.el_, content); + } + + /** + * Updates the position of the volume tooltip relative to the `VolumeBar`. + * + * @param {Object} rangeBarRect + * The `ClientRect` for the {@link VolumeBar} element. + * + * @param {number} rangeBarPoint + * A number from 0 to 1, representing a horizontal/vertical reference point + * from the left edge of the {@link VolumeBar} + * + * @param {boolean} vertical + * Referees to the Volume control position + * in the control bar{@link VolumeControl} + * + * @param {number} volume + * The volume level to update the tooltip to + * + * @param {Function} cb + * A function that will be called during the request animation frame + * for tooltips that need to do additional animations from the default + */ + updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) { + this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => { + this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0)); + if (cb) { + cb(); + } + }); + } +} + +Component.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip); +export default VolumeLevelTooltip; diff --git a/javascript/videojs/src/js/control-bar/volume-control/volume-level.js b/javascript/videojs/src/js/control-bar/volume-control/volume-level.js new file mode 100644 index 0000000..f5de046 --- /dev/null +++ b/javascript/videojs/src/js/control-bar/volume-control/volume-level.js @@ -0,0 +1,36 @@ +/** + * @file volume-level.js + */ +import Component from '../../component.js'; + +/** + * Shows volume level + * + * @extends Component + */ +class VolumeLevel extends Component { + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + createEl() { + const el = super.createEl('div', { + className: 'vjs-volume-level' + }); + + this.setIcon('circle', el); + + el.appendChild(super.createEl('span', { + className: 'vjs-control-text' + })); + + return el; + } + +} + +Component.registerComponent('VolumeLevel', VolumeLevel); +export default VolumeLevel; |
