├── .babelrc ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo └── index.html ├── dist └── clappr-markers-plugin.js ├── package-lock.json ├── package.json ├── screenshot.jpg ├── src ├── base-marker.js ├── image-marker.js ├── index.js ├── marker.js ├── standard-marker.js └── style.sass └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "add-module-exports" 5 | ] 6 | } -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [10.x, 12.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install and build 20 | run: | 21 | npm ci 22 | npm run build 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tom Jenkinson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/clappr-markers-plugin.svg)](https://badge.fury.io/js/clappr-markers-plugin) 2 | # Clappr Markers Plugin 3 | A plugin for clappr which will display markers (and tooltips) at configured points scrub bar. 4 | 5 | ![Screenshot](screenshot.jpg) 6 | 7 | # Usage 8 | Add both Clappr and the markers plugin scripts to your HTML: 9 | 10 | ```html 11 | 12 | 13 | 14 | 15 | ``` 16 | 17 | You can also find the project on npm: https://www.npmjs.com/package/clappr-markers-plugin 18 | 19 | Then just add `ClapprMarkersPlugin` into the list of plugins of your player instance, and the options for the plugin go in the `markersPlugin` property as shown below. 20 | 21 | ```javascript 22 | var player = new Clappr.Player({ 23 | source: "http://your.video/here.mp4", 24 | plugins: { 25 | core: [ClapprMarkersPlugin] 26 | }, 27 | markersPlugin: { 28 | markers: [ 29 | new ClapprMarkersPlugin.StandardMarker(0, "The beginning!"), 30 | new ClapprMarkersPlugin.StandardMarker(90, "Something interesting."), 31 | new ClapprMarkersPlugin.StandardMarker(450, "The conclusion.") 32 | ], 33 | tooltipBottomMargin: 17 // optional 34 | } 35 | }); 36 | ``` 37 | 38 | The first paramater to `StandardMarker` is the time in seconds that the marker represents, and the second is the message to be displayed on the tooltip when the user hovers over the marker, and is optional. 39 | 40 | The `tooltipBottomMargin` option is optional and specifies the amount of space below tooltips. It defaults to 17. 41 | 42 | You can customise both the marker and the tooltip by extending the [`ClapprMarkersPlugin.Marker` class](src/marker.js). Look at the comments in that file for details. 43 | 44 | ## Image Marker 45 | `ImageMarker` works in the same way as `StandardMarker`, but the second parameter is a url to an image to show in the tooltip. 46 | 47 | ## Updating The Time of a Marker 48 | You can update the time of a marker by calling the `setTime()` method on `StandardMarker`. 49 | 50 | ## Adding and Removing Markers Programatically 51 | You can add a marker programatically by using the `addMarker()` method. To remove a marker use the `removeMarker()` method and provide the instance to the `Marker` to remove as the first argument. To remove all markers use the `clearMarkers()` method. 52 | 53 | # Demo 54 | To run the demo start a web server with the root directory being the root of this repo, and then browse to the "index.html" file in the "demo" folder. 55 | 56 | I am also hosting a demo at http://tjenkinson.me/clappr-markers-plugin/ 57 | 58 | # Development 59 | When submitting a PR please don't include changes to the dist folder. 60 | 61 | Install dependencies: 62 | 63 | `npm install` 64 | 65 | Build: 66 | 67 | `npm run build` 68 | 69 | Minified version: 70 | 71 | `npm run release` 72 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 49 | 50 | -------------------------------------------------------------------------------- /dist/clappr-markers-plugin.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("clappr")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["clappr"], factory); 6 | else if(typeof exports === 'object') 7 | exports["ClapprMarkersPlugin"] = factory(require("clappr")); 8 | else 9 | root["ClapprMarkersPlugin"] = factory(root["Clappr"]); 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_1__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ (function(module, exports, __webpack_require__) { 56 | 57 | 'use strict';Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i=0;i--){this._removeInternalMarker(i);}}/* 66 | * Get all markers 67 | */},{key:'getAll',value:function getAll(){return this._markers.map(function(internalMarker){return internalMarker.source;});}/* 68 | * Get marker by index. Can be used with removeMarker() to remove a marker by index. 69 | */},{key:'getByIndex',value:function getByIndex(index){if(index>=this._markers.length||index<0){return null;}return this._markers[index].source;}},{key:'_bindContainerEvents',value:function _bindContainerEvents(){if(this._oldContainer){this.stopListening(this._oldContainer,_clappr.Events.CONTAINER_TIMEUPDATE,this._onTimeUpdate);this.stopListening(this._oldContainer,_clappr.Events.CONTAINER_MEDIACONTROL_SHOW,this._onMediaControlShow);}this._oldContainer=this.core.mediaControl.container;this.listenTo(this.core.mediaControl.container,_clappr.Events.CONTAINER_TIMEUPDATE,this._onTimeUpdate);this.listenTo(this.core.mediaControl.container,_clappr.Events.CONTAINER_MEDIACONTROL_SHOW,this._onMediaControlShow);}},{key:'_getOptions',value:function _getOptions(){if(!("markersPlugin"in this.core.options)){throw"'markersPlugin' property missing from options object.";}return this.core.options.markersPlugin;}// build a marker object for internal use from the provided Marker 70 | },{key:'_buildInternalMarker',value:function _buildInternalMarker(marker){var $tooltip=marker.getTooltipEl();if($tooltip){$tooltip=(0,_clappr.$)($tooltip);}return{source:marker,emitter:marker.getEmitter(),$marker:(0,_clappr.$)(marker.getMarkerEl()),markerLeft:null,$tooltip:$tooltip,$tooltipContainer:null,tooltipContainerLeft:null,tooltipContainerBottom:null,tooltipChangedHandler:null,time:marker.getTime(),timeChangedHandler:null,onDestroy:marker.onDestroy};}},{key:'_createInitialMarkers',value:function _createInitialMarkers(){var _this2=this;var markers=this._getOptions().markers;if(!markers){return;}this._markers=[];markers.forEach(function(a){_this2._markers.push(_this2._buildInternalMarker(a));});// append the marker elements to the dom 71 | this._markers.forEach(function(marker){_this2._createMarkerEl(marker);});this._renderMarkers();}},{key:'_createMarkerEl',value:function _createMarkerEl(marker){var _this3=this;// marker 72 | var $marker=marker.$marker;marker.timeChangedHandler=function(){// fired from marker if it's time changes 73 | _this3._updateMarkerTime(marker);};marker.emitter.on("timeChanged",marker.timeChangedHandler);$marker.click(function(e){// when marker clicked seek to the exact time represented by the marker 74 | _this3.core.mediaControl.container.seek(marker.time);e.preventDefault();e.stopImmediatePropagation();});this._$markers.append($marker);// tooltip 75 | var $tooltip=marker.$tooltip;if($tooltip){// there is a tooltip 76 | var $tooltipContainer=(0,_clappr.$)("
").addClass("tooltip-container");marker.$tooltipContainer=$tooltipContainer;$tooltipContainer.append($tooltip);this._$tooltips.append($tooltipContainer);marker.tooltipChangedHandler=function(){// fired from marker if it's tooltip contents changes 77 | _this3._updateTooltipPosition(marker);};marker.emitter.on("tooltipChanged",marker.tooltipChangedHandler);this._updateTooltipPosition(marker);}}},{key:'_updateMarkerTime',value:function _updateMarkerTime(marker){marker.time=marker.source.getTime();this._renderMarkers();}// calculates and sets the position of the tooltip 78 | },{key:'_updateTooltipPosition',value:function _updateTooltipPosition(marker){if(!this._mediaControlContainerLoaded||!this._duration){// renderMarkers() will be called when it has loaded, which will call this 79 | return;}var $tooltipContainer=marker.$tooltipContainer;if(!$tooltipContainer){// no tooltip 80 | return;}var bottomMargin=this._getOptions().tooltipBottomMargin||17;var width=$tooltipContainer.width();var seekBarWidth=this._$tooltips.width();var leftPos=seekBarWidth*(marker.time/this._duration)-width/2;leftPos=Math.max(0,Math.min(leftPos,seekBarWidth-width));if(bottomMargin!==marker.tooltipContainerBottom||leftPos!==marker.tooltipContainerLeft){$tooltipContainer.css({bottom:bottomMargin+"px",left:leftPos+"px"});marker.tooltipContainerBottom=bottomMargin;marker.tooltipContainerLeft=leftPos;}}},{key:'_onMediaControlRendered',value:function _onMediaControlRendered(){this._appendElToMediaControl();}},{key:'_updateDuration',value:function _updateDuration(){this._duration=this.core.mediaControl.container.getDuration()||null;}},{key:'_onMediaControlContainerChanged',value:function _onMediaControlContainerChanged(){this._bindContainerEvents();this._mediaControlContainerLoaded=true;this._updateDuration();this._renderMarkers();}},{key:'_onTimeUpdate',value:function _onTimeUpdate(){// need to render on time update because if duration is increasing 81 | // markers will need to be repositioned 82 | this._updateDuration();this._renderMarkers();}},{key:'_onMediaControlShow',value:function _onMediaControlShow(){this._renderMarkers();}},{key:'_renderMarkers',value:function _renderMarkers(){var _this4=this;if(!this._mediaControlContainerLoaded||!this._duration){// this will be called again once loaded, or there is a duration > 0 83 | return;}this._markers.forEach(function(marker){var $el=marker.$marker;var percentage=Math.min(Math.max(marker.time/_this4._duration*100,0),100);if(marker.markerLeft!==percentage){$el.css("left",percentage+"%");marker.markerLeft=percentage;}_this4._updateTooltipPosition(marker);});}},{key:'_appendElToMediaControl',value:function _appendElToMediaControl(){this.core.mediaControl.$el.find(".bar-container").first().append(this.el);}},{key:'render',value:function render(){this._$markers=(0,_clappr.$)("
").addClass("markers-plugin-markers");this._$tooltips=(0,_clappr.$)("
").addClass("markers-plugin-tooltips");var $el=(0,_clappr.$)(this.el);$el.append(this._$markers);$el.append(this._$tooltips);this._appendElToMediaControl();return this;}},{key:'destroy',value:function destroy(){// remove any listeners and call onDestroy() 84 | this._markers.forEach(function(marker){if(marker.tooltipChangedHandler){marker.emitter.off("timeChanged",marker.timeChangedHandler);marker.emitter.off("tooltipChanged",marker.tooltipChangedHandler);}marker.onDestroy();});}}]);return MarkersPlugin;}(_clappr.UICorePlugin);exports.default=MarkersPlugin;module.exports=exports['default']; 85 | 86 | /***/ }), 87 | /* 1 */ 88 | /***/ (function(module, exports) { 89 | 90 | module.exports = __WEBPACK_EXTERNAL_MODULE_1__; 91 | 92 | /***/ }), 93 | /* 2 */ 94 | /***/ (function(module, exports, __webpack_require__) { 95 | 96 | "use strict";var _undefined=__webpack_require__(7)();// Support ES3 engines 97 | module.exports=function(val){return val!==_undefined&&val!==null;}; 98 | 99 | /***/ }), 100 | /* 3 */ 101 | /***/ (function(module, exports, __webpack_require__) { 102 | 103 | 'use strict';Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i').addClass('standard-marker');$marker.append((0,_clappr.$)('
').addClass('standard-marker-inner'));return $marker;}/* 117 | * Set the tooltip element for this marker. 118 | * 119 | * The tooltip will placed above the marker element, inside a container, 120 | * and this containers position will be managed for you. 121 | */},{key:'_setTooltipEl',value:function _setTooltipEl($el){if(this._$tooltip){throw new Error("Tooltip can only be set once.");}this._$tooltip=$el;this._addListenersForTooltip();}},{key:'_addListenersForTooltip',value:function _addListenersForTooltip(){var _this2=this;if(!this._$tooltip){return;}var $marker=this._$marker;var hovering=false;$marker.bind('mouseover',function(){if(hovering){return;}hovering=true;_this2._$tooltip.attr('data-show','1');_this2.notifyTooltipChanged();});$marker.bind('mouseout',function(){if(!hovering){return;}hovering=false;_this2._$tooltip.attr('data-show','0');});}}]);return BaseMarker;}(_marker2.default);exports.default=BaseMarker;module.exports=exports['default']; 122 | 123 | /***/ }), 124 | /* 4 */ 125 | /***/ (function(module, exports, __webpack_require__) { 126 | 127 | "use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i-1;}; 264 | 265 | /***/ }), 266 | /* 21 */ 267 | /***/ (function(module, exports, __webpack_require__) { 268 | 269 | 'use strict';var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj;}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj;};var d=__webpack_require__(6),callable=__webpack_require__(16),apply=Function.prototype.apply,call=Function.prototype.call,create=Object.create,defineProperty=Object.defineProperty,defineProperties=Object.defineProperties,hasOwnProperty=Object.prototype.hasOwnProperty,descriptor={configurable:true,enumerable:false,writable:true},on,_once2,off,emit,methods,descriptors,base;on=function on(type,listener){var data;callable(listener);if(!hasOwnProperty.call(this,'__ee__')){data=descriptor.value=create(null);defineProperty(this,'__ee__',descriptor);descriptor.value=null;}else{data=this.__ee__;}if(!data[type])data[type]=listener;else if(_typeof(data[type])==='object')data[type].push(listener);else data[type]=[data[type],listener];return this;};_once2=function once(type,listener){var _once,self;callable(listener);self=this;on.call(this,type,_once=function once(){off.call(self,type,_once);apply.call(listener,this,arguments);});_once.__eeOnceListener__=listener;return this;};off=function off(type,listener){var data,listeners,candidate,i;callable(listener);if(!hasOwnProperty.call(this,'__ee__'))return this;data=this.__ee__;if(!data[type])return this;listeners=data[type];if((typeof listeners==='undefined'?'undefined':_typeof(listeners))==='object'){for(i=0;candidate=listeners[i];++i){if(candidate===listener||candidate.__eeOnceListener__===listener){if(listeners.length===2)data[type]=listeners[i?0:1];else listeners.splice(i,1);}}}else{if(listeners===listener||listeners.__eeOnceListener__===listener){delete data[type];}}return this;};emit=function emit(type){var i,l,listener,listeners,args;if(!hasOwnProperty.call(this,'__ee__'))return;listeners=this.__ee__[type];if(!listeners)return;if((typeof listeners==='undefined'?'undefined':_typeof(listeners))==='object'){l=arguments.length;args=new Array(l-1);for(i=1;i').attr('src',this._tooltipImage).css({width:this._width,height:this._height});$img.one('load',this.notifyTooltipChanged.bind(this));return(0,_clappr.$)('
').addClass('image-tooltip').append($img);}}]);return ImageMarker;}(_baseMarker2.default);exports.default=ImageMarker;module.exports=exports['default']; 283 | 284 | /***/ }), 285 | /* 23 */ 286 | /***/ (function(module, exports, __webpack_require__) { 287 | 288 | 'use strict';Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i').addClass('standard-tooltip').text(this._tooltipText);}}]);return StandardMarker;}(_baseMarker2.default);exports.default=StandardMarker;module.exports=exports['default']; 294 | 295 | /***/ }), 296 | /* 24 */ 297 | /***/ (function(module, exports, __webpack_require__) { 298 | 299 | exports = module.exports = __webpack_require__(5)(); 300 | // imports 301 | 302 | 303 | // module 304 | exports.push([module.id, ".markers-plugin {\n pointer-events: none; }\n .markers-plugin .markers-plugin-markers {\n overflow: hidden;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0; }\n .markers-plugin .markers-plugin-markers > * {\n pointer-events: auto; }\n .markers-plugin .markers-plugin-markers .standard-marker {\n position: absolute;\n -webkit-transform: translateX(-50%);\n -moz-transform: translateX(-50%);\n -ms-transform: translateX(-50%);\n -o-transform: translateX(-50%);\n transform: translateX(-50%);\n top: 2px;\n left: 0;\n width: 20px;\n height: 20px; }\n .markers-plugin .markers-plugin-markers .standard-marker .standard-marker-inner {\n position: absolute;\n left: 7.5px;\n top: 7.5px;\n width: 5px;\n height: 5px;\n border-radius: 2.5px;\n box-shadow: 0 0 0 3px rgba(200, 200, 200, 0.2);\n background-color: #c8c8c8; }\n .markers-plugin .markers-plugin-markers .standard-marker:hover {\n cursor: pointer; }\n .markers-plugin .markers-plugin-markers .standard-marker:hover .standard-marker-inner {\n left: 6px;\n top: 6px;\n width: 8px;\n height: 8px;\n border-radius: 4px;\n box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.2);\n background-color: white; }\n .markers-plugin .markers-plugin-tooltips {\n position: relative;\n height: 0; }\n .markers-plugin .markers-plugin-tooltips .tooltip-container {\n position: absolute;\n white-space: nowrap;\n line-height: normal; }\n .markers-plugin .markers-plugin-tooltips .tooltip-container > * {\n pointer-events: auto; }\n .markers-plugin .markers-plugin-tooltips .tooltip-container .standard-tooltip, .markers-plugin .markers-plugin-tooltips .tooltip-container .image-tooltip {\n display: none;\n background-color: rgba(2, 2, 2, 0.5);\n color: white;\n font-size: 10px;\n padding: 4px 7px;\n line-height: normal; }\n .markers-plugin .markers-plugin-tooltips .tooltip-container .standard-tooltip[data-show=\"1\"], .markers-plugin .markers-plugin-tooltips .tooltip-container .image-tooltip[data-show=\"1\"] {\n display: inline-block; }\n", ""]); 305 | 306 | // exports 307 | 308 | 309 | /***/ }), 310 | /* 25 */ 311 | /***/ (function(module, exports, __webpack_require__) { 312 | 313 | /* 314 | MIT License http://www.opensource.org/licenses/mit-license.php 315 | Author Tobias Koppers @sokra 316 | */ 317 | var stylesInDom = {}, 318 | memoize = function(fn) { 319 | var memo; 320 | return function () { 321 | if (typeof memo === "undefined") memo = fn.apply(this, arguments); 322 | return memo; 323 | }; 324 | }, 325 | isOldIE = memoize(function() { 326 | return /msie [6-9]\b/.test(self.navigator.userAgent.toLowerCase()); 327 | }), 328 | getHeadElement = memoize(function () { 329 | return document.head || document.getElementsByTagName("head")[0]; 330 | }), 331 | singletonElement = null, 332 | singletonCounter = 0, 333 | styleElementsInsertedAtTop = []; 334 | 335 | module.exports = function(list, options) { 336 | if(false) { 337 | if(typeof document !== "object") throw new Error("The style-loader cannot be used in a non-browser environment"); 338 | } 339 | 340 | options = options || {}; 341 | // Force single-tag solution on IE6-9, which has a hard limit on the # of