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,28 @@
|
|||
;;(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));
|
||||
7
assets/webapps/homescreen.gaiamobile.org/shared/elements/gaia-site-icon/external/style.css
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/* XXX Bug 1018269
|
||||
* animation keyframes have to be specified outside of the shadow DOM.
|
||||
*/
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(1deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#5F5F5F" d="M42 0c23.3 0 42 18.7 42 42S65.3 84 42 84 0 65.3 0 42 18.7 0 42 0z"/><path fill="#FFF" d="M60.7 40.1c-.9-9.3-9.3-16.8-18.7-16.8-10.3 0-18.7 8.4-18.7 18.7S31.7 60.7 42 60.7c3.7 0 7.5-.9 10.3-2.8l-2.8-3.7C46.7 56.1 44.8 57 42 57c-8.4 0-14.9-6.5-14.9-14.9s6.5-15 14.9-15c7.5 0 13.1 5.6 14 13.1h-6.5l7.5 7.5c.9.9 2.8.9 3.7 0l7.5-7.5h-7.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 443 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#E51E1E" d="M42 0c23.3 0 42 18.7 42 42S65.3 84 42 84 0 65.3 0 42 18.7 0 42 0z"/><path fill="#FFF" d="M60.7 40.1c-.9-9.3-9.3-16.8-18.7-16.8-10.3 0-18.7 8.4-18.7 18.7S31.7 60.7 42 60.7c3.7 0 7.5-.9 10.3-2.8l-2.8-3.7C46.7 56.1 44.8 57 42 57c-8.4 0-14.9-6.5-14.9-14.9s6.5-15 14.9-15c7.5 0 13.1 5.6 14 13.1h-6.5l7.5 7.5c.9.9 2.8.9 3.7 0l7.5-7.5h-7.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 443 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="24.134" y1="31.414" x2="81.176" y2="46.699"><stop offset="0" stop-color="#00B4CF"/><stop offset=".979" stop-color="#00B4CF" stop-opacity="0"/></linearGradient><path fill-rule="evenodd" clip-rule="evenodd" fill="url(#a)" d="M83.9 39.5v2.6h-3.3c0 21.3-17.3 38.6-38.7 38.6C20.6 80.7 3.3 63.4 3.3 42 3.3 20.6 20.6 3.3 42 3.3V0C18.8 0 0 18.8 0 42s18.8 42 42 42 42-18.8 42-42c0-.8 0-1.7-.1-2.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 518 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#E51E1E" d="M42 0c23.3 0 42 18.8 42 42 0 23.3-18.8 42-42 42-23.1.1-42-18.7-42-42C0 18.8 18.9 0 42 0z"/><path fill="#FFF" d="M42 25.4c-.9 0-2.5 2.5-3.4 5.5 1.1-.2 2.3-.3 3.5-.3s2.3.1 3.3.3c-.9-3-2.5-5.5-3.4-5.5M27.1 39.1l5.7-5.7c1.7-8.3 6.7-16.7 9.2-16.7s7.5 8.4 9.2 16.7l5.7 5.7V54h-4l-5.6-5.6c-.6.6-1.2 1.1-1.9 1.5h-7c-.7-.4-1.3-.9-1.9-1.5L31.1 54h-4V39.1M16.6 59.2c.4.9 1.3 1.5 2.4 1.5h46c1 0 1.9-.6 2.4-1.5H16.6z"/></svg>
|
||||
|
After Width: | Height: | Size: 513 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#333" d="M42 0c23.3 0 42 18.8 42 42 0 23.3-18.8 42-42 42-23.1.1-42-18.7-42-42C0 18.8 18.9 0 42 0z"/><path fill="#FFF" d="M29.9 50c.4.9 1.3 1.5 2.4 1.5h19.3c1 0 1.9-.6 2.4-1.5H29.9zm22.3-17.8L51 33.4v.1L42.4 42c-.3.3-.7.3-1 0l-8.6-8.6v-.1l-1.2-1.2c-1.6.3-3.1.6-4.6.9.2.5.4 1 .9 1.5l11.3 11.3c1.5 1.5 3.8 1.5 5.3 0l11.3-11.3c.4-.4.7-.9.9-1.5-1.4-.3-2.9-.6-4.5-.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 459 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="42" x2="42" y2="84"><stop offset="0" stop-color="#E5EBEF"/><stop offset="1" stop-color="#EDF1F4"/></linearGradient><path fill="url(#a)" d="M42 0c23.3 0 42 18.8 42 42 0 23.3-18.8 42-42 42-23.1.1-42-18.7-42-42C0 18.8 18.9 0 42 0z"/><path fill="#9AB0C0" d="M41.5 25.9c-.9 0-2.5 2.6-3.5 5.7 1.1-.2 2.3-.3 3.6-.3 1.2 0 2.4.1 3.4.3-1-3.1-2.6-5.7-3.5-5.7M43 53.3h2.2c.3.9.4 2 .4 3.1 0 4-3.1 7.2-4.1 7.2s-4.1-3.2-4.1-7.2c0-1.1.1-2.2.4-3.1H40c-.2.5-.3 1.2-.3 1.9 0 1.8 1.4 3.3 1.9 3.3s1.9-1.5 1.9-3.3c-.2-.7-.3-1.4-.5-1.9M26.1 40l5.9-5.9c1.7-8.6 6.9-17.2 9.5-17.2s7.8 8.7 9.5 17.2l5.9 5.9v15.4h-4.1l-5.7-5.7c-.6.6-1.2 1.1-1.9 1.5h-7.3c-.7-.4-1.3-.9-1.9-1.5l-5.7 5.7h-4.1L26.1 40"/></svg>
|
||||
|
After Width: | Height: | Size: 799 B |
|
|
@ -0,0 +1,32 @@
|
|||
;'use strict';window.GaiaAppIcon=(function(exports){const LAUNCH_TIMEOUT=500;const PREDEFINED_ICONS={default:'default_icon.svg',unrecoverable:'app_install_unrecoverable.svg',installing:'app_installing.svg',paused:'app_install_canceled.svg',failed:'app_install_failed.svg'};const SHADOW_BLUR=1;const SHADOW_OFFSET={x:1,y:1};const SHADOW_COLOR='rgba(0, 0, 0, 0.2)';const DEFAULT_BACKGROUND_COLOR='rgb(228, 234, 238)';const CANVAS_PADDING=2*window.devicePixelRatio;const FAVICON_SCALE=0.55;var proto=Object.create(HTMLElement.prototype);var base=window.GAIA_ICONS_BASE_URL||window.COMPONENTS_BASE_URL||'bower_components/';var baseurl=window.GaiaAppIconBaseurl||base+'gaia-site-icon/';proto.createdCallback=function(){this._template=template.content.cloneNode(true);var shadow=this.createShadowRoot();shadow.appendChild(this._template);this._container=shadow.getElementById('image-container');this._titleContainer=shadow.getElementById('title-container');this._subtitle=shadow.getElementById('subtitle');this._showName=true;this._image=null;this._app=null;this._bookmark=null;this._entryPoint='';this._size=-1;this._hasIcon=false;this._hasUserSetIcon=false;this._hasPredefinedIcon=false;this._iconUrl=null;this._pendingIconUrl=null;this._pendingIconRefresh=false;this._lastState=null;this._predefinedIcons={};for(var key in PREDEFINED_ICONS){this._predefinedIcons[key]=baseurl+'images/'+PREDEFINED_ICONS[key];}
|
||||
this.tabIndex=0;this.setAttribute('role','link');var activate=()=>{if(!this.dispatchEvent(new CustomEvent('activated',{cancelable:true}))){return;}
|
||||
this.launch();};this.addEventListener('keydown',(e)=>{switch(e.keyCode){case 32:case 13:activate();}});this.addEventListener('click',activate);this.refresh();};Object.defineProperty(proto,'app',{get:function(){return this._app;},set:function(app){if(this._app){this._app.removeEventListener('progress',this);this._app.removeEventListener('downloaderror',this);this._app.removeEventListener('downloadsuccess',this);this._app.removeEventListener('downloadapplied',this);}
|
||||
delete this.dataset.testIconUrl;this._cancelIconLoad();this._removeOldIcon();this._hasIcon=false;this._app=app;if(!app){return;}
|
||||
this._bookmark=null;this._app.addEventListener('progress',this);this._app.addEventListener('downloaderror',this);this._app.addEventListener('downloadsuccess',this);this._app.addEventListener('downloadapplied',this);this._container.classList.add('initial-load');},enumerable:true});Object.defineProperty(proto,'entryPoint',{get:function(){return this._entryPoint;},set:function(entryPoint){this._entryPoint=entryPoint?entryPoint:'';this.refresh();},enumerable:true});Object.defineProperty(proto,'state',{get:function(){if(!this.app){return'unavailable';}
|
||||
if(this.app.installState==='pending'&&!this.app.downloadAvailable&&!this.app.readyToApplyDownload){return'unrecoverable';}
|
||||
if(this.app.downloading){return'installing';}
|
||||
if(this.app.downloadError){if(this.app.downloadError.name==='DOWNLOAD_CANCELED'){return'paused';}else{return'error';}}
|
||||
if(this.app.installState==='pending'){return'paused';}
|
||||
return'installed';},enumberable:true});Object.defineProperty(proto,'name',{get:function(){return this._subtitle?this._subtitle.textContent:'';},enumberable:true});Object.defineProperty(proto,'size',{get:function(){return(this._size===-1)?this.clientWidth:this._size;},set:function(size){this._size=size>=0?size:-1;},enumerable:true});Object.defineProperty(proto,'bookmark',{get:function(){return this._bookmark;},set:function(bookmark){if(!this._bookmark||this._bookmark.id!==bookmark.id){this.app=null;}
|
||||
this._bookmark=bookmark;},enumerable:true});Object.defineProperty(proto,'icon',{get:function(){if(window.VulpesCompat&&this.app&&this._vulpesSourceIcon)return Promise.resolve(this._vulpesSourceIcon);return new Promise((resolve,reject)=>{if(!this._hasIcon||this._hasPredefinedIcon){return reject();}
|
||||
var image=this._container.querySelector('img');if(!image){return reject();}
|
||||
var canvas=document.createElement('canvas');var size=this.size*window.devicePixelRatio;canvas.width=canvas.height=size;var ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.drawImage(image,0,0,size,size);try{canvas.toBlob((blob)=>{resolve(blob);});}catch(e){console.error('Failed to get canvas data for icon');reject(e);}});},set:function(blob){if(blob){this._prepareIconLoader();this._hasUserSetIcon=true;this._pendingIconUrl='user-set';this._image.src=URL.createObjectURL(blob);}else{this._pendingIconUrl=null;delete this.dataset.testIconUrl;this._cancelIconLoad();this._removeOldIcon();}},enumerable:true});Object.defineProperty(proto,'showName',{get:function(){return this._showName;},set:function(show){show=show?true:false;if(this._showName!==show){this._showName=show;this._titleContainer.classList.toggle('hidden',!show);}},enumerable:true});Object.defineProperty(proto,'isUserSet',{get:function(){return this._iconUrl==='user-set';},enumerable:true});proto._cancelIconLoad=function(){if(!this._image){return;}
|
||||
this._pendingIconUrl=null;this._image.onload=null;this._image.src='';this._image=null;};proto._removeOldIcon=function(){while(this._container.children.length>1){this._container.removeChild(this._container.lastChild);}};proto.launch=function(){if(!this.app&&!this.bookmark){return;}
|
||||
this.classList.add('launching');setTimeout(()=>{this.classList.remove('launching');},LAUNCH_TIMEOUT);if(this.app){window.performance.mark('appLaunch@'+this.app.origin);this.app.launch(this.entryPoint);}else{var features={name:this.bookmark.name,remote:true};if(this.bookmark.scope){features.scope=this.bookmark.scope;}
|
||||
window.open(this.bookmark.url,'_samescope',Object.keys(features).map(function eachFeature(key){return encodeURIComponent(key)+'='+
|
||||
encodeURIComponent(features[key]);}).join(','));}};proto._prepareIconLoader=function(){this._cancelIconLoad();this._hasPredefinedIcon=false;this._hasUserSetIcon=false;this._image=document.createElement('img');this._image.setAttribute('role','presentation');this._image.onload=()=>{this._image.onload=()=>{this._removeOldIcon();this._container.appendChild(this._image);this._container.classList.remove('initial-load');this._image=null;this._hasIcon=true;if(this._pendingIconUrl){this._iconUrl=this.dataset.testIconUrl=this._pendingIconUrl;this._pendingIconUrl=null;}
|
||||
if(!this._hasPredefinedIcon){this.dispatchEvent(new CustomEvent('icon-loaded'));}
|
||||
if(this._pendingIconRefresh){this._image=null;this._pendingIconRefresh=false;this.refresh();}};if(!this._hasUserSetIcon){var canvas=document.createElement('canvas');var size=this.size*window.devicePixelRatio;canvas.width=canvas.height=size;var ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.shadowColor=SHADOW_COLOR;ctx.shadowBlur=SHADOW_BLUR;ctx.shadowOffsetX=SHADOW_OFFSET.x;ctx.shadowOffsetY=SHADOW_OFFSET.y;if(this.bookmark&&!this._hasPredefinedIcon){if(this._image.width<=size/2){ctx.beginPath();ctx.arc(size/2,size/2,size/2-CANVAS_PADDING,0,2*Math.PI);ctx.fillStyle=DEFAULT_BACKGROUND_COLOR;ctx.fill();ctx.shadowBlur=0;ctx.shadowOffsetY=0;ctx.mozImageSmoothingEnabled=false;var iconSize=size*FAVICON_SCALE;ctx.drawImage(this._image,(size-iconSize)/2,(size-iconSize)/2,iconSize,iconSize);}else{var clipCanvas=document.createElement('canvas');clipCanvas.width=clipCanvas.height=size;var clipCtx=clipCanvas.getContext('2d',{willReadFrequently:true});clipCtx.beginPath();clipCtx.arc(size/2,size/2,size/2-CANVAS_PADDING,0,2*Math.PI);clipCtx.clip();clipCtx.drawImage(this._image,CANVAS_PADDING,CANVAS_PADDING,size-CANVAS_PADDING*2,size-CANVAS_PADDING*2);ctx.drawImage(clipCanvas,0,0);}}else{ctx.drawImage(this._image,CANVAS_PADDING,CANVAS_PADDING,size-CANVAS_PADDING*2,size-CANVAS_PADDING*2);}
|
||||
canvas.toBlob(function(image,blob){if(image.onload){image.src=URL.createObjectURL(blob);}}.bind(this,this._image));}
|
||||
if(this._hasUserSetIcon){this._image.onload();}};this._image.onerror=(e)=>{console.error('Failed to load icon',e);this._pendingIconUrl=null;if(this._pendingIconRefresh){this._pendingIconRefresh=false;this._image=null;this.refresh();}else if(!this._hasIcon){this._image.onerror=null;this._setPredefinedIcon('default');}else{this._image=null;}};};proto._setPredefinedIcon=function(name){this._hasPredefinedIcon=true;this._image.src=this._pendingIconUrl=this._predefinedIcons[name];};proto._localizeString=function(str){var userLang=document.documentElement.lang;if(navigator.mozL10n&&navigator.mozL10n.ctx.qps.indexOf(userLang)!==-1){return navigator.mozL10n.qps[userLang].translate(str);}
|
||||
return str;};proto.updateName=function(){if(this.app){var userLang=document.documentElement.lang;var ep=this.entryPoint||undefined;this.app.getLocalizedValue('short_name',userLang,ep).then((shortName)=>{this._subtitle.textContent=this._localizeString(shortName);},()=>{this.app.getLocalizedValue('name',userLang,ep).then((name)=>{this._subtitle.textContent=this._localizeString(name);},(e)=>{var manifest=this.app.manifest||this.app.updateManifest;if(manifest){var nameObject=(manifest.entry_points&&this.entryPoint)?manifest.entry_points[this.entryPoint]:manifest;this._subtitle.textContent=this._localizeString(nameObject.short_name||nameObject.name);}else{console.error('Error retrieving app name',e);this._subtitle.textContent='';}});});}else if(this.bookmark){this._subtitle.textContent=this.bookmark.name;}else{this._subtitle.textContent='';}};proto.refresh=function(){if(!this._template){return;}
|
||||
this.updateName();this._container.classList.remove('downloading');if(this.app){this.dataset.identifier=this.app.manifestURL+
|
||||
(this.entryPoint?'/'+this.entryPoint:'');}else if(this.bookmark){this.dataset.identifier=this.bookmark.id;}else{delete this.dataset.identifier;}
|
||||
if(this._image){this._pendingIconRefresh=true;return;}
|
||||
var size=this.size*window.devicePixelRatio;if(size<1){return;}
|
||||
this._prepareIconLoader();if(this.bookmark){if(!this._hasIcon){this._setPredefinedIcon('default');}
|
||||
return;}
|
||||
var state=this._lastState=this.state;switch(state){case'unrecoverable':this._setPredefinedIcon('unrecoverable');break;case'installing':this._setPredefinedIcon('installing');this._container.classList.add('downloading');break;case'paused':this._setPredefinedIcon('paused');break;case'error':this._setPredefinedIcon('failed');break;case'installed':var handleError=function(image,e){if(e.message!=='NO_ICON'&&e.name!=='NO_ICON'){console.error('Failed to retrieve icon',e);}if(image.onload&&!this._hasIcon){this._setPredefinedIcon('default');}else{this._image=null;}};var getImage=()=>{navigator.mozApps.mgmt.getIcon(this.app,size,this.entryPoint).then(function(image,blob){this._pendingIconUrl='app-icon';if(window.VulpesCompat){this._vulpesSourceIcon=blob;this._hasUserSetIcon=true;}if(image.onload){image.src=URL.createObjectURL(blob);}}.bind(this,this._image),handleError.bind(this,this._image));};getImage();break;}};proto.handleEvent=function(e){switch(e.type){case'progress':case'downloadsuccess':if(this.state!==this._lastState){this.refresh();}
|
||||
break;case'downloaderror':case'downloadapplied':this.refresh();break;}};var template=document.createElement('template');var stylesheet=baseurl+'style.css';template.innerHTML=`<style>@import url(${stylesheet});</style>
|
||||
<div id="image-container"><div id="spinner"></div></div>
|
||||
<div id="title-container"><div dir="auto" id="subtitle"></div></div>`;return document.registerElement('gaia-app-icon',{prototype:proto});})(window);
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
gaia-app-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
#image-container {
|
||||
position: relative;
|
||||
transition: visibility 0.2s, opacity 0.2s;
|
||||
}
|
||||
|
||||
#image-container.initial-load {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#image-container > #spinner {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#image-container.downloading > #spinner {
|
||||
display: block;
|
||||
background: no-repeat center center url('images/app_install_spinner.svg');
|
||||
background-size: contain;
|
||||
animation: rotate 2s infinite linear;
|
||||
}
|
||||
|
||||
#image-container,
|
||||
#image-container > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#subtitle {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
#title-container {
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
}
|
||||
|
||||
#title-container.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 312 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,844 @@
|
|||
'use strict';
|
||||
/* global devicePixelRatio */
|
||||
/* global GaiaGrid */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const ACTIVE_SCALE = 1.4;
|
||||
|
||||
/* This is an alternative delay used to initiate icon movement when in edit
|
||||
* mode. The normal contextmenu delay is too long once we're in edit mode
|
||||
* and causes user confusion, but a short delay globally would cause too many
|
||||
* accidental triggers of edit mode.
|
||||
*/
|
||||
const EDIT_LONG_PRESS_DELAY = 200;
|
||||
|
||||
/* The movement threshold to use for the above synthetic long-press. */
|
||||
const EDIT_LONG_PRESS_THRESHOLD = 4 * devicePixelRatio;
|
||||
|
||||
/* This delay is the time passed once users stop the finger over an icon and
|
||||
* the rearrange is performed */
|
||||
const REARRANGE_DELAY = 30;
|
||||
|
||||
/* The page is scrolled via javascript if an icon is being moved, and is
|
||||
* within a length of a page edge configured by this value */
|
||||
const EDGE_PAGE_THRESHOLD = 50;
|
||||
|
||||
/* This is the delay before calling finish when handling touchend. */
|
||||
const TOUCH_END_FINISH_DELAY = 20;
|
||||
|
||||
/* The maximum frequency with which the grid will be rearranged in response
|
||||
* to the item position changing, in ms.
|
||||
*/
|
||||
const REARRANGE_FREQUENCY = 100;
|
||||
|
||||
/* The amount of cool-down time after a rearrange animation. To avoid
|
||||
* rearranging too frequently and causing jank and user confusion.
|
||||
*/
|
||||
const REARRANGE_COOLDOWN = 750;
|
||||
|
||||
const SCREEN_HEIGHT = window.innerHeight;
|
||||
|
||||
const scrollStep = Math.round(SCREEN_HEIGHT / EDGE_PAGE_THRESHOLD);
|
||||
|
||||
/* The scroll step will be 5 times bigger over the edge */
|
||||
const maxScrollStepFactor = 5;
|
||||
|
||||
function DragDrop(gridView) {
|
||||
this.gridView = gridView;
|
||||
this.container = gridView.element;
|
||||
this.scrollable = document.documentElement;
|
||||
this.container.addEventListener('touchstart', this);
|
||||
this.container.addEventListener('touchmove', this);
|
||||
this.container.addEventListener('touchend', this);
|
||||
window.addEventListener('touchcancel', this);
|
||||
this.container.addEventListener('click', this);
|
||||
this.container.addEventListener('contextmenu', this);
|
||||
}
|
||||
|
||||
DragDrop.prototype = {
|
||||
|
||||
/**
|
||||
* The current touchmove target.
|
||||
* @type {DomElement}
|
||||
*/
|
||||
target: null,
|
||||
|
||||
/**
|
||||
* The edit-mode long-press timeout
|
||||
*/
|
||||
longPressTimeout: null,
|
||||
|
||||
/**
|
||||
* The position of the first touchstart of the current event block
|
||||
*/
|
||||
touchStart: { x: 0, y: 0, screenX: 0, screenY: 0 },
|
||||
|
||||
/**
|
||||
* If we have moved an icon, this indicates that we need to save the state
|
||||
* @type {boolean}
|
||||
*/
|
||||
dirty: false,
|
||||
|
||||
/**
|
||||
* Whether or not we are in edit mode.
|
||||
* @type {boolean}
|
||||
*/
|
||||
inEditMode: false,
|
||||
|
||||
/**
|
||||
* The item being hovered over.
|
||||
* @type {GaiaGrid.GridItem}
|
||||
*/
|
||||
hoverItem: null,
|
||||
|
||||
/**
|
||||
* The group being hovered over.
|
||||
* @type {GaiaGrid.Group}
|
||||
*/
|
||||
hoverGroup: null,
|
||||
|
||||
/*
|
||||
* An element that's aligned to the top of the grid and should be
|
||||
* considered as obscuring the grid when in edit mode.
|
||||
*/
|
||||
editHeaderElement: null,
|
||||
|
||||
/**
|
||||
* The timeout used to rearrange the grid during dragging.
|
||||
*/
|
||||
rearrangeGridTimeout: null,
|
||||
|
||||
/**
|
||||
* The time of the last reposition call.
|
||||
*/
|
||||
lastRepositionTime: 0,
|
||||
|
||||
/**
|
||||
* Returns the maximum active scale value.
|
||||
*/
|
||||
get activeScale() {
|
||||
return ACTIVE_SCALE;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if we are currently dragging an icon.
|
||||
*/
|
||||
get inDragAction() {
|
||||
return this.target && this.target.classList.contains('active');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the delay before calling finish in response to a touch-end event.
|
||||
*/
|
||||
get touchEndFinishDelay() {
|
||||
return TOUCH_END_FINISH_DELAY;
|
||||
},
|
||||
|
||||
/**
|
||||
* Begins the drag/drop interaction.
|
||||
* Enlarges the icon.
|
||||
* Sets additional data to make the touchmove handler faster.
|
||||
*/
|
||||
begin: function(e) {
|
||||
// Stop icon launching while we are in active state
|
||||
this.gridView.stop();
|
||||
window.dispatchEvent(new CustomEvent('gaiagrid-dragdrop-begin'));
|
||||
|
||||
this.hoverItem = null;
|
||||
this.rearrangeDelay = null;
|
||||
this.doRearrange = null;
|
||||
this.enterEditMode();
|
||||
this.container.classList.add('dragging');
|
||||
this.icon.scale = ACTIVE_SCALE;
|
||||
this.icon.setActive(true);
|
||||
this.container.parentNode.style.overflow = 'hidden';
|
||||
|
||||
// Work around e.pageX/e.pageY being null (to make it easier to work with
|
||||
// injected events, or old versions of Marionette)
|
||||
this.initialPageX = (typeof e.pageX === 'undefined') ?
|
||||
this.icon.x : e.pageX;
|
||||
this.initialPageY = (typeof e.pageY === 'undefined') ?
|
||||
this.icon.y : e.pageY;
|
||||
this.currentTouch = {
|
||||
pageX: this.initialPageX,
|
||||
pageY: this.initialPageY
|
||||
};
|
||||
|
||||
var items = this.gridView.items;
|
||||
var lastElement = items[items.length - 1];
|
||||
this.maxScroll = lastElement.y + lastElement.pixelHeight +
|
||||
(this.icon.pixelHeight * ACTIVE_SCALE);
|
||||
|
||||
// If this is a group, or the sole icon in a group, make sure the
|
||||
// surrounding groups are marked as invalid-drop so you can't initiate a
|
||||
// move that would have no result.
|
||||
var itemIndex = this.icon.detail.index;
|
||||
if (this.icon.detail.type === 'divider') {
|
||||
this.icon.element.classList.add('invalid-drop');
|
||||
if (itemIndex > 0) {
|
||||
for (var i = itemIndex - 1; i >= 0; i--) {
|
||||
var item = items[i];
|
||||
if (item.detail.type === 'divider') {
|
||||
item.element.classList.add('invalid-drop');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var itemBefore = itemIndex ? items[itemIndex - 1] : null;
|
||||
var itemAfter = items[itemIndex + 1];
|
||||
|
||||
if ((itemAfter.detail.type === 'placeholder' ||
|
||||
itemAfter.detail.type === 'divider') &&
|
||||
(!itemBefore || itemBefore.detail.type === 'divider')) {
|
||||
if (itemBefore) {
|
||||
itemBefore.element.classList.add('invalid-drop');
|
||||
}
|
||||
var group, groupIndex = itemIndex;
|
||||
do {
|
||||
group = items[++groupIndex];
|
||||
} while (group.detail.type !== 'divider');
|
||||
group.element.classList.add('invalid-drop');
|
||||
}
|
||||
}
|
||||
|
||||
// Redraw the icon at the new position and scale
|
||||
this.updateIconPosition();
|
||||
|
||||
// Rearrange grid to highlight the group underneath the icon
|
||||
this.rearrangeGrid();
|
||||
},
|
||||
|
||||
finish: function() {
|
||||
// Complete the repositioning timeout
|
||||
if (this.rearrangeGridTimeout !== null) {
|
||||
this.rearrangeGrid();
|
||||
}
|
||||
|
||||
// Remove the dragging property after the icon has transitioned into
|
||||
// place to avoid jank due to animations starting that are disabled
|
||||
// when dragging.
|
||||
this.icon.element.addEventListener('transitionend', this);
|
||||
this.currentTouch = null;
|
||||
|
||||
if (this.doRearrange !== null) {
|
||||
if (this.rearrangeDelay) {
|
||||
clearTimeout(this.rearrangeDelay);
|
||||
}
|
||||
this.doRearrange.call(this);
|
||||
} else {
|
||||
this.icon.requestAttention();
|
||||
}
|
||||
|
||||
// Hand back responsibility to Grid view to render the dragged item.
|
||||
this.icon.scale = 1;
|
||||
this.icon.setActive(false);
|
||||
this.icon.element.classList.remove('hovering');
|
||||
this.target = null;
|
||||
|
||||
this.gridView.render();
|
||||
|
||||
// Save icon state if we need to
|
||||
if (this.dirty) {
|
||||
window.dispatchEvent(new CustomEvent('gaiagrid-saveitems'));
|
||||
}
|
||||
this.dirty = false;
|
||||
|
||||
setTimeout(function nextTick() {
|
||||
this.gridView.start();
|
||||
window.dispatchEvent(new CustomEvent('gaiagrid-dragdrop-finish'));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
finalize: function() {
|
||||
this.container.parentNode.style.overflow = '';
|
||||
this.container.classList.remove('dragging');
|
||||
this.container.classList.remove('hover-over-top');
|
||||
if (this.icon) {
|
||||
this.icon.element.removeEventListener('transitionend', this);
|
||||
this.icon = null;
|
||||
}
|
||||
if (this.hoverItem) {
|
||||
this.hoverItem.element.classList.remove('hovered');
|
||||
this.hoverItem = null;
|
||||
}
|
||||
if (this.hoverGroup) {
|
||||
this.hoverGroup.element.classList.remove('drop-target');
|
||||
this.hoverGroup = null;
|
||||
}
|
||||
for (var i = 0, iLen = this.gridView.items.length;
|
||||
i < iLen; i++) {
|
||||
var item = this.gridView.items[i];
|
||||
item.element.classList.remove('invalid-drop');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The closer to edge the faster (bigger step).
|
||||
** Distance 0px -> 5 times faster
|
||||
** Distance 25px -> 2.5 times faster
|
||||
** Distance 50px (EDGE_PAGE_THRESHOLD) -> 0 times
|
||||
*/
|
||||
getScrollStep: function(distanceToEdge) {
|
||||
var factor = maxScrollStepFactor;
|
||||
|
||||
if (distanceToEdge > 0) {
|
||||
factor *= ((EDGE_PAGE_THRESHOLD - distanceToEdge) /
|
||||
EDGE_PAGE_THRESHOLD);
|
||||
}
|
||||
|
||||
return Math.round(scrollStep * factor);
|
||||
},
|
||||
|
||||
/**
|
||||
* Positions the dragged icon, rearranges the grid and scrolls the page if
|
||||
* needed.
|
||||
* The page is scrolled via javascript if an icon is being moved,
|
||||
* and is within a percentage of a page edge.
|
||||
*/
|
||||
positionAndScrollIfNeeded: function() {
|
||||
var touch = this.currentTouch;
|
||||
if (!touch || !this.inDragAction) {
|
||||
this.isScrolling = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var scrollStep;
|
||||
var docScroll = this.scrollable.scrollTop;
|
||||
var distanceFromTop = Math.abs(touch.pageY - docScroll);
|
||||
var distanceFromHeader = distanceFromTop -
|
||||
(this.editHeaderElement ? this.editHeaderElement.clientHeight : 0);
|
||||
this.isScrolling = true;
|
||||
|
||||
if (distanceFromTop > SCREEN_HEIGHT - EDGE_PAGE_THRESHOLD) {
|
||||
var maxY = this.maxScroll;
|
||||
scrollStep = this.getScrollStep(SCREEN_HEIGHT - distanceFromTop);
|
||||
// We cannot exceed the maximum scroll value
|
||||
if (touch.pageY >= maxY || maxY - touch.pageY < scrollStep) {
|
||||
this.isScrolling = false;
|
||||
}
|
||||
} else if (touch.pageY > 0 && distanceFromHeader < EDGE_PAGE_THRESHOLD) {
|
||||
// We cannot go below the minimum scroll value
|
||||
scrollStep = -this.getScrollStep(distanceFromHeader);
|
||||
scrollStep = Math.max(scrollStep, -this.scrollable.scrollTop);
|
||||
if (scrollStep >= 0) {
|
||||
this.isScrolling = false;
|
||||
}
|
||||
} else {
|
||||
this.isScrolling = false;
|
||||
}
|
||||
|
||||
if (!this.isScrolling) {
|
||||
this.updateIconPosition();
|
||||
this.deferredRearrangeGrid();
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateIconPosition();
|
||||
this.scrollable.scrollTop += scrollStep;
|
||||
touch.pageY += scrollStep;
|
||||
|
||||
exports.requestAnimationFrame(this.positionAndScrollIfNeeded.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Highlights the group of the given icon index, or the group if the index
|
||||
* belongs to a group.
|
||||
* @param {Integer} index The grid index of the icon or group.
|
||||
*/
|
||||
highlightGroup: function(index) {
|
||||
for (var i = index, iLen = this.gridView.items.length;
|
||||
i < iLen; i++) {
|
||||
var item = this.gridView.items[i];
|
||||
if (item.detail.type === 'divider') {
|
||||
if (this.hoverGroup != item) {
|
||||
if (this.hoverGroup) {
|
||||
this.hoverGroup.element.classList.remove('drop-target');
|
||||
}
|
||||
this.hoverGroup = item;
|
||||
item.element.classList.add('drop-target');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
inFirstGroup: function(index) {
|
||||
for (var i = index; i >= 0; i--) {
|
||||
if (this.gridView.items[i].detail.type === 'divider') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Positions an icon on the grid using the current touch coordinates.
|
||||
*/
|
||||
updateIconPosition: function() {
|
||||
var oldX = this.icon.x;
|
||||
var oldY = this.icon.y;
|
||||
var newX = this.currentTouch.pageX - (this.initialPageX - this.icon.x);
|
||||
var newY = this.currentTouch.pageY - (this.initialPageY - this.icon.y);
|
||||
|
||||
// Adjust new icon coordinates for the slightly inflated scale so that
|
||||
// it appears centered around the touch point. Dividers aren't scaled
|
||||
// during dragging.
|
||||
if (this.icon.detail.type !== 'divider') {
|
||||
newX = newX - ((this.icon.scale * this.gridView.layout.gridItemWidth) -
|
||||
this.gridView.layout.gridItemWidth) / 2;
|
||||
newY = newY - ((this.icon.scale * this.icon.pixelHeight) -
|
||||
this.icon.pixelHeight) / 2;
|
||||
}
|
||||
|
||||
this.icon.setCoordinates(newX, newY);
|
||||
this.icon.render();
|
||||
this.icon.setCoordinates(oldX, oldY);
|
||||
},
|
||||
|
||||
/**
|
||||
* Will call rearrangeGrid after a short period, calculated based on how
|
||||
* recently the grid was last rearranged.
|
||||
*/
|
||||
deferredRearrangeGrid: function() {
|
||||
if (this.rearrangeGridTimeout !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var delay = Math.max(
|
||||
REARRANGE_FREQUENCY, REARRANGE_COOLDOWN -
|
||||
Math.max(0, Date.now() - this.lastRepositionTime));
|
||||
this.rearrangeGridTimeout = setTimeout(() => {
|
||||
this.rearrangeGridTimeout = null;
|
||||
this.rearrangeGrid();
|
||||
}, delay);
|
||||
},
|
||||
|
||||
/**
|
||||
* Rearranges the grid icons and groups based on the current position of
|
||||
* the dragged icon.
|
||||
*/
|
||||
rearrangeGrid: function() {
|
||||
if (this.rearrangeGridTimeout !== null) {
|
||||
clearTimeout(this.rearrangeGridTimeout);
|
||||
this.rearrangeGridTimeout = null;
|
||||
}
|
||||
|
||||
if (this.isScrolling) {
|
||||
// We don't want the grid to shift while we're auto-scrolling
|
||||
return;
|
||||
}
|
||||
|
||||
var iconIsDivider = this.icon.detail.type === 'divider';
|
||||
var pageX = this.currentTouch.pageX - (this.initialPageX - this.icon.x);
|
||||
var pageY = this.currentTouch.pageY - (this.initialPageY - this.icon.y);
|
||||
|
||||
// Reposition in the icons array if necessary.
|
||||
// Find the icon with the closest X/Y position of the move,
|
||||
// and insert ours before it.
|
||||
var foundIndex = 0;
|
||||
var insertDividerAtTop = !this.gridView.config.features.disableSections;
|
||||
pageX += this.gridView.layout.gridItemWidth / 2;
|
||||
pageY += this.icon.pixelHeight / 2;
|
||||
if (pageY >= 0) {
|
||||
foundIndex =
|
||||
this.gridView.getNearestItemIndex(pageX, pageY, iconIsDivider);
|
||||
|
||||
// If we're dragging a group over the first icon, a divider will
|
||||
// be inserted at the top.
|
||||
if (!(iconIsDivider && foundIndex === 0)) {
|
||||
this.container.classList.remove('hover-over-top');
|
||||
insertDividerAtTop = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the rearrange callback and hover item if we aren't hovering over
|
||||
// anything.
|
||||
this.doRearrange = null;
|
||||
if (this.rearrangeDelay) {
|
||||
clearTimeout(this.rearrangeDelay);
|
||||
this.rearrangeDelay = null;
|
||||
}
|
||||
if (this.hoverItem) {
|
||||
this.hoverItem.element.classList.remove('hovered');
|
||||
this.hoverItem = null;
|
||||
}
|
||||
|
||||
// Add the 'hovering' class to the dragged icon.
|
||||
if (foundIndex !== null) {
|
||||
this.icon.element.classList.add('hovering');
|
||||
} else {
|
||||
this.icon.element.classList.remove('hovering');
|
||||
}
|
||||
|
||||
// Nothing to do if we find the dragged icon or no icon
|
||||
if (!insertDividerAtTop &&
|
||||
(foundIndex === null || foundIndex === this.icon.detail.index)) {
|
||||
if (!iconIsDivider) {
|
||||
this.highlightGroup(this.icon.detail.index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var foundItem = this.gridView.items[foundIndex];
|
||||
|
||||
// If we're a divider being dragged, only allow dropping on either the
|
||||
// first item or another divider.
|
||||
if (iconIsDivider &&
|
||||
foundIndex > 0 &&
|
||||
foundItem.detail.type !== 'divider') {
|
||||
// Remove the hovering class from a group if it's not hovering over
|
||||
// a valid drop position or itself.
|
||||
this.icon.element.classList.remove('hovering');
|
||||
return;
|
||||
}
|
||||
|
||||
// If the item isn't a group, trigger the
|
||||
// hovered state on the found item.
|
||||
if (!insertDividerAtTop && !iconIsDivider) {
|
||||
this.hoverItem = foundItem;
|
||||
this.hoverItem.element.classList.add('hovered');
|
||||
}
|
||||
|
||||
// Delay rearranging when we're dragging groups to when they're dropped
|
||||
var rearrangeAfterDelay = !iconIsDivider;
|
||||
|
||||
// Determine if we want to create a new divider. We do this when hovering
|
||||
// over a divider with an icon, or when dragging an icon to the very top
|
||||
// of the grid.
|
||||
var createDivider = insertDividerAtTop;
|
||||
if (!insertDividerAtTop && !iconIsDivider &&
|
||||
(foundItem.detail.type === 'divider')) {
|
||||
// Allow dropping into a collapsed group if the new position is in
|
||||
// the top 2/3 of the group.
|
||||
if (foundItem.detail.collapsed &&
|
||||
pageY <= foundItem.y + (foundItem.pixelHeight * 2/3)) {
|
||||
rearrangeAfterDelay = false;
|
||||
foundItem.element.classList.remove('hovered');
|
||||
|
||||
// When dragging an item forwards, it gets placed after the found
|
||||
// item, adjust to the item before the divider.
|
||||
if (this.icon.detail.index < foundIndex) {
|
||||
foundItem = this.gridView.items[foundIndex - 1];
|
||||
}
|
||||
} else {
|
||||
createDivider = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (createDivider) {
|
||||
// Remove the group background highlight if we're between groups
|
||||
if (this.hoverGroup) {
|
||||
this.hoverGroup.element.classList.remove('drop-target');
|
||||
this.hoverGroup = null;
|
||||
}
|
||||
|
||||
// Cancel rearrangement if it would have no effect.
|
||||
var redundantRearrange = false;
|
||||
if (insertDividerAtTop) {
|
||||
if (iconIsDivider) {
|
||||
redundantRearrange = this.inFirstGroup(this.icon.detail.index - 1);
|
||||
} else {
|
||||
redundantRearrange =
|
||||
(this.icon.detail.index === 0 &&
|
||||
this.gridView.items[1].detail.type === 'placeholder');
|
||||
}
|
||||
} else {
|
||||
redundantRearrange = this.gridView.items[foundIndex].element.
|
||||
classList.contains('invalid-drop');
|
||||
}
|
||||
|
||||
if (redundantRearrange) {
|
||||
if (this.hoverItem) {
|
||||
this.hoverItem.element.classList.remove('hovered');
|
||||
this.hoverItem = null;
|
||||
}
|
||||
return;
|
||||
} else if (insertDividerAtTop) {
|
||||
this.container.classList.add('hover-over-top');
|
||||
}
|
||||
|
||||
this.doRearrange =
|
||||
this.createNewDivider.bind(this,
|
||||
insertDividerAtTop ? null : foundItem);
|
||||
} else {
|
||||
if (!iconIsDivider) {
|
||||
// Change the display of the group the icon is hovering over to
|
||||
// indicate it can be dropped.
|
||||
this.highlightGroup(foundIndex);
|
||||
}
|
||||
|
||||
this.doRearrange = this.rearrange.bind(this, foundItem);
|
||||
|
||||
if (rearrangeAfterDelay) {
|
||||
this.rearrangeDelay =
|
||||
setTimeout(this.doRearrange.bind(this), REARRANGE_DELAY);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new divider in GridView.items and rearranges the currently
|
||||
* dragged icon into the newly created section.
|
||||
* @param {Object} tDivider The divider after which to place a new divider,
|
||||
* or null to insert a divider at the beginning on the grid.
|
||||
*/
|
||||
createNewDivider: function(tDivider) {
|
||||
var items = this.gridView.items;
|
||||
var tIndex = tDivider ? items.indexOf(tDivider) + 1 : 0;
|
||||
|
||||
// Create the new divider
|
||||
var newDivider = new GaiaGrid.Divider();
|
||||
items.splice(tIndex, 0, newDivider);
|
||||
|
||||
// Place the dragged item into the new empty section
|
||||
var sIndex = items.indexOf(this.icon);
|
||||
this.rearrange(sIndex >= tIndex ? newDivider : tDivider);
|
||||
|
||||
// Make sure the new divider/group is in view
|
||||
newDivider.requestAttention();
|
||||
},
|
||||
|
||||
/**
|
||||
* Rearranges items in GridView.items
|
||||
* @param {Object} tItem The item to position the dragged icon at.
|
||||
*/
|
||||
rearrange: function(tItem) {
|
||||
// Clear the rearrange callback
|
||||
this.doRearrange = null;
|
||||
this.rearrangeDelay = null;
|
||||
|
||||
// We get a reference to the position of this.icon within the items
|
||||
// array. Because placeholders are shifting around while we are dragging,
|
||||
// we can't trust the detail.index attribute. This will be fixed on every
|
||||
// render call though.
|
||||
var sIndex = this.gridView.items.indexOf(this.icon);
|
||||
var tIndex = this.gridView.items.indexOf(tItem);
|
||||
|
||||
// Check how many items we need to rearrange (if it's a group, there will
|
||||
// be multiple items.
|
||||
var eIndex = sIndex;
|
||||
if (this.icon.detail.type === 'divider') {
|
||||
for (; sIndex > 0 &&
|
||||
this.gridView.items[sIndex - 1].detail.type !== 'divider';
|
||||
sIndex--) {}
|
||||
|
||||
// Modify the tIndex to make sure we're inserting immediately after
|
||||
// another divider or at the start of the container.
|
||||
if (this.gridView.items[tIndex].detail.type === 'divider') {
|
||||
tIndex++;
|
||||
} else {
|
||||
tIndex = 0;
|
||||
}
|
||||
|
||||
if (sIndex === tIndex) {
|
||||
// Nothing to do.
|
||||
return;
|
||||
}
|
||||
} else if (sIndex < tIndex) {
|
||||
tIndex++;
|
||||
}
|
||||
|
||||
// We may have changed tIndex, so refresh our item pointer
|
||||
tItem = this.gridView.items[tIndex];
|
||||
|
||||
// Remove the items from the array
|
||||
var lastItemOffset = eIndex - sIndex;
|
||||
var toInsert = this.gridView.items.splice(sIndex, lastItemOffset + 1);
|
||||
|
||||
// Craft the parameter list to reinsert the items at the correct place
|
||||
toInsert.unshift(tItem ? this.gridView.items.indexOf(tItem) :
|
||||
this.gridView.items.length, 0);
|
||||
|
||||
// Reinsert items
|
||||
this.rearrangeDelay = null;
|
||||
this.dirty = true;
|
||||
this.gridView.items.splice.apply(this.gridView.items, toInsert);
|
||||
|
||||
var oldX = this.icon.x;
|
||||
var oldY = this.icon.y;
|
||||
this.gridView.render();
|
||||
|
||||
// In this case, we don't want to compensate for the icon moving, so
|
||||
// we need to correct our initial values to stop the icon from jumping
|
||||
// after rearranging.
|
||||
this.initialPageX -= oldX - this.icon.x;
|
||||
this.initialPageY -= oldY - this.icon.y;
|
||||
|
||||
this.lastRepositionTime = Date.now();
|
||||
},
|
||||
|
||||
enterEditMode: function() {
|
||||
this.inEditMode = true;
|
||||
this.container.classList.add('edit-mode');
|
||||
document.body.classList.add('edit-mode');
|
||||
this.gridView.element.dispatchEvent(
|
||||
new CustomEvent('editmode-start'));
|
||||
document.addEventListener('visibilitychange', this);
|
||||
this.gridView.render();
|
||||
},
|
||||
|
||||
exitEditMode: function() {
|
||||
// If we're in the middle of a drag, cancel it.
|
||||
if (this.icon) {
|
||||
this.finish();
|
||||
this.finalize();
|
||||
}
|
||||
|
||||
this.inEditMode = false;
|
||||
this.cancelLongPressTimeout();
|
||||
this.container.classList.remove('edit-mode');
|
||||
document.body.classList.remove('edit-mode');
|
||||
this.gridView.element.dispatchEvent(new CustomEvent('editmode-end'));
|
||||
document.removeEventListener('visibilitychange', this);
|
||||
this.gridView.render();
|
||||
},
|
||||
|
||||
cancelLongPressTimeout: function() {
|
||||
if (this.longPressTimeout !== null) {
|
||||
clearTimeout(this.longPressTimeout);
|
||||
this.longPressTimeout = null;
|
||||
}
|
||||
},
|
||||
|
||||
inLongPressThreshold: function(x, y) {
|
||||
return (
|
||||
Math.abs(x - this.touchStart.screenX) < EDIT_LONG_PRESS_THRESHOLD &&
|
||||
Math.abs(y - this.touchStart.screenY) < EDIT_LONG_PRESS_THRESHOLD);
|
||||
},
|
||||
|
||||
/**
|
||||
* General event handler.
|
||||
*/
|
||||
handleEvent: function(e) {
|
||||
switch(e.type) {
|
||||
case 'visibilitychange':
|
||||
if (document.hidden) {
|
||||
this.exitEditMode();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'touchstart':
|
||||
this.cancelLongPressTimeout();
|
||||
|
||||
if (e.touches.length > 1) {
|
||||
if (this.inDragAction) {
|
||||
this.finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inEditMode && !this.icon) {
|
||||
this.touchStart.x = e.touches[0].pageX;
|
||||
this.touchStart.y = e.touches[0].pageY;
|
||||
this.touchStart.screenX = e.touches[0].screenX;
|
||||
this.touchStart.screenY = e.touches[0].screenY;
|
||||
this.longPressTimeout = setTimeout(() => {
|
||||
this.longPressTimeout = null;
|
||||
this.handleEvent({
|
||||
type: 'contextmenu',
|
||||
target: e.target,
|
||||
pageX: this.touchStart.x,
|
||||
pageY: this.touchStart.y,
|
||||
preventDefault: function() {},
|
||||
stopImmediatePropagation: function() {}
|
||||
});
|
||||
}, EDIT_LONG_PRESS_DELAY);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'contextmenu':
|
||||
this.cancelLongPressTimeout();
|
||||
|
||||
if (this.icon) {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.defaultPrevented) {
|
||||
// other handlers already handled this.
|
||||
// in the future, we should use the shadow root and dispatch a
|
||||
// "contextmenu" event from here instead.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.icon = this.gridView.findItemFromElement(e.target, true);
|
||||
|
||||
if (!this.icon || !this.icon.element || !this.icon.isDraggable() ||
|
||||
this.icon.detail.type === 'placeholder') {
|
||||
this.icon = null;
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this.target = this.icon.element;
|
||||
this.begin(e);
|
||||
|
||||
break;
|
||||
|
||||
case 'touchmove':
|
||||
var touch = e.touches[0];
|
||||
|
||||
if (this.inDragAction) {
|
||||
this.currentTouch = {
|
||||
pageX: touch.pageX,
|
||||
pageY: touch.pageY
|
||||
};
|
||||
|
||||
if (!this.isScrolling) {
|
||||
this.positionAndScrollIfNeeded();
|
||||
}
|
||||
} else if (this.longPressTimeout !== null &&
|
||||
!this.inLongPressThreshold(touch.screenX,
|
||||
touch.screenY)) {
|
||||
this.cancelLongPressTimeout();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'click':
|
||||
case 'touchcancel':
|
||||
if (this.inDragAction) {
|
||||
this.finish();
|
||||
this.finalize();
|
||||
}
|
||||
|
||||
this.cancelLongPressTimeout();
|
||||
break;
|
||||
|
||||
case 'touchend':
|
||||
if (this.inDragAction) {
|
||||
// Ensure the app is not launched
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// As contextmenu event can be synthesized, it's possible for it
|
||||
// to happen in the same event-loop as touchend, meaning
|
||||
// finish would be called immediately after begin. This would
|
||||
// mean that the icon doesn't get a chance to transition and
|
||||
// the transitionend that we expect won't be called, so finalize
|
||||
// also won't be called, leaving us in an inconsistent state.
|
||||
// It would be so great if there was a transitionstart event :(
|
||||
setTimeout(() => { this.finish(); }, TOUCH_END_FINISH_DELAY);
|
||||
}
|
||||
|
||||
this.cancelLongPressTimeout();
|
||||
break;
|
||||
|
||||
case 'transitionend':
|
||||
this.finalize();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.GridDragDrop = DragDrop;
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
;'use strict';(function(exports){const SHADOW_BLUR=1;const SHADOW_OFFSET_Y=1;const SHADOW_OFFSET_X=1;const SHADOW_COLOR='rgba(0, 0, 0, 0.2)';const DEFAULT_BACKGROUND_COLOR='rgb(228, 234, 238)';const UNSCALED_CANVAS_PADDING=2;const CANVAS_PADDING=UNSCALED_CANVAS_PADDING*devicePixelRatio;function IconRenderer(icon){this._icon=icon;}
|
||||
IconRenderer.TYPE={CLIP:'clip',FAVICON:'favicon',STANDARD:'standard',};IconRenderer.prototype={unscaledCanvasPadding:UNSCALED_CANVAS_PADDING*2,get _maxSize(){return this._icon.grid.layout.gridMaxIconSize;},_createCanvas:function(){const CANVAS_PADDING_BOTTOM=1*devicePixelRatio;var canvas=document.createElement('canvas');canvas.width=this._maxSize+(CANVAS_PADDING*2);canvas.height=this._maxSize+CANVAS_PADDING+CANVAS_PADDING_BOTTOM;return canvas;},_createClipCanvas:function(){var canvas=document.createElement('canvas');canvas.width=this._maxSize+(CANVAS_PADDING*2);canvas.height=this._maxSize+(CANVAS_PADDING*2);return canvas;},_decorateShadowCanvas:function(canvas){var ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.shadowColor=SHADOW_COLOR;ctx.shadowBlur=SHADOW_BLUR;ctx.shadowOffsetY=SHADOW_OFFSET_Y;ctx.shadowOffsetX=SHADOW_OFFSET_X;return ctx;},clip:function(img){return new Promise((resolve)=>{var shadowCanvas=this._createCanvas();var shadowCtx=this._decorateShadowCanvas(shadowCanvas);var clipCanvas=this._createClipCanvas();var clipCtx=clipCanvas.getContext('2d',{willReadFrequently:true});clipCtx.beginPath();clipCtx.arc(clipCanvas.width/2,clipCanvas.height/2,clipCanvas.height/2,0,2*Math.PI);clipCtx.clip();clipCtx.drawImage(img,0,0,clipCanvas.width,clipCanvas.height);shadowCtx.drawImage(clipCanvas,CANVAS_PADDING,CANVAS_PADDING,this._maxSize,this._maxSize);shadowCanvas.toBlob(resolve);});},favicon:function(img){if(img.width>this._icon.grid.layout.gridIconSize/2){return this.clip(img);}
|
||||
return new Promise((resolve)=>{var shadowCanvas=this._createCanvas();var shadowCtx=this._decorateShadowCanvas(shadowCanvas);var iconWidth;var iconHeight;shadowCtx.beginPath();shadowCtx.arc(shadowCanvas.width/2,shadowCanvas.height/2,shadowCanvas.height/2-CANVAS_PADDING,0,2*Math.PI,false);shadowCtx.fillStyle=DEFAULT_BACKGROUND_COLOR;shadowCtx.fill();iconWidth=iconHeight=this._maxSize*0.55;shadowCtx.shadowBlur=0;shadowCtx.shadowOffsetY=0;shadowCtx.mozImageSmoothingEnabled=false;shadowCtx.drawImage(img,(shadowCanvas.width-iconWidth)/2,(shadowCanvas.height-iconHeight)/2,iconWidth,iconHeight);shadowCanvas.toBlob(resolve);});},standard:function(img){return new Promise((resolve)=>{var shadowCanvas=this._createCanvas();var shadowCtx=this._decorateShadowCanvas(shadowCanvas);shadowCtx.drawImage(img,CANVAS_PADDING,CANVAS_PADDING,this._maxSize,this._maxSize);shadowCanvas.toBlob(resolve);});}};exports.GridIconRenderer=IconRenderer;}(window));
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
'use strict';
|
||||
/* global devicePixelRatio */
|
||||
/* global verticalPreferences */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const maxIconsPerRow = 4;
|
||||
|
||||
// 320 / 5 = 64px | 480 / 5 = 96px | 540 / 5 = 108px | ...
|
||||
const iconScaleFactorMaxIconsPerRow = 5;
|
||||
|
||||
const minIconsPerRow = 3;
|
||||
|
||||
// 320 / 3.8 = 84px | 480 / 3.8 = 126px | 540 / 3.8 = 142px | ...
|
||||
const iconScaleFactorMinIconsPerRow = 3.8;
|
||||
|
||||
const distanceBetweenIconsWithMinIconsPerRow = 36;
|
||||
|
||||
const distanceBetweenIconsWithMaxIconsPerRow = 38;
|
||||
|
||||
const eachTextRowHeight = 20;
|
||||
|
||||
var constraintSize;
|
||||
|
||||
function GridLayout(gridView) {
|
||||
this.gridView = gridView;
|
||||
|
||||
if (window.verticalPreferences) {
|
||||
verticalPreferences.get('grid.cols').then(value => {
|
||||
this.cols = value;
|
||||
this.onReady();
|
||||
}, this.onReady);
|
||||
verticalPreferences.addEventListener('updated', this);
|
||||
} else {
|
||||
this.onReady();
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout.prototype = {
|
||||
|
||||
minIconsPerRow: minIconsPerRow,
|
||||
|
||||
maxIconsPerRow: maxIconsPerRow,
|
||||
|
||||
_cols: minIconsPerRow,
|
||||
|
||||
_offsetY: 0,
|
||||
|
||||
_percent: 1,
|
||||
|
||||
/**
|
||||
* The visible height of each divider.
|
||||
* Calculated by the divider class and cached here.
|
||||
*/
|
||||
_dividerLineHeight: 0,
|
||||
|
||||
get percent() {
|
||||
return this._percent;
|
||||
},
|
||||
|
||||
get cols() {
|
||||
return this._cols;
|
||||
},
|
||||
|
||||
set cols(value) {
|
||||
if (!value || value === this._cols) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.verticalPreferences) {
|
||||
verticalPreferences.put('grid.cols', value);
|
||||
}
|
||||
|
||||
// Reset the y-offset because we will re-render everything anyway.
|
||||
this._offsetY = 0;
|
||||
|
||||
this._percent = value == minIconsPerRow ? 1 :
|
||||
minIconsPerRow / maxIconsPerRow;
|
||||
this._cols = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* The width of the grid.
|
||||
*/
|
||||
get constraintSize() {
|
||||
if (!constraintSize) {
|
||||
this.calculateSize();
|
||||
}
|
||||
return constraintSize;
|
||||
},
|
||||
|
||||
/**
|
||||
* The height of each grid item.
|
||||
*/
|
||||
get gridItemHeight() {
|
||||
var height = this.gridIconSize +
|
||||
(this._cols === minIconsPerRow ?
|
||||
distanceBetweenIconsWithMinIconsPerRow :
|
||||
distanceBetweenIconsWithMaxIconsPerRow);
|
||||
|
||||
// Increase height for additional rows of text.
|
||||
var defaultTextRows = 2;
|
||||
var definedRows = parseInt(
|
||||
this.gridView.element.getAttribute('text-rows'), 10);
|
||||
if (definedRows > defaultTextRows) {
|
||||
height += (definedRows - defaultTextRows) * eachTextRowHeight;
|
||||
}
|
||||
|
||||
return height;
|
||||
},
|
||||
|
||||
/**
|
||||
* The width of each grid item.
|
||||
* This number changes based on current zoom level.
|
||||
*/
|
||||
get gridItemWidth() {
|
||||
return this.constraintSize / this._cols;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the maximum size in pixels for an icon image. It is the size when
|
||||
* the grid is displayed with the minimum number of columns per row.
|
||||
*/
|
||||
get gridMaxIconSize() {
|
||||
var baseSize = (this.constraintSize / iconScaleFactorMinIconsPerRow);
|
||||
return baseSize * devicePixelRatio;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the icon image size depending on grid configuration and screen
|
||||
* characteristics.
|
||||
*/
|
||||
get gridIconSize() {
|
||||
var numCols = this._cols;
|
||||
|
||||
var size = this.constraintSize / numCols;
|
||||
if (numCols === minIconsPerRow) {
|
||||
size = this.constraintSize / iconScaleFactorMinIconsPerRow;
|
||||
} else if (numCols === maxIconsPerRow) {
|
||||
size = this.constraintSize / iconScaleFactorMaxIconsPerRow;
|
||||
}
|
||||
return Math.floor(size);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the current offset of the Y-axis for the current zoom level.
|
||||
* This value is updated by calling zoom.stepYAxis. For example, each
|
||||
* group of three icons, or a divider, should increment this value.
|
||||
* The value is reset and recalculated when the zoom level changes.
|
||||
*/
|
||||
get offsetY() {
|
||||
return this._offsetY;
|
||||
},
|
||||
|
||||
set offsetY(value) {
|
||||
this._offsetY = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* After we render a row we need to store the current position of the y-axis
|
||||
*/
|
||||
stepYAxis: function(value) {
|
||||
this._offsetY += value;
|
||||
},
|
||||
|
||||
/**
|
||||
* General event handler.
|
||||
*/
|
||||
handleEvent: function(e) {
|
||||
switch(e.type) {
|
||||
case 'updated':
|
||||
var prop = e.target;
|
||||
if (prop.name === 'grid.cols') {
|
||||
this.cols = parseInt(prop.value, 10);
|
||||
this.gridView.render();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
calculateSize: function() {
|
||||
constraintSize = Math.min(window.screen.width, window.screen.height);
|
||||
},
|
||||
|
||||
onReady: function() {
|
||||
window.dispatchEvent(new CustomEvent('gaiagrid-layout-ready'));
|
||||
}
|
||||
};
|
||||
|
||||
exports.GridLayout = GridLayout;
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,588 @@
|
|||
'use strict';
|
||||
/* global GridDragDrop */
|
||||
/* global GaiaGrid */
|
||||
/* global GridLayout */
|
||||
/* global GridZoom */
|
||||
/* global LazyLoader */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
/* The time for which we'll disable launching other icons after tapping on
|
||||
* an app icon. This will also disable edit mode.
|
||||
*/
|
||||
const APP_LAUNCH_TIMEOUT = 3000;
|
||||
|
||||
/**
|
||||
* GridView is a generic class to render and display a grid of items.
|
||||
* @param {Object} config Configuration object containing:
|
||||
* - element: The shadow root of the grid
|
||||
*/
|
||||
function GridView(config) {
|
||||
this.config = config;
|
||||
this.clickIcon = this.clickIcon.bind(this);
|
||||
this.onVisibilityChange = this.onVisibilityChange.bind(this);
|
||||
|
||||
if (config.features.zoom) {
|
||||
this.zoom = new GridZoom(this);
|
||||
}
|
||||
|
||||
this.layout = new GridLayout(this);
|
||||
|
||||
// Set columns if we have a 'cols' attribute
|
||||
if (config.element.hasAttribute('cols')) {
|
||||
this.layout.cols = parseInt(config.element.getAttribute('cols'), 10);
|
||||
}
|
||||
|
||||
// Enable event listeners when instantiated.
|
||||
this.start();
|
||||
}
|
||||
|
||||
GridView.prototype = {
|
||||
/**
|
||||
* List of all application icons.
|
||||
* Maps an icon identifier to an icon object.
|
||||
*/
|
||||
icons: {},
|
||||
|
||||
/**
|
||||
* Lists of all displayed objects in the homescreen.
|
||||
* Includes app icons, dividers, and bookmarks.
|
||||
*/
|
||||
items: [],
|
||||
|
||||
/**
|
||||
* Returns a reference to the gaia-grid element.
|
||||
*/
|
||||
get element() {
|
||||
return this.config.element;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the number of columns.
|
||||
*/
|
||||
set cols(value) {
|
||||
this.layout.cols = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* We are in the state of launching an app.
|
||||
*/
|
||||
_launchingApp: false,
|
||||
|
||||
/**
|
||||
* Adds an item into the items array.
|
||||
* If the item is an icon, add it to icons.
|
||||
* @param {Object} item The grid object, should inherit from GridItem.
|
||||
* @param {Object} insertTo The position to insert the item into our list.
|
||||
* @param {Boolean} expandGroup Expand the group this item is will be in.
|
||||
*/
|
||||
add: function(item, insertTo, expandGroup) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.identifier) {
|
||||
// If we already have an item with this identifier, exit.
|
||||
// This avoids a potential race condition where we might have duplicate
|
||||
// items with the same identifiers in the grid. This should not happen.
|
||||
if (this.icons[item.identifier]) {
|
||||
console.log('Error, duplicate identifier: ',
|
||||
item.identifier, new Error().stack);
|
||||
return;
|
||||
}
|
||||
|
||||
this.icons[item.identifier] = item;
|
||||
} else if (item.detail.type !== 'divider' &&
|
||||
item.detail.type !== 'placeholder') {
|
||||
// If the item does not have an identifier, and is not a placeholder
|
||||
// or divider, do not add it to the grid.
|
||||
console.log('Error, could not load identifier for object: ',
|
||||
JSON.stringify(item.detail));
|
||||
return;
|
||||
}
|
||||
|
||||
// If insertTo it is a number, splice.
|
||||
if (!isNaN(parseFloat(insertTo)) && isFinite(insertTo)) {
|
||||
this.items.splice(insertTo, 0, item);
|
||||
} else {
|
||||
insertTo = this.items.length;
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
if (expandGroup) {
|
||||
for (var i = insertTo + 1, iLen = this.items.length;
|
||||
i < iLen; i++) {
|
||||
var divider = this.items[i];
|
||||
if (divider.detail.type === 'divider') {
|
||||
if (divider.detail.collapsed) {
|
||||
divider.expand();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds nearest item by and returns an index, or null if an item isn't
|
||||
* found.
|
||||
* @param {Number} x relative to the screen
|
||||
* @param {Number} y relative to the screen
|
||||
* @param {Boolean} isRow whether the position is to be considered as a row
|
||||
* instead of an individual point.
|
||||
*/
|
||||
getNearestItemIndex: function(x, y, isRow) {
|
||||
var foundIndex = null;
|
||||
var leastDistance = null;
|
||||
var itemMiddleOffset = this.layout.gridItemWidth / 2;
|
||||
for (var i = 0, iLen = this.items.length; i < iLen; i++) {
|
||||
var item = this.items[i];
|
||||
|
||||
// Don't consider items that can't be dragged
|
||||
if (!item.isDraggable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If sections are disabled, don't consider dividers
|
||||
if (this.config.features.disableSections &&
|
||||
item.detail.type === 'divider') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not consider collapsed items, unless they are dividers.
|
||||
if (item.detail.type !== 'divider' &&
|
||||
item.element.classList.contains('collapsed')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var middleX = item.x + itemMiddleOffset;
|
||||
var middleY = item.y + item.pixelHeight / 2;
|
||||
|
||||
var xDistance = (isRow || item.detail.type === 'divider') ?
|
||||
0 : x - middleX;
|
||||
var yDistance = y - middleY;
|
||||
|
||||
var distance = Math.sqrt(
|
||||
xDistance * xDistance +
|
||||
yDistance * yDistance);
|
||||
if (leastDistance === null || distance < leastDistance) {
|
||||
leastDistance = distance;
|
||||
foundIndex = i;
|
||||
}
|
||||
}
|
||||
return foundIndex;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.element.addEventListener('click', this.clickIcon);
|
||||
window.addEventListener('visibilitychange', this.onVisibilityChange);
|
||||
},
|
||||
|
||||
stop: function() {
|
||||
this.element.removeEventListener('click', this.clickIcon);
|
||||
window.removeEventListener('visibilitychange', this.onVisibilityChange);
|
||||
},
|
||||
|
||||
findItemFromElement: function(element, excludeCollapsedIcons) {
|
||||
while (element && element.parentNode !== this.element) {
|
||||
element = element.parentNode;
|
||||
}
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var i, iLen = this.items.length;
|
||||
var identifier = element.dataset.identifier;
|
||||
var icon = this.icons[identifier];
|
||||
|
||||
// If the element didn't have an identifier, try to search for it
|
||||
// manually.
|
||||
if (!icon) {
|
||||
for (i = 0; i < iLen; i++) {
|
||||
if (this.items[i].element === element) {
|
||||
icon = this.items[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (icon && excludeCollapsedIcons) {
|
||||
// If this is a collapsed item, return its group instead
|
||||
if (icon.detail.type !== 'divider' &&
|
||||
icon.detail.type !== 'placeholder' &&
|
||||
icon.element.classList.contains('collapsed')) {
|
||||
for (i = icon.detail.index + 1; i < iLen; i++) {
|
||||
if (this.items[i].detail.type === 'divider') {
|
||||
return this.items[i];
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('Collapsed icon found with no group');
|
||||
icon = null;
|
||||
}
|
||||
}
|
||||
|
||||
return icon;
|
||||
},
|
||||
|
||||
onVisibilityChange: function() {
|
||||
this._launchingApp = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Launches an app.
|
||||
*/
|
||||
clickIcon: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var inEditMode = this.dragdrop && this.dragdrop.inEditMode;
|
||||
|
||||
var action = 'launch';
|
||||
if (e.target.classList.contains('remove')) {
|
||||
action = 'remove';
|
||||
}
|
||||
var icon = this.findItemFromElement(e.target);
|
||||
if (!icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'launch') {
|
||||
// We do not allow users to launch icons in edit mode
|
||||
if (inEditMode && e.target.classList.contains('icon')) {
|
||||
// Check if we're trying to edit a bookmark
|
||||
if (!icon.isEditable()) {
|
||||
return;
|
||||
}
|
||||
action = 'edit';
|
||||
} else {
|
||||
// If the icon can't be launched, bail out early
|
||||
if (!icon[action]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a 'launching' class to the icon to style it with CSS.
|
||||
icon.element.classList.add('launching');
|
||||
|
||||
// XXX: We can't have nice things. Remove the launching class after an
|
||||
// arbitrary time to restore the state. We want the icon to return
|
||||
// to its original state after launching the app, but visibilitychange
|
||||
// will not work because activities do not fire it.
|
||||
var returnTimeout = 500;
|
||||
setTimeout(function stateReturn() {
|
||||
if (icon.element) {
|
||||
icon.element.classList.remove('launching');
|
||||
}
|
||||
}, returnTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
if ((icon.detail.type === 'app' || icon.detail.type === 'bookmark') &&
|
||||
this._launchingApp) {
|
||||
return;
|
||||
}
|
||||
if ((icon.detail.type === 'app' && icon.appState === 'ready') ||
|
||||
icon.detail.type === 'bookmark') {
|
||||
this._launchingApp = true;
|
||||
if (this._launchingTimeout) {
|
||||
window.clearTimeout(this._launchingTimeout);
|
||||
this._launchingTimeout = null;
|
||||
}
|
||||
// This avoids some edge cases if we didn't get visibilitychange anyway.
|
||||
this._launchingTimeout = window.setTimeout(function() {
|
||||
this._launchingTimeout = null;
|
||||
this._launchingApp = false;
|
||||
}.bind(this), APP_LAUNCH_TIMEOUT);
|
||||
}
|
||||
|
||||
icon[action](e.target);
|
||||
},
|
||||
|
||||
/**
|
||||
* Scrubs the list of items, removing empty sections.
|
||||
*/
|
||||
cleanItems: function(skipDivider) {
|
||||
var appCount = 0;
|
||||
var toRemove = [];
|
||||
|
||||
this.items.forEach(function(item, idx) {
|
||||
if (item instanceof GaiaGrid.Divider) {
|
||||
if (appCount === 0) {
|
||||
toRemove.push(idx);
|
||||
}
|
||||
appCount = 0;
|
||||
} else {
|
||||
appCount++;
|
||||
}
|
||||
}, this);
|
||||
|
||||
toRemove.reverse();
|
||||
toRemove.forEach(function(idx) {
|
||||
var removed = this.items.splice(idx, 1)[0];
|
||||
removed.remove();
|
||||
}, this);
|
||||
|
||||
// There should always be a divider at the end, it's hidden in CSS when
|
||||
// not in edit mode.
|
||||
if (skipDivider) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the last item is not a divider, add a new divider at the end.
|
||||
var lastItem = this.items[this.items.length - 1];
|
||||
if (!lastItem || !(lastItem instanceof GaiaGrid.Divider)) {
|
||||
this.items.push(new GaiaGrid.Divider());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes placeholders from the grid.
|
||||
*/
|
||||
removeAllPlaceholders: function() {
|
||||
var toSplice = [];
|
||||
var previousItem;
|
||||
this.items.forEach(function(item, idx) {
|
||||
if (item instanceof GaiaGrid.Placeholder) {
|
||||
|
||||
// If the previous item is a divider, or there is no previous item,
|
||||
// and we are in edit mode, we do not remove the placeholder.
|
||||
// This is so the section will remain even if the user drags the
|
||||
// icon around. Bug 1014982
|
||||
if ((!previousItem ||
|
||||
(previousItem && previousItem instanceof GaiaGrid.Divider)) &&
|
||||
this.dragdrop && this.dragdrop.inDragAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
toSplice.push(idx);
|
||||
}
|
||||
|
||||
previousItem = item;
|
||||
}, this);
|
||||
|
||||
toSplice.reverse().forEach(function(idx) {
|
||||
this.items.splice(idx, 1)[0].remove();
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the grid view of all items.
|
||||
*/
|
||||
clear: function() {
|
||||
for (var i = 0, iLen = this.items.length; i < iLen; i++) {
|
||||
var item = this.items[i];
|
||||
if (item.element) {
|
||||
this.element.removeChild(item.element);
|
||||
|
||||
// We must de-reference element explicitly so we can re-use item
|
||||
// objects the next time we call render.
|
||||
item.element = null;
|
||||
item.lastX = null;
|
||||
item.lastY = null;
|
||||
item.lastScale = null;
|
||||
}
|
||||
}
|
||||
this.items = [];
|
||||
this.icons = {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates placeholders and injects them into the grid.
|
||||
* @param {Array} coordinates [x,y] coordinates on the grid of the first
|
||||
* item in grid units.
|
||||
* @param {Integer} idx The position of the first placeholder.
|
||||
* @param {Integer} idx The number of placeholders to create.
|
||||
*/
|
||||
createPlaceholders: function(coordinates, idx, count) {
|
||||
var isRTL = (document.documentElement.dir === 'rtl');
|
||||
for (var i = 0; i < count; i++) {
|
||||
var item = new GaiaGrid.Placeholder();
|
||||
this.items.splice(idx + i, 0, item);
|
||||
item.setPosition(idx + i);
|
||||
|
||||
var xPosition = (coordinates[0] + i) * this.layout.gridItemWidth;
|
||||
if (isRTL) {
|
||||
xPosition =
|
||||
(this.layout.constraintSize - this.layout.gridItemWidth) -
|
||||
xPosition;
|
||||
}
|
||||
item.setCoordinates(xPosition, this.layout.offsetY);
|
||||
|
||||
item.render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders all icons.
|
||||
* Positions app icons and dividers accoriding to available space
|
||||
* on the grid.
|
||||
* @param {Object} options Options to render with including:
|
||||
* - skipDivider {Boolean} Whether or not to skip the divider
|
||||
* - rerender {Boolean} Whether we should clean elements and re-render.
|
||||
*/
|
||||
render: function(options) {
|
||||
var self = this;
|
||||
options = options || {};
|
||||
|
||||
this.removeAllPlaceholders();
|
||||
this.cleanItems(options.skipDivider);
|
||||
|
||||
// Reset offset steps
|
||||
var oldHeight = this.layout.offsetY;
|
||||
this.layout.offsetY = 0;
|
||||
|
||||
// Grid render coordinates
|
||||
var x = 0;
|
||||
var y = 0;
|
||||
|
||||
/**
|
||||
* Steps the y-axis.
|
||||
* @param {Object} item
|
||||
*/
|
||||
function step(item) {
|
||||
var pixelHeight = item.pixelHeight;
|
||||
self.layout.stepYAxis(pixelHeight);
|
||||
|
||||
x = 0;
|
||||
y++;
|
||||
}
|
||||
|
||||
var pendingCachedIcons = 0;
|
||||
var onCachedIconRendered = () => {
|
||||
if (--pendingCachedIcons <= 0) {
|
||||
this.element.removeEventListener('cached-icon-rendered',
|
||||
onCachedIconRendered);
|
||||
this.element.dispatchEvent(new CustomEvent('cached-icons-rendered'));
|
||||
}
|
||||
};
|
||||
this.element.addEventListener('cached-icon-rendered',
|
||||
onCachedIconRendered);
|
||||
|
||||
var nextDivider = null;
|
||||
var oddDivider = true;
|
||||
var isRTL = (document.documentElement.dir === 'rtl');
|
||||
for (var idx = 0; idx < this.items.length; idx++) {
|
||||
var item = this.items[idx];
|
||||
|
||||
// Remove the element if we are re-rendering.
|
||||
if (options.rerender && item.element) {
|
||||
this.element.removeChild(item.element);
|
||||
item.element = null;
|
||||
}
|
||||
|
||||
if (item.detail.type === 'divider') {
|
||||
nextDivider = null;
|
||||
} else {
|
||||
if (!nextDivider) {
|
||||
for (var i = idx + 1; i < this.items.length; i++) {
|
||||
if (this.items[i].detail.type === 'divider') {
|
||||
nextDivider = this.items[i];
|
||||
oddDivider = !oddDivider;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to leave room for group headers
|
||||
if (nextDivider &&
|
||||
!nextDivider.detail.collapsed) {
|
||||
this.layout.offsetY += nextDivider.headerHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// If this item is in a collapsed group, we need to skip rendering.
|
||||
if (nextDivider && nextDivider.detail.collapsed) {
|
||||
item.setPosition(idx);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If the item would go over the boundary before rendering,
|
||||
// step the y-axis.
|
||||
if (x > 0 && item.gridWidth > 1 &&
|
||||
x + item.gridWidth >= this.layout.cols) {
|
||||
|
||||
// Insert placeholders to fill remaining space
|
||||
var remaining = this.layout.cols - x;
|
||||
this.createPlaceholders([x, y], idx, remaining);
|
||||
|
||||
// Increment the current index due to divider insertion
|
||||
idx += remaining;
|
||||
item = this.items[idx];
|
||||
|
||||
// Step the y-axis by the size of the last row.
|
||||
// For now we just check the height of the last item.
|
||||
var lastItemInRow = this.items[idx - 1];
|
||||
step(lastItemInRow);
|
||||
}
|
||||
|
||||
item.setPosition(idx);
|
||||
|
||||
if (!options.skipItems) {
|
||||
item.hasCachedIcon && ++pendingCachedIcons;
|
||||
var xPosition = x * this.layout.gridItemWidth;
|
||||
if (isRTL) {
|
||||
xPosition =
|
||||
(this.layout.constraintSize - this.layout.gridItemWidth) -
|
||||
xPosition;
|
||||
}
|
||||
item.setCoordinates(xPosition, this.layout.offsetY);
|
||||
if (!item.active) {
|
||||
item.render();
|
||||
}
|
||||
|
||||
if (item.detail.type === 'divider') {
|
||||
if (oddDivider) {
|
||||
item.element.classList.add('odd');
|
||||
} else {
|
||||
item.element.classList.remove('odd');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Increment the x-step by the sizing of the item.
|
||||
// If we go over the current boundary, reset it, and step the y-axis.
|
||||
x += item.gridWidth;
|
||||
if ((x >= this.layout.cols) && (idx < this.items.length - 1)) {
|
||||
step(item);
|
||||
}
|
||||
}
|
||||
|
||||
// All the children of this element are absolutely positioned and then
|
||||
// transformed, so the grid actually has no height. Fire an event that
|
||||
// embedders can listen to discover the grid height.
|
||||
if (this.layout.offsetY != oldHeight) {
|
||||
if (this.dragdrop && this.dragdrop.inDragAction) {
|
||||
// Delay size changes during drags to avoid jankiness when dragging
|
||||
// items around due to touch positions changing.
|
||||
this.layout.offsetY = oldHeight;
|
||||
} else {
|
||||
this.element.dispatchEvent(new CustomEvent('gaiagrid-resize',
|
||||
{ detail: this.layout.offsetY }));
|
||||
}
|
||||
}
|
||||
|
||||
this.element.setAttribute('cols', this.layout.cols);
|
||||
pendingCachedIcons === 0 && onCachedIconRendered();
|
||||
this.loadDragDrop();
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads dragdrop libraries and instantiates if necessary.
|
||||
* DragDrop libraries are lazy laoded to save on startup time. They are
|
||||
* loaded after the initial paint in order to paint the icons as fast
|
||||
* as possible.
|
||||
*/
|
||||
loadDragDrop: function() {
|
||||
if (!this.dragdrop && this.config.features.dragdrop) {
|
||||
LazyLoader.load('shared/elements/gaia_grid/js/grid_dragdrop.js', () => {
|
||||
if (this.dragdrop) {
|
||||
return;
|
||||
}
|
||||
this.dragdrop = new GridDragDrop(this);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.GridView = GridView;
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const pinchThreshold = Math.round(window.innerWidth / 12);
|
||||
|
||||
function GridZoom(gridView) {
|
||||
this.gridView = gridView;
|
||||
|
||||
this.touches = 0;
|
||||
this.zoomStartTouches = [];
|
||||
this.zoomStartDistance = 0;
|
||||
|
||||
this.container = document.getElementById('zoom');
|
||||
this.arrows = this.container.querySelector('.arrows');
|
||||
this.curtain = this.container.querySelector('.curtain');
|
||||
this.indicator = this.container.querySelector('.indicator');
|
||||
|
||||
this.start();
|
||||
window.addEventListener('gaiagrid-dragdrop-begin', this.stop.bind(this));
|
||||
window.addEventListener('gaiagrid-dragdrop-finish', this.start.bind(this));
|
||||
}
|
||||
|
||||
GridZoom.prototype = {
|
||||
|
||||
/**
|
||||
* Starts listening for touchstart events.
|
||||
* This is what starts listening events to start the drag & drop operation.
|
||||
*/
|
||||
start: function() {
|
||||
window.addEventListener('touchstart', this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops listening for touchstart events.
|
||||
* This will effectively disable pinch-to-zoom.
|
||||
*/
|
||||
stop: function() {
|
||||
window.removeEventListener('touchstart', this);
|
||||
},
|
||||
|
||||
_attachGestureListeners: function() {
|
||||
window.addEventListener('touchmove', this);
|
||||
window.addEventListener('touchend', this);
|
||||
},
|
||||
|
||||
_stopGestureListeners: function() {
|
||||
window.removeEventListener('touchmove', this);
|
||||
window.removeEventListener('touchend', this);
|
||||
},
|
||||
|
||||
_resetState: function() {
|
||||
this.zoomInProgress = false;
|
||||
|
||||
this.container.hidden = true;
|
||||
this.indicator.classList.remove('active');
|
||||
this.arrows.classList.remove('zooming', 'grow', 'shrink');
|
||||
this.arrows.style.transform = '';
|
||||
},
|
||||
|
||||
/**
|
||||
* General Event Handler
|
||||
*/
|
||||
handleEvent: function(e) {
|
||||
if (e.type === 'touchend' && this.zoomStartTouches) {
|
||||
if (!this.zoomInProgress) {
|
||||
this._resetState();
|
||||
}
|
||||
|
||||
this._stopGestureListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.touches || e.touches.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort touches by ascending pageX position.
|
||||
var touches = [e.touches[0], e.touches[1]].sort(function(a, b) {
|
||||
return a.pageX - b.pageX;
|
||||
});
|
||||
|
||||
var touchDistance = Math.sqrt(
|
||||
(touches[0].pageX - touches[1].pageX) *
|
||||
(touches[0].pageX - touches[1].pageX) +
|
||||
(touches[0].pageY - touches[1].pageY) *
|
||||
(touches[0].pageY - touches[1].pageY));
|
||||
|
||||
var layout = this.gridView.layout;
|
||||
|
||||
switch(e.type) {
|
||||
case 'touchstart':
|
||||
this.container.hidden = false;
|
||||
this.zoomStartTouches = touches;
|
||||
this.zoomStartDistance = touchDistance;
|
||||
|
||||
if (layout.cols < layout.maxIconsPerRow) {
|
||||
this.arrows.classList.add('grow');
|
||||
} else {
|
||||
this.arrows.classList.add('shrink');
|
||||
}
|
||||
|
||||
this.indicator.dataset.cols = layout.cols;
|
||||
this._attachGestureListeners();
|
||||
break;
|
||||
|
||||
case 'touchmove':
|
||||
if (layout.cols < layout.maxIconsPerRow &&
|
||||
touchDistance < this.zoomStartDistance &&
|
||||
Math.abs(touchDistance - this.zoomStartDistance) >
|
||||
pinchThreshold) {
|
||||
this.gridView.cols = layout.maxIconsPerRow;
|
||||
this.zoomInProgress = true;
|
||||
} else if (layout.cols > layout.minIconsPerRow &&
|
||||
touchDistance > this.zoomStartDistance &&
|
||||
Math.abs(touchDistance - this.zoomStartDistance) >
|
||||
pinchThreshold) {
|
||||
this.gridView.cols = layout.minIconsPerRow;
|
||||
this.zoomInProgress = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// For now just implement a canned animation
|
||||
this._stopGestureListeners();
|
||||
|
||||
var ontransitionend = function() {
|
||||
this.arrows.removeEventListener('transitionend', ontransitionend);
|
||||
// Change the indicator color at the end.
|
||||
this.indicator.classList.add('active');
|
||||
|
||||
this.gridView.render();
|
||||
|
||||
// Reset zoom element state after a set time.
|
||||
var zoomHideTime = 400;
|
||||
setTimeout(this._resetState.bind(this), zoomHideTime);
|
||||
}.bind(this);
|
||||
|
||||
this.arrows.addEventListener('transitionend', ontransitionend);
|
||||
|
||||
this.indicator.dataset.cols = layout.cols;
|
||||
|
||||
this.arrows.classList.add('zooming');
|
||||
// Force a sync reflow
|
||||
document.body.clientHeight;
|
||||
var scaleTransform = 'scale(1)';
|
||||
if (this.zoomInProgress && touchDistance < this.zoomStartDistance) {
|
||||
scaleTransform = 'scale(0.4)';
|
||||
}
|
||||
this.arrows.style.transform = scaleTransform;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
exports.GridZoom = GridZoom;
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
'use strict';
|
||||
/* global GaiaGrid */
|
||||
/* global GridIconRenderer */
|
||||
/* global MozActivity */
|
||||
/* jshint nonew: false */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const TYPE = 'bookmark';
|
||||
|
||||
/**
|
||||
* Represents a single bookmark icon on the homepage.
|
||||
*/
|
||||
function Bookmark(record, features) {
|
||||
this.detail = record;
|
||||
this.features = features || {};
|
||||
this.detail.type = TYPE;
|
||||
}
|
||||
|
||||
Bookmark.prototype = {
|
||||
|
||||
__proto__: GaiaGrid.GridItem.prototype,
|
||||
|
||||
/**
|
||||
* Bookmarks use a custom icon renderer because they are likely much
|
||||
* smaller than a standard icon.
|
||||
*/
|
||||
renderer: GridIconRenderer.TYPE.FAVICON,
|
||||
|
||||
/**
|
||||
* Returns the height in pixels of each icon.
|
||||
*/
|
||||
get pixelHeight() {
|
||||
return this.grid.layout.gridItemHeight;
|
||||
},
|
||||
|
||||
/**
|
||||
* Width in grid units for each icon.
|
||||
*/
|
||||
gridWidth: 1,
|
||||
|
||||
get name() {
|
||||
return this.detail.name;
|
||||
},
|
||||
|
||||
get icon() {
|
||||
return this.detail.icon || this.defaultIcon;
|
||||
},
|
||||
|
||||
get identifier() {
|
||||
return this.detail.id;
|
||||
},
|
||||
|
||||
update: GaiaGrid.GridItem.prototype.updateFromDatastore,
|
||||
|
||||
/**
|
||||
* Bookmarks are always editable unless noted otherwise in features.
|
||||
*/
|
||||
isEditable: function() {
|
||||
return this.features && this.features.isEditable !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Bookmarks are always removable unless noted otherwise in features.
|
||||
*/
|
||||
isRemovable: function() {
|
||||
return this.features && this.features.isRemovable !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Bookmarks are always draggable unless noted otherwise in features.
|
||||
*/
|
||||
isDraggable: function() {
|
||||
return this.features && this.features.isDraggable !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method overrides the GridItem.render function.
|
||||
*/
|
||||
render: function() {
|
||||
GaiaGrid.GridItem.prototype.render.call(this);
|
||||
this.element.classList.add('bookmark');
|
||||
if (this.isEditable()) {
|
||||
this.element.classList.add('editable');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Launches the bookmark in a browser window.
|
||||
*/
|
||||
launch: function() {
|
||||
var features = {
|
||||
name: this.name,
|
||||
icon: this.icon,
|
||||
remote: true
|
||||
};
|
||||
|
||||
var url = this.detail.url;
|
||||
if (this.features.search) {
|
||||
features.searchName = this.name;
|
||||
features.searchUrl = url;
|
||||
}
|
||||
|
||||
window.open(url, '_samescope', Object.keys(features)
|
||||
.map(function eachFeature(key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(features[key]);
|
||||
}).join(','));
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a web activity to edit the bookmark.
|
||||
*/
|
||||
edit: function() {
|
||||
new MozActivity({
|
||||
name: 'save-bookmark',
|
||||
data: {
|
||||
type: 'url',
|
||||
url: this.detail.id
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.GaiaGrid.Bookmark = Bookmark;
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
'use strict';
|
||||
/* global GaiaGrid */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
/**
|
||||
* Represents a single divider on the homepage.
|
||||
*/
|
||||
function Divider() {
|
||||
this.detail = {
|
||||
type: 'divider',
|
||||
index: 0
|
||||
};
|
||||
}
|
||||
|
||||
Divider.prototype = {
|
||||
|
||||
__proto__: GaiaGrid.GridItem.prototype,
|
||||
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
||||
headerHeight: 0,
|
||||
|
||||
/**
|
||||
* Height in pixels of each divider.
|
||||
*/
|
||||
get pixelHeight() {
|
||||
return (this.grid.layout.cols > 3) ? 50 : 60;
|
||||
},
|
||||
|
||||
/**
|
||||
* Width in grid units for each divider.
|
||||
*/
|
||||
gridWidth: 4,
|
||||
|
||||
scale: 1,
|
||||
|
||||
/**
|
||||
* Renders the icon to the grid component.
|
||||
*/
|
||||
render: function() {
|
||||
// Generate the content if we need to
|
||||
if (!this.element) {
|
||||
// Divider is a <section> and the rest of items are <div> containers
|
||||
// in order to hide the last divider via :last-of-type pseudo-class
|
||||
var divider = this.element = document.createElement('section');
|
||||
divider.className = 'divider';
|
||||
|
||||
var span = document.createElement('span');
|
||||
span.className = 'spacer';
|
||||
divider.appendChild(span);
|
||||
|
||||
this.grid.element.appendChild(divider);
|
||||
}
|
||||
|
||||
this.element.style.transform = 'translate(0 ,' + this.y + 'px)';
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
if (this.element) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
},
|
||||
|
||||
isDraggable: function() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
exports.GaiaGrid.Divider = Divider;
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,666 @@
|
|||
'use strict';
|
||||
/* global GridIconRenderer */
|
||||
/* global Promise */
|
||||
/* global UrlHelper */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
// event names
|
||||
const ICON_BLOB_DECORATED_EVENT = 'iconblobdecorated';
|
||||
const ICON_BLOB_ERROR_EVENT = 'gaiagrid-iconbloberror';
|
||||
const FETCH_XHR_TIMEOUT = 10000;
|
||||
|
||||
/**
|
||||
* XHR wrapper for fetching blobs with timeout logic.
|
||||
*
|
||||
* @param {String} uri to fetch.
|
||||
* @return {Promise[Blob]}
|
||||
*/
|
||||
function fetchBlob(uri) {
|
||||
return new Promise(function(accept, reject) {
|
||||
var xhr = new XMLHttpRequest({
|
||||
mozAnon: true,
|
||||
mozSystem: true
|
||||
});
|
||||
|
||||
xhr.open('GET', uri, true);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.timeout = FETCH_XHR_TIMEOUT;
|
||||
|
||||
// remember that send can throw for some non http protocols. The promise
|
||||
// wrapper here protects us.
|
||||
xhr.send();
|
||||
|
||||
xhr.onload = function() {
|
||||
var status = xhr.status;
|
||||
if (status !== 0 && status !== 200) {
|
||||
reject(
|
||||
new Error('Got HTTP status ' + status + ' trying to load ' + uri)
|
||||
);
|
||||
return;
|
||||
}
|
||||
accept(xhr.response);
|
||||
};
|
||||
|
||||
xhr.onerror = xhr.ontimeout = function() {
|
||||
reject(new Error('Error while HTTP GET: ', uri));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a generic grid item from which other items can inherit from.
|
||||
*/
|
||||
function GridItem() {
|
||||
this.detail = {};
|
||||
}
|
||||
|
||||
GridItem.prototype = {
|
||||
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
||||
/**
|
||||
* Height in pixels of each item.
|
||||
*/
|
||||
pixelHeight: 0,
|
||||
|
||||
/**
|
||||
* Width in grid units for each item.
|
||||
*/
|
||||
gridWidth: 1,
|
||||
|
||||
scale: 1,
|
||||
|
||||
/**
|
||||
* Whether or not this icon will persist to the database.
|
||||
*/
|
||||
persistToDB: true,
|
||||
|
||||
/**
|
||||
* Cached values of the last transform of the element, to avoid redundant
|
||||
* style changes.
|
||||
*/
|
||||
lastX: null,
|
||||
lastY: null,
|
||||
lastScale: null,
|
||||
|
||||
/**
|
||||
* Whether or not this item has a cached icon or not.
|
||||
*/
|
||||
get hasCachedIcon() {
|
||||
return this.detail && this.detail.decoratedIconBlob;
|
||||
},
|
||||
|
||||
/**
|
||||
* The icon renderer to use. Sub-classes may override this, and individual
|
||||
* objects may as well with a custom detail.renderer property.
|
||||
* Bookmarks use this for example to render both web results from E.me,
|
||||
* and favicons from the web with different styles.
|
||||
*/
|
||||
renderer: GridIconRenderer.TYPE.STANDARD,
|
||||
|
||||
/**
|
||||
* Every grid item has a desired icon (over the network) if we fail to fetch
|
||||
* or render it for some reason we display another. This string keeps track
|
||||
* of the states:
|
||||
*
|
||||
*
|
||||
* none: no action has been taken yet to display or fetch the icon.
|
||||
* pending: fetching the icon
|
||||
* success: icon was fetched and displayed
|
||||
* error: icon could not be fetched or displayed
|
||||
*
|
||||
*/
|
||||
iconState: 'none',
|
||||
|
||||
/**
|
||||
* Default icon url to use when no other icon can be downloaded
|
||||
*/
|
||||
get defaultIcon() {
|
||||
// XXX: ideally this would be relative to the component
|
||||
return '/shared/elements/gaia_grid/images/default_icon.png';
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a reference to the current grid.
|
||||
* We can currently only have one grid per page.
|
||||
*/
|
||||
get grid() {
|
||||
return document.getElementsByTagName('gaia-grid')[0]._grid;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if this item is removable.
|
||||
*/
|
||||
isRemovable: function() {
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if this item is editable.
|
||||
*/
|
||||
isEditable: function() {
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if this item is draggable.
|
||||
*/
|
||||
isDraggable: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the icon is hosted at an origin.
|
||||
*/
|
||||
isIconFromOrigin: function() {
|
||||
return this.icon.startsWith('http') || this.icon.startsWith('app:');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the item position
|
||||
*/
|
||||
setPosition: function(position) {
|
||||
this.detail.index = position;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the item coordinates
|
||||
*/
|
||||
setCoordinates: function(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatches a gaiagrid-attention signal with the rect of the item.
|
||||
*/
|
||||
requestAttention: function() {
|
||||
var rect = {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.gridWidth * this.grid.layout.gridItemWidth,
|
||||
height: this.pixelHeight
|
||||
};
|
||||
|
||||
this.grid.element.dispatchEvent(
|
||||
new CustomEvent('gaiagrid-attention', { detail: rect }));
|
||||
},
|
||||
|
||||
closestIconSizeFromList: function(choices) {
|
||||
if (!choices) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var sizes = Object.keys(choices);
|
||||
if (!sizes.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var maxSize = this.grid.layout.gridMaxIconSize; // The goal size
|
||||
|
||||
var filter = (a) => a >= maxSize ? a : 0;
|
||||
|
||||
// Ideally, get an icon that is equal to our grid size, or else
|
||||
// the smallest icon which is larger than our grid size.
|
||||
var bestSize = Math.min(...sizes.map(filter));
|
||||
|
||||
// If no icons are larger than (or equal to ) our grid size, get
|
||||
// the largest one.
|
||||
bestSize = bestSize || Math.max(...sizes);
|
||||
return bestSize || 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a list of icons that match a size, return the closest icon to
|
||||
* reduce possible pixelation by picking a wrong size.
|
||||
* @param {Object} choices An object mapping icon size to icon URL.
|
||||
*/
|
||||
closestIconFromList: function(choices) {
|
||||
var icon;
|
||||
|
||||
// Check for W3C web manifest format for icons
|
||||
if (Array.isArray(choices)) {
|
||||
var manifest = {
|
||||
'icons': choices
|
||||
};
|
||||
var maxSize = this.grid.layout.gridMaxIconSize; // The goal size
|
||||
icon = window.IconsHelper.getBestIconFromWebManifest(manifest, maxSize);
|
||||
return icon ? (new URL(icon, this.app.manifestURL)).href : null;
|
||||
}
|
||||
|
||||
var accurateSize = this.closestIconSizeFromList(choices);
|
||||
|
||||
if (accurateSize === 0) {
|
||||
return this.defaultIcon;
|
||||
}
|
||||
|
||||
icon = choices[accurateSize];
|
||||
|
||||
// Handle relative URLs
|
||||
if (!UrlHelper.hasScheme(icon)) {
|
||||
icon = this.app.origin + icon;
|
||||
}
|
||||
|
||||
return icon;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the given image src as the icon (with processing).
|
||||
*
|
||||
* @param {String} url The image url to display.
|
||||
*/
|
||||
renderIconFromSrc: function(url) {
|
||||
var background = new Image();
|
||||
background.src = url;
|
||||
background.onload = this._decorateIcon.bind(this, background);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Renders the given blob as the icon (with processing).
|
||||
*
|
||||
* @param {Blob} blob to render as icon.
|
||||
*/
|
||||
renderIconFromBlob: function(blob) {
|
||||
var background = new Image();
|
||||
background.src = URL.createObjectURL(blob);
|
||||
background.onload = () => {
|
||||
this._decorateIcon(background);
|
||||
URL.revokeObjectURL(background.src);
|
||||
};
|
||||
background.onerror = () => {
|
||||
URL.revokeObjectURL(background.src);
|
||||
|
||||
// If we already have a cached icon, do not overwrite it on error.
|
||||
if (this.hasCachedIcon) {
|
||||
this._stampElementWithIcon('blobcache');
|
||||
this._displayDecoratedIcon(this.detail.decoratedIconBlob, true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderIconFromSrc(this.defaultIcon);
|
||||
this._stampElementWithIcon(this.defaultIcon);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Begins decoration process for a given icon.
|
||||
*
|
||||
* @param {HTMLImageElement} img An image element to display from.
|
||||
*/
|
||||
_decorateIcon: function(img) {
|
||||
var strategy = this.detail.renderer || this.renderer;
|
||||
this.rendererInstance = new GridIconRenderer(this);
|
||||
this.rendererInstance[strategy](img).
|
||||
then(this._displayDecoratedIcon.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Finishes decoration process and displays the augmented icon.
|
||||
*
|
||||
* @param {Blob} blob The image blob to display.
|
||||
*/
|
||||
_displayDecoratedIcon: function(blob, isCachedIcon) {
|
||||
if (!this.element) {
|
||||
// The icon could be removed while it is being decorated
|
||||
return;
|
||||
}
|
||||
|
||||
var style = this.element.style;
|
||||
|
||||
if (!style.backgroundSize) {
|
||||
style.height = this.grid.layout.gridItemHeight + 'px';
|
||||
// icon size + padding for shadows implemented in the icon renderer
|
||||
var backgroundHeight =
|
||||
((this.grid.layout.gridIconSize * (1 / this.grid.layout.percent)) +
|
||||
GridIconRenderer.prototype.unscaledCanvasPadding);
|
||||
|
||||
if (this.grid.layout.gridItemHeight > backgroundHeight) {
|
||||
style.backgroundSize = backgroundHeight + 'px';
|
||||
} else {
|
||||
style.backgroundSize = style.height;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCachedIcon) {
|
||||
var url = URL.createObjectURL(blob);
|
||||
style.backgroundImage = 'url(' + url + ')';
|
||||
this.element.dataset.backgroundImage = url;
|
||||
var img = new Image();
|
||||
img.onload = img.onerror = () => {
|
||||
this.grid.element.dispatchEvent(
|
||||
new CustomEvent('cached-icon-rendered')
|
||||
);
|
||||
};
|
||||
img.src = url;
|
||||
return;
|
||||
}
|
||||
|
||||
this._compareBlobs(blob, this.detail.decoratedIconBlob).
|
||||
then((equal) => {
|
||||
if (equal) {
|
||||
return;
|
||||
}
|
||||
|
||||
style.backgroundImage = 'url(' + URL.createObjectURL(blob) + ')';
|
||||
this.detail.decoratedIconBlob = blob;
|
||||
this.grid.element.dispatchEvent(
|
||||
new CustomEvent(ICON_BLOB_DECORATED_EVENT, {
|
||||
detail: this
|
||||
})
|
||||
);
|
||||
var bgImg = this.element.dataset.backgroundImage;
|
||||
bgImg && URL.revokeObjectURL(bgImg);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* It compares two blobs.
|
||||
*/
|
||||
_compareBlobs: function(blob1, blob2) {
|
||||
return new Promise(function(resolve) {
|
||||
if (!blob1 || !blob2 || blob1.type !== blob2.type ||
|
||||
blob1.size !== blob2.size) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// We skip the first bytes that typically are headers
|
||||
var startBytes = 127;
|
||||
var bytesHash = 16;
|
||||
var reader = new FileReader();
|
||||
|
||||
reader.onloadend = function() {
|
||||
var result1 = reader.result;
|
||||
|
||||
reader = new FileReader();
|
||||
|
||||
reader.onloadend = reader.onerror = function() {
|
||||
resolve(result1 === reader.result);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob2.slice(startBytes, startBytes + bytesHash));
|
||||
};
|
||||
|
||||
reader.onerror = function() {
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob1.slice(startBytes, startBytes + bytesHash));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch an icon blob by a URI. This can be used as an extension hook to
|
||||
* implement caching logic into the fetching process.
|
||||
*
|
||||
* @return {Promise[Null|Blob]}
|
||||
*/
|
||||
fetchIconBlob: function() {
|
||||
return fetchBlob(this.icon);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to stamp the element with an icon uri. This stamp is for
|
||||
* debugging/testing only and has no other effect.
|
||||
*/
|
||||
_stampElementWithIcon: function(uri) {
|
||||
// ensure we don't stamp URI
|
||||
if (!this.element || uri.startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// do not allow long strings on the stamp
|
||||
if (uri.length > 255) {
|
||||
uri = uri.slice(0, 255);
|
||||
}
|
||||
|
||||
// test prefix used to indicate this has no functional value.
|
||||
this.element.dataset.testIcon = uri;
|
||||
},
|
||||
|
||||
/**
|
||||
Fetch and render the desired icon (`.icon`) if possible otherwise fallback
|
||||
to the default icon.
|
||||
|
||||
XXX: This method is not concurrency safe one image may override another
|
||||
without ordering.
|
||||
*/
|
||||
renderIcon: function(renderCachedIcon) {
|
||||
var icon = this.icon;
|
||||
this.iconState = 'pending';
|
||||
|
||||
if (!icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderCachedIcon && this.hasCachedIcon) {
|
||||
// Display cached icons before trying to get icons again.
|
||||
this._displayDecoratedIcon(this.detail.decoratedIconBlob, true);
|
||||
var resolveIcon = () => {
|
||||
this.grid.element.removeEventListener('cached-icons-rendered',
|
||||
resolveIcon);
|
||||
this.doRenderIcon(icon);
|
||||
};
|
||||
this.grid.element.addEventListener('cached-icons-rendered',
|
||||
resolveIcon);
|
||||
} else {
|
||||
this.doRenderIcon(icon);
|
||||
}
|
||||
},
|
||||
|
||||
doRenderIcon: function(icon) {
|
||||
// fast path if the icon is not from an origin display it outright
|
||||
if (!this.isIconFromOrigin()) {
|
||||
// XXX: Should we convert data uri(s) to blobs?
|
||||
this._stampElementWithIcon(icon);
|
||||
this.renderIconFromSrc(icon);
|
||||
// state set after in case of some exception or whatever
|
||||
this.iconState = 'success';
|
||||
|
||||
// noop
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// root element of the web component
|
||||
var eventTarget = this.grid.element;
|
||||
|
||||
// handle any cases where the blob is null or the icon cannot be
|
||||
// fetched.
|
||||
var blobNotFound = () => {
|
||||
this.iconState = 'error';
|
||||
|
||||
if (!this.hasCachedIcon) {
|
||||
this.renderIconFromSrc(this.defaultIcon);
|
||||
this._stampElementWithIcon(this.defaultIcon);
|
||||
} else {
|
||||
this._stampElementWithIcon('blobcache');
|
||||
}
|
||||
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent(ICON_BLOB_ERROR_EVENT, {
|
||||
detail: this
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return this.fetchIconBlob().
|
||||
then((blob) => {
|
||||
// if the fetch was a success but no blob could be found short
|
||||
// short circuit... note that being offline is _not_ an error.
|
||||
if (!blob) {
|
||||
return blobNotFound();
|
||||
}
|
||||
|
||||
this.renderIconFromBlob(blob);
|
||||
this.iconState = 'success';
|
||||
this._stampElementWithIcon(icon);
|
||||
}).
|
||||
catch((err) => {
|
||||
blobNotFound();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Safely remove this item from the grid and DOM.
|
||||
*/
|
||||
removeFromGrid: function() {
|
||||
var idx = this.grid.items.indexOf(this);
|
||||
|
||||
// This should never happen but is remotely possible item is not in the
|
||||
// grid.
|
||||
if (idx === -1) {
|
||||
console.error('Attempting to remove self before item has been added!');
|
||||
return;
|
||||
}
|
||||
|
||||
// update the state of the grid and DOM so this item is no longer
|
||||
// referenced.
|
||||
this.grid.items.splice(idx, 1);
|
||||
delete this.grid.icons[this.identifier];
|
||||
|
||||
if (this.element) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
|
||||
// ensure we don't end up with empty cruft..
|
||||
this.grid.render({ from: idx - 1 });
|
||||
},
|
||||
|
||||
/**
|
||||
Removes item from the dom and dispatches a removeitem event.
|
||||
*/
|
||||
remove: function() {
|
||||
this.grid.element.dispatchEvent(new CustomEvent('removeitem', {
|
||||
detail: this
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the icon to the container.
|
||||
*/
|
||||
render: function() {
|
||||
var scale = this.grid.layout.percent * this.scale;
|
||||
|
||||
// Generate an element if we need to
|
||||
if (!this.element) {
|
||||
var tile = document.createElement('div');
|
||||
tile.className = 'icon';
|
||||
tile.dataset.identifier = this.identifier;
|
||||
tile.dataset.isDraggable = this.isDraggable();
|
||||
tile.setAttribute('role', 'link');
|
||||
tile.setAttribute('tabindex', 0);
|
||||
tile.style.width = (this.grid.layout.constraintSize / 3) + 'px';
|
||||
|
||||
// This <p> has been added in order to place the title with respect
|
||||
// to this container via CSS without touching JS.
|
||||
var nameContainerEl = document.createElement('p');
|
||||
nameContainerEl.style.marginTop = ((this.grid.layout.gridIconSize *
|
||||
(1 / this.grid.layout.percent)) +
|
||||
(GridIconRenderer.prototype.unscaledCanvasPadding / 2)) + 'px';
|
||||
tile.appendChild(nameContainerEl);
|
||||
|
||||
var nameEl = document.createElement('span');
|
||||
nameEl.className = 'title';
|
||||
nameEl.setAttribute('dir', 'auto');
|
||||
|
||||
nameContainerEl.appendChild(nameEl);
|
||||
|
||||
// Add delete link if this icon is removable
|
||||
if (this.isRemovable()) {
|
||||
var removeEl = document.createElement('span');
|
||||
removeEl.className = 'remove';
|
||||
tile.appendChild(removeEl);
|
||||
}
|
||||
|
||||
this.element = tile;
|
||||
this.updateTitle();
|
||||
this.renderIcon(true);
|
||||
this.grid.element.appendChild(tile);
|
||||
}
|
||||
|
||||
this.transform(this.x, this.y, scale);
|
||||
},
|
||||
|
||||
/**
|
||||
* Positions and scales an icon.
|
||||
*/
|
||||
transform: function(x, y, scale, element) {
|
||||
scale = scale || 1;
|
||||
|
||||
if (!element) {
|
||||
if (x === this.lastX && y === this.lastY && scale === this.lastScale) {
|
||||
return;
|
||||
}
|
||||
element = this.element;
|
||||
this.lastX = x;
|
||||
this.lastY = y;
|
||||
this.lastScale = scale;
|
||||
}
|
||||
|
||||
element.style.transform =
|
||||
'translate(' + x + 'px,' + y + 'px) scale(' + scale + ')';
|
||||
},
|
||||
|
||||
/**
|
||||
* Set whether the item is being dragged.
|
||||
*/
|
||||
setActive: function(active) {
|
||||
if (active) {
|
||||
this.element.classList.add('active');
|
||||
this.active = true;
|
||||
} else {
|
||||
this.element.classList.remove('active');
|
||||
this.active = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Updates the title of the icon on the grid.
|
||||
*/
|
||||
updateTitle: function() {
|
||||
// it is remotely possible that we have not .rendered yet
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
var nameEl = this.element.querySelector('.title');
|
||||
|
||||
if (this.asyncName) {
|
||||
this.asyncName().then(function(name) {
|
||||
nameEl.textContent = name;
|
||||
});
|
||||
} else {
|
||||
nameEl.textContent = this.name;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates an icon on the page from a datastore record.
|
||||
* Used for bookmarks.
|
||||
* @param {Object} record The datastore record.
|
||||
*/
|
||||
updateFromDatastore: function(record) {
|
||||
var iconChanged = record.icon !== this.icon;
|
||||
var nameChanged = record.name !== this.name;
|
||||
|
||||
var type = this.detail.type;
|
||||
var lastIcon = this.icon;
|
||||
record.type = type;
|
||||
this.detail = record;
|
||||
if (nameChanged) {
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
if (iconChanged && record.icon) {
|
||||
this.renderIcon();
|
||||
} else if (!record.icon) {
|
||||
this.detail.icon = lastIcon;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.GaiaGrid.GridItem = GridItem;
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,430 @@
|
|||
'use strict';
|
||||
/* global GaiaGrid */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
/**
|
||||
* The relative size of icons when in a collapsed group.
|
||||
*/
|
||||
const COLLAPSE_RATIO = 0.33;
|
||||
|
||||
/**
|
||||
* Maximum number of icons that are visible in a collapsed group.
|
||||
*/
|
||||
const COLLAPSED_GROUP_SIZE = 6;
|
||||
|
||||
/**
|
||||
* Space to be reserved at the sides of collapsed group items, in pixels.
|
||||
*/
|
||||
const COLLAPSED_GROUP_MARGIN_LEFT = 23;
|
||||
const COLLAPSED_GROUP_MARGIN_RIGHT = 53;
|
||||
|
||||
/**
|
||||
* Time, in ms, before removing the 'toggling' class from the expand/collapse
|
||||
* toggle element.
|
||||
*/
|
||||
const TOGGLE_TIMEOUT = 250;
|
||||
|
||||
/**
|
||||
* A replacement for the default Divider class that implements group
|
||||
* collapsing and provides convenience functions for group drag'n'drop.
|
||||
*/
|
||||
function Group(detail) {
|
||||
this.detail = detail || {};
|
||||
this.detail.type = 'divider';
|
||||
this.detail.index = 0;
|
||||
this.pendingCollapse = this.detail.collapsed = !!this.detail.collapsed;
|
||||
}
|
||||
|
||||
Group.prototype = {
|
||||
|
||||
__proto__: GaiaGrid.GridItem.prototype,
|
||||
|
||||
gridWidth: 4,
|
||||
|
||||
/**
|
||||
* Height in pixels of the header part of the group.
|
||||
*/
|
||||
get headerHeight() {
|
||||
return this.detail.collapsed ? 20 : 30;
|
||||
},
|
||||
|
||||
/**
|
||||
* The collapsed state. Used to defer the actual state change of
|
||||
* collapse/expand so that anything re-rendering the grid between setting
|
||||
* the state and the re-render caused by the state change doesn't cause
|
||||
* the group to be positioned incorrectly.
|
||||
*/
|
||||
pendingCollapse: false,
|
||||
pendingCollapseTimeout: null,
|
||||
|
||||
/**
|
||||
* Height in pixels of the background of the group.
|
||||
*/
|
||||
backgroundHeight: 0,
|
||||
lastBackgroundHeight: null,
|
||||
|
||||
/**
|
||||
* Height in pixels of the separator at the bottom of the group.
|
||||
*/
|
||||
separatorHeight: 0,
|
||||
|
||||
/**
|
||||
* The scale used for collapsed icons, taking into account column size.
|
||||
*/
|
||||
collapseRatio: COLLAPSE_RATIO,
|
||||
|
||||
/**
|
||||
* Height in pixels of the group. When collapsed, this includes the
|
||||
* icons associated with the group.
|
||||
*/
|
||||
get pixelHeight() {
|
||||
return this.detail.collapsed ?
|
||||
this.backgroundHeight :
|
||||
this.separatorHeight;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the number of items in the group. Relies on the item index
|
||||
* being correct.
|
||||
*/
|
||||
get size() {
|
||||
var size = 0;
|
||||
for (var i = this.detail.index - 1, items = this.grid.items;
|
||||
i >= 0 && items[i].detail.type !== 'divider'; i--) {
|
||||
size++;
|
||||
}
|
||||
return size;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates the element and its children for this group item.
|
||||
*/
|
||||
_createElement: function() {
|
||||
var group = this.element = document.createElement('section');
|
||||
group.className = 'divider group newly-created';
|
||||
|
||||
// Create the background (only seen in edit mode)
|
||||
var span = document.createElement('span');
|
||||
span.className = 'background';
|
||||
group.appendChild(span);
|
||||
this.backgroundSpanElement = span;
|
||||
|
||||
// Create an element for a drop-shadow (only seen when active)
|
||||
span = document.createElement('span');
|
||||
span.className = 'shadow';
|
||||
group.appendChild(span);
|
||||
this.shadowSpanElement = span;
|
||||
|
||||
// Create the header (container for the move gripper and
|
||||
// expand/collapse toggle)
|
||||
span = document.createElement('span');
|
||||
span.className = 'header';
|
||||
group.appendChild(span);
|
||||
this.headerSpanElement = span;
|
||||
|
||||
// Create the move gripper
|
||||
span = document.createElement('span');
|
||||
span.className = 'gripper';
|
||||
this.headerSpanElement.appendChild(span);
|
||||
|
||||
// Create the expand/collapse toggle
|
||||
var button = document.createElement('button');
|
||||
button.className = 'toggle';
|
||||
button.dataset.l10nId = 'gaia-grid-toggle-expanded';
|
||||
this.headerSpanElement.appendChild(button);
|
||||
this.toggleElement = button;
|
||||
|
||||
// Create the group separator (only seen in non-edit mode)
|
||||
span = document.createElement('span');
|
||||
span.className = 'separator';
|
||||
group.appendChild(span);
|
||||
this.dividerSpanElement = span;
|
||||
|
||||
// Create a child span for the separator to act as the indicator for
|
||||
// dropping icons/groups during editing.
|
||||
this.dividerSpanElement.appendChild(document.createElement('span'));
|
||||
|
||||
this.grid.element.appendChild(group);
|
||||
this.separatorHeight = this.dividerSpanElement.clientHeight;
|
||||
this.lastBackgroundHeight = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the apps within the group. If the group is not collapsed, this
|
||||
* just validates the style class of the group.
|
||||
*/
|
||||
_renderChildren: function(nApps) {
|
||||
var isRTL = (document.documentElement.dir === 'rtl');
|
||||
if (!this.detail.collapsed) {
|
||||
this.element.classList.remove('collapsed');
|
||||
return;
|
||||
}
|
||||
|
||||
// If collapsed, we need to style and position the icons in the group
|
||||
// into one row.
|
||||
this.element.classList.add('collapsed');
|
||||
|
||||
var y = this.y;
|
||||
var index = this.detail.index;
|
||||
|
||||
var width = Math.round(
|
||||
(this.grid.layout.constraintSize -
|
||||
COLLAPSED_GROUP_MARGIN_LEFT - COLLAPSED_GROUP_MARGIN_RIGHT) /
|
||||
COLLAPSED_GROUP_SIZE);
|
||||
var x = isRTL ?
|
||||
(this.grid.layout.constraintSize - COLLAPSED_GROUP_MARGIN_RIGHT) :
|
||||
COLLAPSED_GROUP_MARGIN_LEFT;
|
||||
y += this.headerHeight;
|
||||
|
||||
var maxGridItemWidth =
|
||||
this.grid.layout.constraintSize / this.grid.layout.minIconsPerRow;
|
||||
this.collapseRatio =
|
||||
(maxGridItemWidth / this.grid.layout.gridItemWidth) * COLLAPSE_RATIO;
|
||||
|
||||
for (var i = index - nApps; i < index; i++) {
|
||||
var item = this.grid.items[i];
|
||||
if (this.detail.collapsed) {
|
||||
item.scale = this.collapseRatio;
|
||||
|
||||
var itemVisible = (i - (index - nApps)) < COLLAPSED_GROUP_SIZE;
|
||||
if (!itemVisible) {
|
||||
item.setCoordinates(isRTL ? x + width : x - width, y);
|
||||
} else {
|
||||
item.setCoordinates(x, y);
|
||||
x += isRTL ? -width: width;
|
||||
}
|
||||
|
||||
item.render();
|
||||
item.element.classList.add('collapsed');
|
||||
if (!itemVisible) {
|
||||
item.element.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the y-position of the group. When the group is expanded, its y
|
||||
* position is actually the y-position of the separator underneath the
|
||||
* group. This gets the visible y-position of the group.
|
||||
*/
|
||||
getRealYPosition: function(nApps) {
|
||||
if (this.detail.collapsed) {
|
||||
return this.y;
|
||||
}
|
||||
return this.grid.items[this.detail.index - nApps].y -
|
||||
this.headerHeight;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the icon to the grid component.
|
||||
*/
|
||||
render: function() {
|
||||
// Generate the content if we need to
|
||||
var createdElements = false;
|
||||
if (!this.element) {
|
||||
createdElements = true;
|
||||
this._createElement();
|
||||
}
|
||||
|
||||
// Render children
|
||||
var nApps = this.size;
|
||||
this._renderChildren(nApps);
|
||||
|
||||
// Calculate group position.
|
||||
// If we're not collapsed, the group's position will be underneath its
|
||||
// icons, but we want it to display above.
|
||||
var y = this.getRealYPosition(nApps);
|
||||
|
||||
if (y !== this.lastY) {
|
||||
// Place the header span
|
||||
this.headerSpanElement.style.transform =
|
||||
'translate(0px, ' + y + 'px)';
|
||||
}
|
||||
|
||||
if (this.toggleElement) {
|
||||
var toggleLabel = this.detail.collapsed ? 'collapsed' : 'expanded';
|
||||
this.toggleElement.dataset.l10nId = 'gaia-grid-toggle-' + toggleLabel;
|
||||
}
|
||||
|
||||
// Calculate the height of the background span
|
||||
if (this.detail.collapsed) {
|
||||
this.backgroundHeight =
|
||||
Math.round(this.collapseRatio * this.grid.layout.gridIconSize * 1.5);
|
||||
} else {
|
||||
var height = Math.ceil(nApps / this.grid.layout.cols);
|
||||
this.backgroundHeight = (height || 1) * this.grid.layout.gridItemHeight;
|
||||
}
|
||||
this.backgroundHeight += this.headerHeight;
|
||||
|
||||
if (y != this.lastY ||
|
||||
this.backgroundHeight !== this.lastBackgroundHeight) {
|
||||
// Place and size the background span element
|
||||
this.backgroundSpanElement.style.transform =
|
||||
'translate(0px, ' + y + 'px) scale(1, ' + this.backgroundHeight + ')';
|
||||
|
||||
// Place and size the shadow span element
|
||||
this.shadowSpanElement.style.transform =
|
||||
'translateY(' + y + 'px)';
|
||||
this.shadowSpanElement.style.height = this.backgroundHeight + 'px';
|
||||
|
||||
// Place the divider after this point
|
||||
this.dividerSpanElement.style.transform =
|
||||
'translate(0px, ' + (y + this.backgroundHeight) + 'px)';
|
||||
}
|
||||
|
||||
// Update the cached size values
|
||||
this.lastBackgroundHeight = this.backgroundHeight;
|
||||
this.lastY = y;
|
||||
|
||||
// Now include the separator in the background height
|
||||
this.backgroundHeight += this.separatorHeight;
|
||||
|
||||
// Fade in newly-created groups
|
||||
if (createdElements) {
|
||||
// Force a reflow to make sure the transform transition doesn't play
|
||||
this.element.clientTop;
|
||||
this.element.classList.remove('newly-created');
|
||||
}
|
||||
},
|
||||
|
||||
forEachItem: function(callback) {
|
||||
var nApps = this.size;
|
||||
var index = this.detail.index;
|
||||
for (var i = index - nApps; i < index; i++) {
|
||||
callback(this.grid.items[i]);
|
||||
}
|
||||
},
|
||||
|
||||
setActive: function(active) {
|
||||
GaiaGrid.GridItem.prototype.setActive.call(this, active);
|
||||
|
||||
// Make sure we're collapsed
|
||||
this.collapse();
|
||||
|
||||
// Mark our child items as active/inactive with us so they pick up the
|
||||
// right style when dragged.
|
||||
var callback = active ?
|
||||
function(item) { item.element.classList.add('active'); } :
|
||||
function(item) { item.element.classList.remove('active'); };
|
||||
this.forEachItem(callback);
|
||||
},
|
||||
|
||||
/*
|
||||
* As the start of a collapse/expand animation can involve a lot of work,
|
||||
* the response isn't always instant. To help alleviate this, we show a
|
||||
* quick response on the toggle element first before initiating the rest
|
||||
* of the work.
|
||||
*/
|
||||
_rerenderOnToggle: function() {
|
||||
if (this.pendingCollapseTimeout) {
|
||||
clearTimeout(this.pendingCollapseTimeout);
|
||||
this.pendingCollapseTimeout = null;
|
||||
}
|
||||
|
||||
if (this.toggleElement) {
|
||||
this.toggleElement.classList.add('toggling');
|
||||
setTimeout(() => { this.toggleElement.classList.remove('toggling'); },
|
||||
TOGGLE_TIMEOUT);
|
||||
}
|
||||
|
||||
// Ideally, this setTimeout would be a requestAnimationFrame, but the
|
||||
// above class-adding seems to end up coalesced with the code inside
|
||||
// requestAnimationFrame, rather than it happening on the next frame.
|
||||
// The same thing happens with any setTimeout lower than about 20ms too.
|
||||
this.pendingCollapseTimeout = setTimeout(() => {
|
||||
this.detail.collapsed = this.pendingCollapse;
|
||||
this.pendingCollapseTimeout = null;
|
||||
|
||||
if (!this.detail.collapsed) {
|
||||
// Remove collapsed styling from all icons
|
||||
this.forEachItem(function(item) {
|
||||
item.scale = 1;
|
||||
if (item.element) {
|
||||
item.element.classList.remove('collapsed');
|
||||
item.element.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If the item is active, re-render it so that its pixel-height is
|
||||
// correct when the rest of the grid is rendered.
|
||||
if (this.active) {
|
||||
this.render();
|
||||
}
|
||||
|
||||
this.grid.render();
|
||||
|
||||
var dragging = this.grid.dragdrop && this.grid.dragdrop.inDragAction;
|
||||
if (dragging) {
|
||||
// If we're dragging, make sure to reposition the icon in the correct
|
||||
// place, as the render call won't redraw us
|
||||
this.grid.dragdrop.updateIconPosition();
|
||||
} else {
|
||||
// If we're not dragging, save the collapsed state
|
||||
window.dispatchEvent(new CustomEvent('gaiagrid-saveitems'));
|
||||
|
||||
// Request attention so that we're as visible as we can be after
|
||||
// expanding/collapsing
|
||||
this.requestAttention();
|
||||
}
|
||||
}, 20);
|
||||
},
|
||||
|
||||
collapse: function() {
|
||||
if (this.detail.collapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingCollapse = true;
|
||||
this._rerenderOnToggle();
|
||||
},
|
||||
|
||||
expand: function() {
|
||||
if (!this.detail.collapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingCollapse = false;
|
||||
this._rerenderOnToggle();
|
||||
},
|
||||
|
||||
launch: function(target) {
|
||||
if (target !== this.toggleElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.detail.collapsed) {
|
||||
this.expand();
|
||||
} else {
|
||||
this.collapse();
|
||||
}
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
if (this.element) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
},
|
||||
|
||||
isDraggable: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
requestAttention: function() {
|
||||
var rect = {
|
||||
x: 0,
|
||||
y: this.getRealYPosition(this.size),
|
||||
width: this.gridWidth * this.grid.layout.gridItemWidth,
|
||||
height: this.backgroundHeight
|
||||
};
|
||||
|
||||
this.grid.element.dispatchEvent(
|
||||
new CustomEvent('gaiagrid-attention', { detail: rect }));
|
||||
}
|
||||
};
|
||||
|
||||
exports.GaiaGrid.Divider = Group; // Override the non-grouping divider
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
'use strict';
|
||||
/* global GaiaGrid */
|
||||
/* global GridIconRenderer */
|
||||
/* global MozActivity */
|
||||
/* jshint nonew: false */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const TYPE = 'marketplace-app';
|
||||
|
||||
/**
|
||||
* Represents a single bookmark icon on the homepage.
|
||||
*/
|
||||
function MarketPlaceApp(record) {
|
||||
this.detail = record;
|
||||
this.detail.type = TYPE;
|
||||
}
|
||||
|
||||
MarketPlaceApp.prototype = {
|
||||
|
||||
__proto__: GaiaGrid.Bookmark.prototype,
|
||||
|
||||
renderer: GridIconRenderer.TYPE.STANDARD,
|
||||
|
||||
/**
|
||||
* Returns the icon image path.
|
||||
*/
|
||||
get icon() {
|
||||
var icon = this._accurateIcon;
|
||||
|
||||
if (!icon) {
|
||||
icon = this._accurateIcon = this.closestIconFromList(
|
||||
this.detail.icons);
|
||||
}
|
||||
|
||||
return icon;
|
||||
},
|
||||
|
||||
launch: function() {
|
||||
new MozActivity({
|
||||
name: 'marketplace-app',
|
||||
data: {
|
||||
slug: this.detail.slug
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
exports.GaiaGrid.MarketPlaceApp = MarketPlaceApp;
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
'use strict';
|
||||
/* global ConfirmDialogHelper, GaiaGrid */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const IDENTIFIER_SEP = '-';
|
||||
|
||||
var MOZAPP_EVENTS = [
|
||||
'downloadsuccess',
|
||||
'downloaderror',
|
||||
'downloadavailable',
|
||||
'downloadapplied',
|
||||
'progress'
|
||||
];
|
||||
|
||||
var APP_LOADING = 'loading';
|
||||
var APP_ERROR = 'error';
|
||||
var APP_UNRECOVERABLE = 'unrecoverable';
|
||||
var APP_PAUSED = 'paused';
|
||||
var APP_READY = 'ready';
|
||||
|
||||
function localizeString(str) {
|
||||
var userLang = document.documentElement.lang;
|
||||
|
||||
// We want to make sure that we translate only if we're using
|
||||
// a runtime pseudolocale.
|
||||
// mozL10n.ctx.qps contains only runtime pseudolocales
|
||||
if (navigator.mozL10n &&
|
||||
navigator.mozL10n.ctx.qps.indexOf(userLang) !== -1) {
|
||||
return navigator.mozL10n.qps[userLang].translate(str);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents single app icon on the homepage.
|
||||
*/
|
||||
function Mozapp(app, entryPoint, details) {
|
||||
this.app = app;
|
||||
this.entryPoint = entryPoint;
|
||||
|
||||
this.detail = {
|
||||
type: 'app',
|
||||
manifestURL: app.manifestURL,
|
||||
entryPoint: entryPoint,
|
||||
index: 0,
|
||||
// XXX: Somewhat ugly hack around the constructor args
|
||||
decoratedIconBlob: details && details.decoratedIconBlob
|
||||
};
|
||||
|
||||
// Yes this is clobbering all other listeners to the app MozApp
|
||||
// should be in charge here otherwise the icon will be incorrect.
|
||||
MOZAPP_EVENTS.forEach(function(type) {
|
||||
this.app['on' + type] = this.handleEvent.bind(this);
|
||||
}, this);
|
||||
|
||||
// Determine and set the initial state of the app when it is inserted.
|
||||
this.setAppState(this._determineState(app));
|
||||
}
|
||||
|
||||
/**
|
||||
`app.manifest.role`s which should not be displayed on the grid.
|
||||
*/
|
||||
Mozapp.HIDDEN_ROLES = [
|
||||
'system', 'input', 'homescreen', 'search', 'addon', 'langpack'
|
||||
];
|
||||
|
||||
Mozapp.prototype = {
|
||||
|
||||
__proto__: GaiaGrid.GridItem.prototype,
|
||||
|
||||
|
||||
/**
|
||||
Determine if this application is supposed to be hidden.
|
||||
|
||||
@return Boolean
|
||||
*/
|
||||
_isHiddenRole: function() {
|
||||
var manifest = this.app.manifest;
|
||||
|
||||
// don't know what the role is so it is not hidden
|
||||
if (!manifest || !manifest.role) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if it is in the hidden role list return true
|
||||
return Mozapp.HIDDEN_ROLES.indexOf(manifest.role) !== -1;
|
||||
},
|
||||
|
||||
/**
|
||||
Figure out which state the app is in
|
||||
*/
|
||||
_determineState: function(app) {
|
||||
// is there is a pending download but we cannot download then this app is
|
||||
// in an unrecoverable state due to some fatal error.
|
||||
if (
|
||||
// Must be pending (meaning not launchable)
|
||||
app.installState === 'pending' &&
|
||||
// Without any option for downloading the app
|
||||
!app.downloadAvailable &&
|
||||
// And not be currently applying a download
|
||||
!app.readyToApplyDownload
|
||||
) {
|
||||
return APP_UNRECOVERABLE;
|
||||
}
|
||||
|
||||
// If the app is currently downloading life is good and we should display
|
||||
// loading.
|
||||
if (app.downloading) {
|
||||
return APP_LOADING;
|
||||
}
|
||||
|
||||
// Bug 1027347 - downloadError is always present even if there is no error
|
||||
// so we check both for the error and that the error has a
|
||||
// name.
|
||||
if (app.downloadError && app.downloadError.name) {
|
||||
|
||||
// If the app was paused while the homescreen was running the
|
||||
// downloaderror event will fire with the DOWNLOAD_CANCELED error.
|
||||
if (app.downloadError.name === 'DOWNLOAD_CANCELED') {
|
||||
return APP_PAUSED;
|
||||
}
|
||||
|
||||
return APP_ERROR;
|
||||
}
|
||||
|
||||
// Note that this logic is correct because app.downloading is caught above
|
||||
// separately.
|
||||
if (app.installState === 'pending') {
|
||||
return APP_PAUSED;
|
||||
}
|
||||
|
||||
return APP_READY;
|
||||
},
|
||||
|
||||
handleEvent: function(event) {
|
||||
switch (event.type) {
|
||||
case 'progress':
|
||||
case 'downloaderror':
|
||||
case 'downloadsuccess':
|
||||
this.setAppState(this._determineState(this.app));
|
||||
break;
|
||||
|
||||
case 'downloadapplied':
|
||||
// Ensure that a hidden app has not somehow been added to the grid...
|
||||
if (this._isHiddenRole()) {
|
||||
console.warn('Removing hidden app from the grid', this.name);
|
||||
return this.removeFromGrid();
|
||||
}
|
||||
|
||||
// we may have updated icons so recalculate the correct icon.
|
||||
delete this._accurateIcon;
|
||||
delete this._accurateIconSize;
|
||||
|
||||
// Need to set app state here to correctly handle the pause/resume
|
||||
// case.
|
||||
this.setAppState(this._determineState(this.app));
|
||||
// XXX: This introduces a potential race condition.
|
||||
// GridItem.renderIcon is not concurrency safe one image may override
|
||||
// another without ordering.
|
||||
this.renderIcon();
|
||||
|
||||
window.dispatchEvent(new CustomEvent('downloadapplied', {
|
||||
detail: {
|
||||
id: this.app.manifestURL
|
||||
}
|
||||
}));
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Overrides GridItem.fetchIconBlob
|
||||
fetchIconBlob: function() {
|
||||
if (!this._accurateIconSize) {
|
||||
this._accurateIconSize =
|
||||
this.closestIconSizeFromList(this.descriptor.icons);
|
||||
}
|
||||
|
||||
var result = this._accurateIconSize ?
|
||||
navigator.mozApps.mgmt.getIcon(this.app, this._accurateIconSize,
|
||||
this.entryPoint || '') :
|
||||
GaiaGrid.GridItem.prototype.fetchIconBlob.call(this);
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the height in pixels of each icon.
|
||||
*/
|
||||
get pixelHeight() {
|
||||
return this.grid.layout.gridItemHeight;
|
||||
},
|
||||
|
||||
/**
|
||||
* Width in grid units for each icon.
|
||||
*/
|
||||
gridWidth: 1,
|
||||
|
||||
get name() {
|
||||
var userLang = document.documentElement.lang;
|
||||
|
||||
if (navigator.mozL10n && userLang in navigator.mozL10n.qps) {
|
||||
return navigator.mozL10n.qps[userLang].
|
||||
translate(this.descriptor.short_name || this.descriptor.name);
|
||||
}
|
||||
|
||||
var locales = this.descriptor.locales;
|
||||
var localized =
|
||||
locales && locales[userLang] &&
|
||||
(locales[userLang].short_name || locales[userLang].name);
|
||||
|
||||
return localized || this.descriptor.short_name || this.descriptor.name;
|
||||
},
|
||||
|
||||
asyncName: function() {
|
||||
var userLang = document.documentElement.lang;
|
||||
|
||||
var ep = this.entryPoint || undefined;
|
||||
|
||||
if (!this.app.getLocalizedValue) {
|
||||
return new Promise((resolve, reject) => { reject(); });
|
||||
}
|
||||
|
||||
return this.app.getLocalizedValue('short_name', userLang, ep).then(
|
||||
shortName => localizeString(shortName),
|
||||
this.app.getLocalizedValue.bind(this.app, 'name', userLang, ep)).then(
|
||||
name => localizeString(name),
|
||||
() => this.name
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the icon image path.
|
||||
*/
|
||||
get icon() {
|
||||
var icon = this._accurateIcon;
|
||||
|
||||
if (!icon) {
|
||||
icon = this._accurateIcon = this.closestIconFromList(
|
||||
this.descriptor.icons);
|
||||
}
|
||||
|
||||
return icon;
|
||||
},
|
||||
|
||||
get descriptor() {
|
||||
var manifest = this.app.manifest || this.app.updateManifest;
|
||||
|
||||
if (this.entryPoint && manifest.entry_points &&
|
||||
manifest.entry_points[this.entryPoint]) {
|
||||
return manifest.entry_points[this.entryPoint];
|
||||
}
|
||||
return manifest;
|
||||
},
|
||||
|
||||
identifierSeparator: IDENTIFIER_SEP,
|
||||
|
||||
get identifier() {
|
||||
var identifier = [this.app.manifestURL];
|
||||
|
||||
if (this.entryPoint) {
|
||||
identifier.push(this.entryPoint);
|
||||
}
|
||||
|
||||
return identifier.join(IDENTIFIER_SEP);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if this app is removable.
|
||||
*/
|
||||
isRemovable: function() {
|
||||
return this.app.removable;
|
||||
},
|
||||
|
||||
/**
|
||||
Show a dialog to handle unrecoverable errors.
|
||||
*/
|
||||
unrecoverableError: function() {
|
||||
navigator.mozApps.mgmt.uninstall(this.app);
|
||||
},
|
||||
|
||||
cancel: function() {
|
||||
var dialog = new ConfirmDialogHelper({
|
||||
type: 'pause',
|
||||
title: {id: 'gaia-grid-stop-download-title', args: { name: this.name }},
|
||||
body: 'gaia-grid-stop-download-body',
|
||||
cancel: {
|
||||
title: 'gaia-grid-cancel'
|
||||
},
|
||||
confirm: {
|
||||
title: 'gaia-grid-stop-download-action',
|
||||
type: 'danger',
|
||||
cb: () => this.app.cancelDownload()
|
||||
}
|
||||
});
|
||||
dialog.show(document.body);
|
||||
},
|
||||
|
||||
resume: function() {
|
||||
var dialog = new ConfirmDialogHelper({
|
||||
type: 'resume',
|
||||
title: 'gaia-grid-resume-download-title',
|
||||
body: {id: 'gaia-grid-resume-download-body', args: { name: this.name }},
|
||||
cancel: {
|
||||
title: 'gaia-grid-cancel'
|
||||
},
|
||||
confirm: {
|
||||
title: 'gaia-grid-resume-download-action',
|
||||
cb: () => {
|
||||
// enter the loading state optimistically
|
||||
this.setAppState(APP_LOADING);
|
||||
this.app.download();
|
||||
}
|
||||
}
|
||||
});
|
||||
dialog.show(document.body);
|
||||
},
|
||||
|
||||
setAppState: function(state) {
|
||||
this.appState = state;
|
||||
|
||||
if (this.element) {
|
||||
this.element.dataset.appState = state;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// If there is no element before we can render we need to update the
|
||||
// app state. Render may also be called to reposition elements.
|
||||
var needsStateSet = !this.element;
|
||||
|
||||
GaiaGrid.GridItem.prototype.render.apply(this, arguments);
|
||||
|
||||
if (needsStateSet) {
|
||||
// ensure the newly rendered item has an app state
|
||||
this.setAppState(this.appState);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolves click action.
|
||||
*/
|
||||
launch: function() {
|
||||
var app = this.app;
|
||||
|
||||
switch (this._determineState(app)) {
|
||||
case APP_UNRECOVERABLE:
|
||||
return this.unrecoverableError();
|
||||
case APP_ERROR:
|
||||
case APP_PAUSED:
|
||||
return this.resume();
|
||||
case APP_LOADING:
|
||||
return this.cancel();
|
||||
}
|
||||
|
||||
window.performance.mark('appLaunch@' + app.origin);
|
||||
|
||||
if (this.entryPoint) {
|
||||
return app.launch(this.entryPoint);
|
||||
}
|
||||
|
||||
// Default action is to launch the app.
|
||||
return app.launch();
|
||||
}
|
||||
};
|
||||
|
||||
exports.GaiaGrid.Mozapp = Mozapp;
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
'use strict';
|
||||
/* global GaiaGrid */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
/**
|
||||
* The placeholder represents an empty place on the grid.
|
||||
* This is generally used to detect distance from an empty spot on the grid.
|
||||
*/
|
||||
function Placeholder() {
|
||||
this.detail = {
|
||||
type: 'placeholder',
|
||||
index: 0
|
||||
};
|
||||
}
|
||||
|
||||
Placeholder.prototype = {
|
||||
|
||||
__proto__: GaiaGrid.GridItem.prototype,
|
||||
|
||||
/**
|
||||
* Returns the height in pixels of each icon.
|
||||
*/
|
||||
get pixelHeight() {
|
||||
return this.grid.layout.gridItemHeight;
|
||||
},
|
||||
|
||||
/**
|
||||
* When the placeholder is rendered in the last row, for group creation
|
||||
* purposes, there is special handling required for height.
|
||||
*/
|
||||
createsGroupOnDrop: false,
|
||||
|
||||
/**
|
||||
* Width in grid units for each icon.
|
||||
*/
|
||||
gridWidth: 1,
|
||||
|
||||
/**
|
||||
* Placeholders do not save. They are re-generated on render calls.
|
||||
*/
|
||||
persistToDB: false,
|
||||
|
||||
get name() {
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders a transparent placeholder.
|
||||
*/
|
||||
render: function() {
|
||||
// Generate an element if we need to
|
||||
if (!this.element) {
|
||||
var tile = document.createElement('div');
|
||||
tile.className = 'icon placeholder';
|
||||
tile.style.height = this.pixelHeight + 'px';
|
||||
this.element = tile;
|
||||
this.grid.element.appendChild(tile);
|
||||
}
|
||||
|
||||
this.transform(this.x, this.y, this.grid.layout.percent);
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
if (this.element) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.GaiaGrid.Placeholder = Placeholder;
|
||||
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,409 @@
|
|||
.icon[data-app-state="loading"]:after {
|
||||
content: "";
|
||||
background: no-repeat top center url('images/app_install_spinner.png');
|
||||
background-size: contain;
|
||||
height: 8.4rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
/**
|
||||
Keyframe must be defined outside of the web component.
|
||||
See bug 1018269
|
||||
*/
|
||||
|
||||
animation: rotate 2.25s linear infinite;
|
||||
}
|
||||
|
||||
.icon[data-app-state="loading"] {
|
||||
/**
|
||||
We must align this value with the value of [data-app-state]:after height the background size
|
||||
here is overridden at the element level elsewhere which causes the spinner to be aligned too
|
||||
low.
|
||||
*/
|
||||
background-size: 8.4rem !important;
|
||||
background-image: url('images/app_installing.png') !important;
|
||||
}
|
||||
|
||||
.icon[data-app-state="unrecoverable"] {
|
||||
background-image: url('images/app_install_unrecoverable.png') !important;
|
||||
}
|
||||
|
||||
.icon[data-app-state="error"] {
|
||||
background-image: url('images/app_install_failed.png') !important;
|
||||
}
|
||||
|
||||
.icon[data-app-state="paused"] {
|
||||
background-image: url('images/app_install_canceled.png') !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
transform-origin: 0 0;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
background: no-repeat top center;
|
||||
z-index: 3;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.icon.launching {
|
||||
opacity: .65;
|
||||
}
|
||||
|
||||
/* XXX: Override the .remove element */
|
||||
.edit-mode .collection.hovered .remove {
|
||||
background: url(images/add.png) no-repeat;
|
||||
background-size: 2rem;
|
||||
}
|
||||
|
||||
.icon p {
|
||||
pointer-events: none;
|
||||
height: 3.2rem;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.icon .title {
|
||||
display: inline-block;
|
||||
margin-top: 0.1rem;
|
||||
/* We need this rule in order not to hide the drop shadow */
|
||||
padding: 0 0.2rem 1rem;
|
||||
text-shadow: 0.1rem 0.1rem 0.3rem rgba(0, 0, 0, 0.3),
|
||||
0.2rem 0.4rem 1rem rgba(0, 0, 0, 0.4);
|
||||
font-size: 1.4rem;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: calc(100% - 0.4rem);
|
||||
box-sizing: border-box;
|
||||
vertical-align: top;
|
||||
transition: opacity var(--transition-duration);
|
||||
}
|
||||
|
||||
/* We allow more space for text in the search app by hooking into the text-rows attribute on gaia-grid. */
|
||||
[text-rows="3"] .icon .title {
|
||||
height: 5.8rem;
|
||||
}
|
||||
|
||||
[cols="4"] .icon .title {
|
||||
/* Font size is the same independent on grid configuration */
|
||||
font-size: calc(1.2rem * 1.333);
|
||||
margin-top: 0.5rem;
|
||||
line-height: 1.94rem;
|
||||
}
|
||||
|
||||
.edit-mode .icon.collapsed {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.icon.collapsed .title {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.icon.hidden {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
padding-top: 2.3rem;
|
||||
border-left: 0.6rem solid transparent;
|
||||
border-right: 0.6rem solid transparent;
|
||||
box-sizing: border-box;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
[cols="4"] .divider {
|
||||
padding-top: 1.6rem;
|
||||
}
|
||||
|
||||
.divider span.spacer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: block;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.5));
|
||||
margin: 0 25%;
|
||||
width: 50%;
|
||||
height: 0.2rem;
|
||||
transition: opacity var(--transition-duration);
|
||||
}
|
||||
|
||||
.dragging .icon.active:not(.hidden) {
|
||||
z-index: 6;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.dragging .divider:not(.group),
|
||||
.icon:not(.active),
|
||||
.dropped {
|
||||
transition: transform var(--transition-duration),
|
||||
opacity var(--transition-duration);
|
||||
}
|
||||
|
||||
.icon.active {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.edit-mode .icon .remove {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0.1rem;
|
||||
left: 0.5rem;
|
||||
width: 2.6rem;
|
||||
height: 2.6rem;
|
||||
background: url(images/remove.png) no-repeat;
|
||||
background-size: 2rem;
|
||||
/*
|
||||
Ensure that remove is on top of the icon and has some extra
|
||||
padding so it is easy to click.
|
||||
*/
|
||||
z-index: 200;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.icon .remove,
|
||||
.icon.collapsed .remove,
|
||||
.edit-mode .icon.active .remove {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.edit-mode .icon.editable .title {
|
||||
background-color: rgba(132, 132, 132, 0.7);
|
||||
border-radius: 2rem;
|
||||
margin-top: .4rem;
|
||||
padding: 0 1rem;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
Disable events for placeholders, so we don't trigger touch events in edit mode.
|
||||
We only do this in edit-mode, otherwise we might trigger entering edit mode
|
||||
instead of the context menu.
|
||||
*/
|
||||
.edit-mode .placeholder {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#confirm-message {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dragging .group.active {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.divider.group {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.group > span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
transition: background-color var(--transition-duration),
|
||||
opacity var(--transition-duration);
|
||||
}
|
||||
|
||||
.group:not(.active):not(.newly-created) > span {
|
||||
transition: background-color var(--transition-duration),
|
||||
opacity var(--transition-duration),
|
||||
transform var(--transition-duration);
|
||||
}
|
||||
|
||||
.group.active > span {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.group .header {
|
||||
height: 4rem;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.group .header span,
|
||||
.group .header button {
|
||||
position: absolute;
|
||||
height: 4rem;
|
||||
top: 0;
|
||||
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 2rem;
|
||||
|
||||
transition: transform 0.25s, opacity 0.25s;
|
||||
}
|
||||
|
||||
.group .header .gripper {
|
||||
left: calc(50% - 4.2rem);
|
||||
width: 8.4rem;
|
||||
background-image: url('images/move_gripper.png');
|
||||
opacity: 0;
|
||||
transform: translate(0, -100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.edit-mode .group:not(.collapsed) .header .gripper {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.group .header .toggle {
|
||||
width: 5.34rem;
|
||||
right: 0;
|
||||
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
background-color: transparent;
|
||||
background-image: url('images/collapse_arrow.png');
|
||||
transform-origin: 50% 1.6rem;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.group .header .toggle.toggling {
|
||||
opacity: 0.5;
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
|
||||
.group.collapsed .header .toggle {
|
||||
transform: translateY(2rem) rotateZ(180deg);
|
||||
}
|
||||
|
||||
.group.active .header .toggle {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.group .background {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
height: 0.1rem;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.group.odd:not(.newly-created) .background,
|
||||
.edit-mode .group:not(.newly-created) .background {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.group.active .background {
|
||||
background-color: rgba(51, 51, 51, 0.9);
|
||||
}
|
||||
|
||||
.group.hovering .background,
|
||||
.group.drop-target .background {
|
||||
background-color: rgba(27, 185, 225, 0.5);
|
||||
}
|
||||
|
||||
.group .shadow {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.edit-mode .group.active .shadow {
|
||||
background-color: transparent;
|
||||
box-shadow: 0 0.9rem 1.2rem rgba(0, 0, 0, 0.63);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.group .separator {
|
||||
margin: 0;
|
||||
height: 0.8rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.group .separator > span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.25s, opacity 0.25s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.group.odd .separator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.group.odd .separator > span {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.edit-mode .group .separator,
|
||||
.edit-mode .group .separator > span {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* For preventing entering edit mode when long-pressing on a placeholder */
|
||||
.edit-mode .group .background,
|
||||
.edit-mode .group .shadow,
|
||||
|
||||
.group.collapsed .header,
|
||||
.group.collapsed .background,
|
||||
.group.collapsed .shadow {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.edit-mode .group.collapsed .background {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.dragging .group:not(.active) .separator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dragging .group:not(.active):not(.invalid-drop) .separator > span {
|
||||
background-color: #0095dd;
|
||||
transform: scaleY(0.325);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.dragging .group.hovered:not(.active) .separator > span {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* A hint element for dragging items to the top of the group */
|
||||
.edit-mode::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 0.8rem;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
background-color: #0095dd;
|
||||
|
||||
position: relative;
|
||||
top: -0.8rem;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.edit-mode.hover-over-top::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Shift container downwards when hovering over the top to reveal the group
|
||||
* creation hint.
|
||||
*/
|
||||
.edit-mode {
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
|
||||
.edit-mode.hover-over-top {
|
||||
transform: translateY(0.8rem);
|
||||
}
|
||||
|
||||
/* RTL View */
|
||||
:-moz-dir(rtl) .group .header .toggle {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
After Width: | Height: | Size: 626 B |
|
|
@ -0,0 +1,16 @@
|
|||
;window.GaiaPinCard=(function(win){'use strict';var proto=Object.create(HTMLElement.prototype);var DEFAULT_COLOR=[77,77,77];var baseurl=window.GaiaPinCardBaseurl||'/shared/elements/gaia_pin_card/';proto.createdCallback=function(test){var shadow=this.createShadowRoot();this._template=template.content.cloneNode(true);this.container=this._template.querySelector('.pin-card');this.bgElement=this._template.querySelector('.background');this.descElement=this._template.querySelector('.description');this.titleElement=this._template.querySelector('header');this.iconElement=this._template.querySelector('i');this._background={};shadow.appendChild(this._template);ComponentUtils.style.call(this,baseurl);};Object.defineProperty(proto,'background',{get:function(){return this._background;},set:function(background){var bgSrc=background.src?'url('+background.src+')':'';this._background=background;this.bgElement.style.backgroundImage=bgSrc;this.bgElement.style.backgroundColor=background.themeColor||'#4d4d4d';var opacity=background.themeColor?'0.30':'0.15';var computedStyle=window.getComputedStyle(this.bgElement);var colorCodes;try{colorCodes=getColorCodes(computedStyle.backgroundColor);}catch(error){colorCodes=DEFAULT_COLOR;opacity=0.15;}
|
||||
var rgbaColor=colorCodes.slice(1).join(',')+', '+opacity;var bgColorRgba='rgba('+rgbaColor+')';var computedWidth=computedStyle.width;var width=computedWidth==='auto'?'140px':computedWidth;var shadow='inset 0 0 0 '+width;this.bgElement.style.boxShadow=shadow+' '+bgColorRgba;}});Object.defineProperty(proto,'meta',{get:function(){return this._meta;},set:function(meta){this._meta=meta;this.renderCard();}});Object.defineProperty(proto,'title',{get:function(){return this.titleElement.textContent;},set:function(title){this.titleElement.textContent=title;}});Object.defineProperty(proto,'icon',{get:function(){return this.iconElement.style.backgroundImage;},set:function(icon){this.iconElement.style.backgroundImage=icon;}});proto.renderCard=function(){var description=getDescription(this._meta);if(description){this.descElement.appendChild(description);this.container.classList.remove('no-content');}
|
||||
var background=getBackgroundBlob(this._meta);if(background){this.background={src:URL.createObjectURL(background),themeColor:this._meta['theme-color']||null};}};var template=document.createElement('template');template.innerHTML=`<article class="pin-card no-content">
|
||||
<div class="icon-container">
|
||||
<i></i>
|
||||
</div>
|
||||
<div class="background"></div>
|
||||
<div class="content">
|
||||
<header></header>
|
||||
<section class="description"></section>
|
||||
</div>
|
||||
</article>`;function getColorCodes(color){var colorCodes=/rgb\((\d+), (\d+), (\d+)\)/.exec(color);return colorCodes;}
|
||||
function getDescription(meta){if(!meta['og:description']){return false;}
|
||||
var desc=document.createElement('p');desc.textContent=meta['og:description'];return desc;}
|
||||
function getBackgroundBlob(meta){return meta['og:image']||meta.screenshot;}
|
||||
return document.registerElement('gaia-pin-card',{prototype:proto});})(window);
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
.pin-card {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
background-size: cover;
|
||||
color: #000;
|
||||
font-weight: 500;
|
||||
font-family: sans-serif;
|
||||
line-height: normal;
|
||||
white-space: normal;
|
||||
font-size: 1.2rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.pin-card > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pin-card .background {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 47%;
|
||||
opacity: 0.7;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.pin-card i {
|
||||
position: absolute;
|
||||
top: .15rem;
|
||||
left: .15rem;
|
||||
width: 3.03rem;
|
||||
height: 3.03rem;
|
||||
background: url(images/default_icon.png);
|
||||
background-size: 3.5rem 3.5rem;
|
||||
background-position: center center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
position: absolute;
|
||||
top: 4.8rem;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
left: calc(50% - 1.65rem);
|
||||
width: 3.33rem;
|
||||
height: 3.33rem;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
|
||||
background-color: white;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pin-card .content {
|
||||
position: relative;
|
||||
height: 53%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pin-card header {
|
||||
position: relative;
|
||||
height: 1.6rem;
|
||||
color: #5f5f5f;
|
||||
font-size: 1.4rem;
|
||||
font-style: italic;
|
||||
padding: 0 0.8rem;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
top: 1.5rem;
|
||||
}
|
||||
|
||||
.pin-card.no-content header {
|
||||
top: 2.5rem;
|
||||
}
|
||||
|
||||
.pin-card.no-content .description {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pin-card .description p {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
height: 3rem;
|
||||
line-height: 1.4rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 300;
|
||||
color: #5f5f5f;
|
||||
padding: 0 0.8rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,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,5 @@
|
|||
;'use strict';(function(exports){const FETCH_XHR_TIMEOUT=10000;function fetchBlob(uri){return new Promise(function(accept,reject){var xhr=new XMLHttpRequest({mozAnon:true,mozSystem:true});xhr.open('GET',uri,true);xhr.responseType='blob';xhr.timeout=FETCH_XHR_TIMEOUT;xhr.send();xhr.onload=function(){var status=xhr.status;if(status!==0&&status!==200){reject(new Error('Got HTTP status '+status+' trying to load '+uri));return;}
|
||||
accept(xhr.response);};xhr.onerror=xhr.ontimeout=function(){reject(new Error('Error while HTTP GET: ',uri));};});}
|
||||
function Icon(elem,uri){this.elem=elem;this.uri=uri;}
|
||||
Icon.prototype={get size(){return this._size||40;},set size(size){this._size=size||this.size;var style=this.elem.style;var sizeInRems=(this._size/10)+'rem';style.backgroundSize=style.width=style.height=sizeInRems;},render:function render(options){options=options||{};var uri=this.uri;this.size=options.size;if(!uri){return;}
|
||||
fetchBlob(uri).then(function(blob){this.renderBlob(blob,options);}.bind(this));},renderBlob:function renderBlob(blob,options){options=options||{};var style=this.elem.style;this.size=options.size;var img=new Image();img.src=URL.createObjectURL(blob);var size=this.size;img.onload=()=>{var renderer=new GridIconRenderer({grid:{layout:{get gridIconSize(){return size;},get gridMaxIconSize(){return size*devicePixelRatio;}}}});var type=options.type||GridIconRenderer.TYPE.FAVICON;renderer[type](img).then((blob)=>{var url=URL.createObjectURL(blob);var imageUrl='url('+url+')';style.backgroundImage=imageUrl;if(options.onLoad){options.onLoad(blob);}});URL.revokeObjectURL(img.src);};img.onerror=()=>{if(options.onerror){options.onerror();}};}};exports.Icon=Icon;}(window));
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
;'use strict';(function IconsHelper(exports){const ICON_CACHE_PERIOD=24*60*60*1000;const FETCH_XHR_TIMEOUT=10000;const DEBUG=false;var dataStore=null;function getDefaultIconSize(){var dpr=window.devicePixelRatio;return(dpr&&dpr>1)?142:84;}
|
||||
function sizeIsNearer(size1,size2,targetSize){var delta1=Math.abs(targetSize-size1);var delta2=Math.abs(targetSize-size2);return(delta1<=delta2);}
|
||||
function getIcon(uri,iconTargetSize,placeObj={},siteObj={}){var iconUrl=null;iconTargetSize=iconTargetSize*window.devicePixelRatio;if(siteObj.webManifestUrl&&siteObj.webManifest){iconUrl=getBestIconFromWebManifest(siteObj.webManifest,iconTargetSize);if(DEBUG&&iconUrl){console.log('Icon from Web Manifest');}}
|
||||
if(!iconUrl&&siteObj.manifest&&siteObj.manifest.icons){iconUrl=getBestIconFromWebManifest({icons:_convertToWebManifestIcons(siteObj.manifest,siteObj.origin||siteObj.manifest.origin)},iconTargetSize);if(DEBUG&&iconUrl){console.log('Icon from Firefox App Manifest');}}
|
||||
if(!iconUrl&&placeObj&&placeObj.icons){iconUrl=getBestIconFromMetaTags(placeObj.icons,iconTargetSize);if(DEBUG&&iconUrl){console.log('Icon from Meta tags');}}
|
||||
if(!iconUrl){var a=document.createElement('a');a.href=uri;iconUrl=a.origin+'/favicon.ico';if(iconTargetSize){iconUrl+='#-moz-resolution='+iconTargetSize+','+iconTargetSize;}
|
||||
DEBUG&&console.log('Icon from favicon.ico');}
|
||||
return new Promise(resolve=>{resolve(iconUrl);});}
|
||||
function getIconBlob(uri,iconTargetSize,placeObj={},siteObj={}){return new Promise((resolve,reject)=>{getIcon(uri,iconTargetSize,placeObj,siteObj).then(iconUrl=>{getStore().then(iconStore=>{iconStore.get(iconUrl).then(iconObj=>{if(!iconObj||!iconObj.timestamp||Date.now()-iconObj.timestamp>=ICON_CACHE_PERIOD){return fetchIconBlob(iconUrl).then(iconBlob=>{var img=document.createElement('img');var icon=new Icon(img,uri);icon.renderBlob(iconBlob,{size:iconTargetSize,onLoad:function(blob){var iconObj={blob:blob,originalUrl:iconUrl.toString(),timestamp:Date.now()};resolve(iconObj);iconStore.add(iconObj,iconUrl);},onerror:function(e){reject(`Failed to fetch icon ${iconUrl}`);}});}).catch(err=>{reject(`Failed to fetch icon ${iconUrl}: ${err}`);});}
|
||||
return resolve(iconObj);}).catch(err=>{reject(`Failed to get icon from dataStore: ${err}`);});}).catch(err=>{reject(`Error opening the dataStore: ${err}`);});});});}
|
||||
function setElementIcon(icon,targetSize){return getIconBlob(icon.bookmark.url,targetSize,icon.bookmark,icon.bookmark).then(iconObj=>{if(iconObj.blob){icon.icon=iconObj.blob;return Promise.resolve();}else if(icon.bookmark.icon){return fetchIconBlob(icon.bookmark.icon).then(iconBlob=>{icon.icon=iconBlob;return Promise.resolve();},Promise.reject.bind(Promise));}
|
||||
return Promise.reject('No icon data found');},Promise.reject.bind(Promise));}
|
||||
function getBestIconFromWebManifest(webManifest,iconSize){var icons=webManifest.icons;if(!icons){return null;}
|
||||
var maxSize=10000;var bestSize=maxSize;var iconURL=null;iconSize=iconSize||getDefaultIconSize();icons.forEach((potentialIcon)=>{if(!iconURL){iconURL=potentialIcon.src;}
|
||||
var sizes=Array.from(potentialIcon.sizes);var nearestSize=getNearestSize(sizes,iconSize,bestSize);if(nearestSize!==bestSize&&sizeIsNearer(nearestSize,bestSize,iconSize)){iconURL=potentialIcon.src;bestSize=nearestSize;}});return iconURL?iconURL:null;}
|
||||
function _convertToWebManifestIcons(manifest,origin){return Object.keys(manifest.icons).map(function(size){var url=manifest.icons[size];var sizes=[size+'x'+size];url=url.indexOf('http')>-1?url:origin+url;return{src:new URL(url),sizes:sizes};});}
|
||||
function getBestIconFromMetaTags(icons,iconSize){if(!icons){return null;}
|
||||
iconSize=iconSize||getDefaultIconSize();var iconURL=null;var bestSize=10000;Object.keys(icons).forEach((uri)=>{var potentialIcon=icons[uri];if(!iconURL){iconURL=uri;}
|
||||
var sizes=Array.from(potentialIcon.sizes);var nearestSize=getNearestSize(sizes,iconSize,bestSize);if(nearestSize!==bestSize&&sizeIsNearer(nearestSize,bestSize,iconSize)){iconURL=uri;bestSize=nearestSize;}
|
||||
if(potentialIcon.rel==='apple-touch-icon'||potentialIcon.rel==='apple-touch-icon-precomposed'){var moreInfoUrl='https://developer.mozilla.org/en-US/'+'Apps/Build/Icon_implementation_for_apps#General_icons_for_web_apps';console.warn('Warning: The apple-touch icons are being used '+'as a fallback only. They will be deprecated in '+'the future. See '+moreInfoUrl);}});return iconURL||null;}
|
||||
function getNearestSize(sizes,iconSize,bestSize){var bogusSize=10000;if(!bestSize){bestSize=bogusSize;}
|
||||
var nearestSize=sizes.reduce(function(nearestSize,sizeString,idx){var size=widthFromSizeString(sizeString);if(isNaN(size)){return nearestSize;}
|
||||
if(sizeIsNearer(size,nearestSize,iconSize)){return size;}else{return nearestSize;}},bestSize);return nearestSize===bogusSize?-1:nearestSize;}
|
||||
function widthFromSizeString(size){size=size||'';var xIndex=size.indexOf('x');if(!xIndex){return NaN;}
|
||||
return parseInt(size.substr(0,xIndex));}
|
||||
function getStore(){return new Promise(resolve=>{if(dataStore){return resolve(dataStore);}
|
||||
navigator.getDataStores('icons').then(stores=>{dataStore=stores[0];return resolve(dataStore);});});}
|
||||
function clear(){return getStore().then(iconStore=>{iconStore.clear();});}
|
||||
function fetchIcon(iconUrl){return new Promise((resolve,reject)=>{fetchIconBlob(iconUrl).then((iconBlob)=>{var img=document.createElement('img');img.src=URL.createObjectURL(iconBlob);img.onload=()=>{var iconSize=Math.max(img.naturalWidth,img.naturalHeight);resolve({blob:iconBlob,url:iconUrl,size:iconSize,timestamp:Date.now()});};img.onerror=()=>{reject(new Error(`Error while loading image.`));};}).catch((e)=>{reject(new Error(`Error while loading image: ${e}`));});});}
|
||||
function fetchIconBlob(iconUrl){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest({mozAnon:true,mozSystem:true});xhr.open('GET',iconUrl,true);xhr.responseType='blob';xhr.timeout=FETCH_XHR_TIMEOUT;xhr.send();xhr.onload=()=>{if(xhr.readyState===XMLHttpRequest.DONE&&xhr.status===200){var iconBlob=xhr.response;resolve(iconBlob);return;}
|
||||
reject(new Error(`Got HTTP status ${xhr.status} trying to load ${iconUrl}.`));};xhr.onerror=xhr.ontimeout=()=>{reject(new Error(`Error while getting ${iconUrl}.`));};});}
|
||||
exports.IconsHelper={getIcon:getIcon,getIconBlob:getIconBlob,setElementIcon:setElementIcon,getBestIconFromWebManifest:getBestIconFromWebManifest,getBestIconFromMetaTags:getBestIconFromMetaTags,fetchIcon:fetchIcon,fetchIconBlob:fetchIconBlob,get defaultIconSize(){return getDefaultIconSize();},clear:clear,getNearestSize:getNearestSize,};})(window);
|
||||
245
assets/webapps/homescreen.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();}());
|
||||