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.
|
|
@ -0,0 +1 @@
|
|||
;;;window.COMPONENTS_BASE_URL='/shared/elements/';
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
373
assets/webapps/system.gaiamobile.org/shared/elements/gaia-header/dist/gaia-header.js
vendored
Normal 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)});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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*/
|
||||
|
|
@ -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));
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 583 B |
|
After Width: | Height: | Size: 583 B |
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 312 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 245 B |
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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); }
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 626 B |
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 755 B |
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 748 B |
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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));
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
84
assets/webapps/system.gaiamobile.org/shared/js/apn_helper.js
Normal 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);
|
||||
|
|
@ -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));
|
||||
180
assets/webapps/system.gaiamobile.org/shared/js/async_storage.js
Normal 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
|
||||
};
|
||||
}());
|
||||
|
||||
137
assets/webapps/system.gaiamobile.org/shared/js/basket_client.js
Normal 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();
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
195
assets/webapps/system.gaiamobile.org/shared/js/custom_dialog.js
Normal 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;
|
||||
}
|
||||
|
||||
};
|
||||
}());
|
||||
|
||||
|
|
@ -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);
|
||||
})();
|
||||
|
|
@ -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);
|
||||
|
|
@ -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));
|
||||
|
|
@ -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
|
||||
};
|
||||
}());
|
||||
|
|
@ -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
|
||||
};
|
||||
}());
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}());
|
||||
4
assets/webapps/system.gaiamobile.org/shared/js/dump.js
Normal 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');});}());
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
39
assets/webapps/system.gaiamobile.org/shared/js/gzip/gzip.js
Normal 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
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
36
assets/webapps/system.gaiamobile.org/shared/js/gzip/gzip.min.js
vendored
Normal 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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -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));
|
||||
|
|
@ -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));
|
||||
|
|
@ -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();
|
||||
186
assets/webapps/system.gaiamobile.org/shared/js/icc_helper.js
Normal 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);
|
||||
441
assets/webapps/system.gaiamobile.org/shared/js/icons_helper.js
Normal 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);
|
||||
121
assets/webapps/system.gaiamobile.org/shared/js/idletimer.js
Normal 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);
|
||||