Publish Vulpes Preview 15 sources for desktop, Android and Tundra

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

View file

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

View file

@ -0,0 +1,14 @@
<element name="confirmation-message" extends="form">
<template>
<section>
<h1>Confirmation title</h1>
<p>Confirmation message</p>
</section>
<menu>
<button>Action 1</button>
<button>Action 2</button>
</menu>
</template>
</element>

View file

@ -0,0 +1,12 @@
<element name="contact-in-overlay" extends="button">
<template>
<div class="js-name ci__name ellipsis" dir="auto"></div>
<div class="ci__details">
<span class="js-tel-type ci__tel-type ellipsis" dir="auto"></span>
<span class="js-separator"></span>
<span class="js-tel ellipsis" dir="ltr"></span>
</div>
</template>
</element>

View file

@ -0,0 +1,10 @@
<element name="contact-list-overlay" extends="form">
<template>
<header></header>
<menu id="contact-list">
<button id="contact-list-overlay-cancel" data-l10n-id="cancel">Cancel</button>
</menu>
</template>
</element>

View file

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

View file

@ -0,0 +1,74 @@
/* Some BiDi-proof icons
* In the future, we might handle this at the font level, see
* https://bugzilla.mozilla.org/show_bug.cgi?id=1214127
*/
[data-icon="back"]:-moz-dir(ltr):before {
content: 'left';
}
[data-icon="back"]:-moz-dir(rtl):before {
content: 'right';
}
[data-icon="forward"]:-moz-dir(ltr):before {
content: 'right';
}
[data-icon="forward"]:-moz-dir(rtl):before {
content: 'left';
}
[data-icon="back-light"]:-moz-dir(ltr):before {
content: 'left-light';
}
[data-icon="back-light"]:-moz-dir(rtl):before {
content: 'right-light';
}
[data-icon="forward-light"]:-moz-dir(ltr):before {
content: 'right-light';
}
[data-icon="forward-light"]:-moz-dir(rtl):before {
content: 'left-light';
}
[data-icon="arrow-back"]:-moz-dir(ltr):before {
content: 'arrow-left';
}
[data-icon="arrow-back"]:-moz-dir(rtl):before {
content: 'arrow-right';
}
[data-icon="arrow-forward"]:-moz-dir(ltr):before {
content: 'arrow-right';
}
[data-icon="arrow-forward"]:-moz-dir(rtl):before {
content: 'arrow-left';
}
[data-icon="clear-input"]:-moz-dir(ltr):before {
content: 'clear-input-left';
}
[data-icon="clear-input"]:-moz-dir(rtl):before {
content: 'clear-input-right';
}
[data-icon="expand"]:-moz-dir(ltr):before {
content: 'expand-left';
}
[data-icon="expand"]:-moz-dir(rtl):before {
content: 'expand-right';
}
[data-icon="send"]:-moz-dir(ltr):before {
content: 'send-right';
}
[data-icon="send"]:-moz-dir(rtl):before {
content: 'send-left';
}
[data-icon="edit"]:-moz-dir(ltr):before {
content: 'edit-left';
}
[data-icon="edit"]:-moz-dir(rtl):before {
content: 'edit-right';
}

View file

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

View file

@ -0,0 +1,246 @@
/*
Gaia theme variables
*/
:root {
/** Grey Colors
---------------------------------------------------------*/
--color-alpha: #333333;
--color-beta: #ffffff;
--color-gamma: #4d4d4d;
--color-delta: #5f5f5f;
--color-epsilon: #858585;
--color-zeta: #a6a6a6;
--color-eta: #c7c7c7;
--color-theta: #e7e7e7;
--color-iota: #f4f4f4;
/** Brand Colors
---------------------------------------------------------*/
--color-darkblue: #00539f;
--color-blue: #00caf2;
--color-turquoise: #27c8c2;
--color-darkorange: #e66000;
--color-orange: #ff9500;
--color-yellow: #ffcb00;
--color-violet: #c40c84;
--color-warning: #fbbd3c;
--color-destructive: #e2443a;
--color-preffered: #00ba91;
/** Background
---------------------------------------------------------*/
--background: var(--color-beta);
--background-plus: var(--color-theta);
--background-minus: var(--color-zeta);
/** Borders
---------------------------------------------------------*/
--border-color: var(--color-theta);
/** Text Color
---------------------------------------------------------*/
--text-color: var(--color-gamma);
--text-color-minus: var(--color-eta);
--label-color: #000;
/** Button
---------------------------------------------------------*/
--button-background: var(--color-iota);
/** Links
---------------------------------------------------------*/
--link-color: var(--highlight-color);
/** Inputs
---------------------------------------------------------*/
--input-background: var(--color-beta);
--input-color: var(--color-alpha);
--input-clear-background: #909ca7;
/** Buttons
---------------------------------------------------------*/
--button-box-shadow:
inset 0 -1px 0 #d8d8d8,
inset 0 1px 0 #eeeeee;
--button-box-shadow-active: var(--button-box-shadow);
/** Header
---------------------------------------------------------*/
--header-disabled-button-color: rgba(255,255,255,0.3);
/** Transitions
---------------------------------------------------------*/
--transition-duration: 0.2s;
}
.theme-productivity {
/** Brand Color
---------------------------------------------------------*/
--highlight-color: var(--color-orange);
/** Header
---------------------------------------------------------*/
--header-background: var(--highlight-color);
--header-color: var(--color-beta);
}
.theme-communications {
/** Brand Color
---------------------------------------------------------*/
--highlight-color: var(--color-turquoise);
/** Links
---------------------------------------------------------*/
--link-color: #177874;
/** Header
---------------------------------------------------------*/
--header-background: var(--highlight-color);
--header-color: var(--color-beta);
--header-icon-color: var(--color-beta);
--header-button-color: var(--link-color);
}
.theme-settings {
/** Background
---------------------------------------------------------*/
--background: var(--color-iota);
--background-plus: var(--color-beta);
--background-minus: var(--color-epsilon);
--background-minus-minus: var(--color-zeta);
/** Highlight
---------------------------------------------------------*/
--highlight-color: var(--color-blue);
/** Borders
---------------------------------------------------------*/
--border-color: var(--color-theta);
/** Text Color
---------------------------------------------------------*/
--text-color: var(--color-gamma);
/** Titles
---------------------------------------------------------*/
--title-color: var(--background-minus);
/** Buttons
---------------------------------------------------------*/
--button-background: var(--background-plus);
/** Switch
---------------------------------------------------------*/
--switch-background: var(--background-minus-minus);
/** Checkbox
---------------------------------------------------------*/
--checkbox-border-color: var(--background-minus-minus);
/** Header
---------------------------------------------------------*/
--header-color: var(--title-color);
--header-background: var(--background);
--header-button-color: var(--highlight-color);
--header-disabled-button-color: var(--color-theta);
--header-action-button-color: var(--title-color);
}
.theme-media {
/** Highlight Color
---------------------------------------------------------*/
--highlight-color: var(--color-blue);
/** Background
---------------------------------------------------------*/
--background: var(--color-alpha);
--background-plus: var(--color-gamma);
--background-minus: #2B2B2B;
--background-minus-minus: #1a1a1a;
/** Text Color
---------------------------------------------------------*/
--text-color: var(--color-beta);
--label-color: var(--color-beta);
/** Button
---------------------------------------------------------*/
--button-background: var(--background-plus);
/** Header
---------------------------------------------------------*/
--header-background: var(--background);
--header-icon-color: var(--text-color);
--header-button-color: var(--highlight-color);
/** Borders
---------------------------------------------------------*/
--border-color: var(--color-gamma);
/** Inputs
---------------------------------------------------------*/
--input-background: var(--background-plus);
/** Text Input
---------------------------------------------------------*/
--text-input-background: var(--background-minus);
/** Switch
---------------------------------------------------------*/
--switch-head-border-color: var(--background-minus-minus);
--switch-background: var(--background-minus-minus);
/** Checkbox
---------------------------------------------------------*/
--checkbox-border-color: var(--background-minus-minus);
/** Buttons
---------------------------------------------------------*/
--button-box-shadow: none;
--button-box-shadow-active: none;
}

View file

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

After

Width:  |  Height:  |  Size: 294 B

View file

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

After

Width:  |  Height:  |  Size: 243 B

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 B

View file

@ -0,0 +1,185 @@
'use strict';
/* global ComponentUtils, LazyLoader */
/**
* GaiaSimPicker is a helper for dynamically generating menus for selecting SIM
* cards when making calls, sending SMS, etc. It also returns any currently
* in-use SIMs if there is an active call, but only when an app has mozTelephony
* permissions.
*
* Dependencies: gaia-menu web component.
*/
window.GaiaSimPicker = (function(win) {
// Extend from HTMLElement prototype
var proto = Object.create(HTMLElement.prototype);
// Allow baseurl to be overridden (used for demo page)
var baseurl = window.GaiaSimPickerBaseurl ||
'/shared/elements/gaia_sim_picker/';
proto.createdCallback = function() {
var shadow = this.createShadowRoot();
shadow.innerHTML = template;
this._menu = this.shadowRoot.querySelector('gaia-menu');
/**
* Workaround for bug 1100912, to be removed when bug is fixed:
* set internal gaia-menu dir, that's enough for directional-specific
* CSS to kick in.
*/
var updateDir = () => {
this._menu.dir = document.dir || 'ltr';
};
updateDir();
window.addEventListener('localized', updateDir);
LazyLoader.load(['/shared/js/component_utils.js'], function() {
ComponentUtils.style.call(this, baseurl);
}.bind(this));
navigator.mozL10n.ready(this._localizeShadowDom.bind(this));
};
proto._domBuilt = false;
proto._simSelectedCallbacks = [];
proto._dispatchSimSelected = function(cardIndex) {
var simSelectedEvent = new CustomEvent('gaiasimpicker-simselected', {
canBubble: true,
cancelable: true,
detail: {
cardIndex: cardIndex
}
});
if (this.dispatchEvent(simSelectedEvent)) {
// Only call the stored callbacks if the event wasn't preventDefaulted.
proto._simSelectedCallbacks.forEach(function(callback) {
callback(cardIndex);
});
proto._simSelectedCallbacks = [];
}
};
/**
* Localize the component manually as l10n attributes are not supported
* within the shadow dom. See also: bug 1026236.
*/
proto._localizeShadowDom = function() {
navigator.mozL10n.translateFragment(this.shadowRoot);
};
proto.getOrPick = function(defaultCardIndex,
phoneNumber,
simSelectedCallback) {
if (simSelectedCallback) {
this._simSelectedCallbacks.push(simSelectedCallback);
}
if (window.TelephonyHelper) {
var inUseSim = window.TelephonyHelper.getInUseSim();
if (inUseSim !== null) {
this._dispatchSimSelected(inUseSim);
return;
}
}
var dialViaElt = this.shadowRoot.querySelector('#sim-picker-dial-via');
if (phoneNumber) {
navigator.mozL10n.setAttributes(dialViaElt,
'gaia-sim-picker-dial-via-with-number',
{phoneNumber: phoneNumber});
} else {
dialViaElt.setAttribute('data-l10n-id', 'gaia-sim-picker-select-sim');
}
this._buildDom();
this._localizeShadowDom();
var simButtons = this.shadowRoot.querySelectorAll(
'button[data-card-index]');
for (var i = 0; i < simButtons.length; i++) {
if (simButtons[i].dataset.cardIndex == defaultCardIndex) {
simButtons[i].classList.add('is-default');
} else {
simButtons[i].classList.remove('is-default');
}
}
// we want to wait for l10n to happen before we display the UI
LazyLoader.load(['/shared/elements/gaia_menu/script.js'], function() {
navigator.mozL10n.once(function() {
this._menu.show();
this.focus();
}.bind(this));
}.bind(this));
};
proto._buildDom = function() {
if (this._domBuilt) {
return;
}
this._domBuilt = true;
var templateNode = this.shadowRoot.querySelector(
'#sim-picker-button-template');
for (var i = 0; i < navigator.mozIccManager.iccIds.length; i++) {
var clonedNode = templateNode.cloneNode(true);
clonedNode.dataset.cardIndex = i;
clonedNode.addEventListener('click', this);
var button = clonedNode.querySelector('.js-sim-picker-button');
// For example only; l10n will overwrite this.
button.textContent = 'SIM' + (i+1);
navigator.mozL10n.setAttributes(
button, 'gaia-sim-picker-button', {n: i + 1});
templateNode.parentNode.insertBefore(clonedNode, templateNode);
}
templateNode.remove();
};
proto.handleEvent = function(e) {
if (e) {
e.preventDefault();
}
if (e.target.nodeName !== 'BUTTON') {
return;
}
if (e.target.dataset.cardIndex) {
this._dispatchSimSelected(parseInt(e.target.dataset.cardIndex));
} else {
// Clear the stored callbacks anyways since the user must have hit cancel.
this._simSelectedCallbacks = [];
}
this._menu.hide();
};
var stylesheet = baseurl + '../gaia_menu/style.css';
// TODO: We should make this a template node and use template.cloneNode, but
// nested web components don't work yet using this method.
var template = '<style scoped>' +
'@import url(' + stylesheet + ');</style>' +
`<gaia-menu hidden>
<header id="sim-picker-dial-via">Dial 12345 via</header>
<button id="sim-picker-button-template">
<span class="js-sim-picker-button">
SIMX
</span>
<span class="sim-default" data-l10n-id="gaia-sim-picker-button-default">
(default)
</span>
</button>
</gaia-menu>`;
// Register and return the constructor
return document.registerElement('gaia-sim-picker', { prototype: proto });
})(window);

View file

@ -0,0 +1,9 @@
.sim-default {
display: none;
margin-inline-start: 0.15rem;
font-style: italic;
}
.is-default .sim-default {
display: initial;
}

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

View file

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

View file

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

View file

@ -0,0 +1 @@
;'use strict';(function(exports){var AccessibilityHelper={setAriaSelected:function ah_setAriaSelected(selectedTab,tabs){Array.prototype.forEach.call(tabs,function setAriaSelectedAttr(tab){tab.setAttribute('aria-selected',tab===selectedTab?'true':'false');});}};exports.AccessibilityHelper=AccessibilityHelper;})(window);

View file

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

View file

@ -0,0 +1,14 @@
;;;;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(){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){return;}
if(n===0||advanced){return;}
advanced=true;cursor.advance(n);};req.onerror=function keyOnError(){console.error('Error in asyncStorage.key(): ',req.error.name);};},function onComplete(){var cursor=req.result;callback(cursor?cursor.key:null);});}
return{getItem:getItem,setItem:setItem,removeItem:removeItem,clear:clear,length:length,key:key};}());

View file

@ -0,0 +1,18 @@
;'use strict';var utils=window.utils||{};utils.binarySearch=function(target,array,options){var arrayField=options.arrayField,transformFunction=options.transformFunction,compareFunction=options.compareFunction;function getItem(array,index){var item=array[index];if(arrayField){item=item[arrayField];if(typeof transformFunction==='function'){item=transformFunction(item);}}
return item;}
function compare(target,item){var out;if(typeof compareFunction==='function'){out=compareFunction(target,item);}
else{if(typeof target==='string'){out=target.localeCompare(item);}
else{out=target.toString().localeCompare(item);}}
return out;}
var from=options.from;if(typeof from==='undefined'){from=0;}
var to=options.to;if(typeof to==='undefined'){to=array.length-1;}
if(to<from){return[];}
var middleIndex=Math.floor((to-from)/2);var item=getItem(array,from+middleIndex);var compareResult=compare(target,item);if(compareResult===0){var results=[from+middleIndex];var next=from+middleIndex+1;var finish=false;while(next<=(array.length-1)&&!finish){item=getItem(array,next);if(compare(target,item)===0){results.push(next);}
else{finish=true;}
next++;}
finish=false;next=from+middleIndex-1;while(next>=0&&!finish){item=getItem(array,next);if(compare(target,item)===0){results.push(next);}
else{finish=true;}
next--;}
return results;}
else if(compareResult<0){return utils.binarySearch(target,array,{from:from,to:to-middleIndex-1,arrayField:arrayField,transformFunction:transformFunction,compareFunction:compareFunction});}
else{return utils.binarySearch(target,array,{from:from+middleIndex+1,to:to,arrayField:arrayField,transformFunction:transformFunction,compareFunction:compareFunction});}};

View file

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

View file

@ -0,0 +1,200 @@
/* exported ConfirmDialog */
'use strict';
var ConfirmDialog = (function() {
var isAnimationPlaying = false;
var isShown = false;
var doc = document; try { if (!document.getElementById("confirmation-message")) doc = parent.document; } catch (_) {}
var screen = doc.getElementById('confirmation-message');
var titleNode = screen.querySelector('h1');
var messageNode = screen.querySelector('p');
var action1Node = screen.querySelector('button:nth-of-type(1)');
var action1Callback;
var action2Node = screen.querySelector('button:nth-of-type(2)');
var action2Callback;
var oldzIndex;
var callBackAndPreventDefault = function callBackAndPreventDefault(ev) {
if (ev.target === action1Node) {
action1Callback();
ev.preventDefault();
return false;
} else if (ev.target === action2Node) {
action2Callback();
ev.preventDefault();
return false;
}
};
var _hide = function _hide() {
// Reset the 'confirmation-message' element and its children to their
// initial values and hide it.
if (!isShown) {
return;
}
titleNode.removeAttribute('data-l10n-id');
titleNode.className = '';
messageNode.removeAttribute('data-l10n-id');
messageNode.className = '';
action1Node.removeAttribute('data-l10n-id');
action1Node.onclick = null;
action2Node.removeAttribute('data-l10n-id');
action2Node.onclick = null;
screen.classList.remove('fade-in');
screen.classList.add('fade-out');
isAnimationPlaying = true;
// Custom event that can be used to apply (screen reader) visibility
// changes.
window.dispatchEvent(new CustomEvent('confirmdialoghiding'));
screen.addEventListener('animationend', function cd_fadeOut(ev) {
action1Node.className = '';
action2Node.className = '';
isAnimationPlaying = false;
screen.removeEventListener('animationend', cd_fadeOut);
screen.classList.add('no-opacity');
screen.classList.add('hide');
isShown = false;
});
};
/**
* Method that shows the dialog
* @param {String} title the title of the dialog. null or empty for
* no title.
* @param {String} msg message for the dialog.
* @param {Object} action1 {title, isDanger, callback} object.
* @param {Object} action2 {title, isDanger, callback} object.
*/
var _show = function _show(title, msg, action1, action2, options) {
var setL10nAttributes = function (element, options){
if ('string' === typeof options) {
document.l10n.setAttributes(element, options);
}
if(options.id) {
document.l10n.setAttributes(element, options.id, options.args);
}
};
if (options && options.zIndex) {
oldzIndex = screen.style.zIndex;
screen.style.zIndex = options.zIndex;
}
else {
oldzIndex = null;
}
if (title) {
setL10nAttributes(titleNode, title);
} else {
titleNode.classList.add('hide');
}
if (msg) {
setL10nAttributes(messageNode, msg);
} else {
messageNode.classList.add('hide');
}
if (action1) {
if (action1.title) {
setL10nAttributes(action1Node, action1.title);
}
if (action1.isDanger) {
action1Node.classList.add('danger');
}
if (action1.isRecommend) {
action1Node.classList.add('recommend');
}
if (action1.callback) {
action1Callback = action1.callback;
action1Node.onclick = callBackAndPreventDefault;
}
if (!action2) {
action2Node.classList.add('hide');
action1Node.classList.add('full');
}
}
if (action2) {
if (action2.title) {
setL10nAttributes(action2Node, action2.title);
}
if (action2.isDanger) {
action2Node.classList.add('danger');
}
if (action2.isRecommend) {
action2Node.classList.add('recommend');
}
if (action2.callback) {
action2Callback = action2.callback;
action2Node.onclick = callBackAndPreventDefault;
}
if (!action1) {
action1Node.classList.add('hide');
action2Node.classList.add('full');
}
}
if (isShown) {
return;
}
screen.classList.remove('hide');
screen.classList.remove('fade-out');
screen.classList.add('fade-in');
isAnimationPlaying = true;
// Custom event that can be used to apply (screen reader) visibility
// changes.
window.dispatchEvent(new CustomEvent('confirmdialogshowing'));
screen.addEventListener('animationend', function cd_fadeIn(ev) {
isAnimationPlaying = false;
screen.removeEventListener('animationend', cd_fadeIn);
screen.classList.remove('no-opacity');
isShown = true;
});
};
return {
hide: function dialog_hide() {
// Reset the 'confirmation-message' element and its children to their
// initial values and hide it.
if (oldzIndex) {
screen.style.zIndex = oldzIndex;
oldzIndex = null;
}
if (!isAnimationPlaying) {
_hide();
return;
}
screen.addEventListener('animationend',
function cd_hideWhenFinished(ev) {
screen.removeEventListener('animationend', cd_hideWhenFinished);
_hide();
}
);
},
/**
* Method that shows the dialog
* @param {String} title the title of the dialog. null or empty for
* no title.
* @param {String} msg message for the dialog.
* @param {Object} action1 {title, isDanger, callback} object.
* @param {Object} action2 {title, isDanger, callback} object.
*/
show: function dialog_show(title, msg, action1, action2, options) {
if (!isAnimationPlaying) {
_show(title, msg, action1, action2, options);
return;
}
screen.addEventListener('animationend',
function cd_showWhenFinished(ev) {
screen.removeEventListener('animationend', cd_showWhenFinished);
_show(title, msg, action1, action2, options);
}
);
}
};
}());

View file

@ -0,0 +1,386 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* globals setImmediate, Normalizer */
/**
* ContactToVcard provides the functionality necessary to export from
* MozContacts to vCard 3.0 (https://www.ietf.org/rfc/rfc2426.txt). The reason
* to choose the 3.0 standard instead of the 4.0 one is that some systems
* most notoriously Android 4.x don't seem to be able to import vCard 4.0.
*/
(function(exports) {
'use strict';
/** Mapping between contact fields and equivalent vCard fields */
var VCARD_MAP = {
'fax' : 'FAX',
'faxoffice' : 'FAX,WORK',
'faxhome' : 'FAX,HOME',
'faxother' : 'FAX',
'home' : 'HOME',
'mobile' : 'CELL',
'pager' : 'PAGER',
'personal' : 'HOME',
'pref' : 'PREF',
'text' : 'TEXT',
'textphone' : 'TEXTPHONE',
'voice' : 'VOICE',
'work' : 'WORK'
};
var CRLF = '\r\n';
/** Field list to be skipped when converting to vCard */
var VCARD_SKIP_FIELD = ['fb_profile_photo'];
var VCARD_VERSION = '3.0';
var HEADER = 'BEGIN:VCARD' + CRLF + 'VERSION:' + VCARD_VERSION + CRLF;
var FOOTER = 'END:VCARD' + CRLF;
function blobToBase64(blob, cb) {
var reader = new FileReader();
reader.onload = function() {
var dataUrl = reader.result;
var base64 = dataUrl.split(',')[1];
cb(base64);
};
reader.readAsDataURL(blob);
}
function ISODateString(d) {
if (typeof d === 'string') {
d = new Date(d);
}
var str = d.toISOString();
// Remove the milliseconds field
return str.slice(0, str.indexOf('.')) + 'Z';
}
/**
* Given an array with contact fields (usually containing only one field),
* returns the equivalent vcard field
*
* @param {Array} sourceField source field from a MozContact
* @param {String} vcardField vCard field name
* @return {Array} Array of vCard string entries
*/
function fromContactField(sourceField, vcardField) {
if (!sourceField || !sourceField.length) {
return [];
}
// Goes to the entries in the given field (usually only one but potentially
// more) and transforms them into string-based, vCard ones.
return sourceField.map(function(field) {
var str = vcardField;
/**
* If the field doesn't have an equivalent in vcard standard.
* Incompatible fields are stored in `VCARD_SKIP_FIELD`.
*
* @type {boolean}
*/
var skipField = false;
var types = [];
// Checks existing types and converts them to vcard types if necessary
// and fill `types` array with the final types.
if (Array.isArray(field.type)) {
var fieldType = field.type.map(function(aType) {
var out = '';
if (aType) {
aType = aType.trim().toLowerCase();
if (VCARD_SKIP_FIELD.indexOf(aType) !== -1) {
skipField = true;
}
out = VCARD_MAP[aType] || aType;
}
return out;
});
types = types.concat(fieldType);
}
if (skipField) {
return;
}
if (field.pref && field.pref === true) {
types.push('PREF');
}
if (types.length) {
str += ';TYPE=' + types.join(',');
}
return str + ':' + (field.value || '');
});
}
function fromStringArray(sourceField, vcardField) {
if (!sourceField) {
return '';
}
return vcardField + ':' + sourceField.join(',');
}
function joinFields(fields) {
return fields.filter(function(f) { return !!f; }).join(CRLF);
}
function toBlob(vcard, type) {
return new Blob([vcard], {'type': type});
}
/**
* Convenience function that converts an array of contacts into a text/vcard
* blob. The blob is passed to the callback once the conversion is done.
*
* @param {Array} contacts An array of mozContact objects.
* @param {Function} callback A function invoked with the generated blob.
*/
function ContactToVcardBlob(contacts, callback, options) {
var targetType = options && options.type || 'text/vcard';
if(targetType.indexOf('charset') === -1) {
targetType += '; charset=utf-8';
}
if (typeof callback !== 'function') {
throw Error('callback() is undefined or not a function');
}
var str = '';
ContactToVcard(contacts, function append(vcards, nCards) {
str += vcards;
}, function success() {
str = str ? toBlob(str) : null;
callback(toBlob(str, targetType));
});
}
/**
* Converts an array of contacts to a string of vCards. The conversion is
* done in batches. For every batch the append callback is invoked with a
* string of vCards and the number of contacts in the batch. Once all
* contacts have been processed the success callback is invoked.
*
* @param {Array} contacts An array of mozContact objects.
* @param {Function} append A function taking two parameters, the first one
* will be passed a string of vCards and the second an integer
* representing the number of contacts in the string.
* @param {Function} success A function with no parameters that will be
* invoked once all the contacts have been processed.
* @param {Number} batchSize An optional parameter specifying the maximum
* number of characters that should be added to the output string
* before invoking the append callback. If this paramter is not
* provided a default value of 1MiB will be used instead.
*/
function ContactToVcard(contacts, append, success, batchSize, skipPhoto) {
var vCardsString = '';
var nextIndex = 0;
var cardsInBatch = 0;
batchSize = batchSize || (1024 * 1024);
if (typeof append !== 'function') {
throw Error('append() is undefined or not a function');
}
if (typeof success !== 'function') {
throw Error('append() is undefined or not a function');
}
/**
* Append the vCard obtained by converting the contact to the string of
* vCards and if necessary pass the string to the user-specified callback
* function. If we're not done processing all the contacts start processing
* the following one.
*
* @param {String} vcard The string obtained from the previously processed
* contact.
*/
function appendVCard(vcard) {
if (vcard.length > 0) {
vCardsString += HEADER + vcard + CRLF + FOOTER;
}
nextIndex++;
cardsInBatch++;
/* Invoke the user-provided callback if we've filled the current batch or
* if we don't have more contacts to process. */
if ((vCardsString.length > batchSize) ||
(nextIndex === contacts.length)) {
append(vCardsString, cardsInBatch);
cardsInBatch = 0;
vCardsString = '';
}
if (nextIndex < contacts.length) {
processContact(contacts[nextIndex]);
} else {
success();
}
}
/**
* Process a contact and invokes appendVCard with the resulting vCard
* string.
*
* @param {Object} contacts A mozContact object.
*/
function processContact(ct) {
if (navigator.mozContact && !(ct instanceof navigator.mozContact)) {
console.error('An instance of mozContact was expected');
setImmediate(function() { appendVCard(''); });
return;
}
/*
* N TYPE
* The structured type value corresponds, in
* sequence, to the Family Name, Given Name, Additional Names, Honorific
* Prefixes, and Honorific Suffixes. The text components are separated
* by the SEMI-COLON character (ASCII decimal 59). Individual text
* components can include multiple text values (e.g., multiple
* Additional Names) separated by the COMMA character (ASCII decimal
* 44). This type is based on the semantics of the X.520 individual name
* attributes. The property MUST be present in the vCard object.
**/
var n = 'n:' + ([
ct.familyName,
ct.givenName,
ct.additionalName,
ct.honorificPrefix,
ct.honorificSuffix
].map(function(f) {
f = f || [''];
return f.join(',') + ';';
}).join(''));
// vCard standard does not accept contacts without 'n' or 'fn' fields.
if (n === 'n:;;;;;' || !ct.name) {
setImmediate(function() { appendVCard(''); });
return;
}
var allFields = [
n,
fromStringArray(ct.name, 'FN'),
fromStringArray(ct.nickname, 'NICKNAME'),
fromStringArray(ct.category, 'CATEGORY'),
fromStringArray(ct.org, 'ORG'),
fromStringArray(ct.jobTitle, 'TITLE'),
fromStringArray(ct.note, 'NOTE'),
fromStringArray(ct.key, 'KEY')
];
if (ct.bday) {
allFields.push('BDAY:' + ISODateString(ct.bday));
}
if (ct.anniversary) {
allFields.push('ANNIVERSARY:' + ISODateString(ct.anniversary));
}
allFields.push.apply(allFields, fromContactField(ct.email, 'EMAIL'));
allFields.push.apply(allFields, fromContactField(ct.url, 'URL'));
allFields.push.apply(allFields, fromContactField(ct.tel, 'TEL'));
var adrs = fromContactField(ct.adr, 'ADR');
allFields.push.apply(allFields, adrs.map(function(adrStr, i) {
var orig = ct.adr[i];
return adrStr + ([
'',
'',
orig.streetAddress || '', orig.locality || '', orig.region || '',
orig.postalCode || '', orig.countryName || ''].join(';'));
}));
/**
* PHOTO TYPE
* The encoding MUST be reset to "b" using the ENCODING
* parameter in order to specify inline, encoded binary data. If the
* value is referenced by a URI value, then the default encoding of 8bit
* is used and no explicit ENCODING parameter is needed.
* Type value: A single value. The default is binary value. It can also
* be reset to uri value. The uri value can be used to specify a value
* outside of this MIME entity.
* Type special notes: The type can include the type parameter "TYPE" to
* specify the graphic image format type. The TYPE parameter values MUST
* be one of the IANA registered image formats or a non-standard image
* format.
*/
if (
(
typeof skipPhoto == 'undefined' ||
skipPhoto === false
) &&
ct.photo &&
ct.photo.length
) {
var photoMeta = ['PHOTO', 'ENCODING=b'];
var blob = ct.photo[0];
blobToBase64(blob, function(b64) {
if (blob.type) {
photoMeta.push('TYPE=' + blob.type);
}
allFields.push(photoMeta.join(';') + ':' + b64);
appendVCard(joinFields(allFields));
});
} else {
setImmediate(function() { appendVCard(joinFields(allFields)); });
}
}
processContact(contacts[0]);
}
// Generates a name for the contact returned as a vcard
function getVcardFilename(theContact) {
var out = '';
var givenName = Array.isArray(theContact.givenName) &&
theContact.givenName[0];
var familyName = Array.isArray(theContact.familyName) &&
theContact.familyName[0];
if (givenName) {
out = givenName;
}
if (familyName) {
if (out) {
out += '_';
}
out += familyName;
}
out = out || (Array.isArray(theContact.org) && theContact.org[0]);
out = out || (Array.isArray(theContact.tel) && theContact.tel[0] &&
( 'c' + '_' + theContact.tel[0].value) );
out = out || (Array.isArray(theContact.email) && theContact.email[0] &&
theContact.email[0].value);
out = Normalizer.toAscii(out).replace(/[\s+@#&?\+\$]/g, '');
if (out) {
return Promise.resolve(out + '.vcf');
}
return navigator.mozL10n.formatValue('noName').then(name => name + '.vcf');
}
exports.ContactToVcard = ContactToVcard;
exports.ContactToVcardBlob = ContactToVcardBlob;
exports.VcardFilename = getVcardFilename;
})(this);

View file

@ -0,0 +1,13 @@
;;(function(exports){'use strict';function getThumbnail(contact){return getOnePhoto(contact,'begin');}
function getFullResolution(contact){return getOnePhoto(contact,'end');}
function getOnePhoto(contact,position){if(!contact||!contact.photo||!contact.photo.length){return null;}
if(contact.photo.length===1){return contact.photo[0];}
var photos=contact.photo;var category=contact.category;if(Array.isArray(category)&&category.indexOf('fb_linked')!==-1){if(photos.length>=4){return photos[(position=='begin')?1:0];}
return photos[0];}
photos=photosBySize(contact);var index=(position=='begin')?0:photos.length-1;return photos[index];}
function photosBySize(contact){var photos=contact.photo.slice(0);photos.sort(function(p1,p2){if(size(p1)<size(p2)){return-1;}
if(size(p1)>size(p2)){return 1;}
return 0;});return photos;}
function size(photo){if(typeof photo=='string'){return photo.length;}
return photo.size;}
exports.ContactPhotoHelper={getThumbnail:getThumbnail,getFullResolution:getFullResolution};})(window);

View file

@ -0,0 +1,222 @@
'use strict';
/* exported ContactsButtons */
/* globals LazyLoader, MozActivity, MultiSimActionButton, Normalizer,
SmsIntegration, TelephonyHelper, utils */
var ContactsButtons = {
DEFAULT_TEL_TYPE: 'other',
DEFAULT_EMAIL_TYPE: 'other',
_listContainer: null,
_contactDetails: null,
_activityHandler: null,
init: function init(listContainer, contactDetails, activityHandler) {
this._listContainer = listContainer;
this._contactDetails = contactDetails;
this._activityHandler = activityHandler;
},
_enableCalls: function enableCalls() {
this._contactDetails.classList.remove('calls-disabled');
},
_disableCalls: function disableCalls() {
this._contactDetails.classList.add('calls-disabled');
},
_call: function call(phoneNumber, cardIndex) {
this._disableCalls();
var enableCalls = this._enableCalls.bind(this);
LazyLoader.load('/dialer/js/telephony_helper.js', () => {
TelephonyHelper.call(phoneNumber, cardIndex, enableCalls, enableCalls,
enableCalls, enableCalls);
});
},
/* For security reasons we cannot directly call MmiManager.send() thus we
* need to special-case MMI numbers. This function detects MMI numbers in
* their most common form; other forms (such as short-string MMI codes)
* don't make sense for the contacts app and thus aren't considered. */
_isMMI: function _isMMI(number) {
return number.charAt(number.length - 1) === '#';
},
// Check current situation and setup different listener for the button
_setupPhoneButtonListener: function setupPhoneButtonListener(button, number) {
var self = this;
if (this._activityHandler &&
this._activityHandler.currentActivityIsNot(['open'])) {
button.addEventListener('click', this._onPickNumber.bind(this));
} else if (this._isMMI(number)) {
button.addEventListener('click', this._onMMICode.bind(this));
} else if (navigator.mozTelephony) {
LazyLoader.load(['/shared/js/multi_sim_action_button.js'], function() {
/* jshint nonew: false */
new MultiSimActionButton(button, self._call.bind(self),
'ril.telephony.defaultServiceId',
() => number);
});
}
},
// If we are currently handing an activity, send the phone
// number as result of clicking in the phone button.
_onPickNumber: function onPickNumber(evt) {
var number = evt.target.dataset.tel;
if (this._activityHandler) {
this._activityHandler.postPickSuccess({ number: number });
}
},
// If the phone number stored in a contact is a MMI code,
// launch the dialer with that specific code.
_onMMICode: function onMMICode(evt) {
var number = evt.target.dataset.tel;
// For security reasons we cannot directly call MmiManager.send(). We
// need to show the MMI number in the dialer instead.
/* jshint nonew: false */
new MozActivity({
name: 'dial',
data: {
type: 'webtelephony/number',
number: number
}
});
},
_onSendSmsClicked: function onSendSmsClicked(evt) {
var tel = evt.target.dataset.tel;
LazyLoader.load('/shared/js/contacts/sms_integration.js', () => {
SmsIntegration.sendSms(tel);
});
},
_onEmailOrPickClick: function onEmailOrPickClick(evt) {
evt.preventDefault();
var email = evt.target.dataset.email;
this.sendEmailOrPick(email);
return false;
},
renderPhones: function cd_renderPhones(contact) {
if (!contact.tel) {
return;
}
var telLength = this._getLength(contact.tel);
for (var tel = 0; tel < telLength; tel++) {
var currentTel = contact.tel[tel];
var typeKey = currentTel.type;
var escapedType = Normalizer.escapeHTML(currentTel.type, true).trim();
var carrier = Normalizer.escapeHTML(currentTel.carrier || '', true) || '';
if (!escapedType) {
typeKey = this.DEFAULT_TEL_TYPE;
}
var telField = {
value: Normalizer.escapeHTML(currentTel.value, true) || '',
type: escapedType,
'type_l10n_id': typeKey,
carrier: carrier,
i: tel
};
var phonesTemplate =
document.querySelector('#phone-details-template-\\#i\\#');
var template = utils.templates.render(phonesTemplate, telField);
// Add event listeners to the phone template components
var sendSmsButton = template.querySelector('#send-sms-button-' + tel);
sendSmsButton.dataset.tel = telField.value;
sendSmsButton.addEventListener('click',
this._onSendSmsClicked.bind(this));
var callOrPickButton = template.querySelector('#call-or-pick-' + tel);
callOrPickButton.dataset.tel = telField.value;
this._setupPhoneButtonListener(callOrPickButton, telField.value);
if (carrier) {
var carrierWrapperElt = template.querySelector('.carrier-wrapper');
carrierWrapperElt.hidden = false;
}
this._listContainer.appendChild(template);
}
},
renderEmails: function cd_renderEmails(contact) {
if (!contact.email) {
return;
}
var emailLength = this._getLength(contact.email);
for (var email = 0; email < emailLength; email++) {
var currentEmail = contact.email[email];
var emailField = {
value: Normalizer.escapeHTML(currentEmail.value, true) || '',
'type_l10n_id': currentEmail.type,
i: email
};
var emailsTemplate =
document.querySelector('#email-details-template-\\#i\\#');
var template = utils.templates.render(emailsTemplate, emailField);
// Send MMS to the email address
var sendSmsButton =
template.querySelector('#send-sms-to-email-button-' + email);
sendSmsButton.dataset.tel = emailField.value;
sendSmsButton.addEventListener('click',
this._onSendSmsClicked.bind(this));
// Add event listeners to the phone template components
var emailButton = template.querySelector('#email-or-pick-' + email);
emailButton.dataset.email = emailField.value;
emailButton.addEventListener('click',
this._onEmailOrPickClick.bind(this));
this._listContainer.appendChild(template);
}
},
reMark: function(field, value, remarkClass) {
var selector = '[data-' + field + '="' + value + '"]';
var elements = this._listContainer.querySelectorAll(selector);
if (typeof remarkClass === 'undefined') {
remarkClass = 'remark';
}
for (var i = 0; i < elements.length; i++) {
elements[i].classList.add(remarkClass);
}
},
_getLength: function(prop) {
if (!prop || !prop.length) {
return 0;
}
return prop.length;
},
sendEmailOrPick: function(address) {
try {
// We don't check the email format, lets the email
// app do that
/* jshint nonew: false */
new MozActivity({
name: 'new',
data: {
type: 'mail',
URI: 'mailto:' + address
}
});
} catch (e) {
console.error('WebActivities unavailable? : ' + e);
}
}
};

View file

@ -0,0 +1,11 @@
;'use strict';window.ContactsCleaner=function(contacts){this.lcontacts=contacts;var total=contacts.length;var next=0;var self=this;var CHUNK_SIZE=5;var numResponses=0;var notifyClean=false;var mustHold=false;var holded=false;var mustFinish=false;this.start=function(){mustHold=holded=mustFinish=false;if(total>0){cleanContacts(0);}
else if(typeof self.onsuccess==='function'){window.setTimeout(self.onsuccess,0);}};this.hold=function(){mustHold=true;};this.finish=function(){mustFinish=true;if(holded){notifySuccess();}};this.resume=function(){mustHold=holded=mustFinish=false;window.setTimeout(function resume_clean(){cleanContacts(next);});};this.performClean=function(contact,number,cbs){var req=navigator.mozContacts.remove(utils.misc.toMozContact(contact));req.number=number;req.onsuccess=cbs.success;req.onerror=function(e){errorHandler(contact.id,e.target.error);};};function successHandler(e){if(notifyClean||typeof self.oncleaned==='function'){notifyClean=true;var cleaned=e.target.number;window.setTimeout(function(){self.oncleaned(cleaned);},0);}
continueCb();}
function errorHandler(contactid,error){if(typeof self.onerror==='function'){self.onerror(contactid,error);}
continueCb();}
function cleanContacts(from){for(var idx=from;idx<(from+CHUNK_SIZE)&&idx<total;idx++){var contact=contacts[idx];self.performClean(contact,idx,{success:successHandler});}}
function notifySuccess(){if(typeof self.onsuccess==='function'){window.setTimeout(self.onsuccess);}}
function continueCb(){next++;numResponses++;if(next<total&&numResponses===CHUNK_SIZE){numResponses=0;if(!mustHold&&!mustFinish){cleanContacts(next);}
else if(mustFinish&&!holded){notifySuccess();}
if(mustHold){holded=true;}}
else if(next>=total){notifySuccess();}}};

View file

@ -0,0 +1,67 @@
;(function(exports){'use strict';function MatcherObj(pdataProvider){var blankRegExp=/\s+/g;var FB_CATEGORY='facebook';var FB_LINKED='fb_linked';var dataProvider=pdataProvider;function MultipleMatcher(ptargets,pmatchingOptions){var next=0;var self=this;var targets=ptargets;var matchingOptions=pmatchingOptions;var finalMatchings={};function doMatchBy(target,callbacks){var options={filterValue:target,filterBy:matchingOptions.filterBy,filterOp:matchingOptions.filterOp};var req=dataProvider.find(options);req.onsuccess=function(){var matchings=req.result.filter(function(aResult){return filterFacebook(aResult,matchingOptions.linkParams);});var filterBy=options.filterBy;var sanitizedTarget=SimplePhoneMatcher.sanitizedNumber(target);var targetVariants=SimplePhoneMatcher.generateVariants(target);matchings.forEach(function(aMatching){if(matchingOptions.selfContactId===aMatching.id){return;}
var field=options.filterBy[0];var values=aMatching[field];values.forEach(function(aValue){var value=aValue.value;var sanitizedValue=SimplePhoneMatcher.sanitizedNumber(value);var valueMatched=false;if(targetVariants.length>1){if(targetVariants.indexOf(sanitizedValue)!==-1){valueMatched=true;}}else if(SimplePhoneMatcher.generateVariants(sanitizedValue).indexOf(sanitizedTarget)!==-1){valueMatched=true;}
if(valueMatched){var matchings,matchingObj;if(!finalMatchings[aMatching.id]){matchingObj={matchings:{},matchingContact:aMatching};finalMatchings[aMatching.id]=matchingObj;matchingObj.matchings[filterBy[0]]=[];}
else{matchingObj=finalMatchings[aMatching.id];}
matchings=matchingObj.matchings[filterBy[0]];var sameValueMatchings=matchings.filter(function(matching){return(matching.matchedValue===value);});if(sameValueMatchings.length===0){matchings.push({'target':target,'matchedValue':value});}}});});carryOn();};req.onerror=function(e){window.console.error('Error while trying to do the matching',e.target.error.name);notifyMismatch(self);};}
function carryOn(){next++;if(next<targets.length){doMatchBy(targets[next],callbacks);}
else if(Object.keys(finalMatchings).length>0){notifyMatch(self,finalMatchings);}
else{notifyMismatch(self);}}
function matched(contacts){carryOn();}
var callbacks={onmatch:matched,onmismatch:carryOn};this.start=function(){doMatchBy(targets[0],callbacks);};}
function notifyMatch(obj,matchings){typeof obj.onmatch==='function'&&obj.onmatch(matchings);}
function notifyMismatch(obj){typeof obj.onmismatch==='function'&&obj.onmismatch();}
function matchBy(aContact,field,filterOper,callbacks,poptions){var options=poptions||{};var values=[];if(Array.isArray(aContact[field])){aContact[field].forEach(function(aField){if(field==='tel'){var variants=SimplePhoneMatcher.generateVariants(aField.value);variants.forEach(function(aVariant){values.push(aVariant);});}
else if(typeof aField.value==='string'){values.push(aField.value.trim());}});}
if(values.length>0){var matcher=new MultipleMatcher(values,{filterBy:[field],filterOp:filterOper,selfContactId:aContact.id,linkParams:options});matcher.onmatch=callbacks.onmatch;matcher.onmismatch=callbacks.onmismatch;matcher.start();}
else{notifyMismatch(callbacks);}}
function matchByTel(aContact,callbacks,options){matchBy(aContact,'tel','match',callbacks,options);}
function matchByEmail(aContact,callbacks,options){matchBy(aContact,'email','startsWith',callbacks,options);}
function doMatch(aContact,mode,callbacks){if(mode==='passive'){doMatchPassive(aContact,callbacks);}
else if(mode==='active'){doMatchActive(aContact,callbacks);}}
function doMatchTelAndEmail(aContact,callbacks,options){var localCbs={onmatch:function(telMatches){var matchCbs={onmatch:function(mailMatches){var allMatches=telMatches;Object.keys(mailMatches).forEach(function(aMatch){if(!allMatches[aMatch]){allMatches[aMatch]=mailMatches[aMatch];}
else{allMatches[aMatch].matchings.email=mailMatches[aMatch].matchings.email;}});notifyMatch(callbacks,allMatches);},onmismatch:function(){notifyMatch(callbacks,telMatches);}};matchByEmail(aContact,matchCbs,options);},onmismatch:function(){matchByEmail(aContact,callbacks,options);}};matchByTel(aContact,localCbs,options);}
function getSourceSimUrl(aContact){var out;if(Array.isArray(aContact.category)&&aContact.category.indexOf('sim')!==-1&&Array.isArray(aContact.url)){for(var j=0;j<aContact.url.length;j++){var aUrl=aContact.url[j];if(aUrl.type.indexOf('source')!==-1&&aUrl.type.indexOf('sim')!==-1){out=aUrl.value;}}}
return out;}
function doMatchPassive(aContact,callbacks){if(!hasName(aContact)){notifyMismatch(callbacks);return;}
var simUrl=getSourceSimUrl(aContact);if(simUrl){performPassiveMatchForSimContact(simUrl,aContact,callbacks);}
else{performPassiveMatch(aContact,callbacks);}}
function performPassiveMatchForSimContact(simUrl,aContact,callbacks){var localCbs={onmatch:function(results){var matchingFound=false;Object.keys(results).forEach(function(aResultId){var matchingContact=results[aResultId].matchingContact;var matchingUrl=getSourceSimUrl(matchingContact);if(matchingUrl&&matchingUrl===simUrl){matchingFound=true;var matchings={};matchings[aResultId]={matchingContact:matchingContact};callbacks.onmatch(matchings);return;}});if(!matchingFound){performPassiveMatch(aContact,callbacks);}},onmismatch:function(){performPassiveMatch(aContact,callbacks);}};matchByName(aContact,localCbs);}
function performPassiveMatch(aContact,callbacks){var matchingsFound={};var localCbs={onmatch:function(results){var names=[];Object.keys(results).forEach(function(aResultId){var mContact=results[aResultId].matchingContact;if(!hasName(mContact)){return;}
var targetFN=null;if(!isEmptyStr(aContact.familyName)){targetFN=Normalizer.toAscii(aContact.familyName[0].trim().toLowerCase()).replace(blankRegExp,'');}
var targetGN=null;if(!isEmptyStr(aContact.givenName)){targetGN=Normalizer.toAscii(aContact.givenName[0].trim().toLowerCase()).replace(blankRegExp,'');}
var targetName=(targetGN||'')+(targetFN||'');var mFamilyName=null;var mGivenName=null;if(!isEmptyStr(mContact.familyName)){mFamilyName=Normalizer.toAscii(mContact.familyName[0].trim().toLowerCase()).replace(blankRegExp,'');}
if(!isEmptyStr(mContact.givenName)){mGivenName=Normalizer.toAscii(mContact.givenName[0].trim().toLowerCase()).replace(blankRegExp,'');}
var mName=(mGivenName||'')+(mFamilyName||'');names.push({contact:mContact,familyName:mFamilyName,givenName:mGivenName,name:mName});var matchingList=names.filter(function(obj){return((obj.familyName===targetFN&&obj.givenName===targetGN)||(obj.name&&obj.name===targetName)&&(!Array.isArray(obj.contact.category)||obj.contact.category.indexOf(FB_CATEGORY)===-1));});matchingList.forEach(function(aMatching){var contact=aMatching.contact;matchingsFound[contact.id]={matchingContact:contact};});});reconcileResults(aContact,matchingsFound,results,callbacks);},onmismatch:function(){notifyMismatch(callbacks);}};doMatchTelAndEmail(aContact,localCbs);}
function doMatchActive(aContact,callbacks){var options={linkedMatched:{},linkedTo:getLinkedTo(aContact)};var localCbs={onmatch:function(results){var cbsName={onmatch:function(nameResults){Object.keys(nameResults).forEach(function(aId){if(!results[aId]){results[aId]=nameResults[aId];}
else{results[aId].matchings.name=nameResults[aId].matchings.name;}});notifyMatch(callbacks,results);},onmismatch:function(){notifyMatch(callbacks,results);}};matchByName(aContact,cbsName,options);},onmismatch:function(){matchByName(aContact,callbacks,options);}};doMatchTelAndEmail(aContact,localCbs,options);}
function notifyFindNameReady(){document.dispatchEvent(new CustomEvent('by_name_ready'));}
function matchByName(aContact,callbacks,options){var isSimContact=(Array.isArray(aContact.category)&&aContact.category.indexOf('sim')!==-1);if((isEmptyStr(aContact.familyName)||isEmptyStr(aContact.givenName))&&!isSimContact){notifyMismatch(callbacks);return;}
var finalResult={};var resultsByName=[];if(!isEmptyStr(aContact.name)){var targetName=aContact.name[0].trim();var reqName=dataProvider.find({filterValue:targetName,filterBy:['name'],filterOp:'startsWith'});reqName.onsuccess=function(){resultsByName=reqName.result.filter(function(aResult){return filterFacebook(aResult,options);});notifyFindNameReady();};reqName.onerror=function(e){window.console.warn('Error while trying to find by name: ',e.target.error.name);notifyFindNameReady();};}
else{notifyFindNameReady();}
if(isEmptyStr(aContact.familyName)){endOfMatchByName(finalResult,aContact,resultsByName,callbacks);}
else{var targetFamilyName=aContact.familyName[0].trim();var reqFamilyName=dataProvider.find({filterValue:targetFamilyName,filterBy:['familyName'],filterOp:'startsWith'});reqFamilyName.onsuccess=function(){var results=reqFamilyName.result;var givenNames=[];var targetGN=null;if(!isEmptyStr(aContact.givenName)){targetGN=Normalizer.toAscii(aContact.givenName[0].trim().toLowerCase()).replace(blankRegExp,'');}
results.forEach(function(mContact){if(mContact.id===aContact.id||isEmptyStr(mContact.givenName)){return;}
givenNames.push({contact:mContact,givenName:Normalizer.toAscii(mContact.givenName[0].trim().toLowerCase()).replace(blankRegExp,'')});});var finalMatchings=givenNames.filter(function(obj){var gn=obj.givenName;return((gn===targetGN||targetGN.startsWith(gn)||gn.startsWith(targetGN))&&filterFacebook(obj.contact,options));});finalMatchings.forEach(function(aMatching){finalResult[aMatching.contact.id]={matchings:{'name':[{target:targetFamilyName,matchedValue:Array.isArray(aMatching.contact.name)?aMatching.contact.name[0]:getCompleteName(aMatching.contact)}]},matchingContact:aMatching.contact};});if(resultsByName){endOfMatchByName(finalResult,aContact,resultsByName,callbacks);}
else{document.addEventListener('by_name_ready',function nameReady(){document.removeEventListener('by_name_ready',nameReady);endOfMatchByName(finalResult,aContact,resultsByName,callbacks);});}};reqFamilyName.onerror=function(e){window.console.error('Error while trying to find by familyName: ',e.target.error.name);notifyMismatch(callbacks);};}}
function getLinkedTo(contact){var out=null;if(Array.isArray(contact.category)){var idx=contact.category.indexOf(FB_LINKED);if(idx!==-1){out=contact.category[idx+1];}}
return out;}
function isFbLinked(contact){return(Array.isArray(contact.category)&&contact.category.indexOf(FB_LINKED)!==-1);}
function isFbContact(contact){return(Array.isArray(contact.category)&&contact.category.indexOf(FB_CATEGORY)!==-1);}
function filterFacebook(contact,linkParams){var out=false;if(!isFbContact(contact)){out=true;}
else if(isFbLinked(contact)){var linkedTo=getLinkedTo(contact);var targetUid=linkParams.linkedTo||'';var linkedMatched=linkParams.linkedMatched||{};if(targetUid===linkedTo){out=true;}
else if((Object.keys(linkedMatched).length===0||linkedMatched[linkedTo])&&!targetUid){linkedMatched[linkedTo]=linkedTo;out=true;}}
return out;}
function endOfMatchByName(finalResult,aContact,resultsByName,callbacks){resultsByName.forEach(function(aResult){if(aResult.id===aContact.id){return;}
var matchingObj={matchings:{'name':[{target:(Array.isArray(aContact.name)&&(typeof aContact.name[0]==='string'))?aContact.name[0].trim():getCompleteName(aContact),matchedValue:Array.isArray(aResult.name)?aResult.name[0]:getCompleteName(aResult)}]},matchingContact:aResult};if(!finalResult[aResult.id]){finalResult[aResult.id]=matchingObj;}
else{finalResult[aResult.id].matchings.name=matchingObj.matchings.name;}});if(Object.keys(finalResult).length>0){notifyMatch(callbacks,finalResult);}
else{notifyMismatch(callbacks);}}
function getCompleteName(contact){var givenName=Array.isArray(contact.givenName)?contact.givenName[0]:'';var familyName=Array.isArray(contact.familyName)?contact.familyName[0]:'';var completeName=givenName&&familyName?givenName+' '+familyName:givenName||familyName;return completeName;}
function isEmpty(collection){return((!Array.isArray(collection)||!collection[0])||(collection[0]&&collection[0].value&&!collection[0].value.trim()));}
function isEmptyStr(collection){return(!Array.isArray(collection)||(typeof collection[0]!=='string')||!(collection[0].trim()));}
function hasName(aContact){return(!isEmptyStr(aContact.givenName)||!isEmptyStr(aContact.familyName));}
function reconcileResults(incomingContact,nameMatches,phoneMailMatches,callbacks){var finalMatchings={};Object.keys(nameMatches).forEach(function(aNameMatching){var matchingContact=nameMatches[aNameMatching].matchingContact;var isPhoneMatching=Array.isArray(phoneMailMatches[aNameMatching].matchings.tel);var isMailMatching=Array.isArray(phoneMailMatches[aNameMatching].matchings.email);if(isPhoneMatching&&isMailMatching){finalMatchings[aNameMatching]=phoneMailMatches[aNameMatching];}
else if(isPhoneMatching&&(isEmpty(incomingContact.email)||isEmpty(matchingContact.email))){finalMatchings[aNameMatching]=phoneMailMatches[aNameMatching];}
else if(isMailMatching&&(isEmpty(incomingContact.tel)||isEmpty(matchingContact.tel))){finalMatchings[aNameMatching]=phoneMailMatches[aNameMatching];}});if(Object.keys(finalMatchings).length>0){notifyMatch(callbacks,finalMatchings);}
else{notifyMismatch(callbacks);}}
this.match=doMatch;Object.defineProperty(this,'dataProvider',{set:function(theProvider){dataProvider=theProvider;}});}
exports.Matcher=new MatcherObj(navigator.mozContacts);}(window));

View file

@ -0,0 +1,33 @@
;'use strict';var contacts=window.contacts||{};contacts.Merger=(function(){var DEFAULT_ADR_TYPE='home';var DEFAULT_TEL_TYPE='other';var DEFAULT_EMAIL_TYPE='other';function doMerge(pmasterContact,pmatchingContacts,callbacks){window.setTimeout(function contactsMerge(){mergeAll(pmasterContact,pmatchingContacts,filled,callbacks);},0);}
function doInMemoryMerge(pmasterContact,pmatchingContacts){return new Promise(function(resolve,reject){mergeAll(pmasterContact,pmatchingContacts,inMemoryMergeDone,{success:function(masterContact){resolve(masterContact);},error:function(err){reject(err);}});});}
function inMemoryMergeDone(callbacks,matchingContacts,masterContact){callbacks.success(masterContact);}
function isSimContact(contact){return Array.isArray(contact.category)&&contact.category.indexOf('sim')!==-1;}
function mergeAll(masterContact,matchingContacts,onFilled,callbacks){var emailsHash;var categoriesHash;var telsHash;var mergedContact={};var mergedPhoto;mergedContact.givenName=[];copyStringArray(masterContact.givenName,mergedContact.givenName);mergedContact.familyName=[];copyStringArray(masterContact.familyName,mergedContact.familyName);mergedContact.photo=masterContact.photo||[];mergedPhoto=mergedContact.photo;mergedContact.bday=masterContact.bday;mergedContact.anniversary=masterContact.anniversary;mergedContact.adr=masterContact.adr||[];telsHash={};mergedContact.tel=[];if(Array.isArray(masterContact.tel)){masterContact.tel.forEach(function(aTel){if(!telsHash[aTel.value]){aTel.type=(Array.isArray(aTel.type)?aTel.type:[aTel.type]);aTel.type[0]=aTel.type[0]||DEFAULT_TEL_TYPE;mergedContact.tel.push(aTel);}
var variants=SimplePhoneMatcher.generateVariants(aTel.value);variants.forEach(function(aVariant){telsHash[aVariant]=true;});});}
mergedContact.email=[];emailsHash={};populateEmails(masterContact.email,emailsHash,mergedContact.email);mergedContact.org=masterContact.org||[];mergedContact.category=[];categoriesHash={};populateNoDuplicates(masterContact.category,categoriesHash,mergedContact.category);mergedContact.url=masterContact.url||[];mergedContact.note=masterContact.note||[];matchingContacts.forEach(function(aResult){var theMatchingContact=aResult.matchingContact;var givenName=theMatchingContact.givenName;if(Array.isArray(givenName)&&givenName[0]){if(mergedContact.givenName.indexOf(givenName[0])===-1){if(mergedContact.givenName[0]&&mergedContact.givenName[0].trim()){mergedContact.givenName.push(givenName[0]);}
else{mergedContact.givenName[0]=givenName[0];}}}
var familyName=theMatchingContact.familyName;if(Array.isArray(familyName)&&familyName[0]){if(mergedContact.familyName.indexOf(familyName[0])===-1){if(mergedContact.familyName[0]&&mergedContact.familyName[0].trim()){mergedContact.familyName.push(familyName[0]);}
else{mergedContact.familyName[0]=familyName[0];}}}
if((!isDefined(masterContact.givenName)||!isDefined(masterContact.familyName))&&isSimContact(theMatchingContact)){if(isDefined(mergedContact.givenName)&&isDefined(mergedContact.familyName)&&mergedContact.familyName[0]===mergedContact.givenName[0]){if(!isDefined(masterContact.givenName)){mergedContact.givenName=[];mergedContact.name=mergedContact.familyName;}
if(!isDefined(masterContact.familyName)){mergedContact.familyName=[];mergedContact.name=mergedContact.givenName;}}}
if(!mergedContact.bday&&theMatchingContact.bday){mergedContact.bday=theMatchingContact.bday;}
if(!mergedContact.anniversary&&theMatchingContact.anniversary){mergedContact.anniversary=theMatchingContact.anniversary;}
if(isDefined(theMatchingContact.org)&&mergedContact.org.length===0){mergedContact.org=theMatchingContact.org;}
if(isDefined(theMatchingContact.category)){populateNoDuplicates(theMatchingContact.category,categoriesHash,mergedContact.category);}
populateEmails(theMatchingContact.email,emailsHash,mergedContact.email);if(Array.isArray(theMatchingContact.tel)){var theMatchings=aResult.matchings||{};var telMatchings=theMatchings.tel;theMatchingContact.tel.forEach(function(aTel){var theValue=aTel.value;var target=theValue,matchedValue='';if(telMatchings){var matchedFrom=telMatchings.filter(function(x){return(x.target===theValue||x.matchedValue===theValue);});if(matchedFrom[0]){target=matchedFrom[0].target;matchedValue=matchedFrom[0].matchedValue;}}
var variants=SimplePhoneMatcher.generateVariants(theValue);var alreadyPresent=false;for(var j=0;j<variants.length;j++){if(telsHash[variants[j]]){alreadyPresent=true;}}
if(!alreadyPresent){theValue=target.length>matchedValue.length?target:matchedValue;mergedContact.tel.push({type:aTel.type||[DEFAULT_TEL_TYPE],value:theValue,carrier:aTel.carrier,pref:aTel.pref});variants.forEach(function(aVariant){telsHash[aVariant]=true;});}});}
if(!Array.isArray(mergedPhoto)||mergedPhoto.length===0&&isDefined(theMatchingContact.photo)){var photo=ContactPhotoHelper.getFullResolution(theMatchingContact);mergedPhoto=[photo];}
populateField(theMatchingContact.adr,mergedContact.adr,DEFAULT_ADR_TYPE);populateField(theMatchingContact.url,mergedContact.url);populateField(theMatchingContact.note,mergedContact.note);});mergedContact.name=[((mergedContact.givenName[0]?mergedContact.givenName[0]:'')+' '+
(mergedContact.familyName[0]?mergedContact.familyName[0]:'')).trim()];fillMasterContact(masterContact,mergedContact,mergedPhoto,onFilled.bind(null,callbacks,matchingContacts));}
function filled(callbacks,matchingContacts,masterContact){var req=navigator.mozContacts.save(utils.misc.toMozContact(masterContact));req.onsuccess=function(){matchingContacts.forEach(function(aMatchingContact){if(aMatchingContact.matchingContact.id){var contact=aMatchingContact.matchingContact;navigator.mozContacts.remove(utils.misc.toMozContact(contact));}});if(typeof callbacks.success==='function'){callbacks.success(masterContact);}};req.onerror=function(){window.console.error('Error while saving merged Contact: ',req.error.name);typeof callbacks.error==='function'&&callbacks.error(req.error);};}
function isDefined(field){return(Array.isArray(field)&&field[0]&&((typeof field[0]==='string'&&field[0].trim().length>0)||typeof field[0]==='object'));}
function copyStringArray(source,dest){if(Array.isArray(source)){source.forEach(function(aVal){if(aVal&&aVal.trim()){dest.push(aVal);}});}}
function populateEmails(sourceEmails,hash,out){if(Array.isArray(sourceEmails)){sourceEmails.forEach(function(aEmail){aEmail.type=Array.isArray(aEmail.type)?aEmail.type:[aEmail.type];aEmail.type[0]=aEmail.type[0]||DEFAULT_EMAIL_TYPE;var value=aEmail.value;if(value&&value.trim()){value=value.toLowerCase();if(!hash[value]){aEmail.value=value;out.push(aEmail);hash[value]=true;}}});}}
function populateNoDuplicates(source,hash,out){if(Array.isArray(source)){source.forEach(function(aCat){if(!hash[aCat]){out.push(aCat);hash[aCat]=true;}});}}
function populateField(source,destination,defaultType){if(Array.isArray(source)){var stringifiedDestination=destination.map(function(element){return JSON.stringify(element);});source.forEach(function(as,index){if(stringifiedDestination.indexOf(JSON.stringify(as))===-1){if(defaultType&&(!as.type||!as.type[0])){as.type=[defaultType];}
destination.push(as);}});}}
function fillMasterContact(masterContact,mergedContact,mergedPhotos,done){var fields=['familyName','givenName','name','org','email','tel','bday','anniversary','adr','category','url','note','photo'];fields.forEach(function(aField){masterContact[aField]=mergedContact[aField];});if(!Array.isArray(mergedPhotos)||!mergedPhotos[0]||mergedPhotos.length>=2){done(masterContact);return;}
var mergedPhoto=mergedPhotos[0];utils.thumbnailImage(mergedPhoto,function gotTumbnail(thumbnail){if(mergedPhoto!==thumbnail){masterContact.photo=[mergedPhoto,thumbnail];}else{masterContact.photo=[mergedPhoto];}
done(masterContact);});}
return{merge:doMerge,inMemoryMerge:doInMemoryMerge};})();

View file

@ -0,0 +1,18 @@
;'use strict';var utils=window.utils||{};if(!utils.alphaScroll){(function initScrollbar(doc){var alphaScroll=utils.alphaScroll={};var scrollToCallback,jumper,overlay,overlayStyle,groupSelector,liSearch,liFavorites;var isScrolling=false;var alreadyRendered=false;var isDesktop=false;var P_SCROLLTO_CB='scrollToCb';var P_JUMPER='jumper';var P_OVERLAY='overlay';var P_GROUP_SELECTOR='groupSelector';var TRANSITION_DELAY='0.3s';var TRANSITION_DURATION='0.2s';var RESET_TRANSITION='0s';var offset=0,lastY=0;var isTouch='ontouchstart'in window;var touchstart=isTouch?'touchstart':'mousedown';var touchmove=isTouch?'touchmove':'mousemove';var touchend=isTouch?'touchend':'mouseup';var getY=(function getYWrapper(){return isTouch?function(e){return e.touches[0].pageY;}:function(e){return e.pageY;};})();var getTarget=(function getTargetWrapper(){if(isTouch){return function(e){var touch=e.touches[0];return document.elementFromPoint(touch.pageX,touch.pageY);};}else{return function(e){return e.target;};}})();alphaScroll.init=function(params,desktop){isDesktop=params&&params.desktop;if(alreadyRendered&&!isDesktop){return;}
scrollToCallback=params[P_SCROLLTO_CB];jumper=params[P_JUMPER];overlay=params[P_OVERLAY];groupSelector=params[P_GROUP_SELECTOR];overlay.textContent='';overlayStyle=overlay.style;jumper.addEventListener(touchstart,scrollStart);jumper.addEventListener(touchmove,scrollTo);jumper.addEventListener(touchend,scrollEnd);if(typeof jumper==='string'){jumper=document.querySelector(jumper);}
var template=jumper.querySelector('*[data-template]');if(!template){alreadyRendered=true;return;}
var frag=document.createDocumentFragment();var alphabet=[];for(var i=65;i<=90;i++){alphabet.push({anchor:String.fromCharCode(i)});}
alphabet.push({anchor:'#'});utils.templates.append(jumper,alphabet,frag);jumper.appendChild(frag);alreadyRendered=true;};alphaScroll.showGroup=function(name){var selector='[data-anchor="group-'+name+'"]';var group=jumper.querySelector(selector);if(group){group.classList.remove('hide');}};alphaScroll.hideGroup=function(name){var selector='[data-anchor="group-'+name+'"]';var group=jumper.querySelector(selector);if(group){group.classList.add('hide');}};function hideExtraItems(value){if(!liSearch){liSearch=jumper.querySelector('li[data-anchor="search-container"]');}
if(!liFavorites){liFavorites=jumper.querySelector('li[data-anchor="group-favorites"]');}
liSearch.hidden=value;liFavorites.hidden=value;}
alphaScroll.toggleFormat=function(type){hideExtraItems(type==='short');};function scrollTo(evt){evt.preventDefault();evt.stopPropagation();if(!isScrolling){return;}
var currentY=getY(evt);if(Math.abs(lastY-currentY)<offset/2&&!isDesktop){return;}
lastY=currentY;var elem=getTarget(evt);if(!elem){return;}
var dataset=elem.dataset;if(dataset.letter){overlay.textContent=dataset.letter;}else if(dataset.img){overlay.textContent='';var img=new Image();img.src='style/images/'+dataset.img;overlay.appendChild(img);}
var anch=dataset.anchor;var selector=anch==='group-#'?'group-und':anch;var domTarget=doc.querySelector('#section-'+selector+', #'+selector);if(!domTarget){return;}
scrollToCallback(domTarget,selector.replace('group-',''));}
function scrollStart(evt){var dataset=getTarget(evt).dataset;evt.preventDefault();evt.stopPropagation();if(!dataset.letter&&!dataset.img){return;}
offset=offset||jumper.querySelector('[data-anchor]').offsetHeight;overlayStyle.MozTransitionDelay=RESET_TRANSITION;overlayStyle.MozTransitionDuration=RESET_TRANSITION;overlayStyle.opacity='1';isScrolling=true;scrollTo(evt);}
function scrollEnd(evt){evt.preventDefault();evt.stopPropagation();var transitionDelay=TRANSITION_DELAY;var transitionDuration=TRANSITION_DURATION;if(overlay.textContent==='#'||overlay.textContent===''){transitionDelay=RESET_TRANSITION;transitionDuration=RESET_TRANSITION;}
overlayStyle.MozTransitionDelay=transitionDelay;overlayStyle.MozTransitionDuration=transitionDuration;overlayStyle.opacity='0';overlay.textContent=null;isScrolling=false;lastY=0;}
(function(doc){var images=doc.querySelectorAll('li[data-img]');Object.keys(images).forEach(function(value){var img=new Image();img.src='contacts/style/images/'+images[value].dataset.img;});}(doc));})(document);}

View file

@ -0,0 +1,19 @@
;'use strict';(function(){var CHUNK_SIZE=5;window.ContactsImporter=function(pContacts,pAccessToken,pConnector){this.contacts=Object.keys(pContacts);var contactsHash=pContacts;var access_token=pAccessToken;var total=this.contacts.length;var self=this;var serviceConnector=pConnector;var numImported=0;var mustHold=false;var holded=false;var mustFinish=false;var isOnLine=navigator.onLine;var numMergedDuplicated=0;window.addEventListener('online',onLineChanged);window.addEventListener('offline',onLineChanged);function onLineChanged(){isOnLine=navigator.onLine;}
function contactSaved(e){var cfdata=this;if(typeof self.oncontactimported==='function'){window.setTimeout(function(){self.oncontactimported(cfdata);},0);}
continueCb();}
function contactSaveError(err){window.console.error('Error while importing contact: ',err.name);if(typeof self.onerror==='function'){window.setTimeout(self.onerror.bind(null,err),0);}
continueCb();}
function saveMozContact(deviceContact,successCb,errorCb){var req=navigator.mozContacts.save(utils.misc.toMozContact(deviceContact));req.onsuccess=successCb;req.onerror=errorCb;}
function pictureReady(blobPicture){var serviceContact=this;var done=function(){var deviceContact=self.adapt(serviceContact);self.persist(deviceContact,contactSaved.bind(serviceContact),contactSaveError);};if(!blobPicture){done();return;}
utils.thumbnailImage(blobPicture,function gotThumbnail(thumbnail){if(blobPicture!==thumbnail){serviceContact.photo=[blobPicture,thumbnail];}else{serviceContact.photo=[blobPicture];}
done();});}
function pictureError(){window.console.error('Error while getting picture for contact: ',this.user_id);self.persist(self.adapt(this),contactSaved.bind(this),contactSaveError);}
function pictureTimeout(){window.console.warn('Timeout while getting picture for contact: ',this.user_id);self.persist(self.adapt(this),contactSaved.bind(this),contactSaveError);}
this.start=function(){numMergedDuplicated=0;mustHold=false;holded=false;mustFinish=false;importContacts(numImported);};this.hold=function(){mustHold=true;};this.finish=function(){mustFinish=true;if(holded){notifySuccess();}};this.resume=function(){mustHold=false;holded=false;mustFinish=false;window.setTimeout(function resume_import(){importContacts(numImported);},0);};this.persist=function(contactData,successCb,errorCb){var cbs={onmatch:function(matches){numMergedDuplicated++;contacts.adaptAndMerge(this,matches,{success:successCb,error:errorCb});}.bind(utils.misc.toMozContact(contactData)),onmismatch:function(){saveMozContact(this,successCb,function onMismatchError(evt){errorCb(evt.target.error);});}.bind(contactData)};Matcher.match(contactData,'passive',cbs);};this.adapt=function(serviceContact){return serviceConnector.adaptDataForSaving(serviceContact);};function importContacts(from){for(var i=from;i<from+CHUNK_SIZE&&i<total;i++){importContact(i);}}
function importContact(index){var serviceContact=contactsHash[self.contacts[index]];var callbacks={success:pictureReady.bind(serviceContact),error:pictureError.bind(serviceContact),timeout:pictureTimeout.bind(serviceContact)};if(isOnLine===true){serviceConnector.downloadContactPicture(serviceContact,access_token,callbacks);}
else{callbacks.success(null);}}
function notifySuccess(){if(typeof self.onsuccess==='function'){window.setTimeout(function do_success(){self.onsuccess(numImported,numMergedDuplicated);},0);}}
function continueCb(){numImported++;var next=numImported+CHUNK_SIZE-1;if(next<total){if(!mustHold&&!mustFinish){importContact(next);}
else if(mustFinish&&!holded){notifySuccess();}
if(mustHold){holded=true;}}
else if(numImported>=total){notifySuccess();}}};})();

View file

@ -0,0 +1,26 @@
;'use strict';if(!window.FacebookConnector){window.FacebookConnector=(function(){var nextUpdateTime;window.fb=window.fb||{};fb.operationsTimeout=parent.config?parent.config.operationsTimeout:null;var reusedFbContact=new fb.Contact();reusedFbContact.ready=true;function createFbImporter(clist,access_token){var out=new window.ContactsImporter(clist,access_token,this);out.persist=persistFbData;return out;}
function persistFbData(data,successCb,errorCb,options){var saveCbs={success:function(e){if(options==='not_match'){successCb();return;}
var cbs={onmatch:function(matches){Object.keys(matches).forEach(function(aMatchId){var fbContact=new
fb.Contact(matches[aMatchId].matchingContact);var mozContReq=fb.utils.getMozContact(data.uid);mozContReq.onsuccess=function(){var linkParams={uid:data.uid,mozContact:mozContReq.result};if(Array.isArray(data.photo)&&data.photo[0]){linkParams.photoUrl=data.pic_big;}
var req=fbContact.linkTo(linkParams);req.onsuccess=successCb;req.onerror=function error(e){window.console.error('Error while automatically linking : ',aMatchId,data.uid,e.target.name);successCb();};};mozContReq.onerror=function(e){window.console.error('Error getting mozContact data in automatic linking: ',e.target.error.name);successCb();};});},onmismatch:successCb};Matcher.match(data,'passive',cbs);},error:errorCb};saveFbContact(data,saveCbs.success,saveCbs.error);}
function saveFbContact(data,successCb,errorCb){var fbContact,successWrapperCb;if(reusedFbContact.ready){reusedFbContact.ready=null;fbContact=reusedFbContact;successWrapperCb=function onsuccess(e){reusedFbContact.ready=true;successCb(e);};}else{fbContact=new fb.Contact();successWrapperCb=successCb;}
fbContact.setData(data);var req=fbContact.save();req.onsuccess=successWrapperCb;req.onerror=errorCb;}
function setInfraForSync(existingContacts,friendsImported,callback){if(!fb.sync){(typeof callback==='function')&&callback();return;}
window.asyncStorage.getItem(fb.utils.ALARM_ID_KEY,function(data){if(!data||(existingContacts&&existingContacts.length===0)&&!friendsImported){fb.utils.setLastUpdate(nextUpdateTime,function(){var req=fb.sync.scheduleNextSync();if(typeof callback==='function'){req.onsuccess=callback;}});}});}
var UID_FILTER_IDX=5;var acc_tk;function buildFriendQuery(uid){var aquery1=[].concat(FRIENDS_QUERY);aquery1[UID_FILTER_IDX]='= '+uid;return aquery1.join('');}
var FRIENDS_QUERY=['SELECT uid, name, first_name, last_name, pic_big, current_location, ','middle_name, birthday_date, email, profile_update_time, ',' work, phones, hometown_location',' FROM user',' WHERE uid ','IN (SELECT uid1 FROM friend WHERE uid2=me())',' ORDER BY ','first_name'];var friendsQueryStr=FRIENDS_QUERY.join('');function contactDataLoaded(options,response){if(!response.error){nextUpdateTime=Date.now();var successCb=this.success;var friend=response.data[0];if(friend){var out1=self.adaptDataForShowing(friend);var photoCbs={};photoCbs.success=function(blobPicture){if(!blobPicture){pictureLoaded(out1,options,successCb);return;}
utils.thumbnailImage(blobPicture,function gotThumbnail(thumbnail){if(blobPicture!==thumbnail){out1.photo=[blobPicture,thumbnail];}else{out1.photo=[blobPicture];}
pictureLoaded(out1,options,successCb);});};self.downloadContactPicture(friend,acc_tk,photoCbs);}
else{window.console.error('FB: No Friend data found');this.error('No friend data found');}}
else{this.error(response.error);}}
function pictureLoaded(friend,options,success){var data=self.adaptDataForSaving(friend);persistFbData(data,function(){success({uid:friend.uid,url:friend.pic_big});},function error(err){console.error('Error while persisting: ',err);},options);window.asyncStorage.getItem(fb.utils.ALARM_ID_KEY,function(data){if(!data){fb.utils.setLastUpdate(nextUpdateTime,fb.sync.scheduleNextSync);}});}
function FacebookConnector(){}
FacebookConnector.prototype={listAllContacts:function(access_token,callbacks,options){if(options&&options.orderBy==='lastName'){FRIENDS_QUERY[FRIENDS_QUERY.length]='last_name';friendsQueryStr=FRIENDS_QUERY.join('');}
return fb.utils.runQuery(friendsQueryStr,{success:callbacks.success,error:callbacks.error,timeout:callbacks.timeout},access_token);},listDeviceContacts:function(callbacks){var req=fb.utils.getAllFbContacts();req.onsuccess=function successHandler(){callbacks.success(req.result);};req.onerror=callbacks.error;},getImporter:function(contactsList,access_token){return createFbImporter.bind(this)(contactsList,access_token);},importContact:function(uid,access_token,callbacks,options){acc_tk=access_token;var oneFriendQuery=buildFriendQuery(uid);var auxCallbacks={success:contactDataLoaded.bind(callbacks,options),error:callbacks.error,timeout:callbacks.timeout};fb.utils.runQuery(oneFriendQuery,auxCallbacks,access_token);},cleanContacts:function(contactsList,mode,cb){if(mode==='update'){var cleaner=new fb.utils.FbContactsCleaner(contactsList,mode);window.setTimeout(cleaner.start,0);cb(cleaner);}
else{var req=fb.utils.clearFbData();req.onsuccess=function(){cb(req.result);};req.onerror=function(e){window.console.error('Error while starting cleaning: ',e.target.error.name);cb(null);};}},adaptDataForShowing:function(source){var box=utils.misc.getPreferredPictureBox();var picWidth=box.width;var picHeight=box.height;var out=fb.friend2mozContact(source);out.contactPictureUri='https://graph.facebook.com/'+
source.uid+'/picture?type=square'+'&width='+picWidth+'&height='+picHeight+'&t='+Date.now();return out;},adaptDataForSaving:function fbAdapt(cfdata){var worksAt=fb.getWorksAt(cfdata);var address=fb.getAddresses(cfdata);var birthDate=null;if(cfdata.birthday_date&&cfdata.birthday_date.length>0){birthDate=fb.getBirthDate(cfdata.birthday_date);}
var fbInfo={bday:birthDate,org:[worksAt]};if(address){fbInfo.adr=address;}
if(cfdata.shortTelephone){fbInfo.shortTelephone=cfdata.shortTelephone;delete cfdata.shortTelephone;}
fbInfo.url=[];if(cfdata.photo){fbInfo.photo=cfdata.photo;if(cfdata.pic_big){fb.setFriendPictureUrl(fbInfo,cfdata.pic_big);}
delete cfdata.photo;}
cfdata.fbInfo=fbInfo;return cfdata;},getContactUid:function(deviceContact){return fb.getFriendUid(deviceContact);},get name(){return'facebook';},get automaticLogout(){return false;},downloadContactPicture:function(contact,access_token,callbacks){return fb.utils.getFriendPicture(contact.uid,callbacks.success,access_token,utils.misc.getPreferredPictureDetail());},oncontactsloaded:function(lfriends){nextUpdateTime=Date.now();fb.utils.setCachedNumFriends(lfriends.length);},oncontactsimported:function(existingContacts,friendsImported,cb){setInfraForSync(existingContacts,friendsImported,cb);fb.contacts.flush();},startSync:function(existingContacts,myFriendsByUid,syncFinished){var callbacks={success:syncFinished};fb.sync.startWithData(existingContacts,myFriendsByUid,callbacks);},scheduleNextSync:function(){fb.sync.scheduleNextSync();}};var self=new FacebookConnector();return self;})();}

View file

@ -0,0 +1,56 @@
;;'use strict';var fb=window.fb||{};fb.Contact=function(deviceContact,cid){var contactData;var devContact=deviceContact;var contactid=cid;function doGetFacebookUid(data){return fb.getFriendUid(data);}
function getFacebookUid(){return doGetFacebookUid(deviceContact);}
function setFacebookUid(value){doSetFacebookUid(deviceContact,value);}
function doSetFacebookUid(dcontact,value){if(!dcontact.category){dcontact.category=[];}
if(dcontact.category.indexOf(fb.CATEGORY)===-1){markAsFb(dcontact);}
var idx=dcontact.category.indexOf(fb.CATEGORY);dcontact.category[idx+2]=value;}
function markAsFb(dcontact){if(!dcontact.category){dcontact.category=[];}
if(dcontact.category.indexOf(fb.CATEGORY)===-1){dcontact.category.push(fb.CATEGORY);dcontact.category.push(fb.NOT_LINKED);}
return dcontact;}
function markAsLinked(dcontact,uid){if(!dcontact.category){dcontact.category=[];}
if(dcontact.category.indexOf(fb.LINKED)===-1){dcontact.category.push(fb.CATEGORY);dcontact.category.push(fb.LINKED);dcontact.category.push(uid);}
return dcontact;}
function promoteToLinked(dcontact){var idx=dcontact.category.indexOf(fb.NOT_LINKED);if(idx!=-1){dcontact.category[idx]=fb.LINKED;}}
this.setData=function(data){contactData=data;};Object.defineProperty(this,'uid',{get:getFacebookUid,set:setFacebookUid,enumerable:true,});Object.defineProperty(this,'mozContact',{get:getDevContact});function getDevContact(){return devContact;}
this.save=function(){var outReq=new fb.utils.Request();if(contactData&&navigator.mozContacts){window.setTimeout(function save_do(){var photoList=contactData.fbInfo.photo;if(photoList&&photoList.length>0&&photoList[0]){utils.squareImage(photoList[0],function squared(squared_image){photoList[0]=squared_image;doSave(outReq);});}
else{doSave(outReq);}},0);}
else{throw'Data or mozContacts not available';}
return outReq;};function doSave(outReq){var contactInfo={};copyNames(contactData,contactInfo);if(contactData.fbInfo.url){contactInfo.url=contactData.fbInfo.url;}
doSetFacebookUid(contactInfo,contactData.uid);var fbReq=persistToFbCache(contactData);fbReq.onsuccess=function(){var mozContactsReq=navigator.mozContacts.save(utils.misc.toMozContact(contactInfo));mozContactsReq.onsuccess=function(e){outReq.done(fbReq.result);};mozContactsReq.onerror=function(e){window.console.error('FB: Error while saving on mozContacts',e.target.error);outReq.failed(e.target.error);};};fbReq.onerror=function(){window.console.error('FB: Error while saving on datastore',fbReq.error&&fbReq.error.name);outReq.failed(fbReq.error);};}
function persistToFbCache(contactData,mUpdate){var isUpdate=(mUpdate===true?mUpdate:false);var outReq=new fb.utils.Request();window.setTimeout(function persist_fb_do(){var data=Object.create(contactData.fbInfo);data.tel=contactData.tel||[];data.email=contactData.email||[];data.uid=contactData.uid;Object.keys(contactData.fbInfo).forEach(function(prop){data[prop]=contactData.fbInfo[prop];});copyNames(contactData,data);var updaterFn=(isUpdate===true?fb.contacts.update:fb.contacts.save);var fbReq=updaterFn(data);fbReq.onsuccess=function(){outReq.done(fbReq.result);};fbReq.onerror=function(){window.console.error('FB: Error while saving Fb data on the Datastore',fbReq.error&&fbReq.error.name);outReq.failed(fbReq.error);};},0);return outReq;}
this.update=function(contactData){function auxDoUpdate(contactData,outReq){if(!contactData.fbInfo.photo){var dataReq=fb.contacts.get(contactData.uid);dataReq.onsuccess=function(){contactData.fbInfo.photo=dataReq.result.photo;auxCachePersist(contactData,outReq,true);};dataReq.onerror=function(){window.console.error('Error while retrieving existing photo for ',contactData.uid,dataReq.error);outReq.failed(dataReq.error);};}
else{utils.squareImage(contactData.fbInfo.photo[0],function sq_img(squaredImg){contactData.fbInfo.photo[0]=squaredImg;auxCachePersist(contactData,outReq,true);});}}
function auxCachePersist(contactData,outReq,isUpdate){var fbReq=persistToFbCache(contactData,isUpdate);fbReq.onsuccess=function(){outReq.done(fbReq.result);};fbReq.onerror=function(){window.console.error('FB: Error while saving to FB cache: ',contactData.uid,fbReq.error);outReq.failed(fbReq.error);};}
var outReq=new fb.utils.Request();window.setTimeout(function update_do(){if(!fb.isFbLinked(devContact)){copyNames(contactData,devContact);}else{revisitPropagatedNames(contactData,devContact);}
if(contactData.fbInfo.photo){devContact.url=contactData.fbInfo.url;}
var auxReq=new fb.utils.Request();auxReq.onsuccess=function(){var mozContactsReq=navigator.mozContacts.save(utils.misc.toMozContact(devContact));mozContactsReq.onsuccess=function(e){outReq.done();};mozContactsReq.onerror=function(e){window.console.error('FB: Error while saving mozContact: ',devContact.id,e.target.error);outReq.failed(e.target.error);};};auxReq.onerror=function(e){outReq.failed(e.target.error);};auxDoUpdate(contactData,auxReq);},0);return outReq;};function asArrayOfValues(value){return Array.isArray(value)?value:[value];}
function copyNames(source,destination){destination.name=asArrayOfValues(source.name);destination.givenName=asArrayOfValues(source.givenName);destination.familyName=asArrayOfValues(source.familyName);destination.additionalName=asArrayOfValues(source.additionalName);}
this.merge=function(fbdata){return fb.mergeContact(devContact,fbdata);};this.getData=function(){return fb.getData(devContact);};this.getDataAndValues=function(){var outReq=new fb.utils.Request();window.setTimeout(function do_getData(){var uid=doGetFacebookUid(devContact);if(uid){var fbreq=fb.contacts.get(uid);fbreq.onsuccess=(function(){var fbdata=fbreq.result;var out1=this.merge(fbdata);var out2={};var duplicates={};Object.keys(fbdata).forEach(function(key){var dataElement=fbdata[key];if(dataElement&&Array.isArray(dataElement)&&key!=='photo'){dataElement.forEach(function(item){if(item.value&&item.value.length>0){var dupList=fb.checkDuplicates(key,item,devContact[key],fbdata.shortTelephone);dupList.forEach(function(aDup){duplicates[aDup]=true;});out2[item.value]=true;}
else if(typeof item==='string'&&item.length>0){out2[item]=true;}
else if(key==='adr'){if(item.locality){out2[item.locality]=true;}
if(item.countryName){out2[item.countryName]=true;}}});}
else if(key==='photo'){out2.hasPhoto=true;}
else if(dataElement){out2[dataElement]=true;}});Object.keys(duplicates).forEach(function(aDup){delete out2[aDup];});outReq.done([out1,out2,fbdata]);}).bind(this);fbreq.onerror=function(){outReq.failed(fbreq.error);};}
else{outReq.done([devContact,{}]);}}.bind(this),0);return outReq;};this.promoteToLinked=function(){promoteToLinked(devContact);};this.linkTo=function(fbFriend){var out=new fb.utils.Request();window.setTimeout(function do_linkTo(){if(!devContact){var req=fb.utils.getContactData(contactid);req.onsuccess=function(){devContact=req.result;doLink(devContact,fbFriend,out);};req.onerror=function(){throw'FB: Error while retrieving contact data';};}
else{doLink(devContact,fbFriend,out);}},0);return out;};function propagateField(field,from,to){var copied=false;if(!Array.isArray(to[field])||!to[field][0]||!to[field][0].trim()){to[field]=from[field];copied=true;}
return copied;}
function createName(contact){contact.name=[];if(Array.isArray(contact.givenName)){contact.name[0]=contact.givenName[0]+' ';}
if(Array.isArray(contact.familyName)){contact.name[0]+=contact.familyName[0];}}
function propagateNames(from,to){var isGivenNamePropagated=propagateField('givenName',from,to);var isFamilyNamePropagated=propagateField('familyName',from,to);if(isGivenNamePropagated||isFamilyNamePropagated){if(isGivenNamePropagated){fb.setPropagatedFlag('givenName',to);}
if(isFamilyNamePropagated){fb.setPropagatedFlag('familyName',to);}
createName(to);}}
function revisitPropagatedNames(from,to){if(fb.isPropagated('givenName',to)){to.givenName=from.givenName;}
if(fb.isPropagated('familyName',to)){to.familyName=from.familyName;}
createName(to);}
function doLink(contactdata,fbFriend,out){if(contactdata){if(fbFriend.uid){markAsLinked(contactdata,fbFriend.uid);if(fbFriend.photoUrl){fb.setFriendPictureUrl(contactdata,fbFriend.photoUrl);}
else if(fbFriend.mozContact){contactdata.url=fbFriend.mozContact.url;}}
else if(fbFriend.mozContact){markAsLinked(contactdata,doGetFacebookUid(fbFriend.mozContact));contactdata.url=fbFriend.mozContact.url;}
propagateNames(fbFriend.mozContact,contactdata);var mozContactsReq=navigator.mozContacts.save(utils.misc.toMozContact(contactdata));mozContactsReq.onsuccess=function(e){if(fbFriend.mozContact&&!fb.isFbLinked(fbFriend.mozContact)){var deleteReq=navigator.mozContacts.remove(utils.misc.toMozContact(fbFriend.mozContact));deleteReq.onsuccess=function(e){out.done(e.target.result);};deleteReq.onerror=function(e){window.console.error('FB: Error while linking');out.failed(e.target.error);};}
else{out.done(e.target.result);}};mozContactsReq.onerror=function(e){out.failed(e.target.error);};}
else{throw'FB: Contact data not defined';}}
this.unlink=function(type){var out=new fb.utils.Request();window.setTimeout(function do_unlink(){if(!devContact){var req=fb.utils.getContactData(contactid);req.onsuccess=function(){devContact=req.result;doUnlink(devContact,out,type);};req.onerror=function(){throw'FB: Error while retrieving contact data';};}
else{doUnlink(devContact,out,type);}},0);return out;};function resetNames(dContact){fb.resetNames(dContact);}
function doUnlink(dContact,out,type){var theType=type||'soft';var uid=doGetFacebookUid(dContact);resetNames(dContact);fb.markAsUnlinked(dContact);var req=navigator.mozContacts.save(utils.misc.toMozContact(dContact));req.onsuccess=function(e){if(theType!=='hard'){var fbNumReq=fb.utils.getNumberMozContacts(uid);fbNumReq.onsuccess=function(){if(fbNumReq.result>=1){out.done();}else{var fbDataReq=fb.contacts.get(uid);fbDataReq.onsuccess=function(){var imported=fbDataReq.result;var data={};copyNames(imported,data);data.url=imported.url;doSetFacebookUid(data,uid);var aMozContact=utils.misc.toMozContact(data);var reqRestore=navigator.mozContacts.save(aMozContact);reqRestore.onsuccess=function(e){out.done(aMozContact.id);};reqRestore.onerror=function(e){out.failed(e.target.error);};};fbDataReq.onerror=function(){window.console.error('FB: Error while unlinking contact data');out.failed(fbDataReq.error);};}};fbNumReq.onerror=function(){window.console.error('FB: Error while unlinking contact data');out.failed(fbNumReq.error);};}
else{var removeReq=fb.contacts.remove(uid,true);removeReq.onsuccess=function(){out.done(removeReq.result);};removeReq.onerror=function(){out.failed(removeReq.error);};}};req.onerror=function(e){out.failed(e.target.error);};}
this.remove=function(pforceFlush){var forceFlush=(pforceFlush===true)?pforceFlush:false;var out=new fb.utils.Request();window.setTimeout(function do_remove(){var uid=doGetFacebookUid(devContact);var fbNumReq=fb.utils.getNumberMozContacts(uid);fbNumReq.onsuccess=function num_onsuccess(){var removeReq;if(fbNumReq.result===1){removeReq=navigator.mozContacts.remove(utils.misc.toMozContact(devContact));removeReq.onsuccess=function(e){var fbReq=fb.contacts.remove(uid,forceFlush);fbReq.onsuccess=function(){out.done(fbReq.result);};fbReq.onerror=function(){out.failed(fbReq.error);};};removeReq.onerror=function(e){out.failed(e.target.error);};}
else{removeReq=navigator.mozContacts.remove(utils.misc.toMozContact(devContact));removeReq.onsuccess=function(e){out.done();};removeReq.onerror=function(e){out.failed(e.target.error);};}};},0);return out;};};

View file

@ -0,0 +1,17 @@
;;'use strict';var fb=this.fb||{};fb.PROPAGATED_PREFIX='fb_propagated_';fb.PROFILE_PHOTO_URI='fb_profile_photo';fb.FRIEND_URI='fb_friend';fb.DEFAULT_PHONE_TYPE='other';fb.DEFAULT_EMAIL_TYPE='other';fb.FLAG_YEAR_IGNORED=9996;fb.CONTACTS_APP_ORIGIN=location.origin;fb.isPropagated=function fcu_isPropagated(field,devContact){return(devContact.category&&devContact.category.indexOf(fb.PROPAGATED_PREFIX+field)!==-1);};fb.removePropagatedFlag=function fcu_removePropagatedFlag(field,devContact){var idx=devContact.category.indexOf(fb.PROPAGATED_PREFIX+field);if(idx!==-1){devContact.category.splice(idx,1);}};fb.setPropagatedFlag=function fcu_setPropagatedFlag(field,devContact){var idx=devContact.category.indexOf(fb.PROPAGATED_PREFIX+field);if(idx===-1){devContact.category.push(fb.PROPAGATED_PREFIX+field);}};fb.getFriendPictureUrl=function(devContact){var out;var urls=devContact.url;if(urls){for(var c=0;c<urls.length;c++){var aurl=urls[c];if(aurl.type.indexOf(fb.PROFILE_PHOTO_URI)!==-1){out=aurl.value;break;}}}
return out;};fb.setFriendPictureUrl=function(devContact,url){var urls=devContact.url||[];urls.push({type:[fb.PROFILE_PHOTO_URI],value:url});devContact.url=urls;};fb.friend2mozContact=function(f){function normalizeFbPhoneNumber(phone){var out=phone.number;if(phone.country_code&&out.indexOf('+')!==0){out='+'+phone.country_code+out;}
return out;}
if(Array.isArray(f.familyName)){return f;}
f.familyName=[f.last_name?f.last_name.trim():(f.last_name||'')];var middleName=f.middle_name?f.middle_name.trim():(f.middle_name||'');f.additionalName=middleName;var firstName=f.first_name?f.first_name.trim():(f.first_name||'');f.givenName=[(firstName+' '+middleName).trim()];delete f.last_name;delete f.middle_name;delete f.first_name;if(f.email){f.email1=f.email;f.email=[{type:[fb.DEFAULT_EMAIL_TYPE],value:f.email}];}
else{f.email1='';}
if(Array.isArray(f.phones)&&f.phones.length>0){f.tel=[];f.shortTelephone=[];f.phones.forEach(function(aphone){f.tel.push({type:[fb.DEFAULT_PHONE_TYPE],value:normalizeFbPhoneNumber(aphone)});f.shortTelephone.push(aphone.number);});}
delete f.phones;f.uid=f.uid.toString();return f;};fb.getWorksAt=function(fbdata){var out='';if(fbdata.work&&fbdata.work.length>0){out=fbdata.work[0].employer.name;}
return out;};fb.getBirthDate=function getBirthDate(sbday){var out=new Date(0);var imonth=sbday.indexOf('/');var smonth=sbday.substring(0,imonth);var iyear=sbday.lastIndexOf('/');if(iyear===imonth){iyear=sbday.length;}
var sday=sbday.substring(imonth+1,iyear);var syear=sbday.substring(iyear+1,sbday.length);out.setUTCDate(parseInt(sday,10));out.setUTCMonth(parseInt(smonth,10)-1,parseInt(sday,10));if(syear&&syear.length>0){out.setUTCFullYear(parseInt(syear,10));}
else{out.setUTCFullYear(fb.FLAG_YEAR_IGNORED);}
out.setUTCHours(0);out.setUTCMinutes(0);out.setUTCSeconds(0);out.setUTCMilliseconds(0);return out;};fb.getAddresses=function(fbdata){function fillAddress(fbAddress,type){var outAddr={};outAddr.type=[type];outAddr.locality=fbAddress.city||'';outAddr.region=fbAddress.state||'';outAddr.countryName=fbAddress.country||'';return outAddr;}
var out=[];var addrTypes={'home':'hometown_location','current':'current_location'};Object.keys(addrTypes).forEach(function onAddressType(type){var addrObj=fbdata[addrTypes[type]];if(addrObj){out.push(fillAddress(addrObj,type));}});return out;};fb.markAsUnlinked=function(devContact){var category=devContact.category;var updatedCategory=[];if(category){var idx=category.indexOf(fb.CATEGORY);if(idx!==-1){for(var c=0;c<idx;c++){updatedCategory.push(category[c]);}
for(c=idx+3;c<category.length;c++){updatedCategory.push(category[c]);}}}
devContact.category=updatedCategory;return devContact;};fb.unlinkClearAll=function(devContact){fb.resetNames(devContact);fb.markAsUnlinked(devContact);};fb.resetNames=function resetNames(dContact){if(fb.isPropagated('givenName',dContact)){dContact.givenName=[''];fb.removePropagatedFlag('givenName',dContact);}
if(fb.isPropagated('familyName',dContact)){dContact.familyName=[''];fb.removePropagatedFlag('familyName',dContact);}
dContact.name=[dContact.givenName[0]+' '+dContact.familyName[0]];};fb.markFbCleaningInProgress=function(value){utils.cookie.update({fbCleaningInProgress:value});};

View file

@ -0,0 +1,34 @@
;;'use strict';var fb=window.fb||{};(function(){var contacts=fb.contacts||{};fb.contacts=contacts;var Reader;var readerLoaded=false;var INDEX_ID=1;var isIndexDirty=false;var READER_LOADED_EV='reader_loaded';var TEL_INDEXER_JS='/shared/js/fb/fb_tel_index.js';var PHONE_MATCHER_JS='/shared/js/simple_phone_matcher.js';var FB_READER_JS='/shared/js/fb/fb_data_reader.js';var BINARY_SEARCH_JS='/shared/js/binary_search.js';contacts.UID_NOT_FOUND='UIDNotFound';contacts.ALREADY_EXISTS='AlreadyExists';if(!contacts.init){var proxyMethods=['get','getLength','getByPhone','search','refresh','init','restart'];proxyMethods.forEach(function(aMethod){contacts[aMethod]=defaultFunction.bind(null,aMethod);});LazyLoader.load(FB_READER_JS,onreaderLoaded);}
else{onreaderLoaded();}
function onreaderLoaded(){readerLoaded=true;Reader=fb.contacts;document.dispatchEvent(new CustomEvent(READER_LOADED_EV));}
function setIndex(index){Reader.dsIndex=index;isIndexDirty=false;}
function datastore(){return Reader.datastore;}
function index(){return Reader.dsIndex;}
function defaultFunction(target){var args=[];for(var j=1;j<arguments.length;j++){args.push(arguments[j]);}
if(!readerLoaded){document.addEventListener(READER_LOADED_EV,function rd_loaded(){document.removeEventListener(READER_LOADED_EV,rd_loaded);Reader[target].apply(this,args);});}
else{Reader[target].apply(this,args);}}
function initError(outRequest,error){outRequest.failed(error);}
function safeError(err){if(err&&err.name){return err;}
return{name:'UnknownError'};}
function defaultError(request){return defaultErrorCb.bind(null,request);}
function defaultSuccess(request){return defaultSuccessCb.bind(null,request);}
function defaultErrorCb(request,error){request.failed(safeError(error));}
function defaultSuccessCb(request,result){request.done(result);}
function doSave(obj,outRequest){LazyLoader.load([TEL_INDEXER_JS,PHONE_MATCHER_JS,BINARY_SEARCH_JS],function(){var uid=obj.uid;datastore().add(obj,uid).then(function success(){indexByPhone(obj,uid);isIndexDirty=true;outRequest.done();},function error(err){if(err.name==='ConstraintError'){err={name:contacts.ALREADY_EXISTS};}
outRequest.failed(err);});});}
function indexByPhone(obj,newId){if(Array.isArray(obj.tel)){obj.tel.forEach(function(aTel){var variants=SimplePhoneMatcher.generateVariants(aTel.value);variants.forEach(function(aVariant){index().byTel[aVariant]=newId;});TelIndexer.index(index().treeTel,aTel.value.substring(1),newId);});}}
function reIndexByPhone(oldObj,newObj,dsId){removePhoneIndex(oldObj);indexByPhone(newObj,dsId);TelIndexer.orderTree(index().treeTel);}
function removePhoneIndex(deletedFriend){if(Array.isArray(deletedFriend.tel)){deletedFriend.tel.forEach(function(aTel){TelIndexer.remove(index().treeTel,aTel.value.substring(1));var variants=SimplePhoneMatcher.generateVariants(aTel.value);variants.forEach(function(aVariant){delete index().byTel[aVariant];});});}}
contacts.save=function(obj){var retRequest=new fb.utils.Request();window.setTimeout(function save(){contacts.init(function(){doSave(obj,retRequest);},function(err){initError(retRequest,err);});},0);return retRequest;};contacts.update=function(obj){var retRequest=new fb.utils.Request();window.setTimeout(function save(){contacts.init(function(){doUpdate(obj,retRequest);},function(err){initError(retRequest,err);});},0);return retRequest;};function doUpdate(obj,outRequest){LazyLoader.load([TEL_INDEXER_JS,PHONE_MATCHER_JS,BINARY_SEARCH_JS],function(){var uid=obj.uid;var successCb=successUpdate.bind(null,outRequest);var errorCb=errorUpdate.bind(null,outRequest,uid);datastore().get(uid).then(function success(oldObj){if(!oldObj){errorCb({name:contacts.UID_NOT_FOUND});return;}
reIndexByPhone(oldObj,obj,uid);datastore().put(obj,uid).then(function success(){return datastore().put(index(),INDEX_ID);},errorCb).then(successCb,errorCb);},errorCb);});}
function successUpdate(outRequest){outRequest.done();}
function errorUpdate(outRequest,uid,error){window.console.error('Error while updating datastore for: ',uid);outRequest.failed(error);}
function doRemove(uid,outRequest,forceFlush){LazyLoader.load([TEL_INDEXER_JS,PHONE_MATCHER_JS,BINARY_SEARCH_JS],function(){var errorCb=errorRemove.bind(null,outRequest,uid);var objToDelete;datastore().get(uid).then(function success_get_remove(object){objToDelete=object;if(!objToDelete){errorRemove(outRequest,uid,{name:contacts.UID_NOT_FOUND});return;}
datastore().remove(uid).then(function success_rm(removed){successRemove(outRequest,objToDelete,forceFlush,removed);},errorCb);},errorCb);});}
function successRemove(outRequest,deletedFriend,forceFlush,removed){if(removed===true){isIndexDirty=true;removePhoneIndex(deletedFriend);if(forceFlush){var flushReq=fb.contacts.flush();flushReq.onsuccess=function(){isIndexDirty=false;outRequest.done(true);};flushReq.onerror=function(){outRequest.failed(flushReq.error);};}
else{outRequest.done(true);}}
else{outRequest.done(false);}}
function errorRemove(outRequest,uid,error){error=safeError(error);window.console.error('FB Data: Error while removing ',uid,': ',error.name);outRequest.failed(error);}
contacts.remove=function(uid,flush){var hasToFlush=(flush===true?flush:false);var retRequest=new fb.utils.Request();window.setTimeout(function remove(){contacts.init(function(){doRemove(uid,retRequest,hasToFlush);},function(err){initError(retRequest,err);});},0);return retRequest;};contacts.clear=function(){var outRequest=new fb.utils.Request();window.setTimeout(function clear(){contacts.init(function(){doClear(outRequest);},function(err){initError(outRequest,err);});},0);return outRequest;};function doClear(outRequest){datastore().clear().then(function success(){setIndex(null);datastore().put(index(),INDEX_ID).then(defaultSuccess(outRequest),function error(err){err=safeError(err);window.console.error('Error while re-creating the index: ',err.name);outRequest.failed(err);});},defaultError(outRequest));}
contacts.flush=function(){var outRequest=new fb.utils.Request();window.setTimeout(function do_Flush(){if(!(datastore())||!isIndexDirty){window.console.warn('The datastore has not been initialized or is not dirty');outRequest.done();return;}
TelIndexer.orderTree(index().treeTel);datastore().put(index(),INDEX_ID).then(defaultSuccess(outRequest),defaultError(outRequest));},0);return outRequest;};})();

View file

@ -0,0 +1,4 @@
;'use strict';var fb=this.fb||{};var self=this;fb.utils=this.fb.utils||{};fb.DEFAULT_TIMEOUT=30000;fb.utils.runQuery=function(query,callback,access_token){function QueryRequest(){this.cancel=function(){if(typeof this.oncancel==='function'){window.setTimeout(function(){this.oncancel();}.bind(this),0);}};}
var outReq=new QueryRequest();var queryService='https://graph.facebook.com/fql?q=';queryService+=encodeURIComponent(query);var params=['access_token'+'='+access_token,'format=json'];var queryParams=params.join('&');var remote=queryService+'&'+queryParams;var xhr=new XMLHttpRequest({mozSystem:true});outReq.xhr=xhr;outReq.oncancel=function(){this.xhr.abort();};xhr.open('GET',remote,true);xhr.responseType='json';xhr.timeout=fb.operationsTimeout||fb.DEFAULT_TIMEOUT;xhr.onload=function(e){if(xhr.status===200||xhr.status===400||xhr.status===0){if(callback&&typeof callback.success==='function'){self.setTimeout(function(){callback.success(xhr.response);},0);}}
else{self.console.error('FB: HTTP error executing query. ',query,' Status: ',xhr.status);if(callback&&typeof callback.error==='function'){self.setTimeout(callback.error,0);}}};xhr.ontimeout=function(e){self.console.error('FB: Timeout!!! while executing query',query);if(callback&&typeof callback.timeout==='function'){self.setTimeout(callback.timeout,0);}};xhr.onerror=function(e){self.console.error('FB: Error while executing query: ',query,': ',e);if(callback&&typeof callback.error==='function'){self.setTimeout(function(){callback.error(e);},0);}};xhr.send();return outReq;};fb.utils.getFriendPicture=function(uid,callback,access_token,targetPictureSize){var imgSrc='https://graph.facebook.com/'+uid+'/picture?';var params=['width='+targetPictureSize,'access_token='+access_token,'t='+Date.now()];var imgService=imgSrc+params.join('&');var xhr=new XMLHttpRequest({mozSystem:true});xhr.open('GET',imgService,true);xhr.responseType='blob';xhr.timeout=fb.operationsTimeout||fb.DEFAULT_TIMEOUT;xhr.onload=function(e){if(xhr.status===200||xhr.status===0){var mblob=e.target.response;if(typeof callback==='function'){self.setTimeout(function(){callback(mblob);},0);}}
else{self.console.error('FB: HTTP error retrieving img for uid: ',uid,' Status: ',xhr.status);if(typeof callback==='function'){self.setTimeout(function(){callback(null);},0);}}};xhr.ontimeout=function(e){self.console.error('FB: Timeout!!! while retrieving img for uid: ',uid);if(typeof callback==='function'){self.setTimeout(function(){callback(null);},0);}};xhr.onerror=function(e){self.console.error('FB: Error while retrieving img for uid: ',uid,'Error: ',e);if(typeof callback==='function'){self.setTimeout(function(){callback(null);},0);}};xhr.send();};

View file

@ -0,0 +1,28 @@
;;'use strict';var fb=window.fb||{};window.fb=fb;(function(document){var Utils=fb.utils||{};fb.utils=Utils;var FRIEND_COUNT_QUERY='select friend_count from user where uid=me()';var CACHE_FRIENDS_KEY=Utils.CACHE_FRIENDS_KEY='numFacebookFriends';var LAST_UPDATED_KEY=Utils.LAST_UPDATED_KEY='lastUpdatedTime';Utils.ALARM_ID_KEY='nextAlarmId';Utils.SCHEDULE_SYNC_KEY='facebookShouldHaveScheduledAt';var REDIRECT_LOGOUT_URI=window.oauthflow?oauthflow.params.facebook.redirectLogout:'';var STORAGE_KEY=Utils.TOKEN_DATA_KEY='tokenData';Utils.setLastUpdate=function(value,cb){window.asyncStorage.setItem(LAST_UPDATED_KEY,{data:value},cb);};Utils.getLastUpdate=function(callback){window.asyncStorage.getItem(LAST_UPDATED_KEY,function(obj){var out=0;if(obj){out=obj.data||out;}
if(typeof callback==='function'){callback(out);}});};Utils.getNonCacheableUrl=function(url){if(url.indexOf('?')===-1){url+='?';}
return url+'&burst_cache='+Date.now();};Utils.getContactData=function(cid){var outReq=new Utils.Request();var req=navigator.mozContacts.find({filterBy:['id'],filterValue:cid,filterOp:'equals'});req.onsuccess=function(e){if(e.target.result&&e.target.result.length>0){outReq.done(e.target.result[0]);}
else{outReq.done(null);}};req.onerror=function(e){outReq.failed(e.target.error);};return outReq;};Utils.getMozContact=function(uid){var outReq=new Utils.Request();window.setTimeout(function get_mozContact_ByUid(){fb.getMozContactByUid(uid,function onsuccess(result){if(Array.isArray(result)&&result.length>0){outReq.done(result[0]);}else{outReq.done(null);}},function onerror(e){outReq.failed(e.target.error);});},0);return outReq;};Utils.getNumberMozContacts=function(uid){var outReq=new Utils.Request();window.setTimeout(function get_mozContact_ByUid(){fb.getMozContactByUid(uid,function onsuccess(result){if(Array.isArray(result)){outReq.done(result.length);}else{outReq.done(0);}},function onerror(error){outReq.failed(error);});},0);return outReq;};Utils.getAllFbContacts=function(){var outReq=new Utils.Request();window.setTimeout(function get_all_fb_contacts(){var filter={filterValue:fb.CATEGORY,filterOp:'contains',filterBy:['category']};var req=navigator.mozContacts.find(filter);req.onsuccess=function(e){outReq.done(e.target.result);};req.onerror=function(e){outReq.failed(e.target.error);};},0);return outReq;};Utils.getNumFbContacts=function(){var outReq=new Utils.Request();window.setTimeout(function get_num_fb_contacts(){var req=fb.contacts.getLength();req.onsuccess=function(){outReq.done(req.result);};req.onerror=function(){outReq.failed(req.error);};},0);return outReq;};Utils.getNumFbFriends=function(callback,access_token){fb.utils.runQuery(FRIEND_COUNT_QUERY,callback,access_token);};Utils.getCachedAccessToken=function(callback){ImportStatusData.get(STORAGE_KEY).then(function(data){var out=null;if(data){out=data.access_token||null;}
if(typeof callback==='function'){callback(out);}});};Utils.setCachedAccessToken=function(data,callback){ImportStatusData.put(STORAGE_KEY,data).then(function done(){if(typeof callback==='function'){callback();}},function error(){console.error('Error while saving access token');if(typeof callback==='function'){callback();}});};Utils.getCachedNumFbFriends=function(callback){ImportStatusData.get(CACHE_FRIENDS_KEY).then(function(data){if(typeof callback==='function'&&typeof data==='number'){callback(data);}});};Utils.setCachedNumFriends=function(value,cb){ImportStatusData.put(CACHE_FRIENDS_KEY,value).then(cb);};Utils.removeCachedNumFriends=function(callback){ImportStatusData.remove(CACHE_FRIENDS_KEY).then(function(){typeof callback==='function'&&callback();});};Utils.getImportChecked=function(callback){Utils.getCachedAccessToken(function(access_token){var out='logged-out';if(access_token){out='logged-in';}
else{Utils.getCachedNumFbFriends(function(value){if(value){out='renew-pwd';if(typeof callback==='function'){callback(out);}}});}
if(typeof callback==='function'){callback(out);}});};Utils.numFbFriendsData=function(callback){var localCb=callback.local;var remoteCb=callback.remote;Utils.getCachedNumFbFriends(localCb);function auxCallback(response){if(response.data&&response.data[0]&&response.data[0].friend_count){remoteCb(response.data[0].friend_count);}}
if(typeof remoteCb==='function'&&navigator.onLine===true){var remoteCallbacks={success:auxCallback,error:null,timeout:null};Utils.getCachedAccessToken(function(access_token){if(access_token){Utils.getNumFbFriends(remoteCallbacks,access_token);}});}};Utils.clearFbData=function(){var outReq=new Utils.Request();window.setTimeout(function do_clearFbData(){var ireq=fb.contacts.clear();ireq.onsuccess=function(){var req=Utils.getAllFbContacts();req.onsuccess=function(){var cleaner=new Utils.FbContactsCleaner(req.result,'clear');outReq.done(cleaner);window.setTimeout(cleaner.start,0);};req.onerror=function(){window.console.error('FB Clean. Error retrieving FB Contacts');outReq.failed(req.error);};};ireq.onerror=function(e){window.console.error('Error while clearing the FB Cache');outReq.failed(ireq.error);};},0);return outReq;};Utils.logout=function(){var outReq=new Utils.Request();var WINDOW_TIMER_INTERVAL=500;var LOGOUT_TIMEOUT=5000;var MAX_TIMER_TICKS=LOGOUT_TIMEOUT/WINDOW_TIMER_INTERVAL;window.setTimeout(function do_logout(){Utils.getCachedAccessToken(function getAccessToken(access_token){if(access_token){var logoutService='https://www.facebook.com/logout.php?';var params=['next'+'='+encodeURIComponent(REDIRECT_LOGOUT_URI),'access_token'+'='+access_token];var logoutParams=params.join('&');var logoutUrl=logoutService+logoutParams;var loggedOut=false;var m_listen=function(e){if(e.origin!==fb.CONTACTS_APP_ORIGIN){return;}
if(e.data==='closed'){loggedOut=true;ImportStatusData.remove(STORAGE_KEY);outReq.done();}
e.stopImmediatePropagation();window.removeEventListener('message',m_listen);};window.addEventListener('message',m_listen);var logoutWindow=window.open(logoutUrl,'','dialog');var timerTicks=0;var timerId=window.setInterval(function(){timerTicks++;var closed=(logoutWindow.closed===true);var timedOut=(timerTicks>=MAX_TIMER_TICKS);if(loggedOut||closed||timedOut){window.clearInterval(timerId);window.removeEventListener('message',m_listen);}
if(closed&&!loggedOut){outReq.failed('UserCancelled');}
else if(timedOut&&!loggedOut){outReq.failed('Timeout');}},WINDOW_TIMER_INTERVAL);}
else{outReq.done();}});},0);return outReq;};Utils.FbContactsCleaner=function(contacts,pmode){this.lcontacts=contacts;var total=contacts.length;var next=0;var self=this;var CHUNK_SIZE=5;var numResponses=0;var mustUpdate=(pmode==='update');var notifyClean=false;var mustHold=false;var holded=false;var mustFinish=false;this.start=function(){mustHold=holded=mustFinish=false;if(total>0){cleanContacts(0);}
else if(typeof self.onsuccess==='function'){window.setTimeout(self.onsuccess);}};this.hold=function(){mustHold=true;};this.finish=function(){mustFinish=true;if(holded){notifySuccess();}};this.resume=function(){mustHold=holded=mustFinish=false;window.setTimeout(function resume_clean(){cleanContacts(next);});};function successHandler(e){if(notifyClean||typeof self.oncleaned==='function'){notifyClean=true;var cleaned=e.target.number;window.setTimeout(function(){self.oncleaned(cleaned);},0);}
continueCb();}
function errorHandler(contactid,error){if(typeof self.onerror==='function'){self.onerror(contactid,error);}
continueCb();}
function cleanContacts(from){for(var idx=from;idx<(from+CHUNK_SIZE)&&idx<total;idx++){var contact=contacts[idx];var number=idx;var req,fbContact;if(fb.isFbLinked(contact)){if(mustUpdate){fbContact=new fb.Contact(contact);req=fbContact.unlink('hard');}
else{fb.unlinkClearAll(contact);req=navigator.mozContacts.save(utils.misc.toMozContact(contact));}}
else{if(mustUpdate){fbContact=new fb.Contact(contact);req=fbContact.remove();}
else{req=navigator.mozContacts.remove(utils.misc.toMozContact(contact));}}
req.number=number;req.onsuccess=successHandler;req.onerror=function(e){errorHandler(contact.id,e.target.error);};}}
function notifySuccess(){if(typeof self.onsuccess==='function'){window.setTimeout(self.onsuccess);}}
function finishHandler(){var req=fb.contacts.flush();req.onsuccess=notifySuccess;req.onerror=function cleaner_flushError(){errorHandler(null,req.error);};}
function continueCb(){next++;numResponses++;if(next<total&&numResponses===CHUNK_SIZE){numResponses=0;if(!mustHold&&!mustFinish){cleanContacts(next);}
else if(mustFinish&&!holded){finishHandler();}
if(mustHold){holded=true;}}
else if(next>=total){finishHandler();}}};})(document);

View file

@ -0,0 +1,21 @@
;'use strict';var FriendListRenderer=(function(){var orderCriteria={firstName:['givenName','familyName','email1'],lastName:['familyName','givenName','email1']};var HEADER_LETTERS=['ABCDEFGHIJKLMNOPQRSTUVWXYZ','ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ','АБВГДЂЕЁЖЗИЙЈКЛЉМНЊОПРСТЋУФХЦЧЏШЩЭЮЯ'].join('');var defaults={container:'#groups-list',orderBy:'lastName'};var finishCb;var totalContacts;var lock;var CHUNK_SIZE=10;var render=function render(contacts,cb,options){for(var key in defaults){if(typeof options[key]==='undefined'){options[key]=defaults[key];}}
finishCb=cb;totalContacts=contacts.length;lock=navigator.requestWakeLock('cpu');doRender(contacts,cb,options);};var doRender=function doRender(contacts,cb,options){var groupsList=document.querySelector(options.container);var orderBy=options.orderBy;var order=orderCriteria[orderBy];contacts.sort(function(a,b){return getStringToBeOrdered(a,order).localeCompare(getStringToBeOrdered(b,order));});var groups={};contacts.forEach(function(contact){var groupName=getGroupName(contact,order);if(!groups[groupName]){groups[groupName]=[];}
groups[groupName].push(contact);});var notRenderedParagraph=groupsList.querySelector('[data-order-by="'+
(orderBy==='firstName'?'lastName':'firstName')+'"]');if(notRenderedParagraph){notRenderedParagraph.parentNode.removeChild(notRenderedParagraph);}
var letterStart=0;doRenderGroupChunk(letterStart,HEADER_LETTERS[letterStart],groupsList,groups);};function doRenderGroupChunk(index,group,groupsList,groups){renderGroup(groupsList,group,groups[group],function(fragment){if(fragment){groupsList.appendChild(fragment);}
fragment=null;var headerLettersLength=HEADER_LETTERS.length;if(index+1<=headerLettersLength){window.setTimeout(function renderNextGroup(){doRenderGroupChunk(index+1,HEADER_LETTERS[index+1],groupsList,groups);});}
else if(group!='#'){window.setTimeout(function renderNextGroup(){doRenderGroupChunk(index+1,'#',groupsList,groups);});}
else{groupsList.removeChild(groupsList.firstElementChild);if(typeof finishCb==='function'){window.setTimeout(finishCb,totalContacts*2);}
if(lock){lock.unlock();}}});}
function doRenderGroupItems(from,groupsList,group,friends,element,cb){var end=from+CHUNK_SIZE;var list=element.children[1];for(var i=from;i<end&&i<friends.length;i++){var friend=friends[i];if(friend.search&&friend.search.length>0){var box=utils.misc.getPreferredPictureBox();friend.picwidth=box.width;friend.picheight=box.height;friend.search=Normalizer.toAscii(friend.search);utils.templates.append(list,friend);}}
if(i<friends.length){window.setTimeout(function renderNextChunk(){doRenderGroupItems(end,groupsList,group,friends,element,cb);});}
else{list.removeChild(list.firstElementChild);cb();}}
function renderGroup(groupsList,group,friends,cb){if(!friends||friends.length===0){window.setTimeout(cb);return;}
var fragment=document.createDocumentFragment();var element=utils.templates.append(groupsList,{group:group},fragment);doRenderGroupItems(0,groupsList,group,friends,element,function(){cb(fragment);});}
function getStringToBeOrdered(contact,order){var ret=contact.search;if(!ret){ret=[];order.forEach(function(field){ret.push(getValue(contact,field));});ret=contact.search=ret.join(' ');}
return ret;}
function getValue(contact,field){var out=contact[field];if(out){out=Array.isArray(out)?out[0]:out;}else{out='';}
return out;}
function getGroupName(contact,order){var ret=getStringToBeOrdered(contact,order);ret=ret.charAt(0).toUpperCase();ret=ret.replace(/[ÁÀ]/ig,'A');ret=ret.replace(/[ÉÈ]/ig,'E');ret=ret.replace(/[ÍÌ]/ig,'I');ret=ret.replace(/[ÓÒ]/ig,'O');ret=ret.replace(/[ÚÙ]/ig,'U');if(HEADER_LETTERS.indexOf(ret)<0){ret='#';}
return ret;}
return{'render':render};})();

View file

@ -0,0 +1,475 @@
/* globals Rest */
/* exported GmailConnector */
'use strict';
/*
Gmail Contacts connector
Provides the capabilities to connect to the Gmail Contacts service
and return the contacts on a readable and importable format.
*/
var GmailConnector = (function GmailConnector() {
// Google contacts service end point,
// force a huge number of contacts to not paginate :S
var END_POINT =
'https://www.google.com/m8/feeds/contacts/default/full/?max-results=10000';
var GROUPS_END_POINT =
'https://www.google.com/m8/feeds/groups/default/full/';
var EXTRA_HEADERS = {
'GData-Version': '3.0'
};
var GD_NAMESPACE = 'http://schemas.google.com/g/2005';
var CATEGORY = 'gmail';
var URN_IDENTIFIER = 'urn:service:gmail:uid:';
// Will be used as a cache for the thumbnail url for each contact
var photoUrls = {};
// In some cases we will need the access token, cache a copy
var accessToken = null;
// We have a xml response from Google, all the entries in an array,
// no matter if they are xml entries.
// Despite of being a node, inject a 'uid' as this will be necessary
// for the selection
var nodeListToArray = function nodeListToArray(response) {
var entries = response.getElementsByTagName('entry');
var contacts = [];
var numContacts = entries.length;
for (var i = 0; i < numContacts; i++) {
contacts.push(gContactToJson(entries[i]));
}
return contacts;
};
// Returns the object used to build the headers necesary by the service
var buildRequestHeaders = function buildRequestHeaders(access_token) {
var requestHeaders = EXTRA_HEADERS;
requestHeaders.Authorization = 'OAuth ' + access_token;
return requestHeaders;
};
// Gets a list of all contacts giving a valid access token
var listAllContacts = function listAllContacts(access_token,
callbacks) {
// Copy the access_token
accessToken = access_token;
photoUrls = {};
getContactsGroup(access_token, callbacks);
};
var getContactsGroup = function getContactsGroup(access_token,
callbacks) {
var groupCallbacks = {
success: function onSuccess(response) {
// Locate the entry witch systemGroup id is 'Contacts'
var feed = response.querySelector('feed');
if (feed === null) {
callbacks.error();
return;
}
var sgc = feed.querySelector('systemGroup[id="Contacts"]');
if (sgc !== null) {
var id = sgc.parentNode.querySelector('id').textContent;
getContactsByGroup(id, access_token, callbacks);
} else {
callbacks.error();
}
},
error: function(e) {
if (e && e.status === 401) {
// This is a token expired / invalid token problem
window.console.warn('GMail Access token expired or invalid. ',
'restarting flow!');
callbacks.success({
error: {
code: 190
}
});
}
else {
callbacks.error();
}
},
timeout: callbacks.timeout
};
return performAPIRequest(GROUPS_END_POINT, groupCallbacks, access_token);
};
// Retrieve all the contacts for the specific groupId
var getContactsByGroup = function getContactsByGroup(groupId, access_token,
callbacks) {
var listingCallbacks = {
success: function onSuccess(response) {
callbacks.success({
'data': nodeListToArray(response)
});
},
error: callbacks.error,
timeout: callbacks.timeout
};
var groupUrl = END_POINT + '&group=' + groupId;
return performAPIRequest(groupUrl, listingCallbacks, access_token);
};
// Given a Google contacts api url add the authentication and
// extra headers to perform the correct request
var performAPIRequest = function performAPIRequest(url, callbacks,
access_token) {
return Rest.get(url, callbacks, {
'requestHeaders': buildRequestHeaders(access_token),
'responseType': 'xml'
});
};
// Return the list of contacts on the device imported using this connector
var listDeviceContacts = function listDeviceContacts(callbacks) {
var filterOptions = {
filterValue: CATEGORY,
filterOp: 'contains',
filterBy: ['category']
};
var req = navigator.mozContacts.find(filterOptions);
req.onsuccess = function() {
callbacks.success(req.result);
};
req.onerror = function onError() {
callbacks.success([]);
};
};
var getImporter = function getImporter(contactsList, access_token) {
return new window.ContactsImporter(contactsList, access_token, this);
};
var cleanContacts = function cleanContacts(contactsList, mode, cb) {
var cleaner = new window.ContactsCleaner(contactsList);
window.setTimeout(cleaner.start, 0);
if (cb) {
cb(cleaner);
}
};
var getValueForNode = function getValueForNode(doc, name, def) {
var defaultValue = def || '';
if (doc == null || name == null) {
return defaultValue;
}
var node = doc.querySelector(name);
if (node && node.textContent) {
return node.textContent;
}
return defaultValue;
};
/*
Given an xml contact entry from gmail contacts we will need to output
a json object that contains the minimun viable information to display
that contact into a list.
The fields for that minimun visualization object are:
uid
givenName
familyName
email1
*/
var adaptDataForShowing = function adaptDataForShowing(contact) {
var output = {
'uid': '-1',
'givenName': '',
'familyName': '',
'email1': '',
'contactPictureUri': ''
};
output.uid = contact.uid;
if (contact.familyName) {
output.familyName = contact.familyName;
}
if (contact.email && contact.email.length > 0) {
output.email1 = contact.email[0].value;
}
var tel;
if (contact.tel && contact.tel.length > 0) {
tel = contact.tel[0].value;
}
if(contact.givenName) {
output.givenName = contact.givenName;
} else if(!contact.familyName) {
output.givenName = output.email1 || contact.org || tel;
} else if(contact.familyName) {
output.givenName = '' ;
}
var photoUrl = buildContactPhotoURL(contact, accessToken);
if (photoUrl) {
output.contactPictureUri = photoUrl;
}
return output;
};
var adaptDataForSaving = function adaptDataForSaving(contact) {
return contact;
};
// Transform a Google contact entry into json format.
// The json format is the same used in Contacts api ;P
var gContactToJson = function gContactToJson(googleContact) {
var output = {};
// This field will be needed for indexing within the
// import process, not for the api
output.uid = getUid(googleContact);
output.name = [getValueForNode(googleContact, 'title')];
// Store the photo url, not in the contact itself
var photoUrl = googleContact.querySelector('link[type="image/*"]');
if (photoUrl) {
photoUrl = photoUrl.getAttribute('href');
} else {
// No image link
photoUrl = '';
}
photoUrls[output.uid] = photoUrl;
var name = googleContact.querySelector('name');
if (name) {
var contactName = getValueForNode(name, 'givenName');
if (contactName) {
output.givenName = [contactName];
}
var contactFamilyName = getValueForNode(name, 'familyName');
if (contactFamilyName) {
output.familyName = [contactFamilyName];
}
var contactSuffix = getValueForNode(name, 'additionalName');
if (contactSuffix) {
output.additionalName = [contactSuffix];
}
}
output.email = parseEmails(googleContact);
output.adr = parseAddress(googleContact);
output.tel = parsePhones(googleContact);
var org = googleContact.querySelector('organization');
if (org) {
output.org = [getValueForNode(org, 'orgName')];
output.jobTitle = [getValueForNode(org, 'orgTitle')];
}
var bday = googleContact.querySelector('birthday');
if (bday) {
var bdayMS = Date.parse(bday.getAttribute('when'));
if (!isNaN(bdayMS)) {
output.bday = new Date(bdayMS);
}
}
var content = googleContact.querySelector('content');
if (content) {
output.note = [content.textContent];
}
output.category = [CATEGORY];
output.url = [{
type: ['source'],
value: getContactURI(output)
}];
return output;
};
var getContactURI = function getContactURI(contact) {
return URN_IDENTIFIER + contact.uid;
};
// This will be a full url like:
// http://www.google.com/m8/feeds/contacts/<email>/base/<contact_id>
// for a specific contact node
var getUid = function getUid(contact) {
return contact.querySelector('id').textContent;
};
// Returns an array with the possible emails found in a contact
// as a ContactField format
var parseEmails = function parseEmails(googleContact) {
var DEFAULT_EMAIL_TYPE = 'other';
var emails = [];
var fields = googleContact.getElementsByTagNameNS(GD_NAMESPACE,
'email');
if (fields && fields.length > 0) {
for (var i = 0; i < fields.length; i++) {
var emailField = fields.item(i);
// Type format: rel="http://schemas.google.com/g/2005#home"
var type = emailField.getAttribute('rel') || DEFAULT_EMAIL_TYPE;
if (type.indexOf('#') > -1) {
type = type.substr(type.indexOf('#') + 1);
}
emails.push({
'type': [type],
'value': emailField.getAttribute('address')
});
}
}
return emails;
};
// Given a google contact returns an array of ContactAddress
var parseAddress = function parseAddress(googleContact) {
var addresses = [];
var fields = googleContact.getElementsByTagNameNS(GD_NAMESPACE,
'structuredPostalAddress');
if (fields && fields.length > 0) {
for (var i = 0; i < fields.length; i++) {
var field = fields.item(i);
var address = {};
address.streetAddress = getValueForNode(field, 'street');
address.locality = getValueForNode(field, 'city');
address.region = getValueForNode(field, 'region');
address.postalCode = getValueForNode(field, 'postcode');
address.countryName = getValueForNode(field, 'country');
addresses.push(address);
}
}
return addresses;
};
// Given a google contact this function returns an array of
// ContactField with the pones stored for that contact
var parsePhones = function parsePhones(googleContact) {
var DEFAULT_PHONE_TYPE = 'other';
var GMAIL_MAP = {
'work_fax' : 'faxOffice',
'home_fax' : 'faxHome',
'pager' : 'other',
'main' : 'other'
};
var phones = [];
var fields = googleContact.getElementsByTagNameNS(GD_NAMESPACE,
'phoneNumber');
if (fields && fields.length > 0) {
for (var i = 0; i < fields.length; i++) {
var field = fields.item(i);
// Type format: rel="http://schemas.google.com/g/2005#home"
var type = field.getAttribute('rel') || DEFAULT_PHONE_TYPE;
if (type.indexOf('#') > -1) {
type = type.substr(type.indexOf('#') + 1);
}
phones.push({
'type': [GMAIL_MAP[type] || type],
'value': field.textContent
});
}
}
return phones;
};
// Given a contact from the mozcontact api, fetch the Google Contacts
// identifier
var getContactUid = function getContactUid(deviceContact) {
var out = -1;
var url = deviceContact.url;
if (Array.isArray(url)) {
var targetUrls = url.filter(function(aUrl) {
return Array.isArray(aUrl.type) &&
aUrl.type.indexOf('source') !== -1 &&
aUrl.value;
});
if (targetUrls[0]) {
out = resolveURI(targetUrls[0].value);
}
}
return out;
};
// From a contact URL, that we expect to be a URI
// return the google contact id if we find it on
// the uri, -1 otherwise
var resolveURI = function resolveURI(uri) {
if (uri && uri.indexOf(URN_IDENTIFIER) === 0) {
var output = uri.substr(URN_IDENTIFIER.length);
if (output && output.length > 0) {
return output;
}
}
return -1;
};
var downloadContactPicture = function downloadContactPicture(googleContact,
access_token, callbacks) {
var url = buildContactPhotoURL(googleContact, access_token);
return Rest.get(url, callbacks, {
'responseType': 'blob'
});
};
// Build the url of the photo with the access token
var buildContactPhotoURL = function contactPhotoURL(contact, access_token) {
if (photoUrls && photoUrls[contact.uid]) {
return photoUrls[contact.uid] + '?access_token=' + access_token;
}
return null;
};
var startSync = function startSync() {
//We don't sync Google contacts (yet)
};
var getServiceName = (function getServiceName() {
return 'gmail';
})();
var getAutomaticLogout = (function getAutomaticLogout() {
return true;
})();
return {
'listAllContacts': listAllContacts,
'listDeviceContacts': listDeviceContacts,
'getImporter': getImporter,
'cleanContacts': cleanContacts,
'adaptDataForShowing': adaptDataForShowing,
'adaptDataForSaving': adaptDataForSaving,
'getContactUid': getContactUid,
'downloadContactPicture': downloadContactPicture,
'startSync': startSync,
'name': getServiceName,
'automaticLogout': getAutomaticLogout,
'gContactToJson': gContactToJson
};
})();

View file

@ -0,0 +1,12 @@
;'use strict';(function(document){var params=parseParams(window.location.search.substring(1));var serviceName=getServiceName();var allowedOrigin=location.origin;function parseParams(paramsStr){var out={};var paramsList;if(paramsStr.indexOf('&')!=-1){paramsList=paramsStr.split('&');}
else{paramsList=[paramsStr];}
paramsList.forEach(function(param){var paramsValues=param.split('=');out[paramsValues[0]]=paramsValues[1];});return out;}
function getServiceName(){return params.service;}
var servicesConnectors={'live':function(cb){LazyLoader.load('/shared/js/contacts/import/live/live_connector.js',function(){cb(LiveConnector);});},'gmail':function gmailLoader(cb){LazyLoader.load('/shared/js/contacts/import/gmail/gmail_connector.js',function onLoad(){cb(GmailConnector);});},'facebook':function(cb){var files=['/shared/js/fb/fb_request.js','/shared/js/contacts/import/facebook/fb_data.js','/shared/js/contacts/import/facebook/fb_utils.js','/shared/js/fb/fb_reader_utils.js','/shared/js/contacts/import/facebook/fb_contact_utils.js','/shared/js/contacts/import/facebook/fb_query.js','/shared/js/contacts/import/facebook/fb_contact.js','/shared/js/contacts/import/facebook/facebook_connector.js','/shared/pages/import/style/facebook.css'];if(!params.ftu){files.push('/facebook/js/fb_sync.js');}
LazyLoader.load(files,function(){cb(FacebookConnector);});}};function getServiceConnector(cb){if(serviceName){servicesConnectors[serviceName](cb);}
else{throw new Error('Service Connector not found !!!');}}
function tokenReady(access_token){if(document.readyState==='complete'){onLoad(access_token);}
else{window.addEventListener('load',function do_load(){onLoad(access_token);window.removeEventListener('load',do_load);});}}
function onLoad(access_token){utils.listeners.add({'#import-action':importer.ui.importAll,'#done-search':window.Search.exitSearchMode,'#groups-list':importer.ui.selection,'#header':[{event:'action',handler:importer.ui.end}],'#search-start':[{event:'click',handler:window.Search.enterSearchMode}]});document.querySelector('#select-all').onclick=importer.ui.selectAll;document.querySelector('#deselect-all').onclick=importer.ui.unSelectAll;importer.ui.init();getServiceConnector(function(connector){importer.start(access_token,connector,allowedOrigin);});}
window.addEventListener('message',function getAccessToken(e){if(e.origin!==allowedOrigin){return;}
window.removeEventListener('message',getAccessToken);if(e.data.type==='token'){tokenReady(e.data.data);}});parent.postMessage({type:'messaging_ready',data:''},allowedOrigin);})(document);

View file

@ -0,0 +1,4 @@
;;;;'use strict';if(!window.ImportStatusData){var ImportStatusData=(function(){var store=null;var DS_NAME='Import_Status_Data';var datastoreLoading=false;var datastoreLoaded=false;var DS_LOADED_EVENT='datastore_loaded';var getDatastore=function getDatastore(){return new Promise(function(resolve,reject){if(!navigator.getDataStores){reject({name:'DatastoreNotEnabled'});return;}
if(datastoreLoaded){resolve(store);return;}
if(datastoreLoading){document.addEventListener(DS_LOADED_EVENT,function loadedHandler(){document.removeEventListener(DS_LOADED_EVENT,loadedHandler);resolve(store);});}
datastoreLoading=true;navigator.getDataStores(DS_NAME).then(function(stores){store=stores[0];},reject).then(function(){datastoreLoading=false;datastoreLoaded=true;document.dispatchEvent(new CustomEvent(DS_LOADED_EVENT));resolve(store);},reject);});};var put=function put(key,obj){return new Promise(function(resolve,reject){getDatastore().then(function success(store){store.put(obj,key).then(resolve,reject);},reject);});};var remove=function remove(key){return new Promise(function(resolve,reject){getDatastore().then(function success(store){store.remove(key).then(resolve,reject);},reject);});};var get=function get(key){return new Promise(function(resolve,reject){getDatastore().then(function success(store){store.get(key).then(resolve,reject);},reject);});};var clear=function clear(){return new Promise(function(resolve,reject){getDatastore().then(function success(store){store.clear().then(resolve,reject);},reject);});};return{put:put,remove:remove,get:get,clear:clear};})();}

View file

@ -0,0 +1,69 @@
;'use strict';if(typeof window.importer==='undefined'){(function(document){var Importer=window.importer={};var UI=Importer.ui={};var TOKEN_EXPIRED_STR='request_token_expired';var TOKEN_EXPIRED_CODE=190;var serviceConnector;var targetApp;var access_token;var startCallback;var selectedContacts={};var unSelectedContacts={};var selectableFriends={};var myFriends,myFriendsByUid;var checked=0;var checkNodeList;var existingContacts=[];var existingContactsByUid={};var contactsLoaded=false,friendsLoaded=false;var currentRequest;var currentNetworkRequest=null;var cancelled=false;var friendsImported;var syncOngoing=false;var updateButton,selectAllButton,deSelectAllButton,contactList,headerElement,friendsMsgElement,scrollableElement;var imgLoader;var HARD_LIMIT_SYNC=300;var tokenKey;var isOnLine=navigator.onLine;var ongoingImport=false;var ongoingClean=false;var theImporter,theCleaner;window.addEventListener('online',onLineChanged);window.addEventListener('offline',onLineChanged);function notifyParent(message,origin){parent.postMessage({type:message.type||'',data:message.data||'',messageId:message.messageId,additionalMessageId:message.additionalMessageId},origin);}
function showOfflineDialog(yesCb,noCb){var recommend=serviceConnector.name==='facebook';var dialog=parent.document.getElementById('confirmation-message');parent.LazyLoader.load(dialog,function(){LazyLoader.load('/shared/js/confirm.js',function(){ConfirmDialog.show('connectionLost','connectionLostMsg',{title:'noOption',isRecommend:!recommend,callback:function(){ConfirmDialog.hide();noCb();}},{title:'yesOption',isRecommend:recommend,callback:function(){ConfirmDialog.hide();yesCb();}},{zIndex:'10000'});});});}
function onLineChanged(){isOnLine=navigator.onLine;if(isOnLine===false&&ongoingImport){theImporter.hold();showOfflineDialog(function(){theImporter.resume();},function(){theImporter.finish();});}}
function getTokenKey(){var key='tokenData';var serviceName=serviceConnector.name;if(serviceName!=='facebook'){key+=('_'+serviceName);}
return key;}
var searchSource={getNodes:function(){return contactList.querySelectorAll('section > ol > li');},getFirstNode:function(){return contactList.querySelector('section > ol > li');},getNextNode:function(contact){var out=contact.nextElementSibling;var nextParent=contact.parentNode.parentNode.nextElementSibling;while(!out&&nextParent){out=nextParent.querySelector('ol > li:first-child');nextParent=nextParent.nextElementSibling;}
return out;},expectMoreNodes:function(){return false;},clone:function(node){return node.cloneNode(true);},getNodeById:function(id){return contactsList.querySelector('[data-uuid="'+id+'"]');},getSearchText:function(node){return node.dataset.search;},click:onSearchResultCb};UI.init=function(){var overlay=document.querySelector('nav[data-type="scrollbar"] p');var jumper=document.querySelector('nav[data-type="scrollbar"] ol');updateButton=document.getElementById('import-action');selectAllButton=document.querySelector('#select-all');deSelectAllButton=document.querySelector('#deselect-all');contactList=document.querySelector('#groups-list');headerElement=document.querySelector('header');friendsMsgElement=document.querySelector('#friends-msg');scrollableElement=document.querySelector('#mainContent');var params={overlay:overlay,jumper:jumper,groupSelector:'#group-',scrollToCb:scrollToCb};utils.alphaScroll.init(params);window.Search.init(searchSource,true);};function notifyLogout(){var msg={type:'logout_finished',data:''};parent.postMessage(msg,targetApp);}
UI.end=function(event){notifyParent({type:'window_close'},targetApp);notifyLogout();};function removeToken(cb){var theCb=(typeof cb==='function')?cb:function(){};ImportStatusData.remove(tokenKey).then(theCb,theCb);}
function markPendingLogout(url,service,cb){var PENDING_LOGOUT_KEY='pendingLogout';window.asyncStorage.getItem(PENDING_LOGOUT_KEY,function(data){var obj=data||{};obj[service]=url;var theCb=(typeof cb==='function')?cb:function(){};window.asyncStorage.setItem(PENDING_LOGOUT_KEY,obj,theCb,theCb);});}
function serviceLogout(cb){if(serviceConnector.automaticLogout){var serviceName=serviceConnector.name;var logoutUrl=oauthflow.params[serviceName].logoutUrl;var callbacks={error:function(){window.console.warn('Error while logging out user ',logoutUrl);removeToken();markPendingLogout(logoutUrl,serviceName,cb);},timeout:function(){window.console.warn('Timeout while logging out user ',logoutUrl);removeToken();markPendingLogout(logoutUrl,serviceName,cb);},success:function(){window.console.log('Successfully logged out');removeToken(cb);}};if(navigator.onLine===true){Rest.get(logoutUrl,callbacks);}
else{removeToken();markPendingLogout(logoutUrl,serviceName,cb);}}
else{cb();}}
function tokenExpired(error){return(error.code===TOKEN_EXPIRED_CODE||error===TOKEN_EXPIRED_STR);}
function scrollToCb(groupContainer){scrollableElement.scrollTop=groupContainer.offsetTop;}
function onSearchResultCb(e){var target=e.target;var uid=target.dataset.uuid;var checkbox=target.querySelector('input[type="checkbox"]');checkbox.checked=!checkbox.checked;var realNode=contactList.querySelector('[data-uuid='+'"'+uid+'"'+']');UI.selection({target:realNode});}
Importer.reset=function(){selectedContacts={};unSelectedContacts={};selectableFriends={};myFriends=[];myFriendsByUid={};checked=0;existingContacts=[];existingContactsByUid={};contactsLoaded=false;friendsLoaded=false;currentRequest=null;currentNetworkRequest=null;friendsImported=false;syncOngoing=false;ongoingImport=ongoingClean=false;selectAllButton=document.getElementById('select-all');deSelectAllButton=document.getElementById('deselect-all');};Importer.start=function(acc_tk,connector,ptargetApp,startCb){startCallback=startCb;var serviceName=connector.name;document.querySelector('#content h1').setAttribute('data-l10n-id',serviceName+'-serviceName');document.body.classList.add(serviceName);serviceConnector=connector;targetApp=ptargetApp;tokenKey=getTokenKey();setCurtainHandlers();if(acc_tk){access_token=acc_tk;Importer.getFriends(acc_tk);}
else{oauth2.getAccessToken(function(new_acc_tk){access_token=new_acc_tk;Importer.getFriends(new_acc_tk);},'friends',serviceConnector.name);}};function contactsReady(result){existingContacts=result;contactsLoaded=true;if(friendsLoaded){markExisting(existingContacts);}}
function syncSuccess(numChanged){window.console.log('Synchronization ended!!!');syncOngoing=false;serviceConnector.scheduleNextSync();var msg={type:'sync_finished',data:numChanged||''};parent.postMessage(msg,targetApp);}
function startSync(){var totalExisting=existingContacts.length;if(totalExisting>0&&totalExisting<HARD_LIMIT_SYNC&&!syncOngoing){syncOngoing=true;serviceConnector.startSync(existingContacts,myFriendsByUid,syncSuccess);}
else{var msg={type:'sync_finished',data:0};parent.postMessage(msg,targetApp);}}
function friendsAvailable(){imgLoader=new ImageLoader('#mainContent','.block-item:not([data-uuid="#uid#"])');var s='.block-item:not([data-uuid="#uid#"]) input[type="checkbox"]';checkNodeList=contactList.querySelectorAll(s);Array.prototype.slice.call(checkNodeList,0,checkNodeList.length);friendsLoaded=true;if(contactsLoaded){window.addEventListener('message',function importOnViewPort(e){if(e.origin!==targetApp){return;}
var data=e.data;if(data&&data.type==='dom_transition_end'){window.removeEventListener('message',importOnViewPort);window.setTimeout(startSync,0);}});markExisting(existingContacts);}
Curtain.hide(sendReadyEvent);if(typeof startCallback==='function'){window.setTimeout(startCallback,0);}}
function sendReadyEvent(){parent.postMessage({type:'ready',data:''},targetApp);}
function markExisting(deviceFriends){updateButton.setAttribute('data-l10n-id',deviceFriends.length===0?'import':'update');var reallyExisting=0;deviceFriends.forEach(function(fbContact){var uid=serviceConnector.getContactUid(fbContact);if(myFriendsByUid[uid]){reallyExisting++;}
delete selectableFriends[uid];var ele=document.querySelector('[data-uuid="'+uid+'"]');if(ele){setChecked(ele.querySelector('input[type="checkbox"]'),true);}
if(existingContactsByUid[uid]){existingContactsByUid[uid].push(fbContact);}else{existingContactsByUid[uid]=[fbContact];}});if(myFriends.length<1){friendsMsgElement.setAttribute('data-l10n-id','fbNoFriends');}else{var newValue=myFriends.length-
Object.keys(existingContactsByUid).length;navigator.mozL10n.setAttributes(friendsMsgElement,'fbFriendsFound',{numFriends:newValue<0?0:newValue});}
checkDisabledButtons();}
Importer.setSelected=function(friends){selectedContacts=friends;};function setCurtainHandlers(){Curtain.oncancel=cancelCb;}
Importer.getFriends=function(acc_tk){currentNetworkRequest=serviceConnector.listAllContacts(acc_tk,{success:importer.friendsReady,error:importer.errorHandler,timeout:importer.timeoutHandler});if(!navigator.mozContacts){return;}
var callbacks={success:contactsReady,error:function(errorName){window.console.error('Error while retrieving existing dev contacts: ',errorName);}};serviceConnector.listDeviceContacts(callbacks);};Importer.friendsReady=function(response){if(typeof response.error==='undefined'){var lmyFriends=response.data;if(lmyFriends.length===0){showNoFriends(function(){Curtain.hide(UI.end);});return;}
if(typeof serviceConnector.oncontactsloaded==='function'){serviceConnector.oncontactsloaded(lmyFriends);}
myFriendsByUid={};myFriends=[];var totalMyFriends=lmyFriends.length;for(var i=0;i<totalMyFriends;i++){var aFriend=lmyFriends[i];aFriend._idxFriendsArray=i;myFriends.push(serviceConnector.adaptDataForShowing(aFriend));myFriendsByUid[aFriend.uid]=aFriend;selectableFriends[aFriend.uid]=aFriend;}
asyncStorage.getItem('order.lastname',function orderValue(lastName){var options={container:'#groups-list',orderBy:lastName?'lastName':'firstName'};FriendListRenderer.render(myFriends,friendsAvailable,options);});}
else{window.console.error('Error, while retrieving friends',response.error.message);if(!tokenExpired(response.error)){setCurtainHandlersErrorFriends();Curtain.show('error','friends');}
else{window.console.warn('Access Token expired or revoked');Curtain.hide(notifyParent.bind(null,{type:'token_error'},targetApp));ImportStatusData.remove(tokenKey).then(function token_removed(){oauth2.getAccessToken(function(new_acc_tk){access_token=new_acc_tk;Importer.getFriends(new_acc_tk);},'friends',serviceConnector.name);});}}};function showNoFriends(callback){var dialog=parent.document.getElementById('confirmation-message');parent.LazyLoader.load(dialog,function(){LazyLoader.load('/shared/js/confirm.js',function(){ConfirmDialog.show(null,'emptyAccount',{title:'ok',isRecommend:true,callback:function(){ConfirmDialog.hide();if(typeof callback==='function'){callback();}}},null,{zIndex:'10000'});if(typeof startCallback==='function'){window.setTimeout(startCallback,0);}});});}
function cancelImport(){cancelled=true;var cancelFunc=onUpdate;if(ongoingImport){cancelFunc=theImporter.finish;}else if(ongoingClean){cancelFunc=theCleaner.finish;}
cancelFunc();Curtain.show('message','canceling');}
function cancelCb(){if(currentNetworkRequest){currentNetworkRequest.cancel();currentNetworkRequest=null;}
Curtain.hide(notifyParent.bind(null,{type:'abort'},targetApp));}
function setCurtainHandlersErrorFriends(){Curtain.oncancel=function friends_cancel(){Curtain.hide(notifyParent.bind(null,{type:'abort'},targetApp));};Curtain.onretry=function get_friends(){Curtain.oncancel=cancelCb;Curtain.show('wait','friends');Importer.getFriends(access_token);};}
function checkUpdateButton(){if(Object.keys(selectedContacts).length>0||Object.keys(unSelectedContacts).length>0){updateButton.disabled=false;}else{updateButton.disabled=true;}}
function checkDisabledButtons(){checkUpdateButton();switch(checked){case 0:deSelectAllButton.disabled=true;selectAllButton.disabled=false;break;case myFriends.length:selectAllButton.disabled=true;deSelectAllButton.disabled=false;break;default:deSelectAllButton.disabled=false;selectAllButton.disabled=false;break;}}
function setChecked(element,value){if(element.checked!==value){value?++checked:--checked;}
element.checked=value;}
Importer.baseHandler=function(type){setCurtainHandlersErrorFriends();Curtain.show(type,'friends');};Importer.timeoutHandler=function(){Importer.baseHandler('timeout');};Importer.errorHandler=function(){Importer.baseHandler('error');};function onUpdate(numFriends,numMergedDuplicated){serviceLogout(notifyLogout);if(Importer.getContext()==='ftu'){Curtain.hide(notifyParent.bind(null,{type:'window_close',messageId:cancelled?null:{id:'friendsUpdated',args:{numFriends:numFriends}},additionalMessageId:(cancelled||!numMergedDuplicated)?null:{id:'friendsMerged',args:{numDups:numMergedDuplicated}}},targetApp));}else{notifyParent({type:'import_updated'},targetApp);window.addEventListener('message',function finished(e){if(e.origin!==targetApp){return;}
if(e.data.type==='contacts_loaded'){var messageId=cancelled?null:{id:'friendsUpdated',args:{numFriends:numFriends}};var additionalMessageId=(cancelled||!numMergedDuplicated)?null:{id:'friendsMerged',args:{numDups:numMergedDuplicated}};Curtain.hide(notifyParent.bind(null,{type:'window_close',messageId:messageId,additionalMessageId:additionalMessageId},targetApp));window.removeEventListener('message',finished);}});}}
function cleanContacts(onsuccess,progress){if(cancelled){return;}
var contacts=[];var unSelectedKeys=Object.keys(unSelectedContacts);unSelectedKeys.forEach(function iterator(uid){var deviceContacts=unSelectedContacts[uid];for(var i=0;i<deviceContacts.length;i++){contacts.push(deviceContacts[i]);}});var mode='update';if(unSelectedKeys.length===existingContacts.length&&Object.keys(selectedContacts).length===0){mode='clear';Curtain.hideMenu();}
serviceConnector.cleanContacts(contacts,mode,function gotCleaner(cleaner){if(cleaner){theCleaner=cleaner;ongoingClean=true;cleaner.oncleaned=progress.update;cleaner.onsuccess=function(){ongoingClean=false;theCleaner=null;onsuccess();};}
else{Importer.errorHandler();}});}
function getTotalUnselected(){var total=0;Object.keys(unSelectedContacts).forEach(function surfing(uid){total=total+unSelectedContacts[uid].length;});return total;}
UI.importAll=function(e){imgLoader.unload();var selected=Object.keys(selectedContacts).length;var unSelected=getTotalUnselected();var total=selected+unSelected;cancelled=false;var progress;if(selected>0){progress=Curtain.show('progress','import');progress.setTotal(total);Curtain.oncancel=cancelImport;Importer.importAll(function on_all_imported(totalImported,numDups){if(typeof serviceConnector.oncontactsimported==='function'){serviceConnector.oncontactsimported(existingContacts,friendsImported,function(){friendsImported=true;});}
if(!cancelled&&unSelected>0){progress.setFrom('update');cleanContacts(function callback(){onUpdate(progress.getValue(),numDups);},progress);}else{onUpdate(progress.getValue(),numDups);}},progress);}else if(unSelected>0){progress=Curtain.show('progress','update');progress.setTotal(total);Curtain.oncancel=cancelImport;cleanContacts(function callback(){onUpdate(progress.getValue());},progress);}};UI.selectAll=function(e){deSelectAllButton.disabled=false;selectAllButton.disabled=true;window.setTimeout(function doSelectAll(){bulkSelection(true);unSelectedContacts={};selectedContacts={};for(var uid in selectableFriends){selectedContacts[uid]=selectableFriends[uid];}
checkUpdateButton();},0);return false;};UI.unSelectAll=function(e){deSelectAllButton.disabled=true;selectAllButton.disabled=false;window.setTimeout(function doUnSelectAll(){bulkSelection(false);selectedContacts={};unSelectedContacts={};for(var uid in existingContactsByUid){unSelectedContacts[uid]=existingContactsByUid[uid];}
checkUpdateButton();},0);return false;};function bulkSelection(value){window.setTimeout(function(){doSelect(value,checkNodeList,0,10);},0);}
function doSelect(value,list,from,chunkSize){var total=list.length;for(var j=from;j<from+chunkSize&&j<total;j++){setChecked(list[j],value);}
var leftInterval=from+chunkSize;var rightInterval=leftInterval+chunkSize<total?leftInterval+chunkSize:total;window.setTimeout(function(){doSelect(value,list,leftInterval,rightInterval);},0);}
UI.selection=function(e){var out=false;var target=e.target;if(target&&target.dataset.uuid){var uuid=target.dataset.uuid;var checkbox=target.querySelector('input[type="checkbox"]');setChecked(checkbox,!checkbox.checked);if(checkbox.checked===true){if(unSelectedContacts[uuid]){delete unSelectedContacts[uuid];}else{selectedContacts[uuid]=myFriendsByUid[uuid];}}else{delete selectedContacts[uuid];if(existingContactsByUid[uuid]){unSelectedContacts[uuid]=existingContactsByUid[uuid];}}
checkDisabledButtons();out=true;}
return out;};Importer.getContext=function(){var out='contacts';if(window.location.search.indexOf('ftu')!==-1){out='ftu';}
return out;};function releaseObjs(cfdata){if(!cfdata){return;}
selectedContacts[cfdata.uid]=null;myFriendsByUid[cfdata.uid]=null;selectableFriends[cfdata.uid]=null;myFriends[cfdata._idxFriendsArray]=null;if(cfdata.fbInfo&&Array.isArray(cfdata.fbInfo.photo)){cfdata.fbInfo.photo=null;}}
function doImportAll(importedCB,progress){if(cancelled){return;}
checkNodeList=null;theImporter=serviceConnector.getImporter(selectedContacts,access_token);var cpuLock,screenLock;theImporter.oncontactimported=function(cfdata){releaseObjs(cfdata);progress.update();};theImporter.onsuccess=function(totalImported,totalMerged){ongoingImport=false;window.setTimeout(function imported(){utils.misc.setTimestamp(serviceConnector.name,()=>{importedCB(totalImported,totalMerged);});},0);if(cpuLock){cpuLock.unlock();}
if(screenLock){screenLock.unlock();}};cpuLock=navigator.requestWakeLock('cpu');screenLock=navigator.requestWakeLock('screen');document.querySelector('#groups-list').innerHTML='';ongoingImport=true;theImporter.start();}
Importer.importAll=function(importedCB,progress){if(isOnLine===true){doImportAll(importedCB,progress);}
else{window.console.warn('User is not online!!');showOfflineDialog(function(){doImportAll(importedCB,progress);},function(){Curtain.hide(UI.end);});}};})(document);}

View file

@ -0,0 +1,210 @@
'use strict';
/* global Rest */
/* global utils */
if (!window.LiveConnector) {
window.LiveConnector = (function() {
var LIVE_ENDPOINT = 'https://apis.live.net/v5.0/';
var CONTACTS_RESOURCE = 'me/contacts';
var PICTURE_RESOURCE = '/picture';
var LIVE_CATEGORY = 'live';
var itemsTypeMap = {
'personal': 'personal',
'mobile': 'mobile',
'business': 'work',
'other': 'other',
'preferred': 'personal'
};
function fillAddress(addrType, addrValue) {
var out = {
type: [addrType],
streetAddress: addrValue.street || '',
locality: addrValue.city || '',
region: addrValue.state || '',
countryName: addrValue.region || '',
postalCode: addrValue.postal_code || ''
};
return out;
}
function getURI(liveContact) {
return 'urn:uuid:' + (liveContact.user_id || liveContact.id);
}
function resolveURI(uri) {
var components = uri.split(':');
// The third element is the user id of the live contact
return components[2];
}
function LiveConnector() {
}
LiveConnector.prototype = {
listAllContacts: function(access_token, callbacks) {
var uriElements = [LIVE_ENDPOINT, CONTACTS_RESOURCE, '?',
'access_token', '=', access_token];
return Rest.get(uriElements.join(''), callbacks);
},
listDeviceContacts: function(callbacks) {
var filterOptions = {
filterValue: LIVE_CATEGORY,
filterOp: 'contains',
filterBy: ['category']
};
var req = navigator.mozContacts.find(filterOptions);
req.onsuccess = function() {
callbacks.success(req.result);
};
req.onerror = function() {
callbacks.error(req.error);
};
},
getImporter: function(contactsList, access_token) {
return new window.ContactsImporter(contactsList, access_token, this);
},
cleanContacts: function(contactsList, mode, cb) {
// Just a placeholder for the moment
var cleaner = new window.ContactsCleaner(contactsList);
window.setTimeout(cleaner.start, 0);
cb(cleaner);
},
adaptDataForShowing: function(source) {
var out = source;
out.uid = source.user_id || source.id;
out.givenName = [source.first_name || ''];
out.familyName = [source.last_name || ''];
out.email1 = source.emails.account ||
source.emails.preferred ||
source.emails.personal ||
'';
out.contactPictureUri = [LIVE_ENDPOINT, out.uid,
PICTURE_RESOURCE, '?type=medium'].join('');
return out;
},
adaptDataForSaving: function live2MozContact(liveContact) {
var out = {
givenName: [liveContact.first_name || ''],
familyName: [liveContact.last_name || ''],
name: [liveContact.name || ''],
tel: [],
email: [],
adr: [],
photo: liveContact.photo,
category: [LIVE_CATEGORY],
url: [{
type: ['source'],
value: getURI(liveContact)
}]
};
// Months in JS goes from 0 to 11 :(
var bmonth = liveContact.birth_month - 1;
var bday = liveContact.birth_day;
if (bmonth > -1 && bday) {
var birthdate = out.bday = new Date();
birthdate.setUTCDate(bday);
birthdate.setUTCMonth(bmonth, bday);
birthdate.setUTCFullYear(utils.misc.FLAG_YEAR_IGNORED);
}
var liveEmails = liveContact.emails || {};
var alreadyAddedEmails = {};
Object.keys(liveEmails).forEach(function(emailType) {
var emailValue = liveEmails[emailType];
var present = (typeof alreadyAddedEmails[emailValue] === 'boolean');
if (emailValue && !present) {
out.email.push({
type: [itemsTypeMap[emailType]],
value: emailValue
});
alreadyAddedEmails[emailValue] = true;
}
});
var livePhones = liveContact.phones || {};
Object.keys(livePhones).forEach(function(phoneType) {
var phoneValue = livePhones[phoneType];
if (phoneValue) {
out.tel.push({
type: [itemsTypeMap[phoneType]],
value: phoneValue
});
}
});
var liveAddresses = liveContact.addresses || {};
Object.keys(liveAddresses).forEach(function(addrType) {
var addrValue = liveAddresses[addrType];
if (addrValue) {
out.adr.push(fillAddress(itemsTypeMap[addrType], addrValue));
}
});
return out;
},
// It would allow to know the UID of a service contact already imported
// on the device. That is needed by the generic importer, for live
// a dummy implementation as we are currently not supporting updates
getContactUid: function(deviceContact) {
var out = '-1';
var url = deviceContact.url;
if (Array.isArray(url)) {
var targetUrls = url.filter(function(aUrl) {
return Array.isArray(aUrl.type) &&
aUrl.type.indexOf('source') !== -1;
});
if (targetUrls[0]) {
out = resolveURI(targetUrls[0].value);
}
}
return out;
},
get name() {
return 'live';
},
get automaticLogout() {
return true;
},
downloadContactPicture: function(contact, access_token, callbacks) {
if (contact.user_id) {
var uriElements = [LIVE_ENDPOINT, contact.user_id, PICTURE_RESOURCE,
'?', 'access_token', '=', access_token];
return Rest.get(uriElements.join(''), callbacks, {
responseType: 'blob'
});
}
else {
callbacks.success(null);
}
},
startSync: function() {
// Sync not supported
}
};
return new LiveConnector();
})();
}

View file

@ -0,0 +1,8 @@
;'use strict';var utils=window.utils||{};if(typeof utils.config==='undefined'){(function(){var configs={};var initializing={};var initialized={};var EVENT_INITIALIZED='config_initialized';utils.config=utils.config||{};utils.config.reset=function(){configs={};initializing={};initialized={};};function onLoad(xhr,file,resolveCb){configs[file]=xhr.response;delete initializing[file];initialized[file]=true;document.dispatchEvent(new CustomEvent(EVENT_INITIALIZED,{detail:{file:file,success:true}}));resolveCb(configs[file]);}
utils.config.load=function(file){return new Promise(function(resolve,reject){var isInitialized=initialized[file];if(isInitialized===true){resolve(configs[file]);return;}
var isInitializing=initializing[file];if(isInitializing){var handler=function(expectedFile,e){document.removeEventListener(EVENT_INITIALIZED,handler);var file=e.detail.file;if(expectedFile!==file){return;}
if(e.detail.success){resolve(configs[file]);}
else{reject();}}.bind(null,file);document.addEventListener(EVENT_INITIALIZED,handler);return;}
initializing[file]=true;var xhr=new XMLHttpRequest();xhr.open('GET',file,true);xhr.responseType='json';xhr.onload=function(){var resolvedCb=resolve;if(xhr.status!==200){resolvedCb=reject;}
onLoad(xhr,file,resolvedCb);};xhr.onerror=function(){document.dispatchEvent(new CustomEvent(EVENT_INITIALIZED,{detail:{file:file,success:false}}));delete initializing[file];reject(xhr.error);};try{xhr.send(null);}
catch(e){console.error('Error while loading config file: ',e.message);onLoad({status:404},file,reject);}});};})();}

View file

@ -0,0 +1,188 @@
/* global VCardReader, ConfirmDialog, contacts, LazyLoader, Loader */
/* global Matcher */
'use strict';
var utils = window.utils || {};
/**
* Loads the file passed on the activity, reads the text from it, and
* import the info
* Passes the id of last imported contact in the callback
*/
utils.importFromVcard = function(file, callback) {
var MERGE_DEPENDENCIES = [
'/shared/js/simple_phone_matcher.js',
'/shared/js/contacts/merger_adapter.js',
'/shared/js/contacts/contacts_merger.js',
'/shared/js/contacts/contacts_matcher.js'
];
Loader.utility('Overlay', function() {
LazyLoader.load(MERGE_DEPENDENCIES, _loaded);
});
function _loaded() {
var importedContacts = 0;
var cancelled = false;
var importer = null;
var text = null;
var progress = utils.overlay.show(
'memoryCardContacts-reading',
'activityBar'
);
utils.overlay.showMenu();
utils.overlay.oncancel = function oncancel() {
cancelled = true;
if (importer) {
importer.finish();
} else {
utils.overlay.hide();
}
};
readVCard(function(vcardText) {
if (vcardText) {
processTextFromFile(vcardText);
} else {
callback();
}
});
function readVCard(cb) {
var reader;
if (typeof file === 'string') {
cb(file);
} else {
reader = new FileReader();
reader.onloadend = function() {
text = reader.result;
if (text) {
cb(text);
}
};
try {
reader.readAsText(file);
}
catch (ex) {
console.error('Error reading the file ' + ex.message);
import_error();
cb();
}
}
}
function processTextFromFile(textFromFile) {
if (cancelled) {
return;
}
var reader = new VCardReader(textFromFile);
var match = textFromFile && textFromFile.match(/END:VCARD/g);
var cursor = reader.getAll();
var numContacts = match ? match.length : 0;
var numDupsMerged = 0;
var firstContact = null;
var _doContinue = function(savedContact) {
if (savedContact) {
if (!firstContact) {
firstContact = savedContact;
}
imported_contact();
}
if (!cancelled) {
cursor.continue();
}
};
cursor.onsuccess = function(evt) {
if (evt.target.result) {
var contact = evt.target.result;
Matcher.match(contact, 'passive', {
onmatch: function(matches) {
var callbacks = {
success: function(mergedContact) {
numDupsMerged++;
_doContinue(mergedContact);
},
error: _doContinue
};
contacts.adaptAndMerge(contact, matches, callbacks);
},
onmismatch: function() {
var req = navigator.mozContacts.save(contact);
req.onsuccess = function() {
_doContinue(contact);
};
}
});
// No more contacts
} else {
utils.overlay.hide();
if (!cancelled) {
var msgImported = {
id: 'memoryCardContacts-imported3',
args: {n: importedContacts}
};
var msgDupsMerged = numDupsMerged ? {
id: 'contactsMerged',
args: {numDups: numDupsMerged}
} : null;
utils.status.show(msgImported, msgDupsMerged);
}
if (importedContacts > 0) {
callback(importedContacts, firstContact.id);
} else {
callback(importedContacts);
}
}
};
import_read(numContacts);
}
function import_read(n) {
progress.setClass('progressBar');
progress.setHeaderMsg('memoryCardContacts-importing');
progress.setTotal(n);
}
function imported_contact() {
importedContacts++;
if (!cancelled) {
progress.update();
}
}
function import_error(e) {
console.error('Error importing from vcard: ' + e.message);
// Showing error message allowing user to retry
var cancel = {
title: 'cancel',
callback: function() {
ConfirmDialog.hide();
}
};
var retry = {
title: 'retry',
isRecommend: true,
callback: function() {
ConfirmDialog.hide();
// And now the action is reproduced one more time
processTextFromFile(text);
}
};
ConfirmDialog.show(null, 'memoryCardContacts-error',
cancel, retry);
utils.overlay.hide();
}
}
};

View file

@ -0,0 +1,237 @@
/*jshint loopfunc: true */
/* global mozContact, LazyLoader, utils, contacts */
/* global Matcher */
/* exported SimContactsImporter */
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
/**
* Imports contacts stored in the SIM card and saves them into
* navigator.mozContacts. Three steps => three callback as arguments:
* - onread: SIM card has been read properly;
* - onimport: A Contact has been imported
* - onfinish: contacts have been saved into navigator.mozContacts;
* - onerror: SIM card is empty or could not be read.
*/
function SimContactsImporter(targetIcc) {
if (targetIcc === null) {
throw new Error('We need an icc to continue with this operation');
}
var pointer = 0;
var CHUNK_SIZE = 5;
var numResponses = 0;
var self = this;
var mustFinish = false;
var loadedMatch = false;
var DEFAULT_TEL_TYPE = 'other';
var icc = targetIcc;
var iccId = icc.iccInfo && icc.iccInfo.iccid;
var numDupsMerged = 0;
function generateIccContactUrl(contactid) {
var urlValue = 'urn:' + 'uuid:' + (iccId || 'iccId') + '-' + contactid;
return [{
type: ['source', 'sim'],
value: urlValue
}];
}
function notifyFinish() {
if (typeof self.onfinish === 'function') {
window.setTimeout(self.onfinish, 0, numDupsMerged, iccId);
}
}
function notifyImported() {
if (typeof self.onimported === 'function') {
window.setTimeout(self.onimported, 0);
}
}
function continueCb() {
numResponses++;
pointer++;
notifyImported();
if (pointer < self.items.length && numResponses === CHUNK_SIZE) {
numResponses = 0;
mustFinish ? notifyFinish() : importSlice(pointer);
}
else if (pointer >= self.items.length) {
notifyFinish();
}
}
function startMigration() {
if (!mustFinish && Array.isArray(self.items) && self.items.length > 0) {
importSlice(0);
}
else {
notifyFinish();
}
}
function onContactsReadyForImport() {
if (typeof self.onread === 'function') {
// This way the total number can be known by the caller
self.onread(self.items.length);
}
if (loadedMatch) {
startMigration();
}
else {
document.addEventListener('matchLoaded', function mloaded() {
document.removeEventListener('matchLoaded', mloaded);
startMigration();
});
}
}
this.start = function() {
numDupsMerged = 0;
if (mustFinish) {
notifyFinish();
return;
}
LazyLoader.load([
'/shared/js/simple_phone_matcher.js',
'/shared/js/contacts/contacts_matcher.js',
'/shared/js/contacts/contacts_merger.js',
'/shared/js/contacts/utilities/image_thumbnail.js',
'/shared/js/contacts/merger_adapter.js'
], function loaded() {
loadedMatch = true;
document.dispatchEvent(new CustomEvent('matchLoaded'));
});
var requestAdn, requestSdn;
// request contacts with readContacts() -- valid types are:
// 'adn': Abbreviated Dialing Numbers
// 'fdn': Fixed Dialing Numbers
// 'sdn': Service Dialing Numbers
if (icc && icc.readContacts) {
requestAdn = icc.readContacts('adn');
} else {
throw new Error('Not able to obtain a SIM import function from platform');
}
requestAdn.onsuccess = function onsuccess() {
if (mustFinish) {
notifyFinish();
return;
}
self.items = requestAdn.result || [];
requestSdn = icc.readContacts('sdn');
requestSdn.onsuccess = function onsuccess() {
if (mustFinish) {
notifyFinish();
return;
}
if (Array.isArray(requestSdn.result)) {
self.items = self.items.concat(requestSdn.result);
}
onContactsReadyForImport();
};
requestSdn.onerror = function error() {
if (mustFinish) {
notifyFinish();
return;
}
console.warn('Could not read SDN Contacts from SIM Card', error.name);
onContactsReadyForImport();
};
};
requestAdn.onerror = function error() {
if (typeof self.onerror === 'function') {
self.onerror(requestAdn.error);
}
};
};
this.finish = function() {
mustFinish = true;
};
/**
* store mozContact elements -- each returned mozContact has two properties:
* .name : [ string ]
* .tel : [{ number: string, type: string }]
* The 'name' property is only related to the mozContact element itself --
* let's use it as the default 'givenName' value.
*/
function importSlice(from) {
for (var i = from; i < from + CHUNK_SIZE && i < self.items.length; i++) {
var item = self.items[i];
var parsedName = utils.misc.parseName(item.name[0]);
item.givenName = [parsedName.givenName];
item.familyName = [parsedName.familyName];
if (Array.isArray(item.tel)) {
var telItems = [];
for (var j = 0; j < item.tel.length; j++) {
var aTel = item.tel[j];
// Filtering out empty values
if (aTel.value && aTel.value.trim()) {
aTel.type = [DEFAULT_TEL_TYPE];
telItems.push(aTel);
}
}
item.tel = telItems;
}
item.category = ['sim'];
item.url = generateIccContactUrl(item.id);
// Item is presumably a mozContact but for some reason if
// we don't create a new mozContact sometimes the save call fails
var contact = item;
var cbs = {
onmatch: function(results) {
var mergeCbs = {
success: function() {
numDupsMerged++;
continueCb();
},
error: function(e) {
window.console.error('Error while merging: ', e.name);
continueCb();
}
};
contacts.adaptAndMerge(this, results, mergeCbs);
}.bind(contact),
onmismatch: function() {
saveContact(this);
}.bind(contact)
};
Matcher.match(item, 'passive', cbs);
}
} // importSlice
function saveContact(contact) {
var req = window.navigator.mozContacts.save(new mozContact(contact));
req.onsuccess = function saveSuccess() {
continueCb();
};
req.onerror = function saveError() {
console.error('SIM Import: Error importing ', contact.id,
req.error.name);
continueCb();
};
}
}

View file

@ -0,0 +1,18 @@
;;;'use strict';var utils=window.utils||{};var SCALE_RATIO=window.devicePixelRatio||1;var LAST_IMPORT_TIMESTAMP_SUFFIX='_last_import_timestamp';function scale(size){return Math.round(SCALE_RATIO*size);}
if(!utils.misc){utils.misc={};utils.misc.toMozContact=function(contact){var outContact=contact;if(!(contact instanceof mozContact)){outContact=new mozContact(contact);outContact.id=contact.id||outContact.id;}
return outContact;};const FLAG_YEAR_IGNORED=9996;utils.misc.FLAG_YEAR_IGNORED=FLAG_YEAR_IGNORED;utils.misc.formatDate=function(date,customFormat){var dateString=null;try{var format=customFormat||{month:'short',day:'numeric'};var offset=date.getTimezoneOffset()*60*1000;var normalizedDate=new Date(date.getTime()+offset);var year=normalizedDate.getFullYear();if(year!==FLAG_YEAR_IGNORED){format.year='numeric';}
var f=new Intl.DateTimeFormat(navigator.languages,format);dateString=f.format(normalizedDate);}catch(err){console.error('Error parsing date: ',err);throw err;}
return dateString;};utils.misc.parseName=function(nameString){var MIN_LENGHT_SIGNIFICATIVE=3;var MIN_LENGHT_FN=2;var out={givenName:'',familyName:''};if(!nameString){return out;}
var str=nameString.trim();if(!str){return out;}
function startsWithUpper(str){var firstLetter=str.charAt(0);var capFirstLetter=firstLetter.toLocaleUpperCase();return(capFirstLetter===firstLetter);}
var tokens=nameString.split(/\s+/);var significativeTokens={};for(var j=0;j<tokens.length;j++){var token=tokens[j];if(token.length>MIN_LENGHT_SIGNIFICATIVE||startsWithUpper(token)){significativeTokens[token]=true;}}
var totalTokens=tokens.length;var lastToken=totalTokens-1;var rolePrevToken;var remainingTokens=Object.keys(significativeTokens).length;var outGivenNames=[],outFamilyNames=[];var numFns=0;while(lastToken>=0){if(significativeTokens[tokens[lastToken]]){remainingTokens--;}
var currToken=tokens[lastToken],nextToken=tokens[lastToken+1],prevToken=tokens[lastToken-1];if(!nextToken&&prevToken){outFamilyNames.push(currToken);numFns++;rolePrevToken='FN';}
else if(!prevToken){outGivenNames.push(currToken);rolePrevToken='GN';}
else if(nextToken){if(!significativeTokens[currToken]){if(rolePrevToken==='FN'){outFamilyNames.push(currToken);}
else{outGivenNames.push(currToken);}}
else if(currToken.length<MIN_LENGHT_FN){outGivenNames.push(currToken);rolePrevToken='GN';}
else{if(remainingTokens>=2&&numFns<2){outFamilyNames.push(currToken);numFns++;rolePrevToken='FN';}
else{outGivenNames.push(currToken);rolePrevToken='GN';}}}
lastToken--;}
out.givenName=outGivenNames.reverse().join(' ').trim();out.familyName=outFamilyNames.reverse().join(' ').trim();return out;};utils.misc.getPreferredPictureBox=function(){var imgThumbSize=oauthflow.params.facebook.imgThumbSize;var out={width:scale(imgThumbSize)};out.height=out.width;return out;};utils.misc.getPreferredPictureDetail=function(){var imgDetailWidth=oauthflow.params.facebook.imgDetailWidth;return scale(imgDetailWidth);};utils.misc.setTimestamp=function(type,callback){ImportStatusData.put(type+LAST_IMPORT_TIMESTAMP_SUFFIX,Date.now()).then(callback);};utils.misc.getTimestamp=function(type,callback){ImportStatusData.get(type+LAST_IMPORT_TIMESTAMP_SUFFIX).then(callback);};}

View file

@ -0,0 +1,196 @@
'use strict';
var utils = window.utils || {};
(function() {
utils.overlay = {};
var overlay = document.querySelector('#loading-overlay'),
statusContainer = overlay.querySelector('p[role="status"]'),
progressActivity = document.querySelector('#progress-activity'),
progressTitle = document.querySelector('#progress-title'),
progressElement = document.querySelector('#progress-element'),
progressMsg = document.querySelector('#progress-msg'),
menu = document.querySelector('#loading-overlay menu'),
cancelButton = document.querySelector('#cancel-overlay');
// Constructor for the progress element
function ProgressBar(pMsgId, pClass) {
var counter = 0;
var total = 0;
var progressTextId = pMsgId || 'genericProgress';
var clazz = pClass;
progressElement.setAttribute('value', 0);
function showMessage() {
var attributes = progressTextId == 'infiniteProgress' ?
{ n: counter } :
{ current: counter, total: total };
document.l10n.setAttributes(progressMsg,
progressTextId,
attributes);
}
/**
* Updates progress bar and message. It takes an optional `value` parameter
* to overwrite the internal counter with this value.
* @param {Number} value Overrides internal counter.
*/
this.update = function(value) {
if (value && value <= total && value >= counter) {
counter = value;
} else {
counter++;
}
progressElement.setAttribute('value',
((counter * 100) / total).toFixed()); // Percent fraction is not needed.
showMessage();
};
this.setTotal = function(ptotal) {
total = ptotal;
};
this.setClass = function(clazzName) {
setClass(clazzName);
clazz = clazzName;
// To refresh the message according to the new clazzName
if (clazzName === 'activityBar' || clazzName === 'spinner') {
progressMsg.removeAttribute('data-l10n-id');
}
};
this.setHeaderMsg = function(headerMsgId) {
progressTitle.setAttribute('data-l10n-id', headerMsgId);
};
} // ProgressBar
utils.overlay.isAnimationPlaying = false;
utils.overlay.isShown = false;
utils.overlay._show = function _show(messageId, progressClass, textId) {
progressActivity.classList.remove('hide');
if (typeof messageId === 'string') {
progressTitle.setAttribute('data-l10n-id', messageId);
} else {
document.l10n.setAttributes(progressTitle,
messageId.id,
messageId.args);
}
progressMsg.textContent = null;
progressMsg.removeAttribute('data-l10n-id');
if (utils.overlay.isShown) {
return;
}
overlay.classList.remove('hide');
overlay.classList.remove('fade-out');
overlay.classList.add('fade-in');
utils.overlay.isAnimationPlaying = true;
// Custom event that can be used to apply (screen reader) visibility
// changes.
window.dispatchEvent(new CustomEvent('loadingoverlayshowing'));
overlay.addEventListener('animationend', function ov_onFadeIn(ev) {
utils.overlay.isAnimationPlaying = false;
overlay.removeEventListener('animationend', ov_onFadeIn);
overlay.classList.remove('no-opacity');
utils.overlay.isShown = true;
});
};
utils.overlay.show = function show(messageId, progressClass, textId) {
var out;
// In the case of an spinner this object will not be really used
out = new ProgressBar(textId, progressClass);
setClass(progressClass);
utils.overlay.hideMenu(); // By default
if (!utils.overlay.isAnimationPlaying) {
utils.overlay._show(messageId, progressClass, textId);
return out;
}
overlay.addEventListener('animationend',
function ov_showWhenFinished(ev) {
overlay.removeEventListener('animationend', ov_showWhenFinished);
utils.overlay._show(messageId, progressClass, textId);
}
);
return out;
};
utils.overlay.showMenu = function showMenu() {
menu.classList.add('showed');
};
utils.overlay.hideMenu = function hideMenu() {
menu.classList.remove('showed');
};
Object.defineProperty(utils.overlay, 'oncancel', {
set: function(cancelCb) {
if (typeof cancelCb === 'function') {
cancelButton.disabled = false;
cancelButton.onclick = function on_cancel(e) {
delete cancelButton.onclick;
cancelCb();
return false;
};
}
}
});
function setAsProgress() {
statusContainer.classList.remove('loading-icon');
progressElement.setAttribute('max', '100');
progressElement.setAttribute('value', '0');
}
function setClass(clazzName) {
switch (clazzName) {
case 'spinner':
progressElement.classList.remove('pack-activity');
statusContainer.classList.add('loading-icon');
progressElement.removeAttribute('max');
progressElement.removeAttribute('value');
break;
case 'activityBar':
case 'progressActivity':
progressElement.classList.add('pack-activity');
setAsProgress();
break;
case 'progressBar':
progressElement.classList.remove('pack-activity');
setAsProgress();
break;
}
}
utils.overlay._hide = function ov__hide() {
if (!utils.overlay.isShown) {
return;
}
overlay.classList.remove('fade-in');
overlay.classList.add('fade-out');
utils.overlay.isAnimationPlaying = true;
// Custom event that can be used to apply (screen reader) visibility
// changes.
window.dispatchEvent(new CustomEvent('loadingoverlayhiding'));
overlay.addEventListener('animationend', function ov_onFadeOut(ev) {
utils.overlay.isAnimationPlaying = false;
overlay.removeEventListener('animationend', ov_onFadeOut);
progressActivity.classList.add('hide');
overlay.classList.add('no-opacity');
overlay.classList.add('hide');
utils.overlay.isShown = false;
});
};
utils.overlay.hide = function ov_hide() {
if (!utils.overlay.isAnimationPlaying) {
utils.overlay._hide();
return;
}
overlay.addEventListener('animationend',
function ov_hideWhenFinished(ev) {
overlay.removeEventListener('animationend', ov_hideWhenFinished);
utils.overlay._hide();
}
);
};
})();

View file

@ -0,0 +1,217 @@
'use strict';
var utils = window.utils || {};
if (!utils.sdcard) {
var SdCard = utils.sdcard = {
NOT_INITIALIZED: 0,
NOT_AVAILABLE: 1,
AVAILABLE: 2,
SHARED: 3,
observers: {}
};
SdCard.status = SdCard.NOT_INITIALIZED;
SdCard.deviceStorages = navigator.getDeviceStorages('sdcard');
SdCard.deviceStorage = Array.isArray(SdCard.deviceStorages) &&
SdCard.deviceStorages[0];
SdCard._toStatus = function toStatus(state) {
switch (state) {
case 'available':
SdCard.status = SdCard.AVAILABLE;
break;
// NOTE: The 'SHARED' state is equivalent to NOT_AVAILABLE, since we
// could have inconsistencies if we allow changing the sdcard content
// meanwhile exporting/importing.
// It's needed to have it's own state solely for showing the proper
// message when needed
case 'shared':
SdCard.status = SdCard.SHARED;
break;
case 'unavailable':
case 'deleted':
SdCard.status = SdCard.NOT_AVAILABLE;
break;
}
};
SdCard.updateStorageState = function sd_updateStorageState(state) {
SdCard._toStatus(state);
Object.keys(this.observers).forEach(function onObserver(name) {
if (typeof(SdCard.observers[name]) === 'function') {
SdCard.observers[name].call(null, state);
}
});
};
if (SdCard.deviceStorage) {
SdCard.deviceStorage
.addEventListener('change', function sd_deviceStorageChangeHandler(e) {
SdCard.updateStorageState(e.reason);
});
if (SdCard.status === SdCard.NOT_INITIALIZED) {
SdCard.deviceStorage.available().onsuccess = (function(e) {
SdCard.updateStorageState(e.target.result);
});
}
}
/**
* Check whether there is a SD card inserted in the device.
* @return {Boolean} true if sdcard available, false otherwise.
*/
SdCard.checkStorageCard = function sd_checkStorageCard() {
return SdCard.status === SdCard.AVAILABLE;
};
SdCard.getStatus = function(cb) {
if (!SdCard.deviceStorage) {
SdCard._toStatus('unavailable');
cb(SdCard.status);
return;
}
var req = SdCard.deviceStorage.available();
req.onsuccess = function() {
SdCard._toStatus(req.result);
cb(SdCard.status);
};
req.onerror = function() {
console.error('Error while determining SD Status', req.error.name);
cb(SdCard.status);
};
};
/**
* Retrieves files that adjust to the kind specified in the
* parameters from the SD card inserted in the device.
*
* @param {String[]} mimes Possible mime types of the files to be found.
* @param {String[]} exts Possible file extensions of the files to be found.
* @param {Function} cb Callback.
*/
SdCard.retrieveFiles = function retrieveFilesContent(mimes, exts, cb) {
var fileArray = [];
var forbiddenDirectories = ['.Trash'];
// getDeviceStorages('sdcard') can return more than one storage unit
// in different order, depends on the default media storage setting,
// so we need to iterate though them to be sure we checked the proper
// SD Card
var storages = navigator.getDeviceStorages('sdcard');
var numberOfStorages = storages.length;
var currentStorage = 0;
var cursorOnError = function() {
cb(this.error.name);
};
// RegExp that checks if a string contains any of the extensions.
var reExt = new RegExp('.(' + exts.join('|') + ')$');
var cursorOnSuccess = function(e) {
var file = e.target.result;
// If we don't have any more files on the storage...
if (!file) {
// ... but we have more storages...
if (++currentStorage < numberOfStorages) {
// ... check the next one
cursor = storages[currentStorage].enumerate();
cursor.onsuccess = cursorOnSuccess;
cursor.onerror = cursorOnError;
} else {
cb(null, fileArray);
}
} else {
if ((mimes.indexOf(file.type) === -1) && !reExt.test(file.name)) {
cursor.continue();
} else {
if (forbiddenDirectories.indexOf(file.name) === -1) {
fileArray.push(file);
cursor.continue();
}
}
}
};
var cursor = storages[currentStorage].enumerate();
cursor.onsuccess = cursorOnSuccess;
cursor.onerror = cursorOnError;
};
/**
* Incrementally extracts vcard text from a file.
*
* @param {Object} file File Object.
* @param {Function} cb Function to call for each chunk of vcards retrieved
* from the file.
*/
SdCard.getTextFromFile = function(file, cb) {
if (!file) {
return Promise.reject();
}
return new Promise((resolve, reject) => {
const END_VCARD = 'end:vcard';
const CHUNK_SIZE = 1 * 1024 * 1024; // 1Mb
var fileReader = new FileReader();
var result = 0;
(function readChunk(offset, buffer) {
if (offset >= file.size) {
resolve(result);
return;
}
var chunk = file.slice(offset, offset + CHUNK_SIZE);
offset += CHUNK_SIZE;
fileReader.readAsText(chunk);
fileReader.onload = function() {
buffer += fileReader.result;
if (!buffer.match(/end:vcard/gi)) {
readChunk(offset, buffer);
return;
}
var lastContactInChunk =
buffer.toLowerCase().lastIndexOf(END_VCARD) + END_VCARD.length;
cb(buffer.substr(0, lastContactInChunk)).then(partialResult => {
result += partialResult;
readChunk(offset, buffer.substr(lastContactInChunk + 1));
}).catch(error => {
console.warn(error);
});
};
})(0 /* initial offset */, '' /* initial empty buffer */);
});
};
// Subscribe a callback for sdcard changes. A name is needed
// to refer to this observer.
// @param {String} identifier for the observer
// @param {Function} the callback to be invoqued on a change
// @param {Boolean} if the callback exists, override it
SdCard.subscribeToChanges = function(name, func, force) {
if (this.observers[name] !== undefined && !force) {
return false;
}
this.observers[name] = func;
return true;
};
// Remove the subscription to listen to changes for the specific
// identifier
// @param {String} the identifier
SdCard.unsubscribeToChanges = function(name) {
if (this.observers[name]) {
delete this.observers[name];
return true;
} else {
return false;
}
};
}

View file

@ -0,0 +1,5 @@
;'use strict';var utils=window.utils||{};utils.status=(function(){var STATUS_TIME=2000;var statusMsg=document.querySelector('#statusMsg');var hidingTimeout;var transitionEndTimeout;var additionalLine;var hideAnimationDone=function(){statusMsg.removeEventListener('transitionend',hideAnimationDone);statusMsg.classList.add('hidden');};var showAnimationDone=function(){if(transitionEndTimeout){clearTimeout(transitionEndTimeout);}
statusMsg.removeEventListener('transitionend',showAnimationDone);hidingTimeout=setTimeout(utils.status.hide,STATUS_TIME);};var hideStatus=function(){statusMsg.addEventListener('transitionend',hideAnimationDone);statusMsg.classList.remove('opening');statusMsg.classList.remove('bannerStart');statusMsg.querySelector('p').removeAttribute('data-l10n-id');if(additionalLine){statusMsg.removeChild(additionalLine);additionalLine=null;}};var setL10nAttributes=function(node,l10n){if(l10n&&l10n.id){navigator.mozL10n.setAttributes(node,l10n.id,l10n.args);return true;}else{console.error('Status arguments must be objects');return false;}};var showStatus=function(mainMessage,extra){statusMsg.removeEventListener('transitionend',showAnimationDone);statusMsg.removeEventListener('transitionend',hideAnimationDone);LazyLoader.load([statusMsg],function _loaded(){if(!setL10nAttributes(statusMsg.querySelector('p'),mainMessage)){return;}
if(extra){additionalLine=document.createElement('p');statusMsg.appendChild(additionalLine);setL10nAttributes(additionalLine,extra);}
if(statusMsg.classList.contains('opening')){clearTimeout(hidingTimeout);hidingTimeout=setTimeout(hideStatus,STATUS_TIME);return;}
statusMsg.classList.remove('hidden');statusMsg.addEventListener('transitionend',showAnimationDone);setTimeout(function displaying(){statusMsg.classList.add('opening');statusMsg.classList.add('bannerStart');transitionEndTimeout=setTimeout(showAnimationDone,STATUS_TIME);},10);});};return{show:showStatus,hide:hideStatus};})();

View file

@ -0,0 +1,885 @@
/* global contacts, LazyLoader, utils, Rest, MimeMapper */
/* global Matcher */
/* exported VCFReader */
'use strict';
function parseDataUri(str) {
var re = /^data:(.+);(charset=([^;]+))?;?base64,(.*)$/;
var matches = re.exec(str);
if (matches) {
return {
mime: matches[1],
charset: matches[3],
value: matches[4]
};
}
return null;
}
function ensureMimeType(type) {
type = type.toLowerCase();
if (!MimeMapper.isSupportedType(type)) {
type = MimeMapper.guessTypeFromExtension(type) || type;
}
return type || '';
}
/**
* Given a string, expected to be in base64 format, an a
* content type, returns the corresponding Blob.
* If cannot be parsed, will return undefined.
*
* @param {string} data as a string.
* @param {string} content type of the expected blob. Can be null.
* @param {int} size of the slice to be parsed.
*/
function b64toBlob(b64Data, contentType, sliceSize) {
if (!b64Data) {
console.error('No b64 data provided to convert to blob.');
return;
}
contentType = contentType || '';
sliceSize = sliceSize || 1024;
function charCodeFromCharacter(c) {
return c.charCodeAt(0);
}
var blob;
try {
var byteCharacters = atob(b64Data);
var byteArrays = [];
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
var slice = byteCharacters.slice(offset, offset + sliceSize);
var byteNumbers = [];
for (var i = 0, l = slice.length; i < l; i++) {
byteNumbers.push(charCodeFromCharacter(slice[i]));
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
blob = new Blob(byteArrays, {type: contentType});
} catch (e) {
console.error('Error parsing base64 data');
}
return blob;
}
var VCFReader = (function _VCFReader() {
var ReBasic = /^([^:]+):(.+)$/;
var ReTuple = /([a-zA-Z]+)=(.+)/;
var WHITE_SPACE = ' ';
// Default tel type class
var DEFAULT_PHONE_TYPE = 'other';
// basic type vcard to mobile type
var VCARD_SIMPLE_TYPES = {
'fax' : 'faxOther',
'faxother' : 'faxOther',
'home' : 'home',
'internet' : 'internet',
'cell' : 'mobile',
'pager' : 'pager',
'personal' : 'home',
'pref' : 'pref',
'text' : 'text',
'textphone' : 'textphone',
'voice' : 'voice',
'work' : 'work'
};
// complex type vcard to mobile type
var VCARD_COMPLEX_TYPES = {
'fax,work' : 'faxOffice',
'fax,home' : 'faxHome',
'voice,work' : 'work',
'voice,home' : 'home'
};
var REST_TIMEOUT = 5000;
function _parseTuple(p) {
var match = p.match(ReTuple);
return match ? [match[1].toLowerCase(), match[2]] : ['type', p];
}
/**
* Parses a line and creates an object with the line type (name), its meta
* properties and its actual value.
*
* @param {string} line Line to be parsed from a VCF.
* @return {{key: string, data: {meta, value}}}
* @private
*/
function _parseLine(line) {
var parsed = ReBasic.exec(line);
if (!parsed) {
return null;
}
// `tuples` is an array containing all the extra properties
var tuples = parsed[1].split(/[;,]/);
var fieldName = tuples.shift().toLowerCase();
// Value will be an empty array if there is only whitespace and
// semi-colons.
var fieldValue = /[^\s;]/.test(parsed[2]) ? parsed[2].split(';') : [];
var meta = {
type: []
};
var len = tuples.length;
for (var i = 0; i < len; i++) {
var tuple = _parseTuple(tuples[i]);
if (tuple[0] === 'type') {
meta.type.push(tuple[1]);
} else {
meta[tuple[0]] = tuple[1];
}
}
// If this is a photo, we correct the value and type
if (fieldName === 'photo') {
if (meta.type.length) {
meta.type = ensureMimeType(meta.type[0]);
}
fieldValue = parsed[2];
// vCard 3.0 expects a mandatory encoding = 'b' parameter for base64
// encoded images, and vCard 2.1 expects a 'BASE64' one.
if (meta.encoding && (meta.encoding.toLowerCase() === 'b' ||
meta.encoding.toLowerCase() === 'base64')) {
meta.encoding = 'base64';
} else {
var parsedUri = parseDataUri(fieldValue);
if (parsedUri) {
meta.type = ensureMimeType(parsedUri.mime);
meta.encoding = 'base64';
fieldValue = parsedUri.value;
}
}
}
return {
key: fieldName,
data: {
meta: meta,
value: fieldValue
}
};
}
/**
* Parse vCard entries split by lines and pass the converted object back to
* the main thread.
*
* @param {string[][]} cardArray Array of array of strings representing vcard.
* @param {function} cb Callback to call on finishe.
*/
var _parseEntries = function(cardArray, cb) {
var parsedCards = [];
function sendIfFinished(contactObj) {
parsedCards.push(contactObj);
if (parsedCards.length === cardArray.length) {
cb(parsedCards);
}
}
for (var i = 0; i < cardArray.length; i++) {
var lines = cardArray[i];
// If there is no array of strings at cardArray[i] (representing a single
// vcard), push a null value to account for a processed card.
if (!lines) {
sendIfFinished(null);
continue;
}
var fields = {};
var len = lines.length;
for (var j = 0; j < len; j++) {
var line = lines[j];
var parsedLine = _parseLine(line);
if (!parsedLine) {
continue;
}
if (!fields[parsedLine.key]) {
fields[parsedLine.key] = [];
}
fields[parsedLine.key].push(parsedLine.data);
}
// If there isn't either a First Name (fn) or a Name (n) this card is not
// a valid one, ignore.
if (!fields.fn && !fields.n) {
sendIfFinished(null);
continue;
}
vcardToContact(fields, sendIfFinished);
}
};
/**
* Matches Quoted-Printable characters in a string
* @type {RegExp}
*/
var qpRegexp = /=([a-zA-Z0-9]{2})/g;
/**
* Decodes a string encoded in Quoted-Printable format.
* @param {string} str String to be decoded.
* @return {string}
*/
var _decodeQuoted = function(str) {
return decodeURIComponent(
str.replace(qpRegexp, '%$1'));
};
/**
* Decodes Quoted-Printable encoding into UTF-8
* http://en.wikipedia.org/wiki/Quoted-printable
*
* @param {object} metaObj Checks for 'encoding' key to be quoted printable.
* @param {string} value String to be decoded.
* @return {string}
*/
function _decodeQP(metaObj, value) {
var isQP = metaObj && metaObj.encoding &&
(/quoted-printable/i).test(metaObj.encoding);
if (isQP) {
value = _decodeQuoted(value);
}
return value;
}
var NAME_PARTS = [
'familyName',
'givenName',
'additionalName',
'honorificPrefix',
'honorificSuffix'
];
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed name fields properly filled, inferred from
* `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processName(vcardObj, contactObj) {
// Set First Name right away as the 'name' property
if (vcardObj.fn && vcardObj.fn.length) {
var fnMeta = vcardObj.fn[0].meta;
var fnValue = vcardObj.fn[0].value[0];
// For the name field, we want the data as it is
contactObj.name = [_decodeQP(fnMeta, fnValue)];
}
if (vcardObj.n && vcardObj.n.length) {
var values = vcardObj.n[0].value;
var meta = vcardObj.n[0].meta;
for (var i = 0; i < values.length; i++) {
var namePart = values[i];
if (namePart && NAME_PARTS[i]) {
// For the rest of the fields, we want to keep the merged data
// separated the same way, in different positions of the array.
// see bug 981674
contactObj[NAME_PARTS[i]] = _decodeQP(meta, namePart).split(',');
}
}
// If we don't have a contact name at this point, make `name` be the
// unification of all the name parts.
if (!contactObj.name) {
contactObj.name = [_decodeQP(meta, values.join(' ').trim())];
}
}
contactObj.givenName = contactObj.givenName || contactObj.name;
return contactObj;
}
var ADDR_PARTS = [null, null, 'streetAddress', 'locality', 'region',
'postalCode', 'countryName'
];
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed address fields properly filled, inferred from
* `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processAddr(vcardObj, contactObj) {
if (!vcardObj.adr) {
return contactObj;
}
contactObj.adr = [];
for (var i = 0; i < vcardObj.adr.length; i++) {
var cur = {};
var adr = vcardObj.adr[i];
if (adr.meta && adr.meta.type) {
cur.type = adr.meta.type;
}
for (var j = 2; j < adr.value.length; j++) {
var decoded = _decodeQP(adr.meta, adr.value[j]);
// Because of adding empty fields while parsing the vCard
// merging contacts sometimes doesn't work as expected
// Check Bug 935636 for reference
if (decoded !== '') {
cur[ADDR_PARTS[j]] = decoded;
}
}
contactObj.adr.push(cur);
}
return contactObj;
}
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed phone, email and url fields properly filled,
* inferred from `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processComm(vcardObj, contactObj) {
contactObj.tel = [];
['tel', 'email', 'url'].forEach(function field2field(field) {
if (!vcardObj[field]) {
return;
}
var len = vcardObj[field].length;
var hasTypeMapper = function(x) {
return x.trim().toLowerCase();
};
var notTypeMapper = function(v, key) {
return v.meta[key].trim().toLowerCase();
};
var noType = function(field) {
return field !== 'type';
};
var noPref = function(field) {
return field !== 'pref';
};
var typeFilter = function(metaValue) {
return !!VCARD_SIMPLE_TYPES[metaValue];
};
for (var i = 0; i < len; i++) {
var v = vcardObj[field][i];
var metaValues;
var cur = {};
if (v.meta) {
if (v.value) {
cur.value = _decodeQP(v.meta, v.value[0]);
cur.value = cur.value.replace(/^tel:/i, '');
}
if (v.meta.type) {
metaValues = ([].slice.call(v.meta.type)).map(hasTypeMapper);
} else {
metaValues = Object.keys(v.meta).filter(noType).map(
notTypeMapper.bind(null, v));
}
if (metaValues.indexOf('pref') !== -1) {
cur.pref = true;
metaValues = metaValues.filter(noPref);
}
/*
* metaValues is an array of types. If metaValues length is:
* 0: it returns the default type (No type was defined),
* 1: If the element matches a simple type, returns the simple type,
* if not, it returns the default type
* 2: If the elements match a complex type, it returns the complex
* type. If not, then try to match the elements with a simple type,
* in order. If one of the elements matches a simple type, it
* returns the first element that matches a simple type or if there
* exists no matching, it returns the default type value.
* otherwise --> Returns the first element that matches a simple type
* or if there exists no matching, it returns the default type value
* */
switch (metaValues.length) {
case 0:
cur.type = [DEFAULT_PHONE_TYPE];
break;
case 1:
cur.type = [VCARD_SIMPLE_TYPES[metaValues[0]] ||
DEFAULT_PHONE_TYPE];
break;
case 2:
var complexType1 = metaValues[0] + ',' + metaValues[1];
var complexType2 = metaValues[1] + ',' + metaValues[0];
cur.type = [VCARD_COMPLEX_TYPES[complexType1] ||
VCARD_COMPLEX_TYPES[complexType2] ||
VCARD_SIMPLE_TYPES[metaValues[0]] ||
VCARD_SIMPLE_TYPES[metaValues[1]] ||
DEFAULT_PHONE_TYPE];
break;
default:
cur.type = [
VCARD_SIMPLE_TYPES[metaValues.filter(typeFilter).shift()] ||
DEFAULT_PHONE_TYPE
];
}
}
if (!contactObj[field]) {
contactObj[field] = [];
}
contactObj[field].push(cur);
}
});
return contactObj;
}
function _isValidDate(dateValue) {
return !isNaN(Date.parse(dateValue));
}
function _processFields(vcardObj, contactObj) {
['org', 'title', 'bday', 'anniversary'].forEach(function(field) {
if (!vcardObj[field]) {
return;
}
var v = vcardObj[field][0];
if (!v || !Array.isArray(v.value) || v.value.length === 0) {
return;
}
var dateValue;
if (field === 'bday') {
dateValue = v.value[0];
if (_isValidDate(dateValue)) {
contactObj.bday = new Date(dateValue);
}
return;
}
if (field === 'anniversary') {
dateValue = v.value[0];
if (_isValidDate(dateValue)) {
contactObj.anniversary = new Date(dateValue);
}
return;
}
if (!v) {
return;
}
if (field === 'title') {
field = 'jobTitle';
}
switch (typeof v) {
case 'object':
contactObj[field] = [_decodeQP(v.meta, v.value[0])];
break;
case 'string':
contactObj[field] = [v];
break;
}
});
return contactObj;
}
function _processPhoto(vcardObj, contactObj, cb) {
if (!Array.isArray(vcardObj.photo) ||
vcardObj.photo.length === 0 ||
!vcardObj.photo[0] ||
!vcardObj.photo[0].value) {
return cb(contactObj);
}
var photo = vcardObj.photo[0];
var photoContents = photo.value.trim();
if (!photoContents) {
return cb(contactObj);
}
if (photoContents && photo.meta && photo.meta.encoding === 'base64') {
var blob = b64toBlob(photoContents, photo.meta.type);
if (blob) {
utils.thumbnailImage(blob, function gotThumbnail(thumbnail) {
if (blob !== thumbnail) {
contactObj.photo = [blob, thumbnail];
} else {
contactObj.photo = [blob];
}
cb(contactObj);
});
} else {
cb(contactObj);
}
}
// Else we assume it is a http url
else {
var callbacks = {
success: function(blobPic) {
if (blobPic) {
contactObj.photo = [blobPic];
}
cb(contactObj);
},
error: function() {
console.error('Error getting photo for contact');
cb(contactObj);
},
timeout: function() {
console.error('Timeout getting photo for contact');
cb(contactObj);
}
};
Rest.get(photoContents, callbacks, {
responseType: 'blob',
operationsTimeout: REST_TIMEOUT
});
}
}
/**
* Converts a parsed vCard to a mozContact.
*
* @param {Object} vcard JSON representation of an vCard.
* @param {Function} cb Function to call with the resulting moxContact object
* @return {Object, null} An object implementing mozContact interface.
*/
var vcardToContact = function(vcard, cb) {
if (!vcard) {
return null;
}
var obj = {};
_processName(vcard, obj);
_processAddr(vcard, obj);
_processComm(vcard, obj);
_processFields(vcard, obj);
_processPhoto(vcard, obj, function(contactObj) {
cb(utils.misc.toMozContact(contactObj));
});
};
/**
* Class used to parse vCard files (http://tools.ietf.org/html/rfc6350).
*
* @param {String} contents vCard formatted text.
* @constructor
*/
var VCFReader = function(contents) {
this.contents = contents;
this.processed = 0;
this.finished = false;
this.currentChar = 0;
this.numDupsMerged = 0;
};
// Number of contacts processed at a given time.
VCFReader.CONCURRENCY = 5;
/**
* Used to stop contact processing.
*/
VCFReader.prototype.finish = function() {
this.finished = true;
};
/**
* Starting point of vcard processing.
* @param {function} cb Function to call after the process is finished.
*/
VCFReader.prototype.process = function(cb) {
/**
* Calculate the total amount of contacts to be imported. This number could
* change in case there are vcards with syntax errors or that our processor
* can't parse.
*/
var self = this;
var match = this.contents.match(/end:vcard/gi);
// If there are no matches, then this probably isn't a vcard and we should
// stop processing.
if (!match) {
if (cb) {
cb();
}
return;
}
this.importedContacts = [];
this.total = match.length;
this.onread && this.onread(this.total);
this.ondone = function(numImported) {
cb(numImported, self.numDupsMerged);
};
LazyLoader.load(['/shared/js/simple_phone_matcher.js',
'/shared/js/mime_mapper.js',
'/shared/js/contact_photo_helper.js',
'/shared/js/contacts/import/utilities/misc.js',
'/shared/js/contacts/contacts_matcher.js',
'/shared/js/contacts/contacts_merger.js',
'/shared/js/contacts/utilities/image_thumbnail.js',
'/shared/js/contacts/merger_adapter.js',
'/shared/js/contacts/utilities/http_rest.js'
], function() {
// Start processing the text
// The data pump flows as follows:
// 1) splitlines() reads char-by-char to split the text into lines
// 2) each line is parsed to parseEntries() when we have accumulated
// enough lines to represent CONCURRENCY cards.
// 3) call post() for each contact found by parseEntries()
// 4) perform matching for each contact
// 5) save the contact if appropriate
// 6) call onParsed() which either goes back to (4) for the next contact
// or goes back to (1) to get the next set of lines
//
// Note: The async callbacks in the matching and mozContacts.save() code
// prevent this code from being purely recursive.
this.splitLines();
}.bind(this));
};
/**
* Called when every contact is effectively saved.
*
* @param {Error} err Error object in case there was one.
* @param {mozContact} ct Contact that has been just saved.
*/
VCFReader.prototype.onParsed = function(err, ct) {
this.processed += 1;
this.importedContacts.push(ct);
this.onimported && this.onimported(ct && ct.name);
if (this.finished || this.processed === this.total) {
this.ondone(this.importedContacts);
return;
}
var processed = this.processed;
if (processed < this.total && processed % VCFReader.CONCURRENCY === 0) {
this.splitLines();
}
};
/**
* This will be called every time we manage to process a contact
* @param {object[]} contactObjects Objects with contact structure.
*/
VCFReader.prototype.post = function(contactObjects) {
var _onParsed = this.onParsed.bind(this);
var cursor = 0;
var self = this;
function afterSave(ct, e) {
_onParsed(e, ct);
cursor += 1;
if (cursor < contactObjects.length) {
saveContact(contactObjects[cursor]);
}
}
function saveContact(ct) {
if (!ct) {
afterSave(null, null);
return;
}
var contact = utils.misc.toMozContact(ct);
var afterSaveFn = afterSave.bind(null, contact);
var matchCbs = {
onmatch: function(matches) {
var callbacks = {
success: function(mergedContact) {
self.numDupsMerged++;
afterSave(mergedContact, null);
},
error: afterSaveFn
};
contacts.adaptAndMerge(contact, matches, callbacks);
},
onmismatch: function() {
VCFReader._save(contact, afterSaveFn);
}
};
Matcher.match(contact, 'passive', matchCbs);
}
saveContact(contactObjects[cursor]);
};
/**
* Saves a single raw entry into the phone contacts
*
* @param {Object} item represents a single vCard entry.
* @param {Function} cb Callback.
*/
VCFReader._save = function(item, cb) {
var req = navigator.mozContacts.save(utils.misc.toMozContact(item));
req.onsuccess = cb;
req.onerror = cb;
};
var reBeginCard = /begin:vcard$/i;
var reEndCard = /end:vcard$/i;
var reVersion = /^VERSION:([\d\.]*)/i;
/**
* Splits vcard text into arrays of lines (one for each vcard field) and
* sends an array of arrays of lines over to process.
*/
VCFReader.prototype.splitLines = function() {
var currentLine = '';
var currentVersion = 0;
var inLabel = false;
var multiline = false;
var cardArray = [
[]
];
/**
* Number of cards processed. Quite faster than looking at `cardArray`
* length.
* @type {number}
*/
var cardsProcessed = 0;
// We start at the last cursor position
var i = this.currentChar;
var callPost = this.post.bind(this);
for (var l = this.contents.length; i < l; i++) {
this.currentChar = i;
var ch = this.contents[i];
if (currentVersion >= 4 && ch === '"') {
inLabel = !inLabel;
currentLine += ch;
continue;
}
// Ignore beginning whitespace that indicates multiline field.
if (multiline === true) {
if ((/[\r\t\s\n]/).test(ch)) {
continue;
} else {
//currentLine += '\n'
multiline = false;
}
}
var next = this.contents[i + 1];
if (inLabel || (ch !== '\n' && ch !== '\r')) {
// If we have a quoted-printable sign for multiline (/=\n/) AND
// we are not in a data URI field, ignore the character.
if (!(/^PHOTO/i).test(currentLine) && ch === '=' && next &&
next.search(/(\r|\n)/) !== -1) {
multiline = true;
continue;
}
currentLine += ch;
// Continue only if this is not the last char in the string
if (i !== l - 1) {
continue;
}
}
// If the field is a photo, the field could be multiline, to know if the
// following line is part of the photo, we must check if the first char
// of the following line is a space.
var firstCharNextLine = this.contents[i + 2];
if ((firstCharNextLine === WHITE_SPACE) && (/[\r\t\s\n]/).test(next) &&
(/^PHOTO/i).test(currentLine)) {
multiline = true;
continue;
}
// At this point, we know that ch is a newline, and in the vcard format,
// if we have a space after a newline, it indicates multiline field.
if (next && (next === WHITE_SPACE || next === '\t')) {
multiline = true;
continue;
}
if (reBeginCard.test(currentLine)) {
currentLine = '';
currentVersion = 0;
continue;
}
// If the current line indicates the end of a card,
if (reEndCard.test(currentLine)) {
cardsProcessed += 1;
if (cardsProcessed === VCFReader.CONCURRENCY ||
(cardsProcessed + this.processed) === this.total) {
_parseEntries(cardArray, callPost);
break;
}
currentLine = '';
cardArray.push([]);
continue;
}
if (currentLine) {
var matches = reVersion.exec(currentLine);
if (matches === null) {
cardArray[cardArray.length - 1].push(currentLine);
} else {
currentVersion = parseFloat(matches[1] || '0');
}
}
currentLine = '';
}
};
VCFReader._decodeQuoted = _decodeQuoted;
VCFReader.processAddr = _processAddr;
VCFReader.processName = _processName;
VCFReader.vcardToContact = vcardToContact;
//Expose some utility methods
VCFReader.utils = {
parseDataUri: parseDataUri,
b64toBlob: b64toBlob
};
return VCFReader;
})();

View file

@ -0,0 +1,758 @@
/* global LazyLoader, utils, Rest, MimeMapper */
/* exported VCardReader */
(function() {
'use strict';
/* WARNING this module LazyLoads these modules
* '/shared/js/mime_mapper.js',
* '/shared/js/contacts/utilities/image_thumbnail.js',
* '/shared/js/contacts/import/utilities/misc.js',
* '/shared/js/contacts/utilities/http_rest.js'
*/
var dependencies = [
'/shared/js/mime_mapper.js',
'/shared/js/contacts/utilities/image_thumbnail.js',
'/shared/js/contacts/import/utilities/misc.js',
'/shared/js/contacts/utilities/http_rest.js'
];
var ReBasic = /^([^:]+):(.+)$/;
var ReTuple = /([a-zA-Z]+)=(.+)/;
var WHITE_SPACE = ' ';
var reBeginCard = /begin:vcard$/i;
var reEndCard = /end:vcard$/i;
var reVersion = /^VERSION:/i;
// Default tel type class
var DEFAULT_PHONE_TYPE = 'other';
// basic type vcard to mobile type
var VCARD_SIMPLE_TYPES = {
'fax' : 'faxOther',
'faxother' : 'faxOther',
'home' : 'home',
'internet' : 'internet',
'cell' : 'mobile',
'pager' : 'pager',
'personal' : 'home',
'pref' : 'pref',
'text' : 'text',
'textphone' : 'textphone',
'voice' : 'voice',
'work' : 'work'
};
// complex type vcard to mobile type
var VCARD_COMPLEX_TYPES = {
'fax,work' : 'faxOffice',
'fax,home' : 'faxHome',
'voice,work' : 'work',
'voice,home' : 'home'
};
var REST_TIMEOUT = 5000;
/**
* VCardReader object. It allows to read sequentially a set of
* vCard objects present on a vCard File.
*
* Example of use:
*
* var reader = new VCardReader(vcardData);
* var cursor = reader.getAll();
*
* cursor.onsuccess = function(evt) {
* var contact = evt.target.result;
* if(contact) {
* // do something
* cursor.continue();
* }
* else {
* // We've just finished
* }
* }
*
*/
window.VCardReader = function VCardReader(data) {
var content = data.replace(/\r?\n/g, '\n');
var currentIndex = 0;
this.getAll = function() {
return new Cursor();
};
function Cursor() {
var self = this,
depsLoaded = false;
this._loadDependenciesAndRead = function() {
if (depsLoaded) {
self._doRead();
} else {
LazyLoader.load(dependencies, function() {
depsLoaded = true;
self._doRead();
});
}
};
this._doRead = function() {
_readNext(function(contact) {
self.result = contact;
if (self.callback) {
self.callback({
target: self
});
}
});
};
Object.defineProperty(this, 'onsuccess', {
set: function(cb) {
self.callback = cb;
self._loadDependenciesAndRead();
}
});
this.continue = function() {
// If there was no previous result. do nothing
if (this.result) {
this._loadDependenciesAndRead();
}
};
}
function _getCardLines() {
var currentLine = '';
var inLabel = false;
var multiline = false;
var lines = [];
// We start at the last cursor position
var i = currentIndex;
for (var l = content.length; i < l; i++) {
currentIndex = i;
var ch = content[i];
if (ch === '"') {
inLabel = !inLabel;
currentLine += ch;
continue;
}
// Ignore beginning whitespace that indicates multiline field.
if (multiline === true) {
if ((/[\r\t\s\n]/).test(ch)) {
continue;
} else {
multiline = false;
}
}
var next = content[i + 1];
if (inLabel || (ch !== '\n' && ch !== '\r')) {
// If we have a quoted-printable sign for multiline (/=\n/) AND
// we are not in a data URI field, ignore the character.
if (!(/^PHOTO/i).test(currentLine) && ch === '=' && next &&
next.search(/(\r|\n)/) !== -1) {
multiline = true;
continue;
}
currentLine += ch;
// Continue only if this is not the last char in the string
if (i !== l - 1) {
continue;
}
}
// If the field is a photo, the field could be multiline, to know if the
// following line is part of the photo, we must check if the first char
// of the following line is a space.
var firstCharNextLine = content[i + 2];
if ((firstCharNextLine === WHITE_SPACE) && (/[\r\t\s\n]/).test(next) &&
(/^PHOTO/i).test(currentLine)) {
multiline = true;
continue;
}
// At this point, we know that ch is a newline, and in the vcard format,
// if we have a space after a newline, it indicates multiline field.
if (next && (next === WHITE_SPACE || next === '\t')) {
multiline = true;
continue;
}
if (reBeginCard.test(currentLine)) {
currentLine = '';
continue;
}
// If the current line indicates the end of a card,
if (reEndCard.test(currentLine)) {
break;
}
if (currentLine && !reVersion.test(currentLine)) {
lines.push(currentLine);
}
currentLine = '';
}
currentIndex++;
return lines;
}
// look if there is more cards to read
function _checkFinished() {
return !content.substr(currentIndex).match(/end:vcard/gi);
}
function _readNext(cb) {
var fields = {};
var lines;
var line;
var parsedLine;
if (_checkFinished()) {
cb();
return;
}
lines = _getCardLines();
for (var i = 0; i < lines.length; i++) {
line = lines[i];
parsedLine = _parseLine(line);
if (parsedLine) {
if (!fields[parsedLine.key]) {
fields[parsedLine.key] = [];
}
fields[parsedLine.key].push(parsedLine.data);
}
}
// If there isn't either a First Name (fn) or a Name (n) this card is not
// a valid one, ignore and try to parse the next one.
if (!fields.fn && !fields.n) {
console.warn('Invalid vCard found. Skipping');
if (_checkFinished()) {
cb();
} else {
_readNext(function(contact) {
cb(contact);
});
}
} else {
vcardToContact(fields, cb);
}
}
};
function parseDataUri(str) {
var re = /^data:(.+);(charset=([^;]+))?;?base64,(.*)$/;
var matches = re.exec(str);
if (matches) {
return {
mime: matches[1],
charset: matches[3],
value: matches[4]
};
}
return null;
}
function ensureMimeType(type) {
type = type.toLowerCase();
if (!MimeMapper.isSupportedType(type)) {
type = MimeMapper.guessTypeFromExtension(type) || type;
}
return type || '';
}
/**
* Given a string, expected to be in base64 format, an a
* content type, returns the corresponding Blob.
* If cannot be parsed, will return undefined.
*
* @param {string} data as a string.
* @param {string} content type of the expected blob. Can be null.
* @param {int} size of the slice to be parsed.
*/
function b64toBlob(b64Data, contentType, sliceSize) {
if (!b64Data) {
console.error('No b64 data provided to convert to blob.');
return;
}
contentType = contentType || '';
sliceSize = sliceSize || 1024;
function charCodeFromCharacter(c) {
return c.charCodeAt(0);
}
var blob;
try {
var byteCharacters = atob(b64Data);
var byteArrays = [];
for (var offset = 0; offset < byteCharacters.length;
offset += sliceSize) {
var slice = byteCharacters.slice(offset, offset + sliceSize);
var byteNumbers = [];
for (var i = 0, l = slice.length; i < l; i++) {
byteNumbers.push(charCodeFromCharacter(slice[i]));
}
var byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
blob = new Blob(byteArrays, {type: contentType});
} catch (e) {
console.error('Error parsing base64 data');
}
return blob;
}
/**
* Parses a line and creates an object with the line type (name), its meta
* properties and its actual value.
*
* @param {string} line Line to be parsed from a VCF.
* @return {{key: string, data: {meta, value}}}
* @private
*/
function _parseLine(line) {
var parsed = ReBasic.exec(line);
if (!parsed) {
return null;
}
// `tuples` is an array containing all the extra properties
var tuples = parsed[1].split(/[;,]/);
var fieldName = tuples.shift().toLowerCase();
fieldName = fieldName.split('.').pop(); // If grouped element, get real name
// Value will be an empty array if there is only whitespace and
// semi-colons.
var fieldValue = /[^\s;]/.test(parsed[2]) ? parsed[2].split(';') : [];
var meta = {
type: []
};
var len = tuples.length;
for (var i = 0; i < len; i++) {
var tuple = _parseTuple(tuples[i]);
if (tuple[0] === 'type') {
meta.type.push(tuple[1]);
} else {
meta[tuple[0]] = tuple[1];
}
}
// If this is a photo, we correct the value and type
if (fieldName === 'photo') {
if (meta.type.length) {
meta.type = ensureMimeType(meta.type[0]);
}
fieldValue = parsed[2];
// vCard 3.0 expects a mandatory encoding = 'b' parameter for base64
// encoded images, and vCard 2.1 expects a 'BASE64' one.
if (meta.encoding && (meta.encoding.toLowerCase() === 'b' ||
meta.encoding.toLowerCase() === 'base64')) {
meta.encoding = 'base64';
} else {
var parsedUri = parseDataUri(fieldValue);
if (parsedUri) {
meta.type = ensureMimeType(parsedUri.mime);
meta.encoding = 'base64';
fieldValue = parsedUri.value;
}
}
}
return {
key: fieldName,
data: {
meta: meta,
value: fieldValue
}
};
}
function _parseTuple(p) {
var match = p.match(ReTuple);
return match ? [match[1].toLowerCase(), match[2]] : ['type', p];
}
/**
* Converts a parsed vCard to a mozContact.
*
* @param {Object} vcard JSON representation of an vCard.
* @param {Function} cb Function to call with the resulting moxContact object
* @return {Object, null} An object implementing mozContact interface.
*/
function vcardToContact(vcard, cb) {
if (!vcard) {
return null;
}
var obj = {};
_processName(vcard, obj);
_processAddr(vcard, obj);
_processComm(vcard, obj);
_processFields(vcard, obj);
_processPhoto(vcard, obj, function(contactObj) {
cb(utils.misc.toMozContact(contactObj));
});
return utils.misc.toMozContact(obj);
}
/**
* Matches Quoted-Printable characters in a string
* @type {RegExp}
*/
var qpRegexp = /=([a-zA-Z0-9]{2})/g;
/**
* Decodes a string encoded in Quoted-Printable format.
* @param {string} str String to be decoded.
* @return {string}
*/
var _decodeQuoted = function(str) {
return decodeURIComponent(
str.replace(qpRegexp, '%$1'));
};
/**
* Decodes Quoted-Printable encoding into UTF-8
* http://en.wikipedia.org/wiki/Quoted-printable
*
* @param {object} metaObj Checks for 'encoding' key to be quoted printable.
* @param {string} value String to be decoded.
* @return {string}
*/
function _decodeQP(metaObj, value) {
var isQP = metaObj && metaObj.encoding &&
(/quoted-printable/i).test(metaObj.encoding);
if (isQP) {
value = _decodeQuoted(value);
}
return value;
}
var NAME_PARTS = [
'familyName',
'givenName',
'additionalName',
'honorificPrefix',
'honorificSuffix'
];
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed name fields properly filled, inferred from
* `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processName(vcardObj, contactObj) {
// Set First Name right away as the 'name' property
if (vcardObj.fn && vcardObj.fn.length) {
var fnMeta = vcardObj.fn[0].meta;
var fnValue = vcardObj.fn[0].value[0];
// For the name field, we want the data as it is
contactObj.name = [_decodeQP(fnMeta, fnValue)];
}
if (vcardObj.n && vcardObj.n.length) {
var values = vcardObj.n[0].value;
var meta = vcardObj.n[0].meta;
for (var i = 0; i < values.length; i++) {
var namePart = values[i];
if (namePart && NAME_PARTS[i]) {
// For the rest of the fields, we want to keep the merged data
// separated the same way, in different positions of the array.
// see bug 981674
contactObj[NAME_PARTS[i]] = _decodeQP(meta, namePart).split(',');
}
}
// If we don't have a contact name at this point, make `name` be the
// unification of all the name parts.
if (!contactObj.name) {
contactObj.name = [_decodeQP(meta, values.join(' ').trim())];
}
}
return contactObj;
}
var ADDR_PARTS = [null, null, 'streetAddress', 'locality', 'region',
'postalCode', 'countryName'
];
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed address fields properly filled, inferred from
* `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processAddr(vcardObj, contactObj) {
if (!vcardObj.adr) {
return contactObj;
}
contactObj.adr = [];
for (var i = 0; i < vcardObj.adr.length; i++) {
var cur = {};
var adr = vcardObj.adr[i];
if (adr.meta && adr.meta.type) {
cur.type = adr.meta.type;
}
for (var j = 2; j < adr.value.length; j++) {
var decoded = _decodeQP(adr.meta, adr.value[j]);
// Because of adding empty fields while parsing the vCard
// merging contacts sometimes doesn't work as expected
// Check Bug 935636 for reference
if (decoded !== '') {
cur[ADDR_PARTS[j]] = decoded;
}
}
contactObj.adr.push(cur);
}
return contactObj;
}
/**
* Takes an object with vCard properties and a mozContact object and returns
* the latter with the computed phone, email and url fields properly filled,
* inferred from `vcardObj`.
*
* @param {Object} vcardObj
* @param {Object} contactObj a mozContact to be filled with name fields.
* @return {Object}
*/
function _processComm(vcardObj, contactObj) {
contactObj.tel = [];
['tel', 'email', 'url'].forEach(function field2field(field) {
if (!vcardObj[field]) {
return;
}
var len = vcardObj[field].length;
var hasTypeMapper = function(x) {
return x.trim().toLowerCase();
};
var notTypeMapper = function(v, key) {
return v.meta[key].trim().toLowerCase();
};
var noType = function(field) {
return field !== 'type';
};
var noPref = function(field) {
return field !== 'pref';
};
var typeFilter = function(metaValue) {
return !!VCARD_SIMPLE_TYPES[metaValue];
};
for (var i = 0; i < len; i++) {
var v = vcardObj[field][i];
var metaValues;
var cur = {};
if (v.meta) {
if (v.value) {
cur.value = _decodeQP(v.meta, v.value[0]);
cur.value = cur.value.replace(/^tel:/i, '');
}
if (v.meta.type) {
metaValues = ([].slice.call(v.meta.type)).map(hasTypeMapper);
} else {
metaValues = Object.keys(v.meta).filter(noType).map(
notTypeMapper.bind(null, v));
}
if (metaValues.indexOf('pref') !== -1) {
cur.pref = true;
metaValues = metaValues.filter(noPref);
}
/*
* metaValues is an array of types. If metaValues length is:
* 0: it returns the default type (No type was defined),
* 1: If the element matches a simple type, returns the simple type,
* if not, it returns the default type
* 2: If the elements match a complex type, it returns the complex
* type. If not, then try to match the elements with a simple type,
* in order. If one of the elements matches a simple type, it
* returns the first element that matches a simple type or if there
* exists no matching, it returns the default type value.
* otherwise --> Returns the first element that matches a simple type
* or if there exists no matching, it returns the default type value
* */
switch (metaValues.length) {
case 0:
cur.type = [DEFAULT_PHONE_TYPE];
break;
case 1:
cur.type = [VCARD_SIMPLE_TYPES[metaValues[0]] ||
DEFAULT_PHONE_TYPE];
break;
case 2:
var complexType1 = metaValues[0] + ',' + metaValues[1];
var complexType2 = metaValues[1] + ',' + metaValues[0];
cur.type = [VCARD_COMPLEX_TYPES[complexType1] ||
VCARD_COMPLEX_TYPES[complexType2] ||
VCARD_SIMPLE_TYPES[metaValues[0]] ||
VCARD_SIMPLE_TYPES[metaValues[1]] ||
DEFAULT_PHONE_TYPE];
break;
default:
cur.type = [
VCARD_SIMPLE_TYPES[metaValues.filter(typeFilter).shift()] ||
DEFAULT_PHONE_TYPE
];
}
}
if (!contactObj[field]) {
contactObj[field] = [];
}
contactObj[field].push(cur);
}
});
return contactObj;
}
function _isValidDate(dateValue) {
return !isNaN(Date.parse(dateValue));
}
function _processFields(vcardObj, contactObj) {
['org', 'title', 'bday', 'anniversary'].forEach(function(field) {
if (!vcardObj[field]) {
return;
}
var v = vcardObj[field][0];
var dateValue;
if (field === 'bday') {
dateValue = v.value[0];
if (_isValidDate(dateValue)) {
contactObj.bday = new Date(dateValue);
}
return;
}
if (field === 'anniversary') {
dateValue = v.value[0];
if (_isValidDate(dateValue)) {
contactObj.anniversary = new Date(dateValue);
}
return;
}
if (!v) {
return;
}
if (field === 'title') {
field = 'jobTitle';
}
switch (typeof v) {
case 'object':
contactObj[field] = [_decodeQP(v.meta, v.value[0])];
break;
case 'string':
contactObj[field] = [v];
break;
}
});
return contactObj;
}
function _processPhoto(vcardObj, contactObj, cb) {
if (!Array.isArray(vcardObj.photo) ||
vcardObj.photo.length === 0 ||
!vcardObj.photo[0] ||
!vcardObj.photo[0].value) {
return cb(contactObj);
}
var photo = vcardObj.photo[0];
var photoContents = photo.value.trim();
if (!photoContents) {
return cb(contactObj);
}
if (photoContents && photo.meta && photo.meta.encoding === 'base64') {
var blob = b64toBlob(photoContents, photo.meta.type);
if (blob) {
utils.thumbnailImage(blob, function gotThumbnail(thumbnail) {
if (blob !== thumbnail) {
contactObj.photo = [blob, thumbnail];
} else {
contactObj.photo = [blob];
}
cb(contactObj);
});
} else {
cb(contactObj);
}
}
// Else we assume it is a http url
else {
var callbacks = {
success: function(blobPic) {
if (blobPic) {
contactObj.photo = [blobPic];
}
cb(contactObj);
},
error: function() {
console.error('Error getting photo for contact');
cb(contactObj);
},
timeout: function() {
console.error('Timeout getting photo for contact');
cb(contactObj);
}
};
Rest.get(photoContents, callbacks, {
responseType: 'blob',
operationsTimeout: REST_TIMEOUT
});
}
}
window.VCardReader.b64toBlob = b64toBlob;
window.VCardReader.parseDataUri = parseDataUri;
})();

View file

@ -0,0 +1,2 @@
;'use strict';var contacts=window.contacts||{};contacts.adaptAndMerge=function(incomingContact,matches,callbacks){var listIds=Object.keys(matches);var totalMatches=listIds.length;var masterContact=matches[listIds[0]].matchingContact;var matchingContacts=[];for(var j=1;j<totalMatches;j++){matchingContacts.push(matches[listIds[j]]);}
matchingContacts.push({matchingContact:incomingContact});contacts.Merger.merge(masterContact,matchingContacts,callbacks);};

View file

@ -0,0 +1,56 @@
;'use strict';(function(exports){var inSearchMode=false,searchView,searchBox,searchList,searchNoResult,searchProgress,searchTimer=null,contactNodes=null,selectableForm=null,searchableNodes=null,currentTextToSearch='',currentSearchTerms=[],prevTextToSearch='',currentSet={},searchTextCache={},canReuseSearchables=false,blurList=false,theClones={},CHUNK_SIZE=10,searchPageSize=-1,hardLimit=25,emptySearch=true,remainingPending=true,imgLoader,searchEnabled=false,source=null,navigationController=null;var init=function load(_source,defaultEnabled,navigation){searchView=document.getElementById('search-view');searchList=document.getElementById('search-list');selectableForm=document.getElementById('selectable-form');if(!_source){throw new Error('Search requires a contact source!');}
source=_source;if(typeof source.click==='function'){searchList.addEventListener('click',source.click);}
searchEnabled=!!defaultEnabled;navigationController=navigation||window.MainNavigation;};var initialized=false;var ignoreReturnKey=function ignoreReturnKey(evt){if(evt.keyCode==13){evt.preventDefault();}};var doInit=function doInit(){if(initialized){return;}
utils.listeners.add({'#cancel-search':exitSearchMode,'#search-contact':[{event:'keypress',handler:ignoreReturnKey}]});initialized=true;searchBox=document.getElementById('search-contact');var resetButton=searchBox.nextElementSibling;resetButton.addEventListener('ontouchstart'in window?'touchstart':'mousedown',function(){searchBox.value='';searchBox.focus();resetState();window.setTimeout(fillInitialSearchPage,0);});searchList.parentNode.addEventListener('touchstart',function(){if(searchableNodes&&remainingPending){addRemainingResults(searchableNodes,searchPageSize);}
blurList=true;});searchNoResult=document.getElementById('no-result');searchProgress=document.getElementById('search-progress');searchBox.addEventListener('blur',function(){window.setTimeout(onSearchBlur,0);});searchBox.addEventListener('focus',function(){blurList=false;});LazyLoader.load(['/contacts/js/fb_resolver.js','/shared/js/contacts/utilities/image_loader.js'],function(){imgLoader=new ImageLoader('#groups-list-search','li');imgLoader.setResolver(fb.resolver);});};var clearHighlights=function(node){var highlights=node.getElementsByTagName('mark');while(highlights.length){var parent=highlights[0].parentNode;while(highlights[0].firstChild){parent.insertBefore(highlights[0].firstChild,highlights[0]);}
parent.removeChild(highlights[0]);}};var highlightNode=function(node){function doHighlight(text){if(text===''||text===' '){return document.createTextNode('');}
return HtmlHelper.createHighlightHTML(text,currentSearchTerms);}
function locateHTMLTag(text){var tagLocation=[];tagLocation.push(text.indexOf('<'));tagLocation.push(text.indexOf('>',tagLocation[0])+1);tagLocation.push(text.indexOf('<',tagLocation[1]));tagLocation.push(text.indexOf('>',tagLocation[2])+1);return tagLocation;}
var textNode=node.querySelector('.contact-text bdi');if(textNode===null){return;}
var text=textNode.innerHTML;var tagPos=locateHTMLTag(text);var beforeTag=text.substring(0,tagPos[0]);var inTag=text.substring(tagPos[1],tagPos[2]);var afterTag=text.substring(tagPos[3]);var realText=[beforeTag,inTag,afterTag];if(tagPos[0]==-1){var content=doHighlight(Normalizer.unescapeHTML(text));textNode.textContent='';textNode.appendChild(content);}else{for(var i=0,len=realText.length;i<len;i++){realText[i]=doHighlight(Normalizer.unescapeHTML(realText[i]));}
var fragment=document.createDocumentFragment();var strongTag=document.createElement('strong');strongTag.appendChild(realText[1]);fragment.appendChild(realText[0]);fragment.appendChild(strongTag);fragment.appendChild(realText[2]);textNode.textContent='';textNode.appendChild(fragment);}};var updateSearchList=function updateSearchList(cb){if(!inSearchMode){if(cb){cb();}
return;}
window.setTimeout(function(){resetStateAndCache();search(cb);});};var resetStateAndCache=function resetStateAndCache(){contactNodes=null;searchTextCache={};resetState();};var exitSearchMode=function exitSearchMode(evt){if(evt){evt.preventDefault();}
searchView.classList.remove('insearchmode');if(selectableForm){selectableForm.classList.remove('insearchmode');}
if(navigationController){navigationController.back();}
window.setTimeout(function exit_search(){hideProgressResults();searchBox.value='';contactNodes=null;searchTextCache={};resetState();inSearchMode=false;},0);window.removeEventListener('input',onInput);};function resetState(){prevTextToSearch='';currentTextToSearch='';currentSearchTerms=[];searchableNodes=null;canReuseSearchables=false;currentSet={};theClones={};utils.dom.removeChildNodes(searchList);emptySearch=true;remainingPending=true;}
function addRemainingResults(nodes,from){if(remainingPending!==true){return;}
var fragment=document.createDocumentFragment();for(var i=from;i<hardLimit&&i<nodes.length;i++){var node=nodes[i].node;var clon=getClone(node);highlightNode(clon);fragment.appendChild(clon);currentSet[node.dataset.uuid]=clon;}
if(fragment.hasChildNodes()){searchList.appendChild(fragment);imgLoader.reload();}
remainingPending=false;}
function onSearchBlur(e){if(canReuseSearchables&&searchableNodes&&searchView.classList.contains('insearchmode')&&blurList){addRemainingResults(searchableNodes,searchPageSize);}
else if(emptySearch===true&&remainingPending===true){var lastNode=searchList.querySelector('li:last-child');if(lastNode){var lastNodeUid=lastNode.dataset.uuid;var startNode=source.getNextNode(source.getNodeById(lastNodeUid));fillIdentityResults(startNode,hardLimit-searchPageSize);remainingPending=false;imgLoader.reload();}}}
function fillIdentityResults(startNode,number){var fragment=document.createDocumentFragment();var contact=startNode;for(var i=0;i<number&&contact;i++){var clonedNode=getClone(contact);fragment.appendChild(clonedNode);currentSet[contact.dataset.uuid]=clonedNode;contact=source.getNextNode(contact);}
if(fragment.hasChildNodes()){searchList.appendChild(fragment);}}
function getClone(node){var id=node.dataset.uuid;var out=theClones[id];if(!out){out=source.clone(node);cacheClone(id,out);}
return out;}
function cacheClone(id,clone){theClones[id]=clone;}
function onInput(e){if(e.target.id===searchBox.id){search();}}
var enterSearchMode=function searchMode(evt){evt.preventDefault();if(!inSearchMode){window.addEventListener('input',onInput);searchView.classList.add('insearchmode');if(selectableForm){selectableForm.classList.add('insearchmode');}
doInit();fillInitialSearchPage();inSearchMode=true;emptySearch=true;if(navigationController){navigationController.go('search-view','none');}
setTimeout(function nextTick(){searchBox.focus();});}};function fillInitialSearchPage(done){hideProgressResults();var startContact=source.getFirstNode();var numToFill=searchPageSize;if(startContact&&searchPageSize<1){fillIdentityResults(startContact,1);var viewHeight=searchList.getBoundingClientRect().height;var rowHeight=searchList.children[0].getBoundingClientRect().height;searchPageSize=Math.ceil(viewHeight/rowHeight);hardLimit=~~(3.5*searchPageSize);startContact=source.getNextNode(startContact);numToFill=searchPageSize-1;}
fillIdentityResults(startContact,numToFill);if(typeof done==='function'){done();}
if(imgLoader){imgLoader.reload();}}
function doSearch(contacts,from,searchText,pattern,state){if(currentTextToSearch.localeCompare(searchText)!==0){canReuseSearchables=false;window.console.warn('**** Cancelling current search ****');return;}
var end=from+CHUNK_SIZE;currentSearchTerms=pattern.source.split(/\s+/);for(var c=from;c<end&&c<contacts.length;c++){var contact=contacts[c].node||contacts[c];var contactText=contacts[c].text||getSearchText(contacts[c]);contactText=Normalizer.unescapeHTML(contactText);if(!checkContactMatch(currentSearchTerms,contactText)){if(contact.dataset.uuid in currentSet){searchList.removeChild(currentSet[contact.dataset.uuid]);delete currentSet[contact.dataset.uuid];}}else{if(state.count===0){hideProgressResults();}
if(Object.keys(currentSet).length<searchPageSize&&!(contact.dataset.uuid in currentSet)){var clonedNode=getClone(contact);currentSet[contact.dataset.uuid]=clonedNode;searchList.appendChild(clonedNode);}
if(currentSet[contact.dataset.uuid]){clearHighlights(currentSet[contact.dataset.uuid]);highlightNode(currentSet[contact.dataset.uuid]);}
state.searchables.push({node:contact,text:contactText});state.count++;}}
if(c<contacts.length){searchTimer=window.setTimeout(function do_search(){searchTimer=null;doSearch(contacts,from+CHUNK_SIZE,searchText,pattern,state);},0);return;}else if(source.expectMoreNodes()){var delay=250;searchTimer=window.setTimeout(function do_search(){doSearch(contacts,Math.min(end,contacts.length),searchText,pattern,state);},delay);return;}else if(state.count===0){showNoResults();canReuseSearchables=false;}else{imgLoader.reload();searchableNodes=state.searchables;canReuseSearchables=true;if(blurList===true){searchTimer=window.setTimeout(function(){searchTimer=null;addRemainingResults(searchableNodes,searchPageSize);},0);}}
if(typeof state.searchDoneCb==='function'){state.searchDoneCb();}}
var enableSearch=function enableSearch(){if(searchEnabled){return;}
searchEnabled=true;invalidateCache();search();};var appendNodes=function appendNodes(nodes){if(!nodes||!nodes.length||!contactNodes){return;}
contactNodes.push.apply(contactNodes,nodes);if(!currentTextToSearch||!canReuseSearchables||!searchableNodes){return;}
for(var i=0,n=nodes.length;i<n;++i){var node=nodes[i];var nodeText=Normalizer.unescapeHTML(getSearchText(node));if(!checkContactMatch(currentSearchTerms,nodeText)){searchableNodes.push({node:node,text:nodeText});}}};var checkContactMatch=function checkContactMatch(searchTerms,text){for(var i=0,m=searchTerms.length;i<m;i++){if(!RegExp(searchTerms[i],'i').test(text)){return false;}}
return true;};var search=function performSearch(searchDoneCb){prevTextToSearch=currentTextToSearch;currentTextToSearch=Normalizer.toAscii(searchBox.value.trim());currentTextToSearch=Normalizer.escapeRegExp(currentTextToSearch);var thisSearchText=String(currentTextToSearch);if(thisSearchText.length===0){resetState();window.setTimeout(fillInitialSearchPage,0,searchDoneCb);}
else{showProgress();if(!searchEnabled){resetState();return;}
emptySearch=false;remainingPending=true;var pattern=new RegExp(thisSearchText,'i');var contactsToSearch=getContactsToSearch(thisSearchText,prevTextToSearch);var state={count:0,searchables:[],searchDoneCb:searchDoneCb};searchTimer=window.setTimeout(function do_search(){searchTimer=null;doSearch(contactsToSearch,0,thisSearchText,pattern,state);},0);}};function getSearchText(contact){var out='';var uuid=contact.dataset.uuid;if(uuid){out=searchTextCache[uuid];if(!out){out=source.getSearchText(contact);searchTextCache[uuid]=out;}}
else{window.console.error('Search: Not uuid found for the provided node');}
return out;}
var getContactsDom=function contactsDom(){if(!contactNodes){contactNodes=source.getNodes();}
return contactNodes;};var getContactsToSearch=function getContactsToSearch(newText,prevText){var out;if(canReuseSearchables&&newText.length>prevText.length&&prevText.length>0&&newText.indexOf(prevText)===0){out=searchableNodes||getContactsDom();}else{utils.dom.removeChildNodes(searchList);currentSet={};out=getContactsDom();canReuseSearchables=false;}
return out;};var isInSearchMode=function isInSearchMode(){return inSearchMode;};var invalidateCache=function s_invalidate(){if(searchTimer){window.clearTimeout(searchTimer);searchTimer=null;}
currentTextToSearch='';canReuseSearchables=false;searchableNodes=null;contactNodes=null;currentSet={};searchTextCache={};};var removeContact=function s_removeContact(id){var contact=searchList.querySelector('li[data-uuid=\"'+id+'\"]');searchList.removeChild(contact);};var selectRow=function s_selectRow(id,value){var check=searchList.querySelector('#search-view input[value="'+id+'"]');if(check){check.checked=value;}};function showProgress(){searchNoResult.classList.add('hide');searchProgress.classList.remove('hidden');}
function showNoResults(){searchNoResult.classList.remove('hide');searchProgress.classList.add('hidden');}
function hideProgressResults(){searchNoResult.classList.add('hide');searchProgress.classList.add('hidden');}
exports.Search={'init':init,'invalidateCache':invalidateCache,'appendNodes':appendNodes,'removeContact':removeContact,'search':search,'enterSearchMode':enterSearchMode,'exitSearchMode':exitSearchMode,'isInSearchMode':isInSearchMode,'enableSearch':enableSearch,'selectRow':selectRow,'updateSearchList':updateSearchList};})(window);

View file

@ -0,0 +1,20 @@
'use strict';
/* global MozActivity */
/* exported SmsIntegration */
/* jshint nonew: false */
var SmsIntegration = {
sendSms: function si_sendSms(number) {
try {
new MozActivity({
name: 'new',
data: {
type: 'websms/sms',
target: number
}
});
} catch (e) {
console.log('WebActivities unavailable? : ' + e);
}
}
};

View file

@ -0,0 +1,2 @@
;;'use strict';var utils=window.utils||{};if(!utils.dom){(function(document){var Dom=utils.dom={};Dom.removeChildNodes=function(node){while(node.hasChildNodes()){node.removeChild(node.firstChild);}};Dom.addClassToNodes=function(container,selector,clazz){var nodes=container.querySelectorAll(selector);for(var i=0,n=nodes.length;i<n;++i){nodes[i].classList.add(clazz);}};Dom.removeClassFromNodes=function(container,selector,clazz){var nodes=container.querySelectorAll(selector);for(var i=0,n=nodes.length;i<n;++i){nodes[i].classList.remove(clazz);}};Dom.updatePhoto=function(photo,dest){var background='';if(photo!=null){background='url('+URL.createObjectURL(photo)+')';}
dest.style.backgroundImage=background;dest.dataset.photoReady='true';};})(document);}

View file

@ -0,0 +1,3 @@
;;'use strict';var utils=window.utils||{};if(!utils.listeners){(function(document){var Listeners=utils.listeners={};Listeners.add=function(config){try{for(var id in config){var handler=config[id];var nodes=document.querySelectorAll(id);for(var i=0;i<nodes.length;i++){var node=nodes[i];if(Array.isArray(handler)){handler.forEach(function handle(item){if(!item.hasOwnProperty('event')&&!item.hasOwnProperty('handler')){return;}
node.addEventListener(item.event,item.handler);});}else{node.addEventListener('click',handler);}}}}
catch(e){window.console.error('Error while registering listener for: ',id,e);}};})(document);}

View file

@ -0,0 +1,6 @@
;'use strict';if(!window.Rest){window.Rest=(function(){function RestRequest(xhr){var cancelled=false;this.cancel=function oncancel(){cancelled=true;window.setTimeout(xhr.abort.bind(xhr),0);};this.isCancelled=function isCancelled(){return cancelled;};}
function Rest(){}
Rest.prototype={get:function(uri,callback,pOptions){var DEFAULT_TIMEOUT=30000;var options=pOptions||{};var xhr=new XMLHttpRequest({mozSystem:true});var outReq=new RestRequest(xhr);xhr.open('GET',uri,true);var responseType=options.responseType||'json';xhr.responseType=responseType;var responseProperty=responseType==='xml'?'responseXML':'response';xhr.timeout=options.operationsTimeout||DEFAULT_TIMEOUT;if(!xhr.timeout||xhr.timeout===DEFAULT_TIMEOUT&&(parent&&parent.config&&parent.config.operationsTimeout)){xhr.timeout=parent.config.operationsTimeout;}
if(options.requestHeaders){for(var header in options.requestHeaders){xhr.setRequestHeader(header,options.requestHeaders[header]);}}
xhr.onload=function(e){if(xhr.status===200||xhr.status===400||xhr.status===0){if(callback&&typeof callback.success==='function'){setTimeout(function(){callback.success(xhr[responseProperty]);},0);}}
else{console.error('HTTP error executing GET. ',uri,' Status: ',xhr.status);if(callback&&typeof callback.error==='function'){setTimeout(function errorHandler(){callback.error({status:xhr.status});},0);}}};xhr.ontimeout=function(e){console.error('Timeout!!! while HTTP GET: ',uri);if(callback&&typeof callback.timeout==='function'){setTimeout(callback.timeout,0);}};xhr.onerror=function(e){console.error('Error while executing HTTP GET: ',uri,': ',e);if(callback&&typeof callback.error==='function'&&!outReq.isCancelled()){setTimeout(function(){callback.error(e);},0);}};xhr.send();return outReq;}};return new Rest();})();}

View file

@ -0,0 +1,103 @@
/* global Promise, Event */
'use strict';
/**
* Helper library to deal with in case of emergency (ICE)
* Contacts.
* Those contacts are suppose to be first responders to
* an emergency. More info:
* https://wiki.mozilla.org/Gaia/Comms/2.1/Ice_Contacts
*
* The information will be stored in a DS for sharing
* between contacts and dialer.
* The content will be an array of two contacts ids.
* This helper will be used for fetching:
* ICEStore.getContacts().then(function(iceContacts) {
* ...
* });
* and setting the data:
* ICEStore.setContacts(ids).then(function() {
* ...
* });
*/
(function(exports) {
var STORE_NAME = 'ice_contacts';
var store = null;
var status = 'not_initialized';
var FIELD = 'ICE_CONTACTS';
// Get a reference to the store, or returns it if
// we already have.
function init() {
if (store !== null) {
return Promise.resolve(store);
}
if (!navigator.getDataStores) {
return Promise.reject('No DS supported');
}
if (status === 'initialising') {
document.addEventListener('ice_ready', function ready() {
document.removeEventListener('ice_ready', ready);
if (store === null) {
return Promise.reject('Error getting store');
} else {
return Promise.resolve(store);
}
});
}
status = 'initialising';
return new Promise(function(resolve, reject) {
navigator.getDataStores(STORE_NAME).then(function(stores) {
if (!stores || stores.length < 0) {
document.dispatchEvent(new Event('on_ice_ready'));
return reject('No DS found');
} else {
store = stores[0];
status = 'initialised';
document.dispatchEvent(new Event('ice_ready'));
return resolve(store);
}
});
});
}
var ICEStore = {
/**
* Set the in case of emergency contacts.
* @param {Array} iceContacts array of contacts id.
* @returns {Promise}
*/
setContacts: function(iceContacts) {
return init().then(function() {
return store.put(iceContacts, FIELD).then(function() {
return Promise.resolve(iceContacts);
});
});
},
/**
* Retrieves the in case of emergency contacts
* @returns {Promise}
*/
getContacts: function() {
return init().then(function() {
return store.get(FIELD);
});
},
/**
* Subscribe to changes in the store
* @param {Function} Callback when a change happens
*/
onChange: function(cb) {
return init().then(function() {
store.addEventListener('change', function() {
store.get(FIELD).then(cb);
});
});
}
};
exports.ICEStore = ICEStore;
})(window);

View file

@ -0,0 +1,25 @@
;;'use strict';if(!window.ImageLoader){var ImageLoader=function ImageLoader(pContainer,pItems){var container,items,itemsSelector,lastScrollTime,scrollLatency=100,scrollTimer,itemHeight,total,imgsLoading=0,loadImage=defaultLoadImage,self=this;init(pContainer,pItems);function init(pContainer,pItems){itemsSelector=pItems;container=document.querySelector(pContainer);attachHandlers();window.addEventListener('image-loader-resume',resuming);window.addEventListener('image-loader-pause',unload);load();}
function resuming(){window.clearTimeout(scrollTimer);attachHandlers();update();}
function onUpdate(evt){evt.stopPropagation();onScroll();}
function load(){window.clearTimeout(scrollTimer);scrollTimer=null;items=container.querySelectorAll(itemsSelector);itemHeight=items[0]?items[0].offsetHeight:1;total=items.length;window.setTimeout(update,0);}
function attachHandlers(){container.addEventListener('scroll',onScroll);document.addEventListener('onupdate',onUpdate);}
function unload(){container.removeEventListener('scroll',onScroll);document.removeEventListener('onupdate',onUpdate);window.clearTimeout(scrollTimer);scrollTimer=null;}
function setResolver(pResolver){loadImage=pResolver;}
function onScroll(){if(imgsLoading>0){window.stop();imgsLoading=0;}
lastScrollTime=Date.now();if(!scrollTimer){scrollTimer=window.setTimeout(updateFromScroll,scrollLatency);}}
function updateFromScroll(){scrollTimer=null;var deltaLatency=lastScrollTime-Date.now()+scrollLatency;if(deltaLatency>0){scrollTimer=window.setTimeout(updateFromScroll,deltaLatency);}else{update();}}
function defaultLoadImage(item){var image=item.querySelector('span[data-type=img][data-src]');if(!image){image=item.querySelector('img[data-src]');if(!image){image=item.querySelector('span[data-type=img][data-group]');if(image){item.dataset.visited='true';}
return;}}
++imgsLoading;var tmp=new Image();var src=tmp.src=image.dataset.src;tmp.onload=function onload(){--imgsLoading;image.style.backgroundImage='url('+src+')';if(tmp.complete){item.dataset.visited='true';}
tmp=null;};tmp.onabort=tmp.onerror=function onerror(){--imgsLoading;item.dataset.visited='false';tmp=null;};}
function update(){if(total===0){return;}
var viewTop=container.scrollTop;var index=Math.floor(viewTop/itemHeight);var containerHeight=container.offsetHeight;for(var i=index;i>=0;i--){var item=items[i];if(item){if(item.offsetTop+itemHeight<viewTop){break;}
if(item.dataset.visited!=='true'&&item.offsetTop<=viewTop+containerHeight){loadImage(item,self);}}}
for(var j=index+1;j<total;j++){var theItem=items[j];if(!theItem){return;}
if(theItem.offsetTop>viewTop+containerHeight){return;}
if(theItem.dataset.visited!=='true'){loadImage(theItem,self);}}}
function releaseImage(item){var image=item.querySelector('span[data-type=img][data-src]');if(!image){image=item.querySelector('span[data-type=img][data-group]');if(image){item.dataset.visited='false';}
return null;}
image.style.backgroundImage='none';item.dataset.visited='false';return image;}
function destroy(){unload();window.removeEventListener('image-loader-pause',unload);window.removeEventListener('image-loader-resume',resuming);container=items=itemsSelector=lastScrollTime=scrollLatency=null;scrollTimer=itemHeight=total=imgsLoading=loadImage=null;}
this.reload=load;this.unload=unload;this.setResolver=setResolver;this.defaultLoad=defaultLoadImage;this.releaseImage=releaseImage;this.destroy=destroy;};}

View file

@ -0,0 +1,2 @@
;;;'use strict';var utils=window.utils||{};if(typeof utils.squareImage==='undefined'){utils.squareImage=function(blob,callback){var img=document.createElement('img');var url=URL.createObjectURL(blob);img.src=url;function cleanupImg(){img.src='';URL.revokeObjectURL(url);}
img.onerror=cleanupImg;img.onload=function onBlobLoad(){var width=img.width;var height=img.height;if(width===height){cleanupImg();callback(blob);}else{var canvas=document.createElement('canvas');var min=canvas.width=canvas.height=Math.min(width,height);var context=canvas.getContext('2d',{willReadFrequently:true});context.drawImage(img,(width-min)/2,(height-min)/2,min,min,0,0,min,min);cleanupImg();canvas.toBlob(function onCanvasToBlob(canvasBlob){context=null;canvas.width=canvas.height=0;canvas=null;callback(canvasBlob);},'image/jpeg',0.95);}};};}

View file

@ -0,0 +1,4 @@
;;;'use strict';var utils=window.utils||{};(function(utils){var DEPENDENCIES=['/shared/js/contacts/import/utilities/config.js','/shared/js/image_utils.js'];var dpr=window.devicePixelRatio||1;var CONFIG_FILE='/config-images.json';var DEFAULT_CONFIG={'thumbnail':{'format':'image/jpeg','size':65,'quality':1.0}};var THUMB_CONFIG=DEFAULT_CONFIG.thumbnail;if(typeof utils.thumbnailImage!=='undefined'){return;}
function getThumbsConfig(){return new Promise(function(resolve,reject){utils.config.load(CONFIG_FILE).then(function resolved(config){resolve(config);},function rejected(){resolve(DEFAULT_CONFIG);});});}
function scaleImage(blob,configData){var thumbConfig=configData&&configData.thumbnail||{};var thumbnailEdge=(thumbConfig.size||THUMB_CONFIG.size)*dpr;var format=thumbConfig.format||THUMB_CONFIG.format;var encodingQuality=thumbConfig.quality||THUMB_CONFIG.quality;return ImageUtils.resizeAndCropToCover(blob,thumbnailEdge,thumbnailEdge,format,encodingQuality);}
utils.thumbnailImage=function(blob,callback){LazyLoader.load(DEPENDENCIES,function(){getThumbsConfig().then(function(config){return scaleImage(blob,config);}).then(callback).catch(function(err){console.error('Error while converting image to thumbnail:',err.name);callback(blob);});});};})(utils);

View file

@ -0,0 +1,14 @@
;;'use strict';var utils=window.utils||{};if(!utils.templates){(function(){var Templates=utils.templates={};function getTarget(element){var target=element;if(!element.tagName){target=document.querySelector(element);}
return target;}
function getTemplate(target,data){var templates=target.querySelectorAll('*[data-template]');if(templates.length===0){throw new Error('No template declared');}
if(templates.length>1&&templates.item(0).dataset.condition){throw new Error('Only one template supported in this version');}
return{template:templates.item(0),isMulti:false};}
function templateReplace(data){return function(text,property){var out;if(property.indexOf('.')===-1){out=data[property];if(Array.isArray(out)&&out.length>0){out=out[0];}}else{throw new Error('Dotted expressions not supported');}
if(typeof out==='undefined'){out=text;}
return out;};}
function add(element,data,mode,targetNode){var target=getTarget(element);var newElem;var theData=[data];if(data instanceof Array){theData=data;}
var multiTemplate=true;var template;var idx=0;theData.forEach(function(oneData){oneData._idx_=idx++;if(multiTemplate===true){var tresult=getTemplate(target,oneData);template=tresult.template;if(tresult.isMulti===false){multiTemplate=false;}}
if(template){newElem=this.render(template,oneData);target=targetNode||target;if(mode==='A'){target.appendChild(newElem);}else if(mode==='P'){if(target.firstChild){target.insertBefore(newElem,element.firstChild);}else{target.appendChild(newElem);}}}}.bind(Templates));return newElem;}
Templates.append=function(element,data,targetNode){var f=add.bind(this);return f(element,data,'A',targetNode);};Templates.prepend=function(element,data,targetNode){var f=add.bind(this);return f(element,data,'P',targetNode);};Templates.render=function(eleTemplate,data){var newElem=eleTemplate.cloneNode(true);newElem.removeAttribute('data-template');newElem.removeAttribute('data-condition');var pattern=/#(\w+[\w.]*)#/g;var inner=newElem.innerHTML;var replaceFunction=templateReplace(data);var ninner=inner.replace(pattern,replaceFunction);newElem.innerHTML=ninner;var attrs=newElem.attributes;var total=attrs.length;for(var c=0;c<total;c++){var val=attrs[c].value;var nval=val.replace(pattern,replaceFunction);newElem.setAttribute(attrs[c].name,nval);}
if(!newElem.id){if(data.id){newElem.id=data.id;}}
return newElem;};Templates.clear=function(element){var target=getTarget(element);var templates=target.querySelectorAll('*[data-template]');target.innerHTML='';var total=templates.length;for(var c=0;c<total;c++){target.appendChild(templates.item(c));}};})();}

View file

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

View file

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

View file

@ -0,0 +1,33 @@
'use strict';
/* exported getStorageIfAvailable */
// Get a DeviceStorage object for the specified kind of storage and, if it
// is available, and if the specified number of bytes of storage space are
// free then asynchronously pass the DeviceStorage object to the success
// callback. Otherwise, invoke the error callback, if one is specified. If
// the error callback is called because device storage is not available, the
// argument will be a DeviceStorage status string like 'unavailable' or
// 'shared'. If the error callback is called because there is not enough
// storage space, the argument will be the number of bytes that are available.
function getStorageIfAvailable(kind, size, success, error) {
var storage = navigator.getDeviceStorage(kind);
storage.available().onsuccess = function(e) {
if (e.target.result !== 'available') {
if (error) {
error(e.target.result);
}
}
else {
storage.freeSpace().onsuccess = function(e) {
if (e.target.result < size) {
if (error) {
error(e.target.result);
}
}
else {
success(storage);
}
};
}
};
}

View file

@ -0,0 +1,37 @@
'use strict';
/* exported getUnusedFilename */
// This utility function helps avoid overwriting existing files.
// If no file with the specified name exists in the specified storage, pass
// the name to the callback. Otherwise, add a version number to the name
// and try again. Once a name is found that does not already exist, pass it
// to the callback. If the initial name 'base.ext' does not exist, then new
// names of the form 'base_n.ext', where n is a number are tried.
function getUnusedFilename(storage, name, callback) {
var getreq = storage.get(name);
getreq.onerror = function() {
// We get this error if the file does not already exist, and that
// is what we want
callback(name);
};
getreq.onsuccess = function() {
var version = 0;
var p = name.lastIndexOf('/');
var dir = name.substring(0, p + 1);
var file = name.substring(p + 1);
p = file.lastIndexOf('.');
if (p === -1) {
p = file.length;
}
var ext = file.substring(p);
var base = file.substring(0, p);
var parts = base.match(/^(.*)_(\d{1,2})$/);
if (parts) {
base = parts[1];
version = parseInt(parts[2]);
}
var newname = dir + base + '_' + (version + 1) + ext;
getUnusedFilename(storage, newname, callback);
};
}

View file

@ -0,0 +1,15 @@
;'use strict';var Contacts={_FB_FILES:['/shared/js/fb/fb_request.js','/shared/js/fb/fb_data_reader.js','/shared/js/fb/fb_reader_utils.js'],getRevision:function getRevision(callback){var mozContacts=navigator.mozContacts;if(!mozContacts){callback(null);return;}
var req=mozContacts.getRevision();req.onsuccess=function onsuccess(event){callback(event.target.result);};req.onerror=function onerror(event){console.log('Error '+event.target.error);callback(null);};},findByNumber:function findByNumber(number,callback){LazyLoader.load(this._FB_FILES,this._findByNumber.bind(this,number,callback));},_findByNumber:function _findByNumber(number,callback){if(!number){callback(null);return;}
var options;var variants;if(number.length<7){variants=[number];options={filterBy:['tel'],filterOp:'equals',filterValue:number};}else{variants=SimplePhoneMatcher.generateVariants(number);var sanitizedNumber=SimplePhoneMatcher.sanitizedNumber(number);options={filterBy:['tel'],filterOp:'match',filterValue:sanitizedNumber};}
var mozContacts=navigator.mozContacts;if(!mozContacts){callback(null);return;}
var request=mozContacts.find(options);request.onsuccess=function findCallback(){var contacts=request.result;if(contacts.length===0){fb.getContactByNumber(number,function fb_ready(finalContact){var objMatching=null;if(finalContact){objMatching={value:number,type:'personal',carrier:null};}
callback(finalContact,objMatching);},function fb_err(err){callback(null);});return;}
var matches=contacts.map(function getTels(contact){return contact.tel.map(function getNumber(tel){return tel.value;});});var matchResult=SimplePhoneMatcher.bestMatch(variants,matches);var contact=contacts[matchResult.bestMatchIndex];var contactsWithSameNumber;if(contacts.length>1){contactsWithSameNumber=contacts.length-1;}
var matchingTel=contact.tel[matchResult.localIndex];if(fb.isFbLinked(contact)){var req=fb.getData(contact);req.onsuccess=function(){callback(req.result,matchingTel,contactsWithSameNumber);};req.onerror=function(){window.console.error('Error while getting FB Data');callback(contact,matchingTel,contactsWithSameNumber);};}
else{callback(contact,matchingTel,contactsWithSameNumber);}};request.onerror=function findError(){callback(null);};},_mergeOneFbContact:function _mergeOneFbContact(contacts,index){return new Promise(function(resolve,reject){var fbReq=fb.getData(contacts[index]);fbReq.onsuccess=function(){var originalTel=contacts[index].tel;contacts[index]=fbReq.result;contacts[index].tel=originalTel;resolve();};fbReq.onerror=function(){console.error('Could not merge Facebook data for',contacts[index].id);reject();};});},_mergeFbContacts:function _mergeFbContacts(contacts,callback){if(!callback||!(callback instanceof Function)){return;}
if(!contacts){callback(null);}
LazyLoader.load(this._FB_FILES,function(){var pending=contacts.length;var onContactMerged=function(){if(--pending===0){callback(contacts);}};for(var i=0,length=contacts.length;i<length;i++){if(fb.isFbContact(contacts[i])){this._mergeOneFbContact(contacts,i).then(onContactMerged,onContactMerged);}else{onContactMerged();}}}.bind(this));},_findContacts:function _findContacts(options,callback){if(!callback||!(callback instanceof Function)){return;}
if(!navigator.mozContacts||!options){callback(null);return;}
var self=this;var req=navigator.mozContacts.find(options);req.onsuccess=function onsuccess(){var contacts=req.result;if(!contacts.length){callback(null);return;}
self._mergeFbContacts(contacts,callback);};req.onerror=function onerror(){console.error('Contact finding error. Error: '+req.errorCode);callback(null);};},findListByNumber:function findListByNumber(number,limit,callback){var self=this;asyncStorage.getItem('order.lastname',function(value){var sortKey=value?'familyName':'givenName';var options={filterBy:['tel'],filterOp:'contains',filterValue:number,sortBy:sortKey,sortOrder:'ascending',filterLimit:limit};self._findContacts(options,callback);});},getLength:function getLength(prop){if(!prop||!prop.length){return 0;}
return prop.length;},sendEmailOrPick:function sendEmailOrPick(address){try{new MozActivity({name:'new',data:{type:'mail',URI:'mailto:'+address}});}catch(e){console.error('WebActivities unavailable? : '+e);}}};

View file

@ -0,0 +1,2 @@
;'use strict';function DtmfTone(tone,short,serviceId){this.tone=tone;this.short=short;this.serviceId=serviceId;this.timer=0;}
DtmfTone.kShortToneLength=120;DtmfTone.prototype={play:function dt_play(){clearTimeout(this.timer);navigator.mozTelephony.stopTone(this.serviceId);navigator.mozTelephony.startTone(this.tone,this.serviceId);if(this.short){this.timer=window.setTimeout(function dt_stopTone(serviceId){navigator.mozTelephony.stopTone(serviceId);},DtmfTone.kShortToneLength,this.serviceId);}},stop:function dt_stop(){clearTimeout(this.timer);navigator.mozTelephony.stopTone(this.serviceId);}};

View file

@ -0,0 +1,17 @@
;'use strict';var FontSizeManager=(function fontSizeManager(){var DIAL_PAD=0,SINGLE_CALL=1,CALL_WAITING=2,STATUS_BAR=3,SECOND_INCOMING_CALL=4;var _sizes={};_sizes[DIAL_PAD]={min:2.6,max:4.1};_sizes[SINGLE_CALL]={min:2.3,max:3.4,line:3.4};_sizes[CALL_WAITING]={min:2.3,max:2.5,line:1.3};_sizes[STATUS_BAR]={min:1.7,max:1.7};_sizes[SECOND_INCOMING_CALL]={min:2.3,max:2.5,line:3.7};var _defaultFontSize;function _getRootFontSize(){_defaultFontSize=_defaultFontSize||parseInt(window.getComputedStyle(document.body,null).getPropertyValue('font-size'));return _defaultFontSize;}
function _getMinFontSize(scenario){return Math.round(_sizes[scenario].min*_getRootFontSize());}
function _getMaxFontSize(scenario){return Math.round(_sizes[scenario].max*_getRootFontSize());}
function _getAllowedFontSizes(scenario){var minFontSize=_getMinFontSize(scenario);var maxFontSize=_getMaxFontSize(scenario);var allowedSizes=[];for(var size=minFontSize;size<=maxFontSize;size+=4){allowedSizes.push(size);}
if(allowedSizes.indexOf(maxFontSize)===-1){allowedSizes.push(maxFontSize);}
return allowedSizes;}
function adaptToSpace(scenario,view,forceMaxFontSize,ellipsisSide){var computedStyle=window.getComputedStyle(view);if((!view.value&&!view.textContent)||!computedStyle){return;}
var viewWidth=view.getBoundingClientRect().width;var viewFont=computedStyle.fontFamily;var allowedSizes;if(forceMaxFontSize){allowedSizes=[_getMaxFontSize(scenario)];}else{allowedSizes=_getAllowedFontSizes(scenario);}
var infos=FontSizeUtils.getMaxFontSizeInfo(view.value||view.textContent,allowedSizes,viewFont,viewWidth);var newFontSize=infos.fontSize;if(view.style.fontSize!==newFontSize){view.style.fontSize=newFontSize+'px';}
if(infos&&infos.overflow){var overflowCount=FontSizeUtils.getOverflowCount(view.value||view.textContent,newFontSize,viewFont,viewWidth);overflowCount+=2;_useEllipsis(view,overflowCount,ellipsisSide);view.dataset.ellipsedCharacters=overflowCount;}else{view.dataset.ellipsedCharacters=0;}}
function _useEllipsis(view,overflowCount,ellipsisSide){var side=ellipsisSide||'begin';var localizedSide=(side==='begin'?'left':'right');var value=view.value||view.textContent;if(localizedSide=='left'){value='\u2026'+value.substr(-value.length+overflowCount);}else if(localizedSide=='right'){value=value.substr(0,value.length-overflowCount)+'\u2026';}
if(view.value){view.value=value;}else{var el=view.querySelector('bdi')||view;el.textContent=value;}}
function ensureFixedBaseline(scenario,view){var initialLineHeight=_sizes[scenario].line;if(!initialLineHeight){return;}
var maxFontSize=_getMaxFontSize(scenario);var fontSize=parseInt(window.getComputedStyle(view).fontSize,10);view.style.lineHeight=initialLineHeight*_getRootFontSize()+
(maxFontSize-fontSize)/2+'px';}
function resetFixedBaseline(view){view.style.removeProperty('line-height');}
return{DIAL_PAD:DIAL_PAD,SINGLE_CALL:SINGLE_CALL,CALL_WAITING:CALL_WAITING,STATUS_BAR:STATUS_BAR,SECOND_INCOMING_CALL:SECOND_INCOMING_CALL,adaptToSpace:adaptToSpace,ensureFixedBaseline:ensureFixedBaseline,resetFixedBaseline:resetFixedBaseline};})();

View file

@ -0,0 +1,47 @@
;'use strict';var gTonesFrequencies={'1':[697,1209],'2':[697,1336],'3':[697,1477],'4':[770,1209],'5':[770,1336],'6':[770,1477],'7':[852,1209],'8':[852,1336],'9':[852,1477],'*':[941,1209],'0':[941,1336],'#':[941,1477]};var KeypadManager={kMaxDigits:50,_phoneNumber:'',_onCall:false,_keypadSoundIsEnabled:false,_shortTone:false,_vibrationEnabled:false,kVibrationDuration:50,_insertPosition:null,onValueChanged:null,get phoneNumberView(){delete this.phoneNumberView;this.phoneNumberView=document.getElementById('phone-number-view');return this.phoneNumberView;},get phoneNumberViewContainer(){delete this.phoneNumberViewContainer;this.phoneNumberViewContainer=document.getElementById('phone-number-view-container');return this.phoneNumberViewContainer;},get keypad(){delete this.keypad;this.keypad=document.getElementById('keypad');return this.keypad;},get callBar(){delete this.callBar;this.callBar=document.getElementById('keypad-callbar');return this.callBar;},get hideBar(){delete this.hideBar;this.hideBar=document.getElementById('keypad-hidebar');return this.hideBar;},get callBarAddContact(){delete this.callBarAddContact;this.callBarAddContact=document.getElementById('keypad-callbar-add-contact');return this.callBarAddContact;},get callBarCallAction(){delete this.callBarCallAction;this.callBarCallAction=document.getElementById('keypad-callbar-call-action');return this.callBarCallAction;},get callBarCancelAction(){delete this.callBarCancelAction;this.callBarCancelAction=document.getElementById('keypad-callbar-cancel');return this.callBarCancelAction;},get deleteButton(){delete this.deleteButton;this.deleteButton=document.getElementById('keypad-delete');return this.deleteButton;},get hideBarHangUpAction(){delete this.hideBarHangUpAction;this.hideBarHangUpAction=document.getElementById('keypad-hidebar-hang-up-action-wrapper');return this.hideBarHangUpAction;},get hideBarHideAction(){delete this.hideBarHideAction;this.hideBarHideAction=document.getElementById('keypad-hidebar-hide-keypad-action');return this.hideBarHideAction;},multiSimActionButton:null,init:function kh_init(oncall){this._onCall=!!oncall;this._lastPressedKey=null;this.phoneNumberView.value='';this.phoneNumberView.addEventListener('click',this._updateInsertPosition.bind(this));this._phoneNumber='';var keyHandler=this.keyHandler.bind(this);this.keypad.addEventListener('contextmenu',keyHandler);this.keypad.addEventListener('touchstart',keyHandler,true);this.keypad.addEventListener('touchmove',keyHandler,true);this.keypad.addEventListener('touchend',keyHandler,true);this.keypad.addEventListener('touchcancel',keyHandler,true);this.deleteButton.addEventListener('touchstart',keyHandler);this.deleteButton.addEventListener('touchend',keyHandler);if(this.callBarAddContact){this.callBarAddContact.addEventListener('click',this.addContact.bind(this));}
if(this.callBarCallAction){if(typeof MultiSimActionButton!=='undefined'){this.multiSimActionButton=new MultiSimActionButton(this.callBarCallAction,CallHandler.call,'ril.telephony.defaultServiceId',this.phoneNumber.bind(this));}
this.callBarCallAction.addEventListener('click',this.fetchLastCalled.bind(this));}
if(this.callBarCancelAction){this.callBarCancelAction.addEventListener('click',function(){window.parent.LockScreen.switchPanel();});}
if(this.hideBarHideAction){this.hideBarHideAction.addEventListener('click',this.callbarBackAction);}
if(this.hideBarHangUpAction){this.hideBarHangUpAction.addEventListener('click',this.hangUpCallFromKeypad);}
TonePlayer.init('system');this.render();LazyLoader.load(['/shared/style/action_menu.css','/dialer/js/suggestion_bar.js']);this._observePreferences();},render:function hk_render(layoutType){if(layoutType=='oncall'){if(CallsHandler.activeCall){var activeCall=CallsHandler.activeCall.call;this._phoneNumber=activeCall.id.number;}
this._isKeypadClicked=false;this.phoneNumberViewContainer.classList.add('keypad-visible');if(this.callBar){this.callBar.classList.add('hide');}
if(this.hideBar){this.hideBar.classList.remove('hide');}
this.deleteButton.classList.add('hide');}else{this.phoneNumberViewContainer.classList.remove('keypad-visible');if(this.callBar){this.callBar.classList.remove('hide');}
if(this.hideBar){this.hideBar.classList.add('hide');}
this.deleteButton.classList.remove('hide');}},phoneNumber:function hk_phoneNumber(){return this._phoneNumber;},fetchLastCalled:function hk_fetchLastCalled(){if(this._phoneNumber!==''){return;}
var self=this;CallLogDBManager.getGroupAtPosition(1,'lastEntryDate',true,'dialing',function hk_ggap_callback(result){if(result&&(typeof result==='object')&&result.number){self.updatePhoneNumber(result.number);}});},addContact:function hk_addContact(event){var number=this._phoneNumber;if(!number){return;}
LazyLoader.load(['/dialer/js/add_contact_menu.js'],function(){AddContactMenu.show(number);});},callbarBackAction:function hk_callbarBackAction(event){CallScreen.hideKeypad();},hangUpCallFromKeypad:function hk_hangUpCallFromKeypad(event){CallScreen.hideKeypad();CallsHandler.end();},_lastPressedKey:null,_dtmfTone:null,_playDtmfTone:function kh_playDtmfTone(key){var serviceId=0;if(!this._onCall){return;}
if(CallsHandler.activeCall){serviceId=CallsHandler.activeCall.call.serviceId;}else{serviceId=navigator.mozTelephony.active.calls[0].serviceId;}
if(this._dtmfTone){this._dtmfTone.stop();this._dtmfTone=null;}
this._dtmfTone=new DtmfTone(key,this._shortTone,serviceId);this._dtmfTone.play();},_stopDtmfTone:function kh_stopDtmfTone(){if(!this._dtmfTone){return;}
this._dtmfTone.stop();this._dtmfTone=null;},_touchStart:function kh_touchStart(key){this._longPress=false;this._lastPressedKey=key;if(key!=='delete'&&!this._onCall&&(this._phoneNumber.length>=this.kMaxDigits)){return;}
if(key!='delete'){if(this._keypadSoundIsEnabled){TonePlayer.start(gTonesFrequencies[key],!this._onCall||this._shortTone);}
if(this._vibrationEnabled){navigator.vibrate(this.kVibrationDuration);}
this._playDtmfTone(key);}
if(((key=='0'||key=='*')&&!this._onCall)||key=='delete'){this._holdTimer=setTimeout(function(self){self.restoreCaretPosition();if(key=='delete'){self._clearPhoneNumber();}else if(key==='0'){self._replaceLastKey('+');}else if(key==='*'){var isAtTheEnd=self._insertPosition===null;var isFirst=self._insertPosition===1||(isAtTheEnd&&self._phoneNumber.length===1);if(!isFirst){self._replaceLastKey(',');}else{self._deleteAtInsertPosition();}}
self._longPress=true;self._updatePhoneNumberView('begin',false);},400,this);}
if(key==='1'&&this._phoneNumber===''){this._holdTimer=setTimeout(function vm_call(self){self._longPress=true;self._callVoicemail();self._phoneNumber='';self._updatePhoneNumberView('begin',false);},400,this);}
if(key=='delete'){this._deleteAtInsertPosition();}else if(this.phoneNumberViewContainer.classList.contains('keypad-visible')){if(!this._isKeypadClicked){this._isKeypadClicked=true;this._phoneNumber=key;this.replaceAdditionalContactInfo('');}else{this._insertAtCaret(key);}}else{this._insertAtCaret(key);}
if(key==='#'&&!this._onCall){if(this._phoneNumber==='*#06#'){this.multiSimActionButton.performAction();return;}else if(this._isSpeedDialNumber(this._phoneNumber)){this._handleSpeedDial(this._phoneNumber.slice(0,-1));return;}}
setTimeout(function(self){self._updatePhoneNumberView('begin',false);},0,this);},_touchMove:function kh_touchMove(touch){var target=document.elementFromPoint(touch.pageX,touch.pageY);var key=(target&&target.dataset)?target.dataset.value:null;if(key!==this._lastPressedKey||key==='delete'){this._stopDtmfTone();}},_touchEnd:function kh_touchEnd(key){this._lastPressedKey=null;if(key!=='delete'){this._stopDtmfTone();}
if(this._keypadSoundIsEnabled){TonePlayer.stop();}
this.restoreCaretPosition();if(this._longPress){this._longPress=false;this._holdTimer=null;return;}
if(this._holdTimer){clearTimeout(this._holdTimer);}},keyHandler:function kh_keyHandler(event){event.preventDefault();if(event.type=='contextmenu'){return;}
var key=event.target.dataset.value;if(!key){return;}
event.stopPropagation();switch(event.type){case'touchstart':if(this._lastPressedKey){return;}
event.target.classList.add('active');this._touchStart(key);break;case'touchmove':this._touchMove(event.touches[0]);break;case'touchend':case'touchcancel':if(key!==this._lastPressedKey){return;}
event.target.classList.remove('active');this._touchEnd(key);break;}},_isSpeedDialNumber:function(number){return!!number.match(/^[0-9][0-9]{0,2}\#$/);},_getSpeedDialNumber:function(index){var self=this;var cardIndex;index--;return new Promise(function(resolve,reject){LazyLoader.load(['/shared/js/sim_settings_helper.js'],function(){SimSettingsHelper.getCardIndexFrom('outgoingCall',function(defaultCardIndex){if(defaultCardIndex==SimSettingsHelper.ALWAYS_ASK_OPTION_VALUE){LazyLoader.load(['/shared/js/component_utils.js','/shared/elements/gaia_sim_picker/script.js'],function(){var simPicker=document.getElementById('sim-picker');simPicker.getOrPick(defaultCardIndex,null,function(pickedCardIndex){cardIndex=pickedCardIndex;resolve();});});}else{cardIndex=defaultCardIndex;resolve();}});});}).then(function(){return self._getSimContactsList(cardIndex).then(function(simContactsList){if((index>=0)&&(index<simContactsList.length)){return simContactsList[index].number;}else{return Promise.reject('noContactsWereFound');}});});},_handleSpeedDial:function(index){var self=this;this.updatePhoneNumber('','begin',false);this._getSpeedDialNumber(+index).then(function(number){self.updatePhoneNumber(number,'begin',false);},function(error){if(error){ConfirmDialog.show(error,null,{title:'noContactsFoundDialogOk',callback:ConfirmDialog.hide});}});},_createSimContactList:function(contacts){var numbers=new Array(contacts.length);for(var i=0;i<contacts.length;i++){numbers[i]={id:contacts[i].id,number:contacts[i].tel[0].value,};}
numbers.sort(function(a,b){if(a.id.length==b.id.length){return(a.id>b.id)?1:0;}else{return(a.id.length>b.id.length)?1:0;}});return numbers;},_getSimContactsList:function(cardIndex){var self=this;var canceled=false;return new Promise(function(resolve,reject){LazyLoader.load(['/shared/style/confirm.css','/shared/js/confirm.js',document.getElementById('confirmation-message')],function(){var iccId=navigator.mozIccManager.iccIds[cardIndex];var icc=navigator.mozIccManager.getIccById(iccId);if(!icc){reject('noSimCardToLoadContactsFrom');return;}
var req=icc.readContacts('adn');req.onsuccess=function(event){var adnContacts=event.target.result;var contacts=self._createSimContactList(adnContacts);if(!canceled){ConfirmDialog.hide();}
resolve(contacts);};req.onerror=function(error){console.error('Could not retrieve the ADN contacts from SIM card '+
cardIndex+', got error '+error.name);reject('noContactsWereFound');};ConfirmDialog.show('loadingSimContacts',null,{title:'cancel',callback:function(){canceled=true;ConfirmDialog.hide();reject();}});});});},sanitizePhoneNumber:function(number){return number.replace(/\s+/g,'');},updatePhoneNumber:function kh_updatePhoneNumber(number,ellipsisSide,forceMaxFontSize){number=this.sanitizePhoneNumber(number);this._phoneNumber=number;this._updatePhoneNumberView(ellipsisSide,forceMaxFontSize);},press:function(value){this._playDtmfTone(value);TonePlayer.start(gTonesFrequencies[value],true);setTimeout((function nextTick(){TonePlayer.stop();this._stopDtmfTone();}).bind(this));},_updatePhoneNumberView:function kh_updatePhoneNumberview(ellipsisSide,forceMaxFontSize){var phoneNumber=this._phoneNumber;if(this._onCall){this.renderPhoneNumber(phoneNumber,ellipsisSide);}else{var visibility;if(phoneNumber.length>0){visibility='visible';this.callBarAddContact.removeAttribute('disabled');}else{visibility='hidden';this.callBarAddContact.setAttribute('disabled','disabled');}
this.deleteButton.style.visibility=visibility;this.phoneNumberView.blur();this.phoneNumberView.value=phoneNumber;FontSizeManager.adaptToSpace(FontSizeManager.DIAL_PAD,this.phoneNumberView,forceMaxFontSize,ellipsisSide);}
if(this.onValueChanged){this.onValueChanged(this._phoneNumber);}},renderPhoneNumber:function kh_replacePhoneNumber(phoneNumber,ellipsisSide){if(this._onCall&&CallsHandler.activeCall){CallsHandler.activeCall.renderPhoneNumber({raw:phoneNumber},ellipsisSide);}},restorePhoneNumber:function kh_restorePhoneNumber(){if(this._onCall&&CallsHandler.activeCall){CallsHandler.activeCall.restorePhoneNumber();}},replaceAdditionalContactInfo:function kh_replaceAdditionalContactInfo(additionalContactInfo){var call=CallsHandler.activeCall;if(this._onCall&&call){call.replaceAdditionalContactInfo(additionalContactInfo);}},restoreAdditionalContactInfo:function kh_restoreAdditionalContactInfo(){if(this._onCall&&CallsHandler.activeCall){CallsHandler.activeCall.restoreAdditionalContactInfo();}},_callVoicemail:function(){if(navigator.mozIccManager.iccIds.length<=1){this._callVoicemailForSim(0);return;}
var self=this;var key='ril.voicemail.defaultServiceId';var req=navigator.mozSettings.createLock().get(key);req.onsuccess=function(){LazyLoader.load(['/shared/js/component_utils.js','/shared/elements/gaia_sim_picker/script.js'],function(){var simPicker=document.getElementById('sim-picker');navigator.mozL10n.formatValue('voiceMail').then(string=>{simPicker.getOrPick(req.result[key],string,self._callVoicemailForSim.bind(self));});});};},_callVoicemailForSim:function(cardIndex){var settings=navigator.mozSettings;if(!settings){return;}
var transaction=settings.createLock();var request=transaction.get('ril.iccInfo.mbdn');request.onsuccess=(function(){var numbers=request.result['ril.iccInfo.mbdn'];var number;if(typeof numbers=='string'){number=numbers;}else{number=numbers&&numbers[cardIndex];}
var voicemail=navigator.mozVoicemail;if(!number&&voicemail){number=voicemail.getNumber(cardIndex);}
if(number){CallHandler.call(number,cardIndex);}else{this._showNoVoicemailDialog();}}).bind(this);request.onerror=function(){};},_showNoVoicemailDialog:function hk_showNoVoicemailDialog(){var voicemailDialog={title:'voicemailNoNumberTitle',text:'voicemailNoNumberText',confirm:{title:'voicemailNoNumberSettings',recommend:true,callback:this.showVoicemailSettings},cancel:{title:'voicemailNoNumberCancel',callback:this._hideNoVoicemailDialog}};LazyLoader.load(['/shared/js/custom_dialog.js'],function(){CustomDialog.show(voicemailDialog.title,voicemailDialog.text,voicemailDialog.cancel,voicemailDialog.confirm);});},_hideNoVoicemailDialog:function kh_hideNoVoicemailDialog(){CustomDialog.hide();},showVoicemailSettings:function kh_showVoicemailSettings(){var activity=new window.MozActivity({name:'configure',data:{target:'device',section:'call'}});activity.onerror=function(){console.warn('Configure activity error:',activity.error.name);};},_observePreferences:function kh_observePreferences(){var self=this;LazyLoader.load('/shared/js/settings_listener.js',function(){SettingsListener.observe('phone.ring.keypad',false,function(value){self._keypadSoundIsEnabled=!!value;});SettingsListener.observe('phone.dtmf.type','long',function(value){self._shortTone=(value==='short');});SettingsListener.observe('keyboard.vibration',false,function(value){self._vibrationEnabled=!!value;});});},_updateInsertPosition:function kh_updateInsertPosition(){this._insertPosition=this._realStartPosition();},_replaceLastKey:function kh_replaceLastKey(newKey){this._deleteAtInsertPosition();this.restoreCaretPosition();this._insertAtCaret(newKey);},_insertAtCaret:function kh_insertAtCaret(key){if(this._insertPosition===null){this._phoneNumber+=key;}else{var start=this._realStartPosition();var end=this._realEndPosition();this._phoneNumber=this._phoneNumber.substring(0,start)+key+
this._phoneNumber.substring(end);this._insertPosition=start+1;}},_deleteAtInsertPosition:function kh_deleteAtInsertPosition(){if(this._insertPosition===null){this._phoneNumber=this._phoneNumber.slice(0,-1);}else{var start=this._realStartPosition();var end=this._realEndPosition();if(start>0&&start===end){start=end-1;}
this._phoneNumber=this._phoneNumber.substring(0,start)+
this._phoneNumber.substring(end);this._insertPosition=this._phoneNumber?start:null;}},_clearPhoneNumber:function kh_clearPhoneNumber(){if(this._insertPosition===null){this._phoneNumber='';}else{var start=this._realStartPosition();this._phoneNumber=this._phoneNumber.substring(start);this._insertPosition=this._phoneNumber?0:null;}},restoreCaretPosition:function kh_restoreCaretPosition(){if(this._insertPosition!==null){var caretPosition=this._caretPosition(this._insertPosition);this.phoneNumberView.selectionStart=caretPosition;this.phoneNumberView.selectionEnd=caretPosition;this.phoneNumberView.focus();}else{this.phoneNumberView.blur();}},_realStartPosition:function kh_realStartPosition(){var start=this.phoneNumberView.selectionStart;return this._realPosition(this.phoneNumberView,start);},_realEndPosition:function kh_realEndPosition(){var end=this.phoneNumberView.selectionEnd;return this._realPosition(this.phoneNumberView,end);},_caretPosition:function kh_caretPosition(realPosition){return realPosition-this._ellipsisOffset(this.phoneNumberView);},_realPosition:function kh_realPosition(input,position){return position+this._ellipsisOffset(this.phoneNumberView);},_ellipsisOffset:function kh_ellipsisOffset(input){var ellipsedCharacters=+input.dataset.ellipsedCharacters;return ellipsedCharacters?(ellipsedCharacters-1):0;}};

View file

@ -0,0 +1,134 @@
'use strict';
/* globals ConfirmDialog, LazyLoader */
/* exported TelephonyMessages */
var TelephonyMessages = {
/**
* Constants for what message to display when we get a bad number error.
* Depends on the situation.
*/
// Display the regular "invalid number" message, as this is a regular call.
REGULAR_CALL: 0,
// We have no network and/or no SIM, so we can only make emergency calls.
// Display the "no network" message.
NO_NETWORK: 1,
// We are making a call from an emergency call view, so calls can only be
// placed to emergency and ICE numbers. Display the "not an emergency number"
// message.
EMERGENCY_ONLY: 2,
displayMessage: function(message, number) {
var dialogTitle, dialogBody;
switch (message) {
case 'BadNumber':
dialogTitle = 'invalidNumberToDialTitle';
dialogBody = 'invalidNumberToDialMessage';
break;
case 'FlightMode':
dialogTitle = 'callAirplaneModeTitle';
dialogBody = 'callAirplaneModeMessage';
break;
case 'NoNetwork':
dialogTitle = 'emergencyDialogTitle';
dialogBody = 'emergencyDialogBodyBadNumber';
break;
case 'DeviceNotAccepted':
dialogTitle = 'emergencyDialogTitle';
dialogBody = 'emergencyDialogBodyDeviceNotAccepted';
break;
case 'UnableToCall':
dialogTitle = 'unableToCallTitle';
dialogBody = 'unableToCallMessage';
break;
case 'NumberIsBusy':
dialogTitle = 'numberIsBusyTitle';
dialogBody = 'numberIsBusyMessage';
break;
case 'FixedDialingNumbers':
dialogTitle = 'fdnIsActiveTitle';
dialogBody = 'fdnIsActiveMessage';
break;
case 'OtherConnectionInUse':
dialogTitle = 'otherConnectionInUseTitle';
dialogBody = 'otherConnectionInUseMessage';
break;
case 'OperatorDeterminedBarring':
dialogTitle = 'unableToCallTitle';
dialogBody = 'callBarredByTheOperatorMessage';
break;
case 'EmergencyCallOnly':
dialogTitle = 'emergency-call-only';
dialogBody = 'emergency-call-error';
break;
default:
console.error('Invalid message argument'); // Should never happen
return;
}
LazyLoader.load(['/shared/style/confirm.css',
'/shared/js/confirm.js',
document.getElementById('confirmation-message')],
function() {
ConfirmDialog.show(
dialogTitle,
{id: dialogBody, args: {number: number}},
{
title: 'emergencyDialogBtnOk', // Just 'ok' would be better.
callback: function() {
ConfirmDialog.hide();
}
}
);
}
);
},
handleDisconnect: function(reason, number, emergencyOptions) {
if (reason === 'NormalCallClearing') {
return;
}
this.handleError(reason + 'Error', number, emergencyOptions);
},
handleError: function(errorName, number, emergencyOptions) {
// We ignore this error because some networks generate this with
// STK CallControl when calling Voicemail pilots (e.g. TMobile)
if (errorName === 'ModifiedDialError') {
console.log('ModifiedDialError');
return;
}
if (errorName === 'BadNumberError') {
if (emergencyOptions === this.REGULAR_CALL) {
this.displayMessage('BadNumber');
} else if (emergencyOptions === this.NO_NETWORK) {
this.displayMessage('NoNetwork');
} else if (emergencyOptions === this.EMERGENCY_ONLY) {
this.displayMessage('EmergencyCallOnly', number);
} else {
console.error('Unexpected emergency options: ' + emergencyOptions);
}
} else if (errorName === 'DeviceNotAcceptedError') {
this.displayMessage('DeviceNotAccepted');
} else if (errorName === 'RadioNotAvailable') {
this.displayMessage('FlightMode');
} else if (errorName === 'BusyError') {
this.displayMessage('NumberIsBusy');
} else if (errorName === 'FDNBlockedError' ||
errorName === 'FdnCheckFailure') {
this.displayMessage('FixedDialingNumbers', number);
} else if (errorName === 'OtherConnectionInUse') {
this.displayMessage('OtherConnectionInUse');
} else if (errorName === 'OperatorDeterminedBarringError') {
this.displayMessage('OperatorDeterminedBarring');
} else {
// If the call failed for some other reason we should still
// display something to the user. See bug 846403.
console.error('Unexpected error: ', errorName);
this.displayMessage('UnableToCall');
}
},
};

View file

@ -0,0 +1,9 @@
;'use strict';const kMasterVolume=0.5;const kToneVolume=0.7;const kShortPressDuration=0.15;const kAttackDuration=0.025;const kDecayDuration=0.025;const kReleaseDuration=0.05;var TonePlayer={_audioContext:null,_channel:null,_gainNode:null,_playingNodes:[],_maybeTrashAudio:null,_initialized:false,init:function tp_init(channel){var telephony=navigator.mozTelephony;this._reset();this._channel=channel;this._maybeTrashAudio=(function tp_maybeTrashAudio(){var callIsActive=telephony&&(telephony.calls.length||telephony.conferenceGroup.calls.length);if(document.hidden&&!((this._channel==='telephony')&&callIsActive)){this._trashAudio();}}).bind(this);window.addEventListener('visibilitychange',this._maybeTrashAudio);telephony&&telephony.addEventListener('callschanged',this._maybeTrashAudio);this._initialized=true;},teardown:function tp_teardown(){var telephony=navigator.mozTelephony;telephony&&telephony.removeEventListener('callschanged',this._maybeTrashAudio);window.removeEventListener('visibilitychange',this._maybeTrashAudio);this._reset();this._initialized=false;},_reset:function tp_reset(){this._audioContext=null;this._channel=null;this._gainNode=null;this._playingNodes=[];this._maybeTrashAudio=null;},_ensureAudio:function tp_ensureAudio(){if(this._audioContext||!this._initialized){return;}
this._audioContext=new AudioContext();},_trashAudio:function tp_trashAudio(){this.stop();this._audioContext=null;},_startAt:function tp_startAt(frequencies,when,duration){var context=this._audioContext;var sampleRate=context.sampleRate;var envelope=context.createBuffer(1,(duration?duration:0.05)*sampleRate,sampleRate);for(var i=0;i<envelope.length;i++){var factor=kToneVolume;var t=i/sampleRate;if(t<=kAttackDuration){factor=t/kAttackDuration;}else if(t-kAttackDuration<=kDecayDuration){factor=1.0-(1.0-kToneVolume)*(t-kAttackDuration)/kDecayDuration;}
if(!duration){factor-=kToneVolume;}else if(t>duration-kReleaseDuration){factor*=(duration-t)/kReleaseDuration;}
envelope.getChannelData(0)[i]=factor*kMasterVolume;}
var gainNode=context.createGain();gainNode.connect(context.destination);if(!duration){this._gainNode=gainNode;}
var envelopeNode=context.createBufferSource();envelopeNode.buffer=envelope;envelopeNode.start(when);envelopeNode.connect(gainNode.gain);gainNode.gain.setValueAtTime(duration?0.0:kToneVolume*kMasterVolume,0.0);for(i=0;i<frequencies.length;++i){var oscNode=this._audioContext.createOscillator();oscNode.type='sine';oscNode.frequency.value=frequencies[i];oscNode.start(when);if(duration){oscNode.stop(Math.max(when,context.currentTime+0.5)+duration);}else{this._playingNodes.push(oscNode);}
oscNode.connect(gainNode);}},start:function tp_start(frequencies,shortPress){this._ensureAudio();this._startAt(frequencies,0,shortPress?kShortPressDuration:0);},stop:function tp_stop(){if(!this._gainNode){return;}
var context=this._audioContext;var sampleRate=context.sampleRate;var gain=this._gainNode.gain;this._gainNode=null;var ramp=context.createBuffer(1,kReleaseDuration*sampleRate,sampleRate);for(var i=0;i<ramp.length;i++){ramp.getChannelData(0)[i]=(ramp.length-i-1)/ramp.length*kToneVolume*kMasterVolume;}
var rampNode=context.createBufferSource();rampNode.buffer=ramp;rampNode.start();rampNode.connect(gain);gain.setValueAtTime(0.0,0.0);while(this._playingNodes.length){this._playingNodes.pop().stop(context.currentTime+kReleaseDuration+0.5);}},playSequence:function tp_playSequence(sequence){this._ensureAudio();var when=this._audioContext.currentTime+0.2;for(var index=0;index<sequence.length;++index){var step=sequence[index];var frequencies=step.slice(0,2);var duration=step[2]/1000;this._startAt(frequencies,when,duration);when+=duration;}}};

View file

@ -0,0 +1,9 @@
;'use strict';var Utils={prettyDate:function ut_prettyDate(time){var f=Intl.DateTimeFormat(navigator.languages,{hour12:navigator.mozHour12,hour:'numeric',minute:'numeric'});return f.format(new Date(time));},prettyDuration:function(node,duration,l10nPrefix){var elapsed=new Date(duration);var h=elapsed.getUTCHours();var m=elapsed.getUTCMinutes();var s=elapsed.getUTCSeconds();var l10nId=l10nPrefix||'callDuration';var durationL10n={h:h+'',m:m+'',s:s+''};if(l10nId==='callDuration'){durationL10n={h:(h>9?'':'0')+h,m:(m>9?'':'0')+m,s:(s>9?'':'0')+s};}
if(l10nPrefix==='callDurationTextFormat'&&h===0&&m===0){l10nId+='Seconds';}else{l10nId+=h>0?'Hours':'Minutes';}
navigator.mozL10n.setAttributes(node,l10nId,durationL10n);},setHeaderDate:function ut_setHeaderDate(elem,time){var diff=(Date.now()-time)/1000;var day_diff=Math.floor(diff/86400);var formattedTime;if(isNaN(day_diff)){elem.setAttribute('data-l10n-id','incorrectDate');return;}else if(day_diff===0){elem.setAttribute('data-l10n-id','today');return;}else if(day_diff===1){elem.setAttribute('data-l10n-id','yesterday');return;}else if(day_diff<6){formattedTime=(new Date(time)).toLocaleString(navigator.languages,{weekday:'long',});}else{formattedTime=(new Date(time)).toLocaleString(navigator.languages,{year:'2-digit',month:'2-digit',day:'2-digit'});}
elem.removeAttribute('data-l10n-id');elem.textContent=formattedTime;},getPhoneNumberPrimaryInfo:function ut_getPhoneNumberPrimaryInfo(matchingTel,contact){if(contact){if(contact.name&&contact.name.length&&contact.name[0]!==''){return contact.name;}else if(contact.org&&contact.org.length&&contact.org[0]!==''){return contact.org;}}
if(matchingTel){return matchingTel.value;}
return null;},toCamelCase:function ut_toCamelCase(str){return str.replace(/\-(.)/g,function replacer(str,p1){return p1.toUpperCase();});},_phoneTypesL10n:{'mobile':'phone_type_mobile','home':'phone_type_home','work':'phone_type_work','personal':'phone_type_personal','faxHome':'phone_type_fax_home','faxOffice':'phone_type_fax_office','faxOther':'phone_type_fax_other','other':'phone_type_other'},getPhoneNumberAdditionalInfo:function ut_getPhoneNumberAdditionalInfo(matchingTel){var type=matchingTel.type||'mobile';var carrier=matchingTel.carrier;if(Array.isArray(type)){type=type[0];}
var id;var args={};if(this._phoneTypesL10n.hasOwnProperty(type)){id=this._phoneTypesL10n[type];}else{id='phone_type_custom';args.type=type;}
if(carrier){id+='_and_carrier';args.carrier=carrier;}
return{id:id,args:Object.keys(args).length?args:null};},isPhoneType:function ut_isPhoneType(type){return this._phoneTypesL10n.hasOwnProperty(type);}};

View file

@ -0,0 +1,5 @@
;'use strict';(function(exports){var Voicemail={check:function vm_check(number,cardIndex){return new Promise(function(resolve,reject){if(!number){resolve(false);return;}
var serviceId=cardIndex;var voicemail=navigator.mozVoicemail;if(voicemail){var voicemailNumber=voicemail.getNumber(serviceId);if(voicemailNumber===number){resolve(true);return;}}
var settings=navigator.mozSettings;var req=settings.createLock().get('ril.iccInfo.mbdn');req.onsuccess=function getVoicemailNumber(){var isVoicemailNumber=false;var voicemailNumbers=req.result['ril.iccInfo.mbdn'];var voicemailNumber;if(typeof voicemailNumbers==='string'){voicemailNumber=voicemailNumbers;}else{voicemailNumber=voicemailNumbers&&voicemailNumbers[serviceId];}
if(voicemailNumber===number){isVoicemailNumber=true;}
resolve(isVoicemailNumber);};req.onerror=function getVoicemailNumberError(){resolve(false);};});}};exports.Voicemail=Voicemail;}(window));

View file

@ -0,0 +1,423 @@
'use strict';
/* global LazyLoader, TelIndexer */
// Reader Module for FB Data in a Datastore
// To use this library you need to include 'shared/js/fb/fb_request.js'
// WARNING: This file lazy loads 'shared/js/fb/fb_tel_index.js'
var fb = self.fb || {};
self.fb = fb;
(function() {
var contacts = fb.contacts || {};
fb.contacts = contacts;
var datastore;
// Datastore name declared on the manifest.webapp
var DATASTORE_NAME = 'Gaia_Facebook_Friends';
// Record Id for the index
var INDEX_ID = 1;
// Indicates the initialization state
var readyState = 'notInitialized';
// Custom event for notifying initializations
var INITIALIZE_EVENT = 'fb_ds_init';
// Last known revision Id. Used to determine whether the index has to
// be reloaded or not
var revisionId;
// Creates the internal Object in the datastore that will act as an index
function createIndex() {
return {
// By tel number and all its possible variants
// (We are not supporting dups right now)
byTel: Object.create(null),
// Prefix tree for enabling searching by partial tel numbers
treeTel: []
};
}
// The index we need to keep the correspondance between FB Friends and
// datastore Ids
var index;
// Ensures a proper error object is returned
function safeError(err) {
if (err && err.name) {
return err;
}
return {
name: 'UnknownError'
};
}
function notifyOpenSuccess(cb) {
readyState = 'initialized';
if (typeof cb === 'function') {
window.setTimeout(cb, 0);
}
var ev = new CustomEvent(INITIALIZE_EVENT);
document.dispatchEvent(ev);
}
function initError(outRequest, error) {
outRequest.failed(error);
}
function handleInitError(err, errorCb) {
readyState = 'error';
contacts.error = err;
if (typeof errorCb === 'function') {
window.setTimeout(function() {
errorCb(err);
}, 0);
}
// We resume other callers state
document.dispatchEvent(new CustomEvent(INITIALIZE_EVENT));
}
// Creates a default handler for errors
function defaultError(request) {
return defaultErrorCb.bind(null, request);
}
// Creates a default handler for success
function defaultErrorCb(request, error) {
request.failed(error);
}
function setIndex(obj) {
index = (obj || createIndex());
}
function successGet(outReq, data) {
outReq.done(data);
}
function errorGet(outReq, uid, err) {
window.console.error('Error while getting object for UID: ', uid);
outReq.failed(err.name);
}
Object.defineProperty(contacts, 'datastore', {
get: function getDataStore() { return datastore; },
enumerable: false,
configurable: false
});
Object.defineProperty(contacts, 'dsIndex', {
get: function getIndex() { return index; },
set: setIndex,
enumerable: false,
configurable: false
});
/**
* Allows to obtain the FB friend information by UID
*
*
*/
contacts.get = function(uid) {
var outRequest = new fb.utils.Request();
window.setTimeout(function get() {
contacts.init(function() {
doGet(uid, outRequest);
}, function(err) {
initError(outRequest, err);
});
}, 0);
return outRequest;
};
function doGet(uid, outRequest) {
var successCb = successGet.bind(null, outRequest);
var errorCb = errorGet.bind(null, outRequest, uid);
datastore.get(uid).then(successCb, errorCb);
}
/**
* Allows to get FB information by tel number (short or long)
* The tel number must be complete
*
*/
contacts.getByPhone = function(tel) {
var outRequest = new fb.utils.Request();
window.setTimeout(function get_by_phone() {
contacts.init(function get_by_phone() {
doGetByPhone(tel, outRequest);
},
function(err) {
initError(outRequest, err);
});
}, 0);
return outRequest;
};
function doGetByPhone(tel, outRequest) {
var dsId;
var normalizedNumber = navigator.mozPhoneNumberService.normalize(tel);
if (datastore.revisionId !== revisionId) {
window.console.info('Datastore revision id has changed!');
// Refreshing the index just in case
datastore.get(INDEX_ID).then(function success(obj) {
setIndex(obj);
revisionId = datastore.revisionId;
dsId = index.byTel[normalizedNumber];
if (typeof dsId !== 'undefined') {
datastore.get(dsId).then(function success(friend) {
outRequest.done(friend);
}, defaultError(outRequest));
}
else {
outRequest.done(null);
}
}, function(err) {
err = safeError(err);
window.console.error('The index cannot be refreshed: ', err.name);
outRequest.failed(err);
});
}
else {
dsId = index.byTel[normalizedNumber];
if (typeof dsId !== 'undefined') {
datastore.get(dsId).then(function success(friend) {
outRequest.done(friend);
}, defaultError(outRequest));
}
else {
outRequest.done(null);
}
}
}
/**
* Allows to search for friends by different criteria.
* Numbers can be partial and partial matchings will be found
*
* by === tel : By Tel number
*
*/
contacts.search = function(by, number) {
var outRequest = new fb.utils.Request();
window.setTimeout(function do_search() {
contacts.init(function do_search_init() {
if (by === 'phone') {
doSearchByPhone(number, outRequest);
}
},
function(err) {
initError(outRequest, err);
});
}, 0);
return outRequest;
};
function doSearchByPhone(number, outRequest) {
var normalizedNumber = navigator.mozPhoneNumberService.normalize(number);
LazyLoader.load(['/shared/js/fb/fb_tel_index.js',
'/shared/js/binary_search.js'], function() {
var toSearchNumber = normalizedNumber;
// TODO: Temporal way of searching for international numbers
// A follow-up is needed by using PhoneNumber.js exposed to Gaia
if (number.charAt(0) === '+') {
toSearchNumber = number.substring(1);
}
if (datastore.revisionId !== revisionId) {
window.console.info('Datastore revision id has changed!');
// Refreshing the index
datastore.get(INDEX_ID).then(function success(obj) {
setIndex(obj);
revisionId = datastore.revisionId;
var results = TelIndexer.search(index.treeTel, toSearchNumber);
var out = null;
if (results.length > 0) {
out = datastore.get.apply(datastore, results);
}
return out;
},function(err) {
err = safeError(err);
window.console.error('The index cannot be refreshed: ', err.name);
outRequest.failed(err);
}).then(function success(objList) {
if (objList && !Array.isArray(objList)) {
objList = [objList];
}
else {
objList = [];
}
outRequest.done(objList);
}, function error(err) {
err = safeError(err);
window.console.error('Error while retrieving result data: ',
err.name);
outRequest.failed(err);
});
}
else {
var results = TelIndexer.search(index.treeTel, toSearchNumber);
if (results.length > 0) {
datastore.get.apply(datastore, results).then(
function success(objList) {
if (!Array.isArray(objList)) {
objList = [objList];
}
outRequest.done(objList);
}, function error(err) {
err = safeError(err);
window.console.error('Error while retrieving result data: ',
err.name);
outRequest.failed(err);
});
}
else {
outRequest.done(results);
}
}
});
}
/**
* Refreshes the index data
*
*/
contacts.refresh = function() {
var outRequest = new fb.utils.Request();
window.setTimeout(function clear() {
contacts.init(function() {
doRefresh(outRequest);
},
function(err) {
initError(outRequest, err);
});
}, 0);
return outRequest;
};
function doRefresh(outRequest) {
datastore.get(INDEX_ID).then(function success(obj) {
setIndex(obj);
outRequest.done();
}, defaultError(outRequest));
}
/**
* Allows to obtain how many records contain he FB Database
*
*/
contacts.getLength = function() {
var retRequest = new fb.utils.Request();
window.setTimeout(function() {
contacts.init(function get_all() {
doGetLength(retRequest);
},
function(err) {
initError(retRequest, err);
});
}, 0);
return retRequest;
};
/**
* Allows to return the total number of records in the DataStore (minus 1)
* That's because the index object also counts
*
*/
function doGetLength(outRequest) {
datastore.getLength().then(function success(length) {
outRequest.done(length - 1);
},
function error(err) {
outRequest.failed(safeError(err));
});
}
// Needed only for testing purposes
contacts.restart = function() {
readyState = 'notInitialized';
};
/**
* Initialization function
*
*/
contacts.init = function(cb, errorCb) {
if (!navigator.getDataStores) {
handleInitError({ name: 'DatastoreNotFound' }, errorCb);
return;
}
if (readyState === 'initialized') {
cb();
return;
}
if (readyState === 'initializing') {
document.addEventListener(INITIALIZE_EVENT, function oninitalized() {
cb();
document.removeEventListener(INITIALIZE_EVENT, oninitalized);
});
return;
}
readyState = 'initializing';
navigator.getDataStores(DATASTORE_NAME).then(function success(ds) {
if (ds.length < 1) {
handleInitError({ name: 'DatastoreNotFound' }, errorCb);
return;
}
datastore = ds[0];
// Checking the length as the index should be there
datastore.getLength().then(function(length) {
if (length === 0 && datastore.readOnly === false) {
window.console.info('Adding index as length is 0');
setIndex(createIndex());
return datastore.add(index);
}
else if (length > 0) {
return datastore.get(INDEX_ID);
}
else if (datastore.readOnly === true) {
window.console.warn('The datastore is empty and readonly');
}
return null;
}).then(function add_index_success(v) {
// Index is created in order not to cause errors
if (typeof v !== 'number') {
setIndex(v);
}
revisionId = datastore.revisionId;
notifyOpenSuccess(cb);
}, function add_index_error(err) {
err = safeError(err);
window.console.error('Error while setting the index: ', err.name);
handleInitError(err, errorCb);
});
}, function error(err) {
err = safeError(err);
window.console.error('FB: Error while opening the DataStore: ',
err.name);
handleInitError(err, errorCb);
});
};
})();

View file

@ -0,0 +1,23 @@
;;'use strict';var fb=self.fb||{};self.fb=fb;if(!self.AuxFB){self.AuxFb=(function(){var CATEGORY='facebook';var NOT_LINKED='not_linked';var LINKED='fb_linked';function populate(source,target,propertyNames){propertyNames.forEach(function(property){var propertyValue=source[property];if(propertyValue){if(Array.isArray(propertyValue)){target[property]=propertyValue.slice(0,propertyValue.length);}else{target[property]=propertyValue;}}});}
function mergeFbData(dcontact,fbdata){var multipleFields=['email','tel','photo','org','adr'];multipleFields.forEach(function(field){if(!dcontact[field]){dcontact[field]=[];}
var items=fbdata[field];if(items){items.forEach(function(item){var dupList=checkDuplicates(field,item,dcontact[field],fbdata.shortTelephone);if(dupList.length===0){dcontact[field].push(item);}});}});var singleFields=['bday','anniversary'];singleFields.forEach(function(field){if(!dcontact[field]){dcontact[field]=fbdata[field];}});mergeNames(dcontact,fbdata);}
function mergeNames(devContact,fbContact){var namesChanged=false;var nameFields=['givenName','familyName'];nameFields.forEach(function(anameField){var fieldValue=devContact[anameField];var fbValue=fbContact[anameField];if((!Array.isArray(fieldValue)||fieldValue.length===0)&&Array.isArray(fbValue)&&fbValue.length>0){namesChanged=true;devContact[anameField]=(fieldValue&&fieldValue[0])||[];devContact[anameField].push(fbValue[0]);}});if(namesChanged){var givenName=devContact.givenName[0]||'';var familyName=devContact.familyName[0]||'';devContact.name=[givenName+' '+familyName];}}
function isFbContact(devContact){return(devContact&&devContact.category&&devContact.category.indexOf(CATEGORY)!==-1);}
function isFbLinked(devContact){return(devContact&&devContact.category&&devContact.category.indexOf(LINKED)!==-1);}
function getData(devContact){var outReq=new fb.utils.Request();window.setTimeout(function do_getData(){var uid=getFriendUid(devContact);if(uid){var fbreq=fb.contacts.get(uid);fbreq.onsuccess=function(){var fbdata=fbreq.result;var out=mergeContact(devContact,fbdata);outReq.done(out);};fbreq.onerror=function(){outReq.failed(fbreq.error);};}
else{outReq.done(devContact);}},0);return outReq;}
function getFriendUid(devContact){var out=devContact.uid;if(!out){if(isFbLinked(devContact)){out=getLinkedTo(devContact);}
else if(devContact.category){var idx=devContact.category.indexOf(CATEGORY);if(idx!==-1){out=devContact.category[idx+2];}}}
return out;}
function getLinkedTo(devContact){var out;if(devContact.category){var idx=devContact.category.indexOf(LINKED);if(idx!==-1){out=devContact.category[idx+1];}}
return out;}
function mergeContact(devContact,fbContact){var out=devContact;if(fbContact){out=Object.create(null);out.updated=devContact.updated;out.published=devContact.published;populate(devContact,out,Object.getOwnPropertyNames(devContact));populate(devContact,out,Object.getOwnPropertyNames(Object.getPrototypeOf(devContact)));mergeFbData(out,fbContact);}
return out;}
function getContactByNumber(number,onsuccess,onerror){var req=fb.contacts.getByPhone(number);req.onsuccess=function get_by_phone_success(e){var fbData=req.result;if(fbData){fb.getMozContactByUid(fbData.uid,function merge(result){if(Array.isArray(result)&&result[0]){var finalContact=fb.mergeContact(result[0],fbData);onsuccess(finalContact);}
else{onsuccess(null);}},function error_get_mozContact(err){console.error('Error getting mozContact: ',err.name);onerror(err);});}
else{onsuccess(null);}};req.onerror=function(){onerror(req.error);};}
function checkDuplicates(field,fbItem,devContactItems,extraFbItems){var potentialDuplicatesFields=['email','tel'];var out=[];if(devContactItems&&potentialDuplicatesFields.indexOf(field)!==-1){var total=devContactItems.length;for(var i=0;i<total;i++){var localValue=devContactItems[i].value;var fbValue=fbItem.value;if(localValue){var trimedLocal=localValue.trim();if(trimedLocal===fbValue||(field==='tel'&&Array.isArray(extraFbItems)&&extraFbItems.indexOf(trimedLocal)!==-1)){out.push(trimedLocal);out.push(fbValue);}}}}
return out;}
function getMozContactByUid(uid,onsuccess,onerror){var filter={filterBy:['category'],filterValue:uid,filterOp:'contains'};var req=navigator.mozContacts.find(filter);req.onsuccess=function(){onsuccess(req.result);};req.onerror=function(){onerror(req.error);};}
return{'isFbContact':isFbContact,'isFbLinked':isFbLinked,'getData':getData,'getFriendUid':getFriendUid,'getLinkedTo':getLinkedTo,'mergeContact':mergeContact,'getContactByNumber':getContactByNumber,'getMozContactByUid':getMozContactByUid,'checkDuplicates':checkDuplicates,get CATEGORY(){return CATEGORY;},get LINKED(){return LINKED;},get NOT_LINKED(){return NOT_LINKED;}};})();var props=Object.keys(self.AuxFb);for(var j=0,end=props.length;j<end;j++){var prop=props[j];if(typeof self.fb[prop]==='function'){self.fb[prop]=self.AuxFb[prop];}
else{Object.defineProperty(self.fb,prop,{value:(function(){return self.AuxFb[prop];})(),writable:false,enumerable:true,configurable:false});}}}

View file

@ -0,0 +1 @@
;;'use strict';var fb=self.fb||{};self.fb=fb;fb.utils=self.fb.utils||{};self.fb.utils=fb.utils;if(typeof fb.utils.Request!=='function'){fb.utils.Request=function(){this.done=function(result){this.result=result;if(typeof this.onsuccess==='function'){var ev={};ev.target=this;window.setTimeout(function(){this.onsuccess(ev);}.bind(this),0);}};this.failed=function(error){this.error=error;if(typeof this.onerror==='function'){var ev={};ev.target=this;window.setTimeout(function(){this.onerror(ev);}.bind(this),0);}};};}

View file

@ -0,0 +1,279 @@
'use strict';
/* globals utils */
/* exported TelIndexer */
// This object implements a prefix tree (aka trie) for FB tel numbers
function TrieNode(number) {
this.keys = Object.create(null);
this.leaves = null;
this.value = number;
}
var TelIndexer = {
_MIN_TEL_LENGTH: 3,
_INSERT_MODE: 'insert',
_indexOf: function(array, value, byPrefix) {
var out = -1;
if (!Array.isArray(array)) {
return out;
}
for (var j = 0, l = array.length; j < l; j++) {
if (array[j].value === value || (byPrefix &&
array[j].value.startsWith(value))) {
out = j;
break;
}
}
return out;
},
_getStringVariants: function(number) {
var out = [];
var limit = number.length - this._MIN_TEL_LENGTH + 1;
for (var j = 0; j < limit; j++) {
out.push(number.substr(j));
}
return out;
},
// Allows to index the number passed as parameter
index: function(tree, number, dsId) {
var self = this;
var variants = this._getStringVariants(number);
variants.forEach(function(aVariant) {
self._insert(tree, aVariant, dsId);
});
},
// Inserts a substring in a node. dsId is the object id (uid) associated
_insertInNode: function(node, str, dsId) {
if (str.length === 0) {
return;
}
var currentStr = str;
var insertPointFound = false;
var insertObj;
var pointerOriginal = str.length;
while (!insertPointFound && currentStr.length > 0) {
var inextObj = this._indexOf(node.leaves, currentStr, 'prefix');
if (inextObj !== -1) {
insertPointFound = true;
insertObj = node.leaves[inextObj];
}
else {
currentStr = currentStr.substring(0, currentStr.length - 1);
}
pointerOriginal--;
}
if (insertPointFound) {
if (insertObj.value === str) {
// Exact insertion point
insertObj.keys[dsId] = true;
}
else {
var keys = Object.keys(insertObj.keys);
// Let's check what's the gap
var newString = insertObj.value.substring(0, currentStr.length);
var restString = insertObj.value.substring(currentStr.length);
insertObj.value = newString;
if (restString.length > 0) {
var newNode = new TrieNode(restString);
for (var j = 0; j < keys.length; j++) {
newNode.keys[keys[j]] = true;
}
if (Array.isArray(insertObj.leaves)) {
newNode.leaves = [];
insertObj.leaves.forEach(function(aLeave) {
newNode.leaves.push(aLeave);
});
}
insertObj.leaves = [];
insertObj.leaves.push(newNode);
}
insertObj.keys[dsId] = true;
var restOriginal = str.substring(pointerOriginal + 1);
// And the rest of the original string is inserted in the insertObj
this._insertInNode(insertObj, restOriginal, dsId);
}
}
else {
var nextObj = new TrieNode(str);
nextObj.keys[dsId] = true;
node.leaves = node.leaves || [];
node.leaves.push(nextObj);
}
},
// Compare function used for binary search
_compare: function(a, b) {
return a.localeCompare(b);
},
// Find the first level node (corresponding to the 3 first numbers)
_getFirstLevelNode: function(tree, str, mode) {
var out = null;
var index;
if (mode === this._INSERT_MODE) {
index = this._indexOf(tree, str);
if (index !== -1) {
out = tree[index];
}
}
else {
index = utils.binarySearch(str, tree, {
compareFunction: this._compare,
arrayField: 'value'
});
if (index.length > 0) {
out = tree[index[0]];
}
}
return out;
},
// Orders the tree enabling binary search on the first levels
orderTree: function(tree) {
tree.sort(function(a, b) {
return a.value.localeCompare(b.value);
});
},
// Inserts a number on the tree
_insert: function(tree, str, dsId) {
var firstThreeStr = str.substring(0, this._MIN_TEL_LENGTH);
var rootObj = this._getFirstLevelNode(tree, firstThreeStr,
this._INSERT_MODE);
if (!rootObj) {
rootObj = new TrieNode(firstThreeStr);
tree.push(rootObj);
}
rootObj.keys[dsId] = true;
this._insertInNode(rootObj, str.substring(this._MIN_TEL_LENGTH), dsId);
},
// Search a substring in a node
_searchInNode: function(node, str) {
var currentStr = str;
var found = false;
var pointerOriginal = str.length;
var searchObj;
while (!found && currentStr.length > 0) {
var idxObj = this._indexOf(node.leaves, currentStr, 'prefix');
if (idxObj !== -1) {
found = true;
searchObj = node.leaves[idxObj];
}
else {
currentStr = currentStr.substring(0, currentStr.length - 1);
}
pointerOriginal--;
}
if (found) {
if (searchObj.value === str) {
return searchObj;
}
// Let's calculate what is the gap
var restString = str.substr(pointerOriginal + 1);
if (restString.length > 0) {
return this._searchInNode(searchObj, restString);
}
else {
return searchObj;
}
}
else {
return null;
}
},
// Search for a number (which can be partial) on the tree
search: function(tree, number) {
var out = [];
var totalLength = number.length;
var MIN_TEL_LENGTH = this._MIN_TEL_LENGTH;
if (totalLength >= MIN_TEL_LENGTH) {
var firstThreeStr = number.substring(0, MIN_TEL_LENGTH);
var rootObj = this._getFirstLevelNode(tree, firstThreeStr);
if (rootObj) {
if (totalLength === MIN_TEL_LENGTH) {
out = Object.keys(rootObj.keys);
}
else {
var node = this._searchInNode(rootObj,
number.substring(MIN_TEL_LENGTH));
if (node) {
out = Object.keys(node.keys);
}
}
}
}
return out;
},
// Removes a number from the tree
remove: function(tree, number, dsId) {
var self = this;
var variants = this._getStringVariants(number);
variants.forEach(function(aVariant) {
self._unIndex(tree, aVariant, dsId);
});
},
// Removes a partial string from a node
_removeInNode: function(node, str, dsId) {
if (str.length === 0) {
return;
}
var currentStr = str;
var found = false;
var pointerOriginal = str.length;
var searchObj;
while (!found && currentStr.length > 0) {
var idxObj = this._indexOf(node.leaves, currentStr, 'prefix');
if (idxObj !== -1) {
found = true;
searchObj = node.leaves[idxObj];
}
else {
currentStr = currentStr.substring(0, currentStr.length - 1);
}
pointerOriginal--;
}
if (found) {
delete searchObj.keys[dsId];
this._removeInNode(searchObj, str.substr(pointerOriginal + 1), dsId);
}
},
// Removes a number from the tree
// TODO: Compact the tree when a number is removed
_unIndex: function(tree, number, dsId) {
var firstThreeStr = number.substring(0, this._MIN_TEL_LENGTH);
var rootObj = this._getFirstLevelNode(tree, firstThreeStr);
if (rootObj) {
delete rootObj.keys[dsId];
this._removeInNode(rootObj, number.substr(this._MIN_TEL_LENGTH), dsId);
}
}
};

View file

@ -0,0 +1,8 @@
;(function(exports){'use strict';var FontSizeUtils={_cachedContexts:{},_getCachedContext:function(fontSize,fontFamily,fontStyle){fontStyle=fontStyle||'italic';var cache=this._cachedContexts;var ctx=cache[fontSize]&&cache[fontSize][fontFamily]?cache[fontSize][fontFamily][fontStyle]:null;if(!ctx){var canvas=document.createElement('canvas');canvas.setAttribute('moz-opaque','true');canvas.setAttribute('width','1');canvas.setAttribute('height','1');ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.font=fontStyle+' '+fontSize+'px '+fontFamily;if(!cache[fontSize]){cache[fontSize]={};}
if(!cache[fontSize][fontFamily]){cache[fontSize][fontFamily]={};}
cache[fontSize][fontFamily][fontStyle]=ctx;}
return ctx;},resetCache:function(){this._cachedContexts={};},getFontWidth:function(string,fontSize,fontFamily,fontStyle){var ctx=this._getCachedContext(fontSize,fontFamily,fontStyle);return ctx.measureText(string).width;},getMaxFontSizeInfo:function(string,allowedSizes,fontFamily,maxWidth){var fontSize;var resultWidth;var i=allowedSizes.length-1;do{fontSize=allowedSizes[i];resultWidth=this.getFontWidth(string,fontSize,fontFamily);i--;}while(resultWidth>maxWidth&&i>=0);return{fontSize:fontSize,overflow:resultWidth>maxWidth,textWidth:resultWidth};},_overflowCountCache:-1,getOverflowCount:function(string,fontSize,fontFamily,maxWidth){var substring;var resultWidth;var overflowCount=-1;if(string.length>this._overflowCountCache){substring=string.substr(0,string.length-this._overflowCountCache);resultWidth=this.getFontWidth(substring,fontSize,fontFamily);if(resultWidth>maxWidth){overflowCount=this._overflowCountCache;}}
do{overflowCount++;substring=string.substr(0,string.length-overflowCount);resultWidth=this.getFontWidth(substring,fontSize,fontFamily);}while(substring.length>0&&resultWidth>maxWidth);this._overflowCountCache=overflowCount;return overflowCount;},getContentWidth:function(style){var width=parseInt(style.width,10);if(style.boxSizing==='border-box'){width-=(parseInt(style.paddingRight,10)+
parseInt(style.paddingLeft,10));}
return width;},getStyleProperties:function(element){var style=window.getComputedStyle(element);var contentWidth=this.getContentWidth(style);if(isNaN(contentWidth)){contentWidth=0;}
return{fontFamily:style.fontFamily,contentWidth:contentWidth,paddingRight:parseInt(style.paddingRight,10),paddingLeft:parseInt(style.paddingLeft,10),offsetLeft:element.offsetLeft};},getWindowWidth:function(){return window.innerWidth;}};exports.FontSizeUtils=FontSizeUtils;}(this));

View file

@ -0,0 +1,4 @@
;;'use strict';var HtmlImports={populate:function(callback){var imports=document.querySelectorAll('link[rel="import"]');if(!imports.length){return;}
var pending=imports.length;Array.prototype.forEach.call(imports,function perImport(eachImport){this.getImportContent(eachImport.href,function gotContent(content){var elementTemplates={};var elementRoot=document.createElement('div');elementRoot.innerHTML=content;var elements=elementRoot.querySelectorAll('element');for(var i=0,iLen=elements.length;i<iLen;i++){var element=elements[i];var template=element.querySelector('template');elementTemplates[element.getAttribute('name')]=template.innerHTML;}
var replaceableElements=document.querySelectorAll('*[is]');Array.prototype.forEach.call(replaceableElements,function _each(el){if(elementTemplates[el.getAttribute('is')]){el.innerHTML=elementTemplates[el.getAttribute('is')];el.removeAttribute('is');}});if(!(--pending)){callback();}});},this);},getImportContent:function(path,callback){var parsedURL=new URL(path,location.href);if(parsedURL.origin!==location.origin){return;}
var xhr=new XMLHttpRequest();xhr.onload=function(o){callback(xhr.responseText);};xhr.open('GET',path,true);xhr.send();}};

View file

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

View file

@ -0,0 +1,572 @@
/*
* This shared module defines a single ImageUtils object in the global scope.
*
* ImageUtils.getSizeAndType() takes a Blob as its argument and
* returns a Promise. If the promise resolves, the resolve method is
* passed an object with width, height, and type properties. Width and
* height specify the width and height of the image in pixels. Type
* specifies the MIME type of the image. If the promise is rejected,
* the reject method will be passed a string error message.
*
* Supported image types are JPEG, PNG, GIF and BMP. The MIME type
* strings returned when a promise resolves are "image/jpeg",
* "image/png", "image/gif", and "image/bmp".
*
* If you pass a blob that holds an image of some other type (or a
* blob that is not an image at all) then promise will be rejected.
*
* getSizeAndType() does not decode the image to determine its size
* because can require large amounts of memory. Instead, it parses the
* image file to determine its size and typically only needs to read a
* prefix of the file into memory.
*
* getSizeAndType() differs from the getImageSize() function defined
* in shared/js/media/immage_size.js. That function is similar but
* also parses EXIF metadata for JPEG images and is only suitable if
* you need to access EXIF data.
*/
(function(exports) {
'use strict';
/* global Promise */
var ImageUtils = exports.ImageUtils = {};
// Define constants for the MIME types we use
const JPEG = 'image/jpeg';
const PNG = 'image/png';
const GIF = 'image/gif';
const BMP = 'image/bmp';
// Export them
ImageUtils.JPEG = JPEG;
ImageUtils.PNG = PNG;
ImageUtils.GIF = GIF;
ImageUtils.BMP = BMP;
ImageUtils.getSizeAndType = function getSizeAndType(imageBlob) {
if (!(imageBlob instanceof Blob)) {
console.log('not blob');
return Promise.reject(new TypeError('argument is not a Blob'));
}
return new Promise(function(resolve, reject) {
if (imageBlob.size <= 16) {
reject('corrupt image file');
return;
}
// Start off assuming that we'll read the first 32kb of the file
var bytesToRead = 32 * 1024;
// But if the blob indicates that this is a PNG, GIF, or BMP, then
// we can read less since the size information is always close to the
// start of the file for those formats.
if (imageBlob.type === PNG ||
imageBlob.type === GIF ||
imageBlob.type === BMP) {
bytesToRead = 512;
}
// Try to find the information we want in the first bytes of the file
findSizeAndType(imageBlob, bytesToRead, success, tryagain);
// If we got the size and type data, resolve the promise
function success(data) {
resolve(data);
}
// If we didn't find it, try again for JPEG files
function tryagain(data) {
// If we were able to verify that this blob is a jpeg file
// then we will try again because in JPEG files, the size
// comes after the EXIF data so if there is a large preview image,
// the size might be more than 32kb into the file
if (data.type === JPEG) {
findSizeAndType(imageBlob, imageBlob.size, success, failure);
}
// For all other known image types the type and size data is
// near the start of the file, if we didn't find it on this first
// try we need to give up now.
else {
reject(data.error);
}
}
// If we didn't find the data even after a second attempt then reject.
function failure(data) {
reject(data.error);
}
});
// This is the internal helper function that actually reads the blob
// and finds type and size information.
function findSizeAndType(imageBlob, amountToRead, success, failure) {
var slice = imageBlob.slice(0, Math.min(amountToRead, imageBlob.size));
var reader = new FileReader();
reader.readAsArrayBuffer(slice);
reader.onloadend = function() {
parseImageData(reader.result);
};
function parseImageData(buffer) {
var header = new Uint8Array(buffer, 0, 16);
var view = new DataView(buffer);
if (header[0] === 0x89 &&
header[1] === 0x50 && /* P */
header[2] === 0x4e && /* N */
header[3] === 0x47 && /* G */
header[4] === 0x0d && /* \r */
header[5] === 0x0a && /* \n */
header[6] === 0x1A &&
header[7] === 0x0a &&
header[12] === 0x49 && /* I */
header[13] === 0x48 && /* H */
header[14] === 0x44 && /* D */
header[15] === 0x52) /* R */
{
// This is a PNG file
try {
success({
type: PNG,
width: view.getUint32(16, false), // 32 bit big endian width
height: view.getUint32(20, false) // 32 bit big endian height
});
}
catch (ex) {
failure({ error: ex.toString()});
}
}
else if (header[0] === 0x47 && /* G */
header[1] === 0x49 && /* I */
header[2] === 0x46 && /* F */
header[3] === 0x38 && /* 8 */
(header[4] === 0x37 || /* 7 */
header[4] === 0x39) && /* or 9 */
header[5] === 0x61) /* a */
{
// This is a GIF file
try {
success({
type: GIF,
width: view.getUint16(6, true), // 16-bit little endian width
height: view.getUint16(8, true) // 16-big little endian height
});
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else if (header[0] === 0x42 && /* B */
header[1] === 0x4D && /* M */
view.getUint32(2, true) === imageBlob.size)
{
// This is a BMP file
try {
var width, height;
if (view.getUint16(14, true) === 12) { // check format version
width = view.getUint16(18, true); // 16-bit little endian width
height = view.getUint16(20, true); // 16-bit little endian height
}
else { // newer versions of the format use 32-bit ints
width = view.getUint32(18, true); // 32-bit little endian width
height = view.getUint32(22, true); // 32-bit little endian height
}
success({
type: BMP,
width: width,
height: height
});
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else if (header[0] === 0xFF &&
header[1] === 0xD8) {
var value = {
type: JPEG
};
// This is a JPEG file. To find its width and height we need
// to skip past the EXIF data and find the start of image data.
try {
var offset = 2;
// Loop through the segments of the file until we find an SOF
// segment with image size or until we reach the end of our data.
for (;;) {
// The byte at the current offset should be 0xFF marking
// the start of a new segment.
if (view.getUint8(offset) !== 0xFF) {
failure({ error: 'corrupt JPEG file' });
}
var segmentType = view.getUint8(offset + 1);
var segmentSize = view.getUint16(offset + 2) + 2;
// If we found an SOF "Start of Frame" segment, we can get
// the image size from it.
if ((segmentType >= 0xC0 && segmentType <= 0xC3) ||
(segmentType >= 0xC5 && segmentType <= 0xC7) ||
(segmentType >= 0xC9 && segmentType <= 0xCB) ||
(segmentType >= 0xCD && segmentType <= 0xCF))
{
// 16-bit big-endian dimensions, height first
value.height = view.getUint16(offset + 5, false);
value.width = view.getUint16(offset + 7, false);
success(value);
break;
}
// Otherwise, move on to the next segment
offset += segmentSize;
// We may not have read the entire file into our array buffer
// so we have to make sure we have not gone past the end of
// the buffer before looping again. Note that in this case
// the object we pass to the failure callback includes the
// image type. This is a signal to getSizeAndType that it
// should try again with a larger ArrayBuffer.
if (offset + 9 > view.byteLength) {
value.error = 'corrupt JPEG file';
failure(value);
break;
}
}
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else {
failure({ error: 'unknown image type' });
}
}
}
};
//
// Given a blob containing an image file, this function returns a
// Promise that will resolve to a blob for an image file that has
// the specified width and height. If the blob already has that
// size, it is returned unchanged. If the image is bigger than the
// specified size, then it will be shrunk and cropped as necessary
// to fit the specified dimensions. If the image is smaller than
// the specified size, it will be enlarged and cropped to fit. The
// cropping is done in the same way as with the CSS
// background-size:cover attribute.
//
// If the image is resized and outputType is "image/jpeg" or
// "image/png" then that type will be used for the resized image. If
// a JPEG or PNG image is resized and outputType is not specified,
// then the output image will have the same type as the input image.
// In all other cases, a resized image will be encoded as a PNG image.
// Note that when an image does not need to be resized its type is not
// changed, even if outputType is specified.
// 'encoderOptions' is passed 'as is' to the 'toBlob' method of canvas:
// A Number between 0 and 1 indicating image quality if the requested type
// is image/jpeg or image/webp. If this argument is anything else,
// the default value for image quality is used.
ImageUtils.resizeAndCropToCover = function(inputImageBlob,
outputWidth, outputHeight,
outputType, encoderOptions)
{
if (!outputWidth || !isFinite(outputWidth) || outputWidth <= 0 ||
!outputHeight || !isFinite(outputHeight) || outputHeight <= 0) {
return Promise.reject(new TypeError('invalid output dimensions'));
}
outputWidth = Math.round(outputWidth);
outputHeight = Math.round(outputHeight);
return ImageUtils.getSizeAndType(inputImageBlob).then(
function resolve(data) {
var inputWidth = data.width;
var inputHeight = data.height;
// If the image already has the desired size, just return it unchanged
if (inputWidth === outputWidth && inputHeight === outputHeight) {
return inputImageBlob;
}
// Otherwise, return a promise for the resized image
return resize(data);
},
function reject(error) {
// If we couldn't determine the image size and type for some
// reason we are still going to assume that it is a valid image,
// just one we don't know about. If gecko can decode it we'll
// use it and won't reject unless gecko refuses it.
return resize({});
}
);
// Decode the image in an <img> element, resize and/or crop it into a
// <canvas> and the re-encode it to a blob again. Returns a Promise
// that resolves to the encoded blob.
function resize(data) {
var inputType = data.type;
var inputWidth = data.width;
var inputHeight = data.height;
// Ignore invalid outputType parameters
if (outputType && outputType !== JPEG && outputType !== PNG) {
console.warn('Ignoring unsupported outputType', outputType);
outputType = undefined;
}
// Determine the output type if none was specified.
if (!outputType) {
if (inputType === JPEG || inputType === PNG) {
outputType = inputType;
}
else {
outputType = PNG;
}
}
// Get a URL for the image blob so we can ask gecko to decode it.
var url = URL.createObjectURL(inputImageBlob);
// If we can downsample the image while decoding it, then add a
// #-moz-samplesize media fragment to the URL. For large JPEG inputs
// this can save megabytes of memory.
var mediaFragment;
if (inputType === JPEG &&
inputWidth > outputWidth && inputHeight > outputHeight) {
// How much would we like to scale the image by while decoding?
var reduction = Math.max(outputWidth / inputWidth,
outputHeight / inputHeight);
// Get a media fragment that will downsample the image by up to
// this amount.
mediaFragment = ImageUtils.Downsample.sizeNoMoreThan(reduction);
}
else {
mediaFragment = '';
}
// The next step is to decode (while possibly downsampling) the
// image, but that is asynchronous, so we'll have to return a
// Promise to handle that
return new Promise(function(resolve, reject) {
var offscreenImage = new Image();
offscreenImage.src = url + mediaFragment;
offscreenImage.onerror = function(e) {
cleanupImage();
reject('failed to decode image');
};
offscreenImage.onload = function() {
// The image has been decoded now, so we know its actual size.
// If getSizeAndType failed, or if we used a media fragment then
// this may be different than the inputWidth and inputHeight
// values we used previously.
var actualWidth = offscreenImage.width;
var actualHeight = offscreenImage.height;
// Now figure how much we have to scale the decoded image down
// (or up) so that it covers the specified output dimensions.
var widthScale = outputWidth / actualWidth;
var heightScale = outputHeight / actualHeight;
var scale = Math.max(widthScale, heightScale);
// Scaling the output dimensions by this much tells us the
// dimensions of the crop area on the decoded image
var cropWidth = Math.round(outputWidth / scale);
var cropHeight = Math.round(outputHeight / scale);
// Now center that crop region within the decoded image
var cropLeft = Math.floor((actualWidth - cropWidth) / 2);
var cropTop = Math.floor((actualHeight - cropHeight) / 2);
// Set up the canvas we need to copy the crop region into
var canvas = document.createElement('canvas');
canvas.width = outputWidth;
canvas.height = outputHeight;
var context = canvas.getContext('2d', { willReadFrequently: true });
// Copy the image into the canvas
context.drawImage(offscreenImage,
cropLeft, cropTop, cropWidth, cropHeight,
0, 0, outputWidth, outputHeight);
// We're done with the image now: try to release the decoded
// image memory as fast as we can
cleanupImage();
// Now encode the pixels in the canvas as an image blob
canvas.toBlob(function(blob) {
// We've got the encoded image in the blob, so we don't need
// the pixels in the canvas anymore. Try to release that memory
// right away, too.
canvas.width = 0;
resolve(blob);
}, outputType, encoderOptions);
};
function cleanupImage() {
offscreenImage.onerror = offscreenImage.onload = '';
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
URL.revokeObjectURL(url);
}
});
}
};
//
// This module defines a Downsample object with static
// methods that return objects representing media fragments for
// downsampling images while they are decoded. The current implementation
// is based on the #-moz-samplesize media fragment. But because of
// problems with that fragment (see bug 1004908) it seems likely that a
// new syntax or new fragment will be introduced. If that happens, we can
// just change this module and not have to change anything else that
// depends on it.
//
// The method Downsample.areaAtLeast(scale) returns an object
// representing a media fragment to use to decode an image downsampled by
// at least as much as the specified scale. If you are trying to preview
// an 8mp image and don't want to use more than 2mp of image memory, for
// example, you would pass a scale of .25 (2mp/8mp) here, and the
// resulting media fragment could be appended to the url to make the
// image decode at a size equal to or smaller than 2mp.
//
// The method Downsample.sizeNoMoreThan(scale) returns a media fragment
// object that you can use to reduce the dimensions of an image as much
// as possible without exceeding the specified scale. If you have a
// 1600x1200 image and want to decode it to produce an image that is as
// small as possible but at least 160x120, you would pass a scale of 0.1.
//
// The returned objects have a dimensionScale property that specifies how
// they affect the dimensions of the image and an areaScale property that
// specifies how much they affect the area (number of pixels) in an
// image. (The areaScale is just the square of the scale.) To avoid
// floating-point rounding issues, the values of these scale properties
// are rounded to the nearest hundredth.
//
// The returned objects also have a scale() method that scales a
// dimension with proper rounding (it rounds up to the nearest integer
// just as libjpeg does).
//
// Each object also has a toString() method that returns the required
// media fragment (including the hash mark) so you can simply use
// string concatentation to append one of these objects to the URL of
// the image you want to decode.
//
// Downsample.NONE is a no-op media fragment object with scale set to
// 1, and a toString() method that returns the empty string.
//
(function(exports) {
'use strict'; // jshint ignore:line
// Round to the nearest hundredth to combat floating point rounding errors
function round(x) {
return Math.round(x * 100) / 100;
}
//
// A factory method for returning an object that represents a
// #-moz-samplesize media fragment. The use of Math.ceil() in the
// scale method is from jpeg_core_output_dimensions() in
// media/libjpeg/jdmaster.c and jdiv_round_up() in media/libjpeg/jutils.c
//
function MozSampleSize(n, scale) {
return Object.freeze({
dimensionScale: round(scale),
areaScale: round(scale * scale),
toString: function() { return '#-moz-samplesize=' + n; },
scale: function(x) { return Math.ceil(x * scale); }
});
}
// A fragment object that represents no downsampling with no fragment
var NONE = Object.freeze({
dimensionScale: 1,
areaScale: 1,
toString: function() { return ''; },
scale: function(x) { return x; }
});
//
// The five possible #-moz-samplesize values.
// The mapping from sample size to scale comes from:
// the moz-samplesize code in /image/decoders/nsJPEGDecoder.cpp and
// the jpeg_core_output_dimensions() function in media/libjpeg/jdmaster.c
//
var fragments = [
NONE,
// samplesize=2 reduces size by 1/2 and area by 1/4, etc.
MozSampleSize(2, 1 / 2),
MozSampleSize(3, 3 / 8),
MozSampleSize(4, 1 / 4),
MozSampleSize(8, 1 / 8)
];
// Return the fragment object that has the largest scale and downsamples the
// dimensions of an image at least as much as the specified scale.
// If none of the choices scales enough, return the one that comes closest
function sizeAtLeast(scale) {
scale = round(scale);
for (var i = 0; i < fragments.length; i++) {
var f = fragments[i];
if (f.dimensionScale <= scale) {
return f;
}
}
return fragments[fragments.length - 1];
}
// Return the fragment object that downsamples an image as far as possible
// without going beyond the specified scale. This might return NONE.
function sizeNoMoreThan(scale) {
scale = round(scale);
for (var i = fragments.length - 1; i >= 0; i--) {
var f = fragments[i];
if (f.dimensionScale >= scale) {
return f;
}
}
return NONE;
}
// Return the fragment object that has the largest scale and downsamples the
// area of an image at least as much as the specified scale.
// If none of the choices scales enough, return the one that comes closest
function areaAtLeast(scale) {
scale = round(scale);
for (var i = 0; i < fragments.length; i++) {
var f = fragments[i];
if (f.areaScale <= scale) {
return f;
}
}
return fragments[fragments.length - 1];
}
// Return the fragment object that downsamples the area of an image
// as far as possible without going beyond the specified scale. This
// might return NONE.
function areaNoMoreThan(scale) {
scale = round(scale);
for (var i = fragments.length - 1; i >= 0; i--) {
var f = fragments[i];
if (f.areaScale >= scale) {
return f;
}
}
return NONE;
}
exports.Downsample = {
sizeAtLeast: sizeAtLeast,
sizeNoMoreThan: sizeNoMoreThan,
areaAtLeast: areaAtLeast,
areaNoMoreThan: areaNoMoreThan,
NONE: NONE,
MAX_SIZE_REDUCTION: 1 / fragments[fragments.length - 1].dimensionScale,
MAX_AREA_REDUCTION: 1 / fragments[fragments.length - 1].areaScale
};
}(exports.ImageUtils));
})(window);

View file

@ -0,0 +1,245 @@
;;;;;;;(function(window,undefined){'use strict';function L10nError(message,id,loc){this.name='L10nError';this.message=message;this.id=id;this.loc=loc;}
L10nError.prototype=Object.create(Error.prototype);L10nError.prototype.constructor=L10nError;var io={_load:function(type,url,callback,sync){var xhr=new XMLHttpRequest();var needParse;if(xhr.overrideMimeType){xhr.overrideMimeType(type);}
xhr.open('GET',url,!sync);if(type==='application/json'){if(sync){needParse=true;}else{xhr.responseType='json';}}
xhr.addEventListener('load',function io_onload(e){if(e.target.status===200||e.target.status===0){var res=e.target.response||e.target.responseText;callback(null,needParse?JSON.parse(res):res);}else{callback(new L10nError('Not found: '+url));}});xhr.addEventListener('error',callback);xhr.addEventListener('timeout',callback);try{xhr.send(null);}catch(e){if(e.name==='NS_ERROR_FILE_NOT_FOUND'){callback(new L10nError('Not found: '+url));}else{throw e;}}},load:function(url,callback,sync){return io._load('text/plain',url,callback,sync);},loadJSON:function(url,callback,sync){return io._load('application/json',url,callback,sync);}};function EventEmitter(){}
EventEmitter.prototype.emit=function ee_emit(){if(!this._listeners){return;}
var args=Array.prototype.slice.call(arguments);var type=args.shift();if(!this._listeners[type]){return;}
var typeListeners=this._listeners[type].slice();for(var i=0;i<typeListeners.length;i++){typeListeners[i].apply(this,args);}};EventEmitter.prototype.addEventListener=function ee_add(type,listener){if(!this._listeners){this._listeners={};}
if(!(type in this._listeners)){this._listeners[type]=[];}
this._listeners[type].push(listener);};EventEmitter.prototype.removeEventListener=function ee_rm(type,listener){if(!this._listeners){return;}
var typeListeners=this._listeners[type];var pos=typeListeners.indexOf(listener);if(pos===-1){return;}
typeListeners.splice(pos,1);};function getPluralRule(lang){var locales2rules={'af':3,'ak':4,'am':4,'ar':1,'asa':3,'az':0,'be':11,'bem':3,'bez':3,'bg':3,'bh':4,'bm':0,'bn':3,'bo':0,'br':20,'brx':3,'bs':11,'ca':3,'cgg':3,'chr':3,'cs':12,'cy':17,'da':3,'de':3,'dv':3,'dz':0,'ee':3,'el':3,'en':3,'eo':3,'es':3,'et':3,'eu':3,'fa':0,'ff':5,'fi':3,'fil':4,'fo':3,'fr':5,'fur':3,'fy':3,'ga':8,'gd':24,'gl':3,'gsw':3,'gu':3,'guw':4,'gv':23,'ha':3,'haw':3,'he':2,'hi':4,'hr':11,'hu':0,'id':0,'ig':0,'ii':0,'is':3,'it':3,'iu':7,'ja':0,'jmc':3,'jv':0,'ka':0,'kab':5,'kaj':3,'kcg':3,'kde':0,'kea':0,'kk':3,'kl':3,'km':0,'kn':0,'ko':0,'ksb':3,'ksh':21,'ku':3,'kw':7,'lag':18,'lb':3,'lg':3,'ln':4,'lo':0,'lt':10,'lv':6,'mas':3,'mg':4,'mk':16,'ml':3,'mn':3,'mo':9,'mr':3,'ms':0,'mt':15,'my':0,'nah':3,'naq':7,'nb':3,'nd':3,'ne':3,'nl':3,'nn':3,'no':3,'nr':3,'nso':4,'ny':3,'nyn':3,'om':3,'or':3,'pa':3,'pap':3,'pl':13,'ps':3,'pt':3,'rm':3,'ro':9,'rof':3,'ru':11,'rwk':3,'sah':0,'saq':3,'se':7,'seh':3,'ses':0,'sg':0,'sh':11,'shi':19,'sk':12,'sl':14,'sma':7,'smi':7,'smj':7,'smn':7,'sms':7,'sn':3,'so':3,'sq':3,'sr':11,'ss':3,'ssy':3,'st':3,'sv':3,'sw':3,'syr':3,'ta':3,'te':3,'teo':3,'th':0,'ti':4,'tig':3,'tk':3,'tl':4,'tn':3,'to':0,'tr':0,'ts':3,'tzm':22,'uk':11,'ur':3,'ve':3,'vi':0,'vun':3,'wa':4,'wae':3,'wo':0,'xh':3,'xog':3,'yo':0,'zh':0,'zu':3};function isIn(n,list){return list.indexOf(n)!==-1;}
function isBetween(n,start,end){return typeof n===typeof start&&start<=n&&n<=end;}
var pluralRules={'0':function(){return'other';},'1':function(n){if((isBetween((n%100),3,10))){return'few';}
if(n===0){return'zero';}
if((isBetween((n%100),11,99))){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'2':function(n){if(n!==0&&(n%10)===0){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'3':function(n){if(n===1){return'one';}
return'other';},'4':function(n){if((isBetween(n,0,1))){return'one';}
return'other';},'5':function(n){if((isBetween(n,0,2))&&n!==2){return'one';}
return'other';},'6':function(n){if(n===0){return'zero';}
if((n%10)===1&&(n%100)!==11){return'one';}
return'other';},'7':function(n){if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'8':function(n){if((isBetween(n,3,6))){return'few';}
if((isBetween(n,7,10))){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'9':function(n){if(n===0||n!==1&&(isBetween((n%100),1,19))){return'few';}
if(n===1){return'one';}
return'other';},'10':function(n){if((isBetween((n%10),2,9))&&!(isBetween((n%100),11,19))){return'few';}
if((n%10)===1&&!(isBetween((n%100),11,19))){return'one';}
return'other';},'11':function(n){if((isBetween((n%10),2,4))&&!(isBetween((n%100),12,14))){return'few';}
if((n%10)===0||(isBetween((n%10),5,9))||(isBetween((n%100),11,14))){return'many';}
if((n%10)===1&&(n%100)!==11){return'one';}
return'other';},'12':function(n){if((isBetween(n,2,4))){return'few';}
if(n===1){return'one';}
return'other';},'13':function(n){if((isBetween((n%10),2,4))&&!(isBetween((n%100),12,14))){return'few';}
if(n!==1&&(isBetween((n%10),0,1))||(isBetween((n%10),5,9))||(isBetween((n%100),12,14))){return'many';}
if(n===1){return'one';}
return'other';},'14':function(n){if((isBetween((n%100),3,4))){return'few';}
if((n%100)===2){return'two';}
if((n%100)===1){return'one';}
return'other';},'15':function(n){if(n===0||(isBetween((n%100),2,10))){return'few';}
if((isBetween((n%100),11,19))){return'many';}
if(n===1){return'one';}
return'other';},'16':function(n){if((n%10)===1&&n!==11){return'one';}
return'other';},'17':function(n){if(n===3){return'few';}
if(n===0){return'zero';}
if(n===6){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'18':function(n){if(n===0){return'zero';}
if((isBetween(n,0,2))&&n!==0&&n!==2){return'one';}
return'other';},'19':function(n){if((isBetween(n,2,10))){return'few';}
if((isBetween(n,0,1))){return'one';}
return'other';},'20':function(n){if((isBetween((n%10),3,4)||((n%10)===9))&&!(isBetween((n%100),10,19)||isBetween((n%100),70,79)||isBetween((n%100),90,99))){return'few';}
if((n%1000000)===0&&n!==0){return'many';}
if((n%10)===2&&!isIn((n%100),[12,72,92])){return'two';}
if((n%10)===1&&!isIn((n%100),[11,71,91])){return'one';}
return'other';},'21':function(n){if(n===0){return'zero';}
if(n===1){return'one';}
return'other';},'22':function(n){if((isBetween(n,0,1))||(isBetween(n,11,99))){return'one';}
return'other';},'23':function(n){if((isBetween((n%10),1,2))||(n%20)===0){return'one';}
return'other';},'24':function(n){if((isBetween(n,3,10)||isBetween(n,13,19))){return'few';}
if(isIn(n,[2,12])){return'two';}
if(isIn(n,[1,11])){return'one';}
return'other';}};var index=locales2rules[lang.replace(/-.*$/,'')];if(!(index in pluralRules)){return function(){return'other';};}
return pluralRules[index];}
var MAX_PLACEABLES=100;var PropertiesParser={patterns:null,entryIds:null,init:function(){this.patterns={comment:/^\s*#|^\s*$/,entity:/^([^=\s]+)\s*=\s*(.*)$/,multiline:/[^\\]\\$/,index:/\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i,unicode:/\\u([0-9a-fA-F]{1,4})/g,entries:/[^\r\n]+/g,controlChars:/\\([\\\n\r\t\b\f\{\}\"\'])/g,placeables:/\{\{\s*([^\s]*?)\s*\}\}/,};},parse:function(ctx,source){if(!this.patterns){this.init();}
var ast=[];this.entryIds=Object.create(null);var entries=source.match(this.patterns.entries);if(!entries){return ast;}
for(var i=0;i<entries.length;i++){var line=entries[i];if(this.patterns.comment.test(line)){continue;}
while(this.patterns.multiline.test(line)&&i<entries.length){line=line.slice(0,-1)+entries[++i].trim();}
var entityMatch=line.match(this.patterns.entity);if(entityMatch){try{this.parseEntity(entityMatch[1],entityMatch[2],ast);}catch(e){if(ctx){ctx._emitter.emit('parseerror',e);}else{throw e;}}}}
return ast;},parseEntity:function(id,value,ast){var name,key;var pos=id.indexOf('[');if(pos!==-1){name=id.substr(0,pos);key=id.substring(pos+1,id.length-1);}else{name=id;key=null;}
var nameElements=name.split('.');if(nameElements.length>2){throw new L10nError('Error in ID: "'+name+'".'+' Nested attributes are not supported.');}
var attr;if(nameElements.length>1){name=nameElements[0];attr=nameElements[1];if(attr[0]==='$'){throw new L10nError('Attribute can\'t start with "$"',id);}}else{attr=null;}
this.setEntityValue(name,attr,key,this.unescapeString(value),ast);},setEntityValue:function(id,attr,key,rawValue,ast){var pos,v;var value=rawValue.indexOf('{{')>-1?this.parseString(rawValue):rawValue;if(attr){pos=this.entryIds[id];if(pos===undefined){v={$i:id};if(key){v[attr]={};v[attr][key]=value;}else{v[attr]=value;}
ast.push(v);this.entryIds[id]=ast.length-1;return;}
if(key){if(typeof(ast[pos][attr])==='string'){ast[pos][attr]={$x:this.parseIndex(ast[pos][attr]),$v:{}};}
ast[pos][attr].$v[key]=value;return;}
ast[pos][attr]=value;return;}
if(key){pos=this.entryIds[id];if(pos===undefined){v={};v[key]=value;ast.push({$i:id,$v:v});this.entryIds[id]=ast.length-1;return;}
if(typeof(ast[pos].$v)==='string'){ast[pos].$x=this.parseIndex(ast[pos].$v);ast[pos].$v={};}
ast[pos].$v[key]=value;return;}
ast.push({$i:id,$v:value});this.entryIds[id]=ast.length-1;},parseString:function(str){var chunks=str.split(this.patterns.placeables);var complexStr=[];var len=chunks.length;var placeablesCount=(len-1)/2;if(placeablesCount>=MAX_PLACEABLES){throw new L10nError('Too many placeables ('+placeablesCount+', max allowed is '+MAX_PLACEABLES+')');}
for(var i=0;i<chunks.length;i++){if(chunks[i].length===0){continue;}
if(i%2===1){complexStr.push({t:'idOrVar',v:chunks[i]});}else{complexStr.push(chunks[i]);}}
return complexStr;},unescapeString:function(str){if(str.lastIndexOf('\\')!==-1){str=str.replace(this.patterns.controlChars,'$1');}
return str.replace(this.patterns.unicode,function(match,token){return unescape('%u'+'0000'.slice(token.length)+token);});},parseIndex:function(str){var match=str.match(this.patterns.index);if(!match){throw new L10nError('Malformed index');}
if(match[2]){return[{t:'idOrVar',v:match[1]},match[2]];}else{return[{t:'idOrVar',v:match[1]}];}}};var KNOWN_MACROS=['plural'];var MAX_PLACEABLE_LENGTH=2500;var rePlaceables=/\{\{\s*(.+?)\s*\}\}/g;var nonLatin1=/[^\x01-\xFF]/;var FSI='\u2068';var PDI='\u2069';function createEntry(node,env){var keys=Object.keys(node);if(typeof node.$v==='string'&&keys.length===2){return node.$v;}
var attrs;for(var i=0,key;key=keys[i];i++){if(key[0]==='$'){continue;}
if(!attrs){attrs=Object.create(null);}
attrs[key]=createAttribute(node[key],env,node.$i+'.'+key);}
return{id:node.$i,value:node.$v!==undefined?node.$v:null,index:node.$x||null,attrs:attrs||null,env:env,dirty:false};}
function createAttribute(node,env,id){if(typeof node==='string'){return node;}
return{id:id,value:node.$v||(node!==undefined?node:null),index:node.$x||null,env:env,dirty:false};}
function format(args,entity){if(typeof entity==='string'){return[{},entity];}
if(entity.dirty){throw new L10nError('Cyclic reference detected: '+entity.id);}
entity.dirty=true;var rv;try{rv=resolveValue({},args,entity.env,entity.value,entity.index);}finally{entity.dirty=false;}
return rv;}
function resolveIdentifier(args,env,id){if(KNOWN_MACROS.indexOf(id)>-1){return[{},env['__'+id]];}
if(args&&args.hasOwnProperty(id)){if(typeof args[id]==='string'||(typeof args[id]==='number'&&!isNaN(args[id]))){return[{},args[id]];}else{throw new L10nError('Arg must be a string or a number: '+id);}}
if(id in env&&id!=='__proto__'){return format(args,env[id]);}
throw new L10nError('Unknown reference: '+id);}
function subPlaceable(locals,args,env,id){var res;try{res=resolveIdentifier(args,env,id);}catch(err){return[{error:err},'{{ '+id+' }}'];}
var value=res[1];if(typeof value==='number'){return res;}
if(typeof value==='string'){if(value.length>=MAX_PLACEABLE_LENGTH){throw new L10nError('Too many characters in placeable ('+
value.length+', max allowed is '+
MAX_PLACEABLE_LENGTH+')');}
if(locals.contextIsNonLatin1||value.match(nonLatin1)){res[1]=FSI+value+PDI;}
return res;}
return[{},'{{ '+id+' }}'];}
function interpolate(locals,args,env,arr){return arr.reduce(function(prev,cur){if(typeof cur==='string'){return[prev[0],prev[1]+cur];}else if(cur.t==='idOrVar'){var placeable=subPlaceable(locals,args,env,cur.v);return[prev[0],prev[1]+placeable[1]];}},[locals,'']);}
function resolveSelector(args,env,expr,index){var selectorName=index[0].v;var selector=resolveIdentifier(args,env,selectorName)[1];if(typeof selector!=='function'){return selector;}
var argValue=index[1]?resolveIdentifier(args,env,index[1])[1]:undefined;if(selector===env.__plural){if(argValue===0&&'zero'in expr){return'zero';}
if(argValue===1&&'one'in expr){return'one';}
if(argValue===2&&'two'in expr){return'two';}}
return selector(argValue);}
function resolveValue(locals,args,env,expr,index){if(!expr){return[locals,expr];}
if(typeof expr==='string'||typeof expr==='boolean'||typeof expr==='number'){return[locals,expr];}
if(Array.isArray(expr)){locals.contextIsNonLatin1=expr.some(function($_){return typeof($_)==='string'&&$_.match(nonLatin1);});return interpolate(locals,args,env,expr);}
if(index){var selector=resolveSelector(args,env,expr,index);if(expr.hasOwnProperty(selector)){return resolveValue(locals,args,env,expr[selector]);}}
if('other'in expr){return resolveValue(locals,args,env,expr.other);}
throw new L10nError('Unresolvable value');}
var Resolver={createEntry:createEntry,format:format,rePlaceables:rePlaceables};function walkContent(node,fn){if(typeof node==='string'){return fn(node);}
if(node.t==='idOrVar'){return node;}
var rv=Array.isArray(node)?[]:{};var keys=Object.keys(node);for(var i=0,key;(key=keys[i]);i++){if(key==='$i'||key==='$x'){rv[key]=node[key];}else{rv[key]=walkContent(node[key],fn);}}
return rv;}
var reAlphas=/[a-zA-Z]/g;var reVowels=/[aeiouAEIOU]/g;var ACCENTED_MAP='\u0226\u0181\u0187\u1E12\u1E16\u0191\u0193\u0126\u012A'+'\u0134\u0136\u013F\u1E3E\u0220\u01FE\u01A4\u024A\u0158'+'\u015E\u0166\u016C\u1E7C\u1E86\u1E8A\u1E8E\u1E90'+'[\\]^_`'+'\u0227\u0180\u0188\u1E13\u1E17\u0192\u0260\u0127\u012B'+'\u0135\u0137\u0140\u1E3F\u019E\u01FF\u01A5\u024B\u0159'+'\u015F\u0167\u016D\u1E7D\u1E87\u1E8B\u1E8F\u1E91';var FLIPPED_MAP='\u2200\u0510\u2183p\u018E\u025F\u05E4HI\u017F'+'\u04FC\u02E5WNO\u0500\xD2\u1D1AS\u22A5\u2229\u0245'+'\uFF2DX\u028EZ'+'[\\]\u1D65_,'+'\u0250q\u0254p\u01DD\u025F\u0183\u0265\u0131\u027E'+'\u029E\u0285\u026Fuodb\u0279s\u0287n\u028C\u028Dx\u028Ez';function makeLonger(val){return val.replace(reVowels,function(match){return match+match.toLowerCase();});}
function replaceChars(map,val){return val.replace(reAlphas,function(match){return map.charAt(match.charCodeAt(0)-65);});}
var reWords=/[^\W0-9_]+/g;function makeRTL(val){return val.replace(reWords,function(match){return'\u202e'+match+'\u202c';});}
var reExcluded=/(%[EO]?\w|\{\s*.+?\s*\}|&[#\w]+;|<\s*.+?\s*>)/;function mapContent(fn,val){if(!val){return val;}
var parts=val.split(reExcluded);var modified=parts.map(function(part){if(reExcluded.test(part)){return part;}
return fn(part);});return modified.join('');}
function Pseudo(id,name,charMap,modFn){this.id=id;this.translate=mapContent.bind(null,function(val){return replaceChars(charMap,modFn(val));});this.name=this.translate(name);}
var PSEUDO={'fr-x-psaccent':new Pseudo('fr-x-psaccent','Runtime Accented',ACCENTED_MAP,makeLonger),'ar-x-psbidi':new Pseudo('ar-x-psbidi','Runtime Bidi',FLIPPED_MAP,makeRTL)};function Locale(id,ctx){this.id=id;this.ctx=ctx;this.isReady=false;this.entries=Object.create(null);this.entries.__plural=getPluralRule(this.isPseudo()?this.ctx.defaultLocale:id);}
Locale.prototype.isPseudo=function(){return this.ctx.qps.indexOf(this.id)!==-1;};var bindingsIO={extra:function(id,ver,path,type,callback,errback){if(type==='properties'){type='text';}
navigator.mozApps.getLocalizationResource(id,ver,path,type).then(callback.bind(null,null),errback);},app:function(id,ver,path,type,callback,errback,sync){switch(type){case'properties':io.load(path,callback,sync);break;case'json':io.loadJSON(path,callback,sync);break;}},};Locale.prototype.build=function L_build(callback){var sync=!callback;var ctx=this.ctx;var self=this;var l10nLoads=ctx.resLinks.length;function onL10nLoaded(err){if(err){ctx._emitter.emit('fetcherror',err);}
if(--l10nLoads<=0){self.isReady=true;if(callback){callback();}}}
if(l10nLoads===0){onL10nLoaded();return;}
function onJSONLoaded(err,json){if(!err&&json){self.addAST(json);}
onL10nLoaded(err);}
function onPropLoaded(err,source){if(!err&&source){var ast=PropertiesParser.parse(ctx,source);self.addAST(ast);}
onL10nLoaded(err);}
var idToFetch=this.isPseudo()?ctx.defaultLocale:this.id;var appVersion=null;var source='app';if(typeof(navigator)!=='undefined'){source=navigator.mozL10n._config.localeSources[this.id]||'app';appVersion=navigator.mozL10n._config.appVersion;}
for(var i=0;i<ctx.resLinks.length;i++){var resLink=decodeURI(ctx.resLinks[i]);var path=resLink.replace('{locale}',idToFetch);var type=path.substr(path.lastIndexOf('.')+1);var cb;switch(type){case'json':cb=onJSONLoaded;break;case'properties':cb=onPropLoaded;break;}
bindingsIO[source](this.id,appVersion,path,type,cb,onL10nLoaded,sync);}};function createPseudoEntry(node,entries){return Resolver.createEntry(walkContent(node,PSEUDO[this.id].translate),entries);}
Locale.prototype.addAST=function(ast){var createEntry=this.isPseudo()?createPseudoEntry.bind(this):Resolver.createEntry;for(var i=0;i<ast.length;i++){this.entries[ast[i].$i]=createEntry(ast[i],this.entries);}};function Context(id){this.id=id;this.isReady=false;this.isLoading=false;this.defaultLocale='en-US';this.availableLocales=[];this.supportedLocales=[];this.qps=[];this.resLinks=[];this.locales={};this._emitter=new EventEmitter();this._ready=new Promise(this.once.bind(this));}
function reportMissing(id,err){this._emitter.emit('notfounderror',err);return id;}
function getWithFallback(id){var cur=0;var loc;var locale;while(loc=this.supportedLocales[cur]){locale=this.getLocale(loc);if(!locale.isReady){locale.build(null);}
var entry=locale.entries[id];if(entry===undefined){cur++;reportMissing.call(this,id,new L10nError('"'+id+'"'+' not found in '+loc+' in '+this.id,id,loc));continue;}
return entry;}
throw new L10nError('"'+id+'"'+' missing from all supported locales in '+this.id,id);}
function formatTuple(args,entity){try{return Resolver.format(args,entity);}catch(err){this._emitter.emit('resolveerror',err);var locals={error:err};return[locals,entity.id];}}
function formatValue(args,entity){if(typeof entity==='string'){return entity;}
return formatTuple.call(this,args,entity)[1];}
function formatEntity(args,entity){var entityTuple=formatTuple.call(this,args,entity);var value=entityTuple[1];var formatted={value:value,attrs:null,};if(entity.attrs){formatted.attrs=Object.create(null);}
for(var key in entity.attrs){var attrTuple=formatTuple.call(this,args,entity.attrs[key]);formatted.attrs[key]=attrTuple[1];}
return formatted;}
function formatAsync(fn,id,args){return this._ready.then(getWithFallback.bind(this,id)).then(fn.bind(this,args),reportMissing.bind(this,id));}
Context.prototype.formatValue=function(id,args){return formatAsync.call(this,formatValue,id,args);};Context.prototype.formatEntity=function(id,args){return formatAsync.call(this,formatEntity,id,args);};function legacyGet(fn,id,args){if(!this.isReady){throw new L10nError('Context not ready');}
var entry;try{entry=getWithFallback.call(this,id);}catch(err){if(err.loc){throw err;}
reportMissing.call(this,id,err);return'';}
return fn.call(this,args,entry);}
Context.prototype.get=function(id,args){return legacyGet.call(this,formatValue,id,args);};Context.prototype.getEntity=function(id,args){return legacyGet.call(this,formatEntity,id,args);};Context.prototype.getLocale=function getLocale(code){var locales=this.locales;if(locales[code]){return locales[code];}
return locales[code]=new Locale(code,this);};function negotiate(available,requested,defaultLocale){var supportedLocale;for(var i=0;i<requested.length;i++){var locale=requested[i];if(available.indexOf(locale)!==-1){supportedLocale=locale;break;}}
if(!supportedLocale||supportedLocale===defaultLocale){return[defaultLocale];}
return[supportedLocale,defaultLocale];}
function freeze(supported){var locale=this.getLocale(supported[0]);if(locale.isReady){setReady.call(this,supported);}else{locale.build(setReady.bind(this,supported));}}
function setReady(supported){this.supportedLocales=supported;this.isReady=true;this._emitter.emit('ready');}
Context.prototype.registerLocales=function(defLocale,available){if(defLocale){this.defaultLocale=defLocale;}
this.availableLocales=[this.defaultLocale];this.qps=Object.keys(PSEUDO);if(available){for(var i=0,loc;loc=available[i];i++){if(this.availableLocales.indexOf(loc)===-1){this.availableLocales.push(loc);var pos=this.qps.indexOf(loc);if(pos!==-1){this.qps.splice(pos,1);}}}}};Context.prototype.requestLocales=function requestLocales(){if(this.isLoading&&!this.isReady){throw new L10nError('Context not ready');}
this.isLoading=true;var requested=Array.prototype.slice.call(arguments);if(requested.length===0){throw new L10nError('No locales requested');}
var supported=negotiate(this.availableLocales.concat(this.qps),requested,this.defaultLocale);if(this.supportedLocales[0]!==supported[0]){freeze.call(this,supported);}};Context.prototype.addEventListener=function(type,listener){this._emitter.addEventListener(type,listener);};Context.prototype.removeEventListener=function(type,listener){this._emitter.removeEventListener(type,listener);};Context.prototype.ready=function(callback){if(this.isReady){setTimeout(callback);}
this.addEventListener('ready',callback);};Context.prototype.once=function(callback){if(this.isReady){setTimeout(callback);return;}
var callAndRemove=(function(){this.removeEventListener('ready',callAndRemove);callback();}).bind(this);this.addEventListener('ready',callAndRemove);};var allowed={elements:['a','em','strong','small','s','cite','q','dfn','abbr','data','time','code','var','samp','kbd','sub','sup','i','b','u','mark','ruby','rt','rp','bdi','bdo','span','br','wbr'],attributes:{global:['title','aria-label','aria-valuetext','aria-moz-hint'],a:['download'],area:['download','alt'],input:['alt','placeholder'],menuitem:['label'],menu:['label'],optgroup:['label'],option:['label'],track:['label'],img:['alt'],textarea:['placeholder'],th:['abbr']}};var rtlList=['ar','he','fa','ps','ar-x-psbidi','ur'];var nodeObserver=null;var pendingElements=null;var moConfig={attributes:true,characterData:false,childList:true,subtree:true,attributeFilter:['data-l10n-id','data-l10n-args']};navigator.mozL10n={ctx:null,get:function get(id,ctxdata){return navigator.mozL10n.ctx.get(id,ctxdata);},formatValue:function(id,ctxdata){return navigator.mozL10n.ctx.formatValue(id,ctxdata);},formatEntity:function(id,ctxdata){return navigator.mozL10n.ctx.formatEntity(id,ctxdata);},translateFragment:function(fragment){return translateFragment.call(navigator.mozL10n,fragment);},setAttributes:setL10nAttributes,getAttributes:getL10nAttributes,ready:function ready(callback){return navigator.mozL10n.ctx.ready(callback);},once:function once(callback){return navigator.mozL10n.ctx.once(callback);},get readyState(){return navigator.mozL10n.ctx.isReady?'complete':'loading';},language:{set code(lang){navigator.mozL10n.ctx.requestLocales(lang);},get code(){return navigator.mozL10n.ctx.supportedLocales[0];},get direction(){return getDirection(navigator.mozL10n.ctx.supportedLocales[0]);}},qps:PSEUDO,_config:{appVersion:null,localeSources:Object.create(null),isPretranslated:false,},_getInternalAPI:function(){return{Error:L10nError,Context:Context,Locale:Locale,Resolver:Resolver,getPluralRule:getPluralRule,rePlaceables:rePlaceables,translateDocument:translateDocument,onMetaInjected:onMetaInjected,PropertiesParser:PropertiesParser,walkContent:walkContent,buildLocaleList:buildLocaleList};}};function getDirection(lang){return(rtlList.indexOf(lang)>=0)?'rtl':'ltr';}
var readyStates={loading:0,interactive:1,complete:2};function whenInteractive(callback){if(readyStates[document.readyState]>=readyStates.interactive){callback();return;}
document.addEventListener('readystatechange',function l10n_onrsc(){if(readyStates[document.readyState]>=readyStates.interactive){document.removeEventListener('readystatechange',l10n_onrsc);callback();}});}
function initObserver(){nodeObserver=new MutationObserver(onMutations.bind(navigator.mozL10n));nodeObserver.observe(document,moConfig);}
function init(pretranslate){if(!pretranslate){initObserver();}
initResources.call(navigator.mozL10n);}
function initResources(){var meta={};var nodes=document.head.querySelectorAll('link[rel="localization"],'+'meta[name="availableLanguages"],'+'meta[name="defaultLanguage"],'+'meta[name="appVersion"],'+'script[type="application/l10n"]');for(var i=0,node;node=nodes[i];i++){var type=node.getAttribute('rel')||node.nodeName.toLowerCase();switch(type){case'localization':this.ctx.resLinks.push(node.getAttribute('href'));break;case'meta':onMetaInjected.call(this,node,meta);break;case'script':onScriptInjected.call(this,node);break;}}
var additionalLanguagesPromise;if(navigator.mozApps&&navigator.mozApps.getAdditionalLanguages){additionalLanguagesPromise=navigator.mozApps.getAdditionalLanguages().catch(function(e){console.error('Error while loading getAdditionalLanguages',e);});document.addEventListener('additionallanguageschange',function(evt){registerLocales.call(this,meta,evt.detail);this.ctx.requestLocales.apply(this.ctx,navigator.languages||[navigator.language]);}.bind(this));}else{additionalLanguagesPromise=Promise.resolve();}
additionalLanguagesPromise.then(function(extraLangs){registerLocales.call(this,meta,extraLangs);initLocale.call(this);}.bind(this));}
function registerLocales(meta,extraLangs){var locales=buildLocaleList.call(this,meta,extraLangs);navigator.mozL10n._config.localeSources=locales[1];this.ctx.registerLocales(locales[0],Object.keys(locales[1]));}
function getMatchingLangpack(appVersion,langpacks){for(var i=0,langpack;(langpack=langpacks[i]);i++){if(langpack.target===appVersion){return langpack;}}
return null;}
function buildLocaleList(meta,extraLangs){var loc,lp;var localeSources=Object.create(null);var defaultLocale=meta.defaultLanguage||this.ctx.defaultLocale;if(meta.availableLanguages){for(loc in meta.availableLanguages){localeSources[loc]='app';}}
if(extraLangs){for(loc in extraLangs){lp=getMatchingLangpack(this._config.appVersion,extraLangs[loc]);if(!lp){continue;}
if(!(loc in localeSources)||!meta.availableLanguages[loc]||parseInt(lp.revision)>meta.availableLanguages[loc]){localeSources[loc]='extra';}}}
if(!(defaultLocale in localeSources)){localeSources[defaultLocale]='app';}
return[defaultLocale,localeSources];}
function splitAvailableLanguagesString(str){var langs={};str.split(',').forEach(function(lang){lang=lang.trim().split(':');langs[lang[0]]=parseInt(lang[1]);});return langs;}
function onMetaInjected(node,meta){switch(node.getAttribute('name')){case'availableLanguages':meta.availableLanguages=splitAvailableLanguagesString(node.getAttribute('content'));break;case'defaultLanguage':meta.defaultLanguage=node.getAttribute('content');break;case'appVersion':navigator.mozL10n._config.appVersion=node.getAttribute('content');break;}}
function onScriptInjected(node){var lang=node.getAttribute('lang');var locale=this.ctx.getLocale(lang);locale.addAST(JSON.parse(node.textContent));}
function initLocale(){this.ctx.requestLocales.apply(this.ctx,navigator.languages||[navigator.language]);window.addEventListener('languagechange',function l10n_langchange(){this.ctx.requestLocales.apply(this.ctx,navigator.languages||[navigator.language]);}.bind(this));}
function localizeMutations(mutations){var mutation;var targets=new Set();for(var i=0;i<mutations.length;i++){mutation=mutations[i];if(mutation.type==='childList'){var addedNode;for(var j=0;j<mutation.addedNodes.length;j++){addedNode=mutation.addedNodes[j];if(addedNode.nodeType!==Node.ELEMENT_NODE){continue;}
targets.add(addedNode);}}
if(mutation.type==='attributes'){targets.add(mutation.target);}}
targets.forEach(function(target){if(target.childElementCount){translateFragment.call(this,target);}else if(target.hasAttribute('data-l10n-id')){translateElement.call(this,target);}},this);}
function onMutations(mutations,self){self.disconnect();localizeMutations.call(this,mutations);self.observe(document,moConfig);}
function onReady(){if(!navigator.mozL10n._config.isPretranslated){translateDocument.call(this);}
navigator.mozL10n._config.isPretranslated=false;if(pendingElements){for(var i=0,element;element=pendingElements[i];i++){translateElement.call(this,element);}
pendingElements=null;}
if(!nodeObserver){initObserver();}
fireLocalizedEvent.call(this);}
function fireLocalizedEvent(){var event=new CustomEvent('localized',{'bubbles':false,'cancelable':false,'detail':{'language':this.ctx.supportedLocales[0]}});window.dispatchEvent(event);}
var reOverlay=/<|&#?\w+;/;var reHtml=/[&<>]/g;var htmlEntities={'&':'&amp;','<':'&lt;','>':'&gt;',};function translateDocument(){document.documentElement.lang=this.language.code;document.documentElement.dir=this.language.direction;translateFragment.call(this,document.documentElement);}
function translateFragment(element){if(typeof element.hasAttribute==='function'&&element.hasAttribute('data-l10n-id')){translateElement.call(this,element);}
var nodes=getTranslatableChildren(element);for(var i=0;i<nodes.length;i++){translateElement.call(this,nodes[i]);}}
function setL10nAttributes(element,id,args){element.setAttribute('data-l10n-id',id);if(args){element.setAttribute('data-l10n-args',JSON.stringify(args));}}
function getL10nAttributes(element){return{id:element.getAttribute('data-l10n-id'),args:JSON.parse(element.getAttribute('data-l10n-args'))};}
function getTranslatableChildren(element){return element?element.querySelectorAll('*[data-l10n-id]'):[];}
function camelCaseToDashed(string){if(string==='ariaValueText'){return'aria-valuetext';}
return string.replace(/[A-Z]/g,function(match){return'-'+match.toLowerCase();}).replace(/^-/,'');}
function escapeL10nArgs(match){return htmlEntities[match];}
function translateElement(element){if(!this.ctx.isReady){if(!pendingElements){pendingElements=[];}
pendingElements.push(element);return;}
var l10nId=element.getAttribute('data-l10n-id');if(!l10nId){return false;}
var l10nArgs=element.getAttribute('data-l10n-args');var entity=this.ctx.getEntity(l10nId,l10nArgs?JSON.parse(l10nArgs.replace(reHtml,escapeL10nArgs)):undefined);var value=entity.value;if(typeof value==='string'){if(!reOverlay.test(value)){element.textContent=value;}else{var translation=element.ownerDocument.createElement('template');translation.innerHTML=value;overlayElement(element,translation.content);}}
for(var key in entity.attrs){var attrName=camelCaseToDashed(key);if(isAttrAllowed({name:attrName},element)){element.setAttribute(attrName,entity.attrs[key]);}}}
function overlayElement(sourceElement,translationElement){var result=translationElement.ownerDocument.createDocumentFragment();var k,attr;var childElement;while((childElement=translationElement.childNodes[0])){translationElement.removeChild(childElement);if(childElement.nodeType===Node.TEXT_NODE){result.appendChild(childElement);continue;}
var index=getIndexOfType(childElement);var sourceChild=getNthElementOfType(sourceElement,childElement,index);if(sourceChild){overlayElement(sourceChild,childElement);result.appendChild(sourceChild);continue;}
if(isElementAllowed(childElement)){var sanitizedChild=childElement.ownerDocument.createElement(childElement.nodeName);overlayElement(sanitizedChild,childElement);result.appendChild(sanitizedChild);continue;}
result.appendChild(document.createTextNode(childElement.textContent));}
sourceElement.textContent='';sourceElement.appendChild(result);if(translationElement.attributes){for(k=0,attr;(attr=translationElement.attributes[k]);k++){if(isAttrAllowed(attr,sourceElement)){sourceElement.setAttribute(attr.name,attr.value);}}}}
function isElementAllowed(element){return allowed.elements.indexOf(element.tagName.toLowerCase())!==-1;}
function isAttrAllowed(attr,element){var attrName=attr.name.toLowerCase();var tagName=element.tagName.toLowerCase();if(allowed.attributes.global.indexOf(attrName)!==-1){return true;}
if(!allowed.attributes[tagName]){return false;}
if(allowed.attributes[tagName].indexOf(attrName)!==-1){return true;}
if(tagName==='input'&&attrName==='value'){var type=element.type.toLowerCase();if(type==='submit'||type==='button'||type==='reset'){return true;}}
return false;}
function getNthElementOfType(context,element,index){var nthOfType=0;for(var i=0,child;child=context.children[i];i++){if(child.nodeType===Node.ELEMENT_NODE&&child.tagName===element.tagName){if(nthOfType===index){return child;}
nthOfType++;}}
return null;}
function getIndexOfType(element){var index=0;var child;while((child=element.previousElementSibling)){if(child.tagName===element.tagName){index++;}}
return index;}
var DEBUG=false;navigator.mozL10n.ctx=new Context(window.document?document.URL:null);navigator.mozL10n.ctx.ready(onReady.bind(navigator.mozL10n));navigator.mozL10n.ctx.addEventListener('notfounderror',function reportMissingEntity(e){if(DEBUG||e.loc==='en-US'){console.warn(e.toString());}});if(DEBUG){navigator.mozL10n.ctx.addEventListener('fetcherror',console.error.bind(console));navigator.mozL10n.ctx.addEventListener('parseerror',console.error.bind(console));navigator.mozL10n.ctx.addEventListener('resolveerror',console.error.bind(console));}
if(window.document){navigator.mozL10n._config.isPretranslated=document.documentElement.lang===navigator.language;var forcePretranslate=!navigator.mozL10n._config.isPretranslated;whenInteractive(init.bind(navigator.mozL10n,forcePretranslate));}
document.l10n={setAttributes:navigator.mozL10n.setAttributes,getAttributes:navigator.mozL10n.getAttributes,formatValue:function(id,args){return navigator.mozL10n.formatValue(id,args);},translateFragment:function(frag){return Promise.resolve(navigator.mozL10n.translateFragment(frag));},ready:new Promise(function(resolve){navigator.mozL10n.once(resolve);}),formatValues:function(){var keys=arguments;var resp=keys.map(function(key){if(Array.isArray(key)){return navigator.mozL10n.formatValue(key[0],key[1]);}
return navigator.mozL10n.formatValue(key);});return Promise.all(resp);},requestLanguages:function(langs){navigator.mozL10n.ctx.requestLocales.apply(navigator.mozL10n.ctx,langs);},pseudo:{'fr-x-psaccent':{getName:function(){return Promise.resolve(navigator.mozL10n.qps['fr-x-psaccent'].name);},processString:function(s){return Promise.resolve(navigator.mozL10n.qps['fr-x-psaccent'].translate(s));}},'ar-x-psbidi':{getName:function(){return Promise.resolve(navigator.mozL10n.qps['ar-x-psbidi'].name);},processString:function(s){return Promise.resolve(navigator.mozL10n.qps['ar-x-psbidi'].translate(s));}}},};navigator.mozL10n.ready(function(){document.documentElement.setAttribute('langs',navigator.mozL10n.ctx.supportedLocales.join(' '));});navigator.mozL10n.once(function(){window.addEventListener('localized',function(){document.dispatchEvent(new CustomEvent('DOMRetranslated',{bubbles:false,cancelable:false}));});});})(this);

View file

@ -0,0 +1,11 @@
;;;;;;;;;;'use strict';var LazyLoader=(function(){function LazyLoader(){this._loaded={};this._isLoading={};}
LazyLoader.prototype={_js:function(file,callback){var script=document.createElement('script');script.src=file;script.async=false;script.addEventListener('load',callback);document.head.appendChild(script);this._isLoading[file]=script;},_css:function(file,callback){var style=document.createElement('link');style.type='text/css';style.rel='stylesheet';style.href=file;document.head.appendChild(style);callback();},_html:function(domNode,callback){if(domNode.getAttribute('is')){this.load(['/shared/js/html_imports.js'],function(){HtmlImports.populate(callback);}.bind(this));return;}
for(var i=0;i<domNode.childNodes.length;i++){if(domNode.childNodes[i].nodeType==document.COMMENT_NODE){domNode.innerHTML=domNode.childNodes[i].nodeValue;break;}}
window.dispatchEvent(new CustomEvent('lazyload',{detail:domNode}));callback();},getJSON:function(file,mozSystem){return new Promise(function(resolve,reject){var xhr;if(mozSystem){xhr=new XMLHttpRequest({mozSystem:true});}else{xhr=new XMLHttpRequest();}
xhr.open('GET',file,true);xhr.responseType='json';xhr.onerror=function(error){reject(error);};xhr.onload=function(){if(xhr.response!==null){resolve(xhr.response);}else{reject(new Error('No valid JSON object was found ('+
xhr.status+' '+xhr.statusText+')'));}};xhr.send();});},load:function(files,callback){var deferred={};deferred.promise=new Promise(resolve=>{deferred.resolve=resolve;});if(!Array.isArray(files)){files=[files];}
var loadsRemaining=files.length,self=this;function perFileCallback(file){if(self._isLoading[file]){delete self._isLoading[file];}
self._loaded[file]=true;if(--loadsRemaining===0){deferred.resolve();if(callback){callback();}}}
for(var i=0;i<files.length;i++){var file=files[i];if(this._loaded[file.id||file]){perFileCallback(file);}else if(this._isLoading[file]){this._isLoading[file].addEventListener('load',perFileCallback.bind(null,file));}else{var method,idx;if(typeof file==='string'){method=file.match(/\.([^.]+)$/)[1];idx=file;}else{method='html';idx=file.id;}
this['_'+method](file,perFileCallback.bind(null,idx));}}
return deferred.promise;}};return new LazyLoader();}());

View file

@ -0,0 +1,135 @@
/* exported MimeMapper */
'use strict';
/**
* MimeMapper helps gaia apps to decide the mapping of mimetype and extension.
* The use cases often happen when apps need to know about the exact
* mimetypes or extensions, such as to delegate the open web activity, we must
* have suitable mimetypes or extensions to request the right activity
*
* The mapping is basically created according to:
* http://en.wikipedia.org/wiki/Internet_media_type
*
* The supported formats are considered base on the deviceStorage properties:
* http://dxr.mozilla.org/mozilla-central/toolkit/content/
* devicestorage.properties
*
*/
var MimeMapper = {
// This list only contains the extensions we currently supported
// We should make it more complete for further usages
_typeToExtensionMap: {
// Image
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/bmp': 'bmp',
// Audio
'audio/mpeg': 'mp3',
'audio/mp4': 'm4a',
'audio/ogg': 'ogg',
'audio/webm': 'webm',
'audio/3gpp': '3gp',
'audio/amr': 'amr',
// Video
'video/mp4': 'mp4',
'video/mpeg': 'mpg',
'video/ogg': 'ogg',
'video/webm': 'webm',
'video/3gpp': '3gp',
'video/3gpp2': '3g2',
// Application
// If we want to support some types, like pdf, just add
'application/pdf': 'pdf',
'application/vcard': 'vcf',
// Text
'text/vcard': 'vcf',
'text/x-vcard': 'vcf'
},
// This list only contains the mimetypes we currently supported
// We should make it more complete for further usages
_extensionToTypeMap: {
// Image
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'jpe': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'bmp': 'image/bmp',
// Audio
'mp3': 'audio/mpeg',
'm4a': 'audio/mp4',
'm4b': 'audio/mp4',
'm4p': 'audio/mp4',
'm4r': 'audio/mp4',
'aac': 'audio/aac',
'opus': 'audio/ogg',
'amr': 'audio/amr',
// Video
'mp4': 'video/mp4',
'mpeg': 'video/mpeg',
'mpg': 'video/mpeg',
'ogv': 'video/ogg',
'ogx': 'video/ogg',
'webm': 'video/webm',
'3gp': 'video/3gpp',
'3g2': 'video/3gpp2',
'ogg': 'video/ogg',
// Application
// If we want to support some extensions, like pdf, just add
'pdf': 'application/pdf',
// Text
'vcf': 'text/vcard'
},
_parseExtension: function(filename) {
var array = filename.split('.');
return array.length > 1 ? array.pop() : '';
},
isSupportedType: function(mimetype) {
return (mimetype in this._typeToExtensionMap);
},
isSupportedExtension: function(extension) {
return (extension in this._extensionToTypeMap);
},
isFilenameMatchesType: function(filename, mimetype) {
var extension = this._parseExtension(filename);
var guessedType = this.guessTypeFromExtension(extension);
return (guessedType == mimetype);
},
guessExtensionFromType: function(mimetype) {
return this._typeToExtensionMap[mimetype];
},
guessTypeFromExtension: function(extension) {
return this._extensionToTypeMap[extension];
},
// If mimetype is not in the supported list, we will try to
// predict the possible valid mimetype based on extension.
guessTypeFromFileProperties: function(filename, mimetype) {
var extension = this._parseExtension(filename);
var type = this.isSupportedType(mimetype) ?
mimetype : this.guessTypeFromExtension(extension);
return type || '';
},
// if mimetype is not supported, preserve the original extension
// and add the predict result as new extension.
// If both filename and mimetype are not supported, return the original
// filename.
ensureFilenameMatchesType: function(filename, mimetype) {
if (!this.isFilenameMatchesType(filename, mimetype)) {
var guessedExt = this.guessExtensionFromType(mimetype);
if (guessedExt) {
filename += '.' + guessedExt;
}
}
return filename;
}
};

View file

@ -0,0 +1,100 @@
/* exported MobileInfo, MobileOperator */
'use strict';
var MobileInfo = {
brazil: {
carriers: {
'00': 'NEXTEL',
'02': 'TIM', '03': 'TIM', '04': 'TIM',
'05': 'CLARO', '06': 'VIVO', '07': 'CTBC', '08': 'TIM',
'10': 'VIVO', '11': 'VIVO', '15': 'SERCOMTEL',
'16': 'OI', '23': 'VIVO', '24': 'OI', '31': 'OI',
'32': 'CTBC', '33': 'CTBC', '34': 'CTBC', '37': 'AEIOU'
},
regions: {
'11': 'SP', '12': 'SP', '13': 'SP', '14': 'SP', '15': 'SP', '16': 'SP',
'17': 'SP', '18': 'SP', '19': 'SP',
'21': 'RJ', '22': 'RJ', '24': 'RJ',
'27': 'ES', '28': 'ES',
'31': 'MG', '32': 'MG', '33': 'MG', '34': 'MG', '35': 'MG', '37': 'MG',
'38': 'MG',
'41': 'PR', '42': 'PR', '43': 'PR', '44': 'PR', '45': 'PR', '46': 'PR',
'47': 'SC', '48': 'SC', '49': 'SC',
'51': 'RS', '53': 'RS', '54': 'RS', '55': 'RS',
'61': 'DF',
'62': 'GO',
'63': 'TO',
'64': 'GO',
'65': 'MT', '66': 'MT',
'67': 'MS',
'68': 'AC',
'69': 'RO',
'71': 'BA', '73': 'BA', '74': 'BA', '75': 'BA', '77': 'BA',
'79': 'SE',
'81': 'PE',
'82': 'AL',
'83': 'PB',
'84': 'RN',
'85': 'CE',
'86': 'PI',
'87': 'PE',
'88': 'CE',
'89': 'PI',
'91': 'PA',
'92': 'AM',
'93': 'PA', '94': 'PA',
'95': 'RR',
'96': 'AP',
'97': 'AM',
'98': 'MA', '99': 'MA'
}
}
};
var MobileOperator = {
BRAZIL_MCC: '724',
BRAZIL_CELLBROADCAST_CHANNEL: 50,
userFacingInfo: function mo_userFacingInfo(mobileConnection) {
var network = mobileConnection.voice.network;
var iccid = mobileConnection.iccId;
var iccObj = navigator.mozIccManager.getIccById(iccid);
var iccInfo = iccObj ? iccObj.iccInfo : null;
var operator = network ? (network.shortName || network.longName) : null;
if (operator && iccInfo && iccInfo.isDisplaySpnRequired && iccInfo.spn) {
if (iccInfo.isDisplayNetworkNameRequired && operator !== iccInfo.spn) {
operator = operator + ' ' + iccInfo.spn;
} else {
operator = iccInfo.spn;
}
}
var carrier, region;
if (this.isBrazil(mobileConnection)) {
// We are in Brazil, It is legally required to show local info
// about current registered GSM network in a legally specified way.
var lac = mobileConnection.voice.cell.gsmLocationAreaCode % 100;
var carriers = MobileInfo.brazil.carriers;
var regions = MobileInfo.brazil.regions;
carrier = carriers[network.mnc] ||
(this.BRAZIL_MCC + network.mnc);
region = (regions[lac] ? regions[lac] + ' ' + lac : '');
}
return {
'operator': operator,
'carrier': carrier,
'region': region
};
},
isBrazil: function mo_isBrazil(mobileConnection) {
var cell = mobileConnection.voice.cell;
var net = mobileConnection.voice.network;
return net ?
(net.mcc === this.BRAZIL_MCC && cell && cell.gsmLocationAreaCode) :
null;
}
};

View file

@ -0,0 +1,13 @@
;'use strict';const ALWAYS_ASK_OPTION_VALUE=-1;var MultiSimActionButton=function MultiSimActionButton(button,callCallback,settingsKey,phoneNumberGetter){this._button=button;this._callCallback=callCallback;this._settingsKey=settingsKey;this._phoneNumberGetter=phoneNumberGetter;this._simPicker=null;this._button.addEventListener('click',this._click.bind(this));var self=this;LazyLoader.load(['/shared/js/settings_listener.js'],function(){SettingsListener.observe(settingsKey,0,self._settingsObserver.bind(self));});if(navigator.mozIccManager&&navigator.mozIccManager.iccIds.length>1){this._button.addEventListener('contextmenu',this._contextmenu.bind(this));this._simIndication=this._button.querySelector('.js-sim-indication');var telephony=navigator.mozTelephony;if(telephony){telephony.addEventListener('callschanged',self._updateUI.bind(self));}}};MultiSimActionButton.prototype._settingsObserver=function(cardIndex){this._defaultCardIndex=cardIndex;this._updateUI();if(this._clickQueued){this._clickQueued=false;this._click();}};MultiSimActionButton.prototype._getCardIndexIfLoaded=function(){if(window.TelephonyHelper){var inUseSim=window.TelephonyHelper.getInUseSim();if(inUseSim!==null){return inUseSim;}}
return this._defaultCardIndex;};MultiSimActionButton.prototype._click=function(event){if(event){event.preventDefault();}
var phoneNumber=this._phoneNumberGetter&&this._phoneNumberGetter();if(!navigator.mozIccManager||phoneNumber===''){return;}
if(event){event.stopImmediatePropagation();}
if(navigator.mozIccManager.iccIds.length===0){this.performAction(0);return;}
var cardIndex=this._getCardIndexIfLoaded();if(cardIndex===undefined){this._clickQueued=true;return;}
if(cardIndex===ALWAYS_ASK_OPTION_VALUE){this._getOrPickSim(cardIndex,phoneNumber);}else{this.performAction(cardIndex);}};MultiSimActionButton.prototype._getOrPickSim=function(cardIndex,phoneNumber){var self=this;LazyLoader.load(['/shared/elements/gaia_sim_picker/script.js'],function(){self._simPicker=document.getElementById('sim-picker');self._simPicker.getOrPick(cardIndex,phoneNumber,self.performAction.bind(self));});};MultiSimActionButton.prototype._updateUI=function(){var cardIndex=this._getCardIndexIfLoaded();if(cardIndex>=0&&navigator.mozIccManager&&navigator.mozIccManager.iccIds.length>1){if(this._simIndication){var self=this;var l10nId=this._simIndication.dataset.l10nId||'gaia-sim-picker-button';navigator.mozL10n.setAttributes(self._simIndication,l10nId,{n:cardIndex+1});}
document.body.classList.add('has-preferred-sim');}else{document.body.classList.remove('has-preferred-sim');}};MultiSimActionButton.prototype._contextmenu=function(event){var phoneNumber=this._phoneNumberGetter&&this._phoneNumberGetter();if(!navigator.mozIccManager||navigator.mozIccManager.iccIds.length===0||phoneNumber===''||event.target.disabled||(window.TelephonyHelper&&window.TelephonyHelper.getInUseSim()!==null)){return;}
if(this._getCardIndexIfLoaded()===undefined){return;}
if(event){event.preventDefault();}
this._getOrPickSim(this._getCardIndexIfLoaded(),phoneNumber);};MultiSimActionButton.prototype.performAction=function(cardIndex){var phoneNumber=this._phoneNumberGetter&&this._phoneNumberGetter();if(phoneNumber===''){return;}
if(cardIndex===undefined){cardIndex=this._getCardIndexIfLoaded();}
this._callCallback(phoneNumber,cardIndex);};

View file

@ -0,0 +1,671 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright © 2013, Deutsche Telekom, Inc. */
/* globals MozNDEFRecord, TextEncoder, TextDecoder, StringHelper */
/* exported NDEF, NfcBuffer, NfcUtils */
'use strict';
(function(exports) {
/**
* NDEF (NFC Data Exchange Format) contants and common values.
*/
const NDEF = {
MB: 1 << 7,
ME: 1 << 6,
CF: 1 << 5,
SR: 1 << 4,
IL: 1 << 3,
TNF: 0x07,
TNF_EMPTY: 'empty',
TNF_WELL_KNOWN: 'well-known',
TNF_MIME_MEDIA: 'media-type',
TNF_ABSOLUTE_URI: 'absolute-uri',
TNF_EXTERNAL_TYPE: 'external',
TNF_UNKNOWN: 'unknown',
TNF_UNCHANGED: 'unchanged',
RTD_TEXT: 0,
RTD_URI: 0,
RTD_SMART_POSTER: 0,
RTD_ALTERNATIVE_CARRIER: 0,
RTD_COLLISION_RESOLUTION: 0,
RTD_HANDOVER_CARRIER: 0,
RTD_HANDOVER_REQUEST: 0,
RTD_HANDOVER_SELECT: 0,
RTD_TEXT_IANA_LENGTH: 0x3F,
RTD_TEXT_ENCODING: 0x80,
RTD_TEXT_UTF8: 0,
RTD_TEXT_UTF16: 1,
CPS_INACTIVE: 0,
CPS_ACTIVE: 1,
CPS_ACTIVATING: 2,
CPS_UNKNOWN: 3,
MIME_BLUETOOTH_OOB: 0,
SMARTPOSTER_ACTION: 0,
// Action Record Values:
DO_ACTION: 0x00,
SAVE_FOR_LATER_ACTION: 0x01,
OPEN_FOR_EDITING_ACTION: 0x02,
RFU_ACTION: 0x03, // Reserved from 0x03 to 0xFF
URIS: [],
init: function ndef_init() {
var nfcUtils = new NfcUtils();
this.RTD_TEXT = nfcUtils.fromUTF8('T');
this.RTD_URI = nfcUtils.fromUTF8('U');
this.RTD_SMART_POSTER = nfcUtils.fromUTF8('Sp');
this.RTD_ALTERNATIVE_CARRIER = nfcUtils.fromUTF8('ac');
this.RTD_COLLISION_RESOLUTION = nfcUtils.fromUTF8('cr');
this.RTD_HANDOVER_CARRIER = nfcUtils.fromUTF8('Hc');
this.RTD_HANDOVER_REQUEST = nfcUtils.fromUTF8('Hr');
this.RTD_HANDOVER_SELECT = nfcUtils.fromUTF8('Hs');
this.MIME_BLUETOOTH_OOB =
nfcUtils.fromUTF8('application/vnd.bluetooth.ep.oob');
this.MIME_VCARD_STR_ARR =
['text/vcard', 'text/x-vCard', 'text/x-vcard'];
this.SMARTPOSTER_ACTION = nfcUtils.fromUTF8('act');
this.URIS[0x00] = '';
this.URIS[0x01] = 'http://www.';
this.URIS[0x02] = 'https://www.';
this.URIS[0x03] = 'http://';
this.URIS[0x04] = 'https://';
this.URIS[0x05] = 'tel:';
this.URIS[0x06] = 'mailto:';
this.URIS[0x07] = 'ftp://anonymous:anonymous@';
this.URIS[0x08] = 'ftp://ftp.';
this.URIS[0x09] = 'ftps://';
this.URIS[0x0A] = 'sftp://';
this.URIS[0x0B] = 'smb://';
this.URIS[0x0C] = 'nfs://';
this.URIS[0x0D] = 'ftp://';
this.URIS[0x0E] = 'dav://';
this.URIS[0x0F] = 'news:';
this.URIS[0x10] = 'telnet://';
this.URIS[0x11] = 'imap:';
this.URIS[0x12] = 'rtsp://';
this.URIS[0x13] = 'urn:';
this.URIS[0x14] = 'pop:';
this.URIS[0x15] = 'sip:';
this.URIS[0x16] = 'sips:';
this.URIS[0x17] = 'tftp:';
this.URIS[0x18] = 'btspp://';
this.URIS[0x19] = 'btl2cap://';
this.URIS[0x1A] = 'btgoep://';
this.URIS[0x1B] = 'tcpobex://';
this.URIS[0x1C] = 'irdaobex://';
this.URIS[0x1D] = 'file://';
this.URIS[0x1E] = 'urn:epc:id:';
this.URIS[0x1F] = 'urn:epc:tag:';
this.URIS[0x20] = 'urn:epc:pat:';
this.URIS[0x21] = 'urn:epc:raw:';
this.URIS[0x22] = 'urn:epc:';
this.URIS[0x23] = 'urn:nfc:';
},
payload: {
/**
* Decodes NDEF record payload
* @see NFCForum-TS-NDEF_1.0
* @param String tnf - record TNF
* @param {Uint8Array} type - record type
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload or null if invalid
*/
decode: function decode(tnf, type, payload) {
var nfcUtils = new NfcUtils();
var decodedPayload = { type: 'empty' };
switch (tnf) {
case NDEF.TNF_WELL_KNOWN:
decodedPayload = this.decodeWellKnown(type, payload);
break;
case NDEF.TNF_MIME_MEDIA:
decodedPayload = this.decodeMIME(type, payload);
break;
case NDEF.TNF_ABSOLUTE_URI:
case NDEF.TNF_EXTERNAL_TYPE:
decodedPayload = { type: nfcUtils.toUTF8(type) };
break;
case NDEF.TNF_UNKNOWN:
decodedPayload = {};
break;
case NDEF.TNF_UNCHANGED:
decodedPayload = null;
break;
}
return decodedPayload;
},
/**
* Decodes TNF Well Know NDEF record payload
* @see NFCForum-TS-NDEF_1.0
* @param {Uint8Array} type - record type
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload or null if invalid
*/
decodeWellKnown: function decodeWellKnown(type, payload) {
var nfcUtils = new NfcUtils();
if (nfcUtils.equalArrays(type, NDEF.RTD_TEXT)) {
return this.decodeText(payload);
} else if (nfcUtils.equalArrays(type, NDEF.RTD_URI)) {
return this.decodeURI(payload);
} else if (nfcUtils.equalArrays(type, NDEF.RTD_SMART_POSTER)) {
return this.decodeSmartPoster(payload);
}
return null;
},
/**
* Decodes TNF Well Known RTD Text NDEF record payload
* @see NFCForum-TS-RTD_Text_1.0
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload
*/
decodeText: function decodeText(payload) {
var nfcUtils = new NfcUtils();
var decoded = { type: 'text' };
var langLen = payload[0] & NDEF.RTD_TEXT_IANA_LENGTH;
decoded.language = nfcUtils.toUTF8(payload.subarray(1, langLen + 1));
var encoding = (payload[0] & NDEF.RTD_TEXT_ENCODING) !== 0 ? 1 : 0;
if (encoding === NDEF.RTD_TEXT_UTF8) {
decoded.text = nfcUtils.toUTF8(payload.subarray(langLen + 1));
decoded.encoding = 'UTF-8';
} else if (encoding === NDEF.RTD_TEXT_UTF16) {
decoded.text = nfcUtils.UTF16BytesToStr(payload.subarray(langLen +
1));
decoded.encoding = 'UTF-16';
}
return decoded;
},
/**
* Decodes TNF Well Known RTD URI NDEF record payload
* @see NFCForum-TS-RTD_URI_1.0
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload or null if invalid
*/
decodeURI: function decodeURI(payload) {
var nfcUtils = new NfcUtils();
var prefix = NDEF.URIS[payload[0]];
if (prefix === undefined) {
return null;
}
var suffix = nfcUtils.toUTF8(payload.subarray(1));
return { type: 'uri', uri: prefix + suffix };
},
/**
* Decodes TNF Well Known RTD Smart Poster NDEF record payload
* @see NFCForum-SmartPoster_RTD_1.0
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload
*/
decodeSmartPoster: function decodeSmartPoster(payload) {
var nfcUtils = new NfcUtils();
var buffer = new NfcBuffer(payload);
var records = nfcUtils.parseNDEF(buffer);
// First, decode URI. It's treated specially, because it's the only
// mandatory record in a smart poster.
var URIRecords = records.filter(function(record) {
return nfcUtils.equalArrays(record.type, NDEF.RTD_URI);
});
if (URIRecords.length !== 1) {
return null;
}
var uriPoster = {
type: 'smartposter',
uri: NDEF.payload.decodeURI(URIRecords[0].payload).uri
};
// Now decode all other records and attach their data to poster.
return records.reduce((poster, record) => {
var typeStr = nfcUtils.toUTF8(record.type);
if (nfcUtils.equalArrays(record.type, NDEF.RTD_TEXT)) {
poster.text = poster.text || {};
var textData = NDEF.payload.decodeText(record.payload);
if (poster.text[textData.language]) {
// According to NFCForum-SmartPoster_RTD_1.0 3.3.2,
// there MUST NOT be two or more records with
// the same language identifier.
return null;
}
poster.text[textData.language] = textData.text;
} else if ('act' === typeStr) {
poster.action = record.payload[0];
} else if (NDEF.TNF_MIME_MEDIA === record.tnf) {
poster.icons = poster.icons || [];
poster.icons.push({
type: nfcUtils.toUTF8(record.type),
bytes: record.payload
});
}
return poster;
}, uriPoster);
},
/**
* Decodes TNF MIME Media NDEF Record payload
* @see NFCForum-TS-NDEF_1.0
* @param {Uint8Array} type - record mime-type
* @returns {Object} data - decoded payload
*/
decodeMIME: function docodeMIME(type, payload) {
var nfcUtils = new NfcUtils();
var typeStr = (typeof type === 'string') ? type : nfcUtils.toUTF8(type);
if (NDEF.MIME_VCARD_STR_ARR.indexOf(typeStr) !== -1) {
return {
type: 'text/vcard',
blob: new Blob([nfcUtils.toUTF8(payload)], {type: 'text/vcard'})
};
}
return { type: typeStr };
}
}
};
/**
* NfcBuffer wraps Uint8Array providing helper methods for accessing
* its elements.
*
* @param {Array} array Either a plain Array or an Uint8Array instance.
* @constructor
*/
function NfcBuffer(array) {
this.uint8array = new Uint8Array(array);
this.offset = 0;
}
NfcBuffer.prototype = {
uint8array: null,
offset: 0,
/**
* Returns a single octet from the buffer advancing position
* by one.
*
* @returns {Number} Single element from the buffer.
* @memberof NfcBuffer
*/
getOctet: function getOctet() {
if (this.offset == this.uint8array.length) {
throw Error('NfcBuffer too small');
}
return this.uint8array[this.offset++];
},
/**
* Returns an Uint8Array containing first len elements from the
* buffer.
*
* @param {Number} len Number of elements to return.
* @returns {Uint8Array} Array of len first elements from the buffer.
* @memberof NfcBuffer
*/
getOctetArray: function getOctetArray(len) {
if (typeof len !== 'number' || len < 0 ||
this.offset + len > this.uint8array.length) {
throw Error('NfcBuffer too small');
}
return this.uint8array.subarray(this.offset, this.offset += len);
},
/**
* Discards len elements from the buffer.
*
* @param {Number} len Number of elements to skip over.
* @memberof NfcBuffer
*/
skip: function skip(len) {
if (typeof len !== 'number' || len < 0 ||
this.offset + len > this.uint8array.length) {
throw Error('NfcBuffer too small');
}
this.offset += len;
},
/**
* Returns a single octet from the buffer, but will not
* advance a position.
*
* @returns {Number} Single element from the buffer.
* @memberof NfcBuffer
*/
peek: function peek() {
if (this.offset === this.uint8array.length) {
throw Error('NfcBuffer too small');
}
return this.uint8array[this.offset];
}
};
/**
* NfcUtils offers a set of utility function to handle NDEF messages according
* to NFCForum-TS-NDEF_1.0.
*
* @class NfcUtils
*/
function NfcUtils() { }
NfcUtils.prototype = {
/**
* Returns an Uint8Array representation of a string.
*
* @param {String} str String to convert.
* @return {Uint8Array}
* @memberof NfcUtils
*/
fromUTF8: function fromUTF8(str) {
if (!str) {
return null;
}
var enc = new TextEncoder('utf-8');
return enc.encode(str);
},
/**
* Returns a string representation of an Uint8Array.
*
* @param {Uint8Array} a Uint8Array instance.
* @return {String}
* @memberof NfcUtils
*/
toUTF8: function toUTF8(a) {
if (!a) {
return null;
}
// BOM removal
if (this.equalArrays(a.subarray(0, 3), [0xEF, 0xBB, 0xBF])) {
a = a.subarray(3);
}
var dec = new TextDecoder('utf-8');
return dec.decode(a);
},
/**
* Decodes UTF-16 bytes array into a String
*
* @param {Uint8Array} array containing UTF-16 encoded bytes
* @return {string}
* @memberof NfcUtils
*/
UTF16BytesToStr: function UTF16BytesToStr(array) {
if (!array) {
return null;
}
// if BOM not present Big-endian should be used
// NFCForum-TS-RTD_Text_1.0
var le = false;
var possibleBom = array.subarray(0, 2);
if (this.equalArrays(possibleBom, [0xFF, 0xFE])) {
array = array.subarray(2);
le = true;
} else if (this.equalArrays(possibleBom, [0xFE, 0xFF])) {
array = array.subarray(2);
}
var encoding = (le) ? 'utf-16le' : 'utf-16be';
var dec = new TextDecoder(encoding);
return dec.decode(array);
},
/**
* Ecodes string into Uint8Array conating UTF-16 BE bytes without BOM.
* @param {string}
* @return {Uint8Array}
* @memberof NfcUtils
*/
strToUTF16Bytes: function strToUTF16Bytes(str) {
if (!str) {
return null;
}
var enc = new TextEncoder('utf-16be');
return enc.encode(str);
},
/**
* Compares arrays, array-like objects and typed arrays for equality.
*
* @param {Array} a1 First array
* @param {Array} a2 Second array
* @return {Boolean} True if arrays are equal, false otherwise.
* @memberof NfcUtils
*/
equalArrays: function equalArrays(a1, a2) {
if (!a1 || !a2) {
return false;
}
if (a1.length !== a2.length) {
return false;
}
for (var i = 0; i < a1.length; i++) {
if (a1[i] !== a2[i]) {
return false;
}
}
return true;
},
/**
* Takes an array of NDEFRecords and returns an Array of octets
* representing the binary encoding of the NDEF message according
* to the NFC Forum.
* Note: support for message chunking is not implemented.
*
* @param {Array} ndefRecords Array of NDEF records to encode.
* @return {Array} Byte array containing encoded NDEF message.
* @memberof NfcUtils
*/
encodeNDEF: function encodeNDEF(records) {
var result = [];
function getTnfNum(tnfString) {
var tnf = [
NDEF.TNF_EMPTY, NDEF.TNF_WELL_KNOWN, NDEF.TNF_MIME_MEDIA,
NDEF.TNF_ABSOLUTE_URI, NDEF.TNF_EXTERNAL_TYPE, NDEF.TNF_UNKNOWN,
NDEF.TNF_UNCHANGED, NDEF.TNF_RESERVED];
return tnf.indexOf(tnfString);
}
records.forEach((record, recordIndex) => {
record.payload = record.payload || [];
record.id = record.id || [];
record.type = record.type || [];
var payloadLen = record.payload.length;
var idLen = record.id.length;
var typeLen = record.type.length;
var firstOctet = getTnfNum(record.tnf) & 0x07;
firstOctet |= (recordIndex === 0) ? NDEF.MB : 0;
firstOctet |= (recordIndex === records.length - 1) ? NDEF.ME : 0;
firstOctet |= (idLen > 0) ? NDEF.IL : 0;
firstOctet |= (payloadLen <= 0xFF) ? NDEF.SR : 0;
result.push(firstOctet);
result.push(typeLen);
for (var p = (payloadLen > 0xFF ? 3 : 0); p >= 0; p -= 1) {
result.push((payloadLen >>> (8 * p)) & 0xFF);
}
if (idLen > 0) {
result.push(idLen);
}
result.push.apply(result, record.type);
result.push.apply(result, record.id);
result.push.apply(result, record.payload);
});
return result;
},
/**
* Parses a NDEF message contained in a NfcBuffer instance according to
* NFCForum-TS-NDEF_1.0.
*
* Usage:
* var buf = new NfcBuffer(<Uint8Array that contains the raw NDEF
* message>);
* var ndefMessage = NdefCodec.parse(buf);
*
* @param {NfcBuffer} Buffer containing the NDEF message.
* @return {Array} Array of MozNDEFRecord instances or null if message
* couldn't be parsed.
* @memberof NfcUtils
*/
parseNDEF: function parseNDEF(buffer) {
try {
return this._doParseNDEF(buffer);
} catch (err) {
console.error('[NfcUtils]: ' + err);
return null;
}
},
_doParseNDEF: function doParseNDEF(buffer) {
var records = [];
var isFirstRecord = true;
var firstOctet;
do {
firstOctet = buffer.peek();
if (isFirstRecord && !(firstOctet & NDEF.MB)) {
throw Error('MB bit not set in first NDEF record');
}
if (!isFirstRecord && (firstOctet & NDEF.MB)) {
throw Error('MB can only be set for the first record');
}
if (firstOctet & NDEF.CF) {
throw Error('Chunked payloads are not supported');
}
records.push(this._parseNDEFRecord(buffer));
isFirstRecord = false;
} while (!(firstOctet & NDEF.ME));
if (buffer.offset < buffer.uint8array.length) {
throw Error('ME bit set on non-last record');
}
return records;
},
_parseNDEFRecord: function parseNDEFRecord(buffer) {
var tnfArray = [
NDEF.TNF_EMPTY, NDEF.TNF_WELL_KNOWN, NDEF.TNF_MIME_MEDIA,
NDEF.TNF_ABSOLUTE_URI, NDEF.TNF_EXTERNAL_TYPE, NDEF.TNF_UNKNOWN,
NDEF.TNF_UNCHANGED, NDEF.TNF_RESERVED];
var firstOctet = buffer.getOctet();
var typeLen = buffer.getOctet();
var payloadLen = buffer.getOctet();
if (!(firstOctet & NDEF.SR)) {
for (var i = 0; i < 3; i++) {
payloadLen <<= 8;
payloadLen |= buffer.getOctet();
}
}
var idLen = 0;
if (firstOctet & NDEF.IL) {
idLen = buffer.getOctet();
}
var tnf = firstOctet & NDEF.TNF;
var type = buffer.getOctetArray(typeLen);
var id = buffer.getOctetArray(idLen);
var payload = buffer.getOctetArray(payloadLen);
return new MozNDEFRecord({tnf: tnfArray[tnf],
type: type || undefined,
id: id || undefined,
payload: payload || undefined});
},
/**
* Parses a URL string to MozNDEFRecord format.
*
* @param {String} url url string.
* @return {Array} Array of MozNDEFRecord instances.
* @memberof NfcUtils
*/
parseURIString: function parseURIString(url) {
if (!url) {
return;
}
var content = url;
for (var i = 1; i < NDEF.URIS.length; i++) {
var len = NDEF.URIS[i].length;
if (url.substring(0, len) == NDEF.URIS[i]) {
var uriPayload = url.substring(len);
content = String.fromCharCode(i) + uriPayload;
break;
}
}
var payload = StringHelper.fromUTF8(content);
var record = new MozNDEFRecord({tnf: NDEF.TNF_WELL_KNOWN,
type: NDEF.RTD_URI,
payload: payload});
if (!record) {
return null;
}
return [record];
}
};
NDEF.init();
exports.NfcBuffer = NfcBuffer;
exports.NfcUtils = NfcUtils;
exports.NDEF = NDEF;
})(window);

View file

@ -0,0 +1,8 @@
;;(function(window){'use strict';window.NotificationHelper={getIconURI:function nh_getIconURI(app,entryPoint){var icons=app.manifest.icons;if(entryPoint){icons=app.manifest.entry_points[entryPoint].icons;}
if(!icons){return null;}
var sizes=Object.keys(icons).map(function parse(str){return parseInt(str,10);});sizes.sort(function(x,y){return y-x;});var HVGA=document.documentElement.clientWidth<480;var index=sizes[HVGA?sizes.length-1:0];return app.installOrigin+icons[index];},send:function nh_send(titleL10n,options){return Promise.all([titleL10n,options.bodyL10n].map(getL10n)).then(([title,body])=>{if(body){options.body=body;}
options.lang=document.documentElement.getAttribute('lang');var notification=new window.Notification(title,options);if(options.closeOnClick!==false){notification.addEventListener('click',function nh_click(){notification.removeEventListener('click',nh_click);notification.close();});}
return notification;});},};function getL10n(l10nAttrs){if(!l10nAttrs){return;}
if(typeof l10nAttrs==='string'){return document.l10n.formatValue(l10nAttrs);}
if(typeof l10nAttrs.raw==="string"){return l10nAttrs.raw;}
return document.l10n.formatValue(l10nAttrs.id,l10nAttrs.args);}})(this);

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