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.bind('nextPage', (function(playlist) { 83 return function(event) { 84 playlist.nextPage(); 85 }; 86 })(this)); 87 this.pager.bind('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('media', function(media) { 99 media.bind('ended', (function(playlist) { 100 return function(event) { 101 media.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 // Check to see if we should add a scroll bar functionality. 152 if ((!this.scroll) && 153 (list.length > 0) && 154 (scroll.length > 0) && 155 (list[this.orient.size]() > scroll[this.orient.size]())) { 156 157 // Setup the iScroll component. 158 this.scroll = new iScroll(this.elements.scroll.eq(0)[0], { 159 hScroll: !this.options.vertical, 160 hScrollbar: !this.options.vertical, 161 vScroll: this.options.vertical, 162 vScrollbar: this.options.vertical, 163 hideScrollbar: true 164 }); 165 166 // Use autoScroll for non-touch devices. 167 if ((this.options.scrollMode == 'auto') && !minplayer.hasTouch) { 168 169 // Bind to the mouse events for autoscrolling. 170 this.elements.list.bind('mousemove', (function(playlist) { 171 return function(event) { 172 event.preventDefault(); 173 var offset = playlist.display.offset()[playlist.orient.offset]; 174 playlist.mousePos = event[playlist.orient.pagePos]; 175 playlist.mousePos -= offset; 176 }; 177 })(this)).bind('mouseenter', (function(playlist) { 178 return function(event) { 179 event.preventDefault(); 180 playlist.scrolling = true; 181 var setScroll = function() { 182 if (playlist.scrolling) { 183 var scrollSize = playlist.scroll[playlist.orient.wrapperSize]; 184 var scrollMid = (scrollSize / 2); 185 var delta = playlist.mousePos - scrollMid; 186 if (Math.abs(delta) > playlist.options.hysteresis) { 187 var hyst = playlist.options.hysteresis; 188 hyst *= (delta > 0) ? -1 : 0; 189 delta = (playlist.options.scrollSpeed * (delta + hyst)); 190 delta /= scrollMid; 191 var pos = playlist.scroll[playlist.orient.pos] - delta; 192 var min = playlist.scroll[playlist.orient.minScroll] || 0; 193 var max = playlist.scroll[playlist.orient.maxScroll]; 194 if (pos >= min) { 195 playlist.scrollTo(min); 196 } 197 else if (pos <= max) { 198 playlist.scrollTo(max); 199 } 200 else { 201 playlist.scrollTo(delta, true); 202 } 203 } 204 205 // Set timeout to try again. 206 setTimeout(setScroll, 30); 207 } 208 }; 209 setScroll(); 210 }; 211 })(this)).bind('mouseleave', (function(playlist) { 212 return function(event) { 213 event.preventDefault(); 214 playlist.scrolling = false; 215 }; 216 })(this)); 217 } 218 219 // Need to force the width of the list. 220 if (!this.options.vertical) { 221 var listSize = 0; 222 jQuery.each(this.elements.list.children(), function() { 223 listSize += jQuery(this).outerWidth(); 224 }); 225 this.elements.list.width(listSize); 226 } 227 228 this.scroll.refresh(); 229 this.scroll.scrollTo(0, 0, 200); 230 } 231 else if (this.scroll) { 232 233 // Disable the scroll bar. 234 this.scroll.disable(); 235 this.elements.list 236 .unbind('mousemove') 237 .unbind('mouseenter') 238 .unbind('mouseleave'); 239 } 240 }; 241 242 /** 243 * Sets the playlist. 244 * 245 * @param {object} playlist The playlist object. 246 * @param {integer} loadIndex The index of the item to load. 247 */ 248 osmplayer.playlist.prototype.set = function(playlist, loadIndex) { 249 250 // Check to make sure the playlist is an object. 251 if (typeof playlist !== 'object') { 252 this.trigger('error', 'Playlist must be an object to set'); 253 return; 254 } 255 256 // Check to make sure the playlist has correct format. 257 if (!playlist.hasOwnProperty('total_rows')) { 258 this.trigger('error', 'Unknown playlist format.'); 259 return; 260 } 261 262 // Make sure the playlist has some rows. 263 if (playlist.total_rows && playlist.nodes.length) { 264 265 // Set the total rows. 266 this.totalItems = playlist.total_rows; 267 this.currentItem = 0; 268 269 // Show or hide the next page if there is or is not a next page. 270 if (((this.page + 1) * this.options.pageLimit) >= this.totalItems) { 271 this.pager.nextPage.hide(); 272 } 273 else { 274 this.pager.nextPage.show(); 275 } 276 277 var teaser = null; 278 var numNodes = playlist.nodes.length; 279 this.elements.list.empty(); 280 this.nodes = []; 281 282 // Iterate through all the nodes. 283 for (var index = 0; index < numNodes; index++) { 284 285 // Create the teaser object. 286 teaser = this.create('teaser', 'osmplayer', this.elements.list); 287 teaser.setNode(playlist.nodes[index]); 288 teaser.bind('nodeLoad', (function(playlist, index) { 289 return function(event, data) { 290 playlist.loadItem(index); 291 }; 292 })(this, index)); 293 294 // Add this to our nodes array. 295 this.nodes.push(teaser); 296 297 // If the index is equal to the loadIndex. 298 if (loadIndex === index) { 299 this.loadItem(index); 300 } 301 } 302 303 // Refresh the sizes. 304 this.refreshScroll(); 305 306 // Trigger that the playlist has loaded. 307 this.trigger('playlistLoad', playlist); 308 } 309 310 // Show that we are no longer busy. 311 if (this.elements.playlist_busy) { 312 this.elements.playlist_busy.hide(); 313 } 314 }; 315 316 /** 317 * Stores the current playlist state in the playqueue. 318 */ 319 osmplayer.playlist.prototype.setQueue = function() { 320 321 // Add this item to the playqueue. 322 this.playqueue.push({ 323 page: this.page, 324 item: this.currentItem 325 }); 326 327 // Store the current playqueue position. 328 this.playqueuepos = this.playqueue.length; 329 }; 330 331 /** 332 * Loads the next item. 333 * 334 * @return {boolean} TRUE if loaded, FALSE if not. 335 */ 336 osmplayer.playlist.prototype.next = function() { 337 var item = 0, page = this.page; 338 339 // See if we are at the front of the playqueue. 340 if (this.playqueuepos >= this.playqueue.length) { 341 342 // If this is shuffle, then load a random item. 343 if (this.options.shuffle) { 344 item = Math.floor(Math.random() * this.totalItems); 345 page = Math.floor(item / this.options.pageLimit); 346 item = item % this.options.pageLimit; 347 return this.load(page, item); 348 } 349 else { 350 351 // Otherwise, increment the current item by one. 352 item = (this.currentItem + 1); 353 if (item >= this.nodes.length) { 354 return this.load(page + 1, 0); 355 } 356 else { 357 return this.loadItem(item); 358 } 359 } 360 } 361 else { 362 363 // Load the next item in the playqueue. 364 this.playqueuepos = this.playqueuepos + 1; 365 var currentQueue = this.playqueue[this.playqueuepos]; 366 return this.load(currentQueue.page, currentQueue.item); 367 } 368 }; 369 370 /** 371 * Loads the previous item. 372 * 373 * @return {boolean} TRUE if loaded, FALSE if not. 374 */ 375 osmplayer.playlist.prototype.prev = function() { 376 377 // Move back into the playqueue. 378 this.playqueuepos = this.playqueuepos - 1; 379 this.playqueuepos = (this.playqueuepos < 0) ? 0 : this.playqueuepos; 380 var currentQueue = this.playqueue[this.playqueuepos]; 381 if (currentQueue) { 382 return this.load(currentQueue.page, currentQueue.item); 383 } 384 return false; 385 }; 386 387 /** 388 * Loads a playlist node. 389 * 390 * @param {number} index The index of the item you would like to load. 391 * @return {boolean} TRUE if loaded, FALSE if not. 392 */ 393 osmplayer.playlist.prototype.loadItem = function(index) { 394 if (index < this.nodes.length) { 395 this.setQueue(); 396 397 // Get the teaser at the current index and deselect it. 398 var teaser = this.nodes[this.currentItem]; 399 teaser.select(false); 400 this.currentItem = index; 401 402 // Get the new teaser and select it. 403 teaser = this.nodes[index]; 404 teaser.select(true); 405 this.trigger('nodeLoad', teaser.node); 406 return true; 407 } 408 409 return false; 410 }; 411 412 /** 413 * Loads the next page. 414 * 415 * @param {integer} loadIndex The index of the item to load. 416 * @return {boolean} TRUE if loaded, FALSE if not. 417 */ 418 osmplayer.playlist.prototype.nextPage = function(loadIndex) { 419 return this.load(this.page + 1, loadIndex); 420 }; 421 422 /** 423 * Loads the previous page. 424 * 425 * @param {integer} loadIndex The index of the item to load. 426 * @return {boolean} TRUE if loaded, FALSE if not. 427 */ 428 osmplayer.playlist.prototype.prevPage = function(loadIndex) { 429 return this.load(this.page - 1, loadIndex); 430 }; 431 432 /** 433 * Loads a playlist. 434 * 435 * @param {integer} page The page to load. 436 * @param {integer} loadIndex The index of the item to load. 437 * @return {boolean} TRUE if loaded, FALSE if not. 438 */ 439 osmplayer.playlist.prototype.load = function(page, loadIndex) { 440 441 // If the playlist and pages are the same, then no need to load. 442 if ((this.playlist == this.options.playlist) && (page == this.page)) { 443 return this.loadItem(loadIndex); 444 } 445 446 // Set the new playlist. 447 this.playlist = this.options.playlist; 448 449 // Return if there aren't any playlists to play. 450 if (!this.playlist) { 451 return false; 452 } 453 454 // Determine if we need to loop. 455 var maxPages = Math.floor(this.totalItems / this.options.pageLimit); 456 if (page > maxPages) { 457 if (this.options.loop) { 458 page = 0; 459 loadIndex = 0; 460 } 461 else { 462 return false; 463 } 464 } 465 466 // Say that we are busy. 467 if (this.elements.playlist_busy) { 468 this.elements.playlist_busy.show(); 469 } 470 471 // Normalize the page. 472 page = page || 0; 473 page = (page < 0) ? 0 : page; 474 475 // Set the queue. 476 this.setQueue(); 477 478 // Set the new page. 479 this.page = page; 480 481 // Hide or show the page based on if we are on the first page. 482 if (this.page == 0) { 483 this.pager.prevPage.hide(); 484 } 485 else { 486 this.pager.prevPage.show(); 487 } 488 489 // If the playlist is an object, then go ahead and set it. 490 if (typeof this.playlist == 'object') { 491 this.set(this.playlist, loadIndex); 492 if (this.playlist.endpoint) { 493 this.playlist = this.options.playlist = this.playlist.endpoint; 494 } 495 return true; 496 } 497 498 // Get the highest priority parser. 499 var parser = osmplayer.parser['default']; 500 for (var name in osmplayer.parser) { 501 if (osmplayer.parser.hasOwnProperty(name)) { 502 if (osmplayer.parser[name].valid(this.playlist)) { 503 if (osmplayer.parser[name].priority > parser.priority) { 504 parser = osmplayer.parser[name]; 505 } 506 } 507 } 508 } 509 510 // The start index. 511 var start = this.page * this.options.pageLimit; 512 513 // Get the feed from the parser. 514 var feed = parser.getFeed( 515 this.playlist, 516 start, 517 this.options.pageLimit 518 ); 519 520 // Build our request. 521 var request = { 522 type: 'GET', 523 url: feed, 524 success: (function(playlist) { 525 return function(data) { 526 playlist.set(parser.parse(data), loadIndex); 527 }; 528 })(this), 529 error: (function(playlist) { 530 return function(XMLHttpRequest, textStatus, errorThrown) { 531 if (playlist.elements.playlist_busy) { 532 playlist.elements.playlist_busy.hide(); 533 } 534 playlist.trigger('error', textStatus); 535 } 536 })(this) 537 }; 538 539 // Set the data if applicable. 540 var dataType = ''; 541 if (dataType = parser.getType()) { 542 request.dataType = dataType; 543 } 544 545 // Perform an ajax callback. 546 jQuery.ajax(request); 547 548 // Return that we did something. 549 return true; 550 }; 551