Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
|
|
@ -0,0 +1 @@
|
|||
;;;;;;;window.COMPONENTS_BASE_URL='/shared/elements/';
|
||||
|
|
@ -0,0 +1,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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
373
assets/webapps/communications.gaiamobile.org/shared/elements/gaia-header/dist/gaia-header.js
vendored
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
;;;;;;(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GaiaHeader=f()}})(function(){var define,module,exports;return(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){;(function(define){define(function(require,exports,module){'use strict';var debug=0?console.log.bind(console):function(){};var cache={};var MIN=16;var MAX=24;var BUFFER=3;module.exports=function(config){debug('font fit',config);var space=config.space-BUFFER;var min=config.min||MIN;var max=config.max||MAX;var text=trim(config.text);var fontSize=max;var textWidth;var font;do{font=config.font.replace(/\d+px/,fontSize+'px');textWidth=getTextWidth(text,font);}while(textWidth>space&&fontSize!==min&&fontSize--);return{textWidth:textWidth,fontSize:fontSize,overflowing:textWidth>space};};function getTextWidth(text,font){var ctx=getCanvasContext(font);var width=ctx.measureText(text).width;debug('got text width',width);return width;}
|
||||
function getCanvasContext(font){debug('get canvas context',font);var cached=cache[font];if(cached){return cached;}
|
||||
var canvas=document.createElement('canvas');canvas.setAttribute('moz-opaque','true');canvas.setAttribute('width','1px');canvas.setAttribute('height','1px');debug('created canvas',canvas);var ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.font=font;return cache[font]=ctx;}
|
||||
function trim(text){return text.replace(/\s+/g,' ').trim();}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('font-fit',this));},{}],2:[function(require,module,exports){;(function(define){'use strict';define(function(require,exports,module){var textContent=Object.getOwnPropertyDescriptor(Node.prototype,'textContent');var innerHTML=Object.getOwnPropertyDescriptor(Element.prototype,'innerHTML');var removeAttribute=Element.prototype.removeAttribute;var setAttribute=Element.prototype.setAttribute;var noop=function(){};exports.register=function(name,props){var baseProto=getBaseProto(props.extends);var template=props.template||baseProto.templateString;var extensible=props.extensible=props.hasOwnProperty('extensible')?props.extensible:true;delete props.extends;if(template){if(extensible&&props.template){props.templateString=props.template;}
|
||||
var output=processCss(template,name);props.template=document.createElement('template');props.template.innerHTML=output.template;props.lightCss=output.lightCss;props.globalCss=props.globalCss||'';props.globalCss+=output.globalCss;}
|
||||
injectGlobalCss(props.globalCss);delete props.globalCss;var descriptors=mixin(props.attrs||{},base.descriptors);props._attrs=props.attrs;delete props.attrs;var proto=createProto(baseProto,props);Object.defineProperties(proto,descriptors);try{return document.registerElement(name,{prototype:proto});}catch(e){if(e.name!=='NotSupportedError'){throw e;}}};var base={properties:{GaiaComponent:true,attributeChanged:noop,attached:noop,detached:noop,created:noop,createdCallback:function(){if(this.dirObserver){addDirObserver();}
|
||||
injectLightCss(this);this.created();},attributeChangedCallback:function(name,from,to){var prop=toCamelCase(name);if(this._attrs&&this._attrs[prop]){this[prop]=to;}
|
||||
this.attributeChanged(name,from,to);},attachedCallback:function(){if(this.dirObserver){this.setInnerDirAttributes=setInnerDirAttributes.bind(null,this);document.addEventListener('dirchanged',this.setInnerDirAttributes);}
|
||||
this.attached();},detachedCallback:function(){if(this.dirObserver){document.removeEventListener('dirchanged',this.setInnerDirAttributes);}
|
||||
this.detached();},setupShadowRoot:function(){if(!this.template){return;}
|
||||
var node=document.importNode(this.template.content,true);this.createShadowRoot().appendChild(node);if(this.dirObserver){setInnerDirAttributes(this);}
|
||||
return this.shadowRoot;},setAttr:function(name,value){var internal=this.shadowRoot.firstElementChild;setAttribute.call(internal,name,value);setAttribute.call(this,name,value);},removeAttr:function(name){var internal=this.shadowRoot.firstElementChild;removeAttribute.call(internal,name);removeAttribute.call(this,name);}},descriptors:{textContent:{set:function(value){textContent.set.call(this,value);if(this.lightStyle){this.appendChild(this.lightStyle);}},get:function(){return textContent.get();}},innerHTML:{set:function(value){innerHTML.set.call(this,value);if(this.lightStyle){this.appendChild(this.lightStyle);}},get:innerHTML.get}}};var defaultPrototype=createProto(HTMLElement.prototype,base.properties);function getBaseProto(proto){if(!proto){return defaultPrototype;}
|
||||
proto=proto.prototype||proto;return!proto.GaiaComponent?createProto(proto,base.properties):proto;}
|
||||
function createProto(proto,props){return mixin(Object.create(proto),props);}
|
||||
var hasShadowCSS=(function(){var div=document.createElement('div');try{div.querySelector(':host');div.querySelector('::content');return true;}
|
||||
catch(e){return false;}})();var regex={shadowCss:/(?:\:host|\:\:content)[^{]*\{[^}]*\}/g,':host':/(?:\:host)/g,':host()':/\:host\((.+)\)(?: \:\:content)?/g,':host-context':/\:host-context\((.+)\)([^{,]+)?/g,'::content':/(?:\:\:content)/g};function processCss(template,name){var globalCss='';var lightCss='';if(!hasShadowCSS){template=template.replace(regex.shadowCss,function(match){var hostContext=regex[':host-context'].exec(match);if(hostContext){globalCss+=match.replace(regex['::content'],'').replace(regex[':host-context'],'$1 '+name+'$2').replace(/ +/g,' ');}else{lightCss+=match.replace(regex[':host()'],name+'$1').replace(regex[':host'],name).replace(regex['::content'],name);}
|
||||
return'';});}
|
||||
return{template:template,lightCss:lightCss,globalCss:globalCss};}
|
||||
function injectGlobalCss(css){if(!css){return;}
|
||||
var style=document.createElement('style');style.innerHTML=css.trim();headReady().then(function(){document.head.appendChild(style);});}
|
||||
function headReady(){return new Promise(function(resolve){if(document.head){return resolve();}
|
||||
window.addEventListener('load',function fn(){window.removeEventListener('load',fn);resolve();});});}
|
||||
function injectLightCss(el){if(hasShadowCSS){return;}
|
||||
var stylesheet=el.querySelector('style');if(!stylesheet){stylesheet=document.createElement('style');stylesheet.setAttribute('scoped','');stylesheet.appendChild(document.createTextNode(el.lightCss));el.appendChild(stylesheet);}
|
||||
el.lightStyle=stylesheet;}
|
||||
function toCamelCase(string){return string.replace(/-(.)/g,function replacer(string,p1){return p1.toUpperCase();});}
|
||||
var dirObserver;function setInnerDirAttributes(component){var dir=component.dir||document.dir;Array.from(component.shadowRoot.children).forEach(element=>{if(element.nodeName!=='STYLE'){element.dir=dir;}});}
|
||||
function addDirObserver(){if(dirObserver){return;}
|
||||
dirObserver=new MutationObserver(onChanged);dirObserver.observe(document.documentElement,{attributeFilter:['dir'],attributes:true});function onChanged(mutations){document.dispatchEvent(new Event('dirchanged'));}}
|
||||
function mixin(target,source){for(var key in source){target[key]=source[key];}
|
||||
return target;}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-component',this));},{}],3:[function(require,module,exports){(function(define){'use strict';define(function(require,exports,module){var base=window.GAIA_ICONS_BASE_URL||window.COMPONENTS_BASE_URL||'bower_components/';if(!document.documentElement){window.addEventListener('load',load);}else{load();}
|
||||
function load(){if(isLoaded()){return;}
|
||||
var link=document.createElement('link');link.rel='stylesheet';link.type='text/css';link.href=base+'gaia-icons/gaia-icons.css';document.head.appendChild(link);exports.loaded=true;}
|
||||
function isLoaded(){return exports.loaded||document.querySelector('link[href*=gaia-icons]')||document.documentElement.classList.contains('gaia-icons-loaded');}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-icons',this));},{}],4:[function(require,module,exports){;(function(define){'use strict';define(function(require,exports,module){var component=require('gaia-component');var fontFit=require('font-fit');require('gaia-icons');var debug=0?console.log.bind(console):function(){};const KNOWN_ACTIONS={menu:'menu',back:'back',close:'close'};const TITLE_FONT='italic 300 24px FiraSans';const TITLE_PADDING=10;const MINIMUM_FONT_SIZE_CENTERED=20;const MINIMUM_FONT_SIZE_UNCENTERED=16;const MAXIMUM_FONT_SIZE=23;module.exports=component.register('gaia-header',{extensible:false,dirObserver:true,created:function(){debug('created');this.setupShadowRoot();this.els={actionButton:this.shadowRoot.querySelector('.action-button'),titles:this.getElementsByTagName('h1')};this.els.actionButton.addEventListener('click',e=>this.onActionButtonClick(e));this.observer=new MutationObserver(this.onMutation.bind(this));this.ignoreDir=this.hasAttribute('ignore-dir');this.titleEnd=this.getAttribute('title-end');this.titleStart=this.getAttribute('title-start');this.noFontFit=this.getAttribute('no-font-fit');this.notFlush=this.hasAttribute('not-flush');this.action=this.getAttribute('action');this.unresolved={};this.pending={};this._resizeThrottlingId=null;this.onResize=this.onResize.bind(this);},attached:function(){debug('attached');this.runFontFitSoon();this.observerStart();window.addEventListener('resize',this.onResize);},detached:function(){debug('detached');window.removeEventListener('resize',this.onResize);this.observerStop();this.clearPending();},clearPending:function(){for(var key in this.pending){this.pending[key].clear();delete this.pending[key];}
|
||||
window.cancelAnimationFrame(this._resizeThrottlingId);this._resizeThrottlingId=null;},runFontFit:function(){debug('run font-fit');if(this.noFontFit){return Promise.resolve();}
|
||||
var titles=this.els.titles;var space=this.getTitleSpace();var styles=[].map.call(titles,el=>this.getTitleStyle(el,space));return this.setTitleStylesSoon(styles);},runFontFitSoon:function(){debug('run font-fit soon');if(this.pending.runFontFitSoon){return;}
|
||||
this.pending.runFontFitSoon=this.nextTick(()=>{delete this.pending.runFontFitSoon;this.runFontFit();});},getTitleStyle:function(el,space){debug('get el style',el,space);var text=el.textContent;var styleId=space.start+text+space.end+'#'+space.value;if(!text||!text.trim()){return debug('exit: no text');}
|
||||
if(getStyleId(el)===styleId){return debug('exit: no change');}
|
||||
var marginStart=this.getTitleMarginStart();var textSpace=space.value-Math.abs(marginStart);var fontFitResult=this.fontFit(text,textSpace,{min:MINIMUM_FONT_SIZE_CENTERED});var overflowing=fontFitResult.overflowing;var padding={start:0,end:0};if(overflowing){debug('title overflowing');padding.start=!space.start?TITLE_PADDING:0;padding.end=!space.end?TITLE_PADDING:0;textSpace=space.value-padding.start-padding.end;fontFitResult=this.fontFit(text,textSpace);marginStart=0;}
|
||||
return{id:styleId,fontSize:fontFitResult.fontSize,marginStart:marginStart,overflowing:overflowing,padding:padding};},setTitleStylesSoon:function(styles){debug('set title styles soon',styles);var key='setStyleTitlesSoon';this._titleStyles=styles;if(this.unresolved[key]){return this.unresolved[key];}
|
||||
this.unresolved[key]=new Promise((resolve)=>{this.pending[key]=this.nextTick(()=>{var styles=this._titleStyles;var els=this.els.titles;[].forEach.call(els,(el,i)=>{if(!styles[i]){return debug('exit');}
|
||||
this.setTitleStyle(el,styles[i]);});delete this._titleStyles;delete this.unresolved[key];delete this.pending[key];resolve();});});},setTitleStyle:function(el,style){debug('set title style',style);this.observerStop();if(this.ignoreDir){el.style.marginLeft=style.marginStart+'px';el.style.paddingLeft=style.padding.start+'px';el.style.paddingRight=style.padding.end+'px';}else{el.style.marginInlineStart=style.marginStart+'px';el.style.paddingInlineStart=style.padding.start+'px';el.style.paddingInlineEnd=style.padding.end+'px';}
|
||||
el.style.fontSize=style.fontSize+'px';setStyleId(el,style.id);this.observerStart();},fontFit:function(text,space,opts={}){debug('font fit:',text,space,opts);var fontFitArgs={font:TITLE_FONT,min:opts.min||MINIMUM_FONT_SIZE_UNCENTERED,max:MAXIMUM_FONT_SIZE,text:text,space:space};return fontFit(fontFitArgs);},observerStart:function(){if(this.observing){return;}
|
||||
this.observer.observe(this,{childList:true,attributes:true,subtree:true});this.observing=true;debug('observer started');},observerStop:function(){if(!this.observing){return;}
|
||||
this.observer.disconnect();this.observing=false;debug('observer stopped');},onResize:function(e){debug('onResize',this._resizeThrottlingId);if(this._resizeThrottlingId!==null){return;}
|
||||
this._resizeThrottlingId=window.requestAnimationFrame(()=>{this._resizeThrottlingId=null;this.runFontFitSoon();});},onMutation:function(mutations){debug('on mutation',mutations);if(!this.pending.runFontFitSoon){this.runFontFit();}},getTitleSpace:function(){var start=this.titleStart;var end=this.titleEnd;var space=this.getWidth()-start-end;var result={value:space,start:start,end:end};debug('get title space',result);return result;},getWidth:function(){var value=this.notFlush?this.clientWidth:window.innerWidth;debug('get width',value);return value;},triggerAction:function(){if(this.action){this.els.actionButton.click();}},onActionButtonClick:function(){debug('action button click');var config={detail:{type:this.action}};var e=new CustomEvent('action',config);setTimeout(()=>this.dispatchEvent(e));},getTitleMarginStart:function(){var start=this.titleStart;var end=this.titleEnd;var marginStart=end-start;debug('get title margin start',marginStart);return marginStart;},getButtonsBeforeTitle:function(){var children=this.children;var l=children.length;var els=[];for(var i=0;i<l;i++){var el=children[i];if(el.tagName==='H1'){break;}
|
||||
if(!contributesToLayout(el)){continue;}
|
||||
els.push(el);}
|
||||
if(this.action){els.push(this.els.actionButton);}
|
||||
return els;},getButtonsAfterTitle:function(){var children=this.children;var els=[];for(var i=children.length-1;i>=0;i--){var el=children[i];if(el.tagName==='H1'){break;}
|
||||
if(!contributesToLayout(el)){continue;}
|
||||
els.push(el);}
|
||||
return els;},sumButtonWidths:function(buttons){var defaultWidth=50;var sum=buttons.reduce((prev,button)=>{var isStandardButton=button===this.els.actionButton;var width=isStandardButton?defaultWidth:button.clientWidth;return prev+width;},0);debug('sum button widths',buttons,sum);return sum;},attrs:{action:{get:function(){return this._action;},set:function(value){var action=KNOWN_ACTIONS[value];if(action===this._action){return;}
|
||||
this.setAttr('action',action);this._action=action;}},titleStart:{get:function(){debug('get title-start');if('_titleStart'in this){return this._titleStart;}
|
||||
var buttons=this.getButtonsBeforeTitle();var value=this.sumButtonWidths(buttons);debug('get title-start',buttons,value);return value;},set:function(value){debug('set title-start',value);value=parseInt(value,10);if(value===this._titleStart||isNaN(value)){return;}
|
||||
this.setAttr('title-start',value);this._titleStart=value;debug('set');}},titleEnd:{get:function(){debug('get title-end');if('_titleEnd'in this){return this._titleEnd;}
|
||||
var buttons=this.getButtonsAfterTitle();return this.sumButtonWidths(buttons);},set:function(value){debug('set title-end',value);value=parseInt(value,10);if(value===this._titleEnd||isNaN(value)){return;}
|
||||
this.setAttr('title-end',value);this._titleEnd=value;}},noFontFit:{get:function(){return this._noFontFit||false;},set:function(value){debug('set no-font-fit',value);value=!!(value||value==='');if(value===this.noFontFit){return;}
|
||||
this._noFontFit=value;if(value){this.setAttr('no-font-fit','');}
|
||||
else{this.removeAttr('no-font-fit');}}},ignoreDir:{get:function(){return this._ignoreDir||false;},set:function(value){debug('set ignore-dir',value);value=!!(value||value==='');if(value===this.ignoreDir){return;}
|
||||
this._ignoreDir=value;this.dirObserver=!value;if(value){this.setAttr('ignore-dir','');}
|
||||
else{this.removeAttr('ignore-dir');}}}},template:`<div class="inner">
|
||||
<button class="action-button">
|
||||
<content select="[l10n-action]"></content>
|
||||
</button>
|
||||
<content></content>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
-moz-user-select: none;
|
||||
|
||||
--gaia-header-button-color:
|
||||
var(--header-button-color,
|
||||
var(--header-color,
|
||||
var(--link-color,
|
||||
inherit)));
|
||||
}
|
||||
|
||||
/**
|
||||
* [hidden]
|
||||
*/
|
||||
|
||||
:host[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/** Reset
|
||||
---------------------------------------------------------*/
|
||||
|
||||
::-moz-focus-inner { border: 0; }
|
||||
|
||||
/** Inner
|
||||
---------------------------------------------------------*/
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
min-height: 50px;
|
||||
-moz-user-select: none;
|
||||
|
||||
background:
|
||||
var(--header-background,
|
||||
var(--background,
|
||||
#fff));
|
||||
}
|
||||
|
||||
/** Action Button
|
||||
---------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* 1. Hidden by default
|
||||
*/
|
||||
|
||||
.action-button {
|
||||
position: relative;
|
||||
|
||||
display: none; /* 1 */
|
||||
width: 50px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
|
||||
align-items: center;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: opacity 200ms 280ms;
|
||||
color:
|
||||
var(--header-action-button-color,
|
||||
var(--header-icon-color,
|
||||
var(--gaia-header-button-color)));
|
||||
}
|
||||
|
||||
/**
|
||||
* [action=back]
|
||||
* [action=menu]
|
||||
* [action=close]
|
||||
*
|
||||
* 1. For icon vertical-alignment
|
||||
*/
|
||||
|
||||
[action=back] .action-button,
|
||||
[action=menu] .action-button,
|
||||
[action=close] .action-button {
|
||||
display: flex; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* :active
|
||||
*/
|
||||
|
||||
.action-button:active {
|
||||
transition: none;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/** Action Button Icon
|
||||
---------------------------------------------------------*/
|
||||
|
||||
.action-button:before {
|
||||
font-family: 'gaia-icons';
|
||||
font-style: normal;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[action=close] .action-button:before { content: 'close' }
|
||||
[action=menu] .action-button:before { content: 'menu' }
|
||||
|
||||
[action=back]:-moz-dir(ltr) .action-button:before { content: 'left' }
|
||||
[action=back]:-moz-dir(rtl) .action-button:before { content: 'right' }
|
||||
|
||||
/** Action Button Icon
|
||||
---------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* 1. To enable vertical alignment.
|
||||
*/
|
||||
|
||||
.action-button:before {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/** Action Button Text
|
||||
---------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* To provide custom localized content for
|
||||
* the action-button, we allow the user
|
||||
* to provide an element with the class
|
||||
* .l10n-action. This node is then
|
||||
* pulled inside the real action-button.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* <gaia-header action="back">
|
||||
* <span l10n-action aria-label="Back">Localized text</span>
|
||||
* <h1>title</h1>
|
||||
* </gaia-header>
|
||||
*/
|
||||
|
||||
::content [l10n-action] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
/** Title
|
||||
---------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* 1. Vertically center text. We can't use flexbox
|
||||
* here as it breaks text-overflow ellipsis
|
||||
* without an inner div.
|
||||
*/
|
||||
|
||||
::content h1 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
line-height: 50px; /* 1 */
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
font-size: 24px;
|
||||
|
||||
color:
|
||||
var(--header-title-color,
|
||||
var(--header-color,
|
||||
var(--title-color,
|
||||
var(--text-color,
|
||||
inherit))));
|
||||
}
|
||||
|
||||
/**
|
||||
* [ignore-dir]
|
||||
*
|
||||
* When the <gaia-header> component has an [ignore-dir] attribute, header
|
||||
* direction is forced to LTR but we still want the <h1> text to be reversed
|
||||
* so that strings like '1 selected' become 'selected 1'.
|
||||
*
|
||||
* When we're happy for <gaia-header> to be fully RTL responsive we won't need
|
||||
* these rules anymore, but this depends on all Gaia apps being ready.
|
||||
*
|
||||
* This should be safe to remove when bug 1179459 lands.
|
||||
*/
|
||||
|
||||
:host[ignore-dir] {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
:host[ignore-dir]:-moz-dir(rtl) h1 {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
/** Buttons
|
||||
---------------------------------------------------------*/
|
||||
|
||||
::content a,
|
||||
::content button {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-width: 50px;
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
font-style: italic;
|
||||
cursor: pointer;
|
||||
transition: opacity 200ms 280ms;
|
||||
color: var(--gaia-header-button-color);
|
||||
}
|
||||
|
||||
/**
|
||||
* :active
|
||||
*/
|
||||
|
||||
::content a:active,
|
||||
::content button:active {
|
||||
transition: none;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/**
|
||||
* [hidden]
|
||||
*/
|
||||
|
||||
::content a[hidden],
|
||||
::content button[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* [disabled]
|
||||
*/
|
||||
|
||||
::content a[disabled],
|
||||
::content button[disabled] {
|
||||
pointer-events: none;
|
||||
color: var(--header-disabled-button-color);
|
||||
}
|
||||
|
||||
/** Icon Buttons
|
||||
---------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Icons are a different color to text
|
||||
*/
|
||||
|
||||
::content .icon,
|
||||
::content [data-icon] {
|
||||
color:
|
||||
var(--header-icon-color,
|
||||
var(--gaia-header-button-color));
|
||||
}
|
||||
|
||||
/**
|
||||
* If users want their action button
|
||||
* to be in the component's light-dom
|
||||
* they can add an .action class
|
||||
* to make it look like the
|
||||
* shadow action button.
|
||||
*/
|
||||
|
||||
::content .action {
|
||||
color:
|
||||
var(--header-action-button-color,
|
||||
var(--header-icon-color,
|
||||
var(--gaia-header-button-color)));
|
||||
}
|
||||
|
||||
/**
|
||||
* [data-icon]:empty
|
||||
*
|
||||
* Icon buttons with no textContent,
|
||||
* should always be 50px.
|
||||
*
|
||||
* This is to prevent buttons being
|
||||
* larger than they should be before
|
||||
* icon-font has loaded.
|
||||
*/
|
||||
|
||||
::content [data-icon]:empty {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
</style>`,nextTick:nextTick});function contributesToLayout(el){return el.localName!=='style'&&!el.hasAttribute('l10n-action');}
|
||||
function setStyleId(el,id){el._styleId=id;}
|
||||
function getStyleId(el){return el._styleId;}
|
||||
function nextTick(fn){var cleared;Promise.resolve().then(()=>{if(!cleared){fn();}});return{clear:function(){cleared=true;}};}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-header',this));},{"font-fit":1,"gaia-component":2,"gaia-icons":3}]},{},[4])(4)});
|
||||
|
|
@ -0,0 +1,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';
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/* Generated by grunt-webfont */
|
||||
/* Based on https://github.com/endtwist/fontcustom/blob/master/lib/fontcustom/templates/fontcustom.css */
|
||||
|
||||
@font-face {
|
||||
font-family: "gaia-icons";
|
||||
src: url("fonts/gaia-icons.ttf") format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[data-icon]:before,
|
||||
.ligature-icons {
|
||||
font-family: "gaia-icons";
|
||||
content: attr(data-icon);
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
text-decoration: inherit;
|
||||
text-transform: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 30px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
[data-icon]:not([data-l10n-id]):not([aria-label]):not([aria-hidden="true"]):before {
|
||||
/* Highlight cases where icons are not localized and still visible to screen
|
||||
reader */
|
||||
background: rgba(255, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[data-icon]:not([data-l10n-id]):not([aria-label]):not([aria-hidden="true"]):after {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
width: 20rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
content: "Icon is not localized and is still visible to screen reader. Please add data-l10n-id or aria-label for localization or use aria-hidden to hide the icon from screen reader.";
|
||||
z-index: 9999;
|
||||
}
|
||||
|
|
@ -0,0 +1,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;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><path fill="#667174" fill-opacity=".7" d="m 18.2995,19.66 -5.056,-5.057 5.056,-5.056 c -0.31,-0.516 -0.62,-1.032 -0.93,-1.34 l -5.672,5.673 v 1.652 l 5.675,5.676 c 0.31,-0.515 0.722,-0.928 0.93,-1.547 z"/></svg>
|
||||
|
After Width: | Height: | Size: 294 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path fill="#667174" fill-opacity=".7" d="M11.8,19.9l4.9-4.9l-4.9-4.9c0.3-0.5,0.6-1,0.9-1.3l5.5,5.5c0,0.3,0,0.5,0,0.8c0,0.3,0,0.5,0,0.8l-5.5,5.5 C12.4,20.9,12,20.5,11.8,19.9z"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
|
|
@ -0,0 +1 @@
|
|||
;'use strict';window.GaiaButtons=(function(win){var proto=Object.create(HTMLElement.prototype);var baseurl=window.GaiaButtonsBaseurl||'/shared/elements/gaia_buttons/';proto.createdCallback=function(){ComponentUtils.style.call(this,baseurl);};return document.registerElement('gaia-buttons',{prototype:proto});})(window);
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
/* ---------------------------------------
|
||||
* gaia-buttons:
|
||||
* Container for buttons e.g:
|
||||
* action menu, confirm, edit mode, FTU
|
||||
* --------------------------------------- */
|
||||
|
||||
gaia-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 7rem;
|
||||
box-sizing: border-box;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
gaia-buttons:not(.inline) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
border-top: 0.1rem solid #e7e7e7;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
/* this 'end' padding must match the buttons 'start' margin */
|
||||
gaia-buttons:not(.inline):-moz-dir(ltr) { padding-right: 1.5rem; }
|
||||
gaia-buttons:not(.inline):-moz-dir(rtl) { padding-left: 1.5rem; }
|
||||
|
||||
gaia-buttons.inline a,
|
||||
gaia-buttons.inline button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
position: relative;
|
||||
font-family: sans-serif;
|
||||
font-style: italic;
|
||||
width: 100%;
|
||||
height: 4rem;
|
||||
margin: 0;
|
||||
padding: 0 0.2rem;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
background: #d8d8d8;
|
||||
border: none;
|
||||
border-radius: 2rem;
|
||||
font-weight: normal;
|
||||
font-size: 1.6rem;
|
||||
line-height: 4rem;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
:-moz-dir(ltr) a,
|
||||
:-moz-dir(ltr) button {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
:-moz-dir(rtl) a,
|
||||
:-moz-dir(rtl) button {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
|
||||
/* Recommend */
|
||||
a.recommend,
|
||||
button.recommend {
|
||||
background-color: #00caf2;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Danger */
|
||||
a.danger,
|
||||
button.danger {
|
||||
background-color: #e51e1e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Pressed */
|
||||
a:active,
|
||||
button:active {
|
||||
background-color: #b2f2ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a::-moz-selection,
|
||||
button::-moz-selection {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
a[aria-disabled="true"],
|
||||
button[disabled] {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
color: #c7c7c7;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
a[aria-disabled="true"].recommend,
|
||||
button[disabled].recommend {
|
||||
background-color: #97cbd5;
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
a[aria-disabled="true"].danger,
|
||||
button[disabled].danger {
|
||||
background-color: #eb7474;
|
||||
color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
a[aria-disabled="true"]::-moz-selection,
|
||||
button[disabled]::-moz-selection {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
/* Hidden */
|
||||
[hidden] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
a.icon,
|
||||
button.icon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a.icon:before,
|
||||
button.icon:before {
|
||||
content: "";
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -1.5rem;
|
||||
background: transparent no-repeat center center / 100% auto;
|
||||
}
|
||||
:-moz-dir(ltr) a.icon:before,
|
||||
:-moz-dir(ltr) button.icon:before { right: 0.5rem; }
|
||||
:-moz-dir(rtl) a.icon:before,
|
||||
:-moz-dir(rtl) button.icon:before { left: 0.5rem; }
|
||||
|
||||
a.icon-next:before,
|
||||
button.icon-next:before {
|
||||
background-size: 3rem;
|
||||
}
|
||||
:-moz-dir(ltr) a.icon-next:before,
|
||||
:-moz-dir(ltr) button.icon-next:before {
|
||||
background-image: url(images/right-light.svg);
|
||||
}
|
||||
:-moz-dir(rtl) a.icon-next:before,
|
||||
:-moz-dir(rtl) button.icon-next:before {
|
||||
background-image: url(images/left-light.svg);
|
||||
}
|
||||
|
||||
a.icon-previous:before,
|
||||
button.icon-previous:before {
|
||||
background-size: 3rem;
|
||||
}
|
||||
:-moz-dir(ltr) a.icon-previous:before,
|
||||
:-moz-dir(ltr) button.icon-previous:before {
|
||||
left: 0.5rem;
|
||||
background-image: url(images/left-light.svg);
|
||||
}
|
||||
:-moz-dir(rtl) a.icon-previous:before,
|
||||
:-moz-dir(rtl) button.icon-previous:before {
|
||||
right: 0.5rem;
|
||||
background-image: url(images/right-light.svg);
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
SKIN: dark
|
||||
----------------------------------------------------------------- */
|
||||
|
||||
gaia-buttons[skin="dark"] {
|
||||
background: #4d4d4d;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Pressed */
|
||||
[skin="dark"] a:active,
|
||||
[skin="dark"] button:active {
|
||||
background-color: #00aacc;
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
[skin="dark"] a[aria-disabled="true"],
|
||||
[skin="dark"] button[disabled] {
|
||||
background-color: #565656;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
|
||||
[skin="dark"] a[aria-disabled="true"].recommend,
|
||||
[skin="dark"] button[disabled].recommend {
|
||||
background-color: #006579;
|
||||
}
|
||||
|
||||
[skin="dark"] a[aria-disabled="true"].danger,
|
||||
[skin="dark"] button[disabled].danger {
|
||||
background-color: #730f0f;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Transitions
|
||||
----------------------------------------------------------------- */
|
||||
|
||||
[transition] {
|
||||
display: flex;
|
||||
transition: visibility var(--transition-duration);
|
||||
}
|
||||
|
||||
[transition][hidden] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[transition="slide"] {
|
||||
transform: translateY(0);
|
||||
transition: transform var(--transition-duration) ease,
|
||||
visibility var(--transition-duration);
|
||||
}
|
||||
|
||||
[transition="slide"][hidden] {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
[transition="fade"] {
|
||||
opacity: 1;
|
||||
transition: opacity var(--transition-duration) ease,
|
||||
visibility var(--transition-duration);
|
||||
}
|
||||
|
||||
[transition="fade"][hidden] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
RESPONSIVE: tablet
|
||||
----------------------------------------------------------------- */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
gaia-buttons {
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
gaia-buttons:not(.inline) {
|
||||
border-top-width: 0.2rem;
|
||||
}
|
||||
/* this 'end' padding must match the buttons 'start' margin */
|
||||
gaia-buttons:not(.inline):-moz-dir(ltr) { padding-right: 3rem; }
|
||||
gaia-buttons:not(.inline):-moz-dir(rtl) { padding-left: 3rem; }
|
||||
|
||||
a,
|
||||
button {
|
||||
font-size: 2rem;
|
||||
height: 5rem;
|
||||
line-height: 5rem;
|
||||
border-radius: 2.5rem;
|
||||
max-width: 40rem;
|
||||
}
|
||||
:-moz-dir(ltr) a,
|
||||
:-moz-dir(ltr) button {
|
||||
margin-left: 3rem;
|
||||
}
|
||||
:-moz-dir(rtl) a,
|
||||
:-moz-dir(rtl) button {
|
||||
margin-right: 3rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 245 B |
|
|
@ -0,0 +1,62 @@
|
|||
'use strict';
|
||||
/* global ComponentUtils */
|
||||
|
||||
window.GaiaMenu = (function(win) {
|
||||
// Extend from HTMLElement prototype
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
// Allow baseurl to be overridden (used for demo page)
|
||||
var baseurl = window.GaiaMenuBaseurl ||
|
||||
'/shared/elements/gaia_menu/';
|
||||
|
||||
proto.createdCallback = function () {
|
||||
var shadow = this.createShadowRoot();
|
||||
|
||||
this._template = template.content.cloneNode(true);
|
||||
|
||||
var cancelButton = this._template.querySelector('.gaia-menu-cancel button');
|
||||
|
||||
cancelButton.addEventListener('click', function () {
|
||||
this.hide();
|
||||
this.dispatchEvent(new CustomEvent('gaiamenu-cancel'));
|
||||
}.bind(this));
|
||||
|
||||
shadow.appendChild(this._template);
|
||||
|
||||
ComponentUtils.style.call(this, baseurl);
|
||||
|
||||
document.l10n.ready.then(this.localize.bind(this));
|
||||
document.addEventListener('DOMRetranslated', this.localize.bind(this));
|
||||
};
|
||||
|
||||
proto.localize = function() {
|
||||
document.l10n.formatValue('gaia-menu-cancel').then(value => {
|
||||
this.shadowRoot.querySelector('button').textContent = value;
|
||||
});
|
||||
};
|
||||
|
||||
proto.show = function() {
|
||||
this.removeAttribute('hidden');
|
||||
};
|
||||
|
||||
proto.hide = function() {
|
||||
this.setAttribute('hidden', 'hidden');
|
||||
};
|
||||
|
||||
var template = document.createElement('template');
|
||||
|
||||
template.innerHTML =
|
||||
`<form role="dialog" data-type="action">
|
||||
<content select="header"></content>
|
||||
<menu>
|
||||
<content select="button"></content>
|
||||
<span class="gaia-menu-cancel">
|
||||
<button>Cancel</button>
|
||||
</span>
|
||||
</menu>
|
||||
</form>`;
|
||||
|
||||
// Register and return the constructor
|
||||
return document.registerElement('gaia-menu', { prototype: proto });
|
||||
|
||||
})(window);
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
/* ----------------------------------
|
||||
* gaia_menu
|
||||
* ---------------------------------- */
|
||||
|
||||
/* default alignment follows the UI direction, not the content direction */
|
||||
:-moz-dir(ltr) [dir] { text-align: left; }
|
||||
:-moz-dir(rtl) [dir] { text-align: right; }
|
||||
|
||||
/* Main dialog setup */
|
||||
gaia-menu {
|
||||
background-color: #2d2d2d;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 0 0 7rem;
|
||||
/**
|
||||
* Using "font-size: 0" to avoid the unwanted visual space (about 3px) created
|
||||
* by white spaces and line breaks in the code between inline-block elements.
|
||||
*/
|
||||
font-size: 0;
|
||||
color: #fff;
|
||||
display: block;
|
||||
transition: transform ease-in-out var(--transition-duration),
|
||||
visibility var(--transition-duration);
|
||||
}
|
||||
|
||||
gaia-menu[hidden] {
|
||||
transform: translateY(100%);
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
gaia-menu:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 0.1rem;
|
||||
height: 100%;
|
||||
margin-inline-start: -0.1rem;
|
||||
}
|
||||
|
||||
header {
|
||||
-moz-box-sizing: border-box;
|
||||
font-size: 1.6rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
height: 5rem;
|
||||
line-height: 4.9rem;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-weight: normal;
|
||||
padding: 0 3rem;
|
||||
border-bottom: 0.1rem solid #616262;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.6rem;
|
||||
line-height: 4.9rem;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Generic set of actions in toolbar */
|
||||
header menu.toolbar {
|
||||
height: 100%;
|
||||
margin: 0 -3rem;
|
||||
margin-inline-start: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
header:-moz-dir(ltr) menu.toolbar { float: right; }
|
||||
header:-moz-dir(rtl) menu.toolbar { float: left; }
|
||||
|
||||
header menu.toolbar a,
|
||||
header menu.toolbar button {
|
||||
-moz-box-sizing: border-box;
|
||||
position: relative;
|
||||
width: auto;
|
||||
min-width: 5rem;
|
||||
height: 4.9rem;
|
||||
margin-bottom: 0;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0;
|
||||
line-height: 4.9rem;
|
||||
background: none;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
z-index: 5;
|
||||
}
|
||||
header:-moz-dir(ltr) menu.toolbar a,
|
||||
header:-moz-dir(ltr) menu.toolbar button {
|
||||
float: left;
|
||||
}
|
||||
header:-moz-dir(rtl) menu.toolbar a,
|
||||
header:-moz-dir(rtl) menu.toolbar button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
header a,
|
||||
header button {
|
||||
border: none;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.1em;
|
||||
color: #fff;
|
||||
border-radius: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Pressed state */
|
||||
header a:active .icon:after,
|
||||
header button:active .icon:after,
|
||||
header menu.toolbar a:active,
|
||||
header menu.toolbar button:active {
|
||||
background: #008aaa;
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
header menu.toolbar a[aria-disabled="true"],
|
||||
header menu.toolbar button[disabled] {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Icon definitions */
|
||||
header .icon {
|
||||
position: relative;
|
||||
float: none;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 4rem;
|
||||
height: 4.9rem;
|
||||
margin: 0 -1rem;
|
||||
background: transparent no-repeat center center / 3rem auto;
|
||||
font-size: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header .icon.icon-close {
|
||||
background-image: url(images/icons/close.png);
|
||||
}
|
||||
|
||||
header .icon.icon-back {
|
||||
background-image: url(images/icons/back.png);
|
||||
}
|
||||
|
||||
/* Navigation links (back, cancel, etc) */
|
||||
header > button,
|
||||
header > a {
|
||||
width: 5rem;
|
||||
height: 4.9rem;
|
||||
background: none;
|
||||
overflow: visible;
|
||||
margin: 0 -1rem;
|
||||
margin-inline-start: -3rem;
|
||||
position: relative;
|
||||
}
|
||||
header:-moz-dir(ltr) > button,
|
||||
header:-moz-dir(ltr) > a {
|
||||
float: left;
|
||||
}
|
||||
header:-moz-dir(rtl) > button,
|
||||
header:-moz-dir(rtl) > a {
|
||||
float: right;
|
||||
}
|
||||
|
||||
header > button::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header > button .icon,
|
||||
header > a .icon {
|
||||
position: static;
|
||||
display: block;
|
||||
width: 3rem;
|
||||
height: 4.9rem;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
header > button .icon:after,
|
||||
header > a .icon:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
offset-inline-start: 0;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
width: 3rem;
|
||||
height: 4.9rem;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
section {
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: 7rem;
|
||||
left: 1.5rem;
|
||||
right: 1.5rem;
|
||||
bottom: 2rem;
|
||||
font-size: 1.6rem;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
/* Specific component code */
|
||||
menu {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: calc(100% - 13.5rem); /*header(5rem) + footer(7rem) + padding(1.5rem)*/
|
||||
height: auto;
|
||||
padding-top: 1.5rem;
|
||||
overflow: auto;
|
||||
width: auto;
|
||||
border: none;
|
||||
background: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 7rem;
|
||||
}
|
||||
|
||||
button {
|
||||
width: calc(100% - 3rem);
|
||||
height: 4rem;
|
||||
-moz-box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-weight: normal;
|
||||
line-height: 4rem;
|
||||
outline: none;
|
||||
font-size: 1.6rem;
|
||||
text-shadow: none;
|
||||
padding: 0 1.2rem;
|
||||
margin: 0 1.5rem 1rem;
|
||||
background: rgba(87, 87, 87, 0.8);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
position: relative;
|
||||
color: #fff;
|
||||
font-family: sans-serif;
|
||||
font-style: italic;
|
||||
}
|
||||
button:-moz-dir(ltr) { text-align: left; }
|
||||
button:-moz-dir(rtl) { text-align: right; }
|
||||
|
||||
span.gaia-menu-cancel {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background: #4d4d4d;
|
||||
height: 7rem;
|
||||
offset-inline-start: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
span.gaia-menu-cancel button {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
offset-inline-start: 0;
|
||||
color: #333;
|
||||
background: #d8d8d8;
|
||||
text-align: center;
|
||||
border: none;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
|
||||
button:nth-last-child(2) {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Press state */
|
||||
button:active,
|
||||
span.gaia-menu-cancel button:active {
|
||||
background: #00aacc;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
button[disabled] {
|
||||
background-color: rgba(87, 87, 87, 0.8);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button[disabled]:last-child {
|
||||
background-color: #e7e7e7;
|
||||
color: #a6a6a6;
|
||||
opacity: 1;
|
||||
}
|
||||
|
After Width: | Height: | Size: 76 B |
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
.sim-default {
|
||||
display: none;
|
||||
margin-inline-start: 0.15rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.is-default .sim-default {
|
||||
display: initial;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
;;;;'use strict';window.GaiaSubheader=(function(win){var proto=Object.create(HTMLElement.prototype);var baseurl=window.GaiaSubheaderBaseurl||'/shared/elements/gaia_subheader/';proto.createdCallback=function(){ComponentUtils.style.call(this,baseurl);};return document.registerElement('gaia-subheader',{prototype:proto});})(window);
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
gaia-subheader[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gaia Subheader
|
||||
*
|
||||
* Dividing header usually found
|
||||
* heading and separating lists.
|
||||
*/
|
||||
|
||||
gaia-subheader {
|
||||
display: block;
|
||||
padding: 0.8rem 3rem;
|
||||
border-bottom: solid 0.1rem #e6e6e6;
|
||||
color: #424242;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.8rem;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
/**
|
||||
* [data-skin="organic"]
|
||||
*/
|
||||
|
||||
gaia-subheader[skin="organic"] {
|
||||
border: 0;
|
||||
background-color: #e7e7e7;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
/**
|
||||
* [data-skin="dark"]
|
||||
*/
|
||||
|
||||
gaia-subheader[skin="dark"] {
|
||||
border: 0;
|
||||
background-color: #2c353b;
|
||||
color: #fff;
|
||||
}
|
||||
|
After Width: | Height: | Size: 755 B |
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 748 B |
|
|
@ -0,0 +1,151 @@
|
|||
'use strict';
|
||||
/* global ComponentUtils */
|
||||
|
||||
window.GaiaSwitch = (function(win) {
|
||||
// Extend from the HTMLElement prototype
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
// Allow baseurl to be overridden (used for demo page)
|
||||
var baseurl = window.GaiaSwitchBaseurl ||
|
||||
'/shared/elements/gaia_switch/';
|
||||
|
||||
proto.createdCallback = function() {
|
||||
var shadow = this.createShadowRoot();
|
||||
this._template = template.content.cloneNode(true);
|
||||
this._input = this._template.querySelector('input[type="checkbox"]');
|
||||
|
||||
var checked = this.getAttribute('checked');
|
||||
if (checked !== null) {
|
||||
this._input.checked = true;
|
||||
}
|
||||
|
||||
var wrapper = this._template.getElementById('switch');
|
||||
wrapper.addEventListener('click', this.handleClick.bind(this));
|
||||
|
||||
shadow.appendChild(this._template);
|
||||
|
||||
ComponentUtils.style.call(this, baseurl);
|
||||
|
||||
// Proxy RTL changes to the shadow root so we can style for RTL.
|
||||
var dirObserver = new MutationObserver(this.updateInternalDir.bind(this));
|
||||
dirObserver.observe(document.documentElement, {
|
||||
attributeFilter: ['dir'],
|
||||
attributes: true
|
||||
});
|
||||
this.updateInternalDir();
|
||||
};
|
||||
|
||||
proto.updateInternalDir = function() {
|
||||
var internal = this.shadowRoot.firstElementChild;
|
||||
if (document.documentElement.dir === 'rtl') {
|
||||
internal.setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
internal.removeAttribute('dir');
|
||||
}
|
||||
};
|
||||
|
||||
proto.handleClick = function(e) {
|
||||
// If user clicked the (optional) link, do not toggle gaia-switch.
|
||||
if (e && e.target.tagName === 'A') {
|
||||
return;
|
||||
}
|
||||
|
||||
e && e.preventDefault();
|
||||
e && e.stopImmediatePropagation();
|
||||
|
||||
// Dispatch a click event.
|
||||
var event = new MouseEvent('click', {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
this.dispatchEvent(event);
|
||||
|
||||
if (!event.defaultPrevented) {
|
||||
this.checked = !this.checked;
|
||||
}
|
||||
|
||||
// Dispatch a change event for the gaia-switch.
|
||||
this.dispatchEvent(new CustomEvent('change', {
|
||||
bubbles: true,
|
||||
cancelable: false
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows users to simulate clicking through javascript.
|
||||
*/
|
||||
proto.click = function() {
|
||||
this.handleClick();
|
||||
};
|
||||
|
||||
/**
|
||||
* Proxy the checked property to the input element.
|
||||
*/
|
||||
Object.defineProperty( proto, 'checked', {
|
||||
get: function() {
|
||||
return this._input.checked;
|
||||
},
|
||||
set: function(value) {
|
||||
this._input.checked = value;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Disabled setter
|
||||
*/
|
||||
Object.defineProperty( proto, 'disabled', {
|
||||
get: function() {
|
||||
return this.hasAttribute('disabled');
|
||||
},
|
||||
set: function(value) {
|
||||
if (value) {
|
||||
this.setAttribute('disabled', true);
|
||||
} else {
|
||||
this.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Proxy the name property to the input element.
|
||||
*/
|
||||
Object.defineProperty( proto, 'name', {
|
||||
get: function() {
|
||||
return this.getAttribute('name');
|
||||
},
|
||||
set: function(value) {
|
||||
this.setAttribute('name', value);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Proxy the input type.
|
||||
*/
|
||||
Object.defineProperty( proto, 'type', {
|
||||
get: function() {
|
||||
return 'gaia-switch';
|
||||
}
|
||||
});
|
||||
|
||||
// HACK: Create a <template> in memory at runtime.
|
||||
// When the custom-element is created we clone
|
||||
// this template and inject into the shadow-root.
|
||||
// Prior to this we would have had to copy/paste
|
||||
// the template into the <head> of every app that
|
||||
// wanted to use <gaia-switch>, this would make
|
||||
// markup changes complicated, and could lead to
|
||||
// things getting out of sync. This is a short-term
|
||||
// hack until we can import entire custom-elements
|
||||
// using HTML Imports (bug 877072).
|
||||
var template = document.createElement('template');
|
||||
template.innerHTML = `<span id="switch">
|
||||
<input type="checkbox">
|
||||
<span><content select="label"></content></span>
|
||||
<div class="details"><content select="details"></content></div>
|
||||
<content select="a"></content>
|
||||
</span>`;
|
||||
|
||||
// Register and return the constructor
|
||||
return document.registerElement('gaia-switch', { prototype: proto });
|
||||
})(window);
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
gaia-switch {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#switch {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: none;
|
||||
}
|
||||
|
||||
#switch span {
|
||||
font-size: 1.9rem;
|
||||
color: var(--label-color);
|
||||
padding: 1rem 0;
|
||||
line-height: 3rem;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
/* Override inheriting nowrap from legacy lists building blocks. */
|
||||
white-space: normal;
|
||||
}
|
||||
#switch span:-moz-dir(ltr) { float: left; }
|
||||
#switch span:-moz-dir(rtl) { float: right; }
|
||||
|
||||
#switch span:not(:empty) {
|
||||
padding-inline-end: 6rem;
|
||||
}
|
||||
|
||||
#switch input {
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
offset-inline-start: 0;
|
||||
}
|
||||
|
||||
#switch input:checked ~ span::after {
|
||||
background-position: center bottom;
|
||||
}
|
||||
|
||||
/* ----------------------------------
|
||||
* ON/OFF SWITCHES
|
||||
* ---------------------------------- */
|
||||
|
||||
#switch input ~ span::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
offset-inline-end: 0;
|
||||
top: 2.5rem;
|
||||
width: 6rem;
|
||||
margin: -1.4rem 0 0;
|
||||
height: 2.7rem;
|
||||
border-radius: 1.35rem;
|
||||
overflow: hidden;
|
||||
background-color: #e6e6e6;
|
||||
background-image: url(images/background_off.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 9.2rem 2.7rem;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
#switch input ~ span:-moz-dir(ltr)::after { background-position: -3.2rem 0; }
|
||||
#switch input ~ span:-moz-dir(rtl)::after { background-position: 0 0; }
|
||||
|
||||
/* switch: 'ON' state */
|
||||
#switch input:checked ~ span:-moz-dir(ltr)::after {
|
||||
background-image: url(images/background.png);
|
||||
background-position: 0 0;
|
||||
}
|
||||
#switch input:checked ~ span:-moz-dir(rtl)::after {
|
||||
background-image: url(images/background_rtl.png);
|
||||
background-position: -3.2rem 0;
|
||||
}
|
||||
|
||||
/* switch: disabled state */
|
||||
#switch input:disabled ~ span::after {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
#switch input.uninit ~ span::after {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* A <details> slot may be specified in addition the the <label>.
|
||||
* When provided, it appears under the <label> and can provide additional context.
|
||||
*/
|
||||
#switch div.details {
|
||||
clear: both;
|
||||
position: relative;
|
||||
top: -1.5rem;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding-top: 0.5rem;
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 8rem;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.6rem;
|
||||
color: #505859;
|
||||
font-style: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* A <a> link slow may be specified in addition to the <label> and <details>.
|
||||
* When provided, it will appear under the <details> section.
|
||||
*/
|
||||
gaia-switch a {
|
||||
position: relative;
|
||||
top: -0.8rem;
|
||||
font-size: 1.7rem;
|
||||
line-height: 2.4rem;
|
||||
font-weight: 400;
|
||||
color: #35679a;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
;'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);
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* This module is a helper for recording telemetry data in Gecko’s telemetry
|
||||
* system. Currently, only data from certified apps will be recorded.
|
||||
*
|
||||
* There are three different types of metrics that can be used here:
|
||||
* Counter: This is a simple counter that increments by 1 each time it's
|
||||
* called.
|
||||
*
|
||||
* Linear: This type of metric creates a linear distributed set of buckets
|
||||
* which range between the min and max. You can specify the number
|
||||
* of buckets as well as the min and max.
|
||||
*
|
||||
* Exponential: This type of metric creates an exponential distributed set of
|
||||
* buckets ranging between min and max. You can specify the min,
|
||||
* max, and buckets.
|
||||
*
|
||||
* App Setup:
|
||||
* 1) Add the following to the app (e.g. index.html):
|
||||
* <script defer src="/shared/js/advanced_telemetry_helper.js"></script>
|
||||
* <script defer src="/shared/js/settings_listener.js"></script>
|
||||
* 2) Include AdvancedTelemetryHelper in the globals.
|
||||
*
|
||||
* Counter Metric Usage Example:
|
||||
* // This creates the histogram counter on this metric called
|
||||
* // 'mycount'
|
||||
* var count = new AdvancedTelemetryHelper(ATH.HISTOGRAM_COUNT, 'mycount');
|
||||
* count.add(); //Increment the count. The count of the histogram is now 1.
|
||||
* // DO SOMETHING
|
||||
* count.add(); //Increment the count. The count of the histogram is now 2.
|
||||
*
|
||||
* Linear Metric Usage Example:
|
||||
* // This creates a linear histogram called 'mylinear' with minimum
|
||||
* // of 1 (0 not allowed as a minimum), max of 1000, with 10 buckets.
|
||||
* var linear = new AdvancedTelemetryHelper(ATH.HISTOGRAM_LINEAR, 'mylinear',
|
||||
* 1, 1000, 10);
|
||||
* // This adds the value 15 to the histogram 'mylinear'
|
||||
* linear.add(15);
|
||||
* // DO SOMETHING
|
||||
* // This adds to the existing histogram a value of 800.
|
||||
* linear.add(800); // The histogram now has two values, one in the 15 bucket
|
||||
* // and one in the bucket that holds the value 800
|
||||
*
|
||||
* Exponential Metric Usage Example:
|
||||
* // This creates an exponential histogram called 'myexp' with minimum
|
||||
* // of 1 (0 not allowed as a minimum), max of 1000, with 10 buckets.
|
||||
* var exp = new AdvancedTelemetryHelper(ATH.HISTOGRAM_EXPONENTIAL, 'myexp',
|
||||
* 1, 1000, 10);
|
||||
* // This adds the value 15 to the histogram 'myexp'
|
||||
* exp.add(15);
|
||||
* // DO SOMETHING
|
||||
* // This adds to the existing histogram a value of 800.
|
||||
* exp.add(800); // The histogram now has two values, one in the 15 bucket
|
||||
* // and one in the bucket that holds the value 800
|
||||
*
|
||||
* Caveats:
|
||||
* Note that after a histogram is created, it's not possible to change the type
|
||||
* of the histogram from one type to the other. It's necessary to create a new
|
||||
* Histogram.
|
||||
*
|
||||
* Each histogram should have a unique name.
|
||||
*
|
||||
*/
|
||||
|
||||
/* globals SettingsListener */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const APP_TELEMETRY_LOG_PREFIX = 'telemetry';
|
||||
const ATH = AdvancedTelemetryHelper;
|
||||
// Export Histogram types so they can be used by apps
|
||||
ATH.HISTOGRAM_COUNT = 4;
|
||||
ATH.HISTOGRAM_EXP = 0;
|
||||
ATH.HISTOGRAM_LINEAR = 1;
|
||||
// This is exposed for unit testing purposes
|
||||
ATH.TELEMETRY_LEVEL_KEY = 'metrics.selectedMetrics.level';
|
||||
ATH.SETTINGS_INITIALIZED = false;
|
||||
|
||||
// API for an exponential histogram entry. This function creates the
|
||||
// histogram in the system and prepares it subsequent .log(value) calls
|
||||
// against it.
|
||||
// <type>: One of ATH.HISTOGRAM_COUNT, ATH.HISTOGRAM_EXP,
|
||||
// or ATH.HISTOGRAM_LINEAR.
|
||||
// <name>: Unique name of the metric
|
||||
// <min>: [Required for linear and exponential only]The minimum (non-zero)
|
||||
// numeric value for this histogram.
|
||||
// <max>: [Required for linear and exponential only]The maximum numeric
|
||||
// value for this histogram.
|
||||
// <buckets>: [Required for linear and exponential only]The total number of
|
||||
// buckets for this histogram.
|
||||
function AdvancedTelemetryHelper(type, name, min, max, buckets) {
|
||||
if (!ATH.SETTINGS_INITIALIZED) {
|
||||
ATH.SETTINGS_INITIALIZED = true;
|
||||
ATH.init();
|
||||
}
|
||||
|
||||
if (typeof type !== 'undefined' && typeof name !== 'undefined') {
|
||||
if (type === ATH.HISTOGRAM_COUNT) {
|
||||
this.createHistogram(name, ATH.HISTOGRAM_COUNT, 0, 0, 0);
|
||||
} else if (type === ATH.HISTOGRAM_EXP || type === ATH.HISTOGRAM_LINEAR) {
|
||||
this.createHistogram(name, type, min, max, buckets);
|
||||
} else {
|
||||
console.warn('Must pass a Histogram type and a unique Histogram name.');
|
||||
}
|
||||
} else {
|
||||
console.warn('Must pass a Histogram type and a unique Histogram name.');
|
||||
}
|
||||
}
|
||||
|
||||
AdvancedTelemetryHelper.init = function init() {
|
||||
ATH.telemetryEnabledListener = function telemetryEnabledListener(enabled) {
|
||||
if (enabled === 'Enhanced') {
|
||||
ATH.TELEMETRY_LEVEL = true;
|
||||
} else {
|
||||
ATH.TELEMETRY_LEVEL = false;
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
SettingsListener.observe(ATH.TELEMETRY_LEVEL_KEY,
|
||||
false, ATH.telemetryEnabledListener);
|
||||
};
|
||||
|
||||
AdvancedTelemetryHelper.prototype.createHistogram =
|
||||
function(name, type, min, max, buckets) {
|
||||
if (typeof ATH.TELEMETRY_LEVEL === 'undefined' || ATH.TELEMETRY_LEVEL) {
|
||||
if (typeof this.metricType !== 'undefined') {
|
||||
console.warn('Cannot redefine histogram. Must create a new object');
|
||||
return;
|
||||
}
|
||||
|
||||
this.metricType = type;
|
||||
if (typeof name !== 'undefined') {
|
||||
var varCount = 2;
|
||||
var re = /_/g;
|
||||
var message = APP_TELEMETRY_LOG_PREFIX;
|
||||
// Disallow underscores so telemetry system can parse it.
|
||||
this.metricName = name.replace(re, '-');
|
||||
message += '|' + this.metricName;
|
||||
message += '|' + type + '|';
|
||||
if (typeof min !== 'undefined') { varCount++; }
|
||||
if (typeof max !== 'undefined') { varCount++; }
|
||||
if (typeof buckets !== 'undefined') { varCount++; }
|
||||
|
||||
if (varCount != 5) {
|
||||
console.warn('createHistogram must have 5 params.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (min < 1) {
|
||||
min = 1;
|
||||
}
|
||||
message += min + '|' + max + '|' + buckets;
|
||||
console.info(message);
|
||||
} else {
|
||||
console.warn('A telemetry log entry requires a name.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// API for logging against a pre-existing histogram. This records the value
|
||||
// for this particular histogram in the histogram system.
|
||||
// <value>: The value to record against this histogram.
|
||||
// For a counter histogram, this can be empty or 1 can be passed.
|
||||
// For an exponential or linear histogram a valid value > 0 must be passed.
|
||||
AdvancedTelemetryHelper.prototype.add = function(value) {
|
||||
if (typeof ATH.TELEMETRY_LEVEL === 'undefined' || ATH.TELEMETRY_LEVEL) {
|
||||
if (this.metricType == ATH.HISTOGRAM_COUNT) {
|
||||
value = 1;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined' || !value || value < 1) {
|
||||
console.warn('A valid value is required to log a histogram.');
|
||||
return;
|
||||
}
|
||||
|
||||
var message = APP_TELEMETRY_LOG_PREFIX + '|' + this.metricName +
|
||||
'|' + value;
|
||||
console.info(message);
|
||||
}
|
||||
};
|
||||
|
||||
exports.AdvancedTelemetryHelper = AdvancedTelemetryHelper;
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,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};}());
|
||||
|
|
@ -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});}};
|
||||
|
|
@ -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));
|
||||
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}());
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -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();}}};
|
||||
|
|
@ -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));
|
||||
|
|
@ -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};})();
|
||||
|
|
@ -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&¶ms.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);}
|
||||
|
|
@ -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();}}};})();
|
||||
|
|
@ -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;})();}
|
||||
|
|
@ -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;};};
|
||||
|
|
@ -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});};
|
||||
|
|
@ -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;};})();
|
||||
|
|
@ -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();};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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};})();
|
||||
|
|
@ -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
|
||||
};
|
||||
|
||||
})();
|
||||
|
|
@ -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);
|
||||
|
|
@ -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};})();}
|
||||
|
|
@ -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);}
|
||||
|
|
@ -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();
|
||||
})();
|
||||
}
|
||||
|
|
@ -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);}});};})();}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);};}
|
||||
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
};
|
||||
})();
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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};})();
|
||||
|
|
@ -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;
|
||||
})();
|
||||
|
|
@ -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;
|
||||
|
||||
})();
|
||||
|
|
@ -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);};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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);}
|
||||
|
|
@ -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);}
|
||||
|
|
@ -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();})();}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;};}
|
||||
|
|
@ -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);}};};}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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));}};})();}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
/* exported CustomDialog */
|
||||
//XXX: Waiting for the window.showModalDialog support in B2G
|
||||
|
||||
'use strict';
|
||||
|
||||
var CustomDialog = (function() {
|
||||
|
||||
var container = null;
|
||||
var screen = null;
|
||||
var dialog = null;
|
||||
var header = null;
|
||||
var message = null;
|
||||
var yes = null;
|
||||
var no = null;
|
||||
|
||||
return {
|
||||
hide: function dialog_hide() {
|
||||
if (screen === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
container = document.body;
|
||||
}
|
||||
|
||||
container.removeChild(screen);
|
||||
screen = null;
|
||||
dialog = null;
|
||||
header = null;
|
||||
message = null;
|
||||
yes = null;
|
||||
no = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Method that shows the dialog
|
||||
* @param {Object} title the l10n id for the title of the dialog. null or
|
||||
* empty for no title. or you can give a object with more
|
||||
* options like {icon: path or empty string,
|
||||
* id: an l10n-id, args: an object of arguments that
|
||||
* parameterise the 'id' field}.
|
||||
* @param {String} msg message for the dialog. give a object like the
|
||||
* title to enable more options.
|
||||
* @param {Object} cancel {title, callback} object when confirm.
|
||||
* @param {Object} confirm {title, callback} object when cancel.
|
||||
* @param {Element} [containerElement] an optional element for which the
|
||||
* dialog will be injected
|
||||
*/
|
||||
show: function dialog_show(title, msg, cancel, confirm, containerElement) {
|
||||
container = containerElement || document.body;
|
||||
|
||||
if (screen === null) {
|
||||
screen = document.createElement('form');
|
||||
screen.setAttribute('role', 'dialog');
|
||||
screen.setAttribute('data-type', 'confirm');
|
||||
screen.id = 'dialog-screen';
|
||||
|
||||
dialog = document.createElement('section');
|
||||
screen.appendChild(dialog);
|
||||
|
||||
// Create a reusable function to decorate elements with all
|
||||
// possible options, instead of scattering similar code about
|
||||
// everywhere.
|
||||
//
|
||||
// It's also possible to be extended with more usable decorating
|
||||
// options and elements.
|
||||
//
|
||||
// 'title'|'message' -> Object|String -> the element -> dialog
|
||||
// -> the decorated element
|
||||
var decorateWithOptions = function cd_decorateWithOptions(type, options,
|
||||
elm, dialog) {
|
||||
if ('string' === typeof options) {
|
||||
elm.setAttribute('data-l10n-id', options);
|
||||
return elm;
|
||||
}
|
||||
|
||||
var icon = options.icon;
|
||||
|
||||
var textElm = elm;
|
||||
|
||||
if (icon && '' !== icon) {
|
||||
// We can't localize elements with child nodes
|
||||
// so if there's going to be an icon, we need to create a separate
|
||||
// node for text. See bug 1053629 for details
|
||||
textElm = document.createElement('span');
|
||||
var iconImg = new Image();
|
||||
iconImg.src = icon;
|
||||
iconImg.classList.add('custom-dialog-' + type + '-icon');
|
||||
|
||||
// Icons usually insert as the first element.
|
||||
elm.insertBefore(iconImg, elm.firstChild);
|
||||
elm.appendChild(textElm);
|
||||
}
|
||||
// More decorating options goes here.
|
||||
|
||||
if (options.id) {
|
||||
document.l10n.setAttributes(
|
||||
textElm,
|
||||
options.id,
|
||||
options.args
|
||||
);
|
||||
} else {
|
||||
var text = options[type];
|
||||
textElm.textContent = text;
|
||||
}
|
||||
|
||||
return elm;
|
||||
};
|
||||
|
||||
var setElementText = function (element, options) {
|
||||
if ('string' === typeof options) {
|
||||
element.setAttribute('data-l10n-id', options);
|
||||
}
|
||||
|
||||
if(options.id) {
|
||||
document.l10n.setAttributes(element, options.id, options.args);
|
||||
}
|
||||
};
|
||||
|
||||
header = document.createElement('h1');
|
||||
header.id = 'dialog-title';
|
||||
if (title && title !== '') {
|
||||
header = decorateWithOptions('title', title, header, dialog);
|
||||
}
|
||||
dialog.appendChild(header);
|
||||
|
||||
message = document.createElement('p');
|
||||
message.id = 'dialog-message';
|
||||
message = decorateWithOptions('message', msg, message, dialog);
|
||||
dialog.appendChild(message);
|
||||
|
||||
var menu = document.createElement('menu');
|
||||
menu.dataset.items = 1;
|
||||
|
||||
no = document.createElement('button');
|
||||
|
||||
// The default type of the button element is "Submit",
|
||||
// and form submit in system app would make system app reload.
|
||||
no.type = 'button';
|
||||
|
||||
setElementText(no, cancel.title);
|
||||
no.id = 'dialog-no';
|
||||
no.addEventListener('click', clickHandler);
|
||||
menu.appendChild(no);
|
||||
|
||||
if (confirm) {
|
||||
menu.dataset.items = 2;
|
||||
yes = document.createElement('button');
|
||||
|
||||
// The default type of button element is "Submit",
|
||||
// and form submit in system app would make system app reload.
|
||||
yes.type = 'button';
|
||||
|
||||
setElementText(yes, confirm.title);
|
||||
yes.id = 'dialog-yes';
|
||||
|
||||
//confirm can be with class "danger" or "recommend"
|
||||
//the default is "danger"
|
||||
yes.className = confirm.recommend ? 'recommend' : 'danger';
|
||||
|
||||
yes.addEventListener('click', clickHandler);
|
||||
menu.appendChild(yes);
|
||||
}
|
||||
else { // 1 button, should be full.
|
||||
no.classList.add('full');
|
||||
}
|
||||
|
||||
screen.appendChild(menu);
|
||||
|
||||
container.appendChild(screen);
|
||||
}
|
||||
|
||||
// Make the screen visible
|
||||
screen.classList.add('visible');
|
||||
|
||||
// This is the event listener function for the buttons
|
||||
function clickHandler(evt) {
|
||||
|
||||
// Hide the dialog
|
||||
screen.classList.remove('visible');
|
||||
|
||||
// Call the appropriate callback, if it is defined
|
||||
if (evt.target === yes && confirm.callback) {
|
||||
confirm.callback();
|
||||
} else if (evt.target === no && cancel.callback) {
|
||||
cancel.callback();
|
||||
}
|
||||
}
|
||||
|
||||
return screen;
|
||||
}
|
||||
|
||||
};
|
||||
}());
|
||||
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Shim for navigator.mozHour12 API.
|
||||
* Send `localechanged` event if mozHour12 is changed.
|
||||
*
|
||||
* App need include following permission in manifest:
|
||||
*
|
||||
* "settings":{ "access": "readonly" }
|
||||
*/
|
||||
(function(){
|
||||
'use strict';
|
||||
// not polyfill if API already exists
|
||||
if (window.navigator.mozHour12 || window.navigator.hour12) {return;}
|
||||
|
||||
// mock mozHour12 onto window.navigator
|
||||
window.navigator.mozHour12 = undefined;
|
||||
|
||||
// set hour12 and emit the locale change event if value changed
|
||||
var _setMozHour12 = function(result) {
|
||||
if (result === null) {
|
||||
// locale.hour12 may be true, false, `undefined` or `null`.
|
||||
// We can't write to mozSettings with `undefined`, but we want to cast
|
||||
// null to be undefined because then we can easily assign
|
||||
// Intl hour12: navigator.mozHour12 which is a paradigm we want to use.
|
||||
//
|
||||
// if you set hour12 to undefined it uses automatic value, but it doesn't
|
||||
// work if you set it to `null`, so we cast here to `undefined`.
|
||||
result = undefined;
|
||||
}
|
||||
if (window.navigator.mozHour12 !== result) {
|
||||
window.navigator.mozHour12 = result;
|
||||
// emit the locale change event
|
||||
window.dispatchEvent(new CustomEvent('timeformatchange'));
|
||||
}
|
||||
};
|
||||
|
||||
// handler observer event
|
||||
var _hour12Handler = function(event) {
|
||||
_setMozHour12(event.settingValue);
|
||||
};
|
||||
|
||||
var _kLocaleTime = 'locale.hour12';
|
||||
// update mozHour12 to real value
|
||||
var req = window.navigator.mozSettings.createLock().get(_kLocaleTime);
|
||||
req.onsuccess = function() {
|
||||
_setMozHour12(req.result[_kLocaleTime]);
|
||||
};
|
||||
// monitor settings changes
|
||||
window.navigator.mozSettings.addObserver(_kLocaleTime, _hour12Handler);
|
||||
})();
|
||||
|
|
@ -0,0 +1,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);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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);}}};
|
||||
|
|
@ -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);}};
|
||||
|
|
@ -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};})();
|
||||
|
|
@ -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;}};
|
||||
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -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;}}};
|
||||
|
|
@ -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);}};
|
||||
|
|
@ -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));
|
||||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
|
@ -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});}}}
|
||||
|
|
@ -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);}};};}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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));
|
||||
|
|
@ -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();}};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
245
assets/webapps/communications.gaiamobile.org/shared/js/l10n.js
Normal 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={'&':'&','<':'<','>':'>',};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);
|
||||
|
|
@ -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();}());
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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);};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||