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.
29 observer: 'tracksChanged'
33 * Track index of the current track.
34 * If the tracks property is empty, it should be -1. Otherwise, be a valid
40 observer: 'currentTrackIndexChanged',
45 * Whether shuffling play order is enabled or not.
50 observer: 'shuffleChanged'
55 * Initializes an element. This method is called automatically when the
59 this.observeTrackList();
61 window.addEventListener('resize', this.onWindowResize_.bind(this));
64 observeTrackList: function() {
65 // Unobserve the previous track list.
66 if (this.unobserveTrackList_)
67 this.unobserveTrackList_();
69 // Observe the new track list.
70 var observer = this.tracksValueChanged_.bind(this);
71 Array.observe(this.tracks, observer);
73 // Set the function to unobserve it.
74 this.unobserveTrackList_ = function(tracks, observer) {
75 Array.unobserve(tracks, observer);
76 }.bind(null, this.tracks, observer);
80 * Play order of the tracks. Each value is the index of 'this.tracks'.
81 * @type {Array<number>}
86 * Invoked when 'shuffle' property is changed.
87 * @param {boolean} newValue New value.
88 * @param {boolean} oldValue Old value.
90 shuffleChanged: function(newValue, oldValue) {
91 this.generatePlayOrder(true /* keep the current track */);
95 * Invoked when the current track index is changed.
96 * @param {number} newValue new value.
97 * @param {number} oldValue old value.
99 currentTrackIndexChanged: function(newValue, oldValue) {
100 if (oldValue === newValue)
103 if (!isNaN(oldValue) && 0 <= oldValue && oldValue < this.tracks.length)
104 this.set('tracks.' + oldValue + '.active', false);
106 if (0 <= newValue && newValue < this.tracks.length) {
107 var currentPlayOrder = this.playOrder.indexOf(newValue);
108 if (currentPlayOrder !== -1) {
110 this.set('tracks.' + newValue + '.active', true);
112 this.ensureTrackInViewport_(newValue /* trackIndex */);
118 if (this.tracks.length === 0)
119 this.currentTrackIndex = -1;
121 this.generatePlayOrder(false /* no need to keep the current track */);
125 * Invoked when 'tracks' property is changed.
126 * @param {Array<!TrackInfo>} newValue New value.
127 * @param {Array<!TrackInfo>} oldValue Old value.
129 tracksChanged: function(newValue, oldValue) {
130 // Note: Sometimes both oldValue and newValue are null though the actual
131 // values are not null. Maybe it's a bug of Polymer.
133 // Re-register the observer of 'this.tracks'.
134 this.observeTrackList();
136 if (this.tracks.length !== 0) {
137 // Restore the active track.
138 if (this.currentTrackIndex !== -1 &&
139 this.currentTrackIndex < this.tracks.length) {
140 this.set('tracks.' + this.currentTrackIndex + '.active', true);
143 // Reset play order and current index.
144 this.generatePlayOrder(false /* no need to keep the current track */);
147 this.currentTrackIndex = -1;
152 * Invoked when the value in the 'tracks' is changed.
153 * @param {Array<Object>} changes The detail of the change.
155 tracksValueChanged_: function(changes) {
156 if (this.tracks.length === 0)
157 this.currentTrackIndex = -1;
159 this.set('tracks.' + this.currentTrackIndex + '.active', true);
163 * Invoked when the track element is clicked.
164 * @param {Event} event Click event.
166 trackClicked: function(event) {
167 var index = ~~event.currentTarget.getAttribute('index');
168 var track = this.tracks[index];
170 this.selectTrack(track);
174 * Invoked when the window is resized.
177 onWindowResize_: function() {
178 this.ensureTrackInViewport_(this.currentTrackIndex);
182 * Scrolls the track list to ensure the given track in the viewport.
183 * @param {number} trackIndex The index of the track to be in the viewport.
186 ensureTrackInViewport_: function(trackIndex) {
187 var trackSelector = '::shadow .track[index="' + trackIndex + '"]';
188 var trackElement = this.querySelector(trackSelector);
190 var viewTop = this.scrollTop;
191 var viewHeight = this.clientHeight;
192 var elementTop = trackElement.offsetTop;
193 var elementHeight = trackElement.offsetHeight;
195 if (elementTop < viewTop) {
197 this.scrollTop = elementTop;
198 } else if (elementTop + elementHeight <= viewTop + viewHeight) {
199 // The entire element is in the viewport. Do nothing.
201 // Adjust the bottoms.
202 this.scrollTop = Math.max(0,
203 (elementTop + elementHeight - viewHeight));
209 * Invoked when the track element is clicked.
210 * @param {boolean} keepCurrentTrack Keep the current track or not.
212 generatePlayOrder: function(keepCurrentTrack) {
213 console.assert((keepCurrentTrack !== undefined),
214 'The argument "forward" is undefined');
216 if (this.tracks.length === 0) {
221 // Creates sequenced array.
224 map(function(unused, index) { return index; });
227 // Randomizes the play order array (Schwarzian-transform algorithm).
228 this.playOrder = this.playOrder
230 return {weight: Math.random(), index: a};
232 .sort(function(a, b) { return a.weight - b.weight })
233 .map(function(a) { return a.index });
235 if (keepCurrentTrack) {
236 // Puts the current track at the beginning of the play order.
237 this.playOrder = this.playOrder
238 .filter(function(value) {
239 return this.currentTrackIndex !== value;
241 this.playOrder.splice(0, 0, this.currentTrackIndex);
245 if (!keepCurrentTrack)
246 this.currentTrackIndex = this.playOrder[0];
250 * Sets the current track.
251 * @param {!TrackInfo} track TrackInfo to be set as the current
254 selectTrack: function(track) {
256 for (var i = 0; i < this.tracks.length; i++) {
257 if (this.tracks[i].url === track.url) {
263 // TODO(yoshiki): Clean up the flow and the code around here.
264 if (this.currentTrackIndex == index)
265 this.replayCurrentTrack();
267 this.currentTrackIndex = index;
272 * Request to replay the current music.
274 replayCurrentTrack: function() {
279 * Returns the current track.
280 * @return {TrackInfo} track TrackInfo of the current track.
282 getCurrentTrack: function() {
283 if (this.tracks.length === 0)
286 return this.tracks[this.currentTrackIndex];
290 * Returns the next (or previous) track in the track list. If there is no
291 * next track, returns -1.
293 * @param {boolean} forward Specify direction: forward or previous mode.
294 * True: forward mode, false: previous mode.
295 * @param {boolean} cyclic Specify if cyclically or not: It true, the first
296 * track is succeeding to the last track, otherwise no track after the
298 * @return {number} The next track index.
300 getNextTrackIndex: function(forward, cyclic) {
301 if (this.tracks.length === 0)
304 var defaultTrackIndex =
305 forward ? this.playOrder[0] : this.playOrder[this.tracks.length - 1];
307 var currentPlayOrder = this.playOrder.indexOf(this.currentTrackIndex);
309 (0 <= currentPlayOrder && currentPlayOrder < this.tracks.length),
310 'Insufficient TrackList.playOrder. The current track is not on the ' +
313 var newPlayOrder = currentPlayOrder + (forward ? +1 : -1);
314 if (newPlayOrder === -1 || newPlayOrder === this.tracks.length)
315 return cyclic ? defaultTrackIndex : -1;
317 var newTrackIndex = this.playOrder[newPlayOrder];
319 (0 <= newTrackIndex && newTrackIndex < this.tracks.length),
320 'Insufficient TrackList.playOrder. New Play Order: ' + newPlayOrder);
322 return newTrackIndex;
325 })(); // Anonymous closure