blob: b9fa48c1a66810e60b25643e61c5a821b04a3349 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function() {
'use strict';
Polymer('track-list', {
/**
* Initializes an element. This method is called automatically when the
* element is ready.
*/
ready: function() {
this.tracksObserver_ = new ArrayObserver(
this.tracks,
this.tracksValueChanged_.bind(this));
window.addEventListener('resize', this.onWindowResize_.bind(this));
},
/**
* Registers handlers for changing of external variables
*/
observe: {
'model.shuffle': 'onShuffleChanged',
},
/**
* Model object of the Audio Player.
* @type {AudioPlayerModel}
*/
model: null,
/**
* List of tracks.
* @type {Array.<AudioPlayer.TrackInfo>}
*/
tracks: [],
/**
* Play order of the tracks. Each value is the index of 'this.tracks'.
* @type {Array.<number>}
*/
playOrder: [],
/**
* Track index of the current track.
* If the tracks property is empty, it should be -1. Otherwise, be a valid
* track number.
*
* @type {number}
*/
currentTrackIndex: -1,
/**
* Invoked when 'shuffle' property is changed.
* @param {boolean} oldValue Old value.
* @param {boolean} newValue New value.
*/
onShuffleChanged: function(oldValue, newValue) {
this.generatePlayOrder(true /* keep the current track */);
},
/**
* Invoked when the current track index is changed.
* @param {number} oldValue old value.
* @param {number} newValue new value.
*/
currentTrackIndexChanged: function(oldValue, newValue) {
if (oldValue === newValue)
return;
if (!isNaN(oldValue) && 0 <= oldValue && oldValue < this.tracks.length)
this.tracks[oldValue].active = false;
if (0 <= newValue && newValue < this.tracks.length) {
var currentPlayOrder = this.playOrder.indexOf(newValue);
if (currentPlayOrder !== -1) {
// Success
this.tracks[newValue].active = true;
this.ensureTrackInViewport_(newValue /* trackIndex */);
return;
}
}
// Invalid index
if (this.tracks.length === 0)
this.currentTrackIndex = -1;
else
this.generatePlayOrder(false /* no need to keep the current track */);
},
/**
* Invoked when 'tracks' property is changed.
* @param {Array.<TrackInfo>} oldValue Old value.
* @param {Array.<TrackInfo>} newValue New value.
*/
tracksChanged: function(oldValue, newValue) {
// Note: Sometimes both oldValue and newValue are null though the actual
// values are not null. Maybe it's a bug of Polymer.
// Re-register the observer of 'this.tracks'.
this.tracksObserver_.close();
this.tracksObserver_ = new ArrayObserver(this.tracks);
this.tracksObserver_.open(this.tracksValueChanged_.bind(this));
if (this.tracks.length !== 0) {
// Restore the active track.
if (this.currentTrackIndex !== -1 &&
this.currentTrackIndex < this.tracks.length) {
this.tracks[this.currentTrackIndex].active = true;
}
// Reset play order and current index.
this.generatePlayOrder(false /* no need to keep the current track */);
} else {
this.playOrder = [];
this.currentTrackIndex = -1;
}
},
/**
* Invoked when the value in the 'tracks' is changed.
* @param {Array.<Object>} splices The detail of the change.
*/
tracksValueChanged_: function(splices) {
if (this.tracks.length === 0)
this.currentTrackIndex = -1;
else
this.tracks[this.currentTrackIndex].active = true;
},
/**
* Invoked when the track element is clicked.
* @param {Event} event Click event.
*/
trackClicked: function(event) {
var index = ~~event.currentTarget.getAttribute('index');
var track = this.tracks[index];
if (track)
this.selectTrack(track);
},
/**
* Invoked when the window is resized.
* @private
*/
onWindowResize_: function() {
this.ensureTrackInViewport_(this.currentTrackIndex);
},
/**
* Scrolls the track list to ensure the given track in the viewport.
* @param {number} trackIndex The index of the track to be in the viewport.
* @private
*/
ensureTrackInViewport_: function(trackIndex) {
var trackSelector = '::shadow .track[index="' + trackIndex + '"]';
var trackElement = this.querySelector(trackSelector);
if (trackElement) {
var viewTop = this.scrollTop;
var viewHeight = this.clientHeight;
var elementTop = trackElement.offsetTop;
var elementHeight = trackElement.offsetHeight;
if (elementTop < viewTop) {
// Adjust the tops.
this.scrollTop = elementTop;
} else if (elementTop + elementHeight <= viewTop + viewHeight) {
// The entire element is in the viewport. Do nothing.
} else {
// Adjust the bottoms.
this.scrollTop = Math.max(0,
(elementTop + elementHeight - viewHeight));
}
}
},
/**
* Invoked when the track element is clicked.
* @param {boolean} keepCurrentTrack Keep the current track or not.
*/
generatePlayOrder: function(keepCurrentTrack) {
console.assert((keepCurrentTrack !== undefined),
'The argument "forward" is undefined');
if (this.tracks.length === 0) {
this.playOrder = [];
return;
}
// Creates sequenced array.
this.playOrder =
this.tracks.
map(function(unused, index) { return index; });
if (this.model && this.model.shuffle) {
// Randomizes the play order array (Schwarzian-transform algorithm).
this.playOrder =
this.playOrder.
map(function(a) {
return {weight: Math.random(), index: a};
}).
sort(function(a, b) { return a.weight - b.weight }).
map(function(a) { return a.index });
if (keepCurrentTrack) {
// Puts the current track at the beginning of the play order.
this.playOrder =
this.playOrder.filter(function(value) {
return this.currentTrackIndex !== value;
}, this);
this.playOrder.splice(0, 0, this.currentTrackIndex);
}
}
if (!keepCurrentTrack)
this.currentTrackIndex = this.playOrder[0];
},
/**
* Sets the current track.
* @param {AudioPlayer.TrackInfo} track TrackInfo to be set as the current
* track.
*/
selectTrack: function(track) {
var index = -1;
for (var i = 0; i < this.tracks.length; i++) {
if (this.tracks[i].url === track.url) {
index = i;
break;
}
}
if (index >= 0) {
// TODO(yoshiki): Clean up the flow and the code around here.
if (this.currentTrackIndex == index)
this.replayCurrentTrack();
else
this.currentTrackIndex = index;
}
},
/**
* Request to replay the current music.
*/
replayCurrentTrack: function() {
this.fire('replay');
},
/**
* Returns the current track.
* @param {AudioPlayer.TrackInfo} track TrackInfo of the current track.
*/
getCurrentTrack: function() {
if (this.tracks.length === 0)
return null;
return this.tracks[this.currentTrackIndex];
},
/**
* Returns the next (or previous) track in the track list. If there is no
* next track, returns -1.
*
* @param {boolean} forward Specify direction: forward or previous mode.
* True: forward mode, false: previous mode.
* @param {boolean} cyclic Specify if cyclically or not: It true, the first
* track is succeeding to the last track, otherwise no track after the
* last.
* @return {number} The next track index.
*/
getNextTrackIndex: function(forward, cyclic) {
if (this.tracks.length === 0)
return -1;
var defaultTrackIndex =
forward ? this.playOrder[0] : this.playOrder[this.tracks.length - 1];
var currentPlayOrder = this.playOrder.indexOf(this.currentTrackIndex);
console.assert(
(0 <= currentPlayOrder && currentPlayOrder < this.tracks.length),
'Insufficient TrackList.playOrder. The current track is not on the ' +
'track list.');
var newPlayOrder = currentPlayOrder + (forward ? +1 : -1);
if (newPlayOrder === -1 || newPlayOrder === this.tracks.length)
return cyclic ? defaultTrackIndex : -1;
var newTrackIndex = this.playOrder[newPlayOrder];
console.assert(
(0 <= newTrackIndex && newTrackIndex < this.tracks.length),
'Insufficient TrackList.playOrder. New Play Order: ' + newPlayOrder);
return newTrackIndex;
},
}); // Polymer('track-list') block
})(); // Anonymous closure