Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,25 @@
{
"name": "smart-home",
"version": "0.0.0",
"homepage": "https://github.com/mozilla-b2g/gaia/tree/master/tv_apps/smart-home",
"description": "Smart Screen Home App",
"main": "index.html",
"license": "MPL",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"evt": "fxos-components/evt#~0.4.0",
"smart-button": "fxos-components/smart-button#~0.0.2",
"menu-group": "fxos-components/menu-group",
"smart-icons": "smart-components/smart-icons#~0.0.2",
"smart-bubbles": "smart-components/smart-bubbles",
"smart-dialog": "smart-components/smart-dialog",
"smart-modal-dialog": "smart-components/smart-modal-dialog"
}
}

View file

@ -0,0 +1,28 @@
{
"name": "evt",
"main": "index.js",
"version": "0.4.0",
"homepage": "https://github.com/wilsonpage/evt",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"package.json",
"test",
"tests",
"README.md"
],
"_release": "0.4.0",
"_resolution": {
"type": "version",
"tag": "v0.4.0",
"commit": "3593dd6b4dd19f683d00fd0eaf2581c11a128da6"
},
"_source": "git://github.com/gaia-components/evt.git",
"_target": "~0.4.0",
"_originalSource": "gaia-components/evt"
}

View file

@ -0,0 +1,19 @@
{
"name": "evt",
"main": "index.js",
"version": "0.4.0",
"homepage": "https://github.com/wilsonpage/evt",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"package.json",
"test",
"tests",
"README.md"
]
}

View file

@ -0,0 +1,154 @@
/**
* Evt
*
* A super lightweight
* event emitter library.
*
* @version 0.3.3
* @author Wilson Page <wilson.page@me.com>
*/
;(function() {
/**
* Locals
*/
var proto = Events.prototype;
var slice = [].slice;
/**
* Creates a new event emitter
* instance, or if passed an
* object, mixes the event logic
* into it.
*
* @param {Object} obj
* @return {Object}
*/
function Events(obj) {
if (!(this instanceof Events)) return new Events(obj);
if (obj) return mixin(obj, proto);
}
/**
* Registers a callback
* with an event name.
*
* @param {String} name
* @param {Function} cb
* @return {Event}
*/
proto.on = function(name, cb) {
this._cbs = this._cbs || {};
(this._cbs[name] || (this._cbs[name] = [])).push(cb);
return this;
};
/**
* Attach a callback that once
* called, detaches itself.
*
* TODO: Implement `.off()` to work
* with `once()` callbacks.
*
* @param {String} name
* @param {Function} cb
* @public
*/
proto.once = function(name, cb) {
this.on(name, one);
function one() {
cb.apply(this, arguments);
this.off(name, one);
}
};
/**
* Removes a single callback,
* or all callbacks associated
* with the passed event name.
*
* @param {String} name
* @param {Function} cb
* @return {Event}
*/
proto.off = function(name, cb) {
this._cbs = this._cbs || {};
if (!name) { this._cbs = {}; return; }
if (!cb) { return delete this._cbs[name]; }
var cbs = this._cbs[name] || [];
var i;
while (cbs && ~(i = cbs.indexOf(cb))) { cbs.splice(i, 1); }
return this;
};
/**
* Fires an event, triggering
* all callbacks registered on this
* event name.
*
* @param {String} name
* @return {Event}
*/
proto.fire = proto.emit = function(options) {
var cbs = this._cbs = this._cbs || {};
var name = options.name || options;
var batch = (cbs[name] || []).concat(cbs['*'] || []);
var ctx = options.ctx || this;
if (batch.length) {
this._fireArgs = arguments;
var args = slice.call(arguments, 1);
while (batch.length) {
batch.shift().apply(ctx, args);
}
}
return this;
};
proto.firer = function(name) {
var self = this;
return function() {
var args = slice.call(arguments);
args.unshift(name);
self.fire.apply(self, args);
};
};
/**
* Util
*/
/**
* Mixes in the properties
* of the second object into
* the first.
*
* @param {Object} a
* @param {Object} b
* @return {Object}
*/
function mixin(a, b) {
for (var key in b) a[key] = b[key];
return a;
}
/**
* Expose 'Event' (UMD)
*/
if (typeof exports === 'object') {
module.exports = Events;
} else if (typeof define === 'function' && define.amd) {
define(function(){ return Events; });
} else {
window.evt = Events;
}
})();

View file

@ -0,0 +1,29 @@
{
"name": "menu-group",
"homepage": "https://github.com/gaia-components/menu-group",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
],
"devDependencies": {
"evt": "gaia-components/evt#~0.4.0",
"smart-button": "gaia-components/smart-button"
},
"_release": "088b750c3f",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "088b750c3fd32c898f2fc30125a41f59eaee1257"
},
"_source": "git://github.com/gaia-components/menu-group.git",
"_target": "*",
"_originalSource": "gaia-components/menu-group"
}

View file

@ -0,0 +1,21 @@
{
"name": "menu-group",
"version": "0.0.0",
"homepage": "https://github.com/gaia-components/menu-group",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
],
"devDependencies": {
"evt": "gaia-components/evt#~0.4.0",
"smart-button": "gaia-components/smart-button"
}
}

View file

@ -0,0 +1,128 @@
'use strict';
/* global evt */
window.MenuGroup = (function(win) {
// Extend from the HTMLElement prototype
var proto = evt(Object.create(HTMLElement.prototype));
proto.createdCallback = function() {
this.style.width = '0';
this.classList.add('closed');
this.addEventListener('transitionend', this);
};
proto.calculateChildWidth = function() {
var child = this.firstElementChild;
var childWidth = 0;
var style;
while(child) {
style = window.getComputedStyle(child);
childWidth += child.offsetWidth + parseInt(style.marginLeft, 10) +
parseInt(style.marginRight, 10);
child = child.nextElementSibling;
}
return childWidth;
};
proto.fireEvent = function(event, detail) {
var evtObject = new CustomEvent(event, {
bubbles: false,
detail: detail || this
});
this.dispatchEvent(evtObject);
this.fire(event, detail);
};
proto.handleEvent = function(evt) {
switch(evt.type) {
// Like System app, the transition is our state machine.
case 'transitionend':
// We only process 'background-color' because all states have this
// change.
if ((evt.propertyName !== 'background-color' &&
evt.propertyName !== 'width') ||
evt.target !== this) {
break;
}
if (this.classList.contains('enlarging')) {
this.classList.remove('enlarging');
this.classList.add('shrinking');
// change to shrinking
} else if (this.classList.contains('shrinking')) {
// XXX: this is a workaround of CSS transform. We cannot have a
// rotation right after resizing without any settimeout???
setTimeout((function() {
if (!this.classList.contains('shrinking')) {
// If we don't have shrinking class here, that means this group
// is changing to another state and we don't need to opening
// state.
return;
}
this.classList.remove('shrinking');
this.classList.remove('closed');
this.classList.add('opening');
// change to opening
this.style.width = this.calculateChildWidth() + 'px';
}).bind(this));
} else if (this.classList.contains('opening')) {
this.classList.remove('opening');
// final state: opened
this.fireEvent('opened');
} else if (this.classList.contains('closing')) {
this.classList.remove('closing');
this.classList.add('closed');
// final state: closed
this.fireEvent('closed');
}
break;
}
};
proto.focus = proto.open = function() {
this.fireEvent('will-open');
// If we get focus when we closing the group, we need to cancel the closing
// state.
this.classList.remove('shrinking');
this.classList.remove('opening');
this.classList.remove('closing');
this.classList.add('enlarging');
};
proto.blur = proto.close = function() {
this.fireEvent('will-close');
// We may call close at mid state, we need to reset all of them and go to
// closing state
this.classList.remove('enlarging');
this.classList.remove('shrinking');
this.classList.remove('opening');
this.classList.add('closing');
this.style.width = '0';
};
proto.changeIcon = function(icon) {
if (!this.dataset.icon) {
this.dataset.icon = icon;
return;
}
var current = this.dataset.icon.split(' ')[0];
if (current === icon) {
return;
}
this.classList.add('switching-icon');
this.dataset.icon = icon + ' ' + current;
// Let gecko to recalculate the value and remove it.
// We shouldn't put any value at the setTimeout, but it doesn't work in some
// device, like FirefoxNightly, b2g-desktop. The 100 ms works well in
// these environments.
var self = this;
setTimeout(function() {
self.classList.remove('switching-icon');
}, 100);
};
// Register and return the constructor
return document.registerElement('menu-group', { prototype: proto });
})(window);

View file

@ -0,0 +1,136 @@
/**
* Button state:
* 1. normal (class name: [none])
* 2. enlarging (class name: enlarging)
* 3. shrinking (class name: shrinking)
* 4. opening (class name: opening)
* 5. closing (class name: closing)
* 6. closed (class name: closed)
**/
/**
* focus: closed -> closed + enlarging -> closed + shrinking -> opening -> ''
* close: '' -> closing -> closed;
**/
menu-group {
outline: 0;
display: inline-block;
height: 8rem;
padding-left: 8rem;
padding-right: 4rem;
opacity: 1;
/* we need to set border to none to remove the button border */
border: none;
/* the radius is still make button as round even if we don't have border */
border-radius: 4rem;
background-color: rgba(0, 0, 0, 0.5);
word-break: keep-all;
white-space: nowrap;
overflow: hidden;
position: relative;
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.42s;
}
menu-group:before {
content: " ";
background-repeat: no-repeat;
background-size: 5.4rem auto;
background-position: 1.3rem center;
width: 8rem;
height: 8rem;
display: inline-block;
position: absolute;
left: 0;
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.42s;
}
menu-group[rotate]:before {
transform: rotate(360deg);
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.42s;
}
menu-group[data-icon]:before {
width: 5.4rem;
left: 1.3rem;
font-size: 5.4rem;
line-height: 8rem;
text-align: center;
color: #ffffff;
white-space: normal;
top: 0;
}
menu-group[data-icon].switching-icon:before {
font-size: 5.4rem;
line-height: 8rem;
text-align: center;
color: #ffffff;
/**
* According to visual spec, we have falling down transition while icon
* change.
*
* The trick is:
* 1. to put the next icon at the top of current and slide icons down.
* 2. to set transition duration to 0 here to elimiate the slide up
* transition.
**/
top: -8rem;
transition: none;
}
menu-group.closed {
padding-right: 0;
transition-duration: 0.5s;
background-color: rgba(0, 0, 0, 0.5);
}
menu-group.closed > * {
display: none;
}
menu-group[rotate].closed:before {
transform: rotate(0deg);
}
menu-group.opening {
transition-duration: 0.72s;
/* add the same padding-right of normal to make it run transition */
padding-right: 4rem;
}
menu-group.opening:before {
transition-duration: 0.72s;
}
menu-group.closing {
transition-duration: 0.5s;
padding-right: 0;
background-color: rgba(0, 0, 0, 0.5);
}
menu-group[rotate].closing:before {
transform: rotate(0deg);
transition-duration: 0.5s;
}
menu-group.enlarging {
opacity: 1;
background-color: #ffffff;
transform: scale(1.2);
transition-duration: 0.2s;
}
menu-group.shrinking {
transition-duration: 0.23s;
}

View file

@ -0,0 +1,30 @@
{
"name": "smart-bubbles",
"homepage": "https://github.com/smart-components/smart-bubbles",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md",
"gulpfile.js",
"package.json"
],
"devDependencies": {
"smart-button": "smart-components/smart-button"
},
"_release": "5c08ef5d84",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "5c08ef5d84b0bd1d8252600cfc744aa7869e9f92"
},
"_source": "git://github.com/smart-components/smart-bubbles.git",
"_target": "*",
"_originalSource": "smart-components/smart-bubbles"
}

View file

@ -0,0 +1,22 @@
{
"name": "smart-bubbles",
"version": "0.0.0",
"homepage": "https://github.com/smart-components/smart-bubbles",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md",
"gulpfile.js",
"package.json"
],
"devDependencies": {
"smart-button": "smart-components/smart-button"
}
}

View file

@ -0,0 +1,83 @@
window.SmartBubbles = (function(win) {
'use strict';
var DEFAULT_INTERVAL = 100;
// Extend from the HTMLElement prototype
var proto = Object.create(HTMLElement.prototype);
proto.fireEvent = function(event, detail) {
var evtObject = new CustomEvent(event, {
bubbles: false,
detail: detail || this
});
this.dispatchEvent(evtObject);
};
proto.handleEvent = function(evt) {
switch(evt.type) {
case 'animationend':
this.fireEvent('item-bubbled', ++this._finishedAnimationCount);
if (this._targetAnimationCount === this._finishedAnimationCount) {
this.playing = false;
[].forEach.call(this._elements, function(elem) {
elem.classList.remove('smart-bubble-playing');
// XXX:Usually we use <smart-button> within container of
// smart-bubbles. If we remove style of smart-bubble-playing,
// Gecko would apply original style of smart-button instead and it
// has transition property too. This results in another transition
// happens on <smart-button> while we thought we'd just stop
// animation. That is why we need to add 'smart-bubble-stopped' here
// to prevent unexpected animation from happening.
elem.classList.add('smart-bubble-stopped');
// use getComputedStyle to force flushing re-style
window.getComputedStyle(elem).animationName;
elem.classList.remove('smart-bubble-stopped');
window.getComputedStyle(elem).animationName;
elem.style.animationDelay = '';
}.bind(this));
this._elements = null;
this.fireEvent('all-items-bubbled');
}
evt.currentTarget.removeEventListener('animationend', this);
break;
}
};
proto.play = function(elements) {
this.stopImmediately();
this._elements = elements;
this._finishedAnimationCount = 0;
this._targetAnimationCount = 0;
var interval = parseInt(this.getAttribute('interval'), 10) ||
DEFAULT_INTERVAL;
[].forEach.call(elements, function(elem) {
this._targetAnimationCount++;
var delay = this._targetAnimationCount * interval / 1000;
elem.classList.add('smart-bubble-playing');
elem.style.animationDelay = delay + 's';
elem.addEventListener('animationend', this);
}.bind(this));
this.playing = true;
};
proto.stopImmediately = function() {
if (!this.playing || !this._elements) {
return;
}
[].forEach.call(this._elements, function(elem) {
elem.classList.remove('smart-bubble-playing');
elem.classList.add('smart-bubble-stopped');
elem.removeEventListener('animationend', this);
// use getComputedStyle to force flushing re-style
window.getComputedStyle(elem).animationName;
elem.classList.remove('smart-bubble-stopped');
}.bind(this));
this._elements = null;
this.playing = false;
this.fireEvent('all-items-stopped');
};
// Register and return the constructor
return document.registerElement('smart-bubbles', { prototype: proto });
})(window);

View file

@ -0,0 +1 @@
window.SmartBubbles=function(t){"use strict";var e=100,i=Object.create(HTMLElement.prototype);return i.fireEvent=function(t,e){var i=new CustomEvent(t,{bubbles:!1,detail:e||this});this.dispatchEvent(i)},i.handleEvent=function(t){switch(t.type){case"animationend":this.fireEvent("item-bubbled",++this._finishedAnimationCount),this._targetAnimationCount===this._finishedAnimationCount&&(this.playing=!1,[].forEach.call(this._elements,function(t){t.classList.remove("smart-bubble-playing"),t.classList.add("smart-bubble-stopped"),window.getComputedStyle(t).animationName,t.classList.remove("smart-bubble-stopped"),window.getComputedStyle(t).animationName,t.style.animationDelay=""}.bind(this)),this._elements=null,this.fireEvent("all-items-bubbled")),t.currentTarget.removeEventListener("animationend",this)}},i.play=function(t){this.stopImmediately(),this._elements=t,this._finishedAnimationCount=0,this._targetAnimationCount=0;var i=parseInt(this.getAttribute("interval"),10)||e;[].forEach.call(t,function(t){this._targetAnimationCount++;var e=this._targetAnimationCount*i/1e3;t.classList.add("smart-bubble-playing"),t.style.animationDelay=e+"s",t.addEventListener("animationend",this)}.bind(this)),this.playing=!0},i.stopImmediately=function(){this.playing&&this._elements&&([].forEach.call(this._elements,function(t){t.classList.remove("smart-bubble-playing"),t.classList.add("smart-bubble-stopped"),t.removeEventListener("animationend",this),window.getComputedStyle(t).animationName,t.classList.remove("smart-bubble-stopped")}.bind(this)),this._elements=null,this.playing=!1,this.fireEvent("all-items-stopped"))},document.registerElement("smart-bubbles",{prototype:i})}(window);

View file

@ -0,0 +1,83 @@
window.SmartBubbles = (function(win) {
'use strict';
var DEFAULT_INTERVAL = 100;
// Extend from the HTMLElement prototype
var proto = Object.create(HTMLElement.prototype);
proto.fireEvent = function(event, detail) {
var evtObject = new CustomEvent(event, {
bubbles: false,
detail: detail || this
});
this.dispatchEvent(evtObject);
};
proto.handleEvent = function(evt) {
switch(evt.type) {
case 'animationend':
this.fireEvent('item-bubbled', ++this._finishedAnimationCount);
if (this._targetAnimationCount === this._finishedAnimationCount) {
this.playing = false;
[].forEach.call(this._elements, function(elem) {
elem.classList.remove('smart-bubble-playing');
// XXX:Usually we use <smart-button> within container of
// smart-bubbles. If we remove style of smart-bubble-playing,
// Gecko would apply original style of smart-button instead and it
// has transition property too. This results in another transition
// happens on <smart-button> while we thought we'd just stop
// animation. That is why we need to add 'smart-bubble-stopped' here
// to prevent unexpected animation from happening.
elem.classList.add('smart-bubble-stopped');
// use getComputedStyle to force flushing re-style
window.getComputedStyle(elem).animationName;
elem.classList.remove('smart-bubble-stopped');
window.getComputedStyle(elem).animationName;
elem.style.animationDelay = '';
}.bind(this));
this._elements = null;
this.fireEvent('all-items-bubbled');
}
evt.currentTarget.removeEventListener('animationend', this);
break;
}
};
proto.play = function(elements) {
this.stopImmediately();
this._elements = elements;
this._finishedAnimationCount = 0;
this._targetAnimationCount = 0;
var interval = parseInt(this.getAttribute('interval'), 10) ||
DEFAULT_INTERVAL;
[].forEach.call(elements, function(elem) {
this._targetAnimationCount++;
var delay = this._targetAnimationCount * interval / 1000;
elem.classList.add('smart-bubble-playing');
elem.style.animationDelay = delay + 's';
elem.addEventListener('animationend', this);
}.bind(this));
this.playing = true;
};
proto.stopImmediately = function() {
if (!this.playing || !this._elements) {
return;
}
[].forEach.call(this._elements, function(elem) {
elem.classList.remove('smart-bubble-playing');
elem.classList.add('smart-bubble-stopped');
elem.removeEventListener('animationend', this);
// use getComputedStyle to force flushing re-style
window.getComputedStyle(elem).animationName;
elem.classList.remove('smart-bubble-stopped');
}.bind(this));
this._elements = null;
this.playing = false;
this.fireEvent('all-items-stopped');
};
// Register and return the constructor
return document.registerElement('smart-bubbles', { prototype: proto });
})(window);

View file

@ -0,0 +1,26 @@
*[smart-bubbles="true"] .smart-bubble-playing {
transition-property: none;
transform: none;
animation: bubble-animation 0.5s cubic-bezier(0.25, 0, 0, 1) both;
}
*[smart-bubbles="true"][smart-bubbles-direction="down"] .smart-bubble-playing {
animation: sinking-animation 0.5s cubic-bezier(0.25, 0, 0, 1) both;
}
*[smart-bubbles="true"] .smart-bubble-stopped {
transition-property: none;
transform: none;
animation: none;
}
@keyframes bubble-animation {
0% {opacity: 0; transform: translateY(8rem);}
50% {opacity: 1;}
100% {transform: translateY(0);}
}
@keyframes sinking-animation {
0% {opacity: 1; transform: translateY(0);}
100% {opacity: 0; transform: translateY(8rem);}
}

View file

@ -0,0 +1,26 @@
*[smart-bubbles="true"] .smart-bubble-playing {
transition-property: none;
transform: none;
animation: bubble-animation 0.5s cubic-bezier(0.25, 0, 0, 1) both;
}
*[smart-bubbles="true"][smart-bubbles-direction="down"] .smart-bubble-playing {
animation: sinking-animation 0.5s cubic-bezier(0.25, 0, 0, 1) both;
}
*[smart-bubbles="true"] .smart-bubble-stopped {
transition-property: none;
transform: none;
animation: none;
}
@keyframes bubble-animation {
0% {opacity: 0; transform: translateY(8rem);}
50% {opacity: 1;}
100% {transform: translateY(0);}
}
@keyframes sinking-animation {
0% {opacity: 1; transform: translateY(0);}
100% {opacity: 0; transform: translateY(8rem);}
}

View file

@ -0,0 +1,26 @@
{
"name": "smart-button",
"version": "0.0.2",
"homepage": "https://github.com/gaia-components/smart-button",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
],
"_release": "0.0.2",
"_resolution": {
"type": "version",
"tag": "v0.0.2",
"commit": "04fdae935e2f2ba99d1c4f3e156d17f12eb2d142"
},
"_source": "git://github.com/gaia-components/smart-button.git",
"_target": "~0.0.2",
"_originalSource": "gaia-components/smart-button"
}

View file

@ -0,0 +1,17 @@
{
"name": "smart-button",
"version": "0.0.2",
"homepage": "https://github.com/gaia-components/smart-button",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
]
}

View file

@ -0,0 +1,62 @@
'use strict';
/* global KeyEvent */
window.SmartButton = (function(win) {
// Extend from the HTMLButtonElement prototype
var proto = Object.create(HTMLButtonElement.prototype);
proto.createdCallback = function() {
this.addEventListener('mousedown', this);
this.addEventListener('mouseup', this);
this.addEventListener('touchstart', this);
this.addEventListener('touchend', this);
this.addEventListener('keydown', this);
this.addEventListener('keyup', this);
this.addEventListener('focus', this);
this.addEventListener('blur', this);
this.addEventListener('transitionend', this);
this.tabIndex = 0;
};
proto.handleEvent = function(evt) {
switch(evt.type) {
case 'mousedown':
case 'touchstart':
this.classList.add('pressed');
break;
case 'keydown':
if (evt.keyCode === KeyEvent.DOM_VK_RETURN) {
this.classList.add('pressed');
}
break;
case 'mouseup':
case 'touchend':
this.classList.remove('pressed');
this.classList.add('released');
break;
case 'keyup':
if (evt.keyCode === KeyEvent.DOM_VK_RETURN) {
this.classList.remove('pressed');
this.classList.add('released');
this.click();
}
break;
case 'transitionend':
if (this.classList.contains('released')) {
this.classList.remove('released');
}
break;
case 'focus':
this.classList.add('focused');
break;
case 'blur':
this.classList.remove('pressed');
this.classList.remove('released');
this.classList.remove('focused');
break;
}
};
// Register and return the constructor
return document.registerElement('smart-button', { prototype: proto });
})(window);

View file

@ -0,0 +1,497 @@
/**
* Button state:
* 1. normal
* In this state, we don't add any class.
* 2. focused (class name: focused)
* The focused state may be triggered by keyboard or touch/mouse.
* 3. pressed (class name: pressed)
* This state is triggered by user pressed confirm button, mousedown,
* and touchdown.
* 4. released (class name: released)
* This state is only for changing duration of transition. We enter this
* state when user release the button. After transitionend, we goes back
* to focused state.
**/
smart-button {
font-family: "Fira Sans";
display: inline-block;
width: 8rem;
height: 8rem;
/* we need to set border to none to remove the button border */
border: none;
/* the radius is still make button as round even if we don't have border */
border-radius: 50%;
/* 50% is not a real circle because of rounding, we have to use a value larger
than 50% */
-moz-outline-radius: 60%;
outline: transparent 0 solid;
background-color: rgba(0, 0, 0, 0.5);
background-repeat: no-repeat;
background-size: 5.4rem auto;
background-position: center center;
font-style: italic;
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.42s;
}
smart-button.focused {
outline: 0;
background-color: #ffffff;
transform: scale(1.2);
transition-duration: 0.42s;
}
smart-button.released {
transition-duration: 0.16s;
}
smart-button.toggled {
background-color: #00caf2;
}
smart-button.pressed {
background-color: #00caf2;
transform: scale(0.8);
transition-duration: 0.06s;
}
smart-button.disabled {
background-color: rgba(0, 0, 0, 0.5);
}
smart-button[data-icon]:before {
width: 5.4rem;
left: 1.3rem;
font-size: 5.4rem;
line-height: 8rem;
text-align: center;
color: #ffffff;
white-space: normal;
top: 0;
position: absolute;
}
smart-button[data-icon].focused:before {
color: #2d2d2d;
}
smart-button[data-icon].toggled:before {
color: #00caf2;
}
smart-button[data-icon].pressed:before {
color: #ffffff;
}
smart-button[data-icon].disabled:before {
color: #ffffff;
opacity: 0.3;
}
/**
* Simple Text Button
**/
smart-button[type="simple-text"] {
color: #dddddd;
background: none;
border: none;
border-radius: 0;
width: auto;
height: 8rem;
line-height: 8rem;
font-size: 2.4rem;
-moz-user-select: none;
padding-left: 2.6rem;
padding-right: 2.6rem;
}
smart-button[type="simple-text"].focused {
color: #ffffff;
background: none;
}
smart-button[type="simple-text"].toggled {
color: #00caf2;
}
smart-button[type="simple-text"].pressed {
color: #00caf2;
background: none;
}
/**
* End of Simple Text Button
**/
/**
* underlined text button
*/
smart-button[type="underlined-text"] {
position: relative;
color: white;
font-size: 2.4rem;
padding-left: 3.8rem;
padding-right: 3.8rem;
border-radius: 4rem;
width: auto;
line-height: 8rem;
margin-right: 1.4rem;
overflow: hidden;
}
smart-button[type="underlined-text"]:after {
position: absolute;
width: 100%;
height: 100%;
left: 0;
content: ' ';
border-bottom: 0.6rem #61acd7 solid;
box-sizing: border-box;
}
smart-button[type="underlined-text"].focused {
transform: scale(1.2);
color: #2d2d2d;
}
smart-button[type="underlined-text"].pressed {
transform: scale(1);
color: white;
background-color: #00caf2;
}
/**
* End of underlined text button
*/
/**
* App Icons
**/
smart-button[type="app-button"] {
/* To use 33.4rem instead of to use 33.6rem is to work around the anti-alias
pixel. So, the circle is filled by the background almost totally. */
width: 33.4rem;
height: 33.4rem;
position: relative;
overflow: hidden;
background-color: transparent;
}
smart-button[type="app-button"].focused {
transform: scale(1.4);
/* To use outline will not change the size of button and make the whole button
thrilling. */
outline: 0.8rem solid #ffffff;
}
smart-button[type="app-button"].pressed {
/* While scaling, we may have a gap between outline and icon which may be
anti-alias pixel. So, we use scale(1.22) to prevent such kind of case */
transform: scale(1.22);
/* To use outline will not change the size of button and make the whole button
thrilling. */
outline: 1.0rem solid #ffffff;
}
smart-button[type="app-button"].fullsized {
background-size: 33.6rem auto;
}
smart-button[type="app-button"].fitted {
background-size: 22rem auto;
}
/** we use :after as label */
smart-button[type="app-button"]:after {
content: attr(label);
position: absolute;
bottom: 0;
left: auto;
right: auto;
width: 33.6rem;
height: 8rem;
padding-top: 1rem;
line-height: 3rem;
text-align: center;
font-size: 2.8rem;
font-style: italic;
color: #ffffff;
}
/**
* text backgorund color
**/
smart-button[type="app-button"][app-type="deck"]:after {
background-color: transparent;
}
smart-button[type="app-button"][app-type="tv"]:after {
background-color: #d90c83;
}
smart-button[type="app-button"][app-type="app"]:after {
background-color: rgba(0, 0, 0, 0.4);
}
smart-button[type="app-button"][app-type="device"]:after {
background-color: #4a82ea;
}
smart-button[type="app-button"][app-type="folder"] {
background-color: #595959;
}
smart-button[type="app-button"]:before {
left: 0;
line-height: 30rem;
font-size: 16rem;
width: 100%;
}
smart-button[type="app-button"][app-type="folder"]:before {
color: #CCCCCC;
}
/**
* End of App Icons
**/
/**
* text and icon button
*/
smart-button[type="text-and-icon"] {
position: relative;
color: white;
font-size: 2.4rem;
padding-left: 4.6rem;
padding-right: 4.6rem;
border-radius: 4rem;
width: auto;
line-height: 8rem;
margin-right: 1.4rem;
overflow: hidden;
}
smart-button[type="text-and-icon"][data-icon] {
padding-left: 9.6rem;
}
smart-button[type="text-and-icon"][data-icon]:before {
left: 3.8rem;
}
smart-button[type="text-and-icon"].focused {
transform: scale(1.2);
color: #2d2d2d;
}
smart-button[type="text-and-icon"].pressed {
transform: scale(1);
color: white;
background-color: #00caf2;
}
/**
* End of text and icon button
*/
/**
* banner button
**/
smart-button[type="banner-button"] {
position: relative;
color: #ffffff;
font-size: 2rem;
width: 36rem;
height: 20rem;
overflow: hidden;
border-radius: 0;
border: 0 solid #ffffff;
background-color: #5f5f5f;
background-repeat: no-repeat;
background-size: 120%;
box-sizing: border-box;
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1.0);
transition-duration: 0.1s;
}
smart-button[type="banner-button"][label]:before {
content: attr(label);
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 5rem;
line-height: 5rem;
text-align: center;
background-color: rgba(0, 0, 0, 0.4);
}
smart-button[type="banner-button"]:after {
content: " ";
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.06s;
background-color: rgba(255, 255, 255, 0);
}
smart-button[type="banner-button"].focused {
transform: none;
border-width: 1.2rem;
background-size: 100%;
transition-duration: 0.24s;
}
smart-button[type="banner-button"].pressed {
transform: scale(0.945, 0.9);
}
smart-button[type="banner-button"].pressed:after {
background-color: rgba(255, 255, 255, 0.5);
}
/**
* banner button
**/
/**
* circle text button
*/
smart-button[type="circle-text"] {
width: 21rem;
height: 21rem;
color: white;
background-color: #5f6368;
line-height: 21rem;
padding: 2.5rem 2rem 1.5rem 2rem;
text-align: center;
font-size: 3.2rem;
font-weight: bolder;
overflow: hidden;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].focused {
transform: scale(1.2);
color: #2d2d2d;
background-color: white;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].pressed {
transform: scale(1);
color: white;
background-color: #00caf2;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].primary {
background-color: #00caf2;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].primary.focused {
color: #ffffff;
background-color: #00caf2;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].primary.pressed {
background-color: #0097b5;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].danger {
background-color: #e63d2f;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].danger.focused {
color: #ffffff;
background-color: #e63d2f;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].danger.pressed {
background-color: #ac2e23;
transition-property: transform, color, background-color;
}
/**
* End of circle text button
*/
/**
* rounded-corner text button
*/
smart-button[type="rounded-corner-text"] {
-moz-user-select: none;
width: auto;
height: 8rem;
border-radius: 4rem;
background-color: #5f6368;
padding: 0 3.8rem;
line-height: 8rem;
color: white;
text-align: center;
font-size: 2.4rem;
overflow: hidden;
transition-property: transform, color, background-color;
}
smart-button[type="rounded-corner-text"].focused,
smart-button[type="rounded-corner-text"]:hover {
transform: scale(1.2);
color: #2d2d2d;
background-color: white;
}
smart-button[type="rounded-corner-text"].pressed {
transform: scale(1);
/*color: white;*/
background-color: #00caf2;
}
smart-button[type="rounded-corner-text"].primary {
background-color: #00caf2;
}
smart-button[type="rounded-corner-text"].primary:hover,
smart-button[type="rounded-corner-text"].primary.focused {
color: #ffffff;
background-color: #00caf2;
}
smart-button[type="rounded-corner-text"].primary.pressed {
background-color: #0097b5;
}
/**
* End of rounded-corner text button
*/

View file

@ -0,0 +1,26 @@
{
"name": "smart-dialog",
"homepage": "https://github.com/smart-components/smart-dialog",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
],
"_release": "8910d99b8f",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "8910d99b8fb7c624b32ac494fea63c0898e886df"
},
"_source": "git://github.com/smart-components/smart-dialog.git",
"_target": "*",
"_originalSource": "smart-components/smart-dialog",
"_direct": true
}

View file

@ -0,0 +1,17 @@
{
"name": "smart-dialog",
"version": "0.0.0",
"homepage": "https://github.com/smart-components/smart-dialog",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
]
}

View file

@ -0,0 +1,123 @@
/**
* Smart-Dialog
* - Event:
* 1. will-open
* 2. opened
* 3. will-close
* 4. closed
* - API:
* 1. open - open a smart-dialog: closed -> opening -> opened
* 2. close - close a smart-dialog: opened -> closing -> closed
* - Attribute:
* 1. title - title of the smart-dialog
* 2. esc-close - close dialog when ESC key is clicked (default true).
* Default scale: 1920x1080 (2*sqrt(960*960 + 1080*1080) / 20 = 144.5)
*/
window.SmartDialog = (function(win) {
'use strict';
// The value of openDuration and closeDuration should be the same as
// transitionDuration in opening and closing css class.
const openDuration = 1000;
const closeDuration = 650;
// Extend from the HTMLElement prototype
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function sd_createdCallback() {
this.addEventListener('transitionend', this);
document.addEventListener('keyup', this);
this.transitionTimer = null;
this.classList.add('closed');
if (this.hasAttribute('background-color')) {
this.style.backgroundColor = this.getAttribute('background-color');
}
};
proto._onOpened = function sd_onOpened() {
this.classList.remove('opening');
this.classList.add('opened');
this.fireEvent('opened');
};
proto._onClosed = function sd_onClosed() {
this.classList.remove('closing');
this.classList.add('closed');
this.fireEvent('closed');
};
proto.open = function sd_open() {
if (this.classList.contains('opened') ||
this.classList.contains('opening')) {
return;
}
this.fireEvent('will-open');
this.classList.remove('closing');
this.classList.remove('closed');
this.classList.add('opening');
// handle the case when transitionend event is not fired
clearTimeout(this.transitionTimer);
this.transitionTimer = setTimeout(function() {
this._onOpened();
this.transitionTimer = null;
}.bind(this), openDuration);
};
proto.close = function sd_close() {
if (this.classList.contains('closed') ||
this.classList.contains('closing')) {
return;
}
this.fireEvent('will-close');
this.classList.remove('opening');
this.classList.remove('opened');
this.classList.add('closing');
// handle the case when transitionend event is not fired
clearTimeout(this.transitionTimer);
this.transitionTimer = setTimeout(function() {
this._onClosed();
this.transitionTimer = null;
}.bind(this), closeDuration);
};
proto.handleEvent = function sd_handleEvent(evt) {
switch(evt.type) {
case 'transitionend':
if (evt.target === this && this.transitionTimer) {
if (this.classList.contains('opening')) {
this._onOpened();
} else if (this.classList.contains('closing')) {
this._onClosed();
}
clearTimeout(this.transitionTimer);
this.transitionTimer = null;
}
break;
case 'keyup':
// close dialog when Back is triggered
if (this.getAttribute('esc-close') != 'false' &&
(evt.keyCode === window.KeyEvent.DOM_VK_BACK_SPACE ||
evt.key === 'Backspace') &&
this.classList.contains('opened')) {
evt.preventDefault();
evt.stopPropagation();
this.close();
}
break;
}
};
proto.fireEvent = function sd_fireEvent(event, detail) {
var evtObject = new CustomEvent(event, {
bubbles: false,
detail: detail || this
});
this.dispatchEvent(evtObject);
};
// Register and return the constructor
return document.registerElement('smart-dialog', { prototype: proto });
})(window);

View file

@ -0,0 +1,103 @@
/**
* Smart-Dialog
* - State:
* 1. closed
* 2. opening
* 3. opened - content visible
* 4. closing
* Default scale: 1920x1080 (2*sqrt(960*960 + 1080*1080) / 20 = 144.5)
*/
smart-dialog {
position: fixed;
z-index: 2147483647;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
smart-dialog:before {
content: attr(title);
display: block;
margin-top: 5rem;
margin-left: 7.6rem;
font-family: "Fira Sans";
font-size: 5.4rem;
font-style: italic;
color: #ffffff;
}
smart-dialog:after {
content: "";
display: block;
position: fixed;
height: 2rem;
width: 2rem;
top: calc(100% - 1rem);
left: calc(50% - 1rem);
background-color: rgba(0, 0, 0, 0.9);
transition: none;
transform: scale(20);
}
smart-dialog.closed > *,
smart-dialog.closing > *,
smart-dialog.opening > *,
smart-dialog.closed:before,
smart-dialog.closing:before,
smart-dialog.opening:before {
display: none;
}
smart-dialog.closed {
visibility: hidden;
}
smart-dialog.opened {
background-color: rgba(0, 0, 0, 0.9);
}
smart-dialog.opened:after {
visibility: hidden;
border-radius: 50%;
transform: scale(144.5);
}
smart-dialog.opening:after {
border-radius: 50%;
transform: scale(144.5);
/* transitionDuration should be the same as openDuration variable in script */
transition: transform 1s cubic-bezier(0, 0, 0 ,1);
}
smart-dialog.closing:after {
border-radius: 50%;
transform: scale(20);
/* transitionDuration should be the same as closeDuration variable in script */
transition: transform 0.65s cubic-bezier(0.6, 0, 1 ,1);
}
/* 1280x800 for flatfish */
@media (width: 1280px) and (height: 800px) {
smart-dialog:after,
smart-dialog.closing:after {
transform: scale(7.5);
}
smart-dialog.opening:after {
transform: scale(102.5);
}
}
/* 800x1280 for flatfish vertical */
@media (width: 800px) and (height: 1280px) {
smart-dialog:after,
smart-dialog.closing:after {
transform: scale(8.3);
}
smart-dialog.opening:after {
transform: scale(134);
}
}

View file

@ -0,0 +1,30 @@
{
"name": "smart-icons",
"version": "0.0.2",
"homepage": "https://github.com/smart-components/smart-icons",
"description": "Icon font for smart-screen",
"license": "MPL",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"templates",
"images",
"test",
"tests",
"index.html",
"Gruntfile.js",
"README.md",
"package.json"
],
"_release": "0.0.2",
"_resolution": {
"type": "version",
"tag": "v0.0.2",
"commit": "f3c42aa51a2765fbae61630bc7a3d39d8ead4737"
},
"_source": "git://github.com/smart-components/smart-icons.git",
"_target": "~0.0.2",
"_originalSource": "smart-components/smart-icons"
}

View file

@ -0,0 +1,21 @@
{
"name": "smart-icons",
"version": "0.0.2",
"homepage": "https://github.com/smart-components/smart-icons",
"description": "Icon font for smart-screen",
"license": "MPL",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"templates",
"images",
"test",
"tests",
"index.html",
"Gruntfile.js",
"README.md",
"package.json"
]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,25 @@
/* Generated by grunt-webfont */
/* Based on https://github.com/endtwist/fontcustom/blob/master/lib/fontcustom/templates/fontcustom.css */
@font-face {
font-family: "smart-icons";
src: url("fonts/smart-icons.ttf") format("truetype");
font-weight: 500;
font-style: normal;
}
[data-icon]:before,
.ligature-icons {
font-family: "smart-icons";
content: attr(data-icon);
display: inline-block;
font-weight: 500;
font-style: normal;
text-decoration: inherit;
text-transform: none;
text-rendering: optimizeLegibility;
font-size: 30px;
-webkit-font-smoothing: antialiased;
}

View file

@ -0,0 +1,31 @@
{
"name": "smart-modal-dialog",
"homepage": "https://github.com/smart-components/smart-modal-dialog",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
],
"dependencies": {
"smart-dialog": "smart-components/smart-dialog",
"smart-bubbles": "smart-components/smart-bubbles",
"smart-button": "smart-components/smart-button"
},
"_release": "1e5b8b14db",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "1e5b8b14db99c0d5ae6cefd0b8b5ac9a81942321"
},
"_source": "git://github.com/smart-components/smart-modal-dialog.git",
"_target": "*",
"_originalSource": "smart-components/smart-modal-dialog",
"_direct": true
}

View file

@ -0,0 +1,362 @@
Mozilla Public License, version 2.0
1. Definitions
1.1. "Contributor"
means each individual or legal entity that creates, contributes to the
creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used by a
Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached the
notice in Exhibit A, the Executable Form of such Source Code Form, and
Modifications of such Source Code Form, in each case including portions
thereof.
1.5. "Incompatible With Secondary Licenses"
means
a. that the initial Contributor has attached the notice described in
Exhibit B to the Covered Software; or
b. that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the terms of
a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in a
separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible, whether
at the time of the initial grant or subsequently, any and all of the
rights conveyed by this License.
1.10. "Modifications"
means any of the following:
a. any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered Software; or
b. any new file in Source Code Form that contains any Covered Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the License,
by the making, using, selling, offering for sale, having made, import,
or transfer of either its Contributions or its Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU Lesser
General Public License, Version 2.1, the GNU Affero General Public
License, Version 3.0, or any later versions of those licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that controls, is
controlled by, or is under common control with You. For purposes of this
definition, "control" means (a) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (b) ownership of more than fifty percent (50%) of the
outstanding shares or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
a. under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
b. under Patent Claims of such Contributor to make, use, sell, offer for
sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
a. for any code that a Contributor has removed from Covered Software; or
b. for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
c. under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights to
grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
a. such Covered Software must also be made available in Source Code Form,
as described in Section 3.1, and You must inform recipients of the
Executable Form how they can obtain a copy of such Source Code Form by
reasonable means in a timely manner, at a charge no more than the cost
of distribution to the recipient; and
b. You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter the
recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty, or
limitations of liability) contained within the Source Code Form of the
Covered Software, except that You may alter any license notices to the
extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute,
judicial order, or regulation then You must: (a) comply with the terms of
this License to the maximum extent possible; and (b) describe the
limitations and the code they affect. Such description must be placed in a
text file included with all distributions of the Covered Software under
this License. Except to the extent prohibited by statute or regulation,
such description must be sufficiently detailed for a recipient of ordinary
skill to be able to understand it.
5. Termination
5.1. The rights granted under this License will terminate automatically if You
fail to comply with any of its terms. However, if You become compliant,
then the rights granted under this License from a particular Contributor
are reinstated (a) provisionally, unless and until such Contributor
explicitly and finally terminates Your grants, and (b) on an ongoing
basis, if such Contributor fails to notify You of the non-compliance by
some reasonable means prior to 60 days after You have come back into
compliance. Moreover, Your grants from a particular Contributor are
reinstated on an ongoing basis if such Contributor notifies You of the
non-compliance by some reasonable means, this is the first time You have
received notice of non-compliance with this License from such
Contributor, and You become compliant prior to 30 days after Your receipt
of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
license agreements (excluding distributors and resellers) which have been
validly granted by You or Your distributors under this License prior to
termination shall survive termination.
6. Disclaimer of Warranty
Covered Software is provided under this License on an "as is" basis,
without warranty of any kind, either expressed, implied, or statutory,
including, without limitation, warranties that the Covered Software is free
of defects, merchantable, fit for a particular purpose or non-infringing.
The entire risk as to the quality and performance of the Covered Software
is with You. Should any Covered Software prove defective in any respect,
You (not any Contributor) assume the cost of any necessary servicing,
repair, or correction. This disclaimer of warranty constitutes an essential
part of this License. No use of any Covered Software is authorized under
this License except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort (including
negligence), contract, or otherwise, shall any Contributor, or anyone who
distributes Covered Software as permitted above, be liable to You for any
direct, indirect, special, incidental, or consequential damages of any
character including, without limitation, damages for lost profits, loss of
goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of liability
shall not apply to liability for death or personal injury resulting from
such party's negligence to the extent applicable law prohibits such
limitation. Some jurisdictions do not allow the exclusion or limitation of
incidental or consequential damages, so this exclusion and limitation may
not apply to You.
8. Litigation
Any litigation relating to this License may be brought only in the courts
of a jurisdiction where the defendant maintains its principal place of
business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions. Nothing
in this Section shall prevent a party's ability to bring cross-claims or
counter-claims.
9. Miscellaneous
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides that
the language of a contract shall be construed against the drafter shall not
be used to construe this License against a Contributor.
10. Versions of the License
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses If You choose to distribute Source Code Form that is
Incompatible With Secondary Licenses under the terms of this version of
the License, the notice described in Exhibit B of this License must be
attached.
Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular file,
then You may include the notice in a location (such as a LICENSE file in a
relevant directory) where a recipient would be likely to look for such a
notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible
With Secondary Licenses", as defined by
the Mozilla Public License, v. 2.0.

View file

@ -0,0 +1,22 @@
{
"name": "smart-modal-dialog",
"version": "0.0.0",
"homepage": "https://github.com/smart-components/smart-modal-dialog",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
],
"dependencies": {
"smart-dialog": "smart-components/smart-dialog",
"smart-bubbles": "smart-components/smart-bubbles",
"smart-button": "smart-components/smart-button"
}
}

View file

@ -0,0 +1,194 @@
/**
* SmartModalDialog creates a dialog with a group of buttons as select
* options. It also includes open/close animation and bubbling animation.
* The template is shown below:
* <smart-dialog class="modal-dialog">
* <div class="outer-container">
* <div class="container">
* <div class="modal-dialog-message">
* </div>
* <div class="modal-dialog-button-group">
* <smart-button></smart-button>
* ...
* <smart-button></smart-button>
* </div>
* </div>
* </div>
* </smart-dialog>
*/
(function(exports) {
'use strict';
function SmartModalDialog(container) {
// Determine whether this dialog is opened or not
this.isOpened = false;
this._clickedIndex = null;
this.smartBubble = document.createElement('smart-bubbles');
this.element = document.createElement('smart-dialog');
this.element.classList.add('modal-dialog');
// make dialog focusable in order to catch focus from mouse click or touch
// event.
this.element.setAttribute('tabIndex', '-1');
// in order to make vertical align, we need additional container
this.outerContainer = document.createElement('div');
this.outerContainer.classList.add('outer-container');
this.container = document.createElement('div');
this.container.classList.add('container');
this.messageElement = document.createElement('div');
this.messageElement.classList.add('modal-dialog-message');
this.buttonGroup = document.createElement('div');
this.buttonGroup.classList.add('modal-dialog-button-group');
this.buttonGroup.setAttribute('smart-bubbles', 'true');
this.container.appendChild(this.messageElement);
this.container.appendChild(this.buttonGroup);
this.outerContainer.appendChild(this.container);
this.element.appendChild(this.outerContainer);
this.container = container || document.body;
this.container.appendChild(this.element);
this.element.addEventListener('opened', this);
this.element.addEventListener('closed', this);
this.smartBubble.addEventListener('all-items-bubbled', this);
}
var proto = Object.create(SmartDialog);
proto.open = function(options) {
this._focusedIndex = -1;
this.isOpened = true;
this.message = options.message || {};
// onCancel is triggered when ESC key is pressed.
this.onCancel = options.onCancel || function() {};
this.buttonElements = [];
this.buttonSettings = options.buttonSettings || [];
this.messageElement.textContent = this.message.textRaw || '';
this.messageElement.setAttribute('data-l10n-id', this.message.textL10nId);
this.buttonGroup.innerHTML = '';
this.messageElement.classList.remove('hidden');
if (!this.message.textRaw && !this.message.textL10nId) {
this.messageElement.classList.add('hidden');
}
// Set up every button
this.buttonSettings.forEach(function(buttonSetting, index) {
var button = document.createElement('smart-button');
button.setAttribute('type', 'circle-text');
button.setAttribute('data-l10n-id', buttonSetting.textL10nId);
button.textContent = buttonSetting.textRaw || 'OK';
button.classList.add(buttonSetting.class || 'confirm');
button.addEventListener('click', function() {
// Click action will be handled in closed event
this._clickedIndex = index;
this.element.close();
}.bind(this));
this.buttonElements.push(button);
this.buttonGroup.appendChild(button);
this._focusedIndex =
(buttonSetting.defaultFocus) ? index : this._focusedIndex;
}.bind(this));
this.element.classList.add('visible');
this.element.open();
};
proto.remove = function() {
this.container.removeChild(this.element);
};
proto.startKeyNavigation = function() {
this.element.addEventListener('keydown', this);
if (this.buttonElements.length > 0) {
if (this._focusedIndex === -1) {
this._focusedIndex = 0;
}
this.focus();
}
};
proto.focus = function() {
var elem = this.buttonElements[this._focusedIndex];
if (elem.focus && (typeof elem.focus) === 'function') {
elem.focus();
}
};
proto.blur = function() {
var elem = this.buttonElements[this._focusedIndex];
if (elem.blur && (typeof elem.blur === 'function')) {
elem.blur();
}
};
proto.stopKeyNavigation = function() {
this.element.removeEventListener('keydown', this);
};
proto.movePrevious = function() {
if (this._focusedIndex < 1) {
return;
}
this._focusedIndex--;
this.focus();
};
proto.moveNext = function() {
if (this._focusedIndex > this.buttonElements.length - 2) {
return;
}
this._focusedIndex++;
this.focus();
};
proto.handleEvent = function(e) {
if (e.keyCode === KeyEvent.DOM_VK_LEFT) {
this.movePrevious();
} else if (e.keyCode === KeyEvent.DOM_VK_RIGHT) {
this.moveNext();
}
switch(e.target) {
case this.element:
if (e.type === 'opened') {
// Play bubble animation when the smart-dialog is opened
this.smartBubble.play(this.buttonElements);
} else if (e.type === 'closed') {
// Do click actions when the smart-dialog is closed
this.element.classList.remove('visible');
if ((this._clickedIndex || this._clickedIndex === 0) &&
this.buttonSettings[this._clickedIndex].onClick) {
this.buttonSettings[this._clickedIndex].onClick();
this._clickedIndex = null;
} else if (this.onCancel) {
// ESC key enter
this.onCancel();
}
this.isOpened = false;
this.stopKeyNavigation();
}
break;
case this.smartBubble:
this.startKeyNavigation();
this.focus();
break;
}
};
SmartModalDialog.prototype = proto;
exports.SmartModalDialog = SmartModalDialog;
})(window);

View file

@ -0,0 +1,47 @@
.modal-dialog > .outer-container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
}
.modal-dialog > .outer-container:before {
content: "";
display: inline-block;
height: 100%;
width: 0.1rem;
margin-left: -0.1rem;
vertical-align: middle;
}
.modal-dialog .container {
width: 100%;
vertical-align: middle;
display: inline-block;
}
.modal-dialog .modal-dialog-message {
font-size: 3.8rem;
font-style: italic;
color: #ffffff;
text-align: center;
font-family: Fira Sans;
font-weight: 100;
margin-bottom: 13rem;
}
.modal-dialog .modal-dialog-message.hidden {
display: none;
}
.modal-dialog .modal-dialog-button-group {
text-align: center;
}
.modal-dialog .modal-dialog-button-group > * {
margin-right: 4.4rem;
}
.modal-dialog .modal-dialog-button-group > *:last-child {
margin-right: 0;
}

View file

@ -0,0 +1,199 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<title>Home</title>
<!-- building blocks -->
<link rel="stylesheet" type="text/css" href="bower_components/smart-icons/smart-icons.css">
<link rel="stylesheet" type="text/css" href="bower_components/smart-button/style.css">
<link rel="stylesheet" type="text/css" href="bower_components/smart-dialog/style.css">
<link rel="stylesheet" type="text/css" href="bower_components/smart-bubbles/style.css">
<link rel="stylesheet" type="text/css" href="bower_components/smart-modal-dialog/style.css">
<link rel="stylesheet" type="text/css" href="bower_components/menu-group/style.css">
<link rel="stylesheet" type="text/css" href="shared/style/smart-screen/animations.css">
<!-- home -->
<link rel="stylesheet" type="text/css" href="style/header.css">
<link rel="stylesheet" type="text/css" href="style/home.css">
<link rel="stylesheet" type="text/css" href="style/deck.css">
<link rel="stylesheet" type="text/css" href="style/edit.css">
<link rel="stylesheet" type="text/css" href="shared/style/smart-screen/navigation.css">
<link rel="stylesheet" type="text/css" href="style/info-bar.css">
<link rel="stylesheet" type="text/css" href="style/search-bar.css">
<link rel="stylesheet" type="text/css" href="style/fte.css">
<!-- default cards for first time launch -->
<link rel="resource" type="application/json" href="shared/resources/default-cards.json">
<!-- For test purpose on phone only -->
<script defer src="shared/js/gesture_detector.js"></script>
<script defer src="shared/js/smart-screen/init_gesture.js"></script>
<!-- Shared libraries -->
<script defer src="shared/js/async_semaphore.js"></script>
<script defer src="shared/js/uuid.js"></script>
<script defer src="shared/js/iac_handler.js"></script>
<!-- Helper libraries -->
<script defer src="bower_components/evt/index.js"></script>
<script defer src="shared/js/smart-screen/shared_utils.js"></script>
<script defer src="shared/js/smart-screen/clock.js"></script>
<script defer src="shared/js/smart-screen/piped_promise.js"></script>
<script defer src="shared/js/smart-screen/card_store.js"></script>
<script defer src="shared/js/smart-screen/cards/card.js"></script>
<script defer src="shared/js/smart-screen/cards/application.js"></script>
<script defer src="shared/js/smart-screen/cards/deck.js"></script>
<script defer src="shared/js/smart-screen/cards/folder.js"></script>
<script defer src="shared/js/smart-screen/cards/app_bookmark.js"></script>
<script defer src="shared/js/smart-screen/card_manager.js"></script>
<script defer src="shared/js/smart-screen/animations.js"></script>
<script defer src="shared/js/smart-screen/simple_key_navigation.js"></script>
<script defer src="shared/js/smart-screen/f_t_e_wizard.js"></script>
<!-- building blocks -->
<script defer src="bower_components/smart-button/script.js"></script>
<script defer src="bower_components/smart-dialog/script.js"></script>
<script defer src="bower_components/smart-bubbles/script.js"></script>
<script defer src="bower_components/smart-modal-dialog/script.js"></script>
<!-- menu group depends on evt, it should be added after evt.js -->
<script defer src="bower_components/menu-group/script.js"></script>
<!-- Shared TV library for keyboard-based navigating -->
<script defer src="shared/js/smart-screen/scrollable.js"></script>
<script defer src="shared/js/smart-screen/spatial_navigator.js"></script>
<script defer src="shared/js/smart-screen/key_navigation_adapter.js"></script>
<script defer src="shared/js/smart-screen/connection_manager.js"></script>
<!-- Specific code -->
<script defer src="js/utils.js"></script>
<script defer src="js/card_filter.js"></script>
<script defer src="js/filter_manager.js"></script>
<script defer src="js/message_handler.js"></script>
<script defer src="js/edit.js"></script>
<script defer src="js/search_bar.js"></script>
<script defer src="js/home.js"></script>
<script defer src="js/bootstrap.js"></script>
<!-- Shared L10n libraries -->
<script src="shared/js/l10n.js"></script>
<script src="shared/js/l10n_date.js"></script>
<!-- L10n properties -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/home.{locale}.properties"/>
<link rel="localization" href="shared/locales/date/date.{locale}.properties"/>
</head>
<body>
<article tabIndex="-1" role="region" class="skin-dark" id="main-section">
<section id="background" class="background"></section>
<nav id="nav-regular">
<section id="header-buttons" class="left-top-panel">
<smart-button data-icon="search" id="search-button" class="search-button"></smart-button>
<smart-button type="text-and-icon" id="edit-button" class="edit-button">
<span data-l10n-id="edit">edit</span>
</smart-button>
<smart-button type="text-and-icon" id="settings-button" class="settings-button">
<span data-l10n-id="settings">settings</span>
</smart-button>
</section>
<section id="information-bar" class="information-bar">
<div id="clock-info" class="clock-info"><span id="time" class="time"></span></div>
<div id="weather-info" class="weather-info">
<!-- use span here because we will change to icon font -->
<span id="weather-image" class="weather-image"></span>
<span id="temperature" class="temperature">
<!-- do not add \n here because it adds a space between temperature-number, degree-marker and degree-system -->
<label class="temperature-number">72</label><label class="degree-marker">&#xb0;</label><label class="degree-system">F</label>
</span>
<span id="weather-text" class="weather-text">Mostly Cloudy</span>
</div>
</section>
</nav>
<nav id="nav-edit-title">
<header data-l10n-id="edit-mode-title">
Edit mode
</header>
</nav>
<nav id="nav-edit">
<section id="edit-menu">
<smart-button type="underlined-text" id="done-button">
<span data-l10n-id="done">done</span>
</smart-button>
<smart-button type="text-and-icon" id="add-new-folder-button" data-icon="add">
<span data-l10n-id="add-new-folder">add new folder</span>
</smart-button>
</section>
</nav>
<smart-bubbles id="bubbles" interval="100"></smart-bubbles>
<section id="card-viewer">
<div id="card-list-frame">
<div id="card-list" class="hidden" data-default-item="0" smart-bubbles="true">
</div>
</div>
</section>
<section id="folder-viewer">
<div id="folder-list-frame">
<div id="folder-list" data-default-item="0">
</div>
</div>
</section>
<section id="filter-tab-container" class="filter-tab-container">
<menu-group data-icon="filter" id="filter-tab-group" class="filter-tab-group">
<smart-button type="simple-text" id="filter-all-button" data-icon-type="filter">
<span data-l10n-id="all"></span>
</smart-button>
<smart-button type="simple-text" id="filter-tv-button" data-icon-type="tv">
<span data-l10n-id="tv"></span>
</smart-button>
<smart-button type="simple-text" id="filter-app-button" data-icon-type="application">
<span data-l10n-id="app"></span>
</smart-button>
<smart-button type="simple-text" id="filter-device-button" data-icon-type="device">
<span data-l10n-id="device"></span>
</smart-button>
<smart-button type="simple-text" id="filter-website-button" data-icon-type="web">
<span data-l10n-id="website"></span>
</smart-button>
</menu-group>
</section>
<section id="search-bar" class="search-bar hidden">
<smart-button data-icon="mic" class="search-mic-button fade-out"></smart-button>
<div data-icon="search" class="search-input">
<span data-l10n-id="search-default-text">search</span>
</div>
</section>
<section id="fte">
<section class="fte-page">
<div class="fte-content-image">
<div id="fte-tvdeck-detail" data-l10n-id="fte-tvdeck-detail"></div>
<div id="fte-appdeck-detail" data-l10n-id="fte-appdeck-detail"></div>
<div id="fte-devicedeck-detail" data-l10n-id="fte-devicedeck-detail"></div>
<div class="fte-indicator-p1"></div>
</div>
<section class="fte-panel">
<smart-button data-behavior="next" type="rounded-corner-text" class="fte-button primary" data-l10n-id="next"></smart-button>
</section>
</section>
<section class="fte-page">
<div class="fte-content-image">
<div id="fte-search-detail" data-l10n-id="fte-search-detail"></div>
<div id="fte-settings-detail" data-l10n-id="fte-settings-detail"></div>
<div id="fte-addfolder-detail" data-l10n-id="fte-addfolder-detail"></div>
<div id="fte-filter-detail" data-l10n-id="fte-filter-detail"></div>
<div class="fte-indicator-p2"></div>
</div>
<section class="fte-panel">
<smart-button data-behavior="finish" type="rounded-corner-text" class="fte-button primary" data-l10n-id="ok"></smart-button>
</section>
</section>
</section>
</article>
</body>
</html>

4
gaia/tv_apps/smart-home/js/bootstrap.js vendored Executable file
View file

@ -0,0 +1,4 @@
'use strict';
/* global Home */
window.home = new Home();
window.home.init();

View file

@ -0,0 +1,82 @@
/* global evt */
(function(exports) {
'use strict';
/**
* This class controls UI and user interaction of filter, with the help from
* [MenuGroup](http://bit.ly/1JbbnZ8).
*
* @class CardFilter
* @requires {@link http://bit.ly/1JbbnZ8|MenuGroup}
* @fires CardFilter#opened
* @fires CardFilter#filterchanged
*/
function CardFilter() {}
var proto = CardFilter.prototype = new evt();
Object.defineProperty(proto, 'filter', {
get: function cf_getFilter() {
return this._selectedFilter;
},
set: function cf_setFilter(icon) {
if (this.menuGroup && this._buttons[icon]) {
if (this._buttons[this._selectedFilter]) {
this._buttons[this._selectedFilter].classList.remove('toggled');
}
this._selectedFilter = icon;
this.menuGroup.changeIcon(icon);
this._buttons[icon].classList.add('toggled');
/**
* This event fires whenever filter changes.
* @event CardFilter#filterchanged
*/
this.emit('filterchanged', icon);
}
}
});
proto.start = function cf_start(menuGroup) {
this.menuGroup = menuGroup;
this.menuGroup.addEventListener('opened', function() {
/**
* This event fires whenever [MenuGroup](http://bit.ly/1JbbnZ8) is opened.
* @event CardFilter#opened
*/
this.fire('opened');
}.bind(this));
var buttons = this.menuGroup.querySelectorAll(
'smart-button[data-icon-type]');
this._buttons = {};
for (var i = 0; i < buttons.length; i++) {
this._buttons[buttons[i].dataset.iconType] = buttons[i];
buttons[i].addEventListener('click', this);
}
};
proto.stop = function cf_stop() {
var that = this;
var buttonKeys = Object.keys(this._buttons);
buttonKeys.forEach(function(key) {
that._buttons[key].removeEventListener('click', that);
});
};
proto.handleEvent = function cf_handleEvent(evt) {
if (!evt.target.dataset.iconType) {
return;
}
this.filter = evt.target.dataset.iconType;
};
proto.hide = function cf_hide() {
this.menuGroup.classList.add('hidden');
};
proto.show = function cf_show() {
this.menuGroup.classList.remove('hidden');
};
exports.CardFilter = CardFilter;
})(window);

View file

@ -0,0 +1,551 @@
/* 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));

View file

@ -0,0 +1,310 @@
/* global CardFilter, Folder */
(function(exports) {
'use strict';
/**
* FilterManager is the controller class of filter. It relies on
* {@link CardFilter} to do UI works of filter itself. However it also needs
* to co-operate with {@link http://bit.ly/1JcrKZO|SmartBubbles},
* {@link Edit}, and {@link Home} classes to do the actual *filtering* task
* in smart-home.
*
* The working flow when user change filter is like:
*
* 1. FilterManager receives `filterchanged` from {@link CardFilter}
* 2. Play bubbling down animation, see
* {@link FilterManager#onFilterChanged|onFilterChanged()}.
* 3. When animation ends, change card icon in smart-home, with help from
* {@link Home} and XScrollable. See
* {@link FilterManager#onAnimationEnd|onAnimationEnd()}.
* 4. Play bubbling up animation, see
* {@link FilterManager#_performBubbleUp|_performBubbleUp()}.
*
* @class FilterManager
* @requires {@link Home}
* @requires XScrollable
* @requires {@link Edit}
* @requires {@link CardFilter}
* @requires {@link http://bit.ly/1JcrKZO|SmartBubbles}
*/
var FilterManager = function() {};
/**
* @namespace FilterManager.FILTERS
*/
FilterManager.FILTERS = Object.freeze({
/**
* @readonly
* @memberof FilterManager.FILTERS
* @property {String} name - value is `all`
* @property {String} iconName - value is `filter`
*/
'ALL': {
name: 'all',
iconName: 'filter'
},
/**
* @readonly
* @memberof FilterManager.FILTERS
* @property {String} name - value is `tv`
* @property {String} iconName - value is `tv`
*/
'TV': {
name: 'tv',
iconName: 'tv'
},
/**
* @readonly
* @memberof FilterManager.FILTERS
* @property {String} name - value is `device`
* @property {String} iconName - value is `device`
*/
'DEVICE': {
name: 'device',
iconName: 'device'
},
/**
* @readonly
* @memberof FilterManager.FILTERS
* @property {String} name - value is `application`
* @property {String} iconName - value is `application`
*/
'APPLICATION': {
name: 'application',
iconName: 'application'
},
/**
* @readonly
* @memberof FilterManager.FILTERS
* @property {String} name - value is `website`
* @property {String} iconName - value is `website`
*/
'WEBSITE': {
name: 'website',
iconName: 'web'
}
});
FilterManager.prototype = {
/**
* Initialize FilterManager
*
* @public
* @method FilterManager#init
* @param {Object} options
* @param {Object} options.cardListElem - `HTMLElement` of `card-list`
* @param {Object} options.cardScrollable - instance of XScrollable
* @param {Object} options.home - instance of {@link Home}
* @param {Object} options.cardManager - instance of CardManager
*/
init: function(options) {
this._cardListElem = options.cardListElem;
this._cardScrollable = options.cardScrollable;
this._home = options.home;
this._edit = this._home.edit;
this._cardManager = options.cardManager;
this._cardFilter = new CardFilter();
this._cardFilter.start(document.getElementById('filter-tab-group'));
this._cardFilter.filter = FilterManager.FILTERS.ALL.iconName;
this._filterChangedHandler = this.onFilterChanged.bind(this);
this._cardFilter.on('filterchanged', this._filterChangedHandler);
this._cardFilter.on('opened', function() {
this._home.cleanFolderScrollable(true);
}.bind(this));
this._hideFilter = this.hide.bind(this);
this._showFilter = this.show.bind(this);
this._edit.on('enter-edit-mode', this._hideFilter);
this._edit.on('exit-edit-mode', this._showFilter);
this._smartBubblesElem = document.getElementById('bubbles');
this._animationEndHandler = this.onAnimationEnd.bind(this);
this._smartBubblesElem.addEventListener('all-items-bubbled',
this._animationEndHandler);
},
/**
* Do the opposite of initalization
*
* @public
* @method FilterManager#uninit
*/
uninit: function() {
this._cardFilter.off('filterchanged', this._filterChangedHandler);
this._edit.off('enter-edit-mode', this._hideFilter);
this._edit.off('exit-edit-mode', this._showFilter);
this._smartBubblesElem.removeEventListener('all-items-bubbled',
this._animationEndHandler);
},
_filteredCardList: undefined,
_isFirstFrame: false,
_isFilterChanging: false,
_isBubbleSinking: function fm_isBubbleSinking() {
return (
this._cardListElem.getAttribute('smart-bubbles-direction') === 'down');
},
/**
* Perform bubbling up animation. We must hide the first frame of animation
* because there are sliding animation on XScrollable. We don't want user to
* see this according to UX spec.
*
* @private
* @method FilterManager#_performBubbleUp
*/
_performBubbleUp: function fm_performBubbleUp() {
if (this._isFirstFrame) {
this._isFirstFrame = false;
this._cardListElem.style.transition = 'none';
this._cardScrollable.resetScroll();
window.requestAnimationFrame(this._performBubbleUp.bind(this));
} else {
this._cardListElem.style.opacity = 1;
this._cardListElem.style.transition = '';
this._cardListElem.removeAttribute('smart-bubbles-direction');
this._smartBubblesElem.play(
document.querySelectorAll('#card-list > .card > .app-button'));
}
},
/**
* Get constant object of {@link FilterManager.FILTERS} of the `iconName`
*
* @public
* @method FilterManager#getFilterByIconName
* @param {String} iconName -
*/
getFilterByIconName: function fm_getFilterByIconName(iconName) {
var keys = Object.keys(FilterManager.FILTERS);
var filter;
keys.some(function(key) {
var cursor = FilterManager.FILTERS[key];
if (iconName === cursor.iconName) {
filter = cursor;
return true;
}
});
return filter;
},
/**
* EventHandler of `all-items-bubbled` event.
* [SmartBubbles](http://bit.ly/1JcrKZO) fires `all-items-bubbled` when it
* is done with bubbling down animation. We start to alter the card content
* of XScrollable only after bubbling down animation ends. And after we are
* done with altering content, we'll have to perform bubbling up animation
* by calling {@link FilterManager#_performBubbleUp}.
*
* @public
* @method FilterManager#onAnimationEnd
*/
onAnimationEnd: function fm_onAnimationEnd() {
if (!this._isFilterChanging) {
return;
}
var that = this;
var filter = this.getFilterByIconName(this._cardFilter.filter);
var gotCardLists = function(results) {
// results[0] is the main card list which has been filtered.
// results[1] is the main card list.
var filteredList = results[0];
var cardList = results[1];
// Get filtered card list for each folder.
cardList.filter(elem => elem instanceof Folder).forEach(elem => {
filteredList = filteredList.concat(
elem.getFilteredCardList(filter.name));
});
filteredList.forEach(function(card) {
that._cardScrollable.addNode(that._home.createCardNode(card));
});
that._filteredCardList =
(filter.name !== 'all') ? filteredList : undefined;
that._cardListElem.style.opacity = 0;
window.requestAnimationFrame(that._performBubbleUp.bind(that));
};
if (this._isBubbleSinking()) {
this._cardScrollable.clean();
Promise.all([
this._cardManager.getFilteredCardList(filter.name),
this._cardManager.getCardList()
]).then(gotCardLists);
} else {
this._isFilterChanging = false;
}
},
/**
* EventHandler of `filterchanged` event. {@link CardFilter} fires
* `filterchanged` event whenever user select a different filter. This is
* becasue we need to play bubbling down animation whenever filter is
* changed.
*
* @public
* @method FilterManager#onFilterChanged
*/
onFilterChanged: function fm_onFilterChange(iconName) {
this._isFirstFrame = true;
if (this._isFilterChanging) {
this._smartBubblesElem.stopImmediately();
}
this._isFilterChanging = true;
this._cardListElem.setAttribute('smart-bubbles-direction', 'down');
// Notice that methods like document.querySelectorAll or
// document.getElementsByClassName returns elements based on the
// sequence of time that element added to DOM structure. But we should
// play animation in the left-to-right order. Card scrollable keeps the
// order of all items, we just use them directly to play animation.
// See http://bugzil.la/1169538
this._smartBubblesElem.play(this._cardScrollable.allItems);
},
/**
* Hide filter. This should be called when we are in edit mode.
*
* @public
* @method FilterManager#hide
*/
hide: function fm_hide() {
this._cardFilter.hide();
},
/**
* Hide filter. This should be called when we are exit from edit mode.
*
* @public
* @method FilterManager#show
*/
show: function fm_show() {
this._cardFilter.show();
},
/**
* Reset filter to {@link FilterManager.FILTERS.ALL}. We should always reset
* filter when smart-home is back to foreground.
*
* @public
* @method FilterManager#resetFilter
*/
resetFilter: function fm_resetFilter() {
if (this._cardFilter.filter !== FilterManager.FILTERS.ALL.iconName) {
this._cardFilter.filter = FilterManager.FILTERS.ALL.iconName;
}
}
};
exports.FilterManager = FilterManager;
}(window));

View file

@ -0,0 +1,831 @@
'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));

View file

@ -0,0 +1,134 @@
'use strict';
/* global ConnectionManager */
(function(exports) {
/**
* MessageHandler handles all incoming messages which includes both
* [IAC](http://mzl.la/1TKR6zw) and [MozActivity](http://mzl.la/1Pu8ecc).
* This class relies on {@link ConnectionManager} to monitor incoming `unpin`
* message on [IAC](http://mzl.la/1TKR6zw).
*
* @class MessageHandler
* @requires {@link ConnectionManager}
* @requires {@link Home}
*/
var MessageHandler = function() {
this.isStop = false;
this.activityQueue = [];
};
MessageHandler.prototype = {
_connectionManager: undefined,
_home: undefined,
/**
* Initialize MessageHandler
* @public
* @method MessageHandler#init
*/
init: function cm_init(home) {
this._home = home;
navigator.mozSetMessageHandler(
'activity', this.handleActivity.bind(this));
this._connectionManager = new ConnectionManager();
this._connectionManager.init(['iac-appdeck-channel']);
this._connectionManager.on('unpin', this.unpin.bind(this));
},
/**
* Pin card of `cardEntry`
* @public
* @method MessageHandler#pin
* @param {Object} cardEntry - cardEntry representing the card
* @param {String} cardEntry.type - valid values are 'Application', 'Deck',
* 'AppBookmark', and 'Folder'
* @param {String} cardEntry.group - valud values are 'tv', 'application',
* 'device', and 'dashboard'
* @param {String} cardEntry.manifestURL - manifestURL, if the card
* represents an app or a deck
* @param {String} cardEntry.launchURL - optional
* @param {String} cardEntry.url - URL, if the card represents a bookmark
* @param {String} cardEntry.name.raw - optional display name
* @param {Blob} cardEntry.thumnail - see [blob](http://mzl.la/1BRI7IV)
*/
pin: function mh_pin(cardEntry) {
this._home.cardManager.insertCard({
cardEntry: cardEntry
});
},
/**
* Unpin card
* @public
* @method MessageHandler#unpin
* @param {Object} data
* @param {String} data.manifestURL - manifestURL if unpinning an app
* @param {String} data.launchURL - optional launchURL if unpinning an
* app
* @param {String} data.url - link URL when unpinning an bookmark
*/
unpin: function mh_unpin(data) {
var home = this._home;
// XXX: this is extra step, we should remove this step once we were
// done fixing and refactoring cardManager.removeCard.
// The reason we need this step here is because cardManager.removeCard
// only accept card instance as sole parameter.
var card = home.cardManager.findCardFromCardList(data);
home.cardManager.removeCard(card);
},
handleActivity: function mh_handleActivity(activity) {
if(this.isStop) {
this.activityQueue.push(activity);
return;
}
this._digestActivity(activity);
},
/**
* Stop from processing activity in activityQueue. The reason we need this
* is that we don't need to do pinning when smart-home is not visible.
*
* @public
* @method MessageHandler#stopActivity
*/
stopActivity: function mh_stopActivity() {
this.isStop = true;
},
_digestActivity: function(activity) {
var name = activity.source.name;
switch(name) {
case 'pin':
this.pin(activity.source.data);
break;
}
},
/**
* Resume processing activity in activityQueue. We could call this method to
* continue processing pinning when smart-home is visible.
*
* @public
* @method MessageHandler#resumeActivity
*/
resumeActivity: function mh_resumeActivity() {
this.isStop = false;
if (this.activityQueue.length === 0) {
return false;
}
var activity;
while(this.activityQueue.length > 0) {
activity = this.activityQueue.pop();
this._digestActivity(activity);
}
return true;
}
};
exports.MessageHandler = MessageHandler;
})(window);

View file

@ -0,0 +1,69 @@
/* global evt */
(function(exports) {
'use strict';
function SearchBar() {}
SearchBar.STATES = Object.freeze({
'HIDDEN': 'hidden',
'SHOWING': 'showing',
'SHOWN': 'shown'
});
var proto = SearchBar.prototype = new evt();
Object.defineProperty(proto, 'state', {
get: function sb_getState() {
return this._state;
},
set: function sb_setState(state) {
this._state = state;
this.fire(state);
}
});
proto.init = function sb_init(ui) {
this.searchUI = ui;
this.searchUI.addEventListener('transitionend', this);
this.voiceButton = this.searchUI.querySelector('.search-mic-button');
this.voiceButton.addEventListener('transitionend', this);
this.state = this.searchUI.classList.contains('hidden') ?
SearchBar.STATES.HIDDEN : SearchBar.STATES.SHOWN;
};
proto.uninit = function sb_uninit() {
this.searchUI.removeEventListener('transitionend', this);
this.voiceButton.removeEventListener('transitionend', this);
};
proto.show = function sb_show() {
this.searchUI.classList.remove('hidden');
this.state = SearchBar.STATES.SHOWING;
};
proto.hide = function sb_hide() {
this.searchUI.classList.add('hidden');
this.searchUI.classList.remove('opened');
this.state = SearchBar.STATES.HIDDEN;
};
proto.handleEvent = function sb_handleEvent(evt) {
// We have two steps transition:
// 1. background-color transition on searchUI
// 2. opacity on voiceButton
if (evt.target === this.searchUI &&
evt.propertyName === 'background-color' &&
this.state === SearchBar.STATES.SHOWING) {
this.searchUI.classList.add('opened');
} else if (evt.target === this.voiceButton &&
evt.propertyName === 'opacity' &&
this.state === SearchBar.STATES.SHOWING) {
this.state = SearchBar.STATES.SHOWN;
}
};
exports.SearchBar = SearchBar;
})(window);

View file

@ -0,0 +1,13 @@
'use strict';
(function(exports) {
exports.Utils = {
holdFocusForAnimation: function utils_holdFocusForAnimation() {
// we have to move focus to another element to prevent user press enter
// to trigger unexpected behavior. We cannot use blur() to do it because
// it makes smart-home without focus. In this case, other apps can
// request focus from smart-home which makes focus lost, bug 1161940.
document.getElementById('main-section').focus();
}
};
})(window);

View file

@ -0,0 +1,18 @@
{
"name": "smart-home",
"tags": {
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["tv_apps/smart-home/js/**/*.js"]
},
"plugins": [
"plugins/markdown",
"plugins/summarize"
],
"opts": {
"private": true,
"recurse": true,
"destination": "docs/smart-home"
}
}

View file

@ -0,0 +1,37 @@
#Filter tags
all=All
tv=Live TV
device=Device
app=Application
website=Website
#Buttons
edit=Edit
settings=Settings
done=done
add-new-folder=Add new folder
edit-mode-title=Edit Mode
new-folder=New folder
delete=Delete
cancel=Cancel
ok=OK
next=Next
#search bar
search-default-text=Search
enter-new-name=Please enter new name to the card.
delete-card-alert=Are you sure you want to delete this card?
channel-name=CH {{ number }}
#fte page 1
fte-tvdeck-detail=Watch TV channels
fte-appdeck-detail=Enjoy your applications
fte-devicedeck-detail=Find devices connected to TV
fte-browser-detail=Discover applications
#fte page 2
fte-search-detail=Search
fte-settings-detail=Settings
fte-addfolder-detail=Add a folder
fte-filter-detail=Filter

View file

@ -0,0 +1,57 @@
{
"name": "Home",
"description": "Smart Screen Home App",
"launch_path": "/index.html",
"type": "certified",
"role": "homescreen",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {
"en-US": {
"name": "Home",
"description": "Smart Screen Home App"
}
},
"activities": {
"pin": {
"filters": {
"type": {
"required": true
},
"manifestURL": { "pattern": "(https?|app):.{1,16384}" },
"url": { "pattern": "https?:.{1,16384}" }
}
},
"unpin": {
"filters": {
"manifestURL": { "pattern": "(https?|app):.{1,16384}" },
"url": { "pattern":"https?:.{1,16384}" }
}
}
},
"connections": {
"appdeck-channel": {
"description": "communications for app-deck to add/remove application cards",
"rules": {}
}
},
"default_locale": "en-US",
"icons": {
"128": "/style/icons/Blank.png"
},
"orientation": "landscape",
"permissions": {
"webapps-manage": {},
"open-remote-window":{},
"systemXHR":{},
"moz-extremely-unstable-and-will-change-webcomponents":{}
},
"datastores-owned": {
"home_cards": {
"access": "readwrite",
"description": "Stores all type of cards/decks/apps"
}
}
}

View file

@ -0,0 +1,161 @@
/**
* Deck smart-button
*/
smart-button .icon[data-icon] {
position: relative;
color: white;
display: inline-block;
line-height: 28rem;
}
smart-button .icon[data-icon]:before {
font-size: 30rem;
}
smart-button[type="app-button"].deck-tv {
background-color: #D70270;
}
smart-button[type="app-button"].deck-application {
background-color: #07CAB0;
}
smart-button[type="app-button"].deck-device {
background-color: #0074D4;
}
smart-button[type="app-button"].deck-dashboard {
background-color: #FF7E00;
}
.card smart-button {
text-align: center;
}
.card smart-button[type="app-button"]::after {
left: 0;
}
/**
* Deck wave effect
*/
.deck-wave {
position: absolute;
width: 134.4rem;
height: 67.2rem;
transform: scale(1.2);
animation: defaultWaveAnimation 5.0s linear 0s infinite normal;
animation-play-state: paused;
}
.card.focused .deck-wave {
animation-play-state: running;
}
#main-section[data-mode="edit"] .deck-wave,
#main-section[data-mode="arrange"] .deck-wave {
animation-play-state: paused;
}
.wave-front {
animation-duration: 3.05s
}
.wave-back {
animation-duration: 3.0s;
}
.tv-wave-front {
bottom: -16rem;
animation-name: tvWaveFrontAnimation;
background-image: url('images/wave_pink_01.png');
}
.tv-wave-back {
bottom: -14rem;
animation-name: tvWaveBackAnimation;
background-image: url('images/wave_pink_02.png');
}
.application-wave-front {
bottom: -16rem;
animation-name: applicationWaveFrontAnimation;
background-image: url('images/wave_green_01.png');
}
.application-wave-back {
bottom: -14rem;
animation-name: applicationWaveBackAnimation;
background-image: url('images/wave_green_02.png');
}
.device-wave-front {
bottom: -16rem;
animation-name: deviceWaveFrontAnimation;
background-image: url('images/wave_blue_01.png');
}
.device-wave-back {
bottom: -13rem;
animation-name: deviceWaveBackAnimation;
background-image: url('images/wave_blue_02.png');
}
.dashboard-wave-front {
bottom: -16rem;
animation-name: dashboardWaveFrontAnimation;
background-image: url('images/wave_orange_01.png');
}
.dashboard-wave-back {
bottom: -13rem;
animation-name: dashboardWaveBackAnimation;
background-image: url('images/wave_orange_02.png');
}
@keyframes defaultWaveAnimation {
from {transform: translateX(0);}
to {transform: translateX(-67.2rem);}
}
@keyframes tvWaveFrontAnimation {
from {transform: translateX(0);}
to {transform: translateX(-67.2rem);}
}
@keyframes tvWaveBackAnimation {
from {transform: translateX(-3rem);}
to {transform: translateX(-70.2rem);}
}
@keyframes applicationWaveFrontAnimation {
from {transform: translateX(-16rem);}
to {transform: translateX(-83.2rem);}
}
@keyframes applicationWaveBackAnimation {
from {transform: translateX(-23rem);}
to {transform: translateX(-90.2rem);}
}
@keyframes deviceWaveFrontAnimation {
from {transform: translateX(-22rem);}
to {transform: translateX(-89.2rem);}
}
@keyframes deviceWaveBackAnimation {
from {transform: translateX(-25rem);}
to {transform: translateX(-92.2rem);}
}
@keyframes dashboardWaveFrontAnimation {
from {transform: translateX(-35rem);}
to {transform: translateX(-102.2rem);}
}
@keyframes dashboardWaveBackAnimation {
from {transform: translateX(-32rem);}
to {transform: translateX(-99.2rem);}
}

View file

@ -0,0 +1,147 @@
#main-section[data-mode="edit"] #nav-regular,
#main-section[data-mode="arrange"] #nav-regular,
#main-section:not([data-mode="edit"]) #nav-edit-title,
#main-section:not([data-mode="edit"]) #nav-edit {
transform: translateY(-16rem);
}
#main-section[data-mode="arrange"] #card-viewer.current-scrollable {
z-index: 1;
}
#main-section[data-mode="edit"] #folder-list-frame,
#main-section[data-mode="arrange"] #folder-list-frame {
height: 0;
}
#main-section[data-mode="arrange"] .app-button.focused {
transform: scale(1.72);
}
#main-section[data-mode="edit"] .card.focused > .card-panel {
visibility: visible;
}
#main-section[data-mode="edit"] .card.delete > .card-panel {
display: none;
}
#main-section[data-mode="edit"] .card.focused > smart-button {
transform: scale(1.4);
}
#main-section[data-mode="arrange"] .card.hovered > smart-button {
transform: scale(1.75);
transition-delay: 0.1s;
}
#main-section[data-mode="arrange"] .card.focused.hover > smart-button {
transform: scale(1.2);
transition-delay: 0.1s;
}
#main-section[data-mode="edit"] .card.delete > smart-button {
transform: scale(1.4) translateY(20rem);
opacity: 0;
}
#main-section[data-mode="edit"] .card.focused > .card-panel smart-button {
transform: translateY(0);
opacity: 1;
}
#main-section[data-mode="edit"] .card.focused > .card-panel smart-button.focused {
/* Override the transform of smart-button.focused, otherwise, the smart-button
may drops down when focused */
transform: scale(1.2) translateY(0);
}
#main-section[data-mode="edit"] .card > .card-panel smart-button:nth-child(2) {
transition-delay: 0.1s;
}
/* Delete button may have 0.1s delay when focused */
#main-section[data-mode="edit"] .card.focused > smart-button:not(.focused) + .card-panel smart-button {
transition-delay: 0s;
}
#main-section[data-mode="edit"] .card.focused > smart-button[app-type='deck'] ~ .card-panel,
#main-section[data-mode="edit"] .card.focused > smart-button.animation-bubble-start ~ .card-panel {
display: none;
}
#main-section[data-mode="arrange"] .card.focused:after {
position: absolute;
left: -25.1rem;
right: -25.1rem;
top: -25.1rem;
bottom: -25.1rem;
content: ' ';
}
#card-list.exiting-edit-mode {
transition: transform 0.8s cubic-bezier(.6, 0, 0, 1);
}
#main-section[data-mode="arrange"] .card:not(.focused) {
filter: url(filter.svg#brightness50percent);
}
#main-section[data-mode="arrange"] .card.focused.left_arrow:after {
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem;
}
#main-section[data-mode="arrange"] .card.focused.right_arrow:after {
background: url(images/animated_arrows_right.png) no-repeat right center / 13rem;
}
#main-section[data-mode="arrange"] .card.focused.up_arrow:after {
background: url(images/animated_arrows_up.png) no-repeat top center / 13rem;
}
#main-section[data-mode="arrange"] .card.focused.left_arrow.down_arrow:after {
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem,
url(images/animated_arrows_down.png) no-repeat bottom center / 13rem;
}
#main-section[data-mode="arrange"] .card.focused.right_arrow.down_arrow:after {
background: url(images/animated_arrows_right.png) no-repeat right center / 13rem,
url(images/animated_arrows_down.png) no-repeat bottom center / 13rem;
}
#main-section[data-mode="arrange"] .card.focused.left_arrow.up_arrow:after {
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem,
url(images/animated_arrows_up.png) no-repeat top center / 13rem;
}
#main-section[data-mode="arrange"] .card.focused.right_arrow.up_arrow:after {
background: url(images/animated_arrows_right.png) no-repeat right center / 13rem,
url(images/animated_arrows_up.png) no-repeat top center / 13rem;
}
#main-section[data-mode="arrange"] .card.focused.left_arrow.right_arrow:after {
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem,
url(images/animated_arrows_right.png) no-repeat right center / 13rem;
}
#main-section[data-mode="arrange"] .card.focused.left_arrow.right_arrow.down_arrow:after {
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem,
url(images/animated_arrows_right.png) no-repeat right center / 13rem,
url(images/animated_arrows_down.png) no-repeat bottom center / 13rem;
}
#main-section[data-mode="arrange"] .card.focused.left_arrow.right_arrow.up_arrow:after {
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem,
url(images/animated_arrows_right.png) no-repeat right center / 13rem,
url(images/animated_arrows_up.png) no-repeat top center / 13rem;
}
#main-section[data-mode="edit"] .background,
#main-section[data-mode="arrange"] .background {
background-color: rgba(0, 0, 0, 0.8);
}
#main-section[data-mode="edit"] #card-list,
#main-section[data-mode="arrange"] #card-list {
transition: transform 0.66s cubic-bezier(.25, 0, 0, 1);
}

View file

@ -0,0 +1,9 @@
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
<filter id="brightness50percent">
<feComponentTransfer>
<feFuncR type="linear" slope="0.5"/>
<feFuncG type="linear" slope="0.5"/>
<feFuncB type="linear" slope="0.5"/>
</feComponentTransfer>
</filter>
</svg>

After

Width:  |  Height:  |  Size: 299 B

View file

@ -0,0 +1,124 @@
#fte {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: RGBA(0, 0, 0, 0.3);
opacity: 1;
transition: opacity 0.5s linear;
}
#fte.finish {
opacity: 0;
}
.fte-page {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: url(images/fte_bg.png) 100%;
font-size: 3.6rem;
color: white;
font-style: italic;
opacity: 1;
transition: opacity 0.5s linear;
}
.fte-page.hidden {
opacity: 0;
pointer-events: none;
transition-delay: 0.5s;
}
.fte-content-image {
position: absolute;
left: calc(50vw - 74.9rem);
top: calc(50vh - 42.1rem);
width: 149.8rem;
height: 84.2rem;
background: url(images/fte_content_image.png) 100%;
overflow: visible;
box-shadow: 0.5rem 1rem 1rem RGBA(0, 0, 0, 0.5);
}
.fte-indicator-p1 {
position: absolute;
left: -21.1rem;
top: -11.9rem;
width: 192rem;
height: 108rem;
background: url(images/fte_indicator_p1.png) 100%;
}
.fte-indicator-p2 {
position: absolute;
left: -21.1rem;
top: -11.9rem;
width: 192rem;
height: 108rem;
background: url(images/fte_indicator_p2.png) 100%;
}
.fte-panel {
position: absolute;
bottom: calc(50vh - 42.1rem - 4rem);
text-align: center;
width: 100%;
}
.fte-panel > .fte-button {
width: 30.4rem;
position: relative;
}
#fte-tvdeck-detail {
position: relative;
}
.fte-content-image > div {
position: absolute;
font-weight: 400;
}
#fte-tvdeck-detail {
left: 27.7rem;
top: 15.9rem;
}
#fte-appdeck-detail {
right: 51.9rem;
bottom: 16.5rem;
}
#fte-devicedeck-detail {
left: 84.9rem;
top: 15.9rem;
}
#fte-browser-detail {
left: 105.9rem;
bottom: 16.5rem;
}
#fte-search-detail {
left: -10.6rem;
top: -7.2rem;
}
#fte-settings-detail {
left: 8.7rem;
top: -7.2rem;
}
#fte-addfolder-detail {
left: 30.2rem;
top: -7.2rem;
}
#fte-filter-detail {
left: -10.6rem;
bottom: 16.5rem;
}

View file

@ -0,0 +1,13 @@
.left-top-panel {
left: 6rem;
top: 4rem;
position: absolute;
}
#header-buttons > smart-button {
margin-right: 1.8rem;
}
smart-button[type="text-and-icon"] {
min-width: 15rem;
text-align: center;
box-sizing: border-box;
}

View file

@ -0,0 +1,314 @@
html {
font-size: 10px;
/* We have Fira Sans as default font in real device. But it isn't default in
* browser. That's the main reason this line is here.*/
font-family: "Fira Sans";
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
background-color: transparent;
}
#main-section {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
a {
outline: 0;
}
a:hover, a:active, a:focus {
outline: 0;
}
section {
font-size: 1.6rem;
}
/* Card list */
#card-viewer {
position: absolute;
overflow: hidden;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
}
#card-list-frame {
position: absolute;
width: 100%;
height: 33.6rem;
top: 32.41%;
overflow: visible;
}
#card-list {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.4s cubic-bezier(0.25, 0, 0, 1);
transform-origin: 0 50%;
}
#card-list.hidden {
/* Using display none here would prevent animation, and using visibility
hidden causes some bugs in rendering */
opacity: 0;
}
#card-list.no-transition,
#folder-list.no-transition {
transition-property: none;
}
.card {
margin: 0;
left: 0;
top: 0;
position: absolute;
transition: transform 0.4s cubic-bezier(0.25, 0, 0, 1), filter 0.3s ease;
}
.card smart-button[type="app-button"].tv-channel {
background-color: #272727;
}
.card smart-button[type="app-button"].tv-channel:before {
font-size: 30rem;
color: white;
}
.card smart-button[type="app-button"].new-card {
opacity: 0;
transform: scale(0.5, 0.5);
}
.card smart-button[type="app-button"].new-card-transition {
transition: transform 0.4s cubic-bezier(0.25, 0, 0, 1) 0.2s,
opacity 0.4s cubic-bezier(0.25, 0, 0, 1) 0.2s;
}
.card smart-button[type="app-button"].new-card-transition.last-card {
transition-delay: 0s;
}
#folder-list .card smart-button[type="app-button"].new-card {
opacity: 1;
transform: translateY(-44rem);
}
#folder-list .card smart-button[type="app-button"].new-card-transition {
transition: transform 0.4s cubic-bezier(0.25, 0, 0, 1),
opacity 0.4s cubic-bezier(0.25, 0, 0, 1);
}
.card-panel {
position: absolute;
top: -23.14rem;
width: 100%;
height: 14.926rem;
text-align: center;
/* Use visibility hidden instead of display none here because display none
may prevent transition happens */
visibility: hidden;
z-index: 0;
}
.card-panel smart-button {
position: relative;
margin-left: 1.86em;
width: 14.926rem;
height: 14.926rem;
transform: translateY(12rem);
opacity: 0;
}
.card-panel smart-button:first-child {
margin-left: 0;
}
.card-panel smart-button[data-icon]::before {
width: 10.074rem;
left: 2.43rem;
font-size: 10.07rem;
line-height: 15.47rem;
}
.card smart-button.app-button {
/* smart-button should be in front of card-panel */
z-index: 1;
}
.card.focused {
z-index: 100;
}
/**
* folder list
*/
#folder-viewer {
position: absolute;
overflow: hidden;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
}
#folder-list-frame {
position: absolute;
width: 100%;
height: 33.6rem;
top: calc(32.41% + 32.3rem);
overflow: visible;
}
#folder-list {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.4s cubic-bezier(0.25, 0, 0, 1);
transform-origin: 0 50%;
}
/**
* filters
**/
.filter-tab-container {
position: absolute;
left: 6rem;
bottom: 5.4rem;
}
.filter-tab-group.hidden {
display: none;
}
/**
* background section
**/
.background {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background-color: rgba(0, 0, 0, 0.65);
transition: background-color 0.66s linear;
}
.background:before {
content: "";
height: 29rem;
position: absolute;
top: 0;
left: 0;
right: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
}
.background:after {
content: "";
height: 29rem;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
}
#edit-menu {
position: absolute;
top: 4rem;
right: 6rem;
}
/**
* nav bars
*/
nav {
position: absolute;
top: 0;
left: 0;
width: 100%;
transition: transform 0.4s cubic-bezier(0.165, 0.840, 0.440, 1.000);
}
nav header {
padding-left: 7rem;
padding-top: 5rem;
font-size: 5.4rem;
font-style: italic;
color: white;
}
#main-section[data-mode="edit"] #nav-regular,
#main-section[data-mode="arrange"] #nav-regular,
#main-section:not([data-mode="edit"]) #nav-edit-title,
#main-section:not([data-mode="edit"]) #nav-edit {
transform: translateY(-16rem);
}
smart-button.hidden {
display: none;
}
/* 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.
*/
smart-button[type="app-button"]:after {
content: none;
}
smart-button[type="app-button"] > span.name {
position: absolute;
bottom: 0;
left: 0;
width: 33.6rem;
height: 8rem;
padding-top: 1rem;
line-height: 3rem;
text-align: center;
font-size: 2.8rem;
font-style: italic;
color: #ffffff;
}
smart-button[type="app-button"][app-type="deck"] > span.name {
background-color: transparent;
}
smart-button[type="app-button"][app-type="tv"] > span.name {
background-color: #d90c83;
}
smart-button[type="app-button"][app-type="app"] > span.name,
smart-button[type="app-button"][app-type="appbookmark"] > span.name {
background-color: rgba(0, 0, 0, 0.4);
}
smart-button[type="app-button"][app-type="device"] > span.name {
background-color: #4a82ea;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View file

@ -0,0 +1,97 @@
.information-bar {
position: absolute;
right: 2.4rem;
top: 3rem;
color: #ffffff;
}
.weather-info {
position: absolute;
right: 0;
top: 0;
width: 25rem;
height: 10rem;
}
.weather-image {
display: inline-block;
position: absolute;
top: 0;
left: 0;
width: 10rem;
height: 10rem;
background-image: url(images/img_weather_mostly_cloudy.png);
background-size: 10rem auto;
background-repeat: no-repeat;
}
.temperature {
display: inline-block;
position: absolute;
bottom: 3rem;
left: 10rem; /* sit at the right of weather-image */
height: 5.6rem;
line-height: 5.6rem;
font-size: 6rem;
font-weight: 100;
}
.degree-marker {
font-size: 2.4rem;
line-height: 2.4rem;
font-weight: 100;
position: relative;
top: -2.4rem;
}
.weather-text {
display: inline-block;
position: absolute;
bottom: 0;
left: 10rem; /* sit at the right of weather-image */
font-size: 1.8rem;
font-style: italic;
font-weight: 300;
}
.clock-info {
position: absolute;
top: 0;
/* right align to weather-info(25rem) + 1rem spacing */
right: 26.2rem;
width: 26rem;
height: 10rem;
border-right: solid 0.2rem #ffffff;
}
.time:before {
content: attr(data-ampm);
position: absolute;
height: 1.8rem;
line-height: 1.8rem;
font-size: 1.8rem;
bottom: 1.8rem;
left: -2.8rem;
}
.time {
position: absolute;
top: 0;
right: 1.2rem;
height: 10rem;
line-height: 10rem;
display: inline-block;
text-align: left;
font-weight: 100;
font-size: 10.6rem;
}

View file

@ -0,0 +1,123 @@
.search-bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
opacity: 1;
background-color: rgba(0, 0, 0, 1);
transition-property: background-color;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.92s;
}
.search-bar.hidden {
opacity: 0;
background-color: rgba(0, 0, 0, 0);
transition-property: none;
}
smart-button.search-mic-button {
opacity: 1;
background-color: #5d6368;
position: absolute;
left: 28.6rem;
top: 4rem;
}
.search-bar.hidden .search-input {
left: 6rem;
right: auto;
top: 4rem;
width: 8rem;
border-radius: 4rem;
padding-left: 0;
transition-property: none;
}
.search-input span {
opacity: 0;
}
.search-bar.opened .search-input span {
opacity: 1;
transition-property: opacity;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.92s;
}
/* icon parts */
.search-bar.hidden [data-icon].search-input:before {
left: 1.3rem;
right: auto;
}
[data-icon].search-input:before {
width: 5.4rem;
right: 1.2rem;
font-size: 5.4rem;
line-height: 8rem;
text-align: center;
color: #2d2d2d;
white-space: normal;
top: 0;
position: absolute;
}
.search-input {
/* 38.4rem = 28.6rem (left) + 8rem (voice button) + 1.8rem (margin) */
left: 38.4rem;
/* 77rem = left: 38.4rem + right 38.6rem */
width: calc(100% - 77rem);
top: 4rem;
height: 8rem;
position: absolute;
display: inline-block;
background-color: #ffffff;
color: #808080;
font-size: 3.2rem;
line-height: 8rem;
overflow: hidden;
padding-left: 4rem;
border-radius: 4rem;
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.92s;
}
/* blue underline */
.search-input:after {
position: absolute;
width: 100%;
height: 100%;
left: 0;
content: ' ';
border-bottom: 0.6rem #61acd7 solid;
box-sizing: border-box;
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.92s;
}
.search-bar.hidden .search-input:after {
opacity: 0;
transition-property: none;
}
.search-bar.opened .search-mic-button {
transition-property: opacity;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.66s;
opacity: 1;
}
.search-bar .search-mic-button {
transition-property: none;
opacity: 0;
}

View file

@ -0,0 +1,14 @@
{
"extends": "../../../../.jshintrc",
"node": true,
"predef": [
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"marionette",
"marionetteScriptFinished"
]
}

View file

@ -0,0 +1,124 @@
'use strict';
var APP_URL = 'app://smart-home.gaiamobile.org';
var Keys = {
'up': '\ue013',
'right': '\ue014',
'down': '\ue015',
'left': '\ue012',
'enter': '\ue006',
'esc': '\ue00c'
};
var assert = require('chai').assert;
var containsClass = function(elem, className) {
if (elem) {
return elem.getAttribute('class').indexOf(className) > -1;
}
return false;
};
// Bug 1207453 - Skip the test due to unknown test enviroment issue for now.
// We should investigate the issue and re-enable the test later.
marionette.skip('Smart Home', function() {
var opts = {
hostOptions: {
screen: {
width: 1920,
height: 1080
}
}
};
var client = marionette.client({
profile: opts,
// XXX: Set this to true once Accessibility is implemented in TV
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
setup(function() {
// Launch test app
client.apps.launch(APP_URL);
client.apps.switchToApp(APP_URL);
});
suite('> Card Filter', function() {
test('Hide card filter when enter edit mode and show card filter when' +
' exit edit mode ', { devices: ['tv'] }, function () {
var editButton = client.findElement('#edit-button');
var element = client.findElement('#main-section');
var searchButton = client.findElement('#search-button');
var deckTvCard = client.findElement('#card-list > .card');
var deckTvButton = client.findElement('.app-button.deck-tv');
client.waitFor(function() {
return containsClass(deckTvCard, 'focused');
});
client.waitFor(function() {
return containsClass(deckTvButton, 'focused');
});
element.sendKeys(Keys.up);
client.waitFor(function() {
return containsClass(searchButton, 'focused');
});
element.sendKeys(Keys.right);
client.waitFor(function() {
return containsClass(editButton, 'focused');
});
element.sendKeys(Keys.enter);
client.waitFor(function() {
return element.getAttribute('data-mode') === 'edit';
});
var cardFilter = client.findElement('#filter-tab-group');
assert.isTrue(containsClass(cardFilter, 'hidden'));
element.sendKeys(Keys.up);
element.sendKeys(Keys.enter);
assert.isFalse(containsClass(cardFilter, 'hidden'));
});
test('Collapse folder when filter is focused',
{ devices: ['tv'] }, function() {
// add new folder
client.executeScript(function() {
var home = window.wrappedJSObject.home;
var newFolder = home.cardManager.insertNewFolder({id: 'new-folder'},
home.cardManager._cardList.length);
var card = home.cardManager._deserializeCardEntry({group: 'application',
manifestURL: 'app://tv-epg.gaiamobile.org/manifest.webapp',
type: 'Application'
});
newFolder.addCard(card);
});
var element = client.findElement('#main-section');
var newFolderCard =
client.findElement('#card-list div.card:last-child');
client.waitFor(function() {
return containsClass(newFolderCard, 'focused');
});
// move focus to card filter
element.sendKeys(Keys.down);
element.sendKeys(Keys.down);
// wait until animation of card filter ends
var cardFilter = client.findElement('#filter-tab-group');
client.waitFor(function() {
return !containsClass(cardFilter, 'closed');
});
// wait for folder to be collapsed
client.helper.waitForElementToDisappear('#folder-list .card');
});
});
});

View file

@ -0,0 +1,18 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"assert",
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"require",
"requireApp",
"sinon",
"mocha",
"loadBodyHTML"
]
}

View file

@ -0,0 +1,186 @@
/* global MockSmartButton, MockMenuGroup, CardFilter */
'use strict';
require('/shared/test/unit/mocks/mock_event_target.js');
require('/bower_components/evt/index.js');
requireApp('smart-home/test/unit/mock_smart_button.js');
requireApp('smart-home/test/unit/mock_menu_group.js');
requireApp('smart-home/js/card_filter.js');
suite('smart-home/CardFilter', function() {
var cardFilter;
var mockMenuGroup;
var mockSmartButtons;
var generateSmartButtons = function() {
return [
new MockSmartButton('filter'),
new MockSmartButton('tv'),
new MockSmartButton('dashbaord'),
new MockSmartButton('device'),
new MockSmartButton('application')
];
};
suite('> start', function() {
setup(function() {
mockSmartButtons = generateSmartButtons();
mockSmartButtons.forEach(function(button) {
sinon.spy(button, 'addEventListener');
});
mockMenuGroup = new MockMenuGroup();
sinon.stub(mockMenuGroup, 'querySelectorAll', function() {
return mockSmartButtons;
});
sinon.spy(mockMenuGroup, 'addEventListener');
cardFilter = new CardFilter();
});
teardown(function() {
mockMenuGroup = undefined;
cardFilter = undefined;
mockSmartButtons = undefined;
});
test('start() should register "click" event listeners on smart-button',
function() {
cardFilter.start(mockMenuGroup);
assert.isTrue(mockMenuGroup.querySelectorAll.calledOnce);
mockSmartButtons.forEach(function(button) {
assert.isTrue(button.addEventListener.calledOnce);
});
});
test('start() should listen to "opened" event on menu group', function() {
cardFilter.start(mockMenuGroup);
assert.isTrue(mockMenuGroup.addEventListener.calledOnce);
var call = mockMenuGroup.addEventListener.firstCall;
assert.equal(call.args[0], 'opened');
assert.isTrue(typeof call.args[1] === 'function');
});
});
suite('> stop', function() {
setup(function() {
mockSmartButtons = generateSmartButtons();
mockSmartButtons.forEach(function(button) {
sinon.spy(button, 'removeEventListener');
});
mockMenuGroup = new MockMenuGroup();
sinon.stub(mockMenuGroup, 'querySelectorAll', function() {
return mockSmartButtons;
});
cardFilter = new CardFilter();
cardFilter.start(mockMenuGroup);
});
teardown(function() {
mockMenuGroup = undefined;
cardFilter = undefined;
mockSmartButtons = undefined;
});
test('stop() should remove "click" event listeners on smart-button',
function() {
cardFilter.stop();
mockSmartButtons.forEach(function(button) {
assert.isTrue(button.removeEventListener.calledOnce);
});
});
});
suite('> handleEvent', function() {
setup(function() {
mockSmartButtons = generateSmartButtons();
mockMenuGroup = new MockMenuGroup();
sinon.stub(mockMenuGroup, 'querySelectorAll', function() {
return mockSmartButtons;
});
cardFilter = new CardFilter();
sinon.spy(cardFilter, 'handleEvent');
cardFilter.start(mockMenuGroup);
});
teardown(function() {
mockMenuGroup = undefined;
cardFilter = undefined;
mockSmartButtons = undefined;
});
test('handleEvent() should be called with event argument', function() {
mockSmartButtons[0].dispatchEvent({
type: 'click'
});
assert.isTrue(cardFilter.handleEvent.calledOnce);
assert.equal(cardFilter.handleEvent.getCall(0).args[0].type, 'click');
});
});
suite('> filter', function() {
setup(function() {
mockSmartButtons = generateSmartButtons();
mockMenuGroup = new MockMenuGroup();
sinon.stub(mockMenuGroup, 'querySelectorAll', function() {
return mockSmartButtons;
});
cardFilter = new CardFilter();
cardFilter.filter = 'filter';
sinon.spy(cardFilter, 'emit');
cardFilter.start(mockMenuGroup);
});
teardown(function() {
mockMenuGroup = undefined;
cardFilter = undefined;
mockSmartButtons = undefined;
});
test('should fire filterchanged event when filter is changed', function() {
cardFilter.filter = 'tv';
assert.isTrue(cardFilter.emit.calledOnce);
assert.equal(cardFilter.emit.getCall(0).args[0], 'filterchanged');
assert.equal(cardFilter.emit.getCall(0).args[1], 'tv');
});
});
suite('> show and hide', function() {
setup(function() {
mockSmartButtons = generateSmartButtons();
mockMenuGroup = new MockMenuGroup();
sinon.stub(mockMenuGroup, 'querySelectorAll', function() {
return mockSmartButtons;
});
sinon.spy(mockMenuGroup.classList, 'add');
sinon.spy(mockMenuGroup.classList, 'remove');
cardFilter = new CardFilter();
cardFilter.filter = 'filter';
cardFilter.start(mockMenuGroup);
});
teardown(function() {
mockMenuGroup.classList.add.restore();
mockMenuGroup.classList.remove.restore();
mockMenuGroup = undefined;
cardFilter = undefined;
mockSmartButtons = undefined;
});
test('should add "hidden" class to menu group when calling hide()',
function() {
cardFilter.hide();
assert.isTrue(mockMenuGroup.classList.add.calledWithExactly('hidden'));
});
test('should remove "hidden" class from menu group when calling show()',
function() {
cardFilter.show();
assert.isTrue(
mockMenuGroup.classList.remove.calledWithExactly('hidden'));
});
});
});

View file

@ -0,0 +1,623 @@
'use strict';
/* global Application, CardManager, Deck, Folder, AsyncSemaphore,
MockCardStore, MockPipedPromise, MockXMLHttpRequest */
require('/bower_components/evt/index.js');
require('/shared/js/async_semaphore.js');
require('/shared/js/uuid.js');
require('/shared/js/smart-screen/shared_utils.js');
require('/shared/test/unit/mocks/mock_piped_promise.js');
require('/shared/test/unit/mocks/mock_card_store.js');
require('/shared/test/unit/mocks/mock_xml_http_request.js');
require('/shared/js/smart-screen/cards/card.js');
require('/shared/js/smart-screen/cards/deck.js');
require('/shared/js/smart-screen/cards/folder.js');
require('/shared/js/smart-screen/cards/application.js');
suite('smart-home/CardManager', function() {
var realPipedPromise;
var decks;
var apps;
var folders;
suiteSetup(function(done) {
realPipedPromise = window.PipedPromise;
// real PipedPromise will make test failed because it waits for
// last promise to be resolved. But we need to call done in the last
// promise. So we use a MockPipedPromise (which is just a genuine native
// Promise) instead.
window.PipedPromise = MockPipedPromise;
decks = {
dashboard: new Deck({
name: {
raw: 'Dashboard'
},
group: 'dashboard',
nativeApp: {
name: 'Dashboard',
removable: false,
manifest: {},
manifestURL: 'app://dashboard.gaiamobile.org/manifest.webapp'
}
}),
tv: new Deck({
name: {
raw: 'TV'
},
group: 'tv',
nativeApp: {
name: 'TV',
removable: false,
manifest: {},
manifestURL: 'app://tv-deck.gaiamobile.org/manifest.webapp'
}
})
};
apps = {
music: new Application({
name: {
raw: 'Music'
},
group: 'application',
nativeApp: {
name: 'Music',
removable: false,
manifest: {},
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'
}
}),
video: new Application({
name: {
raw: 'Video'
},
group: 'application',
nativeApp: {
name: 'Video',
removable: false,
manifest: {},
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'
}
}),
gallery: new Application({
name: {
raw: 'Gallery'
},
group: 'application',
nativeApp: {
name: 'Gallery',
removable: false,
manifest: {},
manifestURL: 'app://gallery.gaiamobile.org/manifest.webapp'
}
})
};
folders = {
emptyOne: new Folder({
name: {
raw: 'New Empty Folder'
}
}),
emptyTwo: new Folder({
name: {
raw: 'New Empty Folder 2'
}
}),
nonEmpty: new Folder({
name: {
raw: 'Non Empty'
},
_cardsInFolder: [apps.video, apps.gallery]
})
};
require('/shared/js/smart-screen/card_manager.js', function() {
done();
});
});
suiteTeardown(function() {
decks = undefined;
apps = undefined;
folders = undefined;
window.PipedPromise = realPipedPromise;
});
function prepareCardManagerForTesting() {
var result = new CardManager();
// We bypass init() because it involves navigator.mozApps.mgmt
// Instead we need to prepare necessary properties of cardManager
result._asyncSemaphore = new AsyncSemaphore();
result._cardStore = MockCardStore;
result._cardList = [
decks.dashboard,
apps.music,
decks.tv,
folders.nonEmpty
];
result.installedApps = {
'app://dashboard.gaiamobile.org/manifest.webapp': {
name: 'Dashboard',
removable: false,
manifest: {},
manifestURL: 'app://dashboard.gaiamobile.org/manifest.webapp'
},
'app://music.gaiamobile.org/manifest.webapp': {
name: 'Music',
removable: false,
manifest: {},
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'
},
'app://tv-deck.gaiamobile.org/manifest.webapp': {
name: 'TV',
removable: false,
manifest: {},
manifestURL: 'app://tv-deck.gaiamobile.org/manifest.webapp'
},
'app://video.gaiamobile.org/manifest.webapp': {
name: 'Video',
removable: false,
manifest: {},
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'
},
'app://gallery.gaiamobile.org/manifest.webapp': {
name: 'Gallery',
removable: false,
manifest: {},
manifestURL: 'app://gallery.gaiamobile.org/manifest.webapp'
},
};
return result;
}
suite('_bestMatchingIcon()', function() {
var cardManager;
var app;
var manifestWithIconsInOrder = {
icons: {
'32': '/style/icons/32.png',
'64': '/style/icons/64.png',
'128': '/style/icons/128.png',
'256': '/style/icons/256.png'
}
};
var manifestWithIconsInRandom = {
icons: {
'32': '/style/icons/32.png',
'256': '/style/icons/256.png',
'128': '/style/icons/128.png',
'64': '/style/icons/64.png'
}
};
setup(function() {
cardManager = new CardManager();
app = {
origin: 'app://stub.gaiamobile.org/'
};
});
teardown(function() {
cardManager = undefined;
app = undefined;
});
test('call without preferredSize', function() {
var actual = cardManager._bestMatchingIcon(app, manifestWithIconsInOrder);
assert.equal(actual, 'app://stub.gaiamobile.org/style/icons/256.png');
});
test('call with random order of icon size and ' +
'without preferredSize ', function() {
var actual =
cardManager._bestMatchingIcon(app, manifestWithIconsInRandom);
assert.equal(actual, 'app://stub.gaiamobile.org/style/icons/256.png');
});
test('call with preferredSize', function() {
var actual =
cardManager._bestMatchingIcon(app, manifestWithIconsInOrder, 100);
assert.equal(actual, 'app://stub.gaiamobile.org/style/icons/128.png');
});
test('call with random order of icon size and ' +
'preferredSize ', function() {
var actual =
cardManager._bestMatchingIcon(app, manifestWithIconsInRandom, 100);
assert.equal(actual, 'app://stub.gaiamobile.org/style/icons/128.png');
});
});
suite('_reloadCardList()', function() {
var cardManager;
var realXMLHttpRequest;
setup(function() {
cardManager = new CardManager();
// We bypass init() because it involves navigator.mozApps.mgmt
// Instead we need to prepare necessary properties of cardManager
cardManager._cardList = [];
cardManager._cardStore = MockCardStore;
cardManager._asyncSemaphore = new AsyncSemaphore();
realXMLHttpRequest = window.XMLHttpRequest;
window.XMLHttpRequest = MockXMLHttpRequest;
this.sinon.spy(cardManager, 'writeCardlistInCardStore');
});
teardown(function() {
MockCardStore.mClearData();
window.XMLHttpRequest = realXMLHttpRequest;
cardManager = undefined;
});
test('should load cardList from datastore if possible', function(done) {
MockCardStore.mPrepareData('cardList', [{
'name': 'Television',
'type': 'Deck'
}]);
cardManager._reloadCardList().then(function() {
assert.isTrue(cardManager._cardList.length > 0);
assert.equal(cardManager._cardList[0].name, 'Television');
}).then(done, done);
});
test('should load cardList from file if nothing in datastore',
function(done) {
cardManager._reloadCardList().then(function() {
assert.isTrue(cardManager._cardList.length > 0);
assert.equal(cardManager._cardList[0].name, 'Devices');
assert.isTrue(cardManager.writeCardlistInCardStore.calledOnce);
}, function(reason) {
assert.fail('should not reject promise due to ' + reason);
}).then(done, done);
// XXX: We should remove window.setTimeout in test but since we are
// guarding this test with done(), let's just put it as is until we
// have better solution
window.setTimeout(function() {
MockXMLHttpRequest.triggerReadyStateChange({
status: 200,
response: {
'card_list': [{
'name': 'Devices',
'type': 'Deck'
}]
}
});
});
});
});
suite('findCardFromCardList()', function() {
var cardManager;
var dashboardCardId;
var folderCardId;
setup(function() {
cardManager = prepareCardManagerForTesting();
dashboardCardId = cardManager._cardList[0].cardId;
folderCardId = cardManager._cardList[3].cardId;
});
teardown(function() {
MockCardStore.mClearData();
cardManager = undefined;
dashboardCardId = undefined;
folderCardId = undefined;
});
test('should find card based on cardId', function() {
var card = cardManager.findCardFromCardList({
cardId: dashboardCardId
});
assert.equal(card.name.raw, 'Dashboard');
});
test('should find folder based on cardId', function() {
var card = cardManager.findCardFromCardList({
cardId: folderCardId
});
assert.isTrue(card instanceof Folder);
assert.equal(card.name.raw, 'Non Empty');
});
test('should return undefined by querying non-existing launchURL',
function() {
var card = cardManager.findCardFromCardList({
manifestURL: 'app://tv-deck.gaiamobile.org/manifest.webapp',
launchURL: 'app://tv-deck.gaiamobile.org/#42'
});
assert.isUndefined(card);
});
test('should return undefined if not found', function() {
var card = cardManager.findCardFromCardList({
cardId: 'incorrect-card-id'
});
assert.isUndefined(card);
});
test('should find card inside of folder', function() {
var card = cardManager.findCardFromCardList({
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'
});
assert.equal(card.name.raw, 'Video');
});
test('should be able to find folder by querying via cardEntry', function() {
var folderEntry = folders.nonEmpty.serialize();
var card = cardManager.findCardFromCardList({
cardEntry: folderEntry
});
assert.equal(card, folders.nonEmpty);
});
});
suite('getFilteredCardList()', function() {
var cardManager;
setup(function() {
cardManager = prepareCardManagerForTesting();
});
teardown(function() {
MockCardStore.mClearData();
cardManager = undefined;
});
test('should get only apps if filter="application"', function(done) {
var filter = 'application';
cardManager.getFilteredCardList(filter).then(function(cards) {
assert.isTrue(cards.length === 1);
}).then(done, done);
});
test('should get all cards if filter="all"', function(done) {
var filter = 'all';
cardManager.getFilteredCardList(filter).then(function(cards) {
assert.isTrue(cards.length === cardManager._cardList.length);
}).then(done, done);
});
test('should get empty array if filter is undefined', function(done) {
cardManager.getFilteredCardList().then(function(cards) {
assert.isTrue(cards.length === 0);
}).then(done, done);
});
});
suite('insertNewFolder()', function() {
var cardManager;
var dashboardCardId;
setup(function() {
cardManager = new CardManager();
// We bypass init() because it involves navigator.mozApps.mgmt
// Instead we need to prepare necessary properties of cardManager
cardManager._asyncSemaphore = new AsyncSemaphore();
cardManager._cardStore = MockCardStore;
cardManager._cardList = [decks.dashboard];
cardManager.installedApps = {
'app://dashboard.gaiamobile.org/manifest.webapp': {
name: 'Dashboard',
removable: false,
manifest: {},
manifestURL: 'app://dashboard.gaiamobile.org/manifest.webapp'
}
};
dashboardCardId = cardManager._cardList[0].cardId;
this.sinon.spy(cardManager, 'writeFolderInCardStore');
this.sinon.spy(cardManager, 'writeCardlistInCardStore');
});
teardown(function() {
MockCardStore.mClearData();
cardManager = undefined;
});
test('should return new folder instance whenever a new folder is inserted',
function() {
var newFolder = cardManager.insertNewFolder('an empty folder');
assert.ok(newFolder.folderId);
assert.equal(newFolder.state, Folder.STATES.DETACHED);
});
test('should write to data store when folder has content', function() {
var newFolder = cardManager.insertNewFolder('a test folder');
assert.isFalse(cardManager.writeCardlistInCardStore.calledOnce);
newFolder.addCard(new Application({
name: 'Music',
group: 'application'
}));
assert.isTrue(cardManager.writeCardlistInCardStore.calledOnce);
});
test('should insert at correct location if index is specified', function() {
var targetLocation = 0;
var newFolder =
cardManager.insertNewFolder('a test folder', targetLocation);
assert.isFalse(cardManager.writeCardlistInCardStore.calledOnce);
newFolder.addCard(apps.music);
assert.isTrue(cardManager.writeCardlistInCardStore.calledOnce);
assert.equal(cardManager._cardList[targetLocation], newFolder);
});
});
suite('insertCard()', function() {
var cardManager;
setup(function() {
cardManager = prepareCardManagerForTesting();
});
teardown(function() {
MockCardStore.mClearData();
cardManager = undefined;
});
test('should not be able to insert the same card twice', function() {
var expectedCardListLength = cardManager._cardList.length;
cardManager.insertCard({
cardEntry: {
'type': 'Application',
'group': 'app',
'manifestURL': 'app://music.gaiamobile.org/manifest.webapp'
}
});
assert.equal(cardManager._cardList.length, expectedCardListLength);
});
test('should be able to insert same app twice with different launchURL',
function() {
var expectedCardListLength = cardManager._cardList.length + 1;
cardManager.insertCard({
cardEntry: {
'type': 'Application',
'group': 'tv',
'manifestURL': 'app://tv-deck.gaiamobile.org/manifest.webapp',
'launchURL': 'app://tv-deck.gaiamobile.org/#42'
}
});
assert.equal(cardManager._cardList.length, expectedCardListLength);
});
});
suite('removeCard()', function() {
var cardManager;
setup(function() {
cardManager = prepareCardManagerForTesting();
});
teardown(function() {
MockCardStore.mClearData();
cardManager = undefined;
});
test('should be able to remove card with index specified', function() {
assert.ok(
cardManager.findCardFromCardList({
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'})
);
cardManager.removeCard(1); // remove card of 'Music' app
assert.isUndefined(
cardManager.findCardFromCardList({
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'})
);
});
test('should be able to remove card within folder', function(done) {
assert.ok(
cardManager.findCardFromCardList({
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'})
);
cardManager.getCardList().then(function(cardList) {
cardManager.removeCard(cardList[3].getCardList()[0]);
assert.isUndefined(
cardManager.findCardFromCardList({
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'})
);
}).then(done, done);
});
});
suite('writeCardlistInCardStore()', function() {
var cardManager;
setup(function() {
cardManager = new CardManager();
// We bypass init() because it involves navigator.mozApps.mgmt
// Instead we need to prepare necessary properties of cardManager
cardManager._asyncSemaphore = new AsyncSemaphore();
cardManager._cardStore = MockCardStore;
cardManager._cardList = [
decks.dashbaord,
folders.emptyOne,
folders.emptyTwo,
apps.music
];
cardManager.installedApps = {
'app://dashboard.gaiamobile.org/manifest.webapp': {
name: 'Dashboard',
removable: false,
manifest: {},
manifestURL: 'app://dashboard.gaiamobile.org/manifest.webapp'
},
'app://music.gaiamobile.org/manifest.webapp': {
name: 'Music',
removable: false,
manifest: {},
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'
}
};
});
teardown(function() {
MockCardStore.mClearData();
cardManager = undefined;
});
test('write with empty folder should eliminate it from card list',
function(done) {
var expectedLength = cardManager._cardList.length - 2;
cardManager.writeCardlistInCardStore({
cleanEmptyFolder: true
}).then(function() {
assert.equal(cardManager._cardList.length, expectedLength);
assert.isTrue(cardManager._cardList.indexOf(folders.emptyOne) < 0);
}).then(done, done);
});
});
suite('swapCard()', function() {
var cardManager;
setup(function() {
cardManager = prepareCardManagerForTesting();
});
teardown(function() {
MockCardStore.mClearData();
cardManager = undefined;
});
test('swap adjacent cards', function() {
var card1 = cardManager._cardList[0];
var card2 = cardManager._cardList[1];
cardManager.swapCard(card1, card2);
assert.equal(cardManager._cardList[0], card2);
assert.equal(cardManager._cardList[1], card1);
});
test('swap non-adjacent cards', function() {
var card1 = cardManager._cardList[0];
var card2 = cardManager._cardList[2];
cardManager.swapCard(card1, card2);
assert.equal(cardManager._cardList[0], card2);
assert.equal(cardManager._cardList[2], card1);
});
});
});

View file

@ -0,0 +1,158 @@
/* global MockCardFilter, FilterManager, MockXScrollable, MockCardManager,
MockHome */
'use strict';
require('/bower_components/evt/index.js');
requireApp('smart-home/test/unit/mock_card_filter.js');
requireApp('smart-home/test/unit/mock_x_scrollable.js');
requireApp('smart-home/test/unit/mock_card_manager.js');
requireApp('smart-home/test/unit/mock_home.js');
requireApp('smart-home/js/filter_manager.js');
suite('smart-gome/FilterManager', function() {
var setupDOM = function() {
var filterTabGroup = document.createElement('menu');
filterTabGroup.id = 'filter-tab-group';
filterTabGroup.dataset.iconType = 'filter';
filterTabGroup.classList.add('filter-tab-group');
var bubbles = document.createElement('div');
bubbles.id = 'bubbles';
var cardListElem = document.createElement('div');
cardListElem.id = 'card-list';
cardListElem.setAttribute('smart-bubbles', 'true');
cardListElem.dataset.defaultItem = '0';
document.body.appendChild(filterTabGroup);
document.body.appendChild(bubbles);
document.body.appendChild(cardListElem);
};
var teardownDOM = function() {
document.body.removeChild(document.getElementById('filter-tab-group'));
document.body.removeChild(document.getElementById('bubbles'));
document.body.removeChild(document.getElementById('card-list'));
};
suite('> filterchanged event', function() {
var realCardFilter;
var filterManager;
setup(function() {
realCardFilter = window.CardFilter;
window.CardFilter = MockCardFilter;
setupDOM();
filterManager = new FilterManager();
filterManager.init({
cardListElem: document.getElementById('card-list'),
cardScrollable: new MockXScrollable(),
home: new MockHome(),
cardManager: new MockCardManager()
});
filterManager._smartBubblesElem.play = function() {};
filterManager._smartBubblesElem.stopImmediately = function() {};
sinon.spy(filterManager._smartBubblesElem, 'play');
sinon.spy(filterManager._smartBubblesElem, 'stopImmediately');
});
teardown(function() {
window.cardFilter = realCardFilter;
filterManager._smartBubblesElem.play.restore();
filterManager._smartBubblesElem.stopImmediately.restore();
filterManager = undefined;
teardownDOM();
});
test('start playing animation when filterchanged is fired', function() {
var cardListElem = document.getElementById('card-list');
filterManager._cardFilter.mFireEvent('filterchanged', 'tv');
assert.equal(
cardListElem.getAttribute('smart-bubbles-direction'), 'down');
assert.isTrue(filterManager._smartBubblesElem.play.calledOnce);
});
test('stop playing animation when filterchanged is fired before previous ' +
' animation ends', function() {
filterManager._isFilterChanging = true;
filterManager._cardFilter.mFireEvent('filterchanged', 'tv');
assert.isTrue(filterManager._smartBubblesElem.stopImmediately.calledOnce);
});
});
suite('> all-items-bubbled event', function() {
var realCardFilter;
var filterManager;
var mockXScrollable;
var mockCardManager;
var mockHome;
var cardListElem;
setup(function() {
realCardFilter = window.CardFilter;
window.CardFilter = MockCardFilter;
setupDOM();
cardListElem = document.getElementById('card-list');
mockXScrollable = new MockXScrollable();
mockCardManager = new MockCardManager();
filterManager = new FilterManager();
mockHome = new MockHome();
filterManager.init({
cardListElem: cardListElem,
cardScrollable: mockXScrollable,
home: mockHome,
cardManager: mockCardManager
});
filterManager._smartBubblesElem.play = function() {};
filterManager._smartBubblesElem.stopImmediately = function() {};
sinon.spy(filterManager, 'getFilterByIconName');
sinon.spy(mockXScrollable, 'clean');
sinon.spy(mockCardManager, 'getFilteredCardList');
});
teardown(function() {
window.cardFilter = realCardFilter;
filterManager.getFilterByIconName.restore();
filterManager = undefined;
mockXScrollable.clean.restore();
mockXScrollable = undefined;
mockCardManager.getFilteredCardList.restore();
mockCardManager = undefined;
mockHome = undefined;
teardownDOM();
});
test('clean scrollable and get filtered cards when bubbling down ' +
'animation ends', function() {
// pretend that we already went through 'onFilterChanged'
filterManager._isFilterChanging = true;
cardListElem.setAttribute('smart-bubbles-direction', 'down');
filterManager._smartBubblesElem.dispatchEvent(
new CustomEvent('all-items-bubbled'));
assert.isTrue(mockXScrollable.clean.calledOnce);
assert.isTrue(mockCardManager.getFilteredCardList.calledOnce);
});
test('reset _isFilterChanging to false when bubbling up animation ends',
function() {
// pretend that we already went through 'onFilterChanged'
filterManager._isFilterChanging = true;
cardListElem.removeAttribute('smart-bubbles-direction');
filterManager._smartBubblesElem.dispatchEvent(
new CustomEvent('all-items-bubbled'));
assert.isFalse(mockXScrollable.clean.calledOnce);
assert.isFalse(mockCardManager.getFilteredCardList.calledOnce);
});
});
});

View file

@ -0,0 +1,144 @@
'use strict';
/* global Deck, Folder, Application */
require('/bower_components/evt/index.js');
require('/shared/js/uuid.js');
require('/shared/js/smart-screen/cards/card.js');
require('/shared/js/smart-screen/cards/application.js');
require('/shared/js/smart-screen/cards/deck.js');
require('/shared/js/smart-screen/cards/folder.js');
suite('Folder', function() {
var folder;
var stubFolder;
var nonEmptyFolder;
var apps;
var deck;
var eventSpy;
setup(function() {
apps = {
music: new Application({
name: 'Music',
nativeApp: {
name: 'Music',
removable: false,
manifest: {},
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'
}
}),
video: new Application({
name: 'Video',
nativeApp: {
name: 'Video',
removable: false,
manifest: {},
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'
}
}),
gallery: new Application({
name: 'Gallery',
nativeApp: {
name: 'Gallery',
removable: false,
manifest: {},
manifestURL: 'app://gallery.gaiamobile.org/manifest.webapp'
}
})
};
folder = new Folder({
name: 'folder under test'
});
deck = new Deck({
nativeApp: {},
name: 'a deck'
});
stubFolder = new Folder({
name: 'folder as stub'
});
nonEmptyFolder = new Folder({
name: 'non empty',
_cardsInFolder: [apps.music, apps.video, apps.gallery]
});
eventSpy = this.sinon.spy();
nonEmptyFolder.on('folder-changed', eventSpy);
nonEmptyFolder.on('card-inserted', eventSpy);
nonEmptyFolder.on('card-removed', eventSpy);
nonEmptyFolder.on('card-updated', eventSpy);
nonEmptyFolder.on('card-swapped', eventSpy);
});
teardown(function() {
folder = undefined;
deck = undefined;
stubFolder = undefined;
nonEmptyFolder = undefined;
apps = undefined;
eventSpy = undefined;
});
var assertFolderIsEmpty = function(target) {
assert.isTrue(target._cardsInFolder.length === 0);
};
var assertFolderIsDirty = function(target) {
assert.equal(target.state, Folder.STATES.DIRTY);
};
test('should be able to add deck into folder', function() {
assertFolderIsEmpty(folder);
folder.addCard(deck);
assert.equal(folder.getCardList()[0], deck);
assertFolderIsDirty(folder);
});
test('should not be able to add folder into folder', function() {
assertFolderIsEmpty(stubFolder);
folder.addCard(stubFolder);
assertFolderIsEmpty(stubFolder);
assert.equal(stubFolder.state, Folder.STATES.NORMAL);
});
test('should be able to remove deck from folder', function() {
folder.getCardList()[0] = deck;
folder.removeCard(deck);
assertFolderIsEmpty(folder);
assertFolderIsDirty(folder);
});
test('should be able to update card in folder', function() {
var index = 0;
var cardToBeUpdated = nonEmptyFolder.getCardList()[index];
cardToBeUpdated.name = 'new gallery';
assert.isFalse(eventSpy.called);
nonEmptyFolder.updateCard(cardToBeUpdated, index);
assert.equal(
nonEmptyFolder.getCardList()[0].cardId, cardToBeUpdated.cardId);
assert.equal(nonEmptyFolder.getCardList()[0].name, cardToBeUpdated.name);
assert.isTrue(eventSpy.calledWithExactly(nonEmptyFolder));
assert.isTrue(eventSpy.calledWithExactly(cardToBeUpdated, index));
assertFolderIsDirty(nonEmptyFolder);
});
test('should be able to swap cards in folder', function() {
var card1 = nonEmptyFolder.getCardList()[0];
var card2 = nonEmptyFolder.getCardList()[2];
assert.isFalse(eventSpy.called);
nonEmptyFolder.swapCard(card1, card2);
assert.equal(nonEmptyFolder.getCardList()[0], card2);
assert.equal(nonEmptyFolder.getCardList()[2], card1);
assert.isTrue(eventSpy.calledWithExactly(nonEmptyFolder));
assert.isTrue(eventSpy.calledWithExactly(card2, card1, 0, 2));
assertFolderIsDirty(nonEmptyFolder);
});
});

View file

@ -0,0 +1,359 @@
'use strict';
/* global Folder, navigator, MockL10n, document, MocksHelper,
XScrollable, MozActivity, SpatialNavigator, CardManager, MessageHandler,
Home, SearchBar, FilterManager, Edit, Utils, Application, MockMozActivity,
FTEWizard */
require('/bower_components/evt/index.js');
require('/shared/js/uuid.js');
require('/shared/js/smart-screen/cards/card.js');
require('/shared/js/smart-screen/cards/application.js');
require('/shared/js/smart-screen/cards/deck.js');
require('/shared/js/smart-screen/cards/folder.js');
require('/shared/js/smart-screen/cards/app_bookmark.js');
require('/shared/js/smart-screen/clock.js');
require('/shared/test/unit/mocks/mock_event_target.js');
require('mock_card_manager.js');
require('mock_search_bar.js');
require('mock_menu_group.js');
require('mock_message_handler.js');
require('mock_x_scrollable.js');
require('mock_card_filter.js');
require('mock_edit.js');
require('mock_animations.js');
require('mock_utils.js');
require('mock_filter_manager.js');
require('/shared/test/unit/mocks/smart-screen/mock_spatial_navigator.js');
require('/shared/test/unit/mocks/smart-screen/mock_key_navigation_adapter.js');
require('/shared/test/unit/mocks/smart-screen/mock_f_t_e_wizard.js');
require('/shared/test/unit/mocks/mock_l10n.js');
require('/shared/test/unit/mocks/mocks_helper.js');
require('/shared/test/unit/mocks/mock_moz_activity.js');
require('/tv_apps/smart-home/js/home.js');
var mocksHelperForHomeTest = new MocksHelper([
'CardManager',
'CardFilter',
'SearchBar',
'MessageHandler',
'XScrollable',
'SpatialNavigator',
'KeyNavigationAdapter',
'Animations',
'Edit',
'MozActivity',
'Utils',
'FilterManager',
'FTEWizard'
]).init();
suite('home', function() {
var realL10n = navigator.mozL10n;
var realMozActivity = window.MozActivity;
var searchButton, settingsButton, editButton, timeElem;
var filterTabGroup, filterAllButton;
var subject;
var fakeTimer;
mocksHelperForHomeTest.attachTestHelpers();
suiteSetup(function() {
realL10n = navigator.mozL10n;
navigator.mozL10n = MockL10n;
searchButton = document.createElement('button');
searchButton.id = 'search-button';
document.body.appendChild(searchButton);
settingsButton = document.createElement('button');
settingsButton.id = 'settings-button';
document.body.appendChild(settingsButton);
editButton = document.createElement('button');
editButton.id = 'edit-button';
document.body.appendChild(editButton);
timeElem = document.createElement('div');
timeElem.id = 'time';
document.body.appendChild(timeElem);
filterTabGroup = document.createElement('menu-group');
filterTabGroup.once = function() {};
filterTabGroup.open = function() {};
filterTabGroup.close = function() {};
filterTabGroup.id = 'filter-tab-group';
filterAllButton = document.createElement('smart-button');
filterAllButton.id = 'filter-all-button';
filterTabGroup.appendChild(filterAllButton);
document.body.appendChild(filterTabGroup);
});
suiteTeardown(function() {
navigator.mozL10n = realL10n;
document.body.removeChild(searchButton);
document.body.removeChild(settingsButton);
document.body.removeChild(editButton);
document.body.removeChild(timeElem);
document.body.removeChild(filterTabGroup);
});
setup(function() {
subject = new Home();
subject.cardScrollable = new XScrollable();
subject.folderScrollable = new XScrollable();
subject.messageHandler = new MessageHandler();
subject.spatialNavigator = new SpatialNavigator();
subject.cardManager = new CardManager();
subject.searchBar = new SearchBar();
subject.filterManager = new FilterManager();
subject._fteWizard = new FTEWizard();
subject.edit = new Edit();
subject.cardListElem = document.createElement('div');
subject.spatialNavigator.m_focusedElement = document.createElement('div');
subject.isNavigable = true;
subject.settingsButton = settingsButton;
subject.editButton = editButton;
subject.searchButton = searchButton;
fakeTimer = this.sinon.useFakeTimers();
window.MozActivity = MockMozActivity;
});
teardown(function() {
window.MozActivity = realMozActivity;
});
suite('visibilityChange > ', function() {
suite('visible > ', function() {
setup(function() {
Object.defineProperty(document, 'visibilityState', {
value: 'visible',
writeable: true,
configurable: true
});
});
test('should set focus back on current focused element',
function(done) {
subject.spatialNavigator.focus = done;
subject.onVisibilityChange();
assert.isTrue(Utils.holdFocusForAnimation.calledOnce);
});
test('should start activity', function(done) {
subject.messageHandler.resumeActivity = done;
subject.onVisibilityChange();
});
});
suite('invisible > ', function() {
setup(function() {
Object.defineProperty(document, 'visibilityState', {
value: 'hidden',
writeable: true,
configurable: true
});
});
test('should stop activity when invisible', function() {
var spy = this.sinon.spy(subject.messageHandler, 'stopActivity');
subject.onVisibilityChange();
assert.isTrue(spy.calledOnce);
});
test('should reset filter', function() {
var spy = this.sinon.spy(subject.filterManager, 'resetFilter');
subject.onVisibilityChange();
assert.isTrue(spy.calledOnce);
});
});
});
suite('initClock >', function() {
test('should set time after time elapsed', function() {
subject.initClock();
var stub = this.sinon.stub(subject, 'updateClock');
fakeTimer.tick(1000);
assert.isTrue(stub.calledOnce);
});
});
suite('onCardInserted >', function() {
test('should insert a card into scrollable', function() {
var card1 = new Folder({});
var card2 = new Folder({});
var stub = this.sinon.stub();
var testCase1 = stub.withArgs(sinon.match.truthy, 0);
var testCase2 = stub.withArgs(sinon.match.truthy, 1);
subject.cardScrollable.insertNodeBefore = stub;
subject.onCardInserted(subject.cardScrollable, card1, 0);
assert.isTrue(testCase1.calledOnce);
assert.isFalse(testCase2.calledOnce);
subject.onCardInserted(subject.cardScrollable, card2, 1);
assert.isTrue(testCase2.calledOnce);
});
});
suite('onCardRemoved >', function() {
test('should remove specified cards from scrollable', function() {
var stub = this.sinon.stub();
subject.cardScrollable.removeNodes = stub;
subject.onCardRemoved(subject.cardScrollable, [0, 1]);
assert.isTrue(stub.withArgs([0, 1]).calledOnce);
});
});
suite('onMove >', function() {
test('should ask spatial navigator for next focus on non-scrollable target',
function() {
subject.spatialNavigator.m_focusedElement = document.createElement('div');
var stub = this.sinon.stub();
subject.spatialNavigator.move = stub;
subject.onMove('left');
assert.isTrue(stub.calledWith('left'));
});
test('should ask scrollable to move its focus on scrollable target',
function() {
var scrollable = new XScrollable();
scrollable.move = this.sinon.stub();
scrollable.move.returns(true);
subject.spatialNavigator.m_focusedElement = scrollable;
subject.onMove('right');
assert.isTrue(scrollable.move.calledWith('right'));
});
test('should ask scrollable to move its focus on scrollable target, but ' +
'fall-back to spatial navigator when scrollable didn\'t handle it',
function() {
var scrollable = new XScrollable();
scrollable.move = this.sinon.stub();
scrollable.move.returns(false);
subject.spatialNavigator.m_focusedElement = scrollable;
var spatialNavigatorMoveStub = this.sinon.stub();
subject.spatialNavigator.move = spatialNavigatorMoveStub;
subject.onMove('up');
assert.isTrue(scrollable.move.calledWith('up'));
assert.isTrue(spatialNavigatorMoveStub.calledWith('up'));
});
// TODO: add case
// 'should bypass logic of onMove if edit.onMove returns true'
});
suite('onEnter >', function() {
test('should open settings on settings button', function() {
subject.handleFocus(settingsButton);
assert.equal(MozActivity.calls.length, 0);
subject.onEnter();
assert.equal(MozActivity.calls[0].name, 'configure');
});
test('should toggle edit mode on edit button', function() {
subject.handleFocus(editButton);
subject.edit.toggleEditMode = this.sinon.stub();
subject.onEnter();
assert.isTrue(subject.edit.toggleEditMode.calledOnce);
});
test('should launch application on card ', function() {
var testCard = document.createElement('div');
testCard.dataset.cardId = '123';
var testApp = new Application({});
testApp.launch = this.sinon.stub();
subject.cardManager.m_card['123'] = testApp;
subject.handleFocus(testCard);
subject.onEnter();
assert.isTrue(testApp.launch.calledOnce);
});
// TODO: add case
// 'should bypass logic of onEnter if edit.onEnter returns true'
});
suite('onSearchBarShown >', function() {
setup(function() {
this.sinon.stub(document, 'addEventListener')
.withArgs('visibilitychange').callsArg(1);
subject.searchBar.hide = this.sinon.stub();
});
test('should fire search activity', function() {
assert.equal(MozActivity.calls.length, 0);
subject.onSearchBarShown();
assert.equal(MozActivity.calls[0].name, 'search');
});
test('should hide search bar after visibility has changed', function(done) {
subject.searchBar.hide = function() {
assert.isTrue(true);
done();
};
subject.onSearchBarShown();
});
});
suite('onSearchBarHidden >', function() {
test('should show search button again', function() {
searchButton.classList.add('hidden');
subject.onSearchBarHidden();
assert.isFalse(searchButton.classList.contains('hidden'));
});
});
suite('handleFocus >', function() {
test('should switch focus to scrollable target', function() {
var spy = this.sinon.spy(subject.cardScrollable, 'focus');
subject.handleFocus(subject.cardScrollable);
assert.isTrue(spy.calledOnce);
});
test('should switch focus to next focus for normal target', function() {
var spy = this.sinon.spy(editButton, 'focus');
subject.handleFocus(subject.editButton);
assert.isTrue(spy.calledOnce);
});
test('should open menu for menu group target', function() {
var stub = this.sinon.stub(subject, 'handleFocusMenuGroup');
subject.handleFocus(filterTabGroup);
assert.isTrue(stub.calledOnce);
});
test('should call checkFocusedGroup for non-menu elements', function() {
var stub = this.sinon.stub(subject, 'checkFocusedGroup');
subject.handleFocus(settingsButton);
assert.isTrue(stub.withArgs(settingsButton).calledOnce);
});
});
suite('checkFocusedGroup >', function() {
test('should close group menu when blur', function() {
subject.handleFocusMenuGroup(filterTabGroup);
var stub = this.sinon.stub(filterTabGroup, 'close');
subject.checkFocusedGroup();
assert.isTrue(stub.calledOnce);
});
});
// TODO: Test items:
// handleFocusMenuGroup >
// should switch navigable target to child elements of menu on open
// should switch navigable target back to menu iteslf on close
// handleScrollableItemFocus >
// should focus specified item of scrollable
// handleScrollableItemUnfocus >
// should blur specified item of scrollable
// openSettings >
// should launch configure activity
// updateClock >
// should update clock display
// restartClock >
// should stop and start clock display again
});

View file

@ -0,0 +1,13 @@
/* globals evt */
(function(exports) {
'use strict';
var MockAnimations = evt({
init: function() {},
doBubbleAnimation: function(parentElem, childClass, length, cb) {
cb();
}
});
exports.MockAnimations = MockAnimations;
})(window);

View file

@ -0,0 +1,48 @@
(function(exports) {
'use strict';
var MockCardFilter = function() {
this.mEventHandlers = {};
};
MockCardFilter.FILTERS = Object.freeze({
'ALL': 'filter',
'TV': 'tv',
'DASHBOARD': 'dashboard',
'DEVICE': 'device',
'APPLICATION': 'application'
});
MockCardFilter.prototype = {
start: function() {},
stop: function() {},
on: function(eventName, callback) {
if (typeof callback === 'function') {
if (!this.mEventHandlers[eventName]) {
this.mEventHandlers[eventName] = [];
}
this.mEventHandlers[eventName].push(callback);
}
},
off: function(eventName, callback) {
var handlers = this.mEventHandlers[eventName];
if (typeof callback === 'function' && handlers) {
var index = handlers.indexOf(callback);
handlers.splice(index, 1);
}
},
mFireEvent: function(eventName, detail) {
var handlers = this.mEventHandlers[eventName];
if (handlers) {
handlers.forEach(function(callback) {
if (typeof callback === 'function') {
callback(detail);
}
});
}
}
};
exports.MockCardFilter = MockCardFilter;
}(window));

View file

@ -0,0 +1,40 @@
/* global Promise */
(function(exports) {
'use strict';
var MockCardManager = function() {};
MockCardManager.prototype = {
_cardList: [],
m_card: {},
init: function() {
return null;
},
mPrepareFilteredCardList: function(filteredCardList) {
this.mFilteredCardList = filteredCardList;
},
getFilteredCardList: function(name) {
var that = this;
return new Promise(function(resolve, reject) {
resolve(that.mFilteredCardList);
});
},
getCardList: function getCardList() {
return Promise.resolve(this._cardList);
},
findCardFromCardList: function(param) {
return this.m_card[param.cardId];
},
resolveCardName: function() {
return 'cardname';
}
};
exports.MockCardManager = MockCardManager;
}(window));

View file

@ -0,0 +1,16 @@
/* globals evt */
(function(exports) {
'use strict';
var MockEdit = function() {
};
MockEdit.prototype = evt({
init: function() {},
onMove: function() { return false; },
onEnter: function() {return false; }
});
exports.MockEdit = MockEdit;
})(window);

View file

@ -0,0 +1,15 @@
(function(exports) {
'use strict';
var MockFilterManager = function() {};
MockFilterManager.prototype = {
init: function() {
return null;
},
resetFilter: function() {}
};
exports.MockFilterManager = MockFilterManager;
}(window));

View file

@ -0,0 +1,15 @@
(function(exports) {
'use strict';
var MockHome = function() {};
MockHome.prototype = {
edit: {
on: function() {},
off: function() {}
},
createCardNode: function() {}
};
exports.MockHome = MockHome;
}(window));

Some files were not shown because too many files have changed in this diff Show more