├── .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 | [![Build Status](https://travis-ci.org/brightcove/videojs-contextmenu-ui.svg?branch=master)](https://travis-ci.org/brightcove/videojs-contextmenu-ui) 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/brightcove/videojs-contextmenu-ui.svg)](https://greenkeeper.io/) 5 | [![Slack Status](http://slack.videojs.com/badge.svg)](http://slack.videojs.com) 6 | 7 | [![NPM](https://nodei.co/npm/videojs-contextmenu-ui.png?downloads=true&downloadRank=true)](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 | --------------------------------------------------------------------------------