diff --git a/packages/pillarbox-playlist/README.md b/packages/pillarbox-playlist/README.md index 323e1cd..535b82d 100644 --- a/packages/pillarbox-playlist/README.md +++ b/packages/pillarbox-playlist/README.md @@ -31,7 +31,7 @@ import '@srgssr/pillarbox-playlist/ui'; const player = new Pillarbox('my-player', { plugins: { pillarboxPlaylist: { autoadvance: true, repeat: true }, - pillarboxPlaylistUI: { insertChildBefore: 'fullscreenToggle' } + pillarboxPlaylistUI: true } }); @@ -133,6 +133,49 @@ player.playlistPlugin().on('statechanged', ({ changes }) => { }); ``` +#### User Interface + +As seen before, this library contains an additional plugin that provides a customizable user +interface for the playlist. + +##### Options + +When initializing the playlist-ui plugin, you can pass an `options` object that configures the +behavior of the plugin. Here are the available options: + +| Option | Type | Default | Description | +|---------------------------------------------------------|---------|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `insertChildBefore` | String | `fullscreenToggle` | The control bar child name before which the playlist button should be inserted. | +| `pillarboxPlaylistMenuDialog` | Object | `{}` | Configuration for the modal dialog component. This can take any modal dialog options available in video.js. [See Video.js ModalDialog Documentation](https://docs.videojs.com/tutorial-modal-dialog.html) | +| `pillarboxPlaylistMenuDialog.pauseOnOpen` | Boolean | `false` | If true, the player will pause when the modal dialog is opened. | +| `pillarboxPlaylistMenuDialog.pillarboxPlaylistControls` | Object | `{}` | Configuration for the control buttons within the modal. You can define the order of the buttons, remove buttons you don't need, or add new ones. [See Video.js Component Children Documentation](https://videojs.com/guides/components/#component-children) | + +***Example Usage*** + +```javascript +import Pillarbox from '@srgssr/pillarbox-web'; +import '@srgssr/pillarbox-playlist'; +import '@srgssr/pillarbox-playlist/ui'; + +const player = new Pillarbox('my-player', { + plugins: { + // Include the playlist plugin + pillarboxPlaylist: true, + // Include the playlist UI plugin + pillarboxPlaylistUI: { + // Change the placement of the playlist button + inserChildBefore: 'subsCapsButton', + pillarboxPlaylistMenuDialog: { + // Force the playback to pause when the modal is opened + pauseOnOpen: true, + // Remove the shuffle button + pillarboxPlaylistControls: { pillarboxPlaylistShuffleButton: false } + } + } + } +}); +``` + ## Contributing For detailed contribution guidelines, refer to the main project’s [README file][main-readme]. Please diff --git a/packages/pillarbox-playlist/index.html b/packages/pillarbox-playlist/index.html index d456cc6..11f8a6c 100644 --- a/packages/pillarbox-playlist/index.html +++ b/packages/pillarbox-playlist/index.html @@ -40,8 +40,8 @@ autoplay: true, srgOptions: { dataProviderHost: ilHost }, plugins: { - pillarboxPlaylist: { autoadvance: true, repeat: true }, - pillarboxPlaylistUI: { insertChildBefore: 'fullscreenToggle' } + pillarboxPlaylist: { repeat: true }, + pillarboxPlaylistUI: true } }); diff --git a/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-controls.js b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-controls.js new file mode 100644 index 0000000..379d95b --- /dev/null +++ b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-controls.js @@ -0,0 +1,27 @@ +import videojs from 'video.js'; +import './pillarbox-playlist-next-item-button.js'; +import './pillarbox-previous-item-button.js'; +import './pillarbox-playlist-repeat-button.js'; +import './pillarbox-playlist-shuffle-button.js'; + +/** + * @ignore + * @type {typeof import('video.js/dist/types/component').default} + */ +const Component = videojs.getComponent('Component'); + +class PillarboxPlaylistControls extends Component { + +} + +PillarboxPlaylistControls.prototype.options_ = { + className: 'pbw-playlist-controls', + children: [ + 'pillarboxPlaylistRepeatButton', + 'pillarboxPlaylistShuffleButton', + 'pillarboxPlaylistPreviousItemButton', + 'pillarboxPlaylistNextItemButton' + ] +}; + +videojs.registerComponent('PillarboxPlaylistControls', PillarboxPlaylistControls); diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist-menu-item.js b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-menu-item.js similarity index 83% rename from packages/pillarbox-playlist/src/pillarbox-playlist-menu-item.js rename to packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-menu-item.js index c2abea0..be2bb4b 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist-menu-item.js +++ b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-menu-item.js @@ -2,9 +2,9 @@ import videojs from 'video.js'; /** * @ignore - * @type {typeof import('video.js/dist/types/menu/menu-item').default} + * @type {typeof import('../pillarbox-playlist-base.js').PillarboxPlaylistBaseButton} */ -const Button = videojs.getComponent('Button'); +const Button = videojs.getComponent('PillarboxPlaylistBaseButton'); /** * Class representing a playlist menu item in the Pillarbox plugin. @@ -26,15 +26,6 @@ class PillarboxPlaylistMenuItem extends Button { this.controlText(`${this.options_.item.data?.title} - ${videojs.formatTime(this.options_.item.data?.duration)}`); } - /** - * Gets the Pillarbox playlist associated with the player. - * - * @returns {import('/pillarbox-playlist.js').default} The Pillarbox playlist. - */ - playlist() { - return this.player().pillarboxPlaylist(); - } - /** * Handles the click event on the menu item. * diff --git a/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-modal.js b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-modal.js new file mode 100644 index 0000000..554376e --- /dev/null +++ b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-modal.js @@ -0,0 +1,129 @@ +import videojs from 'video.js'; +import './pillarbox-playlist-menu-item.js'; +import './pillarbox-playlist-controls.js'; + +/** + * @ignore + * @type {typeof import('./pillarbox-playlist-menu-item.js').default} + */ +const PillarboxPlaylistMenuItem = videojs.getComponent('PillarboxPlaylistMenuItem'); +/** + * @ignore + * @type {typeof import('video.js/dist/types/component').default} + */ +const Component = videojs.getComponent('Component'); +/** + * @ignore + * @type {typeof import('../pillarbox-playlist-base.js').PillarboxPlaylistBaseModal} + */ +const ModalDialog = videojs.getComponent('PillarboxPlaylistBaseModal'); + +/** + * PlaylistMenuDialog is a custom dialog that extends the ModalDialog class. + * It is designed to manage and display a playlist with various controls. + */ +class PlaylistMenuDialog extends ModalDialog { + /** + * Handles the 'statechanged' event when triggered by the playlist. This method + * serves as a proxy to the main `statechanged` handler, ensuring that additional + * logic can be executed or making it easier to detach the event listener later. + * + * @private + */ + onPlaylistStateChanged_ = ({ changes }) => { + if ('items' in changes) { + this.removeItems(); + this.renderItems(); + } + + if ('currentIndex' in changes) { + this.select(changes.currentIndex.to); + } + }; + + /** + * Creates an instance of PlaylistMenuDialog. + * + * @param {import('@srgssr/pillarbox-web').Player} player - The pillarbox player instance. + * @param {Object} options - Options for the dialog. + * @param {boolean} [options.pauseOnOpen=false] - If true, the player will pause when the modal dialog is opened. + * @param {Object} [options.pillarboxPlaylistControls={}] - Configuration for the control buttons within the modal. You can define the order of the buttons, remove buttons you don't need, or add new ones. + */ + constructor(player, options) { + options.temporary = false; + options = videojs.mergeOptions({ pauseOnOpen: false }, options); + + super(player, options); + + this.fill(); + this.addChild('PillarboxPlaylistControls', options.pillarboxPlaylistControls); + this.renderItems(); + this.playlist().on('statechanged', this.onPlaylistStateChanged_); + } + + buildCSSClass() { + return `pbw-playlist-dialog ${super.buildCSSClass()}`; + } + + /** + * Dispose of the PlaylistMenuDialog instance. + */ + dispose() { + this.playlist().off('statechanged', this.onPlaylistStateChanged_); + super.dispose(); + } + + /** + * Update the playlist item UI with the selected index. + * + * @param {number} index - The index of the item to select. + */ + select(index) { + const itemList = this.getChild('PillarboxPlaylistMenuItemsList'); + + itemList.children() + .filter(item => item.name() === 'PillarboxPlaylistMenuItem') + .map(item => item.getChild('PillarboxPlaylistMenuItemButton')) + .forEach(button => button.selected(index === button.options().index)); + } + + /** + * Remove all playlist items from the dialog. + */ + removeItems() { + this.removeChild(this.getChild('PillarboxPlaylistMenuItemsList')); + } + + /** + * Render the playlist items in the dialog. + */ + renderItems() { + const itemListEl = new Component(this.player(), { + name: 'PillarboxPlaylistMenuItemsList', + el: videojs.dom.createEl('ol', { + className: 'pbw-playlist-items' + }) + }); + + this.playlist().items.forEach((item, index) => { + const itemEl = new Component(this.player(), { + name: 'PillarboxPlaylistMenuItem', + el: videojs.dom.createEl('li', { + className: 'pbw-playlist-item' + }) + }); + + itemEl.addChild(new PillarboxPlaylistMenuItem(this.player(), { + item, + index, + name: 'PillarboxPlaylistMenuItemButton' + })); + + itemListEl.addChild(itemEl); + }); + + this.addChild(itemListEl); + } +} + +videojs.registerComponent('PillarboxPlaylistMenuDialog', PlaylistMenuDialog); diff --git a/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-next-item-button.js b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-next-item-button.js new file mode 100644 index 0000000..6d93ef5 --- /dev/null +++ b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-next-item-button.js @@ -0,0 +1,35 @@ +import videojs from 'video.js'; + +/** + * @ignore + * @type {typeof import('../pillarbox-playlist-base.js').PillarboxPlaylistBaseButton} + */ +const Button = videojs.getComponent('PillarboxPlaylistBaseButton'); + +/** + * The next item button for the playlist ui. When clicked moves to the + * next item in the playlist. + */ +class PillarboxPlaylistNextItemButton extends Button { + constructor(player, options) { + options = videojs.mergeOptions({ controlText: 'Next Item' }, options); + super(player, options); + this.setIcon('next-item'); + } + + ready() { + this.$('.vjs-icon-placeholder').classList.toggle(`vjs-icon-next-item`, true); + } + + /** + * Handles the click event on the button. + * + * @param {Event} event - The click event. + */ + handleClick(event) { + super.handleClick(event); + this.playlist().next(); + } +} + +videojs.registerComponent('PillarboxPlaylistNextItemButton', PillarboxPlaylistNextItemButton); diff --git a/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-repeat-button.js b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-repeat-button.js new file mode 100644 index 0000000..c04d323 --- /dev/null +++ b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-repeat-button.js @@ -0,0 +1,45 @@ +import videojs from 'video.js'; + +/** + * @ignore + * @type {typeof import('../pillarbox-playlist-base.js').PillarboxPlaylistBaseButton} + */ +const Button = videojs.getComponent('PillarboxPlaylistBaseButton'); + +/** + * The repeat button for the playlist ui. When clicked toggles the repeat mode + * of the playlist. + */ +class PillarboxPlaylistRepeatButton extends Button { + constructor(player, options) { + options = videojs.mergeOptions({ controlText: 'Repeat' }, options); + super(player, options); + this.setIcon('repeat'); + } + + ready() { + this.$('.vjs-icon-placeholder').classList.toggle(`vjs-icon-repeat`, true); + } + + /** + * Builds the CSS class string for the button. + * + * @returns {string} The CSS class string. + */ + buildCSSClass() { + return `${this.playlist().repeat ? 'vjs-selected' : ''} ${super.buildCSSClass()}`; + } + + /** + * Handles the click event on the button. + * + * @param {Event} event - The click event. + */ + handleClick(event) { + super.handleClick(event); + this.playlist().toggleRepeat(); + this.toggleClass('vjs-selected', this.playlist().repeat); + } +} + +videojs.registerComponent('PillarboxPlaylistRepeatButton', PillarboxPlaylistRepeatButton); diff --git a/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-shuffle-button.js b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-shuffle-button.js new file mode 100644 index 0000000..cd57dcb --- /dev/null +++ b/packages/pillarbox-playlist/src/components/modal/pillarbox-playlist-shuffle-button.js @@ -0,0 +1,35 @@ +import videojs from 'video.js'; + +/** + * @ignore + * @type {typeof import('../pillarbox-playlist-base.js').PillarboxPlaylistBaseButton} + */ +const Button = videojs.getComponent('PillarboxPlaylistBaseButton'); + +/** + * The shuffle button for the playlist ui. When clicked shuffles the items + * in the playlist. + */ +class PillarboxPlaylistShuffleButton extends Button { + constructor(player, options) { + options = videojs.mergeOptions({ controlText: 'Shuffle' }, options); + super(player, options); + this.setIcon('shuffle'); + } + + ready() { + this.$('.vjs-icon-placeholder').classList.toggle(`vjs-icon-shuffle`, true); + } + + /** + * Handles the click event on the button. + * + * @param {Event} event - The click event. + */ + handleClick(event) { + super.handleClick(event); + this.playlist().shuffle(); + } +} + +videojs.registerComponent('PillarboxPlaylistShuffleButton', PillarboxPlaylistShuffleButton); diff --git a/packages/pillarbox-playlist/src/components/modal/pillarbox-previous-item-button.js b/packages/pillarbox-playlist/src/components/modal/pillarbox-previous-item-button.js new file mode 100644 index 0000000..cbc53ea --- /dev/null +++ b/packages/pillarbox-playlist/src/components/modal/pillarbox-previous-item-button.js @@ -0,0 +1,35 @@ +import videojs from 'video.js'; + +/** + * @ignore + * @type {typeof import('../pillarbox-playlist-base.js').PillarboxPlaylistBaseButton} + */ +const Button = videojs.getComponent('PillarboxPlaylistBaseButton'); + +/** + * The previous item button for the playlist ui. When clicked moves to the + * previous item in the playlist. + */ +class PillarboxPlaylistPreviousItemButton extends Button { + constructor(player, options) { + options = videojs.mergeOptions({ controlText: 'Previous Item' }, options); + super(player, options); + this.setIcon('previous-item'); + } + + ready() { + this.$('.vjs-icon-placeholder').classList.toggle(`vjs-icon-previous-item`, true); + } + + /** + * Handles the click event on the button. + * + * @param {Event} event - The click event. + */ + handleClick(event) { + super.handleClick(event); + this.playlist().previous(); + } +} + +videojs.registerComponent('PillarboxPlaylistPreviousItemButton', PillarboxPlaylistPreviousItemButton); diff --git a/packages/pillarbox-playlist/src/components/pillarbox-playlist-base.js b/packages/pillarbox-playlist/src/components/pillarbox-playlist-base.js new file mode 100644 index 0000000..8d7b610 --- /dev/null +++ b/packages/pillarbox-playlist/src/components/pillarbox-playlist-base.js @@ -0,0 +1,46 @@ +import videojs from 'video.js'; + +/** + * @ignore + * @type {typeof import('video.js/dist/types/modal').default} + */ +const ModalDialog = videojs.getComponent('ModalDialog'); + +/** + * @abstract + */ +export class PillarboxPlaylistBaseModal extends ModalDialog { + /** + * Get the playlist instance associated with the player. + * + * @returns {import('packages/pillarbox-playlist/src/pillarbox-playlist.js').default} The playlist instance. + */ + playlist() { + return this.player().pillarboxPlaylist(); + } +} + +videojs.registerComponent('PillarboxPlaylistBaseModal', PillarboxPlaylistBaseModal); + +/** + * @ignore + * @type {typeof import('video.js/dist/types/button').default} + */ +const Button = videojs.getComponent('Button'); + +/** + * @abstract + */ +export class PillarboxPlaylistBaseButton extends Button { + /** + * Get the playlist instance associated with the player. + * + * @returns {import('packages/pillarbox-playlist/src/pillarbox-playlist.js').default} The playlist instance. + */ + playlist() { + return this.player().pillarboxPlaylist(); + } +} + + +videojs.registerComponent('PillarboxPlaylistBaseButton', PillarboxPlaylistBaseButton); diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist-button.js b/packages/pillarbox-playlist/src/components/pillarbox-playlist-button.js similarity index 61% rename from packages/pillarbox-playlist/src/pillarbox-playlist-button.js rename to packages/pillarbox-playlist/src/components/pillarbox-playlist-button.js index 7af9008..890187f 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist-button.js +++ b/packages/pillarbox-playlist/src/components/pillarbox-playlist-button.js @@ -1,12 +1,12 @@ import videojs from 'video.js'; -import './pillarbox-playlist-modal.js'; -import './lang'; +import './modal/pillarbox-playlist-modal.js'; +import '../lang/index.js'; /** * @ignore - * @type {typeof import('video.js/dist/types/button').default} + * @type {typeof import('../pillarbox-playlist-base.js').PillarboxPlaylistBaseButton} */ -const Button = videojs.getComponent('Button'); +const Button = videojs.getComponent('PillarboxPlaylistBaseButton'); /** * Class representing a button that opens the playlist menu. @@ -19,7 +19,7 @@ class PillarboxPlaylistButton extends Button { * * @private */ - _onPlaylistStateChanged = ({ changes }) => { + onPlaylistStateChanged_ = ({ changes }) => { if ('items' in changes) { this.toggleVisibility(); } @@ -32,34 +32,24 @@ class PillarboxPlaylistButton extends Button { * @param {Object} options - Options for the button. */ constructor(player, options) { + options = videojs.mergeOptions({ controlText: 'Playlist' }, options); super(player, options); - this.handleLanguagechange(); this.setIcon('chapters'); - player.ready(() => { - this.$('.vjs-icon-placeholder').classList.toggle('vjs-icon-chapters', true); - player.addChild('PlaylistMenuDialog', {pauseOnOpen: false}); - }); + this.playlist().on('statechanged', this.onPlaylistStateChanged_); + } - this.playlist().on('statechanged', this._onPlaylistStateChanged); + ready() { + this.$('.vjs-icon-placeholder').classList.toggle('vjs-icon-chapters', true); } /** * Dispose of the PillarboxPlaylistButton instance. */ dispose() { - this.playlist().off('statechanged', this._onPlaylistStateChanged); + this.playlist().off('statechanged', this.onPlaylistStateChanged_); super.dispose(); } - /** - * Get the playlist instance associated with the player. - * - * @returns {import('pillarbox-playlist.js').default} The playlist instance. - */ - playlist() { - return this.player().pillarboxPlaylist(); - } - /** * Builds the CSS class string for the button. * @@ -76,14 +66,7 @@ class PillarboxPlaylistButton extends Button { */ handleClick(event) { super.handleClick(event); - this.player().getChild('PlaylistMenuDialog').open(); - } - - /** - * Handles the language change event to update the control text. - */ - handleLanguagechange() { - this.controlText(this.localize('Playlist')); + this.player().getChild('PillarboxPlaylistMenuDialog').open(); } /** diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist-modal.js b/packages/pillarbox-playlist/src/pillarbox-playlist-modal.js deleted file mode 100644 index 5f946a6..0000000 --- a/packages/pillarbox-playlist/src/pillarbox-playlist-modal.js +++ /dev/null @@ -1,250 +0,0 @@ -import videojs from 'video.js'; -import './pillarbox-playlist-menu-item.js'; - -/** - * @ignore - * @type {typeof import('./pillarbox-playlist-menu-item.js').default} - */ -const PillarboxPlaylistMenuItem = videojs.getComponent('PillarboxPlaylistMenuItem'); -/** - * @ignore - * @type {typeof import('video.js/dist/types/button').default} - */ -const Button = videojs.getComponent('Button'); -/** - * @ignore - * @type {typeof import('video.js/dist/types/component').default} - */ -const Component = videojs.getComponent('Component'); -/** - * @ignore - * @type {typeof import('video.js/dist/types/modal-dialog').default} - */ -const ModalDialog = videojs.getComponent('ModalDialog'); - -/** - * PlaylistMenuDialog is a custom dialog that extends the ModalDialog class. - * It is designed to manage and display a playlist with various controls. - */ -class PlaylistMenuDialog extends ModalDialog { - /** - * Handles the 'statechanged' event when triggered by the playlist. This method - * serves as a proxy to the main `statechanged` handler, ensuring that additional - * logic can be executed or making it easier to detach the event listener later. - * - * @private - */ - _onPlaylistStateChanged = ({ changes }) => { - if ('items' in changes) { - this.removeItems(); - this.renderItems(); - } - - if ('currentIndex' in changes) { - this.select(changes.currentIndex.to); - } - }; - - /** - * Creates an instance of PlaylistMenuDialog. - * - * @param {import('@srgssr/pillarbox-web').Player} player - The pillarbox player instance. - * @param {Object} options - Options for the dialog. - */ - constructor(player, options) { - options.temporary = false; - - super(player, options); - this.fill(); - this.renderComponent(); - this.playlist().on('statechanged', this._onPlaylistStateChanged); - } - - buildCSSClass() { - return `pbw-playlist-dialog ${super.buildCSSClass()}`; - } - - /** - * Dispose of the PlaylistMenuDialog instance. - */ - dispose() { - this.playlist().off('statechanged', this._onPlaylistStateChanged); - super.dispose(); - } - - /** - * Get the playlist instance associated with the player. - * - * @returns {import('pillarbox-playlist.js').default} The playlist instance. - */ - playlist() { - return this.player().pillarboxPlaylist(); - } - - /** - * Update the playlist item UI with the selected index. - * - * @param {number} index - The index of the item to select. - */ - select(index) { - const itemList = this.getChild('PillarboxPlaylistMenuItemsList'); - - itemList.children() - .filter(item => item.name() === 'PillarboxPlaylistMenuItem') - .map(item => item.getChild('PillarboxPlaylistMenuItemButton')) - .forEach(button => button.selected(index === button.options().index)); - } - - /** - * Remove all playlist items from the dialog. - */ - removeItems() { - this.removeChild(this.getChild('PillarboxPlaylistMenuItemsList')); - } - - /** - * Render the playlist items in the dialog. - */ - renderItems() { - const itemListEl = new Component(this.player(), { - name: 'PillarboxPlaylistMenuItemsList', - el: videojs.dom.createEl('ol', { - className: 'pbw-playlist-items' - }) - }); - - this.playlist().items.forEach((item, index) => { - const itemEl = new Component(this.player(), { - name: 'PillarboxPlaylistMenuItem', - el: videojs.dom.createEl('li', { - className: 'pbw-playlist-item' - }) - }); - - itemEl.addChild(new PillarboxPlaylistMenuItem(this.player(), { - item, index, name: 'PillarboxPlaylistMenuItemButton' - })); - - itemListEl.addChild(itemEl); - }); - - - this.addChild(itemListEl); - } - - /** - * Create the playlist control buttons. - * - * @returns {Component} The component containing the playlist control buttons. - */ - createControls() { - const playlistControls = new Component(this.player(), { - name: 'PlaylistControls', - className: 'pbw-playlist-controls' - }); - - playlistControls.addChild(this.createRepeatButton()); - playlistControls.addChild(this.createSuffleButton()); - playlistControls.addChild(this.createPreviousItemButton()); - playlistControls.addChild(this.createNextItemButton()); - - return playlistControls; - } - - /** - * Create the "Previous Item" button. - * - * @returns {Button} The button to go to the previous item in the playlist. - */ - createPreviousItemButton() { - return this.setButtonIcon(new Button(this.player(), { - name: 'PreviousItemButton', - controlText: this.localize('Previous Item'), - clickHandler: () => this.playlist().previous() - }), 'previous-item'); - } - - /** - * Create the "Repeat" button. - * - * @returns {Button} The button to toggle repeat mode in the playlist. - */ - createRepeatButton() { - const repeatButton = this.setButtonIcon(new Button(this.player(), { - name: 'RepeatButton', - controlText: this.localize('Repeat'), - className: this.playlist().repeat ? 'vjs-selected' : '', - clickHandler: () => { - this.playlist().toggleRepeat(); - repeatButton.toggleClass('vjs-selected', this.playlist().repeat); - } - }), 'repeat'); - - return repeatButton; - } - - /** - * Create the "Shuffle" button. - * - * @returns {Button} The button to shuffle the playlist. - */ - createSuffleButton() { - return this.setButtonIcon(new Button(this.player(), { - name: 'ShuffleButton', - controlText: this.localize('Shuffle'), - clickHandler: () => this.playlist().shuffle() - }), 'shuffle'); - } - - /** - * Create the "Next Item" button. - * - * @returns {Button} The button to go to the next item in the playlist. - */ - createNextItemButton() { - return this.setButtonIcon(new Button(this.player(), { - name: 'NextItemButton', - controlText: this.localize('Next Item'), - clickHandler: () => this.playlist().next() - }), 'next-item'); - } - - /** - * Set the icon for a button. - * - * @param {Button} button The button to set the icon for. - * @param {string} iconName The name of the icon to set. - * - * @returns {Button} The button with the icon set. - */ - setButtonIcon(button, iconName) { - button.setIcon(iconName); - this.player().ready(() => { - button.$('.vjs-icon-placeholder').classList.toggle(`vjs-icon-${iconName}`, true); - }); - - return button; - } - - /** - * Render the component, including controls and playlist items. - */ - renderComponent() { - this.addChild(this.createControls()); - this.renderItems(); - } - - /** - * Handles the language change event to update the control text. - */ - handleLanguagechange() { - const controls = this.getChild('PlaylistControls'); - - controls.getChild('PreviousItemButton').controlText(this.localize('Previous Item')); - controls.getChild('RepeatButton').controlText(this.localize('Repeat')); - controls.getChild('ShuffleButton').controlText(this.localize('Shuffle')); - controls.getChild('NextItemButton').controlText(this.localize('Next Item')); - } -} - -videojs.registerComponent('PlaylistMenuDialog', PlaylistMenuDialog); diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js b/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js index 5951f52..eef15ab 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js +++ b/packages/pillarbox-playlist/src/pillarbox-playlist-ui.js @@ -1,11 +1,13 @@ import videojs from 'video.js'; -import './pillarbox-playlist-button.js'; +import './components/pillarbox-playlist-base.js'; +import './components/pillarbox-playlist-button.js'; /** * @ignore * @type {typeof import('video.js/dist/types/plugin').default} */ const Plugin = videojs.getPlugin('plugin'); +const log = videojs.log.createLogger('pillarbox-playlist-ui'); /** * A plugin that adds a playlist button to the control bar. @@ -17,27 +19,75 @@ class PillarboxPlaylistUI extends Plugin { * * @param {Player} player - The video.js player instance. * @param {Object} options - Plugin options. - * @param {string} [options.insertChildBefore] - The control bar child name before which the playlist button should be inserted. + * @param {string} [options.insertChildBefore='fullscreenToggle'] - The control bar child name before which the playlist button should be inserted. + * @param {Object} [options.pillarboxPlaylistButton={}] - Configuration for the playlist button. + * @param {Object} [options.pillarboxPlaylistMenuDialog={}] - Configuration for the modal dialog component. This can take any modal dialog options available in video.js. */ constructor(player, options) { super(player); - player.ready(function() { - if (!options.insertChildBefore) { - player.controlBar.addChild('PillarboxPlaylistButton'); + if (!player.usingPlugin('pillarboxPlaylist')) { + log.error('pillarbox-playlist plugin is required'); - return; - } + return; + } - const controlBar = player.controlBar; - const insertBefore = controlBar.getChild(options.insertChildBefore); - const index = controlBar.children().indexOf(insertBefore); + options = this.options_ = videojs.obj.merge(this.options_, options); - controlBar.addChild('PillarboxPlaylistButton', {}, index); + player.options({ + pillarboxPlaylistMenuDialog: options.pillarboxPlaylistMenuDialog ?? true, + controlBar: this.mergeControlBarOptions(player, options) }); } + + /** + * This function takes the existing control bar options from the player instance + * and merges them with the provided plugin options: + * + * - If the control bar is disabled in the player options, it returns the existing control bar + * settings without modifications. + * - Otherwise, it merges the default ControlBar options and the player's control bar + * options, and then handles the insertion of the pillarbox playlist button. + * into the control bar's children array. + * + * @param {Player} player - The player instance. + * @param {Object} options - The options to merge into the control bar options. + * @param {string} [options.insertChildBefore] - The name of the child before which to insert the pillarbox playlist button. + * + * @returns {Object|boolean} The merged control bar options, or false if the control bar is disabled. + */ + mergeControlBarOptions(player, options) { + if (player.options_.controlBar === false) return player.options_.controlBar; + + const controlBarOptions = videojs.obj.merge( + videojs.getComponent('ControlBar').prototype.options_, + player.options_.controlBar + ); + const index = controlBarOptions.children.findIndex( + item => item === options.insertChildBefore + ); + + if (options.insertChildBefore && index !== -1) { + const children = [...controlBarOptions.children]; + + children.splice(index, 0, 'pillarboxPlaylistButton'); + controlBarOptions.children = children; + + return controlBarOptions; + } + + controlBarOptions.pillarboxPlaylistButton = + options.pillarboxPlaylistButton ?? true; + + return controlBarOptions; + } + } +PillarboxPlaylistUI.prototype.options_ = { + insertChildBefore: 'fullscreenToggle' +}; + videojs.registerPlugin('pillarboxPlaylistUI', PillarboxPlaylistUI); export default PillarboxPlaylistUI; diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist.js b/packages/pillarbox-playlist/src/pillarbox-playlist.js index 432dccc..0cac8ab 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist.js +++ b/packages/pillarbox-playlist/src/pillarbox-playlist.js @@ -415,6 +415,11 @@ class PillarboxPlaylist extends Plugin { } } +PillarboxPlaylist.prototype.options_ = { + autoadvance: false, + repeat: false +}; + videojs.registerPlugin('pillarboxPlaylist', PillarboxPlaylist); export default PillarboxPlaylist; diff --git a/packages/pillarbox-playlist/test/pillarbox-playlist-ui.spec.js b/packages/pillarbox-playlist/test/pillarbox-playlist-ui.spec.js new file mode 100644 index 0000000..ba0f01e --- /dev/null +++ b/packages/pillarbox-playlist/test/pillarbox-playlist-ui.spec.js @@ -0,0 +1,253 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import pillarbox from '@srgssr/pillarbox-web'; +import '../src/pillarbox-playlist.js'; +import PillarboxPlaylistUi from '../src/pillarbox-playlist-ui.js'; + +const playlist = [ + { + sources: [{ src: 'first-source', type: 'test' }], + poster: 'first-poster', + data: { title: 'first-source', duration: 180 } + }, + { + sources: [{ src: 'second-source', type: 'test' }], + poster: 'second-poster', + data: { title: 'second-source', duration: 150 } + }, + { + sources: [{ src: 'third-source', type: 'test' }], + poster: 'third-poster', + data: { title: 'third-source', duration: 120 } + }, + { + sources: [{ src: 'fourth-source', type: 'test' }], + poster: 'fourth-poster', + data: { title: 'fourth-source', duration: 210 } + } +]; + + +window.HTMLMediaElement.prototype.load = () => {}; + +describe('PillarboxPlaylist', () => { + let videoElement, player; + + describe('Component initialisation', () => { + const controlBarChildIndex = (childName) => { + const children = player.controlBar.children(); + + return children.findIndex(child => child.name() === childName); + }; + + it('should be registered and attached to the player', () => { + player = pillarbox(videoElement, { + plugins: { + pillarboxPlaylist: true, + pillarboxPlaylistUI: true + } + }); + expect(pillarbox.getPlugin('pillarboxPlaylistUI')).toBe(PillarboxPlaylistUi); + expect(player.pillarboxPlaylistUI).toBeDefined(); + }); + + it('should not to initialize if the playlist plugin is not being used', async() => { + player = pillarbox(videoElement, { + plugins: { + pillarboxPlaylistUI: true + } + }); + + await new Promise((resolve) => player.ready(() => resolve())); + + expect(pillarbox.getPlugin('pillarboxPlaylistUI')).toBe(PillarboxPlaylistUi); + expect(player.pillarboxPlaylistUI).toBeDefined(); + expect(player.controlBar.pillarboxPlaylistButton).toBeUndefined(); + expect(player.pillarboxPlaylistMenuDialog).toBeUndefined(); + }); + + it('should merge user defined controlBar options', () => { + player = pillarbox(videoElement, { + controlBar: { + volumePanel: false + }, + plugins: { + pillarboxPlaylist: true, + pillarboxPlaylistUI: true + } + }); + + expect(pillarbox.getPlugin('pillarboxPlaylistUI')).toBe(PillarboxPlaylistUi); + expect(player.pillarboxPlaylistUI).toBeDefined(); + expect(player.controlBar.volumePanel).toBeUndefined(); + expect(player.controlBar.pillarboxPlaylistButton).toBeDefined(); + + const fullscreenBtnIndex = controlBarChildIndex('FullscreenToggle'); + const playlistBtnIndex = controlBarChildIndex('PillarboxPlaylistButton'); + + expect(playlistBtnIndex).toBe(fullscreenBtnIndex-1); + }); + + it('should insert the playlist button at the last position if no sibling was found', () => { + player = pillarbox(videoElement, { + controlBar: { + children: [ + 'playToggle', + 'volumePanel' + ] + }, + plugins: { + pillarboxPlaylist: true, + pillarboxPlaylistUI: true + } + }); + + expect(pillarbox.getPlugin('pillarboxPlaylistUI')).toBe(PillarboxPlaylistUi); + expect(player.pillarboxPlaylistUI).toBeDefined(); + expect(player.controlBar.pillarboxPlaylistButton).toBeDefined(); + + const playlistBtnIndex = controlBarChildIndex('PillarboxPlaylistButton'); + + expect(playlistBtnIndex).toBe(player.controlBar.children().length - 1); + }); + + it('should not insert the playlist button if the control bar is disabled', () => { + player = pillarbox(videoElement, { + controlBar:false, + plugins: { + pillarboxPlaylist: true, + pillarboxPlaylistUI: true + } + }); + + expect(pillarbox.getPlugin('pillarboxPlaylistUI')).toBe(PillarboxPlaylistUi); + expect(player.pillarboxPlaylistUI).toBeDefined(); + expect(player.controlBar).toBeUndefined(); + }); + }); + + + describe('User interface',() => { + let pillarboxPlaylist, dialog, controls, items, button; + + beforeEach(async() => { + player = pillarbox(videoElement, { + plugins: { + pillarboxPlaylist: true, + pillarboxPlaylistUI: true + } + }); + + pillarboxPlaylist = player.pillarboxPlaylist(); + + vi.spyOn(player, 'src').mockImplementation(() => {}); + vi.spyOn(player, 'poster').mockImplementation(() => {}); + pillarboxPlaylist.load(playlist); + + await new Promise((resolve) => player.ready(() => resolve())); + + button = player.controlBar.pillarboxPlaylistButton; + dialog = player.pillarboxPlaylistMenuDialog; + controls = dialog.getChild('PillarboxPlaylistControls'); + items = dialog.getChild('PillarboxPlaylistMenuItemsList').children() + .filter(item => item.name() === 'PillarboxPlaylistMenuItem') + .map(item => item.getChild('PillarboxPlaylistMenuItemButton')); + }); + + it('should modal should display the button and all the items in the playlist', () => { + // Then + expect(button.hasClass('vjs-hidden')).toBeFalsy(); + expect(items.length).toBe(4); + expect(items[pillarboxPlaylist.currentIndex].hasClass('vjs-selected')).toBeTruthy(); + items.filter((item, index) => index !== pillarboxPlaylist.currentIndex) + .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); + expect(dialog.hasClass('vjs-hidden')).toBeTruthy(); + }); + + it('should open the playlist dialog', ()=> { + // When + button.handleClick(); + + // Then + expect(dialog.hasClass('vjs-hidden')).toBeFalsy(); + }); + + it('should hide the button when the playlist is empty', ()=> { + // When + pillarboxPlaylist.clear(); + + // Then + expect(button.hasClass('vjs-hidden')).toBeTruthy(); + }); + + + it('should select an item when clicked', ()=> { + // Given + pillarboxPlaylist.select(0); + + // When + items[2].handleClick(); + + // Then + expect(pillarboxPlaylist.currentIndex).toBe(2); + expect(items[pillarboxPlaylist.currentIndex].hasClass('vjs-selected')).toBeTruthy(); + items.filter((item, index) => index !== pillarboxPlaylist.currentIndex) + .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); + }); + + it('should toggle repeat mode through the dialog controls', ()=> { + // When + controls.getChild('PillarboxPlaylistRepeatButton').handleClick(); + + // Then + expect(pillarboxPlaylist.repeat).toBeTruthy(); + }); + + it('should go the next item through the dialog controls', ()=> { + // Given + pillarboxPlaylist.select(0); + + // When + controls.getChild('PillarboxPlaylistNextItemButton').handleClick(); + + // Then + expect(pillarboxPlaylist.currentIndex).toBe(1); + expect(items[pillarboxPlaylist.currentIndex].hasClass('vjs-selected')).toBeTruthy(); + items.filter((item, index) => index !== pillarboxPlaylist.currentIndex) + .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); + }); + + it('should go the previous item through the dialog controls', ()=> { + // Given + pillarboxPlaylist.select(2); + + // When + controls.getChild('PillarboxPlaylistPreviousItemButton').handleClick(); + + // Then + expect(pillarboxPlaylist.currentIndex).toBe(1); + expect(items[pillarboxPlaylist.currentIndex].hasClass('vjs-selected')).toBeTruthy(); + items.filter((item, index) => index !== pillarboxPlaylist.currentIndex) + .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); + }); + + it('should shuffle the items through the dialog controls', ()=> { + // When + controls.getChild('PillarboxPlaylistShuffleButton').handleClick(); + + // Then + expect(playlist).not.toEqual(pillarboxPlaylist.item); + expect(playlist.length).toBe(pillarboxPlaylist.items.length); + }); + }); + + beforeAll(() => { + document.body.innerHTML = ''; + videoElement = document.querySelector('#test-video'); + }); + + afterEach(() => { + vi.resetAllMocks(); + player.dispose(); + }); + +}); diff --git a/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js b/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js index d804dbb..44ff89b 100644 --- a/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js +++ b/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js @@ -1,7 +1,14 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi +} from 'vitest'; import pillarbox from '@srgssr/pillarbox-web'; import PillarboxPlaylist from '../src/pillarbox-playlist.js'; -import '../src/pillarbox-playlist-button.js'; const playlist = [ { @@ -40,9 +47,6 @@ describe('PillarboxPlaylist', () => { beforeEach(() => { player = pillarbox(videoElement, { - controlBar: { - PillarboxPlaylistButton: true - }, plugins: { pillarboxPlaylist: true } @@ -520,109 +524,4 @@ describe('PillarboxPlaylist', () => { expect(pillarboxPlaylist.currentItem).toBeUndefined(); }); }); - - describe('User interface',() => { - let dialog, controls, items, button; - - beforeEach(async() => { - vi.spyOn(player, 'src').mockImplementation(() => {}); - vi.spyOn(player, 'poster').mockImplementation(() => {}); - pillarboxPlaylist.load(playlist); - - await new Promise((resolve) => player.ready(() => resolve())); - - button = player.controlBar.PillarboxPlaylistButton; - dialog = player.getChild('PlaylistMenuDialog'); - controls = dialog.getChild('PlaylistControls'); - items = dialog.getChild('PillarboxPlaylistMenuItemsList').children() - .filter(item => item.name() === 'PillarboxPlaylistMenuItem') - .map(item => item.getChild('PillarboxPlaylistMenuItemButton')); - }); - - it('should modal should display the button and all the items in the playlist', ()=> { - // Then - expect(button.hasClass('vjs-hidden')).toBeFalsy(); - expect(items.length).toBe(4); - expect(items[pillarboxPlaylist.currentIndex].hasClass('vjs-selected')).toBeTruthy(); - items.filter((item, index) => index !== pillarboxPlaylist.currentIndex) - .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); - expect(dialog.hasClass('vjs-hidden')).toBeTruthy(); - }); - - it('should open the playlist dialog', ()=> { - // When - button.handleClick(); - - // Then - expect(dialog.hasClass('vjs-hidden')).toBeFalsy(); - }); - - it('should hide the button when the playlist is empty', ()=> { - // When - pillarboxPlaylist.clear(); - - // Then - expect(button.hasClass('vjs-hidden')).toBeTruthy(); - }); - - - it('should select an item when clicked', ()=> { - // Given - pillarboxPlaylist.select(0); - - // When - items[2].handleClick(); - - // Then - expect(pillarboxPlaylist.currentIndex).toBe(2); - expect(items[pillarboxPlaylist.currentIndex].hasClass('vjs-selected')).toBeTruthy(); - items.filter((item, index) => index !== pillarboxPlaylist.currentIndex) - .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); - }); - - it('should toggle repeat mode through the dialog controls', ()=> { - // When - controls.getChild('RepeatButton').handleClick(); - - // Then - expect(pillarboxPlaylist.repeat).toBeTruthy(); - }); - - it('should go the next item through the dialog controls', ()=> { - // Given - pillarboxPlaylist.select(0); - - // When - controls.getChild('NextItemButton').handleClick(); - - // Then - expect(pillarboxPlaylist.currentIndex).toBe(1); - expect(items[pillarboxPlaylist.currentIndex].hasClass('vjs-selected')).toBeTruthy(); - items.filter((item, index) => index !== pillarboxPlaylist.currentIndex) - .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); - }); - - it('should go the previous item through the dialog controls', ()=> { - // Given - pillarboxPlaylist.select(2); - - // When - controls.getChild('PreviousItemButton').handleClick(); - - // Then - expect(pillarboxPlaylist.currentIndex).toBe(1); - expect(items[pillarboxPlaylist.currentIndex].hasClass('vjs-selected')).toBeTruthy(); - items.filter((item, index) => index !== pillarboxPlaylist.currentIndex) - .forEach((item) => expect(item.hasClass('vjs-selected')).toBeFalsy()); - }); - - it('should shuffle the items through the dialog controls', ()=> { - // When - controls.getChild('ShuffleButton').handleClick(); - - // Then - expect(playlist).not.toEqual(pillarboxPlaylist.item); - expect(playlist.length).toBe(pillarboxPlaylist.items.length); - }); - }); }); diff --git a/packages/skip-button/src/skip-button.js b/packages/skip-button/src/skip-button.js index 0d684c0..960d360 100644 --- a/packages/skip-button/src/skip-button.js +++ b/packages/skip-button/src/skip-button.js @@ -71,7 +71,7 @@ class SkipButton extends Button { const timeInterval = JSON.parse(this.activeInterval.text); const text = timeInterval.type === 'OPENING_CREDITS' ? 'Skip intro' : 'Skip credits'; - this.controlText(this.localize(text)); + this.controlText(text); this.show(); } }