├── .editorconfig
├── .github
├── ISSUE_TEMPLATE.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .npmignore
├── .nvmrc
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── scripts
├── karma.conf.js
├── postcss.config.js
└── rollup.config.js
├── src
├── context-menu-item.js
├── context-menu.js
├── plugin.css
├── plugin.js
└── util.js
└── test
└── plugin.test.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 2
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 | Please describe the change as necessary.
3 | If it's a feature or enhancement please be as detailed as possible.
4 | If it's a bug fix, please link the issue that it fixes or describe the bug in as much detail.
5 |
6 | ## Specific Changes proposed
7 | Please list the specific changes involved in this pull request.
8 |
9 | ## Requirements Checklist
10 | - [ ] Feature implemented / Bug fixed
11 | - [ ] If necessary, more likely in a feature request than a bug fix
12 | - [ ] Unit Tests updated or fixed
13 | - [ ] Docs/guides updated
14 | - [ ] Reviewed by Two Core Contributors
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS
2 | Thumbs.db
3 | ehthumbs.db
4 | Desktop.ini
5 | .DS_Store
6 | ._*
7 |
8 | # Editors
9 | *~
10 | *.swp
11 | *.tmproj
12 | *.tmproject
13 | *.sublime-*
14 | .idea/
15 | .project/
16 | .settings/
17 | .vscode/
18 |
19 | # Logs
20 | logs
21 | *.log
22 | npm-debug.log*
23 |
24 | # Dependency directories
25 | bower_components/
26 | node_modules/
27 |
28 | # Build-related directories
29 | dist/
30 | docs/api/
31 | test/dist/
32 | .eslintcache
33 | .yo-rc.json
34 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Intentionally left blank, so that npm does not ignore anything by default,
2 | # but relies on the package.json "files" array to explicitly define what ends
3 | # up in the package.
4 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/*
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | dist: trusty
3 | language: node_js
4 | # node version is specified using the .nvmrc file
5 | before_install:
6 | - npm install -g greenkeeper-lockfile@1
7 | before_script:
8 | - export DISPLAY=:99.0
9 | - sh -e /etc/init.d/xvfb start
10 | - greenkeeper-lockfile-update
11 | after_script:
12 | - greenkeeper-lockfile-upload
13 | addons:
14 | firefox: latest
15 | chrome: stable
16 |
17 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # [7.0.0](https://github.com/videojs/videojs-contextmenu-ui/compare/v6.0.0...v7.0.0) (2022-12-16)
3 |
4 | ### Chores
5 |
6 | * address deprecation warnings (#69) ([46d8e7f](https://github.com/videojs/videojs-contextmenu-ui/commit/46d8e7f)), closes [#69](https://github.com/videojs/videojs-contextmenu-ui/issues/69)
7 |
8 |
9 | # [6.0.0](https://github.com/brightcove/videojs-contextmenu-ui/compare/v5.2.0...v6.0.0) (2021-12-17)
10 |
11 | ### Chores
12 |
13 | * skip vjsverify es check (#68) ([2e28a64](https://github.com/brightcove/videojs-contextmenu-ui/commit/2e28a64)), closes [#68](https://github.com/brightcove/videojs-contextmenu-ui/issues/68)
14 | * Update generate-rollup-config to drop older browser support (#66) ([e6e919a](https://github.com/brightcove/videojs-contextmenu-ui/commit/e6e919a)), closes [#66](https://github.com/brightcove/videojs-contextmenu-ui/issues/66)
15 |
16 |
17 | ### BREAKING CHANGES
18 |
19 | * This removes support for some older browsers such as IE 11
20 |
21 |
22 | # [5.2.0](https://github.com/brightcove/videojs-contextmenu-ui/compare/v5.1.1...v5.2.0) (2019-08-28)
23 |
24 | ### Features
25 |
26 | * Prevent context menu from appearing when targeting text input elements (#55) ([31ffc80](https://github.com/brightcove/videojs-contextmenu-ui/commit/31ffc80)), closes [#55](https://github.com/brightcove/videojs-contextmenu-ui/issues/55)
27 |
28 | ### Chores
29 |
30 | * **package:** Update all dependencies (#56) ([26ef0fc](https://github.com/brightcove/videojs-contextmenu-ui/commit/26ef0fc)), closes [#56](https://github.com/brightcove/videojs-contextmenu-ui/issues/56)
31 |
32 |
33 | ## [5.1.1](https://github.com/brightcove/videojs-contextmenu-ui/compare/v5.1.0...v5.1.1) (2019-06-19)
34 |
35 | ### Bug Fixes
36 |
37 | * Work around a Firefox issue where 'oncontextmenu' event triggers 'click' event ([3e65c01](https://github.com/brightcove/videojs-contextmenu-ui/commit/3e65c01))
38 |
39 | ### Chores
40 |
41 | * **package:** Update development dependencies ([0c78097](https://github.com/brightcove/videojs-contextmenu-ui/commit/0c78097))
42 | * **package:** update npm-run-all/videojs-generator-verify for security ([76e8f79](https://github.com/brightcove/videojs-contextmenu-ui/commit/76e8f79))
43 |
44 |
45 | # [5.1.0](https://github.com/brightcove/videojs-contextmenu-ui/compare/v5.0.0...v5.1.0) (2018-10-10)
46 |
47 | ### Features
48 |
49 | * Keep menu within player by default (#5) ([c99d2e8](https://github.com/brightcove/videojs-contextmenu-ui/commit/c99d2e8)), closes [#5](https://github.com/brightcove/videojs-contextmenu-ui/issues/5)
50 |
51 |
52 | # [5.0.0](https://github.com/brightcove/videojs-contextmenu-ui/compare/v4.0.0...v5.0.0) (2018-09-12)
53 |
54 | ### Bug Fixes
55 |
56 | * Remove the postinstall script to prevent install issues (#22) ([812a5f8](https://github.com/brightcove/videojs-contextmenu-ui/commit/812a5f8)), closes [#22](https://github.com/brightcove/videojs-contextmenu-ui/issues/22)
57 |
58 | ### Chores
59 |
60 | * update generator to v7.1.1 ([6e38e5f](https://github.com/brightcove/videojs-contextmenu-ui/commit/6e38e5f))
61 | * Update Rollup to 0.50 (#12) ([08077c4](https://github.com/brightcove/videojs-contextmenu-ui/commit/08077c4)), closes [#12](https://github.com/brightcove/videojs-contextmenu-ui/issues/12)
62 | * **package:** Update dependencies (#19) ([10473f0](https://github.com/brightcove/videojs-contextmenu-ui/commit/10473f0)), closes [#19](https://github.com/brightcove/videojs-contextmenu-ui/issues/19)
63 | * update to generator-videojs-plugin[@7](https://github.com/7).2.0 ([d301a35](https://github.com/brightcove/videojs-contextmenu-ui/commit/d301a35))
64 | * Update to use plugin generator v7.0.2 ([a236811](https://github.com/brightcove/videojs-contextmenu-ui/commit/a236811))
65 | * Update tooling with generator-videojs-plugin v5.2.0 (#13) ([4e915a4](https://github.com/brightcove/videojs-contextmenu-ui/commit/4e915a4)), closes [#13](https://github.com/brightcove/videojs-contextmenu-ui/issues/13)
66 | * **package:** update videojs-generate-rollup-config to version 2.2.0 (#23) ([9e9943f](https://github.com/brightcove/videojs-contextmenu-ui/commit/9e9943f)), closes [#23](https://github.com/brightcove/videojs-contextmenu-ui/issues/23)
67 |
68 | ### Code Refactoring
69 |
70 | * Listen for native contextmenu event instead of emulated vjs-contextmenu. (#18) ([d37c56d](https://github.com/brightcove/videojs-contextmenu-ui/commit/d37c56d)), closes [#18](https://github.com/brightcove/videojs-contextmenu-ui/issues/18)
71 |
72 |
73 | ### BREAKING CHANGES
74 |
75 | * This removes the implicit dependency on the now-deprecated videojs-contextmenu plugin and updates minimum Video.js compatibility to v6 or v7.
76 |
77 |
78 | # 4.0.0 (2017-05-19)
79 |
80 | ### Chores
81 |
82 | * Update tooling using generator v5 prerelease. (#11) ([4b15da5](https://github.com/brightcove/videojs-contextmenu-ui/commit/4b15da5))
83 |
84 | ### BREAKING CHANGES
85 |
86 | * Remove Bower support.
87 |
88 | ## 3.0.5 (2017-02-08)
89 | * chore: better Travis build, and remove deprecation warnings
90 |
91 | ## 3.0.4 (2017-01-26)
92 | * refactor: Updates for Video.js 6.0 compatibility.
93 | * refactor: Removed logging statements.
94 |
95 | ## 3.0.3 (2016-08-02)
96 | * Fix menu display logic (#1)
97 |
98 | ## 3.0.2 (2016-07-20)
99 | * Override some video.js styles
100 |
101 | ## 3.0.1 (2016-07-20)
102 | * Fixes an issue where menu item listeners did not fire
103 |
104 | ## 3.0.0 (2016-07-20)
105 | * Removed the modal in favor of a menu
106 | * Cleaned up UI design
107 | * Context menu now appears only on _alternating_ contextmenu events (if those are what triggered the vjs-contextmenu event)
108 | * Context menu hides when the user begins interacting with the player or the document outside the player
109 |
110 | ## 2.0.0 (2016-07-13)
111 | * Bind item listeners to the player instance
112 | * Expose the modal and all re-initialization of the plugin
113 |
114 | ## 1.0.0 (2016-07-12)
115 | * Initial release
116 |
117 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Brightcove, Inc.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # videojs-contextmenu-ui
2 |
3 | [](https://travis-ci.org/brightcove/videojs-contextmenu-ui)
4 | [](https://greenkeeper.io/)
5 | [](http://slack.videojs.com)
6 |
7 | [](https://nodei.co/npm/videojs-contextmenu-ui/)
8 |
9 | A cross-device context menu UI for video.js players.
10 |
11 | > **Note:** Versions 4.x and lower of this plugin depended on the [videojs-contextmenu][contextmenu] plugin, but that plugin is not included with it. It must be included separately.
12 | >
13 | > Versions 5.x and newer does not use the videojs-contextmenu plugin, so do not include it. Versions 5.x and newer rely on the native `contextmenu` event.
14 |
15 | Maintenance Status: Stable
16 |
17 |
18 |
19 |
20 |
21 | - [Installation](#installation)
22 | - [Usage](#usage)
23 | - [Options](#options)
24 | - [`content`](#content)
25 | - [`keepInside`](#keepinside)
26 | - [`excludeElements`](#excludeelements)
27 | - [Inclusion](#inclusion)
28 | - [`
120 |
121 |
126 | ```
127 |
128 | ### CommonJS/Browserify
129 |
130 | When using with Browserify, install videojs-contextmenu-ui via npm and `require` the plugin as you would any other module.
131 |
132 | ```js
133 | var videojs = require('video.js');
134 |
135 | // The actual plugin function is exported by this module, but it is also
136 | // attached to the `Player.prototype`; so, there is no need to assign it
137 | // to a variable.
138 | require('videojs-contextmenu-ui');
139 |
140 | var player = videojs('my-video');
141 |
142 | player.contextmenuUI();
143 | ```
144 |
145 | ### RequireJS/AMD
146 |
147 | When using with RequireJS (or another AMD library), get the script in whatever way you prefer and `require` the plugin as you normally would:
148 |
149 | ```js
150 | require(['video.js', 'videojs-contextmenu-ui'], function(videojs) {
151 | var player = videojs('my-video');
152 |
153 | player.contextmenuUI();
154 | });
155 | ```
156 |
157 | ## License
158 |
159 | Apache-2.0. Copyright (c) Brightcove, Inc.
160 |
161 |
162 | [contextmenu]: https://github.com/brightcove/videojs-contextmenu
163 | [videojs]: http://videojs.com/
164 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | videojs-contextmenu-ui Demo
6 |
7 |
8 |
9 |
10 |
14 |
18 |
19 |
20 |
21 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "videojs-contextmenu-ui",
3 | "version": "7.0.0",
4 | "description": "A cross-device context menu UI for video.js players.",
5 | "main": "dist/videojs-contextmenu-ui.cjs.js",
6 | "keywords": [
7 | "videojs",
8 | "videojs-plugin"
9 | ],
10 | "author": "Brightcove, Inc.",
11 | "license": "Apache-2.0",
12 | "module": "dist/videojs-contextmenu-ui.es.js",
13 | "generator-videojs-plugin": {
14 | "version": "7.3.2"
15 | },
16 | "scripts": {
17 | "prebuild": "npm run clean",
18 | "build": "npm-run-all -p build:*",
19 | "build:css": "postcss -o dist/videojs-contextmenu-ui.css --config scripts/postcss.config.js src/plugin.css",
20 | "build:js": "rollup -c scripts/rollup.config.js",
21 | "clean": "shx rm -rf ./dist ./test/dist",
22 | "postclean": "shx mkdir -p ./dist ./test/dist",
23 | "docs": "npm-run-all docs:*",
24 | "docs:toc": "doctoc README.md",
25 | "lint": "vjsstandard",
26 | "server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch",
27 | "start": "npm-run-all -p server watch",
28 | "pretest": "npm-run-all lint build",
29 | "test": "karma start scripts/karma.conf.js",
30 | "update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s",
31 | "preversion": "npm test",
32 | "version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md",
33 | "watch": "npm-run-all -p watch:*",
34 | "watch:css": "npm run build:css -- -w",
35 | "watch:js": "npm run build:js -- -w",
36 | "posttest": "shx cat test/dist/coverage/text.txt",
37 | "prepublishOnly": "npm run build && vjsverify --skip-es-check"
38 | },
39 | "vjsstandard": {
40 | "jsdoc": false,
41 | "ignore": [
42 | "dist",
43 | "docs",
44 | "test/dist"
45 | ]
46 | },
47 | "files": [
48 | "CONTRIBUTING.md",
49 | "dist/",
50 | "docs/",
51 | "index.html",
52 | "scripts/",
53 | "src/",
54 | "test/"
55 | ],
56 | "dependencies": {
57 | "global": "^4.4.0",
58 | "video.js": "^8.0.0"
59 | },
60 | "devDependencies": {
61 | "conventional-changelog-cli": "^2.1.1",
62 | "conventional-changelog-videojs": "^3.0.2",
63 | "doctoc": "^1.4.0",
64 | "husky": "^1.3.1",
65 | "karma": "^4.4.1",
66 | "lint-staged": "^8.2.1",
67 | "not-prerelease": "^1.0.1",
68 | "npm-merge-driver-install": "^1.1.1",
69 | "npm-run-all": "^4.1.5",
70 | "pkg-ok": "^2.3.1",
71 | "postcss-cli": "^6.1.3",
72 | "rollup": "^2.61.1",
73 | "shx": "^0.3.3",
74 | "sinon": "^6.3.5",
75 | "videojs-contextmenu": "^2.0.2",
76 | "videojs-generate-karma-config": "^8.0.1",
77 | "videojs-generate-postcss-config": "~2.0.1",
78 | "videojs-generate-rollup-config": "^7.0.0",
79 | "videojs-generator-verify": "^4.0.1",
80 | "videojs-standard": "^9.0.1"
81 | },
82 | "lint-staged": {
83 | "*.js": [
84 | "vjsstandard --fix",
85 | "git add"
86 | ],
87 | "README.md": [
88 | "npm run docs:toc",
89 | "git add"
90 | ]
91 | },
92 | "husky": {
93 | "hooks": {
94 | "pre-commit": "lint-staged"
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/scripts/karma.conf.js:
--------------------------------------------------------------------------------
1 | const generate = require('videojs-generate-karma-config');
2 |
3 | module.exports = function(config) {
4 |
5 | // see https://github.com/videojs/videojs-generate-karma-config
6 | // for options
7 | const options = {};
8 |
9 | config = generate(config, options);
10 |
11 | // any other custom stuff not supported by options here!
12 | };
13 |
14 |
--------------------------------------------------------------------------------
/scripts/postcss.config.js:
--------------------------------------------------------------------------------
1 | const generate = require('videojs-generate-postcss-config');
2 |
3 | module.exports = function(context) {
4 | const result = generate({}, context);
5 |
6 | // do custom stuff here
7 |
8 | return result;
9 | };
10 |
--------------------------------------------------------------------------------
/scripts/rollup.config.js:
--------------------------------------------------------------------------------
1 | const generate = require('videojs-generate-rollup-config');
2 |
3 | // see https://github.com/videojs/videojs-generate-rollup-config
4 | // for options
5 | const options = {};
6 | const config = generate(options);
7 |
8 | // Add additonal builds/customization here!
9 |
10 | // export the builds to rollup
11 | export default Object.values(config.builds);
12 |
--------------------------------------------------------------------------------
/src/context-menu-item.js:
--------------------------------------------------------------------------------
1 | import window from 'global/window';
2 | import videojs from 'video.js';
3 |
4 | const MenuItem = videojs.getComponent('MenuItem');
5 |
6 | class ContextMenuItem extends MenuItem {
7 |
8 | handleClick(e) {
9 | super.handleClick();
10 | this.options_.listener();
11 |
12 | // Close the containing menu after the call stack clears.
13 | window.setTimeout(() => {
14 | this.player().contextmenuUI.menu.dispose();
15 | }, 1);
16 | }
17 | }
18 |
19 | export default ContextMenuItem;
20 |
--------------------------------------------------------------------------------
/src/context-menu.js:
--------------------------------------------------------------------------------
1 | import window from 'global/window';
2 | import videojs from 'video.js';
3 | import ContextMenuItem from './context-menu-item';
4 |
5 | const Menu = videojs.getComponent('Menu');
6 | // support VJS5 & VJS6 at the same time
7 | const dom = videojs.dom || videojs;
8 |
9 | class ContextMenu extends Menu {
10 |
11 | constructor(player, options) {
12 | super(player, options);
13 |
14 | // Each menu component has its own `dispose` method that can be
15 | // safely bound and unbound to events while maintaining its context.
16 | this.dispose = this.dispose.bind(this);
17 |
18 | options.content.forEach(c => {
19 | let fn = function() {};
20 |
21 | if (typeof c.listener === 'function') {
22 | fn = c.listener;
23 | } else if (typeof c.href === 'string') {
24 | fn = () => window.open(c.href);
25 | }
26 |
27 | this.addItem(new ContextMenuItem(player, {
28 | label: c.label,
29 | listener: fn.bind(player)
30 | }));
31 | });
32 | }
33 |
34 | createEl() {
35 | const el = super.createEl();
36 |
37 | dom.addClass(el, 'vjs-contextmenu-ui-menu');
38 | el.style.left = this.options_.position.left + 'px';
39 | el.style.top = this.options_.position.top + 'px';
40 |
41 | return el;
42 | }
43 | }
44 |
45 | export default ContextMenu;
46 |
--------------------------------------------------------------------------------
/src/plugin.css:
--------------------------------------------------------------------------------
1 | .vjs-contextmenu-ui-menu {
2 | position: absolute;
3 | }
4 |
5 | .vjs-contextmenu-ui-menu .vjs-menu-content {
6 |
7 | /* Same background-color as the control bar. */
8 | background-color: #2B333F;
9 | background-color: rgba(43, 51, 63, 0.7);
10 | border-radius: 0.3em;
11 | padding: 0.25em;
12 | }
13 |
14 | .vjs-contextmenu-ui-menu .vjs-menu-item {
15 | border-radius: 0.3em;
16 | cursor: pointer;
17 | margin: 0 0 1px;
18 | padding: 0.5em 1em;
19 |
20 | /* Override video.js styles for menus */
21 | font-size: 1em;
22 | line-height: 1.2;
23 | text-transform: none;
24 | }
25 |
26 | .vjs-contextmenu-ui-menu .vjs-menu-item:active,
27 | .vjs-contextmenu-ui-menu .vjs-menu-item:hover {
28 | background-color: rgba(0, 0, 0, 0.5);
29 | text-shadow: 0em 0em 1em white;
30 | }
31 |
--------------------------------------------------------------------------------
/src/plugin.js:
--------------------------------------------------------------------------------
1 | import document from 'global/document';
2 | import videojs from 'video.js';
3 | import ContextMenu from './context-menu';
4 | import {getPointerPosition} from './util';
5 | import {version as VERSION} from '../package.json';
6 |
7 | /**
8 | * Whether or not the player has an active context menu.
9 | *
10 | * @param {Player} player
11 | * @return {boolean}
12 | */
13 | function hasMenu(player) {
14 | return player.hasOwnProperty('contextmenuUI') &&
15 | player.contextmenuUI.hasOwnProperty('menu') &&
16 | player.contextmenuUI.menu.el();
17 | }
18 |
19 | /**
20 | * Defines which elements should be excluded from displaying the context menu
21 | *
22 | * @param {Object} targetEl The DOM element that is being targeted
23 | * @return {boolean} Whether or not the element should be excluded from displaying the context menu
24 | */
25 | function excludeElements(targetEl) {
26 | const tagName = targetEl.tagName.toLowerCase();
27 |
28 | return tagName === 'input' || tagName === 'textarea';
29 | }
30 |
31 | /**
32 | * Calculates the position of a menu based on the pointer position and player
33 | * size.
34 | *
35 | * @param {Object} pointerPosition
36 | * @param {Object} playerSize
37 | * @return {Object}
38 | */
39 | function findMenuPosition(pointerPosition, playerSize) {
40 | return {
41 | left: Math.round(playerSize.width * pointerPosition.x),
42 | top: Math.round(playerSize.height - (playerSize.height * pointerPosition.y))
43 | };
44 | }
45 |
46 | /**
47 | * Handles contextmenu events.
48 | *
49 | * @param {Event} e
50 | */
51 | function onContextMenu(e) {
52 |
53 | // If this event happens while the custom menu is open, close it and do
54 | // nothing else. This will cause native contextmenu events to be intercepted
55 | // once again; so, the next time a contextmenu event is encountered, we'll
56 | // open the custom menu.
57 | if (hasMenu(this)) {
58 | this.contextmenuUI.menu.dispose();
59 | return;
60 | }
61 |
62 | if (this.contextmenuUI.options_.excludeElements(e.target)) {
63 | return;
64 | }
65 |
66 | // Calculate the positioning of the menu based on the player size and
67 | // triggering event.
68 | const pointerPosition = getPointerPosition(this.el(), e);
69 | const playerSize = this.el().getBoundingClientRect();
70 | const menuPosition = findMenuPosition(pointerPosition, playerSize);
71 | // A workaround for Firefox issue where "oncontextmenu" event
72 | // leaks "click" event to document https://bugzilla.mozilla.org/show_bug.cgi?id=990614
73 | const documentEl = videojs.browser.IS_FIREFOX ? document.documentElement : document;
74 |
75 | e.preventDefault();
76 |
77 | const menu = this.contextmenuUI.menu = new ContextMenu(this, {
78 | content: this.contextmenuUI.content,
79 | position: menuPosition
80 | });
81 |
82 | // This is for backward compatibility. We no longer have the `closeMenu`
83 | // function, but removing it would necessitate a major version bump.
84 | this.contextmenuUI.closeMenu = () => {
85 | videojs.log.warn('player.contextmenuUI.closeMenu() is deprecated, please use player.contextmenuUI.menu.dispose() instead!');
86 | menu.dispose();
87 | };
88 |
89 | menu.on('dispose', () => {
90 | videojs.off(documentEl, ['click', 'tap'], menu.dispose);
91 | this.removeChild(menu);
92 | delete this.contextmenuUI.menu;
93 | });
94 |
95 | this.addChild(menu);
96 |
97 | const menuSize = menu.el_.getBoundingClientRect();
98 | const bodySize = document.body.getBoundingClientRect();
99 |
100 | if (this.contextmenuUI.keepInside ||
101 | menuSize.right > bodySize.width ||
102 | menuSize.bottom > bodySize.height) {
103 | menu.el_.style.left = Math.floor(Math.min(
104 | menuPosition.left,
105 | this.player_.currentWidth() - menu.currentWidth()
106 | )) + 'px';
107 | menu.el_.style.top = Math.floor(Math.min(
108 | menuPosition.top,
109 | this.player_.currentHeight() - menu.currentHeight()
110 | )) + 'px';
111 | }
112 |
113 | videojs.on(documentEl, ['click', 'tap'], menu.dispose);
114 | }
115 |
116 | /**
117 | * Creates a menu for contextmenu events.
118 | *
119 | * @function contextmenuUI
120 | * @param {Object} options
121 | * @param {Array} options.content
122 | * An array of objects which populate a content list within the menu.
123 | * @param {boolean} options.keepInside
124 | * Whether to always keep the menu inside the player
125 | * @param {function} options.excludeElements
126 | * Defines which elements should be excluded from displaying the context menu
127 | */
128 | function contextmenuUI(options) {
129 | const defaults = {
130 | keepInside: true,
131 | excludeElements
132 | };
133 |
134 | options = videojs.obj.merge(defaults, options);
135 |
136 | if (!Array.isArray(options.content)) {
137 | throw new Error('"content" required');
138 | }
139 |
140 | // If we have already invoked the plugin, teardown before setting up again.
141 | if (hasMenu(this)) {
142 | this.contextmenuUI.menu.dispose();
143 | this.off('contextmenu', this.contextmenuUI.onContextMenu);
144 |
145 | // Deleting the player-specific contextmenuUI plugin function/namespace will
146 | // restore the original plugin function, so it can be called again.
147 | delete this.contextmenuUI;
148 | }
149 |
150 | // Wrap the plugin function with an player instance-specific function. This
151 | // allows us to attach the menu to it without affecting other players on
152 | // the page.
153 | const cmui = this.contextmenuUI = function() {
154 | contextmenuUI.apply(this, arguments);
155 | };
156 |
157 | cmui.onContextMenu = onContextMenu.bind(this);
158 | cmui.content = options.content;
159 | cmui.keepInside = options.keepInside;
160 | cmui.options_ = options;
161 | cmui.VERSION = VERSION;
162 |
163 | this.on('contextmenu', cmui.onContextMenu);
164 | this.ready(() => this.addClass('vjs-contextmenu-ui'));
165 | }
166 |
167 | videojs.registerPlugin('contextmenuUI', contextmenuUI);
168 | contextmenuUI.VERSION = VERSION;
169 |
170 | export default contextmenuUI;
171 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | // For now, these are copy-pasted from video.js until they are exposed.
2 |
3 | import document from 'global/document';
4 | import window from 'global/window';
5 |
6 | /**
7 | * Offset Left
8 | * getBoundingClientRect technique from
9 | * John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/
10 | *
11 | * @function findElPosition
12 | * @param {Element} el Element from which to get offset
13 | * @return {Object}
14 | */
15 | export function findElPosition(el) {
16 | let box;
17 |
18 | if (el.getBoundingClientRect && el.parentNode) {
19 | box = el.getBoundingClientRect();
20 | }
21 |
22 | if (!box) {
23 | return {
24 | left: 0,
25 | top: 0
26 | };
27 | }
28 |
29 | const docEl = document.documentElement;
30 | const body = document.body;
31 |
32 | const clientLeft = docEl.clientLeft || body.clientLeft || 0;
33 | const scrollLeft = window.pageXOffset || body.scrollLeft;
34 | const left = box.left + scrollLeft - clientLeft;
35 |
36 | const clientTop = docEl.clientTop || body.clientTop || 0;
37 | const scrollTop = window.pageYOffset || body.scrollTop;
38 | const top = box.top + scrollTop - clientTop;
39 |
40 | // Android sometimes returns slightly off decimal values, so need to round
41 | return {
42 | left: Math.round(left),
43 | top: Math.round(top)
44 | };
45 | }
46 |
47 | /**
48 | * Get pointer position in element
49 | * Returns an object with x and y coordinates.
50 | * The base on the coordinates are the bottom left of the element.
51 | *
52 | * @function getPointerPosition
53 | * @param {Element} el Element on which to get the pointer position on
54 | * @param {Event} event Event object
55 | * @return {Object}
56 | * This object will have x and y coordinates corresponding to the
57 | * mouse position
58 | */
59 | export function getPointerPosition(el, event) {
60 | const position = {};
61 | const box = findElPosition(el);
62 | const boxW = el.offsetWidth;
63 | const boxH = el.offsetHeight;
64 | const boxY = box.top;
65 | const boxX = box.left;
66 | let pageY = event.pageY;
67 | let pageX = event.pageX;
68 |
69 | if (event.changedTouches) {
70 | pageX = event.changedTouches[0].pageX;
71 | pageY = event.changedTouches[0].pageY;
72 | }
73 |
74 | position.y = Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH));
75 | position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
76 |
77 | return position;
78 | }
79 |
--------------------------------------------------------------------------------
/test/plugin.test.js:
--------------------------------------------------------------------------------
1 | import document from 'global/document';
2 | import QUnit from 'qunit';
3 | import sinon from 'sinon';
4 | import videojs from 'video.js';
5 | import plugin from '../src/plugin';
6 |
7 | import 'videojs-contextmenu';
8 |
9 | QUnit.test('the environment is sane', function(assert) {
10 | assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists');
11 | assert.strictEqual(typeof sinon, 'object', 'sinon exists');
12 | assert.strictEqual(typeof videojs, 'function', 'videojs exists');
13 | assert.strictEqual(typeof plugin, 'function', 'plugin is a function');
14 | });
15 |
16 | QUnit.module('videojs-contextmenu-ui', {
17 |
18 | beforeEach() {
19 |
20 | // Mock the environment's timers because certain things - particularly
21 | // player readiness - are asynchronous in video.js 5. This MUST come
22 | // before any player is created; otherwise, timers could get created
23 | // with the actual timer methods!
24 | this.clock = sinon.useFakeTimers();
25 |
26 | this.fixture = document.getElementById('qunit-fixture');
27 | this.video = document.createElement('video');
28 | this.fixture.appendChild(this.video);
29 | this.player = videojs(this.video);
30 |
31 | this.player.contextmenuUI({
32 | content: [{
33 | href: 'https://www.brightcove.com/',
34 | label: 'Brightcove'
35 | }, {
36 | label: 'Example Link',
37 | listener() {
38 | videojs.log('you clicked the example link!');
39 | }
40 | }]
41 | });
42 |
43 | // Tick the clock forward enough to trigger the player to be "ready".
44 | this.clock.tick(1);
45 | },
46 |
47 | afterEach() {
48 |
49 | // Make sure we shut off document-level listeners we may have created!
50 | // videojs.off(document, ['mousedown', 'touchstart']);
51 | this.player.dispose();
52 | this.clock.restore();
53 | }
54 | });
55 |
56 | QUnit.test('opens a custom context menu on the first "contextmenu" event encountered', function(assert) {
57 | this.player.trigger({
58 | type: 'contextmenu',
59 | pageX: 0,
60 | pageY: 0
61 | });
62 |
63 | assert.strictEqual(this.player.$$('.vjs-contextmenu-ui-menu').length, 1);
64 | });
65 |
66 | QUnit.test('closes the custom context menu on the second "contextmenu" event encountered', function(assert) {
67 | this.player.trigger({
68 | type: 'contextmenu',
69 | pageX: 0,
70 | pageY: 0
71 | });
72 |
73 | this.player.trigger({
74 | type: 'contextmenu',
75 | pageX: 0,
76 | pageY: 0
77 | });
78 |
79 | assert.strictEqual(this.player.$$('.vjs-contextmenu-ui-menu').length, 0);
80 | });
81 |
82 | QUnit.test('closes the custom context menu when interacting with the player or document outside the menu', function(assert) {
83 | this.player.trigger({
84 | type: 'contextmenu',
85 | pageX: 0,
86 | pageY: 0
87 | });
88 |
89 | // A workaround for Firefox issue where "oncontextmenu" event
90 | // leaks "click" event to document https://bugzilla.mozilla.org/show_bug.cgi?id=990614
91 | const documentEl = videojs.browser.IS_FIREFOX ? document.documentElement : document;
92 |
93 | videojs.trigger(documentEl, 'click');
94 |
95 | assert.strictEqual(this.player.$$('.vjs-contextmenu-ui-menu').length, 0);
96 | });
97 |
98 | QUnit.test('do not open context menu if in excluded element', function(assert) {
99 | const inputElement = document.createElement('input');
100 |
101 | inputElement.className = 'vjs-input-element';
102 | this.player.createModal(inputElement);
103 |
104 | const rightClick = document.createEvent('MouseEvents');
105 |
106 | rightClick.initMouseEvent('contextmenu', true, true, this.window, 1, 0, 0, 0, 0, false, false, false, false, 2, null);
107 |
108 | this.player.$('.vjs-input-element').dispatchEvent(rightClick);
109 |
110 | assert.strictEqual(this.player.$$('.vjs-contextmenu-ui-menu').length, 0);
111 | });
112 |
113 | QUnit.test('do not open context menu if in custom excluded element', function(assert) {
114 | const divElement = document.createElement('a');
115 |
116 | divElement.className = 'vjs-anchor-element';
117 | this.player.el().appendChild(divElement);
118 |
119 | this.player.contextmenuUI.options_.excludeElements = (targetEl) => {
120 | const tagName = targetEl.tagName.toLowerCase();
121 |
122 | return tagName === 'a';
123 | };
124 |
125 | const rightClick = document.createEvent('MouseEvents');
126 |
127 | rightClick.initMouseEvent('contextmenu', true, true, this.window, 1, 0, 0, 0, 0, false, false, false, false, 2, null);
128 |
129 | this.player.$('.vjs-anchor-element').dispatchEvent(rightClick);
130 |
131 | assert.strictEqual(this.player.$$('.vjs-contextmenu-ui-menu').length, 0);
132 | });
133 |
--------------------------------------------------------------------------------