videojs-contextmenu-pt
Version:
A cross-device context menu UI for video.js players.
339 lines (271 loc) • 11.1 kB
JavaScript
/*! @name videojs-contextmenu-pt @version 5.4.1 @license Apache-2.0 */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('video.js'), require('global/document'), require('global/window')) :
typeof define === 'function' && define.amd ? define(['video.js', 'global/document', 'global/window'], factory) :
(global.videojsContextmenuPt = factory(global.videojs,global.document,global.window));
}(this, (function (videojs,document,window) { 'use strict';
videojs = videojs && videojs.hasOwnProperty('default') ? videojs['default'] : videojs;
document = document && document.hasOwnProperty('default') ? document['default'] : document;
window = window && window.hasOwnProperty('default') ? window['default'] : window;
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
var MenuItem = videojs.getComponent('MenuItem');
var ContextMenuItem =
/*#__PURE__*/
function (_MenuItem) {
_inheritsLoose(ContextMenuItem, _MenuItem);
function ContextMenuItem() {
return _MenuItem.apply(this, arguments) || this;
}
var _proto = ContextMenuItem.prototype;
_proto.handleClick = function handleClick(e) {
var _this = this;
_MenuItem.prototype.handleClick.call(this);
this.options_.listener(); // Close the containing menu after the call stack clears.
window.setTimeout(function () {
_this.player().contextmenuUI.menu.dispose();
}, 1);
};
return ContextMenuItem;
}(MenuItem);
var Menu = videojs.getComponent('Menu'); // support VJS5 & VJS6 at the same time
var dom = videojs.dom || videojs;
var ContextMenu =
/*#__PURE__*/
function (_Menu) {
_inheritsLoose(ContextMenu, _Menu);
function ContextMenu(player, options) {
var _this;
_this = _Menu.call(this, player, options) || this; // Each menu component has its own `dispose` method that can be
// safely bound and unbound to events while maintaining its context.
_this.dispose = videojs.bind(_assertThisInitialized(_this), _this.dispose);
options.content.forEach(function (c) {
var fn = function fn() {};
if (typeof c.listener === 'function') {
fn = c.listener;
} else if (typeof c.href === 'string') {
fn = function fn() {
return window.open(c.href);
};
}
_this.addItem(new ContextMenuItem(player, {
label: c.label,
listener: videojs.bind(player, fn)
}));
});
return _this;
}
var _proto = ContextMenu.prototype;
_proto.createEl = function createEl() {
var el = _Menu.prototype.createEl.call(this);
dom.addClass(el, 'vjs-contextmenu-ui-menu');
el.style.left = this.options_.position.left + 'px';
el.style.top = this.options_.position.top + 'px';
return el;
};
return ContextMenu;
}(Menu);
// For now, these are copy-pasted from video.js until they are exposed.
/**
* Offset Left
* getBoundingClientRect technique from
* John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/
*
* @function findElPosition
* @param {Element} el Element from which to get offset
* @return {Object}
*/
function findElPosition(el) {
var box;
if (el.getBoundingClientRect && el.parentNode) {
box = el.getBoundingClientRect();
}
if (!box) {
return {
left: 0,
top: 0
};
}
var docEl = document.documentElement;
var body = document.body;
var clientLeft = docEl.clientLeft || body.clientLeft || 0;
var scrollLeft = window.pageXOffset || body.scrollLeft;
var left = box.left + scrollLeft - clientLeft;
var clientTop = docEl.clientTop || body.clientTop || 0;
var scrollTop = window.pageYOffset || body.scrollTop;
var top = box.top + scrollTop - clientTop; // Android sometimes returns slightly off decimal values, so need to round
return {
left: Math.round(left),
top: Math.round(top)
};
}
/**
* Get pointer position in element
* Returns an object with x and y coordinates.
* The base on the coordinates are the bottom left of the element.
*
* @function getPointerPosition
* @param {Element} el Element on which to get the pointer position on
* @param {Event} event Event object
* @return {Object}
* This object will have x and y coordinates corresponding to the
* mouse position
*/
function getPointerPosition(el, event) {
var position = {};
var box = findElPosition(el);
var boxW = el.offsetWidth;
var boxH = el.offsetHeight;
var boxY = box.top;
var boxX = box.left;
var pageY = event.pageY;
var pageX = event.pageX;
if (event.changedTouches) {
pageX = event.changedTouches[0].pageX;
pageY = event.changedTouches[0].pageY;
}
position.y = Math.max(0, Math.min(1, (boxY - pageY + boxH) / boxH));
position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
return position;
}
function isFunction(functionToCheck) {
return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}
var version = "5.4.1";
/**
* Whether or not the player has an active context menu.
*
* @param {Player} player
* @return {boolean}
*/
function hasMenu(player) {
return player.hasOwnProperty('contextmenuUI') && player.contextmenuUI.hasOwnProperty('menu') && player.contextmenuUI.menu.el();
}
/**
* Defines which elements should be excluded from displaying the context menu
*
* @param {Object} targetEl The DOM element that is being targeted
* @return {boolean} Whether or not the element should be excluded from displaying the context menu
*/
function excludeElements(targetEl) {
var tagName = targetEl.tagName.toLowerCase();
return tagName === 'input' || tagName === 'textarea';
}
/**
* Calculates the position of a menu based on the pointer position and player
* size.
*
* @param {Object} pointerPosition
* @param {Object} playerSize
* @return {Object}
*/
function findMenuPosition(pointerPosition, playerSize) {
return {
left: Math.round(playerSize.width * pointerPosition.x),
top: Math.round(playerSize.height - playerSize.height * pointerPosition.y)
};
}
/**
* Handles contextmenu events.
*
* @param {Event} e
*/
function onContextMenu(e) {
var _this = this;
// If this event happens while the custom menu is open, close it and do
// nothing else. This will cause native contextmenu events to be intercepted
// once again; so, the next time a contextmenu event is encountered, we'll
// open the custom menu.
if (hasMenu(this)) {
this.contextmenuUI.menu.dispose();
return;
}
if (this.contextmenuUI.options_.excludeElements(e.target)) {
return;
} // Calculate the positioning of the menu based on the player size and
// triggering event.
var pointerPosition = getPointerPosition(this.el(), e);
var playerSize = this.el().getBoundingClientRect();
var menuPosition = findMenuPosition(pointerPosition, playerSize); // A workaround for Firefox issue where "oncontextmenu" event
// leaks "click" event to document https://bugzilla.mozilla.org/show_bug.cgi?id=990614
var documentEl = videojs.browser.IS_FIREFOX ? document.documentElement : document;
e.preventDefault();
var menu = this.contextmenuUI.menu = new ContextMenu(this, {
content: isFunction(this.contextmenuUI.content) && this.contextmenuUI.content() || this.contextmenuUI.content,
position: menuPosition
}); // This is for backward compatibility. We no longer have the `closeMenu`
// function, but removing it would necessitate a major version bump.
this.contextmenuUI.closeMenu = function () {
videojs.log.warn('player.contextmenuUI.closeMenu() is deprecated, please use player.contextmenuUI.menu.dispose() instead!');
menu.dispose();
};
menu.on('dispose', function () {
videojs.off(documentEl, ['click', 'tap'], menu.dispose);
_this.removeChild(menu);
delete _this.contextmenuUI.menu;
});
this.addChild(menu);
var menuSize = menu.el_.getBoundingClientRect();
var bodySize = document.body.getBoundingClientRect();
if (this.contextmenuUI.keepInside || menuSize.right > bodySize.width || menuSize.bottom > bodySize.height) {
menu.el_.style.left = Math.floor(Math.min(menuPosition.left, this.player_.currentWidth() - menu.currentWidth())) + 'px';
menu.el_.style.top = Math.floor(Math.min(menuPosition.top, this.player_.currentHeight() - menu.currentHeight())) + 'px';
}
videojs.on(documentEl, ['click', 'tap'], menu.dispose);
}
/**
* Creates a menu for contextmenu events.
*
* @function contextmenuUI
* @param {Object} options
* @param {Array} options.content
* An array of objects which populate a content list within the menu.
* @param {boolean} options.keepInside
* Whether to always keep the menu inside the player
* @param {function} options.excludeElements
* Defines which elements should be excluded from displaying the context menu
*/
function contextmenuUI(options) {
var _this2 = this;
var defaults = {
keepInside: true,
excludeElements: excludeElements
};
options = videojs.mergeOptions(defaults, options);
if (!Array.isArray(options.content) && !Array.isArray(options.content())) {
throw new Error('"content" required');
} // If we have already invoked the plugin, teardown before setting up again.
if (hasMenu(this)) {
this.contextmenuUI.menu.dispose();
this.off('contextmenu', this.contextmenuUI.onContextMenu); // Deleting the player-specific contextmenuUI plugin function/namespace will
// restore the original plugin function, so it can be called again.
delete this.contextmenuUI;
} // Wrap the plugin function with an player instance-specific function. This
// allows us to attach the menu to it without affecting other players on
// the page.
var cmui = this.contextmenuUI = function () {
contextmenuUI.apply(this, arguments);
};
cmui.onContextMenu = videojs.bind(this, onContextMenu);
cmui.content = options.content;
cmui.keepInside = options.keepInside;
cmui.options_ = options;
cmui.VERSION = version;
this.on('contextmenu', cmui.onContextMenu);
this.ready(function () {
return _this2.addClass('vjs-contextmenu-ui');
});
}
videojs.registerPlugin('contextmenuUI', contextmenuUI);
contextmenuUI.VERSION = version;
return contextmenuUI;
})));