Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / file_manager / audio_player / elements / track_list.js
blob95894edfd8bc4f3fa654395f33a088949537b641
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.
5 /**
6  * @typedef {?{
7  *   url: string,
8  *   title: string,
9  *   artist: string,
10  *   artwork: Object,
11  *   active: boolean
12  * }}
13  */
14 var TrackInfo;
16 (function() {
17   'use strict';
19   Polymer({
20     is: 'track-list',
22     properties: {
23       /**
24        * List of tracks.
25        */
26       tracks: {
27         type: Array,
28         value: [],
29         observer: 'tracksChanged'
30       },
32       /**
33        * Track index of the current track.
34        * If the tracks property is empty, it should be -1. Otherwise, be a valid
35        * track number.
36        */
37       currentTrackIndex: {
38         type: Number,
39         value: -1,
40         observer: 'currentTrackIndexChanged',
41         notify: true
42       },
44       /**
45        * Whether shuffling play order is enabled or not.
46        */
47       shuffle: {
48         type: Boolean,
49         value: false,
50         observer: 'shuffleChanged'
51       }
52     },
54     /**
55      * Initializes an element. This method is called automatically when the
56      * element is ready.
57      */
58     ready: function() {
59       this.observeTrackList();
61       window.addEventListener('resize', this.onWindowResize_.bind(this));
62     },
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);
77     },
79     /**
80      * Play order of the tracks. Each value is the index of 'this.tracks'.
81      * @type {Array<number>}
82      */
83     playOrder: [],
85     /**
86      * Invoked when 'shuffle' property is changed.
87      * @param {boolean} newValue New value.
88      * @param {boolean} oldValue Old value.
89      */
90     shuffleChanged: function(newValue, oldValue) {
91       this.generatePlayOrder(true /* keep the current track */);
92     },
94     /**
95      * Invoked when the current track index is changed.
96      * @param {number} newValue new value.
97      * @param {number} oldValue old value.
98      */
99     currentTrackIndexChanged: function(newValue, oldValue) {
100       if (oldValue === newValue)
101         return;
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) {
109           // Success
110           this.set('tracks.' + newValue + '.active', true);
112           this.ensureTrackInViewport_(newValue /* trackIndex */);
113           return;
114         }
115       }
117       // Invalid index
118       if (this.tracks.length === 0)
119         this.currentTrackIndex = -1;
120       else
121         this.generatePlayOrder(false /* no need to keep the current track */);
122     },
124     /**
125      * Invoked when 'tracks' property is changed.
126      * @param {Array<!TrackInfo>} newValue New value.
127      * @param {Array<!TrackInfo>} oldValue Old value.
128      */
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);
141         }
143         // Reset play order and current index.
144         this.generatePlayOrder(false /* no need to keep the current track */);
145       } else {
146         this.playOrder = [];
147         this.currentTrackIndex = -1;
148       }
149     },
151     /**
152      * Invoked when the value in the 'tracks' is changed.
153      * @param {Array<Object>} changes The detail of the change.
154      */
155     tracksValueChanged_: function(changes) {
156       if (this.tracks.length === 0)
157         this.currentTrackIndex = -1;
158       else
159         this.set('tracks.' + this.currentTrackIndex + '.active', true);
160     },
162     /**
163      * Invoked when the track element is clicked.
164      * @param {Event} event Click event.
165      */
166     trackClicked: function(event) {
167       var index = ~~event.currentTarget.getAttribute('index');
168       var track = this.tracks[index];
169       if (track)
170         this.selectTrack(track);
171     },
173     /**
174      * Invoked when the window is resized.
175      * @private
176      */
177     onWindowResize_: function() {
178       this.ensureTrackInViewport_(this.currentTrackIndex);
179     },
181     /**
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.
184      * @private
185      */
186     ensureTrackInViewport_: function(trackIndex) {
187       var trackSelector = '::shadow .track[index="' + trackIndex + '"]';
188       var trackElement = this.querySelector(trackSelector);
189       if (trackElement) {
190         var viewTop = this.scrollTop;
191         var viewHeight = this.clientHeight;
192         var elementTop = trackElement.offsetTop;
193         var elementHeight = trackElement.offsetHeight;
195         if (elementTop < viewTop) {
196           // Adjust the tops.
197           this.scrollTop = elementTop;
198         } else if (elementTop + elementHeight <= viewTop + viewHeight) {
199           // The entire element is in the viewport. Do nothing.
200         } else {
201           // Adjust the bottoms.
202           this.scrollTop = Math.max(0,
203                                     (elementTop + elementHeight - viewHeight));
204         }
205       }
206     },
208     /**
209      * Invoked when the track element is clicked.
210      * @param {boolean} keepCurrentTrack Keep the current track or not.
211      */
212     generatePlayOrder: function(keepCurrentTrack) {
213       console.assert((keepCurrentTrack !== undefined),
214                      'The argument "forward" is undefined');
216       if (this.tracks.length === 0) {
217         this.playOrder = [];
218         return;
219       }
221       // Creates sequenced array.
222       this.playOrder =
223           this.tracks.
224           map(function(unused, index) { return index; });
226       if (this.shuffle) {
227         // Randomizes the play order array (Schwarzian-transform algorithm).
228         this.playOrder = this.playOrder
229             .map(function(a) {
230               return {weight: Math.random(), index: a};
231             })
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;
240               }, this);
241           this.playOrder.splice(0, 0, this.currentTrackIndex);
242         }
243       }
245       if (!keepCurrentTrack)
246         this.currentTrackIndex = this.playOrder[0];
247     },
249     /**
250      * Sets the current track.
251      * @param {!TrackInfo} track TrackInfo to be set as the current
252      *     track.
253      */
254     selectTrack: function(track) {
255       var index = -1;
256       for (var i = 0; i < this.tracks.length; i++) {
257         if (this.tracks[i].url === track.url) {
258           index = i;
259           break;
260         }
261       }
262       if (index >= 0) {
263         // TODO(yoshiki): Clean up the flow and the code around here.
264         if (this.currentTrackIndex == index)
265           this.replayCurrentTrack();
266         else
267           this.currentTrackIndex = index;
268       }
269     },
271     /**
272      * Request to replay the current music.
273      */
274     replayCurrentTrack: function() {
275       this.fire('replay');
276     },
278     /**
279      * Returns the current track.
280      * @return {TrackInfo} track TrackInfo of the current track.
281      */
282     getCurrentTrack: function() {
283       if (this.tracks.length === 0)
284         return null;
286       return this.tracks[this.currentTrackIndex];
287     },
289     /**
290      * Returns the next (or previous) track in the track list. If there is no
291      * next track, returns -1.
292      *
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
297      *     last.
298      * @return {number} The next track index.
299      */
300     getNextTrackIndex: function(forward, cyclic)  {
301       if (this.tracks.length === 0)
302         return -1;
304       var defaultTrackIndex =
305           forward ? this.playOrder[0] : this.playOrder[this.tracks.length - 1];
307       var currentPlayOrder = this.playOrder.indexOf(this.currentTrackIndex);
308       console.assert(
309           (0 <= currentPlayOrder && currentPlayOrder < this.tracks.length),
310           'Insufficient TrackList.playOrder. The current track is not on the ' +
311               'track list.');
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];
318       console.assert(
319           (0 <= newTrackIndex && newTrackIndex < this.tracks.length),
320           'Insufficient TrackList.playOrder. New Play Order: ' + newPlayOrder);
322       return newTrackIndex;
323     },
324   });
325 })();  // Anonymous closure