17 |
18 |
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 | Briefly describe the issue.
3 | Include a [reduced test case](https://css-tricks.com/reduced-test-cases/).
4 |
5 | ## Steps to reproduce
6 | Explain in detail the exact steps necessary to reproduce the issue.
7 |
8 | 1.
9 | 2.
10 | 3.
11 |
12 | ## Results
13 | ### Expected
14 | Please describe what you expected to see.
15 |
16 | ### Actual
17 | Please describe what actually happened.
18 |
19 | ### Error output
20 | If there are any errors at all, please include them here.
21 |
22 | ## Additional Information
23 | Please include any additional information necessary here. Including the following:
24 |
25 | ### versions
26 | #### videojs
27 | what version of videojs does this occur with?
28 |
29 | #### browsers
30 | what browser are affected?
31 |
32 | #### OSes
33 | what platforms (operating systems and devices) are affected?
34 |
35 | ### plugins
36 | are any videojs plugins being used on the page? If so, please list them below.
37 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # CONTRIBUTING
2 |
3 | We welcome contributions from everyone!
4 |
5 | ## Getting Started
6 |
7 | Make sure you have Node.js 4.8 or higher and npm installed.
8 |
9 | 1. Fork this repository and clone your fork
10 | 1. Install dependencies: `npm install`
11 | 1. Run a development server: `npm start`
12 |
13 | ### Making Changes
14 |
15 | Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship.
16 |
17 | When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository.
18 |
19 | ### Running Tests
20 |
21 | Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma].
22 |
23 | - In all available and supported browsers: `npm test`
24 | - In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc.
25 | - While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local]
26 |
27 |
28 | [karma]: http://karma-runner.github.io/
29 | [local]: http://localhost:9999/test/
30 | [conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "videojs-playlist-ui",
3 | "version": "5.0.0",
4 | "author": "Brightcove, Inc.",
5 | "description": "A user interface for the videojs-playlist API",
6 | "license": "Apache-2.0",
7 | "keywords": [
8 | "playlist",
9 | "videojs",
10 | "videojs-plugin"
11 | ],
12 | "scripts": {
13 | "prebuild": "npm run clean",
14 | "build": "npm-run-all -p build:*",
15 | "build:css": "sass src/plugin.scss dist/videojs-playlist-ui.css --style compressed",
16 | "build:js": "rollup -c scripts/rollup.config.js",
17 | "build:lang": "vjslang --dir dist/lang",
18 | "clean": "shx rm -rf ./dist ./test/dist",
19 | "postclean": "shx mkdir -p ./dist ./test/dist",
20 | "docs": "doctoc README.md",
21 | "lint": "vjsstandard",
22 | "server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch",
23 | "start": "npm-run-all -p server watch",
24 | "pretest": "npm-run-all lint build",
25 | "test": "karma start scripts/karma.conf.js",
26 | "update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s",
27 | "version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md",
28 | "watch": "npm-run-all -p watch:*",
29 | "watch:css": "npm run build:css -- -w",
30 | "watch:js": "npm run build:js -- -w",
31 | "posttest": "shx cat test/dist/coverage/text.txt",
32 | "prepublishOnly": "npm run build && vjsverify --skip-es-check"
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "https://github.com/videojs/videojs-playlist-ui"
37 | },
38 | "dependencies": {
39 | "global": "^4.4.0"
40 | },
41 | "devDependencies": {
42 | "conventional-changelog-cli": "^2.2.2",
43 | "conventional-changelog-videojs": "^3.0.2",
44 | "doctoc": "^2.2.1",
45 | "husky": "^1.3.1",
46 | "karma": "^6.4.2",
47 | "lint-staged": "^13.2.2",
48 | "not-prerelease": "^1.0.1",
49 | "npm-merge-driver-install": "^3.0.0",
50 | "npm-run-all": "^4.1.5",
51 | "pkg-ok": "^2.2.0",
52 | "rollup": "^2.61.1",
53 | "sass": "^1.62.1",
54 | "shx": "^0.3.2",
55 | "sinon": "^6.1.5",
56 | "video.js": "^8.0.0",
57 | "videojs-generate-karma-config": "^8.0.1",
58 | "videojs-generate-rollup-config": "^7.0.0",
59 | "videojs-generator-verify": "^4.0.1",
60 | "videojs-languages": "^1.0.0",
61 | "videojs-playlist": "^5.1.0",
62 | "videojs-standard": "^9.0.1"
63 | },
64 | "peerDependencies": {
65 | "video.js": "^8.0.0",
66 | "videojs-playlist": "^5.1.0"
67 | },
68 | "main": "dist/videojs-playlist-ui.cjs.js",
69 | "module": "dist/videojs-playlist-ui.es.js",
70 | "generator-videojs-plugin": {
71 | "version": "7.3.2"
72 | },
73 | "vjsstandard": {
74 | "jsdoc": false,
75 | "ignore": [
76 | "dist",
77 | "docs",
78 | "test/dist"
79 | ]
80 | },
81 | "files": [
82 | "CONTRIBUTING.md",
83 | "dist/",
84 | "docs/",
85 | "index.html",
86 | "scripts/",
87 | "src/",
88 | "test/"
89 | ],
90 | "lint-staged": {
91 | "*.js": [
92 | "vjsstandard --fix",
93 | "git add"
94 | ],
95 | "README.md": [
96 | "npm run docs",
97 | "git add"
98 | ]
99 | },
100 | "husky": {
101 | "hooks": {
102 | "pre-commit": "lint-staged"
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/plugin.js:
--------------------------------------------------------------------------------
1 | import document from 'global/document';
2 | import videojs from 'video.js';
3 | import {version as VERSION} from '../package.json';
4 | import PlaylistMenu from './playlist-menu';
5 |
6 | // see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/pointerevents.js
7 | const supportsCssPointerEvents = (() => {
8 | const element = document.createElement('x');
9 |
10 | element.style.cssText = 'pointer-events:auto';
11 | return element.style.pointerEvents === 'auto';
12 | })();
13 |
14 | const defaults = {
15 | className: 'vjs-playlist',
16 | playOnSelect: false,
17 | supportsCssPointerEvents
18 | };
19 |
20 | const Plugin = videojs.getPlugin('plugin');
21 |
22 | /**
23 | * Initialize the plugin on a player.
24 | *
25 | * @param {Object} [options]
26 | * An options object.
27 | *
28 | * @param {HTMLElement} [options.el]
29 | * A DOM element to use as a root node for the playlist.
30 | *
31 | * @param {string} [options.className]
32 | * An HTML class name to use to find a root node for the playlist.
33 | *
34 | * @param {boolean} [options.playOnSelect = false]
35 | * If true, will attempt to begin playback upon selecting a new
36 | * playlist item in the UI.
37 | */
38 | class PlaylistUI extends Plugin {
39 |
40 | constructor(player, options) {
41 | super(player, options);
42 |
43 | if (!player.usingPlugin('playlist')) {
44 | player.log.error('videojs-playlist plugin is required by the videojs-playlist-ui plugin');
45 | return;
46 | }
47 |
48 | options = this.options_ = videojs.obj.merge(defaults, options);
49 |
50 | if (!videojs.dom.isEl(options.el)) {
51 | options.el = this.findRoot_(options.className);
52 | }
53 |
54 | // Expose the playlist menu component on the player as well as the plugin
55 | // This is a bit of an anti-pattern, but it's been that way forever and
56 | // there are likely to be integrations relying on it.
57 | this.playlistMenu = player.playlistMenu = new PlaylistMenu(player, options);
58 | }
59 |
60 | /**
61 | * Dispose the plugin.
62 | */
63 | dispose() {
64 | super.dispose();
65 | this.playlistMenu.dispose();
66 | }
67 |
68 | /**
69 | * Returns a boolean indicating whether an element has child elements.
70 | *
71 | * Note that this is distinct from whether it has child _nodes_.
72 | *
73 | * @param {HTMLElement} el
74 | * A DOM element.
75 | *
76 | * @return {boolean}
77 | * Whether the element has child elements.
78 | */
79 | hasChildEls_(el) {
80 | for (let i = 0; i < el.childNodes.length; i++) {
81 | if (videojs.dom.isEl(el.childNodes[i])) {
82 | return true;
83 | }
84 | }
85 | return false;
86 | }
87 |
88 | /**
89 | * Finds the first empty root element.
90 | *
91 | * @param {string} className
92 | * An HTML class name to search for.
93 | *
94 | * @return {HTMLElement}
95 | * A DOM element to use as the root for a playlist.
96 | */
97 | findRoot_(className) {
98 | const all = document.querySelectorAll('.' + className);
99 |
100 | for (let i = 0; i < all.length; i++) {
101 | if (!this.hasChildEls_(all[i])) {
102 | return all[i];
103 | }
104 | }
105 | }
106 | }
107 |
108 | videojs.registerPlugin('playlistUi', PlaylistUI);
109 |
110 | PlaylistUI.VERSION = VERSION;
111 |
112 | export default PlaylistUI;
113 |
--------------------------------------------------------------------------------
/example-custom-class.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Video.js Playlist UI - Using a Custom Class
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Video.js Playlist UI - Using a Custom Class
13 |
14 | You can see the Video.js Playlist UI plugin in action below. Look at the
15 | source of this page to see how to use it with your videos.
16 |
17 |
18 | When using a custom class, the plugin looks for the first element that
19 | matches the given class and uses that as a container for the list.
20 |
21 |
22 | Using this option means the default styles WILL NOT apply; so,
23 | you'll have to define your own. This may be desirable or not -
24 | depending on your implementation. This example has been left un-styled
25 | to demonstrate this fact.
26 |
14 | You can see the Video.js Playlist UI plugin in action below. Look at the
15 | source of this page to see how to use it with your videos.
16 |
17 |
18 | When using a custom element, the plugin uses the element that it was
19 | given as a container for the list.
20 |
21 |
22 | Using this option means the default styles MAY NOT apply (it
23 | depends on whether the element has the "vjs-playlist" class) so, you
24 | might have to define your own. This may be desirable or not -
25 | depending on your implementation. This example has been left un-styled
26 | to demonstrate this fact.
27 |
28 |
29 |
30 |
31 |
40 |
41 |
42 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # videojs-playlist-ui
2 |
3 | [](https://nodei.co/npm/videojs-playlist-ui/)
4 |
5 | A playlist video picker for video.js and videojs-playlist
6 |
7 | Maintenance Status: Stable
8 |
9 |
10 |
11 |
12 | - [Getting Started](#getting-started)
13 | - [Root Element](#root-element)
14 | - [Using Automatic Discovery (default, example)](#using-automatic-discovery-default-example)
15 | - [Using a Custom Class (example)](#using-a-custom-class-example)
16 | - [Using a Custom Element (example)](#using-a-custom-element-example)
17 | - [Other Options](#other-options)
18 | - [`className`](#classname)
19 | - [playOnSelect](#playonselect)
20 | - [Playlists and Advertisements](#playlists-and-advertisements)
21 |
22 |
23 |
24 |
25 | ## Getting Started
26 | Include the plugin script in your page, and a placeholder list element with the class `vjs-playlist` to house the playlist menu:
27 |
28 | ```html
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
50 | ```
51 |
52 | There's also a [working example](example.html) of the plugin you can check out if you're having trouble.
53 |
54 | ## Root Element
55 | Before this plugin will work at all, it needs an element in the DOM to which to attach itself. There are three ways to find or provide this element.
56 |
57 | > **NOTE:** In v2.x of this plugin, the root element was expected to be a list element (i.e., `` or `
`). As of v3.x, the plugin creates a list; so, this root element _must_ be a non-list container element (e.g., `
`).
58 |
59 | ### Using Automatic Discovery (default, [example](example.html))
60 | By default, the plugin will search for the first element in the DOM with the `vjs-playlist` class.
61 |
62 | To defend against problems caused by multiple playlist players on a page, the plugin will only use an element with the `vjs-playlist` class if that element has not been used by another player's playlist.
63 |
64 | ### Using a Custom Class ([example](example-custom-class.html))
65 | A custom `className` option can be passed to override the class the plugin will search for to find the root element. The same defense against multiple playlist players is reused in this case.
66 |
67 | ```js
68 | player.playlistUi({
69 | className: 'hello-world'
70 | });
71 | ```
72 |
73 | ### Using a Custom Element ([example](example-custom-element.html))
74 | A custom element can be passed using the `el` option to explicitly define a specific root element.
75 |
76 | ```js
77 | player.playlistUi({
78 | el: document.getElementById('hello-world')
79 | });
80 | ```
81 |
82 | ## Other Options
83 |
84 | The options passed to the plugin are passed to the internal `PlaylistMenu` [video.js Component][components]; so, you may pass in [any option][components-options] that is accepted by a component.
85 |
86 | In addition, the options object may contain the following specialized properties:
87 |
88 | #### `className`
89 | Type: `string`
90 | Default: `"vjs-playlist"`
91 |
92 | As mentioned [above](#using-a-custom-class), the name of the class to search for to populate the playlist menu.
93 |
94 | #### playOnSelect
95 | Type: `boolean`
96 | Default: `false`
97 |
98 | The default behavior is that the play state is expected to stay the same between videos. If the player is playing when switching playlist items, continue playing. If paused, stay paused.
99 |
100 | When this boolean is set to `true`, clicking on the playlist menu items will always play the video.
101 |
102 | ## Playlists and Advertisements
103 |
104 | The `PlaylistMenu` automatically adapts to ad integrations based on [videojs-contrib-ads][contrib-ads]. When a linear ad is being played, the menu will darken and stop responding to click or touch events. If you'd prefer to allow your viewers to change videos during ad playback, you can override this behavior through CSS. You will also need to make sure that your ad integration is properly cancelled and cleaned up before switching -- consult the documentation for your ad library for details on how to do that.
105 |
106 |
107 | [components]: https://videojs.com/guides/components/
108 | [components-options]: https://videojs.com/guides/components/#using-options
109 | [contrib-ads]: https://github.com/videojs/videojs-contrib-ads
110 |
--------------------------------------------------------------------------------
/src/playlist-menu-item.js:
--------------------------------------------------------------------------------
1 | import document from 'global/document';
2 | import videojs from 'video.js';
3 |
4 | const Component = videojs.getComponent('Component');
5 |
6 | const createThumbnail = function(thumbnail) {
7 | if (!thumbnail) {
8 | const placeholder = document.createElement('div');
9 |
10 | placeholder.className = 'vjs-playlist-thumbnail vjs-playlist-thumbnail-placeholder';
11 | return placeholder;
12 | }
13 |
14 | const picture = document.createElement('picture');
15 |
16 | picture.className = 'vjs-playlist-thumbnail';
17 |
18 | if (typeof thumbnail === 'string') {
19 | // simple thumbnails
20 | const img = document.createElement('img');
21 |
22 | img.loading = 'lazy';
23 | img.src = thumbnail;
24 | img.alt = '';
25 | picture.appendChild(img);
26 | } else {
27 | // responsive thumbnails
28 |
29 | // additional variations of a are specified as
30 | // elements
31 | for (let i = 0; i < thumbnail.length - 1; i++) {
32 | const variant = thumbnail[i];
33 | const source = document.createElement('source');
34 |
35 | // transfer the properties of each variant onto a
36 | for (const prop in variant) {
37 | source[prop] = variant[prop];
38 | }
39 | picture.appendChild(source);
40 | }
41 |
42 | // the default version of a is specified by an
43 | const variant = thumbnail[thumbnail.length - 1];
44 | const img = document.createElement('img');
45 |
46 | img.loading = 'lazy';
47 | img.alt = '';
48 | for (const prop in variant) {
49 | img[prop] = variant[prop];
50 | }
51 | picture.appendChild(img);
52 | }
53 | return picture;
54 | };
55 |
56 | class PlaylistMenuItem extends Component {
57 |
58 | constructor(player, playlistItem, settings) {
59 | if (!playlistItem.item) {
60 | throw new Error('Cannot construct a PlaylistMenuItem without an item option');
61 | }
62 |
63 | playlistItem.showDescription = settings.showDescription;
64 | super(player, playlistItem);
65 | this.item = playlistItem.item;
66 |
67 | this.playOnSelect = settings.playOnSelect;
68 |
69 | this.emitTapEvents();
70 |
71 | this.on(['click', 'tap'], this.switchPlaylistItem_);
72 | this.on('keydown', this.handleKeyDown_);
73 |
74 | }
75 |
76 | handleKeyDown_(event) {
77 | // keycode 13 is
78 | // keycode 32 is
79 | if (event.which === 13 || event.which === 32) {
80 | this.switchPlaylistItem_();
81 | }
82 | }
83 |
84 | switchPlaylistItem_(event) {
85 | this.player_.playlist.currentItem(this.player_.playlist().indexOf(this.item));
86 | if (this.playOnSelect) {
87 | this.player_.play();
88 | }
89 | }
90 |
91 | createEl() {
92 | const li = document.createElement('li');
93 | const item = this.options_.item;
94 | const showDescription = this.options_.showDescription;
95 |
96 | if (typeof item.data === 'object') {
97 | const dataKeys = Object.keys(item.data);
98 |
99 | dataKeys.forEach(key => {
100 | const value = item.data[key];
101 |
102 | li.dataset[key] = value;
103 | });
104 | }
105 |
106 | li.className = 'vjs-playlist-item';
107 | li.setAttribute('tabIndex', 0);
108 |
109 | // Thumbnail image
110 | this.thumbnail = createThumbnail(item.thumbnail);
111 | li.appendChild(this.thumbnail);
112 |
113 | // Duration
114 | if (item.duration) {
115 | const duration = document.createElement('time');
116 | const time = videojs.time.formatTime(item.duration);
117 |
118 | duration.className = 'vjs-playlist-duration';
119 | duration.setAttribute('datetime', 'PT0H0M' + item.duration + 'S');
120 | duration.appendChild(document.createTextNode(time));
121 | li.appendChild(duration);
122 | }
123 |
124 | // Now playing
125 | const nowPlayingEl = document.createElement('span');
126 | const nowPlayingText = this.localize('Now Playing');
127 |
128 | nowPlayingEl.className = 'vjs-playlist-now-playing-text';
129 | nowPlayingEl.appendChild(document.createTextNode(nowPlayingText));
130 | nowPlayingEl.setAttribute('title', nowPlayingText);
131 | this.thumbnail.appendChild(nowPlayingEl);
132 |
133 | // Title container contains title and "up next"
134 | const titleContainerEl = document.createElement('div');
135 |
136 | titleContainerEl.className = 'vjs-playlist-title-container';
137 | this.thumbnail.appendChild(titleContainerEl);
138 |
139 | // Up next
140 | const upNextEl = document.createElement('span');
141 | const upNextText = this.localize('Up Next');
142 |
143 | upNextEl.className = 'vjs-up-next-text';
144 | upNextEl.appendChild(document.createTextNode(upNextText));
145 | upNextEl.setAttribute('title', upNextText);
146 | titleContainerEl.appendChild(upNextEl);
147 |
148 | // Video title
149 | const titleEl = document.createElement('cite');
150 | const titleText = item.name || this.localize('Untitled Video');
151 |
152 | titleEl.className = 'vjs-playlist-name';
153 | titleEl.appendChild(document.createTextNode(titleText));
154 | titleEl.setAttribute('title', titleText);
155 | titleContainerEl.appendChild(titleEl);
156 |
157 | // Populate thumbnails alt with the video title
158 | this.thumbnail.getElementsByTagName('img').alt = titleText;
159 |
160 | // We add thumbnail video description only if specified in playlist options
161 | if (showDescription) {
162 | const descriptionEl = document.createElement('div');
163 | const descriptionText = item.description || '';
164 |
165 | descriptionEl.className = 'vjs-playlist-description';
166 | descriptionEl.appendChild(document.createTextNode(descriptionText));
167 | descriptionEl.setAttribute('title', descriptionText);
168 | titleContainerEl.appendChild(descriptionEl);
169 | }
170 |
171 | return li;
172 | }
173 | }
174 |
175 | videojs.registerComponent('PlaylistMenuItem', PlaylistMenuItem);
176 |
177 | export default PlaylistMenuItem;
178 |
--------------------------------------------------------------------------------
/example-custom-data.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Video.js Playlist UI - Default Implementation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Video.js Playlist UI - Default Implementation
13 |
14 | You can see the Video.js Playlist UI plugin in action below. Look at the
15 | source of this page to see how to use it with your videos.
16 |
17 |
18 | In the default configuration, the plugin looks for the first element that
19 | has the class "vjs-playlist" and uses that as a container for the list.
20 |
14 | You can see the Video.js Playlist UI plugin in action below. Look at the
15 | source of this page to see how to use it with your videos.
16 |
17 |
18 | In the default configuration, the plugin looks for the first element that
19 | has the class "vjs-playlist" and uses that as a container for the list.
20 |
21 |
22 | When using the `horizontal` option, you'll want to set a width on the
23 | "vjs-playlist" element (or the class that gets added,
24 | "vjs-playlist-horizontal").
25 |
14 | You can see the Video.js Playlist UI plugin in action below. Look at the
15 | source of this page to see how to use it with your videos.
16 |
17 |
18 | In the default configuration, the plugin looks for the first element that
19 | has the class "vjs-playlist" and uses that as a container for the list.
20 |
21 |
22 |
23 |
24 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
52 |
53 |
54 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
186 |
187 |
188 |
--------------------------------------------------------------------------------
/src/playlist-menu.js:
--------------------------------------------------------------------------------
1 | import document from 'global/document';
2 | import videojs from 'video.js';
3 | import PlaylistMenuItem from './playlist-menu-item';
4 |
5 | // we don't add `vjs-playlist-now-playing` in addSelectedClass
6 | // so it won't conflict with `vjs-icon-play
7 | // since it'll get added when we mouse out
8 | const addSelectedClass = function(el) {
9 | el.addClass('vjs-selected');
10 | };
11 | const removeSelectedClass = function(el) {
12 | el.removeClass('vjs-selected');
13 |
14 | if (el.thumbnail) {
15 | videojs.dom.removeClass(el.thumbnail, 'vjs-playlist-now-playing');
16 | }
17 | };
18 |
19 | const upNext = function(el) {
20 | el.addClass('vjs-up-next');
21 | };
22 |
23 | const notUpNext = function(el) {
24 | el.removeClass('vjs-up-next');
25 | };
26 |
27 | const Component = videojs.getComponent('Component');
28 |
29 | class PlaylistMenu extends Component {
30 |
31 | constructor(player, options) {
32 | super(player, options);
33 | this.items = [];
34 |
35 | if (options.horizontal) {
36 | this.addClass('vjs-playlist-horizontal');
37 | } else {
38 | this.addClass('vjs-playlist-vertical');
39 | }
40 |
41 | // If CSS pointer events aren't supported, we have to prevent
42 | // clicking on playlist items during ads with slightly more
43 | // invasive techniques. Details in the stylesheet.
44 | if (options.supportsCssPointerEvents) {
45 | this.addClass('vjs-csspointerevents');
46 | }
47 |
48 | this.createPlaylist_();
49 |
50 | if (!videojs.browser.TOUCH_ENABLED) {
51 | this.addClass('vjs-mouse');
52 | }
53 |
54 | this.on(player, ['loadstart', 'playlistchange', 'playlistsorted'], (e) => {
55 |
56 | // The playlistadd and playlistremove events are handled separately. These
57 | // also fire the playlistchange event with an `action` property, so can
58 | // exclude them here.
59 | if (e.type === 'playlistchange' && ['add', 'remove'].includes(e.action)) {
60 | return;
61 | }
62 | this.update();
63 | });
64 |
65 | this.on(player, ['playlistadd'], (e) => this.addItems_(e.index, e.count));
66 | this.on(player, ['playlistremove'], (e) => this.removeItems_(e.index, e.count));
67 |
68 | // Keep track of whether an ad is playing so that the menu
69 | // appearance can be adapted appropriately
70 | this.on(player, 'adstart', () => {
71 | this.addClass('vjs-ad-playing');
72 | });
73 |
74 | this.on(player, 'adend', () => {
75 | this.removeClass('vjs-ad-playing');
76 | });
77 |
78 | this.on('dispose', () => {
79 | this.empty_();
80 | player.playlistMenu = null;
81 | });
82 |
83 | this.on(player, 'dispose', () => {
84 | this.dispose();
85 | });
86 | }
87 |
88 | createEl() {
89 | return videojs.dom.createEl('div', {className: this.options_.className});
90 | }
91 |
92 | empty_() {
93 | if (this.items && this.items.length) {
94 | this.items.forEach(i => i.dispose());
95 | this.items.length = 0;
96 | }
97 | }
98 |
99 | createPlaylist_() {
100 | const playlist = this.player_.playlist() || [];
101 | let list = this.el_.querySelector('.vjs-playlist-item-list');
102 | let overlay = this.el_.querySelector('.vjs-playlist-ad-overlay');
103 |
104 | if (!list) {
105 | list = document.createElement('ol');
106 | list.className = 'vjs-playlist-item-list';
107 | this.el_.appendChild(list);
108 | }
109 |
110 | this.empty_();
111 |
112 | // create new items
113 | for (let i = 0; i < playlist.length; i++) {
114 | const item = new PlaylistMenuItem(this.player_, {
115 | item: playlist[i]
116 | }, this.options_);
117 |
118 | this.items.push(item);
119 | list.appendChild(item.el_);
120 | }
121 |
122 | // Inject the ad overlay. We use this element to block clicks during ad
123 | // playback and darken the menu to indicate inactivity
124 | if (!overlay) {
125 | overlay = document.createElement('li');
126 | overlay.className = 'vjs-playlist-ad-overlay';
127 | list.appendChild(overlay);
128 | } else {
129 | // Move overlay to end of list
130 | list.appendChild(overlay);
131 | }
132 |
133 | // select the current playlist item
134 | const selectedIndex = this.player_.playlist.currentItem();
135 |
136 | if (this.items.length && selectedIndex >= 0) {
137 | addSelectedClass(this.items[selectedIndex]);
138 |
139 | const thumbnail = this.items[selectedIndex].$('.vjs-playlist-thumbnail');
140 |
141 | if (thumbnail) {
142 | videojs.dom.addClass(thumbnail, 'vjs-playlist-now-playing');
143 | }
144 | }
145 | }
146 |
147 | /**
148 | * Adds a number of playlist items to the UI.
149 | *
150 | * Each item that was added to the underlying playlist in a certain range
151 | * has a new PlaylistMenuItem created for it.
152 | *
153 | * @param {number} index
154 | * The index at which to start adding items.
155 | *
156 | * @param {number} count
157 | * The number of items to add.
158 | */
159 | addItems_(index, count) {
160 | const playlist = this.player_.playlist();
161 | const items = playlist.slice(index, count + index);
162 |
163 | if (!items.length) {
164 | return;
165 | }
166 |
167 | const listEl = this.el_.querySelector('.vjs-playlist-item-list');
168 | const listNodes = this.el_.querySelectorAll('.vjs-playlist-item');
169 |
170 | // When appending to the list, `insertBefore` will only reliably accept
171 | // `null` as the second argument, so we need to explicitly fall back to it.
172 | const refNode = listNodes[index] || null;
173 |
174 | const menuItems = items.map((item) => {
175 | const menuItem = new PlaylistMenuItem(this.player_, {item}, this.options_);
176 |
177 | listEl.insertBefore(menuItem.el_, refNode);
178 |
179 | return menuItem;
180 | });
181 |
182 | this.items.splice(index, 0, ...menuItems);
183 | }
184 |
185 | /**
186 | * Removes a number of playlist items from the UI.
187 | *
188 | * Each PlaylistMenuItem component is disposed properly.
189 | *
190 | * @param {number} index
191 | * The index at which to start removing items.
192 | *
193 | * @param {number} count
194 | * The number of items to remove.
195 | */
196 | removeItems_(index, count) {
197 | const components = this.items.slice(index, count + index);
198 |
199 | if (!components.length) {
200 | return;
201 | }
202 |
203 | components.forEach(c => c.dispose());
204 | this.items.splice(index, count);
205 | }
206 |
207 | update() {
208 | // replace the playlist items being displayed, if necessary
209 | const playlist = this.player_.playlist();
210 |
211 | if (this.items.length !== playlist.length) {
212 | // if the menu is currently empty or the state is obviously out
213 | // of date, rebuild everything.
214 | this.createPlaylist_();
215 | return;
216 | }
217 |
218 | for (let i = 0; i < this.items.length; i++) {
219 | if (this.items[i].item !== playlist[i]) {
220 | // if any of the playlist items have changed, rebuild the
221 | // entire playlist
222 | this.createPlaylist_();
223 | return;
224 | }
225 | }
226 |
227 | // the playlist itself is unchanged so just update the selection
228 | const currentItem = this.player_.playlist.currentItem();
229 |
230 | for (let i = 0; i < this.items.length; i++) {
231 | const item = this.items[i];
232 |
233 | if (i === currentItem) {
234 | addSelectedClass(item);
235 | if (document.activeElement !== item.el()) {
236 | videojs.dom.addClass(item.thumbnail, 'vjs-playlist-now-playing');
237 | }
238 | notUpNext(item);
239 | } else if (i === currentItem + 1) {
240 | removeSelectedClass(item);
241 | upNext(item);
242 | } else {
243 | removeSelectedClass(item);
244 | notUpNext(item);
245 | }
246 | }
247 | }
248 | }
249 |
250 | videojs.registerComponent('PlaylistMenu', PlaylistMenu);
251 |
252 | export default PlaylistMenu;
253 |
--------------------------------------------------------------------------------
/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Video.js Playlist UI - Default Implementation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Video.js Playlist UI - Default Implementation
13 |
14 | You can see the Video.js Playlist UI plugin in action below. Look at the
15 | source of this page to see how to use it with your videos.
16 |
17 |
18 | In the default configuration, the plugin looks for the first element that
19 | has the class "vjs-playlist" and uses that as a container for the list.
20 |
21 |
22 |
23 |
28 |
29 |
30 |
39 |
40 |
41 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
230 |
231 |
232 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # [5.0.0](https://github.com/videojs/videojs-playlist-ui/compare/v4.2.1...v5.0.0) (2023-06-15)
3 |
4 | ### Features
5 |
6 | * convert the plugin to a class-based plugin ([#152](https://github.com/videojs/videojs-playlist-ui/issues/152)) ([edc9f84](https://github.com/videojs/videojs-playlist-ui/commit/edc9f84))
7 | * remove deprecated ability to pass an element as options ([#151](https://github.com/videojs/videojs-playlist-ui/issues/151)) ([7ed8ba4](https://github.com/videojs/videojs-playlist-ui/commit/7ed8ba4))
8 | * support playlistadd and playlistremove events ([#154](https://github.com/videojs/videojs-playlist-ui/issues/154)) ([dc5f0fb](https://github.com/videojs/videojs-playlist-ui/commit/dc5f0fb))
9 |
10 | ### Chores
11 |
12 | * do not run tests on npm version ([#155](https://github.com/videojs/videojs-playlist-ui/issues/155)) ([b10ed12](https://github.com/videojs/videojs-playlist-ui/commit/b10ed12))
13 |
14 |
15 | ### BREAKING CHANGES
16 |
17 | * Reimplements the plugin as a class-based plugin. Likely a non-breaking change for simple implementations, but this does change how things work if the plugin is re-initialized.
18 |
19 |
20 | ## [4.2.1](https://github.com/videojs/videojs-playlist-ui/compare/v4.2.0...v4.2.1) (2023-05-01)
21 |
22 | ### Bug Fixes
23 |
24 | * Avoid triggering deprecation warning ([#149](https://github.com/videojs/videojs-playlist-ui/issues/149)) ([5facd9f](https://github.com/videojs/videojs-playlist-ui/commit/5facd9f))
25 | * resolve more deprecation warnings ([#150](https://github.com/videojs/videojs-playlist-ui/issues/150)) ([02dab88](https://github.com/videojs/videojs-playlist-ui/commit/02dab88))
26 |
27 | ### Chores
28 |
29 | * Updated org in package.json ([#145](https://github.com/videojs/videojs-playlist-ui/issues/145)) ([726c97d](https://github.com/videojs/videojs-playlist-ui/commit/726c97d))
30 |
31 |
32 | # [4.2.0](https://github.com/brightcove/videojs-playlist-ui/compare/v4.1.0...v4.2.0) (2023-04-14)
33 |
34 | ### Chores
35 |
36 | * add v8 to dependencies list ([#147](https://github.com/brightcove/videojs-playlist-ui/issues/147)) ([840be2b](https://github.com/brightcove/videojs-playlist-ui/commit/840be2b))
37 |
38 | ### Code Refactoring
39 |
40 | * populate playlist thumbnail's alt tag with video title ([#146](https://github.com/brightcove/videojs-playlist-ui/issues/146)) ([b008a8e](https://github.com/brightcove/videojs-playlist-ui/commit/b008a8e))
41 |
42 |
43 | ## [4.1.1](https://github.com/brightcove/videojs-playlist-ui/compare/v4.0.1...v4.1.1) (2023-03-20)
44 |
45 | ### Features
46 |
47 | * Set lazy loading on images ([#143](https://github.com/brightcove/videojs-playlist-ui/issues/143)) ([b62182f](https://github.com/brightcove/videojs-playlist-ui/commit/b62182f))
48 |
49 | ### Code Refactoring
50 |
51 | * populate playlist thumbnail's alt tag with video title ([#146](https://github.com/brightcove/videojs-playlist-ui/issues/146)) ([b008a8e](https://github.com/brightcove/videojs-playlist-ui/commit/b008a8e))
52 |
53 |
54 | # [4.1.0](https://github.com/brightcove/videojs-playlist-ui/compare/v4.0.1...v4.1.0) (2022-08-05)
55 |
56 | ### Features
57 |
58 | * Set lazy loading on images ([#143](https://github.com/brightcove/videojs-playlist-ui/issues/143)) ([b62182f](https://github.com/brightcove/videojs-playlist-ui/commit/b62182f))
59 |
60 |
61 | ## [4.0.1](https://github.com/brightcove/videojs-playlist-ui/compare/v4.0.0...v4.0.1) (2022-02-11)
62 |
63 | ### Chores
64 |
65 | * Remove IE-specific code and CSS ([#142](https://github.com/brightcove/videojs-playlist-ui/issues/142)) ([263d681](https://github.com/brightcove/videojs-playlist-ui/commit/263d681))
66 |
67 |
68 | # [4.0.0](https://github.com/brightcove/videojs-playlist-ui/compare/v3.8.0...v4.0.0) (2021-12-17)
69 |
70 | ### Chores
71 |
72 | * skip vjsverify es check ([#141](https://github.com/brightcove/videojs-playlist-ui/issues/141)) ([9c6944a](https://github.com/brightcove/videojs-playlist-ui/commit/9c6944a))
73 | * Update generate-rollup-config to drop older browser support ([#139](https://github.com/brightcove/videojs-playlist-ui/issues/139)) ([262acc6](https://github.com/brightcove/videojs-playlist-ui/commit/262acc6))
74 |
75 |
76 | ### BREAKING CHANGES
77 |
78 | * This removes support for some older browsers like IE 11
79 |
80 |
81 | # [3.8.0](https://github.com/brightcove/videojs-playlist-ui/compare/v3.7.0...v3.8.0) (2020-05-06)
82 |
83 | ### Features
84 |
85 | * Enabling option to display video descriptions in playlist thumb… ([#129](https://github.com/brightcove/videojs-playlist-ui/issues/129)) ([b3fbc84](https://github.com/brightcove/videojs-playlist-ui/commit/b3fbc84))
86 |
87 |
88 | # [3.7.0](https://github.com/brightcove/videojs-playlist-ui/compare/v3.6.0...v3.7.0) (2020-02-08)
89 |
90 | ### Features
91 |
92 | * **lang:** Add Arabic translations ([#121](https://github.com/brightcove/videojs-playlist-ui/issues/121)) ([2e820a3](https://github.com/brightcove/videojs-playlist-ui/commit/2e820a3))
93 |
94 | ### Chores
95 |
96 | * **package:** Update all dev dependencies ([#122](https://github.com/brightcove/videojs-playlist-ui/issues/122)) ([90d6135](https://github.com/brightcove/videojs-playlist-ui/commit/90d6135))
97 |
98 |
99 | # [3.6.0](https://github.com/brightcove/videojs-playlist-ui/compare/v3.5.2...v3.6.0) (2019-08-26)
100 |
101 | ### Features
102 |
103 | * Add translations for localizable strings ([#107](https://github.com/brightcove/videojs-playlist-ui/issues/107)) ([96b5ef7](https://github.com/brightcove/videojs-playlist-ui/commit/96b5ef7))
104 |
105 | ### Chores
106 |
107 | * **package:** Update dependencies to fix npm audit issues ([#106](https://github.com/brightcove/videojs-playlist-ui/issues/106)) ([ec20321](https://github.com/brightcove/videojs-playlist-ui/commit/ec20321))
108 | * **package:** update lint-staged to version 8.1.0 ([#91](https://github.com/brightcove/videojs-playlist-ui/issues/91)) ([afd859e](https://github.com/brightcove/videojs-playlist-ui/commit/afd859e))
109 | * **package:** update npm-run-all/videojs-generator-verify for security ([9c579e0](https://github.com/brightcove/videojs-playlist-ui/commit/9c579e0))
110 | * **package:** update rollup to version 0.67.3 ([#89](https://github.com/brightcove/videojs-playlist-ui/issues/89)) ([d969e5d](https://github.com/brightcove/videojs-playlist-ui/commit/d969e5d))
111 | * **package:** update videojs-generate-karma-config to version 5.0.0 ([#90](https://github.com/brightcove/videojs-playlist-ui/issues/90)) ([841cc2d](https://github.com/brightcove/videojs-playlist-ui/commit/841cc2d))
112 | * **package:** update videojs-generate-rollup-config to version 2.3.1 ([#92](https://github.com/brightcove/videojs-playlist-ui/issues/92)) ([ee3a461](https://github.com/brightcove/videojs-playlist-ui/commit/ee3a461))
113 | * **package:** update videojs-standard to version 8.0.2 ([#93](https://github.com/brightcove/videojs-playlist-ui/issues/93)) ([d9066ea](https://github.com/brightcove/videojs-playlist-ui/commit/d9066ea))
114 |
115 |
116 | ## [3.5.2](https://github.com/brightcove/videojs-playlist-ui/compare/v3.5.1...v3.5.2) (2018-10-03)
117 |
118 | ### Bug Fixes
119 |
120 | * Remove the playlist UI when the player is disposed. ([#81](https://github.com/brightcove/videojs-playlist-ui/issues/81)) ([c519585](https://github.com/brightcove/videojs-playlist-ui/commit/c519585))
121 | * Remove the postinstall script to prevent install issues ([#76](https://github.com/brightcove/videojs-playlist-ui/issues/76)) ([fbe09e2](https://github.com/brightcove/videojs-playlist-ui/commit/fbe09e2))
122 |
123 | ### Chores
124 |
125 | * update to generator-videojs-plugin[@7](https://github.com/7).2.0 ([0235fee](https://github.com/brightcove/videojs-playlist-ui/commit/0235fee))
126 | * **package:** update rollup to version 0.66.0 ([#79](https://github.com/brightcove/videojs-playlist-ui/issues/79)) ([dc86980](https://github.com/brightcove/videojs-playlist-ui/commit/dc86980))
127 |
128 |
129 | ## [3.5.1](https://github.com/brightcove/videojs-playlist-ui/compare/v3.5.0...v3.5.1) (2018-08-23)
130 |
131 | ### Chores
132 |
133 | * generator v7 ([#72](https://github.com/brightcove/videojs-playlist-ui/issues/72)) ([c8cb58d](https://github.com/brightcove/videojs-playlist-ui/commit/c8cb58d))
134 |
135 |
136 | # [3.5.0](https://github.com/brightcove/videojs-playlist-ui/compare/v3.4.2...v3.5.0) (2018-08-20)
137 |
138 | ### Features
139 |
140 | * set dataset attributes on playlist items when they have a data object ([#68](https://github.com/brightcove/videojs-playlist-ui/issues/68)) ([e16f2dd](https://github.com/brightcove/videojs-playlist-ui/commit/e16f2dd))
141 |
142 |
143 | ## [3.4.2](https://github.com/brightcove/videojs-playlist-ui/compare/v3.4.1...v3.4.2) (2018-08-03)
144 |
145 | ### Bug Fixes
146 |
147 | * babel the es dist, by updating the generator ([#65](https://github.com/brightcove/videojs-playlist-ui/issues/65)) ([f63f77b](https://github.com/brightcove/videojs-playlist-ui/commit/f63f77b))
148 |
149 | ### Chores
150 |
151 | * **package:** update dependencies, enable greenkeeper ([#62](https://github.com/brightcove/videojs-playlist-ui/issues/62)) ([63a89a7](https://github.com/brightcove/videojs-playlist-ui/commit/63a89a7))
152 |
153 |
154 | ## [3.4.1](https://github.com/brightcove/videojs-playlist-ui/compare/v3.0.7...v3.4.1) (2018-07-20)
155 |
156 | ### Bug Fixes
157 |
158 | * css builds ([#63](https://github.com/brightcove/videojs-playlist-ui/issues/63)) ([603ec73](https://github.com/brightcove/videojs-playlist-ui/commit/603ec73))
159 |
160 | ### Reverts
161 |
162 | * unintended pkg changes ([#64](https://github.com/brightcove/videojs-playlist-ui/issues/64)) ([be83683](https://github.com/brightcove/videojs-playlist-ui/commit/be83683))
163 |
164 |
165 | ## [3.0.8](https://github.com/brightcove/videojs-playlist-ui/compare/v3.0.6...v3.0.8) (2018-07-20)
166 |
167 | ### Bug Fixes
168 |
169 | * update rollup to fix test build ([d329710](https://github.com/brightcove/videojs-playlist-ui/commit/d329710))
170 | * revert: generator update from 3.0.7
171 |
172 |
173 | ## [3.0.7](https://github.com/brightcove/videojs-playlist-ui/compare/v3.4.0...v3.0.7) (2018-07-05)
174 |
175 | ### Chores
176 |
177 | * generator v6 ([#58](https://github.com/brightcove/videojs-playlist-ui/issues/58)) ([e9c2b00](https://github.com/brightcove/videojs-playlist-ui/commit/e9c2b00))
178 |
179 |
180 | # [3.4.0](https://github.com/brightcove/videojs-playlist-ui/compare/v3.3.0...v3.4.0) (2018-03-29)
181 |
182 | ### Features
183 |
184 | * Expose the version of the plugin at the `VERSION` property. ([#56](https://github.com/brightcove/videojs-playlist-ui/issues/56)) ([cb2da9d](https://github.com/brightcove/videojs-playlist-ui/commit/cb2da9d))
185 |
186 | ### Bug Fixes
187 |
188 | * Truncate longer video titles with ellipses when they overflow ([#57](https://github.com/brightcove/videojs-playlist-ui/issues/57)) ([18d8a18](https://github.com/brightcove/videojs-playlist-ui/commit/18d8a18))
189 |
190 | ### Chores
191 |
192 | * Update tooling via the plugin generator. ([#55](https://github.com/brightcove/videojs-playlist-ui/issues/55)) ([b753ab3](https://github.com/brightcove/videojs-playlist-ui/commit/b753ab3))
193 |
194 |
195 | # [3.3.0](https://github.com/brightcove/videojs-playlist-ui/compare/v3.2.1...v3.3.0) (2017-12-04)
196 |
197 | ### Features
198 |
199 | * Support horizontal playlist display. ([#54](https://github.com/brightcove/videojs-playlist-ui/issues/54)) ([85965b6](https://github.com/brightcove/videojs-playlist-ui/commit/85965b6))
200 |
201 |
202 | ## [3.2.1](https://github.com/brightcove/videojs-playlist-ui/compare/v3.2.0...v3.2.1) (2017-11-29)
203 |
204 | ### Bug Fixes
205 |
206 | * Do not concatenate playlist items if the plugin is re-initialized. ([#53](https://github.com/brightcove/videojs-playlist-ui/issues/53)) ([7953ad6](https://github.com/brightcove/videojs-playlist-ui/commit/7953ad6))
207 |
208 |
209 | # [3.2.0](https://github.com/brightcove/videojs-playlist-ui/compare/v3.1.0...v3.2.0) (2017-11-29)
210 |
211 | ### Features
212 |
213 | * Support the 'playlistsorted' event added in videojs-playlist 4.1.0. ([#52](https://github.com/brightcove/videojs-playlist-ui/issues/52)) ([6d79ac1](https://github.com/brightcove/videojs-playlist-ui/commit/6d79ac1))
214 |
215 |
216 | # [3.1.0](https://github.com/brightcove/videojs-playlist-ui/compare/v3.0.6...v3.1.0) (2017-11-15)
217 |
218 | ### Features
219 |
220 | * Better support for multiple in-page players by more intelligently finding a player's associated playlist element. ([#50](https://github.com/brightcove/videojs-playlist-ui/issues/50)) ([50bd97c](https://github.com/brightcove/videojs-playlist-ui/commit/50bd97c))
221 |
222 |
223 | ## [3.0.6](https://github.com/brightcove/videojs-playlist-ui/compare/v3.0.5...v3.0.6) (2017-09-05)
224 |
225 | ### Bug Fixes
226 |
227 | * breaking changed caused by dist files being renamed ([#46](https://github.com/brightcove/videojs-playlist-ui/issues/46)) ([52140f4](https://github.com/brightcove/videojs-playlist-ui/commit/52140f4))
228 | * simplify removal of vjs-ad-playing class ([#45](https://github.com/brightcove/videojs-playlist-ui/issues/45)) ([b49dc82](https://github.com/brightcove/videojs-playlist-ui/commit/b49dc82))
229 |
230 |
231 | ## [3.0.5](https://github.com/brightcove/videojs-playlist-ui/compare/v3.0.3...v3.0.5) (2017-05-19)
232 |
233 | ### Chores
234 |
235 | * Update tooling using generator v5 prerelease. ([#42](https://github.com/brightcove/videojs-playlist-ui/issues/42)) ([6153b64](https://github.com/brightcove/videojs-playlist-ui/commit/6153b64))
236 |
237 | # CHANGELOG
238 |
239 | ## 3.0.4
240 |
241 | * @incompl Fix collision in CSS
242 | * @incompl Fix accessibility for image thumbnails
243 |
244 | ## 3.0.3
245 |
246 | * chore: @brandonocasey Fix Video.js 6 deprecation warnings
247 | * chore: @brandonocasey Update unit tests to use karma
248 |
249 | ## 3.0.2
250 |
251 | * @misteroneill More complete documentation and examples
252 |
253 | ## 3.0.1
254 |
255 | * @misteroneill Update videojs-playlist to v3.0.0 [#24](https://github.com/brightcove/videojs-playlist-ui/pull/24)
256 | * @diniscorreia Fix documentation for placeholder element [#30](https://github.com/brightcove/videojs-playlist-ui/pull/30)
257 | * @diniscorreia Fix querySelector for list creation [#29](https://github.com/brightcove/videojs-playlist-ui/pull/29)
258 |
259 | ## 3.0.0
260 |
261 | * Redesigned UI. Bigger thumbnails, more room for video titles, and more.
262 |
263 | ## 2.3.1
264 |
265 | * @misteroneill More complete documentation and examples [#32](https://github.com/brightcove/videojs-playlist-ui/pull/32)
266 | * @misteroneill Update to videojs-playlist 3.0.0 [#31](https://github.com/brightcove/videojs-playlist-ui/pull/31)
267 |
268 | ## 2.3.0
269 |
270 | * Keep vjs-ad-playing class after postroll until ended event
271 |
272 | ## 2.2.0
273 |
274 | * Fixup babelify and have a proper browserify endpoint
275 |
276 | ...
277 |
278 | ## 0.1.0
279 |
280 | * Initial release
281 |
--------------------------------------------------------------------------------
/test/plugin.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import document from 'global/document';
3 | import window from 'global/window';
4 | import QUnit from 'qunit';
5 | import sinon from 'sinon';
6 | import videojs from 'video.js';
7 |
8 | import 'videojs-playlist';
9 | import '../src/plugin';
10 |
11 | const playlist = [{
12 | name: 'Movie 1',
13 | description: 'Movie 1 description',
14 | duration: 100,
15 | data: {
16 | id: '1',
17 | foo: 'bar'
18 | },
19 | sources: [{
20 | src: '//example.com/movie1.mp4',
21 | type: 'video/mp4'
22 | }]
23 | }, {
24 | sources: [{
25 | src: '//example.com/movie2.mp4',
26 | type: 'video/mp4'
27 | }],
28 | thumbnail: '//example.com/movie2.jpg'
29 | }];
30 |
31 | const resolveUrl = url => {
32 | const a = document.createElement('a');
33 |
34 | a.href = url;
35 | return a.href;
36 | };
37 |
38 | const Html5 = videojs.getTech('Html5');
39 |
40 | QUnit.test('the environment is sane', function(assert) {
41 | assert.ok(true, 'everything is swell');
42 | });
43 |
44 | function setup() {
45 | this.oldVideojsBrowser = videojs.browser;
46 | videojs.browser = videojs.obj.merge({}, videojs.browser);
47 |
48 | this.fixture = document.querySelector('#qunit-fixture');
49 |
50 | // force HTML support so the tests run in a reasonable
51 | // environment under phantomjs
52 | this.realIsHtmlSupported = Html5.isSupported;
53 | Html5.isSupported = function() {
54 | return true;
55 | };
56 |
57 | // create a video element
58 | const video = document.createElement('video');
59 |
60 | this.fixture.appendChild(video);
61 |
62 | // create a video.js player
63 | this.player = videojs(video);
64 |
65 | // Create two playlist container elements.
66 | this.fixture.appendChild(videojs.dom.createEl('div', {className: 'vjs-playlist'}));
67 | this.fixture.appendChild(videojs.dom.createEl('div', {className: 'vjs-playlist'}));
68 | }
69 |
70 | function teardown() {
71 | videojs.browser = this.oldVideojsBrowser;
72 | Html5.isSupported = this.realIsHtmlSupported;
73 | this.player.dispose();
74 | this.player = null;
75 | videojs.dom.emptyEl(this.fixture);
76 | }
77 |
78 | QUnit.module('videojs-playlist-ui', {beforeEach: setup, afterEach: teardown});
79 |
80 | QUnit.test('registers itself', function(assert) {
81 | assert.ok(this.player.playlistUi, 'registered the plugin');
82 | });
83 |
84 | QUnit.test('errors if used without the playlist plugin', function(assert) {
85 | sinon.spy(this.player.log, 'error');
86 |
87 | this.player.playlist = null;
88 | this.player.playlistUi();
89 |
90 | assert.ok(this.player.log.error.calledOnce, 'player.log.error was called');
91 | });
92 |
93 | QUnit.test('is empty if the playlist plugin isn\'t initialized', function(assert) {
94 | this.player.playlistUi();
95 |
96 | const items = this.fixture.querySelectorAll('.vjs-playlist-item');
97 |
98 | assert.ok(this.fixture.querySelector('.vjs-playlist'), 'created the menu');
99 | assert.strictEqual(items.length, 0, 'displayed no items');
100 | });
101 |
102 | QUnit.test('can be initialized with an element', function(assert) {
103 | const elem = videojs.dom.createEl('div');
104 |
105 | this.player.playlist(playlist);
106 | this.player.playlistUi({el: elem});
107 |
108 | assert.strictEqual(
109 | elem.querySelectorAll('li.vjs-playlist-item').length,
110 | playlist.length,
111 | 'created an element for each playlist item'
112 | );
113 | });
114 |
115 | QUnit.test('can look for an element with the class "vjs-playlist" that is not already in use', function(assert) {
116 | const firstEl = this.fixture.querySelectorAll('.vjs-playlist')[0];
117 | const secondEl = this.fixture.querySelectorAll('.vjs-playlist')[1];
118 |
119 | // Give the firstEl a child, so the plugin thinks it is in use and moves on
120 | // to the next one.
121 | firstEl.appendChild(videojs.dom.createEl('div'));
122 |
123 | this.player.playlist(playlist);
124 | this.player.playlistUi();
125 |
126 | assert.strictEqual(this.player.playlistMenu.el(), secondEl, 'used the first matching/empty element');
127 | assert.strictEqual(
128 | secondEl.querySelectorAll('li.vjs-playlist-item').length,
129 | playlist.length,
130 | 'found an element for each playlist item'
131 | );
132 | });
133 |
134 | QUnit.test('can look for an element with a custom class that is not already in use', function(assert) {
135 | const firstEl = videojs.dom.createEl('div', {className: 'super-playlist'});
136 | const secondEl = videojs.dom.createEl('div', {className: 'super-playlist'});
137 |
138 | // Give the firstEl a child, so the plugin thinks it is in use and moves on
139 | // to the next one.
140 | firstEl.appendChild(videojs.dom.createEl('div'));
141 |
142 | this.fixture.appendChild(firstEl);
143 | this.fixture.appendChild(secondEl);
144 |
145 | this.player.playlist(playlist);
146 | this.player.playlistUi({
147 | className: 'super-playlist'
148 | });
149 |
150 | assert.strictEqual(this.player.playlistMenu.el(), secondEl, 'used the first matching/empty element');
151 | assert.strictEqual(
152 | this.fixture.querySelectorAll('li.vjs-playlist-item').length,
153 | playlist.length,
154 | 'created an element for each playlist item'
155 | );
156 | });
157 |
158 | QUnit.test('specializes the class name if touch input is absent', function(assert) {
159 | videojs.browser.TOUCH_ENABLED = false;
160 |
161 | this.player.playlist(playlist);
162 | this.player.playlistUi();
163 |
164 | assert.ok(this.player.playlistMenu.hasClass('vjs-mouse'), 'marked the playlist menu');
165 | });
166 |
167 | QUnit.test('can be re-initialized without doubling the contents of the list', function(assert) {
168 | const el = this.fixture.querySelectorAll('.vjs-playlist')[0];
169 |
170 | this.player.playlist(playlist);
171 | this.player.playlistUi();
172 | this.player.playlistUi();
173 | this.player.playlistUi();
174 |
175 | assert.strictEqual(this.player.playlistMenu.el(), el, 'used the first matching/empty element');
176 | assert.strictEqual(
177 | el.querySelectorAll('li.vjs-playlist-item').length,
178 | playlist.length,
179 | 'found an element for each playlist item'
180 | );
181 | });
182 |
183 | QUnit.module('videojs-playlist-ui: Components', {beforeEach: setup, afterEach: teardown});
184 |
185 | // --------------------
186 | // Creation and Updates
187 | // --------------------
188 |
189 | QUnit.test('includes the video name if provided', function(assert) {
190 | this.player.playlist(playlist);
191 | this.player.playlistUi();
192 |
193 | const items = this.fixture.querySelectorAll('.vjs-playlist-item');
194 |
195 | assert.strictEqual(
196 | items[0].querySelector('.vjs-playlist-name').textContent,
197 | playlist[0].name,
198 | 'wrote the name'
199 | );
200 | assert.strictEqual(
201 | items[1].querySelector('.vjs-playlist-name').textContent,
202 | 'Untitled Video',
203 | 'wrote a placeholder for the name'
204 | );
205 | });
206 |
207 | QUnit.test('includes the video description if user specifies it', function(assert) {
208 | this.player.playlist(playlist);
209 | this.player.playlistUi({showDescription: true});
210 |
211 | const items = this.fixture.querySelectorAll('.vjs-playlist-item');
212 |
213 | assert.strictEqual(
214 | items[0].querySelector('.vjs-playlist-description').textContent,
215 | playlist[0].description,
216 | 'description is displayed'
217 | );
218 | });
219 |
220 | QUnit.test('hides video description by default', function(assert) {
221 | this.player.playlist(playlist);
222 | this.player.playlistUi();
223 |
224 | const items = this.fixture.querySelectorAll('.vjs-playlist-item');
225 |
226 | assert.strictEqual(
227 | items[0].querySelector('.vjs-playlist-description'),
228 | null,
229 | 'description is not displayed'
230 | );
231 | });
232 |
233 | QUnit.test('includes custom data attribute if provided', function(assert) {
234 | this.player.playlist(playlist);
235 | this.player.playlistUi();
236 |
237 | const items = this.fixture.querySelectorAll('.vjs-playlist-item');
238 |
239 | assert.strictEqual(
240 | items[0].dataset.id,
241 | playlist[0].data.id,
242 | 'set a single data attribute'
243 | );
244 | assert.strictEqual(
245 | items[0].dataset.id,
246 | '1',
247 | 'set a single data attribute (actual value)'
248 | );
249 | assert.strictEqual(
250 | items[0].dataset.foo,
251 | playlist[0].data.foo,
252 | 'set an addtional data attribute'
253 | );
254 | assert.strictEqual(
255 | items[0].dataset.foo,
256 | 'bar',
257 | 'set an addtional data attribute'
258 | );
259 | });
260 |
261 | QUnit.test('outputs a for simple thumbnails', function(assert) {
262 | this.player.playlist(playlist);
263 | this.player.playlistUi();
264 |
265 | const pictures = this.fixture.querySelectorAll('.vjs-playlist-item picture');
266 |
267 | assert.strictEqual(pictures.length, 1, 'output one picture');
268 | const imgs = pictures[0].querySelectorAll('img');
269 |
270 | assert.strictEqual(imgs.length, 1, 'output one img');
271 | assert.strictEqual(imgs[0].src, window.location.protocol + playlist[1].thumbnail, 'set the src attribute');
272 | });
273 |
274 | QUnit.test('outputs a for responsive thumbnails', function(assert) {
275 | const playlistOverride = [{
276 | sources: [{
277 | src: '//example.com/movie.mp4',
278 | type: 'video/mp4'
279 | }],
280 | thumbnail: [{
281 | srcset: '/test/example/oceans.jpg',
282 | type: 'image/jpeg',
283 | media: '(min-width: 400px;)'
284 | }, {
285 | src: '/test/example/oceans-low.jpg'
286 | }]
287 | }];
288 |
289 | this.player.playlist(playlistOverride);
290 | this.player.playlistUi();
291 |
292 | const sources = this.fixture.querySelectorAll('.vjs-playlist-item picture source');
293 | const imgs = this.fixture.querySelectorAll('.vjs-playlist-item picture img');
294 |
295 | assert.strictEqual(sources.length, 1, 'output one source');
296 | assert.strictEqual(
297 | sources[0].srcset,
298 | playlistOverride[0].thumbnail[0].srcset,
299 | 'wrote the srcset attribute'
300 | );
301 | assert.strictEqual(
302 | sources[0].type,
303 | playlistOverride[0].thumbnail[0].type,
304 | 'wrote the type attribute'
305 | );
306 | assert.strictEqual(
307 | sources[0].media,
308 | playlistOverride[0].thumbnail[0].media,
309 | 'wrote the type attribute'
310 | );
311 | assert.strictEqual(imgs.length, 1, 'output one img');
312 | assert.strictEqual(
313 | imgs[0].src,
314 | resolveUrl(playlistOverride[0].thumbnail[1].src),
315 | 'output the img src attribute'
316 | );
317 | });
318 |
319 | QUnit.test('outputs a placeholder for items without thumbnails', function(assert) {
320 | this.player.playlist(playlist);
321 | this.player.playlistUi();
322 |
323 | const thumbnails = this.fixture.querySelectorAll('.vjs-playlist-item .vjs-playlist-thumbnail');
324 |
325 | assert.strictEqual(thumbnails.length, playlist.length, 'output two thumbnails');
326 | assert.strictEqual(thumbnails[0].nodeName.toLowerCase(), 'div', 'the second is a placeholder');
327 | });
328 |
329 | QUnit.test('includes the duration if one is provided', function(assert) {
330 | this.player.playlist(playlist);
331 | this.player.playlistUi();
332 |
333 | const durations = this.fixture.querySelectorAll('.vjs-playlist-item .vjs-playlist-duration');
334 |
335 | assert.strictEqual(durations.length, 1, 'skipped the item without a duration');
336 | assert.strictEqual(
337 | durations[0].textContent,
338 | '1:40',
339 | 'wrote the duration'
340 | );
341 | assert.strictEqual(
342 | durations[0].getAttribute('datetime'),
343 | 'PT0H0M' + playlist[0].duration + 'S',
344 | 'wrote a machine-readable datetime'
345 | );
346 | });
347 |
348 | QUnit.test('marks the selected playlist item on startup', function(assert) {
349 | this.player.playlist(playlist);
350 | this.player.currentSrc = () => playlist[0].sources[0].src;
351 | this.player.playlistUi();
352 |
353 | const selectedItems = this.fixture.querySelectorAll('.vjs-playlist-item.vjs-selected');
354 |
355 | assert.strictEqual(selectedItems.length, 1, 'marked one playlist item');
356 | assert.strictEqual(
357 | selectedItems[0].querySelector('.vjs-playlist-name').textContent,
358 | playlist[0].name,
359 | 'marked the first playlist item'
360 | );
361 | });
362 |
363 | QUnit.test('updates the selected playlist item on loadstart', function(assert) {
364 | this.player.playlist(playlist);
365 | this.player.playlistUi();
366 |
367 | this.player.playlist.currentItem(1);
368 | this.player.currentSrc = () => playlist[1].sources[0].src;
369 | this.player.trigger('loadstart');
370 |
371 | const selectedItems = this.fixture.querySelectorAll('.vjs-playlist-item.vjs-selected');
372 |
373 | assert.strictEqual(
374 | this.fixture.querySelectorAll('.vjs-playlist-item').length,
375 | playlist.length,
376 | 'displayed the correct number of items'
377 | );
378 | assert.strictEqual(selectedItems.length, 1, 'marked one playlist item');
379 | assert.strictEqual(
380 | selectedItems[0].querySelector('img').src,
381 | resolveUrl(playlist[1].thumbnail),
382 | 'marked the second playlist item'
383 | );
384 | });
385 |
386 | QUnit.test('selects no item if the playlist is not in use', function(assert) {
387 | this.player.playlist(playlist);
388 | this.player.playlist.currentItem = () => -1;
389 | this.player.playlistUi();
390 |
391 | this.player.trigger('loadstart');
392 |
393 | assert.strictEqual(
394 | this.fixture.querySelectorAll('.vjs-playlist-item.vjs-selected').length,
395 | 0,
396 | 'no items selected'
397 | );
398 | });
399 |
400 | QUnit.test('updates on "playlistchange", different lengths', function(assert) {
401 | this.player.playlist([]);
402 | this.player.playlistUi();
403 |
404 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
405 |
406 | assert.strictEqual(items.length, 0, 'no items initially');
407 |
408 | this.player.playlist(playlist);
409 | this.player.trigger('playlistchange');
410 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
411 | assert.strictEqual(items.length, playlist.length, 'updated with the new items');
412 | });
413 |
414 | QUnit.test('updates on "playlistchange", equal lengths', function(assert) {
415 | this.player.playlist([{sources: []}, {sources: []}]);
416 | this.player.playlistUi();
417 |
418 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
419 |
420 | assert.strictEqual(items.length, 2, 'two items initially');
421 |
422 | this.player.playlist(playlist);
423 | this.player.trigger('playlistchange');
424 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
425 | assert.strictEqual(items.length, playlist.length, 'updated with the new items');
426 | assert.strictEqual(this.player.playlistMenu.items[0].item, playlist[0], 'we have updated items');
427 | assert.strictEqual(this.player.playlistMenu.items[1].item, playlist[1], 'we have updated items');
428 | });
429 |
430 | QUnit.test('updates on "playlistchange", update selection', function(assert) {
431 | this.player.playlist(playlist);
432 | this.player.currentSrc = function() {
433 | return playlist[0].sources[0].src;
434 | };
435 | this.player.playlistUi();
436 |
437 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
438 |
439 | assert.strictEqual(items.length, 2, 'two items initially');
440 |
441 | assert.ok((/vjs-selected/).test(items[0].getAttribute('class')), 'first item is selected by default');
442 | this.player.playlist.currentItem(1);
443 | this.player.currentSrc = function() {
444 | return playlist[1].sources[0].src;
445 | };
446 |
447 | this.player.trigger('playlistchange');
448 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
449 | assert.strictEqual(items.length, playlist.length, 'updated with the new items');
450 | assert.ok((/vjs-selected/).test(items[1].getAttribute('class')), 'second item is selected after update');
451 | assert.ok(!(/vjs-selected/).test(items[0].getAttribute('class')), 'first item is not selected after update');
452 | });
453 |
454 | QUnit.test('updates on "playlistsorted", different lengths', function(assert) {
455 | this.player.playlist([]);
456 | this.player.playlistUi();
457 |
458 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
459 |
460 | assert.strictEqual(items.length, 0, 'no items initially');
461 |
462 | this.player.playlist(playlist);
463 | this.player.trigger('playlistsorted');
464 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
465 | assert.strictEqual(items.length, playlist.length, 'updated with the new items');
466 | });
467 |
468 | QUnit.test('updates on "playlistsorted", equal lengths', function(assert) {
469 | this.player.playlist([{sources: []}, {sources: []}]);
470 | this.player.playlistUi();
471 |
472 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
473 |
474 | assert.strictEqual(items.length, 2, 'two items initially');
475 |
476 | this.player.playlist(playlist);
477 | this.player.trigger('playlistsorted');
478 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
479 | assert.strictEqual(items.length, playlist.length, 'updated with the new items');
480 | assert.strictEqual(this.player.playlistMenu.items[0].item, playlist[0], 'we have updated items');
481 | assert.strictEqual(this.player.playlistMenu.items[1].item, playlist[1], 'we have updated items');
482 | });
483 |
484 | QUnit.test('updates on "playlistsorted", update selection', function(assert) {
485 | this.player.playlist(playlist);
486 | this.player.currentSrc = function() {
487 | return playlist[0].sources[0].src;
488 | };
489 | this.player.playlistUi();
490 |
491 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
492 |
493 | assert.strictEqual(items.length, 2, 'two items initially');
494 |
495 | assert.ok((/vjs-selected/).test(items[0].getAttribute('class')), 'first item is selected by default');
496 | this.player.playlist.currentItem(1);
497 | this.player.currentSrc = function() {
498 | return playlist[1].sources[0].src;
499 | };
500 |
501 | this.player.trigger('playlistsorted');
502 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
503 | assert.strictEqual(items.length, playlist.length, 'updated with the new items');
504 | assert.ok((/vjs-selected/).test(items[1].getAttribute('class')), 'second item is selected after update');
505 | assert.ok(!(/vjs-selected/).test(items[0].getAttribute('class')), 'first item is not selected after update');
506 | });
507 |
508 | QUnit.test('tracks when an ad is playing', function(assert) {
509 | this.player.playlist([]);
510 | this.player.playlistUi();
511 |
512 | this.player.duration = () => 5;
513 |
514 | const playlistMenu = this.player.playlistMenu;
515 |
516 | assert.ok(
517 | !playlistMenu.hasClass('vjs-ad-playing'),
518 | 'does not have class vjs-ad-playing'
519 | );
520 | this.player.trigger('adstart');
521 | assert.ok(
522 | playlistMenu.hasClass('vjs-ad-playing'),
523 | 'has class vjs-ad-playing'
524 | );
525 |
526 | this.player.trigger('adend');
527 | assert.ok(
528 | !playlistMenu.hasClass('vjs-ad-playing'),
529 | 'does not have class vjs-ad-playing'
530 | );
531 | });
532 |
533 | // -----------
534 | // Interaction
535 | // -----------
536 |
537 | QUnit.test('changes the selection when tapped', function(assert) {
538 | let playCalled = false;
539 |
540 | this.player.playlist(playlist);
541 | this.player.playlistUi({playOnSelect: true});
542 | this.player.play = function() {
543 | playCalled = true;
544 | };
545 |
546 | let sources;
547 |
548 | this.player.src = (src) => {
549 | if (src) {
550 | sources = src;
551 | }
552 | return sources[0];
553 | };
554 | this.player.currentSrc = () => sources[0].src;
555 | this.player.playlistMenu.items[1].trigger('tap');
556 | // trigger a loadstart synchronously to simplify the test
557 | this.player.trigger('loadstart');
558 |
559 | assert.ok(
560 | this.player.playlistMenu.items[1].hasClass('vjs-selected'),
561 | 'selected the new item'
562 | );
563 | assert.ok(
564 | !this.player.playlistMenu.items[0].hasClass('vjs-selected'),
565 | 'deselected the old item'
566 | );
567 | assert.strictEqual(playCalled, true, 'play gets called if option is set');
568 | });
569 |
570 | QUnit.test('play should not get called by default upon selection of menu items ', function(assert) {
571 | let playCalled = false;
572 |
573 | this.player.playlist(playlist);
574 | this.player.playlistUi();
575 | this.player.play = function() {
576 | playCalled = true;
577 | };
578 |
579 | let sources;
580 |
581 | this.player.src = (src) => {
582 | if (src) {
583 | sources = src;
584 | }
585 | return sources[0];
586 | };
587 | this.player.currentSrc = () => sources[0].src;
588 | this.player.playlistMenu.items[1].trigger('tap');
589 | // trigger a loadstart synchronously to simplify the test
590 | this.player.trigger('loadstart');
591 | assert.strictEqual(playCalled, false, 'play should not get called by default');
592 | });
593 |
594 | QUnit.test('disposing the playlist menu nulls out the player\'s reference to it', function(assert) {
595 | assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 2, 'there are two playlist containers at the start');
596 |
597 | this.player.playlist(playlist);
598 | this.player.playlistUi();
599 | this.player.playlistMenu.dispose();
600 |
601 | assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 1, 'only the unused playlist container is left');
602 | assert.strictEqual(this.player.playlistMenu, null, 'the playlistMenu property is null');
603 | });
604 |
605 | QUnit.test('disposing the playlist menu removes playlist menu items', function(assert) {
606 | assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 2, 'there are two playlist containers at the start');
607 |
608 | this.player.playlist(playlist);
609 | this.player.playlistUi();
610 |
611 | // Cache some references so we can refer to them after disposal.
612 | const items = [].concat(this.player.playlistMenu.items);
613 |
614 | this.player.playlistMenu.dispose();
615 |
616 | assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 1, 'only the unused playlist container is left');
617 | assert.strictEqual(this.player.playlistMenu, null, 'the playlistMenu property is null');
618 |
619 | items.forEach(i => {
620 | assert.strictEqual(i.el_, null, `the item "${i.id_}" has been disposed`);
621 | });
622 | });
623 |
624 | QUnit.test('disposing the player also disposes the playlist menu', function(assert) {
625 | assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 2, 'there are two playlist containers at the start');
626 |
627 | this.player.playlist(playlist);
628 | this.player.playlistUi();
629 | this.player.dispose();
630 |
631 | assert.strictEqual(this.fixture.querySelectorAll('.vjs-playlist').length, 1, 'only the unused playlist container is left');
632 | assert.strictEqual(this.player.playlistMenu, null, 'the playlistMenu property is null');
633 | });
634 |
635 | QUnit.module('videojs-playlist-ui: add/remove', {beforeEach: setup, afterEach: teardown});
636 |
637 | QUnit.test('adding zero items at the start of the playlist', function(assert) {
638 | this.player.playlist(playlist);
639 | this.player.playlistUi();
640 |
641 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
642 |
643 | assert.strictEqual(items.length, 2, 'two items initially');
644 |
645 | this.player.playlist.add([], 0);
646 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
647 | assert.strictEqual(items.length, playlist.length, 'correct number of items');
648 | });
649 |
650 | QUnit.test('adding one item at the start of the playlist', function(assert) {
651 | this.player.playlist(playlist);
652 | this.player.playlistUi();
653 |
654 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
655 |
656 | assert.strictEqual(items.length, 2, 'two items initially');
657 |
658 | this.player.playlist.add({name: 'Test 1'}, 0);
659 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
660 | assert.strictEqual(items.length, 3, 'correct number of items');
661 | assert.strictEqual(items[0].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
662 | });
663 |
664 | QUnit.test('adding two items at the start of the playlist', function(assert) {
665 | this.player.playlist(playlist);
666 | this.player.playlistUi();
667 |
668 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
669 |
670 | assert.strictEqual(items.length, 2, 'two items initially');
671 |
672 | this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], 0);
673 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
674 | assert.strictEqual(items.length, 4, 'correct number of items');
675 | assert.strictEqual(items[0].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
676 | assert.strictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 2', 'has the correct name in the playlist DOM');
677 | });
678 |
679 | QUnit.test('adding one item in the middle of the playlist', function(assert) {
680 | this.player.playlist(playlist);
681 | this.player.playlistUi();
682 |
683 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
684 |
685 | assert.strictEqual(items.length, 2, 'two items initially');
686 |
687 | this.player.playlist.add({name: 'Test 1'}, 1);
688 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
689 | assert.strictEqual(items.length, 3, 'correct number of items');
690 | assert.strictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
691 | });
692 |
693 | QUnit.test('adding two items in the middle of the playlist', function(assert) {
694 | this.player.playlist(playlist);
695 | this.player.playlistUi();
696 |
697 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
698 |
699 | assert.strictEqual(items.length, 2, 'two items initially');
700 |
701 | this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], 1);
702 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
703 | assert.strictEqual(items.length, 4, 'correct number of items');
704 | assert.strictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
705 | assert.strictEqual(items[2].querySelector('.vjs-playlist-name').textContent, 'Test 2', 'has the correct name in the playlist DOM');
706 | });
707 |
708 | QUnit.test('adding one item at the end of the playlist', function(assert) {
709 | this.player.playlist(playlist);
710 | this.player.playlistUi();
711 |
712 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
713 |
714 | assert.strictEqual(items.length, 2, 'two items initially');
715 |
716 | this.player.playlist.add({name: 'Test 1'}, playlist.length);
717 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
718 | assert.strictEqual(items.length, 3, 'correct number of items');
719 | assert.strictEqual(items[2].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
720 | });
721 |
722 | QUnit.test('adding two items at the end of the playlist', function(assert) {
723 | this.player.playlist(playlist);
724 | this.player.playlistUi();
725 |
726 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
727 |
728 | assert.strictEqual(items.length, 2, 'two items initially');
729 |
730 | this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], playlist.length);
731 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
732 | assert.strictEqual(items.length, 4, 'correct number of items');
733 | assert.strictEqual(items[2].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'has the correct name in the playlist DOM');
734 | assert.strictEqual(items[3].querySelector('.vjs-playlist-name').textContent, 'Test 2', 'has the correct name in the playlist DOM');
735 | });
736 |
737 | QUnit.test('removing zero items at the start of the playlist', function(assert) {
738 | this.player.playlist(playlist);
739 | this.player.playlistUi();
740 |
741 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
742 |
743 | assert.strictEqual(items.length, 2, 'two items initially');
744 |
745 | this.player.playlist.remove(0, 0);
746 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
747 | assert.strictEqual(items.length, playlist.length, 'correct number of items');
748 | });
749 |
750 | QUnit.test('removing one item at the start of the playlist', function(assert) {
751 | this.player.playlist(playlist);
752 | this.player.playlistUi();
753 |
754 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
755 |
756 | assert.strictEqual(items.length, 2, 'two items initially');
757 |
758 | this.player.playlist.add({name: 'Test 1'}, 0);
759 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
760 |
761 | assert.strictEqual(items.length, 3, 'correct number of items');
762 |
763 | this.player.playlist.remove(0, 1);
764 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
765 |
766 | assert.strictEqual(items.length, 2, 'correct number of items');
767 | assert.notStrictEqual(items[0].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
768 | });
769 |
770 | QUnit.test('removing two items at the start of the playlist', function(assert) {
771 | this.player.playlist(playlist);
772 | this.player.playlistUi();
773 |
774 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
775 |
776 | assert.strictEqual(items.length, 2, 'two items initially');
777 |
778 | this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], 0);
779 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
780 |
781 | assert.strictEqual(items.length, 4, 'correct number of items');
782 |
783 | this.player.playlist.remove(0, 2);
784 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
785 |
786 | assert.notStrictEqual(items[0].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
787 | assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 2', 'the added item was properly removed from the DOM');
788 | });
789 |
790 | QUnit.test('removing one item in the middle of the playlist', function(assert) {
791 | this.player.playlist(playlist);
792 | this.player.playlistUi();
793 |
794 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
795 |
796 | assert.strictEqual(items.length, 2, 'two items initially');
797 |
798 | this.player.playlist.add({name: 'Test 1'}, 1);
799 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
800 |
801 | assert.strictEqual(items.length, 3, 'correct number of items');
802 |
803 | this.player.playlist.remove(1, 1);
804 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
805 |
806 | assert.strictEqual(items.length, 2, 'correct number of items');
807 | assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
808 | });
809 |
810 | QUnit.test('removing two items in the middle of the playlist', function(assert) {
811 | this.player.playlist(playlist);
812 | this.player.playlistUi();
813 |
814 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
815 |
816 | assert.strictEqual(items.length, 2, 'two items initially');
817 |
818 | this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], 1);
819 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
820 |
821 | assert.strictEqual(items.length, 4, 'correct number of items');
822 |
823 | this.player.playlist.remove(1, 2);
824 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
825 |
826 | assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
827 | assert.strictEqual(items[2], undefined, 'the added item was properly removed from the DOM');
828 | });
829 |
830 | QUnit.test('removing one item at the end of the playlist', function(assert) {
831 | this.player.playlist(playlist);
832 | this.player.playlistUi();
833 |
834 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
835 |
836 | assert.strictEqual(items.length, 2, 'two items initially');
837 |
838 | this.player.playlist.add({name: 'Test 1'}, 2);
839 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
840 |
841 | assert.strictEqual(items.length, 3, 'correct number of items');
842 |
843 | this.player.playlist.remove(2, 1);
844 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
845 |
846 | assert.strictEqual(items.length, 2, 'correct number of items');
847 | assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
848 | });
849 |
850 | QUnit.test('removing two items at the end of the playlist', function(assert) {
851 | this.player.playlist(playlist);
852 | this.player.playlistUi();
853 |
854 | let items = this.fixture.querySelectorAll('.vjs-playlist-item');
855 |
856 | assert.strictEqual(items.length, 2, 'two items initially');
857 |
858 | this.player.playlist.add([{name: 'Test 1'}, {name: 'Test 2'}], 2);
859 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
860 |
861 | assert.strictEqual(items.length, 4, 'correct number of items');
862 |
863 | this.player.playlist.remove(2, 2);
864 | items = this.fixture.querySelectorAll('.vjs-playlist-item');
865 |
866 | assert.strictEqual(items.length, 2, 'correct number of items');
867 | assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 1', 'the added item was properly removed from the DOM');
868 | assert.notStrictEqual(items[1].querySelector('.vjs-playlist-name').textContent, 'Test 2', 'the added item was properly removed from the DOM');
869 | });
870 |
--------------------------------------------------------------------------------