1 /** The osmplayer namespace. */ 2 var osmplayer = osmplayer || {}; 3 4 /** 5 * @constructor 6 * @extends minplayer.display 7 * @class This class creates the playlist functionality for the minplayer. 8 * 9 * @param {object} context The jQuery context. 10 * @param {object} options This components options. 11 */ 12 osmplayer.playlist = function(context, options) { 13 14 // Derive from display 15 minplayer.display.call(this, 'playlist', context, options); 16 }; 17 18 /** Derive from minplayer.display. */ 19 osmplayer.playlist.prototype = new minplayer.display(); 20 21 /** Reset the constructor. */ 22 osmplayer.playlist.prototype.constructor = osmplayer.playlist; 23 24 /** 25 * @see minplayer.plugin#construct 26 */ 27 osmplayer.playlist.prototype.construct = function() { 28 29 // Make sure we provide default options... 30 this.options = jQuery.extend({ 31 vertical: true, 32 playlist: '', 33 pageLimit: 10, 34 autoNext: true, 35 shuffle: false, 36 loop: false, 37 hysteresis: 40, 38 scrollSpeed: 20, 39 scrollMode: 'auto' 40 }, this.options); 41 42 // Call the minplayer plugin constructor. 43 minplayer.display.prototype.construct.call(this); 44 45 /** The nodes within this playlist. */ 46 this.nodes = []; 47 48 // Current page. 49 this.page = -1; 50 51 // The total amount of nodes. 52 this.totalItems = 0; 53 54 // The current loaded item index. 55 this.currentItem = -1; 56 57 // The play playqueue. 58 this.playqueue = []; 59 60 // The playqueue position. 61 this.playqueuepos = 0; 62 63 // The current playlist. 64 this.playlist = this.options.playlist; 65 66 // Create the scroll bar. 67 this.scroll = null; 68 69 // Create our orientation variable. 70 this.orient = { 71 pos: this.options.vertical ? 'y' : 'x', 72 pagePos: this.options.vertical ? 'pageY' : 'pageX', 73 offset: this.options.vertical ? 'top' : 'left', 74 wrapperSize: this.options.vertical ? 'wrapperH' : 'wrapperW', 75 minScroll: this.options.vertical ? 'minScrollY' : 'minScrollX', 76 maxScroll: this.options.vertical ? 'maxScrollY' : 'maxScrollX', 77 size: this.options.vertical ? 'height' : 'width' 78 }; 79 80 // Create the pager. 81 this.pager = this.create('pager', 'osmplayer'); 82 this.pager.ubind(this.uuid + ':nextPage', (function(playlist) { 83 return function(event) { 84 playlist.nextPage(); 85 }; 86 })(this)); 87 this.pager.ubind(this.uuid + ':prevPage', (function(playlist) { 88 return function(event) { 89 playlist.prevPage(); 90 }; 91 })(this)); 92 93 // Load the "next" item. 94 if (this.next()) { 95 96 // Get the media. 97 if (this.options.autoNext) { 98 this.get('player', function(player) { 99 player.ubind(this.uuid + ':player_ended', (function(playlist) { 100 return function(event) { 101 player.options.autoplay = true; 102 playlist.next(); 103 }; 104 })(this)); 105 }); 106 } 107 } 108 109 // Say that we are ready. 110 this.ready(); 111 }; 112 113 /** 114 * Wrapper around the scroll scrollTo method. 115 * 116 * @param {number} pos The position you would like to set the list. 117 * @param {boolean} relative If this is a relative position change. 118 */ 119 osmplayer.playlist.prototype.scrollTo = function(pos, relative) { 120 if (this.scroll) { 121 this.scroll.options.hideScrollbar = false; 122 if (this.options.vertical) { 123 this.scroll.scrollTo(0, pos, 0, relative); 124 } 125 else { 126 this.scroll.scrollTo(pos, 0, 0, relative); 127 } 128 this.scroll.options.hideScrollbar = true; 129 } 130 }; 131 132 /** 133 * Refresh the scrollbar. 134 */ 135 osmplayer.playlist.prototype.refreshScroll = function() { 136 137 // Make sure that our window has the addEventListener to keep IE happy. 138 if (!window.addEventListener) { 139 setTimeout((function(playlist) { 140 return function() { 141 playlist.refreshScroll.call(playlist); 142 } 143 })(this), 200); 144 return; 145 } 146 147 // Check the size of the playlist. 148 var list = this.elements.list; 149 var scroll = this.elements.scroll; 150 151 // Destroy the scroll bar first. 152 if (this.scroll) { 153 this.scroll.scrollTo(0, 0); 154 this.scroll.destroy(); 155 this.scroll = null; 156 this.elements.list 157 .unbind('mousemove') 158 .unbind('mouseenter') 159 .unbind('mouseleave'); 160 } 161 162 // Need to force the width of the list. 163 if (!this.options.vertical) { 164 var listSize = 0; 165 jQuery.each(this.elements.list.children(), function() { 166 listSize += jQuery(this).outerWidth(); 167 }); 168 this.elements.list.width(listSize); 169 } 170 171 // Check to see if we should add a scroll bar functionality. 172 if ((list.length > 0) && 173 (scroll.length > 0) && 174 (list[this.orient.size]() > scroll[this.orient.size]())) { 175 176 // Setup the iScroll component. 177 this.scroll = new iScroll(this.elements.scroll.eq(0)[0], { 178 hScroll: !this.options.vertical, 179 hScrollbar: !this.options.vertical, 180 vScroll: this.options.vertical, 181 vScrollbar: this.options.vertical, 182 hideScrollbar: (this.options.scrollMode !== 'none') 183 }); 184 185 // Use autoScroll for non-touch devices. 186 if ((this.options.scrollMode == 'auto') && !minplayer.hasTouch) { 187 188 // Bind to the mouse events for autoscrolling. 189 this.elements.list.bind('mousemove', (function(playlist) { 190 return function(event) { 191 event.preventDefault(); 192 var offset = playlist.display.offset()[playlist.orient.offset]; 193 playlist.mousePos = event[playlist.orient.pagePos]; 194 playlist.mousePos -= offset; 195 }; 196 })(this)).bind('mouseenter', (function(playlist) { 197 return function(event) { 198 event.preventDefault(); 199 playlist.scrolling = true; 200 var setScroll = function() { 201 if (playlist.scrolling) { 202 var scrollSize = playlist.scroll[playlist.orient.wrapperSize]; 203 var scrollMid = (scrollSize / 2); 204 var delta = playlist.mousePos - scrollMid; 205 if (Math.abs(delta) > playlist.options.hysteresis) { 206 var hyst = playlist.options.hysteresis; 207 hyst *= (delta > 0) ? -1 : 0; 208 delta = (playlist.options.scrollSpeed * (delta + hyst)); 209 delta /= scrollMid; 210 var pos = playlist.scroll[playlist.orient.pos] - delta; 211 var min = playlist.scroll[playlist.orient.minScroll] || 0; 212 var max = playlist.scroll[playlist.orient.maxScroll]; 213 if (pos >= min) { 214 playlist.scrollTo(min); 215 } 216 else if (pos <= max) { 217 playlist.scrollTo(max); 218 } 219 else { 220 playlist.scrollTo(delta, true); 221 } 222 } 223 224 // Set timeout to try again. 225 setTimeout(setScroll, 30); 226 } 227 }; 228 setScroll(); 229 }; 230 })(this)).bind('mouseleave', (function(playlist) { 231 return function(event) { 232 event.preventDefault(); 233 playlist.scrolling = false; 234 }; 235 })(this)); 236 } 237 238 this.scroll.refresh(); 239 this.scroll.scrollTo(0, 0, 200); 240 } 241 }; 242 243 /** 244 * Sets the playlist. 245 * 246 * @param {object} playlist The playlist object. 247 * @param {integer} loadIndex The index of the item to load. 248 */ 249 osmplayer.playlist.prototype.set = function(playlist, loadIndex) { 250 251 // Check to make sure the playlist is an object. 252 if (typeof playlist !== 'object') { 253 this.trigger('error', 'Playlist must be an object to set'); 254 return; 255 } 256 257 // Check to make sure the playlist has correct format. 258 if (!playlist.hasOwnProperty('total_rows')) { 259 this.trigger('error', 'Unknown playlist format.'); 260 return; 261 } 262 263 // Make sure the playlist has some rows. 264 if (playlist.total_rows && playlist.nodes.length) { 265 266 // Set the total rows. 267 this.totalItems = playlist.total_rows; 268 this.currentItem = 0; 269 270 // Show or hide the next page if there is or is not a next page. 271 if (((this.page + 1) * this.options.pageLimit) >= this.totalItems) { 272 this.pager.nextPage.hide(); 273 } 274 else { 275 this.pager.nextPage.show(); 276 } 277 278 var teaser = null; 279 var numNodes = playlist.nodes.length; 280 this.elements.list.empty(); 281 this.nodes = []; 282 283 // Iterate through all the nodes. 284 for (var index = 0; index < numNodes; index++) { 285 286 // Create the teaser object. 287 teaser = this.create('teaser', 'osmplayer', this.elements.list); 288 teaser.setNode(playlist.nodes[index]); 289 teaser.ubind(this.uuid + ':nodeLoad', (function(playlist, index) { 290 return function(event, data) { 291 playlist.loadItem(index); 292 }; 293 })(this, index)); 294 295 // Add this to our nodes array. 296 this.nodes.push(teaser); 297 298 // If the index is equal to the loadIndex. 299 if (loadIndex === index) { 300 this.loadItem(index); 301 } 302 } 303 304 // Refresh the sizes. 305 this.refreshScroll(); 306 307 // Trigger that the playlist has loaded. 308 this.trigger('playlistLoad', playlist); 309 } 310 311 // Show that we are no longer busy. 312 if (this.elements.playlist_busy) { 313 this.elements.playlist_busy.hide(); 314 } 315 }; 316 317 /** 318 * Stores the current playlist state in the playqueue. 319 */ 320 osmplayer.playlist.prototype.setQueue = function() { 321 322 // Add this item to the playqueue. 323 this.playqueue.push({ 324 page: this.page, 325 item: this.currentItem 326 }); 327 328 // Store the current playqueue position. 329 this.playqueuepos = this.playqueue.length; 330 }; 331 332 /** 333 * Loads the next item. 334 * 335 * @return {boolean} TRUE if loaded, FALSE if not. 336 */ 337 osmplayer.playlist.prototype.next = function() { 338 var item = 0, page = this.page; 339 340 // See if we are at the front of the playqueue. 341 if (this.playqueuepos >= this.playqueue.length) { 342 343 // If this is shuffle, then load a random item. 344 if (this.options.shuffle) { 345 item = Math.floor(Math.random() * this.totalItems); 346 page = Math.floor(item / this.options.pageLimit); 347 item = item % this.options.pageLimit; 348 return this.load(page, item); 349 } 350 else { 351 352 // Otherwise, increment the current item by one. 353 item = (this.currentItem + 1); 354 if (item >= this.nodes.length) { 355 return this.load(page + 1, 0); 356 } 357 else { 358 return this.loadItem(item); 359 } 360 } 361 } 362 else { 363 364 // Load the next item in the playqueue. 365 this.playqueuepos = this.playqueuepos + 1; 366 var currentQueue = this.playqueue[this.playqueuepos]; 367 return this.load(currentQueue.page, currentQueue.item); 368 } 369 }; 370 371 /** 372 * Loads the previous item. 373 * 374 * @return {boolean} TRUE if loaded, FALSE if not. 375 */ 376 osmplayer.playlist.prototype.prev = function() { 377 378 // Move back into the playqueue. 379 this.playqueuepos = this.playqueuepos - 1; 380 this.playqueuepos = (this.playqueuepos < 0) ? 0 : this.playqueuepos; 381 var currentQueue = this.playqueue[this.playqueuepos]; 382 if (currentQueue) { 383 return this.load(currentQueue.page, currentQueue.item); 384 } 385 return false; 386 }; 387 388 /** 389 * Loads a playlist node. 390 * 391 * @param {number} index The index of the item you would like to load. 392 * @return {boolean} TRUE if loaded, FALSE if not. 393 */ 394 osmplayer.playlist.prototype.loadItem = function(index) { 395 if (index < this.nodes.length) { 396 this.setQueue(); 397 398 // Get the teaser at the current index and deselect it. 399 var teaser = this.nodes[this.currentItem]; 400 teaser.select(false); 401 this.currentItem = index; 402 403 // Get the new teaser and select it. 404 teaser = this.nodes[index]; 405 teaser.select(true); 406 this.trigger('nodeLoad', teaser.node); 407 return true; 408 } 409 410 return false; 411 }; 412 413 /** 414 * Loads the next page. 415 * 416 * @param {integer} loadIndex The index of the item to load. 417 * @return {boolean} TRUE if loaded, FALSE if not. 418 */ 419 osmplayer.playlist.prototype.nextPage = function(loadIndex) { 420 return this.load(this.page + 1, loadIndex); 421 }; 422 423 /** 424 * Loads the previous page. 425 * 426 * @param {integer} loadIndex The index of the item to load. 427 * @return {boolean} TRUE if loaded, FALSE if not. 428 */ 429 osmplayer.playlist.prototype.prevPage = function(loadIndex) { 430 return this.load(this.page - 1, loadIndex); 431 }; 432 433 /** 434 * Loads a playlist. 435 * 436 * @param {integer} page The page to load. 437 * @param {integer} loadIndex The index of the item to load. 438 * @return {boolean} TRUE if loaded, FALSE if not. 439 */ 440 osmplayer.playlist.prototype.load = function(page, loadIndex) { 441 442 // If the playlist and pages are the same, then no need to load. 443 if ((this.playlist == this.options.playlist) && (page == this.page)) { 444 return this.loadItem(loadIndex); 445 } 446 447 // Set the new playlist. 448 this.playlist = this.options.playlist; 449 450 // Return if there aren't any playlists to play. 451 if (!this.playlist) { 452 return false; 453 } 454 455 // Determine if we need to loop. 456 var maxPages = Math.floor(this.totalItems / this.options.pageLimit); 457 if (page > maxPages) { 458 if (this.options.loop) { 459 page = 0; 460 loadIndex = 0; 461 } 462 else { 463 return false; 464 } 465 } 466 467 // Say that we are busy. 468 if (this.elements.playlist_busy) { 469 this.elements.playlist_busy.show(); 470 } 471 472 // Normalize the page. 473 page = page || 0; 474 page = (page < 0) ? 0 : page; 475 476 // Set the queue. 477 this.setQueue(); 478 479 // Set the new page. 480 this.page = page; 481 482 // Hide or show the page based on if we are on the first page. 483 if (this.page == 0) { 484 this.pager.prevPage.hide(); 485 } 486 else { 487 this.pager.prevPage.show(); 488 } 489 490 // If the playlist is an object, then go ahead and set it. 491 if (typeof this.playlist == 'object') { 492 this.set(this.playlist, loadIndex); 493 if (this.playlist.endpoint) { 494 this.playlist = this.options.playlist = this.playlist.endpoint; 495 } 496 return true; 497 } 498 499 // Get the highest priority parser. 500 var parser = osmplayer.parser['default']; 501 for (var name in osmplayer.parser) { 502 if (osmplayer.parser.hasOwnProperty(name)) { 503 if (osmplayer.parser[name].valid(this.playlist)) { 504 if (osmplayer.parser[name].priority > parser.priority) { 505 parser = osmplayer.parser[name]; 506 } 507 } 508 } 509 } 510 511 // The start index. 512 var start = this.page * this.options.pageLimit; 513 514 // Get the feed from the parser. 515 var feed = parser.getFeed( 516 this.playlist, 517 start, 518 this.options.pageLimit 519 ); 520 521 // Build our request. 522 var request = { 523 type: 'GET', 524 url: feed, 525 success: (function(playlist) { 526 return function(data) { 527 playlist.set(parser.parse(data), loadIndex); 528 }; 529 })(this), 530 error: (function(playlist) { 531 return function(XMLHttpRequest, textStatus, errorThrown) { 532 if (playlist.elements.playlist_busy) { 533 playlist.elements.playlist_busy.hide(); 534 } 535 playlist.trigger('error', textStatus); 536 } 537 })(this) 538 }; 539 540 // Set the data if applicable. 541 var dataType = ''; 542 if (dataType = parser.getType()) { 543 request.dataType = dataType; 544 } 545 546 // Perform an ajax callback. 547 jQuery.ajax(request); 548 549 // Return that we did something. 550 return true; 551 }; 552