├── .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 | [](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 | 
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