From d4c1af60ac9ff9e6045f52430326b86a705301a1 Mon Sep 17 00:00:00 2001 From: Gary Katsevman Date: Fri, 25 Mar 2016 14:34:06 -0400 Subject: [PATCH] @gkatsev added an option to keep the tooltips inside the player bounds. closes #3149 --- CHANGELOG.md | 1 + build/grunt.js | 2 + docs/guides/components.md | 49 +++++++++++++---- src/css/components/_progress.scss | 30 +++++++++-- .../progress-control/mouse-time-display.js | 53 ++++++++++++++++++ .../progress-control/play-progress-bar.js | 12 +++++ .../control-bar/progress-control/seek-bar.js | 44 ++++++++++++--- .../progress-control/tooltip-progress-bar.js | 54 +++++++++++++++++++ 8 files changed, 224 insertions(+), 21 deletions(-) create mode 100644 src/js/control-bar/progress-control/tooltip-progress-bar.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e03cb4f62..f4f8d0183 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * @kamilbrenk Added lang * @arius28 added greek translation file (el.json) ([view](https://github.com/videojs/video.js/pull/3185)) * @ricardosiri68 changed the relative sass paths ([view](https://github.com/videojs/video.js/pull/3147)) +* @gkatsev added an option to keep the tooltips inside the player bounds ([view](https://github.com/videojs/video.js/pull/3149)) -------------------- diff --git a/build/grunt.js b/build/grunt.js index 0f5d791ee..4c3d2ab80 100644 --- a/build/grunt.js +++ b/build/grunt.js @@ -500,6 +500,8 @@ module.exports = function(grunt) { grunt.file.write(cssPath, css.replace(/(\\f\w+);/g, "'$1';")); }); + grunt.registerTask('skin', ['sass', 'wrapcodepoints']); + // Default task - build and test grunt.registerTask('default', ['test']); diff --git a/docs/guides/components.md b/docs/guides/components.md index f950a7592..9bd686930 100644 --- a/docs/guides/components.md +++ b/docs/guides/components.md @@ -41,19 +41,46 @@ Player BigPlayButton ControlBar PlayToggle - FullscreenToggle - CurrentTimeDisplay - TimeDivider - DurationDisplay - RemainingTimeDisplay + VolumeMenuButton + CurrentTimeDisplay (Hidden by default) + TimeDivider (Hidden by default) + DurationDisplay (Hidden by default) ProgressControl SeekBar LoadProgressBar + MouseTimeDisplay PlayProgressBar - SeekHandle - VolumeControl - VolumeBar - VolumeLevel - VolumeHandle - MuteToggle + LiveDisplay (Hidden by default) + RemainingTimeDisplay + CustomControlsSpacer (No UI) + ChaptersButton (Hidden by default) + SubtitlesButton (Hidden by default) + CaptionsButton (Hidden by default) + FullscreenToggle + ErrorDisplay + TextTrackSettings +``` + +## Progress Control +The progress control is made up of the SeekBar. The seekbar contains the load progress bar +and the play progress bar. In addition, it contains the Mouse Time Display which +is used to display the time tooltip that follows the mouse cursor. +The play progress bar also has a time tooltip that show the current time. + +By default, the progress control is sandwiched between the volume menu button and +the remaining time display inside the control bar, but in some cases, a skin would +want to move the progress control above the control bar and have it span the full +width of the player, in those cases, it is less than ideal to have the tooltips +get cut off or leave the bounds of the player. This can be prevented by setting the +`keepTooltipsInside` option on the progress control. This also makes the tooltips use +a real element instead of pseudo elements so targetting them with css will be different. + +```js +let player = videojs('myplayer', { + controlBar: { + progressControl: { + keepTooltipsInside: true + } + } +}); ``` diff --git a/src/css/components/_progress.scss b/src/css/components/_progress.scss index 9e7ba8e48..b1e4d06f0 100644 --- a/src/css/components/_progress.scss +++ b/src/css/components/_progress.scss @@ -39,15 +39,18 @@ to avoid a weird hitch when you roll off the hover. */ // Also show the current time tooltip +.video-js .vjs-progress-control:hover .vjs-time-tooltip, .video-js .vjs-progress-control:hover .vjs-mouse-display:after, .video-js .vjs-progress-control:hover .vjs-play-progress:after { - display: block; + font-family: VideoJS; + visibility: visible; font-size: 0.6em; } // Progress Bars .video-js .vjs-progress-holder .vjs-play-progress, .video-js .vjs-progress-holder .vjs-load-progress, +.video-js .vjs-progress-holder .vjs-tooltip-progress-bar, .video-js .vjs-progress-holder .vjs-load-progress div { position: absolute; display: block; @@ -83,12 +86,14 @@ // Current Time "tooltip" // By default this is hidden and only shown when hovering over the progress control +.video-js .vjs-time-tooltip, .video-js .vjs-mouse-display:after, .video-js .vjs-play-progress:after { - display: none; + visibility: hidden; + pointer-events: none; position: absolute; top: -3.4em; - right: -1.5em; + right: -1.9em; font-size: 0.9em; color: #000; content: attr(data-current-time); @@ -96,11 +101,17 @@ @include background-color-with-alpha(#fff, 0.8); @include border-radius(0.3em); } + +.video-js .vjs-time-tooltip, .video-js .vjs-play-progress:before, .video-js .vjs-play-progress:after { z-index: 1; } +.video-js .vjs-progress-control .vjs-keep-tooltips-inside:after { + display: none; +} + .video-js .vjs-load-progress { // For IE8 we'll lighten the color background: lighten($secondary-background-color, 25%); @@ -121,6 +132,18 @@ width: auto; } +.video-js .vjs-time-tooltip { + display: inline-block; + height: 2.4em; + position: relative; + float: right; + right: -1.9em; +} + +.vjs-tooltip-progress-bar { + visibility: hidden; +} + .video-js .vjs-progress-control .vjs-mouse-display { display: none; position: absolute; @@ -146,6 +169,7 @@ .video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display:after { display: none; } +.vjs-mouse-display .vjs-time-tooltip, .video-js .vjs-progress-control .vjs-mouse-display:after { color: #fff; @include background-color-with-alpha(#000, 0.8); diff --git a/src/js/control-bar/progress-control/mouse-time-display.js b/src/js/control-bar/progress-control/mouse-time-display.js index b05d752ec..e59be2caa 100644 --- a/src/js/control-bar/progress-control/mouse-time-display.js +++ b/src/js/control-bar/progress-control/mouse-time-display.js @@ -1,6 +1,7 @@ /** * @file mouse-time-display.js */ +import window from 'global/window'; import Component from '../../component.js'; import * as Dom from '../../utils/dom.js'; import * as Fn from '../../utils/fn.js'; @@ -21,6 +22,19 @@ class MouseTimeDisplay extends Component { constructor(player, options) { super(player, options); + if (options.playerOptions && + options.playerOptions.controlBar && + options.playerOptions.controlBar.progressControl && + options.playerOptions.controlBar.progressControl.keepTooltipsInside) { + this.keepTooltipsInside = options.playerOptions.controlBar.progressControl.keepTooltipsInside; + } + + if (this.keepTooltipsInside) { + this.tooltip = Dom.createEl('div', {className: 'vjs-time-tooltip'}); + this.el().appendChild(this.tooltip); + this.addClass('vjs-keep-tooltips-inside'); + } + this.update(0, 0); player.on('ready', () => { @@ -53,11 +67,50 @@ class MouseTimeDisplay extends Component { this.el().style.left = position + 'px'; this.el().setAttribute('data-current-time', time); + + if (this.keepTooltipsInside) { + let clampedPosition = this.clampPosition_(position); + let difference = position - clampedPosition + 1; + let tooltipWidth = parseFloat(window.getComputedStyle(this.tooltip).width); + let tooltipWidthHalf = tooltipWidth / 2; + + this.tooltip.innerHTML = time; + this.tooltip.style.right = `-${tooltipWidthHalf - difference}px`; + } } calculateDistance(event) { return Dom.getPointerPosition(this.el().parentNode, event).x; } + + /** + * This takes in a horizontal position for the bar and returns a clamped position. + * Clamped position means that it will keep the position greater than half the width + * of the tooltip and smaller than the player width minus half the width o the tooltip. + * It will only clamp the position if `keepTooltipsInside` option is set. + * + * @param {Number} position the position the bar wants to be + * @return {Number} newPosition the (potentially) clamped position + * @method clampPosition_ + */ + clampPosition_(position) { + if (!this.keepTooltipsInside) { + return position; + } + + let playerWidth = parseFloat(window.getComputedStyle(this.player().el()).width); + let tooltipWidth = parseFloat(window.getComputedStyle(this.tooltip).width); + let tooltipWidthHalf = tooltipWidth / 2; + let actualPosition = position; + + if (position < tooltipWidthHalf) { + actualPosition = Math.ceil(tooltipWidthHalf); + } else if (position > (playerWidth - tooltipWidthHalf)) { + actualPosition = Math.floor(playerWidth - tooltipWidthHalf); + } + + return actualPosition; + } } Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay); diff --git a/src/js/control-bar/progress-control/play-progress-bar.js b/src/js/control-bar/progress-control/play-progress-bar.js index 5236d1130..7ddea6cf4 100644 --- a/src/js/control-bar/progress-control/play-progress-bar.js +++ b/src/js/control-bar/progress-control/play-progress-bar.js @@ -3,6 +3,7 @@ */ import Component from '../../component.js'; import * as Fn from '../../utils/fn.js'; +import * as Dom from '../../utils/dom.js'; import formatTime from '../../utils/format-time.js'; /** @@ -20,6 +21,17 @@ class PlayProgressBar extends Component { this.updateDataAttr(); this.on(player, 'timeupdate', this.updateDataAttr); player.ready(Fn.bind(this, this.updateDataAttr)); + + if (options.playerOptions && + options.playerOptions.controlBar && + options.playerOptions.controlBar.progressControl && + options.playerOptions.controlBar.progressControl.keepTooltipsInside) { + this.keepTooltipsInside = options.playerOptions.controlBar.progressControl.keepTooltipsInside; + } + + if (this.keepTooltipsInside) { + this.addClass('vjs-keep-tooltips-inside'); + } } /** diff --git a/src/js/control-bar/progress-control/seek-bar.js b/src/js/control-bar/progress-control/seek-bar.js index 09176fbde..83f6b6342 100644 --- a/src/js/control-bar/progress-control/seek-bar.js +++ b/src/js/control-bar/progress-control/seek-bar.js @@ -1,10 +1,12 @@ /** * @file seek-bar.js */ +import window from 'global/window'; import Slider from '../../slider/slider.js'; import Component from '../../component.js'; import LoadProgressBar from './load-progress-bar.js'; import PlayProgressBar from './play-progress-bar.js'; +import TooltipProgressBar from './tooltip-progress-bar.js'; import * as Fn from '../../utils/fn.js'; import formatTime from '../../utils/format-time.js'; import assign from 'object.assign'; @@ -21,8 +23,20 @@ class SeekBar extends Slider { constructor(player, options){ super(player, options); - this.on(player, 'timeupdate', this.updateARIAAttributes); - player.ready(Fn.bind(this, this.updateARIAAttributes)); + this.on(player, 'timeupdate', this.updateProgress); + this.on(player, 'ended', this.updateProgress); + player.ready(Fn.bind(this, this.updateProgress)); + + if (options.playerOptions && + options.playerOptions.controlBar && + options.playerOptions.controlBar.progressControl && + options.playerOptions.controlBar.progressControl.keepTooltipsInside) { + this.keepTooltipsInside = options.playerOptions.controlBar.progressControl.keepTooltipsInside; + } + + if (this.keepTooltipsInside) { + this.tooltipProgressBar = this.addChild('TooltipProgressBar'); + } } /** @@ -44,11 +58,27 @@ class SeekBar extends Slider { * * @method updateARIAAttributes */ - updateARIAAttributes() { - // Allows for smooth scrubbing, when player can't keep up. - let time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); - this.el_.setAttribute('aria-valuenow', (this.getPercent() * 100).toFixed(2)); // machine readable value of progress bar (percentage complete) - this.el_.setAttribute('aria-valuetext', formatTime(time, this.player_.duration())); // human readable value of progress bar (time complete) + updateProgress() { + this.updateAriaAttributes(this.el_); + + if (this.keepTooltipsInside) { + this.updateAriaAttributes(this.tooltipProgressBar.el_); + this.tooltipProgressBar.el_.style.width = this.bar.el_.style.width; + + let playerWidth = parseFloat(window.getComputedStyle(this.player().el()).width); + let tooltipWidth = parseFloat(window.getComputedStyle(this.tooltipProgressBar.tooltip).width); + let tooltipStyle = this.tooltipProgressBar.el().style; + tooltipStyle.maxWidth = Math.floor(playerWidth - (tooltipWidth / 2)) + 'px'; + tooltipStyle.minWidth = Math.ceil(tooltipWidth / 2) + 'px'; + tooltipStyle.right = `-${tooltipWidth / 2}px`; + } + } + + updateAriaAttributes(el) { + // Allows for smooth scrubbing, when player can't keep up. + let time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); + el.setAttribute('aria-valuenow', (this.getPercent() * 100).toFixed(2)); // machine readable value of progress bar (percentage complete) + el.setAttribute('aria-valuetext', formatTime(time, this.player_.duration())); // human readable value of progress bar (time complete) } /** diff --git a/src/js/control-bar/progress-control/tooltip-progress-bar.js b/src/js/control-bar/progress-control/tooltip-progress-bar.js new file mode 100644 index 000000000..f1e092dad --- /dev/null +++ b/src/js/control-bar/progress-control/tooltip-progress-bar.js @@ -0,0 +1,54 @@ +/** + * @file play-progress-bar.js + */ +import Component from '../../component.js'; +import * as Fn from '../../utils/fn.js'; +import * as Dom from '../../utils/dom.js'; +import formatTime from '../../utils/format-time.js'; + +/** + * Shows play progress + * + * @param {Player|Object} player + * @param {Object=} options + * @extends Component + * @class PlayProgressBar + */ +class TooltipProgressBar extends Component { + + constructor(player, options){ + super(player, options); + this.updateDataAttr(); + this.on(player, 'timeupdate', this.updateDataAttr); + player.ready(Fn.bind(this, this.updateDataAttr)); + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + createEl() { + let el = super.createEl('div', { + className: 'vjs-tooltip-progress-bar vjs-slider-bar', + innerHTML: `
+ ${this.localize('Progress')}: 0%` + }); + + this.tooltip = el.querySelector('.vjs-time-tooltip'); + + return el; + } + + updateDataAttr() { + let time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); + let formattedTime = formatTime(time, this.player_.duration()); + this.el_.setAttribute('data-current-time', formattedTime); + this.tooltip.innerHTML = formattedTime; + } + +} + +Component.registerComponent('TooltipProgressBar', TooltipProgressBar); +export default TooltipProgressBar;