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

1
gaia/apps/homescreen/.gitignore vendored Executable file
View file

@ -0,0 +1 @@
bower_components/gaia-component

24
gaia/apps/homescreen/bower.json Executable file
View file

@ -0,0 +1,24 @@
{
"name": "Home screen",
"version": "0.0.0",
"homepage": "https://github.com/mozilla-b2g/gaia/tree/master/apps/homescreen",
"authors": [
"Chris Lord <chrislord.net@gmail.com>",
"Guillaume Marty <edo999@gmail.com>"
],
"main": "index.html",
"license": "MIT",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components"
],
"dependencies": {
"base": "fxos-components/base#master",
"evt": "fxos-components/evt#master",
"gaia-container": "https://gitlab.com/Cwiiis/gaia-container.git",
"gaia-dialog": "fxos-components/gaia-dialog#master",
"pressed": "fxos-components/pressed#master"
}
}

View file

@ -0,0 +1,25 @@
{
"name": "base",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "base.css",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"homepage": "https://github.com/gaia-components/base",
"_release": "737e000325",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "737e0003253ee4f3ceff5c1223543a1b3f7d39dd"
},
"_source": "git://github.com/gaia-components/base.git",
"_target": "master",
"_originalSource": "gaia-components/base"
}

View file

@ -0,0 +1,227 @@
/** Variables
---------------------------------------------------------*/
:root {
--dim-micro: 6px;
--dim-small: 12px;
--dim-medium: 24px;
--dim-mega: 48px;
--base-xs: 6px;
--base-s: 18px;
--base-m: 24px;
--base-l: 30px;
--base-xl: 42px;
--base-xxl: 60px;
--base-font-xs: 10px;
--base-font-s: 14px;
--base-font-m: 17px;
--base-font-l: 23px;
--base-font-xl: 60px;
}
/**
* 1. Apply a natural box layout
* model to all elements
*/
*,
*:after,
*:before {
box-sizing: border-box; /* 1 */
font: inherit;
margin: 0;
}
/** Document
---------------------------------------------------------*/
html {
height: 100%;
}
/** Body
---------------------------------------------------------*/
body {
height: 100%;
font: 300 17px/21px 'FiraSans';
background-color: var(--background);
color: var(--text-color);
padding: 0;
}
/** Tags
---------------------------------------------------------*/
a {
color: var(--highlight-color, inherit);
text-decoration: none;
font-style: italic;
}
hr {
height: 1px;
margin: var(--dim-small) 0 var(--dim-small) 0;
border: 0;
background:
var(--border-color,
var(--background-plus,
rgba(0,0,0,0.2)));
}
em {
font-weight: 500;
}
h1,
h2,
h3,
h4,
h5 {
font-weight: 300;
}
.alpha {
font-size: 23px;
font-weight: 200;
font-style: italic;
line-height: 26px;
margin: 6px 0 24px 0;
}
.beta {
font-size: 17px;
font-weight: 400;
margin: 32px 0 16px 0;
}
.gamma {
font-size: 14px;
text-transform: uppercase;
font-weight: normal;
margin: 32px 0 16px 0;
}
.milli {
font-size: 14px;
font-weight: 300;
}
.large {
font-size: 23px;
line-height: 26px;
}
.mega {
font-size: 60px;
line-height: 60px;
font-style: normal;
}
.giga {}
.kilo {}
/* alignment */
.l-align-right { float: right; }
.l-align-left { float: left; }
.l-align-center { text-align: center; }
.l-text-align-left { text-align: left; }
.l-text-align-right { text-align: right; }
.l-text-valign-top {
vertical-align: top;
}
.l-width-full {
width: 100%;
}
/** Flexbox
---------------------------------------------------------*/
.l-flex { display: flex; }
.l-flex-col { display: flex; flex-direction: column; }
.l-flex-col-v-align {
display: flex !important;
flex-direction: column;
justify-content: center;
}
.l-flex-v-align { align-items: center; }
.l-flex-h-align { justify-content: center; }
.l-flex-grow { flex: 1; }
/** Margin
---------------------------------------------------------*/
/* Top */
.l-mt-small { margin-top: var(--dim-small); }
.l-mt-med { margin-top: var(--dim-medium); }
/* Bottom */
.l-mb-small { margin-bottom: var(--dim-small); }
.l-mb-med { margin-bottom: var(--dim-medium); }
.l-mb-large {}
.l-mb-mega { margin-bottom: var(--dim-mega); }
/* Left */
.l-ml-small { margin-left: var(--dim-small); }
/* Right */
.l-mr-small { margin-right: var(--dim-small); }
/* Auto */
.l-mlr-auto { margin-left: auto; margin-right: auto; }
.l-m-auto { margin: auto }
/* Margin */
.l-m-small { margin: var(--dim-small) }
.l-m-med { margin: var(--dim-medium) }
.l-m-0 { margin: 0 }
/** Padding
---------------------------------------------------------*/
.l-p-small { padding: var(--dim-small); }
.l-pl-small { padding-left: var(--dim-small); }
.l-pr-small { padding-right: var(--dim-small); }
.l-plr-small {
padding-right: var(--dim-small);
padding-left: var(--dim-small);
}
.l-pb-small { padding-bottom: var(--dim-small); }
.l-pt-small { padding-top: var(--dim-small); }
.l-pt-mega { padding-top: var(--dim-mega); }
.l-pt-micro { padding-top: var(--dim-micro); }
.l-pb-micro { padding-bottom: var(--dim-micro); }
.l-p-0 { padding: 0 ;}
/** Helpers
---------------------------------------------------------*/
.l-clearfix:after {
content: "";
display: table;
clear: both;
}
.l-inline-block {
display: inline-block;
}
.is-hidden {
display: none !important;
}
.is-invisible {
visibility: hidden !important;
}

View file

@ -0,0 +1,16 @@
{
"name": "base",
"version": "0.0.0",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "base.css",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}

View file

@ -0,0 +1,27 @@
{
"name": "evt",
"main": "index.js",
"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": "3593dd6b4d",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "3593dd6b4dd19f683d00fd0eaf2581c11a128da6"
},
"_source": "git://github.com/gaia-components/evt.git",
"_target": "master",
"_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,12 @@
{
"name": "gaia-container",
"_release": "16513d8946",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "16513d894678a754a5297772c05d4d761e76a240"
},
"_source": "https://gitlab.com/Cwiiis/gaia-container.git",
"_target": "*",
"_originalSource": "https://gitlab.com/Cwiiis/gaia-container.git"
}

View file

@ -0,0 +1 @@
node_modules

View file

@ -0,0 +1,122 @@
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
<title>GaiaContainer Demo</title>
</head>
<script src='../script.js'></script>
<style>
body {
margin: 0;
}
div:first-of-type {
display: flex;
flex-direction: row;
}
div:first-of-type > * {
flex: 1;
}
gaia-container {
width: 100%;
display: flex;
flex-flow: row wrap;
}
gaia-container.alt {
flex-flow: row-reverse wrap-reverse;
}
gaia-container > .gaia-container-child:not(.added):not(.dragging) {
transition: transform 0.2s;
}
gaia-container > .gaia-container-child.dragging {
z-index: 1;
will-change: transform;
}
gaia-container > .gaia-container-child > div {
display: inline-block;
width: 33vw;
height: 33vw;
}
gaia-container > .gaia-container-child:not(.added):not(.removed) > div {
transition: transform 0.2s, opacity 0.2s;
}
gaia-container > .gaia-container-child.dragging > div {
transform: scale(1.1);
opacity: 0.8;
}
gaia-container > .gaia-container-child.added > div {
animation-name: added;
animation-duration: 0.4s;
}
@keyframes added {
0% { opacity: 0; transform: scale(0.5); }
100% { opacity: 100%; }
}
gaia-container > .gaia-container-child.removed > div {
animation-name: removed;
animation-duration: 0.4s;
}
@keyframes removed {
45% { transform: scaleY(0.1); }
55% { transform: scaleY(0.1); }
100% { transform: scale(0); }
}
</style>
<div>
<button onclick='addBox()'>Add item</button>
<button onclick='container.classList.toggle("alt"); container.synchronise()'>Toggle layout</button>
</div>
<gaia-container drag-and-drop></gaia-container>
<script>
'use strict';
var container = document.getElementsByTagName('gaia-container')[0];
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
function addBox() {
var element = document.createElement('div');
element.style.backgroundColor = 'rgb(' +
randomInt(100, 200) + ',' +
randomInt(100, 200) + ',' +
randomInt(100, 200) + ')';
element.onclick = () => {
container.removeChild(element);
};
var children = container.children;
var insertBefore = children.length ?
children[randomInt(0, children.length - 1)] : null
container.insertBefore(element, insertBefore);
}
var resizeTimeout = null;
window.onresize = () => {
if (resizeTimeout !== null) {
window.clearTimeout(resizeTimeout);
}
resizeTimeout = window.setTimeout(() => {
resizeTimeout = null;
container.synchronise();
}, 100);
}
</script>

View file

@ -0,0 +1,29 @@
{
"name": "gaia-container",
"version": "0.0.0",
"description": "Gaia Container is a drop-in replacement for generic container elements that provides convenience methods and behaviour for animations and drag-and-drop functionality.",
"main": "script.js",
"scripts": {
"test": "./node_modules/karma/bin/karma start test/karma.conf.js --single-run",
"test-dev": "./node_modules/karma/bin/karma start test/karma.conf.js"
},
"repository": {
"type": "git",
"url": "git@gitlab.com:Cwiiis/gaia-container.git"
},
"devDependencies": {
"karma": "^0.12.37",
"karma-firefox-launcher": "^0.1.6",
"karma-mocha": "^0.2.0",
"karma-sinon-chai": "^1.0.0",
"mocha": "^2.2.5",
"mozilla-download": "^1.1.1"
},
"keywords": [
"container",
"homescreen",
"B2G",
"Firefox OS"
],
"license": "MIT"
}

View file

@ -0,0 +1,848 @@
'use strict';
window.GaiaContainer = (function(exports) {
/**
* The time, in ms, to wait for an animation to start in response to a state
* change, before removing the associated style class.
*/
const STATE_CHANGE_TIMEOUT = 100;
/**
* The time, in ms, to wait before initiating a drag-and-drop from a
* long-press.
*/
const DEFAULT_DND_TIMEOUT = 300;
/**
* The distance, in CSS pixels, in which a touch or mouse point can deviate
* before it being discarded for initiation of a drag-and-drop action.
*/
const DND_THRESHOLD = 5;
/**
* The minimum time between sending move events during drag-and-drop.
*/
const DND_MOVE_THROTTLE = 50;
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
this._template = template.content.cloneNode(true);
var shadow = this.createShadowRoot();
shadow.appendChild(this._template);
this._frozen = false;
this._pendingStateChanges = [];
this._children = [];
this._dnd = {
// Whether drag-and-drop is enabled
enabled: false,
// The time, in ms, to wait before initiating a drag-and-drop from a
// long-press
delay: DEFAULT_DND_TIMEOUT,
// Timeout used to initiate drag-and-drop actions
timeout: null,
// The child that was tapped/clicked
child: null,
// Whether a drag is active
active: false,
// The start point of the drag action
start: { pageX: 0, pageY: 0, clientX: 0, clientY: 0 },
// The last point of the drag action
last: { pageX: 0, pageY: 0, clientX: 0, clientY: 0, timeStamp: 0 },
// Timeout used to send drag-move events
moveTimeout: null,
// The last time a move event was fired
lastMoveEventTime: 0,
// Whether to capture the next click event
clickCapture: false
};
var dndObserverCallback = (mutations) => {
if (this._dnd.enabled !== this.dragAndDrop) {
this._dnd.enabled = this.dragAndDrop;
if (this._dnd.enabled) {
this.addEventListener('touchstart', this);
this.addEventListener('touchmove', this);
this.addEventListener('touchcancel', this);
this.addEventListener('touchend', this);
this.addEventListener('mousedown', this);
this.addEventListener('mousemove', this);
this.addEventListener('mouseup', this);
this.addEventListener('click', this, true);
this.addEventListener('contextmenu', this, true);
} else {
this.cancelDrag();
this.removeEventListener('touchstart', this);
this.removeEventListener('touchmove', this);
this.removeEventListener('touchcancel', this);
this.removeEventListener('touchend', this);
this.removeEventListener('mousedown', this);
this.removeEventListener('mousemove', this);
this.removeEventListener('mouseup', this);
this.removeEventListener('click', this, true);
this.removeEventListener('contextmenu', this, true);
}
}
};
this.dndObserver = new MutationObserver(dndObserverCallback);
this.dndObserver.observe(this,
{ attributes: true, attributeFilter: ['drag-and-drop'] });
dndObserverCallback(null);
};
Object.defineProperty(proto, 'children', {
get: function() {
return this._children.map((child) => {
return child.element;
});
},
enumerable: true
});
Object.defineProperty(proto, 'firstChild', {
get: function() {
return this._children.length ? this._children[0].element : null;
},
enumerable: true
});
Object.defineProperty(proto, 'lastChild', {
get: function() {
var length = this._children.length;
return length ? this._children[length - 1].element : null;
},
enumerable: true
});
Object.defineProperty(proto, 'dragAndDrop', {
get: function() {
return this.getAttribute('drag-and-drop') !== null;
},
enumerable: true
});
Object.defineProperty(proto, 'dragAndDropTimeout', {
get: function() {
return this._dnd.delay;
},
set: function(timeout) {
if (timeout >= 0) {
this._dnd.delay = timeout;
} else {
this._dnd.delay = DEFAULT_DND_TIMEOUT;
}
},
enumerable: true
});
proto.realAppendChild = proto.appendChild;
proto.appendChild = function(element, callback) {
this.insertBefore(element, null, callback);
};
proto.realRemoveChild = proto.removeChild;
proto.removeChild = function(element, callback) {
var children = this._children;
var childToRemove = null;
for (var child of children) {
if (child.element === element) {
childToRemove = child;
break;
}
}
if (childToRemove === null) {
throw 'removeChild called on unknown child';
}
this.changeState(childToRemove, 'removed', () => {
this.realRemoveChild(childToRemove.container);
this.realRemoveChild(childToRemove.master);
// Find the child again. We need to do this in case the container was
// manipulated between removing the child and this callback being reached.
for (var i = 0, iLen = children.length; i < iLen; i++) {
if (children[i] === childToRemove) {
children.splice(i, 1);
break;
}
}
if (callback) {
callback();
}
this.synchronise();
});
};
proto.realReplaceChild = proto.replaceChild;
proto.replaceChild = function(newElement, oldElement, callback) {
if (!newElement || !oldElement) {
throw 'replaceChild called with null arguments';
}
// Unparent the newElement if necessary (with support for gaia-container)
if (newElement.parentNode) {
newElement.parentNode.removeChild(newElement, () => {
this.replaceChild(newElement, oldElement, callback);
});
if (newElement.parentNode) {
return;
}
}
// Remove the old child and add the new one, but don't run the removed/
// added state changes.
var children = this._children;
for (var i = 0, iLen = children.length; i < iLen; i++) {
var oldChild = children[i];
if (oldChild.element === oldElement) {
var newChild = new GaiaContainerChild(newElement);
this.realInsertBefore(newChild.container, oldChild.container);
this.realInsertBefore(newChild.master, oldChild.master);
this.realRemoveChild(oldChild.container);
this.realRemoveChild(oldChild.master);
this.children.splice(i, 1, newChild);
this.synchronise();
if (callback) {
callback();
}
return;
}
}
throw 'removeChild called on unknown child';
};
/**
* Reorders the given element to appear before referenceElement.
*/
proto.reorderChild = function(element, referenceElement, callback) {
if (!element) {
throw 'reorderChild called with null element';
}
var children = this._children;
var child = null;
var childIndex = null;
var referenceChild = null;
var referenceChildIndex = null;
for (var i = 0, iLen = children.length; i < iLen; i++) {
if (children[i].element === element) {
child = children[i];
childIndex = i;
} else if (children[i].element === referenceElement) {
referenceChild = children[i];
referenceChildIndex = i;
}
if (child && (referenceChild || !referenceElement)) {
this.realRemoveChild(child.container);
this.realRemoveChild(child.master);
children.splice(childIndex, 1);
if (referenceChild) {
this.realInsertBefore(child.container, referenceChild.container);
this.realInsertBefore(child.master, referenceChild.master);
} else {
(children.length === 0) ?
this.realAppendChild(child.container) :
this.realInsertBefore(child.container, children[0].master);
this.realAppendChild(child.master);
}
referenceChild ?
children.splice(
referenceChildIndex - (childIndex < referenceChildIndex) ? 1 : 0,
0, child) :
children.splice(children.length, 0, child);
this.synchronise();
if (callback) {
callback();
}
return;
}
}
throw child ? 'reorderChild called on unknown reference element' :
'reorderChild called on unknown child';
};
proto.realInsertBefore = proto.insertBefore;
proto.insertBefore = function(element, reference, callback) {
var children = this._children;
var childToInsert = new GaiaContainerChild(element);
var referenceIndex = -1;
if (reference !== null) {
for (var i = 0, iLen = children.length; i < iLen; i++) {
if (children[i].element === reference) {
referenceIndex = i;
break;
}
}
if (referenceIndex === -1) {
throw 'insertBefore called on unknown child';
}
}
if (referenceIndex === -1) {
(children.length === 0) ?
this.realAppendChild(childToInsert.container) :
this.realInsertBefore(childToInsert.container, children[0].master);
this.realAppendChild(childToInsert.master);
children.push(childToInsert);
} else {
this.realInsertBefore(childToInsert.container,
children[referenceIndex].container);
this.realInsertBefore(childToInsert.master,
children[referenceIndex].master);
children.splice(referenceIndex, 0, childToInsert);
}
this.changeState(childToInsert, 'added', callback);
this.synchronise();
};
/**
* Used to execute a state-change of a child that may possibly be animated.
* @state will be added to the child's class-list. If an animation starts that
* has the same name, that animation will complete and @callback will be
* called. Otherwise, the class will be removed and @callback called on the
* next frame.
*/
proto.changeState = function(child, state, callback) {
// Check that the child is still attached to this parent (can happen if
// the child is removed while frozen).
if (child.container.parentNode !== this) {
return;
}
// Check for a redundant state change.
if (child.container.classList.contains(state)) {
return;
}
// Queue up state change if we're frozen.
if (this._frozen) {
this._pendingStateChanges.push(
this.changeState.bind(this, child, state, callback));
return;
}
var animStart = (e) => {
if (!e.animationName.endsWith(state)) {
return;
}
child.container.removeEventListener('animationstart', animStart);
window.clearTimeout(child[state]);
delete child[state];
var self = this;
child.container.addEventListener('animationend', function animEnd() {
child.container.removeEventListener('animationend', animEnd);
child.container.classList.remove(state);
if (callback) {
callback();
}
});
};
child.container.addEventListener('animationstart', animStart);
child.container.classList.add(state);
child[state] = window.setTimeout(() => {
delete child[state];
child.container.removeEventListener('animationstart', animStart);
child.container.classList.remove(state);
if (callback) {
callback();
}
}, STATE_CHANGE_TIMEOUT);
};
proto.getChildOffsetRect = function(element) {
var children = this._children;
for (var i = 0, iLen = children.length; i < iLen; i++) {
var child = children[i];
if (child.element === element) {
var top = child._lastMasterTop;
var left = child._lastMasterLeft;
var width = child._lastElementWidth;
var height = child._lastElementHeight;
return {
top: top,
left: left,
width: width,
height: height,
right: left + width,
bottom: top + height
};
}
}
throw 'getChildOffsetRect called on unknown child';
};
proto.getChildFromPoint = function(x, y) {
var children = this._children;
for (var parent = this.parentElement; parent;
parent = parent.parentElement) {
x += parent.scrollLeft - parent.offsetLeft;
y += parent.scrollTop - parent.offsetTop;
}
for (var i = 0, iLen = children.length; i < iLen; i++) {
var child = children[i];
if (x >= child._lastMasterLeft &&
y >= child._lastMasterTop &&
x < child._lastMasterLeft + child._lastElementWidth &&
y < child._lastMasterTop + child._lastElementHeight) {
return child.element;
}
}
return null;
};
proto.cancelDrag = function() {
if (this._dnd.timeout !== null) {
clearTimeout(this._dnd.timeout);
this._dnd.timeout = null;
}
if (this._dnd.moveTimeout !== null) {
clearTimeout(this._dnd.moveTimeout);
this._dnd.moveTimeout = null;
}
if (this._dnd.active) {
this._dnd.child.container.classList.remove('dragging');
this._dnd.child.container.style.position = 'absolute';
this._dnd.child.container.style.top = '0';
this._dnd.child.container.style.left = '0';
this._dnd.child.markDirty();
this._dnd.child = null;
this._dnd.active = false;
this.synchronise();
this._dnd.clickCapture = true;
this.dispatchEvent(new CustomEvent('drag-finish'));
}
};
proto.startDrag = function() {
if (!this.dispatchEvent(new CustomEvent('drag-start',
{ cancelable: true,
detail: { target: this._dnd.child.element,
pageX: this._dnd.start.pageX,
pageY: this._dnd.start.pageY,
clientX: this._dnd.start.clientX,
clientY: this._dnd.start.clientY } }))) {
return;
}
this._dnd.active = true;
this._dnd.child.container.classList.add('dragging');
this._dnd.child.container.style.position = 'fixed';
var rect = this.getBoundingClientRect();
this._dnd.child.container.style.top = rect.top + 'px';
this._dnd.child.container.style.left = rect.left + 'px';
};
proto.continueDrag = function() {
if (!this._dnd.active) {
return;
}
var left = this._dnd.child.master.offsetLeft +
(this._dnd.last.pageX - this._dnd.start.pageX);
var top = this._dnd.child.master.offsetTop +
(this._dnd.last.pageY - this._dnd.start.pageY);
this._dnd.child.container.style.transform =
'translate(' + left + 'px, ' + top + 'px)';
if (this._dnd.moveTimeout === null) {
var delay = Math.max(0, DND_MOVE_THROTTLE -
(this._dnd.last.timeStamp - this._dnd.lastMoveEventTime));
this._dnd.moveTimeout = setTimeout(() => {
this._dnd.moveTimeout = null;
this._dnd.lastMoveEventTime = this._dnd.last.timeStamp;
this.dispatchEvent(new CustomEvent('drag-move',
{ detail: { target: this._dnd.child.element,
pageX: this._dnd.last.pageX,
pageY: this._dnd.last.pageY,
clientX: this._dnd.last.clientX,
clientY: this._dnd.last.clientY } }));
}, delay);
}
};
proto.endDrag = function(event) {
if (this._dnd.active) {
var dropTarget = this.getChildFromPoint(this._dnd.last.clientX,
this._dnd.last.clientY);
if (this.dispatchEvent(new CustomEvent('drag-end',
{ cancelable: true,
detail: { target: this._dnd.child.element,
dropTarget: dropTarget,
pageX: this._dnd.last.pageX,
pageY: this._dnd.last.pageY,
clientX: this._dnd.last.clientX,
clientY: this._dnd.last.clientY }}))) {
var children = this._children;
if (dropTarget && dropTarget !== this._dnd.child.element) {
var dropChild = null;
var dropIndex = -1;
var childIndex = -1;
var insertBefore = true;
for (var i = 0, iLen = children.length; i < iLen; i++) {
if (children[i] === this._dnd.child) {
childIndex = i;
if (!dropChild) {
insertBefore = false;
}
}
if (children[i].element === dropTarget) {
dropChild = children[i];
dropIndex = i;
}
if (dropIndex >= 0 && childIndex >= 0) {
break;
}
}
if (dropIndex >= 0 && childIndex >= 0) {
// Default action, rearrange the dragged child to before or after
// the child underneath the touch/mouse point.
this.realRemoveChild(this._dnd.child.container);
this.realRemoveChild(this._dnd.child.master);
this.realInsertBefore(this._dnd.child.container,
insertBefore ? dropChild.container :
dropChild.container.nextSibling);
this.realInsertBefore(this._dnd.child.master,
insertBefore ? dropChild.master :
dropChild.master.nextSibling);
children.splice(dropIndex, 0,
children.splice(childIndex, 1)[0]);
this.dispatchEvent(new CustomEvent('drag-rearrange'));
}
}
}
} else if (this._dnd.timeout !== null) {
var handled = !this.dispatchEvent(new CustomEvent('activate',
{ cancelable : true, detail: { target: this._dnd.child.element } }));
if (handled) {
event.stopImmediatePropagation();
event.preventDefault();
}
}
this.cancelDrag();
};
proto.handleEvent = function(event) {
switch (event.type) {
case 'touchstart':
case 'mousedown':
if (this._dnd.active || this._dnd.timeout) {
this.cancelDrag();
break;
}
if (event instanceof MouseEvent) {
this._dnd.start.pageX = event.pageX;
this._dnd.start.pageY = event.pageY;
this._dnd.start.clientX = event.pageX;
this._dnd.start.clientY = event.pageY;
} else {
this._dnd.start.pageX = event.touches[0].pageX;
this._dnd.start.pageY = event.touches[0].pageY;
this._dnd.start.clientX = event.touches[0].clientX;
this._dnd.start.clientY = event.touches[0].clientY;
}
this._dnd.last.pageX = this._dnd.start.pageX;
this._dnd.last.pageY = this._dnd.start.pageY;
this._dnd.last.clientX = this._dnd.start.clientX;
this._dnd.last.clientY = this._dnd.start.clientY;
this._dnd.last.timeStamp = event.timeStamp;
var target = event.target;
for (; target.parentNode !== this; target = target.parentNode) {
if (target === this || !target.parentNode) {
return;
}
}
// Find the child
var children = this._children;
for (var child of children) {
if (child.container === target) {
this._dnd.child = child;
break;
}
}
if (!this._dnd.child) {
return;
}
if (this._dnd.delay > 0) {
this._dnd.timeout = setTimeout(() => {
this._dnd.timeout = null;
this.startDrag();
}, this._dnd.delay);
} else {
this.startDrag();
}
break;
case 'touchmove':
case 'mousemove':
var pageX, pageY, clientX, clientY;
if (event instanceof MouseEvent) {
pageX = event.pageX;
pageY = event.pageY;
clientX = event.clientX;
clientY = event.clientY;
} else {
pageX = event.touches[0].pageX;
pageY = event.touches[0].pageY;
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
}
if (this._dnd.timeout) {
if (Math.abs(pageX - this._dnd.start.pageX) > DND_THRESHOLD ||
Math.abs(pageY - this._dnd.start.pageY) > DND_THRESHOLD) {
clearTimeout(this._dnd.timeout);
this._dnd.timeout = null;
}
} else if (this._dnd.active) {
event.preventDefault();
this._dnd.last.pageX = pageX;
this._dnd.last.pageY = pageY;
this._dnd.last.clientX = clientX;
this._dnd.last.clientY = clientY;
this._dnd.last.timeStamp = event.timeStamp;
this.continueDrag();
}
break;
case 'touchcancel':
this.cancelDrag();
break;
case 'touchend':
case 'mouseup':
if (this._dnd.active) {
event.preventDefault();
event.stopImmediatePropagation();
}
this.endDrag(event);
break;
case 'click':
if (this._dnd.clickCapture) {
this._dnd.clickCapture = false;
event.preventDefault();
event.stopImmediatePropagation();
}
break;
case 'contextmenu':
if (this._dnd.active || this._dnd.timeout) {
event.stopImmediatePropagation();
event.preventDefault();
}
break;
}
};
/**
* Temporarily disables element position synchronisation. Useful when adding
* multiple elements to the container at the same time, or in quick
* succession.
*/
proto.freeze = function() {
this._frozen = true;
};
/**
* Enables element position synchronisation after freeze() has been called.
*/
proto.thaw = function() {
if (this._frozen) {
this._frozen = false;
for (var callback of this._pendingStateChanges) {
callback();
}
this._pendingStateChanges = [];
this.synchronise();
}
};
/**
* Synchronise positions between the managed container and all children.
* This is called automatically when adding/inserting or removing children,
* but must be called manually if the managed container is manipulated
* outside of these methods (for example, if style properties change, or
* if it's resized).
*/
proto.synchronise = function() {
if (this._frozen) {
return;
}
var child;
for (child of this._children) {
if (!this._dnd.active || child !== this._dnd.child) {
child.synchroniseMaster();
}
}
for (child of this._children) {
if (!this._dnd.active || child !== this._dnd.child) {
child.synchroniseContainer();
}
}
};
var template = document.createElement('template');
template.innerHTML =
`<style> :host { position: relative; display: block; }</style>` +
`<content select='*'></content>`;
function GaiaContainerChild(element) {
this._element = element;
this.markDirty();
}
GaiaContainerChild.prototype = {
get element() {
return this._element;
},
/**
* The element that will contain the child element and control its position.
*/
get container() {
if (!this._container) {
// Create container
var container = document.createElement('div');
container.classList.add('gaia-container-child');
container.style.position = 'absolute';
container.style.top = '0';
container.style.left = '0';
container.appendChild(this.element);
this._container = container;
}
return this._container;
},
/**
* The element that will be added to the container that will
* control the element's transform.
*/
get master() {
if (!this._master) {
// Create master
var master = document.createElement('div');
master.style.visibility = 'hidden';
this._master = master;
}
return this._master;
},
/**
* Clears any cached style properties. To be used if elements are
* manipulated outside of the methods of this object.
*/
markDirty() {
this._lastElementWidth = null;
this._lastElementHeight = null;
this._lastElementDisplay = null;
this._lastElementOrder = null;
this._lastMasterTop = null;
this._lastMasterLeft = null;
},
/**
* Synchronise the size of the master with the managed child element.
*/
synchroniseMaster() {
var master = this.master;
var element = this.element;
var style = window.getComputedStyle(element);
var display = style.display;
var order = style.order;
var width = element.offsetWidth;
var height = element.offsetHeight;
if (this._lastElementWidth !== width ||
this._lastElementHeight !== height ||
this._lastElementDisplay !== display ||
this._lastElementOrder !== order) {
this._lastElementWidth = width;
this._lastElementHeight = height;
this._lastElementDisplay = display;
this._lastElementOrder = order;
master.style.width = width + 'px';
master.style.height = height + 'px';
master.style.display = display;
master.style.order = order;
}
},
/**
* Synchronise the container's transform with the position of the master.
*/
synchroniseContainer() {
var master = this.master;
var container = this.container;
var top = master.offsetTop;
var left = master.offsetLeft;
if (this._lastMasterTop !== top ||
this._lastMasterLeft !== left) {
this._lastMasterTop = top;
this._lastMasterLeft = left;
container.style.transform = 'translate(' + left + 'px, ' + top + 'px)';
}
}
};
exports.GaiaContainerChild = GaiaContainerChild;
return document.registerElement('gaia-container', { prototype: proto });
})(window);

View file

@ -0,0 +1,28 @@
/* global module */
// Karma configuration
'use strict';
module.exports = function(config) {
config.set({
frameworks: ['mocha', 'sinon-chai'],
client: {
captureConsole: true,
mocha: {'ui': 'tdd'}
},
basePath: '../',
files: [
'script.js',
'test/test.js'
],
browsers: ['FirefoxLatest'],
customLaunchers: {
FirefoxLatest: {
base: 'FirefoxNightly',
prefs: {'dom.webcomponents.enabled': true}
}
}
});
};

View file

@ -0,0 +1,194 @@
/* global sinon, assert, suite, setup, teardown, test */
'use strict';
suite('GaiaContainer', () => {
var sinonSandbox, dom, el;
setup(() => {
sinonSandbox = sinon.sandbox.create();
// DOM container to put test cases
dom = document.createElement('div');
dom.innerHTML = '<gaia-container></gaia-container>';
el = dom.firstElementChild;
document.body.appendChild(dom);
});
teardown(() => {
sinonSandbox.restore();
document.body.removeChild(dom);
dom = null;
});
suite('Drag and drop feature', () => {
test('drag-and-drop attribute should not be set by default', () => {
assert.equal(el.getAttribute('drag-and-drop'), null);
assert.equal(el.hasAttribute('drag-and-drop'), false);
assert.equal(el.dragAndDrop, false);
});
test('.dragAndDrop property value should change with the attribute', () => {
dom.innerHTML = '<gaia-container drag-and-drop></gaia-container>';
el = dom.firstElementChild;
assert.equal(el.getAttribute('drag-and-drop'), '');
assert.equal(el.hasAttribute('drag-and-drop'), true);
assert.equal(el.dragAndDrop, true);
el.removeAttribute('drag-and-drop');
assert.equal(el.getAttribute('drag-and-drop'), null);
assert.equal(el.hasAttribute('drag-and-drop'), false);
assert.equal(el.dragAndDrop, false);
});
test('.dragAndDropTimeout should only accept positive value', () => {
var otherValue = 9999;
assert.equal(typeof el.dragAndDropTimeout, 'number');
el.dragAndDropTimeout = otherValue;
assert.equal(el.dragAndDropTimeout, otherValue);
el.dragAndDropTimeout = 0;
el.dragAndDropTimeout = -5555;
assert.equal(el.dragAndDropTimeout, otherValue);
});
suite('Events', () => {
var icon;
setup(() => {
icon = document.createElement('div');
el.appendChild(icon);
el.dragAndDropTimeout = 1;
});
test('drag-start', done => {
el.handleEvent({
type: 'touchstart',
target: icon,
touches: [{
pageX: 0,
pageY: 0,
clientX: 0,
clientY: 0
}]
});
el.addEventListener('drag-start', () => {
done();
});
});
test('drag-start resets last-drag', done => {
el._dnd.last.pageX = el._dnd.last.pageY =
el._dnd.last.clientX = el._dnd.last.clientY =
el._dnd.last.timeStamp = 100;
el.handleEvent({
type: 'touchstart',
target: icon,
timeStamp: 0,
touches: [{
pageX: 0,
pageY: 0,
clientX: 0,
clientY: 0
}]
});
el.addEventListener('drag-start', () => {
assert.equal(el._dnd.last.pageX, 0);
assert.equal(el._dnd.last.pageY, 0);
assert.equal(el._dnd.last.clientX, 0);
assert.equal(el._dnd.last.clientY, 0);
assert.equal(el._dnd.last.timeStamp, 0);
done();
});
});
});
});
suite('DOM manipulation', () => {
var nodeA, nodeB;
setup(() => {
nodeA = document.createElement('div');
nodeB = document.createElement('div');
el.appendChild(nodeA);
});
test('should add and remove child nodes', done => {
assert.equal(el.children.length, 1);
el.appendChild(nodeB);
assert.equal(el.children.length, 2);
assert.equal(el.firstChild, nodeA);
assert.equal(el.lastChild, nodeB);
el.removeChild(nodeB, () => {
assert.equal(el.children.length, 1);
assert.equal(el.firstChild, nodeA);
assert.equal(el.lastChild, nodeA);
done();
});
});
test('.replaceChild()', done => {
el.appendChild(nodeB);
el.replaceChild(nodeB, nodeA, () => {
assert.equal(el.children.length, 2);
assert.equal(el.firstChild, nodeA);
assert.equal(el.lastChild, nodeB);
done();
});
});
test('.insertBefore()', done => {
el.insertBefore(nodeB, nodeA, () => {
assert.equal(el.children.length, 2);
assert.equal(el.firstChild, nodeB);
assert.equal(el.lastChild, nodeA);
done();
});
});
test('.reorderChild()', done => {
el.appendChild(nodeB);
el.reorderChild(nodeB, nodeA, () => {
assert.equal(el.children.length, 2);
assert.equal(el.firstChild, nodeB);
assert.equal(el.lastChild, nodeA);
el.reorderChild(nodeB, null, () => {
assert.equal(el.children.length, 2);
assert.equal(el.firstChild, nodeA);
assert.equal(el.lastChild, nodeB);
done();
});
});
});
});
suite('Utility functions', () => {
test('.getBoundingClientRect()', done => {
var nodeA = document.createElement('div');
nodeA.style.width = '200px';
nodeA.style.height = '100px';
el.style.paddingLeft = '100px';
el.style.paddingTop = '50px';
el.appendChild(nodeA, () => {
var rect = el.getChildOffsetRect(nodeA);
assert.equal(rect.width, 200);
assert.equal(rect.height, 100);
assert.equal(rect.left, 100);
assert.equal(rect.top, 50);
assert.equal(rect.right, 300);
assert.equal(rect.bottom, 150);
done();
});
});
});
});

View file

@ -0,0 +1,40 @@
{
"name": "gaia-dialog",
"homepage": "https://github.com/gaia-components/gaia-dialog",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-dialog.js",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"index.html",
"package.json",
"README.md"
],
"devDependencies": {
"base": "gaia-components/base",
"pressed": "gaia-components/pressed",
"gaia-fonts": "gaia-components/gaia-fonts",
"gaia-theme": "gaia-components/gaia-theme",
"gaia-button": "gaia-components/gaia-button"
},
"dependencies": {
"gaia-text-input": "gaia-components/gaia-text-input",
"gaia-icons": "gaia-components/gaia-icons#^0.10.2",
"gaia-component": "gaia-components/gaia-component#^0.3.9"
},
"_release": "0bf523d279",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "0bf523d2791cea3649ac36983699cb5ee410a19a"
},
"_source": "git://github.com/gaia-components/gaia-dialog.git",
"_target": "master",
"_originalSource": "gaia-components/gaia-dialog"
}

View file

@ -0,0 +1,32 @@
{
"name": "gaia-dialog",
"version": "0.2.3",
"homepage": "https://github.com/gaia-components/gaia-dialog",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-dialog.js",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"index.html",
"package.json",
"README.md"
],
"devDependencies": {
"base": "gaia-components/base",
"pressed": "gaia-components/pressed",
"gaia-fonts": "gaia-components/gaia-fonts",
"gaia-theme": "gaia-components/gaia-theme",
"gaia-button": "gaia-components/gaia-button"
},
"dependencies": {
"gaia-text-input": "gaia-components/gaia-text-input",
"gaia-icons": "gaia-components/gaia-icons#^0.10.2",
"gaia-component": "gaia-components/gaia-component#^0.3.9"
}
}

View file

@ -0,0 +1,115 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-action', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog'),
submit: this.shadowRoot.querySelector('.submit'),
cancel: this.shadowRoot.querySelector('.cancel')
};
this.els.dialog.addEventListener('closed',
GaiaDialogProto.hide.bind(this));
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
// First close (hide) inner dialog and then the container.
return this.els.dialog.close()
.then(GaiaDialogProto.hide.bind(this));
},
template: `
<gaia-dialog>
<section>
<content select="h1"></content>
</section>
<content select="button"></content>
<button on-click="close" class="cancel">Cancel</button>
</gaia-dialog>
<style>
:host {
display: none;
}
:host[opened],
:host.animating {
display: block;
position: fixed;
width: 100%;
height: 100%;
}
/** Button
---------------------------------------------------------*/
::content section {
padding: 33px 16px;
}
::content button {
position: relative;
display: block;
width: 100%;
height: 50px;
margin: 0;
border: 0;
padding: 0rem 25px;
font: inherit;
background: var(--color-beta);
color: var(--color-epsilon);
transition: all 200ms;
transition-delay: 300ms;
}
::content button:after {
content: '';
display: block;
position: absolute;
height: 1px;
left: 6px;
right: 6px;
top: 49px;
background: #E7E7E7;
}
::content button:last-child:after {
display: none;
}
::content button:active {
background-color: var(--highlight-color);
color: #fff;
transition: none;
}
::content button:active:after {
background: var(--highlight-color);
transition: none;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-action',this));

View file

@ -0,0 +1,59 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-alert', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog')
};
this.els.dialog.addEventListener('closed',
GaiaDialogProto.hide.bind(this));
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
return this.els.dialog.close()
.then(GaiaDialogProto.hide.bind(this));
},
template: `
<gaia-dialog>
<section>
<p><content></content></p>
</section>
<div>
<button class="submit primary" on-click="close">Ok</button>
</div>
</gaia-dialog>
<style>
:host {
display: none;
position: fixed;
width: 100%;
height: 100%;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-alert',this));

View file

@ -0,0 +1,70 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-confirm', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog'),
submit: this.shadowRoot.querySelector('.submit'),
cancel: this.shadowRoot.querySelector('.cancel')
};
this.els.cancel.addEventListener('click', this.close.bind(this));
this.els.submit.addEventListener('click', this.close.bind(this));
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
// First close (hide) inner dialog and then the container.
return this.els.dialog.close()
.then(GaiaDialogProto.hide.bind(this));
},
template: `
<gaia-dialog>
<section>
<p><content></content></p>
</section>
<fieldset>
<button class="cancel">Cancel</button>
<button class="submit danger">Confirm</button>
</fieldset>
</gaia-dialog>
<style>
:host {
display: none;
}
:host[opened],
:host.animating {
display: block;
position: fixed;
width: 100%;
height: 100%;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-confirm',this));

View file

@ -0,0 +1,104 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-menu', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog'),
items: this.shadowRoot.querySelector('.items')
};
this.els.dialog.addEventListener('closed',
GaiaDialogProto.hide.bind(this));
setTimeout(() => this.makeAccessible());
},
makeAccessible() {
this.els.items.setAttribute('role', 'menu');
[].forEach.call(this.querySelectorAll('button'),
menuitem => menuitem.setAttribute('role', 'menuitem'));
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
return this.els.dialog.close()
.then(GaiaDialogProto.hide.bind(this));
},
template: `
<gaia-dialog>
<div class="items"><content select="button"></content></div>
</gaia-dialog>
<style>
:host {
display: none;
position: fixed;
width: 100%;
height: 100%;
}
::content > button {
position: relative;
display: block;
width: 100%;
height: 50px;
line-height: 51px;
margin: 0;
border: 0;
padding: 0rem 16px;
font: inherit;
font-style: italic;
text-align: start;
background: var(--color-beta);
color: var(--highlight-color);
}
::content > button[data-icon]:before {
width: 50px;
font-size: 22px;
vertical-align: middle;
}
/** Button Divider Line
---------------------------------------------------------*/
::content > button:after {
content: '';
display: block;
position: absolute;
height: 1px;
left: 6px;
right: 6px;
top: 49px;
background: #E7E7E7;
}
::content > button:last-of-type:after {
display: none;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-menu',this));

View file

@ -0,0 +1,83 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
require('gaia-text-input');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-prompt', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog'),
input: this.shadowRoot.querySelector('gaia-text-input'),
submit: this.shadowRoot.querySelector('.submit'),
cancel: this.shadowRoot.querySelector('.cancel')
};
this.els.dialog.addEventListener('opened', () => {
this.setAttribute('opened', '');
});
this.els.dialog.addEventListener('closed', () => {
this.removeAttribute('opened');
});
this.els.input.placeholder = this.firstChild.textContent;
this.els.cancel.addEventListener('click', this.close.bind(this));
this.els.submit.addEventListener('click', this.close.bind(this));
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
return this.els.dialog.close()
.then(GaiaDialogProto.hide.bind(this));
},
template: `
<gaia-dialog>
<div><gaia-text-input></gaia-text-input></div>
<fieldset>
<button class="cancel">Cancel</button>
<button class="submit primary">Ok</button>
</fieldset>
</gaia-dialog>
<style>
:host {
display: none;
}
:host[opened],
:host.animating {
display: block;
position: fixed;
width: 100%;
height: 100%;
}
gaia-text-input {
margin: 16px !important;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-prompt',this));

View file

@ -0,0 +1,247 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-select', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog'),
submit: this.shadowRoot.querySelector('.submit'),
cancel: this.shadowRoot.querySelector('.cancel'),
list: this.shadowRoot.querySelector('ul')
};
this.multiple = this.hasAttribute('multiple');
this.els.list.addEventListener('click', this.onListClick.bind(this));
this.els.submit.addEventListener('click', this.close.bind(this));
this.els.cancel.addEventListener('click', this.close.bind(this));
setTimeout(() => this.makeAccessible());
},
makeAccessible() {
this.els.list.setAttribute('role', 'listbox');
if (this.multiple) {
this.els.list.setAttribute('aria-multiselectable', true);
}
[].forEach.call(this.options, option => {
option.setAttribute('role', 'option');
option.setAttribute('tabindex', '0');
});
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
return this.els.dialog.close()
.then(GaiaDialogProto.hide.bind(this));
},
onListClick: function(e) {
var el = e.target.closest('li');
var selected = el.getAttribute('aria-selected') === 'true';
if (this.multiple) { this.onChangeMultiple(el, selected); }
else { this.onChange(el, selected); }
},
onChange: function(el, selected) {
this.clearSelected();
if (!selected) { el.setAttribute('aria-selected', !selected); }
this.fireChange();
this.close();
},
onChangeMultiple: function(el, selected) {
el.setAttribute('aria-selected', !selected);
this.fireChange();
},
clearSelected: function() {
[].forEach.call(this.selectedOptions, function(option) {
option.removeAttribute('aria-selected');
});
},
fireChange: function() {
var e = new CustomEvent('change', { detail: { value: this.valueString }});
this.dispatchEvent(e);
},
attrs: {
multiple: {
get: function() { return !!this._multiple; },
set: function(value) {
value = value === '' ? true : value;
if (value === this._multiple) { return; }
if (!value) {
this._multiple = false;
this.removeAttribute('multiple');
this.els.list.removeAttribute('aria-multiselectable');
} else {
this._multiple = true;
this.setAttribute('multiple', '');
this.els.list.setAttribute('aria-multiselectable', true);
}
}
},
options: {
get: function() { return this.querySelectorAll('li'); }
},
selectedOptions: {
get: function() {
return this.querySelectorAll('li[aria-selected="true"]');
}
},
selected: {
get: function() { return this.selectedOptions[0]; }
},
value: {
get: function() {
var selected = this.selectedOptions[0];
return selected && selected.getAttribute('value');
}
},
valueString: {
get: function() {
var selected = this.selected;
return selected && selected.textContent;
}
},
length: {
get: function() { return this.options.length; }
}
},
template: `
<gaia-dialog>
<content select="h1"></content>
<ul><content select="li"></content></ul>
<fieldset>
<button class="cancel">Cancel</button>
<button class="submit primary">Select</button>
</fieldset>
</gaia-dialog>
<style>
:host {
display: none;
position: fixed;
width: 100%;
height: 100%;
}
/** Title (duplicate from gaia-dialog)
---------------------------------------------------------*/
::content h1 {
padding: 16px;
font-size: 23px;
line-height: 26px;
font-weight: 200;
font-style: italic;
color: #858585;
}
/** List
---------------------------------------------------------*/
ul {
display: block;
padding: 0;
margin: 0;
}
/** Options
---------------------------------------------------------*/
::content li {
position: relative;
display: block;
padding: 16px;
text-align: start;
-moz-user-select: none;
cursor: pointer;
outline: 0;
}
::content li[aria-selected='true'] {
color: #00CAF2;
font-weight: normal;
}
::content li:after {
content: '';
display: block;
position: absolute;
height: 1px;
left: 16px;
right: 16px;
bottom: 1px;
background: #E7E7E7;
}
::content li:last-of-type:after {
display: none
}
/** Buttons
---------------------------------------------------------*/
::host:not([multiple]) .submit {
display: none !important;
}
::host:not([multiple]) .cancel {
width: 100% !important;
}
::host:not([multiple]) .cancel:after {
display: none% !important;
}
/** Tick Icon
---------------------------------------------------------*/
::content li[aria-selected='true']:before {
content: 'tick';
position: absolute;
top: 50%; right: 20px;
display: block;
margin-top: -12px;
font-family: 'gaia-icons';
font-style: normal;
font-size: 14px;
font-weight: 500;
text-rendering: optimizeLegibility;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-select',this));

View file

@ -0,0 +1,483 @@
/* global define */
;(function(define){'use strict';define((require,exports,module) => {
/**
* Dependencies
*/
var component = require('gaia-component');
/**
* Simple logger (toggle 0)
*
* @type {Function}
*/
var debug = 0 ? console.log.bind(console) : () => {};
/**
* Use the dom-scheduler if it's around,
* else fallback to fake shim.
*
* @type {Object}
*/
var schedule = (window.scheduler && typeof window.scheduler.mutation === "function" ? window.scheduler : null) || {
mutation: block => Promise.resolve(block()),
transition: (block, el, event, timeout) => {
block();
return after(el, event, timeout || 500);
}
};
/**
* Exports
*/
module.exports = component.register('gaia-dialog', {
created() {
this.setupShadowRoot();
this.els = {
inner: this.shadowRoot.querySelector('.dialog-inner'),
background: this.shadowRoot.querySelector('.background'),
window: this.shadowRoot.querySelector('.window')
};
this.shadowRoot.addEventListener('click', e => this.onClick(e));
setTimeout(() => this.makeAccessible());
},
makeAccessible() {
this.setAttribute('role', 'dialog');
},
onClick(e) {
var el = e.target.closest('[on-click]');
if (!el) { return; }
debug('onClick');
var method = el.getAttribute('on-click');
if (typeof this[method] == 'function') { this[method](); }
},
open(options) {
if (this.isOpen) { return; }
debug('open dialog');
this.isOpen = true;
return this.show()
.then(() => this.animateBackgroundIn(options))
.then(() => this.animateWindowIn())
.then(() => this.dispatch('opened'));
},
close(options) {
if (!this.isOpen) { return; }
debug('close dialog');
this.isOpen = false;
return this.animateWindowOut()
.then(() => this.animateBackgroundOut())
.then(() => this.hide())
.then(() => this.dispatch('closed'));
},
animateBackgroundIn(options) {
if (options) { return this.animateBackgroundInFrom(options); }
var el = this.els.background;
return schedule.transition(() => {
debug('animate background in');
el.classList.remove('animate-out');
el.classList.add('animate-in');
}, el, 'animationend');
},
animateBackgroundOut() {
var el = this.els.background;
return schedule.transition(() => {
debug('animate background out');
el.classList.add('animate-out');
el.classList.remove('animate-in');
}, el, 'animationend')
.then(() => el.style = '');
},
animateBackgroundInFrom(pos) {
var el = this.els.background;
var scale = Math.sqrt(window.innerWidth * window.innerHeight) / 15;
var duration = scale * 9;
return schedule.mutation(() => {
el.classList.add('circular');
el.classList.remove('animate-out');
el.style.transform = `translate(${pos.clientX}px, ${pos.clientY}px)`;
el.style.transitionDuration = duration + 'ms';
el.offsetTop; // Hack, any ideas?
})
.then(() => {
return schedule.transition(() => {
debug('animate background in from', pos);
el.style.transform += ` scale(${scale})`;
el.style.opacity = 1;
}, el, 'transitionend', duration * 1.5);
});
},
show() {
return schedule.mutation(() => {
debug('show');
this.style.display = 'block';
});
},
hide() {
return schedule.mutation(() => {
debug('hide');
this.style.display = 'none';
});
},
animateWindowIn() {
var el = this.els.window;
return schedule.transition(() => {
debug('animate window in');
el.classList.add('animate-in');
el.classList.remove('animate-out');
}, el, 'animationend');
},
animateWindowOut() {
var el = this.els.window;
return schedule.transition(() => {
debug('animate window out');
el.classList.add('animate-out');
el.classList.remove('animate-in');
}, el, 'animationend');
},
dispatch(name) {
this.dispatchEvent(new CustomEvent(name));
},
attrs: {
opened: {
get: function() { return !!this.isOpen; },
set: function(value) {
value = value === '' || value;
if (!value) { this.close(); }
else { this.open(); }
}
}
},
template: `
<div class="dialog-inner">
<div class="background" on-click="close"></div>
<div class="window"><content></content></div>
</div>
<style>
::content * {
box-sizing: border-box;
font-weight: inherit;
font-size: inherit;
}
::content p,
::content h1,
::content h2,
::content h3,
::content h4,
::content button,
::content fieldset {
padding: 0;
margin: 0;
border: 0;
}
:host {
display: none;
position: fixed;
top: 0px; left: 0px;
width: 100%;
height: 100%;
z-index: 200;
font-style: italic;
text-align: center;
overflow: hidden;
}
/** Inner
---------------------------------------------------------*/
.dialog-inner {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
/** Background
---------------------------------------------------------*/
.background {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
opacity: 0;
background: rgba(199,199,199,0.85);
}
/**
* .circular
*/
.background.circular {
width: 40px;
height: 40px;
margin: -20px;
border-radius: 50%;
will-change: transform, opacity;
transition-property: opacity, transform;
transition-timing-function: linear;
}
/**
* .animate-in
*/
.background.animate-in {
animation-name: gaia-dialog-fade-in;
animation-duration: 260ms;
animation-fill-mode: forwards;
}
/**
* .animate-out
*/
.background.animate-out {
animation-name: gaia-dialog-fade-out;
animation-duration: 260ms;
animation-fill-mode: forwards;
opacity: 1;
}
/** Window
---------------------------------------------------------*/
.window {
position: relative;
width: 90%;
max-width: 350px;
margin: auto;
box-shadow: 0 1px 0 0px rgba(0,0,0,0.15);
background: var(--color-iota);
transition: opacity 300ms;
opacity: 0;
}
.window.animate-in {
animation-name: gaia-dialog-entrance;
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.275);
animation-fill-mode: forwards;
opacity: 1;
}
.window.animate-out {
animation-name: gaia-dialog-fade-out;
animation-duration: 150ms;
animation-timing-function: linear;
animation-fill-mode: forwards;
opacity: 1;
}
/** Title
---------------------------------------------------------*/
::content h1 {
padding: 16px;
font-size: 23px;
line-height: 26px;
font-weight: 200;
font-style: italic;
color: #858585;
}
::content strong {
font-weight: 700;
}
::content small {
font-size: 0.8em;
}
/** Section
---------------------------------------------------------*/
::content section {
padding: 33px 18px;
color: #858585;
}
::content section > *:not(:last-child) {
margin-bottom: 13px;
}
/** Paragraphs
---------------------------------------------------------*/
::content p {
text-align: -moz-start;
}
/** Buttons
---------------------------------------------------------*/
::content button {
position: relative;
display: block;
width: 100%;
height: 50px;
margin: 0;
border: 0;
padding: 0rem 16px;
cursor: pointer;
font: inherit;
background: var(--color-beta);
color: var(--color-epsilon);
transition: all 200ms;
transition-delay: 300ms;
border-radius: 0;
}
/**
* .primary
*/
::content button.primary {
color: var(--highlight-color);
}
/**
* .danger
*/
::content button.danger {
color: var(--color-destructive);
}
/**
* Disabled buttons
*/
::content button[disabled] {
color: var(--color-zeta);
}
/** Button Divider Line
---------------------------------------------------------*/
::content button:after {
content: '';
display: block;
position: absolute;
height: 1px;
left: 6px;
right: 6px;
top: 49px;
background: #E7E7E7;
}
::content button:last-of-type:after {
display: none;
}
::content button:active {
background-color: var(--highlight-color);
color: #fff;
transition: none;
}
::content button:active:after {
background: var(--highlight-color);
transition: none;
}
::content button[data-icon]:before {
float: left;
}
/** Fieldset (button group)
---------------------------------------------------------*/
::content fieldset {
overflow: hidden;
}
::content fieldset button {
position: relative;
float: left;
width: 50%;
}
::content fieldset button:after {
content: '';
display: block;
position: absolute;
top: 6px;
bottom: 6px;
right: 0px;
left: auto;
width: 1px;
height: calc(100% - 12px);
background: #e7e7e7;
transition: all 200ms;
transition-delay: 200ms;
}
</style>`,
globalCss: `
@keyframes gaia-dialog-entrance {
0% { transform: translateY(100px); }
100% { transform: translateY(0px); }
}
@keyframes gaia-dialog-fade-in {
0% { opacity: 0 }
100% { opacity: 1 }
}
@keyframes gaia-dialog-fade-out {
0% { opacity: 1 }
100% { opacity: 0 }
}`
});
/**
* Utils
*/
function after(target, event, timeout) {
return new Promise(resolve => {
var timer = timeout && setTimeout(cb, timeout);
target.addEventListener(event, cb);
function cb() {
target.removeEventListener(event, cb);
clearTimeout(timer);
resolve();
}
});
}
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog',this));

View file

@ -0,0 +1,39 @@
{
"name": "gaia-icons",
"version": "0.10.3",
"homepage": "https://github.com/wilsonpage/gaia-icons",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": [
"gaia-icons.js",
"gaia-icons.css",
"fonts/gaia-icons.ttf",
"gaia-icons-embedded.css"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"templates",
"images",
"test",
"tests",
"index.html",
"create-svg-icon-set-file.py",
"Gruntfile.js",
"README.md",
"package.json"
],
"_release": "0.10.3",
"_resolution": {
"type": "version",
"tag": "v0.10.3",
"commit": "c3628ea7b1d0950dda833f86ccfd7d2782e5c8e3"
},
"_source": "git://github.com/gaia-components/gaia-icons.git",
"_target": "^0.10.2",
"_originalSource": "gaia-components/gaia-icons"
}

View file

@ -0,0 +1,33 @@
{
"camelcase": false,
"curly": true,
"forin": false,
"latedef": "nofunc",
"newcap": false,
"noarg": true,
"node": true,
"nonew": true,
"quotmark": "single",
"undef": true,
"unused": "vars",
"strict": true,
"trailing": true,
"maxlen": 80,
"eqnull": true,
"esnext": true,
"expr": true,
"globalstrict": true,
"maxerr": 1000,
"regexdash": true,
"laxcomma": true,
"proto": true,
"browser": true,
"devel": true,
"nonstandard": true,
"worker": true,
"-W078": true
}

View file

@ -0,0 +1,58 @@
/* Some BiDi-proof icons */
[data-icon="back"]:-moz-dir(ltr):before {
content: 'left';
}
[data-icon="back"]:-moz-dir(rtl):before {
content: 'right';
}
[data-icon="forward"]:-moz-dir(ltr):before {
content: 'right';
}
[data-icon="forward"]:-moz-dir(rtl):before {
content: 'left';
}
[data-icon="back-light"]:-moz-dir(ltr):before {
content: 'left-light';
}
[data-icon="back-light"]:-moz-dir(rtl):before {
content: 'right-light';
}
[data-icon="forward-light"]:-moz-dir(ltr):before {
content: 'right-light';
}
[data-icon="forward-light"]:-moz-dir(rtl):before {
content: 'left-light';
}
[data-icon="arrow-back"]:-moz-dir(ltr):before {
content: 'arrow-left';
}
[data-icon="arrow-back"]:-moz-dir(rtl):before {
content: 'arrow-right';
}
[data-icon="arrow-forward"]:-moz-dir(ltr):before {
content: 'arrow-right';
}
[data-icon="arrow-forward"]:-moz-dir(rtl):before {
content: 'arrow-left';
}
[data-icon="clear-input"]:-moz-dir(ltr):before {
content: 'clear-input-left';
}
[data-icon="clear-input"]:-moz-dir(rtl):before {
content: 'clear-input-right';
}
[data-icon="expand"]:-moz-dir(ltr):before {
content: 'expand-left';
}
[data-icon="expand"]:-moz-dir(rtl):before {
content: 'expand-right';
}

View file

@ -0,0 +1,118 @@
'use strict';
// Create a report about unused icons in a project.
var fs = require('fs');
var path = require('path');
var exec = require('child_process').exec;
var optimist = require('optimist');
var cli = optimist
.usage('Create a report about unused icons in a project.' + '\n' +
'Usage: $0 path/to/your/project/');
var projectDir = cli.argv._[0];
// No folder passed? Show help message then.
if (!projectDir) {
cli.showHelp();
process.exit(0);
}
// Check if the directory exist.
var stats = fs.lstatSync(projectDir);
if (!stats.isDirectory()) {
console.log('\x1b[31mPlease provide a valid folder.\x1b[0m\n');
cli.showHelp();
process.exit(0);
}
/**
* Get the list of all the icons in the font.
* @returns {Array}
*/
function getIcons() {
var icons = fs.readdirSync('./images/');
icons = icons.map(function(icon) {
return path.basename(icon, '.svg');
});
return icons;
}
/**
* Use a grep pattern to extract used icons from files.
* @param {string} grep
* @param {Function} callback
*/
function getUsedIcons(grep, callback) {
exec(grep, function(err, stdout, stderr) {
var usedIcons = stdout
.trim()
.split('\n')
.map(function(icon) {
// Take only the icon name.
return icon.split(/["']/)[1];
})
.filter(function(icon) {
// Remove non string items.
return icon && icon.match(/^[a-z0-9-]+$/);
});
callback(usedIcons);
if (err) {
console.log(err);
}
});
}
/**
* Get the list of unused icons in a folder. The patterns of extraction are:
* * data-icon="[^"]+"
* * .dataset.icon ?= ?.+
*/
function getUnusedIcons() {
var usedIcons = [];
var length = 2;
var cb = function(icons) {
usedIcons = usedIcons.concat(icons);
length--;
if (length > 0) {
return;
}
var allIcons = getIcons();
var unusedIcons = [];
allIcons.forEach(function(icon) {
if (usedIcons.indexOf(icon) === -1) {
unusedIcons.push(icon);
}
});
outputUnusedIcons(allIcons, unusedIcons);
};
getUsedIcons('grep -rhoP \'data-icon="[^"]+"\' ' + projectDir, cb);
getUsedIcons('grep -rhoP \'.dataset.icon ?= ?.+\' ' + projectDir, cb);
}
/**
* Display the list of unused icons to the screen.
* @param {Array.<string>} icons
* @param {Array.<string>} unusedIcons
*/
function outputUnusedIcons(icons, unusedIcons) {
console.log('\x1b[36mList of unused icons in %s (%d/%d):\x1b[0m',
projectDir, unusedIcons.length, icons.length);
unusedIcons.forEach(function(icon) {
console.log('* ' + icon);
});
}
getUnusedIcons();

View file

@ -0,0 +1,30 @@
{
"name": "gaia-icons",
"version": "0.10.3",
"homepage": "https://github.com/wilsonpage/gaia-icons",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": [
"gaia-icons.js",
"gaia-icons.css",
"fonts/gaia-icons.ttf",
"gaia-icons-embedded.css"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"templates",
"images",
"test",
"tests",
"index.html",
"create-svg-icon-set-file.py",
"Gruntfile.js",
"README.md",
"package.json"
]
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,41 @@
/* Generated by grunt-webfont */
/* Based on https://github.com/endtwist/fontcustom/blob/master/lib/fontcustom/templates/fontcustom.css */
@font-face {
font-family: "gaia-icons";
src: url("fonts/gaia-icons.ttf") format("truetype");
font-weight: 500;
font-style: normal;
}
[data-icon]:before,
.ligature-icons {
font-family: "gaia-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;
}
[data-icon]:not([data-l10n-id]):not([aria-label]):not([aria-hidden="true"]):before {
/* Highlight cases where icons are not localized and still visible to screen
reader */
background: rgba(255, 0, 0, 0.4);
}
[data-icon]:not([data-l10n-id]):not([aria-label]):not([aria-hidden="true"]):after {
background: #333;
color: #fff;
position: absolute;
width: 20rem;
border-radius: 0.5rem;
padding: 0.5rem;
content: "Icon is not localized and is still visible to screen reader. Please add data-l10n-id or aria-label for localization or use aria-hidden to hide the icon from screen reader.";
z-index: 9999;
}

View file

@ -0,0 +1,38 @@
/* global define */
(function(define){'use strict';define(function(require,exports,module){
/**
* Exports
*/
var base = window.GAIA_ICONS_BASE_URL ||
window.COMPONENTS_BASE_URL ||
'bower_components/';
if (!document.documentElement) {
window.addEventListener('load', load);
} else {
load();
}
function load() {
if (isLoaded()) { return; }
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = base + 'gaia-icons/gaia-icons.css';
document.head.appendChild(link);
exports.loaded = true;
}
function isLoaded() {
return exports.loaded ||
document.querySelector('link[href*=gaia-icons]') ||
document.documentElement.classList.contains('gaia-icons-loaded');
}
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-icons',this));

View file

@ -0,0 +1,37 @@
{
"name": "gaia-text-input",
"version": "0.1.4",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-text-input.js",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"README.md"
],
"dependencies": {
"gaia-icons": "gaia-components/gaia-icons#~0.10.0",
"gaia-component": "gaia-components/gaia-component#~0.3.3"
},
"devDependencies": {
"gaia-fonts": "gaia-components/gaia-fonts#~0.2.1",
"gaia-theme": "gaia-components/gaia-theme#~0.4.3",
"base": "gaia-components/base",
"gaia-icons": "gaia-components/gaia-icons#master"
},
"homepage": "https://github.com/gaia-components/gaia-text-input",
"_release": "0.1.4",
"_resolution": {
"type": "version",
"tag": "v0.1.4",
"commit": "d811482e143cc7990be04fdbfcc890f2a9fbd9d1"
},
"_source": "git://github.com/gaia-components/gaia-text-input.git",
"_target": "*",
"_originalSource": "gaia-components/gaia-text-input"
}

View file

@ -0,0 +1,27 @@
{
"name": "gaia-text-input",
"version": "0.1.4",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-text-input.js",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"README.md"
],
"dependencies": {
"gaia-icons": "gaia-components/gaia-icons#~0.10.0",
"gaia-component": "gaia-components/gaia-component#~0.3.3"
},
"devDependencies": {
"gaia-fonts": "gaia-components/gaia-fonts#~0.2.1",
"gaia-theme": "gaia-components/gaia-theme#~0.4.3",
"base": "gaia-components/base",
"gaia-icons": "gaia-components/gaia-icons#master"
}
}

View file

@ -0,0 +1,216 @@
(function(define){define(function(require,exports,module){
/*jshint esnext:true*/
'use strict';
/**
* Dependencies
*/
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-text-input-multiline', {
created: function() {
this.setupShadowRoot();
this.els = {
inner: this.shadowRoot.querySelector('.inner'),
field: this.shadowRoot.querySelector('textarea')
};
this.type = this.getAttribute('type');
this.disabled = this.hasAttribute('disabled');
this.placeholder = this.getAttribute('placeholder');
this.required = this.getAttribute('required');
this.value = this.getAttribute('value');
},
clear: function(e) {
this.value = '';
},
attrs: {
placeholder: {
get: function() { return this.els.field.placeholder; },
set: function(value) { this.els.field.placeholder = value || ''; }
},
value: {
get: function() { return this.els.field.value; },
set: function(value) { this.els.field.value = value; }
},
required: {
get: function() { return this.els.field.required; },
set: function(value) { this.els.field.required = value; }
},
disabled: {
get: function() { return this.els.field.disabled; },
set: function(value) {
value = !!(value === '' || value);
this.els.field.disabled = value;
}
}
},
template: `<div class="inner">
<content select="label"></content>
<div class="fields">
<textarea></textarea>
<div class="focus focus-1"></div>
<div class="focus focus-2"></div>
</div>
</div>
<style>
/** Reset
---------------------------------------------------------*/
textarea {
box-sizing: border-box;
border: 0;
margin: 0;
padding: 0;
}
/** Host
---------------------------------------------------------*/
:host {
display: block;
margin-top: var(--base-m, 18px);
margin-bottom: var(--base-m, 18px);
}
/** Inner
---------------------------------------------------------*/
.inner {
height: 100%;
}
/** Label
---------------------------------------------------------*/
label {
font-size: 14px;
display: block;
margin: 0 0 4px 16px;
}
/**
* [disbled]
*/
[disabled] label {
opacity: 0.3;
}
/** Fields
---------------------------------------------------------*/
.fields {
box-sizing: border-box;
position: relative;
width: 100%;
height: 100%;
--gi-border-color:
var(--input-border-color,
var(--border-color,
var(--background-plus,
#e7e7e7)));
border-color:
var(--gi-border-color);
border:
var(--input-border,
var(--border,
1px solid var(--gi-border-color)));
}
/** Textarea
---------------------------------------------------------*/
textarea {
display: block;
width: 100%;
height: 100%;
min-height: 86px;
padding: 10px 16px;
font-size: inherit;
border: none;
margin: 0;
font: inherit;
resize: none;
/* dynamic */
color:
var(--text-color, #000);
background:
var(--text-input-background,
var(--input-background,
var(--background-minus,
#fff)));
}
/**
* [disabled]
*/
textarea[disabled] {
background: transparent;
}
/** Placeholder Text
---------------------------------------------------------*/
::-moz-placeholder {
font-style: italic;
color:
var(--input-placeholder-color, #909ca7);
}
/** Focus Bar
---------------------------------------------------------*/
.focus {
position: absolute;
bottom: 0px;
width: 100%;
height: 3px;
transition: all 200ms;
transform: scaleX(0);
visibility: hidden;
background: var(--highlight-color, #000);
}
/**
* input:focus
*/
:focus ~ .focus {
transform: scaleX(1);
transition-delay: 200ms;
visibility: visible;
}
.focus-2 {
top: 0;
bottom: auto;
}
</style>`
});
});})((function(n,w){return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:function(c){
var m={exports:{}},r=function(n){return w[n];};w[n]=c(r,m.exports,m)||m.exports;};})('gaia-text-input-multiline',this));

View file

@ -0,0 +1,279 @@
(function(define){define(function(require,exports,module){
/*jshint esnext:true*/
'use strict';
/**
* Dependencies
*/
var component = require('gaia-component');
/**
* Locals
*/
var DEFAULT_LENGTH = 4;
/**
* Exports
*/
module.exports = component.register('gaia-text-input-pin', {
created: function() {
this.setupShadowRoot();
this.els = {
inner: this.shadowRoot.querySelector('.inner'),
fields: this.shadowRoot.querySelector('.fields'),
input: this.shadowRoot.querySelector('input')
};
this.disabled = this.hasAttribute('disabled');
this.length = this.getAttribute('length') || DEFAULT_LENGTH;
this.addEventListener('keyup', () => this.updateCells());
},
updateCells: function() {
var l = this.els.input.value.length;
this.els.cells.forEach((cell, i) => {
cell.classList.toggle('populated', i < l);
cell.classList.toggle('focused', i == l);
});
},
onBackspace: function(e) {
var input = e.target;
var empty = !input.value;
var previous = input.previousElementSibling;
if (!empty && previous) {
previous.clear();
previous.focus();
} else if (empty && previous) {
previous.focus();
}
},
setupFields: function() {
this.els.fields.innerHTML = '';
this.els.cells = [];
for (var i = 0, l = this.length; i < l; i++) {
var el = document.createElement('div');
el.className = 'cell';
this.els.fields.appendChild(el);
this.els.cells.push(el);
}
},
attributeChangedCallback: function(attr, from, to) {
if (attrs[attr]) { this[attr] = to; }
},
clear: function(e) {
this.value = '';
},
/**
* Attributes
*/
attrs: {
length: {
get: function() { return this._length; },
set: function(value) {
value = Number(value);
this._length = value;
this.els.input.setAttribute('maxlength', this.length);
this.setupFields();
}
},
value: {
get: function() { return this.els.input.value; },
set: function(value) { this.els.input.value = value; }
},
disabled: {
get: function() { return this.els.input.disabled; },
set: function(value) {
value = !!(value === '' || value);
this.els.input.disabled = value;
}
}
},
template: `
<div class="inner">
<content select="label"></content>
<div class="container">
<input />
<div class="fields"></div>
</div>
</div>
<style>
/** Host
---------------------------------------------------------*/
:host {
display: block;
height: 40px;
margin-top: var(--base-m, 18px);
margin-bottom: var(--base-m, 18px);
font-size: 40px;
}
/** Inner
---------------------------------------------------------*/
.inner {
height: 100%;
}
/** Label
---------------------------------------------------------*/
label {
font-size: 14px;
display: block;
margin: 0 0 4px 16px;
}
/** Container
---------------------------------------------------------*/
.container {
position: relative;
z-index: 0;
height: 100%;
}
/** Input (hidden)
---------------------------------------------------------*/
input {
opacity: 0;
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
z-index: 1;
}
/** Fields
---------------------------------------------------------*/
.fields {
display: flex;
position: relative;
height: 100%;
margin-left: -1rem;
}
/**
* [disbled]
*/
[disabled] + .fields {
pointer-events: none;
}
/** Cell
---------------------------------------------------------*/
.cell {
position: relative;
height: 100%;
margin-left: 1rem;
flex: 1;
/* dynamic */
--gi-border-color:
var(--input-border-color,
var(--border-color,
var(--background-plus,
#e7e7e7)));
border-color:
var(--gi-border-color);
border:
var(--input-border,
var(--border,
1px solid var(--gi-border-color)));
background:
var(--text-input-background,
var(--input-background,
var(--background-minus,
#fff)));
}
/**
* [disbled]
*/
[disabled] + .fields .cell {
background: none;
}
/** Dot
---------------------------------------------------------*/
.cell::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 14px;
height: 14px;
margin: -7px;
border-radius: 50%;
opacity: 0;
/* dynamic */
background:
var(--text-color);
}
/**
* .populated
*/
.cell.populated::after {
opacity: 1;
}
.cell::before {
content: '';
position: absolute;
bottom: 0;
width: 100%;
height: 3px;
visibility: hidden;
background: var(--highlight-color, #000);
}
/**
* input:focus
*/
input:focus + .fields .cell::before {
visibility: visible;
}
</style>
`
});
});})((function(n,w){return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:function(c){
var m={exports:{}},r=function(n){return w[n];};w[n]=c(r,m.exports,m)||m.exports;};})('gaia-text-input-pin',this));

View file

@ -0,0 +1,335 @@
(function(define){define(function(require,exports,module){
/*jshint esnext:true*/
'use strict';
/**
* Dependencies
*/
var component = require('gaia-component');
require('gaia-icons'); // Load gaia-icons
/**
* Exports
*/
module.exports = component.register('gaia-text-input', {
extends: HTMLInputElement,
rtl: true,
created: function() {
this.setupShadowRoot();
this.els = {
inner: this.shadowRoot.querySelector('.inner'),
input: this.shadowRoot.querySelector('input'),
clear: this.shadowRoot.querySelector('.clear')
};
this.type = this.getAttribute('type');
this.disabled = this.hasAttribute('disabled');
this.clearable = this.hasAttribute('clearable');
this.placeholder = this.getAttribute('placeholder');
this.required = this.getAttribute('required');
this.value = this.getAttribute('value');
// Don't take focus from the input field
this.els.clear.addEventListener('mousedown', (e) => e.preventDefault());
this.els.clear.addEventListener('click', e => this.clear(e));
// Set dir initially and listen for changes
document.addEventListener('dirchanged', e => this.updateDir());
this.updateDir();
},
clear: function(e) {
this.value = '';
},
focus: function() {
this.els.input.focus();
},
updateDir: function() {
this.els.inner.setAttribute('dir', document.dir);
},
/**
* Attributes
*/
attrs: {
type: {
get: function() { return this.els.input.getAttribute('type'); },
set: function(value) {
if (!value) { return; }
this.els.inner.setAttribute('type', value);
this.els.input.setAttribute('type', value);
}
},
placeholder: {
get: function() { return this.field.placeholder; },
set: function(value) {
if (!value && value !== '') { return; }
this.els.input.placeholder = value;
}
},
clearable: {
get: function() { return this._clearable; },
set: function(value) {
var clearable = !!(value === '' || value);
if (clearable === this.clearable) { return; }
if (clearable) {
this.els.inner.setAttribute('clearable', '');
this.setAttribute('clearable', '');
} else {
this.els.inner.removeAttribute('clearable');
this.removeAttribute('clearable');
}
this._clearable = clearable;
}
},
value: {
get: function() { return this.els.input.value; },
set: function(value) { this.els.input.value = value; }
},
required: {
get: function() { return this.els.input.required; },
set: function(value) { this.els.input.required = value; }
},
maxlength: {
get: function() { return this.els.input.getAttribute('maxlength'); },
set: function(value) { this.els.input.setAttribute('maxlength', value); }
},
disabled: {
get: function() { return this.els.input.disabled; },
set: function(value) {
value = !!(value === '' || value);
this.els.input.disabled = value;
}
}
},
template: `
<div class="inner">
<content select="label"></content>
<div class="fields">
<input type="text"/>
<button class="clear" tabindex="-1"></button>
<div class="focus"></div>
</div>
</div>
<style>
:host {
display: block;
height: 40px;
margin-top: var(--base-m, 18px);
margin-bottom: var(--base-m, 18px);
}
/** Reset
---------------------------------------------------------*/
input,
button {
box-sizing: border-box;
border: 0;
margin: 0;
padding: 0;
}
/** Inner
---------------------------------------------------------*/
.inner {
height: 100%;
}
/** Label
---------------------------------------------------------*/
label {
font-size: 14px;
display: block;
margin: 0 0 4px 16px;
}
/**
* [disbled]
*/
[disabled] label {
opacity: 0.3;
}
/** Fields
---------------------------------------------------------*/
.fields {
position: relative;
width: 100%;
height: 100%;
--gi-border-color:
var(--input-border-color,
var(--border-color,
var(--background-plus,
#e7e7e7)));
border-color:
var(--gi-border-color);
border:
var(--input-border,
var(--border,
1px solid var(--gi-border-color)));
}
/**
* [type='search']
*/
[type='search'] .fields {
border-radius: 30px;
overflow: hidden;
}
/** Input Field
---------------------------------------------------------*/
input {
display: block;
width: 100%;
height: 100%;
border: none;
padding: 0 16px;
margin: 0;
font: inherit;
resize: none;
/* Dynamic */
color:
var(--text-color, #000);
background:
var(--text-input-background,
var(--input-background,
var(--background-minus,
#fff)));
}
/**
* [disabled]
*/
input[disabled] {
background: transparent;
}
/** Placeholder Text
---------------------------------------------------------*/
::-moz-placeholder {
font-style: italic;
color:
var(--input-placeholder-color, #909ca7);
}
/** Clear Button
---------------------------------------------------------*/
.clear {
display: none;
position: absolute;
top: 11px;
right: 0;
width: 18px;
height: 18px;
padding: 0;
margin: 0 10px;
border-radius: 50%;
opacity: 0;
color: #fff;
cursor: pointer;
background:
var(--input-clear-background, #999);
}
/**
* [dir=rtl]
*/
[dir=rtl] .clear {
right: initial;
left: 0;
}
/**
* [clearable]
*/
[clearable] .clear {
display: block;
}
/**
* input:focus
*/
input:focus ~ .clear {
opacity: 1;
}
/** Clear Icon
---------------------------------------------------------*/
.clear:before {
font: normal 500 19px/16.5px 'gaia-icons';
content: 'close';
display: block;
text-rendering: optimizeLegibility;
}
/** Focus Bar
---------------------------------------------------------*/
.focus {
position: absolute;
bottom: 0px;
width: 100%;
height: 3px;
transition: all 200ms;
transform: scaleX(0);
visibility: hidden;
background: var(--highlight-color, #000);
}
/**
* input:focus
*/
:focus ~ .focus {
transform: scaleX(1);
transition-delay: 200ms;
visibility: visible;
}
</style>
`
});
});})((function(n,w){return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:function(c){
var m={exports:{}},r=function(n){return w[n];};w[n]=c(r,m.exports,m)||m.exports;};})('gaia-text-input',this));

View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>&lt;gaia-text-input&gt;</title>
<meta name="description" content="gaia-text-input demo">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<link rel="stylesheet" type="text/css" href="bower_components/base/base.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-icons/gaia-icons.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-theme/gaia-theme.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-fonts/style.css">
<script src="bower_components/gaia-theme/lib/gaia-theme-selector.js"></script>
<script src="bower_components/gaia-component/gaia-component.js"></script>
<script src="gaia-text-input.js"></script>
<script src="gaia-text-input-multiline.js"></script>
<script src="gaia-text-input-pin.js"></script>
<style type="text/css">
body {
padding: 48px 24px 24px;
}
</style>
</head>
<body>
<gaia-theme-selector></gaia-theme-selector>
<button id="btn">Focus</button>
<gaia-text-input id="field" placeholder="Default"></gaia-text-input>
<gaia-text-input placeholder="Search" type="search"></gaia-text-input>
<gaia-text-input placeholder="Email" type="email" clearable></gaia-text-input>
<gaia-text-input-multiline placeholder="Multiline"></gaia-text-input-multiline>
<gaia-text-input-pin length="4"></gaia-text-input-pin>
<gaia-text-input placeholder="Disabled" disabled></gaia-text-input>
<gaia-text-input placeholder="Search" type="search" disabled></gaia-text-input>
<gaia-text-input placeholder="Email" type="email" clearable disabled></gaia-text-input>
<gaia-text-input-multiline placeholder="Multiline" disabled></gaia-text-input-multiline>
<gaia-text-input-pin length="4" disabled></gaia-text-input-pin>
<!-- <gaia-text-input-password></gaia-text-input-password> -->
<script>
btn.onclick = e => field.focus();
</script>
</body>
</html>

View file

@ -0,0 +1,29 @@
{
"name": "gaia-text-input",
"version": "0.1.4",
"homepage": "https://github.com/gaia-components/gaia-text-input",
"license": "ISC",
"main": "gaia-text-input.js",
"directories": {
"test": "test"
},
"devDependencies": {
"bower": "^1.3.5",
"karma": "^0.12.16",
"karma-firefox-launcher": "^0.1.3",
"karma-mocha": "^0.1.3",
"karma-sinon-chai": "^0.1.6",
"mozilla-download": "0.4.4"
},
"scripts": {
"test": "./node_modules/karma/bin/karma start test/karma.conf.js --single-run",
"test-dev": "./node_modules/karma/bin/karma start test/karma.conf.js"
},
"repository": {
"type": "git",
"url": "git://github.com/gaia-components/gaia-text-input.git"
},
"bugs": {
"url": "https://github.com/gaia-components/gaia-text-input/issues"
}
}

View file

@ -0,0 +1,26 @@
{
"name": "pressed",
"main": "pressed.js",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"test",
"tests"
],
"homepage": "https://github.com/gaia-components/pressed",
"_release": "0a9ae880fe",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "0a9ae880fe71ebb6bffaf14b9fd1c7eb6ce489cb"
},
"_source": "git://github.com/gaia-components/pressed.git",
"_target": "master",
"_originalSource": "gaia-components/pressed"
}

View file

@ -0,0 +1,4 @@
{
"strict": false,
"node": true
}

View file

@ -0,0 +1,17 @@
{
"name": "pressed",
"main": "pressed.js",
"version": "0.1.2",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"test",
"tests"
]
}

View file

@ -0,0 +1,138 @@
;(function(define){'use strict';define(function(require,exports,module){
/**
* Pointer event abstraction to make
* it work for touch and mouse.
*
* @type {Object}
*/
var pointer = [
{ down: 'touchstart', up: 'touchend', move: 'touchmove' },
{ down: 'mousedown', up: 'mouseup', move: 'mousemove' }
]['ontouchstart' in window ? 0 : 1];
module.exports = function(el, options) {
var released = (options && options.released) || 200;
var scope = (options && options.scope) || el;
var min = (options && options.min) || 300;
var instant = options && options.instant;
var timeouts = {};
var removeReleased;
el.addEventListener(pointer.down, function(e) {
var start = e.timeStamp;
var target = e.target;
var pressed = false;
var last = e;
// If there is a removeRelease pending
// run it before we add any more 'pressed'
if (removeReleased) { removeReleased(); }
if (instant) { onPressed(); }
else { notScrolling(e, onPressed); }
function onPressed() {
classListUp(target, scope, 'add', 'pressed');
}
addEventListener(pointer.up, function fn(e) {
removeEventListener(pointer.up, fn, true);
var duration = e.timeStamp - start;
var delta = min - duration;
var lag = Math.max(delta, 0);
// Once we consider the 'press' event
// to be over, we remove the 'pressed'
// class and add a 'released' class.
timeouts.pressed = setTimeout(function() {
classListUp(target, scope, 'remove', 'pressed');
classListUp(target, scope, 'add', 'released');
removeReleased = function() {
clearTimeout(timeouts.released);
classListUp(target, scope, 'remove', 'released');
removeReleased = null;
};
timeouts.released = setTimeout(removeReleased, released);
}, lag);
}, true);
}, true);
};
function notScrolling(e, fn) {
detectScrolling(e, function(scrolling) {
if (!scrolling) { fn(); }
});
}
function detectScrolling(e, fn) {
var period = 76;
var last = e;
if (windowScrolling) { return fn(true); }
if (!e.touches) { return fn(false); }
addEventListener('touchmove', onTouchMove, true);
setTimeout(detect, period);
function detect() {
removeEventListener('touchmove', onTouchMove, true);
if (windowScrolling) { return fn(true); }
var time = last.timeStamp - e.timeStamp;
var distance = getDistance(e.touches[0], last.touches[0]);
var speed = distance / time;
var scrolling = speed > 0.03;
fn(scrolling);
}
function onTouchMove(e) { last = e; }
function getDistance(a, b) {
var xs = 0;
var ys = 0;
xs = b.clientX - a.clientX;
xs = xs * xs;
ys = b.clientY - a.clientY;
ys = ys * ys;
return Math.sqrt(xs + ys);
}
}
var windowScrolling = false;
var scrollTimeout;
// addEventListener('scroll', function() {
// windowScrolling = true;
// clearTimeout(scrollTimeout);
// scrollTimeout = setTimeout(function() {
// windowScrolling = false;
// }, 60);
// });
/**
* Run a classList method on every
* element up the DOM tree, until
* the given scope.
*
* @param {Element} el
* @param {Element} scope
* @param {String} method
* @param {String} cls
*/
function classListUp(el, scope, method, cls) {
while (el && el.classList && el !== scope.parentNode) {
el.classList[method](cls);
el = el.parentNode;
}
}
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('pressed',this));

View file

@ -0,0 +1,21 @@
'use strict';
/* jshint node: true */
var utils = require('utils');
var manager = require('homescreen-manager');
var HomeScreenAppBuilder = function() {
};
HomeScreenAppBuilder.prototype.execute = function(options) {
var homescreen = manager.getHomescreen(options, 'homescreen');
var stageDir = utils.getFile(options.STAGE_APP_DIR);
var configFile = utils.getFile(stageDir.path, 'js', 'init.json');
utils.writeContent(configFile, JSON.stringify(homescreen));
};
exports.execute = function(options) {
utils.copyToStage(options);
(new HomeScreenAppBuilder()).execute(options);
};

View file

@ -0,0 +1,33 @@
{
"homescreens": [
[
["apps", "communications", "dialer"],
["apps", "sms"],
["apps", "communications", "contacts"],
["apps", "email"],
["apps", "search"]
], [
["apps", "camera"],
["apps", "gallery"],
["apps", "music"],
["apps", "video"],
["apps", "marketplace.firefox.com"]
], [
["apps", "calendar"],
["apps", "clock"],
["apps", "settings"],
["apps", "fm"]
], [
["hosted_apps", "buddyup"],
["hosted_apps", "bugzillalite"],
["hosted_apps", "facebook"],
["hosted_apps", "twitter"],
["hosted_apps", "notes"],
["hosted_apps", "calculator"],
["apps", "costcontrol"]
]
],
"preferences": {
"grid.cols": 3
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
gaia/apps/homescreen/icons/16.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
gaia/apps/homescreen/icons/32.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,000 B

BIN
gaia/apps/homescreen/icons/48.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
gaia/apps/homescreen/icons/60.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
gaia/apps/homescreen/icons/64.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
gaia/apps/homescreen/icons/90.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><path d="M503.5 256c0 136.69-110.81 247.5-247.5 247.5S8.5 392.69 8.5 256 119.31 8.5 256 8.5 503.5 119.31 503.5 256z" fill-opacity=".2"/><path d="M170.564 155.268c-13.73 0-24.863 11.132-24.863 24.833 0 13.743 11.135 24.864 24.835 24.864 13.73 0 24.863-11.106 24.863-24.836 0-13.728-11.133-24.86-24.834-24.86zm83.712 0c-13.728 0-24.863 11.132-24.863 24.833 0 13.743 11.135 24.864 24.834 24.864 13.73 0 24.863-11.106 24.863-24.836 0-13.728-11.133-24.86-24.834-24.86zm83.712 0c-13.714 0-24.863 11.132-24.863 24.833 0 13.743 11.162 24.864 24.835 24.864 13.73 0 24.862-11.106 24.862-24.836 0-13.728-11.132-24.86-24.834-24.86zm-167.424 69.76c-13.73 0-24.863 11.133-24.863 24.835 0 13.742 11.135 24.833 24.835 24.833 13.73 0 24.863-11.094 24.863-24.836 0-13.7-11.133-24.834-24.834-24.834zm83.712 0c-13.728 0-24.863 11.133-24.863 24.835 0 13.742 11.135 24.833 24.834 24.833 13.73 0 24.863-11.094 24.863-24.836 0-13.7-11.133-24.834-24.834-24.834zm83.712 0c-13.714 0-24.863 11.133-24.863 24.835 0 13.742 11.162 24.833 24.835 24.833 13.73 0 24.862-11.094 24.862-24.836 0-13.7-11.132-24.834-24.834-24.834zm-167.424 69.76c-13.73 0-24.863 11.133-24.863 24.835 0 13.7 11.135 24.834 24.835 24.834 13.73 0 24.863-11.106 24.863-24.836 0-13.727-11.133-24.833-24.834-24.833zm83.712 0c-13.728 0-24.863 11.133-24.863 24.835 0 13.7 11.135 24.834 24.834 24.834 13.73 0 24.863-11.106 24.863-24.836 0-13.727-11.133-24.833-24.834-24.833zm83.712 0c-13.714 0-24.863 11.133-24.863 24.835 0 13.7 11.162 24.834 24.835 24.834 13.73 0 24.862-11.106 24.862-24.836 0-13.727-11.132-24.833-24.834-24.833zm-167.424 69.76c-13.73 0-24.863 11.132-24.863 24.833 0 13.702 11.135 24.837 24.835 24.837 13.73 0 24.863-11.107 24.863-24.836 0-13.727-11.133-24.833-24.834-24.833zm83.712 0c-13.728 0-24.863 11.132-24.863 24.833 0 13.702 11.135 24.837 24.834 24.837 13.73 0 24.863-11.107 24.863-24.836 0-13.727-11.133-24.833-24.834-24.833zm83.712 0c-13.714 0-24.863 11.132-24.863 24.833 0 13.702 11.162 24.837 24.835 24.837 13.73 0 24.862-11.107 24.862-24.836 0-13.727-11.132-24.833-24.834-24.833zm25.49-266.765H148.52c-4.925 0-8.93 6.28-8.93 14.022 0 7.743 4.005 14.05 8.93 14.05h214.96c4.94 0 8.93-6.28 8.93-14.05 0-7.743-4.005-14.022-8.93-14.022z" fill="#fff" fill-opacity=".8"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

114
gaia/apps/homescreen/index.html Executable file
View file

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta name="theme-color" content="transparent">
<title>Firefox OS Home Screen</title>
<!-- Setup environment -->
<script defer src="js/setup.js"></script>
<!-- Lazy loader -->
<script defer src="shared/js/lazy_loader.js"></script>
<!-- <script defer src="js/firstrun.js"></script> -->
<!-- gaia-container -->
<script defer src="bower_components/gaia-container/script.js"></script>
<!-- gaia-site-icon -->
<link rel="stylesheet" href="shared/elements/gaia-site-icon/external/style.css">
<script defer src="shared/elements/gaia-site-icon/script.js"></script>
<!-- gaia-pin-card -->
<script defer src="shared/js/component_utils.js"></script>
<script defer src="shared/elements/gaia_pin_card/script.js"></script>
<!-- gaia-components -->
<link rel="stylesheet" href="bower_components/base/base.css">
<link rel="stylesheet" href="shared/elements/gaia-theme/gaia-theme.css">
<script defer src="bower_components/evt/index.js"></script>
<script defer src="bower_components/pressed/pressed.js"></script>
<script defer src="shared/elements/gaia-component/gaia-component.js"></script>
<script defer src="bower_components/gaia-dialog/gaia-dialog.js"></script>
<script defer src="bower_components/gaia-dialog/gaia-dialog-action.js"></script>
<!-- gaia l10n -->
<script defer src="shared/js/l10n.js"></script>
<!-- Local resources -->
<link rel="localization" href="locales/homescreen.{locale}.properties">
<link rel="stylesheet" href="style/window.css">
<link rel="stylesheet" href="style/grid.css">
<link rel="stylesheet" href="style/apps-panel.css">
<link rel="stylesheet" href="style/pages-panel.css">
<!-- Process the icons of pinned pages -->
<script defer src="shared/elements/gaia_grid/js/grid_icon_renderer.js"></script>
<script defer src="shared/js/homescreens/icon.js"></script>
<script defer src="shared/js/icons_helper.js"></script>
<script defer src="js/settings.js"></script>
<script defer src="js/appsmetadata.js"></script>
<script defer src="js/datastore.js"></script>
<script defer src="js/pagesstore.js"></script>
<script defer src="js/pages.js"></script>
<script defer src="js/apps.js"></script>
<script defer src="js/window.js"></script>
<script defer src="js/bootstrap.js"></script>
</head>
<body>
<header id="page-indicator-header">
<div id="page-indicator" data-l10n-id="page-indicator-text" role="slider" aria-valuemin="1" aria-valuenow="1" aria-valuemax="2" aria-controls="panels">
<span></span><span></span>
</div>
</header>
<div id="shadow"></div>
<div id="panels"><!--
--><div id="apps-panel" aria-labelledby="page-indicator-header">
<div class="scrollable">
<div class="edit-underlay"></div>
<gaia-container id="apps" drag-and-drop></gaia-container>
</div>
<gaia-dialog id="cancel-download" class="dialog-action">
<section>
<p class="body" data-l10n-id="stop-download-body"></p>
</section>
<button class="action" data-l10n-id="stop-download-action"></button>
<button on-click="close" data-l10n-id="cancel-action"></button>
</gaia-dialog>
<gaia-dialog id="resume-download" class="dialog-action">
<section>
<p class="body" data-l10n-id="resume-download-body"></p>
</section>
<button class="action" data-l10n-id="resume-download-action"></button>
<button on-click="close" data-l10n-id="cancel-action"></button>
</gaia-dialog>
</div><!--
--><div id="pages-panel" aria-labelledby="page-indicator-header">
<div id="empty-pages">
<h1 data-l10n-id="no-pinned-pages-header"></h1>
<p data-l10n-id="no-pinned-pages-body"></p>
<img src="style/images/empty_home_image.png" role="presentation"></img>
<div id="boxes" role="presentation">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<div class="scrollable">
<div class="edit-underlay"></div>
<gaia-container id="pages"></gaia-container>
</div>
</div><!--
--></div>
<div id="bottombar">
<div id="remove"></div>
<div id="rename"></div>
<div id="done" data-l10n-id="edit-done"></div>
</div>
</body>
</html>

1128
gaia/apps/homescreen/js/apps.js Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,110 @@
'use strict';
(function(exports) {
const DB_NAME = 'home-metadata';
const DB_ORDER_STORE = 'order';
const DB_ICON_STORE = 'icon';
const DB_VERSION = 1;
function AppsMetadata() {}
AppsMetadata.prototype = {
db: null,
init: function() {
return new Promise((resolve, reject) => {
var req = window.indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = this.upgradeSchema;
req.onsuccess = (e) => {
this.db = e.target.result;
resolve();
};
req.onerror = (e) => {
console.error('Error opening homescreen metadata db', e);
reject(e);
};
});
},
upgradeSchema: function(e) {
var db = e.target.result;
var fromVersion = e.oldVersion;
if (fromVersion < 1) {
var store = db.createObjectStore(DB_ORDER_STORE, { keyPath: 'id' });
store.createIndex('order', 'order', { unique: false });
store = db.createObjectStore(DB_ICON_STORE, { keyPath: 'id' });
store.createIndex('icon', 'icon', { unique: false });
}
},
set: function(data) {
return new Promise((resolve, reject) => {
var txn = this.db.transaction([DB_ORDER_STORE, DB_ICON_STORE],
'readwrite');
for (var entry of data) {
if (!entry.id) {
continue;
}
if (typeof entry.order !== 'undefined') {
txn.objectStore(DB_ORDER_STORE).
put({ id: entry.id, order: entry.order });
}
if (typeof entry.icon !== 'undefined') {
txn.objectStore(DB_ICON_STORE).
put({ id: entry.id, icon: entry.icon });
}
}
txn.oncomplete = resolve;
txn.onerror = reject;
});
},
remove: function(id) {
return new Promise((resolve, reject) => {
var txn = this.db.transaction([DB_ORDER_STORE, DB_ICON_STORE],
'readwrite');
txn.objectStore(DB_ORDER_STORE).delete(id);
txn.objectStore(DB_ICON_STORE).delete(id);
txn.oncomplete = resolve;
txn.onerror = reject;
});
},
getAll: function(onResult) {
return new Promise((resolve, reject) => {
var txn = this.db.transaction([DB_ORDER_STORE, DB_ICON_STORE],
'readonly');
var orderStore = txn.objectStore(DB_ORDER_STORE);
var iconStore = txn.objectStore(DB_ICON_STORE);
var cursor = orderStore.index('order').openCursor();
var results = [];
cursor.onsuccess = e => {
var cursor = e.target.result;
if (cursor) {
var result = cursor.value;
var iconRequest = iconStore.get(result.id);
iconRequest.onsuccess = function(result, e) {
if (e.target.result) {
result.icon = e.target.result.icon;
}
results.push(result);
if (onResult) {
onResult(result);
}
}.bind(this, result);
cursor.continue();
}
};
txn.oncomplete = () => {
resolve(results);
};
});
}
};
exports.AppsMetadata = AppsMetadata;
}(window));

5
gaia/apps/homescreen/js/bootstrap.js vendored Executable file
View file

@ -0,0 +1,5 @@
/* global AppWindow */
/* exported appWindow */
'use strict';
var appWindow = new AppWindow();

View file

@ -0,0 +1,234 @@
'use strict';
(function(exports) {
function Datastore(name) {
this.name = name;
}
Datastore.prototype = {
/**
* The name of the datastore
*/
name: '',
/**
* The version of the indexed database
*/
DB_VERSION: 1,
/**
* The datastore
*/
datastore: null,
/**
* The last datastore revision that was synced
*/
lastRevision: null,
/**
* Our local indexed db where we store our copy of bookmarks
*/
db: null,
get revisionName() {
return this.name + '_revision';
},
get idbName() {
return this.name + '_mirror';
},
get storeName() {
return this.name + '_store';
},
init: function() {
var revisionString = localStorage.getItem(this.revisionName);
if (revisionString) {
this.lastRevision = JSON.parse(revisionString);
}
return new Promise((resolve, reject) => {
var req = window.indexedDB.open(this.idbName, this.DB_VERSION);
req.onupgradeneeded = this.upgradeSchema.bind(this);
req.onsuccess = (e) => {
this.db = e.target.result;
resolve();
// Open up the shared datastore
if (!navigator.getDataStores) {
console.error('DataStore API is unavailable');
return;
}
navigator.getDataStores(this.name).then((stores) => {
if (stores.length < 1) {
console.error(this.name + ' inaccessible');
return;
}
this.datastore = stores[0];
this.datastore.addEventListener('change',
this.onChange.bind(this));
this.synchronise();
}, (e) => {
console.error('Error getting datastore', e);
});
};
req.onerror = (e) => {
console.error('Error opening datastore mirror database', e);
reject(e);
};
});
},
upgradeSchema: function(e) {
var db = e.target.result;
var fromVersion = e.oldVersion;
if (fromVersion < 1) {
var store = db.createObjectStore(this.storeName, { keyPath: 'id' });
store.createIndex('data', 'data', { unique: false });
}
},
synchronise: function() {
// Serialise mirror refreshes: overlapping clear/add snapshots erase
// rows while listeners are reading them.
this._syncQueue = (this._syncQueue || Promise.resolve()).then(
() => this._synchronise());
return this._syncQueue;
},
_synchronise: function() {
return new Promise((resolve, reject) => {
var cursor = this.datastore.sync(this.lastRevision);
var self = this;
function cursorResolve(task) {
var promises = [];
switch (task.operation) {
case 'update':
case 'add':
promises.push(self.set(task.data));
break;
case 'remove':
promises.push(self.remove(task.id));
break;
case 'clear':
promises.push(self.clear());
break;
case 'done':
self.updateRevision();
resolve();
return;
}
promises.push(cursor.next());
Promise.all(promises).then(
(results) => {
cursorResolve(results.pop());
}, reject);
}
cursor.next().then(cursorResolve, reject);
});
},
set: function(data) {
return new Promise((resolve, reject) => {
var txn = this.db.transaction([this.storeName], 'readwrite');
txn.oncomplete = function onComplete(data, resolve) {
resolve();
document.dispatchEvent(new CustomEvent(this.name + '-set',
{ detail: { id: data.id }}));
}.bind(this, data, resolve);
txn.onerror = reject;
try {
txn.objectStore(this.storeName).put({ id: data.id, data: data });
} catch(e) {
console.error('Error putting data in ' + this.idbName + ':', e);
resolve();
}
});
},
remove: function(id) {
return new Promise((resolve, reject) => {
var txn = this.db.transaction([this.storeName], 'readwrite');
txn.oncomplete = function onComplete(id, resolve) {
resolve();
document.dispatchEvent(new CustomEvent(this.name + '-removed',
{ detail: { id: id }}));
}.bind(this, id, resolve);
txn.onerror = reject;
try {
txn.objectStore(this.storeName).delete(id);
} catch(e) {
console.error('Error deleting data from ' + this.idbName + ':', e);
resolve();
}
});
},
clear: function() {
return new Promise((resolve, reject) => {
var txn = this.db.transaction([this.storeName], 'readwrite');
txn.oncomplete = function onComplete(resolve) {
resolve();
document.dispatchEvent(new CustomEvent(this.name + '-cleared'));
}.bind(this, resolve);
txn.onerror = reject;
txn.objectStore(this.storeName).clear();
});
},
updateRevision: function() {
this.lastRevision = this.datastore.revisionId;
localStorage.setItem(this.revisionName,
JSON.stringify(this.lastRevision));
},
onChange: function(e) {
this.synchronise().then(() => {},
(error) => {
console.error('Failed to handle ' + this.name + ' change', error);
});
},
get: function(id) {
return new Promise((resolve, reject) => {
var txn = this.db.transaction([this.storeName], 'readonly');
txn.onerror = reject;
txn.objectStore(this.storeName).get(id).onsuccess =
(event) => {
resolve(event.target.result);
};
});
},
getAll: function(filter = () => true) {
return new Promise((resolve, reject) => {
var results = [];
var txn = this.db.transaction([this.storeName], 'readonly');
txn.onerror = reject;
txn.oncomplete = () => { resolve(results); };
txn.objectStore(this.storeName).openCursor().onsuccess =
(event) => {
var cursor = event.target.result;
if (cursor) {
if (filter(cursor.value)) {
results.push(cursor.value);
}
cursor.continue();
}
};
});
}
};
exports.Datastore = Datastore;
}(window));

View file

@ -0,0 +1,180 @@
/* global LazyLoader */
'use strict';
(function(exports) {
const COLS_PREF = 'grid.cols';
const VERTICALHOME_APPS = 'verticalhome_items_migration';
const VERTICALHOME_SETTINGS = 'vertical_preferences_store';
const HOMESCREEN_SETTINGS = 'homescreen_settings';
function FirstRun() {
return importVerticalHome().then(Promise.resolve(),
(e) => {
console.warn('Loading default settings, import failed:', e);
return loadDefault();
});
}
function importVerticalHome() {
return new Promise((resolve, reject) => {
if (!navigator.getDataStores) {
reject('DataStore API is unavailable');
return;
}
navigator.getDataStores(VERTICALHOME_APPS).then(
stores => {
if (stores.length < 1) {
reject('verticalhome datastores inaccessible');
return;
}
stores[0].getLength().then(length => {
if (length === 0) {
reject('No verticalhome app order to import');
return;
}
var cursor = stores[0].sync();
var items = {};
var iterateItems = () => {
cursor.next().then(task => {
switch (task.operation) {
case 'done':
// Convert from verticalhome storage format to something
// homescreen can use, skipping over features that aren't
// supported.
var id;
var order = [];
for (var prop in items) {
var item = items[prop];
switch (item.type) {
case 'app':
id = item.manifestURL + '/' +
(item.entryPoint ? item.entryPoint : '');
break;
case 'bookmark':
id = item.id;
break;
default:
continue;
}
order.push({ id: id, order: item.index });
}
// Sort by order
order.sort((a, b) => { return a.order - b.order; });
// Rewrite order so there are no gaps
order.forEach((entry, index) => {
entry.order = index;
});
// Load columns setting and resolve
navigator.getDataStores(VERTICALHOME_SETTINGS).then(
stores => {
stores[0].get(COLS_PREF).then(cols => {
resolve({ order: order, small: cols >= 4 });
// If 4-column is set, we should synchronise the
// homescreen settings datastore so that Settings
// reflects the correct value. (3 is the default)
if (cols >= 4) {
navigator.getDataStores(HOMESCREEN_SETTINGS).then(
stores => {
stores[0].put(cols, COLS_PREF);
}).catch(
e => {
console.error(
'Error storing homescreen cols setting', e);
});
}
}, () => {
resolve({ order: order, small: false });
});
},
(e) => {
console.error('Error retrieving columns preference', e);
resolve({ order: order, small: false });
});
return;
case 'clear':
items = {};
break;
case 'put':
case 'add':
items[task.id] = task.data;
break;
case 'remove':
delete items[task.id];
break;
default:
reject('Unhandled datastore operation: ' + task.operation);
return;
}
iterateItems();
}, reject);
};
iterateItems();
}, reject);
}, reject);
});
}
function loadDefault() {
return new Promise((resolve, reject) => {
LazyLoader.getJSON('js/init.json').then(
json => {
if (!json) {
resolve({ order: [], small: false});
return;
}
// Convert the initialisation JSON format to something more useful
// for homescreen.
var small = !!(json.preferences && json.preferences[COLS_PREF] &&
(json.preferences[COLS_PREF] > 3));
var order = [];
if (json.grid) {
var index = 0;
json.grid.forEach((section) => {
section.forEach((entry) => {
if (!entry || !entry.manifestURL) {
return;
}
var id = entry.manifestURL + '/' +
(entry.entry_point ? entry.entry_point : '');
order.push({ id: id, order: index });
index ++;
});
});
}
resolve({ order: order, small: small});
},
e => {
reject(e);
}
);
});
}
exports.FirstRun = FirstRun;
}(window));

272
gaia/apps/homescreen/js/pages.js Executable file
View file

@ -0,0 +1,272 @@
/* global PagesStore, IconsHelper */
'use strict';
(function(exports) {
/**
* The size in pixels of the icons in the pinned pages.
*/
const PAGES_ICON_SIZE = 30;
function Pages() {
// Element references
this.panel = document.getElementById('pages-panel');
this.panels = document.getElementById('panels');
this.pages = document.getElementById('pages');
this.scrollable = document.querySelector('#pages-panel > .scrollable');
this.bottombar = document.getElementById('bottombar');
this.remove = document.getElementById('remove');
this.done = document.getElementById('done');
// Tracking if the list is empty
this.empty = true;
// Edit mode
this.editMode = false;
// Dialogs
this.dialogs = [];
// Signal handlers
this.pages.addEventListener('click', this);
this.pages.addEventListener('contextmenu', this);
this.pages.addEventListener('keydown', this);
window.addEventListener('resize', this);
this.done.addEventListener('click', e => {
e.preventDefault();
this.exitEditMode();
});
this.remove.addEventListener('click', e => {
e.preventDefault();
this.unpinSelectedCard();
});
// Initialise and populate pinned pages
this.pagesStore = new PagesStore('places');
this.pagesStore.init().then(() => {
// Triggered when pages are added and updated.
document.addEventListener('places-set', (e) => {
var id = e.detail.id;
this.pagesStore.get(id).then((page) => {
for (var child of this.pages.children) {
if (child.dataset.id === id) {
if (!page.data.pinned) {
this.removeCard(child);
} else {
this.updatePinnedPage(child, page.data);
}
return;
}
}
if (!page.data.pinned) {
return;
}
// A new page was pinned.
this.addPinnedPage(page.data);
});
});
document.addEventListener('places-removed', (e) => {
var id = e.detail.id;
for (var child of this.pages.children) {
if (child.dataset.id === id) {
this.removeCard(child);
return;
}
}
});
document.addEventListener('places-cleared', () => {
this.exitEditMode();
for (var child of this.pages.children) {
this.pages.removeChild(child);
}
if (!this.empty) {
this.empty = true;
this.panel.classList.add('empty');
}
});
},
(e) => {
console.error('Error initialising pinned pages', e);
}).then(() => {
return this.pagesStore.getAll().then((pages) => {
for (var page of pages) {
this.addPinnedPage(page.data);
}
// If there are no pinned pages, display helpful information.
if (this.empty) {
this.panel.classList.add('empty');
}
}, (e) => {
console.error('Error getting pinned pages', e);
});
});
}
Pages.prototype = {
updatePinnedPage: function(card, page) {
card.title = page.title;
card.dataset.id = page.url;
card.style.order = -Math.round(page.pinTime / 1000);
page.meta = page.meta || {};
if (page.screenshot) {
page.meta.screenshot = page.screenshot;
}
setTimeout(function(card, page) {
card.meta = page.meta;
IconsHelper.getIconBlob(page.url, PAGES_ICON_SIZE, page)
.then((iconObj) => {
if (iconObj.blob) {
var iconURL = URL.createObjectURL(iconObj.blob);
card.icon = `url(${iconURL})`;
}
})
.catch((e) => {
console.error('Failed to fetch icon', e);
});
}.bind(this, card, page));
},
addPinnedPage: function(page) {
var pinCard = document.createElement('gaia-pin-card');
// Make the card accessible/allow activation
pinCard.tabIndex = 0;
pinCard.setAttribute('role', 'link');
this.updatePinnedPage(pinCard, page);
this.pages.appendChild(pinCard);
if (this.empty) {
this.empty = false;
this.panel.classList.remove('empty');
}
},
launchCard: function(card) {
var features = {
name: card.title,
icon: card.icon,
remote: true
};
window.open(card.dataset.id, '_blank', Object.keys(features).map(
key => encodeURIComponent(key) + '=' + encodeURIComponent(features[key])
).join(','));
},
enterEditMode: function(card) {
if (this.selectedCard) {
this.selectedCard.classList.remove('selected');
}
if (card) {
this.selectedCard = card;
card.classList.add('selected');
}
this.editMode = true;
document.body.classList.add('edit-mode');
this.remove.classList.add('active');
},
exitEditMode: function() {
if (!this.editMode) {
return;
}
this.editMode = false;
document.body.classList.remove('edit-mode');
this.remove.classList.remove('active');
if (this.selectedCard) {
this.selectedCard.classList.remove('selected');
this.selectedCard = null;
}
},
removeCard: function(card) {
if (this.selectedCard === card) {
this.selectedCard = null;
}
this.pages.removeChild(card, () => {
if (!this.empty && this.pages.children.length === 0) {
this.exitEditMode();
this.empty = true;
this.panel.classList.add('empty');
}
});
},
unpinSelectedCard: function() {
if (!this.selectedCard) {
return;
}
var id = this.selectedCard.dataset.id;
// Remove child immediately, datastore operations can be quite slow
this.removeCard(this.selectedCard);
this.pagesStore.get(id).then(entry => {
entry.data.pinned = false;
this.pagesStore.datastore.put(entry.data, id).then(() => {},
e => {
console.error('Error unpinning page:', e);
});
}, e => {
console.error('Error retrieving page to unpin:', e);
});
},
handleEvent: function(e) {
switch (e.type) {
case 'click':
if (e.target.nodeName !== 'GAIA-PIN-CARD') {
break;
}
if (this.editMode) {
this.enterEditMode(e.target);
} else {
this.launchCard(e.target);
}
break;
case 'contextmenu':
if (e.target.nodeName !== 'GAIA-PIN-CARD') {
break;
}
this.enterEditMode(e.target);
break;
case 'keydown':
if (e.target.nodeName !== 'GAIA-PIN-CARD') {
break;
}
switch (e.keyCode) {
case 32: // Space
case 13: // Enter
this.launchCard(e.target);
}
break;
case 'resize':
this.pages.synchronise();
break;
}
}
};
exports.Pages = Pages;
}(window));

View file

@ -0,0 +1,25 @@
/* global Datastore */
'use strict';
(function(exports) {
function PagesStore(name) {
this.name = name;
}
PagesStore.prototype = Object.create(Datastore.prototype);
PagesStore.prototype.DB_VERSION = 1;
PagesStore.prototype.set = function(data) {
data.id = data.url;
return Datastore.prototype.set.call(this, data);
};
PagesStore.prototype.getAll = function() {
var filter = (value) => value.data && value.data.pinned;
return Datastore.prototype.getAll.call(this, filter);
};
exports.PagesStore = PagesStore;
}(window));

View file

@ -0,0 +1,118 @@
'use strict';
(function(exports) {
/**
* Name of the global home screen settings datastore.
*/
const SETTINGS_STORE = 'homescreen_settings';
/**
* Stored settings version, for use when changing/refactoring settings
* storage.
*/
const SETTINGS_VERSION = 0;
/**
* Name of the global home screen columns setting.
*/
const COLUMNS_SETTING = 'grid.cols';
/**
* Name of the global home screen paging setting.
*/
const PAGING_SETTING = 'grid.paging';
function Settings() {
// Initialise default settings
this.small = false;
this.scrollSnapping = false;
this.firstRun = false;
// Listen to external settings changes
if (navigator.getDataStores) {
navigator.getDataStores(SETTINGS_STORE).then(stores => {
if (stores.length < 1) {
return Promise.reject('Settings datastore inaccessible');
}
var signalChange = () => {
this.save();
window.dispatchEvent(new CustomEvent('settings-changed'));
};
var syncSmallSetting = (signal) => {
stores[0].get(COLUMNS_SETTING).then(cols => {
var oldSmall = this.small;
this.small = cols > 3;
if (this.small !== oldSmall) {
signalChange();
}
});
};
var syncPagingSetting = () => {
stores[0].get(PAGING_SETTING).then(paging => {
var oldSnapping = this.scrollSnapping;
this.scrollSnapping = paging || false;
if (this.scrollSnapping !== oldSnapping) {
signalChange();
}
});
};
stores[0].addEventListener('change', e => {
switch (e.id) {
case COLUMNS_SETTING:
syncSmallSetting();
break;
case PAGING_SETTING:
syncPagingSetting();
break;
}
});
syncSmallSetting();
syncPagingSetting();
}, e => {
console.error('Error retrieving home screen settings datastore:', e);
});
} else {
console.error('Datastore API unavailable');
}
// Restore existing settings
var settingsString = localStorage.getItem('settings');
if (!settingsString) {
this.firstRun = true;
return;
}
var settings = JSON.parse(settingsString);
if (settings.version !== SETTINGS_VERSION) {
return;
}
this.small = settings.small || false;
this.scrollSnapping = settings.scrollSnapping || false;
// Monitor global homescreen settings
if (!navigator.getDataStores) {
console.error('Datastore API unavailable');
return;
}
}
Settings.prototype = {
save: function() {
localStorage.setItem('settings', JSON.stringify({
version: SETTINGS_VERSION,
small: this.small,
scrollSnapping: this.scrollSnapping
}));
}
};
exports.Settings = Settings;
}(window));

View file

@ -0,0 +1 @@
window.GaiaAppIconBaseurl = 'shared/elements/gaia-site-icon/';

175
gaia/apps/homescreen/js/window.js Executable file
View file

@ -0,0 +1,175 @@
/* global Apps, Pages */
'use strict';
(function(exports) {
/**
* Time after receiving a handled hashchange that we ignore hashchange
* events for.
*/
const HASH_CHANGE_DEBOUNCE = 100;
function AppWindow() {
this.header = document.getElementById('page-indicator-header');
this.indicator = document.getElementById('page-indicator');
this.panels = document.getElementById('panels');
this.shadow = document.getElementById('shadow');
this.apps = new Apps();
this.pages = new Pages();
// Panel visibility state
this.appsVisible = undefined;
// Home-button handling
this.ignoreHashChangeTimeout = null;
// Signal handlers
this.indicator.addEventListener('keypress', this);
this.panels.addEventListener('scroll', this);
window.addEventListener('hashchange', this, true);
window.addEventListener('localized', this);
document.addEventListener('visibilitychange', this);
// Update the panel indicator
this.updatePanelIndicator();
// Application has finished initialisation
window.performance.mark('navigationInteractive');
}
AppWindow.prototype = {
updatePanelIndicator: function() {
var appsVisible =
Math.abs(this.panels.scrollLeft) <= this.panels.scrollWidth / 4;
if (this.appsVisible !== appsVisible) {
this.appsVisible = appsVisible;
this.header.setAttribute('data-l10n-id', appsVisible ?
'apps-panel' : 'pages-panel');
this.indicator.children[0].classList.toggle('active', appsVisible);
this.indicator.children[1].classList.toggle('active', !appsVisible);
this.indicator.setAttribute('aria-valuenow', appsVisible ? 1 : 2);
this.indicator.setAttribute('data-l10n-args', JSON.stringify({
currentPage: appsVisible ? 1 : 2,
totalPages: 2
}));
this.apps.panel.setAttribute('aria-hidden', !appsVisible);
this.pages.panel.setAttribute('aria-hidden', appsVisible);
if (appsVisible) {
this.pages.scrollable.removeEventListener('scroll', this);
this.apps.scrollable.addEventListener('scroll', this);
} else {
this.apps.scrollable.removeEventListener('scroll', this);
this.pages.scrollable.addEventListener('scroll', this);
}
this.updateShadowState();
}
},
updateShadowState: function() {
var position = this.appsVisible ?
this.apps.scrollable.scrollTop : this.pages.scrollable.scrollTop;
var scrolled = position > 1;
if (this.shadow.classList.contains('visible') !== scrolled) {
this.shadow.classList.toggle('visible', scrolled);
}
},
handleEvent: function(e) {
switch (e.type) {
// Switch between panels
case 'keypress':
if (!e.ctrlKey) {
return;
}
switch (e.keyCode) {
case e.DOM_VK_RIGHT:
this.panels.scrollTo(
{ left: this.panels.scrollLeftMax, top: 0, behavior: 'smooth' });
return;
case e.DOM_VK_LEFT:
this.panels.scrollTo(
{ left: 0, top: 0, behavior: 'smooth' });
return;
}
return;
// Display the top shadow when scrolling down
case 'scroll':
if (e.target === this.panels) {
this.updatePanelIndicator();
} else {
this.updateShadowState();
}
return;
// Cancel edit modes/scroll to the top/scroll to apps on home-button
case 'hashchange':
if (document.hidden || this.ignoreHashChangeTimeout !== null) {
return;
}
e.preventDefault();
e.stopImmediatePropagation();
this.ignoreHashChangeTimeout = setTimeout(() => {
this.ignoreHashChangeTimeout = null;
}, HASH_CHANGE_DEBOUNCE);
// If a dialog is showing, cancel the dialog
for (var dialog of this.apps.dialogs.concat(this.pages.dialogs)) {
if (!dialog.opened) {
continue;
}
dialog.close();
return;
}
// If we're in edit mode, cancel edit mode
if (this.apps.editMode) {
this.apps.exitEditMode();
return;
}
if (this.pages.editMode) {
this.pages.exitEditMode();
return;
}
// If we're on the pages panel and scrolled to the top, show apps
if (!this.appsVisible && this.pages.scrollable.scrollTop === 0) {
this.panels.scrollTo({ left: 0, top: 0, behavior: 'smooth' });
return;
}
// Otherwise, scroll the visible panel to the top
var visiblePanel = this.appsVisible ?
this.apps.scrollable : this.pages.scrollable;
visiblePanel.scrollTo({ left: 0, top: 0, behavior: 'smooth' });
return;
case 'localized':
this.updatePanelIndicator();
return;
// Exit edit mode if the user switches away from the home screen (e.g.
// by locking the phone).
case 'visibilitychange':
if (document.hidden) {
this.apps.exitEditMode();
this.pages.exitEditMode();
}
return;
}
}
};
exports.AppWindow = AppWindow;
}(window));

18
gaia/apps/homescreen/jsdoc.json Executable file
View file

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

View file

@ -0,0 +1,16 @@
stop-download-body=Do you want to stop downloading {{name}}?
stop-download-action=Stop
resume-download-body=Do you want to download {{name}}?
resume-download-action=Download
cancel-action=Cancel
apps-panel.ariaLabel=Applications
pages-panel.ariaLabel=Pinned Pages
page-indicator-text.ariaValueText = Page {{currentPage}} of {{totalPages}}
no-pinned-pages-header=No Pinned Pages yet.
no-pinned-pages-body=Use your browser to pin any web page to this screen.
edit-done=Done

View file

@ -0,0 +1,74 @@
{
"version": "2.7",
"name": "Default Home Screen",
"description": "Gaia home screen.",
"launch_path": "/index.html",
"type": "privileged",
"role": "homescreen",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"icons": {
"16": "/icons/16.png",
"32": "/icons/32.png",
"48": "/icons/48.png",
"60": "/icons/60.png",
"64": "/icons/64.png",
"90": "/icons/90.png",
"128": "/icons/128.png",
"256": "/icons/256.png",
"512": "/icons/512.png"
},
"permissions": {
"homescreen-webapps-manage": {},
"moz-extremely-unstable-and-will-change-webcomponents": {},
"systemXHR": {}
},
"default_locale": "en-US",
"locales": {
"ar": {
"name": "الشاشة الرئيسية",
"description": "Gaia الشاشة الرئيسية"
},
"en-US": {
"name": "Default Home Screen",
"description": "Gaia Home Screen."
},
"fr": {
"name": "Écran d’accueil",
"description": "Écran d’accueil Gaia"
},
"zh-TW": {
"name": "主畫面",
"description": "Gaia 主畫面"
}
},
"datastores-access": {
"bookmarks_store": {
"readonly": true,
"description": "View bookmarks."
},
"places": {
"readonly": false,
"description": "View and modify data about browsing history."
},
"icons": {
"readonly": false,
"description": "Store and retrieve cached website icons."
},
"homescreen_settings": {
"readonly": false,
"description": "Read and write home screen settings."
},
"vertical_preferences_store": {
"readonly": true,
"description": "Import settings from old home screen."
},
"verticalhome_items_migration": {
"readonly": true,
"description": "Import settings from old home screen."
}
},
"orientation": "default"
}

View file

@ -0,0 +1,10 @@
{
"webide": {
"prepackage": {
"command": "make",
"env": ["APP=homescreen"],
"cwd": "../.."
},
"packageDir": "../../build_stage/homescreen/"
}
}

View file

@ -0,0 +1,169 @@
#apps-panel > .scrollable {
overflow-y: auto;
height: 100%;
transition: transform 0.2s;
padding-top: 1.5vw;
}
#apps-panel > .scrollable.snapping {
scroll-snap-type-y: mandatory;
}
/* This weird arrangement of constant sizes, transforms and containers is so
* the gaia-container can animate fully between small and not-small states.
* Without this animation, the container would be unnecessary and only the
* width would need to be specified.
*/
.icon-container {
position: relative;
display: inline-block;
width: 32vw;
height: calc(32vw + 1.9rem);
}
gaia-app-icon {
position: absolute;
top: 0;
left: 0;
width: 32vw;
box-sizing: border-box;
border: 0.8rem solid transparent;
outline: 0;
transition: transform 0.2s, opacity 0.2s;
font-size: 1.4rem;
font-weight: 400;
color: white;
text-shadow: rgba(0, 0, 0, 0.5) 0 0.1rem 0.3rem;
}
/* Small icons are scaled down 0.75x, so apply the reverse transforms to the
* font so that it conforms to spec.
*/
#apps.small gaia-app-icon {
font-size: calc(1.2rem / 0.75);
text-shadow: rgba(0, 0, 0, 0.5) 0 calc(0.1rem / 0.75) calc(0.3rem / 0.75);
}
gaia-app-icon.launching {
opacity: 0.8;
}
.edit-mode gaia-app-icon {
opacity: 0.5;
}
.edit-mode gaia-app-icon.selected {
transform: scale(1.1);
opacity: 1;
}
#apps.loading {
visibility: hidden;
}
#apps.small > .gaia-container-child > .icon-container {
width: 24vw;
height: calc(24vw + 1.9rem);
}
#apps > .gaia-container-child.dragging > .icon-container > gaia-app-icon {
transform: scale(1.1);
opacity: 0.8;
}
#apps.small > .gaia-container-child > .icon-container > gaia-app-icon {
transform: scale(0.75) translate(-4vw, -4vw);
}
#apps.small > .gaia-container-child.dragging > .icon-container > gaia-app-icon {
transform: scale(0.8) translate(-3.2vw, -3.2vw);
}
.dragging:not(.autoscroll) #apps > .gaia-container-child > .icon-container {
will-change: transform;
}
#apps > .gaia-container-child > .icon-container.hover-before {
transform: translateX(3.5rem);
}
#apps > .gaia-container-child > .icon-container.hover-after {
transform: translateX(-3.5rem);
}
#apps > .gaia-container-child.added gaia-app-icon {
animation-name: icon-added;
animation-duration: 0.4s;
z-index: 1;
}
#apps.small > .gaia-container-child.added gaia-app-icon {
animation-name: small-icon-added;
}
@keyframes icon-added {
0% { opacity: 0; transform: scale(0.5); }
100% { opacity: 1; }
}
@keyframes small-icon-added {
0% { opacity: 0; transform: scale(0.375) translate(-6vw, -6vw); }
100% { opacity: 1; transform: scale(0.75) translate(-4vw, -4vw); }
}
#apps > .gaia-container-child.removed gaia-app-icon {
animation-name: icon-removed;
animation-duration: 0.4s;
}
#apps.small > .gaia-container-child.removed gaia-app-icon {
animation-name: small-icon-removed;
}
@keyframes icon-removed {
0% { transform: scale(1); opacity: 1; }
100% { transform: scale(0.5); opacity: 0; }
}
@keyframes small-icon-removed {
0% { transform: scale(0.75) translate(-4vw, -4vw); opacity: 1; }
100% { transform: scale(0.375) translate(-6vw, -6vw); opacity: 0; }
}
gaia-app-icon.uneditable {
animation-name: icon-uneditable;
animation-duration: 0.4s;
}
#apps.small gaia-app-icon.uneditable {
animation-name: small-icon-uneditable;
}
@keyframes icon-uneditable {
0% { transform: rotateZ(0deg); }
10% { transform: rotateZ(-15deg); }
20% { transform: rotateZ(15deg); }
30% { transform: rotateZ(-15deg); }
40% { transform: rotateZ(15deg); }
50% { transform: rotateZ(-15deg); }
60% { transform: rotateZ(15deg); }
70% { transform: rotateZ(-15deg); }
80% { transform: rotateZ(15deg); }
90% { transform: rotateZ(-15deg); }
100% { transform: rotateZ(0deg); }
}
@keyframes small-icon-uneditable {
0% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(0deg); }
10% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(-15deg); }
20% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(15deg); }
30% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(-15deg); }
40% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(15deg); }
50% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(-15deg); }
60% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(15deg); }
70% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(-15deg); }
80% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(15deg); }
90% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(-15deg); }
100% { transform: scale(0.75) translate(-4vw, -4vw) rotateZ(0deg); }
}

View file

@ -0,0 +1,33 @@
gaia-container {
width: 100vw;
overflow: hidden;
display: flex;
flex-flow: row wrap;
align-content: flex-start;
transition: opacity 0.2s;
box-sizing: padding-box;
padding: 0 0.6rem;
padding-bottom: 6rem;
}
.snapping gaia-container {
background: linear-gradient(to bottom, transparent,
transparent 50%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.15));
background-size: 100% 200%;
background-repeat: repeat-y;
}
gaia-container:not(.loading) > .gaia-container-child:not(.added):not(.dragging) {
transition: transform 0.2s;
}
gaia-container > .gaia-container-child.dragging {
z-index: 1;
will-change: transform;
}
gaia-container > .gaia-container-child:not(.added):not(.removed) > * {
transition: transform 0.2s;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45"><path d="M11.2 17.5l2 20.2c.1 1.5 1.4 2.6 2.9 2.6h13.1c1.4 0 2.7-1.2 2.9-2.6L34 17.5H11.2z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 174 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="45" height="45"><path d="M31.9 9.6h-2v-1c0-2.1-1.7-3.9-3.9-3.9h-7.1c-2.1 0-3.9 1.7-3.9 3.9v1h-2c-2.2 0-4 1.8-4 4v2.7h26.8v-2.7c.1-2.2-1.7-4-3.9-4zm-14.7-.9c0-1.1.9-2 2-2h6.6c1.1 0 2 .9 2 2v.8H17.2v-.8z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19"><path d="M18.56 4.11L4.87 17.8.997 13.922 14.684.235c.545-.543 1.853-.117 2.923.953s1.496 2.378.952 2.922zM3.7 18.786H.008v-3.692L3.7 18.786z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 225 B

View file

@ -0,0 +1,148 @@
#pages-panel {
font-size: 1.5rem;
display: inline-block;
}
#pages-panel > .scrollable {
overflow-y: auto;
height: 100%;
}
#empty-pages {
position: absolute;
width: 100%;
height: 100%;
border-left: 2.2rem solid transparent;
border-right: 2.2rem solid transparent;
overflow: hidden;
color: white;
font-weight: 400;
text-shadow: rgba(0, 0, 0, 0.5) 0 0.1rem 0.3rem;
white-space: normal;
text-align: center;
transition: visibility 0.2s, opacity 0.2s;
}
#empty-pages h1 {
font-size: 1.6rem;
margin: 2.333rem 0;
}
#empty-pages p {
font-size: 1.4rem;
margin: 0 2.333rem 1.266rem;
}
#empty-pages img {
width: 100%;
margin-bottom: 1.8666rem;
}
div#boxes {
position: relative;
height: calc(15rem + 15rem + 1.0666rem);
}
div#boxes > div {
position: absolute;
width: calc(50% - 1.0666rem / 2);
height: 15rem;
border: 0.2rem dashed rgba(255, 255, 255, 0.8);
box-sizing: border-box;
}
/* Pseudo-element to get a shadow */
div#boxes > div:before {
content: ' ';
position: absolute;
top: -0.1rem;
right: -0.2rem;
bottom: -0.3rem;
left: -0.2rem;
border: 0.2rem dashed rgba(0, 0, 0, 0.2);
z-index: -1;
}
div#boxes > div:nth-child(1) {
top: 0;
left: 0;
}
div#boxes > div:nth-child(2) {
top: 0;
right: 0;
}
div#boxes > div:nth-child(3) {
bottom: 0;
left: 0;
}
div#boxes > div:nth-child(4) {
bottom: 0;
right: 0;
}
#pages-panel:not(.empty) #empty-pages {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
#pages {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
min-height: 100%;
padding-top: 1rem;
}
#pages gaia-pin-card {
display: block;
width: calc(50vw - 1.5vw - 0.75rem);
height: 16.5rem;
padding: 0 0 1.5rem 1.5rem;
outline: 0;
transition: opacity 0.2s, transform 0.2s;
}
.edit-mode #pages gaia-pin-card {
opacity: 0.5;
}
.edit-mode #pages gaia-pin-card.selected {
transform: scale(1.1);
opacity: 1;
}
#pages gaia-pin-card.unpinning {
position: fixed;
}
#pages > .gaia-container-child.dragging > gaia-pin-card {
transform: scale(1.1);
opacity: 0.8;
}
#pages > .gaia-container-child.added gaia-pin-card {
animation-name: card-added;
animation-duration: 0.4s;
z-index: 1;
}
@keyframes card-added {
0% { opacity: 0; transform: scale(0.8); }
100% { opacity: 1; }
}
#pages > .gaia-container-child.removed gaia-pin-card {
animation-name: card-removed;
animation-duration: 0.4s;
}
@keyframes card-removed {
0% { opacity: 0.8; }
100% { opacity: 0; }
}

View file

@ -0,0 +1,212 @@
html {
font-size: 10px;
}
body {
background-color: transparent;
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
}
* {
-moz-user-select: none;
}
#page-indicator-header {
width: 100%;
height: 1.5rem;
background-color: rgba(0, 0, 0, 0.1);
}
#page-indicator {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-content: center;
transition: opacity 0.2s;
}
.dialog-active #page-indicator,
.edit-mode #page-indicator {
opacity: 0;
}
#page-indicator span {
position: relative;
display: inline-block;
width: 0.8rem;
height: 0.8rem;
margin: 0 0.25rem 0.5rem;
opacity: 0.6;
}
#page-indicator span:before,
#page-indicator span:after {
position: absolute;
top: 0;
left: 0;
display: block;
content: '';
width: 100%;
height: 100%;
border-radius: 100%;
transition: opacity 0.2s;
}
#page-indicator span:before {
opacity: 0;
background-color: white;
}
#page-indicator span:after {
opacity: 1;
border: 0.1rem solid white;
}
#page-indicator span.active:before {
opacity: 1;
}
#page-indicator span.active:after {
opacity: 0;
}
#panels {
height: calc(100vh - 1.5rem);
overflow-y: hidden;
scroll-snap-type-x: mandatory;
scroll-snap-points-x: repeat(100vw);
white-space: nowrap;
}
.dragging #panels,
.edit-mode #panels {
overflow: hidden;
}
#panels > * {
position: relative;
display: inline-block;
width: 100vw;
height: 100%;
vertical-align: top;
}
#shadow {
position: absolute;
top: 1.5rem;
z-index: 1;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), transparent);
width: 100%;
height: 0.8rem;
transition: visibility 0.3s, opacity 0.3s;
opacity: 0;
visibility: hidden;
pointer-events: none;
}
#shadow.visible {
opacity: 0.5;
visibility: visible;
}
.dialog-active #shadow.visible {
opacity: 0;
}
#bottombar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 5.5rem;
background: rgba(0, 0, 0, 0.35);
transition: transform 0.2s;
transform: translateY(6.5rem);
display: flex;
align-content: center;
}
.edit-mode #bottombar {
transform: none;
}
#bottombar > * {
display: inline-block;
transition: opacity 0.2s;
opacity: 0.2;
pointer-events: none;
}
#bottombar > *.active {
opacity: 1;
pointer-events: auto;
}
#remove {
display: inline-block;
flex: 1;
background: no-repeat center center url('images/delete_bottom.svg');
background-size: 3.0rem 3.0rem;
}
#remove::after {
display: block;
content: '';
width: 100%;
height: 100%;
background: no-repeat center center url('images/delete_lid.svg');
background-size: 3.0rem 3.0rem;
transform-origin: calc(50% + 1.75rem) calc(50% + 0.8rem);
transition: transform 0.2s;
}
#remove:active:after {
transform: rotateZ(30deg);
}
#rename {
flex: 1;
background: no-repeat center center url('images/rename.svg');
background-size: 2.0rem 2.0rem;
transition: transform 0.2s;
}
#rename:active {
transform: rotateZ(-45deg);
}
#done {
display: inline-flex;
flex: 1;
opacity: 1;
font-size: 1.4rem;
font-weight: 400;
font-style: italic;
color: white;
justify-content: center;
align-items: center;
pointer-events: auto;
}
#done:active {
color: #00caf2;
}
.edit-underlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.10);
transition: opacity 0.2s;
opacity: 0;
}
.edit-mode .edit-underlay {
opacity: 1;
}

View file

@ -0,0 +1,27 @@
'use strict';
/* jshint node: true, mocha: true */
/* global suiteSetup */
var helper = require('helper');
suite('Homescreen tests', function() {
suiteSetup(helper.cleanupWorkspace);
teardown(helper.cleanupWorkspace);
test('APP=homescreen make', function(done) {
helper.exec('APP=homescreen make', function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var zipPath = process.cwd() +
'/profile/webapps/homescreen.gaiamobile.org/application.zip';
var checkList = ['manifest.webapp'];
checkList.forEach(function(filePath) {
helper.checkFilePathInZip(zipPath, filePath);
});
done();
});
});
});

View file

@ -0,0 +1,19 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"__dirname",
"module",
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"marionette",
"marionetteScriptFinished",
"require",
"define",
"requireApp",
"sinon"
]
}

View file

@ -0,0 +1,65 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Appcache installed download', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/appcache';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('shows spinner while downloading', function() {
// correctly install the app...
client.switchToFrame();
// ensure appcache path is delayed
server.cork(server.manifest.appcache_path);
appInstall.install(server.manifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.manifestURL);
// wait until we see the nice spinner thing
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// let the rest of the app come through
server.uncork(server.manifest.appcache_path);
// wait until it is no longer loading
client.waitFor(function() {
return !home.iconIsLoading(icon);
});
// ensure the app launches!
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,38 @@
'use strict';
var assert = require('assert');
marionette('Homescreen - App Blacklist', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var home, system;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
client.apps.launch(home.URL);
home.waitForLaunch();
// Fail finding elements quickly.
client.setSearchTimeout(20);
});
function lookForIcon(manifestURL) {
try {
home.getIcon(manifestURL);
} catch(e) {
return false;
}
return true;
}
test('app does not appear on home screen if in blacklist', function() {
// Ensure that a normal icon is there.
var calendarApp = lookForIcon('app://calendar.gaiamobile.org');
assert.ok(calendarApp);
});
});

View file

@ -0,0 +1,54 @@
'use strict';
/* global __dirname */
var assert = require('assert');
marionette('Homescreen - Default app order', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var home, system;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
home.waitForLaunch();
});
test('Default app order is respected', function() {
var expectedOrder = [
'Phone',
'Messages',
'Contacts',
'E-Mail',
'Browser',
'Camera',
'Gallery',
'Music',
'Video',
'Marketplace',
'Calendar',
'Clock',
'Settings',
'FM Radio',
'BuddyUp',
'Bugzilla Lite',
'Facebook',
'Twitter',
'Notes',
'Calculator',
'Usage'
];
var length = expectedOrder.length;
var iconNames = home.visibleIcons.map(function(icon) {
return home.getIconText(icon);
}).slice(0, length);
for (var i = 0; i < length; i++) {
assert.equal(iconNames[i], expectedOrder[i]);
}
});
});

View file

@ -0,0 +1,60 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Hosted app failed icon fetch', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('fallback to default icon', function() {
var iconURL = server.manifest.icons['128'];
// correctly install the app...
client.switchToFrame();
// ensure the icon fails to download!
server.fail(iconURL);
appInstall.install(server.manifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.manifestURL);
// ensure the default icon is shown
home.waitForIconImageUrl(icon, 'default');
// ensure the icon can be launched!
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,44 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Hosted App Install', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('install app', function() {
client.switchToFrame();
appInstall.install(server.manifestURL);
client.switchToFrame(system.getHomescreenIframe());
home.launchIcon(home.getIcon(server.manifestURL));
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,113 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Hosted app failed icon fetch', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var actions, home, system, appInstall;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
function tapAndWaitFor(icon, element) {
client.waitFor(function() {
icon.tap();
actions.wait(0.5).perform();
return element.scriptWith(function(el) {
return getComputedStyle(el).display !== 'none';
});
});
}
test('shows icon after a restart', function() {
// go to the system app
client.switchToFrame();
// don't let the server send the zip archive
server.cork(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.packageManifestURL);
home.scrollIconToCenter(icon);
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// stop the download
tapAndWaitFor(icon, home.cancelDownloadDialog);
home.actionDialog(home.cancelDownloadDialog, 'stop-download-action');
home.waitForIconImageUrl(icon, 'app_install_canceled');
// Restart the download
server.uncork(server.applicationZipUri);
// resume the download from the ui
tapAndWaitFor(icon, home.resumeDownloadDialog);
home.actionDialog(home.resumeDownloadDialog, 'resume-download-action');
// wait until we are showing our desired icon
home.waitForIconImageUrl(icon, 'app-icon');
});
test('fallback to default icon when icon fails', function() {
var iconURL = server.manifest.icons['128'];
// correctly install the app...
client.switchToFrame();
// ensure the icon fails to download!
server.fail(iconURL);
appInstall.install(server.manifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.manifestURL);
// ensure the default icon is shown
home.waitForIconImageUrl(icon, 'default');
server.unfail(iconURL);
// XXX: We don't have real network access but we can sorta emulate it by
// failing then switching back to an online state.
client.executeScript(function() {
window.dispatchEvent(new CustomEvent('online'));
});
// wait until we are showing our desired icon
home.waitForIconImageUrl(icon, 'app-icon');
// ensure the icon can be launched!
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,86 @@
/* global __dirname */
'use strict';
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Hosted app cached icon fetch', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('fallback to default icon', function() {
var iconURL = server.manifest.icons['128'];
// correctly install the app...
client.switchToFrame();
appInstall.install(server.manifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.manifestURL);
// ensure it is cached
home.waitForIconImageUrl(icon, 'app-icon');
var id = home.getIconId(icon);
client.waitFor(function() {
var metadata = client.executeAsyncScript(function() {
window.wrappedJSObject.appWindow.apps.metadata.getAll().then(
marionetteScriptFinished);
});
for (var i = 0, iLen = metadata.length; i < iLen; i++) {
if (metadata[i].id.startsWith(id) &&
typeof metadata[i].icon !== 'undefined') {
return true;
}
}
return false;
});
// ensure http fails so we use the cached icon
server.fail(iconURL);
// Reload the homescreen
home.restart();
icon = home.getIcon(server.manifestURL);
// check for the cached icon...
home.waitForIconImageUrl(icon, 'user-set');
// allow the request to succeed
server.unfail(iconURL);
// trigger the download of a new icon
client.executeScript(function() {
window.wrappedJSObject.dispatchEvent(new CustomEvent('online'));
});
// now shows the freshly redownloaded icon
home.waitForIconImageUrl(icon, 'app-icon');
});
});

View file

@ -0,0 +1,66 @@
'use strict';
/* global __dirname */
var assert = require('assert');
marionette('Homescreen - App order', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var actions, home, system;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
system.waitForFullyLoaded();
home.waitForLaunch();
});
test('App order is retained after restart', function() {
// Drag icon to a different place
var icons = home.visibleIcons;
var location1 = icons[0].location();
var location2 = icons[1].location();
actions.wait(0.5).press(icons[0]).wait(0.5).
move(icons[1]).release().wait(0.5).perform();
client.waitFor(function() {
return icons[0].location().x === location2.x &&
icons[1].location().x === location1.x;
});
// Wait for the icon order to be saved
var ids = home.getIconIdentifiers();
var numIcons = ids.length;
client.waitFor(function() {
var order = client.executeAsyncScript(function() {
window.wrappedJSObject.appWindow.apps.metadata.getAll().then(
marionetteScriptFinished);
});
// The order array is stored in order, but also contains non-visible
// icons, so just skip unknown entries.
var correctlyPlacedIcons = 0;
for (var i = 0, iLen = order.length; i < iLen; i++) {
if (order[i].id.startsWith(ids[correctlyPlacedIcons])) {
++ correctlyPlacedIcons;
}
}
return correctlyPlacedIcons === numIcons;
});
// Test that icon ordering is retained after a restart
home.restart();
client.waitFor(function() {
return home.visibleIcons.length === numIcons;
});
var newIds = home.getIconIdentifiers();
for (var i = 0, iLen = ids.length; i < iLen; i++) {
assert.equal(ids[i], newIds[i]);
}
});
});

View file

@ -0,0 +1,59 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Failed Download', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js'),
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('failed state then retry and launch', function() {
client.switchToFrame();
server.fail(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
appInstall.dismissToast();
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.packageManifestURL);
home.waitForIconImageUrl(icon, 'failed');
server.unfail(server.applicationZipUri);
icon.tap();
home.actionDialog(home.resumeDownloadDialog, 'resume-download-action');
home.waitForIconImageUrl(icon, 'app-icon');
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,45 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Install', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('install app', function() {
client.switchToFrame();
appInstall.installPackage(server.packageManifestURL);
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.packageManifestURL);
home.waitForIconImageUrl(icon, 'app-icon');
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,61 @@
/* global __dirname */
'use strict';
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Pending', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('app loading spinner', function() {
// go to the system app
client.switchToFrame();
// don't let the server send the zip archive
server.cork(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.packageManifestURL);
// wait until the icon is spinning!
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// let the rest of the app come through
server.uncork(server.applicationZipUri);
// wait until it is no longer loading
client.waitFor(function() {
return !home.iconIsLoading(icon);
});
});
});

View file

@ -0,0 +1,63 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Pending Update', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
// wait for the system app to be running
system.waitForFullyLoaded();
client.apps.launch(home.URL);
home.waitForLaunch();
// install the app
client.switchToFrame();
appInstall.installPackage(server.packageManifestURL);
// switch to the homescreen
client.switchToFrame(system.getHomescreenIframe());
});
teardown(function(done) {
server.close(done);
});
test('update an installed app', function() {
var icon = home.getIcon(server.packageManifestURL);
// ensure the app is installed before updating it
home.waitForIconImageUrl(icon, 'app-icon');
// Update the manifests.
server.setRoot(__dirname + '/fixtures/template_app_updated');
// Stage the update
appInstall.stageUpdate(server.packageManifestURL);
// ensure we can still launch an app in the older version
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,93 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Resuming Downloads', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server, actions, home, system, appInstall;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
done(err);
});
});
teardown(function(done) {
server.close(done);
});
function tapAndWaitFor(icon, element) {
client.waitFor(function() {
icon.tap();
actions.wait(0.5).perform();
return element.scriptWith(function(el) {
return getComputedStyle(el).display !== 'none';
});
});
}
test('failed state then retry and launch', function() {
client.switchToFrame();
server.cork(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
client.switchToFrame(system.getHomescreenIframe());
// Get the icon
var icon = home.getIcon(server.packageManifestURL);
// Scroll so the icon is in the middle of the screen and won't be
// obscured by the system toast
home.scrollIconToCenter(icon);
// pause the download
tapAndWaitFor(icon, home.cancelDownloadDialog);
home.actionDialog(home.cancelDownloadDialog, 'stop-download-action');
home.waitForIconImageUrl(icon, 'app_install_canceled');
// resume the download
tapAndWaitFor(icon, home.resumeDownloadDialog);
home.actionDialog(home.resumeDownloadDialog, 'resume-download-action');
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// pause it again!
tapAndWaitFor(icon, home.cancelDownloadDialog);
home.actionDialog(home.cancelDownloadDialog, 'stop-download-action');
home.waitForIconImageUrl(icon, 'app_install_canceled');
// uncork so next resume works...
server.uncork(server.applicationZipUri);
// finally download the entire app
tapAndWaitFor(icon, home.resumeDownloadDialog);
home.actionDialog(home.resumeDownloadDialog, 'resume-download-action');
home.waitForIconImageUrl(icon, 'app-icon');
// now it should work!
home.launchIcon(icon);
assert.equal(client.title(), 'iwrotethis');
});
});

View file

@ -0,0 +1,101 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Resume Update', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var actions, home, system, appInstall;
setup(function() {
actions = client.loader.getActions();
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
// wait for the system app to be running
system.waitForFullyLoaded();
home.waitForLaunch();
// install the app
client.switchToFrame();
appInstall.installPackage(server.packageManifestURL);
// switch to the homescreen
client.switchToFrame(system.getHomescreenIframe());
});
teardown(function(done) {
server.close(done);
});
function tapAndWaitFor(icon, element) {
client.waitFor(function() {
icon.tap();
actions.wait(0.5).perform();
return element.scriptWith(function(el) {
return getComputedStyle(el).display !== 'none';
});
});
}
test('resume update', function() {
var icon = home.getIcon(server.packageManifestURL);
// Scroll so the icon is in the middle of the screen and won't be
// obscured by the system toast
home.scrollIconToCenter(icon);
// ensure the app is installed before updating it
home.waitForIconImageUrl(icon, 'app-icon');
// Cork the zip so we can see the loading spinner.
server.cork('/app.zip');
// Update the manifests.
server.setRoot(__dirname + '/fixtures/template_app_updated');
// Ensure we have an update.
appInstall.update(server.packageManifestURL);
// Ensure we see the loading signs...
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// pause the update
tapAndWaitFor(icon, home.cancelDownloadDialog);
home.actionDialog(home.cancelDownloadDialog, 'stop-download-action');
home.waitForIconImageUrl(icon, 'app_install_canceled');
// resume the update
tapAndWaitFor(icon, home.resumeDownloadDialog);
home.actionDialog(home.resumeDownloadDialog, 'resume-download-action');
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// Wait for the download to be complete.
server.uncork('/app.zip');
home.waitForIconImageUrl(icon, 'app-icon');
// verify the app was really updated
home.launchIcon(icon);
assert.equal(client.title(), 'updatedwow');
});
});

View file

@ -0,0 +1,59 @@
/* global __dirname */
'use strict';
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Pending', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/role_homescreen';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
system.waitForFullyLoaded();
home.waitForLaunch();
});
teardown(function(done) {
server.close(done);
});
test('app is displayed until determined role=homescreen', function() {
// go to the system app
client.switchToFrame();
// don't let the server send the zip archive
server.cork(server.applicationZipUri);
appInstall.installPackage(server.packageManifestURL);
// switch back to the homescreen
client.switchToFrame(system.getHomescreenIframe());
var icon = home.getIcon(server.packageManifestURL);
// wait until the icon is spinning!
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// let the rest of the app come through
server.uncork(server.applicationZipUri);
// wait until the icon is hidden
client.helper.waitForElementToDisappear(icon);
});
});

View file

@ -0,0 +1,93 @@
/* global __dirname */
'use strict';
var assert = require('assert');
var AppInstall =
require('../../../../apps/system/test/marionette/lib/app_install');
var createAppServer = require('./server/parent');
marionette('Homescreen - Packaged App Update', function() {
var client = marionette.client({
profile: require(__dirname + '/client_options.js')
});
var server;
setup(function(done) {
var app = __dirname + '/fixtures/template_app';
createAppServer(app, client, function(err, _server) {
server = _server;
done(err);
});
});
var home, system, appInstall;
setup(function() {
home = client.loader.getAppClass('homescreen');
system = client.loader.getAppClass('system');
appInstall = new AppInstall(client);
// wait for the system app to be running
system.waitForFullyLoaded();
// Launch the homescreen first, then go to the system app.
// Make sure we do this before installing an application.
home.waitForLaunch();
client.switchToFrame();
// install the app with a broken icon
appInstall.installPackage(server.packageManifestURL);
// switch to the homescreen
client.switchToFrame(system.getHomescreenIframe());
});
teardown(function(done) {
server.close(done);
});
test('update an installed app', function() {
var icon = home.getIcon(server.packageManifestURL);
// ensure the app is installed before updating it
home.waitForIconImageUrl(icon, 'app-icon');
// Prepare to screenshot
var iconImage = home.getIconImage(icon);
var iconTitle = home.getIconText(icon);
// Cork the zip so we can see the loading spinner.
server.cork('/app.zip');
// Update the manifests.
server.setRoot(__dirname + '/fixtures/template_app_updated');
// Ensure we have an update.
appInstall.update(server.packageManifestURL);
// Ensure we see the loading signs...
client.waitFor(function() {
return home.iconIsLoading(icon);
});
// Wait for the download to be complete.
server.uncork('/app.zip');
home.waitForIconImageUrl(icon, 'app-icon');
// See bug 826555 for rationale for the title not changing.
assert.equal(
home.getIconText(icon),
iconTitle,
'app name should not be updated'
);
// Make sure the icon changed
assert.notEqual(
home.getIconImage(icon),
iconImage,
'app icon should have updated');
// verify the app was really updated
home.launchIcon(icon);
assert.equal(client.title(), 'updatedwow');
});
});

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