├── .gitignore ├── .npmignore ├── package.json ├── license.md ├── gulpfile.js ├── src ├── jouele.skin.css └── jouele.css ├── dist ├── jouele.min.css └── jouele.min.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | node_modules/ 4 | mp3/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .gitignore 4 | gulpfile.js 5 | node_modules/ 6 | mp3/ 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ilyabirman-jouele", 3 | "description": "A simple and beautiful audio player for the web", 4 | "version": "3.0.7-beta", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ilyabirman/Jouele.git" 8 | }, 9 | "license": "MIT", 10 | "author": { 11 | "name": "Ilya Birman", 12 | "email": "ilyabirman@ilyabirman.ru", 13 | "url": "https://ilyabirman.ru" 14 | }, 15 | "contributors": [ 16 | { 17 | "name": "Evgeniy Lazarev", 18 | "email": "me@lazarev.me", 19 | "url": "https://lazarev.me" 20 | } 21 | ], 22 | "main": "dist/jouele.min.js", 23 | "style": "dist/jouele.min.css", 24 | "dependencies": { 25 | "howler": "2.0.15" 26 | }, 27 | "peerDependencies": { 28 | "jquery": "3.x" 29 | }, 30 | "devDependencies": { 31 | "gulp": "^4.0.2", 32 | "gulp-clean-css": "^4.2.0", 33 | "gulp-concat": "^2.6.1", 34 | "gulp-concat-css": "^3.1.0", 35 | "gulp-uglify": "^3.0.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ilya Birman 4 | Copyright (c) 2015 Evgeniy Lazarev 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var cleanCss = require('gulp-clean-css'); 6 | var concat = require("gulp-concat"); 7 | var concatCss = require('gulp-concat-css'); 8 | var uglify = require('gulp-uglify'); 9 | 10 | gulp.task('patch-howler', function (done) { 11 | var howlerPath = path.join(__dirname, 'node_modules/howler/src/howler.core.js'); 12 | var content = fs.readFileSync(howlerPath, 'utf8'); 13 | 14 | // Patch Opera browser version regex: /OPR\/([0-6].)/ → /OPR\/(\d+)/ 15 | // Simple string replacement - replace ([0-6].) with (\d+) 16 | var patchedContent = content.replace('([0-6].)', '(\\d+)'); 17 | 18 | if (content === patchedContent) { 19 | console.warn('Warning: Howler.js patch was not applied. The file may have already been patched or the pattern changed.'); 20 | } 21 | 22 | fs.writeFileSync(howlerPath, patchedContent, 'utf8'); 23 | done(); 24 | }); 25 | 26 | gulp.task('minify-css', function () { 27 | return gulp.src('src/*.css') 28 | .pipe(concatCss('dist/jouele.min.css')) 29 | .pipe(cleanCss({compatibility: 'ie8'})) 30 | .pipe(gulp.dest('./')) 31 | }); 32 | gulp.task('uglify-js', gulp.series('patch-howler', function() { 33 | return gulp.src(['node_modules/howler/src/howler.core.js', 'src/jouele.js']) 34 | .pipe(concat('jouele.min.js')) 35 | .pipe(uglify()) 36 | .pipe(gulp.dest('dist')) 37 | })); 38 | gulp.task('default', gulp.parallel('minify-css', 'uglify-js')); 39 | -------------------------------------------------------------------------------- /src/jouele.skin.css: -------------------------------------------------------------------------------- 1 | /* 2 | Make a skin for your page using this pattern. 3 | Change "dark" on your preferred skin name and use option "skin" when init Jouele (see documentation). 4 | */ 5 | 6 | .jouele-skin-dark .jouele-progress-line-bar_base:after { /* Control 'timeline' */ 7 | background-color: #bfbfbf; 8 | } 9 | .jouele-skin-dark .jouele-progress-line-bar_play:after { /* Control 'elapsed' */ 10 | background-color: currentColor; 11 | } 12 | .jouele-skin-dark .jouele-progress-line-bar_play.jouele-is-playing:after { /* Control 'elapsed' when playing */ 13 | background-color: #f59331; 14 | } 15 | .jouele-skin-dark .jouele-progress-line:hover .jouele-progress-line-bar_play:after { /* Control 'elapsed' on hover */ 16 | background-color: #d04000; 17 | } 18 | .jouele-skin-dark .jouele-progress-line-lift { /* Control 'position' */ 19 | background-color: currentColor; 20 | } 21 | .jouele-skin-dark .jouele-progress-line-lift.jouele-is-playing { /* Control 'position' when playing */ 22 | background-color: #f59331; 23 | } 24 | .jouele-skin-dark .jouele-progress-line:hover .jouele-progress-line-lift { /* Control 'position' on hover */ 25 | background-color: #d04000; 26 | } 27 | .jouele-skin-dark .jouele-progress-line-lift:before { /* Preloader animation in control 'position' */ 28 | border-color: currentColor; 29 | } 30 | .jouele-skin-dark .jouele-progress-line-lift.jouele-is-playing:before { /* Preloader animation in control 'position' when playing */ 31 | border-color: #f59331; 32 | } 33 | .jouele-skin-dark .jouele-progress-line:hover .jouele-progress-line-lift:before { /* Preloader animation in control 'position' on hover */ 34 | border-color: #d04000; 35 | } 36 | 37 | .jouele-skin-dark .jouele-info-control-button-icon_unavailable .jouele-svg-color, 38 | .jouele-skin-dark .jouele-info-control-button-icon_play .jouele-svg-color { /* Control 'play-pause' */ 39 | fill: currentColor; 40 | } 41 | .jouele-skin-dark .jouele-info-control-button-icon_play.jouele-is-playing .jouele-svg-color { /* Control 'play-pause' when playing */ 42 | fill: #f59331; 43 | } 44 | .jouele-skin-dark .jouele-info-control-button:hover .jouele-info-control-button-icon_play .jouele-svg-color { /* Control 'play-pause' on hover */ 45 | fill: #d04000; 46 | } 47 | .jouele-skin-dark .jouele-info-time { /* Control 'time-elapsed' and 'time-total' */ 48 | color: inherit; 49 | } 50 | -------------------------------------------------------------------------------- /dist/jouele.min.css: -------------------------------------------------------------------------------- 1 | .jouele.jouele_inited{display:block;position:relative;line-height:1.15}.jouele-progress{position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.jouele-progress-line{position:relative;height:.8em}.jouele-progress-line.jouele-is-available{cursor:pointer}.jouele-progress-line-bar_base,.jouele-progress-line-bar_play{position:absolute;top:0;left:0;height:100%;box-sizing:border-box}.jouele-progress-line-bar_play{top:0;left:0}.jouele-progress-line-bar_base{width:100%}.jouele-progress-line-bar_play.jouele-is-unavailable{display:none}.jouele-progress-line.jouele-is-available .jouele-progress-line-bar_play{cursor:pointer}.jouele-progress-line-bar_base:after,.jouele-progress-line-bar_play:after{content:'';position:absolute;top:50%;left:0;right:0;height:1px}.jouele-progress-line-bar_base:after{background-color:#bfbfbf}.jouele-progress-line-bar_play:after{background-color:#000;transition:background-color .16s linear}.jouele-progress-line-bar_play.jouele-is-playing:after{background-color:#f59331}.jouele-progress-line:hover .jouele-progress-line-bar_play:after{background-color:#d04000;transition:none}.jouele-progress-line-lift{left:0;top:50%;width:5px;height:5px;margin:-2px 0 0;border-radius:50%;background-color:#000;position:absolute;transition:width .16s linear,height .16s linear,margin .16s linear,background-color .16s linear}.jouele-progress-line-lift.jouele-is-playing{background-color:#f59331}.jouele-progress-line-lift.jouele-is-unavailable{display:none}.jouele-progress-line.jouele-is-available .jouele-progress-line-lift{cursor:pointer}.jouele-progress-line:hover .jouele-progress-line-lift{background-color:#d04000;width:7px;height:7px;margin-top:-3px;margin-left:-1px;transition:none}.jouele-progress-line-lift:before{content:'';width:100%;height:100%;padding:2px;display:block;font-size:0;position:absolute;left:-3px;top:-3px;border:1px solid #000;border-radius:50%;transition:border-color .16s linear,opacity .16s linear;-webkit-animation:preloader-animate .75s ease infinite;-moz-animation:preloader-animate .75s ease infinite;-o-animation:preloader-animate .75s ease infinite;animation:preloader-animate .75s ease infinite;display:none\0/;opacity:0}.jouele-progress-line-lift.jouele-is-buffering:before{opacity:1;display:block\0/}.jouele-progress-line-lift.jouele-is-playing:before{border-color:#f59331}.jouele-progress-line:hover .jouele-progress-line-lift:before{border-color:#d04000;transition:none}@-webkit-keyframes preloader-animate{0%{-webkit-transform:scale(1);transform:scale(1)}50%{-webkit-transform:scale(1.2);transform:scale(1.2)}100%{-webkit-transform:scale(1);transform:scale(1)}}@-moz-keyframes preloader-animate{0%{-moz-transform:scale(1);transform:scale(1)}50%{-moz-transform:scale(1.2);transform:scale(1.2)}100%{-moz-transform:scale(1);transform:scale(1)}}@-o-keyframes preloader-animate{0%{-o-transform:scale(1);transform:scale(1)}50%{-o-transform:scale(1.2);transform:scale(1.2)}100%{-o-transform:scale(1);transform:scale(1)}}@keyframes preloader-animate{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-o-transform:scale(1);transform:scale(1)}50%{-webkit-transform:scale(1.2);-moz-transform:scale(1.2);-o-transform:scale(1.2);transform:scale(1.2)}100%{-webkit-transform:scale(1);-moz-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}.jouele-info{position:relative;overflow:hidden;padding:0 0 .2em;margin:0 0 0 -.3em;z-index:1}.jouele-info-control{overflow:hidden;font-size:1em;line-height:1}.jouele-info-control *{line-height:1}.jouele-info-control-button{float:left;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.jouele-info-control-link{cursor:pointer;padding:0!important;margin:0!important;background:0 0!important;display:block!important;text-decoration:none!important;border:0!important;color:currentColor!important}.jouele-info-control-button-icon{display:none}.jouele-info-control-button-icon_unavailable{display:block}.jouele-info-control-button-icon_play.jouele-is-available{display:block}.jouele-svg{vertical-align:middle;width:1.15em;height:1.15em}.jouele-svg-color{transition:fill .16s linear}.jouele-info-control-button-icon_play .jouele-svg-color,.jouele-info-control-button-icon_unavailable .jouele-svg-color{fill:#000}.jouele-info-control-button-icon_play.jouele-is-playing .jouele-svg-color{fill:#f59331}.jouele-info-control-button:hover .jouele-info-control-button-icon_play .jouele-svg-color{fill:#d04000;transition:none}.jouele-info-control-text{line-height:1.15!important;padding:0 2em 0 0;overflow:hidden;word-wrap:break-word;white-space:normal}.jouele-info-time{float:right;font-size:.8em;line-height:1.4375!important;color:#606060;text-align:right;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.jouele-info-time__current{float:left;margin-right:.65em}.jouele-info-time__total{float:right}.jouele-info-time__current.jouele-is-unavailable,.jouele-info-time__total.jouele-is-unavailable{display:none}.jouele_timeline_hide .jouele-progress-line{margin-top:-.8em;display:none\0/;opacity:0;transition:opacity .33s ease-out,margin-top .33s ease-out}.jouele_timeline_hide .jouele-progress-line.jouele-is-playing{margin-top:0;opacity:1;display:block\0/}.jouele-hidden{display:none!important}.jouele-skin-dark .jouele-progress-line-bar_base:after{background-color:#bfbfbf}.jouele-skin-dark .jouele-progress-line-bar_play:after{background-color:currentColor}.jouele-skin-dark .jouele-progress-line-bar_play.jouele-is-playing:after{background-color:#f59331}.jouele-skin-dark .jouele-progress-line:hover .jouele-progress-line-bar_play:after{background-color:#d04000}.jouele-skin-dark .jouele-progress-line-lift{background-color:currentColor}.jouele-skin-dark .jouele-progress-line-lift.jouele-is-playing{background-color:#f59331}.jouele-skin-dark .jouele-progress-line:hover .jouele-progress-line-lift{background-color:#d04000}.jouele-skin-dark .jouele-progress-line-lift:before{border-color:currentColor}.jouele-skin-dark .jouele-progress-line-lift.jouele-is-playing:before{border-color:#f59331}.jouele-skin-dark .jouele-progress-line:hover .jouele-progress-line-lift:before{border-color:#d04000}.jouele-skin-dark .jouele-info-control-button-icon_play .jouele-svg-color,.jouele-skin-dark .jouele-info-control-button-icon_unavailable .jouele-svg-color{fill:currentColor}.jouele-skin-dark .jouele-info-control-button-icon_play.jouele-is-playing .jouele-svg-color{fill:#f59331}.jouele-skin-dark .jouele-info-control-button:hover .jouele-info-control-button-icon_play .jouele-svg-color{fill:#d04000}.jouele-skin-dark .jouele-info-time{color:inherit} -------------------------------------------------------------------------------- /src/jouele.css: -------------------------------------------------------------------------------- 1 | .jouele.jouele_inited { 2 | display: block; 3 | position: relative; 4 | line-height: 1.15; 5 | } 6 | 7 | .jouele-progress { 8 | position: relative; 9 | -webkit-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | } 14 | 15 | .jouele-progress-line { 16 | position: relative; 17 | height: 0.8em; 18 | } 19 | .jouele-progress-line.jouele-is-available { 20 | cursor: pointer; 21 | } 22 | 23 | .jouele-progress-line-bar_base, 24 | .jouele-progress-line-bar_play { 25 | position: absolute; 26 | top: 0; 27 | left: 0; 28 | height: 100%; 29 | box-sizing: border-box; 30 | } 31 | .jouele-progress-line-bar_play { 32 | top: 0; 33 | left: 0; 34 | } 35 | .jouele-progress-line-bar_base { 36 | width: 100%; 37 | } 38 | .jouele-progress-line-bar_play.jouele-is-unavailable { 39 | display: none; 40 | } 41 | .jouele-progress-line.jouele-is-available .jouele-progress-line-bar_play { 42 | cursor: pointer; 43 | } 44 | 45 | .jouele-progress-line-bar_base:after, 46 | .jouele-progress-line-bar_play:after { 47 | content: ''; 48 | position: absolute; 49 | top: 50%; 50 | left: 0; 51 | right: 0; 52 | height: 1px; 53 | } 54 | .jouele-progress-line-bar_base:after { 55 | background-color: #bfbfbf; 56 | } 57 | .jouele-progress-line-bar_play:after { 58 | background-color: #000; 59 | transition: background-color 0.16s linear; 60 | } 61 | .jouele-progress-line-bar_play.jouele-is-playing:after { 62 | background-color: #f59331; 63 | } 64 | .jouele-progress-line:hover .jouele-progress-line-bar_play:after { 65 | background-color: #d04000; 66 | transition: none; 67 | } 68 | 69 | .jouele-progress-line-lift { 70 | left: 0; 71 | top: 50%; 72 | width: 5px; 73 | height: 5px; 74 | margin: -2px 0 0; 75 | border-radius: 50%; 76 | background-color: #000; 77 | position: absolute; 78 | transition: width 0.16s linear, height 0.16s linear, margin 0.16s linear, background-color 0.16s linear; 79 | } 80 | .jouele-progress-line-lift.jouele-is-playing { 81 | background-color: #f59331; 82 | } 83 | .jouele-progress-line-lift.jouele-is-unavailable { 84 | display: none; 85 | } 86 | .jouele-progress-line.jouele-is-available .jouele-progress-line-lift { 87 | cursor: pointer; 88 | } 89 | .jouele-progress-line:hover .jouele-progress-line-lift { 90 | background-color: #d04000; 91 | width: 7px; 92 | height: 7px; 93 | margin-top: -3px; 94 | margin-left: -1px; 95 | transition: none; 96 | } 97 | 98 | .jouele-progress-line-lift:before { 99 | content: ''; 100 | width: 100%; 101 | height: 100%; 102 | padding: 2px; 103 | display: block; 104 | font-size: 0; 105 | position: absolute; 106 | left: -3px; 107 | top: -3px; 108 | border: 1px solid #000; 109 | border-radius: 50%; 110 | transition: border-color 0.16s linear, opacity 0.16s linear; 111 | -webkit-animation: preloader-animate .75s ease infinite; 112 | -moz-animation: preloader-animate .75s ease infinite; 113 | -o-animation: preloader-animate .75s ease infinite; 114 | animation: preloader-animate .75s ease infinite; 115 | display: none\0/; /* IE 8 */ 116 | opacity: 0; 117 | } 118 | .jouele-progress-line-lift.jouele-is-buffering:before { 119 | opacity: 1; 120 | display: block\0/; /* IE 8 */ 121 | } 122 | .jouele-progress-line-lift.jouele-is-playing:before { 123 | border-color: #f59331; 124 | } 125 | .jouele-progress-line:hover .jouele-progress-line-lift:before { 126 | border-color: #d04000; 127 | transition: none; 128 | } 129 | @-webkit-keyframes preloader-animate { 130 | 0% { 131 | -webkit-transform: scale(1); 132 | transform: scale(1); 133 | } 134 | 50% { 135 | -webkit-transform: scale(1.2); 136 | transform: scale(1.2); 137 | } 138 | 100% { 139 | -webkit-transform: scale(1); 140 | transform: scale(1); 141 | } 142 | } 143 | @-moz-keyframes preloader-animate { 144 | 0% { 145 | -moz-transform: scale(1); 146 | transform: scale(1); 147 | } 148 | 50% { 149 | -moz-transform: scale(1.2); 150 | transform: scale(1.2); 151 | } 152 | 100% { 153 | -moz-transform: scale(1); 154 | transform: scale(1); 155 | } 156 | } 157 | @-o-keyframes preloader-animate { 158 | 0% { 159 | -o-transform: scale(1); 160 | transform: scale(1); 161 | } 162 | 50% { 163 | -o-transform: scale(1.2); 164 | transform: scale(1.2); 165 | } 166 | 100% { 167 | -o-transform: scale(1); 168 | transform: scale(1); 169 | } 170 | } 171 | @keyframes preloader-animate { 172 | 0% { 173 | -webkit-transform: scale(1); 174 | -moz-transform: scale(1); 175 | -o-transform: scale(1); 176 | transform: scale(1); 177 | } 178 | 50% { 179 | -webkit-transform: scale(1.2); 180 | -moz-transform: scale(1.2); 181 | -o-transform: scale(1.2); 182 | transform: scale(1.2); 183 | } 184 | 100% { 185 | -webkit-transform: scale(1); 186 | -moz-transform: scale(1); 187 | -o-transform: scale(1); 188 | transform: scale(1); 189 | } 190 | } 191 | 192 | 193 | .jouele-info { 194 | position: relative; 195 | overflow: hidden; 196 | padding: 0 0 0.2em; 197 | margin: 0 0 0 -0.3em; 198 | z-index: 1; 199 | } 200 | 201 | .jouele-info-control { 202 | overflow: hidden; 203 | font-size: 1em; 204 | line-height: 1; 205 | } 206 | .jouele-info-control * { 207 | line-height: 1; 208 | } 209 | 210 | .jouele-info-control-button { 211 | float: left; 212 | -webkit-user-select: none; 213 | -moz-user-select: none; 214 | -ms-user-select: none; 215 | user-select: none; 216 | } 217 | .jouele-info-control-link { 218 | cursor: pointer; 219 | /* Перекрываем все возможные каскады на сайте */ 220 | padding: 0 !important; 221 | margin: 0 !important; 222 | background: transparent !important; 223 | display: block !important; 224 | text-decoration: none !important; 225 | border: 0 !important; 226 | color: currentColor !important; 227 | } 228 | .jouele-info-control-button-icon { 229 | display: none; 230 | } 231 | .jouele-info-control-button-icon_unavailable { 232 | display: block; 233 | } 234 | .jouele-info-control-button-icon_play.jouele-is-available { 235 | display: block; 236 | } 237 | .jouele-svg { 238 | vertical-align: middle; 239 | width: 1.15em; 240 | height: 1.15em; 241 | } 242 | .jouele-svg-color { 243 | transition: fill 0.16s linear; 244 | } 245 | .jouele-info-control-button-icon_unavailable .jouele-svg-color, 246 | .jouele-info-control-button-icon_play .jouele-svg-color { 247 | fill: #000; 248 | } 249 | .jouele-info-control-button-icon_play.jouele-is-playing .jouele-svg-color { 250 | fill: #f59331; 251 | } 252 | .jouele-info-control-button:hover .jouele-info-control-button-icon_play .jouele-svg-color { 253 | fill: #d04000; 254 | transition: none; 255 | } 256 | .jouele-info-control-text { 257 | line-height: 1.15 !important; /* Перекрываем все возможные каскады на сайте */ 258 | padding: 0 2em 0 0; 259 | overflow: hidden; 260 | word-wrap: break-word; 261 | white-space: normal; 262 | } 263 | 264 | .jouele-info-time { 265 | float: right; 266 | font-size: 0.8em; 267 | line-height: 1.4375 !important; /* Перекрываем все возможные каскады на сайте */ 268 | color: #606060; 269 | text-align: right; 270 | white-space: nowrap; 271 | -webkit-user-select: none; 272 | -moz-user-select: none; 273 | -ms-user-select: none; 274 | user-select: none; 275 | } 276 | 277 | .jouele-info-time__current { 278 | float: left; 279 | margin-right: .65em; 280 | } 281 | 282 | .jouele-info-time__total { 283 | float: right; 284 | } 285 | 286 | .jouele-info-time__current.jouele-is-unavailable, 287 | .jouele-info-time__total.jouele-is-unavailable { 288 | display: none; 289 | } 290 | 291 | .jouele_timeline_hide .jouele-progress-line { 292 | margin-top: -0.8em; 293 | display: none\0/; /* IE 8 */ 294 | opacity: 0; 295 | transition: opacity 0.33s ease-out, margin-top 0.33s ease-out; 296 | } 297 | 298 | .jouele_timeline_hide .jouele-progress-line.jouele-is-playing { 299 | margin-top: 0; 300 | opacity: 1; 301 | display: block\0/; /* IE 8 */ 302 | } 303 | 304 | .jouele-hidden { 305 | display: none !important; 306 | } 307 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Жуэль 2 | [Жуэль](http://ilyabirman.ru/projects/jouele/) — простой и красивый плеер для веба. 3 | 4 | ## Простое подключение 5 | ```html 6 | 7 | 8 | 9 | 10 | 11 | 12 | ``` 13 | Минифицированный файл [`jouele.min.js`](dist/jouele.min.js) уже содержит в себе необходимый для работы Жуэля howler.js, так что вам не нужно подключать его отдельно. Если вы используете неминифицированный [`jouele.js`](src/jouele.js), подключите [howler.core.js v2.0.15](https://github.com/goldfire/howler.js/blob/v2.0.15/src/howler.core.js) до Жуэля. 14 | Если вы хотите подключить неминифицированный файл стилей, подключите [`jouele.css`](src/jouele.css) и [`jouele.skin.css`](src/jouele.skin.css). 15 | 16 | ## Простое использование (автоматическая инициализация) 17 | Каждая ссылка с классом `jouele` превратится в плеер МП3-файла, на который она ведёт 18 | ```html 19 | Ilya Birman: News 20 | ``` 21 | 22 | Каждый элемент с классом `jouele-control` превратится в контрол МП3-файла: какого-то конкретного (при установке `data-href`) или того, который играет сейчас («глобальный контрол»): 23 | ```html 24 | Play/pause 25 | Play/pause 26 | ``` 27 | 28 | ### Плейлисты, управление пробелом, расцветки, элементы управления 29 | Управляется декларативным АПИ: [https://ilyabirman.ru/projects/jouele/documentation/] 30 | 31 | ## Расширенные возможности 32 | 33 | ### Доступно в npm 34 | `npm install ilyabirman-jouele` 35 | 36 | ### Ручная инициализация 37 | Любая ссылка вида `` может быть превращена в плеер: 38 | ```javascript 39 | $("#music-item").jouele() 40 | ``` 41 | Даже для ручной инициализации класс `jouele` должен быть у ссылки. 42 | 43 | Любой элемент вида `` может быть превращён в контрол: 44 | ```javascript 45 | $("#music-control").jouele() 46 | ``` 47 | Даже для ручной инициализации класс `jouele-control` должен быть у элемента. 48 | 49 | ### Глобальный объект `$.Jouele` 50 | 51 | #### `$.Jouele.tracks` 52 | Тип: `Array` 53 | Список всех треков (МП3-файлов), инциализированных Жуэлем на странице (к одному треку может быть привязано несколько плееров и/или контролов). 54 | #### `$.Jouele.playlist` 55 | Тип: `Array` 56 | Массив со всеми плейлистами, созданными из плееров и контролов на странице. 57 | #### `$.Jouele.history` 58 | Тип: `Array` 59 | История воспроизведения треков. В историю кладутся объекты [JoueleInstance](#jouele-instance-и-api). 60 | #### `$.Jouele.controls` 61 | Тип: `Object` 62 | Объект со всеми «глобальными» контролами (то есть, теми контролами, у которых не указан `data-href`). 63 | #### `$.Jouele.options` 64 | Тип: `Object` 65 | Объект с глобальными опциями `pauseOnSpace`, `playOnSpace`, `scrollOnSpace`. 66 | Можно вручную установить `true` или `false` у каждой опции, чтобы мгновенно изменить [поведение пробела на странице](#для-обработки-нажатия-пробела-на-странице). 67 | #### `$.Jouele.helpers.formatTime(seconds)` 68 | Тип: `Function` 69 | Возвращает: `String` 70 | Возвращает строку вида "2:30", полученную из количества секунд (`seconds`, тип `Number`) 71 | #### `$.Jouele.helpers.makeSeconds(time_string)` 72 | Тип: `Function` 73 | Возвращает: `Number` 74 | Возвращает число секунд, полученное из строки вида "2:30" (`time_string`, тип `String`). 75 | #### `$.Jouele.version` 76 | Тип: `String` 77 | Версия Жуэля. 78 | 79 | ### Опции 80 | Могут быть установлены через data-атрибуты у DOM-элемента, а также (если не указано обратное) через вызов метода setOptions (см. ниже). 81 | 82 | #### Для обработки нажатия пробела на странице 83 | Эти опции можно в том числе менять напрямую в [`$.Jouele.options`](#joueleoptions). 84 | Любую из этих опции достаточно установить на любом одном плейлисте, плеере или контроле, чтобы она подействовала на всю страницу. 85 | 86 | #### `scrollOnSpace` 87 | Тип: `Boolean` 88 | По умолчанию: `true` 89 | Определяет, будет ли страница вести себя стандартно при нажатии пробела — скроллить контент на один экран вниз. 90 | По умолчанию Жуэль не изменяет стандартное поведение пробела в браузере. При установке `false` нажатие пробела будет перехвачено Жуэлем, событие будет обработано методом `preventDefault` — страница перестанет скроллиться. 91 | 92 | #### `playOnSpace` 93 | Тип: `Boolean` 94 | По умолчанию: `false` 95 | Определяет, будет ли Жуэль запускать воспроизведение трека по нажатию пробела. 96 | При установке `true` нажатие пробела будет перехвачено Жуэлем, и будет воспроизведён первый доступный трек (последний игравший до этого или первый на странице). 97 | 98 | #### `pauseOnSpace` 99 | Тип: `Boolean` 100 | По умолчанию: `false` 101 | Определяет, будет ли Жуэль останавливать воспроизведение трека по нажатию пробела. 102 | При установке `true` нажатие пробела будет перехвачено Жуэлем, и воспроизведение играющего трека будет остановлено. 103 | 104 | #### `spaceControl` 105 | Тип: `Boolean` 106 | По умолчанию: `false` 107 | Короткий вариант для трёх предыдущих опций. 108 | В случае `spaceControl: false` устанавливаются следующие значения: `scrollOnSpace = true`, `playOnSpace = false`, `pauseOnSpace = false`. 109 | В случае `spaceControl: true` устанавливаются следующие значения: `scrollOnSpace = false`, `playOnSpace = true`, `pauseOnSpace = true`. 110 | 111 | #### Для плееров, контролов и плейлистов 112 | 113 | ##### `repeat` 114 | Тип: `Boolean` 115 | Определяет, будет ли плейлист или трек автоматически играть заново после окончания. 116 | ```html 117 | 118 | ``` 119 | ```html 120 | 121 | ``` 122 | ```html 123 |
124 | ``` 125 | ```javascript 126 | JoueleInstance.setOptions({ repeat: true }) 127 | ``` 128 | 129 | #### Для плееров и плейлистов 130 | 131 | ##### `skin` 132 | Тип: `String` 133 | Добавляет к плееру класс вида `jouele-skin-{{skin}}`, где `{{skin}}` — определённое этой опцией имя скина. 134 | Если добавить скин к плейлисту, все экземпляры плеера внутри плейлиста будут выглядеть так, как если бы у каждого из них был определён такой скин. 135 | Установка скина имеет смысл только если к странице будет подключен дополнительный CSS со стилями для этого скина. Составить такой CSS можно на основе приложенного [`jouele.skin.css`](src/jouele.skin.css). Скопируйте файл, переименуйте его в `jouele.skin-{{skin}}.css` и все селекторы в нём на `jouele-skin-{{skin}}`, измените цвета на свой вкус, сохраните файл и подключите его после основного CSS-файла Жуэля. 136 | ```html 137 | 138 | ``` 139 | ```html 140 |
141 | ``` 142 | ```javascript 143 | JoueleInstance.setOptions({ skin: "blue" }) 144 | ``` 145 | Пример CSS для скина `blue`: 146 | ```css 147 | .jouele-skin-blue .jouele-progress-line-bar_base:after { background-color: #00f; } 148 | ``` 149 | 150 | #### Для плееров и контролов 151 | 152 | ##### `length` 153 | Тип: `String` или `Number` 154 | Продолжительность трека. Может быть строкой вида `"2:30"` (что означает 2 минуты 30 секунд) или числом вида `150` (что означает 150 секунд). 155 | Имеет смысл в случае, если вы хотите показывать продолжительность трека ещё до того, как он будет загружен. 156 | Если после загрузки трека окажется, что продолжительность не совпадает с заявленной, значение будет изменено на актуальное. 157 | ```html 158 | 159 | ``` 160 | ```html 161 | 162 | ``` 163 | ```javascript 164 | JoueleInstance.setOptions({ length: 150 }) 165 | ``` 166 | 167 | ##### `title` 168 | Тип: `String` 169 | Название трека. Будет отображено во всех контролах типа `"title"`. 170 | Если у трека есть плеер, то в `title` будет установлено содержимое тега ``, из которого был создан плеер. 171 | ```html 172 | Song 173 | ``` 174 | ```html 175 | 176 | ``` 177 | ```javascript 178 | JoueleInstance.setOptions({ title: "Song" }) 179 | ``` 180 | 181 | #### Для плееров 182 | 183 | ##### `hideTimelineOnPause` 184 | Тип: `Boolean` 185 | По умолчанию: `false` 186 | Установка свойства в `true` меняет визуальное поведение плеера — он скрывает таймлайн, если трек не играет. Это обеспечивается добавлением специального класса `jouele_timeline_hide` к DOM-элементу плеера `.jouele`. 187 | ```html 188 | 189 | ``` 190 | 191 | #### Для контролов 192 | 193 | ##### `href` 194 | Тип: `String` 195 | Ссылка на mp3-файл, который будет воспроизводиться Жуэлем. Не может быть установлен через `setOptions`. 196 | ```html 197 | 198 | ``` 199 | 200 | ### `Jouele` instance и API 201 | Экземпляр типа `Jouele` можно получить разными способами. Независимо от способа получения, экземпляр `Jouele` (далее `JoueleInstance`) всегда привязан к одному DOM-элементу (плееру или контролу) и всегда обладает одним и тем же API (за исключением «кастомного API», см. ниже). 202 | Основные способы получить `JoueleInstance`: 203 | - Получить через `$(element).data("jouele")` у нужного DOM-элемента (плеера или контрола) 204 | Например: 205 | ```javascript 206 | $(".music-tracks").find(".jouele").data("jouele") 207 | ``` 208 | ```javascript 209 | $(".music-controls").find(".jouele-control[data-type='play-pause']").data("jouele") 210 | ``` 211 | - Взять из [`$.Jouele.playlist`](#joueleplaylist) или [`$.Jouele.history`](#jouelehistory). 212 | 213 | #### Основное API 214 | 215 | ##### `JoueleInstance.play()` 216 | Запускает воспроизведение трека. 217 | Возвращает `JoueleInstance`. 218 | 219 | ##### `JoueleInstance.pause()` 220 | Останавливает воспроизведение трека (если трек не загрузился до минимально необходимого для воспроизведения уровня, он продолжит загружаться в фоне). 221 | Возвращает `JoueleInstance`. 222 | 223 | ##### `JoueleInstance.playFrom(timestamp)` 224 | Запускает воспроизведение трека с метки `timestamp`, где `timestamp` — строка вида `"2:30"` (что означает 2 минуты 30 секунд) или число вида `150` (что означает 150 секунд). 225 | Можно использовать, даже если длительность трека неизвестна и/или трек ещё не загружался. 226 | Возвращает `JoueleInstance`. 227 | 228 | ##### `JoueleInstance.seek(percent)` 229 | Запускает воспроизведение трека с позиции `percent`, где `percent` — числовое значение в процентах (от 0 до 100). 230 | Например, `.seek(10)` для трека длительностью 60 секунд запустит воспроизведение с 7-й секунды. 231 | Можно использовать, даже если длительность трека неизвестна и/или трек ещё не загружался. 232 | Возвращает `JoueleInstance`. 233 | 234 | ##### `JoueleInstance.destroy()` 235 | В случае вызова метода на плеере, уничтожает `JoueleInstance`, убирает плеер из плейлиста, уничтожает плеер, ставит на его место ссылку, из которой он был создан. Возвращает DOM-элемент этой ссылки. 236 | В случае вызова метода на контроле, уничтожает обработчики на контроле, убирает контрол из плейлиста. Возвращает DOM-элемент этого контрола. 237 | 238 | 239 | #### Дополнительное API 240 | 241 | ##### `JoueleInstance.getOptions()` 242 | Возвращает: `Object` 243 | Возвращает объект опций, применённых к этому `JoueleInstance`. 244 | 245 | ##### `JoueleInstance.setOptions(new_options)` 246 | Устанавливает новые опции через объект `new_options`. 247 | Подробнее в разделе [Опции](#опции). 248 | 249 | ##### `JoueleInstance.getHref()` 250 | Возвращает: `String` 251 | Возвращает URL трека. 252 | 253 | ##### `JoueleInstance.getTitle()` 254 | Возвращает: `String` 255 | Возвращает название трека. 256 | 257 | ##### `JoueleInstance.getTrack()` 258 | Возвращает: `Object` 259 | Возвращает объект трека из [`$.Jouele.tracks`](#joueletracks). 260 | Осторожно, этот объект предназначен для внутренних нужд Жуэля, и любые изменения этого объекта могут повлиять на работоспособность Жуэля. 261 | 262 | ##### `JoueleInstance.getTotalTime()` 263 | Возвращает: `Number` 264 | Возвращает продолжительность трека в секундах вида `150` (или `0`, если продолжительность неизвестна). 265 | 266 | ##### `JoueleInstance.getElapsedTime()` 267 | Возвращает: `Number` 268 | Возвращает текущее время трека в секундах вида `46` (или `0`, если трек не играл или сломан). 269 | 270 | ##### `JoueleInstance.getRemainingTime()` 271 | Возвращает: `Number` 272 | Возвращает оставшееся время трека в секундах вида `104` (или `0`, если продолжительность трека неизвестна). 273 | 274 | ##### `JoueleInstance.getPlaylistDOM()` 275 | Возвращает: `jQuery-object` 276 | Возвращает jQuery-объект с DOM-элементом плейлиста, в котором находится этот контрол/плеер (jQuery-объект может быть «пустым», если плейлиста нет). 277 | 278 | ##### `JoueleInstance.getPlaylist()` 279 | Возвращает: `Array` 280 | Возвращает ссылку на массив с плейлистом из [`$.Jouele.playlist`](#joueleplaylist), в котором находится этот контрол/плеер. 281 | 282 | ##### `JoueleInstance.isPlaying()` 283 | Возвращает: `Boolean` 284 | Показывает, играет ли в данный момент трек (также `true` если трек был запущен, но пока грузится). 285 | 286 | ##### `JoueleInstance.isPlayed()` 287 | Возвращает: `Boolean` 288 | Показывает, играл ли когда-либо трек на самом деле (`false` если трек был запущен, но фактически не проигрывался). 289 | 290 | ##### `JoueleInstance.isPaused()` 291 | Возвращает: `Boolean` 292 | Показывает, поставлен ли трек на паузу (`false` если трек не был запущен, `true` если был запущен, фактически не проигрывался, но был остановлен). 293 | 294 | ##### `JoueleInstance.isBroken()` 295 | Возвращает: `Boolean` 296 | Показывает, был ли трек запущен и определён как «сломанный» (URL недоступен). Если трек «сломан», для него недоступны все остальные методы. 297 | 298 | #### Кастомное API 299 | 300 | ##### Доступное для плееров 301 | 302 | ###### `JoueleInstance.$container` 303 | Содержит: `jQuery-object` 304 | Содержит jQuery-объект с DOM-элементом плеера `.jouele`. 305 | 306 | ###### `JoueleInstance.$link` 307 | Содержит: `jQuery-object` 308 | Содержит jQuery-объект с DOM-элементом ссылки `a`, из которой был создан Жуэль (сама ссылка при инициализации Жуэля убирается из DOM методом `$.detach`). 309 | 310 | ##### Доступное для контролов 311 | 312 | ###### `JoueleInstance.$control` 313 | Содержит: `jQuery-object` 314 | Содержит jQuery-объект с DOM-элементом контрола `.jouele-control`. 315 | 316 | ##### Доступное для контролов, находящихся внутри плеера (стандартный плеер состоит из обычных контролов) 317 | 318 | ###### `JoueleInstance.getParentJouele()` 319 | Содержит: `JoueleInstance` 320 | Содержит JoueleInstance, принадлежащий плееру, в котором этот контрол находится. 321 | 322 | #### События 323 | Некоторые контролы получают триггеры событий в случаях, когда данные, необходимые для их работы, обновляются. Это происходит при помощи метода `$.trigger()`. Ловить события можно через `$.on()`. 324 | Некоторые события приносят с собой данные в дополнительном аргументе. 325 | 326 | ##### `.trigger("jouele:position", position)` 327 | Вызывается на контролах типов `"position"`, `"elapsed"` и `"remaining"`, когда точка воспроизведения трека двигается. 328 | Свойство `position` типа `Number` соответствует проценту воспроизведённой части трека от его общей длительности (для контролов `"position"` и `"elapsed"`) или проценту оставшейся части трека (для контролов `"remaining"`). 329 | 330 | ##### `.trigger("jouele:totaltime", time_total)` 331 | Вызывается на контролах типа `"time-total"`, когда обновляются данные о длительности трека. 332 | Свойство `time_total` типа `Number` содержит длительность трека в секундах вида `150`. 333 | 334 | ##### `.trigger("jouele:elapsedtime", time_elapsed)` 335 | Вызывается на контролах типа `"time-elapsed"`, когда точка воспроизведения трека двигается или обновляются данные о длительности трека. 336 | Свойство `time_elapsed` типа `Number` содержит текущее время трека в секундах вида `46`. 337 | 338 | ##### `.trigger("jouele:remainingtime", time_remaining)` 339 | Вызывается на контролах типа `"time-remaining"`, когда точка воспроизведения трека двигается или обновляются данные о длительности трека. 340 | Свойство `remaining_time` типа `Number` содержит оставшееся время трека в секундах вида `104`. 341 | 342 | ##### `.trigger("jouele:rangein")` 343 | Вызывается на контролах типа `"seek"`, когда воспроизведение трека заходит в отрезок, указанный в `"data-range"` у этого контрола. 344 | 345 | ##### `.trigger("jouele:rangeout")` 346 | Вызывается на контролах типа `"seek"`, когда воспроизведение трека выходит из отрезка, указанного в `"data-range"` у этого контрола. 347 | 348 | ##### `.trigger("jouele:title")` 349 | Вызывается на контролах типа `"title"`, когда обновляются данные о названии трека. Узнать значение `title` можно через [`JoueleInstance.getTitle()`](#joueleinstancegettitle). 350 | 351 | #### Коллбэки 352 | У любого трека (к которому может быть привязано сколько угодно плееров и контролов) есть набор событий, вызываемых во время работы. К любому событию можно «прикрепить» собственный коллбэк, который будет вызываться до того, как будет исполнен код Жуэля. Если по какой-либо причине необходимо не исполнять код Жуэля (не рекомендуется!), нужно вернуть в коллбэке значение `false` (строго `Boolean`). 353 | 354 | ##### Установка коллбэка 355 | ```javascript 356 | JoueleInstance.getTrack().player["callbacks"]["название_коллбэка"] = function() { /* тело функции */ } 357 | ``` 358 | 359 | ##### События, на которые можно «прикрепить» коллбэки 360 | 361 | ###### `onloaderror` 362 | Ошибка загрузки трека (трек недоступен). 363 | Стандартное поведение Жуэля на это событие — «ломать» все плееры и контролы, прикреплённые к этому треку. 364 | 365 | ###### `onload` 366 | Трек загружен и готов к воспроизведению. 367 | Стандартное поведение Жуэля на это событие — запускать воспроизведение трека, если до этого он не был остановлен. 368 | 369 | ###### `onplay` 370 | Трек начал воспроизводиться. 371 | Стандартное поведение Жуэля на это событие — остановка другого играющего трека, изменение интерфейса плеера, запуск движений таймлайна. 372 | 373 | ###### `onpause` 374 | Воспроизведение трека остановлено. 375 | Стандартное поведение Жуэля на это событие — отмена движений таймлайна, изменение интерфейса плеера. 376 | 377 | ###### `onseek` 378 | Трек перемотан на другую позицию. 379 | Стандартное поведение Жуэля на это событие — перемещение таймлайна и запуск воспроизведения, если необходимо. 380 | 381 | ###### `onend` 382 | Трек воспроизвёлся до конца. 383 | Стандартное поведение Жуэля на это событие — переход к следующему треку, если трек находится в плейлисте, или просто остановка воспроизведения. 384 | 385 | 386 | ## Титры 387 | - Идея и разработка — [Илья Бирман](https://ilyabirman.ru) 388 | - Разработка — [Евгений Лазарев](https://lazarev.me) 389 | - Работа с аудио — [howler.js](https://github.com/goldfire/howler.js) 390 | 391 | ## Лицензия 392 | [MIT License](license.md) 393 | -------------------------------------------------------------------------------- /dist/jouele.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function e(){this.init()}e.prototype={init:function(){var e=this||h;return e._counter=1e3,e._codecs={},e._howls=[],e._muted=!1,e._volume=1,e._canPlayEvent="canplaythrough",e._navigator="undefined"!=typeof window&&window.navigator?window.navigator:null,e.masterGain=null,e.noAudio=!1,e.usingWebAudio=!0,e.autoSuspend=!0,e.ctx=null,e.mobileAutoEnable=!0,e._setup(),e},volume:function(e){var t=this||h;if(e=parseFloat(e),t.ctx||d(),void 0!==e&&0<=e&&e<=1){if(t._volume=e,t._muted)return t;t.usingWebAudio&&t.masterGain.gain.setValueAtTime(e,h.ctx.currentTime);for(var a=0;a=l._stop)){var d=l._node;if(r._webAudio){var p=function(){r._refreshBuffer(l);var e=l._muted||r._muted?0:l._volume;d.gain.setValueAtTime(e,h.ctx.currentTime),l._playStart=h.ctx.currentTime,void 0===d.bufferSource.start?l._loop?d.bufferSource.noteGrainOn(0,s,86400):d.bufferSource.noteGrainOn(0,s,c):l._loop?d.bufferSource.start(0,s,86400):d.bufferSource.start(0,s,c),u!=1/0&&(r._endTimers[l._id]=setTimeout(r._ended.bind(r,l),u)),a||setTimeout(function(){r._emit("play",l._id)},0)};"running"===h.state?p():(r.once("resume",p),r._clearTimer(l._id))}else{var g=function(){d.currentTime=s,d.muted=l._muted||r._muted||h._muted||d.muted,d.volume=l._volume*h.volume(),d.playbackRate=l._rate;try{var e=d.play();if(e&&"undefined"!=typeof Promise&&(e instanceof Promise||"function"==typeof e.then)?(r._playLock=!0,e.then(function(){r._playLock=!1,a||r._emit("play",l._id)}).catch(function(){r._playLock=!1,r._emit("playerror",l._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.")})):a||r._emit("play",l._id),d.playbackRate=l._rate,d.paused)return void r._emit("playerror",l._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");"__default"!==t||l._loop?r._endTimers[l._id]=setTimeout(r._ended.bind(r,l),u):(r._endTimers[l._id]=function(){r._ended(l),d.removeEventListener("ended",r._endTimers[l._id],!1)},d.addEventListener("ended",r._endTimers[l._id],!1))}catch(e){r._emit("playerror",l._id,e)}},k=window&&window.ejecta||!d.readyState&&h._navigator.isCocoonJS;if(3<=d.readyState||k)g();else{var f=function(){g(),d.removeEventListener(h._canPlayEvent,f,!1)};d.addEventListener(h._canPlayEvent,f,!1),r._clearTimer(l._id)}}return l._id}r._ended(l)},pause:function(e){var t=this;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"pause",action:function(){t.pause(e)}}),t;for(var a=t._getSoundIds(e),r=0;ro?g.formatTime(n,!0):g.formatTime(n,!e),r=g.formatTime(g.makeSeconds(o)-g.makeSeconds(l),!0);d.each(t["time-total"].add(0'),d(document.createElement("a")).attr("href",e.getHref()).addClass("jouele-info-control-link jouele-hidden").append(d(document.createElement("span")).addClass("jouele-info-control-button-icon jouele-info-control-button-icon_play jouele-control jouele-hidden").attr("data-href",e.getHref()).attr("data-type","play-pause").html(''))),d(document.createElement("div")).addClass("jouele-info-control-text jouele-control").attr("data-href",e.getHref()).attr("data-type","title"))]),r.addClass("jouele-progress").append(d(document.createElement("div")).addClass("jouele-progress-line jouele-control").attr("data-href",e.getHref()).attr("data-type","timeline").append(d(document.createElement("div")).addClass("jouele-progress-line-bar_base"),d(document.createElement("div")).addClass("jouele-progress-line-bar_play jouele-control").attr("data-href",e.getHref()).attr("data-type","elapsed"),d(document.createElement("div")).addClass("jouele-progress-line-lift jouele-hidden jouele-control").attr("data-href",e.getHref()).attr("data-type","position")))).promise().done(function(){n.initInnerControls.call(e)}),e},insertJoueleDOM:function(){var e=this;return e.$container&&(e.$container.find(".jouele-hidden").removeClass("jouele-hidden"),e.$container.find(".jouele-info-control-button-icon_unavailable").addClass("jouele-hidden"),e.$container.find(".jouele-info-control-link").off("click.jouele").on("click.jouele",function(e){e.preventDefault()}),e.$link.after(e.$container),e.$link.detach()),e},findPlaylistInDOM:function(){return void 0!==this.$control&&0t.getTotalTime()&&(e=t.getTotalTime()),g.makeSeconds(e)<0&&(e=0),t.getTrack().player.seekTime=g.makeSeconds(e),f.updateState.call(t),0