1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5(function() { 6 'use strict'; 7 8 Polymer('track-list', { 9 /** 10 * Initializes an element. This method is called automatically when the 11 * element is ready. 12 */ 13 ready: function() { 14 this.tracksObserver_ = new ArrayObserver( 15 this.tracks, 16 this.tracksValueChanged_.bind(this)); 17 18 window.addEventListener('resize', this.onWindowResize_.bind(this)); 19 }, 20 21 /** 22 * Registers handlers for changing of external variables 23 */ 24 observe: { 25 'model.shuffle': 'onShuffleChanged', 26 }, 27 28 /** 29 * Model object of the Audio Player. 30 * @type {AudioPlayerModel} 31 */ 32 model: null, 33 34 /** 35 * List of tracks. 36 * @type {Array.<AudioPlayer.TrackInfo>} 37 */ 38 tracks: [], 39 40 /** 41 * Play order of the tracks. Each value is the index of 'this.tracks'. 42 * @type {Array.<number>} 43 */ 44 playOrder: [], 45 46 /** 47 * Track index of the current track. 48 * If the tracks property is empty, it should be -1. Otherwise, be a valid 49 * track number. 50 * 51 * @type {number} 52 */ 53 currentTrackIndex: -1, 54 55 /** 56 * Invoked when 'shuffle' property is changed. 57 * @param {boolean} oldValue Old value. 58 * @param {boolean} newValue New value. 59 */ 60 onShuffleChanged: function(oldValue, newValue) { 61 this.generatePlayOrder(true /* keep the current track */); 62 }, 63 64 /** 65 * Invoked when the current track index is changed. 66 * @param {number} oldValue old value. 67 * @param {number} newValue new value. 68 */ 69 currentTrackIndexChanged: function(oldValue, newValue) { 70 if (oldValue === newValue) 71 return; 72 73 if (!isNaN(oldValue) && 0 <= oldValue && oldValue < this.tracks.length) 74 this.tracks[oldValue].active = false; 75 76 if (0 <= newValue && newValue < this.tracks.length) { 77 var currentPlayOrder = this.playOrder.indexOf(newValue); 78 if (currentPlayOrder !== -1) { 79 // Success 80 this.tracks[newValue].active = true; 81 82 this.ensureTrackInViewport_(newValue /* trackIndex */); 83 return; 84 } 85 } 86 87 // Invalid index 88 if (this.tracks.length === 0) 89 this.currentTrackIndex = -1; 90 else 91 this.generatePlayOrder(false /* no need to keep the current track */); 92 }, 93 94 /** 95 * Invoked when 'tracks' property is changed. 96 * @param {Array.<TrackInfo>} oldValue Old value. 97 * @param {Array.<TrackInfo>} newValue New value. 98 */ 99 tracksChanged: function(oldValue, newValue) { 100 // Note: Sometimes both oldValue and newValue are null though the actual 101 // values are not null. Maybe it's a bug of Polymer. 102 103 // Re-register the observer of 'this.tracks'. 104 this.tracksObserver_.close(); 105 this.tracksObserver_ = new ArrayObserver(this.tracks); 106 this.tracksObserver_.open(this.tracksValueChanged_.bind(this)); 107 108 if (this.tracks.length !== 0) { 109 // Restore the active track. 110 if (this.currentTrackIndex !== -1 && 111 this.currentTrackIndex < this.tracks.length) { 112 this.tracks[this.currentTrackIndex].active = true; 113 } 114 115 // Reset play order and current index. 116 this.generatePlayOrder(false /* no need to keep the current track */); 117 } else { 118 this.playOrder = []; 119 this.currentTrackIndex = -1; 120 } 121 }, 122 123 /** 124 * Invoked when the value in the 'tracks' is changed. 125 * @param {Array.<Object>} splices The detail of the change. 126 */ 127 tracksValueChanged_: function(splices) { 128 if (this.tracks.length === 0) 129 this.currentTrackIndex = -1; 130 else 131 this.tracks[this.currentTrackIndex].active = true; 132 }, 133 134 /** 135 * Invoked when the track element is clicked. 136 * @param {Event} event Click event. 137 */ 138 trackClicked: function(event) { 139 var index = ~~event.currentTarget.getAttribute('index'); 140 var track = this.tracks[index]; 141 if (track) 142 this.selectTrack(track); 143 }, 144 145 /** 146 * Invoked when the window is resized. 147 * @private 148 */ 149 onWindowResize_: function() { 150 this.ensureTrackInViewport_(this.currentTrackIndex); 151 }, 152 153 /** 154 * Scrolls the track list to ensure the given track in the viewport. 155 * @param {number} trackIndex The index of the track to be in the viewport. 156 * @private 157 */ 158 ensureTrackInViewport_: function(trackIndex) { 159 var trackSelector = '.track[index="' + trackIndex + '"]'; 160 var trackElement = this.querySelector(trackSelector); 161 if (trackElement) { 162 var viewTop = this.scrollTop; 163 var viewHeight = this.clientHeight; 164 var elementTop = trackElement.offsetTop; 165 var elementHeight = trackElement.offsetHeight; 166 167 if (elementTop < viewTop) { 168 // Adjust the tops. 169 this.scrollTop = elementTop; 170 } else if (elementTop + elementHeight <= viewTop + viewHeight) { 171 // The entire element is in the viewport. Do nothing. 172 } else { 173 // Adjust the bottoms. 174 this.scrollTop = Math.max(0, 175 (elementTop + elementHeight - viewHeight)); 176 } 177 } 178 }, 179 180 /** 181 * Invoked when the track element is clicked. 182 * @param {boolean} keepCurrentTrack Keep the current track or not. 183 */ 184 generatePlayOrder: function(keepCurrentTrack) { 185 console.assert((keepCurrentTrack !== undefined), 186 'The argument "forward" is undefined'); 187 188 if (this.tracks.length === 0) { 189 this.playOrder = []; 190 return; 191 } 192 193 // Creates sequenced array. 194 this.playOrder = 195 this.tracks. 196 map(function(unused, index) { return index; }); 197 198 if (this.model && this.model.shuffle) { 199 // Randomizes the play order array (Schwarzian-transform algorithm). 200 this.playOrder = 201 this.playOrder. 202 map(function(a) { 203 return {weight: Math.random(), index: a}; 204 }). 205 sort(function(a, b) { return a.weight - b.weight }). 206 map(function(a) { return a.index }); 207 208 if (keepCurrentTrack) { 209 // Puts the current track at the beginning of the play order. 210 this.playOrder = 211 this.playOrder.filter(function(value) { 212 return this.currentTrackIndex !== value; 213 }, this); 214 this.playOrder.splice(0, 0, this.currentTrackIndex); 215 } 216 } 217 218 if (!keepCurrentTrack) 219 this.currentTrackIndex = this.playOrder[0]; 220 }, 221 222 /** 223 * Sets the current track. 224 * @param {AudioPlayer.TrackInfo} track TrackInfo to be set as the current 225 * track. 226 */ 227 selectTrack: function(track) { 228 var index = -1; 229 for (var i = 0; i < this.tracks.length; i++) { 230 if (this.tracks[i].url === track.url) { 231 index = i; 232 break; 233 } 234 } 235 if (index >= 0) { 236 // TODO(yoshiki): Clean up the flow and the code around here. 237 if (this.currentTrackIndex == index) 238 this.replayCurrentTrack(); 239 else 240 this.currentTrackIndex = index; 241 } 242 }, 243 244 /** 245 * Request to replay the current music. 246 */ 247 replayCurrentTrack: function() { 248 this.fire('replay'); 249 }, 250 251 /** 252 * Returns the current track. 253 * @param {AudioPlayer.TrackInfo} track TrackInfo of the current track. 254 */ 255 getCurrentTrack: function() { 256 if (this.tracks.length === 0) 257 return null; 258 259 return this.tracks[this.currentTrackIndex]; 260 }, 261 262 /** 263 * Returns the next (or previous) track in the track list. If there is no 264 * next track, returns -1. 265 * 266 * @param {boolean} forward Specify direction: forward or previous mode. 267 * True: forward mode, false: previous mode. 268 * @param {boolean} cyclic Specify if cyclically or not: It true, the first 269 * track is succeeding to the last track, otherwise no track after the 270 * last. 271 * @return {number} The next track index. 272 */ 273 getNextTrackIndex: function(forward, cyclic) { 274 if (this.tracks.length === 0) 275 return -1; 276 277 var defaultTrackIndex = 278 forward ? this.playOrder[0] : this.playOrder[this.tracks.length - 1]; 279 280 var currentPlayOrder = this.playOrder.indexOf(this.currentTrackIndex); 281 console.assert( 282 (0 <= currentPlayOrder && currentPlayOrder < this.tracks.length), 283 'Insufficient TrackList.playOrder. The current track is not on the ' + 284 'track list.'); 285 286 var newPlayOrder = currentPlayOrder + (forward ? +1 : -1); 287 if (newPlayOrder === -1 || newPlayOrder === this.tracks.length) 288 return cyclic ? defaultTrackIndex : -1; 289 290 var newTrackIndex = this.playOrder[newPlayOrder]; 291 console.assert( 292 (0 <= newTrackIndex && newTrackIndex < this.tracks.length), 293 'Insufficient TrackList.playOrder. New Play Order: ' + newPlayOrder); 294 295 return newTrackIndex; 296 }, 297 }); // Polymer('track-list') block 298})(); // Anonymous closure 299