Publish Vulpes Preview 15 sources for desktop, Android and Tundra

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

View file

@ -0,0 +1 @@
;;;window.COMPONENTS_BASE_URL='/shared/elements/';

View file

@ -0,0 +1,234 @@
/* globals define */
(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var component = require('gaia-component');
/**
* Simple logger
* @type {Function}
*/
var debug = 0 ? console.log.bind(console) : function() {};
/**
* Exports
*/
module.exports = component.register('gaia-button', {
extends: HTMLButtonElement.prototype,
created: function() {
this.setupShadowRoot();
// Configure
this.disabled = this.getAttribute('disabled');
// process everything that doesn't affect user interaction
// after the component is created
setTimeout(() => this.makeAccessible());
},
/**
* Accessibility enhancements.
* Read gaia-button as button.
* make it tabable
* read its disabled state
*/
makeAccessible: function() {
this.setAttribute('role', 'button');
// Make tabable
this.tabIndex = 0;
if (this.disabled) {
this.setAttribute('aria-disabled', true);
}
},
attrs: {
circular: {
get: function() { return this.getAttribute('circular'); },
set: function(value) {
value = !!(value === '' || value);
if (value) {
this.setAttribute('circular', '');
} else {
this.removeAttribute('circular');
}
}
},
disabled: {
get: function() { return this._disabled; },
set: function(value) {
value = !!(value || value === '');
if (this._disabled === value) { return; }
debug('set disabled', value);
this._disabled = value;
if (value) {
this.setAttr('disabled', '');
this.setAttribute('aria-disabled', true);
} else {
this.removeAttr('disabled');
this.removeAttribute('aria-disabled');
}
}
}
},
template: `
<div class="inner">
<div class="background"></div>
<div class="content"><content></content></div>
</div>
<style>
:host {
display: block;
box-sizing: border-box;
overflow: hidden;
height: 50px;
min-width: 50%;
border-radius: 50px;
margin: var(--base-m, 18px) 0;
outline: 0;
font-style: italic;
font-size: 17px;
cursor: pointer;
-moz-user-select: none;
line-height: 1;
background:
var(--button-background,
var(--input-background,
var(--background-plus,
#fff)));
color:
var(--button-color,
var(--text-color,
inherit));
box-shadow:
var(--button-box-shadow,
var(--box-shadow,
none));
transition: color 0ms 300ms;
}
@media(min-width:500px) {
:host { min-width: 140px; }
}
/**
* :active
*/
:host(:active) {
color: var(--button-color-active, #fff);
box-shadow: var(--button-box-shadow-active, none);
transition: none;
}
/**
* [circular]
*/
:host([circular]) {
width: 50px;
min-width: 0;
max-width: 50px;
border-radius: 50%;
}
:host([disabled]) {
pointer-events: none;
opacity: 0.5;
}
/** Inner
---------------------------------------------------------*/
.inner {
position: relative;
height: 100%;
}
/** Background
---------------------------------------------------------*/
.background {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 500ms 200ms;
background:
var(--button-background-active,
var(--highlight-color,
#333));
}
:active .background {
transition: none;
opacity: 1;
}
/** Content
---------------------------------------------------------*/
/**
* 1. In some cases events seems to be getting
* swallowed by text-nodes. Ignoring pointer-
* events means we can listen on parent nodes
* instead.
*/
.content {
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 2;
height: 100%;
padding: 0 18px;
pointer-events: none; /* 1 */
}
[circular] .content {
padding: 0;
}
::content i:before {
font-size: 22px;
}
::content i {
margin-left: -2px;
margin-right: -2px;
}
::content i + span,
::content span + i {
-moz-margin-start: 8px;
}
</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-button',this));

View file

@ -0,0 +1,460 @@
/* globals define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Locals
*/
var textContent = Object.getOwnPropertyDescriptor(Node.prototype,
'textContent');
var innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
var removeAttribute = Element.prototype.removeAttribute;
var setAttribute = Element.prototype.setAttribute;
var noop = function() {};
/**
* Register a new component.
*
* @param {String} name
* @param {Object} props
* @return {constructor}
* @public
*/
exports.register = function(name, props) {
var baseProto = getBaseProto(props.extends);
var template = props.template || baseProto.templateString;
// Components are extensible by default but can be declared
// as non extensible as an optimization to avoid
// storing the template strings
var extensible = props.extensible = props.hasOwnProperty('extensible')?
props.extensible : true;
// Clean up
delete props.extends;
// Pull out CSS that needs to be in the light-dom
if (template) {
// Stores the string to be reprocessed when
// a new component extends this one
if (extensible && props.template) {
props.templateString = props.template;
}
var output = processCss(template, name);
props.template = document.createElement('template');
props.template.innerHTML = output.template;
props.lightCss = output.lightCss;
props.globalCss = props.globalCss || '';
props.globalCss += output.globalCss;
}
// Inject global CSS into the document,
// and delete as no longer needed
injectGlobalCss(props.globalCss);
delete props.globalCss;
// Merge base getter/setter attributes with the user's,
// then define the property descriptors on the prototype.
var descriptors = mixin(props.attrs || {}, base.descriptors);
// Store the orginal descriptors somewhere
// a little more private and delete the original
props._attrs = props.attrs;
delete props.attrs;
// Create the prototype, extended from base and
// define the descriptors directly on the prototype
var proto = createProto(baseProto, props);
Object.defineProperties(proto, descriptors);
// Register the custom-element and return the constructor
try {
return document.registerElement(name, { prototype: proto });
} catch (e) {
if (e.name !== 'NotSupportedError') {
throw e;
}
}
};
var base = {
properties: {
GaiaComponent: true,
attributeChanged: noop,
attached: noop,
detached: noop,
created: noop,
createdCallback: function() {
if (this.dirObserver) { addDirObserver(); }
injectLightCss(this);
this.created();
},
/**
* It is very common to want to keep object
* properties in-sync with attributes,
* for example:
*
* el.value = 'foo';
* el.setAttribute('value', 'foo');
*
* So we support an object on the prototype
* named 'attrs' to provide a consistent
* way for component authors to define
* these properties. When an attribute
* changes we keep the attr[name]
* up-to-date.
*
* @param {String} name
* @param {String||null} from
* @param {String||null} to
*/
attributeChangedCallback: function(name, from, to) {
var prop = toCamelCase(name);
if (this._attrs && this._attrs[prop]) { this[prop] = to; }
this.attributeChanged(name, from, to);
},
attachedCallback: function() {
if (this.dirObserver) {
this.setInnerDirAttributes = setInnerDirAttributes.bind(null, this);
document.addEventListener('dirchanged', this.setInnerDirAttributes);
}
this.attached();
},
detachedCallback: function() {
if (this.dirObserver) {
document.removeEventListener('dirchanged', this.setInnerDirAttributes);
}
this.detached();
},
/**
* A convenient method for setting up
* a shadow-root using the defined template.
*
* @return {ShadowRoot}
*/
setupShadowRoot: function() {
if (!this.template) { return; }
var node = document.importNode(this.template.content, true);
this.createShadowRoot().appendChild(node);
if (this.dirObserver) { setInnerDirAttributes(this); }
return this.shadowRoot;
},
/**
* Sets an attribute internally
* and externally. This is so that
* we can style internal shadow-dom
* content.
*
* @param {String} name
* @param {String} value
*/
setAttr: function(name, value) {
var internal = this.shadowRoot.firstElementChild;
setAttribute.call(internal, name, value);
setAttribute.call(this, name, value);
},
/**
* Removes an attribute internally
* and externally. This is so that
* we can style internal shadow-dom
* content.
*
* @param {String} name
* @param {String} value
*/
removeAttr: function(name) {
var internal = this.shadowRoot.firstElementChild;
removeAttribute.call(internal, name);
removeAttribute.call(this, name);
}
},
descriptors: {
textContent: {
set: function(value) {
textContent.set.call(this, value);
if (this.lightStyle) { this.appendChild(this.lightStyle); }
},
get: function() {
return textContent.get();
}
},
innerHTML: {
set: function(value) {
innerHTML.set.call(this, value);
if (this.lightStyle) { this.appendChild(this.lightStyle); }
},
get: innerHTML.get
}
}
};
/**
* The default base prototype to use
* when `extends` is undefined.
*
* @type {Object}
*/
var defaultPrototype = createProto(HTMLElement.prototype, base.properties);
/**
* Returns a suitable prototype based
* on the object passed.
*
* @private
* @param {HTMLElementPrototype|undefined} proto
* @return {HTMLElementPrototype}
*/
function getBaseProto(proto) {
if (!proto) { return defaultPrototype; }
proto = proto.prototype || proto;
return !proto.GaiaComponent ?
createProto(proto, base.properties) : proto;
}
/**
* Extends the given proto and mixes
* in the given properties.
*
* @private
* @param {Object} proto
* @param {Object} props
* @return {Object}
*/
function createProto(proto, props) {
return mixin(Object.create(proto), props);
}
/**
* Detects presence of shadow-dom
* CSS selectors.
*
* @private
* @return {Boolean}
*/
var hasShadowCSS = (function() {
var div = document.createElement('div');
try { div.querySelector(':host'); div.querySelector('::content'); return true; }
catch (e) { return false; }
})();
/**
* Regexs used to extract shadow-css
*
* @type {Object}
*/
var regex = {
shadowCss: /(?:\:host|\:\:content)[^{]*\{[^}]*\}/g,
':host': /(?:\:host)/g,
':host()': /\:host\((.+)\)(?: \:\:content)?/g,
':host-context': /\:host-context\((.+)\)([^{,]+)?/g,
'::content': /(?:\:\:content)/g
};
/**
* Extracts the :host and ::content rules
* from the shadow-dom CSS and rewrites
* them to work from the <style scoped>
* injected at the root of the component.
*
* @private
* @return {String}
*/
function processCss(template, name) {
var globalCss = '';
var lightCss = '';
if (!hasShadowCSS) {
template = template.replace(regex.shadowCss, function(match) {
var hostContext = regex[':host-context'].exec(match);
if (hostContext) {
globalCss += match
.replace(regex['::content'], '')
.replace(regex[':host-context'], '$1 ' + name + '$2')
.replace(/ +/g, ' '); // excess whitespace
} else {
lightCss += match
.replace(regex[':host()'], name + '$1')
.replace(regex[':host'], name)
.replace(regex['::content'], name);
}
return '';
});
}
return {
template: template,
lightCss: lightCss,
globalCss: globalCss
};
}
/**
* Some CSS rules, such as @keyframes
* and @font-face don't work inside
* scoped or shadow <style>. So we
* have to put them into 'global'
* <style> in the head of the
* document.
*
* @private
* @param {String} css
*/
function injectGlobalCss(css) {
if (!css) {return;}
var style = document.createElement('style');
style.innerHTML = css.trim();
headReady().then(function() {
document.head.appendChild(style);
});
}
/**
* Resolves a promise once document.head is ready.
*
* @private
*/
function headReady() {
return new Promise(function(resolve) {
if (document.head) { return resolve(); }
window.addEventListener('load', function fn() {
window.removeEventListener('load', fn);
resolve();
});
});
}
/**
* The Gecko platform doesn't yet have
* `::content` or `:host`, selectors,
* without these we are unable to style
* user-content in the light-dom from
* within our shadow-dom style-sheet.
*
* To workaround this, we clone the <style>
* node into the root of the component,
* so our selectors are able to target
* light-dom content.
*
* @private
*/
function injectLightCss(el) {
if (hasShadowCSS) { return; }
var stylesheet = el.querySelector('style');
if (!stylesheet) {
stylesheet = document.createElement('style');
stylesheet.setAttribute('scoped', '');
stylesheet.appendChild(document.createTextNode(el.lightCss));
el.appendChild(stylesheet);
}
el.lightStyle = stylesheet;
}
/**
* Convert hyphen separated
* string to camel-case.
*
* Example:
*
* toCamelCase('foo-bar'); //=> 'fooBar'
*
* @private
* @param {String} string
* @return {String}
*/
function toCamelCase(string) {
return string.replace(/-(.)/g, function replacer(string, p1) {
return p1.toUpperCase();
});
}
/**
* Observer (singleton)
*
* @type {MutationObserver|undefined}
*/
var dirObserver;
/**
* Workaround for bug 1100912: applies a `dir` attribute to all shadowRoot
* children so that :-moz-dir() selectors work on shadow DOM elements.
*
* In order to keep decent performances, the `dir` is the component dir if
* defined, or the document dir otherwise. This won't work if the component's
* direction is defined by CSS or inherited from a parent container.
*
* This method should be removed when bug 1100912 is fixed.
*
* @private
* @param {WebComponent}
*/
function setInnerDirAttributes(component) {
var dir = component.dir || document.dir;
Array.from(component.shadowRoot.children).forEach(element => {
if (element.nodeName !== 'STYLE') {
element.dir = dir;
}
});
}
/**
* Observes the document `dir` (direction) attribute and when it changes:
* - dispatches a global `dirchanged` event;
* - forces the `dir` attribute of all shadowRoot children.
*
* Components can listen to this event and make internal changes if needed.
*
* @private
*/
function addDirObserver() {
if (dirObserver) { return; }
dirObserver = new MutationObserver(onChanged);
dirObserver.observe(document.documentElement, {
attributeFilter: ['dir'],
attributes: true
});
function onChanged(mutations) {
document.dispatchEvent(new Event('dirchanged'));
}
}
/**
* Copy the values of all properties from
* source object `target` to a target object `source`.
* It will return the target object.
*
* @private
* @param {Object} target
* @param {Object} source
* @returns {Object}
*/
function mixin(target, source) {
for (var key in source) {
target[key] = source[key];
}
return target;
}
});})(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-component',this));

View file

@ -0,0 +1,495 @@
/* 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;
z-index: 200;
width: 100%;
height: 100%;
overflow: hidden;
font-size: 17px;
font-style: italic;
text-align: center;
color: var(--text-color);
}
/** Inner
---------------------------------------------------------*/
.dialog-inner {
display: flex;
width: 100%;
height: 100%;
padding: 20px;
box-sizing: border-box;
align-items: center;
justify-content: center;
}
/** Background
---------------------------------------------------------*/
.background {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
opacity: 0;
background: var(--overlay-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: 100%;
margin: auto;
box-shadow: 0 1px 0 0px rgba(0,0,0,0.15);
background: var(--background);
transition: opacity 300ms;
opacity: 0;
}
@media (min-width: 360px) {
.window {
width: auto;
min-width: 320px;
}
}
.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: var(--title-color);
}
::content strong {
font-weight: 700;
}
::content small {
font-size: 0.8em;
}
/** Section
---------------------------------------------------------*/
::content section {
padding: 33px 18px;
}
::content section > *:not(:last-child) {
margin-bottom: 13px;
}
/** Paragraphs
---------------------------------------------------------*/
::content p {
text-align: start;
line-height: 1.3em;
}
/** Buttons
---------------------------------------------------------*/
::content button {
position: relative;
display: block;
width: 100%;
height: 50px;
border: 0;
padding: 0 16px;
cursor: pointer;
font: inherit;
background: var(--background-plus);
color: var(--text-color);
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(--text-color-minus);
}
/** Button Divider Line
---------------------------------------------------------*/
::content button:after {
content: '';
display: block;
position: absolute;
height: 1px;
left: 6px;
right: 6px;
top: 49px;
background: var(--background);
}
::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: var(--background);
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,373 @@
;;;(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GaiaHeader=f()}})(function(){var define,module,exports;return(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){;(function(define){define(function(require,exports,module){'use strict';var debug=0?console.log.bind(console):function(){};var cache={};var MIN=16;var MAX=24;var BUFFER=3;module.exports=function(config){debug('font fit',config);var space=config.space-BUFFER;var min=config.min||MIN;var max=config.max||MAX;var text=trim(config.text);var fontSize=max;var textWidth;var font;do{font=config.font.replace(/\d+px/,fontSize+'px');textWidth=getTextWidth(text,font);}while(textWidth>space&&fontSize!==min&&fontSize--);return{textWidth:textWidth,fontSize:fontSize,overflowing:textWidth>space};};function getTextWidth(text,font){var ctx=getCanvasContext(font);var width=ctx.measureText(text).width;debug('got text width',width);return width;}
function getCanvasContext(font){debug('get canvas context',font);var cached=cache[font];if(cached){return cached;}
var canvas=document.createElement('canvas');canvas.setAttribute('moz-opaque','true');canvas.setAttribute('width','1px');canvas.setAttribute('height','1px');debug('created canvas',canvas);var ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.font=font;return cache[font]=ctx;}
function trim(text){return text.replace(/\s+/g,' ').trim();}});})(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;};})('font-fit',this));},{}],2:[function(require,module,exports){;(function(define){'use strict';define(function(require,exports,module){var textContent=Object.getOwnPropertyDescriptor(Node.prototype,'textContent');var innerHTML=Object.getOwnPropertyDescriptor(Element.prototype,'innerHTML');var removeAttribute=Element.prototype.removeAttribute;var setAttribute=Element.prototype.setAttribute;var noop=function(){};exports.register=function(name,props){var baseProto=getBaseProto(props.extends);var template=props.template||baseProto.templateString;var extensible=props.extensible=props.hasOwnProperty('extensible')?props.extensible:true;delete props.extends;if(template){if(extensible&&props.template){props.templateString=props.template;}
var output=processCss(template,name);props.template=document.createElement('template');props.template.innerHTML=output.template;props.lightCss=output.lightCss;props.globalCss=props.globalCss||'';props.globalCss+=output.globalCss;}
injectGlobalCss(props.globalCss);delete props.globalCss;var descriptors=mixin(props.attrs||{},base.descriptors);props._attrs=props.attrs;delete props.attrs;var proto=createProto(baseProto,props);Object.defineProperties(proto,descriptors);try{return document.registerElement(name,{prototype:proto});}catch(e){if(e.name!=='NotSupportedError'){throw e;}}};var base={properties:{GaiaComponent:true,attributeChanged:noop,attached:noop,detached:noop,created:noop,createdCallback:function(){if(this.dirObserver){addDirObserver();}
injectLightCss(this);this.created();},attributeChangedCallback:function(name,from,to){var prop=toCamelCase(name);if(this._attrs&&this._attrs[prop]){this[prop]=to;}
this.attributeChanged(name,from,to);},attachedCallback:function(){if(this.dirObserver){this.setInnerDirAttributes=setInnerDirAttributes.bind(null,this);document.addEventListener('dirchanged',this.setInnerDirAttributes);}
this.attached();},detachedCallback:function(){if(this.dirObserver){document.removeEventListener('dirchanged',this.setInnerDirAttributes);}
this.detached();},setupShadowRoot:function(){if(!this.template){return;}
var node=document.importNode(this.template.content,true);this.createShadowRoot().appendChild(node);if(this.dirObserver){setInnerDirAttributes(this);}
return this.shadowRoot;},setAttr:function(name,value){var internal=this.shadowRoot.firstElementChild;setAttribute.call(internal,name,value);setAttribute.call(this,name,value);},removeAttr:function(name){var internal=this.shadowRoot.firstElementChild;removeAttribute.call(internal,name);removeAttribute.call(this,name);}},descriptors:{textContent:{set:function(value){textContent.set.call(this,value);if(this.lightStyle){this.appendChild(this.lightStyle);}},get:function(){return textContent.get();}},innerHTML:{set:function(value){innerHTML.set.call(this,value);if(this.lightStyle){this.appendChild(this.lightStyle);}},get:innerHTML.get}}};var defaultPrototype=createProto(HTMLElement.prototype,base.properties);function getBaseProto(proto){if(!proto){return defaultPrototype;}
proto=proto.prototype||proto;return!proto.GaiaComponent?createProto(proto,base.properties):proto;}
function createProto(proto,props){return mixin(Object.create(proto),props);}
var hasShadowCSS=(function(){var div=document.createElement('div');try{div.querySelector(':host');div.querySelector('::content');return true;}
catch(e){return false;}})();var regex={shadowCss:/(?:\:host|\:\:content)[^{]*\{[^}]*\}/g,':host':/(?:\:host)/g,':host()':/\:host\((.+)\)(?: \:\:content)?/g,':host-context':/\:host-context\((.+)\)([^{,]+)?/g,'::content':/(?:\:\:content)/g};function processCss(template,name){var globalCss='';var lightCss='';if(!hasShadowCSS){template=template.replace(regex.shadowCss,function(match){var hostContext=regex[':host-context'].exec(match);if(hostContext){globalCss+=match.replace(regex['::content'],'').replace(regex[':host-context'],'$1 '+name+'$2').replace(/ +/g,' ');}else{lightCss+=match.replace(regex[':host()'],name+'$1').replace(regex[':host'],name).replace(regex['::content'],name);}
return'';});}
return{template:template,lightCss:lightCss,globalCss:globalCss};}
function injectGlobalCss(css){if(!css){return;}
var style=document.createElement('style');style.innerHTML=css.trim();headReady().then(function(){document.head.appendChild(style);});}
function headReady(){return new Promise(function(resolve){if(document.head){return resolve();}
window.addEventListener('load',function fn(){window.removeEventListener('load',fn);resolve();});});}
function injectLightCss(el){if(hasShadowCSS){return;}
var stylesheet=el.querySelector('style');if(!stylesheet){stylesheet=document.createElement('style');stylesheet.setAttribute('scoped','');stylesheet.appendChild(document.createTextNode(el.lightCss));el.appendChild(stylesheet);}
el.lightStyle=stylesheet;}
function toCamelCase(string){return string.replace(/-(.)/g,function replacer(string,p1){return p1.toUpperCase();});}
var dirObserver;function setInnerDirAttributes(component){var dir=component.dir||document.dir;Array.from(component.shadowRoot.children).forEach(element=>{if(element.nodeName!=='STYLE'){element.dir=dir;}});}
function addDirObserver(){if(dirObserver){return;}
dirObserver=new MutationObserver(onChanged);dirObserver.observe(document.documentElement,{attributeFilter:['dir'],attributes:true});function onChanged(mutations){document.dispatchEvent(new Event('dirchanged'));}}
function mixin(target,source){for(var key in source){target[key]=source[key];}
return target;}});})(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-component',this));},{}],3:[function(require,module,exports){(function(define){'use strict';define(function(require,exports,module){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));},{}],4:[function(require,module,exports){;(function(define){'use strict';define(function(require,exports,module){var component=require('gaia-component');var fontFit=require('font-fit');require('gaia-icons');var debug=0?console.log.bind(console):function(){};const KNOWN_ACTIONS={menu:'menu',back:'back',close:'close'};const TITLE_FONT='italic 300 24px FiraSans';const TITLE_PADDING=10;const MINIMUM_FONT_SIZE_CENTERED=20;const MINIMUM_FONT_SIZE_UNCENTERED=16;const MAXIMUM_FONT_SIZE=23;module.exports=component.register('gaia-header',{extensible:false,dirObserver:true,created:function(){debug('created');this.setupShadowRoot();this.els={actionButton:this.shadowRoot.querySelector('.action-button'),titles:this.getElementsByTagName('h1')};this.els.actionButton.addEventListener('click',e=>this.onActionButtonClick(e));this.observer=new MutationObserver(this.onMutation.bind(this));this.ignoreDir=this.hasAttribute('ignore-dir');this.titleEnd=this.getAttribute('title-end');this.titleStart=this.getAttribute('title-start');this.noFontFit=this.getAttribute('no-font-fit');this.notFlush=this.hasAttribute('not-flush');this.action=this.getAttribute('action');this.unresolved={};this.pending={};this._resizeThrottlingId=null;this.onResize=this.onResize.bind(this);},attached:function(){debug('attached');this.runFontFitSoon();this.observerStart();window.addEventListener('resize',this.onResize);},detached:function(){debug('detached');window.removeEventListener('resize',this.onResize);this.observerStop();this.clearPending();},clearPending:function(){for(var key in this.pending){this.pending[key].clear();delete this.pending[key];}
window.cancelAnimationFrame(this._resizeThrottlingId);this._resizeThrottlingId=null;},runFontFit:function(){debug('run font-fit');if(this.noFontFit){return Promise.resolve();}
var titles=this.els.titles;var space=this.getTitleSpace();var styles=[].map.call(titles,el=>this.getTitleStyle(el,space));return this.setTitleStylesSoon(styles);},runFontFitSoon:function(){debug('run font-fit soon');if(this.pending.runFontFitSoon){return;}
this.pending.runFontFitSoon=this.nextTick(()=>{delete this.pending.runFontFitSoon;this.runFontFit();});},getTitleStyle:function(el,space){debug('get el style',el,space);var text=el.textContent;var styleId=space.start+text+space.end+'#'+space.value;if(!text||!text.trim()){return debug('exit: no text');}
if(getStyleId(el)===styleId){return debug('exit: no change');}
var marginStart=this.getTitleMarginStart();var textSpace=space.value-Math.abs(marginStart);var fontFitResult=this.fontFit(text,textSpace,{min:MINIMUM_FONT_SIZE_CENTERED});var overflowing=fontFitResult.overflowing;var padding={start:0,end:0};if(overflowing){debug('title overflowing');padding.start=!space.start?TITLE_PADDING:0;padding.end=!space.end?TITLE_PADDING:0;textSpace=space.value-padding.start-padding.end;fontFitResult=this.fontFit(text,textSpace);marginStart=0;}
return{id:styleId,fontSize:fontFitResult.fontSize,marginStart:marginStart,overflowing:overflowing,padding:padding};},setTitleStylesSoon:function(styles){debug('set title styles soon',styles);var key='setStyleTitlesSoon';this._titleStyles=styles;if(this.unresolved[key]){return this.unresolved[key];}
this.unresolved[key]=new Promise((resolve)=>{this.pending[key]=this.nextTick(()=>{var styles=this._titleStyles;var els=this.els.titles;[].forEach.call(els,(el,i)=>{if(!styles[i]){return debug('exit');}
this.setTitleStyle(el,styles[i]);});delete this._titleStyles;delete this.unresolved[key];delete this.pending[key];resolve();});});},setTitleStyle:function(el,style){debug('set title style',style);this.observerStop();if(this.ignoreDir){el.style.marginLeft=style.marginStart+'px';el.style.paddingLeft=style.padding.start+'px';el.style.paddingRight=style.padding.end+'px';}else{el.style.marginInlineStart=style.marginStart+'px';el.style.paddingInlineStart=style.padding.start+'px';el.style.paddingInlineEnd=style.padding.end+'px';}
el.style.fontSize=style.fontSize+'px';setStyleId(el,style.id);this.observerStart();},fontFit:function(text,space,opts={}){debug('font fit:',text,space,opts);var fontFitArgs={font:TITLE_FONT,min:opts.min||MINIMUM_FONT_SIZE_UNCENTERED,max:MAXIMUM_FONT_SIZE,text:text,space:space};return fontFit(fontFitArgs);},observerStart:function(){if(this.observing){return;}
this.observer.observe(this,{childList:true,attributes:true,subtree:true});this.observing=true;debug('observer started');},observerStop:function(){if(!this.observing){return;}
this.observer.disconnect();this.observing=false;debug('observer stopped');},onResize:function(e){debug('onResize',this._resizeThrottlingId);if(this._resizeThrottlingId!==null){return;}
this._resizeThrottlingId=window.requestAnimationFrame(()=>{this._resizeThrottlingId=null;this.runFontFitSoon();});},onMutation:function(mutations){debug('on mutation',mutations);if(!this.pending.runFontFitSoon){this.runFontFit();}},getTitleSpace:function(){var start=this.titleStart;var end=this.titleEnd;var space=this.getWidth()-start-end;var result={value:space,start:start,end:end};debug('get title space',result);return result;},getWidth:function(){var value=this.notFlush?this.clientWidth:window.innerWidth;debug('get width',value);return value;},triggerAction:function(){if(this.action){this.els.actionButton.click();}},onActionButtonClick:function(){debug('action button click');var config={detail:{type:this.action}};var e=new CustomEvent('action',config);setTimeout(()=>this.dispatchEvent(e));},getTitleMarginStart:function(){var start=this.titleStart;var end=this.titleEnd;var marginStart=end-start;debug('get title margin start',marginStart);return marginStart;},getButtonsBeforeTitle:function(){var children=this.children;var l=children.length;var els=[];for(var i=0;i<l;i++){var el=children[i];if(el.tagName==='H1'){break;}
if(!contributesToLayout(el)){continue;}
els.push(el);}
if(this.action){els.push(this.els.actionButton);}
return els;},getButtonsAfterTitle:function(){var children=this.children;var els=[];for(var i=children.length-1;i>=0;i--){var el=children[i];if(el.tagName==='H1'){break;}
if(!contributesToLayout(el)){continue;}
els.push(el);}
return els;},sumButtonWidths:function(buttons){var defaultWidth=50;var sum=buttons.reduce((prev,button)=>{var isStandardButton=button===this.els.actionButton;var width=isStandardButton?defaultWidth:button.clientWidth;return prev+width;},0);debug('sum button widths',buttons,sum);return sum;},attrs:{action:{get:function(){return this._action;},set:function(value){var action=KNOWN_ACTIONS[value];if(action===this._action){return;}
this.setAttr('action',action);this._action=action;}},titleStart:{get:function(){debug('get title-start');if('_titleStart'in this){return this._titleStart;}
var buttons=this.getButtonsBeforeTitle();var value=this.sumButtonWidths(buttons);debug('get title-start',buttons,value);return value;},set:function(value){debug('set title-start',value);value=parseInt(value,10);if(value===this._titleStart||isNaN(value)){return;}
this.setAttr('title-start',value);this._titleStart=value;debug('set');}},titleEnd:{get:function(){debug('get title-end');if('_titleEnd'in this){return this._titleEnd;}
var buttons=this.getButtonsAfterTitle();return this.sumButtonWidths(buttons);},set:function(value){debug('set title-end',value);value=parseInt(value,10);if(value===this._titleEnd||isNaN(value)){return;}
this.setAttr('title-end',value);this._titleEnd=value;}},noFontFit:{get:function(){return this._noFontFit||false;},set:function(value){debug('set no-font-fit',value);value=!!(value||value==='');if(value===this.noFontFit){return;}
this._noFontFit=value;if(value){this.setAttr('no-font-fit','');}
else{this.removeAttr('no-font-fit');}}},ignoreDir:{get:function(){return this._ignoreDir||false;},set:function(value){debug('set ignore-dir',value);value=!!(value||value==='');if(value===this.ignoreDir){return;}
this._ignoreDir=value;this.dirObserver=!value;if(value){this.setAttr('ignore-dir','');}
else{this.removeAttr('ignore-dir');}}}},template:`<div class="inner">
<button class="action-button">
<content select="[l10n-action]"></content>
</button>
<content></content>
</div>
<style>
:host {
display: block;
-moz-user-select: none;
--gaia-header-button-color:
var(--header-button-color,
var(--header-color,
var(--link-color,
inherit)));
}
/**
* [hidden]
*/
:host[hidden] {
display: none;
}
/** Reset
---------------------------------------------------------*/
::-moz-focus-inner { border: 0; }
/** Inner
---------------------------------------------------------*/
.inner {
display: flex;
min-height: 50px;
-moz-user-select: none;
background:
var(--header-background,
var(--background,
#fff));
}
/** Action Button
---------------------------------------------------------*/
/**
* 1. Hidden by default
*/
.action-button {
position: relative;
display: none; /* 1 */
width: 50px;
font-size: 30px;
margin: 0;
padding: 0;
border: 0;
outline: 0;
align-items: center;
background: none;
cursor: pointer;
transition: opacity 200ms 280ms;
color:
var(--header-action-button-color,
var(--header-icon-color,
var(--gaia-header-button-color)));
}
/**
* [action=back]
* [action=menu]
* [action=close]
*
* 1. For icon vertical-alignment
*/
[action=back] .action-button,
[action=menu] .action-button,
[action=close] .action-button {
display: flex; /* 1 */
}
/**
* :active
*/
.action-button:active {
transition: none;
opacity: 0.2;
}
/** Action Button Icon
---------------------------------------------------------*/
.action-button:before {
font-family: 'gaia-icons';
font-style: normal;
text-rendering: optimizeLegibility;
font-weight: 500;
}
[action=close] .action-button:before { content: 'close' }
[action=menu] .action-button:before { content: 'menu' }
[action=back]:-moz-dir(ltr) .action-button:before { content: 'left' }
[action=back]:-moz-dir(rtl) .action-button:before { content: 'right' }
/** Action Button Icon
---------------------------------------------------------*/
/**
* 1. To enable vertical alignment.
*/
.action-button:before {
display: block;
}
/** Action Button Text
---------------------------------------------------------*/
/**
* To provide custom localized content for
* the action-button, we allow the user
* to provide an element with the class
* .l10n-action. This node is then
* pulled inside the real action-button.
*
* Example:
*
* <gaia-header action="back">
* <span l10n-action aria-label="Back">Localized text</span>
* <h1>title</h1>
* </gaia-header>
*/
::content [l10n-action] {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
font-size: 0;
}
/** Title
---------------------------------------------------------*/
/**
* 1. Vertically center text. We can't use flexbox
* here as it breaks text-overflow ellipsis
* without an inner div.
*/
::content h1 {
flex: 1;
margin: 0;
padding: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
line-height: 50px; /* 1 */
font-weight: 300;
font-style: italic;
font-size: 24px;
color:
var(--header-title-color,
var(--header-color,
var(--title-color,
var(--text-color,
inherit))));
}
/**
* [ignore-dir]
*
* When the <gaia-header> component has an [ignore-dir] attribute, header
* direction is forced to LTR but we still want the <h1> text to be reversed
* so that strings like '1 selected' become 'selected 1'.
*
* When we're happy for <gaia-header> to be fully RTL responsive we won't need
* these rules anymore, but this depends on all Gaia apps being ready.
*
* This should be safe to remove when bug 1179459 lands.
*/
:host[ignore-dir] {
direction: ltr;
}
:host[ignore-dir]:-moz-dir(rtl) h1 {
direction: rtl;
}
/** Buttons
---------------------------------------------------------*/
::content a,
::content button {
position: relative;
z-index: 1;
box-sizing: border-box;
display: flex;
width: auto;
height: auto;
min-width: 50px;
margin: 0;
padding: 0 10px;
outline: 0;
border: 0;
font-size: 14px;
line-height: 1;
align-items: center;
justify-content: center;
text-decoration: none;
text-align: center;
background: none;
border-radius: 0;
font-style: italic;
cursor: pointer;
transition: opacity 200ms 280ms;
color: var(--gaia-header-button-color);
}
/**
* :active
*/
::content a:active,
::content button:active {
transition: none;
opacity: 0.2;
}
/**
* [hidden]
*/
::content a[hidden],
::content button[hidden] {
display: none;
}
/**
* [disabled]
*/
::content a[disabled],
::content button[disabled] {
pointer-events: none;
color: var(--header-disabled-button-color);
}
/** Icon Buttons
---------------------------------------------------------*/
/**
* Icons are a different color to text
*/
::content .icon,
::content [data-icon] {
color:
var(--header-icon-color,
var(--gaia-header-button-color));
}
/**
* If users want their action button
* to be in the component's light-dom
* they can add an .action class
* to make it look like the
* shadow action button.
*/
::content .action {
color:
var(--header-action-button-color,
var(--header-icon-color,
var(--gaia-header-button-color)));
}
/**
* [data-icon]:empty
*
* Icon buttons with no textContent,
* should always be 50px.
*
* This is to prevent buttons being
* larger than they should be before
* icon-font has loaded.
*/
::content [data-icon]:empty {
width: 50px;
}
</style>`,nextTick:nextTick});function contributesToLayout(el){return el.localName!=='style'&&!el.hasAttribute('l10n-action');}
function setStyleId(el,id){el._styleId=id;}
function getStyleId(el){return el._styleId;}
function nextTick(fn){var cleared;Promise.resolve().then(()=>{if(!cleared){fn();}});return{clear:function(){cleared=true;}};}});})(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-header',this));},{"font-fit":1,"gaia-component":2,"gaia-icons":3}]},{},[4])(4)});

View file

@ -0,0 +1,40 @@
/* 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 @@
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#5F5F5F" d="M42 0c23.3 0 42 18.7 42 42S65.3 84 42 84 0 65.3 0 42 18.7 0 42 0z"/><path fill="#FFF" d="M60.7 40.1c-.9-9.3-9.3-16.8-18.7-16.8-10.3 0-18.7 8.4-18.7 18.7S31.7 60.7 42 60.7c3.7 0 7.5-.9 10.3-2.8l-2.8-3.7C46.7 56.1 44.8 57 42 57c-8.4 0-14.9-6.5-14.9-14.9s6.5-15 14.9-15c7.5 0 13.1 5.6 14 13.1h-6.5l7.5 7.5c.9.9 2.8.9 3.7 0l7.5-7.5h-7.5z"/></svg>

After

Width:  |  Height:  |  Size: 443 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#E51E1E" d="M42 0c23.3 0 42 18.7 42 42S65.3 84 42 84 0 65.3 0 42 18.7 0 42 0z"/><path fill="#FFF" d="M60.7 40.1c-.9-9.3-9.3-16.8-18.7-16.8-10.3 0-18.7 8.4-18.7 18.7S31.7 60.7 42 60.7c3.7 0 7.5-.9 10.3-2.8l-2.8-3.7C46.7 56.1 44.8 57 42 57c-8.4 0-14.9-6.5-14.9-14.9s6.5-15 14.9-15c7.5 0 13.1 5.6 14 13.1h-6.5l7.5 7.5c.9.9 2.8.9 3.7 0l7.5-7.5h-7.5z"/></svg>

After

Width:  |  Height:  |  Size: 443 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="24.134" y1="31.414" x2="81.176" y2="46.699"><stop offset="0" stop-color="#00B4CF"/><stop offset=".979" stop-color="#00B4CF" stop-opacity="0"/></linearGradient><path fill-rule="evenodd" clip-rule="evenodd" fill="url(#a)" d="M83.9 39.5v2.6h-3.3c0 21.3-17.3 38.6-38.7 38.6C20.6 80.7 3.3 63.4 3.3 42 3.3 20.6 20.6 3.3 42 3.3V0C18.8 0 0 18.8 0 42s18.8 42 42 42 42-18.8 42-42c0-.8 0-1.7-.1-2.5z"/></svg>

After

Width:  |  Height:  |  Size: 518 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#E51E1E" d="M42 0c23.3 0 42 18.8 42 42 0 23.3-18.8 42-42 42-23.1.1-42-18.7-42-42C0 18.8 18.9 0 42 0z"/><path fill="#FFF" d="M42 25.4c-.9 0-2.5 2.5-3.4 5.5 1.1-.2 2.3-.3 3.5-.3s2.3.1 3.3.3c-.9-3-2.5-5.5-3.4-5.5M27.1 39.1l5.7-5.7c1.7-8.3 6.7-16.7 9.2-16.7s7.5 8.4 9.2 16.7l5.7 5.7V54h-4l-5.6-5.6c-.6.6-1.2 1.1-1.9 1.5h-7c-.7-.4-1.3-.9-1.9-1.5L31.1 54h-4V39.1M16.6 59.2c.4.9 1.3 1.5 2.4 1.5h46c1 0 1.9-.6 2.4-1.5H16.6z"/></svg>

After

Width:  |  Height:  |  Size: 513 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#333" d="M42 0c23.3 0 42 18.8 42 42 0 23.3-18.8 42-42 42-23.1.1-42-18.7-42-42C0 18.8 18.9 0 42 0z"/><path fill="#FFF" d="M29.9 50c.4.9 1.3 1.5 2.4 1.5h19.3c1 0 1.9-.6 2.4-1.5H29.9zm22.3-17.8L51 33.4v.1L42.4 42c-.3.3-.7.3-1 0l-8.6-8.6v-.1l-1.2-1.2c-1.6.3-3.1.6-4.6.9.2.5.4 1 .9 1.5l11.3 11.3c1.5 1.5 3.8 1.5 5.3 0l11.3-11.3c.4-.4.7-.9.9-1.5-1.4-.3-2.9-.6-4.5-.8z"/></svg>

After

Width:  |  Height:  |  Size: 459 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="42" x2="42" y2="84"><stop offset="0" stop-color="#E5EBEF"/><stop offset="1" stop-color="#EDF1F4"/></linearGradient><path fill="url(#a)" d="M42 0c23.3 0 42 18.8 42 42 0 23.3-18.8 42-42 42-23.1.1-42-18.7-42-42C0 18.8 18.9 0 42 0z"/><path fill="#9AB0C0" d="M41.5 25.9c-.9 0-2.5 2.6-3.5 5.7 1.1-.2 2.3-.3 3.6-.3 1.2 0 2.4.1 3.4.3-1-3.1-2.6-5.7-3.5-5.7M43 53.3h2.2c.3.9.4 2 .4 3.1 0 4-3.1 7.2-4.1 7.2s-4.1-3.2-4.1-7.2c0-1.1.1-2.2.4-3.1H40c-.2.5-.3 1.2-.3 1.9 0 1.8 1.4 3.3 1.9 3.3s1.9-1.5 1.9-3.3c-.2-.7-.3-1.4-.5-1.9M26.1 40l5.9-5.9c1.7-8.6 6.9-17.2 9.5-17.2s7.8 8.7 9.5 17.2l5.9 5.9v15.4h-4.1l-5.7-5.7c-.6.6-1.2 1.1-1.9 1.5h-7.3c-.7-.4-1.3-.9-1.9-1.5l-5.7 5.7h-4.1L26.1 40"/></svg>

After

Width:  |  Height:  |  Size: 799 B

View file

@ -0,0 +1,605 @@
'use strict';
window.GaiaAppIcon = (function(exports) {
const LAUNCH_TIMEOUT = 500;
const PREDEFINED_ICONS = {
default: 'default_icon.svg',
unrecoverable: 'app_install_unrecoverable.svg',
installing: 'app_installing.svg',
paused: 'app_install_canceled.svg',
failed: 'app_install_failed.svg'
};
const SHADOW_BLUR = 1;
const SHADOW_OFFSET = { x: 1, y: 1 };
const SHADOW_COLOR = 'rgba(0, 0, 0, 0.2)';
const DEFAULT_BACKGROUND_COLOR = 'rgb(228, 234, 238)';
const CANVAS_PADDING = 2 * window.devicePixelRatio;
const FAVICON_SCALE = 0.55;
var proto = Object.create(HTMLElement.prototype);
// Allow the base URL to be overridden
var base = window.GAIA_ICONS_BASE_URL ||
window.COMPONENTS_BASE_URL ||
'bower_components/';
var baseurl = window.GaiaAppIconBaseurl || base + 'gaia-site-icon/';
proto.createdCallback = function() {
this._template = template.content.cloneNode(true);
var shadow = this.createShadowRoot();
shadow.appendChild(this._template);
this._container = shadow.getElementById('image-container');
this._titleContainer = shadow.getElementById('title-container');
this._subtitle = shadow.getElementById('subtitle');
this._showName = true;
this._image = null;
this._app = null;
this._bookmark = null;
this._entryPoint = '';
this._size = -1;
this._hasIcon = false;
this._hasUserSetIcon = false;
this._hasPredefinedIcon = false;
this._iconUrl = null;
this._pendingIconUrl = null;
this._pendingIconRefresh = false;
this._lastState = null;
this._predefinedIcons = {};
for (var key in PREDEFINED_ICONS) {
this._predefinedIcons[key] = baseurl + 'images/' + PREDEFINED_ICONS[key];
}
// Make the icon accessible/allow activation
this.tabIndex = 0;
this.setAttribute('role', 'link');
var activate = () => {
if (!this.dispatchEvent(new CustomEvent('activated',
{ cancelable: true }))) {
return;
}
this.launch();
};
this.addEventListener('keydown', (e) => {
switch (e.keyCode) {
case 32: // Space
case 13: // Enter
activate();
}
});
this.addEventListener('click', activate);
// Refresh now we've created the DOM
this.refresh();
};
Object.defineProperty(proto, 'app', {
get: function() {
return this._app;
},
set: function(app) {
if (this._app) {
this._app.removeEventListener('progress', this);
this._app.removeEventListener('downloaderror', this);
this._app.removeEventListener('downloadsuccess', this);
this._app.removeEventListener('downloadapplied', this);
}
delete this.dataset.testIconUrl;
this._cancelIconLoad();
this._removeOldIcon();
this._hasIcon = false;
this._app = app;
if (!app) {
return;
}
this._bookmark = null;
this._app.addEventListener('progress', this);
this._app.addEventListener('downloaderror', this);
this._app.addEventListener('downloadsuccess', this);
this._app.addEventListener('downloadapplied', this);
this._container.classList.add('initial-load');
},
enumerable: true
});
Object.defineProperty(proto, 'entryPoint', {
get: function() {
return this._entryPoint;
},
set: function(entryPoint) {
this._entryPoint = entryPoint ? entryPoint : '';
this.refresh();
},
enumerable: true
});
Object.defineProperty(proto, 'state', {
get: function() {
if (!this.app) {
return 'unavailable';
}
if (this.app.installState === 'pending' &&
!this.app.downloadAvailable &&
!this.app.readyToApplyDownload) {
return 'unrecoverable';
}
if (this.app.downloading) {
return 'installing';
}
if (this.app.downloadError) {
if (this.app.downloadError.name === 'DOWNLOAD_CANCELED') {
return 'paused';
} else {
return 'error';
}
}
if (this.app.installState === 'pending') {
return 'paused';
}
return 'installed';
},
enumberable: true
});
Object.defineProperty(proto, 'name', {
get: function() {
return this._subtitle ? this._subtitle.textContent : '';
},
enumberable: true
});
Object.defineProperty(proto, 'size', {
get: function() {
return (this._size === -1) ? this.clientWidth : this._size;
},
set: function(size) {
this._size = size >= 0 ? size : -1;
},
enumerable: true
});
Object.defineProperty(proto, 'bookmark', {
get: function() {
return this._bookmark;
},
set: function(bookmark) {
if (!this._bookmark || this._bookmark.id !== bookmark.id) {
this.app = null;
}
this._bookmark = bookmark;
},
enumerable: true
});
Object.defineProperty(proto, 'icon', {
get: function() {
if (window.VulpesCompat && this.app && this._vulpesSourceIcon) return Promise.resolve(this._vulpesSourceIcon);
return new Promise((resolve, reject) => {
if (!this._hasIcon || this._hasPredefinedIcon) {
return reject();
}
var image = this._container.querySelector('img');
if (!image) {
return reject();
}
var canvas = document.createElement('canvas');
var size = this.size * window.devicePixelRatio;
canvas.width = canvas.height = size;
var ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.drawImage(image, 0, 0, size, size);
try {
canvas.toBlob((blob) => {
resolve(blob);
});
} catch(e) {
console.error('Failed to get canvas data for icon');
reject(e);
}
});
},
set: function(blob) {
if (blob) {
this._prepareIconLoader();
this._hasUserSetIcon = true;
this._pendingIconUrl = 'user-set';
this._image.src = URL.createObjectURL(blob);
} else {
this._pendingIconUrl = null;
delete this.dataset.testIconUrl;
this._cancelIconLoad();
this._removeOldIcon();
}
},
enumerable: true
});
Object.defineProperty(proto, 'showName', {
get: function() {
return this._showName;
},
set: function(show) {
show = show ? true : false;
if (this._showName !== show) {
this._showName = show;
this._titleContainer.classList.toggle('hidden', !show);
}
},
enumerable: true
});
Object.defineProperty(proto, 'isUserSet', {
get: function() {
return this._iconUrl === 'user-set';
},
enumerable: true
});
proto._cancelIconLoad = function() {
if (!this._image) {
return;
}
this._pendingIconUrl = null;
this._image.onload = null;
this._image.src = '';
this._image = null;
};
proto._removeOldIcon = function() {
while (this._container.children.length > 1) {
this._container.removeChild(this._container.lastChild);
}
};
proto.launch = function() {
if (!this.app && !this.bookmark) {
return;
}
this.classList.add('launching');
setTimeout(() => { this.classList.remove('launching'); }, LAUNCH_TIMEOUT);
if (this.app) {
window.performance.mark('appLaunch@' + this.app.origin);
this.app.launch(this.entryPoint);
} else {
var features = {
name: this.bookmark.name,
remote: true
};
if (this.bookmark.scope) {
features.scope = this.bookmark.scope;
}
window.open(this.bookmark.url, '_samescope', Object.keys(features)
.map(function eachFeature(key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(features[key]);
}).join(','));
}
};
proto._prepareIconLoader = function() {
// Create new image element to load icon
this._cancelIconLoad();
this._hasPredefinedIcon = false;
this._hasUserSetIcon = false;
this._image = document.createElement('img');
this._image.setAttribute('role', 'presentation');
this._image.onload = () => {
this._image.onload = () => {
// Add new icon
this._removeOldIcon();
this._container.appendChild(this._image);
this._container.classList.remove('initial-load');
this._image = null;
this._hasIcon = true;
// Set icon URL on dataset, for testing.
if (this._pendingIconUrl) {
this._iconUrl = this.dataset.testIconUrl = this._pendingIconUrl;
this._pendingIconUrl = null;
}
if (!this._hasPredefinedIcon) {
this.dispatchEvent(new CustomEvent('icon-loaded'));
}
if (this._pendingIconRefresh) {
this._image = null;
this._pendingIconRefresh = false;
this.refresh();
}
};
if (!this._hasUserSetIcon) {
// Process the image
var canvas = document.createElement('canvas');
var size = this.size * window.devicePixelRatio;
canvas.width = canvas.height = size;
var ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.shadowColor = SHADOW_COLOR;
ctx.shadowBlur = SHADOW_BLUR;
ctx.shadowOffsetX = SHADOW_OFFSET.x;
ctx.shadowOffsetY = SHADOW_OFFSET.y;
if (this.bookmark && !this._hasPredefinedIcon) {
if (this._image.width <= size / 2) {
// Draw filled background circle
ctx.beginPath();
ctx.arc(size / 2, size / 2,
size / 2 - CANVAS_PADDING, 0, 2 * Math.PI);
ctx.fillStyle = DEFAULT_BACKGROUND_COLOR;
ctx.fill();
// Disable shadow drawing and image smoothing
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
ctx.mozImageSmoothingEnabled = false;
var iconSize = size * FAVICON_SCALE;
ctx.drawImage(this._image,
(size - iconSize) / 2,
(size - iconSize) / 2,
iconSize, iconSize);
} else {
// Clip the favicon in a circle. We do this in a new canvas so
// the shadow-rendering on the main canvas works correctly.
var clipCanvas = document.createElement('canvas');
clipCanvas.width = clipCanvas.height = size;
var clipCtx =
clipCanvas.getContext('2d', { willReadFrequently: true });
clipCtx.beginPath();
clipCtx.arc(size / 2, size / 2,
size / 2 - CANVAS_PADDING, 0, 2 * Math.PI);
clipCtx.clip();
clipCtx.drawImage(this._image,
CANVAS_PADDING, CANVAS_PADDING,
size - CANVAS_PADDING * 2,
size - CANVAS_PADDING * 2);
ctx.drawImage(clipCanvas, 0, 0);
}
} else {
// Just draw the icon straight (without padding if it was set
// explicitly rather than loaded via manifest/bookmark)
ctx.drawImage(this._image,
CANVAS_PADDING, CANVAS_PADDING,
size - CANVAS_PADDING * 2,
size - CANVAS_PADDING * 2);
}
canvas.toBlob(function(image, blob) {
if (image.onload) {
image.src = URL.createObjectURL(blob);
}
}.bind(this, this._image));
}
if (this._hasUserSetIcon) {
this._image.onload();
}
};
this._image.onerror = (e) => {
console.error('Failed to load icon', e);
this._pendingIconUrl = null;
if (this._pendingIconRefresh) {
this._pendingIconRefresh = false;
this._image = null;
this.refresh();
} else if (!this._hasIcon) {
this._image.onerror = null;
this._setPredefinedIcon('default');
} else {
this._image = null;
}
};
};
proto._setPredefinedIcon = function(name) {
this._hasPredefinedIcon = true;
this._image.src = this._pendingIconUrl = this._predefinedIcons[name];
};
proto._localizeString = function(str) {
var userLang = document.documentElement.lang;
// We want to make sure that we tranlsate only if we're using
// a runtime pseudolocale.
// mozL10n.ctx.qps contains only runtime pseudolocales
if (navigator.mozL10n &&
navigator.mozL10n.ctx.qps.indexOf(userLang) !== -1) {
return navigator.mozL10n.qps[userLang].translate(str);
}
return str;
};
proto.updateName = function() {
if (this.app) {
var userLang = document.documentElement.lang;
var ep = this.entryPoint || undefined;
this.app.getLocalizedValue('short_name', userLang, ep).then(
(shortName) => {
this._subtitle.textContent = this._localizeString(shortName);
},
() => {
this.app.getLocalizedValue('name', userLang, ep).then(
(name) => {
this._subtitle.textContent = this._localizeString(name);
},
(e) => {
// Try to fall back to manifest app name
var manifest = this.app.manifest || this.app.updateManifest;
if (manifest) {
var nameObject = (manifest.entry_points && this.entryPoint) ?
manifest.entry_points[this.entryPoint] : manifest;
this._subtitle.textContent =
this._localizeString(nameObject.short_name || nameObject.name);
} else {
console.error('Error retrieving app name', e);
this._subtitle.textContent = '';
}
});
});
} else if (this.bookmark) {
this._subtitle.textContent = this.bookmark.name;
} else {
this._subtitle.textContent = '';
}
};
proto.refresh = function() {
if (!this._template) {
return;
}
this.updateName();
this._container.classList.remove('downloading');
// Set an identifier on the icon to help with automated testing.
if (this.app) {
this.dataset.identifier = this.app.manifestURL +
(this.entryPoint ? '/' + this.entryPoint : '');
} else if (this.bookmark) {
this.dataset.identifier = this.bookmark.id;
} else {
delete this.dataset.identifier;
}
// If we're already loading an icon, let that finish first.
if (this._image) {
this._pendingIconRefresh = true;
return;
}
// If the icon won't be visible, don't bother fetching and processing it.
var size = this.size * window.devicePixelRatio;
if (size < 1) {
return;
}
this._prepareIconLoader();
// Handle icon loading for bookmarks, app icon loading is more involved
// and handled below this block.
if (this.bookmark) {
// Fallback to the default icon
if (!this._hasIcon) {
this._setPredefinedIcon('default');
}
return;
}
// If the app is downloading/paused/failed to download, use a predefined
// icon.
var state = this._lastState = this.state;
switch (state) {
case 'unrecoverable':
this._setPredefinedIcon('unrecoverable');
break;
case 'installing':
this._setPredefinedIcon('installing');
this._container.classList.add('downloading');
break;
case 'paused':
this._setPredefinedIcon('paused');
break;
case 'error':
this._setPredefinedIcon('failed');
break;
case 'installed':
var handleError = function(image, e) {
console.error('Failed to retrieve icon', e);
if (image.onload && !this._hasIcon) {
this._setPredefinedIcon('default');
} else {
this._image = null;
}
};
var getImage = () => {
navigator.mozApps.mgmt.getIcon(this.app, size, this.entryPoint).
then(function(image, blob) {
this._pendingIconUrl = 'app-icon';
if (window.VulpesCompat) { this._vulpesSourceIcon = blob; this._hasUserSetIcon = true; } // Keep original app pixels; skip the legacy canvas pass.
if (image.onload) {
image.src = URL.createObjectURL(blob);
}
}.bind(this, this._image),
handleError.bind(this, this._image));
};
getImage();
break;
}
};
proto.handleEvent = function(e) {
switch(e.type) {
case 'progress':
case 'downloadsuccess':
if (this.state !== this._lastState) {
this.refresh();
}
break;
case 'downloaderror':
case 'downloadapplied':
this.refresh();
break;
}
};
var template = document.createElement('template');
var stylesheet = baseurl + 'style.css';
template.innerHTML =
`<style>@import url(${stylesheet});</style>
<div id="image-container"><div id="spinner"></div></div>
<div id="title-container"><div dir="auto" id="subtitle"></div></div>`;
return document.registerElement('gaia-app-icon', { prototype: proto });
})(window);

View file

@ -0,0 +1,54 @@
gaia-app-icon {
display: flex;
flex-direction: column;
font-size: 0;
}
#image-container {
position: relative;
transition: visibility 0.2s, opacity 0.2s;
}
#image-container.initial-load {
opacity: 0;
visibility: hidden;
}
#image-container > #spinner {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#image-container.downloading > #spinner {
display: block;
background: no-repeat center center url('images/app_install_spinner.svg');
background-size: contain;
animation: rotate 2s infinite linear;
}
#image-container,
#image-container > * {
width: 100%;
}
#subtitle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
margin-top: 0.3rem;
}
#title-container {
transition: opacity 0.2s, visibility 0.2s;
}
#title-container.hidden {
opacity: 0;
visibility: hidden;
height: 0;
}

View file

@ -0,0 +1,378 @@
(define=>{define((require,exports,module)=>{
'use strict';
/**
* Dependencies
*/
var component = require('gaia-component');
require('gaia-icons'); // Load gaia-icons
/**
* Mini logger
*
* @type {Function}
*/
var debug = 0 ? (...args) => console.log('[GaiaTextInput]', ...args) : () => {};
/**
* Exports
*/
module.exports = component.register('gaia-text-input', {
created() {
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.inputmode = this.getAttribute('x-inputmode');
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));
this.els.input.addEventListener('input', e => this.onInput(e));
this.els.input.addEventListener('focus', e => this.onFocus(e));
this.els.input.addEventListener('blur', e => this.onBlur(e));
},
/**
* Clear the field.
*
* @public
*/
clear() {
debug('clear');
this.value = '';
this.emit('clear');
},
/**
* Focus the field.
*
* @public
*/
focus() {
debug('focus');
this.els.input.focus();
},
/**
* Unfocus the field.
*
* @public
*/
blur() {
debug('blur');
this.els.input.blur();
},
/**
* Runs when the field is focused.
*
* @private
*/
onFocus() {
debug('on focus');
this.els.inner.classList.add('focused');
this.emit('focus');
},
/**
* Runs when the field is unfocused.
*
* @private
*/
onBlur() {
debug('on blur');
this.els.inner.classList.remove('focused');
this.emit('blur');
},
/**
* Runs when the value of the
* input is manually changed.
*
* @private
*/
onInput() {
debug('on input');
this.onValueChanged();
},
/**
* Runs when the values changes
* programatically or via keystrokes.
*
* @private
*/
onValueChanged() {
debug('value changed');
var hasValue = !!this.value.length;
this.els.inner.classList.toggle('has-value', hasValue);
this.emit('input');
},
/**
* Emit a DOM event on the component.
*
* @param {String} name
* @param {*} detail
* @private
*/
emit(name, detail) {
var e = new CustomEvent(name, { detail: detail });
this.dispatchEvent(e);
},
/**
* 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);
}
},
inputmode: {
get: function() { return this.els.input.getAttribute('x-inputmode'); },
set: function(value) {
if (!value) {
this.els.input.removeAttribute('x-inputmode');
return;
}
this.els.input.setAttribute('x-inputmode', 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.setAttr('clearable', '');
else this.removeAttr('clearable');
this._clearable = clearable;
}
},
value: {
get: function() { return this.els.input.value; },
set: function(value) {
debug('set value', value);
this.els.input.value = value;
this.onValueChanged();
}
},
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);
font-size: 17px;
overflow: hidden;
--this-background:
var(--text-input-background,
var(--input-background,
var(--background-minus,
#fff)));
--this-border-color:
var(--input-border-color,
var(--border-color,
var(--background-plus,
#e7e7e7)));
}
.inner {
height: 100%;
}
label {
font-size: 14px;
display: block;
margin: 0 0 4px 16px;
}
[disabled] label {
opacity: 0.3;
}
.fields {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
border-color:
var(--this-border-color);
border:
var(--input-border,
var(--border,
1px solid var(--this-border-color)));
}
[type='search'] .fields {
border-radius: 30px;
overflow: hidden;
}
input {
display: block;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 0 16px;
margin: 0;
border: 0;
font: inherit;
resize: none;
/* Dynamic */
color: var(--text-color, #000);
background: var(--this-background);
}
input[disabled] {
background: var(--background);
}
::-moz-placeholder {
font-style: italic;
color:
var(--input-placeholder-color, #909ca7);
}
.clear {
position: absolute;
offset-inline-end: 0;
top: 0;
display: none;
width: 18px;
height: 18px;
padding: 0;
margin: 0;
border: solid 10px transparent;
box-sizing: content-box;
border-radius: 50%;
color: var(--background);
cursor: pointer;
background: var(--input-clear-background, #999);
background-clip: padding-box;
opacity: 0;
pointer-events: none;
}
[clearable] .clear {
display: block;
}
[clearable].has-value.focused .clear {
pointer-events: all;
transition: opacity 200ms;
opacity: 1;
}
.clear:before {
content: 'close';
display: block;
font: normal 500 19px/17px 'gaia-icons';
text-rendering: optimizeLegibility;
}
.focus {
position: absolute;
left: 0;
bottom: 0px;
width: 100%;
height: 3px;
transition: transform 200ms;
transform: scaleX(0);
visibility: hidden;
background: var(--highlight-color, #000);
}
[type='search'] .focus {
border-radius: 0 0 60px 60px;
left: 10px;
width: calc(100% - 20px);
}
/**
* .focused
*/
.focused .focus {
transform: scaleX(1);
transition-delay: 200ms;
visibility: visible;
}
</style>`
});
})})(((n,w)=>{return(typeof define)[0]=='f'&&define.amd?define:(typeof module)[0]=='o'?c =>{c(require,exports,module)}:c=>{var m={exports:{}},r=n=>w[n];w[n]=c(r,m.exports,m)||m.exports;};})('gaia-text-input',this));/*jshint ignore:line*/

View file

@ -0,0 +1,170 @@
;(function(define){define(function(require,exports,module){
/*jshint esnext:true*/
'use strict';
/**
* Dependencies
*/
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-toast', {
created: function() {
this.setupShadowRoot();
this.els = {
inner: this.shadowRoot.querySelector('.inner'),
bread: this.shadowRoot.querySelector('.bread')
};
this.timeout = this.getAttribute('timeout');
},
show: function() {
this.els.bread.removeEventListener('animationend', this.onAnimateOutEnd);
clearTimeout(this.hideTimeout);
this.els.inner.classList.add('visible');
var reflow = this.els.inner.offsetTop;
this.els.bread.classList.remove('animate-out');
this.els.bread.classList.add('animate-in');
this.hideTimeout = setTimeout(this.hide.bind(this), this.timeout);
},
hide: function() {
var self = this;
clearTimeout(this.hideTimeout);
this.els.bread.classList.remove('animate-in');
this.els.bread.classList.add('animate-out');
this.els.bread.removeEventListener('animationend', this.onAnimateOutEnd);
this.onAnimateOutEnd = function() {
self.els.bread.removeEventListener('animationend', self.onAnimateOutEnd);
self.els.bread.classList.remove('animate-out');
self.els.inner.classList.remove('visible');
};
this.els.bread.addEventListener('animationend', this.onAnimateOutEnd);
},
animateOut: function() {},
attrs: {
timeout: {
get: function() { return this.getAttribute('timeout') || 1000; },
set: function(value) {
var current = this.getAttribute('timeout');
if (current == value) { return; }
else if (!value) { this.removeAttribute('timeout'); }
else { this.setAttribute('timeout', value); }
}
}
},
template: `
<div class="inner">
<div class="bread">
<content></content>
</div>
</div>
<style>
/** Host
---------------------------------------------------------*/
:host {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
font-style: italic;
text-align: center;
z-index: 100;
color:
var(--highlight-color);
}
/** Inner
---------------------------------------------------------*/
.inner {
display: none;
padding: 16px;
}
.inner.visible {
display: block;
}
/**
---------------------------------------------------------*/
.bread {
max-width: 600px;
margin: 0 auto;
padding: 16px;
box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.15);
transform: translateY(100%);
background:
var(--background-plus,
white);
}
.bread.animate-in {
animation-name: gaia-toast-enter;
animation-fill-mode: forwards;
animation-duration: 300ms;
}
.bread.animate-out {
animation-name: gaia-toast-leave;
animation-duration: 600ms;
transform: translateY(0%);
}
</style>
`,
globalCss: `
@keyframes gaia-toast-enter {
0% {
transform: translateY(100%);
opacity: 0;
}
40% {
opacity: 0;
}
100% {
transform: translateY(0%);
opacity: 1;
}
}
@keyframes gaia-toast-leave {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
`
});
});})(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-toast',this));

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><path fill="#667174" fill-opacity=".7" d="m 18.2995,19.66 -5.056,-5.057 5.056,-5.056 c -0.31,-0.516 -0.62,-1.032 -0.93,-1.34 l -5.672,5.673 v 1.652 l 5.675,5.676 c 0.31,-0.515 0.722,-0.928 0.93,-1.547 z"/></svg>

After

Width:  |  Height:  |  Size: 294 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path fill="#667174" fill-opacity=".7" d="M11.8,19.9l4.9-4.9l-4.9-4.9c0.3-0.5,0.6-1,0.9-1.3l5.5,5.5c0,0.3,0,0.5,0,0.8c0,0.3,0,0.5,0,0.8l-5.5,5.5 C12.4,20.9,12,20.5,11.8,19.9z"/></svg>

After

Width:  |  Height:  |  Size: 243 B

View file

@ -0,0 +1 @@
;'use strict';window.GaiaButtons=(function(win){var proto=Object.create(HTMLElement.prototype);var baseurl=window.GaiaButtonsBaseurl||'/shared/elements/gaia_buttons/';proto.createdCallback=function(){ComponentUtils.style.call(this,baseurl);};return document.registerElement('gaia-buttons',{prototype:proto});})(window);

View file

@ -0,0 +1,281 @@
/* ---------------------------------------
* gaia-buttons:
* Container for buttons e.g:
* action menu, confirm, edit mode, FTU
* --------------------------------------- */
gaia-buttons {
display: flex;
justify-content: center;
align-items: center;
height: 7rem;
box-sizing: border-box;
pointer-events: auto;
}
gaia-buttons:not(.inline) {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
border-top: 0.1rem solid #e7e7e7;
padding: 0;
background-color: #f4f4f4;
}
/* this 'end' padding must match the buttons 'start' margin */
gaia-buttons:not(.inline):-moz-dir(ltr) { padding-right: 1.5rem; }
gaia-buttons:not(.inline):-moz-dir(rtl) { padding-left: 1.5rem; }
gaia-buttons.inline a,
gaia-buttons.inline button {
margin: 0;
}
button::-moz-focus-inner {
border: none;
outline: none;
}
a,
button {
position: relative;
font-family: sans-serif;
font-style: italic;
width: 100%;
height: 4rem;
margin: 0;
padding: 0 0.2rem;
box-sizing: border-box;
display: inline-block;
vertical-align: middle;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
background: #d8d8d8;
border: none;
border-radius: 2rem;
font-weight: normal;
font-size: 1.6rem;
line-height: 4rem;
color: #333;
text-align: center;
text-decoration: none;
outline: none;
}
:-moz-dir(ltr) a,
:-moz-dir(ltr) button {
margin-left: 1.5rem;
}
:-moz-dir(rtl) a,
:-moz-dir(rtl) button {
margin-right: 1.5rem;
}
/* Recommend */
a.recommend,
button.recommend {
background-color: #00caf2;
color: #fff;
}
/* Danger */
a.danger,
button.danger {
background-color: #e51e1e;
color: #fff;
}
/* Pressed */
a:active,
button:active {
background-color: #b2f2ff;
color: #fff;
}
a::-moz-selection,
button::-moz-selection {
background-color: transparent;
}
/* Disabled */
a[aria-disabled="true"],
button[disabled] {
background-color: rgba(0,0,0,0.05);
color: #c7c7c7;
pointer-events: auto;
}
a[aria-disabled="true"].recommend,
button[disabled].recommend {
background-color: #97cbd5;
color: rgba(255,255,255,0.5);
}
a[aria-disabled="true"].danger,
button[disabled].danger {
background-color: #eb7474;
color: rgba(255,255,255,0.5);
}
a[aria-disabled="true"]::-moz-selection,
button[disabled]::-moz-selection {
-moz-user-select: none;
}
/* Hidden */
[hidden] {
visibility: hidden;
}
/* Icons */
a.icon,
button.icon {
position: relative;
}
a.icon:before,
button.icon:before {
content: "";
width: 3rem;
height: 3rem;
position: absolute;
top: 50%;
margin-top: -1.5rem;
background: transparent no-repeat center center / 100% auto;
}
:-moz-dir(ltr) a.icon:before,
:-moz-dir(ltr) button.icon:before { right: 0.5rem; }
:-moz-dir(rtl) a.icon:before,
:-moz-dir(rtl) button.icon:before { left: 0.5rem; }
a.icon-next:before,
button.icon-next:before {
background-size: 3rem;
}
:-moz-dir(ltr) a.icon-next:before,
:-moz-dir(ltr) button.icon-next:before {
background-image: url(images/right-light.svg);
}
:-moz-dir(rtl) a.icon-next:before,
:-moz-dir(rtl) button.icon-next:before {
background-image: url(images/left-light.svg);
}
a.icon-previous:before,
button.icon-previous:before {
background-size: 3rem;
}
:-moz-dir(ltr) a.icon-previous:before,
:-moz-dir(ltr) button.icon-previous:before {
left: 0.5rem;
background-image: url(images/left-light.svg);
}
:-moz-dir(rtl) a.icon-previous:before,
:-moz-dir(rtl) button.icon-previous:before {
right: 0.5rem;
background-image: url(images/right-light.svg);
}
/* -----------------------------------------------------------------
SKIN: dark
----------------------------------------------------------------- */
gaia-buttons[skin="dark"] {
background: #4d4d4d;
border: none;
}
/* Pressed */
[skin="dark"] a:active,
[skin="dark"] button:active {
background-color: #00aacc;
}
/* Disabled */
[skin="dark"] a[aria-disabled="true"],
[skin="dark"] button[disabled] {
background-color: #565656;
color: rgba(255,255,255,0.4);
}
[skin="dark"] a[aria-disabled="true"].recommend,
[skin="dark"] button[disabled].recommend {
background-color: #006579;
}
[skin="dark"] a[aria-disabled="true"].danger,
[skin="dark"] button[disabled].danger {
background-color: #730f0f;
}
/* -----------------------------------------------------------------
Transitions
----------------------------------------------------------------- */
[transition] {
display: flex;
transition: visibility var(--transition-duration);
}
[transition][hidden] {
pointer-events: none;
}
[transition="slide"] {
transform: translateY(0);
transition: transform var(--transition-duration) ease,
visibility var(--transition-duration);
}
[transition="slide"][hidden] {
transform: translateY(100%);
}
[transition="fade"] {
opacity: 1;
transition: opacity var(--transition-duration) ease,
visibility var(--transition-duration);
}
[transition="fade"][hidden] {
opacity: 0;
}
/* -----------------------------------------------------------------
RESPONSIVE: tablet
----------------------------------------------------------------- */
@media (min-width: 768px) {
gaia-buttons {
height: 10rem;
}
gaia-buttons:not(.inline) {
border-top-width: 0.2rem;
}
/* this 'end' padding must match the buttons 'start' margin */
gaia-buttons:not(.inline):-moz-dir(ltr) { padding-right: 3rem; }
gaia-buttons:not(.inline):-moz-dir(rtl) { padding-left: 3rem; }
a,
button {
font-size: 2rem;
height: 5rem;
line-height: 5rem;
border-radius: 2.5rem;
max-width: 40rem;
}
:-moz-dir(ltr) a,
:-moz-dir(ltr) button {
margin-left: 3rem;
}
:-moz-dir(rtl) a,
:-moz-dir(rtl) button {
margin-right: 3rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

View file

@ -0,0 +1,5 @@
;'use strict';window.GaiaCheckbox=(function(win){var proto=Object.create(HTMLElement.prototype);var baseurl=window.GaiaCheckboxBaseurl||'/shared/elements/gaia_checkbox/';proto.createdCallback=function(){this.lastClick=0;this.throttleTime=250;var shadow=this.createShadowRoot();this._template=template.content.cloneNode(true);this._wrapper=this._template.getElementById('checkbox');this._wrapper.addEventListener('click',this.handleClick.bind(this));this.configureClass();this.checked=this.hasAttribute('checked');this._wrapper.setAttribute('aria-checked',this.checked);this.setAttribute('role','presentation');shadow.appendChild(this._template);ComponentUtils.style.call(this,baseurl);};proto.handleClick=function(e){e.preventDefault();e.stopImmediatePropagation();if(this.lastClick+this.throttleTime>Date.now()){return;}
this.lastClick=Date.now();var event=new MouseEvent('click',{view:window,bubbles:true,cancelable:true});this.dispatchEvent(event);if(!event.defaultPrevented){this.checked=!this.checked;this._wrapper.setAttribute('aria-checked',this.checked);}
this.dispatchEvent(new CustomEvent('change',{bubbles:true,cancelable:false}));};proto.configureClass=function(){this._wrapper.className=this.className;};proto.attributeChangedCallback=function(name,from,to){if(name==='class'){this._wrapper.className=to;}};Object.defineProperty(proto,'checked',{get:function(){return this._checked||false;},set:function(value){this._wrapper.classList.toggle('checked',value);this._checked=value;}});Object.defineProperty(proto,'name',{get:function(){return this.getAttribute('name');},set:function(value){this.setAttribute('name',value);}});var template=document.createElement('template');template.innerHTML=`<span id="checkbox" role="checkbox">
<span role="presentation"><content select="label"></content></span>
</span>`;return document.registerElement('gaia-checkbox',{prototype:proto});})(window);

View file

@ -0,0 +1,71 @@
gaia-checkbox {
display: inline-block;
width: 100%;
}
#checkbox {
display: block;
width: 100%;
/* Normal gaia-checkboxes are the heights of their parents. */
height: 100%;
position: relative;
background: none;
}
#checkbox span {
font-size: 1.9rem;
font-weight: normal;
color: var(--label-color);
line-height: 3rem;
box-sizing: border-box;
display: block;
min-width: 2.2rem;
min-height: 2.2rem;
width: 100%;
padding-inline-end: 2.2rem;
/* Override inheriting nowrap from legacy lists building blocks. */
white-space: normal;
}
/* Inline gaia-checkboxes are the heights of their parents. */
#checkbox.inline {
display: inline-block;
vertical-align: middle;
}
#checkbox:not(.invisible) span:after {
content: '';
position: absolute;
offset-inline-end: 0;
top: 50%;
margin: -1.2rem 0 0;
margin-inline-start: -1.2rem;
width: 2.2rem;
height: 2.2rem;
}
#checkbox.inline span {
padding-inline-start: 3rem;
padding-inline-end: 0;
height: auto;
}
#checkbox.inline span:after {
offset-inline-start: 1.2rem;
offset-inline-end: unset;
margin: -1.2rem 0 0;
margin-inline-start: -1.2rem;
}
#checkbox:not(.invisible) span:after {
background: url(images/default.png) no-repeat center top / 2.2rem auto;
}
#checkbox.checked span:after {
background-position: center bottom;
}
/* 'Dangerous' checkboxes */
#checkbox.danger span:after {
background-image: url(images/danger.png);
}

View file

@ -0,0 +1,72 @@
'use strict';
/* global ComponentUtils */
/**
* The gaia-confirm component displays a dialog in which the user has a
* choice to confirm or cancel the action. It may be displayed along with a
* title, description, and image. Buttons may also be configured.
* @requires GaiaButtons
*/
window.GaiaConfirm = (function(win) {
// Extend from the HTMLElement prototype
var proto = Object.create(HTMLElement.prototype);
// Allow baseurl to be overridden (used for demo page)
var baseurl = window.GaiaConfirmBaseurl ||
'/shared/elements/gaia_confirm/';
/**
preventDefault and prevent other listeners from hearing abut changes to a
particular event object.
@param {Array[Element]|Null} unless a list of elements to allow events to hit.
@param {Event} event triggered by addEventListener, etc...
*/
function eatEvent(unless, event) {
// prevent handlers from everything else unless the target is on the list.
if (unless && unless.indexOf(event.target) !== -1) {
return;
}
event.preventDefault();
event.stopImmediatePropagation();
}
proto.createdCallback = function() {
var shadow = this.createShadowRoot();
this._template = template.content.cloneNode(true);
shadow.appendChild(this._template);
ComponentUtils.style.call(this, baseurl);
var confirm = this.querySelector('gaia-buttons .confirm');
var cancel = this.querySelector('gaia-buttons .cancel');
if (confirm) {
confirm.addEventListener('click', (e) => {
eatEvent(null, e);
this.dispatchEvent(new CustomEvent('confirm'));
});
}
if (cancel) {
cancel.addEventListener('click', (e) => {
eatEvent(null, e);
this.dispatchEvent(new CustomEvent('cancel'));
});
}
};
var template = document.createElement('template');
template.innerHTML = `<form role="dialog" class="confirm">
<section>
<content select="h1"></content>
<content select="p"></content>
</section>
<content select="gaia-buttons">
</content>
</form>`;
// Register and return the constructor
return document.registerElement('gaia-confirm', { prototype: proto });
})(window);

View file

@ -0,0 +1,155 @@
/* ----------------------------------
* Confirm
* ---------------------------------- */
/* Main dialog setup */
gaia-confirm {
background: #2d2d2d;
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding-bottom: 7rem;
font-size: 0;
text-align: start;
/* Using font-size: 0; we avoid the unwanted visual space (about 3px)
created by white-spaces and break lines in the code betewen inline-block elements */
color: #fff;
display: flex;
align-items: stretch;
flex-direction: column;
justify-content: center;
transition: transform ease-in-out 0.3s, visibility 0.3s;
/**
magic number ideally to place confirm dialogs on top.
*/
z-index: 2147483646;
}
gaia-confirm[hidden] {
transform: translateY(100%);
visibility: hidden;
pointer-events: none;
}
.confirm section {
height: 100%;
font-weight: 300;
font-size: 2.2rem;
color: #FAFAFA;
padding: 0 1.5rem;
box-sizing: padding-box;
width: 100%;
display: inline-block;
max-height: 100%;
vertical-align: middle;
white-space: normal;
}
h1 {
font-weight: normal;
font-size: 1.6rem;
line-height: 1.5em;
color: #fff;
margin: 0;
padding: 1rem 1.5rem 0;
overflow: hidden;
max-height: 7rem;
word-wrap: break-word;
}
/* Specific component code */
p {
word-wrap: break-word;
margin: 1rem 0 0;
padding: 1rem 1.5rem;
border-top: 0.1rem solid #686868;
line-height: 3rem;
}
p img {
margin-inline-end: 2rem;
}
p img:-moz-dir(ltr) { float: left; }
p img:-moz-dir(rtl) { float: right; }
p strong {
font-weight: 300;
}
p small {
font-size: 1.4rem;
font-weight: normal;
color: #cbcbcb;
display: block;
}
dl {
border-top: 0.1rem solid #686868;
margin: 1rem 0 0;
overflow: hidden;
padding-top: 1rem;
font-size: 1.6rem;
line-height: 2.2rem;
}
dl > dt {
clear: both;
width: 7rem;
padding-inline-start: 1.5rem;
font-weight: 500;
text-align: start;
}
dl > dt:-moz-dir(ltr) { float: left; }
dl > dt:-moz-dir(rtl) { float: right; }
dl > dd {
padding-inline-end: 1.5rem;
font-weight: 300;
text-overflow: ellipsis;
vertical-align: top;
overflow: hidden;
}
figure {
margin: 1.5rem;
display: flex;
align-items: center;
}
figure img {
padding-inline-end: 1.5rem;
flex-shrink: 0;
}
figure figcaption {
font-size: 2.1rem;
font-weight: 300;
}
ul {
margin: 0;
padding: 2rem 1.5rem;
border-top: 0.1rem solid #686868;
list-style: none;
font-size: 1.6rem;
font-weight: normal;
}
ul li {
word-wrap: break-word;
line-height: 2rem;
padding-bottom: 1.5rem;
}
ul li:last-child {
padding-bottom: 0;
}
ul li ul {
border: none;
margin: 0;
padding: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,844 @@
'use strict';
/* global devicePixelRatio */
/* global GaiaGrid */
(function(exports) {
const ACTIVE_SCALE = 1.4;
/* This is an alternative delay used to initiate icon movement when in edit
* mode. The normal contextmenu delay is too long once we're in edit mode
* and causes user confusion, but a short delay globally would cause too many
* accidental triggers of edit mode.
*/
const EDIT_LONG_PRESS_DELAY = 200;
/* The movement threshold to use for the above synthetic long-press. */
const EDIT_LONG_PRESS_THRESHOLD = 4 * devicePixelRatio;
/* This delay is the time passed once users stop the finger over an icon and
* the rearrange is performed */
const REARRANGE_DELAY = 30;
/* The page is scrolled via javascript if an icon is being moved, and is
* within a length of a page edge configured by this value */
const EDGE_PAGE_THRESHOLD = 50;
/* This is the delay before calling finish when handling touchend. */
const TOUCH_END_FINISH_DELAY = 20;
/* The maximum frequency with which the grid will be rearranged in response
* to the item position changing, in ms.
*/
const REARRANGE_FREQUENCY = 100;
/* The amount of cool-down time after a rearrange animation. To avoid
* rearranging too frequently and causing jank and user confusion.
*/
const REARRANGE_COOLDOWN = 750;
const SCREEN_HEIGHT = window.innerHeight;
const scrollStep = Math.round(SCREEN_HEIGHT / EDGE_PAGE_THRESHOLD);
/* The scroll step will be 5 times bigger over the edge */
const maxScrollStepFactor = 5;
function DragDrop(gridView) {
this.gridView = gridView;
this.container = gridView.element;
this.scrollable = document.documentElement;
this.container.addEventListener('touchstart', this);
this.container.addEventListener('touchmove', this);
this.container.addEventListener('touchend', this);
window.addEventListener('touchcancel', this);
this.container.addEventListener('click', this);
this.container.addEventListener('contextmenu', this);
}
DragDrop.prototype = {
/**
* The current touchmove target.
* @type {DomElement}
*/
target: null,
/**
* The edit-mode long-press timeout
*/
longPressTimeout: null,
/**
* The position of the first touchstart of the current event block
*/
touchStart: { x: 0, y: 0, screenX: 0, screenY: 0 },
/**
* If we have moved an icon, this indicates that we need to save the state
* @type {boolean}
*/
dirty: false,
/**
* Whether or not we are in edit mode.
* @type {boolean}
*/
inEditMode: false,
/**
* The item being hovered over.
* @type {GaiaGrid.GridItem}
*/
hoverItem: null,
/**
* The group being hovered over.
* @type {GaiaGrid.Group}
*/
hoverGroup: null,
/*
* An element that's aligned to the top of the grid and should be
* considered as obscuring the grid when in edit mode.
*/
editHeaderElement: null,
/**
* The timeout used to rearrange the grid during dragging.
*/
rearrangeGridTimeout: null,
/**
* The time of the last reposition call.
*/
lastRepositionTime: 0,
/**
* Returns the maximum active scale value.
*/
get activeScale() {
return ACTIVE_SCALE;
},
/**
* Returns true if we are currently dragging an icon.
*/
get inDragAction() {
return this.target && this.target.classList.contains('active');
},
/**
* Returns the delay before calling finish in response to a touch-end event.
*/
get touchEndFinishDelay() {
return TOUCH_END_FINISH_DELAY;
},
/**
* Begins the drag/drop interaction.
* Enlarges the icon.
* Sets additional data to make the touchmove handler faster.
*/
begin: function(e) {
// Stop icon launching while we are in active state
this.gridView.stop();
window.dispatchEvent(new CustomEvent('gaiagrid-dragdrop-begin'));
this.hoverItem = null;
this.rearrangeDelay = null;
this.doRearrange = null;
this.enterEditMode();
this.container.classList.add('dragging');
this.icon.scale = ACTIVE_SCALE;
this.icon.setActive(true);
this.container.parentNode.style.overflow = 'hidden';
// Work around e.pageX/e.pageY being null (to make it easier to work with
// injected events, or old versions of Marionette)
this.initialPageX = (typeof e.pageX === 'undefined') ?
this.icon.x : e.pageX;
this.initialPageY = (typeof e.pageY === 'undefined') ?
this.icon.y : e.pageY;
this.currentTouch = {
pageX: this.initialPageX,
pageY: this.initialPageY
};
var items = this.gridView.items;
var lastElement = items[items.length - 1];
this.maxScroll = lastElement.y + lastElement.pixelHeight +
(this.icon.pixelHeight * ACTIVE_SCALE);
// If this is a group, or the sole icon in a group, make sure the
// surrounding groups are marked as invalid-drop so you can't initiate a
// move that would have no result.
var itemIndex = this.icon.detail.index;
if (this.icon.detail.type === 'divider') {
this.icon.element.classList.add('invalid-drop');
if (itemIndex > 0) {
for (var i = itemIndex - 1; i >= 0; i--) {
var item = items[i];
if (item.detail.type === 'divider') {
item.element.classList.add('invalid-drop');
break;
}
}
}
} else {
var itemBefore = itemIndex ? items[itemIndex - 1] : null;
var itemAfter = items[itemIndex + 1];
if ((itemAfter.detail.type === 'placeholder' ||
itemAfter.detail.type === 'divider') &&
(!itemBefore || itemBefore.detail.type === 'divider')) {
if (itemBefore) {
itemBefore.element.classList.add('invalid-drop');
}
var group, groupIndex = itemIndex;
do {
group = items[++groupIndex];
} while (group.detail.type !== 'divider');
group.element.classList.add('invalid-drop');
}
}
// Redraw the icon at the new position and scale
this.updateIconPosition();
// Rearrange grid to highlight the group underneath the icon
this.rearrangeGrid();
},
finish: function() {
// Complete the repositioning timeout
if (this.rearrangeGridTimeout !== null) {
this.rearrangeGrid();
}
// Remove the dragging property after the icon has transitioned into
// place to avoid jank due to animations starting that are disabled
// when dragging.
this.icon.element.addEventListener('transitionend', this);
this.currentTouch = null;
if (this.doRearrange !== null) {
if (this.rearrangeDelay) {
clearTimeout(this.rearrangeDelay);
}
this.doRearrange.call(this);
} else {
this.icon.requestAttention();
}
// Hand back responsibility to Grid view to render the dragged item.
this.icon.scale = 1;
this.icon.setActive(false);
this.icon.element.classList.remove('hovering');
this.target = null;
this.gridView.render();
// Save icon state if we need to
if (this.dirty) {
window.dispatchEvent(new CustomEvent('gaiagrid-saveitems'));
}
this.dirty = false;
setTimeout(function nextTick() {
this.gridView.start();
window.dispatchEvent(new CustomEvent('gaiagrid-dragdrop-finish'));
}.bind(this));
},
finalize: function() {
this.container.parentNode.style.overflow = '';
this.container.classList.remove('dragging');
this.container.classList.remove('hover-over-top');
if (this.icon) {
this.icon.element.removeEventListener('transitionend', this);
this.icon = null;
}
if (this.hoverItem) {
this.hoverItem.element.classList.remove('hovered');
this.hoverItem = null;
}
if (this.hoverGroup) {
this.hoverGroup.element.classList.remove('drop-target');
this.hoverGroup = null;
}
for (var i = 0, iLen = this.gridView.items.length;
i < iLen; i++) {
var item = this.gridView.items[i];
item.element.classList.remove('invalid-drop');
}
},
/**
* The closer to edge the faster (bigger step).
** Distance 0px -> 5 times faster
** Distance 25px -> 2.5 times faster
** Distance 50px (EDGE_PAGE_THRESHOLD) -> 0 times
*/
getScrollStep: function(distanceToEdge) {
var factor = maxScrollStepFactor;
if (distanceToEdge > 0) {
factor *= ((EDGE_PAGE_THRESHOLD - distanceToEdge) /
EDGE_PAGE_THRESHOLD);
}
return Math.round(scrollStep * factor);
},
/**
* Positions the dragged icon, rearranges the grid and scrolls the page if
* needed.
* The page is scrolled via javascript if an icon is being moved,
* and is within a percentage of a page edge.
*/
positionAndScrollIfNeeded: function() {
var touch = this.currentTouch;
if (!touch || !this.inDragAction) {
this.isScrolling = false;
return;
}
var scrollStep;
var docScroll = this.scrollable.scrollTop;
var distanceFromTop = Math.abs(touch.pageY - docScroll);
var distanceFromHeader = distanceFromTop -
(this.editHeaderElement ? this.editHeaderElement.clientHeight : 0);
this.isScrolling = true;
if (distanceFromTop > SCREEN_HEIGHT - EDGE_PAGE_THRESHOLD) {
var maxY = this.maxScroll;
scrollStep = this.getScrollStep(SCREEN_HEIGHT - distanceFromTop);
// We cannot exceed the maximum scroll value
if (touch.pageY >= maxY || maxY - touch.pageY < scrollStep) {
this.isScrolling = false;
}
} else if (touch.pageY > 0 && distanceFromHeader < EDGE_PAGE_THRESHOLD) {
// We cannot go below the minimum scroll value
scrollStep = -this.getScrollStep(distanceFromHeader);
scrollStep = Math.max(scrollStep, -this.scrollable.scrollTop);
if (scrollStep >= 0) {
this.isScrolling = false;
}
} else {
this.isScrolling = false;
}
if (!this.isScrolling) {
this.updateIconPosition();
this.deferredRearrangeGrid();
return;
}
this.updateIconPosition();
this.scrollable.scrollTop += scrollStep;
touch.pageY += scrollStep;
exports.requestAnimationFrame(this.positionAndScrollIfNeeded.bind(this));
},
/**
* Highlights the group of the given icon index, or the group if the index
* belongs to a group.
* @param {Integer} index The grid index of the icon or group.
*/
highlightGroup: function(index) {
for (var i = index, iLen = this.gridView.items.length;
i < iLen; i++) {
var item = this.gridView.items[i];
if (item.detail.type === 'divider') {
if (this.hoverGroup != item) {
if (this.hoverGroup) {
this.hoverGroup.element.classList.remove('drop-target');
}
this.hoverGroup = item;
item.element.classList.add('drop-target');
}
break;
}
}
},
inFirstGroup: function(index) {
for (var i = index; i >= 0; i--) {
if (this.gridView.items[i].detail.type === 'divider') {
return false;
}
}
return true;
},
/**
* Positions an icon on the grid using the current touch coordinates.
*/
updateIconPosition: function() {
var oldX = this.icon.x;
var oldY = this.icon.y;
var newX = this.currentTouch.pageX - (this.initialPageX - this.icon.x);
var newY = this.currentTouch.pageY - (this.initialPageY - this.icon.y);
// Adjust new icon coordinates for the slightly inflated scale so that
// it appears centered around the touch point. Dividers aren't scaled
// during dragging.
if (this.icon.detail.type !== 'divider') {
newX = newX - ((this.icon.scale * this.gridView.layout.gridItemWidth) -
this.gridView.layout.gridItemWidth) / 2;
newY = newY - ((this.icon.scale * this.icon.pixelHeight) -
this.icon.pixelHeight) / 2;
}
this.icon.setCoordinates(newX, newY);
this.icon.render();
this.icon.setCoordinates(oldX, oldY);
},
/**
* Will call rearrangeGrid after a short period, calculated based on how
* recently the grid was last rearranged.
*/
deferredRearrangeGrid: function() {
if (this.rearrangeGridTimeout !== null) {
return;
}
var delay = Math.max(
REARRANGE_FREQUENCY, REARRANGE_COOLDOWN -
Math.max(0, Date.now() - this.lastRepositionTime));
this.rearrangeGridTimeout = setTimeout(() => {
this.rearrangeGridTimeout = null;
this.rearrangeGrid();
}, delay);
},
/**
* Rearranges the grid icons and groups based on the current position of
* the dragged icon.
*/
rearrangeGrid: function() {
if (this.rearrangeGridTimeout !== null) {
clearTimeout(this.rearrangeGridTimeout);
this.rearrangeGridTimeout = null;
}
if (this.isScrolling) {
// We don't want the grid to shift while we're auto-scrolling
return;
}
var iconIsDivider = this.icon.detail.type === 'divider';
var pageX = this.currentTouch.pageX - (this.initialPageX - this.icon.x);
var pageY = this.currentTouch.pageY - (this.initialPageY - this.icon.y);
// Reposition in the icons array if necessary.
// Find the icon with the closest X/Y position of the move,
// and insert ours before it.
var foundIndex = 0;
var insertDividerAtTop = !this.gridView.config.features.disableSections;
pageX += this.gridView.layout.gridItemWidth / 2;
pageY += this.icon.pixelHeight / 2;
if (pageY >= 0) {
foundIndex =
this.gridView.getNearestItemIndex(pageX, pageY, iconIsDivider);
// If we're dragging a group over the first icon, a divider will
// be inserted at the top.
if (!(iconIsDivider && foundIndex === 0)) {
this.container.classList.remove('hover-over-top');
insertDividerAtTop = false;
}
}
// Clear the rearrange callback and hover item if we aren't hovering over
// anything.
this.doRearrange = null;
if (this.rearrangeDelay) {
clearTimeout(this.rearrangeDelay);
this.rearrangeDelay = null;
}
if (this.hoverItem) {
this.hoverItem.element.classList.remove('hovered');
this.hoverItem = null;
}
// Add the 'hovering' class to the dragged icon.
if (foundIndex !== null) {
this.icon.element.classList.add('hovering');
} else {
this.icon.element.classList.remove('hovering');
}
// Nothing to do if we find the dragged icon or no icon
if (!insertDividerAtTop &&
(foundIndex === null || foundIndex === this.icon.detail.index)) {
if (!iconIsDivider) {
this.highlightGroup(this.icon.detail.index);
}
return;
}
var foundItem = this.gridView.items[foundIndex];
// If we're a divider being dragged, only allow dropping on either the
// first item or another divider.
if (iconIsDivider &&
foundIndex > 0 &&
foundItem.detail.type !== 'divider') {
// Remove the hovering class from a group if it's not hovering over
// a valid drop position or itself.
this.icon.element.classList.remove('hovering');
return;
}
// If the item isn't a group, trigger the
// hovered state on the found item.
if (!insertDividerAtTop && !iconIsDivider) {
this.hoverItem = foundItem;
this.hoverItem.element.classList.add('hovered');
}
// Delay rearranging when we're dragging groups to when they're dropped
var rearrangeAfterDelay = !iconIsDivider;
// Determine if we want to create a new divider. We do this when hovering
// over a divider with an icon, or when dragging an icon to the very top
// of the grid.
var createDivider = insertDividerAtTop;
if (!insertDividerAtTop && !iconIsDivider &&
(foundItem.detail.type === 'divider')) {
// Allow dropping into a collapsed group if the new position is in
// the top 2/3 of the group.
if (foundItem.detail.collapsed &&
pageY <= foundItem.y + (foundItem.pixelHeight * 2/3)) {
rearrangeAfterDelay = false;
foundItem.element.classList.remove('hovered');
// When dragging an item forwards, it gets placed after the found
// item, adjust to the item before the divider.
if (this.icon.detail.index < foundIndex) {
foundItem = this.gridView.items[foundIndex - 1];
}
} else {
createDivider = true;
}
}
if (createDivider) {
// Remove the group background highlight if we're between groups
if (this.hoverGroup) {
this.hoverGroup.element.classList.remove('drop-target');
this.hoverGroup = null;
}
// Cancel rearrangement if it would have no effect.
var redundantRearrange = false;
if (insertDividerAtTop) {
if (iconIsDivider) {
redundantRearrange = this.inFirstGroup(this.icon.detail.index - 1);
} else {
redundantRearrange =
(this.icon.detail.index === 0 &&
this.gridView.items[1].detail.type === 'placeholder');
}
} else {
redundantRearrange = this.gridView.items[foundIndex].element.
classList.contains('invalid-drop');
}
if (redundantRearrange) {
if (this.hoverItem) {
this.hoverItem.element.classList.remove('hovered');
this.hoverItem = null;
}
return;
} else if (insertDividerAtTop) {
this.container.classList.add('hover-over-top');
}
this.doRearrange =
this.createNewDivider.bind(this,
insertDividerAtTop ? null : foundItem);
} else {
if (!iconIsDivider) {
// Change the display of the group the icon is hovering over to
// indicate it can be dropped.
this.highlightGroup(foundIndex);
}
this.doRearrange = this.rearrange.bind(this, foundItem);
if (rearrangeAfterDelay) {
this.rearrangeDelay =
setTimeout(this.doRearrange.bind(this), REARRANGE_DELAY);
}
}
},
/**
* Creates a new divider in GridView.items and rearranges the currently
* dragged icon into the newly created section.
* @param {Object} tDivider The divider after which to place a new divider,
* or null to insert a divider at the beginning on the grid.
*/
createNewDivider: function(tDivider) {
var items = this.gridView.items;
var tIndex = tDivider ? items.indexOf(tDivider) + 1 : 0;
// Create the new divider
var newDivider = new GaiaGrid.Divider();
items.splice(tIndex, 0, newDivider);
// Place the dragged item into the new empty section
var sIndex = items.indexOf(this.icon);
this.rearrange(sIndex >= tIndex ? newDivider : tDivider);
// Make sure the new divider/group is in view
newDivider.requestAttention();
},
/**
* Rearranges items in GridView.items
* @param {Object} tItem The item to position the dragged icon at.
*/
rearrange: function(tItem) {
// Clear the rearrange callback
this.doRearrange = null;
this.rearrangeDelay = null;
// We get a reference to the position of this.icon within the items
// array. Because placeholders are shifting around while we are dragging,
// we can't trust the detail.index attribute. This will be fixed on every
// render call though.
var sIndex = this.gridView.items.indexOf(this.icon);
var tIndex = this.gridView.items.indexOf(tItem);
// Check how many items we need to rearrange (if it's a group, there will
// be multiple items.
var eIndex = sIndex;
if (this.icon.detail.type === 'divider') {
for (; sIndex > 0 &&
this.gridView.items[sIndex - 1].detail.type !== 'divider';
sIndex--) {}
// Modify the tIndex to make sure we're inserting immediately after
// another divider or at the start of the container.
if (this.gridView.items[tIndex].detail.type === 'divider') {
tIndex++;
} else {
tIndex = 0;
}
if (sIndex === tIndex) {
// Nothing to do.
return;
}
} else if (sIndex < tIndex) {
tIndex++;
}
// We may have changed tIndex, so refresh our item pointer
tItem = this.gridView.items[tIndex];
// Remove the items from the array
var lastItemOffset = eIndex - sIndex;
var toInsert = this.gridView.items.splice(sIndex, lastItemOffset + 1);
// Craft the parameter list to reinsert the items at the correct place
toInsert.unshift(tItem ? this.gridView.items.indexOf(tItem) :
this.gridView.items.length, 0);
// Reinsert items
this.rearrangeDelay = null;
this.dirty = true;
this.gridView.items.splice.apply(this.gridView.items, toInsert);
var oldX = this.icon.x;
var oldY = this.icon.y;
this.gridView.render();
// In this case, we don't want to compensate for the icon moving, so
// we need to correct our initial values to stop the icon from jumping
// after rearranging.
this.initialPageX -= oldX - this.icon.x;
this.initialPageY -= oldY - this.icon.y;
this.lastRepositionTime = Date.now();
},
enterEditMode: function() {
this.inEditMode = true;
this.container.classList.add('edit-mode');
document.body.classList.add('edit-mode');
this.gridView.element.dispatchEvent(
new CustomEvent('editmode-start'));
document.addEventListener('visibilitychange', this);
this.gridView.render();
},
exitEditMode: function() {
// If we're in the middle of a drag, cancel it.
if (this.icon) {
this.finish();
this.finalize();
}
this.inEditMode = false;
this.cancelLongPressTimeout();
this.container.classList.remove('edit-mode');
document.body.classList.remove('edit-mode');
this.gridView.element.dispatchEvent(new CustomEvent('editmode-end'));
document.removeEventListener('visibilitychange', this);
this.gridView.render();
},
cancelLongPressTimeout: function() {
if (this.longPressTimeout !== null) {
clearTimeout(this.longPressTimeout);
this.longPressTimeout = null;
}
},
inLongPressThreshold: function(x, y) {
return (
Math.abs(x - this.touchStart.screenX) < EDIT_LONG_PRESS_THRESHOLD &&
Math.abs(y - this.touchStart.screenY) < EDIT_LONG_PRESS_THRESHOLD);
},
/**
* General event handler.
*/
handleEvent: function(e) {
switch(e.type) {
case 'visibilitychange':
if (document.hidden) {
this.exitEditMode();
}
break;
case 'touchstart':
this.cancelLongPressTimeout();
if (e.touches.length > 1) {
if (this.inDragAction) {
this.finish();
}
return;
}
if (this.inEditMode && !this.icon) {
this.touchStart.x = e.touches[0].pageX;
this.touchStart.y = e.touches[0].pageY;
this.touchStart.screenX = e.touches[0].screenX;
this.touchStart.screenY = e.touches[0].screenY;
this.longPressTimeout = setTimeout(() => {
this.longPressTimeout = null;
this.handleEvent({
type: 'contextmenu',
target: e.target,
pageX: this.touchStart.x,
pageY: this.touchStart.y,
preventDefault: function() {},
stopImmediatePropagation: function() {}
});
}, EDIT_LONG_PRESS_DELAY);
}
break;
case 'contextmenu':
this.cancelLongPressTimeout();
if (this.icon) {
e.stopImmediatePropagation();
e.preventDefault();
return;
}
if (e.defaultPrevented) {
// other handlers already handled this.
// in the future, we should use the shadow root and dispatch a
// "contextmenu" event from here instead.
return;
}
if (!e.target) {
return;
}
this.icon = this.gridView.findItemFromElement(e.target, true);
if (!this.icon || !this.icon.element || !this.icon.isDraggable() ||
this.icon.detail.type === 'placeholder') {
this.icon = null;
return;
}
e.stopImmediatePropagation();
e.preventDefault();
this.target = this.icon.element;
this.begin(e);
break;
case 'touchmove':
var touch = e.touches[0];
if (this.inDragAction) {
this.currentTouch = {
pageX: touch.pageX,
pageY: touch.pageY
};
if (!this.isScrolling) {
this.positionAndScrollIfNeeded();
}
} else if (this.longPressTimeout !== null &&
!this.inLongPressThreshold(touch.screenX,
touch.screenY)) {
this.cancelLongPressTimeout();
}
break;
case 'click':
case 'touchcancel':
if (this.inDragAction) {
this.finish();
this.finalize();
}
this.cancelLongPressTimeout();
break;
case 'touchend':
if (this.inDragAction) {
// Ensure the app is not launched
e.stopImmediatePropagation();
e.preventDefault();
// As contextmenu event can be synthesized, it's possible for it
// to happen in the same event-loop as touchend, meaning
// finish would be called immediately after begin. This would
// mean that the icon doesn't get a chance to transition and
// the transitionend that we expect won't be called, so finalize
// also won't be called, leaving us in an inconsistent state.
// It would be so great if there was a transitionstart event :(
setTimeout(() => { this.finish(); }, TOUCH_END_FINISH_DELAY);
}
this.cancelLongPressTimeout();
break;
case 'transitionend':
this.finalize();
break;
}
}
};
exports.GridDragDrop = DragDrop;
}(window));

View file

@ -0,0 +1,3 @@
;'use strict';(function(exports){const SHADOW_BLUR=1;const SHADOW_OFFSET_Y=1;const SHADOW_OFFSET_X=1;const SHADOW_COLOR='rgba(0, 0, 0, 0.2)';const DEFAULT_BACKGROUND_COLOR='rgb(228, 234, 238)';const UNSCALED_CANVAS_PADDING=2;const CANVAS_PADDING=UNSCALED_CANVAS_PADDING*devicePixelRatio;function IconRenderer(icon){this._icon=icon;}
IconRenderer.TYPE={CLIP:'clip',FAVICON:'favicon',STANDARD:'standard',};IconRenderer.prototype={unscaledCanvasPadding:UNSCALED_CANVAS_PADDING*2,get _maxSize(){return this._icon.grid.layout.gridMaxIconSize;},_createCanvas:function(){const CANVAS_PADDING_BOTTOM=1*devicePixelRatio;var canvas=document.createElement('canvas');canvas.width=this._maxSize+(CANVAS_PADDING*2);canvas.height=this._maxSize+CANVAS_PADDING+CANVAS_PADDING_BOTTOM;return canvas;},_createClipCanvas:function(){var canvas=document.createElement('canvas');canvas.width=this._maxSize+(CANVAS_PADDING*2);canvas.height=this._maxSize+(CANVAS_PADDING*2);return canvas;},_decorateShadowCanvas:function(canvas){var ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.shadowColor=SHADOW_COLOR;ctx.shadowBlur=SHADOW_BLUR;ctx.shadowOffsetY=SHADOW_OFFSET_Y;ctx.shadowOffsetX=SHADOW_OFFSET_X;return ctx;},clip:function(img){return new Promise((resolve)=>{var shadowCanvas=this._createCanvas();var shadowCtx=this._decorateShadowCanvas(shadowCanvas);var clipCanvas=this._createClipCanvas();var clipCtx=clipCanvas.getContext('2d',{willReadFrequently:true});clipCtx.beginPath();clipCtx.arc(clipCanvas.width/2,clipCanvas.height/2,clipCanvas.height/2,0,2*Math.PI);clipCtx.clip();clipCtx.drawImage(img,0,0,clipCanvas.width,clipCanvas.height);shadowCtx.drawImage(clipCanvas,CANVAS_PADDING,CANVAS_PADDING,this._maxSize,this._maxSize);shadowCanvas.toBlob(resolve);});},favicon:function(img){if(img.width>this._icon.grid.layout.gridIconSize/2){return this.clip(img);}
return new Promise((resolve)=>{var shadowCanvas=this._createCanvas();var shadowCtx=this._decorateShadowCanvas(shadowCanvas);var iconWidth;var iconHeight;shadowCtx.beginPath();shadowCtx.arc(shadowCanvas.width/2,shadowCanvas.height/2,shadowCanvas.height/2-CANVAS_PADDING,0,2*Math.PI,false);shadowCtx.fillStyle=DEFAULT_BACKGROUND_COLOR;shadowCtx.fill();iconWidth=iconHeight=this._maxSize*0.55;shadowCtx.shadowBlur=0;shadowCtx.shadowOffsetY=0;shadowCtx.mozImageSmoothingEnabled=false;shadowCtx.drawImage(img,(shadowCanvas.width-iconWidth)/2,(shadowCanvas.height-iconHeight)/2,iconWidth,iconHeight);shadowCanvas.toBlob(resolve);});},standard:function(img){return new Promise((resolve)=>{var shadowCanvas=this._createCanvas();var shadowCtx=this._decorateShadowCanvas(shadowCanvas);shadowCtx.drawImage(img,CANVAS_PADDING,CANVAS_PADDING,this._maxSize,this._maxSize);shadowCanvas.toBlob(resolve);});}};exports.GridIconRenderer=IconRenderer;}(window));

View file

@ -0,0 +1,193 @@
'use strict';
/* global devicePixelRatio */
/* global verticalPreferences */
(function(exports) {
const maxIconsPerRow = 4;
// 320 / 5 = 64px | 480 / 5 = 96px | 540 / 5 = 108px | ...
const iconScaleFactorMaxIconsPerRow = 5;
const minIconsPerRow = 3;
// 320 / 3.8 = 84px | 480 / 3.8 = 126px | 540 / 3.8 = 142px | ...
const iconScaleFactorMinIconsPerRow = 3.8;
const distanceBetweenIconsWithMinIconsPerRow = 36;
const distanceBetweenIconsWithMaxIconsPerRow = 38;
const eachTextRowHeight = 20;
var constraintSize;
function GridLayout(gridView) {
this.gridView = gridView;
if (window.verticalPreferences) {
verticalPreferences.get('grid.cols').then(value => {
this.cols = value;
this.onReady();
}, this.onReady);
verticalPreferences.addEventListener('updated', this);
} else {
this.onReady();
}
}
GridLayout.prototype = {
minIconsPerRow: minIconsPerRow,
maxIconsPerRow: maxIconsPerRow,
_cols: minIconsPerRow,
_offsetY: 0,
_percent: 1,
/**
* The visible height of each divider.
* Calculated by the divider class and cached here.
*/
_dividerLineHeight: 0,
get percent() {
return this._percent;
},
get cols() {
return this._cols;
},
set cols(value) {
if (!value || value === this._cols) {
return;
}
if (window.verticalPreferences) {
verticalPreferences.put('grid.cols', value);
}
// Reset the y-offset because we will re-render everything anyway.
this._offsetY = 0;
this._percent = value == minIconsPerRow ? 1 :
minIconsPerRow / maxIconsPerRow;
this._cols = value;
},
/**
* The width of the grid.
*/
get constraintSize() {
if (!constraintSize) {
this.calculateSize();
}
return constraintSize;
},
/**
* The height of each grid item.
*/
get gridItemHeight() {
var height = this.gridIconSize +
(this._cols === minIconsPerRow ?
distanceBetweenIconsWithMinIconsPerRow :
distanceBetweenIconsWithMaxIconsPerRow);
// Increase height for additional rows of text.
var defaultTextRows = 2;
var definedRows = parseInt(
this.gridView.element.getAttribute('text-rows'), 10);
if (definedRows > defaultTextRows) {
height += (definedRows - defaultTextRows) * eachTextRowHeight;
}
return height;
},
/**
* The width of each grid item.
* This number changes based on current zoom level.
*/
get gridItemWidth() {
return this.constraintSize / this._cols;
},
/**
* Returns the maximum size in pixels for an icon image. It is the size when
* the grid is displayed with the minimum number of columns per row.
*/
get gridMaxIconSize() {
var baseSize = (this.constraintSize / iconScaleFactorMinIconsPerRow);
return baseSize * devicePixelRatio;
},
/**
* Returns the icon image size depending on grid configuration and screen
* characteristics.
*/
get gridIconSize() {
var numCols = this._cols;
var size = this.constraintSize / numCols;
if (numCols === minIconsPerRow) {
size = this.constraintSize / iconScaleFactorMinIconsPerRow;
} else if (numCols === maxIconsPerRow) {
size = this.constraintSize / iconScaleFactorMaxIconsPerRow;
}
return Math.floor(size);
},
/**
* Gets the current offset of the Y-axis for the current zoom level.
* This value is updated by calling zoom.stepYAxis. For example, each
* group of three icons, or a divider, should increment this value.
* The value is reset and recalculated when the zoom level changes.
*/
get offsetY() {
return this._offsetY;
},
set offsetY(value) {
this._offsetY = value;
},
/**
* After we render a row we need to store the current position of the y-axis
*/
stepYAxis: function(value) {
this._offsetY += value;
},
/**
* General event handler.
*/
handleEvent: function(e) {
switch(e.type) {
case 'updated':
var prop = e.target;
if (prop.name === 'grid.cols') {
this.cols = parseInt(prop.value, 10);
this.gridView.render();
}
break;
}
},
calculateSize: function() {
constraintSize = Math.min(window.screen.width, window.screen.height);
},
onReady: function() {
window.dispatchEvent(new CustomEvent('gaiagrid-layout-ready'));
}
};
exports.GridLayout = GridLayout;
}(window));

View file

@ -0,0 +1,588 @@
'use strict';
/* global GridDragDrop */
/* global GaiaGrid */
/* global GridLayout */
/* global GridZoom */
/* global LazyLoader */
(function(exports) {
/* The time for which we'll disable launching other icons after tapping on
* an app icon. This will also disable edit mode.
*/
const APP_LAUNCH_TIMEOUT = 3000;
/**
* GridView is a generic class to render and display a grid of items.
* @param {Object} config Configuration object containing:
* - element: The shadow root of the grid
*/
function GridView(config) {
this.config = config;
this.clickIcon = this.clickIcon.bind(this);
this.onVisibilityChange = this.onVisibilityChange.bind(this);
if (config.features.zoom) {
this.zoom = new GridZoom(this);
}
this.layout = new GridLayout(this);
// Set columns if we have a 'cols' attribute
if (config.element.hasAttribute('cols')) {
this.layout.cols = parseInt(config.element.getAttribute('cols'), 10);
}
// Enable event listeners when instantiated.
this.start();
}
GridView.prototype = {
/**
* List of all application icons.
* Maps an icon identifier to an icon object.
*/
icons: {},
/**
* Lists of all displayed objects in the homescreen.
* Includes app icons, dividers, and bookmarks.
*/
items: [],
/**
* Returns a reference to the gaia-grid element.
*/
get element() {
return this.config.element;
},
/**
* Sets the number of columns.
*/
set cols(value) {
this.layout.cols = value;
},
/**
* We are in the state of launching an app.
*/
_launchingApp: false,
/**
* Adds an item into the items array.
* If the item is an icon, add it to icons.
* @param {Object} item The grid object, should inherit from GridItem.
* @param {Object} insertTo The position to insert the item into our list.
* @param {Boolean} expandGroup Expand the group this item is will be in.
*/
add: function(item, insertTo, expandGroup) {
if (!item) {
return;
}
if (item.identifier) {
// If we already have an item with this identifier, exit.
// This avoids a potential race condition where we might have duplicate
// items with the same identifiers in the grid. This should not happen.
if (this.icons[item.identifier]) {
console.log('Error, duplicate identifier: ',
item.identifier, new Error().stack);
return;
}
this.icons[item.identifier] = item;
} else if (item.detail.type !== 'divider' &&
item.detail.type !== 'placeholder') {
// If the item does not have an identifier, and is not a placeholder
// or divider, do not add it to the grid.
console.log('Error, could not load identifier for object: ',
JSON.stringify(item.detail));
return;
}
// If insertTo it is a number, splice.
if (!isNaN(parseFloat(insertTo)) && isFinite(insertTo)) {
this.items.splice(insertTo, 0, item);
} else {
insertTo = this.items.length;
this.items.push(item);
}
if (expandGroup) {
for (var i = insertTo + 1, iLen = this.items.length;
i < iLen; i++) {
var divider = this.items[i];
if (divider.detail.type === 'divider') {
if (divider.detail.collapsed) {
divider.expand();
}
break;
}
}
}
},
/**
* Finds nearest item by and returns an index, or null if an item isn't
* found.
* @param {Number} x relative to the screen
* @param {Number} y relative to the screen
* @param {Boolean} isRow whether the position is to be considered as a row
* instead of an individual point.
*/
getNearestItemIndex: function(x, y, isRow) {
var foundIndex = null;
var leastDistance = null;
var itemMiddleOffset = this.layout.gridItemWidth / 2;
for (var i = 0, iLen = this.items.length; i < iLen; i++) {
var item = this.items[i];
// Don't consider items that can't be dragged
if (!item.isDraggable()) {
continue;
}
// If sections are disabled, don't consider dividers
if (this.config.features.disableSections &&
item.detail.type === 'divider') {
continue;
}
// Do not consider collapsed items, unless they are dividers.
if (item.detail.type !== 'divider' &&
item.element.classList.contains('collapsed')) {
continue;
}
var middleX = item.x + itemMiddleOffset;
var middleY = item.y + item.pixelHeight / 2;
var xDistance = (isRow || item.detail.type === 'divider') ?
0 : x - middleX;
var yDistance = y - middleY;
var distance = Math.sqrt(
xDistance * xDistance +
yDistance * yDistance);
if (leastDistance === null || distance < leastDistance) {
leastDistance = distance;
foundIndex = i;
}
}
return foundIndex;
},
start: function() {
this.element.addEventListener('click', this.clickIcon);
window.addEventListener('visibilitychange', this.onVisibilityChange);
},
stop: function() {
this.element.removeEventListener('click', this.clickIcon);
window.removeEventListener('visibilitychange', this.onVisibilityChange);
},
findItemFromElement: function(element, excludeCollapsedIcons) {
while (element && element.parentNode !== this.element) {
element = element.parentNode;
}
if (!element) {
return null;
}
var i, iLen = this.items.length;
var identifier = element.dataset.identifier;
var icon = this.icons[identifier];
// If the element didn't have an identifier, try to search for it
// manually.
if (!icon) {
for (i = 0; i < iLen; i++) {
if (this.items[i].element === element) {
icon = this.items[i];
break;
}
}
}
if (icon && excludeCollapsedIcons) {
// If this is a collapsed item, return its group instead
if (icon.detail.type !== 'divider' &&
icon.detail.type !== 'placeholder' &&
icon.element.classList.contains('collapsed')) {
for (i = icon.detail.index + 1; i < iLen; i++) {
if (this.items[i].detail.type === 'divider') {
return this.items[i];
}
}
console.warn('Collapsed icon found with no group');
icon = null;
}
}
return icon;
},
onVisibilityChange: function() {
this._launchingApp = false;
},
/**
* Launches an app.
*/
clickIcon: function(e) {
e.preventDefault();
var inEditMode = this.dragdrop && this.dragdrop.inEditMode;
var action = 'launch';
if (e.target.classList.contains('remove')) {
action = 'remove';
}
var icon = this.findItemFromElement(e.target);
if (!icon) {
return;
}
if (action === 'launch') {
// We do not allow users to launch icons in edit mode
if (inEditMode && e.target.classList.contains('icon')) {
// Check if we're trying to edit a bookmark
if (!icon.isEditable()) {
return;
}
action = 'edit';
} else {
// If the icon can't be launched, bail out early
if (!icon[action]) {
return;
}
// Add a 'launching' class to the icon to style it with CSS.
icon.element.classList.add('launching');
// XXX: We can't have nice things. Remove the launching class after an
// arbitrary time to restore the state. We want the icon to return
// to its original state after launching the app, but visibilitychange
// will not work because activities do not fire it.
var returnTimeout = 500;
setTimeout(function stateReturn() {
if (icon.element) {
icon.element.classList.remove('launching');
}
}, returnTimeout);
}
}
if ((icon.detail.type === 'app' || icon.detail.type === 'bookmark') &&
this._launchingApp) {
return;
}
if ((icon.detail.type === 'app' && icon.appState === 'ready') ||
icon.detail.type === 'bookmark') {
this._launchingApp = true;
if (this._launchingTimeout) {
window.clearTimeout(this._launchingTimeout);
this._launchingTimeout = null;
}
// This avoids some edge cases if we didn't get visibilitychange anyway.
this._launchingTimeout = window.setTimeout(function() {
this._launchingTimeout = null;
this._launchingApp = false;
}.bind(this), APP_LAUNCH_TIMEOUT);
}
icon[action](e.target);
},
/**
* Scrubs the list of items, removing empty sections.
*/
cleanItems: function(skipDivider) {
var appCount = 0;
var toRemove = [];
this.items.forEach(function(item, idx) {
if (item instanceof GaiaGrid.Divider) {
if (appCount === 0) {
toRemove.push(idx);
}
appCount = 0;
} else {
appCount++;
}
}, this);
toRemove.reverse();
toRemove.forEach(function(idx) {
var removed = this.items.splice(idx, 1)[0];
removed.remove();
}, this);
// There should always be a divider at the end, it's hidden in CSS when
// not in edit mode.
if (skipDivider) {
return;
}
// If the last item is not a divider, add a new divider at the end.
var lastItem = this.items[this.items.length - 1];
if (!lastItem || !(lastItem instanceof GaiaGrid.Divider)) {
this.items.push(new GaiaGrid.Divider());
}
},
/**
* Removes placeholders from the grid.
*/
removeAllPlaceholders: function() {
var toSplice = [];
var previousItem;
this.items.forEach(function(item, idx) {
if (item instanceof GaiaGrid.Placeholder) {
// If the previous item is a divider, or there is no previous item,
// and we are in edit mode, we do not remove the placeholder.
// This is so the section will remain even if the user drags the
// icon around. Bug 1014982
if ((!previousItem ||
(previousItem && previousItem instanceof GaiaGrid.Divider)) &&
this.dragdrop && this.dragdrop.inDragAction) {
return;
}
toSplice.push(idx);
}
previousItem = item;
}, this);
toSplice.reverse().forEach(function(idx) {
this.items.splice(idx, 1)[0].remove();
}, this);
},
/**
* Clears the grid view of all items.
*/
clear: function() {
for (var i = 0, iLen = this.items.length; i < iLen; i++) {
var item = this.items[i];
if (item.element) {
this.element.removeChild(item.element);
// We must de-reference element explicitly so we can re-use item
// objects the next time we call render.
item.element = null;
item.lastX = null;
item.lastY = null;
item.lastScale = null;
}
}
this.items = [];
this.icons = {};
},
/**
* Creates placeholders and injects them into the grid.
* @param {Array} coordinates [x,y] coordinates on the grid of the first
* item in grid units.
* @param {Integer} idx The position of the first placeholder.
* @param {Integer} idx The number of placeholders to create.
*/
createPlaceholders: function(coordinates, idx, count) {
var isRTL = (document.documentElement.dir === 'rtl');
for (var i = 0; i < count; i++) {
var item = new GaiaGrid.Placeholder();
this.items.splice(idx + i, 0, item);
item.setPosition(idx + i);
var xPosition = (coordinates[0] + i) * this.layout.gridItemWidth;
if (isRTL) {
xPosition =
(this.layout.constraintSize - this.layout.gridItemWidth) -
xPosition;
}
item.setCoordinates(xPosition, this.layout.offsetY);
item.render();
}
},
/**
* Renders all icons.
* Positions app icons and dividers accoriding to available space
* on the grid.
* @param {Object} options Options to render with including:
* - skipDivider {Boolean} Whether or not to skip the divider
* - rerender {Boolean} Whether we should clean elements and re-render.
*/
render: function(options) {
var self = this;
options = options || {};
this.removeAllPlaceholders();
this.cleanItems(options.skipDivider);
// Reset offset steps
var oldHeight = this.layout.offsetY;
this.layout.offsetY = 0;
// Grid render coordinates
var x = 0;
var y = 0;
/**
* Steps the y-axis.
* @param {Object} item
*/
function step(item) {
var pixelHeight = item.pixelHeight;
self.layout.stepYAxis(pixelHeight);
x = 0;
y++;
}
var pendingCachedIcons = 0;
var onCachedIconRendered = () => {
if (--pendingCachedIcons <= 0) {
this.element.removeEventListener('cached-icon-rendered',
onCachedIconRendered);
this.element.dispatchEvent(new CustomEvent('cached-icons-rendered'));
}
};
this.element.addEventListener('cached-icon-rendered',
onCachedIconRendered);
var nextDivider = null;
var oddDivider = true;
var isRTL = (document.documentElement.dir === 'rtl');
for (var idx = 0; idx < this.items.length; idx++) {
var item = this.items[idx];
// Remove the element if we are re-rendering.
if (options.rerender && item.element) {
this.element.removeChild(item.element);
item.element = null;
}
if (item.detail.type === 'divider') {
nextDivider = null;
} else {
if (!nextDivider) {
for (var i = idx + 1; i < this.items.length; i++) {
if (this.items[i].detail.type === 'divider') {
nextDivider = this.items[i];
oddDivider = !oddDivider;
break;
}
}
// Make sure to leave room for group headers
if (nextDivider &&
!nextDivider.detail.collapsed) {
this.layout.offsetY += nextDivider.headerHeight;
}
}
// If this item is in a collapsed group, we need to skip rendering.
if (nextDivider && nextDivider.detail.collapsed) {
item.setPosition(idx);
continue;
}
}
// If the item would go over the boundary before rendering,
// step the y-axis.
if (x > 0 && item.gridWidth > 1 &&
x + item.gridWidth >= this.layout.cols) {
// Insert placeholders to fill remaining space
var remaining = this.layout.cols - x;
this.createPlaceholders([x, y], idx, remaining);
// Increment the current index due to divider insertion
idx += remaining;
item = this.items[idx];
// Step the y-axis by the size of the last row.
// For now we just check the height of the last item.
var lastItemInRow = this.items[idx - 1];
step(lastItemInRow);
}
item.setPosition(idx);
if (!options.skipItems) {
item.hasCachedIcon && ++pendingCachedIcons;
var xPosition = x * this.layout.gridItemWidth;
if (isRTL) {
xPosition =
(this.layout.constraintSize - this.layout.gridItemWidth) -
xPosition;
}
item.setCoordinates(xPosition, this.layout.offsetY);
if (!item.active) {
item.render();
}
if (item.detail.type === 'divider') {
if (oddDivider) {
item.element.classList.add('odd');
} else {
item.element.classList.remove('odd');
}
}
}
// Increment the x-step by the sizing of the item.
// If we go over the current boundary, reset it, and step the y-axis.
x += item.gridWidth;
if ((x >= this.layout.cols) && (idx < this.items.length - 1)) {
step(item);
}
}
// All the children of this element are absolutely positioned and then
// transformed, so the grid actually has no height. Fire an event that
// embedders can listen to discover the grid height.
if (this.layout.offsetY != oldHeight) {
if (this.dragdrop && this.dragdrop.inDragAction) {
// Delay size changes during drags to avoid jankiness when dragging
// items around due to touch positions changing.
this.layout.offsetY = oldHeight;
} else {
this.element.dispatchEvent(new CustomEvent('gaiagrid-resize',
{ detail: this.layout.offsetY }));
}
}
this.element.setAttribute('cols', this.layout.cols);
pendingCachedIcons === 0 && onCachedIconRendered();
this.loadDragDrop();
},
/**
* Loads dragdrop libraries and instantiates if necessary.
* DragDrop libraries are lazy laoded to save on startup time. They are
* loaded after the initial paint in order to paint the icons as fast
* as possible.
*/
loadDragDrop: function() {
if (!this.dragdrop && this.config.features.dragdrop) {
LazyLoader.load('shared/elements/gaia_grid/js/grid_dragdrop.js', () => {
if (this.dragdrop) {
return;
}
this.dragdrop = new GridDragDrop(this);
});
}
}
};
exports.GridView = GridView;
}(window));

View file

@ -0,0 +1,160 @@
'use strict';
(function(exports) {
const pinchThreshold = Math.round(window.innerWidth / 12);
function GridZoom(gridView) {
this.gridView = gridView;
this.touches = 0;
this.zoomStartTouches = [];
this.zoomStartDistance = 0;
this.container = document.getElementById('zoom');
this.arrows = this.container.querySelector('.arrows');
this.curtain = this.container.querySelector('.curtain');
this.indicator = this.container.querySelector('.indicator');
this.start();
window.addEventListener('gaiagrid-dragdrop-begin', this.stop.bind(this));
window.addEventListener('gaiagrid-dragdrop-finish', this.start.bind(this));
}
GridZoom.prototype = {
/**
* Starts listening for touchstart events.
* This is what starts listening events to start the drag & drop operation.
*/
start: function() {
window.addEventListener('touchstart', this);
},
/**
* Stops listening for touchstart events.
* This will effectively disable pinch-to-zoom.
*/
stop: function() {
window.removeEventListener('touchstart', this);
},
_attachGestureListeners: function() {
window.addEventListener('touchmove', this);
window.addEventListener('touchend', this);
},
_stopGestureListeners: function() {
window.removeEventListener('touchmove', this);
window.removeEventListener('touchend', this);
},
_resetState: function() {
this.zoomInProgress = false;
this.container.hidden = true;
this.indicator.classList.remove('active');
this.arrows.classList.remove('zooming', 'grow', 'shrink');
this.arrows.style.transform = '';
},
/**
* General Event Handler
*/
handleEvent: function(e) {
if (e.type === 'touchend' && this.zoomStartTouches) {
if (!this.zoomInProgress) {
this._resetState();
}
this._stopGestureListeners();
return;
}
if (!e.touches || e.touches.length !== 2) {
return;
}
// Sort touches by ascending pageX position.
var touches = [e.touches[0], e.touches[1]].sort(function(a, b) {
return a.pageX - b.pageX;
});
var touchDistance = Math.sqrt(
(touches[0].pageX - touches[1].pageX) *
(touches[0].pageX - touches[1].pageX) +
(touches[0].pageY - touches[1].pageY) *
(touches[0].pageY - touches[1].pageY));
var layout = this.gridView.layout;
switch(e.type) {
case 'touchstart':
this.container.hidden = false;
this.zoomStartTouches = touches;
this.zoomStartDistance = touchDistance;
if (layout.cols < layout.maxIconsPerRow) {
this.arrows.classList.add('grow');
} else {
this.arrows.classList.add('shrink');
}
this.indicator.dataset.cols = layout.cols;
this._attachGestureListeners();
break;
case 'touchmove':
if (layout.cols < layout.maxIconsPerRow &&
touchDistance < this.zoomStartDistance &&
Math.abs(touchDistance - this.zoomStartDistance) >
pinchThreshold) {
this.gridView.cols = layout.maxIconsPerRow;
this.zoomInProgress = true;
} else if (layout.cols > layout.minIconsPerRow &&
touchDistance > this.zoomStartDistance &&
Math.abs(touchDistance - this.zoomStartDistance) >
pinchThreshold) {
this.gridView.cols = layout.minIconsPerRow;
this.zoomInProgress = true;
} else {
return;
}
// For now just implement a canned animation
this._stopGestureListeners();
var ontransitionend = function() {
this.arrows.removeEventListener('transitionend', ontransitionend);
// Change the indicator color at the end.
this.indicator.classList.add('active');
this.gridView.render();
// Reset zoom element state after a set time.
var zoomHideTime = 400;
setTimeout(this._resetState.bind(this), zoomHideTime);
}.bind(this);
this.arrows.addEventListener('transitionend', ontransitionend);
this.indicator.dataset.cols = layout.cols;
this.arrows.classList.add('zooming');
// Force a sync reflow
document.body.clientHeight;
var scaleTransform = 'scale(1)';
if (this.zoomInProgress && touchDistance < this.zoomStartDistance) {
scaleTransform = 'scale(0.4)';
}
this.arrows.style.transform = scaleTransform;
break;
}
}
};
exports.GridZoom = GridZoom;
}(window));

View file

@ -0,0 +1,127 @@
'use strict';
/* global GaiaGrid */
/* global GridIconRenderer */
/* global MozActivity */
/* jshint nonew: false */
(function(exports) {
const TYPE = 'bookmark';
/**
* Represents a single bookmark icon on the homepage.
*/
function Bookmark(record, features) {
this.detail = record;
this.features = features || {};
this.detail.type = TYPE;
}
Bookmark.prototype = {
__proto__: GaiaGrid.GridItem.prototype,
/**
* Bookmarks use a custom icon renderer because they are likely much
* smaller than a standard icon.
*/
renderer: GridIconRenderer.TYPE.FAVICON,
/**
* Returns the height in pixels of each icon.
*/
get pixelHeight() {
return this.grid.layout.gridItemHeight;
},
/**
* Width in grid units for each icon.
*/
gridWidth: 1,
get name() {
return this.detail.name;
},
get icon() {
return this.detail.icon || this.defaultIcon;
},
get identifier() {
return this.detail.id;
},
update: GaiaGrid.GridItem.prototype.updateFromDatastore,
/**
* Bookmarks are always editable unless noted otherwise in features.
*/
isEditable: function() {
return this.features && this.features.isEditable !== false;
},
/**
* Bookmarks are always removable unless noted otherwise in features.
*/
isRemovable: function() {
return this.features && this.features.isRemovable !== false;
},
/**
* Bookmarks are always draggable unless noted otherwise in features.
*/
isDraggable: function() {
return this.features && this.features.isDraggable !== false;
},
/**
* This method overrides the GridItem.render function.
*/
render: function() {
GaiaGrid.GridItem.prototype.render.call(this);
this.element.classList.add('bookmark');
if (this.isEditable()) {
this.element.classList.add('editable');
}
},
/**
* Launches the bookmark in a browser window.
*/
launch: function() {
var features = {
name: this.name,
icon: this.icon,
remote: true
};
var url = this.detail.url;
if (this.features.search) {
features.searchName = this.name;
features.searchUrl = url;
}
window.open(url, '_samescope', Object.keys(features)
.map(function eachFeature(key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(features[key]);
}).join(','));
},
/**
* Opens a web activity to edit the bookmark.
*/
edit: function() {
new MozActivity({
name: 'save-bookmark',
data: {
type: 'url',
url: this.detail.id
}
});
}
};
exports.GaiaGrid.Bookmark = Bookmark;
}(window));

View file

@ -0,0 +1,73 @@
'use strict';
/* global GaiaGrid */
(function(exports) {
/**
* Represents a single divider on the homepage.
*/
function Divider() {
this.detail = {
type: 'divider',
index: 0
};
}
Divider.prototype = {
__proto__: GaiaGrid.GridItem.prototype,
x: 0,
y: 0,
headerHeight: 0,
/**
* Height in pixels of each divider.
*/
get pixelHeight() {
return (this.grid.layout.cols > 3) ? 50 : 60;
},
/**
* Width in grid units for each divider.
*/
gridWidth: 4,
scale: 1,
/**
* Renders the icon to the grid component.
*/
render: function() {
// Generate the content if we need to
if (!this.element) {
// Divider is a <section> and the rest of items are <div> containers
// in order to hide the last divider via :last-of-type pseudo-class
var divider = this.element = document.createElement('section');
divider.className = 'divider';
var span = document.createElement('span');
span.className = 'spacer';
divider.appendChild(span);
this.grid.element.appendChild(divider);
}
this.element.style.transform = 'translate(0 ,' + this.y + 'px)';
},
remove: function() {
if (this.element) {
this.element.parentNode.removeChild(this.element);
}
},
isDraggable: function() {
return false;
}
};
exports.GaiaGrid.Divider = Divider;
}(window));

View file

@ -0,0 +1,666 @@
'use strict';
/* global GridIconRenderer */
/* global Promise */
/* global UrlHelper */
(function(exports) {
// event names
const ICON_BLOB_DECORATED_EVENT = 'iconblobdecorated';
const ICON_BLOB_ERROR_EVENT = 'gaiagrid-iconbloberror';
const FETCH_XHR_TIMEOUT = 10000;
/**
* XHR wrapper for fetching blobs with timeout logic.
*
* @param {String} uri to fetch.
* @return {Promise[Blob]}
*/
function fetchBlob(uri) {
return new Promise(function(accept, reject) {
var xhr = new XMLHttpRequest({
mozAnon: true,
mozSystem: true
});
xhr.open('GET', uri, true);
xhr.responseType = 'blob';
xhr.timeout = FETCH_XHR_TIMEOUT;
// remember that send can throw for some non http protocols. The promise
// wrapper here protects us.
xhr.send();
xhr.onload = function() {
var status = xhr.status;
if (status !== 0 && status !== 200) {
reject(
new Error('Got HTTP status ' + status + ' trying to load ' + uri)
);
return;
}
accept(xhr.response);
};
xhr.onerror = xhr.ontimeout = function() {
reject(new Error('Error while HTTP GET: ', uri));
};
});
}
/**
* Represents a generic grid item from which other items can inherit from.
*/
function GridItem() {
this.detail = {};
}
GridItem.prototype = {
x: 0,
y: 0,
/**
* Height in pixels of each item.
*/
pixelHeight: 0,
/**
* Width in grid units for each item.
*/
gridWidth: 1,
scale: 1,
/**
* Whether or not this icon will persist to the database.
*/
persistToDB: true,
/**
* Cached values of the last transform of the element, to avoid redundant
* style changes.
*/
lastX: null,
lastY: null,
lastScale: null,
/**
* Whether or not this item has a cached icon or not.
*/
get hasCachedIcon() {
return this.detail && this.detail.decoratedIconBlob;
},
/**
* The icon renderer to use. Sub-classes may override this, and individual
* objects may as well with a custom detail.renderer property.
* Bookmarks use this for example to render both web results from E.me,
* and favicons from the web with different styles.
*/
renderer: GridIconRenderer.TYPE.STANDARD,
/**
* Every grid item has a desired icon (over the network) if we fail to fetch
* or render it for some reason we display another. This string keeps track
* of the states:
*
*
* none: no action has been taken yet to display or fetch the icon.
* pending: fetching the icon
* success: icon was fetched and displayed
* error: icon could not be fetched or displayed
*
*/
iconState: 'none',
/**
* Default icon url to use when no other icon can be downloaded
*/
get defaultIcon() {
// XXX: ideally this would be relative to the component
return '/shared/elements/gaia_grid/images/default_icon.png';
},
/**
* Returns a reference to the current grid.
* We can currently only have one grid per page.
*/
get grid() {
return document.getElementsByTagName('gaia-grid')[0]._grid;
},
/**
* Returns true if this item is removable.
*/
isRemovable: function() {
return false;
},
/**
* Returns true if this item is editable.
*/
isEditable: function() {
return false;
},
/**
* Returns true if this item is draggable.
*/
isDraggable: function() {
return true;
},
/**
* Returns true if the icon is hosted at an origin.
*/
isIconFromOrigin: function() {
return this.icon.startsWith('http') || this.icon.startsWith('app:');
},
/**
* Sets the item position
*/
setPosition: function(position) {
this.detail.index = position;
},
/**
* Sets the item coordinates
*/
setCoordinates: function(x, y) {
this.x = x;
this.y = y;
},
/**
* Dispatches a gaiagrid-attention signal with the rect of the item.
*/
requestAttention: function() {
var rect = {
x: this.x,
y: this.y,
width: this.gridWidth * this.grid.layout.gridItemWidth,
height: this.pixelHeight
};
this.grid.element.dispatchEvent(
new CustomEvent('gaiagrid-attention', { detail: rect }));
},
closestIconSizeFromList: function(choices) {
if (!choices) {
return 0;
}
var sizes = Object.keys(choices);
if (!sizes.length) {
return 0;
}
var maxSize = this.grid.layout.gridMaxIconSize; // The goal size
var filter = (a) => a >= maxSize ? a : 0;
// Ideally, get an icon that is equal to our grid size, or else
// the smallest icon which is larger than our grid size.
var bestSize = Math.min(...sizes.map(filter));
// If no icons are larger than (or equal to ) our grid size, get
// the largest one.
bestSize = bestSize || Math.max(...sizes);
return bestSize || 0;
},
/**
* Given a list of icons that match a size, return the closest icon to
* reduce possible pixelation by picking a wrong size.
* @param {Object} choices An object mapping icon size to icon URL.
*/
closestIconFromList: function(choices) {
var icon;
// Check for W3C web manifest format for icons
if (Array.isArray(choices)) {
var manifest = {
'icons': choices
};
var maxSize = this.grid.layout.gridMaxIconSize; // The goal size
icon = window.IconsHelper.getBestIconFromWebManifest(manifest, maxSize);
return icon ? (new URL(icon, this.app.manifestURL)).href : null;
}
var accurateSize = this.closestIconSizeFromList(choices);
if (accurateSize === 0) {
return this.defaultIcon;
}
icon = choices[accurateSize];
// Handle relative URLs
if (!UrlHelper.hasScheme(icon)) {
icon = this.app.origin + icon;
}
return icon;
},
/**
* Renders the given image src as the icon (with processing).
*
* @param {String} url The image url to display.
*/
renderIconFromSrc: function(url) {
var background = new Image();
background.src = url;
background.onload = this._decorateIcon.bind(this, background);
},
/**
* Renders the given blob as the icon (with processing).
*
* @param {Blob} blob to render as icon.
*/
renderIconFromBlob: function(blob) {
var background = new Image();
background.src = URL.createObjectURL(blob);
background.onload = () => {
this._decorateIcon(background);
URL.revokeObjectURL(background.src);
};
background.onerror = () => {
URL.revokeObjectURL(background.src);
// If we already have a cached icon, do not overwrite it on error.
if (this.hasCachedIcon) {
this._stampElementWithIcon('blobcache');
this._displayDecoratedIcon(this.detail.decoratedIconBlob, true);
return;
}
this.renderIconFromSrc(this.defaultIcon);
this._stampElementWithIcon(this.defaultIcon);
};
},
/**
* Begins decoration process for a given icon.
*
* @param {HTMLImageElement} img An image element to display from.
*/
_decorateIcon: function(img) {
var strategy = this.detail.renderer || this.renderer;
this.rendererInstance = new GridIconRenderer(this);
this.rendererInstance[strategy](img).
then(this._displayDecoratedIcon.bind(this));
},
/**
* Finishes decoration process and displays the augmented icon.
*
* @param {Blob} blob The image blob to display.
*/
_displayDecoratedIcon: function(blob, isCachedIcon) {
if (!this.element) {
// The icon could be removed while it is being decorated
return;
}
var style = this.element.style;
if (!style.backgroundSize) {
style.height = this.grid.layout.gridItemHeight + 'px';
// icon size + padding for shadows implemented in the icon renderer
var backgroundHeight =
((this.grid.layout.gridIconSize * (1 / this.grid.layout.percent)) +
GridIconRenderer.prototype.unscaledCanvasPadding);
if (this.grid.layout.gridItemHeight > backgroundHeight) {
style.backgroundSize = backgroundHeight + 'px';
} else {
style.backgroundSize = style.height;
}
}
if (isCachedIcon) {
var url = URL.createObjectURL(blob);
style.backgroundImage = 'url(' + url + ')';
this.element.dataset.backgroundImage = url;
var img = new Image();
img.onload = img.onerror = () => {
this.grid.element.dispatchEvent(
new CustomEvent('cached-icon-rendered')
);
};
img.src = url;
return;
}
this._compareBlobs(blob, this.detail.decoratedIconBlob).
then((equal) => {
if (equal) {
return;
}
style.backgroundImage = 'url(' + URL.createObjectURL(blob) + ')';
this.detail.decoratedIconBlob = blob;
this.grid.element.dispatchEvent(
new CustomEvent(ICON_BLOB_DECORATED_EVENT, {
detail: this
})
);
var bgImg = this.element.dataset.backgroundImage;
bgImg && URL.revokeObjectURL(bgImg);
});
},
/**
* It compares two blobs.
*/
_compareBlobs: function(blob1, blob2) {
return new Promise(function(resolve) {
if (!blob1 || !blob2 || blob1.type !== blob2.type ||
blob1.size !== blob2.size) {
resolve(false);
return;
}
// We skip the first bytes that typically are headers
var startBytes = 127;
var bytesHash = 16;
var reader = new FileReader();
reader.onloadend = function() {
var result1 = reader.result;
reader = new FileReader();
reader.onloadend = reader.onerror = function() {
resolve(result1 === reader.result);
};
reader.readAsDataURL(blob2.slice(startBytes, startBytes + bytesHash));
};
reader.onerror = function() {
resolve(false);
};
reader.readAsDataURL(blob1.slice(startBytes, startBytes + bytesHash));
});
},
/**
* Fetch an icon blob by a URI. This can be used as an extension hook to
* implement caching logic into the fetching process.
*
* @return {Promise[Null|Blob]}
*/
fetchIconBlob: function() {
return fetchBlob(this.icon);
},
/**
* Helper method to stamp the element with an icon uri. This stamp is for
* debugging/testing only and has no other effect.
*/
_stampElementWithIcon: function(uri) {
// ensure we don't stamp URI
if (!this.element || uri.startsWith('data:')) {
return;
}
// do not allow long strings on the stamp
if (uri.length > 255) {
uri = uri.slice(0, 255);
}
// test prefix used to indicate this has no functional value.
this.element.dataset.testIcon = uri;
},
/**
Fetch and render the desired icon (`.icon`) if possible otherwise fallback
to the default icon.
XXX: This method is not concurrency safe one image may override another
without ordering.
*/
renderIcon: function(renderCachedIcon) {
var icon = this.icon;
this.iconState = 'pending';
if (!icon) {
return;
}
if (renderCachedIcon && this.hasCachedIcon) {
// Display cached icons before trying to get icons again.
this._displayDecoratedIcon(this.detail.decoratedIconBlob, true);
var resolveIcon = () => {
this.grid.element.removeEventListener('cached-icons-rendered',
resolveIcon);
this.doRenderIcon(icon);
};
this.grid.element.addEventListener('cached-icons-rendered',
resolveIcon);
} else {
this.doRenderIcon(icon);
}
},
doRenderIcon: function(icon) {
// fast path if the icon is not from an origin display it outright
if (!this.isIconFromOrigin()) {
// XXX: Should we convert data uri(s) to blobs?
this._stampElementWithIcon(icon);
this.renderIconFromSrc(icon);
// state set after in case of some exception or whatever
this.iconState = 'success';
// noop
return Promise.resolve();
}
// root element of the web component
var eventTarget = this.grid.element;
// handle any cases where the blob is null or the icon cannot be
// fetched.
var blobNotFound = () => {
this.iconState = 'error';
if (!this.hasCachedIcon) {
this.renderIconFromSrc(this.defaultIcon);
this._stampElementWithIcon(this.defaultIcon);
} else {
this._stampElementWithIcon('blobcache');
}
eventTarget.dispatchEvent(
new CustomEvent(ICON_BLOB_ERROR_EVENT, {
detail: this
})
);
};
return this.fetchIconBlob().
then((blob) => {
// if the fetch was a success but no blob could be found short
// short circuit... note that being offline is _not_ an error.
if (!blob) {
return blobNotFound();
}
this.renderIconFromBlob(blob);
this.iconState = 'success';
this._stampElementWithIcon(icon);
}).
catch((err) => {
blobNotFound();
});
},
/**
Safely remove this item from the grid and DOM.
*/
removeFromGrid: function() {
var idx = this.grid.items.indexOf(this);
// This should never happen but is remotely possible item is not in the
// grid.
if (idx === -1) {
console.error('Attempting to remove self before item has been added!');
return;
}
// update the state of the grid and DOM so this item is no longer
// referenced.
this.grid.items.splice(idx, 1);
delete this.grid.icons[this.identifier];
if (this.element) {
this.element.parentNode.removeChild(this.element);
}
// ensure we don't end up with empty cruft..
this.grid.render({ from: idx - 1 });
},
/**
Removes item from the dom and dispatches a removeitem event.
*/
remove: function() {
this.grid.element.dispatchEvent(new CustomEvent('removeitem', {
detail: this
}));
},
/**
* Renders the icon to the container.
*/
render: function() {
var scale = this.grid.layout.percent * this.scale;
// Generate an element if we need to
if (!this.element) {
var tile = document.createElement('div');
tile.className = 'icon';
tile.dataset.identifier = this.identifier;
tile.dataset.isDraggable = this.isDraggable();
tile.setAttribute('role', 'link');
tile.setAttribute('tabindex', 0);
tile.style.width = (this.grid.layout.constraintSize / 3) + 'px';
// This <p> has been added in order to place the title with respect
// to this container via CSS without touching JS.
var nameContainerEl = document.createElement('p');
nameContainerEl.style.marginTop = ((this.grid.layout.gridIconSize *
(1 / this.grid.layout.percent)) +
(GridIconRenderer.prototype.unscaledCanvasPadding / 2)) + 'px';
tile.appendChild(nameContainerEl);
var nameEl = document.createElement('span');
nameEl.className = 'title';
nameEl.setAttribute('dir', 'auto');
nameContainerEl.appendChild(nameEl);
// Add delete link if this icon is removable
if (this.isRemovable()) {
var removeEl = document.createElement('span');
removeEl.className = 'remove';
tile.appendChild(removeEl);
}
this.element = tile;
this.updateTitle();
this.renderIcon(true);
this.grid.element.appendChild(tile);
}
this.transform(this.x, this.y, scale);
},
/**
* Positions and scales an icon.
*/
transform: function(x, y, scale, element) {
scale = scale || 1;
if (!element) {
if (x === this.lastX && y === this.lastY && scale === this.lastScale) {
return;
}
element = this.element;
this.lastX = x;
this.lastY = y;
this.lastScale = scale;
}
element.style.transform =
'translate(' + x + 'px,' + y + 'px) scale(' + scale + ')';
},
/**
* Set whether the item is being dragged.
*/
setActive: function(active) {
if (active) {
this.element.classList.add('active');
this.active = true;
} else {
this.element.classList.remove('active');
this.active = false;
}
},
/**
Updates the title of the icon on the grid.
*/
updateTitle: function() {
// it is remotely possible that we have not .rendered yet
if (!this.element) {
return;
}
var nameEl = this.element.querySelector('.title');
if (this.asyncName) {
this.asyncName().then(function(name) {
nameEl.textContent = name;
});
} else {
nameEl.textContent = this.name;
}
},
/**
* Updates an icon on the page from a datastore record.
* Used for bookmarks.
* @param {Object} record The datastore record.
*/
updateFromDatastore: function(record) {
var iconChanged = record.icon !== this.icon;
var nameChanged = record.name !== this.name;
var type = this.detail.type;
var lastIcon = this.icon;
record.type = type;
this.detail = record;
if (nameChanged) {
this.updateTitle();
}
if (iconChanged && record.icon) {
this.renderIcon();
} else if (!record.icon) {
this.detail.icon = lastIcon;
}
}
};
exports.GaiaGrid.GridItem = GridItem;
}(window));

View file

@ -0,0 +1,430 @@
'use strict';
/* global GaiaGrid */
(function(exports) {
/**
* The relative size of icons when in a collapsed group.
*/
const COLLAPSE_RATIO = 0.33;
/**
* Maximum number of icons that are visible in a collapsed group.
*/
const COLLAPSED_GROUP_SIZE = 6;
/**
* Space to be reserved at the sides of collapsed group items, in pixels.
*/
const COLLAPSED_GROUP_MARGIN_LEFT = 23;
const COLLAPSED_GROUP_MARGIN_RIGHT = 53;
/**
* Time, in ms, before removing the 'toggling' class from the expand/collapse
* toggle element.
*/
const TOGGLE_TIMEOUT = 250;
/**
* A replacement for the default Divider class that implements group
* collapsing and provides convenience functions for group drag'n'drop.
*/
function Group(detail) {
this.detail = detail || {};
this.detail.type = 'divider';
this.detail.index = 0;
this.pendingCollapse = this.detail.collapsed = !!this.detail.collapsed;
}
Group.prototype = {
__proto__: GaiaGrid.GridItem.prototype,
gridWidth: 4,
/**
* Height in pixels of the header part of the group.
*/
get headerHeight() {
return this.detail.collapsed ? 20 : 30;
},
/**
* The collapsed state. Used to defer the actual state change of
* collapse/expand so that anything re-rendering the grid between setting
* the state and the re-render caused by the state change doesn't cause
* the group to be positioned incorrectly.
*/
pendingCollapse: false,
pendingCollapseTimeout: null,
/**
* Height in pixels of the background of the group.
*/
backgroundHeight: 0,
lastBackgroundHeight: null,
/**
* Height in pixels of the separator at the bottom of the group.
*/
separatorHeight: 0,
/**
* The scale used for collapsed icons, taking into account column size.
*/
collapseRatio: COLLAPSE_RATIO,
/**
* Height in pixels of the group. When collapsed, this includes the
* icons associated with the group.
*/
get pixelHeight() {
return this.detail.collapsed ?
this.backgroundHeight :
this.separatorHeight;
},
/**
* Returns the number of items in the group. Relies on the item index
* being correct.
*/
get size() {
var size = 0;
for (var i = this.detail.index - 1, items = this.grid.items;
i >= 0 && items[i].detail.type !== 'divider'; i--) {
size++;
}
return size;
},
/**
* Creates the element and its children for this group item.
*/
_createElement: function() {
var group = this.element = document.createElement('section');
group.className = 'divider group newly-created';
// Create the background (only seen in edit mode)
var span = document.createElement('span');
span.className = 'background';
group.appendChild(span);
this.backgroundSpanElement = span;
// Create an element for a drop-shadow (only seen when active)
span = document.createElement('span');
span.className = 'shadow';
group.appendChild(span);
this.shadowSpanElement = span;
// Create the header (container for the move gripper and
// expand/collapse toggle)
span = document.createElement('span');
span.className = 'header';
group.appendChild(span);
this.headerSpanElement = span;
// Create the move gripper
span = document.createElement('span');
span.className = 'gripper';
this.headerSpanElement.appendChild(span);
// Create the expand/collapse toggle
var button = document.createElement('button');
button.className = 'toggle';
button.dataset.l10nId = 'gaia-grid-toggle-expanded';
this.headerSpanElement.appendChild(button);
this.toggleElement = button;
// Create the group separator (only seen in non-edit mode)
span = document.createElement('span');
span.className = 'separator';
group.appendChild(span);
this.dividerSpanElement = span;
// Create a child span for the separator to act as the indicator for
// dropping icons/groups during editing.
this.dividerSpanElement.appendChild(document.createElement('span'));
this.grid.element.appendChild(group);
this.separatorHeight = this.dividerSpanElement.clientHeight;
this.lastBackgroundHeight = null;
},
/**
* Renders the apps within the group. If the group is not collapsed, this
* just validates the style class of the group.
*/
_renderChildren: function(nApps) {
var isRTL = (document.documentElement.dir === 'rtl');
if (!this.detail.collapsed) {
this.element.classList.remove('collapsed');
return;
}
// If collapsed, we need to style and position the icons in the group
// into one row.
this.element.classList.add('collapsed');
var y = this.y;
var index = this.detail.index;
var width = Math.round(
(this.grid.layout.constraintSize -
COLLAPSED_GROUP_MARGIN_LEFT - COLLAPSED_GROUP_MARGIN_RIGHT) /
COLLAPSED_GROUP_SIZE);
var x = isRTL ?
(this.grid.layout.constraintSize - COLLAPSED_GROUP_MARGIN_RIGHT) :
COLLAPSED_GROUP_MARGIN_LEFT;
y += this.headerHeight;
var maxGridItemWidth =
this.grid.layout.constraintSize / this.grid.layout.minIconsPerRow;
this.collapseRatio =
(maxGridItemWidth / this.grid.layout.gridItemWidth) * COLLAPSE_RATIO;
for (var i = index - nApps; i < index; i++) {
var item = this.grid.items[i];
if (this.detail.collapsed) {
item.scale = this.collapseRatio;
var itemVisible = (i - (index - nApps)) < COLLAPSED_GROUP_SIZE;
if (!itemVisible) {
item.setCoordinates(isRTL ? x + width : x - width, y);
} else {
item.setCoordinates(x, y);
x += isRTL ? -width: width;
}
item.render();
item.element.classList.add('collapsed');
if (!itemVisible) {
item.element.classList.add('hidden');
}
}
}
},
/**
* Gets the y-position of the group. When the group is expanded, its y
* position is actually the y-position of the separator underneath the
* group. This gets the visible y-position of the group.
*/
getRealYPosition: function(nApps) {
if (this.detail.collapsed) {
return this.y;
}
return this.grid.items[this.detail.index - nApps].y -
this.headerHeight;
},
/**
* Renders the icon to the grid component.
*/
render: function() {
// Generate the content if we need to
var createdElements = false;
if (!this.element) {
createdElements = true;
this._createElement();
}
// Render children
var nApps = this.size;
this._renderChildren(nApps);
// Calculate group position.
// If we're not collapsed, the group's position will be underneath its
// icons, but we want it to display above.
var y = this.getRealYPosition(nApps);
if (y !== this.lastY) {
// Place the header span
this.headerSpanElement.style.transform =
'translate(0px, ' + y + 'px)';
}
if (this.toggleElement) {
var toggleLabel = this.detail.collapsed ? 'collapsed' : 'expanded';
this.toggleElement.dataset.l10nId = 'gaia-grid-toggle-' + toggleLabel;
}
// Calculate the height of the background span
if (this.detail.collapsed) {
this.backgroundHeight =
Math.round(this.collapseRatio * this.grid.layout.gridIconSize * 1.5);
} else {
var height = Math.ceil(nApps / this.grid.layout.cols);
this.backgroundHeight = (height || 1) * this.grid.layout.gridItemHeight;
}
this.backgroundHeight += this.headerHeight;
if (y != this.lastY ||
this.backgroundHeight !== this.lastBackgroundHeight) {
// Place and size the background span element
this.backgroundSpanElement.style.transform =
'translate(0px, ' + y + 'px) scale(1, ' + this.backgroundHeight + ')';
// Place and size the shadow span element
this.shadowSpanElement.style.transform =
'translateY(' + y + 'px)';
this.shadowSpanElement.style.height = this.backgroundHeight + 'px';
// Place the divider after this point
this.dividerSpanElement.style.transform =
'translate(0px, ' + (y + this.backgroundHeight) + 'px)';
}
// Update the cached size values
this.lastBackgroundHeight = this.backgroundHeight;
this.lastY = y;
// Now include the separator in the background height
this.backgroundHeight += this.separatorHeight;
// Fade in newly-created groups
if (createdElements) {
// Force a reflow to make sure the transform transition doesn't play
this.element.clientTop;
this.element.classList.remove('newly-created');
}
},
forEachItem: function(callback) {
var nApps = this.size;
var index = this.detail.index;
for (var i = index - nApps; i < index; i++) {
callback(this.grid.items[i]);
}
},
setActive: function(active) {
GaiaGrid.GridItem.prototype.setActive.call(this, active);
// Make sure we're collapsed
this.collapse();
// Mark our child items as active/inactive with us so they pick up the
// right style when dragged.
var callback = active ?
function(item) { item.element.classList.add('active'); } :
function(item) { item.element.classList.remove('active'); };
this.forEachItem(callback);
},
/*
* As the start of a collapse/expand animation can involve a lot of work,
* the response isn't always instant. To help alleviate this, we show a
* quick response on the toggle element first before initiating the rest
* of the work.
*/
_rerenderOnToggle: function() {
if (this.pendingCollapseTimeout) {
clearTimeout(this.pendingCollapseTimeout);
this.pendingCollapseTimeout = null;
}
if (this.toggleElement) {
this.toggleElement.classList.add('toggling');
setTimeout(() => { this.toggleElement.classList.remove('toggling'); },
TOGGLE_TIMEOUT);
}
// Ideally, this setTimeout would be a requestAnimationFrame, but the
// above class-adding seems to end up coalesced with the code inside
// requestAnimationFrame, rather than it happening on the next frame.
// The same thing happens with any setTimeout lower than about 20ms too.
this.pendingCollapseTimeout = setTimeout(() => {
this.detail.collapsed = this.pendingCollapse;
this.pendingCollapseTimeout = null;
if (!this.detail.collapsed) {
// Remove collapsed styling from all icons
this.forEachItem(function(item) {
item.scale = 1;
if (item.element) {
item.element.classList.remove('collapsed');
item.element.classList.remove('hidden');
}
});
}
// If the item is active, re-render it so that its pixel-height is
// correct when the rest of the grid is rendered.
if (this.active) {
this.render();
}
this.grid.render();
var dragging = this.grid.dragdrop && this.grid.dragdrop.inDragAction;
if (dragging) {
// If we're dragging, make sure to reposition the icon in the correct
// place, as the render call won't redraw us
this.grid.dragdrop.updateIconPosition();
} else {
// If we're not dragging, save the collapsed state
window.dispatchEvent(new CustomEvent('gaiagrid-saveitems'));
// Request attention so that we're as visible as we can be after
// expanding/collapsing
this.requestAttention();
}
}, 20);
},
collapse: function() {
if (this.detail.collapsed) {
return;
}
this.pendingCollapse = true;
this._rerenderOnToggle();
},
expand: function() {
if (!this.detail.collapsed) {
return;
}
this.pendingCollapse = false;
this._rerenderOnToggle();
},
launch: function(target) {
if (target !== this.toggleElement) {
return;
}
if (this.detail.collapsed) {
this.expand();
} else {
this.collapse();
}
},
remove: function() {
if (this.element) {
this.element.parentNode.removeChild(this.element);
}
},
isDraggable: function() {
return true;
},
requestAttention: function() {
var rect = {
x: 0,
y: this.getRealYPosition(this.size),
width: this.gridWidth * this.grid.layout.gridItemWidth,
height: this.backgroundHeight
};
this.grid.element.dispatchEvent(
new CustomEvent('gaiagrid-attention', { detail: rect }));
}
};
exports.GaiaGrid.Divider = Group; // Override the non-grouping divider
}(window));

View file

@ -0,0 +1,51 @@
'use strict';
/* global GaiaGrid */
/* global GridIconRenderer */
/* global MozActivity */
/* jshint nonew: false */
(function(exports) {
const TYPE = 'marketplace-app';
/**
* Represents a single bookmark icon on the homepage.
*/
function MarketPlaceApp(record) {
this.detail = record;
this.detail.type = TYPE;
}
MarketPlaceApp.prototype = {
__proto__: GaiaGrid.Bookmark.prototype,
renderer: GridIconRenderer.TYPE.STANDARD,
/**
* Returns the icon image path.
*/
get icon() {
var icon = this._accurateIcon;
if (!icon) {
icon = this._accurateIcon = this.closestIconFromList(
this.detail.icons);
}
return icon;
},
launch: function() {
new MozActivity({
name: 'marketplace-app',
data: {
slug: this.detail.slug
}
});
},
};
exports.GaiaGrid.MarketPlaceApp = MarketPlaceApp;
}(window));

View file

@ -0,0 +1,369 @@
'use strict';
/* global ConfirmDialogHelper, GaiaGrid */
(function(exports) {
const IDENTIFIER_SEP = '-';
var MOZAPP_EVENTS = [
'downloadsuccess',
'downloaderror',
'downloadavailable',
'downloadapplied',
'progress'
];
var APP_LOADING = 'loading';
var APP_ERROR = 'error';
var APP_UNRECOVERABLE = 'unrecoverable';
var APP_PAUSED = 'paused';
var APP_READY = 'ready';
function localizeString(str) {
var userLang = document.documentElement.lang;
// We want to make sure that we translate only if we're using
// a runtime pseudolocale.
// mozL10n.ctx.qps contains only runtime pseudolocales
if (navigator.mozL10n &&
navigator.mozL10n.ctx.qps.indexOf(userLang) !== -1) {
return navigator.mozL10n.qps[userLang].translate(str);
}
return str;
}
/**
* Represents single app icon on the homepage.
*/
function Mozapp(app, entryPoint, details) {
this.app = app;
this.entryPoint = entryPoint;
this.detail = {
type: 'app',
manifestURL: app.manifestURL,
entryPoint: entryPoint,
index: 0,
// XXX: Somewhat ugly hack around the constructor args
decoratedIconBlob: details && details.decoratedIconBlob
};
// Yes this is clobbering all other listeners to the app MozApp
// should be in charge here otherwise the icon will be incorrect.
MOZAPP_EVENTS.forEach(function(type) {
this.app['on' + type] = this.handleEvent.bind(this);
}, this);
// Determine and set the initial state of the app when it is inserted.
this.setAppState(this._determineState(app));
}
/**
`app.manifest.role`s which should not be displayed on the grid.
*/
Mozapp.HIDDEN_ROLES = [
'system', 'input', 'homescreen', 'search', 'addon', 'langpack'
];
Mozapp.prototype = {
__proto__: GaiaGrid.GridItem.prototype,
/**
Determine if this application is supposed to be hidden.
@return Boolean
*/
_isHiddenRole: function() {
var manifest = this.app.manifest;
// don't know what the role is so it is not hidden
if (!manifest || !manifest.role) {
return null;
}
// if it is in the hidden role list return true
return Mozapp.HIDDEN_ROLES.indexOf(manifest.role) !== -1;
},
/**
Figure out which state the app is in
*/
_determineState: function(app) {
// is there is a pending download but we cannot download then this app is
// in an unrecoverable state due to some fatal error.
if (
// Must be pending (meaning not launchable)
app.installState === 'pending' &&
// Without any option for downloading the app
!app.downloadAvailable &&
// And not be currently applying a download
!app.readyToApplyDownload
) {
return APP_UNRECOVERABLE;
}
// If the app is currently downloading life is good and we should display
// loading.
if (app.downloading) {
return APP_LOADING;
}
// Bug 1027347 - downloadError is always present even if there is no error
// so we check both for the error and that the error has a
// name.
if (app.downloadError && app.downloadError.name) {
// If the app was paused while the homescreen was running the
// downloaderror event will fire with the DOWNLOAD_CANCELED error.
if (app.downloadError.name === 'DOWNLOAD_CANCELED') {
return APP_PAUSED;
}
return APP_ERROR;
}
// Note that this logic is correct because app.downloading is caught above
// separately.
if (app.installState === 'pending') {
return APP_PAUSED;
}
return APP_READY;
},
handleEvent: function(event) {
switch (event.type) {
case 'progress':
case 'downloaderror':
case 'downloadsuccess':
this.setAppState(this._determineState(this.app));
break;
case 'downloadapplied':
// Ensure that a hidden app has not somehow been added to the grid...
if (this._isHiddenRole()) {
console.warn('Removing hidden app from the grid', this.name);
return this.removeFromGrid();
}
// we may have updated icons so recalculate the correct icon.
delete this._accurateIcon;
delete this._accurateIconSize;
// Need to set app state here to correctly handle the pause/resume
// case.
this.setAppState(this._determineState(this.app));
// XXX: This introduces a potential race condition.
// GridItem.renderIcon is not concurrency safe one image may override
// another without ordering.
this.renderIcon();
window.dispatchEvent(new CustomEvent('downloadapplied', {
detail: {
id: this.app.manifestURL
}
}));
break;
}
},
// Overrides GridItem.fetchIconBlob
fetchIconBlob: function() {
if (!this._accurateIconSize) {
this._accurateIconSize =
this.closestIconSizeFromList(this.descriptor.icons);
}
var result = this._accurateIconSize ?
navigator.mozApps.mgmt.getIcon(this.app, this._accurateIconSize,
this.entryPoint || '') :
GaiaGrid.GridItem.prototype.fetchIconBlob.call(this);
return result;
},
/**
* Returns the height in pixels of each icon.
*/
get pixelHeight() {
return this.grid.layout.gridItemHeight;
},
/**
* Width in grid units for each icon.
*/
gridWidth: 1,
get name() {
var userLang = document.documentElement.lang;
if (navigator.mozL10n && userLang in navigator.mozL10n.qps) {
return navigator.mozL10n.qps[userLang].
translate(this.descriptor.short_name || this.descriptor.name);
}
var locales = this.descriptor.locales;
var localized =
locales && locales[userLang] &&
(locales[userLang].short_name || locales[userLang].name);
return localized || this.descriptor.short_name || this.descriptor.name;
},
asyncName: function() {
var userLang = document.documentElement.lang;
var ep = this.entryPoint || undefined;
if (!this.app.getLocalizedValue) {
return new Promise((resolve, reject) => { reject(); });
}
return this.app.getLocalizedValue('short_name', userLang, ep).then(
shortName => localizeString(shortName),
this.app.getLocalizedValue.bind(this.app, 'name', userLang, ep)).then(
name => localizeString(name),
() => this.name
);
},
/**
* Returns the icon image path.
*/
get icon() {
var icon = this._accurateIcon;
if (!icon) {
icon = this._accurateIcon = this.closestIconFromList(
this.descriptor.icons);
}
return icon;
},
get descriptor() {
var manifest = this.app.manifest || this.app.updateManifest;
if (this.entryPoint && manifest.entry_points &&
manifest.entry_points[this.entryPoint]) {
return manifest.entry_points[this.entryPoint];
}
return manifest;
},
identifierSeparator: IDENTIFIER_SEP,
get identifier() {
var identifier = [this.app.manifestURL];
if (this.entryPoint) {
identifier.push(this.entryPoint);
}
return identifier.join(IDENTIFIER_SEP);
},
/**
* Returns true if this app is removable.
*/
isRemovable: function() {
return this.app.removable;
},
/**
Show a dialog to handle unrecoverable errors.
*/
unrecoverableError: function() {
navigator.mozApps.mgmt.uninstall(this.app);
},
cancel: function() {
var dialog = new ConfirmDialogHelper({
type: 'pause',
title: {id: 'gaia-grid-stop-download-title', args: { name: this.name }},
body: 'gaia-grid-stop-download-body',
cancel: {
title: 'gaia-grid-cancel'
},
confirm: {
title: 'gaia-grid-stop-download-action',
type: 'danger',
cb: () => this.app.cancelDownload()
}
});
dialog.show(document.body);
},
resume: function() {
var dialog = new ConfirmDialogHelper({
type: 'resume',
title: 'gaia-grid-resume-download-title',
body: {id: 'gaia-grid-resume-download-body', args: { name: this.name }},
cancel: {
title: 'gaia-grid-cancel'
},
confirm: {
title: 'gaia-grid-resume-download-action',
cb: () => {
// enter the loading state optimistically
this.setAppState(APP_LOADING);
this.app.download();
}
}
});
dialog.show(document.body);
},
setAppState: function(state) {
this.appState = state;
if (this.element) {
this.element.dataset.appState = state;
}
},
render: function() {
// If there is no element before we can render we need to update the
// app state. Render may also be called to reposition elements.
var needsStateSet = !this.element;
GaiaGrid.GridItem.prototype.render.apply(this, arguments);
if (needsStateSet) {
// ensure the newly rendered item has an app state
this.setAppState(this.appState);
}
},
/**
* Resolves click action.
*/
launch: function() {
var app = this.app;
switch (this._determineState(app)) {
case APP_UNRECOVERABLE:
return this.unrecoverableError();
case APP_ERROR:
case APP_PAUSED:
return this.resume();
case APP_LOADING:
return this.cancel();
}
window.performance.mark('appLaunch@' + app.origin);
if (this.entryPoint) {
return app.launch(this.entryPoint);
}
// Default action is to launch the app.
return app.launch();
}
};
exports.GaiaGrid.Mozapp = Mozapp;
}(window));

View file

@ -0,0 +1,73 @@
'use strict';
/* global GaiaGrid */
(function(exports) {
/**
* The placeholder represents an empty place on the grid.
* This is generally used to detect distance from an empty spot on the grid.
*/
function Placeholder() {
this.detail = {
type: 'placeholder',
index: 0
};
}
Placeholder.prototype = {
__proto__: GaiaGrid.GridItem.prototype,
/**
* Returns the height in pixels of each icon.
*/
get pixelHeight() {
return this.grid.layout.gridItemHeight;
},
/**
* When the placeholder is rendered in the last row, for group creation
* purposes, there is special handling required for height.
*/
createsGroupOnDrop: false,
/**
* Width in grid units for each icon.
*/
gridWidth: 1,
/**
* Placeholders do not save. They are re-generated on render calls.
*/
persistToDB: false,
get name() {
return '';
},
/**
* Renders a transparent placeholder.
*/
render: function() {
// Generate an element if we need to
if (!this.element) {
var tile = document.createElement('div');
tile.className = 'icon placeholder';
tile.style.height = this.pixelHeight + 'px';
this.element = tile;
this.grid.element.appendChild(tile);
}
this.transform(this.x, this.y, this.grid.layout.percent);
},
remove: function() {
if (this.element) {
this.element.parentNode.removeChild(this.element);
}
}
};
exports.GaiaGrid.Placeholder = Placeholder;
}(window));

View file

@ -0,0 +1,409 @@
.icon[data-app-state="loading"]:after {
content: "";
background: no-repeat top center url('images/app_install_spinner.png');
background-size: contain;
height: 8.4rem;
position: absolute;
top: 0;
left: 0;
width: 100%;
/**
Keyframe must be defined outside of the web component.
See bug 1018269
*/
animation: rotate 2.25s linear infinite;
}
.icon[data-app-state="loading"] {
/**
We must align this value with the value of [data-app-state]:after height the background size
here is overridden at the element level elsewhere which causes the spinner to be aligned too
low.
*/
background-size: 8.4rem !important;
background-image: url('images/app_installing.png') !important;
}
.icon[data-app-state="unrecoverable"] {
background-image: url('images/app_install_unrecoverable.png') !important;
}
.icon[data-app-state="error"] {
background-image: url('images/app_install_failed.png') !important;
}
.icon[data-app-state="paused"] {
background-image: url('images/app_install_canceled.png') !important;
}
.icon {
position: absolute;
transform-origin: 0 0;
display: inline-block;
text-align: center;
background: no-repeat top center;
z-index: 3;
outline: 0;
}
.icon.launching {
opacity: .65;
}
/* XXX: Override the .remove element */
.edit-mode .collection.hovered .remove {
background: url(images/add.png) no-repeat;
background-size: 2rem;
}
.icon p {
pointer-events: none;
height: 3.2rem;
width: 100%;
margin: 0;
padding: 0;
}
.icon .title {
display: inline-block;
margin-top: 0.1rem;
/* We need this rule in order not to hide the drop shadow */
padding: 0 0.2rem 1rem;
text-shadow: 0.1rem 0.1rem 0.3rem rgba(0, 0, 0, 0.3),
0.2rem 0.4rem 1rem rgba(0, 0, 0, 0.4);
font-size: 1.4rem;
color: #fff;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: calc(100% - 0.4rem);
box-sizing: border-box;
vertical-align: top;
transition: opacity var(--transition-duration);
}
/* We allow more space for text in the search app by hooking into the text-rows attribute on gaia-grid. */
[text-rows="3"] .icon .title {
height: 5.8rem;
}
[cols="4"] .icon .title {
/* Font size is the same independent on grid configuration */
font-size: calc(1.2rem * 1.333);
margin-top: 0.5rem;
line-height: 1.94rem;
}
.edit-mode .icon.collapsed {
pointer-events: none;
}
.icon.collapsed .title {
opacity: 0;
}
.icon.hidden {
pointer-events: none;
opacity: 0;
}
.divider {
position: absolute;
width: 100%;
padding-top: 2.3rem;
border-left: 0.6rem solid transparent;
border-right: 0.6rem solid transparent;
box-sizing: border-box;
z-index: 2;
}
[cols="4"] .divider {
padding-top: 1.6rem;
}
.divider span.spacer {
position: absolute;
bottom: 0;
display: block;
background: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.5));
margin: 0 25%;
width: 50%;
height: 0.2rem;
transition: opacity var(--transition-duration);
}
.dragging .icon.active:not(.hidden) {
z-index: 6;
opacity: 0.65;
}
.dragging .divider:not(.group),
.icon:not(.active),
.dropped {
transition: transform var(--transition-duration),
opacity var(--transition-duration);
}
.icon.active {
will-change: transform;
}
.edit-mode .icon .remove {
display: block;
position: absolute;
top: 0.1rem;
left: 0.5rem;
width: 2.6rem;
height: 2.6rem;
background: url(images/remove.png) no-repeat;
background-size: 2rem;
/*
Ensure that remove is on top of the icon and has some extra
padding so it is easy to click.
*/
z-index: 200;
padding: 0.5rem;
}
.icon .remove,
.icon.collapsed .remove,
.edit-mode .icon.active .remove {
display: none;
}
.edit-mode .icon.editable .title {
background-color: rgba(132, 132, 132, 0.7);
border-radius: 2rem;
margin-top: .4rem;
padding: 0 1rem;
width: auto;
max-width: 100%;
}
/*
Disable events for placeholders, so we don't trigger touch events in edit mode.
We only do this in edit-mode, otherwise we might trigger entering edit mode
instead of the context menu.
*/
.edit-mode .placeholder {
pointer-events: none;
}
#confirm-message {
z-index: 100;
}
.dragging .group.active {
z-index: 5;
}
.divider.group {
border: none;
padding: 0;
}
.group > span {
display: block;
position: absolute;
width: 100%;
transition: background-color var(--transition-duration),
opacity var(--transition-duration);
}
.group:not(.active):not(.newly-created) > span {
transition: background-color var(--transition-duration),
opacity var(--transition-duration),
transform var(--transition-duration);
}
.group.active > span {
will-change: transform;
}
.group .header {
height: 4rem;
z-index: 4;
}
.group .header span,
.group .header button {
position: absolute;
height: 4rem;
top: 0;
background-position: center top;
background-repeat: no-repeat;
background-size: 100% 2rem;
transition: transform 0.25s, opacity 0.25s;
}
.group .header .gripper {
left: calc(50% - 4.2rem);
width: 8.4rem;
background-image: url('images/move_gripper.png');
opacity: 0;
transform: translate(0, -100%);
pointer-events: none;
}
.edit-mode .group:not(.collapsed) .header .gripper {
opacity: 1;
transform: translate(0, 0);
pointer-events: auto;
}
.group .header .toggle {
width: 5.34rem;
right: 0;
border: none;
margin: 0;
padding: 0;
background-color: transparent;
background-image: url('images/collapse_arrow.png');
transform-origin: 50% 1.6rem;
pointer-events: auto;
}
.group .header .toggle.toggling {
opacity: 0.5;
transition: transform 0.25s;
}
.group.collapsed .header .toggle {
transform: translateY(2rem) rotateZ(180deg);
}
.group.active .header .toggle {
opacity: 0.5;
}
.group .background {
background-color: rgba(255, 255, 255, 0.15);
opacity: 0;
pointer-events: none;
height: 0.1rem;
transform-origin: 0 0;
}
.group.odd:not(.newly-created) .background,
.edit-mode .group:not(.newly-created) .background {
opacity: 1;
}
.group.active .background {
background-color: rgba(51, 51, 51, 0.9);
}
.group.hovering .background,
.group.drop-target .background {
background-color: rgba(27, 185, 225, 0.5);
}
.group .shadow {
background-color: rgba(255, 255, 255, 0.15);
opacity: 0;
pointer-events: none;
}
.edit-mode .group.active .shadow {
background-color: transparent;
box-shadow: 0 0.9rem 1.2rem rgba(0, 0, 0, 0.63);
opacity: 1;
}
.group .separator {
margin: 0;
height: 0.8rem;
pointer-events: none;
}
.group .separator > span {
display: block;
width: 100%;
height: 100%;
transition: transform 0.25s, opacity 0.25s;
opacity: 0;
}
.group.odd .separator {
opacity: 1;
}
.group.odd .separator > span {
background-color: rgba(255, 255, 255, 0.15);
opacity: 1;
}
.edit-mode .group .separator,
.edit-mode .group .separator > span {
opacity: 0;
}
/* For preventing entering edit mode when long-pressing on a placeholder */
.edit-mode .group .background,
.edit-mode .group .shadow,
.group.collapsed .header,
.group.collapsed .background,
.group.collapsed .shadow {
pointer-events: none;
}
.edit-mode .group.collapsed .background {
pointer-events: auto;
}
.dragging .group:not(.active) .separator {
opacity: 1;
}
.dragging .group:not(.active):not(.invalid-drop) .separator > span {
background-color: #0095dd;
transform: scaleY(0.325);
opacity: 0.4;
}
.dragging .group.hovered:not(.active) .separator > span {
transform: scaleY(1);
opacity: 1;
}
/* A hint element for dragging items to the top of the group */
.edit-mode::after {
content: ' ';
display: block;
width: 100%;
height: 0.8rem;
z-index: 2;
opacity: 0;
background-color: #0095dd;
position: relative;
top: -0.8rem;
transition: opacity 0.25s;
}
.edit-mode.hover-over-top::after {
opacity: 1;
}
/* Shift container downwards when hovering over the top to reveal the group
* creation hint.
*/
.edit-mode {
transition: transform 0.25s;
}
.edit-mode.hover-over-top {
transform: translateY(0.8rem);
}
/* RTL View */
:-moz-dir(rtl) .group .header .toggle {
left: 0;
right: unset;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

View file

@ -0,0 +1,62 @@
'use strict';
/* global ComponentUtils */
window.GaiaMenu = (function(win) {
// Extend from HTMLElement prototype
var proto = Object.create(HTMLElement.prototype);
// Allow baseurl to be overridden (used for demo page)
var baseurl = window.GaiaMenuBaseurl ||
'/shared/elements/gaia_menu/';
proto.createdCallback = function () {
var shadow = this.createShadowRoot();
this._template = template.content.cloneNode(true);
var cancelButton = this._template.querySelector('.gaia-menu-cancel button');
cancelButton.addEventListener('click', function () {
this.hide();
this.dispatchEvent(new CustomEvent('gaiamenu-cancel'));
}.bind(this));
shadow.appendChild(this._template);
ComponentUtils.style.call(this, baseurl);
document.l10n.ready.then(this.localize.bind(this));
document.addEventListener('DOMRetranslated', this.localize.bind(this));
};
proto.localize = function() {
document.l10n.formatValue('gaia-menu-cancel').then(value => {
this.shadowRoot.querySelector('button').textContent = value;
});
};
proto.show = function() {
this.removeAttribute('hidden');
};
proto.hide = function() {
this.setAttribute('hidden', 'hidden');
};
var template = document.createElement('template');
template.innerHTML =
`<form role="dialog" data-type="action">
<content select="header"></content>
<menu>
<content select="button"></content>
<span class="gaia-menu-cancel">
<button>Cancel</button>
</span>
</menu>
</form>`;
// Register and return the constructor
return document.registerElement('gaia-menu', { prototype: proto });
})(window);

View file

@ -0,0 +1,314 @@
/* ----------------------------------
* gaia_menu
* ---------------------------------- */
/* default alignment follows the UI direction, not the content direction */
:-moz-dir(ltr) [dir] { text-align: left; }
:-moz-dir(rtl) [dir] { text-align: right; }
/* Main dialog setup */
gaia-menu {
background-color: #2d2d2d;
overflow: hidden;
position: fixed;
z-index: 100;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 0 0 7rem;
/**
* Using "font-size: 0" to avoid the unwanted visual space (about 3px) created
* by white spaces and line breaks in the code between inline-block elements.
*/
font-size: 0;
color: #fff;
display: block;
transition: transform ease-in-out var(--transition-duration),
visibility var(--transition-duration);
}
gaia-menu[hidden] {
transform: translateY(100%);
visibility: hidden;
}
gaia-menu:before {
content: '';
display: inline-block;
vertical-align: top;
width: 0.1rem;
height: 100%;
margin-inline-start: -0.1rem;
}
header {
-moz-box-sizing: border-box;
font-size: 1.6rem;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
height: 5rem;
line-height: 4.9rem;
color: #fff;
white-space: nowrap;
text-overflow: ellipsis;
display: block;
overflow: hidden;
font-weight: normal;
padding: 0 3rem;
border-bottom: 0.1rem solid #616262;
}
header h1 {
font-size: 1.6rem;
line-height: 4.9rem;
color: #fff;
white-space: nowrap;
text-overflow: ellipsis;
display: block;
overflow: hidden;
margin: 0;
height: 100%;
font-weight: normal;
}
/* Generic set of actions in toolbar */
header menu.toolbar {
height: 100%;
margin: 0 -3rem;
margin-inline-start: 1rem;
padding: 0;
}
header:-moz-dir(ltr) menu.toolbar { float: right; }
header:-moz-dir(rtl) menu.toolbar { float: left; }
header menu.toolbar a,
header menu.toolbar button {
-moz-box-sizing: border-box;
position: relative;
width: auto;
min-width: 5rem;
height: 4.9rem;
margin-bottom: 0;
padding: 0 1rem;
border-radius: 0;
line-height: 4.9rem;
background: none;
text-align: center;
text-shadow: none;
z-index: 5;
}
header:-moz-dir(ltr) menu.toolbar a,
header:-moz-dir(ltr) menu.toolbar button {
float: left;
}
header:-moz-dir(rtl) menu.toolbar a,
header:-moz-dir(rtl) menu.toolbar button {
float: right;
}
header a,
header button {
border: none;
padding: 0;
overflow: hidden;
font-weight: 400;
font-size: 1.5rem;
line-height: 1.1em;
color: #fff;
border-radius: 0;
text-decoration: none;
}
button::-moz-focus-inner {
outline: none;
border: none;
}
/* Pressed state */
header a:active .icon:after,
header button:active .icon:after,
header menu.toolbar a:active,
header menu.toolbar button:active {
background: #008aaa;
}
/* Disabled state */
header menu.toolbar a[aria-disabled="true"],
header menu.toolbar button[disabled] {
opacity: 0.5;
pointer-events: none;
}
/* Icon definitions */
header .icon {
position: relative;
float: none;
display: inline-block;
vertical-align: top;
width: 4rem;
height: 4.9rem;
margin: 0 -1rem;
background: transparent no-repeat center center / 3rem auto;
font-size: 0;
overflow: hidden;
}
header .icon.icon-close {
background-image: url(images/icons/close.png);
}
header .icon.icon-back {
background-image: url(images/icons/back.png);
}
/* Navigation links (back, cancel, etc) */
header > button,
header > a {
width: 5rem;
height: 4.9rem;
background: none;
overflow: visible;
margin: 0 -1rem;
margin-inline-start: -3rem;
position: relative;
}
header:-moz-dir(ltr) > button,
header:-moz-dir(ltr) > a {
float: left;
}
header:-moz-dir(rtl) > button,
header:-moz-dir(rtl) > a {
float: right;
}
header > button::-moz-focus-inner {
border: 0;
padding: 0;
}
header > button .icon,
header > a .icon {
position: static;
display: block;
width: 3rem;
height: 4.9rem;
margin: 0;
overflow: visible;
font-size: 0;
}
header > button .icon:after,
header > a .icon:after {
content: "";
position: absolute;
offset-inline-start: 0;
top: 0;
z-index: -1;
width: 3rem;
height: 4.9rem;
}
/* Content */
section {
color: #fff;
position: absolute;
top: 7rem;
left: 1.5rem;
right: 1.5rem;
bottom: 2rem;
font-size: 1.6rem;
overflow: scroll;
}
/* Specific component code */
menu {
margin: 0;
padding: 0;
max-height: calc(100% - 13.5rem); /*header(5rem) + footer(7rem) + padding(1.5rem)*/
height: auto;
padding-top: 1.5rem;
overflow: auto;
width: auto;
border: none;
background: none;
position: absolute;
left: 0;
right: 0;
bottom: 7rem;
}
button {
width: calc(100% - 3rem);
height: 4rem;
-moz-box-sizing: border-box;
vertical-align: middle;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-weight: normal;
line-height: 4rem;
outline: none;
font-size: 1.6rem;
text-shadow: none;
padding: 0 1.2rem;
margin: 0 1.5rem 1rem;
background: rgba(87, 87, 87, 0.8);
border: none;
border-radius: 0;
position: relative;
color: #fff;
font-family: sans-serif;
font-style: italic;
}
button:-moz-dir(ltr) { text-align: left; }
button:-moz-dir(rtl) { text-align: right; }
span.gaia-menu-cancel {
position: fixed;
width: 100%;
background: #4d4d4d;
height: 7rem;
offset-inline-start: 0;
bottom: 0;
}
span.gaia-menu-cancel button {
position: fixed;
bottom: 0;
offset-inline-start: 0;
color: #333;
background: #d8d8d8;
text-align: center;
border: none;
margin-bottom: 1.5rem;
border-radius: 2rem;
}
button:nth-last-child(2) {
margin-bottom: 1.5rem;
}
/* Press state */
button:active,
span.gaia-menu-cancel button:active {
background: #00aacc;
color: #fff;
}
/* Disabled */
button[disabled] {
background-color: rgba(87, 87, 87, 0.8);
color: rgba(255, 255, 255, 0.4);
opacity: 0.5;
pointer-events: none;
}
button[disabled]:last-child {
background-color: #e7e7e7;
color: #a6a6a6;
opacity: 1;
}

View file

@ -0,0 +1,76 @@
'use strict';
/* global ComponentUtils */
window.GaiaOverflowMenu = (function(win) {
// Extend from HTMLElement prototype
var proto = Object.create(HTMLElement.prototype);
// Allow baseurl to be overridden (used for demo page)
var baseurl = window.GaiaOverflowMenuBaseurl ||
'/shared/elements/gaia_overflow_menu/';
proto.createdCallback = function() {
var shadow = this.createShadowRoot();
this._template = template.content.cloneNode(true);
shadow.appendChild(this._template);
ComponentUtils.style.call(this, baseurl);
this.addEventListener('click', this);
this.addEventListener('animationend', this);
this.addEventListener('transitionend', this);
this.classList.add('hidden');
};
proto.show = function() {
// XXX You can't show a hiding overflow menu. We may want to reconsider
// this behaviour at some point, or make it optional.
if (this.classList.contains('hidden')) {
this.classList.remove('hidden');
this.classList.add('showing');
}
};
proto.hide = function() {
// XXX You can't hide a showing overflow menu. We may want to reconsider
// this behaviour at some point, or make it optional.
if (!this.classList.contains('hidden') &&
!this.classList.contains('showing')) {
this.classList.add('hiding');
}
};
proto.handleEvent = function(evt) {
switch(evt.type) {
case 'click':
evt.preventDefault();
this.hide();
break;
case 'transitionend':
if (evt.target === this) {
if (this.classList.contains('hiding')) {
this.classList.remove('hiding');
this.classList.add('hidden');
}
}
break;
case 'animationend':
this.classList.remove('showing');
break;
}
};
var template = document.createElement('template');
template.innerHTML =
`<div>
<content select="button"></content>
</div>`;
// Register and return the constructor
return document.registerElement('gaia-overflow-menu', { prototype: proto });
})(window);

View file

@ -0,0 +1,103 @@
gaia-overflow-menu {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* Same as titlebar */
z-index: 1001;
padding: 11.4rem 3rem 0;
background-color: rgba(231, 231, 231, 0.6);
transition: opacity var(--transition-duration);
pointer-events: auto;
box-sizing: border-box;
}
gaia-overflow-menu.hidden {
display: none;
}
gaia-overflow-menu.showing {
background-color: transparent;
}
/* XXX Possibly a Gecko bug, if you don't specify this pseudo-element, the
* animation set on it below won't re-play subsequent times.
*/
gaia-overflow-menu::before {
content: '';
}
gaia-overflow-menu.showing::before {
position: absolute;
top: 0;
offset-inline-end: 0;
width: 100%;
height: 100%;
background-color: rgba(231, 231, 231, 0.6);
will-change: transform;
animation-name: grow;
animation-duration: var(--transition-duration);
}
:-moz-dir(ltr) gaia-overflow-menu.showing::before {
transform-origin: 100% 0;
border-radius: 0 0 0 100%;
}
:-moz-dir(rtl) gaia-overflow-menu.showing::before {
transform-origin: 0 0;
border-radius: 0 0 100% 0;
}
gaia-overflow-menu.hiding {
opacity: 0;
}
gaia-overflow-menu.showing > * {
opacity: 0;
}
gaia-overflow-menu button {
display: block;
width: 100%;
height: 5rem;
background-color: white;
border: 0.1rem solid rgba(189, 189, 189, 0.1);
border-bottom: none;
border-radius: 0;
box-sizing: border-box;
padding: 0;
background-position: 0.75rem center;
background-repeat: no-repeat;
font-size: 1.6rem;
font-style: italic;
font-weight: 500;
color: #00aac5;
white-space: nowrap;
text-overflow: ellipsis;
-moz-user-select: none;
transition: opacity 0.2s;
}
gaia-overflow-menu:-moz-dir(ltr) button {
padding-left: 6rem;
text-align: left;
}
gaia-overflow-menu:-moz-dir(rtl) button {
padding-right: 6rem;
text-align: right;
}
gaia-overflow-menu button:active {
background-color: #e7e7e7;
}
gaia-overflow-menu button:last-child {
border-bottom: 0.1rem solid rgba(189, 189, 189, 0.1);
}
gaia-overflow-menu button[hidden] {
display: none;
}

View file

@ -0,0 +1,17 @@
/**
Some css properties are global (keyframes) and cannot be specified
to to only apply to our shadow dom so we hack around this by including
this file without scoping it.
*/
/**
Bug 1018269 - we need to specify the keyframe outside
of the web component...
*/
@keyframes grow {
from { transform: scale(0, 0); }
to { transform: scale(2, 2); }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

View file

@ -0,0 +1,16 @@
;window.GaiaPinCard=(function(win){'use strict';var proto=Object.create(HTMLElement.prototype);var DEFAULT_COLOR=[77,77,77];var baseurl=window.GaiaPinCardBaseurl||'/shared/elements/gaia_pin_card/';proto.createdCallback=function(test){var shadow=this.createShadowRoot();this._template=template.content.cloneNode(true);this.container=this._template.querySelector('.pin-card');this.bgElement=this._template.querySelector('.background');this.descElement=this._template.querySelector('.description');this.titleElement=this._template.querySelector('header');this.iconElement=this._template.querySelector('i');this._background={};shadow.appendChild(this._template);ComponentUtils.style.call(this,baseurl);};Object.defineProperty(proto,'background',{get:function(){return this._background;},set:function(background){var bgSrc=background.src?'url('+background.src+')':'';this._background=background;this.bgElement.style.backgroundImage=bgSrc;this.bgElement.style.backgroundColor=background.themeColor||'#4d4d4d';var opacity=background.themeColor?'0.30':'0.15';var computedStyle=window.getComputedStyle(this.bgElement);var colorCodes;try{colorCodes=getColorCodes(computedStyle.backgroundColor);}catch(error){colorCodes=DEFAULT_COLOR;opacity=0.15;}
var rgbaColor=colorCodes.slice(1).join(',')+', '+opacity;var bgColorRgba='rgba('+rgbaColor+')';var computedWidth=computedStyle.width;var width=computedWidth==='auto'?'140px':computedWidth;var shadow='inset 0 0 0 '+width;this.bgElement.style.boxShadow=shadow+' '+bgColorRgba;}});Object.defineProperty(proto,'meta',{get:function(){return this._meta;},set:function(meta){this._meta=meta;this.renderCard();}});Object.defineProperty(proto,'title',{get:function(){return this.titleElement.textContent;},set:function(title){this.titleElement.textContent=title;}});Object.defineProperty(proto,'icon',{get:function(){return this.iconElement.style.backgroundImage;},set:function(icon){this.iconElement.style.backgroundImage=icon;}});proto.renderCard=function(){var description=getDescription(this._meta);if(description){this.descElement.appendChild(description);this.container.classList.remove('no-content');}
var background=getBackgroundBlob(this._meta);if(background){this.background={src:URL.createObjectURL(background),themeColor:this._meta['theme-color']||null};}};var template=document.createElement('template');template.innerHTML=`<article class="pin-card no-content">
<div class="icon-container">
<i></i>
</div>
<div class="background"></div>
<div class="content">
<header></header>
<section class="description"></section>
</div>
</article>`;function getColorCodes(color){var colorCodes=/rgb\((\d+), (\d+), (\d+)\)/.exec(color);return colorCodes;}
function getDescription(meta){if(!meta['og:description']){return false;}
var desc=document.createElement('p');desc.textContent=meta['og:description'];return desc;}
function getBackgroundBlob(meta){return meta['og:image']||meta.screenshot;}
return document.registerElement('gaia-pin-card',{prototype:proto});})(window);

View file

@ -0,0 +1,99 @@
.pin-card {
position: relative;
box-sizing: border-box;
background-color: #fff;
background-size: cover;
color: #000;
font-weight: 500;
font-family: sans-serif;
line-height: normal;
white-space: normal;
font-size: 1.2rem;
width: 100%;
height: 100%;
overflow: hidden;
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
}
.pin-card > * {
pointer-events: none;
}
.pin-card .background {
position: relative;
width: 100%;
height: 47%;
opacity: 0.7;
top: 0;
left: 0;
z-index: 0;
background-size: cover;
}
.pin-card i {
position: absolute;
top: .15rem;
left: .15rem;
width: 3.03rem;
height: 3.03rem;
background: url(images/default_icon.png);
background-size: 3.5rem 3.5rem;
background-position: center center;
border-radius: 50%;
}
.icon-container {
position: absolute;
top: 4.8rem;
display: block;
overflow: hidden;
left: calc(50% - 1.65rem);
width: 3.33rem;
height: 3.33rem;
border-radius: 50%;
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
background-color: white;
z-index: 1;
}
.pin-card .content {
position: relative;
height: 53%;
width: 100%;
}
.pin-card header {
position: relative;
height: 1.6rem;
color: #5f5f5f;
font-size: 1.4rem;
font-style: italic;
padding: 0 0.8rem;
overflow: hidden;
white-space: pre;
text-overflow: ellipsis;
top: 1.5rem;
}
.pin-card.no-content header {
top: 2.5rem;
}
.pin-card.no-content .description {
display: none;
}
.pin-card .description p {
position: relative;
display: block;
overflow: hidden;
text-align: left;
height: 3rem;
line-height: 1.4rem;
font-size: 1.2rem;
font-weight: 300;
color: #5f5f5f;
padding: 0 0.8rem;
margin: 2rem 0;
}

View file

@ -0,0 +1 @@
;'use strict';window.GaiaProgress=(function(win){var proto=Object.create(HTMLElement.prototype);var baseurl=window.GaiaProgressBaseurl||'/shared/elements/gaia_progress/';proto.createdCallback=function(){this.setAttribute('role','progressbar');this.setAttribute('aria-live','polite');var shadow=this.createShadowRoot();this._template=template.content.cloneNode(true);this._progress=this._template.getElementById('progress');shadow.appendChild(this._template);ComponentUtils.style.call(this,baseurl);if(this.hasAttribute('animated')){this.start();}};proto.start=function(){this.setAttribute('animated','');this._progress.classList.add('animated');this.setAttribute('data-l10n-id','gaia-progress-loading');};proto.stop=function(){this.removeAttribute('animated');this._progress.classList.remove('animated');this.setAttribute('data-l10n-id','gaia-progress-loaded');};var template=document.createElement('template');template.innerHTML='<div id="progress"></div>';return document.registerElement('gaia-progress',{prototype:proto});})(window);

View file

@ -0,0 +1,27 @@
gaia-progress {
display: block;
width: 100%;
overflow: hidden;
height: 0.4rem;
border: none;
position: relative;
}
gaia-progress:-moz-dir(rtl) {
transform: scaleX(-1);
}
#progress {
height: 0.4rem;
/* Since we animate, we need to extend the width to 100% + 1 full animation */
width: calc(100% + 15rem);
background: #00caf2 repeating-linear-gradient(135deg, #92F4FE, #92F4FE 5rem, #00CDF0 5rem, #00CDF0 10rem);
border: none;
position: absolute;
top: 0;
left: 0;
}
#progress.animated {
animation: moving-backward 1.5s steps(15) infinite;
}

View file

@ -0,0 +1,23 @@
/**
Some css properties are global (keyframes) and cannot be specified
to to only apply to our shadow dom so we hack around this by including
this file without scoping it.
Bug 1018269 - we need to specify the keyframe outside of the web component.
*/
/**
Moving backward because:
http://www.chrisharrison.net/projects/progressbars2/ProgressBarsHarrison.pdf
*/
@-moz-keyframes moving-backward {
0% {
transform: translateX(0);
}
100% {
/**
The width of the gradient is 10rem, but we adjust to 14.2 to account for
the gradient rotation. Without this adjustment the animation will jump.
*/
transform: translateX(-14.2rem);
}
}

View file

@ -0,0 +1 @@
;'use strict';window.GaiaSubheader=(function(win){var proto=Object.create(HTMLElement.prototype);var baseurl=window.GaiaSubheaderBaseurl||'/shared/elements/gaia_subheader/';proto.createdCallback=function(){ComponentUtils.style.call(this,baseurl);};return document.registerElement('gaia-subheader',{prototype:proto});})(window);

View file

@ -0,0 +1,40 @@
gaia-subheader[hidden] {
display: none;
}
/**
* Gaia Subheader
*
* Dividing header usually found
* heading and separating lists.
*/
gaia-subheader {
display: block;
padding: 0.8rem 3rem;
border-bottom: solid 0.1rem #e6e6e6;
color: #424242;
font-size: 1.5rem;
line-height: 1.8rem;
text-align: start;
}
/**
* [data-skin="organic"]
*/
gaia-subheader[skin="organic"] {
border: 0;
background-color: #e7e7e7;
color: #4d4d4d;
}
/**
* [data-skin="dark"]
*/
gaia-subheader[skin="dark"] {
border: 0;
background-color: #2c353b;
color: #fff;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

View file

@ -0,0 +1,151 @@
'use strict';
/* global ComponentUtils */
window.GaiaSwitch = (function(win) {
// Extend from the HTMLElement prototype
var proto = Object.create(HTMLElement.prototype);
// Allow baseurl to be overridden (used for demo page)
var baseurl = window.GaiaSwitchBaseurl ||
'/shared/elements/gaia_switch/';
proto.createdCallback = function() {
var shadow = this.createShadowRoot();
this._template = template.content.cloneNode(true);
this._input = this._template.querySelector('input[type="checkbox"]');
var checked = this.getAttribute('checked');
if (checked !== null) {
this._input.checked = true;
}
var wrapper = this._template.getElementById('switch');
wrapper.addEventListener('click', this.handleClick.bind(this));
shadow.appendChild(this._template);
ComponentUtils.style.call(this, baseurl);
// Proxy RTL changes to the shadow root so we can style for RTL.
var dirObserver = new MutationObserver(this.updateInternalDir.bind(this));
dirObserver.observe(document.documentElement, {
attributeFilter: ['dir'],
attributes: true
});
this.updateInternalDir();
};
proto.updateInternalDir = function() {
var internal = this.shadowRoot.firstElementChild;
if (document.documentElement.dir === 'rtl') {
internal.setAttribute('dir', 'rtl');
} else {
internal.removeAttribute('dir');
}
};
proto.handleClick = function(e) {
// If user clicked the (optional) link, do not toggle gaia-switch.
if (e && e.target.tagName === 'A') {
return;
}
e && e.preventDefault();
e && e.stopImmediatePropagation();
// Dispatch a click event.
var event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
this.dispatchEvent(event);
if (!event.defaultPrevented) {
this.checked = !this.checked;
}
// Dispatch a change event for the gaia-switch.
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
cancelable: false
}));
};
/**
* Allows users to simulate clicking through javascript.
*/
proto.click = function() {
this.handleClick();
};
/**
* Proxy the checked property to the input element.
*/
Object.defineProperty( proto, 'checked', {
get: function() {
return this._input.checked;
},
set: function(value) {
this._input.checked = value;
}
});
/**
* Disabled setter
*/
Object.defineProperty( proto, 'disabled', {
get: function() {
return this.hasAttribute('disabled');
},
set: function(value) {
if (value) {
this.setAttribute('disabled', true);
} else {
this.removeAttribute('disabled');
}
}
});
/**
* Proxy the name property to the input element.
*/
Object.defineProperty( proto, 'name', {
get: function() {
return this.getAttribute('name');
},
set: function(value) {
this.setAttribute('name', value);
}
});
/**
* Proxy the input type.
*/
Object.defineProperty( proto, 'type', {
get: function() {
return 'gaia-switch';
}
});
// HACK: Create a <template> in memory at runtime.
// When the custom-element is created we clone
// this template and inject into the shadow-root.
// Prior to this we would have had to copy/paste
// the template into the <head> of every app that
// wanted to use <gaia-switch>, this would make
// markup changes complicated, and could lead to
// things getting out of sync. This is a short-term
// hack until we can import entire custom-elements
// using HTML Imports (bug 877072).
var template = document.createElement('template');
template.innerHTML = `<span id="switch">
<input type="checkbox">
<span><content select="label"></content></span>
<div class="details"><content select="details"></content></div>
<content select="a"></content>
</span>`;
// Register and return the constructor
return document.registerElement('gaia-switch', { prototype: proto });
})(window);

View file

@ -0,0 +1,116 @@
gaia-switch {
display: inline-block;
width: 100%;
}
#switch {
display: inline-block;
vertical-align: middle;
width: 100%;
position: relative;
background: none;
}
#switch span {
font-size: 1.9rem;
color: var(--label-color);
padding: 1rem 0;
line-height: 3rem;
box-sizing: border-box;
display: block;
/* Override inheriting nowrap from legacy lists building blocks. */
white-space: normal;
}
#switch span:-moz-dir(ltr) { float: left; }
#switch span:-moz-dir(rtl) { float: right; }
#switch span:not(:empty) {
padding-inline-end: 6rem;
}
#switch input {
margin: 0;
opacity: 0;
position: absolute;
top: 0;
offset-inline-start: 0;
}
#switch input:checked ~ span::after {
background-position: center bottom;
}
/* ----------------------------------
* ON/OFF SWITCHES
* ---------------------------------- */
#switch input ~ span::after {
content: '';
position: absolute;
offset-inline-end: 0;
top: 2.5rem;
width: 6rem;
margin: -1.4rem 0 0;
height: 2.7rem;
border-radius: 1.35rem;
overflow: hidden;
background-color: #e6e6e6;
background-image: url(images/background_off.png);
background-repeat: no-repeat;
background-size: 9.2rem 2.7rem;
transition: background 0.2s ease;
}
#switch input ~ span:-moz-dir(ltr)::after { background-position: -3.2rem 0; }
#switch input ~ span:-moz-dir(rtl)::after { background-position: 0 0; }
/* switch: 'ON' state */
#switch input:checked ~ span:-moz-dir(ltr)::after {
background-image: url(images/background.png);
background-position: 0 0;
}
#switch input:checked ~ span:-moz-dir(rtl)::after {
background-image: url(images/background_rtl.png);
background-position: -3.2rem 0;
}
/* switch: disabled state */
#switch input:disabled ~ span::after {
opacity: 0.4;
}
#switch input.uninit ~ span::after {
transition: none;
}
/**
* A <details> slot may be specified in addition the the <label>.
* When provided, it appears under the <label> and can provide additional context.
*/
#switch div.details {
clear: both;
position: relative;
top: -1.5rem;
display: block;
margin: 0;
padding-top: 0.5rem;
padding-inline-start: 0;
padding-inline-end: 8rem;
font-size: 1.4rem;
line-height: 1.6rem;
color: #505859;
font-style: normal;
white-space: normal;
}
/**
* A <a> link slow may be specified in addition to the <label> and <details>.
* When provided, it will appear under the <details> section.
*/
gaia-switch a {
position: relative;
top: -0.8rem;
font-size: 1.7rem;
line-height: 2.4rem;
font-weight: 400;
color: #35679a;
}

View file

@ -0,0 +1,182 @@
/* Taken from https://github.com/fxos/achievements-service and in particular,
built from https://github.com/fxos/achievements-service/blob/master/
achievements-service.js */
(function(exports) {
'use strict';
var _toArray = function (arr) {
return Array.isArray(arr) ? arr : Array.from(arr);
};
/* global Notification, console */
var DEFAULT_IMAGE_SIZE = 64;
var MOZ_SETTINGS_NOT_AVAILABLE_MSG = 'navigator.mozSettings is not available';
var ImageHelper = (function () {
var ImageHelper = function ImageHelper() {};
ImageHelper.getImage = function (aSrc) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.src = aSrc;
image.onload = function () {
return resolve(image);
};
image.onerror = function (reason) {
return reject(reason);
};
})['catch'](function (reason) {
return console.warn('Could not load an achievement image:', reason);
});
};
ImageHelper.generateImageDataURL = function (aSrc) {
return ImageHelper.getImage(aSrc).then(function (image) {
try {
var canvas = document.createElement('canvas');
canvas.height = canvas.width = DEFAULT_IMAGE_SIZE;
var context = canvas.getContext('2d');
var dataUrl;
context.drawImage(image, 0, 0, DEFAULT_IMAGE_SIZE,
DEFAULT_IMAGE_SIZE);
dataUrl = canvas.toDataURL();
// Clean up.
canvas.width = canvas.height = 0;
canvas = null;
return dataUrl;
} catch (e) {
return Promise.reject('Could not convert image to Data URL.');
}
})['catch'](function (reason) {
return console.warn(reason);
});
};
return ImageHelper;
})();
var SettingsHelper = (function () {
var SettingsHelper = function SettingsHelper() {};
SettingsHelper.get = function (name, defaultValue) {
if (!name) {
return Promise.reject('Setting name is missing');
}
if (!navigator.mozSettings) {
console.warn(MOZ_SETTINGS_NOT_AVAILABLE_MSG);
return Promise.reject(MOZ_SETTINGS_NOT_AVAILABLE_MSG);
}
return new Promise(function (resolve, reject) {
var setting = navigator.mozSettings.createLock().get(name,
defaultValue);
setting.onsuccess = function () {
var settingValue = setting.result[name] || defaultValue;
resolve(settingValue);
};
setting.onerror = function () {
reject(setting.error);
};
});
};
SettingsHelper.set = function (settings) {
if (!settings) {
return Promise.reject('Settings are missing');
}
if (!navigator.mozSettings) {
console.warn(MOZ_SETTINGS_NOT_AVAILABLE_MSG);
return Promise.reject(MOZ_SETTINGS_NOT_AVAILABLE_MSG);
}
return new Promise(function (resolve, reject) {
var result = navigator.mozSettings.createLock().set(settings);
result.onsuccess = function () {
resolve(result.result);
};
result.onerror = function () {
reject(result.error);
};
});
};
return SettingsHelper;
})();
var AchievementsService = (function () {
var AchievementsService = function AchievementsService() {};
AchievementsService.prototype.reward = function (_ref) {
var criteria = _ref.criteria;
var evidence = _ref.evidence;
var name = _ref.name;
var description = _ref.description;
var image = _ref.image;
if (!evidence) {
return Promise.reject('Evidence is not provided.');
}
var issuedOn;
return SettingsHelper.get('achievements', []).then(
function (achievements) {
var achievement = achievements.find(function (achievement) {
return achievement.criteria === criteria;
});
if (!achievement) {
return Promise.reject('Achievement is not registered.');
}
if (achievement.evidence) {
return Promise.reject('Achievement is already awarded.');
}
achievement.evidence = evidence;
achievement.uid = 'achievement' +
Math.round(Math.random() * 100000000);
achievement.issuedOn = issuedOn = Date.now();
achievement.recipient = {}; // TODO
return achievements;
}).then(function (achievements) {
return Promise.all([ImageHelper.generateImageDataURL(image),
SettingsHelper.set({ achievements: achievements })]);
}).then(function (_ref2) {
var _ref3 = _toArray(_ref2);
var image = _ref3[0];
// Send a Notification via WebAPI to be handled by the Gaia::System
var notification = new Notification(name, {
body: description,
icon: image,
tag: issuedOn
});
notification.onclick = function () {
var activity = new window.MozActivity({
name: 'configure',
data: {
target: 'device',
section: 'achievements'
}
});
activity.onsuccess = activity.onerror = function () {
notification.close();
};
};
})['catch'](function (reason) {
return console.warn(reason);
});
};
return AchievementsService;
})();
exports.AchievementsService = AchievementsService;
})(window);

View file

@ -0,0 +1,141 @@
'use strict';
(function(exports) {
/**
* Match pattern is ported from
* https://mxr.mozilla.org/mozilla-central/source/toolkit/modules/addons
* /MatchPattern.jsm
* which in itself implements pattern matching based on Chrome Apps Platform
* API https://developer.chrome.com/extensions/match_patterns
*
* MatchPattern is used by the add-on manager to match add-ons to apps that
* they affect.
*/
const PERMITTED_SCHEMES = ['http', 'https', 'file', 'ftp', 'app'];
// This function converts a glob pattern (containing * and possibly ?
// as wildcards) to a regular expression.
function globToRegexp(pat, allowQuestion) {
// Escape everything except ? and *.
pat = pat.replace(/[.+^${}()|[\]\\]/g, '\\$&');
if (allowQuestion) {
pat = pat.replace(/\?/g, '.');
} else {
pat = pat.replace(/\?/g, '\\?');
}
pat = pat.replace(/\*/g, '.*');
return new RegExp('^' + pat + '$');
}
// These patterns follow the syntax in
// https://developer.chrome.com/extensions/match_patterns
function SingleMatchPattern(pat) {
if (pat == '<all_urls>') {
this.scheme = PERMITTED_SCHEMES;
this.host = '*';
this.path = new RegExp('.*');
} else if (!pat) {
this.scheme = [];
} else {
var re = new RegExp(
'^(http|https|file|ftp|app|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$');
var match = re.exec(pat);
if (!match) {
console.error(`Invalid match pattern: '${pat}'`);
this.scheme = [];
return;
}
if (match[1] == '*') {
this.scheme = ['http', 'https'];
} else {
this.scheme = [match[1]];
}
this.host = match[2];
this.path = globToRegexp(match[3], false);
// We allow the host to be empty for file URLs.
if (this.host === '' && this.scheme[0] !== 'file') {
console.error(`Invalid match pattern: '${pat}'`);
this.scheme = [];
return;
}
}
}
SingleMatchPattern.prototype = {
matches(uri, ignorePath = false) {
if (this.scheme.indexOf(uri.protocol.slice(0, -1)) == -1) {
return false;
}
// This code ignores the port, as Chrome does.
if (this.host == '*') {
// Don't check anything.
} else if (this.host[0] == '*') {
// It must be *.foo. We also need to allow foo by itself.
var suffix = this.host.substr(2);
if (uri.hostname != suffix && !uri.hostname.endsWith('.' + suffix)) {
return false;
}
} else {
if (this.host != uri.hostname) {
return false;
}
}
if (!ignorePath && !this.path.test(uri.pathname)) {
return false;
}
return true;
}
};
/**
* Match Pattern allows to match patterns based on set of URLs
* @class MatchPattern
* @returns {MatchPattern}
*/
function MatchPattern(pat) {
this.pat = pat;
if (!pat) {
this.matchers = [];
} else if (pat instanceof String || typeof(pat) === 'string') {
this.matchers = [new SingleMatchPattern(pat)];
} else {
this.matchers = Array.from(pat, p => new SingleMatchPattern(p));
}
}
MatchPattern.prototype = {
// |uri| should be an nsIURI.
matches(uri) {
for (var matcher of this.matchers) {
if (matcher.matches(uri)) {
return true;
}
}
return false;
},
matchesIgnoringPath(uri) {
for (var matcher of this.matchers) {
if (matcher.matches(uri, true)) {
return true;
}
}
return false;
},
serialize() {
return this.pat;
},
};
exports.MatchPattern = MatchPattern;
})(window);

View file

@ -0,0 +1,182 @@
/*
* This module is a helper for recording telemetry data in Gecko’s telemetry
* system. Currently, only data from certified apps will be recorded.
*
* There are three different types of metrics that can be used here:
* Counter: This is a simple counter that increments by 1 each time it's
* called.
*
* Linear: This type of metric creates a linear distributed set of buckets
* which range between the min and max. You can specify the number
* of buckets as well as the min and max.
*
* Exponential: This type of metric creates an exponential distributed set of
* buckets ranging between min and max. You can specify the min,
* max, and buckets.
*
* App Setup:
* 1) Add the following to the app (e.g. index.html):
* <script defer src="/shared/js/advanced_telemetry_helper.js"></script>
* <script defer src="/shared/js/settings_listener.js"></script>
* 2) Include AdvancedTelemetryHelper in the globals.
*
* Counter Metric Usage Example:
* // This creates the histogram counter on this metric called
* // 'mycount'
* var count = new AdvancedTelemetryHelper(ATH.HISTOGRAM_COUNT, 'mycount');
* count.add(); //Increment the count. The count of the histogram is now 1.
* // DO SOMETHING
* count.add(); //Increment the count. The count of the histogram is now 2.
*
* Linear Metric Usage Example:
* // This creates a linear histogram called 'mylinear' with minimum
* // of 1 (0 not allowed as a minimum), max of 1000, with 10 buckets.
* var linear = new AdvancedTelemetryHelper(ATH.HISTOGRAM_LINEAR, 'mylinear',
* 1, 1000, 10);
* // This adds the value 15 to the histogram 'mylinear'
* linear.add(15);
* // DO SOMETHING
* // This adds to the existing histogram a value of 800.
* linear.add(800); // The histogram now has two values, one in the 15 bucket
* // and one in the bucket that holds the value 800
*
* Exponential Metric Usage Example:
* // This creates an exponential histogram called 'myexp' with minimum
* // of 1 (0 not allowed as a minimum), max of 1000, with 10 buckets.
* var exp = new AdvancedTelemetryHelper(ATH.HISTOGRAM_EXPONENTIAL, 'myexp',
* 1, 1000, 10);
* // This adds the value 15 to the histogram 'myexp'
* exp.add(15);
* // DO SOMETHING
* // This adds to the existing histogram a value of 800.
* exp.add(800); // The histogram now has two values, one in the 15 bucket
* // and one in the bucket that holds the value 800
*
* Caveats:
* Note that after a histogram is created, it's not possible to change the type
* of the histogram from one type to the other. It's necessary to create a new
* Histogram.
*
* Each histogram should have a unique name.
*
*/
/* globals SettingsListener */
(function(exports) {
'use strict';
const APP_TELEMETRY_LOG_PREFIX = 'telemetry';
const ATH = AdvancedTelemetryHelper;
// Export Histogram types so they can be used by apps
ATH.HISTOGRAM_COUNT = 4;
ATH.HISTOGRAM_EXP = 0;
ATH.HISTOGRAM_LINEAR = 1;
// This is exposed for unit testing purposes
ATH.TELEMETRY_LEVEL_KEY = 'metrics.selectedMetrics.level';
ATH.SETTINGS_INITIALIZED = false;
// API for an exponential histogram entry. This function creates the
// histogram in the system and prepares it subsequent .log(value) calls
// against it.
// <type>: One of ATH.HISTOGRAM_COUNT, ATH.HISTOGRAM_EXP,
// or ATH.HISTOGRAM_LINEAR.
// <name>: Unique name of the metric
// <min>: [Required for linear and exponential only]The minimum (non-zero)
// numeric value for this histogram.
// <max>: [Required for linear and exponential only]The maximum numeric
// value for this histogram.
// <buckets>: [Required for linear and exponential only]The total number of
// buckets for this histogram.
function AdvancedTelemetryHelper(type, name, min, max, buckets) {
if (!ATH.SETTINGS_INITIALIZED) {
ATH.SETTINGS_INITIALIZED = true;
ATH.init();
}
if (typeof type !== 'undefined' && typeof name !== 'undefined') {
if (type === ATH.HISTOGRAM_COUNT) {
this.createHistogram(name, ATH.HISTOGRAM_COUNT, 0, 0, 0);
} else if (type === ATH.HISTOGRAM_EXP || type === ATH.HISTOGRAM_LINEAR) {
this.createHistogram(name, type, min, max, buckets);
} else {
console.warn('Must pass a Histogram type and a unique Histogram name.');
}
} else {
console.warn('Must pass a Histogram type and a unique Histogram name.');
}
}
AdvancedTelemetryHelper.init = function init() {
ATH.telemetryEnabledListener = function telemetryEnabledListener(enabled) {
if (enabled === 'Enhanced') {
ATH.TELEMETRY_LEVEL = true;
} else {
ATH.TELEMETRY_LEVEL = false;
}
}.bind(this);
SettingsListener.observe(ATH.TELEMETRY_LEVEL_KEY,
false, ATH.telemetryEnabledListener);
};
AdvancedTelemetryHelper.prototype.createHistogram =
function(name, type, min, max, buckets) {
if (typeof ATH.TELEMETRY_LEVEL === 'undefined' || ATH.TELEMETRY_LEVEL) {
if (typeof this.metricType !== 'undefined') {
console.warn('Cannot redefine histogram. Must create a new object');
return;
}
this.metricType = type;
if (typeof name !== 'undefined') {
var varCount = 2;
var re = /_/g;
var message = APP_TELEMETRY_LOG_PREFIX;
// Disallow underscores so telemetry system can parse it.
this.metricName = name.replace(re, '-');
message += '|' + this.metricName;
message += '|' + type + '|';
if (typeof min !== 'undefined') { varCount++; }
if (typeof max !== 'undefined') { varCount++; }
if (typeof buckets !== 'undefined') { varCount++; }
if (varCount != 5) {
console.warn('createHistogram must have 5 params.');
return;
}
if (min < 1) {
min = 1;
}
message += min + '|' + max + '|' + buckets;
console.info(message);
} else {
console.warn('A telemetry log entry requires a name.');
}
}
};
// API for logging against a pre-existing histogram. This records the value
// for this particular histogram in the histogram system.
// <value>: The value to record against this histogram.
// For a counter histogram, this can be empty or 1 can be passed.
// For an exponential or linear histogram a valid value > 0 must be passed.
AdvancedTelemetryHelper.prototype.add = function(value) {
if (typeof ATH.TELEMETRY_LEVEL === 'undefined' || ATH.TELEMETRY_LEVEL) {
if (this.metricType == ATH.HISTOGRAM_COUNT) {
value = 1;
}
if (typeof value === 'undefined' || !value || value < 1) {
console.warn('A valid value is required to log a histogram.');
return;
}
var message = APP_TELEMETRY_LOG_PREFIX + '|' + this.metricName +
'|' + value;
console.info(message);
}
};
exports.AdvancedTelemetryHelper = AdvancedTelemetryHelper;
}(window));

View file

@ -0,0 +1,56 @@
/* exported advanced_timer */
'use strict';
var advanced_timer = {
/**
* Maps between user Ids and navigator ones
*/
timers: {},
/**
* Register a new timer with the user's timerId
*/
start: function(timerId, timeout, callback) {
if (typeof(callback) != 'function') {
callback = function() {};
}
var self = this;
var _id = setTimeout(function advTimer() {
var realUsedTime = new Date().getTime() - self.timers[timerId].timestamp;
delete(self.timers[timerId]);
callback(realUsedTime);
}, timeout);
this.timers[timerId] = {
'timeout': timeout,
'internalTimerId': _id,
'timestamp': new Date().getTime()
};
},
/**
* Stops timer and returns the pending time
*/
stop: function(timerId) {
var timer = this.timers[timerId];
if (!timer) {
return 0;
}
clearTimeout(timer.internalTimerId);
var pendingTime = this.queryPendingTime();
delete(this.timers[timerId]);
return pendingTime;
},
/**
* Returns the pending time to timeout the timer
*/
queryPendingTime: function(timerId) {
var timer = this.timers[timerId];
if (!timer) {
return 0;
}
return timer.timeout - (new Date().getTime() - timer.timestamp);
}
};

View file

@ -0,0 +1,84 @@
/**
* @fileoverview This file provides common helper functions used to manipulate
* access point names (APNs)
*/
/* exported ApnHelper */
(function(exports) {
'use strict';
/**
* List of data network types, see source/dom/system/gonk/ril_consts.js in
* mozilla-central. Those match the equivalent Android integer types.
*/
var NETWORK_TYPES = [
null,
'gprs',
'edge',
'umts',
'is95a',
'is95b',
'1xrtt',
'evdo0',
'evdoa',
'hsdpa',
'hsupa',
'hspa',
'evdob',
'ehrpd',
'lte',
'hspa+',
'gsm'
];
/**
* Filter a list of APNs by network type. This involves comparing the bearer
* field if present to the data network type and checking if they match. If
* the bearer value of an APN is not defined or is set to 0 then we consider
* the APN compatible with any network.
*
* @param {Array} apns An array of APNs
* @param {String} type The network type which the APNs must be compatible
* with
*/
function ah_filterByBearer(apns, type) {
var typeIdx = NETWORK_TYPES.indexOf(type);
for (var i = 0; i < apns.length; i++) {
var bearer = apns[i].bearer ? +apns[i].bearer : 0;
if (bearer && (bearer !== typeIdx)) {
// This APN is incompatible, remove it from the array
apns.splice(i, 1);
}
}
}
/**
* Get an APN from the specified list which is compatible with the specified
* MCC, MNC and connection type
*
* @param {Object} list The global APN list, the format should be the same as
* the one used in apn.json
* @param {Integer} mcc The mobile country code
* @param {Integer} mnc The mobile network code
* @param {String} type The network type which the filtered APNs must be
* compatible with
*
* @return {Array} A list of compatible APNs
*/
function ah_getCompatible(list, mcc, mnc, type) {
var apns = list[mcc] ? (list[mcc][mnc] || []) : [];
ah_filterByBearer(apns, type);
return apns;
}
var ApnHelper = {
getCompatible: ah_getCompatible
};
exports.ApnHelper = ApnHelper;
})(this);

View file

@ -0,0 +1,61 @@
'use strict';
(function(exports) {
var AsyncSemaphore = function as_contructor() {
if (!(this instanceof AsyncSemaphore)) {
return new AsyncSemaphore();
}
this._init();
};
// Do not call '_init' and '_execute' directly from outside of AsyncSemaphore
AsyncSemaphore.prototype = {
_init: function as_init() {
// Please don't modify these value directly from outside of AsyncSemaphore
this.pendingTasks = [];
this.semaphore = 0;
},
_execute: function as_execute() {
var task,
context;
if (this.semaphore <= 0) {
while (this.pendingTasks.length > 0) {
task = this.pendingTasks.shift();
context = task.context || this;
if (task && task.callback && typeof task.callback === 'function') {
task.callback.call(context, task.args);
}
}
}
},
v: function as_v(num) {
if (!num) {
num = 1;
}
this.semaphore += num;
},
p: function as_p() {
this.semaphore -= 1;
if (this.semaphore < 0) {
this.semaphore = 0;
}
this._execute();
},
wait: function as_wait(callback, context, args) {
this.pendingTasks.push({
callback: callback,
context: context,
args: args
});
this._execute();
},
getValue: function as_getValue() {
return this.semaphore;
},
getTasksLength: function as_getTasksLength() {
return this.pendingTasks.length;
}
};
exports.AsyncSemaphore = AsyncSemaphore;
}(window));

View file

@ -0,0 +1,180 @@
/* globals indexedDB */
/**
* This file defines an asynchronous version of the localStorage API, backed by
* an IndexedDB database. It creates a global asyncStorage object that has
* methods like the localStorage object.
*
* To store a value use setItem:
*
* asyncStorage.setItem('key', 'value');
*
* If you want confirmation that the value has been stored, pass a callback
* function as the third argument:
*
* asyncStorage.setItem('key', 'newvalue', function() {
* console.log('new value stored');
* });
*
* To read a value, call getItem(), but note that you must supply a callback
* function that the value will be passed to asynchronously:
*
* asyncStorage.getItem('key', function(value) {
* console.log('The value of key is:', value);
* });
*
* Note that unlike localStorage, asyncStorage does not allow you to store and
* retrieve values by setting and querying properties directly. You cannot just
* write asyncStorage.key; you have to explicitly call setItem() or getItem().
*
* removeItem(), clear(), length(), and key() are like the same-named methods of
* localStorage, but, like getItem() and setItem() they take a callback
* argument.
*
* The asynchronous nature of getItem() makes it tricky to retrieve multiple
* values. But unlike localStorage, asyncStorage does not require the values you
* store to be strings. So if you need to save multiple values and want to
* retrieve them together, in a single asynchronous operation, just group the
* values into a single object. The properties of this object may not include
* DOM elements, but they may include things like Blobs and typed arrays.
*
* Unit tests are in apps/gallery/test/unit/asyncStorage_test.js
*/
this.asyncStorage = (function() {
'use strict';
var DBNAME = 'asyncStorage';
var DBVERSION = 1;
var STORENAME = 'keyvaluepairs';
var db = null;
function withDatabase(f) {
if (db) {
f();
} else {
var openreq = indexedDB.open(DBNAME, DBVERSION);
openreq.onerror = function withStoreOnError() {
console.error('asyncStorage: can\'t open database:',
openreq.error.name);
};
openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
// First time setup: create an empty object store
openreq.result.createObjectStore(STORENAME);
};
openreq.onsuccess = function withStoreOnSuccess() {
db = openreq.result;
f();
};
}
}
function withStore(type, callback, oncomplete) {
withDatabase(function() {
var transaction = db.transaction(STORENAME, type);
if (oncomplete) {
transaction.oncomplete = oncomplete;
}
callback(transaction.objectStore(STORENAME));
});
}
function getItem(key, callback) {
var req;
withStore('readonly', function getItemBody(store) {
req = store.get(key);
req.onerror = function getItemOnError() {
console.error('Error in asyncStorage.getItem(): ', req.error.name);
};
}, function onComplete() {
var value = req.result;
if (value === undefined) {
value = null;
}
callback(value);
});
}
function setItem(key, value, callback) {
withStore('readwrite', function setItemBody(store) {
var req = store.put(value, key);
req.onerror = function setItemOnError() {
console.error('Error in asyncStorage.setItem(): ', req.error.name);
};
}, callback);
}
function removeItem(key, callback) {
withStore('readwrite', function removeItemBody(store) {
var req = store.delete(key);
req.onerror = function removeItemOnError() {
console.error('Error in asyncStorage.removeItem(): ', req.error.name);
};
}, callback);
}
function clear(callback) {
withStore('readwrite', function clearBody(store) {
var req = store.clear();
req.onerror = function clearOnError() {
console.error('Error in asyncStorage.clear(): ', req.error.name);
};
}, callback);
}
function length(callback) {
var req;
withStore('readonly', function lengthBody(store) {
req = store.count();
req.onerror = function lengthOnError() {
console.error('Error in asyncStorage.length(): ', req.error.name);
};
}, function onComplete() {
callback(req.result);
});
}
function key(n, callback) {
if (n < 0) {
callback(null);
return;
}
var req;
withStore('readonly', function keyBody(store) {
var advanced = false;
req = store.openCursor();
req.onsuccess = function keyOnSuccess() {
var cursor = req.result;
if (!cursor) {
// this means there weren't enough keys
return;
}
if (n === 0 || advanced) {
// Either 1) we have the first key, return it if that's what they
// wanted, or 2) we've got the nth key.
return;
}
// Otherwise, ask the cursor to skip ahead n records
advanced = true;
cursor.advance(n);
};
req.onerror = function keyOnError() {
console.error('Error in asyncStorage.key(): ', req.error.name);
};
}, function onComplete() {
var cursor = req.result;
callback(cursor ? cursor.key : null);
});
}
return {
getItem: getItem,
setItem: setItem,
removeItem: removeItem,
clear: clear,
length: length,
key: key
};
}());

View file

@ -0,0 +1,137 @@
/* global Promise */
/* exported Basket */
'use strict';
// Simple Javascript client for Mozilla Basket newsletters
var Basket = {
DATASTORE_NAME: 'Basket_Newsletter',
basketUrl: 'https://basket.mozilla.org/news/subscribe/',
newsletterId: 'firefox-os',
callback: null,
xhr: null,
// https://github.com/mozilla/basket-client/blob/master/basket/errors.py
errors: {
NETWORK_FAILURE: 1,
INVALID_EMAIL: 2,
UNKNOWN_EMAIL: 3,
UNKNOWN_TOKEN: 4,
USAGE_ERROR: 5,
EMAIL_PROVIDER_AUTH_FAILURE: 6,
AUTH_ERROR: 7,
SSL_REQUIRED: 8,
INVALID_NEWSLETTER: 9,
INVALID_LANGUAGE: 10,
EMAIL_NOT_CHANGED: 11,
CHANGE_REQUEST_NOT_FOUND: 12,
UNKNOWN_ERROR: 99
},
responseHandler: function b_responseHandler() {
if (this.xhr.readyState === 4) {
if (this.xhr.status === 200) {
if (typeof this.callback === 'function') {
this.callback(null, JSON.parse(this.xhr.responseText));
}
} else {
if (typeof this.callback === 'function') {
if (this.xhr.responseText) {
this.callback(JSON.parse(this.xhr.responseText));
} else {
this.callback('Unknown error');
}
}
}
}
},
timeoutHandler: function b_timeoutHandler() {
this.callback('Timeout');
},
/**
* Send data to Mozilla Basket.
*
* @param {String} [email] email we want to add to the newsletter.
*
* @param {Function} [callback] first argument is error, second
* is result of operation or null
* in the error case.
*/
send: function b_send(email, callback) {
var self = this;
var params = 'email=' + encodeURIComponent(email) +
'&newsletters=' + self.newsletterId;
self.callback = callback;
self.xhr = new XMLHttpRequest({mozSystem: true});
self.xhr.onreadystatechange = self.responseHandler.bind(self);
self.xhr.open('POST', self.basketUrl, true);
self.xhr.setRequestHeader('Content-type',
'application/x-www-form-urlencoded');
self.xhr.setRequestHeader('Connection', 'close');
self.xhr.timeout = 5000; // 5 seconds
self.xhr.ontimeout = self.timeoutHandler.bind(self);
self.getLanguage(function do_send(language) {
if (language) {
params += '&lang=' + language;
}
self.xhr.setRequestHeader('Content-length', params.length);
self.xhr.send(params);
});
},
getDataStore: function() {
var self = this;
return new Promise(function (resolve, reject) {
navigator.getDataStores(self.DATASTORE_NAME).then(function(stores) {
// check it's not empty
if (!stores.length) {
reject(new Error('DataStore not loaded'));
} else {
var dataStore = stores[0];
// check ownership of the DS
if (dataStore.owner === 'app://ftu.gaiamobile.org/manifest.webapp') {
resolve(dataStore);
} else {
reject(new Error('Wrong DataStore'));
}
}
});
});
},
store: function b_store(email, callback) {
this.getDataStore().then(function loaded(store) {
// Store the email address
store.add({ 'newsletter_email': email }).then(function(id) {
if (callback) {
callback(false, id);
}
});
}).catch(function error(err) {
console.error('Something went wrong: ' + err);
callback(err);
});
},
getLanguage: function b_getLanguage(callback) {
var settings = window.navigator.mozSettings;
if (!settings || !settings.createLock) {
callback();
return;
}
var s_name = 'language.current';
var req = settings.createLock().get(s_name);
req.onsuccess = function _onsuccess() {
callback(req.result[s_name]);
};
req.onerror = function _onerror() {
console.error('Error getting setting: ' + s_name);
callback();
};
}
};

View file

@ -0,0 +1,287 @@
'use strict';
/* global Promise */
(function(exports) {
var datastore;
// Datastore name declared on the manifest.webapp
var DATASTORE_NAME = 'bookmarks_store';
// Indicates the initialization state
var readyState;
// Event listeners
var listeners = Object.create(null);
function init() {
return new Promise(function doInit(resolve, reject) {
if (readyState === 'initialized') {
resolve();
return;
}
if (readyState === 'initializing') {
document.addEventListener('ds-initialized', function oninitalized() {
document.removeEventListener('ds-initialized', oninitalized);
resolve();
});
return;
}
readyState = 'initializing';
if (!navigator.getDataStores) {
console.error('Bookmark store: DataStore API is not working');
reject({ name: 'NO_DATASTORE' });
readyState = 'failed';
return;
}
navigator.getDataStores(DATASTORE_NAME).then(function(ds) {
if (ds.length < 1) {
console.error('Bookmark store: Cannot get access to the Store');
reject({ name: 'NO_ACCESS_TO_DATASTORE' });
readyState = 'failed';
return;
}
datastore = ds[0];
datastore.addEventListener('change', onchangeHandler);
readyState = 'initialized';
document.dispatchEvent(new CustomEvent('ds-initialized'));
resolve();
}, reject);
});
}
function doGetAll(resolve, reject) {
var result = Object.create(null);
var cursor = datastore.sync();
function cursorResolve(task) {
switch (task.operation) {
case 'update':
case 'add':
result[task.data.id] = task.data;
break;
case 'remove':
delete result[task.data.id];
break;
case 'clear':
result = Object.create(null);
break;
case 'done':
resolve(result);
return;
}
cursor.next().then(cursorResolve, reject);
}
cursor.next().then(cursorResolve, reject);
}
function get(id) {
return new Promise(function doGet(resolve, reject) {
init().then(function onInitialized() {
datastore.get(id).then(resolve, reject);
}, reject);
});
}
function getAll() {
return new Promise(function doGet(resolve, reject) {
init().then(doGetAll.bind(null, resolve, reject), reject);
});
}
function onchangeHandler(event) {
var operation = event.operation;
var callbacks = listeners[operation];
callbacks && callbacks.forEach(function iterCallback(callback) {
datastore.get(event.id).then(function got(result) {
callback.method.call(callback.context || this, {
type: operation,
target: result || event
});
});
});
}
function addEventListener(type, callback) {
var context;
if (!(type in listeners)) {
listeners[type] = [];
}
var cb = callback;
if (typeof cb === 'object') {
context = cb;
cb = cb.handleEvent;
}
if (cb) {
listeners[type].push({
method: cb,
context: context
});
init();
}
}
function removeEventListener(type, callback) {
if (!(type in listeners)) {
return false;
}
var callbacks = listeners[type];
var length = callbacks.length;
for (var i = 0; i < length; i++) {
var thisCallback = callback;
if (typeof thisCallback === 'object') {
thisCallback = callback.handleEvent;
}
if (callbacks[i] && callbacks[i].method === thisCallback) {
callbacks.splice(i, 1);
return true;
}
}
return false;
}
function add(data) {
return new Promise(function doAdd(resolve, reject) {
init().then(function onInitialized() {
var id = data.url;
Object.defineProperty(data, 'id', {
enumerable: true,
configurable: false,
writable: false,
value: id
});
datastore.add(data, id).then(function add_success() {
resolve(true); // Bookmark was added
}, function add_error() {
datastore.put(data, id).then(function put_success() {
resolve(); // Bookmark was updated
}, reject);
});
}, reject);
});
}
function getRevisionId() {
return new Promise(function doGet(resolve, reject) {
init().then(function onInitialized() {
resolve(datastore.revisionId);
}, reject);
});
}
function put(data) {
return new Promise(function doAdd(resolve, reject) {
init().then(function onInitialized() {
datastore.put(data, data.id).then(function success() {
resolve(); // Bookmark was updated
}, reject);
}, reject);
});
}
function remove(id) {
return new Promise(function doRemove(resolve, reject) {
init().then(function onInitialized() {
datastore.remove(id).then(resolve, reject);
}, reject);
});
}
function clear() {
return new Promise(function doClear(resolve, reject) {
init().then(function onInitialized() {
datastore.clear().then(resolve, reject);
}, reject);
});
}
exports.BookmarksDatabase = {
/*
* This method returns a bookmark object
*
* @param{String} String param that represents an identifier
*/
get: get,
/*
* This method returns an object of bookmarks indexed by id
*/
getAll: getAll,
/*
* Returns the latest revision UUID
*/
getRevisionId: getRevisionId,
/*
* Method registers the specified listener on the API
*
* @param{String} A string representing the event type to listen for
*
* @param{Function} The method that receives a notification when an event of
* the specified type occurs
*
*/
addEventListener: addEventListener,
/*
* Method removes the specified listener on the API
*
* @param{String} A string representing the event type to listen for
*
* @param{Function} The method that received a notification when an event of
* the specified type occurs
*
*/
removeEventListener: removeEventListener,
/*
* This method adds a bookmark in the datastore
*
* @param{Object} The bookmark's data
*/
add: add,
/*
* This method updates a bookmark in the datastore
*
* @param{Object} The bookmark's data
*/
put: put,
/*
* This method removes a bookmark from the datastore
*
* @param{String} The bookmark's id
*/
remove: remove,
/*
*
* This method clears the entire datastore, removing all entries.
*
*/
clear: clear
};
}(window));

View file

@ -0,0 +1,2 @@
;(function(exports){'use strict';exports.ComponentUtils={style:function(baseUrl){if(window.VulpesCompat){window.VulpesCompat.componentStyle(this,baseUrl).catch(console.error);return;}var style=document.createElement('style');var url=baseUrl+'style.css';var self=this;style.setAttribute('scoped','');style.innerHTML='@import url('+url+');';this.appendChild(style);this.style.visibility='hidden';style.addEventListener('load',function(){if(self.shadowRoot){self.shadowRoot.appendChild(style.cloneNode(true));}
self.style.visibility='';});}};}(window));

View file

@ -0,0 +1,195 @@
/* exported CustomDialog */
//XXX: Waiting for the window.showModalDialog support in B2G
'use strict';
var CustomDialog = (function() {
var container = null;
var screen = null;
var dialog = null;
var header = null;
var message = null;
var yes = null;
var no = null;
return {
hide: function dialog_hide() {
if (screen === null) {
return;
}
if (!container) {
container = document.body;
}
container.removeChild(screen);
screen = null;
dialog = null;
header = null;
message = null;
yes = null;
no = null;
},
/**
* Method that shows the dialog
* @param {Object} title the l10n id for the title of the dialog. null or
* empty for no title. or you can give a object with more
* options like {icon: path or empty string,
* id: an l10n-id, args: an object of arguments that
* parameterise the 'id' field}.
* @param {String} msg message for the dialog. give a object like the
* title to enable more options.
* @param {Object} cancel {title, callback} object when confirm.
* @param {Object} confirm {title, callback} object when cancel.
* @param {Element} [containerElement] an optional element for which the
* dialog will be injected
*/
show: function dialog_show(title, msg, cancel, confirm, containerElement) {
container = containerElement || document.body;
if (screen === null) {
screen = document.createElement('form');
screen.setAttribute('role', 'dialog');
screen.setAttribute('data-type', 'confirm');
screen.id = 'dialog-screen';
dialog = document.createElement('section');
screen.appendChild(dialog);
// Create a reusable function to decorate elements with all
// possible options, instead of scattering similar code about
// everywhere.
//
// It's also possible to be extended with more usable decorating
// options and elements.
//
// 'title'|'message' -> Object|String -> the element -> dialog
// -> the decorated element
var decorateWithOptions = function cd_decorateWithOptions(type, options,
elm, dialog) {
if ('string' === typeof options) {
elm.setAttribute('data-l10n-id', options);
return elm;
}
var icon = options.icon;
var textElm = elm;
if (icon && '' !== icon) {
// We can't localize elements with child nodes
// so if there's going to be an icon, we need to create a separate
// node for text. See bug 1053629 for details
textElm = document.createElement('span');
var iconImg = new Image();
iconImg.src = icon;
iconImg.classList.add('custom-dialog-' + type + '-icon');
// Icons usually insert as the first element.
elm.insertBefore(iconImg, elm.firstChild);
elm.appendChild(textElm);
}
// More decorating options goes here.
if (options.id) {
document.l10n.setAttributes(
textElm,
options.id,
options.args
);
} else {
var text = options[type];
textElm.textContent = text;
}
return elm;
};
var setElementText = function (element, options) {
if ('string' === typeof options) {
element.setAttribute('data-l10n-id', options);
}
if(options.id) {
document.l10n.setAttributes(element, options.id, options.args);
}
};
header = document.createElement('h1');
header.id = 'dialog-title';
if (title && title !== '') {
header = decorateWithOptions('title', title, header, dialog);
}
dialog.appendChild(header);
message = document.createElement('p');
message.id = 'dialog-message';
message = decorateWithOptions('message', msg, message, dialog);
dialog.appendChild(message);
var menu = document.createElement('menu');
menu.dataset.items = 1;
no = document.createElement('button');
// The default type of the button element is "Submit",
// and form submit in system app would make system app reload.
no.type = 'button';
setElementText(no, cancel.title);
no.id = 'dialog-no';
no.addEventListener('click', clickHandler);
menu.appendChild(no);
if (confirm) {
menu.dataset.items = 2;
yes = document.createElement('button');
// The default type of button element is "Submit",
// and form submit in system app would make system app reload.
yes.type = 'button';
setElementText(yes, confirm.title);
yes.id = 'dialog-yes';
//confirm can be with class "danger" or "recommend"
//the default is "danger"
yes.className = confirm.recommend ? 'recommend' : 'danger';
yes.addEventListener('click', clickHandler);
menu.appendChild(yes);
}
else { // 1 button, should be full.
no.classList.add('full');
}
screen.appendChild(menu);
container.appendChild(screen);
}
// Make the screen visible
screen.classList.add('visible');
// This is the event listener function for the buttons
function clickHandler(evt) {
// Hide the dialog
screen.classList.remove('visible');
// Call the appropriate callback, if it is defined
if (evt.target === yes && confirm.callback) {
confirm.callback();
} else if (evt.target === no && cancel.callback) {
cancel.callback();
}
}
return screen;
}
};
}());

View file

@ -0,0 +1,49 @@
/**
* Shim for navigator.mozHour12 API.
* Send `localechanged` event if mozHour12 is changed.
*
* App need include following permission in manifest:
*
* "settings":{ "access": "readonly" }
*/
(function(){
'use strict';
// not polyfill if API already exists
if (window.navigator.mozHour12 || window.navigator.hour12) {return;}
// mock mozHour12 onto window.navigator
window.navigator.mozHour12 = undefined;
// set hour12 and emit the locale change event if value changed
var _setMozHour12 = function(result) {
if (result === null) {
// locale.hour12 may be true, false, `undefined` or `null`.
// We can't write to mozSettings with `undefined`, but we want to cast
// null to be undefined because then we can easily assign
// Intl hour12: navigator.mozHour12 which is a paradigm we want to use.
//
// if you set hour12 to undefined it uses automatic value, but it doesn't
// work if you set it to `null`, so we cast here to `undefined`.
result = undefined;
}
if (window.navigator.mozHour12 !== result) {
window.navigator.mozHour12 = result;
// emit the locale change event
window.dispatchEvent(new CustomEvent('timeformatchange'));
}
};
// handler observer event
var _hour12Handler = function(event) {
_setMozHour12(event.settingValue);
};
var _kLocaleTime = 'locale.hour12';
// update mozHour12 to real value
var req = window.navigator.mozSettings.createLock().get(_kLocaleTime);
req.onsuccess = function() {
_setMozHour12(req.result[_kLocaleTime]);
};
// monitor settings changes
window.navigator.mozSettings.addObserver(_kLocaleTime, _hour12Handler);
})();

View file

@ -0,0 +1,191 @@
/* global SettingsHelper */
/* exported DefaultActivityHelper */
(function(exports) {
'use strict';
// To add an element on the list, follow the example:
// {
// name: 'pick',
// type: ['image/jpeg',
// 'image/png',
// 'image/gif',
// 'image/bmp'],
// l10nId: 'default-activity-pickimage',
// settingsId: 'activity.default.pickimage'
// },
//
// NOTE: due to specs (bug1039386), some actions are grouped so when an app
// is set as default for any of the actions, the rest (from the same group)
// will be assigned the same app as default launch too.
// Grouped actions:
//
// new "webcontacts/contact"
// open "webcontacts/contact"
// update "webcontacts/contact"
//
// new "mail"
// view “url” (email)
//
// open "video"
// view “video”
//
var supportedActivities = [
{
name: 'dial',
type: ['webtelephony/number'],
l10nId: 'default-activity-dialnumber',
settingsId: 'activity.default.dialnumber'
},
{
name: 'new',
type: ['webcontacts/contact'],
l10nId: 'default-activity-opencontact',
settingsId: 'activity.default.opencontact'
// same as open contact, see previous note
},
{
name: 'new',
type: ['mail'],
l10nId: 'default-activity-newmail',
settingsId: 'activity.default.newmail'
},
{
name: 'new',
type: ['websms/sms'],
l10nId: 'default-activity-newsms',
settingsId: 'activity.default.newsms'
},
{
name: 'open',
type: ['webcontacts/contact'],
l10nId: 'default-activity-opencontact',
settingsId: 'activity.default.opencontact'
},
{
name: 'open',
type: ['image/jpeg',
'image/png',
'image/gif',
'image/bmp'],
l10nId: 'default-activity-openimage',
settingsId: 'activity.default.openimage'
},
{
name: 'open',
type: ['audio/mpeg',
'audio/ogg',
'audio/mp4'],
l10nId: 'default-activity-openaudio',
settingsId: 'activity.default.openaudio'
},
{
name: 'open',
type: ['video/webm',
'video/mp4',
'video/3gpp',
'video/youtube'],
l10nId: 'default-activity-openvideo',
settingsId: 'activity.default.openvideo'
},
{
name: 'record',
type: ['photos'],
l10nId: 'default-activity-recordphoto',
settingsId: 'activity.default.recordphoto'
},
{
name: 'record',
type: ['videos'],
l10nId: 'default-activity-recordvideo',
settingsId: 'activity.default.recordvideo'
},
{
name: 'view',
type: ['url'],
l10nId: 'default-activity-viewurl',
settingsId: 'activity.default.viewurl'
},
{
name: 'view',
type: ['application/pdf'],
l10nId: 'default-activity-viewpdf',
settingsId: 'activity.default.viewpdf'
},
{
name: 'view',
type: ['video/webm',
'video/mp4',
'video/3gpp',
'video/youtube'],
l10nId: 'default-activity-openvideo',
settingsId: 'activity.default.openvideo'
// same as open video, see previous note
},
{
name: 'update',
type: ['webcontacts/contact'],
l10nId: 'default-activity-opencontact',
settingsId: 'activity.default.opencontact'
// same as open contact, see previous note
}
];
var DefaultActivityHelper = {
_helper: null,
getHelper: function(config) {
this._helper = this._helper || SettingsHelper(config.settingsId, null);
return this._helper;
},
/**
* Looks for an activity on the list given the name and the type
* @parameter {String} name Activity name
* @parameter {String} type Activity type
* @returns {Object} Config object for the activity
*/
getDefaultConfig: function(name, type) {
var config = supportedActivities.find(function(a) {
return a.name === name && a.type.indexOf(type) >= 0;
});
return config;
},
/**
* Returns the current choice as default for a given activity name and type
* @param {String} name Activity name
* @param {String} type Activity type
* @param {Function} cb callback executed when search finished
*/
getDefaultAction: function(name, type) {
var config = this.getDefaultConfig(name, type);
return new Promise((resolve, reject) => {
if (!config) {
resolve(null);
}
this.getHelper(config).get((value) => {
resolve(value);
});
});
},
/**
* Sets a preference with the default choice for a given activity and type
* @param {String} name Activity name
* @param {String} type Activity type
* @param {String} choice App manifest of the chosen app
*/
setDefaultAction: function(name, type, choice) {
var config = this.getDefaultConfig(name, type);
if (!config) {
return;
}
this.getHelper(config).set(choice);
}
};
exports.DefaultActivityHelper = DefaultActivityHelper;
})(window);

View file

@ -0,0 +1,84 @@
/* global mozIntl */
/**
* This lib relies on `l10n.js' to implement localizable date/time strings.
*
* The proposed `DownloadFormatter' object provides features for formatting
* the data retrieved from the new API for Downloads, taking into account
* the structure defined by the API itself.
*/
(function(exports) {
'use strict';
var NUMBER_OF_DECIMALS = 2;
var BYTE_SCALE = ['B', 'KB', 'MB', 'GB', 'TB'];
function _getFormattedSize(bytes) {
if (bytes === undefined || isNaN(bytes)) {
return Promise.resolve(null);
}
var index = 0;
while (bytes >= 1024 && index < BYTE_SCALE.length) {
bytes /= 1024;
++index;
}
return document.l10n.
formatValue('byteUnit-' + BYTE_SCALE[index]).then(unit => {
return document.l10n.formatValue('fileSize', {
size: bytes.toFixed(NUMBER_OF_DECIMALS),
unit: unit
});
});
}
function _calcPercentage(currently, total) {
if (total === 0) {
return 0;
}
return parseInt((100 * currently) / total);
}
var DownloadFormatter = {
getFormattedSize: function(bytes) {
return _getFormattedSize(bytes);
},
getPercentage: function(download) {
return _calcPercentage(download.currentBytes, download.totalBytes);
},
getFileName: function(download) {
return download.path.split('/').pop(); // filename.ext
},
getTotalSize: function(download) {
var bytes = download.totalBytes;
return _getFormattedSize(bytes);
},
getDownloadedSize: function(download) {
var bytes = download.currentBytes;
return _getFormattedSize(bytes);
},
getDate: function(download) {
var date;
try {
date = download.startTime;
} catch (ex) {
date = new Date();
console.error(ex);
}
var formatter = mozIntl._gaia.RelativeDate(navigator.languages);
return formatter.format(date);
},
getUUID: function(download) {
return download.id || this.getFileName(download);
}
};
exports.DownloadFormatter = DownloadFormatter;
}(this));

View file

@ -0,0 +1,685 @@
'use strict';
/* exported DownloadHelper */
/* global DownloadFormatter */
/* global DownloadStore */
/* global DownloadUI */
/* global LazyLoader */
/* global MimeMapper */
/* global MozActivity */
/* global SettingsListener */
/*
* DownloadHelper.js: Perform some utility functions with DOMDownload
* objects.
*
* - You have to include in you HTML:
*
* <!-- <script src="shared/js/mime_mapper.js"></script> -->
* <!-- <script src="shared/js/download/download_store.js"></script> -->
* <script defer src="shared/js/lazy_loader.js"></script>
*
* - How to use this component.
*
* For launching a download
*
* var req = DownloadHelper.open(download);
*
* req.onsuccess = function req_onsuccess() {
* alert('The download was opened so we can remove the notification');
* }
*
* req.onerror = function req_onerror() {
* alert('Something wrong!: ' + req.error.message);
* }
*
*/
var DownloadHelper = (function() {
// Exception code constants
var CODE = {
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
DEVICE_STORAGE: 'DEVICE_STORAGE',
MIME_TYPE_NOT_SUPPORTED: 'MIME_TYPE_NOT_SUPPORTED',
INVALID_STATE: 'INVALID_STATE',
NO_SDCARD: 'NO_SDCARD',
NO_PROVIDER: 'NO_PROVIDER',
UNMOUNTED_SDCARD: 'UNMOUNTED_SDCARD'
};
/*
* Request auxiliary object to support asynchronous calls
*/
var Request = function() {
this.done = function(result) {
if (typeof this.onsuccess === 'function') {
this.result = result;
window.setTimeout(function() {
this.onsuccess({
target: this
});
}.bind(this), 0);
}
};
this.failed = function(error) {
if (typeof this.onerror === 'function') {
this.error = error;
window.setTimeout(function() {
this.onerror({
target: this
});
}.bind(this), 0);
}
};
};
// Storage name by default
var STORAGE_NAME_BY_DEFAULT = 'sdcard';
// Storage name settings key
var STORAGE_NAME_KEY = 'device.storage.writable.name';
// Current storage name
var storageName = STORAGE_NAME_BY_DEFAULT;
LazyLoader.load('shared/js/settings_listener.js', function settingsLoaded() {
SettingsListener.observe(STORAGE_NAME_KEY, STORAGE_NAME_BY_DEFAULT,
function setStorageName(evt) {
var settingValue = evt.settingValue;
if (settingValue) {
storageName = settingValue;
}
}
);
});
/*
* Error auxiliary method
*/
function sendError(req, message, code) {
req.failed({
message: message,
code: code
});
console.error(message);
}
/*
* Returns a blob from the sdcard
*
* @param{Object} The download object
*
*/
function getBlob(download) {
var req = new Request();
var storage = getVolume(download.storageName);
var storeAvailableReq = storage.available();
storeAvailableReq.onsuccess = function available_onsuccess(e) {
var path = download.storagePath;
switch (storeAvailableReq.result) {
case 'unavailable':
sendError(req, ' Could not open the file: ' + path + ' from ' +
download.storageName, CODE.NO_SDCARD);
break;
case 'shared':
sendError(req, ' Could not open the file: ' + path + ' from ' +
download.storageName, CODE.UNMOUNTED_SDCARD);
break;
default:
try {
var storeGetReq = storage.get(path);
storeGetReq.onsuccess = function store_onsuccess() {
req.done(storeGetReq.result);
};
storeGetReq.onerror = function store_onerror() {
sendError(req, storeGetReq.error.name +
' Could not open the file: ' + path + ' from ' +
download.storageName, CODE.FILE_NOT_FOUND);
};
} catch (ex) {
sendError(req, 'Error getting the file ' + path + ' from ' +
download.storageName, CODE.DEVICE_STORAGE);
}
}
};
storeAvailableReq.onerror = function available_onerror() {
sendError(req, 'Error getting storage state ', CODE.DEVICE_STORAGE);
};
return req;
}
/*
* Base action that implements the commom logic for actions based on the
* MozActivity API
*
* @param{Object} Configuration parameters
*/
var Action = function Action(params) {
this.req = params.request;
this.name = params.actionType.activityName;
};
Action.prototype = {
/*
* It performs the action based on MozActivity
*/
run: function a_run() {
var activity = new MozActivity({
name: this.name,
data: this.data
});
activity.onsuccess = this._onsuccess.bind(this);
activity.onerror = this._onerror.bind(this);
},
/*
* This method implements the generic onsuccess callback
*/
_onsuccess: function a_onsuccess() {
this.req.done();
},
/*
* This method implements the generic onerror callback
*/
_onerror: function a_onerror(evt) {
this.req.failed({
message: evt.target.error.name
});
}
};
/*
* This action opens downloads extending the Action Object
*
* @param{Object} Configuration parameters
*/
var OpenAction = function OpenAction(params) {
Action.call(this, params);
this.data = {
url: params.download.path,
filename: DownloadFormatter.getFileName(params.download),
type: params.type,
blob: params.blob
};
};
OpenAction.prototype = {
__proto__: Action.prototype
};
/*
* This action shares downloads extending the Action Object
*
* @param{Object} Configuration parameters
*/
var ShareAction = function ShareAction(params) {
Action.call(this, params);
this.data = {
// 'share' activities do not work with specific mime types
type: params.type.split('/')[0] + '/*',
blobs: [params.blob],
filenames: [DownloadFormatter.getFileName(params.download)]
};
};
ShareAction.prototype = {
__proto__: Action.prototype,
/*
* It overrides the generic onerror callback for another more suitable
*/
_onerror: function sa_onerror(evt) {
if (evt.target.error.name !== 'NO_PROVIDER') {
return;
}
sendError(this.req, 'No provider to share file', CODE.NO_PROVIDER);
}
};
/*
* This action gets the info for a download
*
* @param{Object} Configuration parameters
*/
var InfoAction = function InfoAction(params) {
Action.call(this, params);
this.data = {
name: DownloadFormatter.getFileName(params.download),
type: params.type,
blob: params.blob,
size: params.download.totalBytes,
path: params.download.path
};
};
InfoAction.prototype = {
__proto__: Action.prototype,
/*
* It overrides the generic run method
*/
run: function ia_run() {
this.req.done(this.data);
}
};
// This is a factory that deals with different <Action> objects
var ActionsFactory = {
TYPE: {
OPEN: {
activityName: 'open',
actionClass: OpenAction
},
SHARE: {
activityName: 'share',
actionClass: ShareAction
},
INFO: {
actionClass: InfoAction
},
WALLPAPER: {
activityName: 'setwallpaper',
actionClass: ShareAction
},
RINGTONE: {
activityName: 'setringtone',
actionClass: ShareAction
}
},
create: function af_create(params) {
return new params.actionType.actionClass(params);
}
};
/*
* Returns the mime type
*
* @param{Object} It represents a DOMDownload object
*
* @returns(String) Mime type
*/
function getType(download) {
var fileName = DownloadFormatter.getFileName(download);
var type = MimeMapper.guessTypeFromFileProperties(fileName,
download.contentType);
return type;
}
/*
* This method allows third-parties to open or share downloads
*
* @param{Object} Action types: <ActionsFactory.TYPE.OPEN> or
* <ActionsFactory.TYPE.SHARE>
*
* @param{Object} It represents a DOMDownload object
*/
function runAction(actionType, download) {
var req = new Request();
window.setTimeout(function launching() {
var state = download.state;
if (state === 'succeeded' || state === 'finalized') {
LazyLoader.load(['shared/js/mime_mapper.js',
'shared/js/download/download_formatter.js'],
function loaded() {
var type = getType(download);
//
// The 'open' action will always launch an activity using the original
// content type to allow for third party applications to handle
// arbitrary types of content.
//
// The 'share' action on the other hand only works with known mime
// types at this time.
//
if (type.length === 0) {
type = download.contentType;
if (actionType !== ActionsFactory.TYPE.OPEN &&
actionType !== ActionsFactory.TYPE.INFO) {
sendError(req, 'Mime type not supported: ' + type,
CODE.MIME_TYPE_NOT_SUPPORTED);
return;
}
}
var blobReq = getBlob(download);
blobReq.onsuccess = function() {
ActionsFactory.create({
actionType: actionType,
download: download,
type: type,
blob: blobReq.result,
request: req
}).run();
};
blobReq.onerror = function() {
// Problem getting the blob from the sdcard
req.failed(blobReq.error);
};
});
return;
}
sendError(req, 'Becareful, the download is not finished!',
CODE.INVALID_STATE);
}, 0);
return req;
}
/*
* This method allows clients to remove a download, from the
* list and the phone.
*
* @param{Object} It represents a DOMDownload object
*/
function remove(download) {
var req = new Request();
var incompleteDownload = download.state !== 'succeeded';
// If is not done, use download manager to remove it,
// otherwise, deal with the datastore.
setTimeout(function() {
if (incompleteDownload) {
if (!navigator.mozDownloadManager) {
sendError(req, 'DownloadManager not present', CODE.INVALID_STATE);
} else {
// First we pause the download so that everyone knows it's being
// stopped. The Downloads API itself won't stop the download first,
// it will simply kill it.
// XXXAus: Remove when we fix bug #1090551
download.pause().then(
function() {
navigator.mozDownloadManager.remove(download).then(
function success() {
req.done(download);
},
function error() {
sendError(req,
'DownloadManager doesnt know about this download',
CODE.INVALID_STATE);
}
);
},
function() {
sendError(req,
'Failed to pause download before removal',
CODE.INVALID_STATE);
}
);
}
} else {
req.done(download);
}
}, 0);
return incompleteDownload ? req : doRemoveFromPhone(req, download);
}
/*
* Performs the proper delete of the physical file, also
* from the datastore if the download has finished.
*
* @param{Object} This is the Request object
* @param{Object} It represents a DOMDownload object
*/
function doRemoveFromPhone(deleteRequest, download) {
var req = new Request();
deleteRequest.onsuccess = function() {
var storage = getVolume(download.storageName);
var storeAvailableReq = storage.available();
storeAvailableReq.onsuccess = function available_onsuccess(e) {
var path = download.path;
switch (storeAvailableReq.result) {
case 'unavailable':
sendError(req, ' Could not delete the file: ' + path + ' from ' +
storageName, CODE.NO_SDCARD);
break;
case 'shared':
sendError(req, ' Could not delete the file: ' + path + ' from ' +
storageName, CODE.UNMOUNTED_SDCARD);
break;
default:
var storeDeleteReq = storage.delete(download.storagePath);
storeDeleteReq.onsuccess = function store_onsuccess() {
// Remove from the datastore if status is 'succeeded'
// if we find any problem with the datastore, don't send
// an error, since the physical remove already happened
if (download.state === 'succeeded') {
LazyLoader.load(['/shared/js/download/download_store.js'],
function() {
DownloadStore.remove(download);
}
);
}
req.done(storeDeleteReq.result);
};
storeDeleteReq.onerror = function store_onerror() {
sendError(req, storeDeleteReq.error.name +
' Could not remove the file: ' + download.path + ' from ' +
storageName, CODE.FILE_NOT_FOUND);
};
}
};
storeAvailableReq.onerror = function available_onerror() {
sendError(req, 'Error getting storage state ', CODE.DEVICE_STORAGE);
};
};
deleteRequest.onerror = function(error) {
sendError(req, error.message, error.code);
};
return req;
}
function handlerError(error, download, cb) {
LazyLoader.load('shared/js/download/download_ui.js', (function loaded() {
var req;
var show = DownloadUI.show;
// Canceled activites are normal and shouldn't be interpreted as errors.
// Unfortunately, this isn't reported in a standard way by our
// applications (or third party apps for that matter). This is why we
// have this lazy filter here that may need to be updated in the future
// but hopefully will just get removed.
if (error.message &&
(error.message.endsWith('canceled') ||
error.message.endsWith('cancelled'))) {
// Since this isn't actually an error, we invoke the callback with null.
cb && cb(null);
// and return early.
return;
}
switch (error.code) {
case CODE.NO_SDCARD:
case CODE.UNMOUNTED_SDCARD:
case CODE.FILE_NOT_FOUND:
case CODE.NO_PROVIDER:
req = show(DownloadUI.TYPE[error.code], download, true);
req.onconfirm = cb;
break;
case CODE.MIME_TYPE_NOT_SUPPORTED:
req = show(DownloadUI.TYPE.UNSUPPORTED_FILE_TYPE, download, true);
req.onconfirm = function tuftOnConfirm() {
showRemoveFileUI(download, cb);
};
break;
default:
req = show(DownloadUI.TYPE.FILE_OPEN_ERROR, download, true);
req.onconfirm = function tfoeOnConfirm() {
showRemoveFileUI(download, cb);
};
break;
}
// We have to remove the notification if the user cancels
req.oncancel = cb;
}));
}
function showRemoveFileUI(download, cb) {
var req = DownloadUI.show(DownloadUI.TYPE.DELETE, download, true);
req.oncancel = cb;
req.onconfirm = function doRemove() {
if (typeof cb === 'function') {
cb(download);
} else {
remove(download);
}
};
}
function getFreeSpace(cb) {
var storage = navigator.getDeviceStorage(storageName);
if (!storage) {
console.error('Cannot get free space size in sdcard');
cb(null);
return;
}
var req = storage.freeSpace();
req.onsuccess = function(e) {
cb(e.target.result);
};
req.onerror = function() {
cb(null);
};
}
/**
* Gets the storage for the download based on the volumen
* it was saved (storageName)
*/
function getVolume(volumeName) {
// Per API design, all media type return the same volumes.
// So we use 'sdcard' here for no reason.
// https://bugzilla.mozilla.org/show_bug.cgi?id=856782#c10
var volumes = navigator.getDeviceStorages('sdcard');
if (!volumeName || volumeName === '') {
return volumes[0];
}
for (var i = 0; i < volumes.length; ++i) {
if (volumes[i].storageName === volumeName) {
return volumes[i];
}
}
return volumes[0];
}
return {
/*
* This method allows clients to open a downlaod
*
* @param{Object} It represents a DOMDownload object
*/
open: function(download) {
return runAction(ActionsFactory.TYPE.OPEN, download);
},
/*
* This method allows clients to share a downlaoded file
*
* @param{Object} It represents a DOMDownload object
*/
share: function(download) {
return runAction(ActionsFactory.TYPE.SHARE, download);
},
/*
* This method allows clients to set as wallaper a downlaoded file
*
* @param{Object} It represents a DOMDownload object
*/
wallpaper: function(download) {
return runAction(ActionsFactory.TYPE.WALLPAPER, download);
},
/*
* This method allows clients to set as ringtone a downlaoded file
*
* @param{Object} It represents a DOMDownload object
*/
ringtone: function(download) {
return runAction(ActionsFactory.TYPE.RINGTONE, download);
},
/*
* This method returns information about a download
*
* @param{Object} It represents a DOMDownload object
*/
info: function(download) {
return runAction(ActionsFactory.TYPE.INFO, download);
},
/*
* Given a download, remove it from the DownloadManager
* list, and the file system.
*
* @param{Object} It represents a DOMDownload object
*/
remove: remove,
/*
* Returns exception code constants
*/
get CODE() {
return CODE;
},
/*
* This method handles different errors when users attemp to open files
*
* @param{Object} Error object
*
* @param{Object} It represents a DOMDownload object
*
* @param{Function} This function is performed when the flow is finished
*/
handlerError: handlerError,
/*
* Returns the free memory size in bytes
*
* @param{Function} This function is performed when the free memory size has
* been calculated
*/
getFreeSpace: getFreeSpace
};
}());

View file

@ -0,0 +1,451 @@
'use strict';
/* jshint moz:true */
/* global Promise */
/* global indexedDB */
/*
* DownloadStore.js: Allows to store download finished ordered by timestamp
*
* - Adding a new download:
*
* var req = DownloadStore.add(download);
*
* req.onsuccess = function() {
* console.log('Complete download stored successfully');
* };
*
* req.onerror = function(e) {
* console.error(e);
* };
*
* - Getting all downloads:
*
* var reqStore = DownloadStore.getAll();
*
* reqStore.onsuccess = function(ev) {
* ev.target.result.forEach(function(download) {
* // Do something cool with the download
* });
* };
*
* reqStore.onerror = function(e) {
* console.error(e);
* };
*
* - Removing a existing download:
*
* var req = DownloadStore.remove(download);
*
* req.onsuccess = function() {
* console.log('Deleted download successfully');
* };
*
* req.onerror = function(e) {
* console.error(e);
* };
*
* WARNING:
*
* The system app should add this to its manifest file
*
* "datastores-owned": {
* "download_store": {
* "description": "Stores download finished"
* }
* }
*
* The settings app should add this to its manifest file
*
* "datastores-access": {
* "download_store": {
* "description": "Stores download finished"
* }
* }
*
*/
/* exported DownloadStore */
var DownloadStore = (function() {
var datastore;
// Datastore name declared on the manifest.webapp
const DATASTORE_NAME = 'download_store';
var downloadListDB;
var downloadList = [];
var lastRevisionId;
const LIST_DB = 'download_list';
const LIST_STORE = 'download_list';
const LIST_KEY = 0;
const REVISION_KEY = 1;
/*
* Request auxiliary object to support asynchronous calls
*/
var Request = function() {
this.done = function(result) {
if (typeof this.onsuccess === 'function') {
this.result = result;
window.setTimeout(function() {
this.onsuccess({
target: this
});
}.bind(this), 0);
}
};
this.failed = function(error) {
if (typeof this.onerror === 'function') {
this.error = error;
window.setTimeout(function() {
this.onerror({
target: this
});
}.bind(this), 0);
}
};
};
function promisify(request) {
return new Promise(function(resolve, reject) {
request.onsuccess = (event) => {resolve(event.target.result);};
request.onerror = (event) => {reject(event.target.errorCode);};
});
}
// Indicates the initialization state
var readyState;
function init(success, fail) {
if (readyState === 'initialized') {
success();
return;
}
if (readyState === 'initializing') {
document.addEventListener('ds-initialized', function oninitalized() {
document.removeEventListener('ds-initialized', oninitalized);
success();
});
return;
}
readyState = 'initializing';
if (!navigator.getDataStores) {
var messageError = 'Data store: DataStore API is not working';
console.error(messageError);
fail({
message: messageError
});
return;
}
navigator.getDataStores(DATASTORE_NAME).then(function(ds) {
var messageError = 'Download Store: Cannot get access to the DataStore';
if (ds.length < 1) {
console.error(messageError);
throw ({
message: messageError
});
}
datastore = ds[0];
// Checking the length as the index should be there
}).then(function() {
return openDownloadListIndexedDB();
}).then(function(db) {
downloadListDB = db;
var listStore = downloadListDB.transaction(LIST_STORE, 'readonly')
.objectStore(LIST_STORE);
return Promise.all([
promisify(listStore.get(LIST_KEY))
.then((value) => {downloadList = value;}),
promisify(listStore.get(REVISION_KEY))
.then((value) => {lastRevisionId = value;})
]);
}).then(function() {
readyState = 'initialized';
// Remove the legacy index element, if it exists. See Bug 1180250.
const LEGACY_INDEX_ID = 1;
if (downloadList && downloadList.includes(LEGACY_INDEX_ID)) {
return promisify(get(LEGACY_INDEX_ID)).then((download) => {
if ('byTimestamp' in download) {
downloadList.splice(downloadList.indexOf(LEGACY_INDEX_ID), 1);
}
});
}
}).then(function() {
notifyOpenSuccess(success);
}).catch(function(e) {
console.error('Error while opening the Download Store: ', e.message);
fail(e);
});
}
function openDownloadListIndexedDB() {
return new Promise(function(resolve, reject) {
var request = indexedDB.open(LIST_DB);
request.onsuccess = function(event) {
var db = event.target.result;
resolve(db);
};
request.onerror = function(event) {
reject(event.target.errorCode);
};
request.onupgradeneeded = function(event) {
var db = event.target.result;
promisify(db.createObjectStore(LIST_STORE)).then(() => {
var listStore = db.transaction(LIST_STORE, 'readwrite')
.objectStore(LIST_STORE);
return Promise.all([
promisify(listStore.add([], LIST_KEY)),
promisify(listStore.add(0, REVISION_KEY))
]);
}).catch(reject);
};
request.onblocked = function(event) {
reject(event.target.errorCode);
};
});
}
function notifyOpenSuccess(cb) {
window.setTimeout(cb, 0);
document.dispatchEvent(new CustomEvent('ds-initialized'));
}
// These fields will be stored in our datastore
var fieldsToPropagate = ['url', 'path', 'totalBytes', 'contentType',
'startTime', 'state', 'storageName', 'storagePath'];
function cookDownload(download) {
var ret = Object.create(null);
fieldsToPropagate.forEach(function(field) {
ret[field] = download[field];
});
// This is the timestamp when the download finished
ret.finalizeTime = new Date();
return ret;
}
function defaultError(req) {
return defaultErrorCb.bind(null, req);
}
function defaultErrorCb(request, error) {
request.failed(error);
}
function defaultSuccess(req) {
return defaultSuccessCb.bind(null, req);
}
function defaultSuccessCb(request, result) {
request.done(result);
}
function updateDownloadList() {
var cursor = datastore.sync(lastRevisionId);
function cursorResolve(task) {
switch (task.operation) {
case 'done':
lastRevisionId = task.revisionId;
var listStore = downloadListDB.transaction(LIST_STORE, 'readwrite')
.objectStore(LIST_STORE);
return Promise.all([
promisify(listStore.put(downloadList, LIST_KEY)),
promisify(listStore.put(lastRevisionId, REVISION_KEY))
]);
case 'clear':
downloadList = [];
break;
case 'add':
downloadList.push(task.id);
break;
case 'update':
// This doesn't matter for us.
break;
case 'remove':
var i = downloadList.indexOf(task.id);
if (i >= 0) {
downloadList.splice(i, 1);
}
break;
}
return cursor.next().then(cursorResolve);
}
return cursor.next().then(cursorResolve);
}
function doAdd(download, req) {
var downloadCooked = cookDownload(download);
// Adding our cooked download
datastore.add(downloadCooked).then(function(id) {
// Enriched object with the id provided by the datastore
downloadCooked.id = id;
datastore.put(downloadCooked, id)
.then(function() { req.done(downloadCooked); },
defaultError(req));
}, defaultError(req));
}
function add(download) {
var req = new Request();
window.setTimeout(function() {
init(doAdd.bind(null, download, req), req.failed.bind(req));
});
return req;
}
function doGet(id, req) {
datastore.get(id).then(function(download) { req.done(download); },
defaultError(req));
}
function get(id) {
var req = new Request();
window.setTimeout(function() {
init(doGet.bind(null, id, req), req.failed.bind(req));
});
return req;
}
function doGetAll(req) {
updateDownloadList().then(function() {
datastore.get.apply(datastore, downloadList).then(defaultSuccess(req),
defaultError(req));
}, defaultError(req));
}
function getAll() {
var req = new Request();
window.setTimeout(function() {
init(doGetAll.bind(null, req), req.failed.bind(req));
});
return req;
}
function doRemove(id, req) {
// Removing the download object from datastore
datastore.remove(id).then(function(success) {
if (success) {
defaultSuccess(req);
} else {
req.failed({
message: 'The download with id: ' + id + 'does not exist'
});
}
}, defaultError(req));
}
function remove(download) {
var req = new Request();
window.setTimeout(function() {
init(doRemove.bind(null, download.id, req), req.failed.bind(req));
});
return req;
}
function doAddListener(listener) {
datastore.addEventListener('change', listener);
}
function addListener(listener) {
var req = new Request();
window.setTimeout(function() {
init(doAddListener.bind(null, listener), req.failed.bind(req));
});
return req;
}
function doRemoveListener(listener) {
datastore.removeEventListener('change', listener);
}
function removeListener(listener) {
var req = new Request();
window.setTimeout(function() {
init(doRemoveListener.bind(null, listener), req.failed.bind(req));
});
return req;
}
return {
/*
* This method returns the download with the specified id.
*/
get: get,
/*
* This method returns an array of complete downloads
*/
getAll: getAll,
/*
* It adds a new download object and returns the new datastore
* download object
*
* @param{Object} Download object provided by the API
*
*/
add: add,
/*
* It removes a download object
*
* @param{Object} The download object to be removed from the datastore
*
*/
remove: remove,
/*
* Add a listener that will receive datastore change events.
*
* @param{Function} The listener to be added.
*/
addListener: addListener,
/*
* Remove a listener previously added via addListener.
*
* @param{Function} The listener to be removed.
*/
removeListener: removeListener
};
}());

View file

@ -0,0 +1,358 @@
'use strict';
/* exported DownloadUI */
/* global DownloadFormatter */
/* global LazyLoader */
/* global MimeMapper */
/**
* This file defines a component to show download confirmations
*
* - Stop download (Are you sure you want to stop the download?)
* - Download stopped (Download was stopped. Try downloading again?)
* - Download failed (xfile failed to download. Try downloading again?)
* - Delete download (Delete xfile?)
* - Unsupported file type
* - File not found
* - File open error
* - No provider to share file
*
* var request = DownloadUI.show(DownloadUI.TYPE.STOP, download);
*
* request.oncancel = function() {
* alert('CANCEL');
* };
*
* request.onconfirm = function() {
* alert('CONFIRM');
* };
*
* WARNING: To use this library you need to include 'shared/js/l10n.js'
*
*/
var DownloadUI = (function() {
/**
* Download type constructor
*
* @param {String} Type name
* @param {Array} CSS classes to confirm button
* @param {Boolean} Message without parameters
*/
var DownloadType = function(name, classes, isPlainMessage) {
this.name = name;
this.classes = classes;
this.isPlainMessage = isPlainMessage;
this.numberOfButtons = classes.indexOf('full') !== -1 ? 1 : 2;
};
/**
* Errors reported by the Downloads API.
*/
var ERRORS = {
NO_MEMORY: 2152857616,
NO_SDCARD: 2152857618,
UNMOUNTED_SDCARD: 2152857621
};
var TYPES = {
STOP: new DownloadType('stop', ['danger'], true),
STOPPED: new DownloadType('stopped', ['recommend'], true),
FAILED: new DownloadType('failed', ['recommend']),
DELETE: new DownloadType('delete', ['danger']),
DELETE_ALL: new DownloadType('delete_all', ['danger']),
UNSUPPORTED_FILE_TYPE: new DownloadType('unsupported_file_type',
['danger']),
FILE_NOT_FOUND: new DownloadType('file_not_found', ['recommend', 'full'],
true),
FILE_OPEN_ERROR: new DownloadType('file_open_error', ['danger']),
NO_SDCARD: new DownloadType('no_sdcard_found_2', ['recommend', 'full'],
true),
UNMOUNTED_SDCARD: new DownloadType('unmounted_sdcard_2', ['recommend',
'full'], true),
NO_PROVIDER: new DownloadType('no_provider', ['recommend', 'full'], true),
NO_MEMORY: new DownloadType('no_memory', ['recommend', 'full'], true)
};
var DownloadAction = function(id, type) {
this.id = id;
this.name = id.toLowerCase();
this.title = this.name + '_downloaded_file';
this.type = type;
};
var ACTIONS = {
OPEN: new DownloadAction('OPEN', 'confirm'),
SHARE: new DownloadAction('SHARE', 'confirm'),
WALLPAPER: new DownloadAction('WALLPAPER', 'confirm'),
RINGTONE: new DownloadAction('RINGTONE', 'confirm')
};
// Confirm dialog containers
var confirm = null;
var actionMenu = null;
/**
* Request auxiliary object to support asynchronous calls
*/
var Request = function() {
this.cancel = function() {
removeContainers();
if (typeof this.oncancel === 'function') {
this.oncancel();
}
};
this.confirm = function(result) {
removeContainers();
if (typeof this.onconfirm === 'function') {
this.result = result;
this.onconfirm({
target: this
});
}
};
};
function removeContainers() {
removeConfirm();
removeActionMenu();
}
function addConfirm() {
if (confirm !== null) {
confirm.parentNode.removeChild(confirm);
}
confirm = document.createElement('form');
confirm.id = 'downloadConfirmUI';
confirm.setAttribute('role', 'dialog');
confirm.setAttribute('data-type', 'confirm');
confirm.setAttribute('data-z-index-level', '100');
var screen = document.getElementById('screen');
var container = screen || document.body;
container.appendChild(confirm);
}
function removeConfirm() {
if (confirm === null) {
return;
}
confirm.innerHTML = '';
confirm.style.display = 'none';
}
// When users click or hold on home button UIs should be removed
window.addEventListener('home', removeContainers);
window.addEventListener('holdhome', removeContainers);
function l10n(element, l10nid, l10nargs) {
// First set our args.
if (l10nargs) {
element.setAttribute('data-l10n-args', JSON.stringify(l10nargs));
}
// Then localize.
element.setAttribute('data-l10n-id', l10nid);
return element;
}
function createConfirm(type, req, downloads) {
addConfirm();
var dialog = document.createElement('section');
// Header
var header = document.createElement('h1');
l10n(header, type.name + '_download_title');
dialog.appendChild(header);
// Message
var message = document.createElement('p');
if (type.isPlainMessage) {
l10n(message, type.name + '_download_message');
} else {
var args = Object.create(null);
if (type === TYPES.DELETE_ALL) {
args.number = downloads.length;
} else {
args.name = DownloadFormatter.getFileName(downloads[0]);
}
l10n(message, type.name + '_download_message', args);
}
dialog.appendChild(message);
var menu = document.createElement('menu');
menu.dataset.items = type.numberOfButtons;
if (type.numberOfButtons === 2) {
// Left button
var lButton = document.createElement('button');
lButton.type = 'button';
lButton.setAttribute('data-l10n-id',
type.name + '_download_left_button');
lButton.onclick = function l_cancel() {
lButton.onclick = null;
req.cancel();
};
menu.appendChild(lButton);
}
// Right button
var rButton = document.createElement('button');
rButton.type = 'button';
type.classes.forEach(function(clazz) {
rButton.classList.add(clazz);
});
rButton.setAttribute('data-l10n-id',
type.name + '_download_right_button');
rButton.onclick = function r_confirm() {
rButton.onclick = null;
req.confirm();
};
menu.appendChild(rButton);
dialog.appendChild(menu);
confirm.appendChild(dialog);
confirm.style.display = 'block';
}
function addActionMenu() {
// If we have an actionMenu, empty it and re-create it.
if (actionMenu !== null) {
document.body.removeChild(actionMenu);
}
actionMenu = document.createElement('gaia-menu');
actionMenu.id = 'downloadActionMenuUI';
actionMenu.className = 'actions';
}
function removeActionMenu() {
if (actionMenu === null) {
return;
}
actionMenu.hide();
document.body.removeChild(actionMenu);
actionMenu = null;
}
function createActionMenu(req, download) {
var actions = [ACTIONS.OPEN, ACTIONS.SHARE];
var fileName = DownloadFormatter.getFileName(download);
var type = MimeMapper.guessTypeFromFileProperties(fileName,
download.contentType);
if (type.length > 0) {
if (type.startsWith('image/')) {
actions.push(ACTIONS.WALLPAPER);
} else if (type.startsWith('audio/')) {
actions.push(ACTIONS.RINGTONE);
}
}
doCreateActionMenu(req, fileName, actions);
}
function doCreateActionMenu(req, fileName, actions) {
addActionMenu();
var header = document.createElement('header');
header.textContent = fileName;
actionMenu.appendChild(header);
actions.forEach(function addActionButton(action) {
var button = document.createElement('button');
button.id = action.id;
l10n(button, action.title);
button.dataset.type = action.type;
actionMenu.appendChild(button);
button.addEventListener('click', function buttonCliked(evt) {
button.removeEventListener('click', buttonCliked);
req[evt.target.dataset.type](ACTIONS[evt.target.id]);
});
});
document.body.appendChild(actionMenu);
actionMenu.removeAttribute('hidden');
}
var styleSheets = [
'shared/style/buttons.css',
'shared/style/headers.css',
'shared/style/confirm.css'
];
/*
* Shows a confirmation depending on type. It returns a request object with
* oncancel and onconfirm callbacks
*
* @param {String} Confirmation type
*
* @param {Array} It represents the download(s) object(s)
*
* @param {Boolean} This optional parameter indicates if the library should
* include BBs
*/
function show(type, downloads, ignoreStyles) {
var req = new Request();
downloads = Array.isArray(downloads) ? downloads : [downloads];
window.setTimeout(function() {
var libs = ['shared/js/download/download_formatter.js'];
if (!ignoreStyles) {
libs.push.apply(libs, styleSheets);
}
// We have to discover the type of UI depending on state when type is null
if (type === null) {
type = TYPES.STOPPED;
var download = downloads[0];
if (download.state === 'finalized' ||
download.state === 'stopped' && download.error !== null) {
type = TYPES.FAILED;
}
}
LazyLoader.load(libs, createConfirm.call(this, type, req, downloads));
}, 0);
return req;
}
function showActions(download) {
var req = new Request();
window.setTimeout(function() {
LazyLoader.load(['shared/js/mime_mapper.js',
'shared/js/download/download_formatter.js',
'shared/js/component_utils.js',
'shared/elements/gaia_menu/script.js'],
createActionMenu.call(this, req, download));
}, 0);
return req;
}
return {
show: show,
showActions: showActions,
hide: removeContainers,
get ERRORS() {
return ERRORS;
},
get TYPE() {
return TYPES;
}
};
}());

View file

@ -0,0 +1,4 @@
;(function(){'use strict';function dump_off(msg,optionalObject){}
function dump_on(msg,optionalObject){var output=msg;if(optionalObject){try{output+=JSON.stringify(optionalObject);}catch(e){}}
if(dump){var appName=document.location.hostname.replace(/\..*$/,'');dump('['+appName+'] '+output+'\n');}else{console.log(output);}}
window.DUMP=dump_off;var settings=window.navigator.mozSettings;var reqGaiaDebug=settings.createLock().get('debug.gaia.enabled');reqGaiaDebug.onsuccess=function gaiaDebug(){window.DUMP=reqGaiaDebug.result['debug.gaia.enabled']?dump_on:dump_off;};settings.addObserver('debug.gaia.enabled',function(event){window.DUMP=event.settingValue?dump_on:dump_off;dump_on(event.settingValue?'Enabling DUMP':'Disabling DUMP');});}());

View file

@ -0,0 +1,60 @@
'use strict';
/* exported eventSafety */
/**
* Listens for an event to fire _once_ or a timeout to expire.
* This is generally used for things which are waiting on transitions
* as it's sometimes possible to miss the transitionend event.
* This should not be necessary once we have web animations as they
* will guarantee that a callback is fired.
*
* Usage:
*
* eventSafety(window, 'transitionend', callback, 400);
*
* eventSafety(window, 'transitionend', 400).then(() => { ... });
*
* @param {Object} obj
* Object to listen to the event on.
* @param {String} event
* The name of the event.
* @param {Function} callback
Optional; called after the event is fired.
* @param {Integer} timeout The duration of the safety timeout.
*
* @return {Promise}
* A promise that will never reject.
*/
function eventSafety(obj, event, callback, timeout) {
if (typeof callback === 'number') {
timeout = callback;
callback = null;
}
if (!timeout) {
throw new Error('You must pass a valid timeout value to `eventSafety`.');
}
return new Promise((resolve) => {
var finishTimeout;
function done(e) {
// Both "transitionend" and "animationend" events bubble by default;
// ignore them here if they're not targeted on the element we care about.
if (e && e.target !== obj &&
(e.type === 'transitionend' || e.type === 'animationend')) {
return;
}
clearTimeout(finishTimeout);
obj.removeEventListener(event, done);
if (callback) {
/*jshint validthis:true */
resolve(callback.apply(this, arguments));
} else {
resolve(e);
}
}
finishTimeout = setTimeout(done, timeout);
obj.addEventListener(event, done);
});
}

View file

@ -0,0 +1,25 @@
/* exported IAC_API_WAKEUP_REASON_ENABLED_CHANGED */
/* exported IAC_API_WAKEUP_REASON_TRY_DISABLE */
/* exported IAC_API_WAKEUP_REASON_LOGIN */
/* exported IAC_API_WAKEUP_REASON_LOGOUT */
/* exported IAC_API_WAKEUP_REASON_STALE_REGISTRATION */
/* exported IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED */
/* exported wakeUpFindMyDevice */
'use strict';
const IAC_API_WAKEUP_REASON_ENABLED_CHANGED = 0;
const IAC_API_WAKEUP_REASON_STALE_REGISTRATION = 1;
const IAC_API_WAKEUP_REASON_LOGIN = 2;
const IAC_API_WAKEUP_REASON_LOGOUT = 3;
const IAC_API_WAKEUP_REASON_TRY_DISABLE = 4;
const IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED = 5;
function wakeUpFindMyDevice(reason) {
navigator.mozApps.getSelf().onsuccess = function() {
var app = this.result;
app.connect('findmydevice-wakeup').then(function(ports) {
ports[0].postMessage(reason);
});
};
}

View file

@ -0,0 +1,39 @@
/* exported Gzip */
'use strict';
var Gzip = {
WORKER_URL: '/shared/js/gzip/gzip_worker.js',
compress: function(payload) {
return new Promise((resolve, reject) => {
var worker = new Worker(this.WORKER_URL);
// Protect against the worker failing for whatever reason.
var workerTimeout = setTimeout(function() {
worker.terminate();
reject('Failed to compress: gzip worker did not respond.');
}, 10000);
worker.onmessage = function(msg) {
clearTimeout(workerTimeout);
worker.terminate();
var data = msg.data;
if (data.returnResult) {
resolve(data.gzipData);
}
else {
reject('Failed to compress: ' + data.error);
}
};
worker.postMessage({
cmd: 'gzip',
payload: payload
});
});
}
};

View file

@ -0,0 +1,36 @@
/* jshint ignore: start */
/*
* Original source: https://github.com/imaya/zlib.js
*/
(function() {'use strict';var p=void 0,v=!0,da=this;function fa(f,e){var c=f.split("."),a=da;!(c[0]in a)&&a.execScript&&a.execScript("var "+c[0]);for(var b;c.length&&(b=c.shift());)!c.length&&e!==p?a[b]=e:a=a[b]?a[b]:a[b]={}};var C="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array&&"undefined"!==typeof DataView;function F(f,e){this.index="number"===typeof e?e:0;this.f=0;this.buffer=f instanceof(C?Uint8Array:Array)?f:new (C?Uint8Array:Array)(32768);if(2*this.buffer.length<=this.index)throw Error("invalid index");this.buffer.length<=this.index&&ga(this)}function ga(f){var e=f.buffer,c,a=e.length,b=new (C?Uint8Array:Array)(a<<1);if(C)b.set(e);else for(c=0;c<a;++c)b[c]=e[c];return f.buffer=b}
F.prototype.b=function(f,e,c){var a=this.buffer,b=this.index,g=this.f,l=a[b],m;c&&1<e&&(f=8<e?(H[f&255]<<24|H[f>>>8&255]<<16|H[f>>>16&255]<<8|H[f>>>24&255])>>32-e:H[f]>>8-e);if(8>e+g)l=l<<e|f,g+=e;else for(m=0;m<e;++m)l=l<<1|f>>e-m-1&1,8===++g&&(g=0,a[b++]=H[l],l=0,b===a.length&&(a=ga(this)));a[b]=l;this.buffer=a;this.f=g;this.index=b};F.prototype.finish=function(){var f=this.buffer,e=this.index,c;0<this.f&&(f[e]<<=8-this.f,f[e]=H[f[e]],e++);C?c=f.subarray(0,e):(f.length=e,c=f);return c};
var ia=new (C?Uint8Array:Array)(256),M;for(M=0;256>M;++M){for(var N=M,S=N,ja=7,N=N>>>1;N;N>>>=1)S<<=1,S|=N&1,--ja;ia[M]=(S<<ja&255)>>>0}var H=ia;function ka(f,e,c){var a,b="number"===typeof e?e:e=0,g="number"===typeof c?c:f.length;a=-1;for(b=g&7;b--;++e)a=a>>>8^T[(a^f[e])&255];for(b=g>>3;b--;e+=8)a=a>>>8^T[(a^f[e])&255],a=a>>>8^T[(a^f[e+1])&255],a=a>>>8^T[(a^f[e+2])&255],a=a>>>8^T[(a^f[e+3])&255],a=a>>>8^T[(a^f[e+4])&255],a=a>>>8^T[(a^f[e+5])&255],a=a>>>8^T[(a^f[e+6])&255],a=a>>>8^T[(a^f[e+7])&255];return(a^4294967295)>>>0}
var la=[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,853044451,1172266101,3705015759,
2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,4240017532,1658658271,366619977,
2362670323,4224994405,1303535960,984961486,2747007092,3569037538,1256170817,1037604311,2765210733,3554079995,1131014506,879679996,2909243462,3663771856,1141124467,855842277,2852801631,3708648649,1342533948,654459306,3188396048,3373015174,1466479909,544179635,3110523913,3462522015,1591671054,702138776,2966460450,3352799412,1504918807,783551873,3082640443,3233442989,3988292384,2596254646,62317068,1957810842,3939845945,2647816111,81470997,1943803523,3814918930,2489596804,225274430,2053790376,3826175755,
2466906013,167816743,2097651377,4027552580,2265490386,503444072,1762050814,4150417245,2154129355,426522225,1852507879,4275313526,2312317920,282753626,1742555852,4189708143,2394877945,397917763,1622183637,3604390888,2714866558,953729732,1340076626,3518719985,2797360999,1068828381,1219638859,3624741850,2936675148,906185462,1090812512,3747672003,2825379669,829329135,1181335161,3412177804,3160834842,628085408,1382605366,3423369109,3138078467,570562233,1426400815,3317316542,2998733608,733239954,1555261956,
3268935591,3050360625,752459403,1541320221,2607071920,3965973030,1969922972,40735498,2617837225,3943577151,1913087877,83908371,2512341634,3803740692,2075208622,213261112,2463272603,3855990285,2094854071,198958881,2262029012,4057260610,1759359992,534414190,2176718541,4139329115,1873836001,414664567,2282248934,4279200368,1711684554,285281116,2405801727,4167216745,1634467795,376229701,2685067896,3608007406,1308918612,956543938,2808555105,3495958263,1231636301,1047427035,2932959818,3654703836,1088359270,
936918E3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117],T=C?new Uint32Array(la):la;function U(f){this.buffer=new (C?Uint16Array:Array)(2*f);this.length=0}U.prototype.getParent=function(f){return 2*((f-2)/4|0)};U.prototype.push=function(f,e){var c,a,b=this.buffer,g;c=this.length;b[this.length++]=e;for(b[this.length++]=f;0<c;)if(a=this.getParent(c),b[c]>b[a])g=b[c],b[c]=b[a],b[a]=g,g=b[c+1],b[c+1]=b[a+1],b[a+1]=g,c=a;else break;return this.length};
U.prototype.pop=function(){var f,e,c=this.buffer,a,b,g;e=c[0];f=c[1];this.length-=2;c[0]=c[this.length];c[1]=c[this.length+1];for(g=0;;){b=2*g+2;if(b>=this.length)break;b+2<this.length&&c[b+2]>c[b]&&(b+=2);if(c[b]>c[g])a=c[g],c[g]=c[b],c[b]=a,a=c[g+1],c[g+1]=c[b+1],c[b+1]=a;else break;g=b}return{index:f,value:e,length:this.length}};function ma(f,e){this.h=na;this.j=0;this.input=C&&f instanceof Array?new Uint8Array(f):f;this.c=0;e&&(e.lazy&&(this.j=e.lazy),"number"===typeof e.compressionType&&(this.h=e.compressionType),e.outputBuffer&&(this.a=C&&e.outputBuffer instanceof Array?new Uint8Array(e.outputBuffer):e.outputBuffer),"number"===typeof e.outputIndex&&(this.c=e.outputIndex));this.a||(this.a=new (C?Uint8Array:Array)(32768))}var na=2,V=[],$;
for($=0;288>$;$++)switch(v){case 143>=$:V.push([$+48,8]);break;case 255>=$:V.push([$-144+400,9]);break;case 279>=$:V.push([$-256+0,7]);break;case 287>=$:V.push([$-280+192,8]);break;default:throw"invalid literal: "+$;}
ma.prototype.g=function(){var f,e,c,a,b=this.input;switch(this.h){case 0:c=0;for(a=b.length;c<a;){e=C?b.subarray(c,c+65535):b.slice(c,c+65535);c+=e.length;var g=e,l=c===a,m=p,d=p,h=p,s=p,x=p,n=this.a,k=this.c;if(C){for(n=new Uint8Array(this.a.buffer);n.length<=k+g.length+5;)n=new Uint8Array(n.length<<1);n.set(this.a)}m=l?1:0;n[k++]=m|0;d=g.length;h=~d+65536&65535;n[k++]=d&255;n[k++]=d>>>8&255;n[k++]=h&255;n[k++]=h>>>8&255;if(C)n.set(g,k),k+=g.length,n=n.subarray(0,k);else{s=0;for(x=g.length;s<x;++s)n[k++]=
g[s];n.length=k}this.c=k;this.a=n}break;case 1:var q=new F(C?new Uint8Array(this.a.buffer):this.a,this.c);q.b(1,1,v);q.b(1,2,v);var u=oa(this,b),w,aa,z;w=0;for(aa=u.length;w<aa;w++)if(z=u[w],F.prototype.b.apply(q,V[z]),256<z)q.b(u[++w],u[++w],v),q.b(u[++w],5),q.b(u[++w],u[++w],v);else if(256===z)break;this.a=q.finish();this.c=this.a.length;break;case na:var B=new F(C?new Uint8Array(this.a.buffer):this.a,this.c),ra,L,O,P,Q,Ha=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],W,sa,X,ta,ba,ea=Array(19),
ua,R,ca,y,va;ra=na;B.b(1,1,v);B.b(ra,2,v);L=oa(this,b);W=pa(this.n,15);sa=qa(W);X=pa(this.m,7);ta=qa(X);for(O=286;257<O&&0===W[O-1];O--);for(P=30;1<P&&0===X[P-1];P--);var wa=O,xa=P,G=new (C?Uint32Array:Array)(wa+xa),r,I,t,Y,E=new (C?Uint32Array:Array)(316),D,A,J=new (C?Uint8Array:Array)(19);for(r=I=0;r<wa;r++)G[I++]=W[r];for(r=0;r<xa;r++)G[I++]=X[r];if(!C){r=0;for(Y=J.length;r<Y;++r)J[r]=0}r=D=0;for(Y=G.length;r<Y;r+=I){for(I=1;r+I<Y&&G[r+I]===G[r];++I);t=I;if(0===G[r])if(3>t)for(;0<t--;)E[D++]=0,
J[0]++;else for(;0<t;)A=138>t?t:138,A>t-3&&A<t&&(A=t-3),10>=A?(E[D++]=17,E[D++]=A-3,J[17]++):(E[D++]=18,E[D++]=A-11,J[18]++),t-=A;else if(E[D++]=G[r],J[G[r]]++,t--,3>t)for(;0<t--;)E[D++]=G[r],J[G[r]]++;else for(;0<t;)A=6>t?t:6,A>t-3&&A<t&&(A=t-3),E[D++]=16,E[D++]=A-3,J[16]++,t-=A}f=C?E.subarray(0,D):E.slice(0,D);ba=pa(J,7);for(y=0;19>y;y++)ea[y]=ba[Ha[y]];for(Q=19;4<Q&&0===ea[Q-1];Q--);ua=qa(ba);B.b(O-257,5,v);B.b(P-1,5,v);B.b(Q-4,4,v);for(y=0;y<Q;y++)B.b(ea[y],3,v);y=0;for(va=f.length;y<va;y++)if(R=
f[y],B.b(ua[R],ba[R],v),16<=R){y++;switch(R){case 16:ca=2;break;case 17:ca=3;break;case 18:ca=7;break;default:throw"invalid code: "+R;}B.b(f[y],ca,v)}var ya=[sa,W],za=[ta,X],K,Aa,Z,ha,Ba,Ca,Da,Ea;Ba=ya[0];Ca=ya[1];Da=za[0];Ea=za[1];K=0;for(Aa=L.length;K<Aa;++K)if(Z=L[K],B.b(Ba[Z],Ca[Z],v),256<Z)B.b(L[++K],L[++K],v),ha=L[++K],B.b(Da[ha],Ea[ha],v),B.b(L[++K],L[++K],v);else if(256===Z)break;this.a=B.finish();this.c=this.a.length;break;default:throw"invalid compression type";}return this.a};
function Fa(f,e){this.length=f;this.k=e}
var Ga=function(){function f(b){switch(v){case 3===b:return[257,b-3,0];case 4===b:return[258,b-4,0];case 5===b:return[259,b-5,0];case 6===b:return[260,b-6,0];case 7===b:return[261,b-7,0];case 8===b:return[262,b-8,0];case 9===b:return[263,b-9,0];case 10===b:return[264,b-10,0];case 12>=b:return[265,b-11,1];case 14>=b:return[266,b-13,1];case 16>=b:return[267,b-15,1];case 18>=b:return[268,b-17,1];case 22>=b:return[269,b-19,2];case 26>=b:return[270,b-23,2];case 30>=b:return[271,b-27,2];case 34>=b:return[272,
b-31,2];case 42>=b:return[273,b-35,3];case 50>=b:return[274,b-43,3];case 58>=b:return[275,b-51,3];case 66>=b:return[276,b-59,3];case 82>=b:return[277,b-67,4];case 98>=b:return[278,b-83,4];case 114>=b:return[279,b-99,4];case 130>=b:return[280,b-115,4];case 162>=b:return[281,b-131,5];case 194>=b:return[282,b-163,5];case 226>=b:return[283,b-195,5];case 257>=b:return[284,b-227,5];case 258===b:return[285,b-258,0];default:throw"invalid length: "+b;}}var e=[],c,a;for(c=3;258>=c;c++)a=f(c),e[c]=a[2]<<24|
a[1]<<16|a[0];return e}(),Ia=C?new Uint32Array(Ga):Ga;
function oa(f,e){function c(b,e){var a=b.k,c=[],g=0,f;f=Ia[b.length];c[g++]=f&65535;c[g++]=f>>16&255;c[g++]=f>>24;var d;switch(v){case 1===a:d=[0,a-1,0];break;case 2===a:d=[1,a-2,0];break;case 3===a:d=[2,a-3,0];break;case 4===a:d=[3,a-4,0];break;case 6>=a:d=[4,a-5,1];break;case 8>=a:d=[5,a-7,1];break;case 12>=a:d=[6,a-9,2];break;case 16>=a:d=[7,a-13,2];break;case 24>=a:d=[8,a-17,3];break;case 32>=a:d=[9,a-25,3];break;case 48>=a:d=[10,a-33,4];break;case 64>=a:d=[11,a-49,4];break;case 96>=a:d=[12,a-
65,5];break;case 128>=a:d=[13,a-97,5];break;case 192>=a:d=[14,a-129,6];break;case 256>=a:d=[15,a-193,6];break;case 384>=a:d=[16,a-257,7];break;case 512>=a:d=[17,a-385,7];break;case 768>=a:d=[18,a-513,8];break;case 1024>=a:d=[19,a-769,8];break;case 1536>=a:d=[20,a-1025,9];break;case 2048>=a:d=[21,a-1537,9];break;case 3072>=a:d=[22,a-2049,10];break;case 4096>=a:d=[23,a-3073,10];break;case 6144>=a:d=[24,a-4097,11];break;case 8192>=a:d=[25,a-6145,11];break;case 12288>=a:d=[26,a-8193,12];break;case 16384>=
a:d=[27,a-12289,12];break;case 24576>=a:d=[28,a-16385,13];break;case 32768>=a:d=[29,a-24577,13];break;default:throw"invalid distance";}f=d;c[g++]=f[0];c[g++]=f[1];c[g++]=f[2];var h,l;h=0;for(l=c.length;h<l;++h)n[k++]=c[h];u[c[0]]++;w[c[3]]++;q=b.length+e-1;x=null}var a,b,g,l,m,d={},h,s,x,n=C?new Uint16Array(2*e.length):[],k=0,q=0,u=new (C?Uint32Array:Array)(286),w=new (C?Uint32Array:Array)(30),aa=f.j,z;if(!C){for(g=0;285>=g;)u[g++]=0;for(g=0;29>=g;)w[g++]=0}u[256]=1;a=0;for(b=e.length;a<b;++a){g=
m=0;for(l=3;g<l&&a+g!==b;++g)m=m<<8|e[a+g];d[m]===p&&(d[m]=[]);h=d[m];if(!(0<q--)){for(;0<h.length&&32768<a-h[0];)h.shift();if(a+3>=b){x&&c(x,-1);g=0;for(l=b-a;g<l;++g)z=e[a+g],n[k++]=z,++u[z];break}0<h.length?(s=Ja(e,a,h),x?x.length<s.length?(z=e[a-1],n[k++]=z,++u[z],c(s,0)):c(x,-1):s.length<aa?x=s:c(s,0)):x?c(x,-1):(z=e[a],n[k++]=z,++u[z])}h.push(a)}n[k++]=256;u[256]++;f.n=u;f.m=w;return C?n.subarray(0,k):n}
function Ja(f,e,c){var a,b,g=0,l,m,d,h,s=f.length;m=0;h=c.length;a:for(;m<h;m++){a=c[h-m-1];l=3;if(3<g){for(d=g;3<d;d--)if(f[a+d-1]!==f[e+d-1])continue a;l=g}for(;258>l&&e+l<s&&f[a+l]===f[e+l];)++l;l>g&&(b=a,g=l);if(258===l)break}return new Fa(g,e-b)}
function pa(f,e){var c=f.length,a=new U(572),b=new (C?Uint8Array:Array)(c),g,l,m,d,h;if(!C)for(d=0;d<c;d++)b[d]=0;for(d=0;d<c;++d)0<f[d]&&a.push(d,f[d]);g=Array(a.length/2);l=new (C?Uint32Array:Array)(a.length/2);if(1===g.length)return b[a.pop().index]=1,b;d=0;for(h=a.length/2;d<h;++d)g[d]=a.pop(),l[d]=g[d].value;m=Ka(l,l.length,e);d=0;for(h=g.length;d<h;++d)b[g[d].index]=m[d];return b}
function Ka(f,e,c){function a(b){var c=d[b][h[b]];c===e?(a(b+1),a(b+1)):--l[c];++h[b]}var b=new (C?Uint16Array:Array)(c),g=new (C?Uint8Array:Array)(c),l=new (C?Uint8Array:Array)(e),m=Array(c),d=Array(c),h=Array(c),s=(1<<c)-e,x=1<<c-1,n,k,q,u,w;b[c-1]=e;for(k=0;k<c;++k)s<x?g[k]=0:(g[k]=1,s-=x),s<<=1,b[c-2-k]=(b[c-1-k]/2|0)+e;b[0]=g[0];m[0]=Array(b[0]);d[0]=Array(b[0]);for(k=1;k<c;++k)b[k]>2*b[k-1]+g[k]&&(b[k]=2*b[k-1]+g[k]),m[k]=Array(b[k]),d[k]=Array(b[k]);for(n=0;n<e;++n)l[n]=c;for(q=0;q<b[c-1];++q)m[c-
1][q]=f[q],d[c-1][q]=q;for(n=0;n<c;++n)h[n]=0;1===g[c-1]&&(--l[0],++h[c-1]);for(k=c-2;0<=k;--k){u=n=0;w=h[k+1];for(q=0;q<b[k];q++)u=m[k+1][w]+m[k+1][w+1],u>f[n]?(m[k][q]=u,d[k][q]=e,w+=2):(m[k][q]=f[n],d[k][q]=n,++n);h[k]=0;1===g[k]&&a(k)}return l}
function qa(f){var e=new (C?Uint16Array:Array)(f.length),c=[],a=[],b=0,g,l,m,d;g=0;for(l=f.length;g<l;g++)c[f[g]]=(c[f[g]]|0)+1;g=1;for(l=16;g<=l;g++)a[g]=b,b+=c[g]|0,b<<=1;g=0;for(l=f.length;g<l;g++){b=a[f[g]];a[f[g]]+=1;m=e[g]=0;for(d=f[g];m<d;m++)e[g]=e[g]<<1|b&1,b>>>=1}return e};function La(f,e){this.input=f;this.c=this.i=0;this.d={};e&&(e.flags&&(this.d=e.flags),"string"===typeof e.filename&&(this.filename=e.filename),"string"===typeof e.comment&&(this.l=e.comment),e.deflateOptions&&(this.e=e.deflateOptions));this.e||(this.e={})}
La.prototype.g=function(){var f,e,c,a,b,g,l,m,d=new (C?Uint8Array:Array)(32768),h=0,s=this.input,x=this.i,n=this.filename,k=this.l;d[h++]=31;d[h++]=139;d[h++]=8;f=0;this.d.fname&&(f|=Ma);this.d.fcomment&&(f|=Na);this.d.fhcrc&&(f|=Oa);d[h++]=f;e=(Date.now?Date.now():+new Date)/1E3|0;d[h++]=e&255;d[h++]=e>>>8&255;d[h++]=e>>>16&255;d[h++]=e>>>24&255;d[h++]=0;d[h++]=Pa;if(this.d.fname!==p){l=0;for(m=n.length;l<m;++l)g=n.charCodeAt(l),255<g&&(d[h++]=g>>>8&255),d[h++]=g&255;d[h++]=0}if(this.d.comment){l=
0;for(m=k.length;l<m;++l)g=k.charCodeAt(l),255<g&&(d[h++]=g>>>8&255),d[h++]=g&255;d[h++]=0}this.d.fhcrc&&(c=ka(d,0,h)&65535,d[h++]=c&255,d[h++]=c>>>8&255);this.e.outputBuffer=d;this.e.outputIndex=h;b=new ma(s,this.e);d=b.g();h=b.c;C&&(h+8>d.buffer.byteLength?(this.a=new Uint8Array(h+8),this.a.set(new Uint8Array(d.buffer)),d=this.a):d=new Uint8Array(d.buffer));a=ka(s,p,p);d[h++]=a&255;d[h++]=a>>>8&255;d[h++]=a>>>16&255;d[h++]=a>>>24&255;m=s.length;d[h++]=m&255;d[h++]=m>>>8&255;d[h++]=m>>>16&255;d[h++]=
m>>>24&255;this.i=x;C&&h<d.length&&(this.a=d=d.subarray(0,h));return d};var Pa=255,Oa=2,Ma=8,Na=16;fa("Zlib.Gzip",La);fa("Zlib.Gzip.prototype.compress",La.prototype.g);}).call(this);

View file

@ -0,0 +1,27 @@
/* global Zlib */
'use strict';
importScripts('gzip.min.js');
self.onmessage = function(message) {
var cmd = message.data.cmd;
if (cmd === 'gzip') {
// gzip function requires Uint8Array
var textEncoder = new TextEncoder('utf8');
var payload = message.data.payload;
var uint8Data = textEncoder.encode(payload);
try {
var gzip = new Zlib.Gzip(uint8Data);
var compressed = gzip.compress();
postMessage({returnResult: true, gzipData: compressed});
}
catch(e) {
postMessage({returnResult: false, error: e});
}
} else {
console.warn('GzipWorker, unsupported cmd:', cmd);
}
};

View file

@ -0,0 +1,147 @@
/* global LazyLoader */
(function(exports) {
'use strict';
var resources = ['/shared/js/component_utils.js',
'/shared/elements/gaia_buttons/script.js',
'/shared/elements/gaia_confirm/script.js',
'/shared/elements/gaia_menu/script.js'];
/**
Generic dialog helper this _depends_ on <gaia-confirm> but is not required
to be loaded as part of the main gaia-confirm script.
*/
function ConfirmDialogHelper(config) {
this.config = config;
}
ConfirmDialogHelper.prototype = {
show: function(parent) {
LazyLoader.load(resources, this._show.bind(this, parent));
},
_show: function(parent) {
var config = this.config;
var wrapper = document.createElement('div');
// only include a cancel button if the config was given...
var cancelButton = config.cancel ?
'<button class="cancel" type="button"></button>' :
'';
wrapper.innerHTML =
'<gaia-confirm>' +
'<h1></h1>' +
'<p></p>' +
'<gaia-buttons skin="dark">' +
cancelButton +
'<button class="confirm" type="button"></button>' +
'</gaia-buttons>' +
'</gaia-confirm>';
var element = wrapper.firstElementChild;
this.element = element;
element.dataset.type = config.type;
element.addEventListener('confirm', this);
element.addEventListener('cancel', this);
// XXX: Primarily here for pressing the home screen button.
// The home button triggers a hashchange of the homescreen.
window.addEventListener('hashchange', this);
// TODO: Add visibility change handling...
var title = element.querySelector('h1');
var body = element.querySelector('p');
var cancel = element.querySelector('.cancel');
var confirm = element.querySelector('.confirm');
var setL10nAttributes = function (element, options){
if ('string' === typeof options) {
navigator.mozL10n.setAttributes(element, options);
}
if(options.id) {
navigator.mozL10n.setAttributes(element, options.id, options.args);
}
};
setL10nAttributes(title, config.title);
setL10nAttributes(body, config.body);
setL10nAttributes(confirm, config.confirm.title);
if (config.cancel) {
setL10nAttributes(cancel, config.cancel.title);
}
if (config.confirm.type) {
confirm.classList.add(config.confirm.type);
}
document.activeElement.blur();
element.setAttribute('hidden', '');
parent.appendChild(element);
// This nested requestAnimationFrame is to work around the coalescing
// of the style changes associated with removing of the 'hidden'
// attribute with the creation of the element.
// For whatever reason, flushing the style with the usual trick of
// accessing clientTop doesn't work, and a setTimeout requires an
// unreasonably lengthy timeout (>50ms) to work, and that may not be
// a reliable solution.
// This work-around, though gross, appears to work consistently without
// introducing too much lag or extra work.
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
element.removeAttribute('hidden');
window.dispatchEvent(new CustomEvent('gaia-confirm-open'));
});
});
},
destroy: function() {
if (!this.element) {
return;
}
// Ensure cleanup of our hacks!
window.removeEventListener('hashchange', this);
this.element.addEventListener('transitionend',
function removeAfterHide(e) {
if (e.target !== this.element) {
return;
}
this.element.parentNode.removeChild(this.element);
this.element = null;
window.dispatchEvent(new CustomEvent('gaia-confirm-close'));
}.bind(this));
this.element.setAttribute('hidden', '');
},
handleEvent: function(e) {
// Ensure we hide the dialog in the face of other errors.
this.destroy();
switch (e.type) {
case 'hashchange':
// Hashchange is only here to trigger this to call destroy.
break;
case 'confirm':
var confirm = this.config.confirm.cb;
confirm && confirm();
break;
case 'cancel':
var cancel = this.config.cancel.cb;
cancel && cancel();
break;
}
}
};
// This name is intentionally verbose.
window.ConfirmDialogHelper = ConfirmDialogHelper;
}(window));

View file

@ -0,0 +1,5 @@
;'use strict';(function(exports){const FETCH_XHR_TIMEOUT=10000;function fetchBlob(uri){return new Promise(function(accept,reject){var xhr=new XMLHttpRequest({mozAnon:true,mozSystem:true});xhr.open('GET',uri,true);xhr.responseType='blob';xhr.timeout=FETCH_XHR_TIMEOUT;xhr.send();xhr.onload=function(){var status=xhr.status;if(status!==0&&status!==200){reject(new Error('Got HTTP status '+status+' trying to load '+uri));return;}
accept(xhr.response);};xhr.onerror=xhr.ontimeout=function(){reject(new Error('Error while HTTP GET: ',uri));};});}
function Icon(elem,uri){this.elem=elem;this.uri=uri;}
Icon.prototype={get size(){return this._size||40;},set size(size){this._size=size||this.size;var style=this.elem.style;var sizeInRems=(this._size/10)+'rem';style.backgroundSize=style.width=style.height=sizeInRems;},render:function render(options){options=options||{};var uri=this.uri;this.size=options.size;if(!uri){return;}
fetchBlob(uri).then(function(blob){this.renderBlob(blob,options);}.bind(this));},renderBlob:function renderBlob(blob,options){options=options||{};var style=this.elem.style;this.size=options.size;var img=new Image();img.src=URL.createObjectURL(blob);var size=this.size;img.onload=()=>{var renderer=new GridIconRenderer({grid:{layout:{get gridIconSize(){return size;},get gridMaxIconSize(){return size*devicePixelRatio;}}}});var type=options.type||GridIconRenderer.TYPE.FAVICON;renderer[type](img).then((blob)=>{var url=URL.createObjectURL(blob);var imageUrl='url('+url+')';style.backgroundImage=imageUrl;if(options.onLoad){options.onLoad(blob);}});URL.revokeObjectURL(img.src);};img.onerror=()=>{if(options.onerror){options.onerror();}};}};exports.Icon=Icon;}(window));

View file

@ -0,0 +1,49 @@
'use strict';
var IACHandler = {
_eventPrefix: 'iac-',
_ports: {},
init: function onInit() {
var self = this;
window.navigator.mozSetMessageHandler('connection',
function onConnected(request) {
var keyword = request.keyword;
var port = request.port;
// We will save each port by keyword; ports may get overwritten
// sometimes, but that's ok because there should only be one *active*
// port with a given keyword at any time.
self._ports[keyword] = port;
port.onmessage = function onReceivedMessage(evt) {
var message = evt.data;
/*
* System will emit a event when received message
* e.g. iac-ftucomms
* iac-[keywords]
*/
var evtName = self._eventPrefix + keyword;
var iacEvt = document.createEvent('CustomEvent');
/*
* You can get the message by accessing
* `event.detail`
*
* It will store the whole message you passed from postMessage
*/
iacEvt.initCustomEvent(evtName,
/* canBubble: */ true, /* cancelable */ false, message);
window.dispatchEvent(iacEvt);
};
});
},
getPort: function(keyword) {
return this._ports[keyword];
}
};
IACHandler.init();

View file

@ -0,0 +1,186 @@
'use strict';
/**
* IccHelper redirect calls to ICC related APIs to correct object. IccHelper is
* created for backward compatibility of gaia master after the patch of
* bug 814637 landed in m-c. In the patch the interface provided by IccManager
* was moved to a MozIcc object. Details please refer to the URL[1].
* The helper *MUST* be removed once all gaia developement no longer depend
* on b2g26.
* [1]: https://wiki.mozilla.org/WebAPI/WebIccManager/Multi-SIM
*/
(function(exports) {
var _iccManager = navigator.mozIccManager;
var _iccProxy = null;
Object.defineProperty(exports, 'IccHelper', {
configurable: true,
get: function() {
return _iccProxy;
},
// This is for code that sets value to IccHelper. And for unit tests that
// mock IccHelper.
set: function(value) {
_iccProxy = value;
}
});
if (_iccManager && _iccManager.getIccById) {
var activeIccObj = null;
var eventListeners = {};
var cachedEventListeners = {}; // cache on + 'eventName' event handlers
var events = ['cardstatechange', 'iccinfochange',
'stkcommand', 'stksessionend'];
var getters = ['iccInfo', 'cardState'];
var methods = ['sendStkResponse', 'sendStkMenuSelection',
'sendStkTimerExpiration', 'sendStkEventDownload'];
var domRequests = ['getCardLock', 'unlockCardLock', 'setCardLock',
'getCardLockRetryCount', 'readContacts',
'updateContact', 'iccOpenChannel', 'iccExchangeAPDU',
'iccCloseChannel'];
var getterTemplate = function(name) {
return function() {
if (activeIccObj) {
return activeIccObj[name];
} else {
return null;
}
};
};
var methodTemplate = function(name) {
return function() {
if (activeIccObj) {
return activeIccObj[name].apply(activeIccObj, arguments);
}
};
};
// Multi-SIM API available
var createIccProxy = function() {
var iccProxy = {
addEventListener: function(eventName, callback) {
if (typeof callback !== 'function') {
return;
}
if (events.indexOf(eventName) === -1) {
return;
}
var listeners = eventListeners[eventName];
if (listeners == null) {
listeners = eventListeners[eventName] = [];
}
if (listeners.indexOf(callback) === -1) {
listeners.push(callback);
}
},
removeEventListener: function(eventName, callback) {
var listeners = eventListeners[eventName];
if (listeners) {
var index = listeners.indexOf(callback);
if (index !== -1) {
listeners.splice(index, 1);
}
}
}
};
getters.forEach(function(getter) {
Object.defineProperty(iccProxy, getter, {
enumerable: true,
get: getterTemplate(getter)
});
});
methods.forEach(function(method) {
iccProxy[method] = methodTemplate(method);
});
domRequests.forEach(function(request) {
iccProxy[request] = methodTemplate(request);
});
events.forEach(function(eventName) {
// Define 'on' + eventName properties
// Assigning an event handler and adding an event listener are
// handled separately. We need to simulate the same behavior in
// IccHelper.
Object.defineProperty(iccProxy, 'on' + eventName, {
enumerable: true,
set: function(newListener) {
var oldListener = cachedEventListeners[eventName];
if (oldListener) {
iccProxy.removeEventListener(eventName, oldListener);
}
cachedEventListeners[eventName] = newListener;
if (newListener) {
iccProxy.addEventListener(eventName, newListener);
}
},
get: function() {
return cachedEventListeners[eventName];
}
});
});
return iccProxy;
};
// Initialize icc proxy
_iccProxy = createIccProxy();
if (_iccManager.iccIds && _iccManager.iccIds.length) {
activeIccObj = _iccManager.getIccById(_iccManager.iccIds[0]);
if (activeIccObj) {
// register to callback
events.forEach(function(eventName) {
activeIccObj.addEventListener(eventName, function(event) {
var listeners = eventListeners[eventName];
if (listeners) {
listeners.forEach(function(listener) {
listener(event);
});
}
});
});
}
}
_iccManager.oniccdetected = function(event) {
if (_iccProxy.cardState == null) {
activeIccObj = _iccManager.getIccById(event.iccId);
if (activeIccObj) {
// register to callback
events.forEach(function(eventName) {
activeIccObj.addEventListener(eventName, function(event) {
var listeners = eventListeners[eventName];
if (listeners) {
listeners.forEach(function(listener) {
listener(event);
});
}
});
});
// trigger cardstatechange and iccinfochange manually when a mozIcc
// object becomes valid (detected).
['cardstatechange', 'iccinfochange'].forEach(function(eventName) {
if (eventListeners[eventName]) {
eventListeners[eventName].forEach(function(listener) {
listener();
});
}
});
}
}
};
} else {
_iccProxy = _iccManager;
}
})(window);

View file

@ -0,0 +1,441 @@
/* globals Icon */
/* exported IconsHelper */
'use strict';
/**
* Utility library that will help us to work with icons coming from
* different sources.
*/
(function IconsHelper(exports) {
const ICON_CACHE_PERIOD = 24 * 60 * 60 * 1000; // 1 day
const FETCH_XHR_TIMEOUT = 10000;
const DEBUG = false;
var dataStore = null;
/**
* Return default size in px based in devicePixelRatio
*
* Sized based on current homescreen selected icons for apps
* in a configuration of 3 icons per row. See:
* https://github.com/mozilla-b2g/gaia/blob/master/
* shared/elements/gaia_grid/js/grid_layout.js#L15
* @returns {Number}
*/
function getDefaultIconSize() {
var dpr = window.devicePixelRatio;
return (dpr && dpr > 1) ? 142 : 84;
}
function sizeIsNearer(size1, size2, targetSize) {
// TODO: weight for a larger vs. smaller icon?
var delta1 = Math.abs(targetSize - size1);
var delta2 = Math.abs(targetSize - size2);
return (delta1 <= delta2);
}
/**
* Return a promise that resolves to the URL of the best icon for a web page
* given its meta data and web manifest.
*
* @param uri {string}
* @param iconTargetSize {number}
* @param placeObj {Object}
* @param siteObj {Object}
* @returns {Promise}
*/
function getIcon(uri, iconTargetSize, placeObj = {}, siteObj = {}) {
var iconUrl = null;
iconTargetSize = iconTargetSize * window.devicePixelRatio;
// First look for an icon in the Webmanifest.
if (siteObj.webManifestUrl && siteObj.webManifest) {
iconUrl = getBestIconFromWebManifest(siteObj.webManifest, iconTargetSize);
if (DEBUG && iconUrl) {
console.log('Icon from Web Manifest');
}
}
// Then look for an icon in the Firefox manifest.
if (!iconUrl && siteObj.manifest && siteObj.manifest.icons) {
iconUrl = getBestIconFromWebManifest({
icons: _convertToWebManifestIcons(siteObj.manifest,
siteObj.origin || siteObj.manifest.origin)
}, iconTargetSize);
if (DEBUG && iconUrl) {
console.log('Icon from Firefox App Manifest');
}
}
// Otherwise, look into the meta tags.
if (!iconUrl && placeObj && placeObj.icons) {
iconUrl = getBestIconFromMetaTags(placeObj.icons, iconTargetSize);
if (DEBUG && iconUrl) {
console.log('Icon from Meta tags');
}
}
// Last resort, we look for a favicon.ico file.
if (!iconUrl) {
var a = document.createElement('a');
a.href = uri;
iconUrl = a.origin + '/favicon.ico';
if (iconTargetSize) {
iconUrl += '#-moz-resolution=' + iconTargetSize + ',' + iconTargetSize;
}
DEBUG && console.log('Icon from favicon.ico');
}
return new Promise(resolve => {
resolve(iconUrl);
});
}
/**
* Same as above except the promise resolves as an object containing the blob
* of the icon and its size in pixels.
*
* @param uri {string}
* @param iconTargetSize {number}
* @param placeObj {Object}
* @param siteObj {Object}
* @returns {Promise}
*/
function getIconBlob(uri, iconTargetSize, placeObj = {}, siteObj = {}) {
return new Promise((resolve, reject) => {
getIcon(uri, iconTargetSize, placeObj, siteObj)
.then(iconUrl => {
// @todo Need a better syntax.
getStore().then(iconStore => {
iconStore.get(iconUrl).then(iconObj => {
if (!iconObj || !iconObj.timestamp ||
Date.now() - iconObj.timestamp >= ICON_CACHE_PERIOD) {
return fetchIconBlob(iconUrl)
.then(iconBlob => {
var img = document.createElement('img');
var icon = new Icon(img, uri);
icon.renderBlob(iconBlob, {
size: iconTargetSize,
onLoad: function(blob) {
var iconObj = {
blob: blob,
originalUrl: iconUrl.toString(),
timestamp: Date.now()
};
// We resolve here to avoid I/O blocking on
// dataStore and quicker display.
// Persisting to the dataStore takes place subsequently.
resolve(iconObj);
iconStore.add(iconObj, iconUrl);
},
onerror: function(e) {
reject(`Failed to fetch icon ${iconUrl}`);
}
});
})
.catch(err => {
reject(`Failed to fetch icon ${iconUrl}: ${err}`);
});
}
return resolve(iconObj);
}).catch(err => {
// We should fetch the icon and resolve the promise here, anyhow.
reject(`Failed to get icon from dataStore: ${err}`);
});
}).catch(err => {
// We should fetch the icon and resolve the promise here, anyhow.
reject(`Error opening the dataStore: ${err}`);
});
});
});
}
/**
* Same as above but set the image as the icon property of a gaia-app-icon
* element.
*
* @param icon {Object?}
* @param targetSize {number}
* @returns {Promise}
*/
function setElementIcon(icon, targetSize) {
return getIconBlob(icon.bookmark.url, targetSize,
icon.bookmark, icon.bookmark)
.then(iconObj => {
if (iconObj.blob) {
icon.icon = iconObj.blob;
return Promise.resolve();
} else if (icon.bookmark.icon) {
// We fallback to the bookmark.icon property if no icons were found.
return fetchIconBlob(icon.bookmark.icon)
.then(iconBlob => {
icon.icon = iconBlob;
return Promise.resolve();
}, Promise.reject.bind(Promise)); // XXXbz Why second arg?
}
return Promise.reject('No icon data found');
}, Promise.reject.bind(Promise)); // XXXbz Why bother with second arg?
}
function getBestIconFromWebManifest(webManifest, iconSize) {
var icons = webManifest.icons;
if (!icons) {
return null;
}
var maxSize = 10000;
var bestSize = maxSize;
var iconURL = null;
iconSize = iconSize || getDefaultIconSize();
icons.forEach((potentialIcon) => {
if (!iconURL) {
iconURL = potentialIcon.src;
}
var sizes = Array.from(potentialIcon.sizes);
var nearestSize = getNearestSize(sizes, iconSize, bestSize);
if (nearestSize !== bestSize &&
sizeIsNearer(nearestSize, bestSize, iconSize)) {
iconURL = potentialIcon.src;
bestSize = nearestSize;
}
});
return iconURL ? iconURL : null;
}
function _convertToWebManifestIcons(manifest, origin) {
return Object.keys(manifest.icons).map(function(size) {
var url = manifest.icons[size];
var sizes = [size + 'x' + size];
url = url.indexOf('http') > -1 ? url : origin + url;
return {
src: new URL(url),
sizes: sizes
};
});
}
// See bug 1041482, we will need to support better
// icons for different part of the system application.
// A web page have different ways to defining icons
// based on size, 'touch' capabilities and so on.
// From gecko we will receive all the rel='icon'
// defined which will contain as well the sizes
// supported in that file.
// This function will help to deliver the best suitable
// icon based on that definition list.
// The expected format is the following one:
//
// {
// '[uri 1]': {
// sizes: ['16x16 32x32 48x48', '60x60']
// },
// '[uri 2]': {
// sizes: ['16x16']
// }
// }
//
// iconSize is an additional parameter to specify a concrete
// size or the closest icon.
function getBestIconFromMetaTags(icons, iconSize) {
if (!icons) {
return null;
}
iconSize = iconSize || getDefaultIconSize();
var iconURL = null;
var bestSize = 10000;
Object.keys(icons).forEach((uri) => {
var potentialIcon = icons[uri];
if (!iconURL) {
// Handle the case of no size info in the whole list
iconURL = uri;
}
var sizes = Array.from(potentialIcon.sizes);
var nearestSize = getNearestSize(sizes, iconSize, bestSize);
if (nearestSize !== bestSize &&
sizeIsNearer(nearestSize, bestSize, iconSize)) {
iconURL = uri;
bestSize = nearestSize;
}
if (potentialIcon.rel === 'apple-touch-icon' ||
potentialIcon.rel === 'apple-touch-icon-precomposed') {
var moreInfoUrl = 'https://developer.mozilla.org/en-US/' +
'Apps/Build/Icon_implementation_for_apps#General_icons_for_web_apps';
console.warn('Warning: The apple-touch icons are being used ' +
'as a fallback only. They will be deprecated in ' +
'the future. See ' + moreInfoUrl);
}
});
return iconURL || null;
}
// Given an array of size strings e.g. (64x64), a target iconSize
// and optionally a best-so-far-size,
// return the nearest match
function getNearestSize(sizes, iconSize, bestSize) {
var bogusSize = 10000;
if (!bestSize) {
bestSize = bogusSize;
}
var nearestSize = sizes.reduce(function(nearestSize, sizeString, idx) {
var size = widthFromSizeString(sizeString);
if (isNaN(size)) {
return nearestSize;
}
if (sizeIsNearer(size, nearestSize, iconSize)) {
return size;
} else {
return nearestSize;
}
}, bestSize);
return nearestSize === bogusSize ? -1 : nearestSize;
}
// Given an icon size by string YYxYY returns the
// width measurement, so will assume this will be
// used by strings that identify a square size.
function widthFromSizeString(size) {
size = size || '';
var xIndex = size.indexOf('x');
if (!xIndex) {
return NaN;
}
return parseInt(size.substr(0, xIndex));
}
/**
* Return a promise that resolves to a dataStore for icons.
*
* @returns {Promise}
*/
function getStore() {
return new Promise(resolve => {
if (dataStore) {
return resolve(dataStore);
}
navigator.getDataStores('icons').then(stores => {
dataStore = stores[0];
return resolve(dataStore);
});
});
}
/**
* Clear all the icons in the store.
*
* @returns {Promise}
*/
function clear() {
return getStore().then(iconStore => {
iconStore.clear();
});
}
/**
* Return a promise that resolves to an object containing the blob url
* in pixels of an icon given its URL `iconUrl`.
*
* @param {string} iconUrl
* @returns {Promise}
*/
function fetchIcon(iconUrl) {
return new Promise((resolve, reject) => {
fetchIconBlob(iconUrl)
.then((iconBlob) => {
var img = document.createElement('img');
img.src = URL.createObjectURL(iconBlob);
img.onload = () => {
var iconSize = Math.max(img.naturalWidth, img.naturalHeight);
resolve({
blob: iconBlob,
url: iconUrl,
size: iconSize,
timestamp: Date.now()
});
};
img.onerror = () => {
reject(new Error(`Error while loading image.`));
};
})
.catch((e) => {
reject(new Error(`Error while loading image: ${e}`));
});
});
}
/**
* Return a promise that resolves to an object containing the blob
* given its URL `iconUrl`.
*
* @param {string} iconUrl
* @returns {Promise}
*/
function fetchIconBlob(iconUrl) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest({
mozAnon: true,
mozSystem: true
});
xhr.open('GET', iconUrl, true);
xhr.responseType = 'blob';
xhr.timeout = FETCH_XHR_TIMEOUT;
// Remember that send() can throw for some non http protocols.
// The promise wrapper here protects us.
xhr.send();
xhr.onload = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
var iconBlob = xhr.response;
resolve(iconBlob);
return;
}
reject(new Error(
`Got HTTP status ${xhr.status} trying to load ${iconUrl}.`));
};
xhr.onerror = xhr.ontimeout = () => {
reject(new Error(`Error while getting ${iconUrl}.`));
};
});
}
exports.IconsHelper = {
getIcon: getIcon,
getIconBlob: getIconBlob,
setElementIcon: setElementIcon,
getBestIconFromWebManifest: getBestIconFromWebManifest,
getBestIconFromMetaTags: getBestIconFromMetaTags,
fetchIcon: fetchIcon,
fetchIconBlob: fetchIconBlob,
get defaultIconSize() {
return getDefaultIconSize();
},
clear: clear,
// Make public for unit test purposes.
getNearestSize: getNearestSize,
};
})(window);

View file

@ -0,0 +1,121 @@
/*
This file implements window.setIdleTimeout() and
window.clearIdleTimeout(). They look like setTimeout() except the
setIdleTimeout() function takes two callbacks:
setIdleTimeout(idleCallback, activeCallback, ms):
idleCallback will fire after specific microsecond of idle.
The time it takes will calculated from the time
setIdleTimeout() is called and resets as user interacts.
activeCallback will fire when the first user action *after*
idleCallback fires
returns id.
clearIdleTimeout(id):
takes the id returns from setIdleTimeout() and cancels it.
*/
// Wrap everything into a closure so we will not expose idleTimerRegistry
(function idleTimerAsAIdleObserverWrapper(win) {
'use strict';
// stuff the 0th element so id is always a truey value
var idleTimerRegistry = [undefined];
// setIdleTimeout()
win.setIdleTimeout = function setIdleTimeout(idleCallback,
activeCallback,
ms) {
var idleFired = false;
var idleTimer = {};
var triggerIdleCallback = function() {
// remove the timer
idleTimer.clearTimer();
// set idleFired to true
idleFired = true;
// fire the real idleCallback
idleCallback();
};
idleTimer.timer = setTimeout(triggerIdleCallback, ms);
idleTimer.clearTimer = function clearIdleTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
};
// Create an idle observer with a very short time as
// we are not interested in when onidle fires (since it's inaccuate),
// instead, we need to know when onactive callback calls.
idleTimer.observer = {
onidle: function observerReportIdle() {
// Once the onidle fires, the next user action will trigger onactive.
// No need to reset the timer if it has been set in the init stage.
if (idleTimer.timer) {
return;
}
// The onidle callback will delay 1 second after user stops actions
// so reduce 1000ms here to counteract it.
var time = ms - 1000;
idleTimer.timer = setTimeout(triggerIdleCallback, time < 0 ? 0 : time);
},
onactive: function observerReportActive() {
// Remove the timer set by onidle
idleTimer.clearTimer();
// If idleCallback is not called yet,
// we should not trigger activeCallback here
if (!idleFired) {
return;
}
// fire the real activeCallback
activeCallback();
// reset the flag
idleFired = false;
// After a short time, onidle will fire again.
// timer will be registered there again.
},
time: 1
};
// Register the idleObserver
navigator.addIdleObserver(idleTimer.observer);
// Push the idleTimer object to the registry
idleTimerRegistry.push(idleTimer);
// return the id so people can do clearIdleTimeout();
return (idleTimerRegistry.length - 1);
};
// clearIdleTimeout()
win.clearIdleTimeout = function clearIdleTimeout(id) {
if (!idleTimerRegistry[id]) {
return;
}
// Get the idleTimer object and remove it from registry
var idleTimer = idleTimerRegistry[id];
idleTimerRegistry[id] = undefined;
// Properly clean it up, make sure we will never heard from
// those callbacks ever again.
navigator.removeIdleObserver(idleTimer.observer);
idleTimer.clearTimer();
};
})(this);

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