Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
551 lines
20 KiB
JavaScript
Executable file
551 lines
20 KiB
JavaScript
Executable file
/* global evt, Folder, SmartModalDialog, Utils */
|
|
|
|
'use strict';
|
|
|
|
(function(exports) {
|
|
const EDIT_MODE_SCALE = 0.57;
|
|
const CARD_TRANSFORM_LATENCY = 500;
|
|
|
|
/**
|
|
* Edit is the controller class when smart-home is in edit mode.
|
|
*
|
|
* @class Edit
|
|
*/
|
|
function Edit() {}
|
|
|
|
Edit.prototype = evt({
|
|
mainSection: document.getElementById('main-section'),
|
|
doneButton: document.getElementById('done-button'),
|
|
searchButton: document.getElementById('search-button'),
|
|
addNewFolderButton: document.getElementById('add-new-folder-button'),
|
|
editButton: document.getElementById('edit-button'),
|
|
settingsButton: document.getElementById('settings-button'),
|
|
|
|
regularNavElements: undefined,
|
|
editNavElements: undefined,
|
|
currentNode: undefined,
|
|
|
|
init: function(spatialNavigator, cardManager,
|
|
cardScrollable, folderScrollable) {
|
|
var that = this;
|
|
this.modalDialog = new SmartModalDialog();
|
|
|
|
this.spatialNavigator = spatialNavigator;
|
|
this.cardManager = cardManager;
|
|
this.cardScrollable = cardScrollable;
|
|
this.folderScrollable = folderScrollable;
|
|
|
|
this.regularNavElements =
|
|
[this.searchButton, this.settingsButton, this.editButton];
|
|
this.editNavElements = [this.doneButton, this.addNewFolderButton];
|
|
|
|
this.cardManager.on('card-swapped', this.onCardSwapped.bind(this));
|
|
this.cardManager.getCardList().then(function(cardList) {
|
|
cardList.forEach(function(card) {
|
|
if (card instanceof Folder) {
|
|
card.on('card-swapped', that.onCardSwapped.bind(that));
|
|
}
|
|
});
|
|
});
|
|
|
|
this.cardScrollable.on('listTransformEnd',
|
|
this.handleListTransformEnd.bind(this));
|
|
this.cardScrollable.on('nodeTransformEnd',
|
|
this.onCardMoveAnimationEnd.bind(this));
|
|
this.cardScrollable.on('hovering-node-removed',
|
|
this.onHoveringNodeRemoved.bind(this));
|
|
this.cardScrollable.on('node-inserted-over-folder',
|
|
this.onNodeInsertedOverFolder.bind(this));
|
|
|
|
this.spatialNavigator.on('focus', this.handleFocus.bind(this));
|
|
this.spatialNavigator.on('unfocus', this.handleUnfocus.bind(this));
|
|
|
|
// A workaround for checking if the folder list is navigable
|
|
this.isFolderReady = false;
|
|
// The hovering card
|
|
this._hoveringCard = null;
|
|
},
|
|
/**
|
|
* State of home app looks like this:
|
|
* (normal state)<-->Edit mode<-->Arrange mode
|
|
* (Only adjacant states are switchable)
|
|
*/
|
|
toggleEditMode: function() {
|
|
if (this.mainSection.dataset.mode === 'edit') {
|
|
this.mainSection.dataset.mode = '';
|
|
this.spatialNavigator.multiAdd(this.regularNavElements);
|
|
this.spatialNavigator.multiRemove(this.editNavElements);
|
|
this.cardScrollable.setScale();
|
|
this.folderScrollable.setScale();
|
|
this.cardScrollable.listElem.classList.add('exiting-edit-mode');
|
|
this.cardManager.writeCardlistInCardStore({cleanEmptyFolder: true})
|
|
.then(function() {
|
|
// Since writeCardlistInCardStore triggers card-removed event that
|
|
// causes re-focus on other elements in card list, we need to wait
|
|
// until those actions to be done before focusing editButton.
|
|
this.spatialNavigator.focus(this.editButton);
|
|
this.currentNode.classList.remove('focused');
|
|
}.bind(this));
|
|
this._concealPanel(this.currentScrollable, this.currentNode);
|
|
this.fire('exit-edit-mode');
|
|
} else {
|
|
this.mainSection.dataset.mode = 'edit';
|
|
this.spatialNavigator.multiRemove(this.regularNavElements);
|
|
this.spatialNavigator.multiAdd(this.editNavElements);
|
|
|
|
this._setCurrentScrollable(this.cardScrollable);
|
|
this.currentNode =
|
|
this.cardScrollable.getNodeFromItem(this.cardScrollable.currentItem);
|
|
this._revealPanel(this.currentScrollable, this.currentNode);
|
|
|
|
this.spatialNavigator.focus(this.cardScrollable);
|
|
this.cardScrollable.setScale(EDIT_MODE_SCALE);
|
|
this.folderScrollable.setScale(EDIT_MODE_SCALE);
|
|
this.fire('enter-edit-mode');
|
|
}
|
|
},
|
|
|
|
toggleArrangeMode: function() {
|
|
if (this.mainSection.dataset.mode === 'edit') {
|
|
this.mainSection.dataset.mode = 'arrange';
|
|
this._concealPanel(this.currentScrollable, this.currentNode);
|
|
this.currentScrollable.focus(this.currentScrollable.currentItem);
|
|
this._setHintArrow();
|
|
if (this.currentScrollable === this.folderScrollable) {
|
|
this.cardScrollable.setColspanOnFocus(1);
|
|
}
|
|
this.fire('arrange');
|
|
} else if (this.mainSection.dataset.mode == 'arrange') {
|
|
this.mainSection.dataset.mode = 'edit';
|
|
|
|
this.currentNode = this.currentScrollable.getNodeFromItem(
|
|
this.currentScrollable.currentItem);
|
|
this._revealPanel(this.currentScrollable, this.currentNode);
|
|
this._clearHintArrow();
|
|
this.cardScrollable.setColspanOnFocus(0);
|
|
this.folderScrollable.realignToReferenceElement();
|
|
}
|
|
},
|
|
|
|
_moveQueue: {
|
|
queue: [],
|
|
isEmpty: function() {
|
|
return (this.queue.length === 0);
|
|
},
|
|
push: function(obj) {
|
|
this.queue.push(obj);
|
|
},
|
|
shift: function() {
|
|
return this.queue.shift();
|
|
},
|
|
getLength: function() {
|
|
return this.queue.length;
|
|
},
|
|
clearQueue: function() {
|
|
this.queue.length = 0;
|
|
}
|
|
},
|
|
|
|
_moveTimer: undefined,
|
|
|
|
_produceMove: function(key) {
|
|
this._moveQueue.push(key);
|
|
if (!this._moveTimer) {
|
|
this._consumeMove();
|
|
}
|
|
},
|
|
|
|
_consumeMove: function() {
|
|
var focus = this.spatialNavigator.getFocusedElement();
|
|
|
|
if (!this._moveQueue.isEmpty() && focus.CLASS_NAME === 'XScrollable') {
|
|
var key = this._moveQueue.shift();
|
|
var targetItem = focus.getTargetItem(key);
|
|
|
|
switch (key) {
|
|
case 'left':
|
|
case 'right':
|
|
if (!focus.isHovering && targetItem &&
|
|
focus.currentItem.getAttribute('app-type') !== 'deck' &&
|
|
focus.currentItem.getAttribute('app-type') !== 'folder' &&
|
|
targetItem.getAttribute('app-type') === 'folder') {
|
|
// hover on folder
|
|
this.hoverCard(focus, focus.currentItem, targetItem);
|
|
this._moveTimer =
|
|
window.setTimeout(this.onCardMoveAnimationEnd.bind(this),
|
|
CARD_TRANSFORM_LATENCY, targetItem);
|
|
} else if (focus.isHovering && targetItem !== focus.hoveredItem) {
|
|
this.unhoverCard(focus, true);
|
|
this._moveTimer =
|
|
window.setTimeout(this.onCardMoveAnimationEnd.bind(this),
|
|
CARD_TRANSFORM_LATENCY, targetItem);
|
|
} else if (targetItem) {
|
|
if (focus.isHovering) {
|
|
this.unhoverCard(focus, false);
|
|
}
|
|
var focusedCard = this.cardManager.findCardFromCardList(
|
|
{cardId: focus.currentItem.dataset.cardId});
|
|
var nonFocusedCard = this.cardManager.findCardFromCardList(
|
|
{cardId: targetItem.dataset.cardId});
|
|
if (this.currentScrollable === this.folderScrollable) {
|
|
var containingFolder = this.cardManager.findContainingFolder(
|
|
{cardId: focus.currentItem.dataset.cardId});
|
|
containingFolder.swapCard(focusedCard, nonFocusedCard);
|
|
} else {
|
|
this.cardManager.swapCard(focusedCard, nonFocusedCard);
|
|
}
|
|
|
|
this._moveTimer =
|
|
window.setTimeout(this.onCardMoveAnimationEnd.bind(this),
|
|
CARD_TRANSFORM_LATENCY, targetItem);
|
|
} else {
|
|
// if queue is not empty but we are failed to get targetItem
|
|
// that means we reach the boundary of cards. Thus we should
|
|
// consume next action in queue
|
|
this._consumeMove();
|
|
}
|
|
|
|
break;
|
|
case 'down':
|
|
if (focus.isHovering && this.isFolderReady) {
|
|
this.moveToFolder();
|
|
this._moveTimer =
|
|
window.setTimeout(this.onCardMoveAnimationEnd.bind(this),
|
|
CARD_TRANSFORM_LATENCY);
|
|
}
|
|
this._moveQueue.clearQueue();
|
|
break;
|
|
case 'up':
|
|
if (this.currentScrollable === this.folderScrollable) {
|
|
this.moveToCardList(focus);
|
|
this._moveTimer =
|
|
window.setTimeout(this.onCardMoveAnimationEnd.bind(this),
|
|
CARD_TRANSFORM_LATENCY);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
onCardMoveAnimationEnd: function(elem) {
|
|
if (this._moveTimer) {
|
|
window.clearTimeout(this._moveTimer);
|
|
this._moveTimer = undefined;
|
|
}
|
|
if (elem &&
|
|
!elem.classList.contains('focused') &&
|
|
!this._moveQueue.isEmpty()) {
|
|
this._consumeMove();
|
|
}
|
|
},
|
|
|
|
onCardSwapped: function(card1, card2, idx1, idx2) {
|
|
this.currentScrollable.swap(idx1, idx2);
|
|
this._setHintArrow();
|
|
},
|
|
|
|
onMove: function(key) {
|
|
if (this.modalDialog.isOpened) {
|
|
return true;
|
|
}
|
|
|
|
if (this.mode !== 'arrange') {
|
|
return false;
|
|
}
|
|
this._produceMove(key);
|
|
|
|
return true;
|
|
},
|
|
|
|
_setHintArrow: function(scrollable) {
|
|
var index = parseInt(this.currentNode.dataset.idx, 10);
|
|
if (scrollable && scrollable.isHovering) {
|
|
this.currentNode.classList.toggle('left_arrow', true);
|
|
this.currentNode.classList.toggle('right_arrow', true);
|
|
this.currentNode.classList.toggle('down_arrow', true);
|
|
this.currentNode.classList.remove('up_arrow');
|
|
} else {
|
|
this.currentNode.classList.toggle('left_arrow', index > 0);
|
|
this.currentNode.classList.toggle('right_arrow',
|
|
index < this.currentScrollable.length - 1);
|
|
this.currentNode.classList.remove('down_arrow');
|
|
this.currentNode.classList.toggle('up_arrow',
|
|
this.currentScrollable === this.folderScrollable);
|
|
}
|
|
},
|
|
|
|
_setCurrentScrollable: function(scrollable) {
|
|
this.currentScrollable = scrollable;
|
|
document.getElementById('card-viewer').classList.toggle(
|
|
'current-scrollable', scrollable === this.cardScrollable);
|
|
document.getElementById('folder-viewer').classList.toggle(
|
|
'current-scrollable', scrollable === this.folderScrollable);
|
|
},
|
|
|
|
_clearHintArrow: function() {
|
|
this.currentNode.classList.remove('left_arrow');
|
|
this.currentNode.classList.remove('right_arrow');
|
|
},
|
|
|
|
addNewFolder: function() {
|
|
var folder = this.cardManager.insertNewFolder({id: 'new-folder'},
|
|
this.cardScrollable.currentIndex);
|
|
folder.on('card-swapped', this.onCardSwapped.bind(this));
|
|
this.spatialNavigator.focus(this.cardScrollable);
|
|
},
|
|
|
|
moveToFolder: function() {
|
|
// Remove the card from the main list
|
|
this.cardManager.removeCard(this._hoveringCard);
|
|
},
|
|
|
|
/**
|
|
* Runs after moveToFolder call, when the hovering card is successfully
|
|
* removed by cardManager and the node is removed from cardScrollable.
|
|
*/
|
|
onHoveringNodeRemoved: function(cardItem, folderItem) {
|
|
var card = this._hoveringCard;
|
|
var folder = this.cardManager.findCardFromCardList(
|
|
{cardId: folderItem.dataset.cardId});
|
|
// Add the card into the folder
|
|
folder.addCard(card, this.folderScrollable.isReversed ? null : 0);
|
|
this._hoveringCard = null;
|
|
this._setCurrentScrollable(this.folderScrollable);
|
|
this.folderScrollable.realignToReferenceElement();
|
|
this.currentNode = this.folderScrollable.getNodeFromItem(
|
|
this.folderScrollable.currentItem);
|
|
if (!this.spatialNavigator.focus(this.folderScrollable)) {
|
|
// When a card is added to a new empty folder,
|
|
// add the folderScrollable to this.spatialNavigator.
|
|
this.spatialNavigator.add(this.folderScrollable);
|
|
this.spatialNavigator.focus(this.folderScrollable);
|
|
}
|
|
this._setHintArrow();
|
|
},
|
|
|
|
moveToCardList: function(focus) {
|
|
var folder = this.cardManager.findContainingFolder(
|
|
{cardId: focus.currentItem.dataset.cardId});
|
|
var card = this.cardManager.findCardFromCardList(
|
|
{cardId: focus.currentItem.dataset.cardId});
|
|
var folderItem = this.cardScrollable.currentItem;
|
|
var idx = this.cardScrollable.getNodeFromItem(folderItem).dataset.idx;
|
|
|
|
this._hoveringCard = card;
|
|
|
|
folder.removeCard(card);
|
|
|
|
// insert card
|
|
this.cardManager.insertCard({
|
|
card: card,
|
|
index: parseInt(idx, 10),
|
|
overFolder: true
|
|
});
|
|
},
|
|
|
|
onNodeInsertedOverFolder: function() {
|
|
this.spatialNavigator.focus(this.cardScrollable);
|
|
this._setCurrentScrollable(this.cardScrollable);
|
|
this.currentNode = this.cardScrollable.getNodeFromItem(
|
|
this.cardScrollable.currentItem);
|
|
this._setHintArrow(this.currentScrollable);
|
|
this.folderScrollable.realignToReferenceElement();
|
|
},
|
|
|
|
onEnter: function() {
|
|
if (this.mode !== 'edit' && this.mode !== 'arrange') {
|
|
return false;
|
|
}
|
|
|
|
if (this.modalDialog.isOpened) {
|
|
return true;
|
|
}
|
|
|
|
var focus = this.spatialNavigator.getFocusedElement();
|
|
if (focus === this.doneButton) {
|
|
this.toggleEditMode();
|
|
} else if (focus === this.addNewFolderButton) {
|
|
this.addNewFolder();
|
|
} else if (focus.CLASS_NAME === 'XScrollable') {
|
|
var currentItem = focus.currentItem;
|
|
|
|
if (currentItem.classList.contains('rename-btn')) {
|
|
this.renameCard(focus, focus.getNodeFromItem(currentItem));
|
|
} else if (currentItem.classList.contains('delete-btn')) {
|
|
this.openDeleteCardDialog(focus, focus.getNodeFromItem(currentItem));
|
|
} else if (currentItem.classList.contains('hover')) {
|
|
// When a card is hovering over a folder, do nothing here.
|
|
} else {
|
|
// Current focus is on a card
|
|
this.toggleArrangeMode();
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
renameCard: function(scrollable, nodeElem) {
|
|
var card = this.cardManager.findCardFromCardList({
|
|
cardId: scrollable.getItemFromNode(nodeElem).dataset.cardId
|
|
});
|
|
var _ = navigator.mozL10n.get;
|
|
|
|
var lang = document.documentElement.lang;
|
|
var oldName = this.cardManager.resolveCardName(card, lang);
|
|
oldName = oldName.raw ? oldName.raw : _(oldName.id);
|
|
|
|
var result = prompt(_('enter-new-name'), oldName);
|
|
if (!result) {
|
|
return;
|
|
}
|
|
card.name = {raw: result};
|
|
this.cardManager.updateCard(card);
|
|
},
|
|
|
|
openDeleteCardDialog: function(scrollable, nodeElem) {
|
|
Utils.holdFocusForAnimation();
|
|
this.modalDialog.open({
|
|
message: {
|
|
textL10nId: 'delete-card-alert'
|
|
},
|
|
buttonSettings: [
|
|
{
|
|
textL10nId: 'cancel',
|
|
defaultFocus: true,
|
|
onClick: this.spatialNavigator.focus.bind(this.spatialNavigator)
|
|
},
|
|
{
|
|
textL10nId: 'delete',
|
|
class: 'danger',
|
|
onClick: this.deleteCard.bind(this, scrollable, nodeElem)
|
|
}
|
|
],
|
|
onCancel: this.spatialNavigator.focus.bind(this.spatialNavigator)
|
|
});
|
|
},
|
|
|
|
deleteCard: function(scrollable, nodeElem) {
|
|
var itemElem = scrollable.getItemFromNode(nodeElem);
|
|
this._concealPanel(scrollable, nodeElem);
|
|
nodeElem.classList.add('delete');
|
|
|
|
if (scrollable === this.folderScrollable) {
|
|
var folder = this.cardManager.findContainingFolder(
|
|
{cardId: itemElem.dataset.cardId});
|
|
var card = this.cardManager.findCardFromCardList(
|
|
{cardId: itemElem.dataset.cardId});
|
|
}
|
|
|
|
var deletedItem = scrollable.getItemFromNode(nodeElem);
|
|
deletedItem.addEventListener('transitionend', function(evt) {
|
|
if (evt.propertyName === 'transform') {
|
|
scrollable.spatialNavigator
|
|
.focus(scrollable.getItemFromNode(nodeElem));
|
|
if (scrollable === this.cardScrollable) {
|
|
this.cardManager.removeCard(parseInt(nodeElem.dataset.idx, 10));
|
|
} else {
|
|
folder.removeCard(card);
|
|
|
|
if (folder.isEmpty()) {
|
|
this.spatialNavigator.focus(this.cardScrollable);
|
|
}
|
|
}
|
|
}
|
|
}.bind(this));
|
|
},
|
|
|
|
hoverCard: function(scrollable, focusedItem, targetItem) {
|
|
var card = this.cardManager.findCardFromCardList(
|
|
{cardId: focusedItem.dataset.cardId});
|
|
this._hoveringCard = card;
|
|
scrollable.hover(focusedItem, targetItem);
|
|
// set up/bottom arrow
|
|
this._setHintArrow(scrollable);
|
|
},
|
|
|
|
unhoverCard: function(scrollable, shouldResetCardPositions) {
|
|
scrollable.unhover(shouldResetCardPositions);
|
|
this._hoveringCard = null;
|
|
// clear up/bottom arrow
|
|
if (shouldResetCardPositions) {
|
|
this._setHintArrow();
|
|
}
|
|
},
|
|
|
|
_getPanel: function(nodeElem) {
|
|
var cardPanel = nodeElem.getElementsByClassName('card-panel')[0];
|
|
return {
|
|
panel: cardPanel,
|
|
renameBtn: cardPanel.getElementsByClassName('rename-btn')[0],
|
|
deleteBtn: cardPanel.getElementsByClassName('delete-btn')[0]
|
|
};
|
|
},
|
|
|
|
_revealPanel: function(scrollable, nodeElem) {
|
|
var panel = this._getPanel(nodeElem);
|
|
if(scrollable.getItemFromNode(nodeElem).getAttribute('app-type') ===
|
|
'deck') {
|
|
// Decks can't be renamed or deleted.
|
|
return;
|
|
}
|
|
scrollable.spatialNavigator.add(panel.renameBtn);
|
|
scrollable.spatialNavigator.add(panel.deleteBtn);
|
|
},
|
|
|
|
_concealPanel: function(scrollable, nodeElem) {
|
|
var panel = this._getPanel(nodeElem);
|
|
scrollable.spatialNavigator.remove(panel.renameBtn);
|
|
scrollable.spatialNavigator.remove(panel.deleteBtn);
|
|
},
|
|
|
|
handleFocus: function(elem) {
|
|
if (this.mode === 'edit' && elem.CLASS_NAME === 'XScrollable') {
|
|
this.handleCardFocus(
|
|
elem, elem.currentItem, elem.getNodeFromItem(elem.currentItem));
|
|
}
|
|
},
|
|
|
|
handleUnfocus: function(elem) {
|
|
if (this.mode === 'edit' && elem === this.currentScrollable) {
|
|
this._concealPanel(this.currentScrollable, this.currentNode);
|
|
// Make sure we leave focus on item itself rather than any panel buttons
|
|
this.currentScrollable.silentFocus(
|
|
this.currentScrollable.getItemFromNode(this.currentNode));
|
|
}
|
|
},
|
|
|
|
handleCardFocus: function(scrollable, itemElem, nodeElem) {
|
|
if (this.mode !== 'edit' ||
|
|
// When focus goes outside the scrollable and the last selected item
|
|
// is a panel button, we still focus back to the card corresponding to
|
|
// that button to keep track of the last focused card. But in this
|
|
// case we don't need any further action on UI view.
|
|
this.spatialNavigator.getFocusedElement() !== scrollable) {
|
|
return;
|
|
}
|
|
// If users are moving from card to panel button, they're under the same
|
|
// node element. We need to "unfocus" the node only when user really move
|
|
// to another card. So here we call handleCardUnfocus manually.
|
|
if (this.currentNode != nodeElem) {
|
|
this._concealPanel(this.currentScrollable, this.currentNode);
|
|
|
|
this.currentNode = nodeElem;
|
|
this._setCurrentScrollable(scrollable);
|
|
}
|
|
this._revealPanel(scrollable, nodeElem);
|
|
itemElem.focus();
|
|
nodeElem.classList.add('focused');
|
|
},
|
|
|
|
handleListTransformEnd: function() {
|
|
if (this.mode === '') {
|
|
this.cardScrollable.listElem.classList.remove('exiting-edit-mode');
|
|
}
|
|
},
|
|
|
|
get mode() {
|
|
return this.mainSection.dataset.mode;
|
|
}
|
|
});
|
|
|
|
exports.Edit = Edit;
|
|
}(window));
|