diff --git a/CONTROLS.md b/CONTROLS.md index b7b08c58c..fb45a948b 100644 --- a/CONTROLS.md +++ b/CONTROLS.md @@ -28,6 +28,7 @@ controls: [ 'settings', // Settings menu 'pip', // Picture-in-picture (currently Safari only) 'airplay', // Airplay (currently Safari only) + 'trim', // Trim Control 'download', // Show a download button with a link to either the current source or a custom URL you specify in your options 'fullscreen', // Toggle fullscreen ]; @@ -56,6 +57,10 @@ i18n: { unmute: 'Unmute', enableCaptions: 'Enable captions', disableCaptions: 'Disable captions', + enterTrim: 'Enter trim', + exitTrim: 'Exit trim', + trimStart: 'Trim Start', + trimEnd: 'Trim End', enterFullscreen: 'Enter fullscreen', exitFullscreen: 'Exit fullscreen', frameTitle: 'Player for {title}', diff --git a/README.md b/README.md index 5ec7ad88f..1c6d39011 100644 --- a/README.md +++ b/README.md @@ -462,6 +462,9 @@ player.fullscreen.enter(); // Enter fullscreen | `increaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. | | `decreaseVolume(step)` | Number | Increase volume by the specified step. If no parameter is passed, the default step will be used. | | `toggleCaptions(toggle)` | Boolean | Toggle captions display. If no parameter is passed, it will toggle based on current status. | +| `trim.enter()` | - | Enter Trimming tool. | +| `trim.exit()` | - | Exit Trimming tool. | +| `trim.toggle()` | - | Toggle Trimming tool. | | `fullscreen.enter()` | - | Enter fullscreen. If fullscreen is not supported, a fallback "full window/viewport" is used instead. | | `fullscreen.exit()` | - | Exit fullscreen. | | `fullscreen.toggle()` | - | Toggle fullscreen. | @@ -520,6 +523,8 @@ player.fullscreen.active; // false; | `pip`¹ | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ (on MacOS Sierra+ and iOS 10+) and Chrome 70+. | | `ratio` | ✓ | ✓ | Gets or sets the video aspect ratio. The setter accepts a string in the same format as the `ratio` option. | | `download` | ✓ | ✓ | Gets or sets the URL for the download button. The setter accepts a string containing a valid absolute URL. | +| `trim.startTime` | ✓ | ✓ | Gets or sets the trimming range start time. The setter accepts a float in seconds. | +| `trim.endTime` | ✓ | ✓ | Gets or sets the trimming range end time. The setter accepts a float in seconds. | 1. HTML5 only @@ -676,6 +681,9 @@ player.on('ready', event => { | `waiting` | Sent when the requested operation (such as playback) is delayed pending the completion of another operation (such as a seek). | | `emptied` | he media has become empty; for example, this event is sent if the media has already been loaded (or partially loaded), and the `load()` method is called to reload it. | | `cuechange` | Sent when a `TextTrack` has changed the currently displaying cues. | +| `entertrim` | Sent when the player enters the trimming tool. | +| `exittrim` | Sent when the player exits the trimming tool mode. | +| `trimchange` | Sent when the trimming region has changed. | | `error` | Sent when an error occurs. The element's `error` attribute contains more information. | ### YouTube only @@ -717,6 +725,7 @@ document then the shortcuts will work when any element has focus, apart from an | `F` | Toggle fullscreen | | `C` | Toggle captions | | `L` | Toggle loop | +| `T` | Toggle trimming tool | # Preview thumbnails @@ -728,6 +737,10 @@ You can see the example VTT files [here](https://cdn.plyr.io/static/demo/thumbs/ Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen). +# Trimming + +It's possible to create a trim region from your video using the trim mode. This is useful when creating clips or repeating loops. The 'trimchange' event will give you the start and end time of the selected region. + # Browser support Plyr supports the last 2 versions of most _modern_ browsers. diff --git a/dist/plyr.svg b/dist/plyr.svg deleted file mode 100644 index 62ab2579b..000000000 --- a/dist/plyr.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 74083e534..0e8f7bb0c 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -110,6 +110,11 @@ const defaults = { update: false, }, + // Trim settings + trim: { + enabled: true, // Allow trim? + }, + // Fullscreen settings fullscreen: { enabled: true, // Allow fullscreen? @@ -143,6 +148,7 @@ const defaults = { 'pip', 'airplay', // 'download', + // 'trim', 'fullscreen', ], settings: ['captions', 'quality', 'speed'], @@ -166,6 +172,10 @@ const defaults = { enableCaptions: 'Enable captions', disableCaptions: 'Disable captions', download: 'Download', + enterTrim: 'Enter trim', + exitTrim: 'Exit trim', + trimStart: 'Trim Start', + trimEnd: 'Trim End', enterFullscreen: 'Enter fullscreen', exitFullscreen: 'Exit fullscreen', frameTitle: 'Player for {title}', @@ -222,6 +232,7 @@ const defaults = { mute: null, volume: null, captions: null, + trim: null, download: null, fullscreen: null, pip: null, @@ -284,6 +295,11 @@ const defaults = { 'adsallcomplete', 'adsimpression', 'adsclick', + + // Trimming + 'entertrim', + 'exittrim', + 'trimchange', ], // Selectors @@ -305,6 +321,7 @@ const defaults = { mute: '[data-plyr="mute"]', captions: '[data-plyr="captions"]', download: '[data-plyr="download"]', + trim: '[data-plyr="trim"]', fullscreen: '[data-plyr="fullscreen"]', pip: '[data-plyr="pip"]', airplay: '[data-plyr="airplay"]', @@ -368,6 +385,16 @@ const defaults = { enabled: 'plyr--captions-enabled', active: 'plyr--captions-active', }, + trim: { + enabled: 'plyr--trim-enabled', + active: 'plyr--trim-active', + // Trim tool + trimTool: 'plyr__trim-tool', + leftThumb: 'plyr__trim-tool__thumb-left', + rightThumb: 'plyr__trim-tool__thumb-right', + timeContainer: 'plyr__trim-tool__time-container', + timeContainerShown: 'plyr__trim-tool__time-container--is-shown', + }, fullscreen: { enabled: 'plyr--fullscreen-enabled', fallback: 'plyr--fullscreen-fallback', diff --git a/src/js/controls.js b/src/js/controls.js index ff20982ee..f1f06afef 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -63,6 +63,7 @@ const controls = { airplay: getElement.call(this, this.config.selectors.buttons.airplay), settings: getElement.call(this, this.config.selectors.buttons.settings), captions: getElement.call(this, this.config.selectors.buttons.captions), + trim: getElement.call(this, this.config.selectors.buttons.trim), fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen), }; @@ -228,6 +229,14 @@ const controls = { props.iconPressed = 'captions-on'; break; + case 'trim': + props.toggle = true; + props.label = 'enterTrim'; + props.labelPressed = 'exitTrim'; + props.icon = 'enter-trim'; + props.iconPressed = 'exit-trim'; + break; + case 'fullscreen': props.toggle = true; props.label = 'enterFullscreen'; @@ -1590,6 +1599,11 @@ const controls = { container.appendChild(createButton.call(this, 'download', attributes)); } + // Toggle trim button + if (control === 'trim') { + container.appendChild(createButton.call(this, 'trim', defaultAttributes)); + } + // Toggle fullscreen button if (control === 'fullscreen') { container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes)); diff --git a/src/js/listeners.js b/src/js/listeners.js index 48734bcf8..eb59877e4 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -131,6 +131,11 @@ class Listeners { player.rewind(); break; + case 84: + // T key + player.trim.toggle(); + break; + case 70: // F key player.fullscreen.toggle(); @@ -620,6 +625,16 @@ class Listeners { 'download', ); + // Trim toggle + this.bind( + elements.buttons.trim, + 'click', + () => { + player.trim.toggle(); + }, + 'trim', + ); + // Fullscreen toggle this.bind( elements.buttons.fullscreen, @@ -802,6 +817,24 @@ class Listeners { } }); + // Move trim handles if selected + this.bind(elements.controls, 'mousemove touchmove', event => { + const { trim } = player; + + if (trim && trim.editing) { + trim.setTrimLength(event); + } + }); + + // Stop trimming when handle is no longer selected + this.bind(elements.controls, 'mouseup touchend', event => { + const { trim } = player; + + if (trim && trim.editing) { + trim.setEditing(event); + } + }); + // Polyfill for lower fill in for webkit if (browser.isWebkit) { Array.from(getElements.call(player, 'input[type="range"]')).forEach((element) => { diff --git a/src/js/plugins/trim.js b/src/js/plugins/trim.js new file mode 100644 index 000000000..08b03880f --- /dev/null +++ b/src/js/plugins/trim.js @@ -0,0 +1,377 @@ +// ========================================================================== +// Plyr Trim control +// ========================================================================== + +import { createElement, toggleClass, toggleHidden } from '../utils/elements'; +import { on, triggerEvent } from '../utils/events'; +import i18n from '../utils/i18n'; +import is from '../utils/is'; +import { clamp } from '../utils/numbers'; +import { extend } from '../utils/objects'; +import { formatTime } from '../utils/time'; + +class Trim { + constructor(player) { + // Keep reference to parent + this.player = player; + this.config = player.config.trim; + this.loaded = false; + this.trimming = false; + this.editing = false; + this.defaultTrimLength = 20; // Trim length in percent + this.startTime = 0; + this.endTime = 0; + this.timeUpdateFunction = this.timeUpdate.bind(this); + this.elements = { + bar: {}, + }; + + this.load(); + } + + // Determine if trim is enabled + get enabled() { + const { config } = this; + return config.enabled && this.player.isHTML5 && this.player.isVideo; + } + + // Get active state + get active() { + if (!this.enabled) { + return false; + } + + return this.trimming; + } + + // Get the current trim time + get trimTime() { + return { startTime: this.startTime, endTime: this.endTime }; + } + + load() { + // Handle event (incase user presses escape etc) + on.call(this.player, document, () => { + this.onChange(); + }); + + // Update the UI + this.update(); + + // Setup player listeners + this.listeners(); + } + + // Store the trim start time in seconds + setStartTime(percentage) { + this.startTime = this.player.media.duration * (parseFloat(percentage) / 100); + } + + // Store the trim end time in seconds + setEndTime(percentage) { + this.endTime = this.player.media.duration * (parseFloat(percentage) / 100); + } + + // Show the trim toolbar on the timeline + showTrimTool() { + if (is.empty(this.elements.bar)) { + this.createTrimTool(); + } + toggleHidden(this.elements.bar, false); + } + + // Hide the trim toolbar from the timeline + hideTrimTool() { + toggleHidden(this.elements.bar, true); + } + + // Add trim toolbar to the timeline + createTrimTool() { + const seekElement = this.player.elements.progress; + if (is.element(seekElement) && this.loaded) { + this.createTrimBar(seekElement); + this.createTrimBarThumbs(); + /* Only display the thumb time when displaying preview thumbnails as we don't want to display the whole thumbnail but + want to keep the same styling */ + if (this.player.config.previewThumbnails.enabled) { + this.createThumbTime(); + } + } + } + + // Add trim bar to the timeline + createTrimBar(seekElement) { + // Set the trim bar from the current seek time percentage to x percent after and limit the end percentage to 100% + const start = this.player.elements.inputs.seek.value; + const end = Math.min(parseFloat(start) + this.defaultTrimLength, 100); + + // Store the start and end video percentages in seconds + this.setStartTime(start); + this.setEndTime(end); + + this.elements.bar = createElement('span', { + class: this.player.config.classNames.trim.trimTool, + }); + + this.elements.bar.style.left = `${start.toString()}%`; + this.elements.bar.style.width = `${end - start.toString()}%`; + seekElement.appendChild(this.elements.bar); + + triggerEvent.call(this.player, this.player.media, 'trimchange', false, this.trimTime); + } + + // Add trim length thumbs to the timeline + createTrimBarThumbs() { + const { trim } = this.player.config.classNames; + + // Create the trim bar thumb elements + this.elements.bar.leftThumb = createElement( + 'span', + extend({ + class: trim.leftThumb, + role: 'slider', + 'aria-valuemin': 0, + 'aria-valuemax': this.player.duration, + 'aria-valuenow': this.startTime, + 'aria-valuetext': formatTime(this.startTime), + 'aria-label': i18n.get('trimStart', this.player.config), + }), + ); + + // Create the trim bar thumb elements + this.elements.bar.rightThumb = createElement( + 'span', + extend({ + class: trim.rightThumb, + role: 'slider', + 'aria-valuemin': 0, + 'aria-valuemax': this.player.duration, + 'aria-valuenow': this.endTime, + 'aria-valuetext': formatTime(this.endTime), + 'aria-label': i18n.get('trimEnd', this.player.config), + }), + ); + + // Add the thumbs to the bar + this.elements.bar.appendChild(this.elements.bar.leftThumb); + this.elements.bar.appendChild(this.elements.bar.rightThumb); + + // Add listens for trim thumb (handle) selection + this.player.listeners.bind(this.elements.bar.leftThumb, 'mousedown touchstart', event => { + if (this.elements.bar) { + this.setEditing(event); + } + }); + + // Listen for trim thumb (handle) selection + this.player.listeners.bind(this.elements.bar.rightThumb, 'mousedown touchstart', event => { + if (this.elements.bar) { + this.setEditing(event); + } + }); + } + + createThumbTime() { + // Create HTML element, parent+span: time text (e.g., 01:32:00) + this.elements.bar.leftThumb.timeContainer = createElement('div', { + class: this.player.config.classNames.trim.timeContainer, + }); + + this.elements.bar.rightThumb.timeContainer = createElement('div', { + class: this.player.config.classNames.trim.timeContainer, + }); + + // Append the time element to the container + this.elements.bar.leftThumb.timeContainer.time = createElement('span', {}, formatTime(this.startTime)); + this.elements.bar.leftThumb.timeContainer.appendChild(this.elements.bar.leftThumb.timeContainer.time); + this.elements.bar.rightThumb.timeContainer.time = createElement('span', {}, formatTime(this.endTime)); + this.elements.bar.rightThumb.timeContainer.appendChild(this.elements.bar.rightThumb.timeContainer.time); + + // Append the time container to the bar + this.elements.bar.leftThumb.appendChild(this.elements.bar.leftThumb.timeContainer); + this.elements.bar.rightThumb.appendChild(this.elements.bar.rightThumb.timeContainer); + } + + setEditing(event) { + const { leftThumb, rightThumb } = this.player.config.classNames.trim; + const { type, target } = event; + + if ((type === 'mouseup' || type === 'touchend') && this.editing === leftThumb) { + this.editing = null; + this.toggleTimeContainer(this.elements.bar.leftThumb, false); + triggerEvent.call(this.player, this.player.media, 'trimchange', false, this.trimTime); + } else if ((type === 'mouseup' || type === 'touchend') && this.editing === rightThumb) { + this.editing = null; + this.toggleTimeContainer(this.elements.bar.rightThumb, false); + triggerEvent.call(this.player, this.player.media, 'trimchange', false, this.trimTime); + } else if ((type === 'mousedown' || type === 'touchstart') && target.classList.contains(leftThumb)) { + this.editing = leftThumb; + this.toggleTimeContainer(this.elements.bar.leftThumb, true); + } else if ((type === 'mousedown' || type === 'touchstart') && target.classList.contains(rightThumb)) { + this.editing = rightThumb; + this.toggleTimeContainer(this.elements.bar.rightThumb, true); + } + } + + setTrimLength(event) { + if (!this.editing) return; + + // Calculate hover position + const clientRect = this.player.elements.progress.getBoundingClientRect(); + const xPos = event.type === 'touchmove' ? event.touches[0].pageX : event.pageX; + const percentage = clamp((100 / clientRect.width) * (xPos - clientRect.left), 0, 100); + // Get the current position of the trim tool bar + const { leftThumb, rightThumb } = this.player.config.classNames.trim; + const { bar } = this.elements; + + // Update the position of the trim range tool + if (this.editing === leftThumb) { + // Set the width to be in the position previously + bar.style.width = `${parseFloat(bar.style.width) - (percentage - parseFloat(bar.style.left))}%`; + // Increase the left thumb + bar.style.left = `${percentage}%`; + // Store and convert the start percentage to time + this.setStartTime(percentage); + // Prevent the end time being before the start time + if (this.startTime > this.endTime) { + this.setEndTime(percentage); + } + // Set the timestamp of the current trim handle position + if (bar.leftThumb.timeContainer) { + bar.leftThumb.timeContainer.time.innerText = formatTime(this.startTime); + } + // Update the aria-value and text + bar.leftThumb.setAttribute('aria-valuenow', this.startTime); + bar.leftThumb.setAttribute('aria-valuetext', formatTime(this.startTime)); + } else if (this.editing === rightThumb) { + // Prevent the end time to be before the start time + if (percentage <= parseFloat(bar.style.left)) { + return; + } + // Update the width of trim bar (right thumb) + bar.style.width = `${percentage - parseFloat(bar.style.left)}%`; + // Store and convert the start percentage to time + this.setEndTime(percentage); + // Set the timestamp of the current trim handle position + if (bar.rightThumb.timeContainer) { + bar.rightThumb.timeContainer.time.innerText = formatTime(this.endTime); + } + // Update the aria-value and text + bar.rightThumb.setAttribute('aria-valuenow', this.endTime); + bar.rightThumb.setAttribute('aria-valuetext', formatTime(this.endTime)); + } + } + + toggleTimeContainer(element, toggle = false) { + if (!element.timeContainer) { + return; + } + + const className = this.player.config.classNames.trim.timeContainerShown; + element.timeContainer.classList.toggle(className, toggle); + } + + // Set the seektime to the start of the trim timeline, if the seektime is outside of the region. + timeUpdate() { + if (!this.active || !this.trimming || !this.player.playing || this.editing) { + return; + } + + const { currentTime } = this.player; + if (currentTime < this.startTime || currentTime >= this.endTime) { + this.player.currentTime = this.startTime; + + if (currentTime >= this.endTime) { + this.player.pause(); + } + } + } + + listeners() { + /* Prevent the trim tool from being added until the player is in a playable state + If the user has pressed the trim tool before this event has fired, show the tool + */ + this.player.once('canplay', () => { + this.loaded = true; + if (this.trimming) { + this.createTrimTool(); + } + }); + + /* Listen for time changes so we can reset the seek point to within the clip. + Additionally, use the reference to the binding so we can remove and create a new instance of this listener + when we change source + */ + this.player.on('timeupdate', this.timeUpdateFunction); + } + + // On toggle of trim control, trigger event + onChange() { + if (!this.enabled) { + return; + } + + // Update toggle button + const button = this.player.elements.buttons.trim; + if (is.element(button)) { + button.pressed = this.active; + } + + // Trigger an event + triggerEvent.call(this.player, this.player.media, this.active ? 'entertrim' : 'exittrim', true, this.trimTime); + } + + // Update UI + update() { + if (this.enabled) { + this.player.debug.log(`trim enabled`); + } else { + this.player.debug.log('Trimming is not supported'); + } + + // Add styling hook to show button + toggleClass(this.player.elements.container, this.player.config.classNames.trim.enabled, this.enabled); + } + + destroy() { + // Remove the elements with listeners on + if (this.elements.bar && !is.empty(this.elements.bar)) { + this.elements.bar.remove(); + } + + this.player.off('timeupdate', this.timeUpdateFunction); + } + + // Enter trim tool + enter() { + if (!this.enabled) { + return; + } + this.trimming = true; + this.showTrimTool(); + + this.onChange(); + } + + // Exit trim tool + exit() { + if (!this.enabled) { + return; + } + this.trimming = false; + this.hideTrimTool(); + + this.onChange(); + } + + // Toggle state + toggle() { + if (!this.active) { + this.enter(); + } else { + this.exit(); + } + } +} + +export default Trim; diff --git a/src/js/plyr.d.ts b/src/js/plyr.d.ts index 4b332aeb5..75abf923c 100644 --- a/src/js/plyr.d.ts +++ b/src/js/plyr.d.ts @@ -251,6 +251,8 @@ declare namespace Plyr { seeked: PlyrEvent; ratechange: PlyrEvent; ended: PlyrEvent; + entertrim: PlyrEvent; + exitTrim: PlyrEvent; enterfullscreen: PlyrEvent; exitfullscreen: PlyrEvent; captionsenabled: PlyrEvent; @@ -455,6 +457,11 @@ declare namespace Plyr { */ captions?: CaptionOptions; + /** + * enabled: Toggles whether trimming should be enabled. + */ + trim?: TrimOptions; + /** * enabled: Toggles whether fullscreen should be enabled. fallback: Allow fallback to a full-window solution. * iosNative: whether to use native iOS fullscreen when entering fullscreen (no custom controls) @@ -537,6 +544,17 @@ declare namespace Plyr { seek?: boolean; } + interface TrimOptions { + enabled?: boolean; + active?: boolean; + trimTime?: TrimTime; + } + + interface TrimTime { + startTime: number; + endTime: number; + } + interface FullScreenOptions { enabled?: boolean; fallback?: boolean | 'force'; diff --git a/src/js/plyr.js b/src/js/plyr.js index 6c5078936..d183b9039 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -17,6 +17,7 @@ import Listeners from './listeners'; import media from './media'; import Ads from './plugins/ads'; import PreviewThumbnails from './plugins/preview-thumbnails'; +import Trim from './plugins/trim'; import source from './source'; import Storage from './storage'; import support from './support'; @@ -287,6 +288,9 @@ class Plyr { }); } + // Setup trim + this.trim = new Trim(this); + // Setup fullscreen this.fullscreen = new Fullscreen(this); diff --git a/src/js/source.js b/src/js/source.js index a62edbba7..297e6fffa 100644 --- a/src/js/source.js +++ b/src/js/source.js @@ -6,6 +6,7 @@ import { providers } from './config/types'; import html5 from './html5'; import media from './media'; import PreviewThumbnails from './plugins/preview-thumbnails'; +import Trim from './plugins/trim'; import support from './support'; import ui from './ui'; import { createElement, insertElement, removeElement } from './utils/elements'; @@ -147,6 +148,20 @@ const source = { } } + // Create new instance of trim plugin + if (this.trim && this.trim.loaded) { + this.trim.destroy(); + this.trim = null; + } + + // Create new instance if it is still enabled + if (this.config.trim.enabled) { + this.trim = new Trim(this); + } + + // Update trimming tool support + this.trim.update(); + // Update the fullscreen support this.fullscreen.update(); }, diff --git a/src/sass/components/controls.scss b/src/sass/components/controls.scss index 60ee774df..0568c0523 100644 --- a/src/sass/components/controls.scss +++ b/src/sass/components/controls.scss @@ -53,12 +53,14 @@ .plyr [data-plyr='captions'], .plyr [data-plyr='pip'], .plyr [data-plyr='airplay'], +.plyr [data-plyr='trim'], .plyr [data-plyr='fullscreen'] { display: none; } .plyr--captions-enabled [data-plyr='captions'], .plyr--pip-supported [data-plyr='pip'], .plyr--airplay-supported [data-plyr='airplay'], +.plyr--trim-enabled [data-plyr='trim'], .plyr--fullscreen-enabled [data-plyr='fullscreen'] { display: inline-block; } diff --git a/src/sass/components/trim.scss b/src/sass/components/trim.scss new file mode 100644 index 000000000..5f3bb6b09 --- /dev/null +++ b/src/sass/components/trim.scss @@ -0,0 +1,21 @@ +// -------------------------------------------------------------- +// Trim +// -------------------------------------------------------------- + +.plyr__trim { + animation: plyr-fade-in 0.3s ease; + bottom: 0; + color: $plyr-captions-text-color; + display: none; + font-size: $plyr-font-size-captions-small; + left: 0; + padding: $plyr-control-spacing; + position: absolute; + text-align: center; + transition: transform 0.4s ease-in-out; + width: 100%; + + span:empty { + display: none; + } +} diff --git a/src/sass/plugins/trim.scss b/src/sass/plugins/trim.scss new file mode 100644 index 000000000..4f3940f36 --- /dev/null +++ b/src/sass/plugins/trim.scss @@ -0,0 +1,78 @@ +// -------------------------------------------------------------- +// Trim Tool +// -------------------------------------------------------------- + +$plyr-trim-thumb-width: 7px !default; +$plyr-trim-thumb-height: calc((#{$plyr-range-thumb-active-shadow-width} * 2) + #{$plyr-range-thumb-height}) !default; +$plyr-trim-top-margin: calc((#{$plyr-range-thumb-active-shadow-width} * 2) + #{$plyr-range-thumb-height}) !default; +$plyr-trim-thumb-border: 2px !default; +$plyr-trim-thumb-border-radius: 5px !default; +$plyr-trim-box-shadow: inset 0 0 0 0.5px $plyr-color-main !default; +$plyr-trim-time-bg: rgba(0, 0, 0, 0.55); +$plyr-trim-time-radius: $plyr-tooltip-radius !default; +$plyr-trim-time-color: #fff; +$plyr-trim-time-font-size: $plyr-font-size-time !default; +$plyr-trim-time-padding: 3px 6px !default; +$plyr-trim-time-bottom-offset: 1px !default; + +// Trim Tool for the progress bar +.plyr__trim-tool { + background: currentColor; + display: block; + height: $plyr-range-track-height; + left: 0; + margin-top: calc((#{$plyr-range-track-height} / 2) * -1); + position: absolute; + top: 50%; + width: 3px; + + &__thumb-left { + left: -$plyr-trim-thumb-width; + } + + &__thumb-right { + right: -$plyr-trim-thumb-width; + } + + &__thumb-left, + &__thumb-right { + background: $plyr-color-main; + border: $plyr-trim-thumb-border; + border-radius: $plyr-trim-thumb-border-radius; + box-shadow: $plyr-trim-box-shadow; + height: $plyr-trim-thumb-height; + margin-top: calc((#{$plyr-range-thumb-height} / 2) * -1); + position: absolute; + touch-action: none; + user-select: none; + width: $plyr-trim-thumb-width; + z-index: 3; + + &:active { + @include plyr-range-thumb-active($plyr-color-main); + } + } + + &__time-container { + bottom: calc(#{$plyr-trim-thumb-height} + #{$plyr-trim-time-bottom-offset}); + left: 50%; + opacity: 0; + position: absolute; + transform: translate(-50%, -50%); + transition: opacity 0.3s ease; + white-space: nowrap; + z-index: 3; + + &--is-shown { + opacity: 1; + } + + span { + background-color: $plyr-trim-time-bg; + border-radius: ($plyr-trim-time-radius - 1px); + color: $plyr-trim-time-color; + font-size: $plyr-trim-time-font-size; + padding: $plyr-trim-time-padding; + } + } +} diff --git a/src/sass/plyr.scss b/src/sass/plyr.scss index 3a8ca0934..aed5c648d 100644 --- a/src/sass/plyr.scss +++ b/src/sass/plyr.scss @@ -30,6 +30,7 @@ $css-vars-use-native: true; @import 'components/badges'; @import 'components/captions'; +@import 'components/trim'; @import 'components/control'; @import 'components/controls'; @import 'components/menus'; @@ -47,6 +48,7 @@ $css-vars-use-native: true; @import 'plugins/ads'; @import 'plugins/preview-thumbnails/index'; +@import 'plugins/trim'; @import 'utils/animation'; @import 'utils/hidden'; diff --git a/src/sprite/plyr-enter-trim.svg b/src/sprite/plyr-enter-trim.svg new file mode 100644 index 000000000..f7947344a --- /dev/null +++ b/src/sprite/plyr-enter-trim.svg @@ -0,0 +1,5 @@ + +image/svg+xml + + + \ No newline at end of file diff --git a/src/sprite/plyr-exit-trim.svg b/src/sprite/plyr-exit-trim.svg new file mode 100644 index 000000000..bdb0d2cd8 --- /dev/null +++ b/src/sprite/plyr-exit-trim.svg @@ -0,0 +1,5 @@ + +image/svg+xml + + +