├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md ├── stale.yml └── workflow │ └── actions-ci.yml ├── .gitignore ├── .hound.yml ├── .yarnclean ├── .yarnrc ├── AUTHORS ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── bump ├── karma.conf.js ├── package.json ├── postcss.config.js ├── public ├── i │ ├── clappr_logo_black.png │ └── favico.png ├── index.html ├── j │ ├── add-external.js │ ├── clappr-config.js │ ├── editor │ │ ├── ace.js │ │ ├── mode-javascript.js │ │ ├── theme-katzenmilch.js │ │ └── worker-javascript.js │ └── main.js └── stylesheets │ ├── bootstrap-theme.min.css │ ├── bootstrap.min.css │ └── style.css ├── rollup.config.base.js ├── rollup.config.dev.js ├── rollup.config.js ├── src ├── icons │ ├── 01-play.svg │ ├── 02-pause.svg │ ├── 03-stop.svg │ ├── 04-volume.svg │ ├── 05-mute.svg │ ├── 06-expand.svg │ ├── 07-shrink.svg │ ├── 08-hd.svg │ ├── 09-cc.svg │ └── 10-reload.svg ├── main.js ├── plugins │ ├── click_to_pause │ │ ├── click_to_pause.js │ │ └── click_to_pause.test.js │ ├── closed_captions │ │ ├── closed_captions.js │ │ └── public │ │ │ ├── closed_captions.html │ │ │ └── closed_captions.scss │ ├── dvr_controls │ │ ├── dvr_controls.js │ │ └── public │ │ │ ├── dvr_controls.scss │ │ │ └── index.html │ ├── end_video.js │ ├── error_screen │ │ ├── error_screen.js │ │ ├── error_screen.test.js │ │ └── public │ │ │ ├── error_screen.html │ │ │ └── error_screen.scss │ ├── favicon.js │ ├── google_analytics │ │ ├── google_analytics.js │ │ └── google_analytics.test.js │ ├── media_control │ │ ├── media_control.js │ │ ├── media_control.test.js │ │ └── public │ │ │ ├── closed-hand.cur │ │ │ ├── media-control.html │ │ │ └── media-control.scss │ ├── poster │ │ ├── poster.js │ │ ├── poster.test.js │ │ └── public │ │ │ ├── default.png │ │ │ ├── poster.html │ │ │ └── poster.scss │ ├── seek_time │ │ ├── public │ │ │ ├── seek_time.html │ │ │ └── seek_time.scss │ │ └── seek_time.js │ ├── spinner_three_bounce │ │ ├── public │ │ │ ├── spinner.html │ │ │ └── spinner.scss │ │ └── spinner_three_bounce.js │ ├── stats │ │ ├── stats.js │ │ └── stats.test.js │ └── watermark │ │ ├── public │ │ ├── watermark.html │ │ ├── watermark.png │ │ └── watermark.scss │ │ └── watermark.js ├── public │ ├── Roboto.ttf │ └── fonts.css └── vendor │ ├── index.js │ └── kibo.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": false }] 4 | ], 5 | "env": { 6 | "test": { 7 | "plugins": ["istanbul"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | csslint: 3 | enabled: true 4 | duplication: 5 | enabled: true 6 | config: 7 | languages: 8 | - javascript 9 | eslint: 10 | enabled: true 11 | channel: "eslint-2" 12 | fixme: 13 | enabled: true 14 | ratings: 15 | paths: 16 | - "**.css" 17 | - "**.js" 18 | exclude_paths: 19 | - dist/ 20 | - test/ 21 | - node_modules/ 22 | - public/ 23 | - src/vendor/ 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | indent_style = space 12 | indent_size = 2 13 | 14 | trim_trailing_whitespace = true 15 | 16 | [*.md] 17 | # add Markdown specifics if needed 18 | 19 | [*json] 20 | # add JSON specifics if needed 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | public/ 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "globals": { 9 | "_gaq": false, 10 | "process": false, 11 | "ActiveXObject": false, 12 | "VERSION": false, 13 | "__dirname": false, 14 | "after": false, 15 | "afterEach": false, 16 | "assert": false, 17 | "before": false, 18 | "beforeEach": false, 19 | "describe": false, 20 | "expect": false, 21 | "it": false, 22 | "sinon": false, 23 | "xit": false, 24 | "jest": false, 25 | "test": false, 26 | "module": false, 27 | "require": false, 28 | "CLAPPR_CORE_VERSION": false 29 | }, 30 | "extends": "eslint:recommended", 31 | "parserOptions": { 32 | "sourceType": "module", 33 | "ecmaVersion": 2018 34 | }, 35 | "rules": { 36 | "indent": [ 37 | "error", 38 | 2 39 | ], 40 | "linebreak-style": [ 41 | "error", 42 | "unix" 43 | ], 44 | "quotes": [ 45 | "error", 46 | "single" 47 | ], 48 | "semi": [ 49 | "error", 50 | "never" 51 | ], 52 | "no-var": "error", 53 | "block-spacing": "error", 54 | "curly": ["error", "multi-or-nest", "consistent"], 55 | "object-curly-spacing": ["error", "always"], 56 | "brace-style": ["error", "1tbs", { "allowSingleLine": true }], 57 | "keyword-spacing": "error", 58 | "space-before-blocks": "error", 59 | "arrow-spacing": "error", 60 | "max-len": 0, 61 | "max-statements": 0 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/* -diff 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | This PR implements / updates / fixes... 4 | 5 | 9 | 10 | ## Changes 11 | 12 | - `archive_name1.js`: 13 | - ... 14 | - ... 15 | - `archive_name2.js`: ... 16 | 17 | ## How to test 18 | 19 | 1. ... 20 | 1. ... 21 | 1. ... 22 | 23 | ## Images 24 | 25 | ### Before this PR 26 | 27 | 31 | 32 | ### After this PR 33 | 34 | 35 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - backlog 8 | - bug 9 | - feature 10 | - high-priority 11 | - in-progress 12 | - enhancement 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflow/actions-ci.yml: -------------------------------------------------------------------------------- 1 | name: player-plugins CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: install node 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: 14.x 20 | cache: 'yarn' 21 | 22 | - name: install dependencies 23 | run: yarn 24 | 25 | - name: run lint 26 | run: yarn lint 27 | 28 | - name: run unit tests 29 | run: yarn test 30 | 31 | - name: coveralls 32 | uses: coverallsapp/github-action@1.1.3 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules 3 | .DS_Store 4 | .env 5 | coverage 6 | build/ 7 | docs/ 8 | src/base/jst.js 9 | *.cache 10 | aws.json 11 | npm-debug.log 12 | yarn-error.log 13 | package-lock.json 14 | 15 | # bump 16 | *.bkp 17 | 18 | # Vim 19 | *~ 20 | *.swp 21 | *.swo 22 | 23 | # PhpStorm 24 | .idea 25 | 26 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | eslint: 2 | enabled: true 3 | config_file: .eslintrc.json 4 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | 13 | # examples 14 | example 15 | examples 16 | 17 | # code coverage directories 18 | coverage 19 | .nyc_output 20 | 21 | # build scripts 22 | Makefile 23 | Gulpfile.js 24 | Gruntfile.js 25 | 26 | # configs 27 | appveyor.yml 28 | circle.yml 29 | codeship-services.yml 30 | codeship-steps.yml 31 | wercker.yml 32 | .tern-project 33 | .gitattributes 34 | .editorconfig 35 | .*ignore 36 | .flowconfig 37 | .documentup.json 38 | .yarn-metadata.json 39 | .travis.yml 40 | 41 | # misc 42 | *.md 43 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --install.ignore-engines true -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Globo.com Player authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | Globo.com 6 | 7 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the official list of people who have contributed code to 2 | # Clappr Player. 3 | # The AUTHORS file lists the copyright holders; this file lists people. 4 | # For example, Globo.com employees are listed here but not in AUTHORS, 5 | # because Globo.com holds the copyright. 6 | # 7 | # You can update this list using the following command: 8 | # 9 | # % git shortlog -se | awk '{$1=""; print $0}' | sed -e 's/^ //' 10 | # 11 | # Please keep this file sorted, and group users with multiple emails. 12 | 13 | Ahmad Mayahi 14 | Alvyn McQuitty 15 | Andrey Filimonow 16 | Andrey Nikitin 17 | Andrey Polischuk 18 | Andriy Lysnevych 19 | Ari Selseng 20 | Ben 21 | Bernardo Camilo 22 | Bruno Torres 23 | Celio Latorraca 24 | Chris Wiggins 25 | Daniel Afonso 26 | David Beitey 27 | Denis Sikuler 28 | Diego.Péres 29 | Eddie Lau 3dd13 30 | EmileP <36151637+emilepommier@users.noreply.github.com> 31 | Emre Karataşoğlu 32 | Flávio Ribeiro 33 | Guilherme 34 | Guilherme Heynemann Bruzzi 35 | Guilherme Medeiros 36 | Gustavo Barbosa 37 | Henrik Lundgren 38 | Henrique Breim 39 | Iulian Onofrei 40 | Jamie Stackhouse 41 | Jarom McDonald 42 | Jedidiah Hurt 43 | Jeff Fairley 44 | Jenna Smith 45 | Jhonatan Gomes 46 | João Paulo da Silva Vieira 47 | Jussi Keranen 48 | Jérôme DENIS 49 | Karim Slimani 50 | Ke Xu 51 | Leandro Moreira 52 | Leonardo 53 | Lewis Cowper 54 | Loris Mancel 55 | Lucas Costa 56 | Marcel 57 | Mark Allen Matney, Jr 58 | Martin Kolárik 59 | Maxwell Dayvson da Silva 60 | Michael Cunningham 61 | Mike Griffith 62 | Nicholas Asimov 63 | Niko78 64 | Ogün Karakuş 65 | Quentin V 66 | Raphael Amorim 67 | Robert Nagy 68 | Rodrigo Machado 69 | Roland Starke 70 | RussCoder 71 | Sander Van Schoote 72 | Sean Hussey 73 | Sergey Chooh 74 | Setsusan 75 | Stanislav Gurnik 76 | Stanley Gurnik 77 | Stephan Hesse 78 | Steven Lu 79 | Thiago Pontes 80 | Tom Jenkinson 81 | Tomasz Rybarczyk 82 | Vadim Anufriev 83 | Vagner Santana 84 | Vlad V. Teteria 85 | Wahaj Dar 86 | argoilves 87 | derrod 88 | hounvs 89 | hxl-dy 90 | kolpax 91 | nixx 92 | otmjka 93 | rheber 94 | shaharmor 95 | tchakabam 96 | thiagopnts 97 | leaofelipe 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Globo.com Player authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Globo.com nor the names of its contributors 12 | may be used to endorse or promote products derived from this software without 13 | specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Clappr-plugins 3 |

4 | 5 | Clappr-plugins is part of Clappr project that contains the main builtins plugins of the Clappr Player. 6 | 7 | Clappr is under development but production-ready. Feel free to open issues and send us pull requests. 8 | 9 |

10 | 11 | 12 | 13 | 14 | 15 | jsDelivr hits (npm) 16 |

17 | 18 | This project exports individually and as a single object (Plugins) the builtins plugins to create the Clappr Player. Those plugins are: 19 | 20 | - [MediaControl](https://github.com/clappr/clappr-plugins#media-control) 21 | - [ClickToPausePlugin](https://github.com/clappr/clappr-plugins#click-to-pause) 22 | - [ClosedCaptions](https://github.com/clappr/clappr-plugins#closed-captions) 23 | - [DVRControls](https://github.com/clappr/clappr-plugins#dvr-controls) 24 | - [ErrorScreen](https://github.com/clappr/clappr-plugins#error-screen) 25 | - [GoogleAnalytics](https://github.com/clappr/clappr-plugins#google-analytics) 26 | - [PosterPlugin](https://github.com/clappr/clappr-plugins#poster) 27 | - [SeekTime](https://github.com/clappr/clappr-plugins#seek-time) 28 | - [SpinnerThreeBouncePlugin](https://github.com/clappr/clappr-plugins#spinner-three-bounce) 29 | - [StatsPlugin](https://github.com/clappr/clappr-plugins#stats) 30 | - [WaterMarkPlugin](https://github.com/clappr/clappr-plugins#watermark) 31 | - [EndVideo](https://github.com/clappr/clappr-plugins#end-video) 32 | - [Favicon](https://github.com/clappr/clappr-plugins#favicon) 33 | 34 | ## About plugins 35 | 36 | A plugin can add new capabilities to Clappr or extend an existing one. A plugin can also be either visual (change or add some behavior in the user interface) or internal, collecting data from the video that is playing, for example. 37 | 38 | The plugins are organized into categories that define their responsibility and level of permission to access the internal components of the player. For more information, see the section about [plugin types](https://github.com/clappr/clappr-plugins#types-of-plugins). 39 | 40 | ## Types of plugins 41 | 42 | A plugin can be loaded within an internal permission level (scope) of the Player and must extend some base type supported by Clappr. 43 | 44 | The scopes are: [Core](https://github.com/clappr/clappr-plugins#core), [Container](https://github.com/clappr/clappr-plugins#container) and [Playback](https://github.com/clappr/clappr-plugins#playback). To learn more about the Clappr architecture, visit: https://github.com/clappr/clappr/wiki/Architecture 45 | 46 | Following the classes that a plugin can extend, this will define its type and structure. 47 | 48 | Plugins that do not handle UI, can extend from: [CorePlugin](https://github.com/clappr/clappr-plugins#coreplugin), [ContainerPlugin](https://github.com/clappr/clappr-plugins#containerplugin) or [Playback](https://github.com/clappr/clappr-plugins#playback). 49 | 50 | Plugins that need to manipulate the UI must extend from: [UICorePlugin]() or [UIContainerPlugin](). 51 | 52 | ## Scope of Plugins 53 | 54 | ### Core 55 | 56 | The plugin has full access to the player, being able to access all events and internal data or manage and/or create an internal layer (Container for example). 57 | 58 | Examples: [MediaControl](https://github.com/clappr/clappr-plugins/blob/master/src/plugins/media_control/media_control.js), [ClosedCaptions](https://github.com/clappr/clappr-plugins/blob/master/src/plugins/closed_captions/closed_captions.js), [EndVideo](https://github.com/clappr/clappr-plugins/blob/master/src/plugins/end_video.js), and others. 59 | 60 | #### CorePlugin Class 61 | 62 | | Methods | Description | 63 | |------------|-----------| 64 | | bindEvents | A method called in the `constructor` where listeners for the desired events must be created. | 65 | | enable | Method to activate the plugin. The default implementation calls the `bindEvents` method and changes the value of the `this.enabled` flag to `true` if the plugin is disabled. | 66 | | disable | Method to disable the plugin. The default implementation removes existing listeners and changes the value of the `this.enabled` flag to `false` if the plugin is enabled. | 67 | | destroy | A method that destroys the plugin's listeners. | 68 | 69 | #### UICorePlugin Class 70 | 71 | | Methods | Description | 72 | |------------|-----------| 73 | | bindEvents | A method called in the `constructor` where listeners for the desired events must be created. | 74 | | enable | Method to activate the plugin. The default implementation calls the `bindEvents` method, changes the value of the `this.enabled` flag to `true` and leaves the plugin visible in the Player if it's disabled. | 75 | | disable | Method to disable the plugin. The default implementation removes existing listeners, changes the value of the `this.enabled` flag to `false` and hides the plugin in the player if it's enabled. | 76 | | render | A method called in the `constructor` of the base class where the element that will be rendered in the player must be filled out. | 77 | | destroy | A method that removes the plugin of the `DOM`. | 78 | 79 | --- 80 | 81 | ### Container 82 | 83 | The plugin has control only within the scope of the media. 84 | 85 | Examples: [SpinnerThreeBounce](https://github.com/clappr/clappr-plugins/blob/master/src/plugins/spinner_three_bounce/spinner_three_bounce.js), [WaterMark](https://github.com/clappr/clappr-plugins/blob/master/src/plugins/watermark/watermark.js), [Stats](https://github.com/clappr/clappr-plugins/blob/master/src/plugins/stats/stats.js) and others. 86 | 87 | #### ContainerPlugin Class 88 | 89 | | Métodos | Description | 90 | |------------|-----------| 91 | | bindEvents |A method called in the `constructor` where listeners for the desired events must be created. | 92 | | enable | Method to activate the plugin. The default implementation calls the `bindEvents` method and changes the value of the` this.enabled` flag to `true` if the plugin is disabled. | 93 | | disable | Method to disable the plugin. The default implementation removes existing listeners, changes the value of the `this.enabled` flag to` false` and hides the plugin in the player if it's enabled. | 94 | | destroy | A method that destroys the plugin's listeners. | 95 | 96 | #### UIContainerPlugin Class 97 | 98 | | Métodos | Description | 99 | |------------|-----------| 100 | | bindEvents | A method called in the `constructor` where listeners for the desired events must be created. | 101 | | enable | Method to activate the plugin. The default implementation calls the `bindEvents` method, changes the value of the `this.enabled` flag to `true` and leaves the plugin visible in the Player if it's disabled. | 102 | | disable | Method to disable the plugin. The default implementation removes existing listeners, changes the value of the `this.enabled` flag to `false` and hides the plugin in the player if it's enabled. | 103 | | render | A method called in the `constructor` of the base class where the element that will be rendered in the player must be filled out. | 104 | | destroy | A method that removes the plugin of the `DOM`. | 105 | 106 | --- 107 | 108 | ### Playback 109 | 110 | Controls execution and adds support for different types of media. Since playback plugins are intended to support a specific type of media, the plugin is extended from some other existing playback plugin or the base playback class. 111 | 112 | More information about the Playback base class in the Clappr-core repository: https://github.com/clappr/clappr-core/blob/master/src/base/playback/playback.js 113 | 114 | Exemplos: [HlsPlayback](https://github.com/clappr/hlsjs-playback), [DashShakaPlayback](https://github.com/clappr/dash-shaka-playback), [HTML5Video](https://github.com/clappr/clappr-core/blob/master/src/playbacks/html5_video/html5_video.js) and others. 115 | 116 | 117 | ## Plugins Description 118 | 119 | ### Media Control 120 | A plugin that renders the interface over the video container and add the possibility to control playback actions (play, pause, stop). 121 | 122 | ### Click to pause 123 | Adds the possibility to toggle between the `play`/`pause` playback states by clicking on the container element. 124 | ```javascript 125 | var player = new Clappr.Player({ 126 | source: "http://your.video/here.mp4", 127 | // Optionally, send a payload upon the container's pausing with the `onClickPayload` parameter 128 | clickToPauseConfig: { 129 | onClickPayload: { any: 'any' } // sends the payload to container when clicked 130 | }); 131 | ``` 132 | 133 | ### Closed captions 134 | Adds the possibility to customize the label and title of the subtitles. 135 | ```javascript 136 | var player = new Clappr.Player({ 137 | source: "http://your.video/here.mp4", 138 | closedCaptionsConfig: { 139 | title: 'Subtitles', // default is none 140 | ariaLabel: 'Closed Captions', // Default is 'cc-button' 141 | labelCallback: function (track) { return track.name }, // track is an object with id, name and track properties (track is TextTrack object) 142 | }, 143 | }); 144 | ``` 145 | 146 | ### DVR controls 147 | Add controls to interact with the media in DVR mode. 148 | 149 | ### Error screen 150 | Add a screen to inform the error name and the possibility to retry to play the media. 151 | 152 | ### Google analytics 153 | Enable Google Analytics events dispatch (play/pause/stop/buffering/etc) adding your `gaAccount`. Optionally, pass your favorite trackerName as `gaTrackerName`. 154 | ```javascript 155 | var player = new Clappr.Player({ 156 | source: "http://your.video/here.mp4", 157 | gaAccount: 'UA-44332211-1', 158 | gaTrackerName: 'MyPlayerInstance' 159 | }); 160 | ``` 161 | ### Poster 162 | Defines a poster image by adding the poster option to Clappr player. It will appear after video embed, disappear on play and go back when user stops the video. For audio broadcasts, the poster stays visible while playing. 163 | ```javascript 164 | var player = new Clappr.Player({ 165 | source: "http://your.video/here.mp4", 166 | poster: "http://url/img.png" 167 | }); 168 | ``` 169 | 170 | ### Seek time 171 | Inform the current time when a hover on media control seekbar occurs. 172 | ```javascript 173 | var player = new Clappr.Player({ 174 | source: "http://your.video/here.mp4", 175 | // Only for live stream with DVR 176 | actualLiveTime: true, // default is false 177 | // Meant to be used with actualLiveTime 178 | actualLiveServerTime: "2015/11/26 06:01:03" // default is current date 179 | }); 180 | ``` 181 | 182 | ### Spinner three bounce 183 | Signals when player enter on buffering state. 184 | 185 | ### Stats 186 | A native statistics plugin that accounts QoE metrics such playing time, rebuffering time, total rebuffer count, etc. 187 | 188 | ### Watermark 189 | Add a watermark over the video. Put `watermark` option on your embed parameters to automatically add watermark on your video. 190 | 191 | Choose corner position by defining `position` option. 192 | 193 | Positions can be `bottom-left`, `bottom-right`, `top-left` and `top-right`. To define an URL to open when the watermark is clicked, use `watermarkLink` option. 194 | 195 | If the `watermarkLink` parameter not defined, the watermark will not be clickable. 196 | ```javascript 197 | var player = new Clappr.Player({ 198 | source: "http://your.video/here.mp4", 199 | watermark: "http://url/img.png", 200 | position: 'top-right', 201 | watermarkLink: "http://example.net/" 202 | }); 203 | ``` 204 | ### End video 205 | Add the possibility to exit from fullscreen mode when the video ends. The option `exitFullscreenOnEnd` disable this behavior. 206 | ```javascript 207 | var player = new Clappr.Player({ 208 | source: "http://your.video/here.mp4", 209 | exitFullscreenOnEnd: false 210 | }); 211 | ``` 212 | ### Favicon 213 | Add a favicon with the current state of the playback. 214 | -------------------------------------------------------------------------------- /bump: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJECT_NAME='clappr-plugins' 4 | CDN_PATH="npm/@clappr/plugins@latest/dist/$PROJECT_NAME.min.js" 5 | 6 | update_dependencies() { 7 | echo 'updating dependencies' && 8 | yarn install 9 | } 10 | 11 | update_version() { 12 | current_tag=$(git describe --abbrev=0 --tags master) && 13 | echo 'bump from '$current_tag' to '$1 && 14 | sed -i ".bkp" "s/\(version\":[ ]*\"\)$current_tag/\1$1/" package.json 15 | } 16 | 17 | build() { 18 | echo "building $PROJECT_NAME.min.js" && 19 | yarn release 20 | } 21 | 22 | run_tests() { 23 | yarn lint 24 | yarn test 25 | } 26 | 27 | make_release_commit() { 28 | git add package.json yarn.lock && 29 | git commit -m 'chore(package): bump to '$1 && 30 | git tag -m "$1" $1 31 | } 32 | 33 | git_push() { 34 | echo 'pushing to github' 35 | git push origin master --tags 36 | } 37 | 38 | npm_publish() { 39 | npm publish 40 | } 41 | 42 | purge_cdn_cache() { 43 | echo 'purging cdn cache' 44 | curl -q "http://purge.jsdelivr.net/$CDN_PATH" 45 | } 46 | 47 | main() { 48 | npm whoami 49 | if (("$?" != "0")); then 50 | echo "you are not logged into npm" 51 | exit 1 52 | fi 53 | update_dependencies && 54 | update_version $1 && 55 | build 56 | if (("$?" != "0")); then 57 | echo "something failed during dependency update, version update, or build" 58 | exit 1 59 | fi 60 | run_tests 61 | if (("$?" == "0")); then 62 | make_release_commit $1 && 63 | git_push && 64 | npm_publish && 65 | purge_cdn_cache && 66 | exit 0 67 | 68 | echo "something failed" 69 | exit 1 70 | else 71 | echo "you broke the tests. fix it before bump another version." 72 | exit 1 73 | fi 74 | } 75 | 76 | if [ "$1" != "" ]; then 77 | main $1 78 | else 79 | echo "Usage: bump [new_version]" 80 | fi 81 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 2 | const { baseConfig } = require('./rollup.config.base') 3 | 4 | const rollupPreprocessor = { 5 | ...baseConfig, 6 | external: [], 7 | output: { 8 | format: 'iife', // Helps prevent naming collisions. 9 | name: 'ClapprPlugins', // Required for 'iife' format. 10 | sourcemap: 'inline', // Sensible for testing. 11 | }, 12 | } 13 | 14 | module.exports = function(config) { 15 | config.set({ 16 | 17 | // base path that will be used to resolve all patterns (eg. files, exclude) 18 | basePath: '', 19 | 20 | // frameworks to use 21 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 22 | frameworks: ['mocha', 'chai-sinon'], 23 | 24 | // list of files / patterns to load in the browser 25 | files: [ 26 | { pattern: 'node_modules/@clappr/core/dist/clappr-core.js', served: true, included: true }, 27 | { pattern: 'src/**/*.test.js', watched: true }, 28 | ], 29 | 30 | // list of files to exclude 31 | exclude: [ 32 | ], 33 | 34 | // preprocess matching files before serving them to the browser 35 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 36 | preprocessors: { 37 | 'src/**/*.test.js': ['rollup'], 38 | }, 39 | 40 | rollupPreprocessor, 41 | 42 | coverageReporter: { 43 | reporters: [ 44 | { type: 'lcovonly' }, 45 | { type: 'text-summary' } 46 | ], 47 | dir: 'coverage', 48 | instrumenterOptions: { 49 | istanbul: { noCompact: true } 50 | } 51 | }, 52 | 53 | plugins: [ 54 | require('karma-rollup-preprocessor'), 55 | require('karma-mocha'), 56 | 'karma-chrome-launcher', 57 | 'karma-firefox-launcher', 58 | 'karma-chai-sinon', 59 | 'karma-coverage' 60 | ], 61 | 62 | // test results reporter to use 63 | // possible values: 'dots', 'progress' 64 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 65 | reporters: ['progress', 'coverage'], 66 | 67 | // web server port 68 | port: 9876, 69 | 70 | 71 | // enable / disable colors in the output (reporters and logs) 72 | colors: true, 73 | 74 | 75 | // level of logging 76 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 77 | logLevel: config.LOG_INFO, 78 | 79 | 80 | // enable / disable watching file and executing tests whenever any file changes 81 | autoWatch: true, 82 | 83 | 84 | // start these browsers 85 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 86 | //browsers: ['Chrome', 'Firefox', 'Safari'], 87 | browsers: [!process.env.TRAVIS ? 'Chrome' : 'Chrome_travis_ci', 'Firefox'], 88 | 89 | customLaunchers: { 90 | Chrome_travis_ci: { 91 | base: 'Chrome', 92 | flags: ['--no-sandbox'] 93 | } 94 | }, 95 | 96 | // to avoid DISCONNECTED messages 97 | browserDisconnectTimeout : 10000, // default 2000 98 | browserDisconnectTolerance : 1, // default 0 99 | browserNoActivityTimeout : 600000, //default 10000 100 | 101 | 102 | // Continuous Integration mode 103 | // if true, Karma captures browsers, runs the tests and exits 104 | singleRun: false 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@clappr/plugins", 3 | "version": "0.5.0", 4 | "description": "Main plugins for the Clappr project", 5 | "main": "./dist/clappr-plugins.js", 6 | "module": "./dist/clappr-plugins.esm.js", 7 | "scripts": { 8 | "bundle-check": "ANALYZE_BUNDLE=true rollup -c", 9 | "release": "MINIMIZE=true rollup -c", 10 | "build": "rollup -c", 11 | "watch": "rollup -c --watch", 12 | "test": "NODE_ENV=test karma start --single-run", 13 | "test:watch": "NODE_ENV=test karma start --no-single-run --watch", 14 | "lint": "eslint *.js src/", 15 | "lint:fix": "npm run lint -- --fix", 16 | "start": "rollup -c rollup.config.dev.js --watch", 17 | "commitzen": "git-cz" 18 | }, 19 | "files": [ 20 | "/dist", 21 | "/src" 22 | ], 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git@github.com:clappr/clappr-plugins.git" 29 | }, 30 | "author": "Globo.com", 31 | "license": "BSD-3-Clause", 32 | "bugs": { 33 | "url": "https://github.com/clappr/clappr-plugins/issues" 34 | }, 35 | "homepage": "https://github.com/clappr/clappr-plugins", 36 | "peerDependencies": { 37 | "@clappr/core": "^0.4.27" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.22.17", 41 | "@babel/preset-env": "^7.22.15", 42 | "@clappr/core": "^0.6.1", 43 | "@rollup/plugin-commonjs": "^25.0.4", 44 | "@rollup/plugin-node-resolve": "^15.2.1", 45 | "@rollup/plugin-replace": "^5.0.2", 46 | "autoprefixer": "^10.4.15", 47 | "babel-plugin-istanbul": "^6.1.1", 48 | "chai": "^4.3.8", 49 | "coveralls": "^3.1.1", 50 | "cz-conventional-changelog": "^3.3.0", 51 | "eslint": "^8.49.0", 52 | "istanbul": "^0.4.5", 53 | "karma": "^6.4.2", 54 | "karma-chai-sinon": "^0.1.5", 55 | "karma-chrome-launcher": "^3.2.0", 56 | "karma-cli": "^2.0.0", 57 | "karma-coverage": "^2.2.1", 58 | "karma-firefox-launcher": "^2.1.2", 59 | "karma-mocha": "^2.0.1", 60 | "karma-rollup-preprocessor": "^7.0.8", 61 | "mocha": "^10.2.0", 62 | "node-sass": "^9.0.0", 63 | "postcss": "^8.4.29", 64 | "postcss-url": "^10.1.3", 65 | "rollup": "^3.29.1", 66 | "rollup-plugin-analyzer": "^4.0.0", 67 | "rollup-plugin-babel": "^4.4.0", 68 | "rollup-plugin-html": "^0.2.1", 69 | "rollup-plugin-livereload": "^2.0.5", 70 | "rollup-plugin-named-directory": "^1.0.0", 71 | "rollup-plugin-postcss": "^4.0.2", 72 | "rollup-plugin-serve": "^2.0.2", 73 | "rollup-plugin-svg": "^2.0.0", 74 | "rollup-plugin-terser": "^7.0.2", 75 | "sinon": "^15.2.0", 76 | "sinon-chai": "^3.7.0" 77 | }, 78 | "config": { 79 | "commitizen": { 80 | "path": "./node_modules/cz-conventional-changelog" 81 | } 82 | }, 83 | "nyc": { 84 | "exclude": [ 85 | "**/*.test.js", 86 | "**/*.spec.js", 87 | "src/vendor", 88 | "node_modules" 89 | ] 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer') 2 | const postcssUrl = require('postcss-url') 3 | 4 | module.exports = { 5 | plugins: [ 6 | autoprefixer, 7 | postcssUrl({ url: 'rebase' }) 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /public/i/clappr_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clappr/clappr-plugins/ffaa9d27005fa5a8a7c243ffc47eb5655b84b371/public/i/clappr_logo_black.png -------------------------------------------------------------------------------- /public/i/favico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clappr/clappr-plugins/ffaa9d27005fa5a8a7c243ffc47eb5655b84b371/public/i/favico.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | Clappr 19 |
    20 |
  • 21 | docs 22 |
  • 23 |
24 |
25 |
26 |
27 |

28 | Add external plugins: 29 | 30 | 31 |

32 |
33 |
34 |
35 |
36 | 53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /public/j/add-external.js: -------------------------------------------------------------------------------- 1 | window.clappr = window.clappr || {} 2 | window.clappr.externals = [] 3 | 4 | function addExternal() { 5 | var url = document.getElementById('js-link') 6 | window.clappr.externals.push(url.value) 7 | addTag(url.value) 8 | url.value = '' 9 | } 10 | 11 | function addTag(url) { 12 | var colors = ["aliceblue", "antiquewhite", "azure", "black", "blue", "brown", "yellow", "teal"] 13 | var color = colors[Math.floor(Math.random() * colors.length)] 14 | var span = document.createElement('span') 15 | 16 | span.style.backgroundColor = color 17 | span.className = "external-js" 18 | span.innerText = url.split(/\//).pop().split(/\./)[0] 19 | 20 | document.getElementById('external-js-panel').appendChild(span) 21 | } 22 | -------------------------------------------------------------------------------- /public/j/clappr-config.js: -------------------------------------------------------------------------------- 1 | const playerElement = document.getElementById("player-wrapper") 2 | 3 | const plugins = Object.values(ClapprPlugins.Plugins) 4 | 5 | player = new Clappr.Player({ 6 | source: urlParams.src || 'http://clappr.io/highline.mp4', 7 | poster: urlParams.poster || 'http://clappr.io/poster.png', 8 | mute: true, 9 | autoPlay: true, 10 | height: 360, 11 | width: 640, 12 | plugins: plugins, 13 | }) 14 | 15 | player.attachTo(playerElement) 16 | -------------------------------------------------------------------------------- /public/j/editor/mode-javascript.js: -------------------------------------------------------------------------------- 1 | define("ace/mode/javascript",["require","exports","module","ace/lib/oop","ace/mode/text","ace/tokenizer","ace/mode/javascript_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/worker/worker_client","ace/mode/behaviour/cstyle","ace/mode/folding/cstyle"],function(e,t,n){var r=e("../lib/oop"),i=e("./text").Mode,s=e("../tokenizer").Tokenizer,o=e("./javascript_highlight_rules").JavaScriptHighlightRules,u=e("./matching_brace_outdent").MatchingBraceOutdent,a=e("../range").Range,f=e("../worker/worker_client").WorkerClient,l=e("./behaviour/cstyle").CstyleBehaviour,c=e("./folding/cstyle").FoldMode,h=function(){this.HighlightRules=o,this.$outdent=new u,this.$behaviour=new l,this.foldingRules=new c};r.inherits(h,i),function(){this.lineCommentStart="//",this.blockComment={start:"/*",end:"*/"},this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t),i=this.getTokenizer().getLineTokens(t,e),s=i.tokens,o=i.state;if(s.length&&s[s.length-1].type=="comment")return r;if(e=="start"||e=="no_regex"){var u=t.match(/^.*(?:\bcase\b.*\:|[\{\(\[])\s*$/);u&&(r+=n)}else if(e=="doc-start"){if(o=="start"||o=="no_regex")return"";var u=t.match(/^\s*(\/?)\*/);u&&(u[1]&&(r+=" "),r+="* ")}return r},this.checkOutdent=function(e,t,n){return this.$outdent.checkOutdent(t,n)},this.autoOutdent=function(e,t,n){this.$outdent.autoOutdent(t,n)},this.createWorker=function(e){var t=new f(["ace"],"ace/mode/javascript_worker","JavaScriptWorker");return t.attachToDocument(e.getDocument()),t.on("jslint",function(t){e.setAnnotations(t.data)}),t.on("terminate",function(){e.clearAnnotations()}),t},this.$id="ace/mode/javascript"}.call(h.prototype),t.Mode=h}),define("ace/mode/javascript_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(e,t,n){var r=e("../lib/oop"),i=e("./doc_comment_highlight_rules").DocCommentHighlightRules,s=e("./text_highlight_rules").TextHighlightRules,o=function(){var e=this.createKeywordMapper({"variable.language":"Array|Boolean|Date|Function|Iterator|Number|Object|RegExp|String|Proxy|Namespace|QName|XML|XMLList|ArrayBuffer|Float32Array|Float64Array|Int16Array|Int32Array|Int8Array|Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|Error|EvalError|InternalError|RangeError|ReferenceError|StopIteration|SyntaxError|TypeError|URIError|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|isNaN|parseFloat|parseInt|JSON|Math|this|arguments|prototype|window|document",keyword:"const|yield|import|get|set|break|case|catch|continue|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|debugger|__parent__|__count__|escape|unescape|with|__proto__|class|enum|extends|super|export|implements|private|public|interface|package|protected|static","storage.type":"const|let|var|function","constant.language":"null|Infinity|NaN|undefined","support.function":"alert","constant.language.boolean":"true|false"},"identifier"),t="case|do|else|finally|in|instanceof|return|throw|try|typeof|yield|void",n="[a-zA-Z\\$_\xa1-\uffff][a-zA-Z\\d\\$_\xa1-\uffff]*\\b",r="\\\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.)";this.$rules={no_regex:[{token:"comment",regex:"\\/\\/",next:"line_comment"},i.getStartRule("doc-start"),{token:"comment",regex:/\/\*/,next:"comment"},{token:"string",regex:"'(?=.)",next:"qstring"},{token:"string",regex:'"(?=.)',next:"qqstring"},{token:"constant.numeric",regex:/0[xX][0-9a-fA-F]+\b/},{token:"constant.numeric",regex:/[+-]?\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/},{token:["storage.type","punctuation.operator","support.function","punctuation.operator","entity.name.function","text","keyword.operator"],regex:"("+n+")(\\.)(prototype)(\\.)("+n+")(\\s*)(=)",next:"function_arguments"},{token:["storage.type","punctuation.operator","entity.name.function","text","keyword.operator","text","storage.type","text","paren.lparen"],regex:"("+n+")(\\.)("+n+")(\\s*)(=)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["entity.name.function","text","keyword.operator","text","storage.type","text","paren.lparen"],regex:"("+n+")(\\s*)(=)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["storage.type","punctuation.operator","entity.name.function","text","keyword.operator","text","storage.type","text","entity.name.function","text","paren.lparen"],regex:"("+n+")(\\.)("+n+")(\\s*)(=)(\\s*)(function)(\\s+)(\\w+)(\\s*)(\\()",next:"function_arguments"},{token:["storage.type","text","entity.name.function","text","paren.lparen"],regex:"(function)(\\s+)("+n+")(\\s*)(\\()",next:"function_arguments"},{token:["entity.name.function","text","punctuation.operator","text","storage.type","text","paren.lparen"],regex:"("+n+")(\\s*)(:)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["text","text","storage.type","text","paren.lparen"],regex:"(:)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:"keyword",regex:"(?:"+t+")\\b",next:"start"},{token:["punctuation.operator","support.function"],regex:/(\.)(s(?:h(?:ift|ow(?:Mod(?:elessDialog|alDialog)|Help))|croll(?:X|By(?:Pages|Lines)?|Y|To)?|t(?:op|rike)|i(?:n|zeToContent|debar|gnText)|ort|u(?:p|b(?:str(?:ing)?)?)|pli(?:ce|t)|e(?:nd|t(?:Re(?:sizable|questHeader)|M(?:i(?:nutes|lliseconds)|onth)|Seconds|Ho(?:tKeys|urs)|Year|Cursor|Time(?:out)?|Interval|ZOptions|Date|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Date|FullYear)|FullYear|Active)|arch)|qrt|lice|avePreferences|mall)|h(?:ome|andleEvent)|navigate|c(?:har(?:CodeAt|At)|o(?:s|n(?:cat|textual|firm)|mpile)|eil|lear(?:Timeout|Interval)?|a(?:ptureEvents|ll)|reate(?:StyleSheet|Popup|EventObject))|t(?:o(?:GMTString|S(?:tring|ource)|U(?:TCString|pperCase)|Lo(?:caleString|werCase))|est|a(?:n|int(?:Enabled)?))|i(?:s(?:NaN|Finite)|ndexOf|talics)|d(?:isableExternalCapture|ump|etachEvent)|u(?:n(?:shift|taint|escape|watch)|pdateCommands)|j(?:oin|avaEnabled)|p(?:o(?:p|w)|ush|lugins.refresh|a(?:ddings|rse(?:Int|Float)?)|r(?:int|ompt|eference))|e(?:scape|nableExternalCapture|val|lementFromPoint|x(?:p|ec(?:Script|Command)?))|valueOf|UTC|queryCommand(?:State|Indeterm|Enabled|Value)|f(?:i(?:nd|le(?:ModifiedDate|Size|CreatedDate|UpdatedDate)|xed)|o(?:nt(?:size|color)|rward)|loor|romCharCode)|watch|l(?:ink|o(?:ad|g)|astIndexOf)|a(?:sin|nchor|cos|t(?:tachEvent|ob|an(?:2)?)|pply|lert|b(?:s|ort))|r(?:ou(?:nd|teEvents)|e(?:size(?:By|To)|calc|turnValue|place|verse|l(?:oad|ease(?:Capture|Events)))|andom)|g(?:o|et(?:ResponseHeader|M(?:i(?:nutes|lliseconds)|onth)|Se(?:conds|lection)|Hours|Year|Time(?:zoneOffset)?|Da(?:y|te)|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Da(?:y|te)|FullYear)|FullYear|A(?:ttention|llResponseHeaders)))|m(?:in|ove(?:B(?:y|elow)|To(?:Absolute)?|Above)|ergeAttributes|a(?:tch|rgins|x))|b(?:toa|ig|o(?:ld|rderWidths)|link|ack))\b(?=\()/},{token:["punctuation.operator","support.function.dom"],regex:/(\.)(s(?:ub(?:stringData|mit)|plitText|e(?:t(?:NamedItem|Attribute(?:Node)?)|lect))|has(?:ChildNodes|Feature)|namedItem|c(?:l(?:ick|o(?:se|neNode))|reate(?:C(?:omment|DATASection|aption)|T(?:Head|extNode|Foot)|DocumentFragment|ProcessingInstruction|E(?:ntityReference|lement)|Attribute))|tabIndex|i(?:nsert(?:Row|Before|Cell|Data)|tem)|open|delete(?:Row|C(?:ell|aption)|T(?:Head|Foot)|Data)|focus|write(?:ln)?|a(?:dd|ppend(?:Child|Data))|re(?:set|place(?:Child|Data)|move(?:NamedItem|Child|Attribute(?:Node)?)?)|get(?:NamedItem|Element(?:sBy(?:Name|TagName)|ById)|Attribute(?:Node)?)|blur)\b(?=\()/},{token:["punctuation.operator","support.constant"],regex:/(\.)(s(?:ystemLanguage|cr(?:ipts|ollbars|een(?:X|Y|Top|Left))|t(?:yle(?:Sheets)?|atus(?:Text|bar)?)|ibling(?:Below|Above)|ource|uffixes|e(?:curity(?:Policy)?|l(?:ection|f)))|h(?:istory|ost(?:name)?|as(?:h|Focus))|y|X(?:MLDocument|SLDocument)|n(?:ext|ame(?:space(?:s|URI)|Prop))|M(?:IN_VALUE|AX_VALUE)|c(?:haracterSet|o(?:n(?:structor|trollers)|okieEnabled|lorDepth|mp(?:onents|lete))|urrent|puClass|l(?:i(?:p(?:boardData)?|entInformation)|osed|asses)|alle(?:e|r)|rypto)|t(?:o(?:olbar|p)|ext(?:Transform|Indent|Decoration|Align)|ags)|SQRT(?:1_2|2)|i(?:n(?:ner(?:Height|Width)|put)|ds|gnoreCase)|zIndex|o(?:scpu|n(?:readystatechange|Line)|uter(?:Height|Width)|p(?:sProfile|ener)|ffscreenBuffering)|NEGATIVE_INFINITY|d(?:i(?:splay|alog(?:Height|Top|Width|Left|Arguments)|rectories)|e(?:scription|fault(?:Status|Ch(?:ecked|arset)|View)))|u(?:ser(?:Profile|Language|Agent)|n(?:iqueID|defined)|pdateInterval)|_content|p(?:ixelDepth|ort|ersonalbar|kcs11|l(?:ugins|atform)|a(?:thname|dding(?:Right|Bottom|Top|Left)|rent(?:Window|Layer)?|ge(?:X(?:Offset)?|Y(?:Offset)?))|r(?:o(?:to(?:col|type)|duct(?:Sub)?|mpter)|e(?:vious|fix)))|e(?:n(?:coding|abledPlugin)|x(?:ternal|pando)|mbeds)|v(?:isibility|endor(?:Sub)?|Linkcolor)|URLUnencoded|P(?:I|OSITIVE_INFINITY)|f(?:ilename|o(?:nt(?:Size|Family|Weight)|rmName)|rame(?:s|Element)|gColor)|E|whiteSpace|l(?:i(?:stStyleType|n(?:eHeight|kColor))|o(?:ca(?:tion(?:bar)?|lName)|wsrc)|e(?:ngth|ft(?:Context)?)|a(?:st(?:M(?:odified|atch)|Index|Paren)|yer(?:s|X)|nguage))|a(?:pp(?:MinorVersion|Name|Co(?:deName|re)|Version)|vail(?:Height|Top|Width|Left)|ll|r(?:ity|guments)|Linkcolor|bove)|r(?:ight(?:Context)?|e(?:sponse(?:XML|Text)|adyState))|global|x|m(?:imeTypes|ultiline|enubar|argin(?:Right|Bottom|Top|Left))|L(?:N(?:10|2)|OG(?:10E|2E))|b(?:o(?:ttom|rder(?:Width|RightWidth|BottomWidth|Style|Color|TopWidth|LeftWidth))|ufferDepth|elow|ackground(?:Color|Image)))\b/},{token:["storage.type","punctuation.operator","support.function.firebug"],regex:/(console)(\.)(warn|info|log|error|time|timeEnd|assert)\b/},{token:e,regex:n},{token:"keyword.operator",regex:/--|\+\+|[!$%&*+\-~]|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\|\||\?\:|\*=|%=|\+=|\-=|&=|\^=/,next:"start"},{token:"punctuation.operator",regex:/\?|\:|\,|\;|\./,next:"start"},{token:"paren.lparen",regex:/[\[({]/,next:"start"},{token:"paren.rparen",regex:/[\])}]/},{token:"keyword.operator",regex:/\/=?/,next:"start"},{token:"comment",regex:/^#!.*$/}],start:[i.getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment_regex_allowed"},{token:"comment",regex:"\\/\\/",next:"line_comment_regex_allowed"},{token:"string.regexp",regex:"\\/",next:"regex"},{token:"text",regex:"\\s+|^$",next:"start"},{token:"empty",regex:"",next:"no_regex"}],regex:[{token:"regexp.keyword.operator",regex:"\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)"},{token:"string.regexp",regex:"/[sxngimy]*",next:"no_regex"},{token:"invalid",regex:/\{\d+\b,?\d*\}[+*]|[+*$^?][+*]|[$^][?]|\?{3,}/},{token:"constant.language.escape",regex:/\(\?[:=!]|\)|\{\d+\b,?\d*\}|[+*]\?|[()$^+*?.]/},{token:"constant.language.delimiter",regex:/\|/},{token:"constant.language.escape",regex:/\[\^?/,next:"regex_character_class"},{token:"empty",regex:"$",next:"no_regex"},{defaultToken:"string.regexp"}],regex_character_class:[{token:"regexp.keyword.operator",regex:"\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)"},{token:"constant.language.escape",regex:"]",next:"regex"},{token:"constant.language.escape",regex:"-"},{token:"empty",regex:"$",next:"no_regex"},{defaultToken:"string.regexp.charachterclass"}],function_arguments:[{token:"variable.parameter",regex:n},{token:"punctuation.operator",regex:"[, ]+"},{token:"punctuation.operator",regex:"$"},{token:"empty",regex:"",next:"no_regex"}],comment_regex_allowed:[{token:"comment",regex:"\\*\\/",next:"start"},{defaultToken:"comment"}],comment:[{token:"comment",regex:"\\*\\/",next:"no_regex"},{defaultToken:"comment"}],line_comment_regex_allowed:[{token:"comment",regex:"$|^",next:"start"},{defaultToken:"comment"}],line_comment:[{token:"comment",regex:"$|^",next:"no_regex"},{defaultToken:"comment"}],qqstring:[{token:"constant.language.escape",regex:r},{token:"string",regex:"\\\\$",next:"qqstring"},{token:"string",regex:'"|$',next:"no_regex"},{defaultToken:"string"}],qstring:[{token:"constant.language.escape",regex:r},{token:"string",regex:"\\\\$",next:"qstring"},{token:"string",regex:"'|$",next:"no_regex"},{defaultToken:"string"}]},this.embedRules(i,"doc-",[i.getEndRule("no_regex")])};r.inherits(o,s),t.JavaScriptHighlightRules=o}),define("ace/mode/doc_comment_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},{token:"comment.doc.tag",regex:"\\bTODO\\b"},{defaultToken:"comment.doc"}]}};r.inherits(s,i),s.getStartRule=function(e){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:e}},s.getEndRule=function(e){return{token:"comment.doc",regex:"\\*\\/",next:e}},t.DocCommentHighlightRules=s}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(e,t,n){var r=e("../range").Range,i=function(){};(function(){this.checkOutdent=function(e,t){return/^\s+$/.test(e)?/^\s*\}/.test(t):!1},this.autoOutdent=function(e,t){var n=e.getLine(t),i=n.match(/^(\s*\})/);if(!i)return 0;var s=i[1].length,o=e.findMatchingBracket({row:t,column:s});if(!o||o.row==t)return 0;var u=this.$getIndent(e.getLine(o.row));e.replace(new r(t,0,t,s-1),u)},this.$getIndent=function(e){return e.match(/^\s*/)[0]}}).call(i.prototype),t.MatchingBraceOutdent=i}),define("ace/mode/behaviour/cstyle",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"],function(e,t,n){var r=e("../../lib/oop"),i=e("../behaviour").Behaviour,s=e("../../token_iterator").TokenIterator,o=e("../../lib/lang"),u=["text","paren.rparen","punctuation.operator"],a=["text","paren.rparen","punctuation.operator","comment"],f,l={},c=function(e){var t=-1;e.multiSelect&&(t=e.selection.id,l.rangeCount!=e.multiSelect.rangeCount&&(l={rangeCount:e.multiSelect.rangeCount}));if(l[t])return f=l[t];f=l[t]={autoInsertedBrackets:0,autoInsertedRow:-1,autoInsertedLineEnd:"",maybeInsertedBrackets:0,maybeInsertedRow:-1,maybeInsertedLineStart:"",maybeInsertedLineEnd:""}},h=function(){this.add("braces","insertion",function(e,t,n,r,i){var s=n.getCursorPosition(),u=r.doc.getLine(s.row);if(i=="{"){c(n);var a=n.getSelectionRange(),l=r.doc.getTextRange(a);if(l!==""&&l!=="{"&&n.getWrapBehavioursEnabled())return{text:"{"+l+"}",selection:!1};if(h.isSaneInsertion(n,r))return/[\]\}\)]/.test(u[s.column])||n.inMultiSelectMode?(h.recordAutoInsert(n,r,"}"),{text:"{}",selection:[1,1]}):(h.recordMaybeInsert(n,r,"{"),{text:"{",selection:[1,1]})}else if(i=="}"){c(n);var p=u.substring(s.column,s.column+1);if(p=="}"){var d=r.$findOpeningBracket("}",{column:s.column+1,row:s.row});if(d!==null&&h.isAutoInsertedClosing(s,u,i))return h.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}else{if(i=="\n"||i=="\r\n"){c(n);var v="";h.isMaybeInsertedClosing(s,u)&&(v=o.stringRepeat("}",f.maybeInsertedBrackets),h.clearMaybeInsertedClosing());var p=u.substring(s.column,s.column+1);if(p==="}"){var m=r.findMatchingBracket({row:s.row,column:s.column+1},"}");if(!m)return null;var g=this.$getIndent(r.getLine(m.row))}else{if(!v){h.clearMaybeInsertedClosing();return}var g=this.$getIndent(u)}var y=g+r.getTabString();return{text:"\n"+y+"\n"+g+v,selection:[1,y.length,1,y.length]}}h.clearMaybeInsertedClosing()}}),this.add("braces","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="{"){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.end.column,i.end.column+1);if(u=="}")return i.end.column++,i;f.maybeInsertedBrackets--}}),this.add("parens","insertion",function(e,t,n,r,i){if(i=="("){c(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return{text:"("+o+")",selection:!1};if(h.isSaneInsertion(n,r))return h.recordAutoInsert(n,r,")"),{text:"()",selection:[1,1]}}else if(i==")"){c(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f==")"){var l=r.$findOpeningBracket(")",{column:u.column+1,row:u.row});if(l!==null&&h.isAutoInsertedClosing(u,a,i))return h.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("parens","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="("){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==")")return i.end.column++,i}}),this.add("brackets","insertion",function(e,t,n,r,i){if(i=="["){c(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return{text:"["+o+"]",selection:!1};if(h.isSaneInsertion(n,r))return h.recordAutoInsert(n,r,"]"),{text:"[]",selection:[1,1]}}else if(i=="]"){c(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f=="]"){var l=r.$findOpeningBracket("]",{column:u.column+1,row:u.row});if(l!==null&&h.isAutoInsertedClosing(u,a,i))return h.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("brackets","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="["){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u=="]")return i.end.column++,i}}),this.add("string_dquotes","insertion",function(e,t,n,r,i){if(i=='"'||i=="'"){c(n);var s=i,o=n.getSelectionRange(),u=r.doc.getTextRange(o);if(u!==""&&u!=="'"&&u!='"'&&n.getWrapBehavioursEnabled())return{text:s+u+s,selection:!1};var a=n.getCursorPosition(),f=r.doc.getLine(a.row),l=f.substring(a.column-1,a.column);if(l=="\\")return null;var p=r.getTokens(o.start.row),d=0,v,m=-1;for(var g=0;go.start.column)break;d+=p[g].value.length}if(!v||m<0&&v.type!=="comment"&&(v.type!=="string"||o.start.column!==v.value.length+d-1&&v.value.lastIndexOf(s)===v.value.length-1)){if(!h.isSaneInsertion(n,r))return;return{text:s+s,selection:[1,1]}}if(v&&v.type==="string"){var y=f.substring(a.column,a.column+1);if(y==s)return{text:"",selection:[1,1]}}}}),this.add("string_dquotes","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&(s=='"'||s=="'")){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==s)return i.end.column++,i}})};h.isSaneInsertion=function(e,t){var n=e.getCursorPosition(),r=new s(t,n.row,n.column);if(!this.$matchTokenType(r.getCurrentToken()||"text",u)){var i=new s(t,n.row,n.column+1);if(!this.$matchTokenType(i.getCurrentToken()||"text",u))return!1}return r.stepForward(),r.getCurrentTokenRow()!==n.row||this.$matchTokenType(r.getCurrentToken()||"text",a)},h.$matchTokenType=function(e,t){return t.indexOf(e.type||e)>-1},h.recordAutoInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isAutoInsertedClosing(r,i,f.autoInsertedLineEnd[0])||(f.autoInsertedBrackets=0),f.autoInsertedRow=r.row,f.autoInsertedLineEnd=n+i.substr(r.column),f.autoInsertedBrackets++},h.recordMaybeInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isMaybeInsertedClosing(r,i)||(f.maybeInsertedBrackets=0),f.maybeInsertedRow=r.row,f.maybeInsertedLineStart=i.substr(0,r.column)+n,f.maybeInsertedLineEnd=i.substr(r.column),f.maybeInsertedBrackets++},h.isAutoInsertedClosing=function(e,t,n){return f.autoInsertedBrackets>0&&e.row===f.autoInsertedRow&&n===f.autoInsertedLineEnd[0]&&t.substr(e.column)===f.autoInsertedLineEnd},h.isMaybeInsertedClosing=function(e,t){return f.maybeInsertedBrackets>0&&e.row===f.maybeInsertedRow&&t.substr(e.column)===f.maybeInsertedLineEnd&&t.substr(0,e.column)==f.maybeInsertedLineStart},h.popAutoInsertedClosing=function(){f.autoInsertedLineEnd=f.autoInsertedLineEnd.substr(1),f.autoInsertedBrackets--},h.clearMaybeInsertedClosing=function(){f&&(f.maybeInsertedBrackets=0,f.maybeInsertedRow=-1)},r.inherits(h,i),t.CstyleBehaviour=h}),define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/(\{|\[)[^\}\]]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/,this.getFoldWidgetRange=function(e,t,n,r){var i=e.getLine(n),s=i.match(this.foldingStartMarker);if(s){var o=s.index;if(s[1])return this.openingBracketBlock(e,s[1],n,o);var u=e.getCommentFoldRange(n,o+s[0].length,1);return u&&!u.isMultiLine()&&(r?u=this.getSectionRange(e,n):t!="all"&&(u=null)),u}if(t==="markbegin")return;var s=i.match(this.foldingStopMarker);if(s){var o=s.index+s[0].length;return s[1]?this.closingBracketBlock(e,s[1],n,o):e.getCommentFoldRange(n,o,-1)}},this.getSectionRange=function(e,t){var n=e.getLine(t),r=n.search(/\S/),s=t,o=n.length;t+=1;var u=t,a=e.getLength();while(++tf)break;var l=this.getFoldWidgetRange(e,"all",t);if(l){if(l.start.row<=s)break;if(l.isMultiLine())t=l.end.row;else if(r==f)break}u=t}return new i(s,o,u,e.getLine(u).length)}}.call(o.prototype)}) -------------------------------------------------------------------------------- /public/j/editor/theme-katzenmilch.js: -------------------------------------------------------------------------------- 1 | define("ace/theme/katzenmilch",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-katzenmilch",t.cssText=".ace-katzenmilch .ace_gutter,/* THIS THEME WAS AUTOGENERATED BY Theme.tmpl.css (UUID: ) */.ace-katzenmilch .ace_gutter {background: #e8e8e8;color: #333}.ace-katzenmilch .ace_print-margin {width: 1px;background: #e8e8e8}.ace-katzenmilch {background-color: #f3f2f3;color: rgba(15, 0, 9, 1.0)}.ace-katzenmilch .ace_cursor {border-left: 2px solid #100011}.ace-katzenmilch .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid #100011}.ace-katzenmilch .ace_marker-layer .ace_selection {background: rgba(100, 5, 208, 0.27)}.ace-katzenmilch.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #f3f2f3;border-radius: 2px}.ace-katzenmilch .ace_marker-layer .ace_step {background: rgb(198, 219, 174)}.ace-katzenmilch .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #000000}.ace-katzenmilch .ace_marker-layer .ace_active-line {background: rgb(232, 242, 254)}.ace-katzenmilch .ace_gutter-active-line {background-color: rgb(232, 242, 254)}.ace-katzenmilch .ace_marker-layer .ace_selected-word {border: 1px solid rgba(100, 5, 208, 0.27)}.ace-katzenmilch .ace_fold {background-color: rgba(2, 95, 73, 0.97);border-color: rgba(15, 0, 9, 1.0)}.ace-katzenmilch .ace_keyword {color: #674Aa8;rbackground-color: rgba(163, 170, 216, 0.055)}.ace-katzenmilch .ace_constant.ace_language {color: #7D7e52;rbackground-color: rgba(189, 190, 130, 0.059)}.ace-katzenmilch .ace_constant.ace_numeric {color: rgba(79, 130, 123, 0.93);rbackground-color: rgba(119, 194, 187, 0.059)}.ace-katzenmilch .ace_constant.ace_character,.ace-katzenmilch .ace_constant.ace_other {color: rgba(2, 95, 105, 1.0);rbackground-color: rgba(127, 34, 153, 0.063)}.ace-katzenmilch .ace_support.ace_function {color: #9D7e62;rbackground-color: rgba(189, 190, 130, 0.039)}.ace-katzenmilch .ace_support.ace_class {color: rgba(239, 106, 167, 1.0);rbackground-color: rgba(239, 106, 167, 0.063)}.ace-katzenmilch .ace_storage {color: rgba(123, 92, 191, 1.0);rbackground-color: rgba(139, 93, 223, 0.051)}.ace-katzenmilch .ace_invalid {color: #DFDFD5;rbackground-color: #CC1B27}.ace-katzenmilch .ace_string {color: #5a5f9b;rbackground-color: rgba(170, 175, 219, 0.035)}.ace-katzenmilch .ace_comment {font-style: italic;color: rgba(64, 79, 80, 0.67);rbackground-color: rgba(95, 15, 255, 0.0078)}.ace-katzenmilch .ace_entity.ace_name.ace_function,.ace-katzenmilch .ace_variable {color: rgba(2, 95, 73, 0.97);rbackground-color: rgba(34, 255, 73, 0.12)}.ace-katzenmilch .ace_variable.ace_language {color: #316fcf;rbackground-color: rgba(58, 175, 255, 0.039)}.ace-katzenmilch .ace_variable.ace_parameter {font-style: italic;color: rgba(51, 150, 159, 0.87);rbackground-color: rgba(5, 214, 249, 0.043)}.ace-katzenmilch .ace_entity.ace_other.ace_attribute-name {color: rgba(73, 70, 194, 0.93);rbackground-color: rgba(73, 134, 194, 0.035)}.ace-katzenmilch .ace_entity.ace_name.ace_tag {color: #3976a2;rbackground-color: rgba(73, 166, 210, 0.039)}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /public/j/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | Query String 3 | */ 4 | var urlParams 5 | (function() { 6 | Clappr.Log.setLevel(Clappr.Log.LEVEL_WARN) 7 | window.onpopstate = function () { 8 | var match, 9 | pl = /\+/g, // Regex for replacing addition symbol with a space 10 | search = /([^&=]+)=?([^&]*)/g, 11 | decode = function (s) { return decodeURIComponent(s.replace(pl, ' ')) }, 12 | query = window.location.search.substring(1) 13 | 14 | urlParams = {} 15 | while (match = search.exec(query)) 16 | urlParams[decode(match[1])] = decode(match[2]) 17 | } 18 | window.onpopstate() 19 | })() 20 | 21 | /* 22 | Parser 23 | */ 24 | var Parser = function(output) { 25 | this.output = output; 26 | this.console = $("#console"); 27 | this.context = document; 28 | }; 29 | 30 | Parser.prototype = { 31 | parse: function(code) { 32 | try { 33 | var old = player; 34 | eval(code); 35 | old && old.destroy(); 36 | window.player = player; 37 | this.console.empty(); 38 | } catch(err) { 39 | this.console.html(err.message); 40 | } 41 | } 42 | }; 43 | 44 | $(document).ready(function() { 45 | var parser = new Parser($('#output')) 46 | var load = function (fn) { 47 | if (window.clappr.externals.length > 0) { 48 | var lastScript = window.clappr.externals.length 49 | window.clappr.externals.forEach(function (url, index) { 50 | var script = document.createElement('script') 51 | 52 | script.setAttribute("type", "text/javascript") 53 | script.setAttribute("src", url) 54 | if (index === (lastScript - 1)) { 55 | script.onload = fn 56 | } 57 | script.onerror = function (e) { alert('we cant load ' + url + ': e' + e) } 58 | 59 | document.body.appendChild(script) 60 | }) 61 | } else { 62 | fn() 63 | } 64 | } 65 | $('.run').click(function() { 66 | var code = ace.edit('editor').getSession().getValue() 67 | load(function () { parser.parse(code) }) 68 | }) 69 | }) 70 | 71 | /* 72 | Editor 73 | */ 74 | window.onload = function() { 75 | var editor = ace.edit('editor') 76 | var session = editor.getSession() 77 | 78 | editor.setTheme('ace/theme/katzenmilch') 79 | editor.$blockScrolling = Infinity 80 | session.setMode('ace/mode/javascript') 81 | session.setTabSize(2) 82 | session.setUseSoftTabs(true) 83 | editor.commands.addCommand({ 84 | name: 'run', 85 | bindKey: {mac: 'Command-Enter'}, 86 | exec: function(editor) { 87 | document.querySelector('.run').click() 88 | }, 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /public/stylesheets/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top, #fff 0, #e0e0e0 100%);background-image:linear-gradient(to bottom, #fff 0, #e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top, #428bca 0, #2d6ca2 100%);background-image:linear-gradient(to bottom, #428bca 0, #2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #419641 100%);background-image:linear-gradient(to bottom, #5cb85c 0, #419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #2aabd2 100%);background-image:linear-gradient(to bottom, #5bc0de 0, #2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #eb9316 100%);background-image:linear-gradient(to bottom, #f0ad4e 0, #eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c12e2a 100%);background-image:linear-gradient(to bottom, #d9534f 0, #c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-color:#357ebd}.navbar-default{background-image:-webkit-linear-gradient(top, #fff 0, #f8f8f8 100%);background-image:linear-gradient(to bottom, #fff 0, #f8f8f8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #ebebeb 0, #f3f3f3 100%);background-image:linear-gradient(to bottom, #ebebeb 0, #f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top, #3c3c3c 0, #222 100%);background-image:linear-gradient(to bottom, #3c3c3c 0, #222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top, #222 0, #282828 100%);background-image:linear-gradient(to bottom, #222 0, #282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top, #dff0d8 0, #c8e5bc 100%);background-image:linear-gradient(to bottom, #dff0d8 0, #c8e5bc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top, #d9edf7 0, #b9def0 100%);background-image:linear-gradient(to bottom, #d9edf7 0, #b9def0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #f8efc0 100%);background-image:linear-gradient(to bottom, #fcf8e3 0, #f8efc0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top, #f2dede 0, #e7c3c3 100%);background-image:linear-gradient(to bottom, #f2dede 0, #e7c3c3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top, #ebebeb 0, #f5f5f5 100%);background-image:linear-gradient(to bottom, #ebebeb 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top, #428bca 0, #3071a9 100%);background-image:linear-gradient(to bottom, #428bca 0, #3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #449d44 100%);background-image:linear-gradient(to bottom, #5cb85c 0, #449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #31b0d5 100%);background-image:linear-gradient(to bottom, #5bc0de 0, #31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #ec971f 100%);background-image:linear-gradient(to bottom, #f0ad4e 0, #ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c9302c 100%);background-image:linear-gradient(to bottom, #d9534f 0, #c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top, #428bca 0, #3278b3 100%);background-image:linear-gradient(to bottom, #428bca 0, #3278b3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top, #dff0d8 0, #d0e9c6 100%);background-image:linear-gradient(to bottom, #dff0d8 0, #d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top, #d9edf7 0, #c4e3f3 100%);background-image:linear-gradient(to bottom, #d9edf7 0, #c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top, #fcf8e3 0, #faf2cc 100%);background-image:linear-gradient(to bottom, #fcf8e3 0, #faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top, #f2dede 0, #ebcccc 100%);background-image:linear-gradient(to bottom, #f2dede 0, #ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0)}.well{background-image:-webkit-linear-gradient(top, #e8e8e8 0, #f5f5f5 100%);background-image:linear-gradient(to bottom, #e8e8e8 0, #f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} -------------------------------------------------------------------------------- /public/stylesheets/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:normal;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%} -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .header { 7 | position: relative; 8 | margin: 0; 9 | right: 0; 10 | left: 0; 11 | padding: 0 4%; 12 | top: 0; 13 | background-color: rgba(116, 116, 116, 1); 14 | color: #eee; 15 | height: 50px; 16 | line-height: 50px; 17 | width: 100%; 18 | } 19 | 20 | .header span { 21 | font-size: 30px; 22 | font-weight: bold; 23 | } 24 | 25 | .header img { 26 | height: 45px; 27 | } 28 | 29 | .header ul { 30 | right: 7%; 31 | } 32 | 33 | .header ul, .header li, .header a { 34 | position: relative; 35 | display: inline; 36 | float: right; 37 | color: #ddd; 38 | margin: 0; 39 | padding: 0; 40 | outline: none; 41 | } 42 | 43 | a:hover { 44 | color: #fff; 45 | text-decoration: none; 46 | } 47 | 48 | .header a:visited, a:active, a:link { 49 | color: #ddd; 50 | text-decoration: none; 51 | } 52 | 53 | .run { 54 | display: block; 55 | float: right; 56 | margin: 20px 0; 57 | } 58 | 59 | .container { 60 | text-align: center; 61 | } 62 | 63 | .main { 64 | display: inline-block; 65 | margin: 0; 66 | padding: 0px 20px 0 20px; 67 | border: 0; 68 | } 69 | 70 | .external-js { 71 | border-style: solid; 72 | border-width: 1px; 73 | color: #dcdcdc; 74 | font-size: 10px; 75 | padding: 2px; 76 | margin-right: 2px; 77 | font-family: monospace; 78 | } 79 | 80 | #player-wrapper { 81 | min-width: 320px; 82 | min-height: 180px; 83 | } 84 | 85 | .sidebar { 86 | display: inline-block; 87 | text-align: left; 88 | width: 680px; 89 | margin: 0; 90 | padding: 15px 20px 0 20px; 91 | border: 0; 92 | } 93 | 94 | #editor { 95 | border: 1px solid #c0c0EE; 96 | min-height: 230px; 97 | } 98 | 99 | #console { 100 | position: relative; 101 | color: red; 102 | left: 2%; 103 | top: 65px; 104 | } 105 | 106 | .btn:focus { outline: none; } 107 | 108 | .player { 109 | display: inline-block; 110 | margin: 0 auto; 111 | height: auto; 112 | width: auto; 113 | } 114 | -------------------------------------------------------------------------------- /rollup.config.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const babel = require('rollup-plugin-babel') 4 | const commonjs = require('@rollup/plugin-commonjs') 5 | const html = require('rollup-plugin-html') 6 | const namedDirectory = require('rollup-plugin-named-directory') 7 | const postcss = require('rollup-plugin-postcss') 8 | const replace = require('@rollup/plugin-replace') 9 | const { nodeResolve } = require('@rollup/plugin-node-resolve') 10 | const svg = require('rollup-plugin-svg') 11 | 12 | const { version } = require('./package.json') 13 | const { version: clapprCoreVersion } = require('@clappr/core/package.json') 14 | 15 | const postcssOptions = { 16 | use: [ 17 | ['sass', { 18 | includePaths: [ 19 | path.resolve('node_modules/@clappr/core/src/base/scss') 20 | ] 21 | }] 22 | ], 23 | inject: false, 24 | } 25 | 26 | const baseConfig = { 27 | input: 'src/main.js', 28 | external: ['@clappr/core'], 29 | output: [ 30 | { 31 | file: 'dist/clappr-plugins.js', 32 | format: 'umd', 33 | name: 'ClapprPlugins', 34 | globals: { 35 | '@clappr/core': 'Clappr', 36 | } 37 | }, 38 | ], 39 | plugins: [ 40 | replace({ 41 | VERSION: JSON.stringify(version), 42 | CLAPPR_CORE_VERSION: JSON.stringify(clapprCoreVersion), 43 | preventAssignment: false, 44 | }), 45 | commonjs(), 46 | nodeResolve(), 47 | namedDirectory(), 48 | babel({ 49 | exclude: 'node_modules/**' 50 | }), 51 | html(), 52 | svg(), 53 | postcss(postcssOptions), 54 | ], 55 | } 56 | 57 | module.exports = { 58 | baseConfig, 59 | postcssOptions, 60 | } 61 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | const livereload = require('rollup-plugin-livereload') 2 | const serve = require('rollup-plugin-serve') 3 | 4 | const { baseConfig } = require('./rollup.config.base') 5 | 6 | const serveConfig = { 7 | contentBase: ['dist', 'public'], 8 | host: '0.0.0.0', 9 | port: 8080, 10 | } 11 | 12 | const plugins = [ 13 | ...baseConfig.plugins, 14 | serve(serveConfig), 15 | livereload({ 16 | watch: ['dist', 'public'] 17 | }), 18 | ] 19 | 20 | module.exports = { 21 | ...baseConfig, 22 | plugins, 23 | } 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | 2 | const analyze = require('rollup-plugin-analyzer') 3 | const { terser } = require('rollup-plugin-terser') 4 | 5 | const { baseConfig } = require('./rollup.config.base') 6 | 7 | const minimize = !!process.env.MINIMIZE 8 | const analyzeBundle = !!process.env.ANALYZE_BUNDLE 9 | 10 | const output = [ 11 | ...baseConfig.output, 12 | ...(minimize 13 | ? [ 14 | { 15 | file: 'dist/clappr-plugins.min.js', 16 | format: 'umd', 17 | name: 'ClapprPlugins', 18 | globals: { '@clappr/core': 'Clappr' }, 19 | plugins: terser(), 20 | }, 21 | ] 22 | : []), 23 | { 24 | file: 'dist/clappr-plugins.esm.js', 25 | format: 'esm', 26 | }, 27 | ] 28 | 29 | const plugins = [ 30 | ...baseConfig.plugins, 31 | ...(analyzeBundle ? [analyze()] : []), 32 | ] 33 | 34 | module.exports = { 35 | ...baseConfig, 36 | output, 37 | plugins, 38 | } 39 | -------------------------------------------------------------------------------- /src/icons/01-play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/02-pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/03-stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/04-volume.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/05-mute.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/06-expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/07-shrink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/08-hd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/09-cc.svg: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/10-reload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Globo.com Player authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import ClickToPause from './plugins/click_to_pause' 6 | import ClosedCaptions from './plugins/closed_captions' 7 | import DVRControls from './plugins/dvr_controls' 8 | import EndVideo from './plugins/end_video' 9 | import ErrorScreen from './plugins/error_screen' 10 | import Favicon from './plugins/favicon' 11 | import GoogleAnalytics from './plugins/google_analytics' 12 | import MediaControl from './plugins/media_control' 13 | import Poster from './plugins/poster' 14 | import SeekTime from './plugins/seek_time' 15 | import SpinnerThreeBounce from './plugins/spinner_three_bounce' 16 | import Stats from './plugins/stats' 17 | import WaterMark from './plugins/watermark' 18 | 19 | import Vendor from './vendor' 20 | 21 | const version = VERSION 22 | 23 | const Plugins = { 24 | ClickToPause, 25 | ClosedCaptions, 26 | DVRControls, 27 | EndVideo, 28 | ErrorScreen, 29 | Favicon, 30 | GoogleAnalytics, 31 | MediaControl, 32 | Poster, 33 | SeekTime, 34 | SpinnerThreeBounce, 35 | Stats, 36 | WaterMark, 37 | } 38 | 39 | export { 40 | ClickToPause, 41 | ClosedCaptions, 42 | DVRControls, 43 | EndVideo, 44 | ErrorScreen, 45 | Favicon, 46 | GoogleAnalytics, 47 | MediaControl, 48 | Poster, 49 | SeekTime, 50 | SpinnerThreeBounce, 51 | Stats, 52 | WaterMark, 53 | Vendor, 54 | Plugins, 55 | version, 56 | } 57 | -------------------------------------------------------------------------------- /src/plugins/click_to_pause/click_to_pause.js: -------------------------------------------------------------------------------- 1 | //Copyright 2014 Globo.com Player authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import { ContainerPlugin, Events, Playback } from '@clappr/core' 6 | 7 | export default class ClickToPausePlugin extends ContainerPlugin { 8 | get name() { return 'click_to_pause' } 9 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 10 | get config() { return this.container.options.clickToPauseConfig || {} } 11 | 12 | constructor(container) { 13 | super(container) 14 | } 15 | 16 | bindEvents() { 17 | this.listenTo(this.container, Events.CONTAINER_CLICK, this.click) 18 | this.listenTo(this.container, Events.CONTAINER_SETTINGSUPDATE, this.settingsUpdate) 19 | } 20 | 21 | click() { 22 | const onClickPayload = this.config.onClickPayload 23 | 24 | if (this.container.getPlaybackType() !== Playback.LIVE || this.container.isDvrEnabled()) { 25 | if (this.container.isPlaying()) 26 | this.container.pause(onClickPayload) 27 | else 28 | this.container.play(onClickPayload) 29 | 30 | } 31 | } 32 | 33 | settingsUpdate() { 34 | const pointerEnabled = this.container.getPlaybackType() !== Playback.LIVE || this.container.isDvrEnabled() 35 | if (pointerEnabled === this.pointerEnabled) return 36 | 37 | const method = pointerEnabled ? 'addClass' : 'removeClass' 38 | this.container.$el[method]('pointer-enabled') 39 | this.pointerEnabled = pointerEnabled 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/plugins/click_to_pause/click_to_pause.test.js: -------------------------------------------------------------------------------- 1 | import { Events, Container, Playback } from '@clappr/core' 2 | 3 | import ClickToPause from './click_to_pause' 4 | 5 | describe('clickToPause', function() { 6 | beforeEach(function() { 7 | this.playback = new Playback() 8 | this.container = new Container({ playback: this.playback }) 9 | this.plugin = new ClickToPause(this.container) 10 | }) 11 | 12 | it('has name', function() { 13 | expect(this.plugin.name).to.be.equal('click_to_pause') 14 | }) 15 | 16 | it('call pause when playing and dvr is enabled', function(done) { 17 | sinon.stub(this.container, 'isPlaying').callsFake(() => true) 18 | sinon.stub(this.container, 'isDvrEnabled').callsFake(() => true) 19 | sinon.spy(this.container, 'pause') 20 | 21 | this.container.on(Events.CONTAINER_CLICK, () => { 22 | this.container.pause.should.have.been.calledOnce 23 | done() 24 | }) 25 | 26 | this.container.trigger(Events.CONTAINER_CLICK) 27 | }) 28 | 29 | it('call play when not playing and dvr is enabled', function(done) { 30 | sinon.stub(this.container, 'isPlaying').callsFake(() => false) 31 | sinon.stub(this.container, 'isDvrEnabled').callsFake(() => true) 32 | sinon.spy(this.container, 'play') 33 | 34 | this.container.on(Events.CONTAINER_CLICK, () => { 35 | this.container.play.should.have.been.calledOnce 36 | done() 37 | }) 38 | 39 | this.container.trigger(Events.CONTAINER_CLICK) 40 | }) 41 | 42 | it('not call play nor pause when playback type is live and dvr is disable', function(done) { 43 | sinon.stub(this.container, 'getPlaybackType').callsFake(() => Playback.LIVE) 44 | sinon.stub(this.container, 'isDvrEnabled').callsFake(() => false) 45 | sinon.spy(this.container, 'play') 46 | sinon.spy(this.container, 'pause') 47 | 48 | this.container.on(Events.CONTAINER_CLICK, () => { 49 | this.container.play.should.not.have.been.called 50 | this.container.pause.should.not.have.been.called 51 | done() 52 | }) 53 | 54 | this.container.trigger(Events.CONTAINER_CLICK) 55 | }) 56 | 57 | it('not show cursor pointer when playback is live and drv is disable', function(done) { 58 | sinon.stub(this.container, 'getPlaybackType').callsFake(() => Playback.LIVE) 59 | sinon.stub(this.container, 'isDvrEnabled').callsFake(() => false) 60 | 61 | this.container.on(Events.CONTAINER_SETTINGSUPDATE, () => { 62 | expect(this.container.$el.hasClass('pointer-enabled')).to.be.false 63 | done() 64 | }) 65 | 66 | this.container.trigger(Events.CONTAINER_SETTINGSUPDATE) 67 | }) 68 | 69 | it('show cursor pointer when playback is live and drv is enable', function(done) { 70 | sinon.stub(this.container, 'getPlaybackType').callsFake(() => Playback.LIVE) 71 | sinon.stub(this.container, 'isDvrEnabled').callsFake(() => true) 72 | 73 | this.container.on(Events.CONTAINER_SETTINGSUPDATE, () => { 74 | expect(this.container.$el.hasClass('pointer-enabled')).to.be.true 75 | done() 76 | }) 77 | 78 | this.container.trigger(Events.CONTAINER_SETTINGSUPDATE) 79 | }) 80 | 81 | describe('show cursor pointer when playback is not live and', function() { 82 | 83 | it('playback is VOD', function(done) { 84 | sinon.stub(this.container, 'getPlaybackType').callsFake(() => Playback.VOD) 85 | 86 | this.container.on(Events.CONTAINER_SETTINGSUPDATE, () => { 87 | expect(this.container.$el.hasClass('pointer-enabled')).to.be.true 88 | done() 89 | }) 90 | 91 | this.container.trigger(Events.CONTAINER_SETTINGSUPDATE) 92 | }) 93 | 94 | it('playback is AOD', function(done) { 95 | sinon.stub(this.container, 'getPlaybackType').callsFake(() => Playback.AOD) 96 | 97 | this.container.on(Events.CONTAINER_SETTINGSUPDATE, () => { 98 | expect(this.container.$el.hasClass('pointer-enabled')).to.be.true 99 | done() 100 | }) 101 | 102 | this.container.trigger(Events.CONTAINER_SETTINGSUPDATE) 103 | }) 104 | 105 | it('playback is NO_OP', function(done) { 106 | sinon.stub(this.container, 'getPlaybackType').callsFake(() => Playback.NO_OP) 107 | 108 | this.container.on(Events.CONTAINER_SETTINGSUPDATE, () => { 109 | expect(this.container.$el.hasClass('pointer-enabled')).to.be.true 110 | done() 111 | }) 112 | 113 | this.container.trigger(Events.CONTAINER_SETTINGSUPDATE) 114 | }) 115 | }) 116 | 117 | describe('on playback live and dvr enabled', function() { 118 | beforeEach(function(done) { 119 | sinon.stub(this.container, 'getPlaybackType').callsFake(() => Playback.LIVE) 120 | sinon.stub(this.container, 'isDvrEnabled').callsFake(() => true) 121 | sinon.spy(this.container.$el, 'addClass') 122 | sinon.spy(this.container.$el, 'removeClass') 123 | 124 | this.container.once(Events.CONTAINER_SETTINGSUPDATE, done) 125 | this.container.trigger(Events.CONTAINER_SETTINGSUPDATE) 126 | }) 127 | 128 | it('add css class when state changes', function() { 129 | this.container.$el.addClass.should.have.been.calledOnce 130 | }) 131 | 132 | it('do not toggle when state do not changes', function(done) { 133 | this.container.on(Events.CONTAINER_SETTINGSUPDATE, () => { 134 | this.container.$el.removeClass.should.not.have.been.called 135 | this.container.$el.addClass.should.have.been.calledOnce 136 | done() 137 | }) 138 | this.container.trigger(Events.CONTAINER_SETTINGSUPDATE) 139 | }) 140 | }) 141 | 142 | it('call container play with parameters when received from config', function(done) { 143 | this.container = new Container({ 144 | playback: this.playback, 145 | clickToPauseConfig: { onClickPayload: { testing: true } } 146 | }) 147 | const plugin = new ClickToPause(this.container) // eslint-disable-line 148 | 149 | 150 | sinon.stub(this.container, 'isPlaying').callsFake(() => false) 151 | sinon.stub(this.container, 'isDvrEnabled').callsFake(() => true) 152 | sinon.spy(this.container, 'play') 153 | 154 | this.container.on(Events.CONTAINER_CLICK, () => { 155 | this.container.play.should.have.been.calledWith({ testing: true }) 156 | done() 157 | }) 158 | 159 | this.container.trigger(Events.CONTAINER_CLICK) 160 | }) 161 | 162 | it('call container pause with parameters when received from config', function(done) { 163 | this.container = new Container({ 164 | playback: this.playback, 165 | clickToPauseConfig: { onClickPayload: { testing: true } } 166 | }) 167 | const plugin = new ClickToPause(this.container) // eslint-disable-line 168 | 169 | 170 | sinon.stub(this.container, 'isPlaying').callsFake(() => true) 171 | sinon.stub(this.container, 'isDvrEnabled').callsFake(() => true) 172 | sinon.spy(this.container, 'pause') 173 | 174 | this.container.on(Events.CONTAINER_CLICK, () => { 175 | this.container.pause.should.have.been.calledWith({ testing: true }) 176 | done() 177 | }) 178 | 179 | this.container.trigger(Events.CONTAINER_CLICK) 180 | }) 181 | 182 | }) 183 | -------------------------------------------------------------------------------- /src/plugins/closed_captions/closed_captions.js: -------------------------------------------------------------------------------- 1 | import { Events, UICorePlugin, template, Styler } from '@clappr/core' 2 | import ccIcon from '../../icons/09-cc.svg' 3 | import ccHTML from './public/closed_captions.html' 4 | import ccStyle from './public/closed_captions.scss' 5 | 6 | export default class ClosedCaptions extends UICorePlugin { 7 | get name() { return 'closed_captions' } 8 | 9 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 10 | 11 | get template() { return template(ccHTML) } 12 | 13 | get events() { 14 | return { 15 | 'click [data-cc-button]': 'toggleContextMenu', 16 | 'click [data-cc-select]': 'onTrackSelect', 17 | } 18 | } 19 | 20 | get attributes() { 21 | return { 22 | 'class': 'cc-controls', 23 | 'data-cc-controls': '' 24 | } 25 | } 26 | 27 | constructor(core) { 28 | super(core) 29 | const config = core.options.closedCaptionsConfig 30 | this._title = config && config.title ? config.title : null 31 | this._ariaLabel = config && config.ariaLabel ? config.ariaLabel : 'cc-button' 32 | this._labelCb = config && config.labelCallback && typeof config.labelCallback === 'function' 33 | ? config.labelCallback 34 | : track => { return track.name } 35 | } 36 | 37 | bindEvents() { 38 | this.listenTo(this.core, Events.CORE_ACTIVE_CONTAINER_CHANGED, this.containerChanged) 39 | this.listenTo(this.core.mediaControl, Events.MEDIACONTROL_RENDERED, this.render) 40 | this.listenTo(this.core.mediaControl, Events.MEDIACONTROL_HIDE, this.hideContextMenu) 41 | 42 | this.bindContainerEvents() 43 | } 44 | 45 | bindContainerEvents() { 46 | this.container = this.core.activeContainer 47 | if (this.container) { 48 | this.listenTo(this.container, Events.CONTAINER_SUBTITLE_AVAILABLE, this.onSubtitleAvailable) 49 | this.listenTo(this.container, Events.CONTAINER_SUBTITLE_CHANGED, this.onSubtitleChanged) 50 | this.listenTo(this.container, Events.CONTAINER_STOP, this.onContainerStop) 51 | } 52 | } 53 | 54 | onContainerStop() { 55 | this.ccAvailable(false) 56 | } 57 | 58 | containerChanged() { 59 | this.ccAvailable(false) 60 | this.stopListening() 61 | this.bindEvents() 62 | } 63 | 64 | onSubtitleAvailable() { 65 | this.renderCcButton() 66 | this.ccAvailable(true) 67 | } 68 | 69 | onSubtitleChanged(track) { 70 | this.setCurrentContextMenuElement(track.id) 71 | } 72 | 73 | onTrackSelect(event) { 74 | const trackId = parseInt(event.target.dataset.ccSelect, 10) 75 | this.container.closedCaptionsTrackId = trackId 76 | this.hideContextMenu() 77 | event.stopPropagation() 78 | return false 79 | } 80 | 81 | ccAvailable(hasCC) { 82 | const method = hasCC ? 'addClass' : 'removeClass' 83 | this.$el[method]('available') 84 | } 85 | 86 | toggleContextMenu() { 87 | this.$el.find('ul').toggle() 88 | } 89 | 90 | hideContextMenu() { 91 | this.$el.find('ul').hide() 92 | } 93 | 94 | contextMenuElement(id) { 95 | return this.$el.find('ul a'+(!isNaN(id) ? '[data-cc-select="'+id+'"]' : '')).parent() 96 | } 97 | 98 | setCurrentContextMenuElement(trackId) { 99 | if (this._trackId !== trackId) { 100 | this.contextMenuElement().removeClass('current') 101 | this.contextMenuElement(trackId).addClass('current') 102 | const method = trackId > -1 ? 'addClass' : 'removeClass' 103 | this.$ccButton[method]('enabled') 104 | this._trackId = trackId 105 | } 106 | } 107 | 108 | renderCcButton() { 109 | let tracks = this.container ? this.container.closedCaptionsTracks : [] 110 | for (let i = 0; i < tracks.length; i++) 111 | tracks[i].label = this._labelCb(tracks[i]) 112 | 113 | 114 | const style = Styler.getStyleFor(ccStyle, { baseUrl: this.options.baseUrl }) 115 | this.$el.html(this.template({ 116 | ariaLabel: this._ariaLabel, 117 | disabledLabel: this.core.i18n.t('disabled'), 118 | title: this._title, 119 | tracks: tracks 120 | })) 121 | 122 | this.$ccButton = this.$el.find('button.cc-button[data-cc-button]') 123 | this.$ccButton.append(ccIcon) 124 | this.$el.append(style[0]) 125 | } 126 | 127 | render() { 128 | this.renderCcButton() 129 | 130 | const $fullscreen = this.core.mediaControl.$el.find('button[data-fullscreen]') 131 | if ($fullscreen[0]) 132 | this.$el.insertAfter($fullscreen) 133 | else 134 | this.core.mediaControl.$el.find('.media-control-right-panel[data-media-control]').prepend(this.$el) 135 | 136 | 137 | return this 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/plugins/closed_captions/public/closed_captions.html: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /src/plugins/closed_captions/public/closed_captions.scss: -------------------------------------------------------------------------------- 1 | .cc-controls[data-cc-controls] { 2 | float: right; 3 | position: relative; 4 | display: none; 5 | 6 | &.available { 7 | display: block; 8 | } 9 | 10 | .cc-button { 11 | padding: 6px !important; 12 | 13 | &.enabled { 14 | display: block; 15 | opacity: 1.0; 16 | 17 | &:hover { 18 | opacity: 1.0; 19 | text-shadow: none; 20 | } 21 | } 22 | } 23 | 24 | & > ul { 25 | list-style-type: none; 26 | position: absolute; 27 | bottom: 25px; 28 | border: 1px solid black; 29 | display: none; 30 | background-color: #e6e6e6; 31 | } 32 | 33 | li { 34 | font-size: 10px; 35 | 36 | &[data-title] { 37 | background-color: #c3c2c2; 38 | padding: 5px; 39 | } 40 | 41 | a { 42 | color: #444; 43 | padding: 2px 10px; 44 | display: block; 45 | text-decoration: none; 46 | 47 | &:hover { 48 | background-color: #555; 49 | color: white; 50 | a { 51 | color: white; 52 | text-decoration: none; 53 | } 54 | } 55 | } 56 | 57 | &.current a { 58 | color: #f00; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/plugins/dvr_controls/dvr_controls.js: -------------------------------------------------------------------------------- 1 | import { Events, Playback, UICorePlugin, template, Styler } from '@clappr/core' 2 | import dvrHTML from './public/index.html' 3 | import dvrStyle from './public/dvr_controls.scss' 4 | 5 | export default class DVRControls extends UICorePlugin { 6 | get template() { return template(dvrHTML) } 7 | get name() { return 'dvr_controls' } 8 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 9 | get events() { 10 | return { 11 | 'click .live-button': 'click' 12 | } 13 | } 14 | get attributes() { 15 | return { 16 | 'class': 'dvr-controls', 17 | 'data-dvr-controls': '' 18 | } 19 | } 20 | 21 | constructor(core) { 22 | super(core) 23 | this.settingsUpdate() 24 | } 25 | 26 | bindEvents() { 27 | this.bindCoreEvents() 28 | this.bindContainerEvents() 29 | } 30 | 31 | bindCoreEvents() { 32 | if (this.core.mediaControl.settings) { 33 | this.listenTo(this.core.mediaControl, Events.MEDIACONTROL_CONTAINERCHANGED, this.containerChanged) 34 | this.listenTo(this.core.mediaControl, Events.MEDIACONTROL_RENDERED, this.settingsUpdate) 35 | this.listenTo(this.core, Events.CORE_OPTIONS_CHANGE, this.render) 36 | } else { 37 | setTimeout(() => this.bindCoreEvents(), 100) 38 | } 39 | } 40 | 41 | bindContainerEvents() { 42 | if (this.core.activeContainer) { 43 | this.listenToOnce(this.core.activeContainer, Events.CONTAINER_TIMEUPDATE, this.render) 44 | this.listenTo(this.core.activeContainer, Events.CONTAINER_PLAYBACKDVRSTATECHANGED, this.dvrChanged) 45 | } 46 | } 47 | 48 | containerChanged() { 49 | this.stopListening() 50 | this.bindEvents() 51 | } 52 | 53 | dvrChanged(dvrEnabled) { 54 | if (this.core.getPlaybackType() !== Playback.LIVE) return 55 | this.settingsUpdate() 56 | this.core.mediaControl.$el.addClass('live') 57 | if (dvrEnabled) { 58 | this.core.mediaControl.$el.addClass('dvr') 59 | this.core.mediaControl.$el.find('.media-control-indicator[data-position], .media-control-indicator[data-duration]').hide() 60 | } else { this.core.mediaControl.$el.removeClass('dvr') } 61 | 62 | } 63 | 64 | click() { 65 | const mediaControl = this.core.mediaControl 66 | const container = mediaControl.container 67 | if (!container.isPlaying()) 68 | container.play() 69 | 70 | if (mediaControl.$el.hasClass('dvr')) 71 | container.seek(container.getDuration()) 72 | 73 | } 74 | 75 | settingsUpdate() { 76 | this.stopListening() 77 | this.core.mediaControl.$el.removeClass('live') 78 | if (this.shouldRender()) { 79 | this.render() 80 | this.$el.click(() => this.click()) 81 | } 82 | this.bindEvents() 83 | } 84 | 85 | shouldRender() { 86 | const useDvrControls = this.core.options.useDvrControls === undefined || !!this.core.options.useDvrControls 87 | return useDvrControls && this.core.getPlaybackType() === Playback.LIVE 88 | } 89 | 90 | render() { 91 | const style = Styler.getStyleFor(dvrStyle, { baseUrl: this.options.baseUrl }) 92 | this.$el.html(this.template({ 93 | live: this.core.i18n.t('live'), 94 | backToLive: this.core.i18n.t('back_to_live') 95 | })) 96 | this.$el.append(style[0]) 97 | if (this.shouldRender()) { 98 | this.core.mediaControl.$el.addClass('live') 99 | this.core.mediaControl.$('.media-control-left-panel[data-media-control]').append(this.$el) 100 | } 101 | return this 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/plugins/dvr_controls/public/dvr_controls.scss: -------------------------------------------------------------------------------- 1 | $live-color: #ff0101; 2 | $dvr-color: #fff; 3 | $vod-color: #005aff; 4 | 5 | $disabled-opacity: 0.3; 6 | 7 | $control-height: 32px; 8 | $circle-radius: 3.5px; 9 | 10 | .dvr-controls[data-dvr-controls] { 11 | display: inline-block; 12 | float: left; 13 | color: $dvr-color; 14 | line-height: 32px; 15 | font-size: 10px; 16 | font-weight: bold; 17 | margin-left: 6px; 18 | 19 | .live-info { 20 | cursor: default; 21 | font-family: "Roboto", "Open Sans", Arial, sans-serif; 22 | text-transform: uppercase; 23 | &:before { 24 | content: ""; 25 | display: inline-block; 26 | position: relative; 27 | width: $circle-radius*2; 28 | height: $circle-radius*2; 29 | border-radius: $circle-radius; 30 | margin-right: $circle-radius; 31 | background-color: $live-color; 32 | } 33 | &.disabled { 34 | opacity: $disabled-opacity; 35 | &:before { 36 | background-color: $dvr-color; 37 | } 38 | } 39 | } 40 | 41 | .live-button { 42 | cursor: pointer; 43 | outline: none; 44 | display: none; 45 | border: 0; 46 | color: $dvr-color; 47 | background-color: transparent; 48 | height: 32px; 49 | padding: 0; 50 | opacity: 0.7; 51 | font-family: "Roboto", "Open Sans", Arial, sans-serif; 52 | text-transform: uppercase; 53 | transition: all 0.1s ease; 54 | 55 | &:before { 56 | content: ""; 57 | display: inline-block; 58 | position: relative; 59 | width: $circle-radius*2; 60 | height: $circle-radius*2; 61 | border-radius: $circle-radius; 62 | margin-right: $circle-radius; 63 | background-color: $dvr-color; 64 | } 65 | 66 | &:hover { 67 | opacity: 1; 68 | text-shadow: rgba(255,255,255,.75) 0 0 5px; 69 | } 70 | } 71 | } 72 | 73 | .dvr { 74 | .dvr-controls[data-dvr-controls] { 75 | .live-info { 76 | display: none; 77 | } 78 | .live-button { 79 | display: block; 80 | } 81 | } 82 | 83 | &.media-control.live[data-media-control] { 84 | .media-control-layer[data-controls] .bar-container[data-seekbar] .bar-background[data-seekbar] .bar-fill-2[data-seekbar] { 85 | background-color: $vod-color; 86 | } 87 | } 88 | } 89 | 90 | .media-control.live[data-media-control] { 91 | .media-control-layer[data-controls] .bar-container[data-seekbar] .bar-background[data-seekbar] .bar-fill-2[data-seekbar] { 92 | background-color: $live-color; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/plugins/dvr_controls/public/index.html: -------------------------------------------------------------------------------- 1 |
<%= live %>
2 | 3 | -------------------------------------------------------------------------------- /src/plugins/end_video.js: -------------------------------------------------------------------------------- 1 | import { CorePlugin, Events } from '@clappr/core' 2 | 3 | export default class EndVideo extends CorePlugin { 4 | get name() { return 'end_video' } 5 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 6 | bindEvents() { 7 | this.listenTo(this.core, Events.CORE_ACTIVE_CONTAINER_CHANGED, this.containerChanged) 8 | const container = this.core.activeContainer 9 | if (container) { 10 | this.listenTo(container, Events.CONTAINER_ENDED, this.ended) 11 | this.listenTo(container, Events.CONTAINER_STOP, this.ended) 12 | } 13 | } 14 | 15 | containerChanged() { 16 | this.stopListening() 17 | this.bindEvents() 18 | } 19 | 20 | ended() { 21 | const exitOnEnd = typeof (this.core.options.exitFullscreenOnEnd) === 'undefined' || this.core.options.exitFullscreenOnEnd 22 | if (exitOnEnd && this.core.isFullscreen()) 23 | this.core.toggleFullscreen() 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/plugins/error_screen/error_screen.js: -------------------------------------------------------------------------------- 1 | import { Events, PlayerError, UICorePlugin, template, Styler } from '@clappr/core' 2 | 3 | import reloadIcon from '../../icons/10-reload.svg' 4 | import templateHtml from './public/error_screen.html' 5 | import errorScreenStyle from './public/error_screen.scss' 6 | 7 | export default class ErrorScreen extends UICorePlugin { 8 | get name() { return 'error_screen' } 9 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 10 | get template() { return template(templateHtml) } 11 | get container() { return this.core.getCurrentContainer() } 12 | get attributes() { 13 | return { 14 | 'class': 'player-error-screen', 15 | 'data-error-screen': '', 16 | } 17 | } 18 | 19 | constructor(core) { 20 | super(core) 21 | 22 | if (this.options.disableErrorScreen) return this.disable() 23 | } 24 | 25 | bindEvents() { 26 | this.listenTo(this.core, Events.ERROR, this.onError) 27 | this.listenTo(this.core, Events.CORE_ACTIVE_CONTAINER_CHANGED, this.onContainerChanged) 28 | } 29 | 30 | bindReload() { 31 | this.reloadButton = this.$el.find('.player-error-screen__reload') 32 | this.reloadButton && this.reloadButton.on('click', this.reload.bind(this)) 33 | } 34 | 35 | reload() { 36 | this.listenToOnce(this.core, Events.CORE_READY, () => this.container.play()) 37 | this.core.load(this.options.sources, this.options.mimeType) 38 | this.unbindReload() 39 | } 40 | 41 | unbindReload() { 42 | this.reloadButton && this.reloadButton.off('click') 43 | } 44 | 45 | onContainerChanged() { 46 | this.err = null 47 | this.unbindReload() 48 | this.hide() 49 | } 50 | 51 | onError(err = {}) { 52 | if (err.level === PlayerError.Levels.FATAL) { 53 | this.err = err 54 | this.container.disableMediaControl() 55 | this.container.stop() 56 | this.show() 57 | } 58 | } 59 | 60 | show() { 61 | this.render() 62 | this.$el.show() 63 | } 64 | 65 | hide() { 66 | this.$el.hide() 67 | } 68 | 69 | render() { 70 | if (!this.err) return 71 | 72 | const style = Styler.getStyleFor(errorScreenStyle, { baseUrl: this.options.baseUrl }) 73 | this.$el.html(this.template({ 74 | title: this.err.UI.title, 75 | message: this.err.UI.message, 76 | code: this.err.code, 77 | icon: this.err.UI.icon || '', 78 | reloadIcon, 79 | })) 80 | this.$el.append(style[0]) 81 | 82 | this.core.$el.append(this.el) 83 | 84 | this.bindReload() 85 | 86 | return this 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/plugins/error_screen/error_screen.test.js: -------------------------------------------------------------------------------- 1 | import { Core, Container, Events, Playback, PlayerError } from '@clappr/core' 2 | 3 | import ErrorScreen from './error_screen' 4 | 5 | describe('ErrorScreen', function() { 6 | beforeEach(() => { 7 | this.core = new Core({}) 8 | this.errorScreen = new ErrorScreen(this.core) 9 | this.core.addPlugin(this.errorScreen) 10 | }) 11 | 12 | it('is named error_screen', () => { 13 | expect(this.errorScreen.name).to.equal('error_screen') 14 | }) 15 | 16 | describe('on ERROR event', () => { 17 | it('calls onError', () => { 18 | const spy = sinon.spy(this.errorScreen, 'onError') 19 | 20 | this.errorScreen.stopListening() 21 | this.errorScreen.bindEvents() 22 | this.core.trigger(Events.ERROR, {}) 23 | 24 | expect(spy).to.have.been.called 25 | }) 26 | 27 | describe('when error level is fatal', () => { 28 | beforeEach(() => { 29 | this.fakeError = { 30 | code: '42', 31 | level: PlayerError.Levels.FATAL, 32 | UI: { 33 | title: 'tigle', 34 | message: 'message', 35 | } 36 | } 37 | this.playback = new Playback() 38 | this.container = new Container({ playback: this.playback }) 39 | this.core.setupContainers([this.container]) 40 | }) 41 | 42 | it('disables media control', () => { 43 | const containerStopSpy = sinon.spy(this.container, 'stop') 44 | 45 | this.errorScreen.onError(this.fakeError) 46 | 47 | expect(containerStopSpy).to.have.been.called 48 | }) 49 | 50 | it('stops media', () => { 51 | const containerDisableMediaControlSpy = sinon.spy(this.container, 'disableMediaControl') 52 | 53 | this.errorScreen.onError(this.fakeError) 54 | 55 | expect(containerDisableMediaControlSpy).to.have.been.called 56 | }) 57 | 58 | it('shows component', () => { 59 | const pluginShowSpy = sinon.spy(this.errorScreen, 'show') 60 | const pluginRenderSpy = sinon.spy(this.errorScreen, 'render') 61 | 62 | this.errorScreen.onError(this.fakeError) 63 | 64 | expect(pluginShowSpy).to.have.been.called 65 | expect(pluginRenderSpy).to.have.been.called 66 | }) 67 | 68 | it('bind method to reload player', () => { 69 | const pluginReloadSpy = sinon.spy(this.errorScreen, 'bindReload') 70 | 71 | this.errorScreen.onError(this.fakeError) 72 | 73 | expect(pluginReloadSpy).to.have.been.called 74 | }) 75 | 76 | describe('when reload is clicked', () => { 77 | it('loads media again', () => { 78 | this.core.load = sinon.spy() 79 | 80 | this.errorScreen.reload() 81 | 82 | expect(this.core.load).to.have.been.called 83 | }) 84 | 85 | it('plays when core is ready', () => { 86 | this.core.load = () => {} 87 | const playSpy = sinon.spy() 88 | this.core.getCurrentContainer = () => ({ play: playSpy }) 89 | 90 | this.errorScreen.reload() 91 | this.core.trigger(Events.CORE_READY) 92 | 93 | expect(this.errorScreen.container.play).to.have.been.called 94 | }) 95 | }) 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /src/plugins/error_screen/public/error_screen.html: -------------------------------------------------------------------------------- 1 |
2 | <% if (icon) { %> 3 |
<%= icon %>
4 | <% } %> 5 |
<%= title %>
6 |
<%= message %>
7 |
Error code: <%= code %>
8 |
<%= reloadIcon %>
9 |
10 | -------------------------------------------------------------------------------- /src/plugins/error_screen/public/error_screen.scss: -------------------------------------------------------------------------------- 1 | @import 'fontsmoothing'; 2 | 3 | 4 | [data-player] .player-error-screen { 5 | @include font-smoothing(antialiased); 6 | color: #CCCACA; 7 | position: absolute; 8 | top: 0; 9 | height: 100%; 10 | width: 100%; 11 | background-color: rgba(0, 0, 0, 0.7); 12 | z-index: 2000; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | 17 | &__content[data-error-screen] { 18 | font-size: 14px; 19 | color: #CCCACA; 20 | margin-top: 45px; 21 | } 22 | 23 | &__title[data-error-screen] { 24 | font-weight: bold; 25 | line-height: 30px; 26 | font-size: 18px; 27 | } 28 | 29 | &__message[data-error-screen] { 30 | width: 90%; 31 | margin: 0 auto; 32 | } 33 | 34 | &__code[data-error-screen] { 35 | font-size: 13px; 36 | margin-top: 15px; 37 | } 38 | 39 | &__reload { 40 | cursor: pointer; 41 | width: 30px; 42 | margin: 15px auto 0 !important; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/plugins/favicon.js: -------------------------------------------------------------------------------- 1 | import { CorePlugin, Events, $ } from '@clappr/core' 2 | 3 | import playIcon from '../icons/01-play.svg' 4 | import pauseIcon from '../icons/02-pause.svg' 5 | 6 | const oldIcon = $('link[rel="shortcut icon"]') 7 | 8 | export default class Favicon extends CorePlugin { 9 | get name() { return 'favicon' } 10 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 11 | get oldIcon() { return oldIcon } 12 | 13 | constructor(core) { 14 | super(core) 15 | this._container = null 16 | this.configure() 17 | } 18 | 19 | configure() { 20 | if (this.core.options.changeFavicon) { 21 | if (!this.enabled) { 22 | this.stopListening(this.core, Events.CORE_OPTIONS_CHANGE) 23 | this.enable() 24 | } 25 | } else if (this.enabled) { 26 | this.disable() 27 | this.listenTo(this.core, Events.CORE_OPTIONS_CHANGE, this.configure) 28 | } 29 | } 30 | 31 | bindEvents() { 32 | this.listenTo(this.core, Events.CORE_OPTIONS_CHANGE, this.configure) 33 | this.listenTo(this.core, Events.CORE_ACTIVE_CONTAINER_CHANGED, this.containerChanged) 34 | this.core.activeContainer && this.containerChanged() 35 | } 36 | 37 | containerChanged() { 38 | this._container && this.stopListening(this._container) 39 | this._container = this.core.activeContainer 40 | this.listenTo(this._container, Events.CONTAINER_PLAY, this.setPlayIcon) 41 | this.listenTo(this._container, Events.CONTAINER_PAUSE, this.setPauseIcon) 42 | this.listenTo(this._container, Events.CONTAINER_STOP, this.resetIcon) 43 | this.listenTo(this._container, Events.CONTAINER_ENDED, this.resetIcon) 44 | this.listenTo(this._container, Events.CONTAINER_ERROR, this.resetIcon) 45 | this.resetIcon() 46 | } 47 | 48 | disable() { 49 | super.disable() 50 | this.resetIcon() 51 | } 52 | 53 | destroy() { 54 | super.destroy() 55 | this.resetIcon() 56 | } 57 | 58 | createIcon(svg) { 59 | const canvas = $('') 60 | canvas[0].width = 16 61 | canvas[0].height = 16 62 | const ctx = canvas[0].getContext('2d') 63 | ctx.fillStyle = '#000' 64 | const d = $(svg).find('path').attr('d') 65 | const path = new Path2D(d) 66 | ctx.fill(path) 67 | const icon = $('') 68 | icon.attr('href', canvas[0].toDataURL('image/png')) 69 | return icon 70 | } 71 | 72 | setPlayIcon() { 73 | if (!this.playIcon) 74 | this.playIcon = this.createIcon(playIcon) 75 | 76 | this.changeIcon(this.playIcon) 77 | } 78 | 79 | setPauseIcon() { 80 | if (!this.pauseIcon) 81 | this.pauseIcon = this.createIcon(pauseIcon) 82 | 83 | this.changeIcon(this.pauseIcon) 84 | } 85 | 86 | resetIcon() { 87 | $('link[rel="shortcut icon"]').remove() 88 | $('head').append(this.oldIcon) 89 | } 90 | 91 | changeIcon(icon) { 92 | if (icon) { 93 | $('link[rel="shortcut icon"]').remove() 94 | $('head').append(icon) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/plugins/google_analytics/google_analytics.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Globo.com Player authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import { ContainerPlugin, Events } from '@clappr/core' 6 | 7 | export default class GoogleAnalytics extends ContainerPlugin { 8 | get name() { return 'google_analytics' } 9 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 10 | constructor(container) { 11 | super(container) 12 | if (this.container.options.gaAccount) { 13 | this.account = this.container.options.gaAccount 14 | this.trackerName = (this.container.options.gaTrackerName) ? this.container.options.gaTrackerName + '.' : 'Clappr.' 15 | this.domainName = this.container.options.gaDomainName 16 | this.currentHDState = undefined 17 | this.embedScript() 18 | } 19 | } 20 | 21 | embedScript() { 22 | if (!window._gat) { 23 | const script = document.createElement('script') 24 | script.setAttribute('type', 'text/javascript') 25 | script.setAttribute('async', 'async') 26 | script.setAttribute('src', '//www.google-analytics.com/ga.js') 27 | script.onload = () => this.addEventListeners() 28 | document.body.appendChild(script) 29 | } else { this.addEventListeners() } 30 | 31 | } 32 | 33 | addEventListeners() { 34 | if (this.container) { 35 | this.listenTo(this.container, Events.CONTAINER_READY, this.onReady) 36 | this.listenTo(this.container, Events.CONTAINER_PLAY, this.onPlay) 37 | this.listenTo(this.container, Events.CONTAINER_STOP, this.onStop) 38 | this.listenTo(this.container, Events.CONTAINER_PAUSE, this.onPause) 39 | this.listenTo(this.container, Events.CONTAINER_ENDED, this.onEnded) 40 | this.listenTo(this.container, Events.CONTAINER_STATE_BUFFERING, this.onBuffering) 41 | this.listenTo(this.container, Events.CONTAINER_STATE_BUFFERFULL, this.onBufferFull) 42 | this.listenTo(this.container, Events.CONTAINER_ERROR, this.onError) 43 | this.listenTo(this.container, Events.CONTAINER_PLAYBACKSTATE, this.onPlaybackChanged) 44 | this.listenTo(this.container, Events.CONTAINER_VOLUME, (event) => this.onVolumeChanged(event)) 45 | this.listenTo(this.container, Events.CONTAINER_SEEK, (event) => this.onSeek(event)) 46 | this.listenTo(this.container, Events.CONTAINER_FULL_SCREEN, this.onFullscreen) 47 | this.listenTo(this.container, Events.CONTAINER_HIGHDEFINITIONUPDATE, this.onHD) 48 | this.listenTo(this.container, Events.CONTAINER_PLAYBACKDVRSTATECHANGED, this.onDVR) 49 | } 50 | _gaq.push([this.trackerName + '_setAccount', this.account]) 51 | if (this.domainName) 52 | _gaq.push([this.trackerName + '_setDomainName', this.domainName]) 53 | } 54 | 55 | onReady() { 56 | this.push(['Video', 'Playback', this.container.playback.name]) 57 | } 58 | 59 | onPlay() { 60 | this.push(['Video', 'Play', this.container.playback.src]) 61 | } 62 | 63 | onStop() { 64 | this.push(['Video', 'Stop', this.container.playback.src]) 65 | } 66 | 67 | onEnded() { 68 | this.push(['Video', 'Ended', this.container.playback.src]) 69 | } 70 | 71 | onBuffering() { 72 | this.push(['Video', 'Buffering', this.container.playback.src]) 73 | } 74 | 75 | onBufferFull() { 76 | this.push(['Video', 'Bufferfull', this.container.playback.src]) 77 | } 78 | 79 | onError() { 80 | this.push(['Video', 'Error', this.container.playback.src]) 81 | } 82 | 83 | onHD(isHD) { 84 | const status = isHD ? 'ON': 'OFF' 85 | if (status !== this.currentHDState) { 86 | this.currentHDState = status 87 | this.push(['Video', 'HD - ' + status, this.container.playback.src]) 88 | } 89 | } 90 | 91 | onPlaybackChanged(playbackState) { 92 | if (playbackState.type !== null) 93 | this.push(['Video', 'Playback Type - ' + playbackState.type, this.container.playback.src]) 94 | 95 | } 96 | 97 | onDVR(dvrInUse) { 98 | const status = dvrInUse? 'ON': 'OFF' 99 | this.push(['Interaction', 'DVR - ' + status, this.container.playback.src]) 100 | } 101 | 102 | onPause() { 103 | this.push(['Video', 'Pause', this.container.playback.src]) 104 | } 105 | 106 | onSeek() { 107 | this.push(['Video', 'Seek', this.container.playback.src]) 108 | } 109 | 110 | onVolumeChanged() { 111 | this.push(['Interaction', 'Volume', this.container.playback.src]) 112 | } 113 | 114 | onFullscreen() { 115 | this.push(['Interaction', 'Fullscreen', this.container.playback.src]) 116 | } 117 | 118 | 119 | push(array) { 120 | const res = [this.trackerName + '_trackEvent'].concat(array) 121 | _gaq.push(res) 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/plugins/google_analytics/google_analytics.test.js: -------------------------------------------------------------------------------- 1 | import { Container, Playback } from '@clappr/core' 2 | 3 | import GoogleAnalytics from './google_analytics' 4 | 5 | const FakePlayback = Playback 6 | 7 | describe('GoogleAnalytics', function() { 8 | describe('constructor without gaAccount', function() { 9 | it('no trackerName by default', function() { 10 | const fakePlayback = new FakePlayback() 11 | const container = new Container({ playback: fakePlayback }) 12 | const gaControl = new GoogleAnalytics(container) 13 | 14 | expect(gaControl.trackerName).to.not.exist 15 | }) 16 | }) 17 | 18 | describe('constructor with gaAccount', function() { 19 | beforeEach(function() { 20 | window._gaq = [] 21 | }) 22 | it('trackerName equals to Clappr. by default', function() { 23 | const fakePlayback = new FakePlayback() 24 | const container = new Container({ playback: fakePlayback, gaAccount: 'UA-XXXXX-X' }) 25 | const gaControl = new GoogleAnalytics(container) 26 | 27 | expect(gaControl.trackerName).to.equal('Clappr.') 28 | }) 29 | it('tracks data with Clappr. as trackerName', function() { 30 | const fakePlayback = new FakePlayback() 31 | const container = new Container({ playback: fakePlayback, gaAccount: 'UA-XXXXX-X' }) 32 | const gaControl = new GoogleAnalytics(container) 33 | 34 | gaControl.push(['Video', 'Play', 'video.mp4']) 35 | 36 | expect(window._gaq[0][0]).to.equal('Clappr._trackEvent') 37 | expect(window._gaq[0][1]).to.equal('Video') 38 | expect(window._gaq[0][2]).to.equal('Play') 39 | expect(window._gaq[0][3]).to.equal('video.mp4') 40 | }) 41 | }) 42 | 43 | describe('constructor with gaAccount, gaDomainName and gaTrackerName', function() { 44 | beforeEach(function() { 45 | window._gat = {} 46 | window._gaq = [] 47 | }) 48 | it('trackerName equals to gaTrackerName parameter', function() { 49 | const fakePlayback = new FakePlayback() 50 | const options = { playback: fakePlayback, gaAccount: 'UA-XXXXX-X', gaTrackerName: 'MyPlayerInstance', gaDomainName: 'some.domain.com' } 51 | const container = new Container(options) 52 | const gaControl = new GoogleAnalytics(container) 53 | 54 | expect(gaControl.trackerName).to.equal('MyPlayerInstance.') 55 | }) 56 | it('sets the account to gaAccount value', function() { 57 | const fakePlayback = new FakePlayback() 58 | const options = { playback: fakePlayback, gaAccount: 'UA-XXXXX-X', gaTrackerName: 'MyPlayerInstance', gaDomainName: 'some.domain.com' } 59 | const container = new Container(options) 60 | const gaControl = new GoogleAnalytics(container) // eslint-disable-line no-unused-vars 61 | 62 | expect(window._gaq[0][0]).to.equal('MyPlayerInstance._setAccount') 63 | expect(window._gaq[0][1]).to.equal('UA-XXXXX-X') 64 | }) 65 | it('sets the domain name to gaDomainName value', function() { 66 | const fakePlayback = new FakePlayback() 67 | const options = { playback: fakePlayback, gaAccount: 'UA-XXXXX-X', gaTrackerName: 'MyPlayerInstance', gaDomainName: 'some.domain.com' } 68 | const container = new Container(options) 69 | const gaControl = new GoogleAnalytics(container) // eslint-disable-line no-unused-vars 70 | 71 | expect(window._gaq[1][0]).to.equal('MyPlayerInstance._setDomainName') 72 | expect(window._gaq[1][1]).to.equal('some.domain.com') 73 | }) 74 | it('tracks data with gaTrackerName parameter as trackerName', function() { 75 | const fakePlayback = new FakePlayback() 76 | const options = { playback: fakePlayback, gaAccount: 'UA-XXXXX-X', gaTrackerName: 'MyPlayerInstance', gaDomainName: 'some.domain.com' } 77 | const container = new Container(options) 78 | const gaControl = new GoogleAnalytics(container) 79 | gaControl.push(['Video', 'Play', 'video.mp4']) 80 | 81 | expect(window._gaq[2][0]).to.equal('MyPlayerInstance._trackEvent') 82 | expect(window._gaq[2][1]).to.equal('Video') 83 | expect(window._gaq[2][2]).to.equal('Play') 84 | expect(window._gaq[2][3]).to.equal('video.mp4') 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /src/plugins/media_control/media_control.test.js: -------------------------------------------------------------------------------- 1 | import { Container, Core, Events, Playback, Utils, template } from '@clappr/core' 2 | 3 | import MediaControl from './media_control' 4 | 5 | const { Config } = Utils 6 | 7 | describe('MediaControl', function() { 8 | beforeEach(function() { 9 | this.playback = new Playback() 10 | this.playback.getPlaybackType = function() { 11 | return Playback.VOD 12 | } 13 | this.container = new Container({ playback: this.playback }) 14 | this.core = new Core({ playerId: 0 }) 15 | this.mediaControl = new MediaControl(this.core) 16 | this.core.activeContainer = this.container 17 | localStorage.removeItem('clappr.localhost.volume') 18 | }) 19 | 20 | describe('#constructor', function() { 21 | it('can be built muted', function() { 22 | const container = new Container({ playback: this.playback, mute: true }) 23 | const mediaControl = new MediaControl(this.core) 24 | this.core.activeContainer = container 25 | expect(mediaControl.muted).to.be.equal(true) 26 | expect(mediaControl.volume).to.be.equal(0) 27 | }) 28 | 29 | it('restores saved volume', function() { 30 | Config.persist('volume', 42) 31 | Object.assign(this.core.options, { persistConfig: true }) 32 | const mediaControl = new MediaControl(this.core) 33 | expect(mediaControl.volume).to.be.equal(42) 34 | }) 35 | }) 36 | 37 | describe('#setVolume', function() { 38 | 39 | it('sets the volume', function() { 40 | sinon.spy(this.container, 'setVolume') 41 | sinon.spy(this.mediaControl, 'updateVolumeUI') 42 | 43 | this.mediaControl.setVolume(42) 44 | this.container.trigger(Events.CONTAINER_READY) 45 | 46 | expect(this.mediaControl.volume).to.be.equal(42) 47 | expect(this.mediaControl.muted).to.be.equal(false) 48 | expect(this.container.setVolume).to.have.been.called 49 | expect(this.mediaControl.updateVolumeUI).to.have.been.called 50 | }) 51 | 52 | it('limits volume to an integer between 0 and 100', function() { 53 | this.mediaControl.setVolume(1000) 54 | expect(this.mediaControl.volume).to.be.equal(100) 55 | 56 | this.mediaControl.setVolume(101) 57 | expect(this.mediaControl.volume).to.be.equal(100) 58 | 59 | this.mediaControl.setVolume(481) 60 | expect(this.mediaControl.volume).to.be.equal(100) 61 | 62 | this.mediaControl.setVolume(-1) 63 | expect(this.mediaControl.volume).to.be.equal(0) 64 | 65 | this.mediaControl.setVolume(0) 66 | expect(this.mediaControl.volume).to.be.equal(0) 67 | }) 68 | 69 | it('mutes when volume is 0 or less than 0', function() { 70 | this.mediaControl.setVolume(10) 71 | expect(this.mediaControl.muted).to.be.equal(false) 72 | 73 | this.mediaControl.setVolume(0) 74 | expect(this.mediaControl.muted).to.be.equal(true) 75 | }) 76 | 77 | it('persists volume when persistence is on', function() { 78 | // expected to be default value (100) 79 | expect(Config.restore('volume')).to.be.equal(100) 80 | 81 | Object.assign(this.core.options, { persistConfig: true }) 82 | const mediacontrol = new MediaControl(this.core) 83 | this.core.activeContainer = this.container 84 | mediacontrol.setVolume(78) 85 | 86 | expect(Config.restore('volume')).to.be.equal(78) 87 | }) 88 | 89 | it('reset volume after configure', function () { 90 | Object.assign(this.core.options, { persistConfig: true }) 91 | 92 | const container = new Container({ playback: this.playback, mute: true }) 93 | const mediacontrol = new MediaControl(this.core) 94 | 95 | this.core.activeContainer = container 96 | 97 | container.configure({ mute: false }) 98 | 99 | expect(mediacontrol.volume).to.be.equal(100) 100 | }) 101 | 102 | it('do not persist when is initial volume', function () { 103 | Config.persist = sinon.spy() 104 | 105 | Object.assign(this.core.options, { persistConfig: true }) 106 | 107 | const container = new Container({ playback: this.playback, mute: false }) 108 | 109 | new MediaControl(this.core) 110 | 111 | this.core.activeContainer = container 112 | 113 | Config.persist.should.not.have.been.called 114 | }) 115 | }) 116 | 117 | it('can appear when playback type is not NO_OP', function() { 118 | const mediaControl = new MediaControl(this.core) 119 | this.core.trigger(Events.CORE_ACTIVE_CONTAINER_CHANGED, this.container) 120 | mediaControl.enable() 121 | expect(mediaControl.$el.hasClass('media-control-hide')).to.be.false 122 | expect(mediaControl.disabled).to.be.false 123 | }) 124 | 125 | describe('never appears when', function() { 126 | it('playback type is NO_OP', function() { 127 | this.container.getPlaybackType = function() { 128 | return Playback.NO_OP 129 | } 130 | const mediaControl = new MediaControl(this.core) 131 | this.core.activeContainer = this.container 132 | mediaControl.render() 133 | mediaControl.enable() 134 | expect(mediaControl.$el.hasClass('media-control-hide')).to.be.true 135 | expect(mediaControl.disabled).to.be.true 136 | }) 137 | 138 | it('option chromeless has value true', function() { 139 | this.core.options.chromeless = true 140 | this.core.activeContainer = this.container 141 | const mediaControl = new MediaControl(this.core) 142 | this.core.trigger(Events.CORE_ACTIVE_CONTAINER_CHANGED, this.container) 143 | expect(mediaControl.$el.hasClass('media-control-hide')).to.be.true 144 | expect(mediaControl.disabled).to.be.true 145 | }) 146 | }) 147 | 148 | describe('custom media control', function() { 149 | it('can be extend the base mediacontrol with a custom template', function() { 150 | class MyMediaControl extends MediaControl { 151 | get template() { return template('
My HTML here
') } 152 | constructor(options) { super(options) } 153 | } 154 | 155 | const container = new Container({ playback: this.playback, mute: true }) 156 | const mediaControl = new MyMediaControl(this.core) 157 | 158 | this.core.activeContainer = container 159 | 160 | mediaControl.render() 161 | mediaControl.$el.find('.clappr-style').remove() 162 | expect(mediaControl.muted).to.be.equal(true) 163 | expect(mediaControl.volume).to.be.equal(0) 164 | expect(mediaControl.$el.html()).to.be.equal( 165 | '
My HTML here
' 166 | ) 167 | }) 168 | }) 169 | 170 | it('can be configured after its creation', function() { 171 | expect(this.mediaControl._options.hideMediaControl).to.be.undefined 172 | expect(this.mediaControl._options.mediacontrol).to.be.undefined 173 | 174 | this.core.configure({ hideMediaControl: false, mediacontrol: { seekbar: '#E113D3', buttons: '#66B2FF' } }) 175 | expect(this.mediaControl._options.hideMediaControl).to.be.false 176 | expect(this.mediaControl._options.mediacontrol).not.to.be.undefined 177 | 178 | this.core.configure({ hideMediaControl: true }) 179 | expect(this.mediaControl._options.hideMediaControl).to.be.true 180 | }) 181 | }) 182 | -------------------------------------------------------------------------------- /src/plugins/media_control/public/closed-hand.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clappr/clappr-plugins/ffaa9d27005fa5a8a7c243ffc47eb5655b84b371/src/plugins/media_control/public/closed-hand.cur -------------------------------------------------------------------------------- /src/plugins/media_control/public/media-control.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% var renderBar = function(name) { %> 4 |
> 5 |
> 6 |
>
7 |
>
8 |
>
9 |
10 |
> 11 |
>
12 |
13 |
14 | <% }; %> 15 | <% var renderSegmentedBar = function(name, segments) { 16 | segments = segments || 10; %> 17 |
> 18 | <% for (var i = 0; i < segments; i++) { %> 19 |
>
20 | <% } %> 21 |
22 | <% }; %> 23 | <% var renderDrawer = function(name, renderContent) { %> 24 |
> 25 |
> 26 |
>
27 | > 28 |
29 | <% renderContent(name); %> 30 |
31 | <% }; %> 32 | <% var renderIndicator = function(name) { %> 33 |
>
34 | <% }; %> 35 | <% var renderButton = function(name) { %> 36 | 37 | <% }; %> 38 | <% var templates = { 39 | bar: renderBar, 40 | segmentedBar: renderSegmentedBar, 41 | }; 42 | var render = function(settingsList) { 43 | settingsList.forEach(function(setting) { 44 | if(setting === "seekbar") { 45 | renderBar(setting); 46 | } else if (setting === "volume") { 47 | renderDrawer(setting, settings.volumeBarTemplate ? templates[settings.volumeBarTemplate] : function(name) { return renderSegmentedBar(name); }); 48 | } else if (setting === "duration" || setting === "position") { 49 | renderIndicator(setting); 50 | } else { 51 | renderButton(setting); 52 | } 53 | }); 54 | }; %> 55 | <% if (settings.default && settings.default.length) { %> 56 |
57 | <% render(settings.default); %> 58 |
59 | <% } %> 60 | <% if (settings.left && settings.left.length) { %> 61 |
62 | <% render(settings.left); %> 63 |
64 | <% } %> 65 | <% if (settings.right && settings.right.length) { %> 66 |
67 | <% render(settings.right); %> 68 |
69 | <% } %> 70 |
71 | -------------------------------------------------------------------------------- /src/plugins/media_control/public/media-control.scss: -------------------------------------------------------------------------------- 1 | .media-control-notransition { 2 | transition: none !important; 3 | } 4 | 5 | .media-control[data-media-control] { 6 | position: absolute; 7 | width: 100%; 8 | height: 100%; 9 | z-index: 9999; 10 | pointer-events: none; 11 | 12 | &.dragging { 13 | pointer-events: auto; 14 | cursor: -webkit-grabbing !important; 15 | cursor: grabbing !important; 16 | cursor: url("./closed-hand.cur"), move; 17 | * { 18 | cursor: -webkit-grabbing !important; 19 | cursor: grabbing !important; 20 | cursor: url("./closed-hand.cur"), move; 21 | } 22 | } 23 | 24 | .media-control-background[data-background] { 25 | position: absolute; 26 | height: 40%; 27 | width: 100%; 28 | bottom: 0; 29 | background: linear-gradient(transparent, rgba(0, 0, 0, 0.9)); 30 | will-change: transform, opacity; 31 | transition: opacity 0.6s ease-out; 32 | } 33 | 34 | .media-control-icon { 35 | line-height: 0; 36 | letter-spacing: 0; 37 | speak: none; 38 | color: #fff; 39 | opacity: 0.5; 40 | vertical-align: middle; 41 | text-align: left; 42 | transition: all 0.1s ease; 43 | } 44 | 45 | .media-control-icon:hover { 46 | color: white; 47 | opacity: 0.75; 48 | text-shadow: rgba(255,255,255,0.8) 0 0 5px; 49 | } 50 | 51 | &.media-control-hide { 52 | .media-control-background[data-background] { 53 | opacity: 0; 54 | } 55 | .media-control-layer[data-controls] { 56 | transform: translateY(50px); 57 | .bar-container[data-seekbar]{ 58 | .bar-scrubber[data-seekbar] { 59 | opacity: 0; 60 | } 61 | } 62 | } 63 | } 64 | 65 | .media-control-layer[data-controls] { 66 | position: absolute; 67 | transform: translateY(-7px); 68 | bottom: 0; 69 | width: 100%; 70 | height: 32px; 71 | font-size: 0; 72 | vertical-align: middle; 73 | pointer-events: auto; 74 | transition: bottom 0.4s ease-out; 75 | 76 | .media-control-left-panel[data-media-control] { 77 | position: absolute; 78 | top: 0; 79 | left: 4px; 80 | height: 100%; 81 | } 82 | 83 | .media-control-center-panel[data-media-control] { 84 | height: 100%; 85 | text-align: center; 86 | line-height: 32px; 87 | } 88 | 89 | .media-control-right-panel[data-media-control] { 90 | position: absolute; 91 | top: 0; 92 | right: 4px; 93 | height: 100%; 94 | } 95 | 96 | button.media-control-button { 97 | background-color: transparent; 98 | border: 0; 99 | margin: 0 6px; 100 | padding: 0; 101 | cursor: pointer; 102 | display: inline-block; 103 | width: 32px; 104 | height: 100%; 105 | 106 | svg { 107 | width: 100%; 108 | height: 22px; 109 | path { fill: white; } 110 | } 111 | 112 | &:focus { 113 | outline: none; 114 | } 115 | 116 | &[data-play] { 117 | float: left; 118 | height: 100%; 119 | } 120 | 121 | &[data-pause] { 122 | float: left; 123 | height: 100%; 124 | } 125 | 126 | &[data-stop] { 127 | float: left; 128 | height: 100%; 129 | } 130 | 131 | &[data-fullscreen] { 132 | float: right; 133 | background-color: transparent; 134 | border: 0; 135 | height: 100%; 136 | } 137 | 138 | &[data-hd-indicator] { 139 | background-color: transparent; 140 | border: 0; 141 | cursor: default; 142 | display: none; 143 | float: right; 144 | height: 100%; 145 | 146 | &.enabled { 147 | display: block; 148 | opacity: 1.0; 149 | 150 | &:hover { 151 | opacity: 1.0; 152 | text-shadow: none; 153 | } 154 | } 155 | } 156 | 157 | &[data-playpause] { 158 | float: left; 159 | } 160 | 161 | &[data-playstop] { 162 | float: left; 163 | } 164 | } 165 | 166 | .media-control-indicator { 167 | &[data-position], &[data-duration] { 168 | display: inline-block; 169 | font-size: 10px; 170 | color: white; 171 | cursor: default; 172 | line-height: 32px; 173 | position: relative; 174 | } 175 | &[data-position] { 176 | margin: 0 6px 0 7px; 177 | } 178 | &[data-duration] { 179 | &:before { content: "|"; margin-right: 7px; } 180 | color: rgba(255, 255, 255, 0.5); 181 | margin-right: 6px; 182 | } 183 | } 184 | 185 | .bar-container[data-seekbar] { 186 | position: absolute; 187 | top: -20px; 188 | left: 0; 189 | display: inline-block; 190 | vertical-align: middle; 191 | width: 100%; 192 | height: 25px; 193 | cursor: pointer; 194 | 195 | .bar-background[data-seekbar] { 196 | width: 100%; 197 | height: 1px; 198 | position: relative; 199 | top: 12px; 200 | background-color: #666666; 201 | 202 | .bar-fill-1[data-seekbar] { 203 | position: absolute; 204 | top: 0; 205 | left: 0; 206 | width: 0; 207 | height: 100%; 208 | background-color: #c2c2c2; 209 | transition: all 0.1s ease-out; 210 | } 211 | 212 | .bar-fill-2[data-seekbar] { 213 | position: absolute; 214 | top: 0; 215 | left: 0; 216 | width: 0; 217 | height: 100%; 218 | background-color: #005aff; 219 | transition: all 0.1s ease-out; 220 | } 221 | 222 | .bar-hover[data-seekbar] { 223 | opacity: 0; 224 | position: absolute; 225 | top: -3px; 226 | width: 5px; 227 | height: 7px; 228 | background-color: rgba(255,255,255,0.5); 229 | transition: opacity 0.1s ease; 230 | } 231 | } 232 | 233 | &:hover { 234 | .bar-background[data-seekbar] { 235 | .bar-hover[data-seekbar] { 236 | opacity: 1; 237 | } 238 | } 239 | } 240 | 241 | &.seek-disabled { 242 | cursor: default; 243 | &:hover { 244 | .bar-background[data-seekbar] { 245 | .bar-hover[data-seekbar] { 246 | opacity: 0; 247 | } 248 | } 249 | } 250 | } 251 | 252 | .bar-scrubber[data-seekbar] { 253 | position: absolute; 254 | transform: translateX(-50%); 255 | top: 2px; 256 | left: 0; 257 | width: 20px; 258 | height: 20px; 259 | opacity: 1; 260 | transition: all 0.1s ease-out; 261 | 262 | .bar-scrubber-icon[data-seekbar] { 263 | position: absolute; 264 | left: 6px; 265 | top: 6px; 266 | width: 8px; 267 | height: 8px; 268 | border-radius: 10px; 269 | box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.2); 270 | background-color: white; 271 | } 272 | } 273 | } 274 | 275 | .drawer-container[data-volume] { 276 | float: right; 277 | display: inline-block; 278 | height: 32px; 279 | cursor: pointer; 280 | margin: 0 6px; 281 | box-sizing: border-box; 282 | 283 | .drawer-icon-container[data-volume] { 284 | float: left; 285 | bottom: 0; 286 | 287 | .drawer-icon[data-volume] { 288 | background-color: transparent; 289 | border: 0; 290 | box-sizing: content-box; 291 | width: 32px; 292 | height: 32px; 293 | opacity: 0.5; 294 | &:hover { opacity: 0.75; } 295 | 296 | svg { 297 | height: 24px; 298 | position: relative; 299 | top: 3px; 300 | path { fill: white; } 301 | } 302 | 303 | &.muted svg { margin-left: 2px; } 304 | } 305 | } 306 | 307 | .bar-container[data-volume] { 308 | float: left; 309 | position: relative; 310 | overflow: hidden; 311 | top: 6px; 312 | width: 42px; 313 | height: 18px; 314 | padding: 3px 0; 315 | transition: width .2s ease-out; 316 | 317 | .bar-background[data-volume] { 318 | height: 1px; 319 | position: relative; 320 | top: 7px; 321 | margin: 0 3px; 322 | background-color: #666666; 323 | 324 | .bar-fill-1[data-volume] { 325 | position: absolute; 326 | top: 0; 327 | left: 0; 328 | width: 0; 329 | height: 100%; 330 | background-color: #c2c2c2; 331 | transition: all 0.1s ease-out; 332 | } 333 | 334 | .bar-fill-2[data-volume] { 335 | position: absolute; 336 | top: 0; 337 | left: 0; 338 | width: 0; 339 | height: 100%; 340 | background-color: #005aff; 341 | transition: all 0.1s ease-out; 342 | } 343 | 344 | .bar-hover[data-volume] { 345 | opacity: 0; 346 | position: absolute; 347 | top: -3px; 348 | width: 5px; 349 | height: 7px; 350 | background-color: rgba(255,255,255,0.5); 351 | transition: opacity 0.1s ease; 352 | } 353 | } 354 | 355 | .bar-scrubber[data-volume] { 356 | position: absolute; 357 | transform: translateX(-50%); 358 | top: 0px; 359 | left: 0; 360 | width: 20px; 361 | height: 20px; 362 | opacity: 1; 363 | transition: all 0.1s ease-out; 364 | 365 | .bar-scrubber-icon[data-volume] { 366 | position: absolute; 367 | left: 6px; 368 | top: 6px; 369 | width: 8px; 370 | height: 8px; 371 | border-radius: 10px; 372 | box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.2); 373 | background-color: white; 374 | } 375 | } 376 | 377 | .segmented-bar-element[data-volume] { 378 | float: left; 379 | width: 4px; 380 | padding-left: 2px; 381 | height: 12px; 382 | opacity: 0.5; 383 | box-shadow: inset 2px 0 0 white; 384 | transition: transform .2s ease-out; 385 | 386 | &.fill { 387 | box-shadow: inset 2px 0 0 #fff; 388 | opacity: 1; 389 | } 390 | 391 | &:nth-of-type(1) { 392 | padding-left: 0; 393 | } 394 | 395 | &:hover { 396 | transform: scaleY(1.5); 397 | } 398 | } 399 | } 400 | } 401 | } 402 | 403 | &.w320 { 404 | .media-control-layer[data-controls] { 405 | .drawer-container[data-volume] { 406 | .bar-container[data-volume] { 407 | &.volume-bar-hide { 408 | width: 0; 409 | height: 12px; 410 | top: 9px; 411 | padding: 0; 412 | } 413 | } 414 | } 415 | } 416 | } 417 | 418 | } 419 | -------------------------------------------------------------------------------- /src/plugins/poster/poster.js: -------------------------------------------------------------------------------- 1 | //Copyright 2014 Globo.com Player authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import { Events, Playback, PlayerError, UIContainerPlugin, template, Styler } from '@clappr/core' 6 | 7 | import posterHTML from './public/poster.html' 8 | import playIcon from '../../icons/01-play.svg' 9 | import posterStyle from './public/poster.scss' 10 | 11 | export default class PosterPlugin extends UIContainerPlugin { 12 | get name() { return 'poster' } 13 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 14 | get template() { return template(posterHTML) } 15 | get shouldRender() { 16 | const showForNoOp = !!(this.options.poster && this.options.poster.showForNoOp) 17 | return this.container.playback.name !== 'html_img' && (this.container.playback.getPlaybackType() !== Playback.NO_OP || showForNoOp) 18 | } 19 | 20 | get attributes() { 21 | return { 22 | 'class': 'player-poster', 23 | 'data-poster': '' 24 | } 25 | } 26 | 27 | get events() { 28 | return { 29 | 'click': 'clicked' 30 | } 31 | } 32 | 33 | get showOnVideoEnd() { 34 | return !this.options.poster || this.options.poster.showOnVideoEnd || this.options.poster.showOnVideoEnd === undefined 35 | } 36 | 37 | constructor(container) { 38 | super(container) 39 | this.hasStartedPlaying = false 40 | this.playRequested = false 41 | this.render() 42 | setTimeout(() => this.update(), 0) 43 | } 44 | 45 | bindEvents() { 46 | this.listenTo(this.container, Events.CONTAINER_STOP, this.onStop) 47 | this.listenTo(this.container, Events.CONTAINER_PLAY, this.onPlay) 48 | this.listenTo(this.container, Events.CONTAINER_STATE_BUFFERING, this.update) 49 | this.listenTo(this.container, Events.CONTAINER_STATE_BUFFERFULL, this.update) 50 | this.listenTo(this.container, Events.CONTAINER_OPTIONS_CHANGE, this.render) 51 | this.listenTo(this.container, Events.CONTAINER_ERROR, this.onError) 52 | this.showOnVideoEnd && this.listenTo(this.container, Events.CONTAINER_ENDED, this.onStop) 53 | } 54 | 55 | onError(error) { 56 | this.hasFatalError = error.level === PlayerError.Levels.FATAL 57 | 58 | if (this.hasFatalError) { 59 | this.hasStartedPlaying = false 60 | this.playRequested = false 61 | this.showPlayButton() 62 | } 63 | } 64 | 65 | onPlay() { 66 | this.hasStartedPlaying = true 67 | this.update() 68 | } 69 | 70 | onStop() { 71 | this.hasStartedPlaying = false 72 | this.playRequested = false 73 | this.update() 74 | } 75 | 76 | updatePlayButton(show) { 77 | if (show && (!this.options.chromeless || this.options.allowUserInteraction)) 78 | this.showPlayButton() 79 | else 80 | this.hidePlayButton() 81 | } 82 | 83 | showPlayButton() { 84 | if (this.hasFatalError && !this.options.disableErrorScreen) return 85 | 86 | this.$playButton.show() 87 | this.$el.addClass('clickable') 88 | } 89 | 90 | hidePlayButton() { 91 | this.$playButton.hide() 92 | this.$el.removeClass('clickable') 93 | } 94 | 95 | clicked() { 96 | // Let "click_to_pause" plugin handle click event if media has started playing 97 | if (! this.hasStartedPlaying) { 98 | if (!this.options.chromeless || this.options.allowUserInteraction) { 99 | this.playRequested = true 100 | this.update() 101 | this.container.playback && (this.container.playback._consented = true) 102 | this.container.play() 103 | } 104 | return false 105 | } 106 | } 107 | 108 | shouldHideOnPlay() { 109 | // Audio broadcasts should keep the poster up; video should hide poster while playing. 110 | return !this.container.playback.isAudioOnly 111 | } 112 | 113 | update() { 114 | if (!this.shouldRender) 115 | return 116 | 117 | let showPlayButton = !this.playRequested && !this.hasStartedPlaying && !this.container.buffering 118 | this.updatePlayButton(showPlayButton) 119 | this.updatePoster() 120 | } 121 | 122 | updatePoster() { 123 | if (!this.hasStartedPlaying) this.showPoster() 124 | else this.hidePoster() 125 | } 126 | 127 | showPoster() { 128 | this.container.disableMediaControl() 129 | this.$el.show() 130 | } 131 | 132 | hidePoster() { 133 | this.container.enableMediaControl() 134 | if (this.shouldHideOnPlay()) 135 | this.$el.hide() 136 | } 137 | 138 | render() { 139 | if (!this.shouldRender) 140 | return 141 | 142 | const style = Styler.getStyleFor(posterStyle, { baseUrl: this.options.baseUrl }) 143 | this.$el.html(this.template()) 144 | this.$el.append(style[0]) 145 | 146 | const isRegularPoster = this.options.poster && this.options.poster.custom === undefined 147 | 148 | if (isRegularPoster) { 149 | const posterUrl = this.options.poster.url || this.options.poster 150 | this.$el.css({ 'background-image': 'url(' + posterUrl + ')' }) 151 | this.removeVideoElementPoster() 152 | } else if (this.options.poster) { 153 | this.$el.css({ 'background': this.options.poster.custom }) 154 | this.removeVideoElementPoster() 155 | } 156 | 157 | this.container.$el.append(this.el) 158 | this.$playWrapper = this.$el.find('.play-wrapper') 159 | this.$playWrapper.append(playIcon) 160 | this.$playButton = this.$playWrapper.find('svg') 161 | this.$playButton.addClass('poster-icon') 162 | this.$playButton.attr('data-poster', '') 163 | 164 | let buttonsColor = this.options.mediacontrol && this.options.mediacontrol.buttons 165 | if (buttonsColor) 166 | this.$el.find('svg path').css('fill', buttonsColor) 167 | 168 | if (this.options.mediacontrol && this.options.mediacontrol.buttons) { 169 | buttonsColor = this.options.mediacontrol.buttons 170 | this.$playButton.css('color', buttonsColor) 171 | } 172 | this.update() 173 | return this 174 | } 175 | 176 | removeVideoElementPoster() { 177 | this.container.playback && 178 | this.container.playback.$el && 179 | this.container.playback.$el[0] && 180 | this.container.playback.$el[0].removeAttribute && 181 | this.container.playback.$el[0].removeAttribute('poster') 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/plugins/poster/poster.test.js: -------------------------------------------------------------------------------- 1 | import { Container, Events, Playback, $ } from '@clappr/core' 2 | 3 | import Poster from './poster' 4 | 5 | describe('Poster', function() { 6 | beforeEach(function() { 7 | this.playback = new Playback() 8 | this.playback.getPlaybackType = function() { 9 | return Playback.VOD 10 | } 11 | this.container = new Container({ playback: this.playback }) 12 | this.poster = new Poster(this.container) 13 | this.container.addPlugin(this.poster) 14 | }) 15 | 16 | it('is named poster', function() { 17 | expect(this.poster.name).to.equal('poster') 18 | }) 19 | 20 | it('disables media control by default', function() { 21 | expect(this.container.mediaControlDisabled).to.be.true 22 | }) 23 | 24 | it('renders if the playback type is not NO_OP', function() { 25 | expect(this.poster.shouldRender).to.be.true 26 | }) 27 | 28 | it('does not render if the playback type is NO_OP', function() { 29 | this.playback.getPlaybackType = function() { 30 | return Playback.NO_OP 31 | } 32 | expect(this.poster.shouldRender).to.be.false 33 | }) 34 | 35 | it('does not render if the playback name is html_img', function() { 36 | this.playback.name = 'html_img' 37 | expect(this.poster.shouldRender).to.be.false 38 | }) 39 | 40 | it('listens to container:stop event', function() { 41 | sinon.spy(this.container, 'disableMediaControl') 42 | sinon.spy(this.poster, 'showPlayButton') 43 | this.container.trigger(Events.CONTAINER_STOP) 44 | 45 | expect(this.container.disableMediaControl).to.have.been.calledOnce 46 | expect(this.poster.showPlayButton).to.have.been.calledOnce 47 | 48 | sinon.spy(this.poster, 'onStop') 49 | this.poster.bindEvents() 50 | 51 | this.container.trigger(Events.CONTAINER_STOP) 52 | 53 | expect(this.poster.onStop).to.have.been.calledOnce 54 | }) 55 | 56 | it('treats container:ended event as container:stop', function() { 57 | sinon.spy(this.container, 'disableMediaControl') 58 | sinon.spy(this.poster, 'showPlayButton') 59 | this.container.trigger(Events.CONTAINER_ENDED) 60 | 61 | expect(this.container.disableMediaControl).to.have.been.calledOnce 62 | expect(this.poster.showPlayButton).to.have.been.calledOnce 63 | 64 | const spy = sinon.spy(this.poster, 'onStop') 65 | this.poster.bindEvents() 66 | 67 | this.container.trigger(Events.CONTAINER_STOP) 68 | 69 | expect(spy).to.have.been.calledOnce 70 | }) 71 | 72 | it('disables handling container:ended event as container:stop', function() { 73 | this.container = new Container({ playback: this.playback, poster: { showOnVideoEnd: false } }) 74 | this.poster = new Poster(this.container) 75 | this.container.addPlugin(this.poster) 76 | sinon.spy(this.container, 'disableMediaControl') 77 | sinon.spy(this.poster, 'showPlayButton') 78 | this.container.trigger(Events.CONTAINER_ENDED) 79 | 80 | expect(this.container.disableMediaControl).to.not.have.been.called 81 | expect(this.poster.showPlayButton).to.not.have.been.called 82 | }) 83 | 84 | it('plays the container on click', function() { 85 | sinon.spy(this.container, 'play') 86 | $(this.poster.$el).click() 87 | expect(this.container.play).to.have.been.calledOnce 88 | }) 89 | 90 | it('keeps the poster up for audio only sources', function() { 91 | expect(this.poster.shouldHideOnPlay()).to.equal(true) 92 | Object.defineProperty(this.playback, 'isAudioOnly', { get: function () { return true } }) 93 | expect(this.poster.shouldHideOnPlay()).to.equal(false) 94 | }) 95 | 96 | it('renders custom background', function() { 97 | this.container = new Container({ 98 | playback: this.playback, 99 | poster: { custom: 'linear-gradient(rgb(238, 238, 238), rgb(153, 153, 153))' } 100 | }) 101 | this.poster = new Poster(this.container) 102 | this.container.addPlugin(this.poster) 103 | expect($(this.poster.$el).css('background')).include('linear-gradient(rgb(238, 238, 238), rgb(153, 153, 153))') 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /src/plugins/poster/public/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clappr/clappr-plugins/ffaa9d27005fa5a8a7c243ffc47eb5655b84b371/src/plugins/poster/public/default.png -------------------------------------------------------------------------------- /src/plugins/poster/public/poster.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/plugins/poster/public/poster.scss: -------------------------------------------------------------------------------- 1 | .player-poster[data-poster] { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | position: absolute; 6 | height: 100%; 7 | width: 100%; 8 | z-index: 998; 9 | top: 0; 10 | left: 0; 11 | background-color: transparent; 12 | background-size: cover; 13 | background-repeat: no-repeat; 14 | background-position: 50% 50%; 15 | 16 | &.clickable { cursor: pointer; } 17 | &:hover .play-wrapper[data-poster] { opacity: 1; } 18 | 19 | .play-wrapper[data-poster] { 20 | width: 100%; 21 | height: 25%; 22 | margin: 0 auto; 23 | opacity: 0.75; 24 | transition: opacity 0.1s ease; 25 | svg { 26 | height: 100%; 27 | path { fill: #fff; } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/plugins/seek_time/public/seek_time.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/plugins/seek_time/public/seek_time.scss: -------------------------------------------------------------------------------- 1 | .seek-time[data-seek-time] { 2 | position: absolute; 3 | white-space: nowrap; 4 | height: 20px; 5 | line-height: 20px; 6 | font-size: 0; 7 | left: -100%; 8 | bottom: 55px; 9 | background-color: rgba(2, 2, 2, 0.5); 10 | z-index: 9999; 11 | transition: opacity 0.1s ease; 12 | 13 | &.hidden[data-seek-time] { 14 | opacity: 0; 15 | } 16 | 17 | [data-seek-time] { 18 | display: inline-block; 19 | color: white; 20 | font-size: 10px; 21 | padding-left: 7px; 22 | padding-right: 7px; 23 | vertical-align: top; 24 | } 25 | 26 | [data-duration] { 27 | display: inline-block; 28 | color: rgba(255,255,255,.5); 29 | font-size: 10px; 30 | padding-right: 7px; 31 | vertical-align: top; 32 | &:before { content: "|"; margin-right: 7px; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/plugins/seek_time/seek_time.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Globo.com Player authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import { Events, Playback, UICorePlugin, Utils, template, Styler } from '@clappr/core' 6 | import seekTimeHTML from './public/seek_time.html' 7 | import seekTimeStyle from './public/seek_time.scss' 8 | 9 | const { formatTime } = Utils 10 | 11 | export default class SeekTime extends UICorePlugin { 12 | get name() { return 'seek_time' } 13 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 14 | get template() { 15 | return template(seekTimeHTML) 16 | } 17 | get attributes() { 18 | return { 19 | 'class': 'seek-time', 20 | 'data-seek-time': '' 21 | } 22 | } 23 | get mediaControl() { return this.core.mediaControl } 24 | get mediaControlContainer() { return this.mediaControl.container } 25 | get isLiveStreamWithDvr() { return this.mediaControlContainer && this.mediaControlContainer.getPlaybackType() === Playback.LIVE && this.mediaControlContainer.isDvrEnabled() } 26 | get durationShown() { return this.isLiveStreamWithDvr && !this.actualLiveTime } 27 | get useActualLiveTime() { return this.actualLiveTime && this.isLiveStreamWithDvr } 28 | constructor(core) { 29 | super(core) 30 | this.hoveringOverSeekBar = false 31 | this.hoverPosition = null 32 | this.duration = null 33 | this.firstFragDateTime = null 34 | this.actualLiveTime = !!this.mediaControl.options.actualLiveTime 35 | if (this.actualLiveTime) { 36 | if (this.mediaControl.options.actualLiveServerTime) 37 | this.actualLiveServerTimeDiff = new Date().getTime() - new Date(this.mediaControl.options.actualLiveServerTime).getTime() 38 | else 39 | this.actualLiveServerTimeDiff = 0 40 | } 41 | } 42 | 43 | bindEvents() { 44 | this.listenTo(this.mediaControl, Events.MEDIACONTROL_RENDERED, this.render) 45 | this.listenTo(this.mediaControl, Events.MEDIACONTROL_MOUSEMOVE_SEEKBAR, this.showTime) 46 | this.listenTo(this.mediaControl, Events.MEDIACONTROL_MOUSELEAVE_SEEKBAR, this.hideTime) 47 | this.listenTo(this.mediaControl, Events.MEDIACONTROL_CONTAINERCHANGED, this.onContainerChanged) 48 | if (this.mediaControlContainer) { 49 | this.listenTo(this.mediaControlContainer, Events.CONTAINER_PLAYBACKDVRSTATECHANGED, this.update) 50 | this.listenTo(this.mediaControlContainer, Events.CONTAINER_TIMEUPDATE, this.updateDuration) 51 | } 52 | } 53 | 54 | onContainerChanged() { 55 | this.stopListening() 56 | this.bindEvents() 57 | } 58 | 59 | updateDuration(timeProgress) { 60 | this.duration = timeProgress.total 61 | this.firstFragDateTime = timeProgress.firstFragDateTime 62 | this.update() 63 | } 64 | 65 | showTime(event) { 66 | this.hoveringOverSeekBar = true 67 | this.calculateHoverPosition(event) 68 | this.update() 69 | } 70 | 71 | hideTime() { 72 | this.hoveringOverSeekBar = false 73 | this.update() 74 | } 75 | 76 | calculateHoverPosition(event) { 77 | const offset = event.pageX - this.mediaControl.$seekBarContainer.offset().left 78 | // proportion into the seek bar that the mouse is hovered over 0-1 79 | this.hoverPosition = Math.min(1, Math.max(offset/this.mediaControl.$seekBarContainer.width(), 0)) 80 | } 81 | 82 | getSeekTime() { 83 | let seekTime, secondsSinceMidnight, d, e 84 | if (this.useActualLiveTime) { 85 | if (this.firstFragDateTime) { 86 | e = new Date(this.firstFragDateTime) 87 | d = new Date(this.firstFragDateTime) 88 | d.setHours(0,0,0,0) 89 | secondsSinceMidnight = ((e.getTime() - d.getTime()) / 1000) + this.duration 90 | } else { 91 | d = new Date(new Date().getTime() - this.actualLiveServerTimeDiff) 92 | e = new Date(d) 93 | secondsSinceMidnight = (e - d.setHours(0,0,0,0)) / 1000 94 | } 95 | seekTime = (secondsSinceMidnight - this.duration) + (this.hoverPosition * this.duration) 96 | if (seekTime < 0) 97 | seekTime += 86400 98 | 99 | } else { seekTime = this.hoverPosition * this.duration } 100 | 101 | return { seekTime, secondsSinceMidnight } 102 | } 103 | 104 | update() { 105 | if (!this.rendered) { 106 | // update() is always called after a render 107 | return 108 | } 109 | if (!this.shouldBeVisible()) { 110 | this.$el.hide() 111 | this.$el.css('left', '-100%') 112 | } else { 113 | const seekTime = this.getSeekTime() 114 | const currentSeekTime = formatTime(seekTime.seekTime, this.useActualLiveTime) 115 | // only update dom if necessary, ie time actually changed 116 | if (currentSeekTime !== this.displayedSeekTime) { 117 | this.$seekTimeEl.text(currentSeekTime) 118 | this.displayedSeekTime = currentSeekTime 119 | } 120 | 121 | if (this.durationShown) { 122 | this.$durationEl.show() 123 | const currentDuration = formatTime(this.actualLiveTime ? seekTime.secondsSinceMidnight : this.duration, this.actualLiveTime) 124 | if (currentDuration !== this.displayedDuration) { 125 | this.$durationEl.text(currentDuration) 126 | this.displayedDuration = currentDuration 127 | } 128 | } else { this.$durationEl.hide() } 129 | 130 | 131 | // the element must be unhidden before its width is requested, otherwise it's width will be reported as 0 132 | this.$el.show() 133 | const containerWidth = this.mediaControl.$seekBarContainer.width() 134 | const elWidth = this.$el.width() 135 | let elLeftPos = this.hoverPosition * containerWidth 136 | elLeftPos -= elWidth / 2 137 | elLeftPos = Math.max(0, Math.min(elLeftPos, containerWidth - elWidth)) 138 | this.$el.css('left', elLeftPos) 139 | } 140 | } 141 | 142 | shouldBeVisible() { 143 | return this.mediaControlContainer && this.mediaControlContainer.settings.seekEnabled && this.hoveringOverSeekBar && this.hoverPosition !== null && this.duration !== null 144 | } 145 | 146 | render() { 147 | const style = Styler.getStyleFor(seekTimeStyle, { baseUrl: this.options.baseUrl }) 148 | this.rendered = true 149 | this.displayedDuration = null 150 | this.displayedSeekTime = null 151 | this.$el.html(this.template()) 152 | this.$el.append(style[0]) 153 | this.$el.hide() 154 | this.mediaControl.$el.append(this.el) 155 | this.$seekTimeEl = this.$el.find('[data-seek-time]') 156 | this.$durationEl = this.$el.find('[data-duration]') 157 | this.$durationEl.hide() 158 | this.update() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/plugins/spinner_three_bounce/public/spinner.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/plugins/spinner_three_bounce/public/spinner.scss: -------------------------------------------------------------------------------- 1 | .spinner-three-bounce[data-spinner] { 2 | position: absolute; 3 | margin: 0 auto; 4 | width: 70px; 5 | text-align: center; 6 | z-index: 999; 7 | left: 0; 8 | right: 0; 9 | margin-left: auto; 10 | margin-right: auto; 11 | /* center vertically */ 12 | top: 50%; 13 | transform: translateY(-50%); 14 | 15 | &> div { 16 | width: 18px; 17 | height: 18px; 18 | background-color: #FFFFFF; 19 | border-radius: 100%; 20 | display: inline-block; 21 | animation: bouncedelay 1.4s infinite ease-in-out; 22 | /* Prevent first frame from flickering when animation starts */ 23 | animation-fill-mode: both; 24 | } 25 | 26 | [data-bounce1] { 27 | animation-delay: -0.32s; 28 | } 29 | 30 | [data-bounce2] { 31 | animation-delay: -0.16s; 32 | } 33 | } 34 | 35 | @keyframes bouncedelay { 36 | 0%, 80%, 100% { transform: scale(0.0); } 37 | 40% { transform: scale(1.0); } 38 | } 39 | -------------------------------------------------------------------------------- /src/plugins/spinner_three_bounce/spinner_three_bounce.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Globo.com Player authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import { Events, UIContainerPlugin, template, Styler } from '@clappr/core' 6 | 7 | import spinnerHTML from './public/spinner.html' 8 | import spinnerStyle from './public/spinner.scss' 9 | 10 | export default class SpinnerThreeBouncePlugin extends UIContainerPlugin { 11 | get name() { return 'spinner' } 12 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 13 | get attributes() { 14 | return { 15 | 'data-spinner':'', 16 | 'class': 'spinner-three-bounce' 17 | } 18 | } 19 | 20 | constructor(container) { 21 | super(container) 22 | this.template = template(spinnerHTML) 23 | this.showTimeout = null 24 | this.listenTo(this.container, Events.CONTAINER_STATE_BUFFERING, this.onBuffering) 25 | this.listenTo(this.container, Events.CONTAINER_STATE_BUFFERFULL, this.onBufferFull) 26 | this.listenTo(this.container, Events.CONTAINER_STOP, this.onStop) 27 | this.listenTo(this.container, Events.CONTAINER_ENDED, this.onStop) 28 | this.listenTo(this.container, Events.CONTAINER_ERROR, this.onStop) 29 | this.render() 30 | } 31 | 32 | onBuffering() { 33 | this.show() 34 | } 35 | 36 | onBufferFull() { 37 | this.hide() 38 | } 39 | 40 | onStop() { 41 | this.hide() 42 | } 43 | 44 | show() { 45 | if (this.showTimeout === null) 46 | this.showTimeout = setTimeout(() => this.$el.show(), 300) 47 | 48 | } 49 | 50 | hide() { 51 | if (this.showTimeout !== null) { 52 | clearTimeout(this.showTimeout) 53 | this.showTimeout = null 54 | } 55 | this.$el.hide() 56 | } 57 | 58 | render() { 59 | const style = Styler.getStyleFor(spinnerStyle, { baseUrl: this.options.baseUrl }) 60 | this.$el.html(this.template()) 61 | this.$el.append(style[0]) 62 | this.container.$el.append(this.$el) 63 | this.$el.hide() 64 | if (this.container.buffering) 65 | this.onBuffering() 66 | 67 | return this 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/plugins/stats/stats.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Globo.com Player authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import { ContainerPlugin, Events, $ } from '@clappr/core' 6 | 7 | export default class StatsPlugin extends ContainerPlugin { 8 | get name() { return 'stats' } 9 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 10 | 11 | constructor(container) { 12 | super(container) 13 | this.setInitialAttrs() 14 | this.reportInterval = this.options.reportInterval || 5000 15 | this.state = 'IDLE' 16 | } 17 | 18 | bindEvents() { 19 | this.listenTo(this.container.playback, Events.PLAYBACK_PLAY, this.onPlay) 20 | this.listenTo(this.container, Events.CONTAINER_STOP, this.onStop) 21 | this.listenTo(this.container, Events.CONTAINER_ENDED, this.onStop) 22 | this.listenTo(this.container, Events.CONTAINER_DESTROYED, this.onStop) 23 | this.listenTo(this.container, Events.CONTAINER_STATE_BUFFERING, this.onBuffering) 24 | this.listenTo(this.container, Events.CONTAINER_STATE_BUFFERFULL, this.onBufferFull) 25 | this.listenTo(this.container, Events.CONTAINER_STATS_ADD, this.onStatsAdd) 26 | this.listenTo(this.container, Events.CONTAINER_BITRATE, this.onStatsAdd) 27 | this.listenTo(this.container.playback, Events.PLAYBACK_STATS_ADD, this.onStatsAdd) 28 | } 29 | 30 | setInitialAttrs() { 31 | this.firstPlay = true 32 | this.startupTime = 0 33 | this.rebufferingTime = 0 34 | this.watchingTime = 0 35 | this.rebuffers = 0 36 | this.externalMetrics = {} 37 | } 38 | 39 | onPlay() { 40 | this.state = 'PLAYING' 41 | this.watchingTimeInit = Date.now() 42 | if (!this.intervalId) 43 | this.intervalId = setInterval(this.report.bind(this), this.reportInterval) 44 | 45 | } 46 | 47 | onStop() { 48 | clearInterval(this.intervalId) 49 | this.report() 50 | this.intervalId = undefined 51 | this.state = 'STOPPED' 52 | } 53 | 54 | onBuffering() { 55 | if (this.firstPlay) 56 | this.startupTimeInit = Date.now() 57 | else 58 | this.rebufferingTimeInit = Date.now() 59 | 60 | this.state = 'BUFFERING' 61 | this.rebuffers++ 62 | } 63 | 64 | onBufferFull() { 65 | if (this.firstPlay && this.startupTimeInit) { 66 | this.firstPlay = false 67 | this.startupTime = Date.now() - this.startupTimeInit 68 | this.watchingTimeInit = Date.now() 69 | } else if (this.rebufferingTimeInit) { this.rebufferingTime += this.getRebufferingTime() } 70 | 71 | this.rebufferingTimeInit = undefined 72 | this.state = 'PLAYING' 73 | } 74 | 75 | getRebufferingTime() { 76 | return Date.now() - this.rebufferingTimeInit 77 | } 78 | 79 | getWatchingTime() { 80 | const totalTime = (Date.now() - this.watchingTimeInit) 81 | return totalTime - this.rebufferingTime 82 | } 83 | 84 | isRebuffering() { 85 | return !!this.rebufferingTimeInit 86 | } 87 | 88 | onStatsAdd(metric) { 89 | $.extend(this.externalMetrics, metric) 90 | } 91 | 92 | getStats() { 93 | const metrics = { 94 | startupTime: this.startupTime, 95 | rebuffers: this.rebuffers, 96 | rebufferingTime: this.isRebuffering()? this.rebufferingTime + this.getRebufferingTime(): this.rebufferingTime, 97 | watchingTime: this.isRebuffering()? this.getWatchingTime() - this.getRebufferingTime(): this.getWatchingTime() 98 | } 99 | $.extend(metrics, this.externalMetrics) 100 | return metrics 101 | } 102 | 103 | report() { 104 | this.container.statsReport(this.getStats()) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/plugins/stats/stats.test.js: -------------------------------------------------------------------------------- 1 | import { Container, Events, Playback } from '@clappr/core' 2 | 3 | import Stats from './stats' 4 | 5 | const FakePlayback = Playback 6 | 7 | describe('StatsPlugin', function() { 8 | beforeEach(function() { 9 | this.playback = new FakePlayback() 10 | this.container = new Container({ playback: this.playback }) 11 | this.stats = new Stats(this.container) 12 | this.container.addPlugin(this.stats) 13 | this.clock = sinon.useFakeTimers(Date.now()) 14 | }) 15 | 16 | afterEach(function() { this.clock.restore() }) 17 | 18 | it('should calculate startup time', function() { 19 | this.container.onBuffering() 20 | this.clock.tick(1000) 21 | this.container.bufferfull() 22 | expect(this.stats.getStats().startupTime).to.equal(1000) 23 | }) 24 | 25 | it('should calculate rebuffer events', function() { 26 | // to maintain compatibility with the first ping version 27 | // we'll increment rebuffers even on the startup rebuffer event 28 | this.container.onBuffering() 29 | this.container.bufferfull() 30 | 31 | this.container.onBuffering() 32 | this.container.bufferfull() 33 | 34 | expect(this.stats.getStats().rebuffers).to.equal(2) 35 | }) 36 | 37 | it('should calculate total rebuffer time', function() { 38 | this.container.play() 39 | this.container.onBuffering() // startup time 40 | this.clock.tick(1000) 41 | this.container.bufferfull() 42 | 43 | this.container.onBuffering() 44 | this.clock.tick(1000) 45 | this.container.bufferfull() 46 | 47 | this.container.onBuffering() 48 | this.clock.tick(500) 49 | this.container.bufferfull() 50 | 51 | expect(this.stats.getStats().rebufferingTime).to.equal(1500) 52 | }) 53 | 54 | it('should avoid NaN on watching time and rebuffering time when more than one bufferfull is dispatched', function() { 55 | this.container.play() 56 | this.container.onBuffering() // startup time 57 | this.clock.tick(1000) 58 | this.container.bufferfull() 59 | this.container.bufferfull() 60 | 61 | this.clock.tick(2000) // watching for 2 secs 62 | expect(this.stats.getStats().watchingTime).to.equal(2000) 63 | expect(this.stats.getStats().rebufferingTime).to.equal(0) 64 | expect(this.stats.getStats().startupTime).to.equal(1000) 65 | }) 66 | 67 | it('should calculate total watching time', function() { 68 | this.container.play() 69 | this.container.onBuffering() // startup time 70 | this.clock.tick(1000) 71 | this.container.bufferfull() 72 | 73 | this.clock.tick(2000) // watching for 2 secs 74 | expect(this.stats.getStats().watchingTime).to.equal(2000) 75 | 76 | this.container.onBuffering() 77 | this.clock.tick(500) 78 | this.container.bufferfull() 79 | 80 | this.clock.tick(2000) // watching for 2 secs 81 | expect(this.stats.getStats().watchingTime).to.equal(4000) 82 | }) 83 | 84 | it('should consider current rebuffering state', function() { 85 | this.container.play() 86 | this.container.onBuffering() // startup time 87 | this.clock.tick(1000) 88 | this.container.bufferfull() 89 | 90 | this.container.onBuffering() 91 | this.clock.tick(1000) 92 | this.container.bufferfull() 93 | this.clock.tick(10000) 94 | 95 | this.container.onBuffering() 96 | this.clock.tick(500) 97 | // still rebuffering 98 | 99 | expect(this.stats.getStats().rebufferingTime).to.equal(1500) 100 | expect(this.stats.getStats().watchingTime).to.equal(10000) 101 | }) 102 | 103 | it('should announce statistics periodically', function() { 104 | sinon.spy(this.container, 'statsReport') 105 | this.container.reportInterval = 10 106 | 107 | const stats = new Stats(this.container) 108 | this.container.addPlugin(stats) 109 | this.playback.trigger(Events.PLAYBACK_PLAY) 110 | // clock.tick freezes when used with {set,clear}Interval and I don't know why 111 | setTimeout(function() { 112 | assert.ok(this.container.statsReport.calledTwice) 113 | this.container.restore() 114 | }, 20) 115 | }) 116 | 117 | }) 118 | -------------------------------------------------------------------------------- /src/plugins/watermark/public/watermark.html: -------------------------------------------------------------------------------- 1 |
> 2 | <% if(typeof imageLink !== 'undefined') { %> 3 | 4 | <% } %> 5 | 6 | <% if(typeof imageLink !== 'undefined') { %> 7 | 8 | <% } %> 9 |
10 | -------------------------------------------------------------------------------- /src/plugins/watermark/public/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clappr/clappr-plugins/ffaa9d27005fa5a8a7c243ffc47eb5655b84b371/src/plugins/watermark/public/watermark.png -------------------------------------------------------------------------------- /src/plugins/watermark/public/watermark.scss: -------------------------------------------------------------------------------- 1 | .clappr-watermark[data-watermark] { 2 | position: absolute; 3 | min-width: 70px; 4 | max-width: 200px; 5 | width: 12%; 6 | text-align: center; 7 | z-index: 10; 8 | } 9 | 10 | .clappr-watermark[data-watermark] a { 11 | outline: none; 12 | cursor: pointer; 13 | } 14 | 15 | .clappr-watermark[data-watermark] img { 16 | max-width: 100%; 17 | } 18 | 19 | .clappr-watermark[data-watermark-bottom-left] { 20 | bottom: 10px; 21 | left: 10px; 22 | } 23 | 24 | .clappr-watermark[data-watermark-bottom-right] { 25 | bottom: 10px; 26 | right: 42px; 27 | } 28 | 29 | .clappr-watermark[data-watermark-top-left] { 30 | top: 10px; 31 | left: 10px; 32 | } 33 | 34 | .clappr-watermark[data-watermark-top-right] { 35 | top: 10px; 36 | right: 37px; 37 | } 38 | -------------------------------------------------------------------------------- /src/plugins/watermark/watermark.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Globo.com Player authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | import { Events, UIContainerPlugin, template, Styler } from '@clappr/core' 6 | 7 | import watermarkHTML from './public/watermark.html' 8 | import watermarkStyle from './public/watermark.scss' 9 | 10 | export default class WaterMarkPlugin extends UIContainerPlugin { 11 | get name() { return 'watermark' } 12 | get supportedVersion() { return { min: CLAPPR_CORE_VERSION } } 13 | get template() { return template(watermarkHTML) } 14 | 15 | constructor(container) { 16 | super(container) 17 | this.configure() 18 | } 19 | 20 | bindEvents() { 21 | this.listenTo(this.container, Events.CONTAINER_PLAY, this.onPlay) 22 | this.listenTo(this.container, Events.CONTAINER_STOP, this.onStop) 23 | this.listenTo(this.container, Events.CONTAINER_OPTIONS_CHANGE, this.configure) 24 | } 25 | 26 | configure() { 27 | this.position = this.options.position || 'bottom-right' 28 | if (this.options.watermark) { 29 | this.imageUrl = this.options.watermark 30 | this.imageLink = this.options.watermarkLink 31 | this.render() 32 | } else { this.$el.remove() } 33 | 34 | } 35 | 36 | onPlay() { 37 | if (!this.hidden) 38 | this.$el.show() 39 | } 40 | 41 | onStop() { 42 | this.$el.hide() 43 | } 44 | 45 | render() { 46 | this.$el.hide() 47 | const style = Styler.getStyleFor(watermarkStyle, { baseUrl: this.options.baseUrl }) 48 | const templateOptions = { position: this.position, imageUrl: this.imageUrl, imageLink: this.imageLink } 49 | this.$el.html(this.template(templateOptions)) 50 | this.$el.append(style[0]) 51 | this.container.$el.append(this.$el) 52 | return this 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/public/Roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clappr/clappr-plugins/ffaa9d27005fa5a8a7c243ffc47eb5655b84b371/src/public/Roboto.ttf -------------------------------------------------------------------------------- /src/public/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Roboto"; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local("Roboto"), local("Roboto-Regular"), url("./Roboto.ttf") format("truetype"); 6 | } 7 | -------------------------------------------------------------------------------- /src/vendor/index.js: -------------------------------------------------------------------------------- 1 | import Kibo from './kibo' 2 | 3 | export { Kibo } 4 | export default { Kibo } 5 | -------------------------------------------------------------------------------- /src/vendor/kibo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // Kibo is released under the MIT License. Copyright (c) 2013 marquete. 3 | // see https://github.com/marquete/kibo 4 | 5 | var Kibo = function(element) { 6 | this.element = element || window.document; 7 | this.initialize(); 8 | }; 9 | 10 | Kibo.KEY_NAMES_BY_CODE = { 11 | 8: 'backspace', 9: 'tab', 13: 'enter', 12 | 16: 'shift', 17: 'ctrl', 18: 'alt', 13 | 20: 'caps_lock', 14 | 27: 'esc', 15 | 32: 'space', 16 | 37: 'left', 38: 'up', 39: 'right', 40: 'down', 17 | 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', 54: '6', 55: '7', 56: '8', 57: '9', 18 | 65: 'a', 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h', 73: 'i', 74: 'j', 19 | 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o', 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 20 | 85: 'u', 86: 'v', 87: 'w', 88: 'x', 89: 'y', 90: 'z', 112: 'f1', 113: 'f2', 114: 'f3', 21 | 115: 'f4', 116: 'f5', 117: 'f6', 118: 'f7', 119: 'f8', 120: 'f9', 121: 'f10', 122: 'f11', 123: 'f12' 22 | }; 23 | 24 | Kibo.KEY_CODES_BY_NAME = {}; 25 | (function() { 26 | for(var key in Kibo.KEY_NAMES_BY_CODE) { 27 | if(Object.prototype.hasOwnProperty.call(Kibo.KEY_NAMES_BY_CODE, key)) { 28 | Kibo.KEY_CODES_BY_NAME[Kibo.KEY_NAMES_BY_CODE[key]] = +key; 29 | } 30 | } 31 | })(); 32 | 33 | Kibo.MODIFIERS = ['shift', 'ctrl', 'alt']; 34 | 35 | Kibo.registerEvent = (function() { 36 | if(document.addEventListener) { 37 | return function(element, eventName, func) { 38 | element.addEventListener(eventName, func, false); 39 | }; 40 | } 41 | else if(document.attachEvent) { 42 | return function(element, eventName, func) { 43 | element.attachEvent('on' + eventName, func); 44 | }; 45 | } 46 | })(); 47 | 48 | Kibo.unregisterEvent = (function() { 49 | if(document.removeEventListener) { 50 | return function(element, eventName, func) { 51 | element.removeEventListener(eventName, func, false); 52 | }; 53 | } 54 | else if(document.detachEvent) { 55 | return function(element, eventName, func) { 56 | element.detachEvent('on' + eventName, func); 57 | }; 58 | } 59 | })(); 60 | 61 | Kibo.stringContains = function(string, substring) { 62 | return string.indexOf(substring) !== -1; 63 | }; 64 | 65 | Kibo.neatString = function(string) { 66 | return string.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' '); 67 | }; 68 | 69 | Kibo.capitalize = function(string) { 70 | return string.toLowerCase().replace(/^./, function(match) { return match.toUpperCase(); }); 71 | }; 72 | 73 | Kibo.isString = function(what) { 74 | return Kibo.stringContains(Object.prototype.toString.call(what), 'String'); 75 | }; 76 | 77 | Kibo.arrayIncludes = (function() { 78 | if(Array.prototype.indexOf) { 79 | return function(haystack, needle) { 80 | return haystack.indexOf(needle) !== -1; 81 | }; 82 | } 83 | else { 84 | return function(haystack, needle) { 85 | for(var i = 0; i < haystack.length; i++) { 86 | if(haystack[i] === needle) { 87 | return true; 88 | } 89 | } 90 | return false; 91 | }; 92 | } 93 | })(); 94 | 95 | Kibo.extractModifiers = function(keyCombination) { 96 | var modifiers, i 97 | modifiers = []; 98 | for(i = 0; i < Kibo.MODIFIERS.length; i++) { 99 | if(Kibo.stringContains(keyCombination, Kibo.MODIFIERS[i])) { 100 | modifiers.push(Kibo.MODIFIERS[i]); 101 | } 102 | } 103 | return modifiers; 104 | } 105 | 106 | Kibo.extractKey = function(keyCombination) { 107 | var keys, i; 108 | keys = Kibo.neatString(keyCombination).split(' '); 109 | for(i = 0; i < keys.length; i++) { 110 | if(!Kibo.arrayIncludes(Kibo.MODIFIERS, keys[i])) { 111 | return keys[i]; 112 | } 113 | } 114 | }; 115 | 116 | Kibo.modifiersAndKey = function(keyCombination) { 117 | var result, key; 118 | 119 | if(Kibo.stringContains(keyCombination, 'any')) { 120 | return Kibo.neatString(keyCombination).split(' ').slice(0, 2).join(' '); 121 | } 122 | 123 | result = Kibo.extractModifiers(keyCombination); 124 | 125 | key = Kibo.extractKey(keyCombination); 126 | if(key && !Kibo.arrayIncludes(Kibo.MODIFIERS, key)) { 127 | result.push(key); 128 | } 129 | 130 | return result.join(' '); 131 | } 132 | 133 | Kibo.keyName = function(keyCode) { 134 | return Kibo.KEY_NAMES_BY_CODE[keyCode + '']; 135 | }; 136 | 137 | Kibo.keyCode = function(keyName) { 138 | return +Kibo.KEY_CODES_BY_NAME[keyName]; 139 | }; 140 | 141 | Kibo.prototype.initialize = function() { 142 | var i, that = this; 143 | 144 | this.lastKeyCode = -1; 145 | this.lastModifiers = {}; 146 | for(i = 0; i < Kibo.MODIFIERS.length; i++) { 147 | this.lastModifiers[Kibo.MODIFIERS[i]] = false; 148 | } 149 | 150 | this.keysDown = { any: [] }; 151 | this.keysUp = { any: [] }; 152 | this.downHandler = this.handler('down'); 153 | this.upHandler = this.handler('up'); 154 | 155 | Kibo.registerEvent(this.element, 'keydown', this.downHandler); 156 | Kibo.registerEvent(this.element, 'keyup', this.upHandler); 157 | Kibo.registerEvent(window, 'unload', function unloader() { 158 | Kibo.unregisterEvent(that.element, 'keydown', that.downHandler); 159 | Kibo.unregisterEvent(that.element, 'keyup', that.upHandler); 160 | Kibo.unregisterEvent(window, 'unload', unloader); 161 | }); 162 | }; 163 | 164 | Kibo.prototype.handler = function(upOrDown) { 165 | var that = this; 166 | return function(e) { 167 | var i, registeredKeys, lastModifiersAndKey; 168 | 169 | e = e || window.event; 170 | 171 | that.lastKeyCode = e.keyCode; 172 | for(i = 0; i < Kibo.MODIFIERS.length; i++) { 173 | that.lastModifiers[Kibo.MODIFIERS[i]] = e[Kibo.MODIFIERS[i] + 'Key']; 174 | } 175 | if(Kibo.arrayIncludes(Kibo.MODIFIERS, Kibo.keyName(that.lastKeyCode))) { 176 | that.lastModifiers[Kibo.keyName(that.lastKeyCode)] = true; 177 | } 178 | 179 | registeredKeys = that['keys' + Kibo.capitalize(upOrDown)]; 180 | 181 | for(i = 0; i < registeredKeys.any.length; i++) { 182 | if((registeredKeys.any[i](e) === false) && e.preventDefault) { 183 | e.preventDefault(); 184 | } 185 | } 186 | 187 | lastModifiersAndKey = that.lastModifiersAndKey(); 188 | if(registeredKeys[lastModifiersAndKey]) { 189 | for(i = 0; i < registeredKeys[lastModifiersAndKey].length; i++) { 190 | if((registeredKeys[lastModifiersAndKey][i](e) === false) && e.preventDefault) { 191 | e.preventDefault(); 192 | } 193 | } 194 | } 195 | }; 196 | }; 197 | 198 | Kibo.prototype.registerKeys = function(upOrDown, newKeys, func) { 199 | var i, keys, registeredKeys = this['keys' + Kibo.capitalize(upOrDown)]; 200 | 201 | if(Kibo.isString(newKeys)) { 202 | newKeys = [newKeys]; 203 | } 204 | 205 | for(i = 0; i < newKeys.length; i++) { 206 | keys = newKeys[i]; 207 | keys = Kibo.modifiersAndKey(keys + ''); 208 | 209 | if(registeredKeys[keys]) { 210 | registeredKeys[keys].push(func); 211 | } else { 212 | registeredKeys[keys] = [func]; 213 | } 214 | } 215 | 216 | return this; 217 | }; 218 | 219 | // jshint maxdepth:5 220 | Kibo.prototype.unregisterKeys = function(upOrDown, newKeys, func) { 221 | var i, j, keys, registeredKeys = this['keys' + Kibo.capitalize(upOrDown)]; 222 | 223 | if(Kibo.isString(newKeys)) { 224 | newKeys = [newKeys]; 225 | } 226 | 227 | for(i = 0; i < newKeys.length; i++) { 228 | keys = newKeys[i]; 229 | keys = Kibo.modifiersAndKey(keys + ''); 230 | 231 | if(func === null) { 232 | delete registeredKeys[keys]; 233 | } else { 234 | if(registeredKeys[keys]) { 235 | for(j = 0; j < registeredKeys[keys].length; j++) { 236 | if(String(registeredKeys[keys][j]) === String(func)) { 237 | registeredKeys[keys].splice(j, 1); 238 | break; 239 | } 240 | } 241 | } 242 | } 243 | } 244 | 245 | return this; 246 | }; 247 | 248 | Kibo.prototype.off = function(keys) { 249 | return this.unregisterKeys('down', keys, null); 250 | } 251 | 252 | Kibo.prototype.delegate = function(upOrDown, keys, func) { 253 | return (func !== null || func !== undefined) ? this.registerKeys(upOrDown, keys, func) : this.unregisterKeys(upOrDown, keys, func); 254 | }; 255 | 256 | Kibo.prototype.down = function(keys, func) { 257 | return this.delegate('down', keys, func); 258 | }; 259 | 260 | Kibo.prototype.up = function(keys, func) { 261 | return this.delegate('up', keys, func); 262 | }; 263 | 264 | Kibo.prototype.lastKey = function(modifier) { 265 | if(!modifier) { 266 | return Kibo.keyName(this.lastKeyCode); 267 | } 268 | 269 | return this.lastModifiers[modifier]; 270 | }; 271 | 272 | Kibo.prototype.lastModifiersAndKey = function() { 273 | var result, i; 274 | 275 | result = []; 276 | for(i = 0; i < Kibo.MODIFIERS.length; i++) { 277 | if(this.lastKey(Kibo.MODIFIERS[i])) { 278 | result.push(Kibo.MODIFIERS[i]); 279 | } 280 | } 281 | 282 | if(!Kibo.arrayIncludes(result, this.lastKey())) { 283 | result.push(this.lastKey()); 284 | } 285 | 286 | return result.join(' '); 287 | }; 288 | 289 | export default Kibo 290 | --------------------------------------------------------------------------------