├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── compass.rb ├── docs ├── adaptive-streaming.md ├── api.md ├── developers │ ├── basics.md │ ├── coding-standards.md │ ├── compilation.md │ ├── i18n.md │ ├── installation.md │ ├── prerequisites.md │ └── unit-tests.md ├── examples.md ├── getting-started.md ├── images │ ├── browsers │ │ ├── chrome.gif │ │ └── firefox.gif │ └── screenshots │ │ └── player.png ├── index.md └── prerequisites.md ├── fonts └── openveo-player-icons │ ├── openveo-player-icons.svg │ ├── openveo-player-icons.ttf │ └── openveo-player-icons.woff ├── karmaConf.js ├── mkdocs.yml ├── package-lock.json ├── package.json ├── scripts ├── .eslintrc.json └── build.js └── src ├── .eslintrc.json ├── components ├── Player.service.js ├── Player.service.spec.js ├── html.html ├── player.component.js ├── player.component.spec.js ├── player.controller.js ├── player.html ├── player.scss ├── players │ ├── .eslintrc.json │ ├── HTMLPlayer.factory.js │ ├── HTMLPlayer.factory.spec.js │ ├── Player.factory.js │ ├── VimeoPlayer.factory.js │ ├── VimeoPlayer.factory.spec.js │ └── YoutubePlayer.factory.js ├── preview │ ├── preview.component.js │ ├── preview.component.spec.js │ ├── preview.controller.js │ ├── preview.html │ └── preview.scss ├── shared │ ├── button │ │ ├── button.component.js │ │ ├── button.component.spec.js │ │ ├── button.controller.js │ │ ├── button.html │ │ └── button.scss │ ├── dom.service.js │ ├── events.service.js │ ├── i18n │ │ ├── i18n.filter.js │ │ ├── i18n.service.js │ │ └── i18n.service.spec.js │ ├── millisecondsToTime.filter.js │ ├── millisecondsToTime.filter.spec.js │ ├── scroller │ │ ├── scroller.component.js │ │ ├── scroller.component.spec.js │ │ ├── scroller.controller.js │ │ ├── scroller.html │ │ └── scroller.scss │ ├── settings │ │ ├── settings.component.js │ │ ├── settings.component.spec.js │ │ ├── settings.controller.js │ │ ├── settings.html │ │ └── settings.scss │ ├── slider │ │ ├── slider.component.js │ │ ├── slider.component.spec.js │ │ ├── slider.controller.js │ │ ├── slider.html │ │ └── slider.scss │ ├── tabs │ │ ├── tabs.component.js │ │ ├── tabs.component.spec.js │ │ ├── tabs.controller.js │ │ ├── tabs.controller.spec.js │ │ ├── tabs.html │ │ └── tabs.scss │ ├── templateSelector │ │ ├── templateSelector.component.js │ │ ├── templateSelector.component.spec.js │ │ ├── templateSelector.controller.js │ │ ├── templateSelector.html │ │ └── templateSelector.scss │ ├── toggleIconButton │ │ ├── toggleIconButton.component.js │ │ ├── toggleIconButton.component.spec.js │ │ ├── toggleIconButton.controller.js │ │ ├── toggleIconButton.html │ │ └── toggleIconButton.scss │ ├── view │ │ ├── view.component.js │ │ ├── view.component.spec.js │ │ ├── view.controller.js │ │ └── view.html │ └── volume │ │ ├── volume.component.js │ │ ├── volume.component.spec.js │ │ ├── volume.controller.js │ │ ├── volume.html │ │ └── volume.scss ├── tile │ ├── tile.component.js │ ├── tile.component.spec.js │ ├── tile.controller.js │ ├── tile.html │ └── tile.scss ├── tiles │ ├── tiles.component.js │ ├── tiles.component.spec.js │ ├── tiles.controller.js │ ├── tiles.html │ └── tiles.scss ├── vimeo.html └── youtube.html ├── index.module.js └── index.scss /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | // Make this configuration file as the root one 4 | "root": true, 5 | 6 | // Extends eslint recommended rules 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:node/recommended" 10 | ], 11 | 12 | "env": { 13 | 14 | // Add browser environment globals 15 | "browser": true, 16 | 17 | // Add node.js environment globals 18 | "node": true, 19 | 20 | // Enable ECMAScript 2017 features 21 | "es2017": true 22 | 23 | }, 24 | "globals": { 25 | 26 | // Add angular to environment globals but do not authorize to overwrite it 27 | "angular": false 28 | 29 | }, 30 | "rules": { 31 | 32 | // Disallow unused vars but not unused arguments 33 | "no-unused-vars": [2, {"vars": "local", "args": "none"}], 34 | 35 | // Treat var as block scoped 36 | "block-scoped-var": 2, 37 | 38 | // Require default case in switch statements 39 | "default-case": 2, 40 | 41 | // Disallow use of alert 42 | "no-alert": 2, 43 | 44 | // Disallow use of caller/callee 45 | "no-caller": 2, 46 | 47 | // Disallow null comparisons 48 | "no-eq-null": 2, 49 | 50 | // Disallow eval() 51 | "no-eval": 2, 52 | 53 | // Disallow adding to native types 54 | "no-extend-native": 2, 55 | 56 | // Disallow unnecessary function binding 57 | "no-extra-bind": 2, 58 | 59 | // Disallow floating decimals 60 | "no-floating-decimal": 2, 61 | 62 | // Disallow the type conversion with shorter notations 63 | "no-implicit-coercion": 2, 64 | 65 | // Disallow implied eval 66 | "no-implied-eval": 2, 67 | 68 | // Disallow Iterator 69 | "no-iterator": 2, 70 | 71 | // Disallow labeled statements 72 | "no-labels": 2, 73 | 74 | // Disallow unnecessary nested blocks 75 | "no-lone-blocks": 2, 76 | 77 | // Disallow multiple spaces 78 | "no-multi-spaces": 2, 79 | 80 | // Disallow multiline strings 81 | "no-multi-str": 2, 82 | 83 | // Disallow reassignment of native objects 84 | "no-native-reassign": 2, 85 | 86 | // Disallow function constructor 87 | "no-new-func": 2, 88 | 89 | // Disallow octal escapes 90 | "no-octal-escape": 2, 91 | 92 | // Disallow octal literals 93 | "no-octal": 2, 94 | 95 | // Disallow use of __proto__ 96 | "no-proto": 2, 97 | 98 | // Disallow script URLs 99 | "no-script-url": 2, 100 | 101 | // Disallow self compare 102 | "no-self-compare": 2, 103 | 104 | // Restrict what can be thrown as an exception 105 | "no-throw-literal": 2, 106 | 107 | // Disallow unused expressions 108 | "no-unused-expressions": [2, {"allowShortCircuit": true, "allowTernary": true}], 109 | 110 | // Disallow unnecessary .call() and .apply() 111 | "no-useless-call": 2, 112 | 113 | // Disallow unncessary concatenation of strings 114 | "no-useless-concat": 2, 115 | 116 | // No with statements 117 | "no-with": 2, 118 | 119 | // Strict mode 120 | "strict": [2, "global"], 121 | 122 | // Disallow shadowing of variables inside of catch 123 | "no-catch-shadow": 2, 124 | 125 | // Disallow variables deletion 126 | "no-delete-var": 2, 127 | 128 | // Disallow shadowing of restricted names 129 | "no-shadow-restricted-names": 2, 130 | 131 | // Disallow early use 132 | "no-use-before-define": 2, 133 | 134 | // Disallow spaces inside of brackets 135 | "array-bracket-spacing": 2, 136 | 137 | // Disallow spaces inside of single line blocks 138 | "block-spacing": [2, "always"], 139 | 140 | // Require brace style 141 | "brace-style": [2, "1tbs", { "allowSingleLine": false }], 142 | 143 | // Require camelcase 144 | "camelcase": [2, {"properties": "always"}], 145 | 146 | // Enforces spacing around commas 147 | "comma-spacing": [2, {"before": false, "after": true}], 148 | 149 | // Comma style 150 | "comma-style": [2, "last"], 151 | 152 | // Disallow spaces inside of computed properties 153 | "computed-property-spacing": [2, "never"], 154 | 155 | // Require consistent this 156 | "consistent-this": [2, "self"], 157 | 158 | // Require file to end with single newline 159 | "eol-last" : 2, 160 | 161 | // Validate Indentation 162 | "indent": [2, 2, {"SwitchCase": 1, "VariableDeclarator": { "var": 1, "let": 1, "const": 1}}], 163 | 164 | // Enforce property spacing 165 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 166 | 167 | // Enforce empty lines around comments 168 | "lines-around-comment": [2, { "beforeBlockComment": true, "beforeLineComment": true, "allowBlockStart": true, "allowBlockEnd": true }], 169 | 170 | // Require constructors to use initial caps 171 | "new-cap": [2, {"newIsCap": true, "capIsNew": false}], 172 | 173 | // Require parens for constructors 174 | "new-parens": 2, 175 | 176 | // Disallow if as the only statement in an else block 177 | "no-lonely-if": 2, 178 | 179 | // Disallow if as the only statement in an else block 180 | "no-mixed-spaces-and-tabs": 2, 181 | 182 | // Disallows multiple blank lines 183 | "no-multiple-empty-lines": [2, {"max": 2}], 184 | 185 | // Disallow spaces in function calls 186 | "no-spaced-func": 2, 187 | 188 | // Disallow trailing spaces at the end of lines 189 | "no-trailing-spaces": 2, 190 | 191 | // Disallow spaces inside of curly braces in objects. 192 | "object-curly-spacing": 2, 193 | 194 | // Operator linebreak 195 | "operator-linebreak": [2, "after"], 196 | 197 | // Quoting style for property names 198 | "quote-props": [2, "as-needed"], 199 | 200 | // Enforce quote style 201 | "quotes": [2, "single"], 202 | 203 | // Require JSDoc comment 204 | "require-jsdoc": 2, 205 | 206 | // Enforce spacing before and after semicolons 207 | "semi-spacing": 2, 208 | 209 | // Enforce semicolons 210 | "semi": 2, 211 | 212 | // Require spaces following keywords 213 | "keyword-spacing": 2, 214 | 215 | // Require or disallow space before blocks 216 | "space-before-blocks": 2, 217 | 218 | // Disallow a space before function parenthesis 219 | "space-before-function-paren": [2, "never"], 220 | 221 | // Disallow spaces inside of parentheses 222 | "space-in-parens": 2, 223 | 224 | // Require spaces around infix operators 225 | "space-infix-ops": 2, 226 | 227 | // Require or disallow spaces before/after unary operators 228 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 229 | 230 | // Requires or disallows a whitespace (space or tab) beginning a comment 231 | "spaced-comment": [2, "always"], 232 | 233 | // Limit Maximum Length of Line 234 | "max-len": [2, 120, 2, {"ignoreUrls": true}], 235 | 236 | // Disallow mixed requires 237 | "node/no-mixed-requires": 2, 238 | 239 | // Disallow new require 240 | "node/no-new-require": 2, 241 | 242 | // Disallow string concatenation when using _dirname and _filename 243 | "node/no-path-concat": 2, 244 | 245 | // Disallow process.exit() 246 | "node/no-process-exit": 2, 247 | 248 | // Disallow synchronous methods 249 | "node/no-sync": 2 250 | 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Linux 2 | .directory 3 | *~ 4 | .*.swp 5 | 6 | # OS X 7 | .DS_Store* 8 | Icon? 9 | ._* 10 | 11 | # Windows 12 | Thumbs.db 13 | Desktop.ini 14 | ehthumbs.db 15 | 16 | # Project npm dependencies 17 | /node_modules 18 | 19 | # Build 20 | .sass-cache 21 | /dist 22 | /build 23 | 24 | # IDE 25 | nbproject 26 | 27 | # Documentation 28 | /site 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Linux 2 | # *.swp is already ignored by npm 3 | .directory 4 | *~ 5 | 6 | # OS X 7 | # .DS_Store* and ._* are already ignored by npm 8 | Icon? 9 | 10 | # Windows 11 | Thumbs.db 12 | Desktop.ini 13 | ehthumbs.db 14 | 15 | # Project npm dependencies 16 | /node_modules 17 | 18 | # IDE 19 | nbproject 20 | 21 | # Sources 22 | /src 23 | 24 | # Sass cache files 25 | .sass-cache 26 | /compass.rb 27 | 28 | # Git 29 | .gitignore 30 | .gitattributes 31 | 32 | # Unit tests 33 | /karmaConf.js 34 | 35 | # Documentation 36 | /site 37 | /docs 38 | /mkdocs.yml 39 | 40 | # Eslint 41 | .eslintrc.json 42 | 43 | # Build 44 | /build 45 | 46 | # Source maps 47 | /dist/js 48 | /dist/scss 49 | /dist/*.map 50 | /dist/*.scss 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | group: edge 4 | language: node_js 5 | node_js: 6 | - "7" 7 | addons: 8 | chrome: stable 9 | branches: 10 | only: 11 | - master 12 | - develop 13 | git: 14 | depth: 3 15 | notifications: 16 | email: 17 | recipients: 18 | - platform@veo-labs.com 19 | on_success: always 20 | on_failure: always 21 | before_install: 22 | - rvm install ruby-latest 23 | - npm install -g grunt-cli 24 | - gem install sass 25 | - gem install compass 26 | - google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost & 27 | install: 28 | - npm install --ignore-scripts 29 | - grunt dist --production 30 | before_script: 31 | - "export DISPLAY=:99.0" 32 | - "sh -e /etc/init.d/xvfb start" 33 | script: 34 | - grunt test 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenVeo Player 2 | 3 | OpenVeo Player is an [AngularJS](https://angularjs.org/) directive to wrap an HTML5(Video.js), Youtube or Vimeo player aiming to offer images synchronization, chapters and multi-videos synchronization. 4 | 5 | # Documentation 6 | 7 | Documentation is available on [Github pages](http://veo-labs.github.io/openveo-player/8.0.0/index.html). 8 | 9 | # Contributors 10 | 11 | Maintainer: [Veo-Labs](http://www.veo-labs.com/) 12 | 13 | # License 14 | 15 | [AGPL](http://www.gnu.org/licenses/agpl-3.0.en.html) 16 | -------------------------------------------------------------------------------- /compass.rb: -------------------------------------------------------------------------------- 1 | environment = :development 2 | output_style = :nested 3 | sourcemap = true 4 | sass_options = {:precision => 10} 5 | -------------------------------------------------------------------------------- /docs/adaptive-streaming.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | OpenVeo Player support Adaptive Streaming DASH and HLS. It will automaticaly switch between protocols according browser capabilities. 4 | 5 | # Prerequisites 6 | 7 | OpenVeo Player embeds Video.js to display the HTML player. Video.js natively supports HLS but in order to support DASH, you have to install and import dependencies: 8 | 9 | Install Dash.js: 10 | 11 | npm install dashjs@{DASH_JS_VERSION} 12 | 13 | Install videojs-contrib-dash plugin: 14 | 15 | npm install videojs-contrib-dash@{CONTRIB_DASH_VERSION} 16 | 17 | And import dependencies to use adaptive sources: 18 | ```html 19 | 20 | 21 | ``` 22 | 23 | # How to play adaptive sources 24 | You need to define your adaptive sources by setting their mimetype and their link. 25 | ```javascript 26 | $scope.data.sources = [ 27 | { 28 | adaptive: [ // The list of video adaptive sources (only for "html" player) 29 | { // Dash source 30 | height: 720, 31 | mimeType: 'application/dash+xml', 32 | link: 'https://host.local/openveo/mp4:bunny.mp4/manifest.mpd' 33 | }, 34 | { // HLS Source 35 | height: 720, 36 | mimeType: 'application/vnd.apple.mpegurl', 37 | link: 'https://host.local/openveo/mp4:bunny.mp4/manifest.m3u8' 38 | }, 39 | { // RTMP source 40 | mimeType: 'rtmp/mp4', 41 | link: 'rtmp://host.local/openveo/&mp4:bunny.mp4' 42 | } 43 | ], 44 | files : [ // The list of different resolutions sources for this video (only for "html" player) 45 | { 46 | width : 640, // Video width for this file 47 | height : 360, // Video height for this file 48 | link : 'https://host.local/pathToSmallMP4.mp4' // Video url 49 | }, 50 | { 51 | width : 1280, // Video width for this file 52 | height : 720, // Video height for this file 53 | link : 'https://host.local/pathToHDMP4.mp4' // Video url 54 | }, 55 | ... 56 | ] 57 | } 58 | ] 59 | ``` 60 | 61 | And set you player type to 'html': 62 | ```html 63 | 68 | ``` 69 | 70 | **NB**: "Adaptive" sources are always prioritized. "files" sources will be ignored if "adaptive" property is defined. 71 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Player can be controlled by methods and emits catchable events on the player HTML element. 4 | 5 | # Methods 6 | 7 | ## selectTemplate(template) 8 | 9 | Sets the display template. 10 | 11 | Usage: 12 | 13 | ```javascript 14 | var myPlayer = document.getElementById('myPlayer'); 15 | 16 | angular.element(myPlayer).on('ready', function(event){ 17 | console.log('ready'); 18 | 19 | var playerController = angular.element(myPlayer).controller('oplPlayer'); 20 | playerController.selectTemplate('split_2'); 21 | }); 22 | ``` 23 | 24 | Arguments: 25 | 26 | Param | Type | Details 27 | ----- | ---- | ---- 28 | template | String | Display template (can be either **split_1**, **split_50_50**, **split_25_75** or **split_2**) 29 | 30 | ## playPause() 31 | 32 | Starts / Pauses the player. 33 | 34 | Usage: 35 | 36 | ```javascript 37 | var myPlayer = document.getElementById('myPlayer'); 38 | 39 | angular.element(myPlayer).on('ready', function(event){ 40 | console.log('ready'); 41 | 42 | var playerController = angular.element(myPlayer).controller('oplPlayer'); 43 | playerController.playPause(); 44 | }); 45 | ``` 46 | 47 | ## setVolume(volume) 48 | 49 | Sets the player volume. 50 | 51 | Usage: 52 | 53 | ```javascript 54 | var myPlayer = document.getElementById('myPlayer'); 55 | 56 | angular.element(myPlayer).on('ready', function(event){ 57 | console.log('ready'); 58 | 59 | var playerController = angular.element(myPlayer).controller('oplPlayer'); 60 | playerController.setVolume(50); 61 | }); 62 | ``` 63 | 64 | Arguments: 65 | 66 | Param | Type | Details 67 | ----- | ---- | ---- 68 | volume | Number | The volume to set from 0 to 100 69 | 70 | ## setTime(time) 71 | 72 | Sets the player time. 73 | 74 | Usage: 75 | 76 | ```javascript 77 | var myPlayer = document.getElementById('myPlayer'); 78 | 79 | angular.element(myPlayer).on('ready', function(event){ 80 | console.log('ready'); 81 | 82 | var playerController = angular.element(myPlayer).controller('oplPlayer'); 83 | playerController.setTime(50000); 84 | }); 85 | ``` 86 | 87 | Arguments: 88 | 89 | Param | Type | Details 90 | ----- | ---- | ---- 91 | time | Number | The time to set (in milliseconds) relative to the cut media 92 | 93 | ## setDefinition(definition) 94 | 95 | Sets actual media definition. 96 | 97 | Usage: 98 | 99 | ```javascript 100 | var myPlayer = document.getElementById('myPlayer'); 101 | 102 | angular.element(myPlayer).on('ready', function(event){ 103 | console.log('ready'); 104 | 105 | var playerController = angular.element(myPlayer).controller('oplPlayer'); 106 | playerController.setDefinition('720'); 107 | }); 108 | ``` 109 | 110 | Arguments: 111 | 112 | Param | Type | Details 113 | ----- | ---- | ---- 114 | definition | String | The definition height as String 115 | 116 | ## setSource 117 | 118 | Sets actual media source if multi sources. 119 | 120 | Usage: 121 | 122 | ```javascript 123 | var myPlayer = document.getElementById('myPlayer'); 124 | 125 | angular.element(myPlayer).on('ready', function(event){ 126 | console.log('ready'); 127 | 128 | var playerController = angular.element(myPlayer).controller('oplPlayer'); 129 | playerController.setSource(1); 130 | }); 131 | ``` 132 | 133 | Arguments: 134 | 135 | Param | Type | Details 136 | ----- | ---- | ---- 137 | source | Number | The index of the source to load from the list of sources 138 | 139 | # Events 140 | 141 | ## ready 142 | 143 | The player is ready to receive actions. 144 | 145 | ```javascript 146 | var myPlayer = document.getElementById('myPlayer'); 147 | 148 | angular.element(myPlayer).on('ready', function(event){ 149 | console.log('ready'); 150 | }); 151 | ``` 152 | 153 | ## waiting 154 | 155 | Media playback has stopped because the next frame is not available. 156 | 157 | ```javascript 158 | var myPlayer = document.getElementById('myPlayer'); 159 | 160 | angular.element(myPlayer).on('waiting', function(event){ 161 | console.log('waiting'); 162 | }); 163 | ``` 164 | 165 | ## playing 166 | 167 | Media playback is ready to start after being paused or delayed due to lack of media data. 168 | 169 | ```javascript 170 | var myPlayer = document.getElementById('myPlayer'); 171 | 172 | angular.element(myPlayer).on('playing', function(event){ 173 | console.log('playing'); 174 | }); 175 | ``` 176 | 177 | ## durationChange 178 | 179 | The duration attribute has just been updated. 180 | 181 | ```javascript 182 | var myPlayer = document.getElementById('myPlayer'); 183 | 184 | angular.element(myPlayer).on('durationChange', function(event, duration){ 185 | console.log('durationChange with new duration = ' + duration + 'ms'); 186 | }); 187 | ``` 188 | 189 | ## play 190 | 191 | Media is no longer paused. 192 | 193 | ```javascript 194 | var myPlayer = document.getElementById('myPlayer'); 195 | 196 | angular.element(myPlayer).on('play', function(event, duration){ 197 | console.log('play'); 198 | }); 199 | ``` 200 | 201 | ## pause 202 | 203 | Media has been paused. 204 | 205 | ```javascript 206 | var myPlayer = document.getElementById('myPlayer'); 207 | 208 | angular.element(myPlayer).on('pause', function(event, duration){ 209 | console.log('pause'); 210 | }); 211 | ``` 212 | 213 | ## loadProgress 214 | 215 | Got buffering information. 216 | 217 | ```javascript 218 | var myPlayer = document.getElementById('myPlayer'); 219 | 220 | angular.element(myPlayer).on('loadProgress', function(event, percents){ 221 | console.log('loadProgress'); 222 | console.log('Buffering start = ' + percents.loadedStart); 223 | console.log('Buffering end = ' + percents.loadedPercent); 224 | }); 225 | ``` 226 | 227 | ## playProgress 228 | 229 | Media playback position has changed. 230 | 231 | ```javascript 232 | var myPlayer = document.getElementById('myPlayer'); 233 | 234 | angular.element(myPlayer).on('playProgress', function(event, data){ 235 | console.log('playProgress'); 236 | console.log('Current time = ' + data.time + 'ms'); 237 | console.log('Played percent = ' + data.percent); 238 | }); 239 | ``` 240 | 241 | ## end 242 | 243 | Media playback has reached the end. 244 | 245 | ```javascript 246 | var myPlayer = document.getElementById('myPlayer'); 247 | 248 | angular.element(myPlayer).on('end', function(event, duration){ 249 | console.log('end'); 250 | }); 251 | ``` 252 | 253 | ## error 254 | 255 | Player has encountered an error. 256 | 257 | ```javascript 258 | var myPlayer = document.getElementById('myPlayer'); 259 | 260 | angular.element(myPlayer).on('error', function(event, error){ 261 | console.log(error.message); 262 | console.log(error.code); 263 | }); 264 | ``` 265 | 266 | ## needPoiConversion 267 | 268 | Player has detected the old format of chapters / tags / indexes. Time of chapters / tags and indexes have to be expressed in milliseconds and not in percentage. 269 | 270 | ```javascript 271 | var myPlayer = document.getElementById('myPlayer'); 272 | 273 | angular.element(myPlayer).on('needPoiConversion', function(event, duration){ 274 | console.log('needPoiConversion'); 275 | console.log('Video duration = ' + duration + 'ms'); 276 | }); 277 | ``` 278 | -------------------------------------------------------------------------------- /docs/developers/basics.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | OpenVeo Player defines an opl-player component with several sub components: 4 | 5 | - **opl-preview** Used to display the index preview when cursor is over the timebar 6 | - **opl-slider** Used to display the timebar 7 | - **opl-volume** Used to display the volume controller (makes use of the opl-slider) 8 | - **opl-toggle-icon-button** Used for all toggle buttons 9 | - **opl-template-selector** Used to select a display template 10 | - **opl-settings** Used to set current quality and source 11 | - **opl-tabs** and **opl-view** Used to display points of interest as tabs 12 | - **opl-tiles** and **opl-tile** Used to display the list of tags, the list of chapters and the list of indexes 13 | 14 | # Players 15 | 16 | Each player (HTML, Youtube, Vimeo) as its own implementation and associated template. 17 | -------------------------------------------------------------------------------- /docs/developers/coding-standards.md: -------------------------------------------------------------------------------- 1 | OpenVeo Player uses Node.js coding standards. [ESLint](http://eslint.org/) is used to validate coding rules. You can launch a code verification using the following command: 2 | 3 | npm run lint 4 | -------------------------------------------------------------------------------- /docs/developers/compilation.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | OpenVeo player is written using AngularJS and SASS / Compass. SASS files need to be compiled to generate the CSS and JavaScript files can be minified and aggregated for better performance. 4 | 5 | # Compiling the OpenVeo Player for development 6 | 7 | You can compile sources for development using: 8 | 9 | npm run build:development 10 | 11 | You can also automatically compile sources for development on file change using: 12 | 13 | npm run watch 14 | 15 | # Compiling the OpenVeo Player for production 16 | 17 | You can compile sources for production using: 18 | 19 | npm run build 20 | -------------------------------------------------------------------------------- /docs/developers/i18n.md: -------------------------------------------------------------------------------- 1 | OpenVeo Player translations are defined in ov.player module constant **oplI18nTranslations**. -------------------------------------------------------------------------------- /docs/developers/installation.md: -------------------------------------------------------------------------------- 1 | # Clone project from git 2 | 3 | git clone git@github.com:veo-labs/openveo-player.git 4 | 5 | # Install project's dependencies 6 | 7 | cd openveo-player 8 | npm ci 9 | -------------------------------------------------------------------------------- /docs/developers/prerequisites.md: -------------------------------------------------------------------------------- 1 | OpenVeo Player requires additional elements for development: 2 | 3 | - [Git](http://git-scm.com/) - openveo-player is versioned with git 4 | - [Ruby](https://www.ruby-lang.org/en/) / [Sass](http://sass-lang.com/) / [Compass](http://compass-style.org/) - CSS is written using SASS / Compass 5 | - [Karma](http://karma-runner.github.io/1.0/index.html) - Unit tested are performed using karma 6 | -------------------------------------------------------------------------------- /docs/developers/unit-tests.md: -------------------------------------------------------------------------------- 1 | Unit tests are performed using [Karma](http://karma-runner.github.io/1.0/index.html). You can launch unit tests with the following command: 2 | 3 | npm run test 4 | -------------------------------------------------------------------------------- /docs/images/browsers/chrome.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veo-labs/openveo-player/e94d6a0fb76c1da0163fb066e89f180254ee08f6/docs/images/browsers/chrome.gif -------------------------------------------------------------------------------- /docs/images/browsers/firefox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veo-labs/openveo-player/e94d6a0fb76c1da0163fb066e89f180254ee08f6/docs/images/browsers/firefox.gif -------------------------------------------------------------------------------- /docs/images/screenshots/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veo-labs/openveo-player/e94d6a0fb76c1da0163fb066e89f180254ee08f6/docs/images/screenshots/player.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # What's OpenVeo Player? 2 | 3 | OpenVeo Player is an [AngularJS](https://angularjs.org/) component to wrap an HTML5, Youtube or Vimeo player aiming to offer images synchronization and points of interest (chapters, tags). 4 | 5 | ## Compatibility 6 | 7 | OpenVeo Player has been tested on the following browsers: 8 | 9 | - Google Chrome 10 | - Mozilla Firefox 11 | 12 | ![Firefox](images/browsers/firefox.gif) 13 | ![Google Chrome](images/browsers/chrome.gif) 14 | 15 | ## Screenshots 16 | 17 | ![Player](images/screenshots/player.png) 18 | -------------------------------------------------------------------------------- /docs/prerequisites.md: -------------------------------------------------------------------------------- 1 | OpenVeo Player requires and has been tested with: 2 | 3 | - [AngularJS](https://angularjs.org/) (**>=1.5.11**) 4 | - [AngularJS cookies](https://angularjs.org/) (**>=1.5.11**) 5 | 6 | To play video with HTML5 player, OpenVeo requires: 7 | 8 | - [Video.js](http://videojs.com/) (**=7.\***) 9 | -------------------------------------------------------------------------------- /fonts/openveo-player-icons/openveo-player-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veo-labs/openveo-player/e94d6a0fb76c1da0163fb066e89f180254ee08f6/fonts/openveo-player-icons/openveo-player-icons.ttf -------------------------------------------------------------------------------- /fonts/openveo-player-icons/openveo-player-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veo-labs/openveo-player/e94d6a0fb76c1da0163fb066e89f180254ee08f6/fonts/openveo-player-icons/openveo-player-icons.woff -------------------------------------------------------------------------------- /karmaConf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | /** 6 | * Resolves an NPM module path. 7 | * 8 | * @param {String} npmModule The module to resolve 9 | * @return {String} The NPM module absolute path 10 | */ 11 | function resolveModulePath(npmModule) { 12 | return path.dirname(require.resolve(path.join(npmModule, 'package.json'))); 13 | } 14 | 15 | // Karma configuration 16 | module.exports = function(config) { 17 | var resources = require('./build/ng-components-files.json'); 18 | var files = []; 19 | 20 | // Libraries 21 | files.push(path.join(resolveModulePath('angular'), 'angular.min.js')); 22 | files.push(path.join(resolveModulePath('angular-animate'), 'angular-animate.min.js')); 23 | files.push(path.join(resolveModulePath('angular-cookies'), 'angular-cookies.min.js')); 24 | files.push(path.join(resolveModulePath('angular-route'), 'angular-route.min.js')); 25 | files.push(path.join(resolveModulePath('angular-mocks'), 'angular-mocks.js')); 26 | files.push(path.join(resolveModulePath('video.js'), 'dist/video.min.js')); 27 | files.push(path.join(resolveModulePath('chai-spies'), 'chai-spies.js')); 28 | 29 | // Sources 30 | resources.js.forEach(function(file) { 31 | files.push(file); 32 | }); 33 | 34 | // Templates 35 | files.push('src/**/*.html'); 36 | 37 | // Tests 38 | files.push('src/**/*.spec.js'); 39 | 40 | config.set({ 41 | 42 | // Use mocha and chai for tests 43 | frameworks: ['mocha', 'chai'], 44 | 45 | // Web server port 46 | port: 9876, 47 | 48 | // Disable colors in the output (reporters and logs) 49 | colors: true, 50 | 51 | // Level of logging 52 | // Possible values: OFF || ERROR || WARN || INFO || DEBUG 53 | logLevel: 'INFO', 54 | 55 | // Disable watching files and executing tests whenever any file changes 56 | autoWatch: false, 57 | 58 | // List of browsers to execute tests on 59 | browsers: ['ChromeHeadlessCI'], 60 | 61 | // Configure custom ChromHeadlessCI as an extension of ChromeHeadlessCI without sandbox 62 | customLaunchers: { 63 | ChromeHeadlessCI: { 64 | base: 'ChromeHeadless', 65 | flags: ['--no-sandbox'] 66 | } 67 | }, 68 | 69 | // Continuous Integration mode 70 | // if true, Karma captures browsers, runs the tests and exits 71 | singleRun: true, 72 | 73 | // Base path that will be used to resolve all patterns (eg. files, exclude) 74 | basePath: '', 75 | 76 | // Plugins to load 77 | plugins: [ 78 | 'karma-chai', 79 | 'karma-mocha', 80 | 'karma-chrome-launcher', 81 | 'karma-ng-html2js-preprocessor' 82 | ], 83 | 84 | // HTML templates mock 85 | ngHtml2JsPreprocessor: { 86 | moduleName: 'templates', 87 | cacheIdFromPath: function(filepath) { 88 | return filepath.replace(/^(.*\/)(.*)$/, function(match, match1, match2) { 89 | return 'opl-' + match2; 90 | }); 91 | } 92 | }, 93 | 94 | // Files to preprocess 95 | preprocessors: { 96 | 'src/**/*.html': ['ng-html2js'] 97 | }, 98 | 99 | // List of files / patterns to load in the browser 100 | files: files 101 | 102 | }); 103 | 104 | }; 105 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Openveo Player 2 | repo_url: https://github.com/veo-labs/openveo-player 3 | theme: readthedocs 4 | markdown_extensions: 5 | - fenced_code 6 | - tables 7 | nav: 8 | - 'INTRODUCTION': 'index.md' 9 | - 'USERS': 10 | - 'Prerequisites': 'prerequisites.md' 11 | - 'Getting started': 'getting-started.md' 12 | - 'Adaptive streaming': 'adaptive-streaming.md' 13 | - 'API': 'api.md' 14 | - 'Examples': 'examples.md' 15 | - 'DEVELOPERS': 16 | - 'The basics': 'developers/basics.md' 17 | - 'Prerequisites': 'developers/prerequisites.md' 18 | - 'Installation': 'developers/installation.md' 19 | - 'Compilation': 'developers/compilation.md' 20 | - 'I18N and I10N': 'developers/i18n.md' 21 | - 'Coding standards': 'developers/coding-standards.md' 22 | - 'Unit tests': 'developers/unit-tests.md' 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openveo/player", 3 | "version": "8.0.0", 4 | "description": "OpenVeo player to play a video with associated images and chapters", 5 | "keywords": [ 6 | "openveo", 7 | "veo-labs", 8 | "player", 9 | "vimeo", 10 | "html", 11 | "embed", 12 | "chapters", 13 | "synchronization", 14 | "video" 15 | ], 16 | "homepage": "https://github.com/veo-labs/openveo-player", 17 | "bugs": { 18 | "url": "https://github.com/veo-labs/openveo-player/issues" 19 | }, 20 | "license": "AGPL-3.0", 21 | "author": "Veo-Labs (http://www.veo-labs.com/)", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/veo-labs/openveo-player.git" 25 | }, 26 | "devDependencies": { 27 | "@openveo/api": "^8.0.2", 28 | "angular": "1.5.11", 29 | "angular-animate": "1.5.11", 30 | "angular-cookies": "1.5.11", 31 | "angular-mocks": "1.5.11", 32 | "angular-route": "1.5.11", 33 | "chai": "^4.3.4", 34 | "chai-spies": "^1.0.0", 35 | "eslint": "^7.32.0", 36 | "eslint-plugin-node": "^11.1.0", 37 | "karma": "^6.3.4", 38 | "karma-chai": "^0.1.0", 39 | "karma-chrome-launcher": "^3.1.0", 40 | "karma-mocha": "^2.0.1", 41 | "karma-ng-html2js-preprocessor": "^1.0.0", 42 | "mocha": "^9.1.1", 43 | "pre-commit": "^1.2.2", 44 | "roboto-fontface": "^0.10.0", 45 | "uglify-js": "^3.14.2", 46 | "video.js": "^7.15.4" 47 | }, 48 | "scripts": { 49 | "build": "npm run build:clean && ./scripts/build.js production", 50 | "build:development": "./scripts/build.js", 51 | "build:clean": "npx ovRemove ./build ./dist", 52 | "doc": "mkdocs build -c -d \"./site/$(echo $npm_package_version)\"", 53 | "doc:clean": "npx ovRemove ./site", 54 | "doc:deploy": "npx ovDeployGithubPages \"site/$(echo $npm_package_version)\"", 55 | "lint": "npx eslint \"*.js\" \"src/**/*.js\" \"scripts/**/*.js\"", 56 | "postpublish": "npm run doc && npm run doc:deploy", 57 | "prepack": "npm run build", 58 | "test": "npm run build && npx karma start ./karmaConf.js", 59 | "watch": "npm run build:development && npx ovWatch -d ./src -c build:development" 60 | }, 61 | "precommit": [ 62 | "lint", 63 | "test" 64 | ], 65 | "engines": { 66 | "node": ">=16.3.0", 67 | "npm": ">=7.15.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /scripts/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "rules": { 4 | "node/shebang": 0, 5 | 6 | // Allow to require @openveo/api even if it is not in the list of dependencies but only in devDependencies 7 | "node/no-unpublished-require": 0 8 | 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | 4 | // Add mocha environment globals 5 | "mocha": true 6 | 7 | }, 8 | "globals": { 9 | 10 | // Add chai to environment globals but do not authorize to overwrite it 11 | "chai": false, 12 | "assert": false, 13 | 14 | // Add angular test properties to environment globals but do not authorize to overwrite it 15 | "inject": false, 16 | 17 | // Add videojs to environment globals but do not authorize to overwrite it 18 | "videojs": false, 19 | 20 | // Add dashjs to environment globals but do not authorize to overwrite it 21 | "dashjs": false, 22 | 23 | // Add touch properties to environment globals but do not authorize to overwrite it 24 | "DocumentTouch": false 25 | 26 | }, 27 | "rules": { 28 | 29 | // Do not require consistent this as AngularJS encourages to use "ctrl" for controllers 30 | "consistent-this": 0 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/components/html.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 | 10 | -------------------------------------------------------------------------------- /src/components/player.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 9 |
10 | 11 |
12 |
13 | 14 |
15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 | 28 |
29 | 39 |
40 |
41 | 50 | 56 |
60 | {{$ctrl.time | oplMillisecondsToTime}} / {{$ctrl.duration | oplMillisecondsToTime}} 61 |
62 |
63 |
64 | 71 | 80 | 90 |
91 |
92 |
93 | 94 |
95 | 96 |
97 |
98 | 107 | 115 |
119 | {{$ctrl.time | oplMillisecondsToTime}} 120 |
121 |
122 | 129 |
130 |
131 |
132 | 133 | 134 | 142 | 149 | 150 | 158 | 165 | 166 | 174 | 181 | 182 | 183 |
184 |
185 | -------------------------------------------------------------------------------- /src/components/players/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "env": { 4 | 5 | // Add browser environment globals 6 | "browser": true, 7 | 8 | // Add node.js environment globals 9 | "node": true 10 | 11 | }, 12 | "globals": { 13 | 14 | // Add angular to environment globals but do not authorize to overwrite it 15 | "angular": false, 16 | "YT": false, 17 | "videojs": false 18 | 19 | }, 20 | "rules": { 21 | 22 | // Require camelcase 23 | "camelcase": 0 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/players/HTMLPlayer.factory.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.assert = chai.assert; 4 | 5 | describe('HTMLPlayer', function() { 6 | var player; 7 | var videoElement; 8 | var $document; 9 | var $injector; 10 | 11 | // Load module player 12 | beforeEach(module('ov.player')); 13 | 14 | // Dependencies injections 15 | beforeEach(inject(function(_$injector_, _$document_) { 16 | $injector = _$injector_; 17 | $document = _$document_; 18 | })); 19 | 20 | // Initializes tests 21 | beforeEach(function() { 22 | var playerId = 'player_42'; 23 | videoElement = $document[0].createElement('video'); 24 | videoElement.setAttribute('id', playerId); 25 | videoElement.load = function() { 26 | }; 27 | videoElement.play = function() { 28 | }; 29 | $document[0].body.appendChild(videoElement); 30 | 31 | var OvHTMLPlayer = $injector.get('OplHtmlPlayer'); 32 | player = new OvHTMLPlayer(angular.element(videoElement), { 33 | type: 'html', 34 | mediaId: ['1'], 35 | timecodes: {}, 36 | sources: [{ 37 | files: [{ 38 | width: 640, 39 | height: 360, 40 | link: 'http://video.mp4' 41 | }, { 42 | width: 1280, 43 | height: 720, 44 | link: 'http://video.mp4' 45 | }] 46 | }], 47 | thumbnail: '/1439286245225/thumbnail.jpg' 48 | }, playerId); 49 | player.initialize(); 50 | }); 51 | 52 | // Destroy player after each test 53 | afterEach(function() { 54 | player.destroy(); 55 | videoElement = null; 56 | }); 57 | 58 | it('should be able to get media thumbnail', function() { 59 | assert.equal(player.getMediaThumbnail(), '/1439286245225/thumbnail.jpg'); 60 | }); 61 | 62 | it('should be able to play the media if paused', function(done) { 63 | player.player.paused = function() { 64 | return true; 65 | }; 66 | 67 | player.player.play = function() { 68 | done(); 69 | }; 70 | 71 | player.playPause(); 72 | }); 73 | 74 | it('should be able to play the media if ended', function(done) { 75 | player.player.paused = function() { 76 | return false; 77 | }; 78 | player.player.ended = function() { 79 | return true; 80 | }; 81 | 82 | player.player.play = function() { 83 | done(); 84 | }; 85 | 86 | player.playPause(); 87 | }); 88 | 89 | it('should be able to pause the media if playing', function(done) { 90 | player.player.paused = function() { 91 | return false; 92 | }; 93 | 94 | player.player.pause = function() { 95 | done(); 96 | }; 97 | 98 | player.playPause(); 99 | }); 100 | 101 | it('should be able to set player\'s volume in percent', function() { 102 | player.player.volume = function(volume) { 103 | if (volume) this.var = volume; 104 | else return this.var; 105 | }; 106 | player.player.volume(0); 107 | player.setVolume(90); 108 | assert.equal(player.player.volume(), 0.9); 109 | }); 110 | 111 | it('should be able to set player\'s current time in milliseconds', function() { 112 | player.player.currentTime = function(currentTime) { 113 | if (currentTime) this.var = currentTime; 114 | else return this.var; 115 | }; 116 | player.player.currentTime(0); 117 | player.setTime(1000); 118 | assert.equal(player.player.currentTime(), 1); 119 | }); 120 | 121 | it('should order the list of media definitions', function() { 122 | var definitions = player.getAvailableDefinitions(); 123 | assert.isDefined(definitions); 124 | assert.equal(definitions[0].id, '720', 'Wrong quality id for quality 0'); 125 | assert.equal(definitions[0].label, '720p', 'Wrong quality label for quality 0'); 126 | assert.ok(definitions[0].hd, 'Expected HD quality for quality 0'); 127 | assert.equal(definitions[1].id, '360', 'Wrong quality id for quality 1'); 128 | assert.equal(definitions[1].label, '360p', 'Wrong quality label for quality 1'); 129 | assert.notOk(definitions[1].hd, 'Unexpected HD quality for quality 1'); 130 | }); 131 | 132 | it('should order the list of media sources if adaptive sources are defined', function() { 133 | player.media.sources[player.getSourceIndex()].adaptive = [ 134 | { 135 | link: 'http://manifest.mpd', 136 | mimeType: 'application/dash+xml' 137 | }, 138 | { 139 | link: 'http://playlist.m3u8', 140 | mimeType: 'application/x-mpegURL' 141 | } 142 | ]; 143 | var definitions = player.getAvailableDefinitions(); 144 | assert.isNull(definitions); 145 | }); 146 | 147 | }); 148 | -------------------------------------------------------------------------------- /src/components/players/Player.factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Defines a factory describing a player. 11 | * 12 | * All players must implement the methods of this object. 13 | * All events are dispatched to the given jPlayerElement. 14 | * The following events are emitted by the player: 15 | * - **oplPlay**: Player starts playing 16 | * - **oplPause**: Player pauses 17 | * - **oplLoadProgress**: Player is buffering 18 | * - **oplPlayProgress**: Player is playing 19 | * - **oplReady**: Player is ready to play the media 20 | * - **oplDurationChange**: Media duration has changed 21 | * - **oplEnd**: Media has reached the end 22 | * - **oplWaiting**: Media is waiting for buffering 23 | * - **oplPlaying**: Media is ready to play after buffering 24 | * - **oplError**: Player as encountered an error 25 | * 26 | * e.g. 27 | * 28 | * // Get an instance of the OplVimeoPlayer 29 | * // (which extends OplPlayer) 30 | * var OplVimeoPlayer = $injector.get('OplVimeoPlayer'); 31 | * var player = new OplVimeoPlayer(element, 'player_id', '118786909'); 32 | * 33 | * @class OplPlayer 34 | */ 35 | function OplPlayer() { 36 | 37 | /** 38 | * Defines a Player interface which every Player must implement. 39 | */ 40 | function Player() {} 41 | 42 | /** 43 | * Checks if data object is valid. 44 | * 45 | * @method init 46 | * @param {Object} jPlayerElement The JQLite HTML element corresponding to the HTML element which will receive 47 | * events dispatched by the player 48 | * @param {String} id The player id to use as the "id" attribute 49 | */ 50 | Player.prototype.init = function(jPlayerElement, id) { 51 | if (!jPlayerElement) 52 | throw new Error('A player JQLite Element is expected as Player argument'); 53 | 54 | this.jPlayerElement = jPlayerElement; 55 | this.selectedSourceIndex = 0; 56 | this.playerId = id; 57 | }; 58 | 59 | /** 60 | * Sets the source. 61 | * 62 | * @method setMediaSource 63 | * @param {Number} index of the source in the list of media sources 64 | */ 65 | Player.prototype.setMediaSource = function(index) { 66 | this.selectedSourceIndex = index; 67 | }; 68 | 69 | /** 70 | * Gets index of the selected media source. 71 | * 72 | * @method getSourceIndex 73 | * @return {Number} index of the selected media source in the list of sources 74 | */ 75 | Player.prototype.getSourceIndex = function() { 76 | return this.selectedSourceIndex; 77 | }; 78 | 79 | /** 80 | * Gets media ids. 81 | * 82 | * @method getMediaIds 83 | * @return {Array} The list of media ids 84 | */ 85 | Player.prototype.getMediaIds = function() { 86 | return this.media.mediaId; 87 | }; 88 | 89 | /** 90 | * Gets player id. 91 | * 92 | * @method getId 93 | * @return {String} The player id 94 | */ 95 | Player.prototype.getId = function() { 96 | return this.playerId; 97 | }; 98 | 99 | /** 100 | * Sets current media. 101 | * 102 | * @method setMedia 103 | * @param {Objet} media New media 104 | */ 105 | Player.prototype.setMedia = function(media) { 106 | if (!media) 107 | throw new Error('Player needs a valid media'); 108 | 109 | this.media = media; 110 | }; 111 | 112 | /** 113 | * Gets the url of the selected source. 114 | * 115 | * @method getSourceUrl 116 | * @return {String|Null} The url of the source 117 | */ 118 | Player.prototype.getSourceUrl = function() { 119 | return null; 120 | }; 121 | 122 | /** 123 | * Gets media MIME Type. 124 | * 125 | * @method getMediaUrl 126 | * @param {Object} definition Media definition object 127 | * @return {String} The media url 128 | */ 129 | Player.prototype.getMediaMIME = function(definition) { 130 | return definition && definition.mimeType ? definition.mimeType : 'video/mp4'; 131 | }; 132 | 133 | /** 134 | * Gets media definitions. 135 | * 136 | * @method getAvailableDefinitions 137 | * @return {Array} The list of definitions 138 | */ 139 | Player.prototype.getAvailableDefinitions = function() { 140 | throw new Error('getAvailableDefinitions method not implemented for this player'); 141 | }; 142 | 143 | /** 144 | * Gets media thumbnail. 145 | * 146 | * @method getMediaThumbnail 147 | * @return {String} The media thumbnail 148 | */ 149 | Player.prototype.getMediaThumbnail = function() { 150 | throw new Error('getMediaThumbnail method not implemented for this player'); 151 | }; 152 | 153 | /** 154 | * Inititializes the player after DOM is loaded. 155 | * 156 | * @method initialize 157 | */ 158 | Player.prototype.initialize = function() { 159 | throw new Error('initialize method not implemented for this player'); 160 | }; 161 | 162 | /** 163 | * Loads current media. 164 | * 165 | * @method load 166 | */ 167 | Player.prototype.load = function() { 168 | throw new Error('load method not implemented for this player'); 169 | }; 170 | 171 | /** 172 | * Tests if player actual state is pause. 173 | * 174 | * @method isPaused 175 | * @param {Boolean} true if paused, false otherwise 176 | */ 177 | Player.prototype.isPaused = function() { 178 | throw new Error('isPaused method not implemented for this player'); 179 | }; 180 | 181 | /** 182 | * Tests if player actual state is playing. 183 | * 184 | * @method isPlaying 185 | * @return {Boolean} true if playing, false otherwise 186 | */ 187 | Player.prototype.isPlaying = function() { 188 | throw new Error('isPlaying method not implemented for this player'); 189 | }; 190 | 191 | /** 192 | * Plays or pauses the media depending on media actual state. 193 | * 194 | * @method playPause 195 | */ 196 | Player.prototype.playPause = function() { 197 | throw new Error('play method not implemented for this player'); 198 | }; 199 | 200 | /** 201 | * Sets volume. 202 | * 203 | * @method setVolume 204 | * @param {Number} volume The new volume from 0 to 100. 205 | */ 206 | Player.prototype.setVolume = function() { 207 | throw new Error('setVolume method not implemented for this player'); 208 | }; 209 | 210 | /** 211 | * Sets time. 212 | * 213 | * @method setTime 214 | * @param {Number} time The time to seek to in milliseconds 215 | */ 216 | Player.prototype.setTime = function() { 217 | throw new Error('setTime method not implemented for this player'); 218 | }; 219 | 220 | /** 221 | * Gets player type. 222 | * 223 | * @method getPlayerTYpe 224 | * @return {String} A string representation of the player type 225 | */ 226 | Player.prototype.getPlayerType = function() { 227 | throw new Error('getPlayerType method not implemented for this player'); 228 | }; 229 | 230 | /** 231 | * Destroys the player. 232 | * 233 | * @method destroy 234 | */ 235 | Player.prototype.destroy = function() { 236 | throw new Error('destroy method not implemented for this player'); 237 | }; 238 | 239 | /** 240 | * Gets current definition id. 241 | * 242 | * @method getDefinition 243 | * @return {String} The current definition id 244 | */ 245 | Player.prototype.getDefinition = function() { 246 | throw new Error('getDefinition method not implemented for this player'); 247 | }; 248 | 249 | /** 250 | * Changes definition of the current source. 251 | * 252 | * @method setDefinition 253 | * @param {Object} definition Definition from the list of available definitions 254 | */ 255 | Player.prototype.setDefinition = function(definition) { 256 | throw new Error('setDefinition method not implemented for this player'); 257 | }; 258 | 259 | /** 260 | * Indicates if the player supports an overlay play / pause button. 261 | * 262 | * @method isOverlayPlayPauseSupported 263 | * @return {Boolean} true if overlay play / pause button is supported, false otherwise 264 | */ 265 | Player.prototype.isOverlayPlayPauseSupported = function() { 266 | throw new Error('isOverlayPlayPauseSupported method not implemented for this player'); 267 | }; 268 | 269 | return Player; 270 | } 271 | 272 | app.factory('OplPlayer', OplPlayer); 273 | 274 | })(angular.module('ov.player')); 275 | -------------------------------------------------------------------------------- /src/components/players/VimeoPlayer.factory.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.assert = chai.assert; 4 | 5 | describe('VimeoPlayer', function() { 6 | var player; 7 | var playerElement; 8 | var videoElement; 9 | var $document; 10 | var $injector; 11 | var jWindowElement; 12 | var $window; 13 | var mediaId; 14 | var playerId; 15 | 16 | // Load module player 17 | beforeEach(module('ov.player')); 18 | 19 | // Dependencies injections 20 | beforeEach(inject(function(_$injector_, _$document_, _$window_) { 21 | $window = _$window_; 22 | $injector = _$injector_; 23 | $document = _$document_; 24 | })); 25 | 26 | // Initializes tests 27 | beforeEach(function() { 28 | mediaId = '42'; 29 | playerId = 'player_id_42'; 30 | jWindowElement = angular.element($window); 31 | playerElement = $document[0].createElement('div'); 32 | videoElement = $document[0].createElement('div'); 33 | videoElement.setAttribute('id', playerId); 34 | $document[0].body.appendChild(videoElement); 35 | 36 | videoElement.contentWindow = { 37 | postMessage: function() { 38 | } 39 | }; 40 | 41 | var OvVimeoPlayer = $injector.get('OplVimeoPlayer'); 42 | player = new OvVimeoPlayer(angular.element(playerElement), { 43 | type: 'vimeo', 44 | mediaId: [mediaId], 45 | timecodes: {} 46 | }, playerId); 47 | player.initialize(); 48 | }); 49 | 50 | // Destroy player after each test 51 | afterEach(function() { 52 | $document[0].body.removeChild(videoElement); 53 | videoElement = null; 54 | player.destroy(); 55 | }); 56 | 57 | it('should be able to build Vimeo player url', function() { 58 | assert.equal( 59 | player.getSourceUrl().valueOf(), 60 | '//player.vimeo.com/video/' + mediaId + '?api=1&player_id=' + playerId 61 | ); 62 | }); 63 | 64 | it('should register to Vimeo player events', function(done) { 65 | var events = []; 66 | var message = {}; 67 | 68 | // Simulate Vimeo player 69 | videoElement.contentWindow.postMessage = function() { 70 | events.push({}); 71 | if (events.length === 6) 72 | done(); 73 | }; 74 | 75 | message['event'] = 'ready'; 76 | message['player_id'] = playerId; 77 | 78 | jWindowElement.triggerHandler('message', message); 79 | }); 80 | 81 | it('should be able to play the media', function(done) { 82 | var message = {}; 83 | player.playing = 0; 84 | 85 | angular.element(playerElement).on('oplPlay', function() { 86 | done(); 87 | }); 88 | 89 | // Simulate Vimeo player 90 | videoElement.contentWindow.postMessage = function(data) { 91 | var message = {}; 92 | data = JSON.parse(data); 93 | 94 | if (data.value === 'play') { 95 | message['event'] = 'play'; 96 | message['player_id'] = playerId; 97 | jWindowElement.triggerHandler('message', message); 98 | } 99 | }; 100 | 101 | message['event'] = 'ready'; 102 | message['player_id'] = playerId; 103 | jWindowElement.triggerHandler('message', message); 104 | player.playPause(); 105 | }); 106 | 107 | it('should be able to pause the media', function(done) { 108 | var message = {}; 109 | player.playing = 1; 110 | 111 | angular.element(playerElement).on('oplPause', function() { 112 | done(); 113 | }); 114 | 115 | // Simulate Vimeo player 116 | videoElement.contentWindow.postMessage = function(data) { 117 | var message = {}; 118 | data = JSON.parse(data); 119 | 120 | if (data.value === 'pause') { 121 | message['event'] = 'pause'; 122 | message['player_id'] = playerId; 123 | jWindowElement.triggerHandler('message', message); 124 | } 125 | }; 126 | 127 | message['event'] = 'ready'; 128 | message['player_id'] = playerId; 129 | jWindowElement.triggerHandler('message', message); 130 | player.playPause(); 131 | }); 132 | 133 | it('should be able to change media volume', function(done) { 134 | var message = {}; 135 | 136 | // Simulate Vimeo player 137 | videoElement.contentWindow.postMessage = function(data) { 138 | data = JSON.parse(data); 139 | 140 | if (data.method === 'setVolume' && data.value !== 1) { 141 | assert.equal(data.value, 0.5); 142 | done(); 143 | } 144 | }; 145 | 146 | message['event'] = 'ready'; 147 | message['player_id'] = playerId; 148 | jWindowElement.triggerHandler('message', message); 149 | player.setVolume(50); 150 | }); 151 | 152 | it('should be able to seek to media specific time', function(done) { 153 | var message = {}; 154 | 155 | // Simulate Vimeo player 156 | videoElement.contentWindow.postMessage = function(data) { 157 | data = JSON.parse(data); 158 | if (data.method === 'seekTo') { 159 | assert.equal(data.value, 50); 160 | done(); 161 | } 162 | }; 163 | 164 | message['event'] = 'ready'; 165 | message['player_id'] = playerId; 166 | jWindowElement.triggerHandler('message', message); 167 | player.setTime(50000); 168 | }); 169 | 170 | }); 171 | -------------------------------------------------------------------------------- /src/components/preview/preview.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as an opl-preview HTML element to create a time related image preview. 9 | * 10 | * opl-preview is composed of a time and an image related to this time. 11 | * 12 | * Attributes are: 13 | * - [Number] **opl-time** The time in milliseconds 14 | * - [Object|String] **opl-image** The image URL or the sprite image description object with: 15 | * - [String] **url** Sprite URL 16 | * - [Number] **x** x coordinate of the image in the sprite 17 | * - [Number] **y** y coordinate of the image in the sprite 18 | * Image size must be 142 pixels width and 80 pixels height 19 | * 20 | * @example 21 | * var time = 42000; 22 | * var image = { 23 | * url: 'http://local.url/sprite.jpg', 24 | * x: 142, 25 | * y: 0, 26 | * }; 27 | * 28 | * 32 | * 33 | * @class oplPreview 34 | */ 35 | (function(app) { 36 | 37 | app.component('oplPreview', { 38 | templateUrl: 'opl-preview.html', 39 | controller: 'OplPreviewController', 40 | bindings: { 41 | oplTime: '<', 42 | oplImage: '<' 43 | } 44 | }); 45 | 46 | })(angular.module('ov.player')); 47 | -------------------------------------------------------------------------------- /src/components/preview/preview.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Manages oplPreview component. 11 | * 12 | * @param {Object} $scope The component isolated scope 13 | * @param {Object} $element The HTML element holding the component 14 | * @param {Object} $http AngularJS $http service 15 | * @class OplPreviewController 16 | * @constructor 17 | */ 18 | function OplPreviewController($scope, $element, $http) { 19 | var ctrl = this; 20 | var preloadedImageUrls = []; 21 | $scope.url = null; 22 | $scope.position = { 23 | x: 0, 24 | y: 0 25 | }; 26 | 27 | /** 28 | * Preloads the image. 29 | * 30 | * @param {String} url The image URL to preload 31 | */ 32 | function preloadImage(url) { 33 | if (!url || ctrl.preloading || ctrl.preloaded) return; 34 | ctrl.preloading = true; 35 | 36 | $http.get(url).then(function() { 37 | ctrl.preloading = false; 38 | ctrl.preloaded = true; 39 | preloadedImageUrls.push(url); 40 | }).catch(function() { 41 | ctrl.preloading = false; 42 | ctrl.preloaded = true; 43 | ctrl.error = true; 44 | }); 45 | } 46 | 47 | Object.defineProperties(ctrl, { 48 | 49 | /** 50 | * Indicates if image is preloading or not. 51 | * 52 | * @property preloading 53 | * @type Boolean 54 | */ 55 | preloading: { 56 | value: false, 57 | writable: true 58 | }, 59 | 60 | /** 61 | * Indicates if image is preloaded or not. 62 | * 63 | * @property preloaded 64 | * @type Boolean 65 | */ 66 | preloaded: { 67 | value: false, 68 | writable: true 69 | }, 70 | 71 | /** 72 | * Image error message. 73 | * 74 | * @property error 75 | * @type String 76 | */ 77 | error: { 78 | value: false, 79 | writable: true 80 | }, 81 | 82 | /** 83 | * Handles one-way binding properties changes. 84 | * 85 | * @method $onChanges 86 | * @param {Object} changedProperties Properties which have changed since last digest loop 87 | * @param {Object} [changedProperties.oplImage] oplImage old and new value 88 | * @param {String} [changedProperties.oplImage.currentValue] oplImage new value 89 | */ 90 | $onChanges: { 91 | value: function(changedProperties) { 92 | if (changedProperties.oplImage && changedProperties.oplImage.currentValue) { 93 | ctrl.error = false; 94 | 95 | if (preloadedImageUrls.indexOf(ctrl.oplImage.url || ctrl.oplImage) === -1) { 96 | ctrl.preloaded = false; 97 | ctrl.preloading = false; 98 | preloadImage(ctrl.oplImage.url || ctrl.oplImage); 99 | } 100 | 101 | $scope.url = ctrl.oplImage.url || ctrl.oplImage; 102 | $scope.position.x = ctrl.oplImage.x || 0; 103 | $scope.position.y = ctrl.oplImage.y || 0; 104 | } 105 | } 106 | } 107 | 108 | }); 109 | 110 | } 111 | 112 | app.controller('OplPreviewController', OplPreviewController); 113 | OplPreviewController.$inject = ['$scope', '$element', '$http']; 114 | 115 | })(angular.module('ov.player')); 116 | -------------------------------------------------------------------------------- /src/components/preview/preview.html: -------------------------------------------------------------------------------- 1 |
2 |
6 | 7 |
8 |
9 |
10 | 11 |

12 |
13 | -------------------------------------------------------------------------------- /src/components/preview/preview.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/animation"; 2 | @import "compass/css3/user-interface"; 3 | @import "compass/css3/transform"; 4 | 5 | $OPL_PREVIEW_PRIMARY_COLOR: #ffffff; 6 | $OPL_PREVIEW_BACKGROUND_COLOR: #000000; 7 | $OPL_PREVIEW_ACCENT_COLOR: #e2287b; 8 | 9 | /* 10 | Animate the loader with an elastic effect from left to right and right to left with easing. 11 | */ 12 | @include keyframes(opl-preview-loader-elastic) { 13 | 0% { 14 | @include animation-timing-function(cubic-bezier(0.35, 0.35, 0.43, 0.9)); 15 | @include translateX(0%); 16 | } 17 | 50% { 18 | @include animation-timing-function(cubic-bezier(0.35, 0.35, 0.43, 0.9)); 19 | @include translateX(100%); 20 | } 21 | 100% { 22 | @include translateX(0%); 23 | } 24 | } 25 | 26 | opl-preview { 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | .opl-preview { 32 | position: relative; 33 | --opl-preview-primary-color: #{$OPL_PREVIEW_PRIMARY_COLOR}; 34 | --opl-preview-accent-color: #{$OPL_PREVIEW_ACCENT_COLOR}; 35 | --opl-preview-accent-color-0: #{rgba($OPL_PREVIEW_ACCENT_COLOR, 0.1)}; 36 | --opl-preview-background-color: #{$OPL_PREVIEW_BACKGROUND_COLOR}; 37 | --opl-preview-background-color-0: #{rgba($OPL_PREVIEW_BACKGROUND_COLOR, 0.8)}; 38 | width: 100%; 39 | height: 100%; 40 | background-color: var(--opl-preview-background-color, $OPL_PREVIEW_BACKGROUND_COLOR); 41 | @include user-select(none); 42 | 43 | /* 44 | Time container. 45 | */ 46 | .opl-time { 47 | position: absolute; 48 | display: flex; 49 | width: 100%; 50 | justify-content: center; 51 | bottom: 0; 52 | font-size: 12px; 53 | letter-spacing: 0.4px; 54 | color: var(--opl-preview-primary-color, $OPL_PREVIEW_PRIMARY_COLOR); 55 | 56 | /* 57 | Time text. 58 | */ 59 | & > div { 60 | flex-shrink: 1; 61 | background-color: var(--opl-preview-background-color-0, rgba($OPL_PREVIEW_BACKGROUND_COLOR, 0.8)); 62 | padding: 8px; 63 | } 64 | } 65 | 66 | /* 67 | Loader. 68 | */ 69 | .opl-loader { 70 | position: absolute; 71 | top: 0; 72 | left: 0; 73 | width: 100%; 74 | height: 4px; 75 | background-color: var(--opl-preview-accent-color-0, rgba($OPL_PREVIEW_ACCENT_COLOR, 0.1)); 76 | will-change: transform; 77 | 78 | /* 79 | Loader bar. 80 | */ 81 | &::before { 82 | position: absolute; 83 | top: 0; 84 | left: 0; 85 | width: 50%; 86 | height: 100%; 87 | content: ''; 88 | pointer-events: none; 89 | background-color: var(--opl-preview-accent-color, $OPL_PREVIEW_ACCENT_COLOR); 90 | @include animation-name(opl-preview-loader-elastic); 91 | @include animation-iteration-count(infinite); 92 | @include animation-duration(2000ms); 93 | } 94 | 95 | } 96 | 97 | /* 98 | Error message. 99 | */ 100 | .opl-error-message { 101 | padding-top: 16px; 102 | text-align: center; 103 | letter-spacing: 5px; 104 | font-size: 12px; 105 | color: var(--opl-preview-primary-color, $OPL_PREVIEW_PRIMARY_COLOR); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/components/shared/button/button.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as an opl-button HTML element to create a simple button. 9 | * 10 | * opl-button is composed of an icon, a focus ring and a ripple. 11 | * 12 | * Attributes are: 13 | * - [String] **opl-icon** The ligature name of the icon to use (from "OpenVeo-Player-Icons" font) 14 | * - [String] **opl-label** The ARIA label to apply to the button. Empty by default. 15 | * - [Boolean] **opl-no-sequential-focus** true to set button tabindex to -1, false to set button tabindex to 0 16 | * - [Function] **opl-on-update** The function to call when actioned 17 | * - [Function] **opl-on-focus** The function to call when component enters in focus state 18 | * 19 | * @example 20 | * var handleOnUpdate = function() { 21 | * console.log('Button actioned'); 22 | * }; 23 | * var handleOnFocus = function() { 24 | * console.log('Component has received focus'); 25 | * }; 26 | * 27 | * 34 | * 35 | * @class oplButton 36 | */ 37 | (function(app) { 38 | 39 | app.component('oplButton', { 40 | templateUrl: 'opl-button.html', 41 | controller: 'OplButtonController', 42 | bindings: { 43 | oplIcon: '@?', 44 | oplLabel: '@?', 45 | oplNoSequentialFocus: '@?', 46 | oplOnUpdate: '&', 47 | oplOnFocus: '&' 48 | } 49 | }); 50 | 51 | })(angular.module('ov.player')); 52 | -------------------------------------------------------------------------------- /src/components/shared/button/button.component.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.assert = chai.assert; 4 | 5 | describe('OplButton', function() { 6 | var $compile; 7 | var $rootScope; 8 | var oplEventsFactory; 9 | var scope; 10 | var originalRequestAnimationFrame; 11 | var bodyElement; 12 | 13 | // Load modules 14 | beforeEach(function() { 15 | module('ov.player'); 16 | module('templates'); 17 | 18 | // Mock requestAnimationFrame 19 | originalRequestAnimationFrame = window.requestAnimationFrame; 20 | window.requestAnimationFrame = function(callback) { 21 | callback(); 22 | }; 23 | }); 24 | 25 | // Dependencies injections 26 | beforeEach(inject(function(_$compile_, _$rootScope_, _$window_, _oplEventsFactory_) { 27 | $rootScope = _$rootScope_; 28 | $compile = _$compile_; 29 | oplEventsFactory = _oplEventsFactory_; 30 | bodyElement = angular.element(_$window_.document.body); 31 | })); 32 | 33 | afterEach(function() { 34 | window.requestAnimationFrame = originalRequestAnimationFrame; 35 | }); 36 | 37 | // Initializes tests 38 | beforeEach(function() { 39 | scope = $rootScope.$new(); 40 | }); 41 | 42 | it('should display a button with an icon and a label', function() { 43 | var expectedIconLigature = 'ligature'; 44 | var expectedLabel = 'Button label'; 45 | var element = angular.element(''); 49 | element = $compile(element)(scope); 50 | scope.$digest(); 51 | 52 | var buttonElement = element[0].querySelector('button'); 53 | assert.isNotNull(buttonElement, 'Expected a button'); 54 | 55 | assert.equal( 56 | angular.element(buttonElement.querySelector('.opl-icon')).text(), 57 | expectedIconLigature, 58 | 'Wrong icon' 59 | ); 60 | assert.equal(angular.element(buttonElement).attr('aria-label'), expectedLabel, 'Wrong label'); 61 | }); 62 | 63 | it('should be able to deactivate sequential focus', function() { 64 | var element = angular.element(''); 67 | element = $compile(element)(scope); 68 | scope.$digest(); 69 | 70 | assert.equal( 71 | angular.element(element[0].querySelector('button')).attr('tabindex'), 72 | '-1', 73 | 'Wrong tabindex' 74 | ); 75 | }); 76 | 77 | it('should call oplOnUpdate function if clicked', function() { 78 | var element = angular.element(''); 79 | 80 | scope.expectedOnUpdateFunction = chai.spy(function() {}); 81 | element = $compile(element)(scope); 82 | scope.$digest(); 83 | 84 | var buttonElement = element.find('button'); 85 | buttonElement.triggerHandler(oplEventsFactory.EVENTS.DOWN); 86 | bodyElement.triggerHandler(oplEventsFactory.EVENTS.UP); 87 | 88 | scope.expectedOnUpdateFunction.should.have.been.called.exactly(1); 89 | }); 90 | 91 | it('should call oplOnUpdate function if actioned using "enter" key', function() { 92 | var element = angular.element(''); 93 | 94 | scope.expectedOnUpdateFunction = chai.spy(function() {}); 95 | element = $compile(element)(scope); 96 | scope.$digest(); 97 | 98 | var buttonElement = element.find('button'); 99 | buttonElement.triggerHandler({type: 'keydown', keyCode: 13}); 100 | 101 | scope.expectedOnUpdateFunction.should.have.been.called.exactly(1); 102 | }); 103 | 104 | it('should add "opl-focus" class and call oplOnFocus when focused', function() { 105 | var element = angular.element(''); 106 | 107 | scope.expectedOnFocusFunction = chai.spy(function() {}); 108 | element = $compile(element)(scope); 109 | scope.$digest(); 110 | 111 | var buttonElement = element.find('button'); 112 | buttonElement.triggerHandler('focus'); 113 | 114 | assert.ok(buttonElement.hasClass('opl-focus'), 'Expected class "opl-focus"'); 115 | scope.expectedOnFocusFunction.should.have.been.called.exactly(1); 116 | }); 117 | 118 | it('should remove "opl-focus" class when unfocused', function() { 119 | var element = angular.element(''); 120 | 121 | element = $compile(element)(scope); 122 | scope.$digest(); 123 | 124 | var buttonElement = element.find('button'); 125 | buttonElement.triggerHandler('focus'); 126 | 127 | assert.ok(buttonElement.hasClass('opl-focus'), 'Expected class "opl-focus"'); 128 | 129 | buttonElement.triggerHandler('blur'); 130 | 131 | assert.notOk(buttonElement.hasClass('opl-focus'), 'Unexpected class "opl-focus"'); 132 | }); 133 | 134 | describe('focus', function() { 135 | 136 | it('should focus the button', function() { 137 | var element = angular.element(''); 138 | 139 | element = $compile(element)(scope); 140 | scope.$digest(); 141 | 142 | var buttonElement = element.find('button'); 143 | var buttonController = buttonElement.controller('oplButton'); 144 | 145 | buttonElement[0].focus = chai.spy(buttonElement[0].focus); 146 | 147 | buttonController.focus(); 148 | 149 | buttonElement[0].focus.should.have.been.called.exactly(1); 150 | }); 151 | 152 | }); 153 | 154 | describe('isFocused', function() { 155 | 156 | it('should indicate if button is focused or not', function() { 157 | var element = angular.element(''); 158 | 159 | element = $compile(element)(scope); 160 | scope.$digest(); 161 | 162 | var buttonElement = element.find('button'); 163 | var buttonController = buttonElement.controller('oplButton'); 164 | 165 | buttonElement.triggerHandler('focus'); 166 | 167 | assert.ok(buttonController.isFocused(), 'Expected button to be focused'); 168 | 169 | buttonElement.triggerHandler('blur'); 170 | 171 | assert.notOk(buttonController.isFocused(), 'Unexpected focus'); 172 | }); 173 | 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /src/components/shared/button/button.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Manages oplButton component. 11 | * 12 | * @param {Object} $element The HTML element holding the component 13 | * @param {Object} $timeout The AngularJS $timeout service 14 | * @param {Object} $scope Component isolated scope 15 | * @param {Object} $q The AngularJS $q service 16 | * @param {Object} $window The AngularJS $window service 17 | * @param {Object} oplEventsFactory Helper to manipulate the DOM events 18 | * @class OplButtonController 19 | * @constructor 20 | */ 21 | function OplButtonController($element, $timeout, $scope, $q, $window, oplEventsFactory) { 22 | var ctrl = this; 23 | var buttonElement; 24 | var bodyElement; 25 | var activationTimer; 26 | var deactivationTimer; 27 | var deactivationAnimationRequested; 28 | var activated; 29 | 30 | /** 31 | * Focuses the button. 32 | */ 33 | function focus() { 34 | buttonElement.addClass('opl-focus'); 35 | if (ctrl.oplOnFocus) ctrl.oplOnFocus(); 36 | } 37 | 38 | /** 39 | * Unfocuses the button. 40 | */ 41 | function unfocus() { 42 | buttonElement.removeClass('opl-focus'); 43 | } 44 | 45 | /** 46 | * Animates the deactivation of the button. 47 | * 48 | * Deactivation is performed only if activation animation is ended and activation is ended. 49 | * 50 | * @return {Promise} Promise resolving when animation is finished 51 | */ 52 | function animateDeactivation() { 53 | if (!deactivationAnimationRequested || activationTimer) return $q.when(); 54 | var deferred = $q.defer(); 55 | 56 | deactivationAnimationRequested = false; 57 | activated = false; 58 | buttonElement.removeClass('opl-activation'); 59 | 60 | // Start deactivation animation 61 | buttonElement.addClass('opl-deactivation'); 62 | 63 | // An animation is associated to the opl-deactivation class, wait for it to finish before removing the 64 | // deactivation class 65 | // Delay corresponds to the animation duration 66 | deactivationTimer = $timeout(function() { 67 | buttonElement.removeClass('opl-deactivation'); 68 | deferred.resolve(); 69 | }, 150); 70 | 71 | return deferred.promise; 72 | } 73 | 74 | /** 75 | * Animates the activation of the button. 76 | * 77 | * Activation animation can be performed only if not activated. 78 | * 79 | * @return {Promise} Promise resolving when animation is finished 80 | */ 81 | function animateActivation() { 82 | var deferred = $q.defer(); 83 | 84 | // Remove any ongoing activation / deactivation animations 85 | buttonElement.removeClass('opl-deactivation'); 86 | if (activationTimer) $timeout.cancel(activationTimer); 87 | if (deactivationTimer) $timeout.cancel(deactivationTimer); 88 | 89 | // Start activation animation 90 | buttonElement.addClass('opl-activation'); 91 | 92 | // An animation is associated to the opl-activation class, wait for it to finish before running the deactivation 93 | // animation 94 | // Delay corresponds to the animation duration 95 | activationTimer = $timeout(function() { 96 | activationTimer = null; 97 | requestAnimationFrame(function() { 98 | animateDeactivation().then(function() { 99 | deferred.resolve(); 100 | }); 101 | }); 102 | }, 225); 103 | 104 | return deferred.promise; 105 | } 106 | 107 | /** 108 | * Calls the oplOnUpdate function. 109 | */ 110 | function callAction() { 111 | if (ctrl.oplOnUpdate) ctrl.oplOnUpdate(); 112 | } 113 | 114 | /** 115 | * Handles keydown events. 116 | * 117 | * Toggle button captures the following keyboard keys: 118 | * - ENTER to action the button 119 | * 120 | * Captured keys will prevent default browser actions. 121 | * 122 | * @param {KeyboardEvent} event The captured event 123 | */ 124 | function handleKeyDown(event) { 125 | if ((event.key !== 'Enter' && event.keyCode !== 13)) return; 126 | 127 | event.stopPropagation(); 128 | event.stopImmediatePropagation(); 129 | event.preventDefault(); 130 | $scope.$apply(callAction); 131 | } 132 | 133 | /** 134 | * Handles release events. 135 | * 136 | * After releasing, button is actioned and is not longer active. 137 | * 138 | * @param {Event} event The captured event which may defer depending on the device (mouse, touchpad, pen etc.) 139 | */ 140 | function handleUp(event) { 141 | event.stopPropagation(); 142 | event.stopImmediatePropagation(); 143 | event.preventDefault(); 144 | 145 | bodyElement.off(oplEventsFactory.EVENTS.UP, handleUp); 146 | requestAnimationFrame(function() { 147 | deactivationAnimationRequested = true; 148 | animateDeactivation(); 149 | }); 150 | activated = false; 151 | ctrl.focus(); 152 | callAction(); 153 | } 154 | 155 | /** 156 | * Handles pressed events. 157 | * 158 | * Pressing the button makes it active. 159 | * 160 | * @param {Event} event The captured event which may defer depending on the device (mouse, touchpad, pen etc.) 161 | */ 162 | function handleDown(event) { 163 | if (activated) return; 164 | 165 | event.stopPropagation(); 166 | event.stopImmediatePropagation(); 167 | event.preventDefault(); 168 | 169 | activated = true; 170 | requestAnimationFrame(function() { 171 | animateActivation(); 172 | }); 173 | bodyElement.on(oplEventsFactory.EVENTS.UP, handleUp); 174 | } 175 | 176 | /** 177 | * Handles focus event. 178 | * 179 | * @param {FocusEvent} event The captured event 180 | */ 181 | function handleFocus(event) { 182 | focus(); 183 | } 184 | 185 | /** 186 | * Handles blur event. 187 | * 188 | * @param {FocusEvent} event The captured event 189 | */ 190 | function handleBlur(event) { 191 | unfocus(); 192 | } 193 | 194 | Object.defineProperties(ctrl, { 195 | 196 | /** 197 | * Focuses the button. 198 | * 199 | * @method focus 200 | */ 201 | focus: { 202 | value: function() { 203 | buttonElement[0].focus(); 204 | } 205 | }, 206 | 207 | /** 208 | * Indicates if button is focused or not. 209 | * 210 | * @method isFocused 211 | * @return {Boolean} true if focused, false otherwise 212 | */ 213 | isFocused: { 214 | value: function() { 215 | return buttonElement.hasClass('opl-focus'); 216 | } 217 | }, 218 | 219 | /** 220 | * Initializes controller. 221 | * 222 | * @method $onInit 223 | */ 224 | $onInit: { 225 | value: function() { 226 | bodyElement = angular.element($window.document.body); 227 | buttonElement = angular.element($element[0].querySelector('.opl-button')); 228 | 229 | buttonElement.on('keydown', handleKeyDown); 230 | buttonElement.on('focus', handleFocus); 231 | buttonElement.on('blur', handleBlur); 232 | buttonElement.on(oplEventsFactory.EVENTS.DOWN, handleDown); 233 | } 234 | }, 235 | 236 | /** 237 | * Removes event listeners. 238 | * 239 | * @method $onDestroy 240 | */ 241 | $onDestroy: { 242 | value: function() { 243 | buttonElement.off( 244 | oplEventsFactory.EVENTS.DOWN + ' keydown focus blur' 245 | ); 246 | bodyElement.off(oplEventsFactory.EVENTS.UP, handleUp); 247 | if (activationTimer) $timeout.cancel(activationTimer); 248 | if (deactivationTimer) $timeout.cancel(deactivationTimer); 249 | } 250 | } 251 | }); 252 | 253 | } 254 | 255 | app.controller('OplButtonController', OplButtonController); 256 | OplButtonController.$inject = ['$element', '$timeout', '$scope', '$q', '$window', 'oplEventsFactory']; 257 | 258 | })(angular.module('ov.player')); 259 | -------------------------------------------------------------------------------- /src/components/shared/button/button.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/shared/button/button.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/user-interface"; 2 | @import "compass/css3/box-sizing"; 3 | @import "compass/css3/transform"; 4 | @import "compass/css3/animation"; 5 | @import "compass/css3/transition"; 6 | 7 | $OPL_BUTTON_PRIMARY_COLOR: #000000; 8 | 9 | /* 10 | Animate the ripple with a growing effect from 0 to 100%. 11 | */ 12 | @include keyframes(opl-button-ripple-radius-in) { 13 | from { 14 | @include animation-timing-function(cubic-bezier(0.4, 0, 0.2, 1)); 15 | @include scale(0); 16 | } 17 | to { 18 | @include scale(1); 19 | } 20 | } 21 | 22 | /* 23 | Animate the ripple with a resolve effect from 0 to 0.16. 24 | */ 25 | @include keyframes(opl-button-ripple-resolve) { 26 | from { 27 | @include animation-timing-function(cubic-bezier(0.4, 0, 0.2, 1)); 28 | opacity: 0; 29 | } 30 | to { 31 | opacity: 0.16; 32 | } 33 | } 34 | 35 | /* 36 | Animate the ripple with a dissolve effect from focus 0.16 to 0. 37 | */ 38 | @include keyframes(opl-button-ripple-opacity-dissolve) { 39 | from { 40 | @include animation-timing-function(linear); 41 | opacity: 0.16; 42 | } 43 | to { 44 | opacity: 0; 45 | } 46 | } 47 | 48 | opl-button { 49 | display: inline-block; 50 | width: 32px; 51 | height: 32px; 52 | } 53 | 54 | .opl-button { 55 | --opl-button-primary-color: #{$OPL_BUTTON_PRIMARY_COLOR}; 56 | position: relative; 57 | display: inline-block; 58 | width: 32px; 59 | height: 32px; 60 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 61 | will-change: transform, opacity; 62 | padding: 4px; 63 | outline: none; 64 | border: none; 65 | background-color: transparent; 66 | cursor: pointer; 67 | color: var(--opl-button-primary-color, $OPL_BUTTON_PRIMARY_COLOR); 68 | @include box-sizing(border-box); 69 | @include user-select(none); 70 | 71 | /* 72 | Focus ring and ripple in resting state. 73 | */ 74 | &::before, 75 | &::after { 76 | position: absolute; 77 | top: 0; 78 | left: 0; 79 | width: 100%; 80 | height: 100%; 81 | content: ''; 82 | opacity: 0; 83 | border-radius: 50%; 84 | pointer-events: none; 85 | } 86 | 87 | /* 88 | Focus ring in resting state. 89 | */ 90 | &::before { 91 | @include transition-property(opacity); 92 | @include transition-timing-function(linear); 93 | @include transition-duration(15ms); 94 | background-color: var(--opl-button-primary-color, $OPL_BUTTON_PRIMARY_COLOR); 95 | } 96 | 97 | /* 98 | Ripple in resting state. 99 | */ 100 | &::after { 101 | @include scale(0); 102 | @include transform-origin(center, center); 103 | background-color: var(--opl-button-primary-color, $OPL_BUTTON_PRIMARY_COLOR); 104 | } 105 | 106 | /* 107 | Focus ring in over state. 108 | */ 109 | &:hover::before { 110 | opacity: 0.04; 111 | } 112 | 113 | /* 114 | Focus ring in focus state. 115 | */ 116 | &.opl-focus::before { 117 | @include transition-duration(75ms); 118 | opacity: 0.12; 119 | } 120 | 121 | /* 122 | Ripple in activation state. 123 | */ 124 | &.opl-activation::after { 125 | @include animation-name(opl-button-ripple-radius-in, opl-button-ripple-resolve); 126 | @include animation-duration(225ms, 75ms); 127 | @include animation-fill-mode(forwards, forwards); 128 | } 129 | 130 | /* 131 | Ripple in deactivation state. 132 | */ 133 | &.opl-deactivation::after { 134 | @include animation-name(opl-button-ripple-opacity-dissolve); 135 | @include animation-duration(150ms); 136 | @include scale(1); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/components/shared/dom.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Helps manipulate the DOM. 11 | * 12 | * @param {Object} $q The AngularJS $q service 13 | * @param {Object} $timeout The AngularJS $timeout service 14 | * @class OplDomFactory 15 | * @constructor 16 | */ 17 | function OplDomFactory($q, $timeout) { 18 | 19 | /** 20 | * Waits for something to be valid. 21 | * 22 | * @param {Function} isValid A function to call to test the validity, if it returns true the promise is 23 | * resolved, if it returns false the function waits and call the function again later 24 | * @param {Number} [timeout=50] The maximum number of milliseconds to wait for it to be valid before rejecting 25 | * the promise 26 | * @return {Promise} A promise resolving when valid 27 | * @method wait 28 | */ 29 | function wait(isValid, timeout) { 30 | if (!timeout) timeout = 50; 31 | var deferred = $q.defer(); 32 | var waitPromise = null; 33 | 34 | var waitToBeValid = function() { 35 | if (isValid()) return $q.when(); 36 | var validDeferred = $q.defer(); 37 | 38 | waitPromise = $timeout(function() { 39 | if (isValid()) return validDeferred.resolve(); 40 | 41 | waitToBeValid().then(function() { 42 | validDeferred.resolve(); 43 | }).catch(function(reason) { 44 | validDeferred.reject(reason); 45 | }); 46 | }, 10); 47 | 48 | return validDeferred.promise; 49 | }; 50 | 51 | $q.race([ 52 | $timeout(function() { 53 | return 0; 54 | }, timeout), 55 | waitToBeValid() 56 | ]).then(function(result) { 57 | if (result === 0) { 58 | 59 | // Timeout 60 | deferred.reject('Timed out'); 61 | 62 | } else { 63 | deferred.resolve(); 64 | } 65 | 66 | }).catch(function(reason) { 67 | deferred.reject(reason); 68 | }).finally(function() { 69 | if (waitPromise) $timeout.cancel(waitPromise); 70 | }); 71 | 72 | return deferred.promise; 73 | } 74 | 75 | /** 76 | * Waits for a controller associated to an HTML element. 77 | * 78 | * @param {HTMLElement} element The HTML element 79 | * @param {String} componentName The AngularJS compoment name 80 | * @param {Number} [timeout=500] The maximum number of milliseconds to wait for the element controller 81 | * @return {Promise} A promise resolving when the controller is available 82 | * @method waitForController 83 | */ 84 | function waitForController(element, componentName, timeout) { 85 | if (!timeout) timeout = 50; 86 | if (!element) return $q.reject('Element not specified'); 87 | var deferred = $q.defer(); 88 | var controller = null; 89 | 90 | var isControllerReady = function() { 91 | controller = angular.element(element).controller(componentName); 92 | return controller ? true : false; 93 | }; 94 | 95 | wait(isControllerReady, timeout).then(function() { 96 | deferred.resolve(controller); 97 | }).catch(function(reason) { 98 | deferred.reject(reason); 99 | }); 100 | 101 | return deferred.promise; 102 | } 103 | 104 | /** 105 | * Waits for an HTML element being repainted by the browser. 106 | * 107 | * @param {HTMLElement} element The HTML element to wait for 108 | * @param {Array} validators The list of property validators to wait for. Each validator is a description object 109 | * with: 110 | * - [String] property The name of the property concerned by the validator (either "bottom", "top", "width", 111 | * "height", "left" or "top") 112 | * - [Number] [notEqual] A value the property should not be equal to 113 | * - [Number] [equal] A value the property should be equal to 114 | * @param {Number} [timeout=500] The maximum number of milliseconds to wait for the element to be ready 115 | * @return {Promise} A promise resolving when the element is ready 116 | * @method waitForElementDimension 117 | */ 118 | function waitForElementDimension(element, validators, timeout) { 119 | if (!timeout) timeout = 50; 120 | if (!element) return $q.reject('Element not specified'); 121 | var deferred = $q.defer(); 122 | var elementBoundingRectangle = null; 123 | 124 | var isElementReady = function() { 125 | var boundingRectangle = element.getBoundingClientRect(); 126 | 127 | elementBoundingRectangle = { 128 | bottom: boundingRectangle.bottom, 129 | height: boundingRectangle.height, 130 | left: boundingRectangle.left, 131 | right: boundingRectangle.right, 132 | top: boundingRectangle.top, 133 | width: boundingRectangle.width, 134 | clientWidth: element.clientWidth, 135 | clientHeight: element.clientHeight 136 | }; 137 | 138 | for (var i = 0; i < validators.length; i++) { 139 | var validator = validators[i]; 140 | 141 | if ( 142 | ( 143 | validator.notEqual !== undefined && 144 | elementBoundingRectangle[validator.property] === validator.notEqual 145 | ) || 146 | ( 147 | validator.equal !== undefined && 148 | elementBoundingRectangle[validator.property] !== validator.equal 149 | ) 150 | ) { 151 | return false; 152 | } 153 | } 154 | 155 | return true; 156 | }; 157 | 158 | wait(isElementReady, timeout).then(function() { 159 | deferred.resolve(elementBoundingRectangle); 160 | }).catch(function(reason) { 161 | deferred.reject(reason); 162 | }); 163 | 164 | return deferred.promise; 165 | } 166 | 167 | /** 168 | * Waits for an HTML element to be present in the DOM. 169 | * 170 | * @param {HTMLElement} parentElement A parent element of the expected HTML element 171 | * @param {String} selector The query selector to select the element from parentElement 172 | * @param {Number} [timeout=500] The maximum number of milliseconds to wait for the element 173 | * @return {Promise} A promise resolving when the element is added to the DOM 174 | * @method waitForElement 175 | */ 176 | function waitForElement(parentElement, selector, timeout) { 177 | if (!timeout) timeout = 50; 178 | if (!parentElement || !selector) return $q.reject('Parent element or selector not specified'); 179 | var deferred = $q.defer(); 180 | 181 | var isElementPresent = function() { 182 | return parentElement.querySelector(selector) ? true : false; 183 | }; 184 | 185 | wait(isElementPresent, timeout).then(function() { 186 | deferred.resolve(); 187 | }).catch(function(reason) { 188 | deferred.reject(reason); 189 | }); 190 | 191 | return deferred.promise; 192 | } 193 | 194 | return { 195 | waitForElement: waitForElement, 196 | waitForElementDimension: waitForElementDimension, 197 | waitForController: waitForController 198 | }; 199 | 200 | } 201 | 202 | app.factory('oplDomFactory', OplDomFactory); 203 | OplDomFactory.$inject = ['$q', '$timeout']; 204 | 205 | })(angular.module('ov.player')); 206 | -------------------------------------------------------------------------------- /src/components/shared/events.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Helps manipulating DOM events. 11 | * 12 | * @class OplEventsFactory 13 | * @param {Object} $window AngularJS $window service 14 | * @constructor 15 | */ 16 | function OplEventsFactory($window) { 17 | var EVENTS = {}; 18 | 19 | if ($window.PointerEvent) { 20 | EVENTS.UP = 'pointerup'; 21 | EVENTS.DOWN = 'pointerdown'; 22 | EVENTS.MOVE = 'pointermove'; 23 | EVENTS.OVER = 'pointerover'; 24 | EVENTS.OUT = 'pointerout'; 25 | } else if ($window.TouchEvent) { 26 | EVENTS.UP = 'touchend'; 27 | EVENTS.DOWN = 'touchstart'; 28 | EVENTS.MOVE = 'touchmove'; 29 | } else { 30 | EVENTS.UP = 'mouseup'; 31 | EVENTS.DOWN = 'mousedown'; 32 | EVENTS.MOVE = 'mousemove'; 33 | EVENTS.OVER = 'mouseover'; 34 | EVENTS.OUT = 'mouseout'; 35 | } 36 | 37 | /** 38 | * Gets the coordinates of an UIEvent. 39 | * 40 | * @param {UIEvent} event Either a MouseEvent, PointerEvent or TouchEvent 41 | * @return {Object} The coordinates with a property "x" and a property "y" 42 | */ 43 | function getUiEventCoordinates(event) { 44 | var coordinates = {}; 45 | 46 | if (event.targetTouches && event.targetTouches.length > 0) { 47 | 48 | // This is a TouchEvent with one or several touch targets 49 | // Get coordinates of the first touch point relative to the document edges 50 | coordinates.x = event.targetTouches[0].pageX; 51 | coordinates.y = event.targetTouches[0].pageY; 52 | 53 | } else { 54 | 55 | // This is a MouseEvent or a PointerEvent 56 | coordinates.x = event.pageX; 57 | coordinates.y = event.pageY; 58 | 59 | } 60 | 61 | return coordinates; 62 | } 63 | 64 | return { 65 | EVENTS: EVENTS, 66 | getUiEventCoordinates: getUiEventCoordinates 67 | }; 68 | 69 | } 70 | 71 | app.factory('oplEventsFactory', OplEventsFactory); 72 | OplEventsFactory.$inject = ['$window']; 73 | 74 | })(angular.module('ov.player')); 75 | -------------------------------------------------------------------------------- /src/components/shared/i18n/i18n.filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Defines a filter to translate an id, contained inside a dictionary of translations, into the appropriated text. 11 | * 12 | * 13 | * @example 14 | * // Translation dictionary 15 | * var translations = { 16 | * SIMPLE_TRANSLATION_ID: 'Simple translation', 17 | * TRANSLATION_WITH_PARAMETERS_ID: 'Translation with "%parameter%"' 18 | * }; 19 | * 20 | * $scope.translationParameters = { 21 | * '%parameter%': 'my value' 22 | * }; 23 | * 24 | * @example 25 | * 26 | *

{{'SIMPLE_TRANSLATION_ID' | oplTranslate}}

27 | * 28 | * 29 | *

{{'TRANSLATION_WITH_PARAMETERS_ID' | oplTranslate:translationParameters}}

30 | * 31 | * @class TranslateFilter 32 | */ 33 | function TranslateFilter(oplI18nService) { 34 | 35 | /** 36 | * Translates an id into the appropriated text. 37 | * 38 | * @method translate 39 | * @param {String} id The id of the translation 40 | * @param {Object} [parameters] Parameters with for each parameter the key as the placeholder to replace and the 41 | * value as the placeholder substitution string 42 | * @return {String} The translated string 43 | */ 44 | return function(id, parameters) { 45 | return oplI18nService.translate(id, parameters); 46 | }; 47 | 48 | } 49 | 50 | app.filter('oplTranslate', TranslateFilter); 51 | TranslateFilter.$inject = ['oplI18nService']; 52 | 53 | })(angular.module('ov.player')); 54 | -------------------------------------------------------------------------------- /src/components/shared/i18n/i18n.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(angular, app) { 8 | 9 | /** 10 | * Defines an internationalization service to manage string translations. 11 | * 12 | * @class oplI18nService 13 | */ 14 | function oplI18nService(oplI18nTranslations) { 15 | var currentLanguage = navigator.language || navigator.browserLanguage; 16 | 17 | /** 18 | * Tests if a language is supported. 19 | * 20 | * @method isLanguageSupported 21 | * @param {String} language The language code to test (e.g en-CA) 22 | * @return {Boolean} true if supported, false otherwise 23 | */ 24 | function isLanguageSupported(language) { 25 | return Object.keys(oplI18nTranslations).indexOf(language) >= 0; 26 | } 27 | 28 | /** 29 | * Sets current language. 30 | * 31 | * @method setLanguage 32 | * @param {String} language The current language country code (e.g en-CA) 33 | */ 34 | function setLanguage(language) { 35 | if (isLanguageSupported(language)) 36 | currentLanguage = language; 37 | else 38 | currentLanguage = 'en'; 39 | } 40 | 41 | /** 42 | * Gets current language. 43 | * 44 | * @method getLanguage 45 | * @return {String} The current language country code (e.g en-US) 46 | */ 47 | function getLanguage() { 48 | return currentLanguage; 49 | } 50 | 51 | /** 52 | * Translates the given id using current language. 53 | * 54 | * @method translate 55 | * @param {String} id The id of the translation 56 | * @param {Object} [parameters] Parameters with for each parameter the key as the placeholder to replace and the 57 | * value as the placeholder substitution string 58 | * @return {String} The translated string 59 | */ 60 | function translate(id, parameters) { 61 | var translatedText = (oplI18nTranslations[currentLanguage] && oplI18nTranslations[currentLanguage][id]) || id; 62 | 63 | // Translation does not exist 64 | // Use english language as default 65 | if (translatedText === id) 66 | translatedText = oplI18nTranslations['en'][id] || id; 67 | 68 | // Parameters 69 | if (parameters) { 70 | var reg = new RegExp(Object.keys(parameters).join('|'), 'gi'); 71 | translatedText = translatedText.replace(reg, function(matched) { 72 | return parameters[matched]; 73 | }); 74 | } 75 | 76 | return translatedText; 77 | } 78 | 79 | return { 80 | translate: translate, 81 | isLanguageSupported: isLanguageSupported, 82 | setLanguage: setLanguage, 83 | getLanguage: getLanguage 84 | }; 85 | 86 | } 87 | 88 | app.service('oplI18nService', oplI18nService); 89 | oplI18nService.$inject = ['oplI18nTranslations']; 90 | 91 | })(angular, angular.module('ov.player')); 92 | -------------------------------------------------------------------------------- /src/components/shared/i18n/i18n.service.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.assert = chai.assert; 4 | 5 | describe('oplI18nService', function() { 6 | var oplI18nService; 7 | var expectedTranslations; 8 | 9 | // Mocks 10 | beforeEach(function() { 11 | expectedTranslations = { 12 | en: {}, 13 | fr: {} 14 | }; 15 | module('ov.player', function($provide) { 16 | 17 | // Mock players 18 | $provide.constant('oplI18nTranslations', expectedTranslations); 19 | 20 | }); 21 | }); 22 | 23 | // Dependencies injections 24 | beforeEach(inject(function(_oplI18nService_) { 25 | oplI18nService = _oplI18nService_; 26 | })); 27 | 28 | describe('isLanguageSupported', function() { 29 | 30 | it('should indicate if a language is supported or not', function() { 31 | assert.ok( 32 | oplI18nService.isLanguageSupported(Object.keys(expectedTranslations)[0]), 33 | 'Expected language support' 34 | ); 35 | assert.notOk(oplI18nService.isLanguageSupported('not supported language'), 'Unexpected language support'); 36 | }); 37 | 38 | }); 39 | 40 | describe('setLanguage', function() { 41 | 42 | it('should set a different language', function() { 43 | oplI18nService.setLanguage('en'); 44 | assert.equal(oplI18nService.getLanguage(), 'en', 'Expected "en" language'); 45 | oplI18nService.setLanguage('fr'); 46 | assert.equal(oplI18nService.getLanguage(), 'fr', 'Expected "fr" language'); 47 | }); 48 | 49 | it('should set language to "en" if specified language is not supported', function() { 50 | oplI18nService.setLanguage('not supported language'); 51 | assert.equal(oplI18nService.getLanguage(), 'en', 'Expected "en" language'); 52 | }); 53 | 54 | }); 55 | 56 | describe('translate', function() { 57 | 58 | it('should translate a string using current language', function() { 59 | expectedTranslations.en.TRANSLATION_ID = 'English'; 60 | expectedTranslations.fr.TRANSLATION_ID = 'Français'; 61 | 62 | oplI18nService.setLanguage('en'); 63 | assert.equal( 64 | oplI18nService.translate('TRANSLATION_ID'), 65 | expectedTranslations.en.TRANSLATION_ID, 66 | 'Wrong english translation' 67 | ); 68 | oplI18nService.setLanguage('fr'); 69 | assert.equal( 70 | oplI18nService.translate('TRANSLATION_ID'), 71 | expectedTranslations.fr.TRANSLATION_ID, 72 | 'Wrong french translation' 73 | ); 74 | }); 75 | 76 | it('should translate string in english if no translation found for the curent language', function() { 77 | expectedTranslations.en.TRANSLATION_ID = 'English'; 78 | oplI18nService.setLanguage('fr'); 79 | assert.equal( 80 | oplI18nService.translate('TRANSLATION_ID'), 81 | expectedTranslations.en.TRANSLATION_ID, 82 | 'Wrong translation' 83 | ); 84 | }); 85 | 86 | it('should not translate string if no translation found', function() { 87 | var expectedTranslation = 'wrong translation id'; 88 | assert.equal(oplI18nService.translate(expectedTranslation), expectedTranslation, 'Wrong translation'); 89 | }); 90 | 91 | it('should replace specified placeholders by associated values in the translated string', function() { 92 | var expectedParameters = { 93 | '%placeholder%': 'Substitution text' 94 | }; 95 | expectedTranslations.en.TRANSLATION_ID = 'Text to replace: %placeholder%'; 96 | oplI18nService.setLanguage('en'); 97 | assert.equal( 98 | oplI18nService.translate('TRANSLATION_ID', expectedParameters), 99 | expectedTranslations.en.TRANSLATION_ID.replace('%placeholder%', expectedParameters['%placeholder%']), 100 | 'Wrong translation' 101 | ); 102 | }); 103 | 104 | }); 105 | 106 | }); 107 | -------------------------------------------------------------------------------- /src/components/shared/millisecondsToTime.filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Creates a filter to convert a time in milliseconds to an hours:minutes:seconds format. 11 | * 12 | * e.g. 13 | * {{60000 | oplMillisecondsToTime}} // 01:00 14 | * {{3600000 | oplMillisecondsToTime}} // 01:00:00 15 | * 16 | * @class oplMillisecondsToTime 17 | */ 18 | function oplMillisecondsToTime() { 19 | return function(time) { 20 | if (time < 0 || isNaN(time)) 21 | return ''; 22 | 23 | time = parseInt(time); 24 | 25 | var seconds = parseInt((time / 1000) % 60); 26 | var minutes = parseInt((time / (60000)) % 60); 27 | var hours = parseInt((time / (3600000)) % 24); 28 | 29 | hours = (hours < 10) ? '0' + hours : hours; 30 | minutes = (minutes < 10) ? '0' + minutes : minutes; 31 | seconds = (seconds < 10) ? '0' + seconds : seconds; 32 | 33 | return ((hours !== '00') ? hours + ':' : '') + minutes + ':' + seconds; 34 | }; 35 | } 36 | 37 | app.filter('oplMillisecondsToTime', oplMillisecondsToTime); 38 | 39 | })(angular.module('ov.player')); 40 | -------------------------------------------------------------------------------- /src/components/shared/millisecondsToTime.filter.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.assert = chai.assert; 4 | 5 | describe('oplMillisecondsToTimeFilter', function() { 6 | var millisecondsToTimeFilter; 7 | var $filter; 8 | 9 | // Load module player 10 | beforeEach(module('ov.player')); 11 | 12 | // Dependencies injections 13 | beforeEach(inject(function(_$filter_) { 14 | $filter = _$filter_; 15 | })); 16 | 17 | // Initializes tests 18 | beforeEach(function() { 19 | millisecondsToTimeFilter = $filter('oplMillisecondsToTime'); 20 | }); 21 | 22 | it('should return an empty String if time < 0', function() { 23 | var emptyTime = millisecondsToTimeFilter(-1); 24 | assert.notOk(emptyTime, 'Expected time to be empty'); 25 | assert.isString(emptyTime, 'Expected time to be String'); 26 | }); 27 | 28 | it('should return an empty String if time is undefined', function() { 29 | var emptyTime = millisecondsToTimeFilter(undefined); 30 | assert.notOk(emptyTime, 'Expected time to be empty'); 31 | assert.isString(emptyTime, 'Expected time to be String'); 32 | }); 33 | 34 | it('should be able to convert milliseconds to hh:mm:ss', function() { 35 | var time = millisecondsToTimeFilter(8804555); 36 | assert.equal(time, '02:26:44', 'Wrong time'); 37 | }); 38 | 39 | it('should be able to convert milliseconds to mm:ss while no hours', function() { 40 | var time = millisecondsToTimeFilter(884555); 41 | assert.equal(time, '14:44', 'Wrong time'); 42 | }); 43 | 44 | it('should be able to convert milliseconds to 00:ss while no hours and no minutes', function() { 45 | var time = millisecondsToTimeFilter(5500); 46 | assert.equal(time, '00:05', 'Wrong time'); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/shared/scroller/scroller.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as an opl-scroller HTML element to scroll a content. 9 | * 10 | * opl-scroller is composed of a container, the transcluded content to scroll and a scroll bar. 11 | * 12 | * Scroller value must be between 0 and 100. 13 | * 14 | * Attributes are: 15 | * - [String] **opl-id** The unique id of the scrollable content. 16 | * - [Number] **opl-step** The step to use when scrolling using keyboard (Default to 10). 17 | * - [Boolean] **opl-no-sequential-focus** true to set scrollbar tabindex to -1, false to set scrollbar tabindex to 0 18 | * - [Boolean] **opl-deactivated** "true" to deactivate the scroller 19 | * - [String] **opl-orientation** The scrollbar orientation, either "horizontal" or "vertical" 20 | * - [Function] **opl-on-touch** The function to call when scroll is manipulated by the user 21 | * - [Function] **opl-on-ready** The function to call when the scroller is ready 22 | * 23 | * @example 24 | * var value = 42; 25 | * var deactivated = false; 26 | * 27 | * var handleScrollTouch = function() { 28 | * console.log('Scroller manually updated'); 29 | * }; 30 | * 31 | * var handleOnReady = function() { 32 | * console.log('Scroller ready'); 33 | * }; 34 | * 35 | * 45 | * 46 | * @class oplScroller 47 | */ 48 | (function(app) { 49 | 50 | app.component('oplScroller', { 51 | templateUrl: 'opl-scroller.html', 52 | controller: 'OplScrollerController', 53 | require: ['?ngModel'], 54 | transclude: true, 55 | bindings: { 56 | oplId: '@?', 57 | oplStep: '@?', 58 | oplNoSequentialFocus: '@?', 59 | oplDeactivated: '<', 60 | oplOrientation: '@?', 61 | oplOnTouch: '&', 62 | oplOnReady: '&' 63 | } 64 | }); 65 | 66 | })(angular.module('ov.player')); 67 | -------------------------------------------------------------------------------- /src/components/shared/scroller/scroller.html: -------------------------------------------------------------------------------- 1 |
2 |
12 |
13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /src/components/shared/scroller/scroller.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/user-interface"; 2 | @import "compass/css3/transform"; 3 | @import "compass/css3/transition"; 4 | 5 | $OPL_SCROLLER_PRIMARY_COLOR: #000000; 6 | 7 | opl-scroller { 8 | width: 100%; 9 | height: 100%; 10 | } 11 | 12 | .opl-scroller { 13 | --opl-scroller-primary-color: #{$OPL_SCROLLER_PRIMARY_COLOR}; 14 | --opl-scroller-primary-color-0: #{rgba($OPL_SCROLLER_PRIMARY_COLOR, 0.2)}; 15 | --opl-scroller-primary-color-1: #{rgba($OPL_SCROLLER_PRIMARY_COLOR, 0.4)}; 16 | --opl-scroller-primary-color-2: #{rgba($OPL_SCROLLER_PRIMARY_COLOR, 0.6)}; 17 | position: relative; 18 | width: 100%; 19 | height: 100%; 20 | outline: 0; 21 | @include user-select(none); 22 | 23 | /* 24 | Scrollbar in resting state. 25 | */ 26 | .opl-scrollbar { 27 | position: absolute; 28 | z-index: 2; 29 | opacity: 0; 30 | background-color: var(--opl-scroller-primary-color-0, rgba($OPL_SCROLLER_PRIMARY_COLOR, 0.2)); 31 | cursor: pointer; 32 | will-change: opacity; 33 | } 34 | 35 | /* 36 | Vertical scrollbar in resting state. 37 | */ 38 | &.opl-vertical > .opl-scrollbar { 39 | right: 0; 40 | width: 8px; 41 | height: 100%; 42 | } 43 | 44 | /* 45 | Horizontal scrollbar in resting state. 46 | */ 47 | &.opl-horizontal > .opl-scrollbar { 48 | bottom: 0; 49 | width: 100%; 50 | height: 8px; 51 | } 52 | 53 | /* 54 | Cursor in resting state. 55 | */ 56 | .opl-thumb { 57 | position: absolute; 58 | z-index: 2; 59 | opacity: 0; 60 | border-radius: 8px; 61 | background-color: var(--opl-scroller-primary-color-0, rgba($OPL_SCROLLER_PRIMARY_COLOR, 0.2)); 62 | cursor: pointer; 63 | will-change: opacity, transform; 64 | @include transition-property(transform, -webkit-transform, opacity); 65 | @include transition-timing-function(ease, ease, cubic-bezier(0.4, 0.0, 1, 1)); 66 | @include transition-duration(80ms, 80ms, 100ms); 67 | 68 | /* 69 | Cursor in over state. 70 | */ 71 | &.opl-thumb-over { 72 | background-color: var(--opl-scroller-primary-color-1, rgba($OPL_SCROLLER_PRIMARY_COLOR, 0.4)); 73 | } 74 | 75 | /* 76 | Cursor in active state. 77 | */ 78 | &.opl-thumb-active { 79 | opacity: 1; 80 | background-color: var(--opl-scroller-primary-color-2, rgba($OPL_SCROLLER_PRIMARY_COLOR, 0.6)); 81 | } 82 | } 83 | 84 | /* 85 | Vertical scrollbar cursor in resting state. 86 | */ 87 | &.opl-vertical > .opl-thumb { 88 | top: 0; 89 | right: 2px; 90 | width: 4px; 91 | @include transform-origin(top, left); 92 | @include transform(translateY(0)); 93 | @include scaleX(1); 94 | } 95 | 96 | /* 97 | Horizontal scrollbar cursor in resting state. 98 | */ 99 | &.opl-horizontal > .opl-thumb { 100 | bottom: 2px; 101 | left: 0; 102 | height: 4px; 103 | @include transform-origin(bottom, left); 104 | @include transform(translateX(0)); 105 | @include scaleY(1); 106 | } 107 | 108 | /* 109 | Scroller in over state and focus state. 110 | */ 111 | &.opl-scroller-over, 112 | &.opl-scroller-focus, 113 | { 114 | 115 | /* 116 | Cursor in over and focus state. 117 | */ 118 | & > .opl-thumb { 119 | opacity: 1; 120 | } 121 | 122 | } 123 | 124 | /* 125 | Scrollable content. 126 | */ 127 | .opl-scroller-content { 128 | position: relative; 129 | overflow: hidden; 130 | width: 100%; 131 | height: 100%; 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/components/shared/settings/settings.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as an opl-settings HTML element to create player settings. 9 | * 10 | * opl-settings is composed of a button and a list of settings (qualities and sources). The button and the list of 11 | * settings use the OpenVeo Player Icons font. 12 | * 13 | * Attributes are: 14 | * - [Array] **opl-qualities** The list of available video qualities 15 | * - [Array] **opl-sources** The list of available video sources 16 | * - [String] **opl-quality** The selected quality 17 | * - [String] **opl-source** The selected source 18 | * - [Function] **opl-on-update** The function to call when quality or source changes 19 | * - [Function] **opl-on-focus** The function to call when component enters in focus state 20 | * 21 | * Requires: 22 | * - **oplTranslate** OpenVeo Player i18n filter 23 | * 24 | * @example 25 | * var handleOnUpdate = function(quality, source) { 26 | * if (quality) console.log('Quality selected:' + quality); 27 | * if (source) console.log('Source selected:' + source); 28 | * }; 29 | * var handleOnFocus = function() { 30 | * console.log('Component has received focus'); 31 | * }; 32 | * var qualities = [ 33 | * {id: '1', label: '360p', hd: false}, 34 | * {id: '2', label: '720p', hd: true} 35 | * ]; 36 | * var sources = [ 37 | * {id: '1', label: 'Source 1'}, 38 | * {id: '2', label: 'Source 2'} 39 | * ]; 40 | * 41 | * 49 | * 50 | * @class oplSettings 51 | */ 52 | (function(app) { 53 | 54 | app.component('oplSettings', { 55 | templateUrl: 'opl-settings.html', 56 | controller: 'OplSettingsController', 57 | bindings: { 58 | oplQualities: '<', 59 | oplSources: '<', 60 | oplQuality: '@?', 61 | oplSource: '@?', 62 | oplOnUpdate: '&', 63 | oplOnFocus: '&' 64 | } 65 | }); 66 | 67 | })(angular.module('ov.player')); 68 | -------------------------------------------------------------------------------- /src/components/shared/settings/settings.html: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 |
10 |

11 |
    12 |
  • 22 | check 23 | 24 | hd 25 |
  • 26 |
27 |

28 |
    29 |
  • 39 | check 40 | 41 |
  • 42 |
43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /src/components/shared/settings/settings.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/user-interface"; 2 | @import "compass/css3/transform"; 3 | @import "compass/css3/transition"; 4 | @import "compass/css3/animation"; 5 | @import "compass/css3/box-shadow"; 6 | 7 | $OPL_SETTINGS_PRIMARY_COLOR: #ffffff; 8 | $OPL_SETTINGS_BACKGROUND_COLOR: #000000; 9 | $OPL_SETTINGS_ACCENT_COLOR: #f63d98; 10 | 11 | /* 12 | Animate the settings item ripple with a growing effect from 0 to 1100%. 13 | */ 14 | @include keyframes(opl-settings-item-ripple-radius-in) { 15 | from { 16 | @include animation-timing-function(cubic-bezier(0.4, 0, 0.2, 1)); 17 | @include scale(0); 18 | } 19 | to { 20 | @include scale(11); 21 | } 22 | } 23 | 24 | /* 25 | Animate the settings item ripple with a resolve effect from 0 to 0.32. 26 | */ 27 | @include keyframes(opl-settings-item-ripple-resolve) { 28 | from { 29 | @include animation-timing-function(cubic-bezier(0.4, 0, 0.2, 1)); 30 | opacity: 0; 31 | } 32 | to { 33 | opacity: 0.32; 34 | } 35 | } 36 | 37 | /* 38 | Animate the settings item ripple with a dissolve effect from actual opacity to 0. 39 | */ 40 | @include keyframes(opl-settings-item-ripple-dissolve) { 41 | from { 42 | @include animation-timing-function(linear); 43 | } 44 | to { 45 | opacity: 0; 46 | } 47 | } 48 | 49 | /* 50 | Animate the settings dialog with grow effect. 51 | */ 52 | @include keyframes(opl-settings-grow) { 53 | 0% { 54 | @include animation-timing-function(cubic-bezier(0.4, 0, 0.2, 1)); 55 | width: 0; 56 | height: 0; 57 | } 58 | 100% { 59 | @include transform(translateX(-194px)); 60 | width: 226px; 61 | height: var(--opl-settings-dialog-height); 62 | } 63 | } 64 | 65 | /* 66 | Animate the settings dialog with reduce effect. 67 | */ 68 | @include keyframes(opl-settings-reduce) { 69 | from { 70 | @include animation-timing-function(linear); 71 | @include transform(translateX(-194px)); 72 | width: 226px; 73 | height: var(--opl-settings-dialog-height); 74 | } 75 | to { 76 | width: 0; 77 | height: 0; 78 | } 79 | } 80 | 81 | /* 82 | Animate the settings dialog with a resolve effect. 83 | */ 84 | @include keyframes(opl-settings-resolve) { 85 | from { 86 | @include animation-timing-function(cubic-bezier(0.4, 0, 0.2, 1)); 87 | opacity: 0; 88 | } 89 | to { 90 | opacity: 1; 91 | } 92 | } 93 | 94 | /* 95 | Animate the settings dialog with a dissolve effect. 96 | */ 97 | @include keyframes(opl-settings-dissolve) { 98 | from { 99 | @include animation-timing-function(linear); 100 | opacity: 1; 101 | } 102 | to { 103 | opacity: 0; 104 | } 105 | } 106 | 107 | opl-settings { 108 | display: inline-block; 109 | width: 32px; 110 | height: 32px; 111 | vertical-align: top; 112 | } 113 | 114 | .opl-settings { 115 | --opl-settings-primary-color: #{$OPL_SETTINGS_PRIMARY_COLOR}; 116 | --opl-settings-primary-color-0: #{rgba($OPL_SETTINGS_PRIMARY_COLOR, 0.2)}; 117 | --opl-settings-primary-color-1: #{rgba($OPL_SETTINGS_PRIMARY_COLOR, 0.9)}; 118 | --opl-settings-background-color: #{$OPL_SETTINGS_BACKGROUND_COLOR}; 119 | --opl-settings-background-color-0: #{rgba($OPL_SETTINGS_BACKGROUND_COLOR, 0.8)}; 120 | --opl-settings-accent-color: #{$OPL_SETTINGS_ACCENT_COLOR}; 121 | position: relative; 122 | display: inline-block; 123 | width: 32px; 124 | height: 32px; 125 | vertical-align: top; 126 | 127 | /* 128 | Settings dialog in resting state. 129 | */ 130 | .opl-dialog { 131 | position: absolute; 132 | bottom: 64px; 133 | left: 0; 134 | width: 226px; 135 | background-color: var(--opl-settings-background-color-0, rgba($OPL_SETTINGS_BACKGROUND_COLOR, 0.8)); 136 | will-change: transform, opacity; 137 | opacity: 0; 138 | @include box-shadow(0px 4px 24px var(--opl-settings-background-color, $OPL_SETTINGS_BACKGROUND_COLOR)); 139 | @include transform-origin(bottom, left); 140 | } 141 | 142 | /* 143 | Settings dialog in posting state. 144 | */ 145 | &.opl-posting .opl-dialog { 146 | @include animation-name(opl-settings-grow, opl-settings-resolve); 147 | @include animation-duration(250ms, 250ms); 148 | @include animation-fill-mode(forwards, forwards); 149 | 150 | /* 151 | Dialog content in posting state. 152 | */ 153 | & > div { 154 | @include animation-name(opl-settings-resolve); 155 | @include animation-duration(500ms); 156 | @include animation-fill-mode(forwards); 157 | } 158 | } 159 | 160 | /* 161 | Settings dialog in masking state. 162 | */ 163 | &.opl-masking .opl-dialog { 164 | @include animation-name(opl-settings-reduce, opl-settings-dissolve); 165 | @include animation-duration(150ms, 150ms); 166 | @include animation-fill-mode(forwards, forwards); 167 | 168 | /* 169 | Dialog content in masking state. 170 | */ 171 | & > div { 172 | @include animation-name(opl-settings-dissolve); 173 | @include animation-duration(75ms); 174 | @include animation-fill-mode(forwards); 175 | } 176 | } 177 | 178 | /* 179 | Settings dialog in posted state. 180 | */ 181 | &.opl-posted .opl-dialog { 182 | opacity: 1; 183 | width: 226px; 184 | height: var(--opl-settings-dialog-height); 185 | @include transform(translateX(-194px)); 186 | 187 | /* 188 | Dialog content in masking state. 189 | */ 190 | & > div { 191 | opacity: 1; 192 | } 193 | } 194 | 195 | h1 { 196 | font-family: 'Roboto-Medium', sans-serif; 197 | background-color: var(--opl-settings-primary-color-0, rgba($OPL_SETTINGS_PRIMARY_COLOR, 0.20)); 198 | height: 32px; 199 | margin: 0; 200 | padding-left: 8px; 201 | line-height: 32px; 202 | letter-spacing: 0.1px; 203 | font-size: 14px; 204 | color: var(--opl-settings-primary-color-1, rgba($OPL_SETTINGS_PRIMARY_COLOR, 0.9)); 205 | } 206 | 207 | ul { 208 | margin: 0; 209 | padding: 0; 210 | list-style: none; 211 | } 212 | 213 | /* 214 | Item (quality / source). 215 | */ 216 | li { 217 | font-family: 'Roboto', sans-serif; 218 | overflow: hidden; 219 | position: relative; 220 | height: 32px; 221 | line-height: 32px; 222 | padding: 0 8px; 223 | outline: none; 224 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 225 | will-change: transform, opacity; 226 | cursor: pointer; 227 | text-overflow: ellipsis; 228 | white-space: nowrap; 229 | color: var(--opl-settings-primary-color-1, rgba($OPL_SETTINGS_PRIMARY_COLOR, 0.9)); 230 | @include user-select(none); 231 | 232 | span { 233 | font-size: 12px; 234 | vertical-align: middle; 235 | } 236 | 237 | /* 238 | Item label in resting state. 239 | */ 240 | .opl-item-label { 241 | padding-left: 26px; 242 | letter-spacing: 0.4px; 243 | } 244 | 245 | /* 246 | Selected icon in resting state. 247 | */ 248 | .opl-check { 249 | padding-top: 5px; 250 | vertical-align: top; 251 | font-size: 22px; 252 | } 253 | 254 | /* 255 | HD icon in resting state. 256 | */ 257 | .opl-hd { 258 | font-size: 22px; 259 | } 260 | 261 | /* 262 | Ripple and focus overlay in resting state. 263 | */ 264 | &::after, &::before { 265 | position: absolute; 266 | top: 0; 267 | height: 100%; 268 | content: ''; 269 | opacity: 0; 270 | pointer-events: none; 271 | } 272 | 273 | /* 274 | Focus overlay in resting state. 275 | */ 276 | &::before { 277 | @include transition-property(opacity); 278 | @include transition-timing-function(linear); 279 | @include transition-duration(15ms); 280 | left: 0; 281 | width: 100%; 282 | background-color: var(--opl-settings-primary-color, $OPL_SETTINGS_PRIMARY_COLOR); 283 | } 284 | 285 | /* 286 | Ripple in resting state. 287 | */ 288 | &::after { 289 | @include scale(0); 290 | @include transform-origin(center, center); 291 | left: -10%; 292 | width: 20%; 293 | border-radius: 50%; 294 | background-color: var(--opl-settings-primary-color, $OPL_SETTINGS_PRIMARY_COLOR); 295 | } 296 | 297 | /* 298 | Focus overlay in focus state. 299 | */ 300 | &.opl-focus::before { 301 | opacity: 0.24; 302 | } 303 | 304 | /* 305 | Item in selected state. 306 | */ 307 | &.opl-selected { 308 | 309 | span { 310 | color: var(--opl-settings-accent-color, $OPL_SETTINGS_ACCENT_COLOR); 311 | } 312 | 313 | /* 314 | Item label in selected state. 315 | */ 316 | .opl-item-label { 317 | padding-left: 0; 318 | } 319 | 320 | /* 321 | Focus overlay in both hover and selected states. 322 | */ 323 | &::before { 324 | background-color: var(--opl-settings-accent-color, $OPL_SETTINGS_ACCENT_COLOR); 325 | } 326 | 327 | /* 328 | Ripple in selected state. 329 | */ 330 | &::after { 331 | background-color: var(--opl-settings-accent-color, $OPL_SETTINGS_ACCENT_COLOR); 332 | } 333 | 334 | } 335 | 336 | /* 337 | Focus overlay in hover state. 338 | */ 339 | &:hover::before { 340 | opacity: 0.078; 341 | } 342 | 343 | /* 344 | Ripple in item activation state. 345 | */ 346 | &.opl-item-activation::after { 347 | @include animation-name(opl-settings-item-ripple-radius-in, opl-settings-item-ripple-resolve); 348 | @include animation-duration(225ms, 75ms); 349 | @include animation-fill-mode(forwards, forwards); 350 | } 351 | 352 | /* 353 | Ripple in item deactivation state. 354 | */ 355 | &.opl-item-deactivation::after { 356 | @include animation-name(opl-settings-item-ripple-dissolve); 357 | @include animation-duration(150ms); 358 | @include scale(1); 359 | } 360 | 361 | } 362 | 363 | } 364 | -------------------------------------------------------------------------------- /src/components/shared/slider/slider.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as an opl-slider HTML element to be able to set a value between 0% and 100% 9 | * using a slider. 10 | * 11 | * opl-slider is based on the Material specification but implements it partially. Only the continuous slider is 12 | * implemented. 13 | * 14 | * It could have been the HTML type="range" slider unfortunately it is not enough customizable. 15 | * It could also have been the slider component from Material but it embeds more features than required by the OpenVeo 16 | * Player, it adds another dependency to the OpenVeo Player and an AngularJS wrapper should have been implemented 17 | * anyway. 18 | * 19 | * opl-slider is composed of a track bar and a thumb. 20 | * 21 | * Attributes are: 22 | * - [Number] **opl-step** The step to use, it won't be possible to place the slider outside steps it set. 23 | * Default to 0 (no step) meaning that the slider can be drag & drop at any value of the slider and will move one by 24 | * one when using keyboard. 25 | * - [String] **opl-label** The ARIA label of the slider. Default to "Select a value". 26 | * - [String] **opl-value-text** The human readable text alternative of the slider value. Text will be processed by 27 | * oplTranslate filter and supports parameter "%value%". Empty by default. 28 | * - [Boolean] **opl-no-sequential-focus** true to set slider tabindex to -1, false to set slider tabindex to 0 29 | * - [Function] **opl-on-focus** The function to call when component enters in focus state 30 | * - [Function] **opl-on-over** The function to call when pointer enters the component 31 | * - [Function] **opl-on-out** The function to call when pointer leaves the component 32 | * - [Function] **opl-on-move** The function to call when pointer moves over the component 33 | * 34 | * Requires: 35 | * - **oplTranslate** OpenVeo Player i18n filter 36 | * 37 | * @example 38 | * var handleOnFocus = function() { 39 | * console.log('Component has received focus'); 40 | * }; 41 | * var handleOnOver = function() { 42 | * console.log('Pointer has entered the component'); 43 | * }; 44 | * var handleOnOut = function() { 45 | * console.log('Pointer has left the component'); 46 | * }; 47 | * var handleOnMove = function(value, coordinates, sliderBoundingRectangle) { 48 | * console.log( 49 | * 'Pointer has moved over value ' + value + ' in coordinates (' + coordinates.x + ',' + coordinates.y + ')' 50 | * ); 51 | * console.log(sliderBoundingRectangle); 52 | * }; 53 | * var sliderValue = 50; 54 | * 55 | * 66 | * 67 | * @class oplSlider 68 | */ 69 | (function(app) { 70 | 71 | app.component('oplSlider', { 72 | templateUrl: 'opl-slider.html', 73 | controller: 'OplSliderController', 74 | require: ['?ngModel'], 75 | bindings: { 76 | oplStep: '@?', 77 | oplLabel: '@?', 78 | oplValueText: '@?', 79 | oplNoSequentialFocus: '@?', 80 | oplOnFocus: '&', 81 | oplOnOver: '&', 82 | oplOnOut: '&', 83 | oplOnMove: '&' 84 | } 85 | }); 86 | 87 | })(angular.module('ov.player')); 88 | -------------------------------------------------------------------------------- /src/components/shared/slider/slider.html: -------------------------------------------------------------------------------- 1 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /src/components/shared/slider/slider.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/animation"; 2 | @import "compass/css3/transition"; 3 | @import "compass/css3/transform"; 4 | 5 | $OPL_SLIDER_PRIMARY_COLOR: #000000; 6 | $OPL_SLIDER_ACCENT_COLOR: #000000; 7 | 8 | /* 9 | Animation to emphasize an element with an inflate / deflate effect. 10 | */ 11 | @include keyframes(opl-slider-emphasize) { 12 | 0% { 13 | @include animation-timing-function(ease-out); 14 | } 15 | 50% { 16 | @include animation-timing-function(ease-in); 17 | @include scale(0.85); 18 | } 19 | 100% { 20 | @include scale(0.5); 21 | } 22 | } 23 | 24 | opl-slider { 25 | display: inline-block; 26 | width: 100%; 27 | height: 32px; 28 | } 29 | 30 | .opl-slider { 31 | --opl-slider-primary-color: #{$OPL_SLIDER_PRIMARY_COLOR}; 32 | --opl-slider-primary-color-0: #{rgba($OPL_SLIDER_PRIMARY_COLOR, .098)}; 33 | --opl-slider-accent-color: #{$OPL_SLIDER_ACCENT_COLOR}; 34 | position: relative; 35 | width: 100%; 36 | height: 32px; 37 | cursor: pointer; 38 | touch-action: pan-x; 39 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 40 | 41 | .opl-slider-track-container { 42 | position: absolute; 43 | top: 14px; 44 | width: 100%; 45 | height: 4px; 46 | background-color: var(--opl-slider-primary-color-0, rgba($OPL_SLIDER_PRIMARY_COLOR, .098)); 47 | 48 | /* 49 | Hack for MS Edge which has rendering issues when transitioning the track bar, making the track bar 50 | appear higher while no transformation is operated on the height of the element. 51 | */ 52 | overflow: hidden; 53 | } 54 | 55 | .opl-slider-track { 56 | position: absolute; 57 | width: 100%; 58 | height: 100%; 59 | background-color: var(--opl-slider-accent-color, $OPL_SLIDER_ACCENT_COLOR); 60 | @include transform-origin(left, top); 61 | will-change: transform; 62 | } 63 | 64 | .opl-slider-thumb-container { 65 | position: absolute; 66 | width: 32px; 67 | height: 32px; 68 | @include translateX(-50%); 69 | } 70 | 71 | /* 72 | Thumb size is fixed by the SVG circle element. 73 | Thumb resting size is half the size of the SVG circle. 74 | */ 75 | .opl-slider-thumb { 76 | position: absolute; 77 | top: 4px; 78 | left: 4px; 79 | fill: var(--opl-slider-accent-color, $OPL_SLIDER_ACCENT_COLOR); 80 | stroke: var(--opl-slider-accent-color, $OPL_SLIDER_ACCENT_COLOR); 81 | @include scale(0.5); 82 | @include transition-property(transform, fill, stroke, -webkit-transform); 83 | @include transition-timing-function(ease-out, ease-out, ease-out, ease-out); 84 | @include transition-duration(0.1s, 0.1s, 0.1s, 0.1s); 85 | } 86 | 87 | /* 88 | Ring resting state size is the same size as the SVG circle of the thumb. 89 | Ring resting state visibility is hidden. 90 | */ 91 | .opl-slider-focus-ring { 92 | position: absolute; 93 | top: 4px; 94 | left: 4px; 95 | width: 24px; 96 | height: 24px; 97 | background-color: var(--opl-slider-accent-color, $OPL_SLIDER_ACCENT_COLOR); 98 | border-radius: 50%; 99 | opacity: 0; 100 | @include transition-property(transform, opacity, -webkit-transform); 101 | @include transition-timing-function(ease-out, ease-out, ease-out); 102 | @include transition-duration(.26667s, .26667s, .26667s); 103 | } 104 | 105 | &:focus { 106 | outline: none; 107 | 108 | /* 109 | Slider controlled by the keyboard. 110 | */ 111 | &:not(.opl-slider-active) .opl-slider-thumb-container, 112 | &:not(.opl-slider-active) .opl-slider-track { 113 | @include transition-property(transform, -webkit-transform); 114 | @include transition-timing-function(ease, ease); 115 | @include transition-duration(80ms, 80ms, 80ms); 116 | } 117 | 118 | } 119 | 120 | /* 121 | Slider becomes in transition while thumb container is moving. 122 | */ 123 | &.opl-slider-in-transition { 124 | 125 | & .opl-slider-thumb-container, 126 | & .opl-slider-track { 127 | @include transition-property(transform, -webkit-transform); 128 | @include transition-timing-function(ease, ease); 129 | @include transition-duration(80ms, 80ms, 80ms); 130 | } 131 | 132 | /* 133 | Delay thumb transition to make sure it does not happen when clicking on the slider but when moving the slider. 134 | */ 135 | .opl-slider-thumb { 136 | @include transition-delay(100ms); 137 | } 138 | 139 | } 140 | 141 | /* 142 | Slider gets focused when using the keyboard. 143 | */ 144 | &.opl-slider-focus { 145 | 146 | /* 147 | Ring focus visibility is visible. 148 | */ 149 | .opl-slider-focus-ring { 150 | opacity: 0.36; 151 | @include scale3d(1.3333, 1.3333, 1.3333); 152 | } 153 | 154 | /* 155 | Thumb gets animated on focus with an inflate / deflate effect ending to its resting state. 156 | */ 157 | .opl-slider-thumb { 158 | @include animation(opl-slider-emphasize 0.26667s linear); 159 | } 160 | 161 | } 162 | 163 | /* 164 | Slider becomes active when using a pointer. 165 | */ 166 | &.opl-slider-active { 167 | 168 | /* 169 | Thumb active size is the size of the SVG circle. 170 | */ 171 | .opl-slider-thumb { 172 | @include scale(1); 173 | } 174 | 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/components/shared/tabs/tabs.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as HTML element opl-tabs to be able to manage a list of views (opl-view elements) 9 | * and switch between them using tabs. 10 | * 11 | * opl-tabs element does not have any attributes. 12 | * 13 | * Attributes are: 14 | * - [String] **opl-no-tabs** "true" to hide tabs (without hiding the view) 15 | * - [Function] **opl-on-select** The function to call when a view is actioned 16 | * 17 | * @example 18 | * var handleOnSelect = function() { 19 | * console.log('Button actioned'); 20 | * }; 21 | * 22 | * 23 | * 24 | * Content of the first view 25 | * 26 | * 27 | * Content of the second view 28 | * 29 | * 30 | * 31 | * @class oplTabs 32 | */ 33 | (function(app) { 34 | 35 | app.component('oplTabs', { 36 | templateUrl: 'opl-tabs.html', 37 | controller: 'OplTabsController', 38 | transclude: true, 39 | bindings: { 40 | oplNoTabs: '@?', 41 | oplOnSelect: '&' 42 | } 43 | }); 44 | 45 | })(angular.module('ov.player')); 46 | -------------------------------------------------------------------------------- /src/components/shared/tabs/tabs.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Manages oplTabs component. 11 | * 12 | * @param {Object} $scope Component isolated scope 13 | * @param {Object} $filter The AngularJS $filter service 14 | * @param {Object} $element The component HTML element 15 | * @param {Object} $timeout AngularJS $timeout service 16 | * @class OplTabsController 17 | * @constructor 18 | */ 19 | function OplTabsController($scope, $filter, $element, $timeout) { 20 | var ctrl = this; 21 | var tabsListElement; 22 | var tabsWrapperElement; 23 | var buttonElements; 24 | var leaveRequested = false; 25 | 26 | /** 27 | * Focus previous/next tab based on focused tab. 28 | * 29 | * @param {Boolean} next true to focus next tab, false to focus previous tab 30 | */ 31 | function focusSiblingTab(next) { 32 | 33 | // Find focused tab 34 | var focusedTabIndex = 0; 35 | for (var i = 0; i < buttonElements.length; i++) { 36 | if (angular.element(buttonElements[i]).controller('oplButton').isFocused()) { 37 | focusedTabIndex = i; 38 | break; 39 | } 40 | } 41 | 42 | var siblingTabIndex = next ? focusedTabIndex + 1 : focusedTabIndex - 1; 43 | 44 | if (siblingTabIndex >= buttonElements.length) siblingTabIndex = 0; 45 | if (siblingTabIndex < 0) siblingTabIndex = buttonElements.length - 1; 46 | 47 | angular.element(buttonElements[siblingTabIndex]).controller('oplButton').focus(); 48 | } 49 | 50 | /** 51 | * Updates the list of tab elements. 52 | */ 53 | function updateTabElementsList() { 54 | $timeout(function() { 55 | buttonElements = angular.element($element[0].querySelectorAll('.opl-tabs opl-button')); 56 | }); 57 | } 58 | 59 | /** 60 | * Handles keydown events. 61 | * 62 | * Tabs component captures the following keyboard keys: 63 | * - LEFT and TOP keys to select previous tab 64 | * - RIGHT and BOTTOM keys to select next tab 65 | * 66 | * Captured keys will prevent default browser actions. 67 | * 68 | * @param {KeyboardEvent} event The captured event 69 | */ 70 | function handleKeyDown(event) { 71 | if ((event.key === 'Tab' || event.keyCode === 9) && event.shiftKey) { 72 | leaveRequested = true; 73 | return; 74 | } else if ((event.key === 'ArrowLeft' || event.keyCode === 37) || 75 | (event.key === 'ArrowUp' || event.keyCode === 38) 76 | ) { 77 | focusSiblingTab(false); 78 | } else if ((event.key === 'ArrowRight' || event.keyCode === 39) || 79 | (event.key === 'ArrowDown' || event.keyCode === 40) 80 | ) { 81 | focusSiblingTab(true); 82 | } else return; 83 | 84 | event.preventDefault(); 85 | } 86 | 87 | /** 88 | * Handles focus event. 89 | * 90 | * Add focus class to the wrapper element. 91 | * 92 | * @param {FocusEvent} event The captured event 93 | */ 94 | function handleFocus(event) { 95 | tabsWrapperElement.addClass('opl-focus'); 96 | 97 | if (leaveRequested) { 98 | leaveRequested = false; 99 | return; 100 | } 101 | 102 | angular.element(buttonElements[0]).controller('oplButton').focus(); 103 | } 104 | 105 | /** 106 | * Handles blur event. 107 | * 108 | * Remove focus class from the wrapper element. 109 | * 110 | * @param {FocusEvent} event The captured event 111 | */ 112 | function handleBlur(event) { 113 | leaveRequested = false; 114 | tabsWrapperElement.removeClass('opl-focus'); 115 | } 116 | 117 | Object.defineProperties(ctrl, { 118 | 119 | /** 120 | * The list of registered views. 121 | * 122 | * @property views 123 | * @type Array 124 | * @final 125 | */ 126 | views: { 127 | value: [] 128 | }, 129 | 130 | /** 131 | * Selects a view. 132 | * 133 | * @method selectViewById 134 | * @param {String} viewId The id of the view to select 135 | */ 136 | selectViewById: { 137 | value: function(viewId) { 138 | var view = $filter('filter')( 139 | ctrl.views, 140 | { 141 | oplViewId: viewId 142 | }, 143 | true 144 | ); 145 | if (view.length != 0) ctrl.select(view[0]); 146 | } 147 | }, 148 | 149 | /** 150 | * Adds the scope of an oplView directive to the list of tabs. 151 | * 152 | * @method addView 153 | * @param {Object} view The oplView to add to tabs 154 | */ 155 | addView: { 156 | value: function(view) { 157 | if (!ctrl.views.length) 158 | ctrl.select(view); 159 | 160 | ctrl.views.push(view); 161 | updateTabElementsList(); 162 | } 163 | }, 164 | 165 | /** 166 | * Removes an oplView directive from the list of tabs. 167 | * 168 | * @method removeView 169 | * @param {Object} view The oplView to remove from tabs 170 | */ 171 | removeView: { 172 | value: function(view) { 173 | var index = ctrl.views.indexOf(view); 174 | 175 | if (index !== -1) 176 | ctrl.views.splice(index, 1); 177 | 178 | updateTabElementsList(); 179 | } 180 | }, 181 | 182 | /** 183 | * Selects a view. 184 | * 185 | * @method select 186 | * @param {Object} view The oplView to select 187 | */ 188 | select: { 189 | value: function(view) { 190 | angular.forEach(ctrl.views, function(view) { 191 | view.selected = false; 192 | }); 193 | view.selected = true; 194 | } 195 | }, 196 | 197 | /** 198 | * Gets selected view. 199 | * 200 | * @method getSelectedView 201 | * @return {Object} The selected view 202 | */ 203 | getSelectedView: { 204 | value: function(view) { 205 | for (var i = 0; i < ctrl.views.length; i++) { 206 | if (ctrl.views[i].selected) return ctrl.views[i]; 207 | } 208 | return null; 209 | } 210 | }, 211 | 212 | /** 213 | * Initializes controller. 214 | * 215 | * @method $onInit 216 | */ 217 | $onInit: { 218 | value: function() { 219 | tabsWrapperElement = angular.element($element[0].querySelector('.opl-tabs-wrapper')); 220 | tabsListElement = angular.element($element[0].querySelector('.opl-tabs ul')); 221 | 222 | tabsListElement.on('keydown', handleKeyDown); 223 | tabsListElement.on('focus', handleFocus); 224 | tabsListElement.on('blur', handleBlur); 225 | } 226 | }, 227 | 228 | /** 229 | * Initializes child components. 230 | * 231 | * @method $postLink 232 | */ 233 | $postLink: { 234 | value: function() { 235 | updateTabElementsList(); 236 | } 237 | }, 238 | 239 | /** 240 | * Removes event listeners. 241 | * 242 | * @method $onDestroy 243 | */ 244 | $onDestroy: { 245 | value: function() { 246 | tabsListElement.off('keydown focus blur'); 247 | } 248 | } 249 | 250 | }); 251 | 252 | /** 253 | * Handles tab buttons actions. 254 | * 255 | * Call the function defined in "opl-on-select" attribute with the actioned view. 256 | * 257 | * @method handleButtonAction 258 | * @param {Object} view The view actioned 259 | */ 260 | $scope.handleButtonAction = function(view) { 261 | if (ctrl.oplOnSelect) ctrl.oplOnSelect({view: view}); 262 | }; 263 | 264 | } 265 | 266 | app.controller('OplTabsController', OplTabsController); 267 | OplTabsController.$inject = ['$scope', '$filter', '$element', '$timeout']; 268 | 269 | })(angular.module('ov.player')); 270 | -------------------------------------------------------------------------------- /src/components/shared/tabs/tabs.controller.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.assert = chai.assert; 4 | 5 | describe('OplTabsController', function() { 6 | var $componentController; 7 | var $rootScope; 8 | var ctrl; 9 | 10 | // Load module player 11 | beforeEach(module('ov.player')); 12 | 13 | // Dependencies injections 14 | beforeEach(inject(function(_$componentController_, _$rootScope_) { 15 | $componentController = _$componentController_; 16 | $rootScope = _$rootScope_; 17 | ctrl = $componentController('oplTabs', { 18 | $element: {} 19 | }); 20 | })); 21 | 22 | describe('addView', function() { 23 | 24 | it('should register a view', function() { 25 | var expectedViewController = {}; 26 | 27 | ctrl.addView(expectedViewController); 28 | $rootScope.$digest(); 29 | 30 | assert.lengthOf(ctrl.views, 1, 'Expected view to be registered'); 31 | }); 32 | 33 | it('should select first registered view', function() { 34 | var expectedViewControllers = [{}, {}, {}, {}]; 35 | 36 | expectedViewControllers.forEach(function(expectedViewController) { 37 | ctrl.addView(expectedViewController); 38 | }); 39 | 40 | $rootScope.$digest(); 41 | 42 | assert.ok(expectedViewControllers[0].selected, 'Expected first view to be selected'); 43 | }); 44 | 45 | }); 46 | 47 | describe('removeView', function() { 48 | 49 | it('should unregister a view', function() { 50 | var expectedViewController = {}; 51 | ctrl.addView(expectedViewController); 52 | $rootScope.$digest(); 53 | 54 | assert.lengthOf(ctrl.views, 1, 'Expected view to be registered'); 55 | 56 | ctrl.removeView(expectedViewController); 57 | $rootScope.$digest(); 58 | 59 | assert.lengthOf(ctrl.views, 0, 'Expected view to be unregistered'); 60 | }); 61 | 62 | it('should no unregister view if not registered', function() { 63 | var expectedViewController = {}; 64 | ctrl.addView(expectedViewController); 65 | $rootScope.$digest(); 66 | 67 | ctrl.removeView({}); 68 | $rootScope.$digest(); 69 | 70 | assert.lengthOf(ctrl.views, 1, 'Expected view to still be registered'); 71 | }); 72 | 73 | }); 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /src/components/shared/tabs/tabs.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
    4 |
  • 5 | 11 |
  • 12 |
13 |
14 |
15 |
-------------------------------------------------------------------------------- /src/components/shared/tabs/tabs.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/box-shadow"; 2 | @import "compass/css3/box-sizing"; 3 | 4 | $OPL_TABS_BACKGROUND_COLOR: #ffffff; 5 | $OPL_TABS_PRIMARY_COLOR: #000000; 6 | $OPL_TABS_ACCENT_COLOR: #e6007e; 7 | 8 | opl-tabs { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | height: 100%; 13 | width: 100%; 14 | } 15 | 16 | .opl-tabs { 17 | --opl-tabs-background-color: #{$OPL_TABS_BACKGROUND_COLOR}; 18 | --opl-tabs-primary-color: #{$OPL_TABS_PRIMARY_COLOR}; 19 | --opl-tabs-primary-color-0: #{rgba($OPL_TABS_PRIMARY_COLOR, 0.05)}; 20 | --opl-tabs-primary-color-1: #{rgba($OPL_TABS_PRIMARY_COLOR, 0.12)}; 21 | --opl-tabs-primary-color-2: #{rgba($OPL_TABS_PRIMARY_COLOR, 0.541)}; 22 | --opl-tabs-accent-color: #{$OPL_TABS_ACCENT_COLOR}; 23 | height: 100%; 24 | width: 100%; 25 | 26 | /* 27 | Views placeholder. 28 | */ 29 | & > div { 30 | overflow: hidden; 31 | position: absolute; 32 | left: 32px; 33 | top: 0; 34 | right: 0; 35 | height: 100%; 36 | border-bottom: 1px solid var(--opl-tabs-primary-color-0, rgba($OPL_TABS_PRIMARY_COLOR, 0.05)); 37 | @include box-sizing(border-box); 38 | } 39 | 40 | /* 41 | Views wrappers. 42 | */ 43 | opl-view, 44 | opl-view > div { 45 | height: 100%; 46 | width: 100%; 47 | } 48 | 49 | /* 50 | Tabs. 51 | */ 52 | .opl-tabs-wrapper { 53 | z-index: 1; 54 | position: absolute; 55 | left: 0; 56 | height: 100%; 57 | width: 32px; 58 | @include box-shadow(0px 4px 4px var(--opl-tabs-primary-color-1, rgba($OPL_TABS_PRIMARY_COLOR, 0.12))); 59 | background-color: var(--opl-tabs-background-color, $OPL_TABS_BACKGROUND_COLOR); 60 | 61 | ul { 62 | position: absolute; 63 | bottom: 0; 64 | outline: none; 65 | 66 | } 67 | 68 | &.opl-focus { 69 | background-color: var(--opl-tabs-primary-color-0, rgba($OPL_TABS_PRIMARY_COLOR, 0.05)); 70 | } 71 | 72 | /* 73 | Tabs buttons. 74 | */ 75 | button { 76 | color: var(--opl-tabs-primary-color-2, rgba($OPL_TABS_PRIMARY_COLOR, 0.541)); 77 | 78 | /* 79 | Tab buttons focus ring and ripple in resting state. 80 | */ 81 | &::before, 82 | &::after { 83 | background-color: var(--opl-tabs-primary-color, $OPL_TABS_PRIMARY_COLOR); 84 | } 85 | } 86 | 87 | .opl-selected { 88 | 89 | /* 90 | Tab buttons in selected state. 91 | */ 92 | button { 93 | color: var(--opl-tabs-accent-color, $OPL_TABS_ACCENT_COLOR); 94 | 95 | /* 96 | Tab buttonss focus ring and ripple in selected state. 97 | */ 98 | &::before, 99 | &::after { 100 | background-color: var(--opl-tabs-accent-color, $OPL_TABS_ACCENT_COLOR); 101 | } 102 | } 103 | } 104 | } 105 | 106 | /* 107 | No tabs. 108 | */ 109 | &.opl-no-tabs { 110 | 111 | /* 112 | Views placeholder. 113 | */ 114 | & > div { 115 | left: 0; 116 | } 117 | 118 | /* 119 | Tabs. 120 | */ 121 | .opl-tabs-wrapper { 122 | display: none; 123 | } 124 | 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/components/shared/templateSelector/templateSelector.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as an opl-template-selector HTML element to create a player templates selector. 9 | * 10 | * opl-template-selector is composed of a button and a list of templates. The button and the list of templates use the 11 | * OpenVeo Player Icons font. The button icon reflects the template actually selected. 12 | * 13 | * Attributes are: 14 | * - [String] **opl-template** The template to choose, either "split_1", "split_2", "split_50_50" or "split_25_75" 15 | * - [String] **opl-label** The ARIA label of the slider. Default to "Select a value" 16 | * - [Function] **opl-on-update** The function to call when a new template is chosen 17 | * - [Function] **opl-on-focus** The function to call when component enters in focus state 18 | * 19 | * Requires: 20 | * - **oplTranslate** OpenVeo Player i18n filter 21 | * 22 | * @example 23 | * var handleOnUpdate = function(template) { 24 | * console.log('New selected template:' + template); 25 | * }; 26 | * var handleOnFocus = function() { 27 | * console.log('Component has received focus'); 28 | * }; 29 | * var template = 'split_1'; 30 | * 31 | * 37 | * 38 | * @class oplTemplateSelector 39 | */ 40 | (function(app) { 41 | 42 | app.component('oplTemplateSelector', { 43 | templateUrl: 'opl-templateSelector.html', 44 | controller: 'OplTemplateSelectorController', 45 | bindings: { 46 | oplTemplate: '@?', 47 | oplLabel: '@?', 48 | oplOnUpdate: '&', 49 | oplOnFocus: '&' 50 | } 51 | }); 52 | 53 | })(angular.module('ov.player')); 54 | -------------------------------------------------------------------------------- /src/components/shared/templateSelector/templateSelector.html: -------------------------------------------------------------------------------- 1 |
2 | 13 |
14 | -------------------------------------------------------------------------------- /src/components/shared/templateSelector/templateSelector.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/box-sizing"; 2 | @import "compass/css3/user-interface"; 3 | @import "compass/css3/transform"; 4 | @import "compass/css3/animation"; 5 | @import "compass/css3/transition"; 6 | 7 | $OPL_TEMPLATE_SELECTOR_PRIMARY_COLOR: #000000; 8 | 9 | /* 10 | Animate the template with a resolve effect. 11 | */ 12 | @include keyframes(opl-template-selector-template-ripple-resolve) { 13 | from { 14 | @include animation-timing-function(cubic-bezier(0.4, 0, 0.2, 1)); 15 | opacity: 0; 16 | } 17 | to { 18 | opacity: 0.60; 19 | } 20 | } 21 | 22 | /* 23 | Animate the ripple with dissolve effect from actual opacity to 0 24 | */ 25 | @include keyframes(opl-template-selector-template-ripple-dissolve) { 26 | from { 27 | @include animation-timing-function(linear); 28 | } 29 | to { 30 | opacity: 0; 31 | } 32 | } 33 | 34 | /* 35 | Animate the template ripple with a growing effect from 0 to 100%. 36 | */ 37 | @include keyframes(opl-template-selector-template-ripple-radius-in) { 38 | from { 39 | @include animation-timing-function(cubic-bezier(0.4, 0, 0.2, 1)); 40 | @include scale(0); 41 | } 42 | to { 43 | @include scale(1); 44 | } 45 | } 46 | 47 | opl-template-selector { 48 | display: inline-block; 49 | width: 32px; 50 | height: 32px; 51 | margin-left: 100px; 52 | vertical-align: top; 53 | } 54 | 55 | .opl-template-selector { 56 | --opl-template-selector-primary-color: #{$OPL_TEMPLATE_SELECTOR_PRIMARY_COLOR}; 57 | position: relative; 58 | display: inline-block; 59 | width: 32px; 60 | height: 32px; 61 | outline: none; 62 | vertical-align: top; 63 | 64 | button { 65 | z-index: 1; 66 | position: absolute; 67 | top: 0; 68 | width: 32px; 69 | height: 32px; 70 | padding: 4px; 71 | outline: none; 72 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 73 | will-change: transform, opacity; 74 | border: none; 75 | background-color: transparent; 76 | cursor: pointer; 77 | color: var(--opl-template-selector-primary-color, $OPL_TEMPLATE_SELECTOR_PRIMARY_COLOR); 78 | @include box-sizing(border-box); 79 | @include user-select(none); 80 | 81 | /* 82 | Button is selected state. 83 | */ 84 | &.opl-selected { 85 | z-index: 2; 86 | } 87 | 88 | /* 89 | Focus ring and ripple resting state sizes are the same as the button. 90 | Focus ring and ripple resting state visibility are hidden. 91 | */ 92 | &::before, 93 | &::after { 94 | position: absolute; 95 | top: 0; 96 | left: 0; 97 | width: 100%; 98 | height: 100%; 99 | content: ''; 100 | opacity: 0; 101 | border-radius: 50%; 102 | pointer-events: none; 103 | } 104 | 105 | /* 106 | Focus ring in resting state. 107 | */ 108 | &::before { 109 | @include transition-property(opacity); 110 | @include transition-timing-function(linear); 111 | @include transition-duration(15ms); 112 | background-color: var(--opl-template-selector-primary-color, $OPL_TEMPLATE_SELECTOR_PRIMARY_COLOR); 113 | } 114 | 115 | /* 116 | Ripple resting state is scaled down to 0 and has its center matching the center of the button. 117 | */ 118 | &::after { 119 | @include scale(0); 120 | @include transform-origin(center, center); 121 | background-color: var(--opl-template-selector-primary-color, $OPL_TEMPLATE_SELECTOR_PRIMARY_COLOR); 122 | } 123 | 124 | /* 125 | Focus ring in button hover state. 126 | */ 127 | &:hover::before { 128 | opacity: 0.04; 129 | } 130 | 131 | /* 132 | Focus ring in button focus state. 133 | */ 134 | &.opl-template-focus::before { 135 | @include transition-duration(75ms); 136 | opacity: 0.12; 137 | } 138 | 139 | /* 140 | Ripple in template button activation state. 141 | */ 142 | &.opl-template-activation::after { 143 | @include animation-name(opl-template-selector-template-ripple-radius-in, opl-template-selector-template-ripple-resolve); 144 | @include animation-duration(225ms, 75ms); 145 | @include animation-fill-mode(forwards, forwards); 146 | } 147 | 148 | /* 149 | Ripple in button deactivation state. 150 | */ 151 | &.opl-template-deactivation::after { 152 | @include animation-name(opl-template-selector-template-ripple-dissolve); 153 | @include animation-duration(150ms); 154 | @include scale(1); 155 | } 156 | 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/components/shared/toggleIconButton/toggleIconButton.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as an opl-toggle-icon-button HTML element to create a two states icon button to 9 | * switch on and off. 10 | * 11 | * opl-toggle-icon-button button does not change its state on its own, it only calls the oplOnUpdate function when 12 | * actioned. 13 | * 14 | * opl-toggle-icon-button is composed of an "off" icon, a "on" icon, a focus ring and a ripple. 15 | * 16 | * Attributes are: 17 | * - [Boolean] **opl-on** Either "true" ("on") or false ("off"). Default to false. 18 | * - [String] **opl-off-icon** The ligature name of the icon to use for the "off" state (from "OpenVeo-Player-Icons" 19 | * font) 20 | * - [String] **opl-on-icon** The ligature name of the icon to use for the "on" state (from "OpenVeo-Player-Icons" 21 | * font) 22 | * - [String] **opl-off-label** The ARIA label to apply to the button when state is "off". Empty by default. 23 | * - [String] **opl-on-label** The ARIA label to apply to the button when state is "on". Empty by default. 24 | * - [Boolean] **opl-no-sequential-focus** true to set button tabindex to -1, false to set button tabindex to 0 25 | * - [Function] **opl-on-update** The function to call when actioned 26 | * - [Function] **opl-on-focus** The function to call when component enters in focus state 27 | * 28 | * @example 29 | * var handleOnUpdate = function(on) { 30 | * console.log('Toggle button actioned'); 31 | * }; 32 | * 33 | * 43 | * 44 | * @class oplToggleIconButton 45 | */ 46 | (function(app) { 47 | 48 | app.component('oplToggleIconButton', { 49 | templateUrl: 'opl-toggleIconButton.html', 50 | controller: 'OplToggleIconButtonController', 51 | bindings: { 52 | oplOn: '@?', 53 | oplOffIcon: '@?', 54 | oplOnIcon: '@?', 55 | oplOffLabel: '@?', 56 | oplOnLabel: '@?', 57 | oplNoSequentialFocus: '@?', 58 | oplOnUpdate: '&', 59 | oplOnFocus: '&' 60 | } 61 | }); 62 | 63 | })(angular.module('ov.player')); 64 | -------------------------------------------------------------------------------- /src/components/shared/toggleIconButton/toggleIconButton.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Manages oplToggleIconButton component. 11 | * 12 | * @param {Object} $element The HTML element holding the component 13 | * @param {Object} $timeout The AngularJS $timeout service 14 | * @param {Object} $scope Component isolated scope 15 | * @param {Object} $q The AngularJS $q service 16 | * @param {Object} $window The AngularJS $window service 17 | * @param {Object} oplEventsFactory To help manipulate DOM events 18 | * @class OplToggleIconButtonController 19 | * @constructor 20 | */ 21 | function OplToggleIconButtonController($element, $timeout, $scope, $q, $window, oplEventsFactory) { 22 | var ctrl = this; 23 | var bodyElement; 24 | var buttonElement; 25 | var activationTimer; 26 | var deactivationTimer; 27 | var deactivationAnimationRequested; 28 | var activated; 29 | 30 | $scope.on = false; 31 | $scope.onIcon = null; 32 | $scope.offIcon = null; 33 | $scope.label = null; 34 | 35 | /** 36 | * Focuses the button. 37 | */ 38 | function focus() { 39 | buttonElement.addClass('opl-focus'); 40 | if (ctrl.oplOnFocus) ctrl.oplOnFocus(); 41 | } 42 | 43 | /** 44 | * Unfocuses the button. 45 | */ 46 | function unfocus() { 47 | buttonElement.removeClass('opl-focus'); 48 | } 49 | 50 | /** 51 | * Animates the deactivation of the button. 52 | * 53 | * Deactivation is performed only if activation animation is ended and activation is ended. 54 | * 55 | * @return {Promise} Promise resolving when animation is finished 56 | */ 57 | function animateDeactivation() { 58 | if (!deactivationAnimationRequested || activationTimer) return $q.when(); 59 | var deferred = $q.defer(); 60 | 61 | deactivationAnimationRequested = false; 62 | activated = false; 63 | buttonElement.removeClass('opl-activation'); 64 | 65 | // Start deactivation animation 66 | buttonElement.addClass('opl-deactivation'); 67 | 68 | // An animation is associated to the opl-deactivation class, wait for it to finish before removing the 69 | // deactivation class 70 | // Delay corresponds to the animation duration 71 | deactivationTimer = $timeout(function() { 72 | buttonElement.removeClass('opl-deactivation'); 73 | deferred.resolve(); 74 | }, 150); 75 | 76 | return deferred.promise; 77 | } 78 | 79 | /** 80 | * Animates the activation of the button. 81 | * 82 | * Activation animation can be performed only if not activated. 83 | */ 84 | function animateActivation() { 85 | var deferred = $q.defer(); 86 | 87 | // Remove any ongoing activation / deactivation animations 88 | buttonElement.removeClass('opl-deactivation'); 89 | if (activationTimer) $timeout.cancel(activationTimer); 90 | if (deactivationTimer) $timeout.cancel(deactivationTimer); 91 | 92 | // Start activation animation 93 | buttonElement.addClass('opl-activation'); 94 | 95 | // An animation is associated to the opl-activation class, wait for it to finish before running the deactivation 96 | // animation 97 | // Delay corresponds to the animation duration 98 | activationTimer = $timeout(function() { 99 | activationTimer = null; 100 | requestAnimationFrame(function() { 101 | animateDeactivation().then(function() { 102 | deferred.resolve(); 103 | }); 104 | }); 105 | }, 225); 106 | 107 | return deferred.promise; 108 | } 109 | 110 | /** 111 | * Calls the oplOnUpdate function with the actual state. 112 | */ 113 | function callAction() { 114 | if (ctrl.oplOnUpdate) ctrl.oplOnUpdate({on: !$scope.on}); 115 | } 116 | 117 | /** 118 | * Handles keydown events. 119 | * 120 | * Toggle button captures the following keyboard keys: 121 | * - ENTER to action the button 122 | * 123 | * Captured keys will prevent default browser actions. 124 | * 125 | * @param {KeyboardEvent} event The captured event 126 | */ 127 | function handleKeyDown(event) { 128 | if ((event.key !== 'Enter' && event.keyCode !== 13)) return; 129 | 130 | event.preventDefault(); 131 | $scope.$apply(callAction); 132 | } 133 | 134 | /** 135 | * Handles release events. 136 | * 137 | * After releasing, button is actioned and is not longer active. 138 | * 139 | * @param {Event} event The captured event which may defer depending on the device (mouse, touchpad, pen etc.) 140 | */ 141 | function handleUp(event) { 142 | bodyElement.off(oplEventsFactory.EVENTS.UP, handleUp); 143 | 144 | if (ctrl.oplOnIcon && ctrl.oplOffIcon) { 145 | requestAnimationFrame(function() { 146 | deactivationAnimationRequested = true; 147 | animateDeactivation(); 148 | }); 149 | } 150 | 151 | activated = false; 152 | callAction(); 153 | } 154 | 155 | /** 156 | * Handles pressed events. 157 | * 158 | * Pressing the button makes it active. 159 | * 160 | * @param {Event} event The captured event which may defer depending on the device (mouse, touchpad, pen etc.) 161 | */ 162 | function handleDown(event) { 163 | if (activated) return; 164 | 165 | activated = true; 166 | 167 | if (ctrl.oplOnIcon && ctrl.oplOffIcon) { 168 | requestAnimationFrame(function() { 169 | animateActivation(); 170 | }); 171 | } 172 | bodyElement.on(oplEventsFactory.EVENTS.UP, handleUp); 173 | } 174 | 175 | /** 176 | * Handles focus event. 177 | * 178 | * @param {FocusEvent} event The captured event 179 | */ 180 | function handleFocus(event) { 181 | focus(); 182 | } 183 | 184 | /** 185 | * Handles blur event. 186 | * 187 | * @param {FocusEvent} event The captured event 188 | */ 189 | function handleBlur(event) { 190 | unfocus(); 191 | } 192 | 193 | /** 194 | * Sets ARIA label depending on value. 195 | */ 196 | function updateLabel() { 197 | $scope.label = $scope.on ? ctrl.oplOnLabel : ctrl.oplOffLabel; 198 | } 199 | 200 | Object.defineProperties(ctrl, { 201 | 202 | /** 203 | * Initializes controller. 204 | * 205 | * @method $onInit 206 | */ 207 | $onInit: { 208 | value: function() { 209 | bodyElement = angular.element($window.document.body); 210 | buttonElement = angular.element($element[0].querySelector('.opl-toggle-icon-button')); 211 | 212 | buttonElement.on('keydown', handleKeyDown); 213 | buttonElement.on('focus', handleFocus); 214 | buttonElement.on('blur', handleBlur); 215 | buttonElement.on(oplEventsFactory.EVENTS.DOWN, handleDown); 216 | } 217 | }, 218 | 219 | /** 220 | * Removes event listeners. 221 | * 222 | * @method $onDestroy 223 | */ 224 | $onDestroy: { 225 | value: function() { 226 | buttonElement.off('keydown focus blur ' + oplEventsFactory.EVENTS.DOWN); 227 | bodyElement.off(oplEventsFactory.EVENTS.UP, handleUp); 228 | if (activationTimer) $timeout.cancel(activationTimer); 229 | if (deactivationTimer) $timeout.cancel(deactivationTimer); 230 | } 231 | }, 232 | 233 | /** 234 | * Handles one-way binding properties changes. 235 | * 236 | * @method $onChanges 237 | * @param {Object} changedProperties Properties which have changed since last digest loop 238 | * @param {Object} [changedProperties.oplOn] oplOn old and new value 239 | * @param {String} [changedProperties.oplOn.currentValue] oplOn new value 240 | * @param {Object} [changedProperties.oplOffLabel] oplOffLabel old and new value 241 | * @param {String} [changedProperties.oplOffLabel.currentValue] oplOffLabel new value 242 | * @param {Object} [changedProperties.oplOnLabel] oplOnLabel old and new value 243 | * @param {String} [changedProperties.oplOnLabel.currentValue] oplOnLabel new value 244 | * @param {Object} [changedProperties.oplOnIcon] oplOnIcon old and new value 245 | * @param {String} [changedProperties.oplOnIcon.currentValue] oplOnIcon new value 246 | * @param {Object} [changedProperties.oplOffIcon] oplOffIcon old and new value 247 | * @param {String} [changedProperties.oplOffIcon.currentValue] oplOffIcon new value 248 | */ 249 | $onChanges: { 250 | value: function(changedProperties) { 251 | var newValue; 252 | 253 | if (changedProperties.oplOn && changedProperties.oplOn.currentValue) { 254 | newValue = changedProperties.oplOn.currentValue; 255 | $scope.on = (!newValue || newValue === 'undefined') ? false : JSON.parse(newValue); 256 | updateLabel(); 257 | } 258 | 259 | if (changedProperties.oplOnIcon && changedProperties.oplOnIcon.currentValue) { 260 | newValue = changedProperties.oplOnIcon.currentValue; 261 | $scope.onIcon = (!newValue || newValue === 'undefined') ? null : newValue; 262 | } 263 | 264 | if (changedProperties.oplOffIcon && changedProperties.oplOffIcon.currentValue) { 265 | newValue = changedProperties.oplOffIcon.currentValue; 266 | $scope.offIcon = (!newValue || newValue === 'undefined') ? null : newValue; 267 | } 268 | 269 | if (changedProperties.oplOffLabel && changedProperties.oplOffLabel.currentValue || 270 | changedProperties.oplOnLabel && changedProperties.oplOnLabel.currentValue 271 | ) { 272 | updateLabel(); 273 | } 274 | } 275 | } 276 | }); 277 | 278 | } 279 | 280 | app.controller('OplToggleIconButtonController', OplToggleIconButtonController); 281 | OplToggleIconButtonController.$inject = ['$element', '$timeout', '$scope', '$q', '$window', 'oplEventsFactory']; 282 | 283 | })(angular.module('ov.player')); 284 | -------------------------------------------------------------------------------- /src/components/shared/toggleIconButton/toggleIconButton.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/components/shared/toggleIconButton/toggleIconButton.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/user-interface"; 2 | @import "compass/css3/box-sizing"; 3 | @import "compass/css3/transform"; 4 | @import "compass/css3/animation"; 5 | @import "compass/css3/transition"; 6 | 7 | $OPL_TOGGLE_ICON_BUTTON_PRIMARY_COLOR: #000000; 8 | 9 | /* 10 | Animate the ripple with a growing effect from 0 to 100% 11 | */ 12 | @include keyframes(opl-toggle-icon-button-ripple-radius-in) { 13 | from { 14 | @include animation-timing-function(cubic-bezier(0.4, 0, 0.2, 1)); 15 | @include scale(0); 16 | } 17 | to { 18 | @include scale(1); 19 | } 20 | } 21 | 22 | /* 23 | Animate the ripple with dissolve effect from 0 to the focus percentage 24 | */ 25 | @include keyframes(opl-toggle-icon-button-ripple-opacity-in) { 26 | from { 27 | @include animation-timing-function(cubic-bezier(0.4, 0, 0.2, 1)); 28 | opacity: 0; 29 | } 30 | to { 31 | opacity: 0.16; 32 | } 33 | } 34 | 35 | /* 36 | Animate the ripple with dissolve effect from focus percentage to 0 37 | */ 38 | @include keyframes(opl-toggle-icon-button-ripple-opacity-out) { 39 | from { 40 | @include animation-timing-function(linear); 41 | opacity: 0.16; 42 | } 43 | to { 44 | opacity: 0; 45 | } 46 | } 47 | 48 | opl-toggle-icon-button { 49 | display: inline-block; 50 | width: 32px; 51 | height: 32px; 52 | vertical-align: top; 53 | } 54 | 55 | .opl-toggle-icon-button { 56 | --opl-toggle-icon-button-primary-color: #{$OPL_TOGGLE_ICON_BUTTON_PRIMARY_COLOR}; 57 | position: relative; 58 | display: inline-block; 59 | width: 32px; 60 | height: 32px; 61 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 62 | will-change: transform, opacity; 63 | padding: 4px; 64 | vertical-align: top; 65 | outline: none; 66 | border: none; 67 | background-color: transparent; 68 | cursor: pointer; 69 | color: var(--opl-toggle-icon-button-primary-color, $OPL_TOGGLE_ICON_BUTTON_PRIMARY_COLOR); 70 | @include box-sizing(border-box); 71 | @include user-select(none); 72 | 73 | /* 74 | Focus ring and ripple resting state sizes are the same as the button. 75 | Focus ring and ripple resting state visibility are hidden. 76 | */ 77 | &::before, 78 | &::after { 79 | position: absolute; 80 | top: 0; 81 | left: 0; 82 | width: 100%; 83 | height: 100%; 84 | content: ''; 85 | opacity: 0; 86 | border-radius: 50%; 87 | pointer-events: none; 88 | } 89 | 90 | /* 91 | Focus ring in resting state. 92 | */ 93 | &::before { 94 | @include transition-property(opacity); 95 | @include transition-timing-function(linear); 96 | @include transition-duration(15ms); 97 | background-color: var(--opl-toggle-icon-button-primary-color, $OPL_TOGGLE_ICON_BUTTON_PRIMARY_COLOR); 98 | } 99 | 100 | /* 101 | Ripple resting state is scaled down to 0 and has its center matching the center of the button. 102 | */ 103 | &::after { 104 | @include scale(0); 105 | @include transform-origin(center, center); 106 | background-color: var(--opl-toggle-icon-button-primary-color, $OPL_TOGGLE_ICON_BUTTON_PRIMARY_COLOR); 107 | } 108 | 109 | /* 110 | Focus ring in button hover state. 111 | */ 112 | &:hover::before { 113 | opacity: 0.04; 114 | } 115 | 116 | /* 117 | Focus ring in button focus state. 118 | */ 119 | &.opl-focus::before { 120 | @include transition-duration(75ms); 121 | opacity: 0.12; 122 | } 123 | 124 | /* 125 | Ripple in button activation state. 126 | */ 127 | &.opl-activation::after { 128 | @include animation-name(opl-toggle-icon-button-ripple-radius-in, opl-toggle-icon-button-ripple-opacity-in); 129 | @include animation-duration(225ms, 75ms); 130 | @include animation-fill-mode(forwards, forwards); 131 | } 132 | 133 | /* 134 | Ripple in button deactivation state. 135 | */ 136 | &.opl-deactivation::after { 137 | @include animation-name(opl-toggle-icon-button-ripple-opacity-out); 138 | @include animation-duration(150ms); 139 | @include scale(1); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/components/shared/view/view.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as HTML element opl-view to be able to group HTML elements which will be added to 9 | * an opl-tabs element. 10 | * 11 | * Available attributes are: 12 | * - [String] **opl-label** The ARIA label to apply to the button. Empty by default. 13 | * - [String] **opl-class** A CSS class that will be added to the main container of the view 14 | * - [String] **opl-view-id** The view identifier 15 | * - [String] **opl-icon** The ligature name of the icon to use (from "OpenVeo-Player-Icons" font) 16 | * - [Function] **opl-on-selected** Function to call when view has been selected 17 | * 18 | * @example 19 | * var handleOnSelected = function(id) { 20 | * console.log('View ' + id + ' has been selected'); 21 | * }; 22 | * 23 | * 24 | * 31 | * Content of the first view 32 | * 33 | * 39 | * Content of the second view 40 | * 41 | * 42 | * 43 | * @class oplView 44 | */ 45 | (function(app) { 46 | 47 | app.component('oplView', { 48 | templateUrl: 'opl-view.html', 49 | controller: 'OplViewController', 50 | require: ['^oplTabs'], 51 | transclude: true, 52 | bindings: { 53 | oplLabel: '@?', 54 | oplClass: '@?', 55 | oplViewId: '@', 56 | oplIcon: '@', 57 | oplOnSelect: '&' 58 | } 59 | }); 60 | 61 | })(angular.module('ov.player')); 62 | -------------------------------------------------------------------------------- /src/components/shared/view/view.component.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.assert = chai.assert; 4 | 5 | describe('OplView', function() { 6 | var $compile; 7 | var $rootScope; 8 | var $timeout; 9 | var scope; 10 | var element; 11 | var viewElement; 12 | var tabsController; 13 | var ctrl; 14 | 15 | // Load modules 16 | beforeEach(function() { 17 | module('ov.player'); 18 | module('templates'); 19 | }); 20 | 21 | // Dependencies injections 22 | beforeEach(inject(function(_$compile_, _$rootScope_, _$timeout_) { 23 | $rootScope = _$rootScope_; 24 | $compile = _$compile_; 25 | $timeout = _$timeout_; 26 | })); 27 | 28 | // Initializes tests 29 | beforeEach(function() { 30 | scope = $rootScope.$new(); 31 | scope.label = 'Label'; 32 | scope.html = 'HTML'; 33 | scope.class = 'class'; 34 | scope.id = 'id'; 35 | scope.icon = 'icon_ligature'; 36 | scope.handleOnSelect = chai.spy(function() {}); 37 | 38 | element = angular.element('' + 39 | '' + 46 | '{{html}}' + 47 | '' + 48 | ''); 49 | element = $compile(element)(scope); 50 | scope.$digest(); 51 | $timeout.flush(); 52 | 53 | tabsController = element.controller('oplTabs'); 54 | 55 | viewElement = angular.element(element[0].querySelector('.' + scope.class)); 56 | ctrl = angular.element(element[0].querySelector('opl-view')).controller('oplView'); 57 | }); 58 | 59 | it('should display a tab panel with the transcluded content', function() { 60 | assert.equal(viewElement.attr('role'), 'tabpanel', 'Wrong role'); 61 | assert.equal(viewElement.attr('id'), scope.id, 'Wrong id'); 62 | assert.equal(viewElement.attr('class'), scope.class, 'Wrong class'); 63 | assert.equal(viewElement.text(), scope.html, 'Wrong content'); 64 | assert.notOk(viewElement.hasClass('ng-hide'), 'Expected view to be displayed'); 65 | }); 66 | 67 | it('should add itself to parent oplTabs component', function() { 68 | assert.strictEqual(tabsController.getSelectedView(), ctrl, 'Wrong view'); 69 | }); 70 | 71 | it('should remove itself from parent oplTabs component when destroyed', function() { 72 | scope.$destroy(); 73 | assert.isNull(tabsController.getSelectedView(), 'Unexpected view'); 74 | }); 75 | 76 | it('should be masked if not selected', function() { 77 | ctrl.selected = false; 78 | scope.$digest(); 79 | 80 | assert.ok(viewElement.hasClass('ng-hide'), 'Expected view to be hidden'); 81 | }); 82 | 83 | it('should call function defined in attribute "opl-on-select" when view is selected', function() { 84 | scope.handleOnSelect = chai.spy(function(id) { 85 | assert.equal(id, scope.id, 'Wrong id'); 86 | }); 87 | 88 | ctrl.selected = false; 89 | scope.$digest(); 90 | 91 | ctrl.selected = true; 92 | scope.$digest(); 93 | 94 | 95 | scope.handleOnSelect.should.have.been.called.exactly(1); 96 | }); 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /src/components/shared/view/view.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Manages oplView component. 11 | * 12 | * @param {Object} $scope The component isolated scope 13 | * @param {Object} $element The component HTML element 14 | * @class OplViewController 15 | * @constructor 16 | */ 17 | function OplViewController($scope, $element) { 18 | var ctrl = this; 19 | var oplTabsCtrl = $element.controller('oplTabs'); 20 | 21 | Object.defineProperties(ctrl, { 22 | 23 | /** 24 | * Indicates if view is selected or not. 25 | * 26 | * @property selected 27 | * @type Boolean 28 | */ 29 | selected: { 30 | value: false, 31 | writable: true 32 | } 33 | 34 | }); 35 | 36 | oplTabsCtrl.addView(ctrl); 37 | 38 | $scope.$watch('$ctrl.selected', function(newValue, oldValue) { 39 | if (newValue && ctrl.oplOnSelect) ctrl.oplOnSelect({id: ctrl.oplViewId}); 40 | }); 41 | 42 | // Detach view from tabs when view is destroyed 43 | $scope.$on('$destroy', function() { 44 | oplTabsCtrl.removeView(ctrl); 45 | }); 46 | 47 | } 48 | 49 | app.controller('OplViewController', OplViewController); 50 | OplViewController.$inject = ['$scope', '$element']; 51 | 52 | })(angular.module('ov.player')); 53 | -------------------------------------------------------------------------------- /src/components/shared/view/view.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/components/shared/volume/volume.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as an opl-volume HTML element to create a volume controller. 9 | * 10 | * opl-volume is composed of a opl-toggle-icon-button component (mute / unmute button) and a opl-slider 11 | * component (volume controller). 12 | * 13 | * Attributes are: 14 | * - [Boolean] **opl-no-sequential-focus** true to set volume tabindex to -1, false to set volume tabindex to 0 15 | * - [Function] **opl-on-focus** The function to call when component enters in focus state 16 | * - [Function] **opl-on-open** The function to call when volume controller is opened 17 | * - [Function] **opl-on-close** The function to call when volume controller is closed 18 | * 19 | * @example 20 | * var handleOnFocus = function() { 21 | * console.log('Component has received focus'); 22 | * }; 23 | * var handleOnOpen = function() { 24 | * console.log('Volume controller is opened'); 25 | * }; 26 | * var handleOnClose = function() { 27 | * console.log('Volume controller is closed'); 28 | * }; 29 | * var volumeLevel = 50; 30 | * 31 | * 38 | * 39 | * Requires: 40 | * - **oplTranslate** OpenVeo Player i18n filter 41 | * 42 | * @class oplVolume 43 | */ 44 | (function(app) { 45 | 46 | app.component('oplVolume', { 47 | templateUrl: 'opl-volume.html', 48 | controller: 'OplVolumeController', 49 | require: ['?ngModel'], 50 | bindings: { 51 | oplNoSequentialFocus: '@?', 52 | oplOnFocus: '&', 53 | oplOnOpen: '&', 54 | oplOnClose: '&' 55 | } 56 | }); 57 | 58 | })(angular.module('ov.player')); 59 | -------------------------------------------------------------------------------- /src/components/shared/volume/volume.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(app) { 8 | 9 | /** 10 | * Manages oplVolume component. 11 | * 12 | * @param {Object} $scope The component isolated scope 13 | * @param {Object} $element The HTML element holding the component 14 | * @param {Object} $timeout The AngularJS $timeout service 15 | * @param {Object} $q The AngularJS $q service 16 | * @param {Object} oplEventsFactory Helper to manipulate the DOM events 17 | * @class OplVolumeController 18 | * @constructor 19 | */ 20 | function OplVolumeController($scope, $element, $timeout, $q, oplEventsFactory) { 21 | var ctrl = this; 22 | var ngModelCtrl = $element.controller('ngModel'); 23 | var volumeElement; 24 | var sliderElement; 25 | var buttonElement; 26 | var sliderWrapperElement; 27 | var opening; 28 | var closing; 29 | var opened = false; 30 | 31 | $scope.level = 50; 32 | $scope.sliderValue = 0; 33 | 34 | /** 35 | * Tests if an HTML element is part of the volume component or not. 36 | * 37 | * @param {HTMLElement} element The element suspected to be part of the volume component 38 | * @return {Boolean} true if element is part of the volume component, false otherwise 39 | */ 40 | function isElementFromComponent(element) { 41 | if (!element) return false; 42 | 43 | element = angular.element(element); 44 | if (element.hasClass('opl-volume')) return true; 45 | 46 | return isElementFromComponent(element.parent()[0]); 47 | } 48 | 49 | /** 50 | * Animates the volume controller opening. 51 | * 52 | * @return {Promise} Promise resolving when animation is finished 53 | */ 54 | function animateOpening() { 55 | var deferred = $q.defer(); 56 | 57 | // Start opening animation 58 | volumeElement.addClass('opl-over'); 59 | 60 | // An animation is associated to the "opl-over" class, wait for it to finish 61 | var onTransitionEnd = function onTransitionEnd() { 62 | sliderWrapperElement.off('transitionend'); 63 | deferred.resolve(); 64 | }; 65 | sliderWrapperElement.on('transitionend', onTransitionEnd); 66 | 67 | return deferred.promise; 68 | } 69 | 70 | /** 71 | * Animates the volume controller closing. 72 | * 73 | * @return {Promise} Promise resolving when animation is finished 74 | */ 75 | function animateClosing() { 76 | var deferred = $q.defer(); 77 | 78 | // Start closing animation 79 | volumeElement.removeClass('opl-over'); 80 | 81 | // An animation is associated to the "opl-over" class, wait for it to finish 82 | var onTransitionEnd = function onTransitionEnd() { 83 | sliderWrapperElement.off('transitionend'); 84 | deferred.resolve(); 85 | }; 86 | sliderWrapperElement.on('transitionend', onTransitionEnd); 87 | 88 | return deferred.promise; 89 | } 90 | 91 | /** 92 | * Updates model with actual level. 93 | */ 94 | function updateModel() { 95 | ngModelCtrl.$setViewValue(ctrl.muted ? 0 : $scope.level); 96 | ngModelCtrl.$validate(); 97 | } 98 | 99 | /** 100 | * Sets volume level. 101 | * 102 | * @param {Number} value The value to apply to the volume between 0 and 100 103 | * @param {Boolean} muted true to mute, false to unmute 104 | */ 105 | function setLevel(level, muted) { 106 | if (level === $scope.level && muted === ctrl.muted) return; 107 | 108 | $scope.level = Math.min(100, Math.max(0, level || 0)); 109 | ctrl.muted = !$scope.level || muted || false; 110 | $scope.sliderValue = ctrl.muted ? 0 : $scope.level; 111 | 112 | updateModel(); 113 | } 114 | 115 | /** 116 | * Handles focus event. 117 | * 118 | * Set a focus class to the volume HTML element. 119 | * 120 | * @param {FocusEvent} event The captured event 121 | */ 122 | function handleFocus(event) { 123 | volumeElement.addClass('opl-volume-focus'); 124 | } 125 | 126 | /** 127 | * Handles blur event. 128 | * 129 | * Remove the focus class from the volume HTML element. 130 | * 131 | * @param {FocusEvent} event The captured event 132 | */ 133 | function handleBlur(event) { 134 | volumeElement.removeClass('opl-volume-focus'); 135 | } 136 | 137 | /** 138 | * Handles over event. 139 | * 140 | * @param {Event} event The captured event which may defer depending on the device (mouse, pen etc.) 141 | */ 142 | function handleOver(event) { 143 | if (opening || closing || opened) return; 144 | 145 | if (!isElementFromComponent(event.relatedTarget)) { 146 | opening = true; 147 | requestAnimationFrame(function() { 148 | animateOpening().then(function() { 149 | opening = false; 150 | opened = true; 151 | 152 | if (ctrl.oplOnOpen) ctrl.oplOnOpen(); 153 | }); 154 | }); 155 | } 156 | } 157 | 158 | /** 159 | * Handles out event. 160 | * 161 | * @param {Event} event The captured event which may defer depending on the device (mouse, pen etc.) 162 | */ 163 | function handleOut(event) { 164 | if (closing || opening || !opened) return; 165 | 166 | if (!isElementFromComponent(event.relatedTarget)) { 167 | closing = true; 168 | requestAnimationFrame(function() { 169 | animateClosing().then(function() { 170 | opened = false; 171 | closing = false; 172 | 173 | if (ctrl.oplOnClose) ctrl.oplOnClose(); 174 | }); 175 | }); 176 | } 177 | } 178 | 179 | Object.defineProperties(ctrl, { 180 | 181 | /** 182 | * Indicate if volume is muted. 183 | * 184 | * @property muted 185 | * @type Boolean 186 | */ 187 | muted: { 188 | value: false, 189 | writable: true 190 | }, 191 | 192 | /** 193 | * Initializes controller and attributes. 194 | * 195 | * @method $onInit 196 | */ 197 | $onInit: { 198 | value: function() { 199 | volumeElement = angular.element($element[0].querySelector('.opl-volume')); 200 | } 201 | }, 202 | 203 | /** 204 | * Initializes child components. 205 | * 206 | * @method $postLink 207 | */ 208 | $postLink: { 209 | value: function() { 210 | 211 | // Wait for oplSlider and oplToggleIconButton components. 212 | // Templates for oplSlider and oplToggleIconButton components are stored in cache and will 213 | // be processed in next loop. 214 | $timeout(function() { 215 | sliderWrapperElement = angular.element($element[0].querySelector('.opl-volume > div')); 216 | sliderElement = angular.element($element[0].querySelector('.opl-slider')); 217 | buttonElement = angular.element($element[0].querySelector('.opl-toggle-icon-button')); 218 | 219 | if (oplEventsFactory.EVENTS.OVER) { 220 | volumeElement.on(oplEventsFactory.EVENTS.OVER, handleOver); 221 | volumeElement.on(oplEventsFactory.EVENTS.OUT, handleOut); 222 | } 223 | sliderElement.on('focus', handleFocus); 224 | sliderElement.on('blur', handleBlur); 225 | buttonElement.on('focus', handleFocus); 226 | buttonElement.on('blur', handleBlur); 227 | sliderWrapperElement.on('transitionend', function(event) { 228 | if (event.target === sliderWrapperElement[0] && event.propertyName === 'width') 229 | sliderElement.controller('oplSlider').reset(); 230 | }); 231 | }); 232 | 233 | } 234 | }, 235 | 236 | /** 237 | * Removes event listeners. 238 | * 239 | * @method $onDestroy 240 | */ 241 | $onDestroy: { 242 | value: function() { 243 | if (oplEventsFactory.EVENTS.OVER) 244 | volumeElement.off(oplEventsFactory.EVENTS.OVER + ' ' + oplEventsFactory.EVENTS.OUT); 245 | 246 | if (sliderElement) sliderElement.off('focus blur'); 247 | if (buttonElement) buttonElement.off('focus blur'); 248 | if (sliderWrapperElement) sliderWrapperElement.off('transitionend'); 249 | } 250 | }, 251 | 252 | /** 253 | * Mutes / Unmutes. 254 | * 255 | * @method toggleSound 256 | * @param {Boolean} muted true to mute, false to unmute 257 | */ 258 | toggleSound: { 259 | value: function(muted) { 260 | setLevel($scope.level, muted); 261 | } 262 | }, 263 | 264 | /** 265 | * Sets volume level. 266 | * 267 | * @method setLevel 268 | * @param {Number} level The volume level 269 | */ 270 | setLevel: { 271 | value: function(level) { 272 | setLevel(level, false); 273 | } 274 | } 275 | 276 | }); 277 | 278 | /** 279 | * Updates the slider value from model. 280 | * 281 | * It overrides AngularJS $render. 282 | */ 283 | ngModelCtrl.$render = function() { 284 | setLevel(ngModelCtrl.$viewValue || 0); 285 | }; 286 | 287 | /** 288 | * Tests if the model is empty. 289 | * 290 | * It overrides AngularJS $isEmpty. The model value can't be empty. 291 | * 292 | * @param {Number} value The model value 293 | * @return {Boolean} false as the model can't by empty 294 | */ 295 | ngModelCtrl.$isEmpty = function(value) { 296 | return false; 297 | }; 298 | 299 | /** 300 | * Handles sub components focus. 301 | */ 302 | $scope.handleFocus = function() { 303 | if (ctrl.oplOnFocus) ctrl.oplOnFocus(); 304 | }; 305 | 306 | } 307 | 308 | app.controller('OplVolumeController', OplVolumeController); 309 | OplVolumeController.$inject = [ 310 | '$scope', 311 | '$element', 312 | '$timeout', 313 | '$q', 314 | 'oplEventsFactory' 315 | ]; 316 | 317 | })(angular.module('ov.player')); 318 | -------------------------------------------------------------------------------- /src/components/shared/volume/volume.html: -------------------------------------------------------------------------------- 1 |
2 | 12 |
13 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/components/shared/volume/volume.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/transition"; 2 | 3 | opl-volume { 4 | display: inline-block; 5 | height: 32px; 6 | } 7 | 8 | .opl-volume { 9 | display: inline-block; 10 | overflow: hidden; 11 | height: 32px; 12 | vertical-align: top; 13 | 14 | /* 15 | Slider. 16 | The slider must have a width even if the slider wrapper is not displayed. 17 | This is required by the slider to init correctly. 18 | */ 19 | opl-slider, .opl-slider { 20 | width: 62px; 21 | } 22 | 23 | /* 24 | Slider wrapper. 25 | The slider is not displayed in volume resting state. 26 | */ 27 | & > div { 28 | display: inline-block; 29 | width: 0; 30 | opacity: 0; 31 | margin-left: 8px; 32 | vertical-align: top; 33 | will-change: opacity; 34 | @include transition-property(width, opacity); 35 | @include transition-timing-function(cubic-bezier(0.4, 0.0, 1, 1), cubic-bezier(0.4, 0.0, 1, 1)); 36 | @include transition-duration(100ms, 100ms); 37 | } 38 | 39 | /* 40 | Volume in both over and focus states. 41 | */ 42 | &.opl-over, 43 | &.opl-volume-focus { 44 | overflow: visible; 45 | } 46 | 47 | /* 48 | Slider wrapper. 49 | The slider is displayed in both volume over and focus states. 50 | */ 51 | &.opl-over > div, 52 | &.opl-volume-focus > div { 53 | width: 62px; 54 | opacity: 1; 55 | margin: 0 12px; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/components/tile/tile.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as an opl-tile HTML element to create a point of interest. 9 | * 10 | * opl-tile is composed of information about a point of interest. A point of interest is represented by a time and the 11 | * resources associated to it. The ressources associated to the point of interest depends on its type. 12 | * The tile is reduced by default and can be enlarged. When tile is reduced only the time and the title (tile of type 13 | * "text") or the small image (tile of type "image") are displayed. When tile is enlarged a close button, title, 14 | * description and attachment file are displayed for tiles of type "text" while close button and large image are 15 | * displayed for tiles of type "image". 16 | * 17 | * Attributes are: 18 | * - [Array] **opl-data** The list of tiles description objects with for each object: 19 | * - [Number] **id** The tile id 20 | * - [String] **type** The tile type (either "image" or "text") 21 | * - [Number] **time** The tile time value in milliseconds 22 | * 23 | * Only for tiles of type "image": 24 | * - [Object] **[image]** The tile image with: 25 | * - [Object] **small** The small image. Small image is displayed when tile is reduced. Expected small image size 26 | * is 142x80 27 | * - [String] **url** The URL of the sprite containing the small image 28 | * - [Number] **x** x coordinate of the small image inside the sprite 29 | * - [Number] **y** y coordinate of the small image inside the sprite 30 | * - [String] **large** The Large image URL to display when tile is enlarged 31 | * 32 | * Only for tiles of type "text": 33 | * - [String] **title** The tile title 34 | * - [String] **[description]** The tile description (may contain HTML) 35 | * - [Object] **[file]** The tile attachment file 36 | * - [String] **url** The file URL. Displayed file name is retrieved from this URL 37 | * - [String] **originalName** The name of the file to propose to the user when downloading 38 | * - [Boolean] **opl-abstract** true to show the abstract (reduced), false to show the full tile (enlarged) 39 | * - [Function] **opl-on-select** The function to call when tile is actioned 40 | * - [Function] **opl-on-more** The function to call when tile "more info" button is actioned 41 | * - [Function] **opl-on-close** The function to call when tile "close" button is actioned 42 | * - [Function] **opl-on-ready** The function to call when tile is ready. It can be called several time if the value 43 | * of opl-abstract changes 44 | * - [Function] **opl-on-image-preloaded** The function to call when large image is loaded 45 | * - [Function] **opl-on-image-error** The function to call when large image couldn't be retrieved 46 | * 47 | * @example 48 | * var textTile = { 49 | * id: 42, 50 | * type: 'text', 51 | * title: 'Title', 52 | * time: 20000, 53 | * description: '

Description', 54 | * file: { 55 | * url: 'http://host.local/document.pdf', 56 | * originalName: 'download-name-without-extension' 57 | * } 58 | * }; 59 | * 60 | * var imageTile = { 61 | * id: 43, 62 | * type: 'image', 63 | * time: 40000, 64 | * image: { 65 | * small: 'http://host.local/image-small.jpg', 66 | * large: 'http://host.local/image-large.jpg' 67 | * } 68 | * }; 69 | * 70 | * var handleTextTileSelect = function(tile) { 71 | * console.log('Tile ' + tile.id + ' has been actioned'); 72 | * }; 73 | * 74 | * var handleTextTileMore = function(tile) { 75 | * console.log('Tile ' + tile.id + ' "more info" button has been actioned'); 76 | * }; 77 | * 78 | * var handleTextTileClose = function(tile) { 79 | * console.log('Tile ' + tile.id + ' "close" button has been actioned'); 80 | * }; 81 | * 82 | * var handleOnReady = function() { 83 | * console.log('Tile is ready'); 84 | * }; 85 | * 86 | * var handleOnImageLoaded = function(tile, size) { 87 | * console.log('Image of tile ' + tile.id + ' has been preloaded'); 88 | * console.log('Image size is ' + size.width + 'x' + size.height); 89 | * }; 90 | * 91 | * var handleOnImageError = function(tile) { 92 | * console.log('Image of tile ' + tile.id + ' is on error'); 93 | * }; 94 | * 95 | * var selected = false; 96 | * 97 | * 98 | * 108 | * 109 | * @class oplTile 110 | */ 111 | (function(app) { 112 | 113 | app.component('oplTile', { 114 | templateUrl: 'opl-tile.html', 115 | controller: 'OplTileController', 116 | bindings: { 117 | oplData: '<', 118 | oplAbstract: '<', 119 | oplOnSelect: '&', 120 | oplOnMore: '&', 121 | oplOnClose: '&', 122 | oplOnReady: '&', 123 | oplOnImagePreloaded: '&', 124 | oplOnImageError: '&' 125 | } 126 | }); 127 | 128 | })(angular.module('ov.player')); 129 | -------------------------------------------------------------------------------- /src/components/tile/tile.html: -------------------------------------------------------------------------------- 1 |
6 |
10 | 11 |
12 | 13 |
14 |

15 | 16 |

17 |
18 |

19 |
20 |
21 | access_time 22 | 23 | 24 | 30 | 31 |
32 |
33 | 34 |
35 | 39 |

40 |
41 | 47 |
48 | 49 |
50 |
51 |

52 |
53 |
54 | 60 |
61 |
62 | 63 |
64 | 71 |
72 |
73 |
74 | 75 | 84 |
85 | 86 |
87 | 88 | 89 |
90 | -------------------------------------------------------------------------------- /src/components/tiles/tiles.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | /** 8 | * Creates a new AngularJS component as an opl-tiles HTML element to create a list of points of interest. 9 | * 10 | * opl-tiles is composed of scrollable list of opl-tile components. 11 | * 12 | * Attributes are: 13 | * - [Array] **opl-data**: The list of tiles description objects as defined in opl-tile component 14 | * - [Number] **opl-time** The actual time in milliseconds, all tiles with a time inferior to this time will be 15 | * marked as past tiles, tile corresponding to actual time will be marked as selected 16 | * - [Function] **opl-on-tile-select** The function to call when a tile is actioned 17 | * - [Function] **opl-on-tile-info** The function to call when a tile "more info" button is actioned 18 | * - [Function] **opl-on-tile-close** The function to call when a tile close button is actioned 19 | * 20 | * @example 21 | * var tiles = [ 22 | * { 23 | * id: 42, 24 | * type: 'text', 25 | * title: 'Title', 26 | * time: 20000, 27 | * description: '

Description', 28 | * file: { 29 | * url: 'http://host.local/document.pdf', 30 | * originalName: 'download-name-without-extension' 31 | * } 32 | * }, 33 | * { 34 | * id: 43, 35 | * type: 'image', 36 | * time: 40000, 37 | * image: { 38 | * small: { 39 | * url: 'http://host.local/image-small.jpg', 40 | * x: 0, 41 | * y: 0 42 | * }, 43 | * large: 'http://host.local/image-large.jpg' 44 | * } 45 | * } 46 | * ]; 47 | * 48 | * var time = 2000; 49 | * 50 | * var handleTileSelect = function(tile) { 51 | * console.log('Tile ' + tile.id + ' has been actioned'); 52 | * }; 53 | * 54 | * var handleTileInfo = function(tile) { 55 | * console.log('Tile ' + tile.id + ' more button has been actioned'); 56 | * }; 57 | * 58 | * var handleTileClose = function(tile) { 59 | * console.log('Tile ' + tile.id + ' close button has been actioned'); 60 | * }; 61 | * 62 | * 69 | * 70 | * @class oplTiles 71 | */ 72 | (function(app) { 73 | 74 | app.component('oplTiles', { 75 | templateUrl: 'opl-tiles.html', 76 | controller: 'OplTilesController', 77 | bindings: { 78 | oplData: '<', 79 | oplTime: '<', 80 | oplOnTileSelect: '&', 81 | oplOnTileInfo: '&', 82 | oplOnTileClose: '&' 83 | } 84 | }); 85 | 86 | })(angular.module('ov.player')); 87 | -------------------------------------------------------------------------------- /src/components/tiles/tiles.html: -------------------------------------------------------------------------------- 1 |
2 | 11 |
12 |
    13 |
  • 14 | 24 |
  • 25 |
26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /src/components/vimeo.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/youtube.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/index.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ov.player 5 | */ 6 | 7 | (function(angular) { 8 | 9 | /** 10 | * Creates the ov.player module. 11 | * 12 | * ov.player offers a directive to easily create a player with associated indexes, chapters and tags. All you have 13 | * to do is use the component oplPlayer. 14 | * 15 | * @main ov.player 16 | */ 17 | var app = angular.module('ov.player', ['ngCookies']); 18 | 19 | // Player translations 20 | app.constant('oplI18nTranslations', { 21 | en: { 22 | LOADING: 'Loading...', 23 | MEDIA_ERR_NO_SOURCE: 'A network error caused the video download to fail part-way.', 24 | MEDIA_ERR_NETWORK: 'A network error caused the video download to fail part-way.', 25 | MEDIA_ERR_DECODE: 'The video playback was aborted due to a corruption problem ' + 26 | 'or because the video used features your browser did not support.', 27 | MEDIA_ERR_SRC_NOT_SUPPORTED: 'The video could not be loaded, either because the server or network failed ' + 28 | 'or because the format is not supported.', 29 | MEDIA_ERR_PERMISSION: 'Video not available or private.', 30 | MEDIA_ERR_DEFAULT: 'An unknown error occurred.', 31 | PREVIEW_IMAGE_PRELOAD_ERROR: 'Image not available', 32 | TILE_MORE_INFO_BUTTON_LABEL: 'More info', 33 | TILE_CLOSE_BUTTON_LABEL: 'Close', 34 | TILE_IMAGE_PRELOAD_ERROR: 'Image not available', 35 | CONTROLS_VOLUME_MUTE_ARIA_LABEL: 'Mute', 36 | CONTROLS_VOLUME_UNMUTE_ARIA_LABEL: 'Mute', 37 | CONTROLS_VOLUME_CURSOR_ARIA_LABEL: 'Change volume', 38 | CONTROLS_VOLUME_TEXT_ARIA_LABEL: 'Volume %value%%', 39 | CONTROLS_TEMPLATE_SPLIT_50_50_ARIA_LABEL: 'Mode 50/50', 40 | CONTROLS_TEMPLATE_FULL_1_ARIA_LABEL: 'Mode video only', 41 | CONTROLS_TEMPLATE_FULL_2_ARIA_LABEL: 'Mode presentation only', 42 | CONTROLS_TEMPLATE_SPLIT_25_75_ARIA_LABEL: 'Mode with priority on presentation', 43 | CONTROLS_SETTINGS_ARIA_LABEL: 'Settings', 44 | CONTROLS_SETTINGS_QUALITIES_TITLE: 'Quality', 45 | CONTROLS_SETTINGS_SOURCES_TITLE: 'Source', 46 | CONTROLS_SETTINGS_SOURCE_LABEL: 'Source %source%', 47 | CONTROLS_PLAY_ARIA_LABEL: 'Play', 48 | CONTROLS_PAUSE_ARIA_LABEL: 'Pause', 49 | CONTROLS_TEMPLATES_SELECTOR_ARIA_LABEL: 'Choose player template', 50 | CONTROLS_TIME_BAR_ARIA_LABEL: 'Navigate in the video', 51 | CONTROLS_TIME_BAR_ARIA_VALUE_TEXT: '%value%% seen', 52 | CONTROLS_FULLSCREEN_ARIA_LABEL: 'Fullscreen', 53 | CONTROLS_FULLSCREEN_EXIT_ARIA_LABEL: 'Exit fullscreen', 54 | TABS_CHAPTERS_ARIA_LABEL: 'Display chapters', 55 | TABS_TIMECODES_ARIA_LABEL: 'Display images', 56 | TABS_TAGS_ARIA_LABEL: 'Display tags' 57 | }, 58 | fr: { 59 | LOADING: 'Chargement...', 60 | MEDIA_ERR_NO_SOURCE: 'Une erreur réseau à causé l\'échec du téléchargement de la vidéo.', 61 | MEDIA_ERR_NETWORK: 'Une erreur réseau à causé l\'échec du téléchargement de la vidéo.', 62 | MEDIA_ERR_DECODE: 'La lecture de la vidéo a été abandonnée en raison d\' un problème de corruption ' + 63 | 'ou parce que la vidéo utilise des fonctionnalités que votre navigateur ne supporte pas.', 64 | MEDIA_ERR_SRC_NOT_SUPPORTED: 'La vidéo ne peut être chargée , soit parce que le serveur ou le réseau à échoué ' + 65 | 'ou parce que le format ne sont pas supportées.', 66 | MEDIA_ERR_PERMISSION: 'Vidéo indisponible ou privée.', 67 | MEDIA_ERR_DEFAULT: 'Une erreur inconnue est survenue.', 68 | PREVIEW_IMAGE_PRELOAD_ERROR: 'Image non disponible', 69 | TILE_MORE_INFO_BUTTON_LABEL: 'Plus d\'info', 70 | TILE_CLOSE_BUTTON_LABEL: 'Fermer', 71 | TILE_IMAGE_PRELOAD_ERROR: 'Image non disponible', 72 | CONTROLS_VOLUME_MUTE_ARIA_LABEL: 'Désactiver le son', 73 | CONTROLS_VOLUME_UNMUTE_ARIA_LABEL: 'Activer le son', 74 | CONTROLS_VOLUME_CURSOR_ARIA_LABEL: 'Modifier le volume', 75 | CONTROLS_VOLUME_TEXT_ARIA_LABEL: 'Volume à %value%%', 76 | CONTROLS_TEMPLATE_SPLIT_50_50_ARIA_LABEL: 'Mode 50/50', 77 | CONTROLS_TEMPLATE_FULL_1_ARIA_LABEL: 'Mode vidéo seule', 78 | CONTROLS_TEMPLATE_FULL_2_ARIA_LABEL: 'Mode présentation seule', 79 | CONTROLS_TEMPLATE_SPLIT_25_75_ARIA_LABEL: 'Mode avec priorité sur la présentation', 80 | CONTROLS_SETTINGS_ARIA_LABEL: 'Paramètres', 81 | CONTROLS_SETTINGS_QUALITIES_TITLE: 'Qualité', 82 | CONTROLS_SETTINGS_SOURCES_TITLE: 'Source', 83 | CONTROLS_SETTINGS_SOURCE_LABEL: 'Source %source%', 84 | CONTROLS_PLAY_ARIA_LABEL: 'Lire', 85 | CONTROLS_PAUSE_ARIA_LABEL: 'Pause', 86 | CONTROLS_TEMPLATES_SELECTOR_ARIA_LABEL: 'Choisir un modèle de player', 87 | CONTROLS_TIME_BAR_ARIA_LABEL: 'Se déplacer dans la vidéo', 88 | CONTROLS_TIME_BAR_ARIA_VALUE_TEXT: '%value%% vu', 89 | CONTROLS_FULLSCREEN_ARIA_LABEL: 'Plein écran', 90 | CONTROLS_FULLSCREEN_EXIT_ARIA_LABEL: 'Quitter le plein écran', 91 | TABS_CHAPTERS_ARIA_LABEL: 'Afficher les chapitres', 92 | TABS_TIMECODES_ARIA_LABEL: 'Afficher les indexes', 93 | TABS_TAGS_ARIA_LABEL: 'Afficher les tags' 94 | } 95 | }); 96 | 97 | // Player errors 98 | // Errors from 1 to 4 are the same as in the HTMLVideoElement specification 99 | app.constant('oplPlayerErrors', { 100 | MEDIA_ERR_NO_SOURCE: 0, 101 | MEDIA_ERR_ABORTED: 1, 102 | MEDIA_ERR_NETWORK: 2, 103 | MEDIA_ERR_DECODE: 3, 104 | MEDIA_ERR_SRC_NOT_SUPPORTED: 4, 105 | MEDIA_ERR_PERMISSION: 5, 106 | MEDIA_ERR_UNKNOWN: 6 107 | }); 108 | 109 | // Configures the ov.player application 110 | app.config(function() { 111 | var deactivateDashJsLogs = function(player, mediaPlayer) { 112 | if (videojs && videojs.log) { 113 | mediaPlayer.updateSettings({ 114 | debug: {logLevel: dashjs.Debug.LOG_LEVEL_NONE} 115 | }); 116 | } 117 | }; 118 | 119 | if (typeof videojs !== 'undefined' && videojs.Html5DashJS && videojs.Html5DashJS.hook) 120 | videojs.Html5DashJS.hook('beforeinitialize', deactivateDashJsLogs); 121 | }); 122 | 123 | })(angular); 124 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | // INJECT_EXTERNAL_FONTS_SCSS 2 | 3 | $MEDIA_LARGE_BREAKPOINT: 768px; 4 | $MEDIA_SMALL_BREAKPOINT: 320px; 5 | $HASH: unique-id(); 6 | 7 | @font-face { 8 | font-family: "OpenVeo-Player-Icons"; 9 | src: url("./fonts/openveo-player-icons/openveo-player-icons.woff?#{$HASH}") format("woff"), 10 | url("./fonts/openveo-player-icons/openveo-player-icons.ttf?#{$HASH}") format("truetype"), 11 | url("./fonts/openveo-player-icons/openveo-player-icons.svg?#{$HASH}") format("svg"); 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | 16 | .opl-icon { 17 | display: inline-block; 18 | font-family: 'OpenVeo-Player-Icons'; 19 | font-weight: normal; 20 | font-style: normal; 21 | font-size: 24px; 22 | line-height: 1; 23 | text-transform: none; 24 | letter-spacing: normal; 25 | word-wrap: normal; 26 | white-space: nowrap; 27 | direction: ltr; 28 | text-rendering: optimizeLegibility; 29 | font-feature-settings: 'liga'; 30 | -webkit-font-smoothing: antialiased; 31 | -moz-osx-font-smoothing: grayscale; 32 | } 33 | 34 | // INJECT_SCSS 35 | --------------------------------------------------------------------------------