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.
10 * @extends {PolymerElement}
12 var TrackListElement = function() {};
14 TrackListElement.prototype = {
16 * Initializes an element. This method is called automatically when the
20 this.observeTrackList();
22 window.addEventListener('resize', this.onWindowResize_.bind(this));
25 observeTrackList: function() {
26 // Unobserve the previous track list.
27 if (this.unobserveTrackList_)
28 this.unobserveTrackList_();
30 // Observe the new track list.
31 var observer = this.tracksValueChanged_.bind(this);
32 Array.observe(this.tracks, observer);
34 // Set the function to unobserve it.
35 this.unobserveTrackList_ = function(tracks, observer) {
36 Array.unobserve(tracks, observer);
37 }.bind(null, this.tracks, observer);
41 * Registers handlers for changing of external variables
44 'model.shuffle': 'onShuffleChanged',
48 * Model object of the Audio Player.
49 * @type {AudioPlayerModel}
55 * @type {Array.<AudioPlayer.TrackInfo>}
60 * Play order of the tracks. Each value is the index of 'this.tracks'.
61 * @type {Array.<number>}
66 * Track index of the current track.
67 * If the tracks property is empty, it should be -1. Otherwise, be a valid
72 currentTrackIndex: -1,
75 * Invoked when 'shuffle' property is changed.
76 * @param {boolean} oldValue Old value.
77 * @param {boolean} newValue New value.
79 onShuffleChanged: function(oldValue, newValue) {
80 this.generatePlayOrder(true /* keep the current track */);
84 * Invoked when the current track index is changed.
85 * @param {number} oldValue old value.
86 * @param {number} newValue new value.
88 currentTrackIndexChanged: function(oldValue, newValue) {
89 if (oldValue === newValue)
92 if (!isNaN(oldValue) && 0 <= oldValue && oldValue < this.tracks.length)
93 this.tracks[oldValue].active = false;
95 if (0 <= newValue && newValue < this.tracks.length) {
96 var currentPlayOrder = this.playOrder.indexOf(newValue);
97 if (currentPlayOrder !== -1) {
99 this.tracks[newValue].active = true;
101 this.ensureTrackInViewport_(newValue /* trackIndex */);
107 if (this.tracks.length === 0)
108 this.currentTrackIndex = -1;
110 this.generatePlayOrder(false /* no need to keep the current track */);
114 * Invoked when 'tracks' property is changed.
115 * @param {Array.<AudioPlayer.TrackInfo>} oldValue Old value.
116 * @param {Array.<AudioPlayer.TrackInfo>} newValue New value.
118 tracksChanged: function(oldValue, newValue) {
119 // Note: Sometimes both oldValue and newValue are null though the actual
120 // values are not null. Maybe it's a bug of Polymer.
122 // Re-register the observer of 'this.tracks'.
123 this.observeTrackList();
125 if (this.tracks.length !== 0) {
126 // Restore the active track.
127 if (this.currentTrackIndex !== -1 &&
128 this.currentTrackIndex < this.tracks.length) {
129 this.tracks[this.currentTrackIndex].active = true;
132 // Reset play order and current index.
133 this.generatePlayOrder(false /* no need to keep the current track */);
136 this.currentTrackIndex = -1;
141 * Invoked when the value in the 'tracks' is changed.
142 * @param {Array.<Object>} changes The detail of the change.
144 tracksValueChanged_: function(changes) {
145 if (this.tracks.length === 0)
146 this.currentTrackIndex = -1;
148 this.tracks[this.currentTrackIndex].active = true;
152 * Invoked when the track element is clicked.
153 * @param {Event} event Click event.
155 trackClicked: function(event) {
156 var index = ~~event.currentTarget.getAttribute('index');
157 var track = this.tracks[index];
159 this.selectTrack(track);
163 * Invoked when the window is resized.
166 onWindowResize_: function() {
167 this.ensureTrackInViewport_(this.currentTrackIndex);
171 * Scrolls the track list to ensure the given track in the viewport.
172 * @param {number} trackIndex The index of the track to be in the viewport.
175 ensureTrackInViewport_: function(trackIndex) {
176 var trackSelector = '::shadow .track[index="' + trackIndex + '"]';
177 var trackElement = this.querySelector(trackSelector);
179 var viewTop = this.scrollTop;
180 var viewHeight = this.clientHeight;
181 var elementTop = trackElement.offsetTop;
182 var elementHeight = trackElement.offsetHeight;
184 if (elementTop < viewTop) {
186 this.scrollTop = elementTop;
187 } else if (elementTop + elementHeight <= viewTop + viewHeight) {
188 // The entire element is in the viewport. Do nothing.
190 // Adjust the bottoms.
191 this.scrollTop = Math.max(0,
192 (elementTop + elementHeight - viewHeight));
198 * Invoked when the track element is clicked.
199 * @param {boolean} keepCurrentTrack Keep the current track or not.
201 generatePlayOrder: function(keepCurrentTrack) {
202 console.assert((keepCurrentTrack !== undefined),
203 'The argument "forward" is undefined');
205 if (this.tracks.length === 0) {
210 // Creates sequenced array.
213 map(function(unused, index) { return index; });
215 if (this.model && this.model.shuffle) {
216 // Randomizes the play order array (Schwarzian-transform algorithm).
217 this.playOrder = this.playOrder
219 return {weight: Math.random(), index: a};
221 .sort(function(a, b) { return a.weight - b.weight })
222 .map(function(a) { return a.index });
224 if (keepCurrentTrack) {
225 // Puts the current track at the beginning of the play order.
226 this.playOrder = this.playOrder
227 .filter(function(value) {
228 return this.currentTrackIndex !== value;
230 this.playOrder.splice(0, 0, this.currentTrackIndex);
234 if (!keepCurrentTrack)
235 this.currentTrackIndex = this.playOrder[0];
239 * Sets the current track.
240 * @param {AudioPlayer.TrackInfo} track TrackInfo to be set as the current
243 selectTrack: function(track) {
245 for (var i = 0; i < this.tracks.length; i++) {
246 if (this.tracks[i].url === track.url) {
252 // TODO(yoshiki): Clean up the flow and the code around here.
253 if (this.currentTrackIndex == index)
254 this.replayCurrentTrack();
256 this.currentTrackIndex = index;
261 * Request to replay the current music.
263 replayCurrentTrack: function() {
268 * Returns the current track.
269 * @return {AudioPlayer.TrackInfo} track TrackInfo of the current track.
271 getCurrentTrack: function() {
272 if (this.tracks.length === 0)
275 return this.tracks[this.currentTrackIndex];
279 * Returns the next (or previous) track in the track list. If there is no
280 * next track, returns -1.
282 * @param {boolean} forward Specify direction: forward or previous mode.
283 * True: forward mode, false: previous mode.
284 * @param {boolean} cyclic Specify if cyclically or not: It true, the first
285 * track is succeeding to the last track, otherwise no track after the
287 * @return {number} The next track index.
289 getNextTrackIndex: function(forward, cyclic) {
290 if (this.tracks.length === 0)
293 var defaultTrackIndex =
294 forward ? this.playOrder[0] : this.playOrder[this.tracks.length - 1];
296 var currentPlayOrder = this.playOrder.indexOf(this.currentTrackIndex);
298 (0 <= currentPlayOrder && currentPlayOrder < this.tracks.length),
299 'Insufficient TrackList.playOrder. The current track is not on the ' +
302 var newPlayOrder = currentPlayOrder + (forward ? +1 : -1);
303 if (newPlayOrder === -1 || newPlayOrder === this.tracks.length)
304 return cyclic ? defaultTrackIndex : -1;
306 var newTrackIndex = this.playOrder[newPlayOrder];
308 (0 <= newTrackIndex && newTrackIndex < this.tracks.length),
309 'Insufficient TrackList.playOrder. New Play Order: ' + newPlayOrder);
311 return newTrackIndex;
313 }; // TrackListElement.prototype for 'track-list'
315 Polymer('track-list', TrackListElement.prototype);
316 })(); // Anonymous closure