diff --git a/src/scripts/interactive-video.js b/src/scripts/interactive-video.js index bb32b347a..3f35fdc21 100644 --- a/src/scripts/interactive-video.js +++ b/src/scripts/interactive-video.js @@ -524,6 +524,8 @@ function InteractiveVideo(params, id, contentData) { */ self.toggleMute = (refocus = true) => { const $muteButton = self.controls.$volume; + const $volumeSlider = self.controls.$volumeSlider; + const defaultVol = 15; if (!self.deactivateSound) { if ($muteButton.hasClass('h5p-muted')) { @@ -532,6 +534,16 @@ function InteractiveVideo(params, id, contentData) { .attr('aria-label', self.l10n.mute); self.video.unMute(); + + // Set slider to a default low volume if video was muted by dragging the slider down to 0 + if (self.video.getVolume() < 1) { + self.video.setVolume(defaultVol); + $volumeSlider.slider('value', defaultVol); + } + // Otherwise reset to the previous volume level + else { + $volumeSlider.slider('value', self.video.getVolume()); + } } else { $muteButton @@ -539,6 +551,8 @@ function InteractiveVideo(params, id, contentData) { .attr('aria-label', self.l10n.unmute); self.video.mute(); + // Set slider to 0 (but do not adjust volume), getVolume() will persist our previous volume level + $volumeSlider.slider('value', 0); } if (refocus) { @@ -2065,9 +2079,90 @@ InteractiveVideo.prototype.attachControls = function ($wrapper) { self.controls.$playbackRateChooser.insertAfter(self.controls.$playbackRateButton); + // Add volume control wrapper + if (!isAndroid() && !isIpad()) { + self.controls.$volumeWrapper = $('
', { + tabindex: 0, + class: 'h5p-control h5p-volume-wrapper', + on: { + mouseenter: function (event) { + self.controls.$volumeSliderOverlay.addClass('h5p-show'); + }, + mouseleave: function (event) { + self.controls.$volumeSliderOverlay.removeClass('h5p-show'); + } + }, + appendTo: $right + }); + } + + // Add volume slider + if (!isAndroid() && !isIpad()) { + self.controls.$volumeSliderOverlay = $('
', { + class: 'h5p-volume-slider', + appendTo: self.controls.$volumeWrapper + }); + + self.controls.$volumeSlider = $('
', { + appendTo: self.controls.$volumeSliderOverlay + }).slider({ + value: self.video.getVolume(), + step: 1, + orientation: 'vertical', + range: 'min', + max: 100, + create: function (event) { + const $handle = $(event.target).find('.ui-slider-handle'); + const volumeNow = self.video.getVolume(); + // Slider style are wide to capture events, add another slider rail + $(event.target).prepend('
') + + $handle + .attr('role', 'slider') + .attr('aria-valuemin', '0') + .attr('aria-valuemax', '100') + .attr('aria-valuetext', volumeNow) // TODO: l10n, eg. "Volume 30%" ? + .attr('aria-valuenow', volumeNow); + + if (self.deactivateSound) { + self.setDisabled($handle).attr('aria-hidden', 'true'); + } + }, + slide: function (event, ui) { + const $handle = $(event.target).find('.ui-slider-handle'); + let volume = Math.floor(ui.value); + self.controls.$volumeSliderOverlay.addClass('h5p-active'); + + // When the slider is dragged to/from 0, toggle the mute button + if (volume < 1 && !self.video.isMuted()) { + self.toggleMute.call(this); + } + if (volume > 0 && self.video.isMuted()) { + self.toggleMute.call(this); + } + + // Update volume + self.video.setVolume(volume); + $handle + .attr('aria-valuetext', volume) // TODO: l10n, eg. "Volume 30%" ? + .attr('aria-valuenow', volume); + + // Make overlay visible to catch mouseup/move events. + self.$overlay.addClass('h5p-visible'); + }, + stop: function (event, ui) { + self.controls.$volumeSliderOverlay.removeClass('h5p-active'); + + // Done catching mouse events + self.$overlay.removeClass('h5p-visible'); + } + // TODO: add event on: keydown -> adjust volume ? + }); + } + // Add volume button control (toggle mute) if (!isAndroid() && !isIpad()) { - self.controls.$volume = self.createButton('mute', 'h5p-control', $right, self.toggleMute); + self.controls.$volume = self.createButton('mute', 'h5p-control', self.controls.$volumeWrapper, self.toggleMute); if (self.deactivateSound) { self.controls.$volume .addClass('h5p-muted') diff --git a/src/styles/interactive-video.css b/src/styles/interactive-video.css index b6b4dbb6a..b8d9211d0 100644 --- a/src/styles/interactive-video.css +++ b/src/styles/interactive-video.css @@ -1875,3 +1875,61 @@ .h5p-interactive-video .h5p-control:not(.h5p-no-tooltip):hover > .h5p-interactive-video-tooltip { visibility: visible; } +.h5p-interactive-video .h5p-control.h5p-volume-wrapper .h5p-volume-slider { + position: absolute; + bottom: 35px; + width: 36px; + height: 66px; + padding: 20px 0 18px; + z-index: 10; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; +} +.h5p-interactive-video .h5p-control.h5p-volume-wrapper .h5p-volume-slider.h5p-show, +.h5p-interactive-video .h5p-control.h5p-volume-wrapper .h5p-volume-slider.h5p-active { + opacity: 1; + visibility: visible; +} +.h5p-interactive-video .h5p-control.h5p-volume-wrapper .h5p-volume-slider .ui-slider { + height: 66px; + border: 0; + border-radius: 0; + z-index: 1; + background: none; + cursor: pointer; + -ms-touch-action: manipulation; + touch-action: manipulation; +} +.h5p-interactive-video .h5p-control.h5p-volume-wrapper .h5p-volume-slider .ui-slider.ui-slider-vertical { + width: 36px; +} +.h5p-interactive-video .h5p-control.h5p-volume-wrapper .h5p-volume-slider .ui-slider .ui-slider-handle { + margin-left: auto; + margin-right: auto; + left: 0px; + right: 0px; + width: 12px; + height: 12px; + border-radius: 50px; + border-color: rgb(102, 102, 102); + transition: box-shadow 0.1s ease; +} +.h5p-interactive-video .h5p-volume-slider .ui-slider-range { + margin-left: auto; + margin-right: auto; + left: 0px; + right: 0px; + width: 4px; + border: 0; + border-radius: 0; + color: #e2e2e2; +} +.h5p-interactive-video .h5p-volume-slider .ui-slider-range.h5p-volume-rail { + height: 100%; + background: #7a7a7a; + box-shadow: 0 0 6px rgba(0, 0, 0, .4); +} +.h5p-volume-slider .ui-slider .ui-slider-handle.ui-state-active { + box-shadow: 0px 0px 6px rgba(0, 0, 0, .4), 0px 0px 0px 6px rgba(255, 255, 255, .2); +} \ No newline at end of file