Fix search results being clipped in app list.
[chromium-blink-merge.git] / ui / file_manager / audio_player / elements / track_list.js
blob0f14416c13a729cd39e2693ecd992ba4e831e7f7
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 (function() {
6   'use strict';
8   /**
9    * @constructor
10    * @extends {PolymerElement}
11    */
12   var TrackListElement = function() {};
14   TrackListElement.prototype = {
15     /**
16      * Initializes an element. This method is called automatically when the
17      * element is ready.
18      */
19     ready: function() {
20       this.observeTrackList();
22       window.addEventListener('resize', this.onWindowResize_.bind(this));
23     },
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);
38     },
40     /**
41      * Registers handlers for changing of external variables
42      */
43     observe: {
44       'model.shuffle': 'onShuffleChanged',
45     },
47     /**
48      * Model object of the Audio Player.
49      * @type {AudioPlayerModel}
50      */
51     model: null,
53     /**
54      * List of tracks.
55      * @type {Array.<AudioPlayer.TrackInfo>}
56      */
57     tracks: [],
59     /**
60      * Play order of the tracks. Each value is the index of 'this.tracks'.
61      * @type {Array.<number>}
62      */
63     playOrder: [],
65     /**
66      * Track index of the current track.
67      * If the tracks property is empty, it should be -1. Otherwise, be a valid
68      * track number.
69      *
70      * @type {number}
71      */
72     currentTrackIndex: -1,
74     /**
75      * Invoked when 'shuffle' property is changed.
76      * @param {boolean} oldValue Old value.
77      * @param {boolean} newValue New value.
78      */
79     onShuffleChanged: function(oldValue, newValue) {
80       this.generatePlayOrder(true /* keep the current track */);
81     },
83     /**
84      * Invoked when the current track index is changed.
85      * @param {number} oldValue old value.
86      * @param {number} newValue new value.
87      */
88     currentTrackIndexChanged: function(oldValue, newValue) {
89       if (oldValue === newValue)
90         return;
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) {
98           // Success
99           this.tracks[newValue].active = true;
101           this.ensureTrackInViewport_(newValue /* trackIndex */);
102           return;
103         }
104       }
106       // Invalid index
107       if (this.tracks.length === 0)
108         this.currentTrackIndex = -1;
109       else
110         this.generatePlayOrder(false /* no need to keep the current track */);
111     },
113     /**
114      * Invoked when 'tracks' property is changed.
115      * @param {Array.<AudioPlayer.TrackInfo>} oldValue Old value.
116      * @param {Array.<AudioPlayer.TrackInfo>} newValue New value.
117      */
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;
130         }
132         // Reset play order and current index.
133         this.generatePlayOrder(false /* no need to keep the current track */);
134       } else {
135         this.playOrder = [];
136         this.currentTrackIndex = -1;
137       }
138     },
140     /**
141      * Invoked when the value in the 'tracks' is changed.
142      * @param {Array.<Object>} changes The detail of the change.
143      */
144     tracksValueChanged_: function(changes) {
145       if (this.tracks.length === 0)
146         this.currentTrackIndex = -1;
147       else
148         this.tracks[this.currentTrackIndex].active = true;
149     },
151     /**
152      * Invoked when the track element is clicked.
153      * @param {Event} event Click event.
154      */
155     trackClicked: function(event) {
156       var index = ~~event.currentTarget.getAttribute('index');
157       var track = this.tracks[index];
158       if (track)
159         this.selectTrack(track);
160     },
162     /**
163      * Invoked when the window is resized.
164      * @private
165      */
166     onWindowResize_: function() {
167       this.ensureTrackInViewport_(this.currentTrackIndex);
168     },
170     /**
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.
173      * @private
174      */
175     ensureTrackInViewport_: function(trackIndex) {
176       var trackSelector = '::shadow .track[index="' + trackIndex + '"]';
177       var trackElement = this.querySelector(trackSelector);
178       if (trackElement) {
179         var viewTop = this.scrollTop;
180         var viewHeight = this.clientHeight;
181         var elementTop = trackElement.offsetTop;
182         var elementHeight = trackElement.offsetHeight;
184         if (elementTop < viewTop) {
185           // Adjust the tops.
186           this.scrollTop = elementTop;
187         } else if (elementTop + elementHeight <= viewTop + viewHeight) {
188           // The entire element is in the viewport. Do nothing.
189         } else {
190           // Adjust the bottoms.
191           this.scrollTop = Math.max(0,
192                                     (elementTop + elementHeight - viewHeight));
193         }
194       }
195     },
197     /**
198      * Invoked when the track element is clicked.
199      * @param {boolean} keepCurrentTrack Keep the current track or not.
200      */
201     generatePlayOrder: function(keepCurrentTrack) {
202       console.assert((keepCurrentTrack !== undefined),
203                      'The argument "forward" is undefined');
205       if (this.tracks.length === 0) {
206         this.playOrder = [];
207         return;
208       }
210       // Creates sequenced array.
211       this.playOrder =
212           this.tracks.
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
218             .map(function(a) {
219               return {weight: Math.random(), index: a};
220             })
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;
229               }, this);
230           this.playOrder.splice(0, 0, this.currentTrackIndex);
231         }
232       }
234       if (!keepCurrentTrack)
235         this.currentTrackIndex = this.playOrder[0];
236     },
238     /**
239      * Sets the current track.
240      * @param {AudioPlayer.TrackInfo} track TrackInfo to be set as the current
241      *     track.
242      */
243     selectTrack: function(track) {
244       var index = -1;
245       for (var i = 0; i < this.tracks.length; i++) {
246         if (this.tracks[i].url === track.url) {
247           index = i;
248           break;
249         }
250       }
251       if (index >= 0) {
252         // TODO(yoshiki): Clean up the flow and the code around here.
253         if (this.currentTrackIndex == index)
254           this.replayCurrentTrack();
255         else
256           this.currentTrackIndex = index;
257       }
258     },
260     /**
261      * Request to replay the current music.
262      */
263     replayCurrentTrack: function() {
264       this.fire('replay');
265     },
267     /**
268      * Returns the current track.
269      * @return {AudioPlayer.TrackInfo} track TrackInfo of the current track.
270      */
271     getCurrentTrack: function() {
272       if (this.tracks.length === 0)
273         return null;
275       return this.tracks[this.currentTrackIndex];
276     },
278     /**
279      * Returns the next (or previous) track in the track list. If there is no
280      * next track, returns -1.
281      *
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
286      *     last.
287      * @return {number} The next track index.
288      */
289     getNextTrackIndex: function(forward, cyclic)  {
290       if (this.tracks.length === 0)
291         return -1;
293       var defaultTrackIndex =
294           forward ? this.playOrder[0] : this.playOrder[this.tracks.length - 1];
296       var currentPlayOrder = this.playOrder.indexOf(this.currentTrackIndex);
297       console.assert(
298           (0 <= currentPlayOrder && currentPlayOrder < this.tracks.length),
299           'Insufficient TrackList.playOrder. The current track is not on the ' +
300               'track list.');
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];
307       console.assert(
308           (0 <= newTrackIndex && newTrackIndex < this.tracks.length),
309           'Insufficient TrackList.playOrder. New Play Order: ' + newPlayOrder);
311       return newTrackIndex;
312     },
313   };  // TrackListElement.prototype for 'track-list'
315   Polymer('track-list', TrackListElement.prototype);
316 })();  // Anonymous closure