'use strict'; define(function(require, exports, module) { var cardsInit = require('cards_init'), htmlCache = require('html_cache'), mozL10n = require('l10n!'), evt = require('evt'), toaster = require('toaster'), transitionEnd = require('transition_end'), hookupInputAreaResetButtons = require('input_areas'); function addClass(domNode, name) { if (domNode) { domNode.classList.add(name); } } function removeClass(domNode, name) { if (domNode) { domNode.classList.remove(name); } } /** * Fairly simple card abstraction with support for simple horizontal animated * transitions. We are cribbing from deuxdrop's mobile UI's cards.js * implementation created jrburke. */ var cards = { _cardDefs: {}, /* * Existing cards, left-to-right, new cards getting pushed onto the right. */ _cardStack: [], activeCardIndex: -1, /* * @oneof[null @listof[cardName modeName]]{ * If a lazy load is causing us to have to wait before we push a card, this * is the type of card we are planning to push. This is used by hasCard * to avoid returning misleading answers while an async push is happening. * } */ _pendingPush: null, /** * Cards can stack on top of each other, make sure the stacked set is * visible over the lower sets. */ _zIndex: 0, /** * The DOM node that contains the _containerNode ("#cardContainer") and which * we inject popup and masking layers into. The choice of doing the popup * stuff at this layer is arbitrary. */ _rootNode: null, /** * The "#cardContainer" node which serves as the scroll container for the * contained _cardsNode ("#cards"). It is as wide as the viewport. */ _containerNode: null, /** * The "#cards" node that holds the cards; it is as wide as all of the cards * it contains and has its left offset changed in order to change what card * is visible. */ _cardsNode: null, /** * The DOM nodes that should be removed from their parent when our current * transition ends. */ _animatingDeadDomNodes: [], /** * Tracks the number of transition events per card animation. Since each * animation ends up with two transitionend events since two cards are * moving, need to wait for the last one to be finished before doing * cleanup, like DOM removal. */ _transitionCount: 0, /** * Tracks if startup events have been emitted. The events only need to be * emitted once. * @type {Boolean} */ _startupEventsEmitted: false, /** * Is a popup visible, suggesting that any click that is not on the popup * should be taken as a desire to close the popup? This is not a boolean, * but rather info on the active popup. */ _popupActive: null, /** * Are we eating all click events we see until we transition to the next * card (possibly due to a call to pushCard that has not yet occurred?). * Set by calling `eatEventsUntilNextCard`. */ _eatingEventsUntilNextCard: false, /** * Initialize and bind ourselves to the DOM which should now be fully loaded. */ init: function() { this._rootNode = document.body; this._containerNode = document.getElementById('cardContainer'); this._cardsNode = document.getElementById('cards'); this._statusColorMeta = document.querySelector('meta[name="theme-color"]'); toaster.init(this._containerNode); this._containerNode.addEventListener('click', this._onMaybeIntercept.bind(this), true); // XXX be more platform detecty. or just add more events. unless the // prefixes are already gone with webkit and opera? transitionEnd(this._cardsNode, this._onTransitionEnd.bind(this), false); // Listen for visibility changes to let current card know of them too. // Do this here instead of each card needing to listen, and needing to know // if it is also the current card. document.addEventListener('visibilitychange', function(evt) { var card = this._cardStack[this.activeCardIndex]; if (card && card.onCurrentCardDocumentVisibilityChange) { card.onCurrentCardDocumentVisibilityChange(document.hidden); } }.bind(this)); cardsInit(this); }, /** * If the tray is active and a click happens in the tray area, transition * back to the visible thing (which must be to our right currently.) */ _onMaybeIntercept: function(event) { if (this._eatingEventsUntilNextCard) { event.stopPropagation(); event.preventDefault(); return; } if (this._popupActive) { event.stopPropagation(); event.preventDefault(); this._popupActive.close(); return; } // Find the card containing the event target. var cardNode = event.target; for (cardNode = event.target; cardNode; cardNode = cardNode.parentElement) { if (cardNode.classList.contains('card')) { break; } } }, /** * Push a card onto the card-stack. */ /* @args[ * @param[type] * @param[showMethod @oneof[ * @case['animate']{ * Perform an animated scrolling transition. * } * @case['immediate']{ * Immediately warp to the card without animation. * } * @case['none']{ * Don't touch the view at all. * } * ]] * @param[args Object]{ * An arguments object to provide to the card's constructor when * instantiating. * } * @param[placement #:optional @oneof[ * @case[undefined]{ * The card gets pushed onto the end of the stack. * } * @case['left']{ * The card gets inserted to the left of the current card. * } * @case['right']{ * The card gets inserted to the right of the current card. * } * } * ] */ pushCard: function(type, showMethod, args, placement) { var cardDef = this._cardDefs[type]; args = args || {}; if (!cardDef) { var cbArgs = Array.slice(arguments); this._pendingPush = [type]; // Only eat clicks if the card will be visibly displayed. if (showMethod !== 'none') { this.eatEventsUntilNextCard(); } require(['element!cards/' + type], function(Ctor) { this._cardDefs[type] = Ctor; this.pushCard.apply(this, cbArgs); }.bind(this)); return; } this._pendingPush = null; console.log('pushCard for type: ' + type); var domNode = args.cachedNode || new cardDef(); if (args && domNode.onArgs) { domNode.onArgs(args); } var cardIndex, insertBuddy; if (!placement) { cardIndex = this._cardStack.length; insertBuddy = null; domNode.classList.add(cardIndex === 0 ? 'before' : 'after'); } else if (placement === 'left') { cardIndex = this.activeCardIndex++; insertBuddy = this._cardsNode.children[cardIndex]; domNode.classList.add('before'); } else if (placement === 'right') { cardIndex = this.activeCardIndex + 1; if (cardIndex >= this._cardStack.length) { insertBuddy = null; } else { insertBuddy = this._cardsNode.children[cardIndex]; } domNode.classList.add('after'); } this._cardStack.splice(cardIndex, 0, domNode); if (!args.cachedNode) { this._cardsNode.insertBefore(domNode, insertBuddy); } // If the card has any