Publish Vulpes Preview 15 sources for desktop, Android and Tundra

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

View file

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

View file

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

After

Width:  |  Height:  |  Size: 294 B

View file

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

After

Width:  |  Height:  |  Size: 243 B

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

View file

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

View file

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

View file

@ -0,0 +1,11 @@
;'use strict';window.GaiaConfirm=(function(win){var proto=Object.create(HTMLElement.prototype);var baseurl=window.GaiaConfirmBaseurl||'/shared/elements/gaia_confirm/';function eatEvent(unless,event){if(unless&&unless.indexOf(event.target)!==-1){return;}
event.preventDefault();event.stopImmediatePropagation();}
proto.createdCallback=function(){var shadow=this.createShadowRoot();this._template=template.content.cloneNode(true);shadow.appendChild(this._template);ComponentUtils.style.call(this,baseurl);var confirm=this.querySelector('gaia-buttons .confirm');var cancel=this.querySelector('gaia-buttons .cancel');if(confirm){confirm.addEventListener('click',(e)=>{eatEvent(null,e);this.dispatchEvent(new CustomEvent('confirm'));});}
if(cancel){cancel.addEventListener('click',(e)=>{eatEvent(null,e);this.dispatchEvent(new CustomEvent('cancel'));});}};var template=document.createElement('template');template.innerHTML=`<form role="dialog" class="confirm">
<section>
<content select="h1"></content>
<content select="p"></content>
</section>
<content select="gaia-buttons">
</content>
</form>`;return document.registerElement('gaia-confirm',{prototype:proto});})(window);

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

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

View file

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

View file

@ -0,0 +1,9 @@
;'use strict';(function(exports){const maxIconsPerRow=4;const iconScaleFactorMaxIconsPerRow=5;const minIconsPerRow=3;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,_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);}
this._offsetY=0;this._percent=value==minIconsPerRow?1:minIconsPerRow/maxIconsPerRow;this._cols=value;},get constraintSize(){if(!constraintSize){this.calculateSize();}
return constraintSize;},get gridItemHeight(){var height=this.gridIconSize+
(this._cols===minIconsPerRow?distanceBetweenIconsWithMinIconsPerRow:distanceBetweenIconsWithMaxIconsPerRow);var defaultTextRows=2;var definedRows=parseInt(this.gridView.element.getAttribute('text-rows'),10);if(definedRows>defaultTextRows){height+=(definedRows-defaultTextRows)*eachTextRowHeight;}
return height;},get gridItemWidth(){return this.constraintSize/this._cols;},get gridMaxIconSize(){var baseSize=(this.constraintSize/iconScaleFactorMinIconsPerRow);return baseSize*devicePixelRatio;},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);},get offsetY(){return this._offsetY;},set offsetY(value){this._offsetY=value;},stepYAxis:function(value){this._offsetY+=value;},handleEvent:function(e){switch(e.type){case'updated':var prop=e.target;if(prop.name==='grid.cols'){this.cols=parseInt(prop.value,10);this.gridView.render();}
break;}},calculateSize:function(){constraintSize=Math.min(window.screen.width,window.screen.height);},onReady:function(){window.dispatchEvent(new CustomEvent('gaiagrid-layout-ready'));}};exports.GridLayout=GridLayout;}(window));

View file

@ -0,0 +1,47 @@
;'use strict';(function(exports){const APP_LAUNCH_TIMEOUT=3000;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);if(config.element.hasAttribute('cols')){this.layout.cols=parseInt(config.element.getAttribute('cols'),10);}
this.start();}
GridView.prototype={icons:{},items:[],get element(){return this.config.element;},set cols(value){this.layout.cols=value;},_launchingApp:false,add:function(item,insertTo,expandGroup){if(!item){return;}
if(item.identifier){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'){console.log('Error, could not load identifier for object: ',JSON.stringify(item.detail));return;}
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;}}}},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];if(!item.isDraggable()){continue;}
if(this.config.features.disableSections&&item.detail.type==='divider'){continue;}
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(!icon){for(i=0;i<iLen;i++){if(this.items[i].element===element){icon=this.items[i];break;}}}
if(icon&&excludeCollapsedIcons){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;},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'){if(inEditMode&&e.target.classList.contains('icon')){if(!icon.isEditable()){return;}
action='edit';}else{if(!icon[action]){return;}
icon.element.classList.add('launching');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._launchingTimeout=window.setTimeout(function(){this._launchingTimeout=null;this._launchingApp=false;}.bind(this),APP_LAUNCH_TIMEOUT);}
icon[action](e.target);},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);if(skipDivider){return;}
var lastItem=this.items[this.items.length-1];if(!lastItem||!(lastItem instanceof GaiaGrid.Divider)){this.items.push(new GaiaGrid.Divider());}},removeAllPlaceholders:function(){var toSplice=[];var previousItem;this.items.forEach(function(item,idx){if(item instanceof GaiaGrid.Placeholder){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);},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);item.element=null;item.lastX=null;item.lastY=null;item.lastScale=null;}}
this.items=[];this.icons={};},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();}},render:function(options){var self=this;options=options||{};this.removeAllPlaceholders();this.cleanItems(options.skipDivider);var oldHeight=this.layout.offsetY;this.layout.offsetY=0;var x=0;var y=0;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];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;}}
if(nextDivider&&!nextDivider.detail.collapsed){this.layout.offsetY+=nextDivider.headerHeight;}}
if(nextDivider&&nextDivider.detail.collapsed){item.setPosition(idx);continue;}}
if(x>0&&item.gridWidth>1&&x+item.gridWidth>=this.layout.cols){var remaining=this.layout.cols-x;this.createPlaceholders([x,y],idx,remaining);idx+=remaining;item=this.items[idx];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');}}}
x+=item.gridWidth;if((x>=this.layout.cols)&&(idx<this.items.length-1)){step(item);}}
if(this.layout.offsetY!=oldHeight){if(this.dragdrop&&this.dragdrop.inDragAction){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();},loadDragDrop:function(){if(!this.dragdrop&&this.config.features.dragdrop){LazyLoader.load('shared/elements/gaia_grid/js/grid_dragdrop.js',()=>{if(this.dragdrop){return;}
this.dragdrop=new GridDragDrop(this);});}}};exports.GridView=GridView;}(window));

View file

@ -0,0 +1,9 @@
;'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={start:function(){window.addEventListener('touchstart',this);},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='';},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;}
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;}
this._stopGestureListeners();var ontransitionend=function(){this.arrows.removeEventListener('transitionend',ontransitionend);this.indicator.classList.add('active');this.gridView.render();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');document.body.clientHeight;var scaleTransform='scale(1)';if(this.zoomInProgress&&touchDistance<this.zoomStartDistance){scaleTransform='scale(0.4)';}
this.arrows.style.transform=scaleTransform;break;}}};exports.GridZoom=GridZoom;}(window));

View file

@ -0,0 +1,4 @@
;'use strict';(function(exports){const TYPE='bookmark';function Bookmark(record,features){this.detail=record;this.features=features||{};this.detail.type=TYPE;}
Bookmark.prototype={__proto__:GaiaGrid.GridItem.prototype,renderer:GridIconRenderer.TYPE.FAVICON,get pixelHeight(){return this.grid.layout.gridItemHeight;},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,isEditable:function(){return this.features&&this.features.isEditable!==false;},isRemovable:function(){return this.features&&this.features.isRemovable!==false;},isDraggable:function(){return this.features&&this.features.isDraggable!==false;},render:function(){GaiaGrid.GridItem.prototype.render.call(this);this.element.classList.add('bookmark');if(this.isEditable()){this.element.classList.add('editable');}},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(','));},edit:function(){new MozActivity({name:'save-bookmark',data:{type:'url',url:this.detail.id}});}};exports.GaiaGrid.Bookmark=Bookmark;}(window));

View file

@ -0,0 +1,3 @@
;'use strict';(function(exports){function Divider(){this.detail={type:'divider',index:0};}
Divider.prototype={__proto__:GaiaGrid.GridItem.prototype,x:0,y:0,headerHeight:0,get pixelHeight(){return(this.grid.layout.cols>3)?50:60;},gridWidth:4,scale:1,render:function(){if(!this.element){var divider=this.element=document.createElement('section');divider.className='divider';var span=document.createElement('span');span.className='spacer';divider.appendChild(span);this.grid.element.appendChild(divider);}
this.element.style.transform='translate(0 ,'+this.y+'px)';},remove:function(){if(this.element){this.element.parentNode.removeChild(this.element);}},isDraggable:function(){return false;}};exports.GaiaGrid.Divider=Divider;}(window));

View file

@ -0,0 +1,31 @@
;'use strict';(function(exports){const ICON_BLOB_DECORATED_EVENT='iconblobdecorated';const ICON_BLOB_ERROR_EVENT='gaiagrid-iconbloberror';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 GridItem(){this.detail={};}
GridItem.prototype={x:0,y:0,pixelHeight:0,gridWidth:1,scale:1,persistToDB:true,lastX:null,lastY:null,lastScale:null,get hasCachedIcon(){return this.detail&&this.detail.decoratedIconBlob;},renderer:GridIconRenderer.TYPE.STANDARD,iconState:'none',get defaultIcon(){return'/shared/elements/gaia_grid/images/default_icon.png';},get grid(){return document.getElementsByTagName('gaia-grid')[0]._grid;},isRemovable:function(){return false;},isEditable:function(){return false;},isDraggable:function(){return true;},isIconFromOrigin:function(){return this.icon.startsWith('http')||this.icon.startsWith('app:');},setPosition:function(position){this.detail.index=position;},setCoordinates:function(x,y){this.x=x;this.y=y;},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;var filter=(a)=>a>=maxSize?a:0;var bestSize=Math.min(...sizes.map(filter));bestSize=bestSize||Math.max(...sizes);return bestSize||0;},closestIconFromList:function(choices){var icon;if(Array.isArray(choices)){var manifest={'icons':choices};var maxSize=this.grid.layout.gridMaxIconSize;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];if(!UrlHelper.hasScheme(icon)){icon=this.app.origin+icon;}
return icon;},renderIconFromSrc:function(url){var background=new Image();background.src=url;background.onload=this._decorateIcon.bind(this,background);},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(this.hasCachedIcon){this._stampElementWithIcon('blobcache');this._displayDecoratedIcon(this.detail.decoratedIconBlob,true);return;}
this.renderIconFromSrc(this.defaultIcon);this._stampElementWithIcon(this.defaultIcon);};},_decorateIcon:function(img){var strategy=this.detail.renderer||this.renderer;this.rendererInstance=new GridIconRenderer(this);this.rendererInstance[strategy](img).then(this._displayDecoratedIcon.bind(this));},_displayDecoratedIcon:function(blob,isCachedIcon){if(!this.element){return;}
var style=this.element.style;if(!style.backgroundSize){style.height=this.grid.layout.gridItemHeight+'px';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);});},_compareBlobs:function(blob1,blob2){return new Promise(function(resolve){if(!blob1||!blob2||blob1.type!==blob2.type||blob1.size!==blob2.size){resolve(false);return;}
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));});},fetchIconBlob:function(){return fetchBlob(this.icon);},_stampElementWithIcon:function(uri){if(!this.element||uri.startsWith('data:')){return;}
if(uri.length>255){uri=uri.slice(0,255);}
this.element.dataset.testIcon=uri;},renderIcon:function(renderCachedIcon){var icon=this.icon;this.iconState='pending';if(!icon){return;}
if(renderCachedIcon&&this.hasCachedIcon){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){if(!this.isIconFromOrigin()){this._stampElementWithIcon(icon);this.renderIconFromSrc(icon);this.iconState='success';return Promise.resolve();}
var eventTarget=this.grid.element;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(!blob){return blobNotFound();}
this.renderIconFromBlob(blob);this.iconState='success';this._stampElementWithIcon(icon);}).catch((err)=>{blobNotFound();});},removeFromGrid:function(){var idx=this.grid.items.indexOf(this);if(idx===-1){console.error('Attempting to remove self before item has been added!');return;}
this.grid.items.splice(idx,1);delete this.grid.icons[this.identifier];if(this.element){this.element.parentNode.removeChild(this.element);}
this.grid.render({from:idx-1});},remove:function(){this.grid.element.dispatchEvent(new CustomEvent('removeitem',{detail:this}));},render:function(){var scale=this.grid.layout.percent*this.scale;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';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);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);},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+')';},setActive:function(active){if(active){this.element.classList.add('active');this.active=true;}else{this.element.classList.remove('active');this.active=false;}},updateTitle:function(){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;}},updateFromDatastore:function(record){var iconChanged=record.icon!==this.icon;var nameChanged=record.name!==this.name;var type=this.detail.type;var lastIcon=this.icon;record.type=type;this.detail=record;if(nameChanged){this.updateTitle();}
if(iconChanged&&record.icon){this.renderIcon();}else if(!record.icon){this.detail.icon=lastIcon;}}};exports.GaiaGrid.GridItem=GridItem;}(window));

View file

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

View file

@ -0,0 +1,3 @@
;'use strict';(function(exports){const TYPE='marketplace-app';function MarketPlaceApp(record){this.detail=record;this.detail.type=TYPE;}
MarketPlaceApp.prototype={__proto__:GaiaGrid.Bookmark.prototype,renderer:GridIconRenderer.TYPE.STANDARD,get icon(){var icon=this._accurateIcon;if(!icon){icon=this._accurateIcon=this.closestIconFromList(this.detail.icons);}
return icon;},launch:function(){new MozActivity({name:'marketplace-app',data:{slug:this.detail.slug}});},};exports.GaiaGrid.MarketPlaceApp=MarketPlaceApp;}(window));

View file

@ -0,0 +1,19 @@
;'use strict';(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;if(navigator.mozL10n&&navigator.mozL10n.ctx.qps.indexOf(userLang)!==-1){return navigator.mozL10n.qps[userLang].translate(str);}
return str;}
function Mozapp(app,entryPoint,details){this.app=app;this.entryPoint=entryPoint;this.detail={type:'app',manifestURL:app.manifestURL,entryPoint:entryPoint,index:0,decoratedIconBlob:details&&details.decoratedIconBlob};MOZAPP_EVENTS.forEach(function(type){this.app['on'+type]=this.handleEvent.bind(this);},this);this.setAppState(this._determineState(app));}
Mozapp.HIDDEN_ROLES=['system','input','homescreen','search','addon','langpack'];Mozapp.prototype={__proto__:GaiaGrid.GridItem.prototype,_isHiddenRole:function(){var manifest=this.app.manifest;if(!manifest||!manifest.role){return null;}
return Mozapp.HIDDEN_ROLES.indexOf(manifest.role)!==-1;},_determineState:function(app){if(app.installState==='pending'&&!app.downloadAvailable&&!app.readyToApplyDownload){return APP_UNRECOVERABLE;}
if(app.downloading){return APP_LOADING;}
if(app.downloadError&&app.downloadError.name){if(app.downloadError.name==='DOWNLOAD_CANCELED'){return APP_PAUSED;}
return APP_ERROR;}
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':if(this._isHiddenRole()){console.warn('Removing hidden app from the grid',this.name);return this.removeFromGrid();}
delete this._accurateIcon;delete this._accurateIconSize;this.setAppState(this._determineState(this.app));this.renderIcon();window.dispatchEvent(new CustomEvent('downloadapplied',{detail:{id:this.app.manifestURL}}));break;}},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;},get pixelHeight(){return this.grid.layout.gridItemHeight;},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);},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);},isRemovable:function(){return this.app.removable;},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:()=>{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(){var needsStateSet=!this.element;GaiaGrid.GridItem.prototype.render.apply(this,arguments);if(needsStateSet){this.setAppState(this.appState);}},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);}
return app.launch();}};exports.GaiaGrid.Mozapp=Mozapp;}(window));

View file

@ -0,0 +1,3 @@
;'use strict';(function(exports){function Placeholder(){this.detail={type:'placeholder',index:0};}
Placeholder.prototype={__proto__:GaiaGrid.GridItem.prototype,get pixelHeight(){return this.grid.layout.gridItemHeight;},createsGroupOnDrop:false,gridWidth:1,persistToDB:false,get name(){return'';},render:function(){if(!this.element){var tile=document.createElement('div');tile.className='icon placeholder';tile.style.height=this.pixelHeight+'px';this.element=tile;this.grid.element.appendChild(tile);}
this.transform(this.x,this.y,this.grid.layout.percent);},remove:function(){if(this.element){this.element.parentNode.removeChild(this.element);}}};exports.GaiaGrid.Placeholder=Placeholder;}(window));

View file

@ -0,0 +1,7 @@
;window.GaiaGrid=(function(win){'use strict';var proto=Object.create(HTMLElement.prototype);var baseurl=window.GaiaGridBaseurl||'/shared/elements/gaia_grid/';proto.createdCallback=function(){var shadow=this.createShadowRoot();this._template=template.content.cloneNode(true);this._styleHack();this._grid=new GridView({element:this,features:{dragdrop:this.getAttribute('dragdrop')!==null,zoom:this.getAttribute('zoom')!==null,disableSections:this.getAttribute('disable-sections')!==null}});shadow.appendChild(this._template);};proto.add=function(){this._grid.add.apply(this._grid,arguments);};proto.clear=function(){this._grid.clear.apply(this._grid,arguments);};proto.render=function(){this._grid.layout.calculateSize();this._grid.render.apply(this._grid,arguments);};proto.start=function(){this._grid.start.apply(this._grid,arguments);};proto.stop=function(){this._grid.stop.apply(this._grid,arguments);};proto.getIcons=function(){return this._grid.icons;};proto.getItems=function(){return this._grid.items;};proto.getIcon=function(identifier){return this._grid.icons[identifier];};proto.removeIconByIdentifier=function(identifier){delete this._grid.icons[identifier];};proto.removeItemByIndex=function(itemIndex){this._grid.items.splice(itemIndex,1);};proto.popDivider=function(){var items=this._grid.items;if(items.length&&items[items.length-1].detail.type==='divider'){return items.pop();}
return null;};proto.appendItem=function(item){var items=this._grid.items;var lastIndex=items.length-1;if(items[lastIndex].detail.type==='divider'){this.add(item,lastIndex);}else{this.add(item);}};proto.appendItemToExpandedGroup=function(item){var items=this._grid.items;var lastIndex=items.length-1;if(items[lastIndex].detail.type==='divider'&&items[lastIndex].detail.collapsed){items[lastIndex].expand();}
this.appendItem(item);};proto.setEditHeaderElement=function(element){if(this._grid.dragdrop){this._grid.dragdrop.editHeaderElement=element;}};Object.defineProperty(proto,'maxIconSize',{get:function(){return this._grid.layout.gridMaxIconSize;}});proto.getIndexLastIcon=function(){var items=this._grid.items;for(var i=this._grid.items.length-1;i>=0;i--){if(items[i]instanceof GaiaGrid.Mozapp){return i;}}};proto.removeNonVisualElements=function(){var nonVisualElements=[GaiaGrid.Placeholder];function isNonVisual(elem){var retVal=false;for(var i=0,iLen=nonVisualElements.length;i<iLen&&!retVal;i++){retVal=elem instanceof nonVisualElements[i];}
return retVal;}
var i=0;var iLen=this._grid.items.length;while(i<iLen){this._grid.items[i].detail.index=i;if(isNonVisual(this._grid.items[i])){this._grid.items.splice(i,1);iLen-=1;}else{i+=1;}}};proto.moveTo=function(orig,dst){this._grid.items.splice(dst,0,this._grid.items.splice(orig,1)[0]);};proto._styleHack=function(){var style=this._template.querySelector('style');this.appendChild(style.cloneNode(true));};var stylesheet=baseurl+'style.css';var template=document.createElement('template');template.innerHTML=`<style scoped>
@import url(${stylesheet});</style>
<content></content>`;return document.registerElement('gaia-grid',{prototype:proto});})(window);

View file

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

View file

@ -0,0 +1,24 @@
/**
Some css properties are global (keyframes) and cannot be specified
to to only apply to our shadow dom so we hack around this by including
this file without scoping it.
*/
/**
Bug 1018269 - we need to specify the keyframe outside
of the web component...
*/
@keyframes rotate {
from {
transform: rotate(1deg)
}
to {
transform: rotate(360deg)
}
}
:-moz-dir(rtl) gaia-grid {
direction: ltr;
}