Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
831 lines
29 KiB
JavaScript
Executable file
831 lines
29 KiB
JavaScript
Executable file
'use strict';
|
|
/* global Application, FilterManager, CardManager, Clock, Deck, Edit, Folder,
|
|
KeyNavigationAdapter, MessageHandler, MozActivity, SearchBar,
|
|
SharedUtils, SpatialNavigator, URL, XScrollable, Animations,
|
|
Utils, FTEWizard, AppBookmark */
|
|
/* jshint nonew: false */
|
|
|
|
(function(exports) {
|
|
|
|
const FULLSIZED_ICON = 336 * (window.devicePixelRatio || 1);
|
|
const DEFAULT_ICON = 'url("/style/images/appic_developer.png")';
|
|
const DEFAULT_BGCOLOR = 'rgba(0, 0, 0, 0.5)';
|
|
const DEFAULT_BGCOLOR_ARRAY = [0, 0, 0, 0.5];
|
|
const CARDLIST_LEFT_MARGIN = 8.4;
|
|
|
|
/**
|
|
* Main class of smart-home
|
|
* @class Home
|
|
*/
|
|
function Home() {}
|
|
|
|
Home.prototype = {
|
|
navigableIds:
|
|
['search-button', 'search-input', 'edit-button', 'settings-button',
|
|
'filter-tab-group'],
|
|
|
|
topElementIds: ['search-button', 'search-input', 'edit-button',
|
|
'settings-button'],
|
|
|
|
bottomElementIds: ['filter-tab-group', 'filter-all-button',
|
|
'filter-tv-button', 'filter-device-button', 'filter-app-button',
|
|
'filter-website-button'],
|
|
|
|
isNavigable: true,
|
|
navigableClasses: ['filter-tab', 'command-button'],
|
|
navigableScrollable: [],
|
|
cardScrollable: undefined,
|
|
folderScrollable: undefined,
|
|
_focus: undefined,
|
|
_focusScrollable: undefined,
|
|
_folderCard: undefined,
|
|
|
|
filterElementIds: ['filter-all-button', 'filter-tv-button',
|
|
'filter-device-button', 'filter-app-button', 'filter-website-button'],
|
|
|
|
filterManager: undefined,
|
|
cardListElem: document.getElementById('card-list'),
|
|
folderListElem: document.getElementById('folder-list'),
|
|
cardManager: undefined,
|
|
editButton: document.getElementById('edit-button'),
|
|
settingsButton: document.getElementById('settings-button'),
|
|
searchButton: document.getElementById('search-button'),
|
|
timeElem: document.getElementById('time'),
|
|
fteElem: document.getElementById('fte'),
|
|
|
|
|
|
init: function() {
|
|
var that = this;
|
|
|
|
this.initClock();
|
|
|
|
this.cardManager = new CardManager();
|
|
this.cardManager.init();
|
|
|
|
this.searchBar = new SearchBar();
|
|
this.searchBar.init(document.getElementById('search-bar'));
|
|
this.searchBar.on('shown', this.onSearchBarShown.bind(this));
|
|
this.searchBar.on('hidden', this.onSearchBarHidden.bind(this));
|
|
|
|
this.cardManager.getCardList().then(function(cardList) {
|
|
that.messageHandler = new MessageHandler();
|
|
that.messageHandler.init(that);
|
|
|
|
that._createCardList(cardList);
|
|
that.cardScrollable = new XScrollable({
|
|
frameElem: 'card-list-frame',
|
|
listElem: 'card-list',
|
|
itemClassName: 'app-button',
|
|
leftMargin: CARDLIST_LEFT_MARGIN});
|
|
|
|
that.folderScrollable = new XScrollable({
|
|
frameElem: 'folder-list-frame',
|
|
listElem: 'folder-list',
|
|
itemClassName: 'app-button',
|
|
leftMargin: CARDLIST_LEFT_MARGIN,
|
|
scale: 0.68,
|
|
referenceElement: that.cardScrollable});
|
|
|
|
that.navigableScrollable = [that.cardScrollable, that.folderScrollable];
|
|
var collection = that._getNavigateElements();
|
|
|
|
that.spatialNavigator = new SpatialNavigator(collection);
|
|
that.spatialNavigator.straightOnly = true;
|
|
|
|
that.keyNavigatorAdapter = new KeyNavigationAdapter();
|
|
that.keyNavigatorAdapter.init();
|
|
that.keyNavigatorAdapter.on('move', that.onMove.bind(that));
|
|
// All behaviors which no need to have multple events while holding
|
|
// the key should use keyup.
|
|
that.keyNavigatorAdapter.on('enter-keyup', that.onEnter.bind(that));
|
|
|
|
that.cardListElem.addEventListener('transitionend',
|
|
that.determineFolderExpand.bind(that));
|
|
|
|
that.cardManager.on('card-inserted',
|
|
that.onCardInserted.bind(that, that.cardScrollable));
|
|
that.cardManager.on('card-removed',
|
|
that.onCardRemoved.bind(that, that.cardScrollable));
|
|
that.cardManager.on('card-updated', that.onCardUpdated.bind(that));
|
|
|
|
that.spatialNavigator.on('focus', that.handleFocus.bind(that));
|
|
that.spatialNavigator.on('unfocus', that.handleUnfocus.bind(that));
|
|
var handleCardFocusBound = that.handleCardFocus.bind(that);
|
|
var handleCardUnfocusBound = that.handleCardUnfocus.bind(that);
|
|
var handleCardUnhoverBound = that.handleCardUnhover.bind(that);
|
|
|
|
that.navigableScrollable.forEach(function(scrollable) {
|
|
scrollable.on('focus', handleCardFocusBound);
|
|
scrollable.on('unfocus', handleCardUnfocusBound);
|
|
scrollable.on('unhover', handleCardUnhoverBound);
|
|
if (scrollable.isEmpty()) {
|
|
that.spatialNavigator.remove(scrollable);
|
|
}
|
|
});
|
|
|
|
that.edit = new Edit();
|
|
that.edit.init(that.spatialNavigator, that.cardManager,
|
|
that.cardScrollable, that.folderScrollable);
|
|
that.edit.on('arrange', that.onArrangeMode.bind(that));
|
|
|
|
that.filterManager = new FilterManager();
|
|
that.filterManager.init({
|
|
cardListElem: that.cardListElem,
|
|
cardScrollable: that.cardScrollable,
|
|
home: that,
|
|
cardManager: that.cardManager,
|
|
});
|
|
|
|
// In some case, we can do action at keydown which is translated as
|
|
// onEnter in home.js. But in button click case, we need to listen
|
|
// keyup. So, instead keydown/keyup, we just use click event to handle
|
|
// it. The click event is translated at smart-button when use press
|
|
// enter on smart-button.
|
|
that.searchButton.addEventListener('click', function() {
|
|
that.searchBar.show();
|
|
// hide the searchButton because searchBar has an element whose
|
|
// appearance is the same as it.
|
|
that.searchButton.classList.add('hidden');
|
|
}.bind(that));
|
|
|
|
// handle animation
|
|
that.endBubble = null;
|
|
document.addEventListener(
|
|
'visibilitychange', that.onVisibilityChange.bind(that));
|
|
// if this init function is executed after the document is set to
|
|
// visible, the visibilitychange event may not be triggered.
|
|
if (document.visibilityState === 'visible') {
|
|
that.onVisibilityChange();
|
|
}
|
|
|
|
cardList.forEach(function(card) {
|
|
if (card instanceof Folder) {
|
|
card.on('card-inserted',
|
|
that.onCardInserted.bind(that, that.folderScrollable));
|
|
card.on('card-removed',
|
|
that.onCardRemoved.bind(that, that.folderScrollable));
|
|
}
|
|
});
|
|
that._fteWizard = new FTEWizard('homeFTE');
|
|
that._fteWizard.init({
|
|
container: that.fteElem,
|
|
onfinish: () => {
|
|
that.spatialNavigator.focus(that.cardScrollable);
|
|
that.isNavigable = true;
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
onVisibilityChange: function() {
|
|
if (document.visibilityState === 'visible') {
|
|
Utils.holdFocusForAnimation();
|
|
this.cardListElem.classList.remove('hidden');
|
|
var that = this;
|
|
Promise.all([new Promise(function(resolve) {
|
|
that.skipBubble = Animations.doBubbleAnimation(
|
|
that.cardListElem, '.app-button', 100, resolve);
|
|
|
|
}), new Promise(function(resolve) {
|
|
if (that._folderCard) {
|
|
that.skipFolderBubble = Animations.doBubbleAnimation(
|
|
that.folderListElem, '.app-button', 100, resolve);
|
|
} else {
|
|
resolve();
|
|
}
|
|
|
|
})]).then(function() {
|
|
// Catch focus back unless there is a pin activity since callback of
|
|
// pinning would catch the focus for us.
|
|
if (that._fteWizard.running) {
|
|
that._fteWizard.focus();
|
|
} else if (!that.messageHandler.resumeActivity()) {
|
|
that.spatialNavigator.focus();
|
|
}
|
|
|
|
that.isNavigable = true;
|
|
that.skipBubble = null;
|
|
that.skipFolderBubble = null;
|
|
});
|
|
} else {
|
|
this.cardListElem.classList.add('hidden');
|
|
this.messageHandler.stopActivity();
|
|
this.isNavigable = false;
|
|
// An user may close home app when bubbling or sliding animations are
|
|
// still playing, and then open home app again right away. In this case,
|
|
// the user will see the last unfinished animations. In order to solve
|
|
// this, we have to force disable all the animations and trigger their
|
|
// callbacks when home app is in hidden state.
|
|
if (this.skipBubble) {
|
|
this.skipBubble();
|
|
}
|
|
if (this.skipFolderBubble) {
|
|
this.skipFolderBubble();
|
|
}
|
|
if (this.cardScrollable.isSliding) {
|
|
this.cardScrollable.endSlide();
|
|
}
|
|
this.filterManager.resetFilter();
|
|
}
|
|
},
|
|
|
|
initClock: function() {
|
|
var that = this;
|
|
navigator.mozL10n.ready(function() {
|
|
that.clock = new Clock();
|
|
that.clock.start(that.updateClock.bind(that));
|
|
// Listen to 'moztimechange'
|
|
window.addEventListener('moztimechange',
|
|
that.restartClock.bind(that));
|
|
// Listen to 'timeformatchange'
|
|
window.addEventListener('timeformatchange',
|
|
that.restartClock.bind(that));
|
|
});
|
|
},
|
|
|
|
_localizeCardName: function(elem, card) {
|
|
if (!elem || !card) {
|
|
return;
|
|
}
|
|
|
|
// We should use user given name first, otherwise we use localized
|
|
// application/deck name.
|
|
var lang = document.documentElement.lang;
|
|
var name = this.cardManager.resolveCardName(card, lang);
|
|
if (name && name.raw) {
|
|
elem.removeAttribute('data-l10n-id');
|
|
elem.textContent = name.raw;
|
|
} else if (name && name.id) {
|
|
SharedUtils.localizeElement(elem, name);
|
|
}
|
|
},
|
|
|
|
onCardInserted: function(scrollable, card, idx, overFolder) {
|
|
if (card instanceof Folder) {
|
|
card.on('card-inserted',
|
|
this.onCardInserted.bind(this, this.folderScrollable));
|
|
card.on('card-removed',
|
|
this.onCardRemoved.bind(this, this.folderScrollable));
|
|
}
|
|
|
|
var newCardElem = this.createCardNode(card);
|
|
var newCardButtonElem = newCardElem.firstElementChild;
|
|
// Initial transition for new card
|
|
newCardButtonElem.classList.add('new-card');
|
|
newCardButtonElem.classList.add('new-card-transition');
|
|
newCardButtonElem.addEventListener('transitionend', function onPinned() {
|
|
newCardButtonElem.classList.remove('new-card-transition');
|
|
newCardButtonElem.classList.remove('last-card');
|
|
newCardButtonElem.removeEventListener('transitionend', onPinned);
|
|
});
|
|
// insert new card into cardScrollable
|
|
this.isNavigable = false;
|
|
scrollable.on('slideEnd', function() {
|
|
newCardButtonElem.classList.remove('new-card');
|
|
if (scrollable.nodes.indexOf(newCardElem) ===
|
|
scrollable.nodes.length - 1) {
|
|
newCardButtonElem.classList.add('last-card');
|
|
}
|
|
this.isNavigable = true;
|
|
}.bind(this));
|
|
if (!overFolder) {
|
|
scrollable.insertNodeBefore(newCardElem, idx);
|
|
} else {
|
|
scrollable.insertNodeOver(newCardElem, idx);
|
|
}
|
|
},
|
|
|
|
onCardUpdated: function(card, idx) {
|
|
var that = this;
|
|
var cardButton = this.cardScrollable.getItemFromNode(
|
|
this.cardScrollable.getNode(idx));
|
|
var spans =
|
|
SharedUtils.nodeListToArray(cardButton.getElementsByTagName('span'));
|
|
spans.forEach(function(span) {
|
|
if (span.classList.contains('name')) {
|
|
that._localizeCardName(span, card);
|
|
}
|
|
});
|
|
},
|
|
|
|
onCardRemoved: function(scrollable, indices) {
|
|
indices.forEach(function(idx) {
|
|
var elm = scrollable.getNode(idx);
|
|
if (elm && elm.dataset.revokableURL) {
|
|
URL.revokeObjectURL(elm.dataset.revokableURL);
|
|
}
|
|
}, this);
|
|
scrollable.removeNodes(indices);
|
|
},
|
|
|
|
_setCardIcon: function (cardButton, card, blob, bgColor) {
|
|
try {
|
|
var bgUrl = URL.createObjectURL(blob);
|
|
if (bgColor) {
|
|
cardButton.style.backgroundColor = bgColor;
|
|
cardButton.classList.add('fitted');
|
|
card.backgroundType = 'fitted';
|
|
} else {
|
|
cardButton.classList.add('fullsized');
|
|
card.backgroundType = 'fullsized';
|
|
}
|
|
cardButton.dataset.revokableURL = bgUrl;
|
|
cardButton.style.backgroundImage = 'url("' + bgUrl + '")';
|
|
} catch (e) {
|
|
// If the blob is broken, we may get an exception while creating object
|
|
// URL.
|
|
cardButton.style.backgroundImage = DEFAULT_ICON;
|
|
cardButton.style.backgroundColor = DEFAULT_BGCOLOR;
|
|
}
|
|
},
|
|
|
|
_createWave: function(cardButton, card) {
|
|
|
|
// deck's icon using gaia font
|
|
var deckIcon = document.createElement('span');
|
|
deckIcon.className = 'icon';
|
|
deckIcon.dataset.icon = card.deckClass;
|
|
|
|
// front wave of a deck
|
|
var waveFront = document.createElement('div');
|
|
waveFront.className = 'deck-wave';
|
|
waveFront.classList.add('wave-front');
|
|
waveFront.classList.add(card.deckClass + '-wave-front');
|
|
waveFront.classList.add('wave-paused');
|
|
|
|
// back wave of a deck
|
|
var waveBack = document.createElement('div');
|
|
waveBack.className = 'deck-wave';
|
|
waveBack.classList.add('wave-back');
|
|
waveBack.classList.add(card.deckClass + '-wave-back');
|
|
|
|
cardButton.appendChild(waveBack);
|
|
cardButton.appendChild(deckIcon);
|
|
cardButton.appendChild(waveFront);
|
|
cardButton.classList.add('deck-' + card.deckClass);
|
|
},
|
|
|
|
_fillCardIcon: function(cardButton, card) {
|
|
var manifestURL = card.nativeApp && card.nativeApp.manifestURL;
|
|
var that = this;
|
|
// We have thumbnail which is created by pin
|
|
if (card.thumbnail) {
|
|
this._setCardIcon(cardButton, card, card.thumbnail,
|
|
card.backgroundColor);
|
|
// TODO add backgroundColor??? How to do it???
|
|
} else if (!card.cachedIconBlob) {
|
|
// We don't have cachedIconBlob, just get icon from app
|
|
this.cardManager.getIconBlob({
|
|
manifestURL: manifestURL,
|
|
entryPoint: card.entryPoint,
|
|
// XXX: preferredSize should be determined by
|
|
// real offsetWidth of cardThumbnailElem instead of hard-coded value
|
|
preferredSize: FULLSIZED_ICON
|
|
}).then(function(iconData) {
|
|
var blob = iconData[0];
|
|
var size = iconData[1];
|
|
if (size >= FULLSIZED_ICON) {
|
|
that._setCardIcon(cardButton, card, blob, null);
|
|
} else {
|
|
that._getIconColor(blob, function(color, err) {
|
|
if (err) {
|
|
that._setCardIcon(cardButton, card, blob, DEFAULT_BGCOLOR);
|
|
} else {
|
|
that._setCardIcon(cardButton, card, blob, 'rgba(' + color[0] +
|
|
', ' + color[1] + ', ' + color[2] + ', ' + color[3] + ')');
|
|
}
|
|
});
|
|
}
|
|
card.cachedIconBlob = blob;
|
|
});
|
|
} else if (card.cachedIconBlob) {
|
|
// We already have cacedIconBlob which is created by previous step.
|
|
this._setCardIcon(cardButton, card, card.cachedIconBlob,
|
|
card.backgroundColor);
|
|
}
|
|
},
|
|
|
|
_getIconColor: function(blob, callback) {
|
|
var dy = 0;
|
|
function checkColor(color, err) {
|
|
if (err) {
|
|
callback(null, err);
|
|
} else if (color[3] < 255 && dy < 0.5) {
|
|
dy += 0.25;
|
|
SharedUtils.readColorCode(blob, 0.5, dy, checkColor);
|
|
} else {
|
|
callback(color[3] < 255 ? DEFAULT_BGCOLOR_ARRAY : color, err);
|
|
}
|
|
}
|
|
|
|
SharedUtils.readColorCode(blob, 0.5, 0, checkColor);
|
|
},
|
|
|
|
onArrangeMode: function() {
|
|
if (this._focusScrollable !== this.folderScrollable) {
|
|
this.cleanFolderScrollable();
|
|
}
|
|
},
|
|
|
|
createCardNode: function(card) {
|
|
// card element would be created like this:
|
|
// <div class="card">
|
|
// <smart-button>/* Card button */</smart-button>
|
|
// <section class="card-panel">
|
|
// <smart-button>/* Rename button */</smart-button>
|
|
// <smart-button>/* Delete button */</smart-button>
|
|
// </section>
|
|
// </div>
|
|
// and return DOM element
|
|
var cardNode = document.createElement('div');
|
|
cardNode.classList.add('card');
|
|
|
|
var cardButton = document.createElement('smart-button');
|
|
cardButton.setAttribute('type', 'app-button');
|
|
cardButton.className = 'app-button';
|
|
cardButton.dataset.cardId = card.cardId;
|
|
|
|
var cardPanel = document.createElement('section');
|
|
cardPanel.className = 'card-panel';
|
|
|
|
var renameButton = document.createElement('smart-button');
|
|
renameButton.dataset.icon = 'rename';
|
|
renameButton.classList.add('rename-btn');
|
|
|
|
var deleteButton = document.createElement('smart-button');
|
|
deleteButton.dataset.icon = 'delete';
|
|
deleteButton.classList.add('delete-btn');
|
|
|
|
cardPanel.appendChild(renameButton);
|
|
cardPanel.appendChild(deleteButton);
|
|
|
|
cardNode.appendChild(cardButton);
|
|
cardNode.appendChild(cardPanel);
|
|
|
|
// XXX: will support Folder and other type of Card in the future
|
|
// for now, we only create card element for Application and Deck
|
|
if (card instanceof Application) {
|
|
if (card.group === 'tv') {
|
|
cardButton.classList.add('tv-channel');
|
|
cardButton.dataset.icon = 'tv';
|
|
cardButton.setAttribute('app-type', 'tv');
|
|
} else {
|
|
cardButton.setAttribute('app-type', 'app');
|
|
this._fillCardIcon(cardButton, card);
|
|
}
|
|
} else if (card instanceof Deck) {
|
|
cardButton.setAttribute('app-type', 'deck');
|
|
if (card.group === 'website') {
|
|
this._fillCardIcon(cardButton, card);
|
|
} else {
|
|
this._createWave(cardButton, card);
|
|
}
|
|
} else if (card instanceof AppBookmark) {
|
|
cardButton.setAttribute('app-type', 'appbookmark');
|
|
this._fillCardIcon(cardButton, card);
|
|
} else if (card instanceof Folder) {
|
|
cardButton.setAttribute('app-type', 'folder');
|
|
cardButton.dataset.icon = 'folder';
|
|
}
|
|
|
|
// For smart-button, we put card name in pseudo-element :after. However,
|
|
// we need to localize card name and l10n library do not support
|
|
// localizing element with children elements.
|
|
// Instead of using :after, we create a 'span' element under smart-button
|
|
// and put card name in it.
|
|
var nameSpan = document.createElement('span');
|
|
nameSpan.classList.add('name');
|
|
this._localizeCardName(nameSpan, card);
|
|
cardButton.appendChild(nameSpan);
|
|
|
|
return cardNode;
|
|
},
|
|
|
|
_createCardList: function(cardList) {
|
|
cardList.forEach(function(card) {
|
|
this.cardListElem.appendChild(this.createCardNode(card));
|
|
}.bind(this));
|
|
},
|
|
|
|
onMove: function(key) {
|
|
if (!this.isNavigable || this.edit.onMove(key)) {
|
|
return;
|
|
}
|
|
|
|
var focus = this.spatialNavigator.getFocusedElement();
|
|
|
|
if (!(focus.CLASS_NAME == 'XScrollable' && focus.move(key))) {
|
|
this.spatialNavigator.move(key);
|
|
}
|
|
},
|
|
|
|
onEnter: function() {
|
|
if (!this.isNavigable || this.edit.onEnter()) {
|
|
return;
|
|
}
|
|
|
|
var focusElem = this.focusElem;
|
|
|
|
if (focusElem === this.settingsButton) {
|
|
this.openSettings();
|
|
} else if (focusElem === this.editButton) {
|
|
this.cleanFolderScrollable();
|
|
this.edit.toggleEditMode();
|
|
// XXX: Reset card filter when entering edit mode
|
|
this.filterManager.resetFilter();
|
|
} else if (focusElem &&
|
|
this.filterElementIds.indexOf(focusElem.id) > -1) {
|
|
this.cleanFolderScrollable();
|
|
} else {
|
|
// Current focus is on a card
|
|
var cardId = focusElem.dataset.cardId;
|
|
var card;
|
|
if (this.focusScrollable === this.folderScrollable) {
|
|
card = this._folderCard.findCard({cardId: cardId});
|
|
} else {
|
|
card = this.cardManager.findCardFromCardList({cardId: cardId});
|
|
}
|
|
|
|
if (card) {
|
|
card.launch();
|
|
}
|
|
}
|
|
},
|
|
|
|
onSearchBarShown: function() {
|
|
var hideSearchBar = function() {
|
|
document.removeEventListener('visibilitychange', hideSearchBar);
|
|
this.searchBar.hide();
|
|
}.bind(this);
|
|
document.addEventListener('visibilitychange', hideSearchBar);
|
|
|
|
var activity = new MozActivity({
|
|
name: 'search',
|
|
data: { keyword: '' }
|
|
});
|
|
|
|
activity.onerror = hideSearchBar;
|
|
},
|
|
|
|
onSearchBarHidden: function() {
|
|
this.searchButton.classList.remove('hidden');
|
|
},
|
|
|
|
_getNavigateElements: function() {
|
|
var elements = [];
|
|
this.navigableIds.forEach(function(id) {
|
|
var elem = document.getElementById(id);
|
|
if (elem) {
|
|
elements.push(elem);
|
|
}
|
|
});
|
|
this.navigableClasses.forEach(function(className) {
|
|
var elems = document.getElementsByClassName(className);
|
|
if (elems.length) {
|
|
// Change HTMLCollection to array before concatenating
|
|
elements = elements.concat(Array.prototype.slice.call(elems));
|
|
}
|
|
});
|
|
elements = elements.concat(this.navigableScrollable);
|
|
return elements;
|
|
},
|
|
|
|
handleFocus: function(elem) {
|
|
if (elem.CLASS_NAME == 'XScrollable') {
|
|
this._focusScrollable = elem;
|
|
elem.focus();
|
|
this.checkFocusedGroup();
|
|
} else if (elem.nodeName) {
|
|
if (this._focus) {
|
|
this._focus.blur();
|
|
}
|
|
|
|
switch(elem.nodeName.toLowerCase()) {
|
|
case 'menu-group':
|
|
this.handleFocusMenuGroup(elem);
|
|
break;
|
|
default:
|
|
elem.focus();
|
|
this._focus = elem;
|
|
this._focusScrollable = undefined;
|
|
this.checkFocusedGroup(elem);
|
|
break;
|
|
}
|
|
} else {
|
|
this._focusScrollable = undefined;
|
|
}
|
|
},
|
|
|
|
handleUnfocus: function(elem, nodeElem) {
|
|
if(elem.CLASS_NAME == 'XScrollable') {
|
|
this.handleCardUnfocus(
|
|
elem, elem.currentItem, elem.getNodeFromItem(elem.currentItem));
|
|
}
|
|
},
|
|
|
|
checkFocusedGroup: function(elem) {
|
|
if (!this._focusedGroup) {
|
|
return;
|
|
}
|
|
|
|
// close the focused group when we move focus out of this group.
|
|
if (!elem || !this._focusedGroup.contains(elem)) {
|
|
this._focusedGroup.close();
|
|
this._focusedGroup = null;
|
|
}
|
|
},
|
|
|
|
handleFocusMenuGroup: function(menuGroup) {
|
|
var self = this;
|
|
menuGroup.once('opened', function() {
|
|
self.spatialNavigator.remove(menuGroup);
|
|
var childElement = menuGroup.firstElementChild;
|
|
var firstFocusable = null;
|
|
while(childElement) {
|
|
switch(childElement.nodeName.toLowerCase()) {
|
|
case 'style':
|
|
case 'script':
|
|
break;
|
|
default:
|
|
firstFocusable = firstFocusable || childElement;
|
|
self.spatialNavigator.add(childElement);
|
|
}
|
|
childElement = childElement.nextElementSibling;
|
|
}
|
|
if (firstFocusable) {
|
|
self.spatialNavigator.focus(firstFocusable);
|
|
}
|
|
});
|
|
menuGroup.once('will-close', function() {
|
|
// Clear all opened event listener because we won't have it if opened is
|
|
// not fired and the group is trying to close it self.
|
|
menuGroup.off('opened');
|
|
self.spatialNavigator.add(menuGroup);
|
|
var childElement = menuGroup.firstElementChild;
|
|
while(childElement) {
|
|
switch(childElement.nodeName.toLowerCase()) {
|
|
case 'style':
|
|
case 'script':
|
|
break;
|
|
default:
|
|
self.spatialNavigator.remove(childElement);
|
|
}
|
|
childElement = childElement.nextElementSibling;
|
|
}
|
|
});
|
|
this.checkFocusedGroup(menuGroup);
|
|
this._focusedGroup = menuGroup;
|
|
menuGroup.open();
|
|
},
|
|
|
|
handleCardFocus: function(scrollable, itemElem, nodeElem) {
|
|
this._focus = itemElem;
|
|
|
|
if (this.edit.mode === 'edit') {
|
|
this.edit.handleCardFocus(scrollable, itemElem, nodeElem);
|
|
}
|
|
itemElem.focus();
|
|
nodeElem.classList.add('focused');
|
|
if(scrollable === this.cardScrollable && this._folderCard &&
|
|
itemElem.dataset.cardId !== this._folderCard.cardId &&
|
|
!this.cardScrollable.isHovering) {
|
|
this.cleanFolderScrollable();
|
|
}
|
|
},
|
|
|
|
cleanFolderScrollable: function(doNotChangeFocus) {
|
|
if (this._focusScrollable === this.folderScrollable &&
|
|
!doNotChangeFocus) {
|
|
this.spatialNavigator.focus(this.cardScrollable);
|
|
}
|
|
this.spatialNavigator.remove(this.folderScrollable);
|
|
this.folderScrollable.clean();
|
|
this._folderCard = undefined;
|
|
this.edit.isFolderReady = false;
|
|
this.cardScrollable.setColspanOnFocus(0);
|
|
},
|
|
|
|
handleCardUnfocus: function(scrollable, itemElem, nodeElem) {
|
|
// Fix null error when the last card in a folder is removed.
|
|
if (nodeElem) {
|
|
nodeElem.classList.remove('focused');
|
|
}
|
|
},
|
|
|
|
handleCardUnhover: function(scrollable, itemElem, nodeElem) {
|
|
this.cleanFolderScrollable();
|
|
},
|
|
|
|
determineFolderExpand: function(evt) {
|
|
// Folder expansion is performed on only when user moves cursor onto a
|
|
// folder or hover a folder in edit mode and it finished its focus
|
|
// transition.
|
|
if (this.focusScrollable === this.cardScrollable &&
|
|
evt.originalTarget.classList.contains('app-button') &&
|
|
(!this._folderCard ||
|
|
this._folderCard.cardId !== evt.originalTarget.dataset.cardId) &&
|
|
(evt.originalTarget.classList.contains('focused') &&
|
|
// Listen to 'outline-width' rather than 'transform' here since it
|
|
// also applies to edit mode when user moves from panel button back
|
|
// to card.
|
|
evt.propertyName === 'outline-width' &&
|
|
document.getElementById('main-section').dataset.mode !== 'arrange' ||
|
|
// Folder needs to be expanded when hovered as well.
|
|
evt.originalTarget.classList.contains('hovered'))) {
|
|
this.buildFolderList(evt.originalTarget);
|
|
}
|
|
},
|
|
|
|
buildFolderList: function(target) {
|
|
var cardId = target.dataset.cardId;
|
|
var card = this.cardManager.findCardFromCardList({cardId: cardId});
|
|
if (!(card instanceof Folder)) {
|
|
return;
|
|
}
|
|
|
|
this._folderCard = card;
|
|
var folderList = this._folderCard.getCardList();
|
|
|
|
if (folderList.length === 0) {
|
|
this.edit.isFolderReady = true;
|
|
// Needs to disable the folder-list animation right away to
|
|
// prevent it from affecting the new-card into folder animation.
|
|
this.folderListElem.style.transition = 'none';
|
|
return;
|
|
}
|
|
|
|
// Build folder list
|
|
folderList.forEach(function(card) {
|
|
this.folderScrollable.addNode(this.createCardNode(card));
|
|
}, this);
|
|
|
|
var isFirstFrame = true;
|
|
var initFolderAnimation = function() {
|
|
if (isFirstFrame) {
|
|
isFirstFrame = false;
|
|
// At first frame, we call setReferenceElement to move folder list
|
|
// right under folder card. Transition should be replaced by 'none'
|
|
// since we don't need to show this process as animation to user.
|
|
this.folderListElem.style.transition = 'none';
|
|
this.folderScrollable.realignToReferenceElement();
|
|
this.skipFolderBubble = Animations.doBubbleAnimation(
|
|
this.folderListElem, '.app-button', 100, function() {
|
|
this.spatialNavigator.add(this.folderScrollable);
|
|
this.edit.isFolderReady = true;
|
|
this.skipFolderBubble = undefined;
|
|
}.bind(this));
|
|
|
|
window.requestAnimationFrame(initFolderAnimation);
|
|
} else {
|
|
// 2nd frame, recover original transition.
|
|
this.folderListElem.style.transition = '';
|
|
}
|
|
}.bind(this);
|
|
window.requestAnimationFrame(initFolderAnimation);
|
|
},
|
|
|
|
openSettings: function() {
|
|
/* jshint nonew: false */
|
|
new MozActivity({
|
|
name: 'configure',
|
|
data: {}
|
|
});
|
|
},
|
|
|
|
updateClock: function() {
|
|
var now = new Date();
|
|
var _ = navigator.mozL10n.get;
|
|
var use12Hour = window.navigator.mozHour12;
|
|
|
|
var f = new navigator.mozL10n.DateTimeFormat();
|
|
|
|
var timeFormat = use12Hour ? _('shortTimeFormat12') :
|
|
_('shortTimeFormat24');
|
|
// remove AM/PM and we use our owned style to show it.
|
|
timeFormat = timeFormat.replace('%p', '').trim();
|
|
var formatted = f.localeFormat(now, timeFormat);
|
|
|
|
this.timeElem.innerHTML = formatted;
|
|
this.timeElem.dataset.ampm = use12Hour ? f.localeFormat(now, '%p') : '';
|
|
},
|
|
|
|
restartClock: function() {
|
|
navigator.mozL10n.ready((function() {
|
|
// restart clcok
|
|
this.clock.stop();
|
|
this.clock.start(this.updateClock.bind(this));
|
|
}).bind(this));
|
|
},
|
|
|
|
get focusElem() {
|
|
return this._focus;
|
|
},
|
|
|
|
get focusScrollable() {
|
|
return this._focusScrollable;
|
|
}
|
|
};
|
|
|
|
exports.Home = Home;
|
|
}(window));
|
|
|