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,28 @@
{
"name": "bridge",
"main": "bridge.js",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"README.md",
"node_modules",
"bower_components",
"test",
"tests",
"docs",
"examples"
],
"homepage": "https://github.com/gaia-components/bridge",
"_release": "807683a641",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "807683a641b316f21af7dce39cb678f8e1d12760"
},
"_source": "git://github.com/gaia-components/bridge.git",
"_target": "master",
"_originalSource": "gaia-components/bridge"
}

View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,19 @@
{
"name": "bridge",
"version": "1.0.1",
"main": "bridge.js",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"README.md",
"node_modules",
"bower_components",
"test",
"tests",
"docs",
"examples"
]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,31 @@
(function e$$0(l,n,b){function m(h,f){if(!n[h]){if(!l[h]){var e="function"==typeof require&&require;if(!f&&e)return e(h,!0);if(p)return p(h,!0);e=Error("Cannot find module '"+h+"'");throw e.code="MODULE_NOT_FOUND",e;}e=n[h]={exports:{}};l[h][0].call(e.exports,function(c){var a=l[h][1][c];return m(a?a:c)},e,e.exports,e$$0,l,n,b)}return n[h].exports}for(var p="function"==typeof require&&require,k=0;k<b.length;k++)m(b[k]);return m})({1:[function(g,l,n){var b={service:g("../src/service"),client:g("../src/client"),
_m:g("../src/message")};"u"!=(typeof define)[0]?define([],function(){return b}):self.bridge=b},{"../src/client":2,"../src/message":4,"../src/service":6}],2:[function(g,l,n){function b(a,d,c){if(!(this instanceof b))return new b(a,d,c);"object"==typeof a&&(d=a.endpoint,c=a.timeout,a=a.service);this.id=f();this.service=a;this.timeout=c;this.endpoint=d||this.endpoint;if(!this.endpoint)throw m(1);this.setPort(this.endpoint);this.pending=new Set;this.receiver=h.receiver(this.id).on("_push",this.onPush.bind(this));
e("initialized",a)}function m(a,d){d=[].slice.call(arguments,1);return Error({1:"an endpoint must be defined",2:'Unable to establish a connection with "'+d[0]+'". Either the target endpoint is not alive or the Service is not `.listen()`ing.',3:'Method "'+d[0]+"\" didn't get a response. Either the target endpoint is not alive or the Service is not `.listen()`ing."}[a])}var p=g("./message/port-adaptors"),k=g("./emitter"),h=g("./message"),f=g("./utils").uuid;l.exports=b;var e={0:function(){},1:function(a){return performance.mark("["+
self.constructor.name+"][Client] - "+a)},2:function(a,d){d=[].slice.call(arguments,1);console.log.apply(console,[].concat(["[Client]"+("["+self.constructor.name+"]["+location.pathname+"]")+' - "'+a+'"'],d))}}[0],c=constructor.name;b.prototype={connect:function(){var a=this;e("connect");if(this.connected)return this.connected;e("connecting...",this.service);var d=new MessageChannel;this.channel=d.port1;this.channel.start();var f={clientId:this.id,service:this.service,originEnv:c};return this.connected=
this.message("_connect").set("transfer",[d.port2]).set("data",f).listen(d.port1).send().then(function(d){e("connected",d);d.event.target===a.channel?a.setPort(a.channel):(a.channel.close(),delete a.channel);a.receiver.listen(a.port)}).catch(function(d){"timeout"==(d&&d.message)&&(d=m(2,a.service),console.error(d.message));throw d;})},disconnect:function(a){var d=this;if(!this.connected)return Promise.resolve();e("disconnecting ...");a={noRespond:a&&a.noRespond,data:this.id};this.cancelPending();return this.message("_disconnect").set(a).send().then(function(){return d.onDisconnected()})},
method:function(a,d){var c=this;d=[].slice.call(arguments,1);return this.connect().then(function(){e("method",a);return c.message("_method").set({recipient:c.service,data:{name:a,args:d}}).send()}).then(function(a){return a.value}).catch(function(d){"timeout"==(d&&d.message)&&(d=m(3,a),console.error(d.message));throw d;})},plugin:function(a){a(this,{Emitter:k,uuid:f});return this},message:function(a){var d=this;e("create message",a);var c=h(a).set("port",this.port).set("timeout",this.timeout).on("response",
function(){return d.pending.delete(c)}).on("cancel",function(){return d.pending.delete(c)});this.pending.add(c);return c},cancelPending:function(){e("cancel pending");this.pending.forEach(function(a){a.cancel()});this.pending.clear()},pendingResponded:function(){var a=[];this.pending.forEach(function(d){return a.push(d.responded)});return Promise.all(a)},onPush:function(a){e("on push",a.data);this._emit(a.data.type,a.data.data)},onDisconnected:function(){var a=this;delete this.connected;this.pendingResponded().then(function(){e("disconnected");
a.channel&&a.channel.close();a._emit("disconnected")})},setPort:function(a){e("set port");this.port=p(a)},destroy:function(){var a=this;return this.disconnect().then(function(){a.destroyed||(e("destroy"),a.destroyed=!0,a.receiver.destroy(),a._off(),a.port=a.endpoint=a.receiver=null)})},_on:k.prototype.on,_off:k.prototype.off,_emit:k.prototype.emit};b.prototype.on=function(a,d){var c=this;this.connect().then(function(){e("bind on",a);k.prototype.on.call(c,a,d);c.message("_on").set("noRespond",!0).set("data",
{name:a,clientId:c.id}).send(c.port)});return this};b.prototype.off=function(a,d){var c=this;this.connect().then(function(){k.prototype.off.call(c,a,d);c.message("_off").set("noRespond",!0).set("data",{name:a,clientId:c.id}).send(c.port)});return this}},{"./emitter":3,"./message":4,"./message/port-adaptors":5,"./utils":7}],3:[function(g,l,n){function b(m){if(m)return Object.assign(m,b.prototype)}l.exports=b;b.prototype={on:function(b,g){this._callbacks||(this._callbacks={});this._callbacks[b]||(this._callbacks[b]=
[]);this._callbacks[b].push(g);return this},off:function(b,g){if(this._callbacks)switch(arguments.length){case 0:this._callbacks={};break;case 1:delete this._callbacks[b];break;default:var k=this._callbacks[b];if(!k)return;var h=k.indexOf(g);~h&&k.splice(h,1)}return this},emit:function(b,g){if(this._callbacks)for(var k=this._callbacks[b]||[],k=k.concat(this._callbacks["*"]||[]),h=0;h<k.length;h++)k[h].call(this,g,b);return this}};g=b.prototype;g.off=g.off;g.on=g.on},{}],4:[function(g,l,n){function b(a){this.cancelled=
!1;this.listeners=[];this.deferred=f();this.onMessage=this.onMessage.bind(this);this.onTimeout=this.onTimeout.bind(this);"object"===typeof a?this.setupInbound(a):this.setupOutbound(a);c("initialized",a)}function m(a){this.name=a;this.ports=new Set;this.onMessage=this.onMessage.bind(this);this.listen=this.listen.bind(this);this.unlisten=this.unlisten.bind(this);c("receiver initialized",a)}function p(a,c){c=[].slice.call(arguments,1);return Error({1:".send() can only be called once",2:"response already sent for this message",
3:"a port must be defined",4:"timeout"}[a])}var k=g("./port-adaptors"),h=g("../emitter");g=g("../utils");var f=g.deferred,e=g.uuid;n=l.exports=function(a){return new b(a)};n.receiver=function(a,c){return new m(a,c)};n.Receiver=m;n.Message=b;var c={0:function(){},1:function(a){return performance.mark("["+self.constructor.name+"][Message] - "+a)},2:function(a,c){c=[].slice.call(arguments,1);console.log.apply(console,[].concat(["[Message]"+("["+self.constructor.name+"]["+location.pathname+"]")+' - "'+
a+'"'],c))}}[0];b.prototype={setupOutbound:function(a){this.id=e();this.type=a;this.sent=!1;this.recipient="*"},setupInbound:function(a){c("inbound");this.hasResponded=!1;this.setSourcePort(a.source||a.target);this.event=a;Object.assign(this,a.data)},setSourcePort:function(a){c("set source",a.constructor.name);this.sourcePort=k(a,{ready:!0});return this},set:function(a,d){c("set",a,d);"object"==typeof a?Object.assign(this,a):this[a]=d;return this},serialize:function(){return{id:this.id,type:this.type,
data:this.data,recipient:this.recipient,noRespond:this.noRespond}},preventDefault:function(){c("prevent default");this.defaultPrevented=!0},send:function(a){c("send",this.type);if(this.sent)throw p(1);var d=this.serialize(),f=!this.noRespond;this.port=a?k(a):this.port;if(!this.port)throw p(3);f?(this.listen(this.port),this.setResponseTimeout()):this.deferred.resolve();this.port.postMessage(d,this.getTransfer());c("sent",d);return this.deferred.promise},setResponseTimeout:function(){!1!==this.timeout&&
(this._timer=setTimeout(this.onTimeout,this.timeout||1E3))},clearResponseTimeout:function(){clearTimeout(this._timer)},getTransfer:function(){return this.transfer||this.event&&this.event.ports},onMessage:function(a){if(a.data.response&&a.data.id===this.id&&!this.cancelled)this.onResponse(a)},onTimeout:function(){c("response timeout",this.type);this.silentTimeout||this.deferred.reject(p(4));this.teardown()},listen:function(a){c("add response listener",a);a=k(a);a.addListener(this.onMessage);this.listeners.push(a);
return this},unlisten:function(){var a=this;c("remove response listeners");this.listeners.forEach(function(c){return c.removeListener(a.onMessage)});this.listeners=[]},cancel:function(){this.teardown();this.cancelled=!0;this.emit("cancel")},teardown:function(){this.clearResponseTimeout();this.unlisten()},respond:function(a){function d(a){c("resolve",a);b({type:"resolve",value:a})}function f(a){a=a&&a.message||a;c("reject",a);b({type:"reject",value:a})}function b(a){e.response=a;e.sourcePort.postMessage({id:e.id,
response:a},e.transfer);c("responded with:",a)}c("respond",a);if(this.hasResponded)throw p(2);if(this.sourcePort&&!this.noRespond){var e=this;this.hasResponded=!0;a instanceof Error&&f(a);Promise.resolve(a).then(d,f).catch(f)}},forward:function(a){var d=this;c("forward");return this.set("silentTimeout",!0).send(a).then(function(a){return d.respond(a.value)})},onResponse:function(a){c("on response",a.data);var d=a.data.response,f="reject"==d.type?d.value:d;d.event=a;this.response=d;this.teardown();
this.deferred[this.response.type](f);this.emit("response",d)}};h(b.prototype);m.prototype={listen:function(a){c("listen");a=k(a||self,{receiver:!0});if(!this.ports.has(a))return a.addListener(this.onMessage,this.listen),this.ports.add(a),this},unlisten:function(){var a=this;c("unlisten");this.ports.forEach(function(c){c.removeListener(a.onMessage,a.unlisten)})},onMessage:function(a){if(a.data.id&&a.data.type&&this.isRecipient(a.data.recipient)&&(c("receiver on message",a.data),a=new b(a),this.emit("message",
a),!a.defaultPrevented))try{this.emit(a.type,a)}catch(d){throw a.respond(d),d;}},isRecipient:function(a){return a==this.name||"*"==a||"*"==this.name},destroy:function(){this.unlisten();delete this.name;return this}};h(m.prototype)},{"../emitter":3,"../utils":7,"./port-adaptors":5}],5:[function(g,l,n){function b(c){this.target=c}function m(c,a,d){c.addEventListener(a,d)}function p(c,a,d){a={data:a,source:self};d&&(a.ports=d);c.dispatchEvent(new MessageEvent("message",a))}var k=g("../utils").deferred;
l.exports=function(c,a){if(!c)throw Error({1:"target is undefined"}[1]);if(c&&c.addListener)return c;var d=f[c.constructor.name];return d?d(c,a):new b(c,a)};var h=b.prototype={constructor:b,addListener:function(c){this.target.addEventListener("message",c)},removeListener:function(c){this.target.removeEventListener("message",c)},postMessage:function(c,a){this.target.postMessage(c,a)}},f={HTMLIFrameElement:function(c){var a=e(c);return{addListener:function(a,c){window.addEventListener("message",a)},
removeListener:function(a,c){window.removeEventListener("message",a)},postMessage:function(d,f){a.then(function(){return p(c.contentWindow,d,f)})}}},BroadcastChannel:function(c,a){function d(){var a=k();c.postMessage("ready?");m(c,"message",function q(d){"ready"==d.data&&(c.removeEventListener("message",q),a.resolve())});return a.promise}var f=a&&a.receiver,b=a&&a.ready,b=b||f?Promise.resolve():d();f&&(c.postMessage("ready"),m(c,"message",function(a){"ready?"==a.data&&c.postMessage("ready")}));return{target:c,
addListener:h.addListener,removeListener:h.removeListener,postMessage:function(a,d){b.then(function(){return c.postMessage(a,d)})}}},Window:function(c,a){var d=a&&a.ready||c===parent||c===self,d=d?Promise.resolve():e(c);return{addListener:function(a,c){window.addEventListener("message",a)},removeListener:function(a,c){window.removeEventListener("message",a)},postMessage:function(a,f){d.then(function(){return p(c,a,f)})}}},SharedWorker:function(c){c.port.start();return new b(c.port)},SharedWorkerGlobalScope:function(){var c=
[];return{postMessage:function(){},addListener:function(a,d){this.onconnect=function(a){a=a.ports[0];c.push(a);a.start();d(a)};self.addEventListener("connect",this.onconnect)},removeListener:function(a,d){self.removeEventListener("connect",this.onconnect);c.forEach(function(a){a.close();d(a)})}}}},e=function(){if("undefined"!=typeof window){var c=window.opener||window.parent,a=new WeakSet;c!=self&&m(window,"DOMContentLoaded",function r(){window.removeEventListener("DOMContentLoaded",r);p(c,"load")});
m(self,"message",function(c){return"load"==c.data&&a.add(c.source)});return function(c){var f=c.contentWindow||c;if(a.has(f)||f==window.parent)return Promise.resolve();var b=k();m(window,"message",function q(a){"load"==a.data&&a.source==f&&(window.removeEventListener("message",q),b.resolve())});return b.promise}}}()},{"../utils":7}],6:[function(g,l,n){function b(f){if(!(this instanceof b))return new b(f);k.Receiver.call(this,f);this.clients={};this.methods={};this.on("_disconnect",this.onDisconnect.bind(this)).on("_connect",
this.onConnect.bind(this)).on("_method",this.onMethod.bind(this)).on("_off",this.onOff.bind(this)).on("_on",this.onOn.bind(this));this.destroy=this.destroy.bind(this);h("initialized",f)}function m(f){var b=[].slice.call(arguments,1);return Error({4:'method "'+b[0]+"\" doesn't exist"}[f])}var p=g("./utils").uuid,k=g("./message");g=k.Receiver;l.exports=b;var h={0:function(){},1:function(f){return performance.mark("["+self.constructor.name+"][Service] - "+f)},2:function(f,b){b=[].slice.call(arguments,
1);console.log.apply(console,[].concat(["[Service]"+("["+self.constructor.name+"]["+location.pathname+"]")+' - "'+f+'"'],b))}}[0];b.prototype=Object.create(g.prototype);b.prototype.inWindow="Window"===constructor.name;b.prototype.method=function(f,b){this.methods[f]=b;return this};b.prototype.broadcast=function(f,b,c){var a=this;h("broadcast",f,b,c);this.eachClient(function(d){if(!c||~c.indexOf(d.id))h("broadcasting to",d.id),a.push(f,b,d.id,{noRespond:!0})});return this};b.prototype.push=function(b,
e,c,a){a=a&&a.noRespond;var d=this.getClient(c);return k("_push").set({recipient:c,noRespond:a,data:{type:b,data:e}}).send(d.port)};b.prototype.eachClient=function(b){for(var e in this.clients)b(this.clients[e])};b.prototype.getClient=function(b){return this.clients[b]};b.prototype.onConnect=function(b){h("connection attempt",b.data,this.name);var e=b.data,c=e.clientId;c&&e.service===this.name&&!this.clients[c]&&(this.emit("before-connect",b),b.defaultPrevented||(this.upgradeChannel(b),this.addClient(c,
b.sourcePort),b.respond(),this.emit("connected",c),h("connected",c)))};b.prototype.upgradeChannel=function(b){if(!this.inWindow||"Window"!==b.data.originEnv){var e=b.event.ports;if(e=e&&e[0])b.setSourcePort(e),this.listen(e),e.start();h("channel upgraded")}};b.prototype.onDisconnect=function(b){var e=this.clients[b.data];e&&(this.emit("before-disconnect",b),b.defaultPrevented||(this.removeClient(e.id),b.respond(),this.emit("disconnected",e.id),h("disconnected",e.id)))};b.prototype.onMethod=function(b){h("on method",
b.data);this.emit("before-method",b);if(!b.defaultPrevented){var e=b.data,c=e.name,a,d=this.methods[c];if(!d)throw m(4,c);try{a=d.apply(this,e.args)}catch(g){a=g}b.respond(a)}};b.prototype.onOn=function(b){h("on on",b.data);this.emit("on",b.data)};b.prototype.onOff=function(b){h("on off");this.emit("off",b.data)};b.prototype.addClient=function(b,e){this.clients[b]={id:b,port:e}};b.prototype.removeClient=function(b){delete this.clients[b]};b.prototype.plugin=function(b){b(this,{uuid:p});return this};
b.prototype.disconnect=function(b){this.removeClient(b.id);k("disconnect").set({recipient:b.id,noRespond:!0}).send(b.port)};b.prototype.destroy=function(){delete this.clients;this.unlisten();this.off()};l=b.prototype;l.broadcast=l.broadcast;l.destroy=l.destroy;l.method=l.method;l.plugin=l.plugin},{"./message":4,"./utils":7}],7:[function(g,l,n){n.uuid=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(b){var g=16*Math.random()|0;return("x"==b?g:g&3|8).toString(16)})};
n.deferred=function(){var b={};b.promise=new Promise(function(g,l){b.resolve=g;b.reject=l});return b}},{}]},{},[1]);

View file

@ -0,0 +1,9 @@
var bridge = {
'service': require('../src/service'),
'client': require('../src/client'),
_m: require('../src/message')
};
if ((typeof define)[0] != 'u') define([], () => bridge);
else self['bridge'] = bridge;

View file

@ -0,0 +1,4 @@
var bridge = module.exports = self['bridge'] || {};
bridge['client'] = require('../src/client');
if ((typeof define)[0] != 'u') define([], () => bridge);
else self['bridge'] = bridge;

View file

@ -0,0 +1,4 @@
var bridge = module.exports = self['bridge'] || {};
bridge['service'] = require('../src/service');
if ((typeof define)[0] != 'u') define([], () => bridge);
else self['bridge'] = bridge;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,25 @@
(function(q){"object"===typeof exports&&"undefined"!==typeof module?module.exports=q():"function"===typeof define&&define.amd?define([],q):("undefined"!==typeof window?window:"undefined"!==typeof global?global:"undefined"!==typeof self?self:this).bridge=q()})(function(){return function e(l,h,d){function k(f,p){if(!h[f]){if(!l[f]){var g="function"==typeof require&&require;if(!p&&g)return g(f,!0);if(m)return m(f,!0);g=Error("Cannot find module '"+f+"'");throw g.code="MODULE_NOT_FOUND",g;}g=h[f]={exports:{}};
l[f][0].call(g.exports,function(b){var a=l[f][1][b];return k(a?a:b)},g,g.exports,e,l,h,d)}return h[f].exports}for(var m="function"==typeof require&&require,f=0;f<d.length;f++)k(d[f]);return k}({1:[function(e,l,h){var d=l.exports=self.bridge||{};d.client=e("../src/client");"u"!="undefined"[0]?(void 0)([],function(){return d}):self.bridge=d},{"../src/client":2}],2:[function(e,l,h){function d(a,c,b){if(!(this instanceof d))return new d(a,c,b);"object"==typeof a&&(c=a.endpoint,b=a.timeout,a=a.service);
this.id=p();this.service=a;this.timeout=b;this.endpoint=c||this.endpoint;if(!this.endpoint)throw k(1);this.setPort(this.endpoint);this.pending=new Set;this.receiver=n.receiver(this.id).on("_push",this.onPush.bind(this));g("initialized",a)}function k(a,c){c=[].slice.call(arguments,1);return Error({1:"an endpoint must be defined",2:'Unable to establish a connection with "'+c[0]+'". Either the target endpoint is not alive or the Service is not `.listen()`ing.',3:'Method "'+c[0]+"\" didn't get a response. Either the target endpoint is not alive or the Service is not `.listen()`ing."}[a])}
var m=e("./message/port-adaptors"),f=e("./emitter"),n=e("./message"),p=e("./utils").uuid;l.exports=d;var g={0:function(){},1:function(a){return performance.mark("["+self.constructor.name+"][Client] - "+a)},2:function(a,c){c=[].slice.call(arguments,1);console.log.apply(console,[].concat(["[Client]"+("["+self.constructor.name+"]["+location.pathname+"]")+' - "'+a+'"'],c))}}[0],b=constructor.name;d.prototype={connect:function(){var a=this;g("connect");if(this.connected)return this.connected;g("connecting...",
this.service);var c=new MessageChannel;this.channel=c.port1;this.channel.start();var r={clientId:this.id,service:this.service,originEnv:b};return this.connected=this.message("_connect").set("transfer",[c.port2]).set("data",r).listen(c.port1).send().then(function(c){g("connected",c);c.event.target===a.channel?a.setPort(a.channel):(a.channel.close(),delete a.channel);a.receiver.listen(a.port)}).catch(function(c){"timeout"==(c&&c.message)&&(c=k(2,a.service),console.error(c.message));throw c;})},disconnect:function(a){var c=
this;if(!this.connected)return Promise.resolve();g("disconnecting ...");a={noRespond:a&&a.noRespond,data:this.id};this.cancelPending();return this.message("_disconnect").set(a).send().then(function(){return c.onDisconnected()})},method:function(a,c){var b=this;c=[].slice.call(arguments,1);return this.connect().then(function(){g("method",a);return b.message("_method").set({recipient:b.service,data:{name:a,args:c}}).send()}).then(function(a){return a.value}).catch(function(c){"timeout"==(c&&c.message)&&
(c=k(3,a),console.error(c.message));throw c;})},plugin:function(a){a(this,{Emitter:f,uuid:p});return this},message:function(a){var c=this;g("create message",a);var b=n(a).set("port",this.port).set("timeout",this.timeout).on("response",function(){return c.pending.delete(b)}).on("cancel",function(){return c.pending.delete(b)});this.pending.add(b);return b},cancelPending:function(){g("cancel pending");this.pending.forEach(function(a){a.cancel()});this.pending.clear()},pendingResponded:function(){var a=
[];this.pending.forEach(function(c){return a.push(c.responded)});return Promise.all(a)},onPush:function(a){g("on push",a.data);this._emit(a.data.type,a.data.data)},onDisconnected:function(){var a=this;delete this.connected;this.pendingResponded().then(function(){g("disconnected");a.channel&&a.channel.close();a._emit("disconnected")})},setPort:function(a){g("set port");this.port=m(a)},destroy:function(){var a=this;return this.disconnect().then(function(){a.destroyed||(g("destroy"),a.destroyed=!0,a.receiver.destroy(),
a._off(),a.port=a.endpoint=a.receiver=null)})},_on:f.prototype.on,_off:f.prototype.off,_emit:f.prototype.emit};d.prototype.on=function(a,c){var b=this;this.connect().then(function(){g("bind on",a);f.prototype.on.call(b,a,c);b.message("_on").set("noRespond",!0).set("data",{name:a,clientId:b.id}).send(b.port)});return this};d.prototype.off=function(a,c){var b=this;this.connect().then(function(){f.prototype.off.call(b,a,c);b.message("_off").set("noRespond",!0).set("data",{name:a,clientId:b.id}).send(b.port)});
return this}},{"./emitter":3,"./message":4,"./message/port-adaptors":5,"./utils":6}],3:[function(e,l,h){function d(k){if(k)return Object.assign(k,d.prototype)}l.exports=d;d.prototype={on:function(d,e){this._callbacks||(this._callbacks={});this._callbacks[d]||(this._callbacks[d]=[]);this._callbacks[d].push(e);return this},off:function(d,e){if(this._callbacks)switch(arguments.length){case 0:this._callbacks={};break;case 1:delete this._callbacks[d];break;default:var f=this._callbacks[d];if(!f)return;
var h=f.indexOf(e);~h&&f.splice(h,1)}return this},emit:function(d,e){if(this._callbacks)for(var f=this._callbacks[d]||[],f=f.concat(this._callbacks["*"]||[]),h=0;h<f.length;h++)f[h].call(this,e,d);return this}};e=d.prototype;e.off=e.off;e.on=e.on},{}],4:[function(e,l,h){function d(a){this.cancelled=!1;this.listeners=[];this.deferred=p();this.onMessage=this.onMessage.bind(this);this.onTimeout=this.onTimeout.bind(this);"object"===typeof a?this.setupInbound(a):this.setupOutbound(a);b("initialized",a)}
function k(a){this.name=a;this.ports=new Set;this.onMessage=this.onMessage.bind(this);this.listen=this.listen.bind(this);this.unlisten=this.unlisten.bind(this);b("receiver initialized",a)}function m(a,c){c=[].slice.call(arguments,1);return Error({1:".send() can only be called once",2:"response already sent for this message",3:"a port must be defined",4:"timeout"}[a])}var f=e("./port-adaptors"),n=e("../emitter");e=e("../utils");var p=e.deferred,g=e.uuid;h=l.exports=function(a){return new d(a)};h.receiver=
function(a,c){return new k(a,c)};h.Receiver=k;h.Message=d;var b={0:function(){},1:function(a){return performance.mark("["+self.constructor.name+"][Message] - "+a)},2:function(a,c){c=[].slice.call(arguments,1);console.log.apply(console,[].concat(["[Message]"+("["+self.constructor.name+"]["+location.pathname+"]")+' - "'+a+'"'],c))}}[0];d.prototype={setupOutbound:function(a){this.id=g();this.type=a;this.sent=!1;this.recipient="*"},setupInbound:function(a){b("inbound");this.hasResponded=!1;this.setSourcePort(a.source||
a.target);this.event=a;Object.assign(this,a.data)},setSourcePort:function(a){b("set source",a.constructor.name);this.sourcePort=f(a,{ready:!0});return this},set:function(a,c){b("set",a,c);"object"==typeof a?Object.assign(this,a):this[a]=c;return this},serialize:function(){return{id:this.id,type:this.type,data:this.data,recipient:this.recipient,noRespond:this.noRespond}},preventDefault:function(){b("prevent default");this.defaultPrevented=!0},send:function(a){b("send",this.type);if(this.sent)throw m(1);
var c=this.serialize(),d=!this.noRespond;this.port=a?f(a):this.port;if(!this.port)throw m(3);d?(this.listen(this.port),this.setResponseTimeout()):this.deferred.resolve();this.port.postMessage(c,this.getTransfer());b("sent",c);return this.deferred.promise},setResponseTimeout:function(){!1!==this.timeout&&(this._timer=setTimeout(this.onTimeout,this.timeout||1E3))},clearResponseTimeout:function(){clearTimeout(this._timer)},getTransfer:function(){return this.transfer||this.event&&this.event.ports},onMessage:function(a){if(a.data.response&&
a.data.id===this.id&&!this.cancelled)this.onResponse(a)},onTimeout:function(){b("response timeout",this.type);this.silentTimeout||this.deferred.reject(m(4));this.teardown()},listen:function(a){b("add response listener",a);a=f(a);a.addListener(this.onMessage);this.listeners.push(a);return this},unlisten:function(){var a=this;b("remove response listeners");this.listeners.forEach(function(c){return c.removeListener(a.onMessage)});this.listeners=[]},cancel:function(){this.teardown();this.cancelled=!0;
this.emit("cancel")},teardown:function(){this.clearResponseTimeout();this.unlisten()},respond:function(a){function c(a){b("resolve",a);f({type:"resolve",value:a})}function d(a){a=a&&a.message||a;b("reject",a);f({type:"reject",value:a})}function f(a){e.response=a;e.sourcePort.postMessage({id:e.id,response:a},e.transfer);b("responded with:",a)}b("respond",a);if(this.hasResponded)throw m(2);if(this.sourcePort&&!this.noRespond){var e=this;this.hasResponded=!0;a instanceof Error&&d(a);Promise.resolve(a).then(c,
d).catch(d)}},forward:function(a){var c=this;b("forward");return this.set("silentTimeout",!0).send(a).then(function(a){return c.respond(a.value)})},onResponse:function(a){b("on response",a.data);var c=a.data.response,d="reject"==c.type?c.value:c;c.event=a;this.response=c;this.teardown();this.deferred[this.response.type](d);this.emit("response",c)}};n(d.prototype);k.prototype={listen:function(a){b("listen");a=f(a||self,{receiver:!0});if(!this.ports.has(a))return a.addListener(this.onMessage,this.listen),
this.ports.add(a),this},unlisten:function(){var a=this;b("unlisten");this.ports.forEach(function(c){c.removeListener(a.onMessage,a.unlisten)})},onMessage:function(a){if(a.data.id&&a.data.type&&this.isRecipient(a.data.recipient)&&(b("receiver on message",a.data),a=new d(a),this.emit("message",a),!a.defaultPrevented))try{this.emit(a.type,a)}catch(c){throw a.respond(c),c;}},isRecipient:function(a){return a==this.name||"*"==a||"*"==this.name},destroy:function(){this.unlisten();delete this.name;return this}};
n(k.prototype)},{"../emitter":3,"../utils":6,"./port-adaptors":5}],5:[function(e,l,h){function d(b){this.target=b}function k(b,a,c){b.addEventListener(a,c)}function m(b,a,c){a={data:a,source:self};c&&(a.ports=c);b.dispatchEvent(new MessageEvent("message",a))}var f=e("../utils").deferred;l.exports=function(b,a){if(!b)throw Error({1:"target is undefined"}[1]);if(b&&b.addListener)return b;var c=p[b.constructor.name];return c?c(b,a):new d(b,a)};var n=d.prototype={constructor:d,addListener:function(b){this.target.addEventListener("message",
b)},removeListener:function(b){this.target.removeEventListener("message",b)},postMessage:function(b,a){this.target.postMessage(b,a)}},p={HTMLIFrameElement:function(b){var a=g(b);return{addListener:function(a,b){window.addEventListener("message",a)},removeListener:function(a,b){window.removeEventListener("message",a)},postMessage:function(c,d){a.then(function(){return m(b.contentWindow,c,d)})}}},BroadcastChannel:function(b,a){function c(){var a=f();b.postMessage("ready?");k(b,"message",function t(c){"ready"==
c.data&&(b.removeEventListener("message",t),a.resolve())});return a.promise}var d=a&&a.receiver,e=a&&a.ready,e=e||d?Promise.resolve():c();d&&(b.postMessage("ready"),k(b,"message",function(a){"ready?"==a.data&&b.postMessage("ready")}));return{target:b,addListener:n.addListener,removeListener:n.removeListener,postMessage:function(a,c){e.then(function(){return b.postMessage(a,c)})}}},Window:function(b,a){var c=a&&a.ready||b===parent||b===self,c=c?Promise.resolve():g(b);return{addListener:function(a,
c){window.addEventListener("message",a)},removeListener:function(a,c){window.removeEventListener("message",a)},postMessage:function(a,d){c.then(function(){return m(b,a,d)})}}},SharedWorker:function(b){b.port.start();return new d(b.port)},SharedWorkerGlobalScope:function(){var b=[];return{postMessage:function(){},addListener:function(a,c){this.onconnect=function(a){a=a.ports[0];b.push(a);a.start();c(a)};self.addEventListener("connect",this.onconnect)},removeListener:function(a,c){self.removeEventListener("connect",
this.onconnect);b.forEach(function(a){a.close();c(a)})}}}},g=function(){if("undefined"!=typeof window){var b=window.opener||window.parent,a=new WeakSet;b!=self&&k(window,"DOMContentLoaded",function r(){window.removeEventListener("DOMContentLoaded",r);m(b,"load")});k(self,"message",function(b){return"load"==b.data&&a.add(b.source)});return function(b){var d=b.contentWindow||b;if(a.has(d)||d==window.parent)return Promise.resolve();var e=f();k(window,"message",function t(a){"load"==a.data&&a.source==
d&&(window.removeEventListener("message",t),e.resolve())});return e.promise}}}()},{"../utils":6}],6:[function(e,l,h){h.uuid=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(d){var e=16*Math.random()|0;return("x"==d?e:e&3|8).toString(16)})};h.deferred=function(){var d={};d.promise=new Promise(function(e,h){d.resolve=e;d.reject=h});return d}},{}]},{},[1])(1)});

View file

@ -0,0 +1 @@
<meta http-equiv="refresh" content="0; url=docs/out/index.html" />

View file

@ -0,0 +1,6 @@
module.exports = {
service: require('./src/service'),
client: require('./src/client'),
_message: require('./src/message')
};

View file

@ -0,0 +1,52 @@
{
"name": "bridge",
"version": "1.0.1",
"homepage": "https://github.com/gaia-components/bridge",
"license": "MIT",
"main": "index.js",
"scripts": {
"test": "./node_modules/karma/bin/karma start test/unit/karma.conf.js --single-run",
"test-dev": "./node_modules/karma/bin/karma start test/unit/karma.conf.js",
"docs": "rm -rf docs/bridge; babel src --out-dir src-es5; jsdoc -c docs/jsdoc.json; rm -rf src-es5",
"bundle": "browserify build/bridge.js -o bridge.js; npm run bundle-client; npm run bundle-service",
"bundle-client": "browserify build/client -s bridge -o client.js",
"bundle-service": "browserify build/service -s bridge -o service.js",
"compress": "ccjs bridge.js --language_in=ECMASCRIPT6 --language_out=ES5 > bridge.min.js; npm run compress-client; npm run compress-service",
"compress-client": "ccjs client.js --language_in=ECMASCRIPT6 --language_out=ES5 > client.min.js",
"compress-service": "ccjs service.js --language_in=ECMASCRIPT6 --language_out=ES5 > service.min.js",
"build": "npm run bundle; npm run compress",
"watch": "watchify build/bridge.js -o bridge.js",
"watch-client": "watchify src/client.js -s client -o client.js",
"watch-service": "watchify src/service.js -s service -o service.js",
"gh-pages": "git checkout gh-pages; git reset master --hard; git checkout -"
},
"repository": {
"type": "git",
"url": "git://github.com/gaia-components/bridge.git"
},
"bugs": {
"url": "https://github.com/gaia-components/bridge/issues"
},
"dependencies": {
"browserify": "^10.1.0",
"karma": "^0.12.31",
"karma-firefox-launcher": "^0.1.4",
"karma-mocha": "^0.1.10",
"karma-sinon-chai": "^0.3.0"
},
"devDependencies": {
"browserify": "^9.0.8",
"closurecompiler": "^1.5.2",
"jsdoc": "git+https://github.com/jsdoc3/jsdoc.git",
"karma": "^0.12.37",
"karma-firefox-launcher": "0.1.3",
"karma-mocha": "0.1.3",
"karma-sinon-chai": "0.1.6",
"mozilla-download": "^1.0.4",
"watchify": "^3.2.3"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
(function(p){"object"===typeof exports&&"undefined"!==typeof module?module.exports=p():"function"===typeof define&&define.amd?define([],p):("undefined"!==typeof window?window:"undefined"!==typeof global?global:"undefined"!==typeof self?self:this).bridge=p()})(function(){return function e(h,m,b){function l(f,c){if(!m[f]){if(!h[f]){var k="function"==typeof require&&require;if(!c&&k)return k(f,!0);if(n)return n(f,!0);k=Error("Cannot find module '"+f+"'");throw k.code="MODULE_NOT_FOUND",k;}k=m[f]={exports:{}};
h[f][0].call(k.exports,function(d){var a=h[f][1][d];return l(a?a:d)},k,k.exports,e,h,m,b)}return m[f].exports}for(var n="function"==typeof require&&require,g=0;g<b.length;g++)l(b[g]);return l}({1:[function(e,h,m){var b=h.exports=self.bridge||{};b.service=e("../src/service");"u"!="undefined"[0]?(void 0)([],function(){return b}):self.bridge=b},{"../src/service":5}],2:[function(e,h,m){function b(l){if(l)return Object.assign(l,b.prototype)}h.exports=b;b.prototype={on:function(b,e){this._callbacks||(this._callbacks=
{});this._callbacks[b]||(this._callbacks[b]=[]);this._callbacks[b].push(e);return this},off:function(b,e){if(this._callbacks)switch(arguments.length){case 0:this._callbacks={};break;case 1:delete this._callbacks[b];break;default:var g=this._callbacks[b];if(!g)return;var f=g.indexOf(e);~f&&g.splice(f,1)}return this},emit:function(b,e){if(this._callbacks)for(var g=this._callbacks[b]||[],g=g.concat(this._callbacks["*"]||[]),f=0;f<g.length;f++)g[f].call(this,e,b);return this}};e=b.prototype;e.off=e.off;
e.on=e.on},{}],3:[function(e,h,m){function b(a){this.cancelled=!1;this.listeners=[];this.deferred=c();this.onMessage=this.onMessage.bind(this);this.onTimeout=this.onTimeout.bind(this);"object"===typeof a?this.setupInbound(a):this.setupOutbound(a);d("initialized",a)}function l(a){this.name=a;this.ports=new Set;this.onMessage=this.onMessage.bind(this);this.listen=this.listen.bind(this);this.unlisten=this.unlisten.bind(this);d("receiver initialized",a)}function n(a,d){d=[].slice.call(arguments,1);return Error({1:".send() can only be called once",
2:"response already sent for this message",3:"a port must be defined",4:"timeout"}[a])}var g=e("./port-adaptors"),f=e("../emitter");e=e("../utils");var c=e.deferred,k=e.uuid;m=h.exports=function(a){return new b(a)};m.receiver=function(a,d){return new l(a,d)};m.Receiver=l;m.Message=b;var d={0:function(){},1:function(a){return performance.mark("["+self.constructor.name+"][Message] - "+a)},2:function(a,d){d=[].slice.call(arguments,1);console.log.apply(console,[].concat(["[Message]"+("["+self.constructor.name+
"]["+location.pathname+"]")+' - "'+a+'"'],d))}}[0];b.prototype={setupOutbound:function(a){this.id=k();this.type=a;this.sent=!1;this.recipient="*"},setupInbound:function(a){d("inbound");this.hasResponded=!1;this.setSourcePort(a.source||a.target);this.event=a;Object.assign(this,a.data)},setSourcePort:function(a){d("set source",a.constructor.name);this.sourcePort=g(a,{ready:!0});return this},set:function(a,c){d("set",a,c);"object"==typeof a?Object.assign(this,a):this[a]=c;return this},serialize:function(){return{id:this.id,
type:this.type,data:this.data,recipient:this.recipient,noRespond:this.noRespond}},preventDefault:function(){d("prevent default");this.defaultPrevented=!0},send:function(a){d("send",this.type);if(this.sent)throw n(1);var c=this.serialize(),b=!this.noRespond;this.port=a?g(a):this.port;if(!this.port)throw n(3);b?(this.listen(this.port),this.setResponseTimeout()):this.deferred.resolve();this.port.postMessage(c,this.getTransfer());d("sent",c);return this.deferred.promise},setResponseTimeout:function(){!1!==
this.timeout&&(this._timer=setTimeout(this.onTimeout,this.timeout||1E3))},clearResponseTimeout:function(){clearTimeout(this._timer)},getTransfer:function(){return this.transfer||this.event&&this.event.ports},onMessage:function(a){if(a.data.response&&a.data.id===this.id&&!this.cancelled)this.onResponse(a)},onTimeout:function(){d("response timeout",this.type);this.silentTimeout||this.deferred.reject(n(4));this.teardown()},listen:function(a){d("add response listener",a);a=g(a);a.addListener(this.onMessage);
this.listeners.push(a);return this},unlisten:function(){var a=this;d("remove response listeners");this.listeners.forEach(function(d){return d.removeListener(a.onMessage)});this.listeners=[]},cancel:function(){this.teardown();this.cancelled=!0;this.emit("cancel")},teardown:function(){this.clearResponseTimeout();this.unlisten()},respond:function(a){function c(a){d("resolve",a);k({type:"resolve",value:a})}function b(a){a=a&&a.message||a;d("reject",a);k({type:"reject",value:a})}function k(a){f.response=
a;f.sourcePort.postMessage({id:f.id,response:a},f.transfer);d("responded with:",a)}d("respond",a);if(this.hasResponded)throw n(2);if(this.sourcePort&&!this.noRespond){var f=this;this.hasResponded=!0;a instanceof Error&&b(a);Promise.resolve(a).then(c,b).catch(b)}},forward:function(a){var c=this;d("forward");return this.set("silentTimeout",!0).send(a).then(function(a){return c.respond(a.value)})},onResponse:function(a){d("on response",a.data);var c=a.data.response,b="reject"==c.type?c.value:c;c.event=
a;this.response=c;this.teardown();this.deferred[this.response.type](b);this.emit("response",c)}};f(b.prototype);l.prototype={listen:function(a){d("listen");a=g(a||self,{receiver:!0});if(!this.ports.has(a))return a.addListener(this.onMessage,this.listen),this.ports.add(a),this},unlisten:function(){var a=this;d("unlisten");this.ports.forEach(function(d){d.removeListener(a.onMessage,a.unlisten)})},onMessage:function(a){if(a.data.id&&a.data.type&&this.isRecipient(a.data.recipient)&&(d("receiver on message",
a.data),a=new b(a),this.emit("message",a),!a.defaultPrevented))try{this.emit(a.type,a)}catch(c){throw a.respond(c),c;}},isRecipient:function(a){return a==this.name||"*"==a||"*"==this.name},destroy:function(){this.unlisten();delete this.name;return this}};f(l.prototype)},{"../emitter":2,"../utils":6,"./port-adaptors":4}],4:[function(e,h,m){function b(d){this.target=d}function l(d,a,c){d.addEventListener(a,c)}function n(d,a,c){a={data:a,source:self};c&&(a.ports=c);d.dispatchEvent(new MessageEvent("message",
a))}var g=e("../utils").deferred;h.exports=function(d,a){if(!d)throw Error({1:"target is undefined"}[1]);if(d&&d.addListener)return d;var k=c[d.constructor.name];return k?k(d,a):new b(d,a)};var f=b.prototype={constructor:b,addListener:function(d){this.target.addEventListener("message",d)},removeListener:function(d){this.target.removeEventListener("message",d)},postMessage:function(d,a){this.target.postMessage(d,a)}},c={HTMLIFrameElement:function(d){var a=k(d);return{addListener:function(a,d){window.addEventListener("message",
a)},removeListener:function(a,d){window.removeEventListener("message",a)},postMessage:function(c,b){a.then(function(){return n(d.contentWindow,c,b)})}}},BroadcastChannel:function(d,a){function c(){var a=g();d.postMessage("ready?");l(d,"message",function q(c){"ready"==c.data&&(d.removeEventListener("message",q),a.resolve())});return a.promise}var b=a&&a.receiver,k=a&&a.ready,k=k||b?Promise.resolve():c();b&&(d.postMessage("ready"),l(d,"message",function(a){"ready?"==a.data&&d.postMessage("ready")}));
return{target:d,addListener:f.addListener,removeListener:f.removeListener,postMessage:function(a,c){k.then(function(){return d.postMessage(a,c)})}}},Window:function(c,a){var b=a&&a.ready||c===parent||c===self,b=b?Promise.resolve():k(c);return{addListener:function(a,c){window.addEventListener("message",a)},removeListener:function(a,c){window.removeEventListener("message",a)},postMessage:function(a,k){b.then(function(){return n(c,a,k)})}}},SharedWorker:function(c){c.port.start();return new b(c.port)},
SharedWorkerGlobalScope:function(){var c=[];return{postMessage:function(){},addListener:function(a,b){this.onconnect=function(a){a=a.ports[0];c.push(a);a.start();b(a)};self.addEventListener("connect",this.onconnect)},removeListener:function(a,b){self.removeEventListener("connect",this.onconnect);c.forEach(function(a){a.close();b(a)})}}}},k=function(){if("undefined"!=typeof window){var c=window.opener||window.parent,a=new WeakSet;c!=self&&l(window,"DOMContentLoaded",function r(){window.removeEventListener("DOMContentLoaded",
r);n(c,"load")});l(self,"message",function(c){return"load"==c.data&&a.add(c.source)});return function(c){var b=c.contentWindow||c;if(a.has(b)||b==window.parent)return Promise.resolve();var d=g();l(window,"message",function q(a){"load"==a.data&&a.source==b&&(window.removeEventListener("message",q),d.resolve())});return d.promise}}}()},{"../utils":6}],5:[function(e,h,m){function b(c){if(!(this instanceof b))return new b(c);g.Receiver.call(this,c);this.clients={};this.methods={};this.on("_disconnect",
this.onDisconnect.bind(this)).on("_connect",this.onConnect.bind(this)).on("_method",this.onMethod.bind(this)).on("_off",this.onOff.bind(this)).on("_on",this.onOn.bind(this));this.destroy=this.destroy.bind(this);f("initialized",c)}function l(c){var b=[].slice.call(arguments,1);return Error({4:'method "'+b[0]+"\" doesn't exist"}[c])}var n=e("./utils").uuid,g=e("./message");e=g.Receiver;h.exports=b;var f={0:function(){},1:function(c){return performance.mark("["+self.constructor.name+"][Service] - "+
c)},2:function(c,b){b=[].slice.call(arguments,1);console.log.apply(console,[].concat(["[Service]"+("["+self.constructor.name+"]["+location.pathname+"]")+' - "'+c+'"'],b))}}[0];b.prototype=Object.create(e.prototype);b.prototype.inWindow="Window"===constructor.name;b.prototype.method=function(c,b){this.methods[c]=b;return this};b.prototype.broadcast=function(c,b,d){var a=this;f("broadcast",c,b,d);this.eachClient(function(e){if(!d||~d.indexOf(e.id))f("broadcasting to",e.id),a.push(c,b,e.id,{noRespond:!0})});
return this};b.prototype.push=function(c,b,d,a){a=a&&a.noRespond;var f=this.getClient(d);return g("_push").set({recipient:d,noRespond:a,data:{type:c,data:b}}).send(f.port)};b.prototype.eachClient=function(c){for(var b in this.clients)c(this.clients[b])};b.prototype.getClient=function(c){return this.clients[c]};b.prototype.onConnect=function(c){f("connection attempt",c.data,this.name);var b=c.data,d=b.clientId;d&&b.service===this.name&&!this.clients[d]&&(this.emit("before-connect",c),c.defaultPrevented||
(this.upgradeChannel(c),this.addClient(d,c.sourcePort),c.respond(),this.emit("connected",d),f("connected",d)))};b.prototype.upgradeChannel=function(c){if(!this.inWindow||"Window"!==c.data.originEnv){var b=c.event.ports;if(b=b&&b[0])c.setSourcePort(b),this.listen(b),b.start();f("channel upgraded")}};b.prototype.onDisconnect=function(c){var b=this.clients[c.data];b&&(this.emit("before-disconnect",c),c.defaultPrevented||(this.removeClient(b.id),c.respond(),this.emit("disconnected",b.id),f("disconnected",
b.id)))};b.prototype.onMethod=function(c){f("on method",c.data);this.emit("before-method",c);if(!c.defaultPrevented){var b=c.data,d=b.name,a,e=this.methods[d];if(!e)throw l(4,d);try{a=e.apply(this,b.args)}catch(g){a=g}c.respond(a)}};b.prototype.onOn=function(b){f("on on",b.data);this.emit("on",b.data)};b.prototype.onOff=function(b){f("on off");this.emit("off",b.data)};b.prototype.addClient=function(b,f){this.clients[b]={id:b,port:f}};b.prototype.removeClient=function(b){delete this.clients[b]};b.prototype.plugin=
function(b){b(this,{uuid:n});return this};b.prototype.disconnect=function(b){this.removeClient(b.id);g("disconnect").set({recipient:b.id,noRespond:!0}).send(b.port)};b.prototype.destroy=function(){delete this.clients;this.unlisten();this.off()};h=b.prototype;h.broadcast=h.broadcast;h.destroy=h.destroy;h.method=h.method;h.plugin=h.plugin},{"./message":3,"./utils":6}],6:[function(e,h,m){m.uuid=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(b){var e=16*Math.random()|
0;return("x"==b?e:e&3|8).toString(16)})};m.deferred=function(){var b={};b.promise=new Promise(function(e,h){b.resolve=e;b.reject=h});return b}},{}]},{},[1])(1)});

View file

@ -0,0 +1,449 @@
'use strict';
/**
* Dependencies
* @ignore
*/
var createPort = require('./message/port-adaptors');
var Emitter = require('./emitter');
var message = require('./message');
var uuid = require('./utils').uuid;
/**
* Exports
* @ignore
*/
module.exports = Client;
/**
* Mini Logger
*
* 0: off
* 1: performance
* 2: console.log
*
* @type {Function}
* @private
*/
var debug = {
0: () => {},
1: arg => performance.mark(`[${self.constructor.name}][Client] - ${arg}`),
2: (arg1, ...args) => {
var type = `[${self.constructor.name}][${location.pathname}]`;
console.log(`[Client]${type} - "${arg1}"`, ...args);
}
}[0];
/**
* The type environment.
* @type {String}
*/
var env = constructor.name
/**
* A Client is a remote interface
* to a Service within a given endpoint.
*
* See {@tutorial What's an endpoint?}
* for more information on 'endpoints'.
*
* @example
*
* var endpoint = document.querySelector('iframe');
* var client = bridge.client('my-service', endpoint);
*
* @constructor
* @param {String} service The service name to connect to
* @param {(Iframe|Worker|MessagePort|BroadcastChannel|Window)} endpoint
* @param {Number} [timeout] Override default response timeout
* The context/thread this service can be found in.
* @public
*/
function Client(service, endpoint, timeout) {
if (!(this instanceof Client)) return new Client(service, endpoint, timeout);
// Parameters can be passed as single object
if (typeof service == 'object') {
endpoint = service.endpoint;
timeout = service.timeout;
service = service.service;
}
this.id = uuid();
this.service = service;
this.timeout = timeout;
// Keep a reference to the original endpoint
// so that it's not garbage collected (Workers)
this.endpoint = endpoint || this.endpoint;
if (!this.endpoint) throw error(1);
this.setPort(this.endpoint);
this.pending = new Set();
this.receiver = message.receiver(this.id)
.on('_push', this.onPush.bind(this));
debug('initialized', service);
}
Client.prototype = {
/**
* Connect with the Service. Called
* automatically internally, so
* only required if you have
* perposely called .disconnect().
*
* @public
*/
connect() {
debug('connect');
if (this.connected) return this.connected;
debug('connecting...', this.service);
var mc = new MessageChannel();
this.channel = mc.port1;
this.channel.start();
var data = {
clientId: this.id,
service: this.service,
originEnv: env
};
return this.connected = this.message('_connect')
.set('transfer', [mc.port2])
.set('data', data)
.listen(mc.port1)
.send()
.then(response => {
debug('connected', response);
// Check if the response came back on
// the MessageChannel. If it did then
// update the endpoint so that all
// subsequent messaging uses this channel.
var usingChannel = response.event.target === this.channel;
if (usingChannel) this.setPort(this.channel);
else {
this.channel.close();
delete this.channel;
}
// Begin listening so that Clients can
// respond to service pushed messages
this.receiver.listen(this.port);
})
// In the event of message timeout we
// upgrade the message to something more
// informative. console.error() is used to
// makesure the message is seen even when
// the user hasn't registered a .catch() handler.
.catch(err => {
var msg = err && err.message;
if (msg == 'timeout') {
err = error(2, this.service);
console.error(err.message);
}
throw err;
});
},
/**
* Disconnect from the `Service`.
*
* @public
*/
disconnect(options) {
if (!this.connected) return Promise.resolve();
debug('disconnecting ...');
var config = {
noRespond: options && options.noRespond,
data: this.id
};
this.cancelPending();
return this.message('_disconnect')
.set(config)
.send()
.then(() => this.onDisconnected());
},
/**
* Call a method on the connected Service.
*
* @example
*
* client.method('greet', 'wilson').then(result => {
* console.log(result); //=> 'hello wilson'
* });
*
* // my-service.js:
*
* service.method('greet', name => {
* return 'hello ' + name;
* });
*
* @param {String} name The method name
* @param {...*} [args] Arguments to send
* @return {Promise}
*/
method(name, ...args) {
return this.connect()
.then(() => {
debug('method', name);
return this.message('_method')
.set({
recipient: this.service,
data: {
name: name,
args: args
}
})
.send();
})
// Only send back the response value
.then(response => response.value)
// In the event of message timeout we
// upgrade the message to something more
// informative. console.error() is used to
// make sure the message is seen even when
// the user hasn't registered a .catch() handler.
.catch(err => {
var msg = err && err.message;
if (msg == 'timeout') {
err = error(3, name);
console.error(err.message);
}
throw err;
});
},
/**
* Use a plugin with this Client.
* See {@tutorial Writing plugins}.
*
* @example
*
* client.plugin(megaPlugin);
*
* @param {Function} fn The plugin
* @return {this} for chaining
* @public
*/
plugin(fn) {
fn(this, {
'Emitter': Emitter,
'uuid': uuid
});
return this;
},
/**
* A wrapper around Message that
* ensures pending messages are
* noted and the Client's endpoint
* is predefined.
*
* @param {String} type The message type
* @return {Message}
* @private
*/
message(type) {
debug('create message', type);
var msg = message(type)
.set('port', this.port)
.set('timeout', this.timeout)
.on('response', () => this.pending.delete(msg))
.on('cancel', () => this.pending.delete(msg));
this.pending.add(msg);
return msg;
},
/**
* Cancel any message that we have
* not recieved a response from yet.
*
* @private
*/
cancelPending() {
debug('cancel pending');
this.pending.forEach(msg => { msg.cancel();});
this.pending.clear();
},
/**
* Returns a Promise that resolves
* once all pending messages have
* responded.
*
* @private
* @return {Promise}
*/
pendingResponded() {
var responded = [];
this.pending.forEach(msg => responded.push(msg.responded));
return Promise.all(responded);
},
/**
* Emits a event when a 'push' Message
* is recieved from the Service.
*
* @private
* @param {Message} message The pushed message
*/
onPush(message) {
debug('on push', message.data);
this._emit(message.data.type, message.data.data);
},
// Needs testing!
onDisconnected() {
delete this.connected;
this.pendingResponded().then(() => {
debug('disconnected');
if (this.channel) this.channel.close();
this._emit('disconnected');
});
},
/**
* Set the port which all messages
* will be sent over. This can differ
* to the endpoint if we successfully
* upgrade transport to MessageChannel.
*
* @param {(Iframe|Worker|MessagePort|BroadcastChannel|Window)} endpoint
*/
setPort(endpoint) {
debug('set port');
this.port = createPort(endpoint);
},
/**
* Destroy the Client. Waits from all
* pending Messages to have responded.
*
* @example
*
* client.destroy().then(() => ...);
*
* @public
* @return {Promise}
*/
destroy: function() {
return this.disconnect()
.then(() => {
if (this.destroyed) return;
debug('destroy');
this.destroyed = true;
this.receiver.destroy();
this._off();
// Wipe references
this.port
= this.endpoint
= this.receiver
= null;
});
},
_on: Emitter.prototype.on,
_off: Emitter.prototype.off,
_emit: Emitter.prototype.emit
};
/**
* Listen to a Service .broadcast() or .push().
*
* Services get notified whenever a Client
* starts listening to a particular event.
*
* @example
*
* client
* .on('importantevent', data => ...)
* .on('thingchanged', thing => ...);
*
* @param {String} name The event name
* @param {Function} fn Callback function
* @return {this} for chaining
* @public
*/
Client.prototype.on = function(name, fn) {
this.connect().then(() => {
debug('bind on', name);
Emitter.prototype.on.call(this, name, fn);
this.message('_on')
.set('noRespond', true)
.set('data', {
name: name,
clientId: this.id
})
.send(this.port);
});
return this;
};
/**
* Unlisten to a Service event.
*
* @example
*
* client
* .off('importantevent') // remove all
* .off('thingchanged', onThingChanged); // remove one
*
* @this Client
* @param {String} name The event name
* @param {Function} fn Callback function
* @return {this} for chaining
* @public
*/
Client.prototype.off = function(name, fn) {
this.connect().then(() => {
Emitter.prototype.off.call(this, name, fn);
this.message('_off')
.set('noRespond', true)
.set('data', {
name: name,
clientId: this.id
})
.send(this.port);
});
return this;
};
/**
* Creates new `Error` from registery.
*
* @param {Number} id Error Id
* @return {Error}
* @private
*/
function error(id, ...args) {
/*jshint maxlen:false*/
var help = 'Either the target endpoint is not alive or the Service is not `.listen()`ing.';
return new Error({
1: 'an endpoint must be defined',
2: `Unable to establish a connection with "${args[0]}". ${help}`,
3: `Method "${args[0]}" didn't get a response. ${help}`
}[id]);
}

View file

@ -0,0 +1,104 @@
'use strict';
/**
* Exports
* @ignore
*/
module.exports = Emitter;
/**
* Simple logger
*
* @type {Function}
* @private
*/
var debug = 0 ? console.log.bind(console, '[Emitter]') : () => {};
/**
* Create new `Emitter`
*
* @class Emitter
*/
function Emitter(host) {
if (host) return Object.assign(host, Emitter.prototype);
}
Emitter.prototype = {
/**
* Add an event listener.
*
* It is possible to subscript to * events.
*
* @param {String} type
* @param {Function} callback
* @return {this} for chaining
*/
on: function(type, callback) {
debug('on', type, callback);
if (!this._callbacks) this._callbacks = {};
if (!this._callbacks[type]) this._callbacks[type] = [];
this._callbacks[type].push(callback);
return this;
},
/**
* Remove an event listener.
*
* @example
*
* emitter.off('name', fn); // remove one callback
* emitter.off('name'); // remove all callbacks for 'name'
* emitter.off(); // remove all callbacks
*
* @param {String} [type]
* @param {Function} [callback]
* @return {this} for chaining
*/
off: function(type, callback) {
debug('off', type, callback);
if (this._callbacks) {
switch (arguments.length) {
case 0: this._callbacks = {}; break;
case 1: delete this._callbacks[type]; break;
default:
var typeListeners = this._callbacks[type];
if (!typeListeners) return;
var i = typeListeners.indexOf(callback);
if (~i) typeListeners.splice(i, 1);
}
}
return this;
},
/**
* Emit an event.
*
* @example
*
* emitter.emit('name', { some: 'data' });
*
* @param {String} type
* @param {*} [data]
* @return {this} for chaining
*/
emit: function(type, data) {
debug('emit', type, data);
if (this._callbacks) {
var fns = this._callbacks[type] || [];
fns = fns.concat(this._callbacks['*'] || []);
for (var i = 0; i < fns.length; i++) fns[i].call(this, data, type);
}
return this;
}
};
var p = Emitter.prototype;
p['off'] = p.off;
p['on'] = p.on;

View file

@ -0,0 +1,441 @@
'use strict';
/**
* Dependencies
* @ignore
*/
var createPort = require('./port-adaptors');
var Emitter = require('../emitter');
var utils = require('../utils');
var defer = utils.deferred;
var uuid = utils.uuid;
/**
* Exports
* @ignore
*/
exports = module.exports = type => new Message(type);
exports.receiver = (id, n) => new Receiver(id, n);
exports.Receiver = Receiver;
exports.Message = Message;
/**
* Mini Logger
*
* 0: off
* 1: performance
* 2: console.log
*
* @type {Function}
* @private
*/
var debug = {
0: () => {},
1: arg => performance.mark(`[${self.constructor.name}][Message] - ${arg}`),
2: (arg1, ...args) => {
var type = `[${self.constructor.name}][${location.pathname}]`;
console.log(`[Message]${type} - "${arg1}"`, ...args);
}
}[0];
/**
* Default response timeout.
* @type {Number}
* @private
*/
var TIMEOUT = 1000;
/**
* Initialize a new `Message`
*
* @class Message
* @borrows Emitter#on as #on
* @borrows Emitter#off as #off
* @borrows Emitter#emit as #emit
* @param {String} type Message type
*/
function Message(type) {
this.cancelled = false;
this.listeners = [];
this.deferred = defer();
this.onMessage = this.onMessage.bind(this);
this.onTimeout = this.onTimeout.bind(this);
if (typeof type === 'object') this.setupInbound(type);
else this.setupOutbound(type);
debug('initialized', type);
}
Message.prototype = {
setupOutbound(type) {
this.id = uuid();
this.type = type;
this.sent = false;
this.recipient = '*';
},
setupInbound (e) {
debug('inbound');
this.hasResponded = false;
this.setSourcePort(e.source || e.target);
// Keep a reference to the MessageEvent
this.event = e;
// Mixin the properties of the original message
Object.assign(this, e.data);
},
setSourcePort(endpoint) {
debug('set source', endpoint.constructor.name);
this.sourcePort = createPort(endpoint, { ready: true });
return this;
},
set(key, value) {
debug('set', key, value);
if (typeof key == 'object') Object.assign(this, key);
else this[key] = value;
return this;
},
serialize() {
return {
id: this.id,
type: this.type,
data: this.data,
recipient: this.recipient,
noRespond: this.noRespond
};
},
preventDefault() {
debug('prevent default');
this.defaultPrevented = true;
},
/**
* Send the message to an endpoint.
*
* @param {(Iframe|Window|Worker|MessagePort)} endpoint
* @return {Promise}
*/
send(endpoint) {
debug('send', this.type);
if (this.sent) throw error(1);
var serialized = this.serialize();
var expectsResponse = !this.noRespond;
// A port is resolved from either a predefined
// port, or an endpoint given as first argument
this.port = endpoint ? createPort(endpoint) : this.port;
if (!this.port) throw error(3);
// If we're expecting a response listen
// on the port else resolve promise instantly
if (expectsResponse) {
this.listen(this.port);
this.setResponseTimeout();
} else this.deferred.resolve();
this.port.postMessage(serialized, this.getTransfer());
debug('sent', serialized);
return this.deferred.promise;
},
/**
* Set the response timeout.
*
* When set to `false` no timeout
* is installed.
*
* @private
*/
setResponseTimeout() {
if (this.timeout === false) return;
var ms = this.timeout || TIMEOUT;
this._timer = setTimeout(this.onTimeout, ms);
},
/**
* Clear the response timeout.
*
* @private
*/
clearResponseTimeout() {
clearTimeout(this._timer);
},
getTransfer() {
return this.transfer || this.event && this.event.ports;
},
onMessage(e) {
var valid = !!e.data.response
&& e.data.id === this.id
&& !this.cancelled;
if (valid) this.onResponse(e);
},
onTimeout() {
debug('response timeout', this.type);
if (!this.silentTimeout) this.deferred.reject(error(4));
this.teardown();
},
listen(thing) {
debug('add response listener', thing);
var port = createPort(thing);
port.addListener(this.onMessage);
this.listeners.push(port);
return this;
},
unlisten() {
debug('remove response listeners');
this.listeners.forEach(port => port.removeListener(this.onMessage));
this.listeners = [];
},
/**
* Cancel a pending Message.
*
* @example
*
* var msg = message('foo')
*
* msg.send(new Worker('my-worker.js'))
* .then(response => {
* // this will never run
* })
*
* msg.cancel();
*
* @public
*/
cancel() {
this.teardown();
this.cancelled = true;
this.emit('cancel');
},
teardown() {
this.clearResponseTimeout();
this.unlisten();
},
/**
* Respond to a message.
*
* @example
*
* receiver.on('hello', message => {
* message.respond('world');
* });
*
* @public
* @param {*} [result] Data to send back with the response
*/
respond(result) {
debug('respond', result);
if (this.hasResponded) throw error(2);
if (!this.sourcePort) return;
if (this.noRespond) return;
var self = this;
this.hasResponded = true;
// Repsond with rejection when result is an `Error`
if (result instanceof Error) reject(result);
// Call the handler and make
// sure return value is a promise.
// If the returned value is unclonable
// then the send() method will throw,
// the .catch() will reject in this case.
Promise.resolve(result)
.then(resolve, reject)
.catch(reject);
function resolve(value) {
debug('resolve', value);
respond({
type: 'resolve',
value: value
});
}
function reject(err) {
var msg = err && err.message || err;
debug('reject', msg);
respond({
type: 'reject',
value: msg
});
}
function respond(response) {
self.response = response;
self.sourcePort.postMessage({
id: self.id,
response: response
}, self.transfer);
debug('responded with:', response);
}
},
/**
* Forward a `Message` onto another endpoint.
*
* The `silentTrue` option prevents the
* message request timing out should
* the response come back via an
* alternative route.
*
* @param {(HTMLIframeElement|MessagePort|Window)} endpoint
* @public
*/
forward(endpoint) {
debug('forward');
return this
.set('silentTimeout', true)
.send(endpoint)
.then(result => this.respond(result.value));
},
onResponse(e) {
debug('on response', e.data);
var response = e.data.response;
var type = response.type;
var value = type == 'reject'
? response.value
: response;
response.event = e;
this.response = response;
this.teardown();
this.deferred[this.response.type](value);
this.emit('response', response);
}
};
// Mixin Emitter methods
Emitter(Message.prototype);
/**
* Initialize a new `Receiver`.
*
* @class Receiver
* @extends Emitter
* @param {String} name - corresponds to `Message.recipient`
*/
function Receiver(name) {
this.name = name;
this.ports = new Set();
this.onMessage = this.onMessage.bind(this);
this['listen'] = this['listen'].bind(this);
this['unlisten'] = this['unlisten'].bind(this);
debug('receiver initialized', name);
}
Receiver.prototype = {
/**
* Begin listening for inbound messages.
*
* @example
*
* // When no arguments are given
* // messages will be listened for
* // on the default global scope
* .listen();
*
* // When an endpoint is out of reach
* // BroadcastChannel can be used.
* .listen(new BroadcastChannel('foo'));
*
* @param {(HTMLIframeElement|Worker|MessagePort|
* BroadcastChannel|Window|Object)} [thing]
* @public
*/
listen(thing) {
debug('listen');
var _port = createPort(thing || self, { receiver: true });
if (this.ports.has(_port)) return;
_port.addListener(this.onMessage, this.listen);
this.ports.add(_port);
return this;
},
/**
* Stop listening for inbound messages
* on all endpoints listened to prior.
*
* @public
*/
unlisten() {
debug('unlisten');
this.ports.forEach(port => {
port.removeListener(this.onMessage, this.unlisten);
});
},
/**
* Callback to handle inbound messages.
* @param {MessageEvent} e
* @private
*/
onMessage(e) {
if (!e.data.id) return;
if (!e.data.type) return;
if (!this.isRecipient(e.data.recipient)) return;
debug('receiver on message', e.data);
var message = new Message(e);
// Before hook
this.emit('message', message);
if (message.defaultPrevented) return;
try { this.emit(message.type, message); }
catch (e) {
message.respond(e);
throw e;
}
},
isRecipient(recipient) {
return recipient == this.name
|| recipient == '*'
|| this.name == '*';
},
destroy: function() {
this.unlisten();
delete this.name;
return this;
}
};
// Mixin Emitter methods
Emitter(Receiver.prototype);
/**
* Creates new `Error` from registry.
*
* @param {Number} id Error Id
* @return {Error}
* @private
*/
function error(id, ...args) {
return new Error({
1: '.send() can only be called once',
2: 'response already sent for this message',
3: 'a port must be defined',
4: 'timeout'
}[id]);
}

View file

@ -0,0 +1,282 @@
'use strict';
/**
* Dependencies
* @ignore
*/
var deferred = require('../utils').deferred;
/**
* Message event name
* @type {String}
*/
const MSG = 'message';
/**
* Mini Logger
* @type {Function}
* @private
*/
var debug = 0 ? function(arg1, ...args) {
var type = `[${self.constructor.name}][${location.pathname}]`;
console.log(`[PortAdaptor]${type} - "${arg1}"`, ...args);
} : () => {};
/**
* Creates a bridge.js port abstraction
* with a consistent interface.
*
* @param {Object} target
* @param {Object} options
* @return {PortAdaptor}
*/
module.exports = function create(target, options) {
if (!target) throw error(1);
if (isEndpoint(target)) return target;
var type = target.constructor.name;
var CustomAdaptor = adaptors[type];
debug('creating port adaptor for', type);
if (CustomAdaptor) return CustomAdaptor(target, options);
return new PortAdaptor(target, options);
};
/**
* The default adaptor.
* @private
*/
function PortAdaptor(target) {
debug('PortAdaptor');
this.target = target;
}
var PortAdaptorProto = PortAdaptor.prototype = {
constructor: PortAdaptor,
addListener(callback) { on(this.target, MSG, callback); },
removeListener(callback) { off(this.target, MSG, callback); },
postMessage(data, transfer) { this.target.postMessage(data, transfer); }
};
/**
* A registry of specific adaptors
* for when the default PortAdaptor
* is not suitable.
*
* @type {Object}
*/
var adaptors = {
/**
* Create an HTMLIframeElement PortAdaptor.
*
* @param {HTMLIframeElement} iframe
*/
'HTMLIFrameElement': function(iframe) {
debug('HTMLIFrameElement');
var ready = windowReady(iframe);
return {
addListener(callback, listen) { on(window, MSG, callback); },
removeListener(callback, listen) { off(window, MSG, callback); },
postMessage(data, transfer) {
ready.then(() => postMessageSync(iframe.contentWindow, data, transfer));
}
};
},
/**
* Create a BroadcastChannel port-adaptor.
*
* @param {Object} channel
* @param {[type]} options [description]
*/
'BroadcastChannel': function(channel, options) {
debug('BroadcastChannel', channel.name);
var receiver = options && options.receiver;
var ready = options && options.ready;
var sendReady = () => {
channel.postMessage('ready');
debug('sent ready');
};
ready = ready || receiver
? Promise.resolve()
: setupSender();
if (receiver) {
sendReady();
on(channel, MSG, e => {
if (e.data != 'ready?') return;
sendReady();
});
}
function setupSender() {
debug('setup sender');
var promise = deferred();
channel.postMessage('ready?');
on(channel, MSG, function fn(e) {
if (e.data != 'ready') return;
off(channel, MSG, fn);
debug('BroadcastChannel: ready');
promise.resolve();
});
return promise.promise;
}
return {
target: channel,
addListener: PortAdaptorProto.addListener,
removeListener: PortAdaptorProto.removeListener,
postMessage(data, transfer) {
ready.then(() => channel.postMessage(data, transfer));
}
};
},
'Window': function(win, options) {
debug('Window');
var ready = options && options.ready
|| win === parent // parent always ready
|| win === self; // self always ready
ready = ready ? Promise.resolve() : windowReady(win);
return {
addListener(callback, listen) { on(window, MSG, callback); },
removeListener(callback, listen) { off(window, MSG, callback); },
postMessage(data, transfer) {
ready.then(() => postMessageSync(win, data, transfer));
}
};
},
'SharedWorker': function(worker) {
worker.port.start();
return new PortAdaptor(worker.port);
},
'SharedWorkerGlobalScope': function() {
var ports = [];
return {
postMessage() {}, // noop
addListener(callback, listen) {
this.onconnect = e => {
var port = e.ports[0];
ports.push(port);
port.start();
listen(port);
};
on(self, 'connect', this.onconnect);
},
removeListener(callback, unlisten) {
off(self, 'connect', this.onconnect);
ports.forEach(port => {
port.close();
unlisten(port);
});
}
};
}
};
/**
* Return a Promise that resolves
* when a Window is ready to start
* recieving messages.
*
* @param {Window} target
* @return {Promise}
*/
var windowReady = (function() {
if (typeof window == 'undefined') return;
var parent = window.opener || window.parent;
var domReady = 'DOMContentLoaded';
var windows = new WeakSet();
// Side B: Dispatches 'load'
// from the child window
if (parent != self) {
on(window, domReady, function fn() {
off(window, domReady, fn);
postMessageSync(parent, 'load');
});
}
// Side A: Listens for 'ready' in the parent window
on(self, 'message', e => e.data == 'load' && windows.add(e.source));
return target => {
var win = target.contentWindow || target;
// Ready if the target has previously announces itself ready
if (windows.has(win)) return Promise.resolve();
// Ready if the target is the parent window
if (win == window.parent) return Promise.resolve();
var def = deferred();
debug('waiting for Window to be ready ...');
on(window, 'message', function fn(e) {
if (e.data == 'load' && e.source == win) {
debug('Window ready');
off(window, 'message', fn);
def.resolve();
}
});
return def.promise;
};
})();
/**
* Utils
* @ignore
*/
function isEndpoint(thing) {
return !!(thing && thing.addListener);
}
// Shorthand
function on(target, name, fn) { target.addEventListener(name, fn); }
function off(target, name, fn) { target.removeEventListener(name, fn); }
/**
* Dispatches syncronous 'message'
* event on a Window.
*
* We use this because standard
* window.postMessage() gets blocked
* until the main-thread is free.
*
* @param {Window} win
* @param {*} data
* @private
*/
function postMessageSync(win, data, transfer) { try { void win.document; } catch (_) { win.postMessage(data, location.origin, transfer || []); return; }
var event = {
data: data,
source: self
};
if (transfer) event.ports = transfer;
win.dispatchEvent(new MessageEvent('message', event));
}
/**
* Creates new `Error` from registery.
*
* @param {Number} id Error Id
* @return {Error}
* @private
*/
function error(id) {
return new Error({
1: 'target is undefined'
}[id]);
}

View file

@ -0,0 +1,9 @@
!((define)=>{define((require,exports,module)=>{
module.exports = function() {
};
});})((typeof define)[0]=='f'&&define.amd?define:((n,w)=>{return(typeof
module)[0]=='o'?c=>{c(require,exports,module);}:(c)=>{var m={exports:{}};
c(n=>w[n],m.exports,m);w[n]=m.exports;};})('contractsPlugin',this));

View file

@ -0,0 +1,197 @@
!((define)=>{define((require,exports,module)=>{
/**
* Mini Logger
*
* @type {Function}
*/
var debug = 0 ? function(arg1, ...args) {
var type = `[${self.constructor.name}][${location.pathname}]`;
console.log(`[ClientStream]${type} - "${arg1}"`, ...args);
} : () => {};
/**
* Exports
*/
module.exports = (client, utils) => {
client._activeStreams = [];
client.on('streamevent', onStreamEvent);
client.stream = function(name) {
debug('stream open', name);
var args = [].slice.call(arguments, 1);
var stream = new ClientStream(client, utils);
var self = this;
this.method('_stream', name, stream.id, this.id, args)
.then(() => {
debug('stream connected', name);
stream._connected.resolve();
})
.catch(err => {
debug('error', err);
onStreamEvent({
type: 'abort',
id: stream.id,
data: err
});
});
this._activeStreams[stream.id] = stream;
stream.emitter.on('cancel', onEnd);
stream.emitter.on('abort', onEnd);
stream.emitter.on('close', onEnd);
function onEnd() {
debug('end');
delete self._activeStreams[stream.id];
stream.destroy();
}
return stream;
};
/**
* Called every time the service calls
* write/abort/close on the ServiceStream
*
* @param {Object} broadcast
* @param {String} broadcast.id Stream ID
* @param {String} broadcast.type Event type ('write', 'abort' or 'close')
* @private
*/
function onStreamEvent(data) {
debug('stream event', data.type);
var stream = client._activeStreams[data.streamId];
stream.emitter.emit(data.type, data.data);
}
};
/**
* Readable stream instance returned by
* a `client.stream('methodName')` call.
*
* @param {Object} options
* @param {String} options.id Stream Id, used to match client/service streams
* @param {Client} options.client Client instance
*/
function ClientStream(client, utils) {
this.id = utils.uuid();
this.client = client;
this.emitter = new utils.Emitter();
this.emitter.on('close', this.onClose.bind(this));
this.emitter.on('abort', this.onAbort.bind(this));
this._connected = defer();
this.connected = this._connected.promise;
this._closed = defer();
this.closed = this._closed.promise;
debug('initialized');
}
/**
* Prototype assigned to variable
* to improve compression.
*
* @type {Object}
*/
ClientStream.prototype = {
/**
* Add a listener that will be called
* every time the service broadcasts
* a new chunk of data.
*
* @param {Function} callback
*/
listen(callback) {
debug('listen', callback);
this.emitter.on('write', callback);
},
/**
* Removes 'data' listener
*
* @param {Function} callback
*/
unlisten(callback) {
debug('unlisten', callback);
this.emitter.off('write', callback);
},
/**
* Notify the service that
* action should be canceled
*
* @param {*} [reason] Optional data to be sent to service.
*/
cancel(reason) {
debug('cancel', reason);
return this.client.method('_streamcancel', this.id, reason)
.then(result => {
debug('cancelled', result);
this.emitter.emit('cancel');
return result;
})
.catch(e => {
this.emitter.emit('cancel');
});
},
/**
* Used internally by Client when
* it receives an 'abort' event
* from the service.
*
* @private
*/
onAbort(reason) {
debug('abort', reason);
this._closed.reject(reason);
},
/**
* Used internally by Client when
* it receives a 'close' event
* from the service.
*
* @private
*/
onClose() {
debug('close');
this._closed.resolve();
},
destroy() {
debug('destroy');
this.emitter.off();
}
};
/**
* Utils
*/
function defer() {
var result = {};
result.promise = new Promise((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
}
});})((typeof define)[0]=='f'&&define.amd?define:((n,w)=>{return(typeof
module)[0]=='o'?c=>{c(require,exports,module);}:(c)=>{var m={exports:{}};
c(n=>w[n],m.exports,m);w[n]=m.exports;};})('streamClient',this));

View file

@ -0,0 +1,206 @@
!((define)=>{define((require,exports,module)=>{
/**
* Mini Logger
*
* @type {Function}
*/
var debug = 0 ? function(arg1, ...args) {
var type = `[${self.constructor.name}][${location.pathname}]`;
console.log(`[ServiceStream]${type} - "${arg1}"`, ...args);
} : () => {};
module.exports = function(service) {
debug('attaching plugin');
service
.method('_stream', onStream)
.method('_streamcancel', onStreamCancel);
service._streamHandlers = {};
service._activeStreams = [];
service.stream = function(name, fn) {
debug('register stream', name);
this._streamHandlers[name] = fn;
return this;
};
function onStream(name, id, clientId, args) {
debug('on stream open', name);
var e = {
preventDefault: () => e.defaultPrevented = true,
arguments: arguments
};
// Allow other plugins to override default behaviour
service.emit('before-stream', e);
if (e.defaultPrevented) return;
var fn = service._streamHandlers[name];
if (!fn) throw error(1, name);
var stream = new ServiceStream(id, service, clientId);
service._activeStreams[id] = stream;
// Always pass stream object as first
// argument to simplify the process
fn.apply(service, [stream].concat(args));
}
function onStreamCancel(id, reason) {
debug('stream cancel');
var stream = service._activeStreams[id];
delete service._activeStreams[id];
return stream.cancel(reason);
}
};
/**
* Writable Stream instance passed to the
* `service.stream` implementation
*
* @param {Object} options
* @param {String} options.id Stream ID used to sync client and service streams
* @param {BroadcastChannel} options.channel Channel used to postMessage
* @param {String} options.serviceId ID of the service
* @param {String} options.clientId ID of client that should receive message
*/
function ServiceStream(streamId, service, clientId) {
this.id = streamId;
this.service = service;
this.clientId = clientId;
this.writable = true;
debug('initialized');
}
/**
* Prototype assigned to variable
* to improve compression.
*
* @type {Object}
*/
var ServiceStreamPrototype = ServiceStream.prototype;
/**
* Services that allows clients to
* cancel the operation before it's
* complete should override the
* `stream.cancel` method.
*
* @param {*} [reason] Data sent from client about the cancellation
* @returns {(Promise|*)}
*/
ServiceStreamPrototype.cancel = function(reason) {
var err = new TypeError('service should implement stream.cancel()');
return Promise.reject(err);
};
/**
* Signal to client that action was
* aborted during the process, this
* should be used as a way to
* communicate errors.
*
* @param {*} [data] Reason of failure
* @returns {Promise}
* @public
*/
ServiceStreamPrototype.abort = function(data) {
debug('abort', data);
this._post('abort', data);
this.writable = false;
};
/**
* Sends a chunk of data to the client.
*
* @param {*} data Chunk of data to be sent to client.
* @returns {Promise}
* @public
*/
ServiceStreamPrototype.write = function(data) {
debug('write', data);
this._post('write', data);
};
/**
* Closes the stream, signals that
* action was completed with success.
*
* According to whatwg streams spec,
* WritableStream#close() doesn't send data.
*
* @returns {Promise}
*/
ServiceStreamPrototype.close = function() {
debug('close');
this._post('close');
this.writable = false;
};
/**
* Validate the current state and
* call cancel on the target stream.
*
* Called by the Service when client
* sends a 'streamcancel' message.
*
* @param {*} [reason] Reason for cancelation sent by the client
* @returns {Promise}
* @private
*/
ServiceStreamPrototype._cancel = function(reason) {
debug('cancel', reason);
return this.cancel(reason)
.then(result => {
this.writable = false;
return result;
});
};
/**
* Validate the current state and post message to client.
*
* @param {String} type 'write', 'abort' or 'close'
* @param {String} state 'writable', 'aborted' or 'closed'
* @param {*} [data] Data to be sent to the client
* @returns {Promise}
* @private
*/
ServiceStreamPrototype._post = function(type, data) {
debug('post', type, data, this.writable);
if (!this.writable) return Promise.reject();
this.service.broadcast('streamevent', {
streamId: this.id,
type: type,
data: data
}, [this.clientId]);
};
/**
* Create new `Error` from registery.
*
* @param {Number} id Error Id
* @return {Error}
* @private
*/
function error(id) {
var args = [].slice.call(arguments, 1);
return new Error({
1: 'stream "' + args[0] + '" doesn\'t exist'
}[id]);
}
});})((typeof define)[0]=='f'&&define.amd?define:((n,w)=>{return(typeof
module)[0]=='o'?c=>{c(require,exports,module);}:(c)=>{var m={exports:{}};
c(n=>w[n],m.exports,m);w[n]=m.exports;};})('streamService',this));

View file

@ -0,0 +1,462 @@
'use strict';
/**
* Dependencies
* @ignore
*/
var uuid = require('./utils').uuid;
var message = require('./message');
var Receiver = message.Receiver;
/**
* Exports
* @ignore
*/
module.exports = Service;
/**
* Debug logger
*
* 0: off
* 1: performance
* 2: console.log
*
* @type {Function}
* @private
*/
var debug = {
0: () => {},
1: arg => performance.mark(`[${self.constructor.name}][Service] - ${arg}`),
2: (arg1, ...args) => {
var type = `[${self.constructor.name}][${location.pathname}]`;
console.log(`[Service]${type} - "${arg1}"`, ...args);
}
}[0];
/**
* Extends `Receiver`
* @ignore
*/
Service.prototype = Object.create(Receiver.prototype);
/**
* A `Service` is a collection of methods
* exposed to a `Client`. Methods can be
* sync or async (using Promises).
*
* @example
*
* bridge.service('my-service')
* .method('ping', param => 'pong: ' + param)
* .listen();
*
* @class Service
* @extends Receiver
* @param {String} name The service name
* @public
*/
function Service(name) {
if (!(this instanceof Service)) return new Service(name);
message.Receiver.call(this, name); // call super
this.clients = {};
this.methods = {};
this
.on('_disconnect', this.onDisconnect.bind(this))
.on('_connect', this.onConnect.bind(this))
.on('_method', this.onMethod.bind(this))
.on('_off', this.onOff.bind(this))
.on('_on', this.onOn.bind(this));
this.destroy = this.destroy.bind(this);
debug('initialized', name);
}
Service.prototype.inWindow = constructor.name === 'Window';
/**
* Define a method to expose to Clients.
* The return value of the result of a
* returned Promise will be sent back
* to the Client.
*
* @example
*
* bridge.service('my-service')
*
* // sync return value
* .method('myMethod', function(param) {
* return 'hello: ' + param;
* })
*
* // or async Promise
* .method('myOtherMethod', function() {
* return new Promise(resolve => {
* setTimeout(() => resolve('result'), 1000);
* });
* })
*
* .listen();
*
* @param {String} name
* @param {Function} fn
* @return {this} for chaining
*/
Service.prototype.method = function(name, fn) {
this.methods[name] = fn;
return this;
};
/**
* Broadcast's an event from a `Service`
* to connected `Client`s.
*
* The third argument can be used to
* target selected clients by their
* `client.id`.
*
* @example
*
* service.broadcast('my-event', { some: data }); // all clients
* service.broadcast('my-event', { some: data }, [ clientId ]); // one client
*
* @memberof Service
* @param {String} type The message type/name
* @param {*} [data] Data to send with the event
* @param {Array} [only] A select list of clients to message
* @return {this}
*/
Service.prototype.broadcast = function(type, data, only) {
debug('broadcast', type, data, only);
this.eachClient(client => {
if (only && !~only.indexOf(client.id)) return;
debug('broadcasting to', client.id);
this.push(type, data, client.id, { noRespond: true });
});
return this;
};
/**
* Push message to a single connected Client.
*
* @example
*
* client.on('my-event', data => ...)
*
* ...
*
* service.push('my-event', { some: data}, clientId)
* .then(() => console.log('sent'));
*
* @public
* @param {String} type
* @param {Object} data
* @param {String} clientId The Id of the Client to push to
* @param {Object} options Optional parameters
* @param {Boolean} options.noResponse Tell the Client not to respond
* (Promise resolves instantly)
* @return {Promise}
*/
Service.prototype.push = function(type, data, clientId, options) {
var noRespond = options && options.noRespond;
var client = this.getClient(clientId);
return message('_push')
.set({
recipient: clientId,
noRespond: noRespond,
data: {
type: type,
data: data
}
}).send(client.port);
};
Service.prototype.eachClient = function(fn) {
for (var id in this.clients) fn(this.clients[id]);
};
Service.prototype.getClient = function(id) {
return this.clients[id];
};
/**
* @fires Service#before-connect
* @fires Service#connected
* @param {Message} message
* @private
*/
Service.prototype.onConnect = function(message) {
debug('connection attempt', message.data, this.name);
var data = message.data;
var clientId = data.clientId;
if (!clientId) return;
if (data.service !== this.name) return;
if (this.clients[clientId]) return;
// before hook
this.emit('before-connect', message);
if (message.defaultPrevented) return;
this.upgradeChannel(message);
this.addClient(clientId, message.sourcePort);
message.respond();
this.emit('connected', clientId);
debug('connected', clientId);
};
/**
* When a Client attempt to connect we
* can sometimes upgrade the to a direct
* MessageChannel 'pipe' to prevent
* hopping threads.
*
* We only do this if both:
*
* A. `MessagePort` was supplied with the 'connect' event.
* B. The Client and Service are not both in `Window` contexts
* (it's faster to use sync messaging window -> window).
*
* @param {Message} message the 'connect' message
* @private
*/
Service.prototype.upgradeChannel = function(message) {
if (this.inWindow && message.data.originEnv === 'Window') return;
var ports = message.event.ports;
var channel = ports && ports[0];
if (channel) {
message.setSourcePort(channel);
this.listen(channel);
channel.start();
}
debug('channel upgraded');
};
/**
* @fires Service#before-disconnect
* @fires Service#disconnected
* @param {Message} message
* @private
*/
Service.prototype.onDisconnect = function(message) {
var client = this.clients[message.data];
if (!client) return;
// before hook
this.emit('before-disconnect', message);
if (message.defaultPrevented) return;
this.removeClient(client.id);
message.respond();
this.emit('disconnected', client.id);
debug('disconnected', client.id);
};
/**
* @fires Service#before-method
* @param {Message} message
* @private
*/
Service.prototype.onMethod = function(message) {
debug('on method', message.data);
this.emit('before-method', message);
if (message.defaultPrevented) return;
var method = message.data;
var name = method.name;
var result;
var fn = this.methods[name];
if (!fn) throw error(4, name);
try { result = fn.apply(this, method.args); }
catch (err) { result = err; }
message.respond(result);
};
/**
* @fires Service#on
* @param {Message} message
* @private
*/
Service.prototype.onOn = function(message) {
debug('on on', message.data);
this.emit('on', message.data);
};
/**
* @fires Service#off
* @param {Message} message
* @private
*/
Service.prototype.onOff = function(message) {
debug('on off');
this.emit('off', message.data);
};
Service.prototype.addClient = function(id, port) {
this.clients[id] = {
id: id,
port: port
};
};
Service.prototype.removeClient = function(id) {
delete this.clients[id];
};
/**
* Use a plugin with this Service.
* @param {Function} fn Plugin function
* @return {this} for chaining
* @public
*/
Service.prototype.plugin = function(fn) {
fn(this, { 'uuid': uuid });
return this;
};
/**
* Disconnect a Client from the Service.
* @param {Object} client
* @private
*/
Service.prototype.disconnect = function(client) {
this.removeClient(client.id);
message('disconnect')
.set({
recipient: client.id,
noRespond: true
})
.send(client.port);
};
/**
* Destroy the Service.
* @public
*/
Service.prototype.destroy = function() {
delete this.clients;
this.unlisten();
this.off();
};
var sp = Service.prototype;
sp['broadcast'] = sp.broadcast;
sp['destroy'] = sp.destroy;
sp['method'] = sp.method;
sp['plugin'] = sp.plugin;
/**
* Creates new `Error` from registery.
*
* @param {Number} id Error Id
* @return {Error}
* @private
*/
function error(id) {
var args = [].slice.call(arguments, 1);
return new Error({
4: 'method "' + args[0] + '" doesn\'t exist'
}[id]);
}
/**
* Fires before the default 'connect' logic.
* This event acts as a hook for plugin authors
* to override default 'connect' behaviour.
*
* @example
*
* service.on('before-connect', message => {
* message.preventDefault();
* // alternative connection logic ...
* });
*
* @event Service#before-connect
* @param {Message} message - The connect message
*/
/**
* Signals that a Client has connected.
*
* @example
*
* service.on('connected', clientId => {
* console.log('client (%s) has connected', clientId);
* });
*
* @event Service#connected
* @param {String} clientId - The id of the connected Client
*/
/**
* Fires before the default 'disconnect' logic.
* This event acts as a hook for plugin authors
* to override default 'disconnect' behaviour.
*
* @example
*
* service.on('before-disconnect', message => {
* message.preventDefault();
* // alternative disconnection logic ...
* });
*
* @event Service#before-disconnect
* @param {Message} message - The disconnect message
*/
/**
* Signals that a Client has disconnected.
*
* @example
*
* service.on('disconnected', clientId => {
* console.log('client (%s) has disconnected', clientId);
* });
*
* @event Service#disconnected
* @param {String} clientId - The id of the disconnected Client
*/
/**
* Signals that a Client has begun
* listening to a broadcast event.
*
* @example
*
* service.on('on', data => {
* console.log('client (%s) is listening to %s', data.clientId, data.name);
* });
*
* @event Service#on
* @type {Object}
* @property {String} name - The broadcast name
* @property {String} clientId - The id of the Client that started listening
*/
/**
* Signals that a Client has stopped
* listening to a broadcast event.
*
* @example
*
* service.on('off', data => {
* console.log('client (%s) stopped listening to %s', data.clientId, data.name);
* });
*
* @event Service#off
* @param {Object} data
* @param {String} data.name - The broadcast name
* @param {String} data.clientId - The id of the Client that stopped listening
*/

View file

@ -0,0 +1,25 @@
'use strict';
/**
* Create a UUID string.
*
* http://jsperf.com/guid-generation-stackoverflow
*
* @return {String}
*/
exports.uuid = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
};
exports.deferred = function() {
var promise = {};
promise.promise = new Promise((resolve, reject) => {
promise.resolve = resolve;
promise.reject = reject;
});
return promise;
};

View file

@ -0,0 +1,34 @@
{
"name": "dom-scheduler",
"version": "1.0.1",
"homepage": "https://github.com/etiennesegonzac/dom-scheduler",
"authors": [
"Etienne Segonzac <etienne@segonzac.info>"
],
"description": "Making the performant thing the easy thing.",
"main": "lib/dom-scheduler.js",
"keywords": [
"web",
"performance",
"transition",
"animation",
"promise"
],
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"demo-app/bower_components",
"test",
"tests"
],
"_release": "1.0.1",
"_resolution": {
"type": "version",
"tag": "v1.0.1",
"commit": "1f3a138fb104d067774315acf7ae23f7d940bc4f"
},
"_source": "git://github.com/etiennesegonzac/dom-scheduler.git",
"_target": "~1.0.1",
"_originalSource": "etiennesegonzac/dom-scheduler"
}

View file

@ -0,0 +1,25 @@
{
"name": "dom-scheduler",
"version": "1.0.0",
"homepage": "https://github.com/etiennesegonzac/dom-scheduler",
"authors": [
"Etienne Segonzac <etienne@segonzac.info>"
],
"description": "Making the performant thing the easy thing.",
"main": "lib/dom-scheduler.js",
"keywords": [
"web",
"performance",
"transition",
"animation",
"promise"
],
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"demo-app/bower_components",
"test",
"tests"
]
}

View file

@ -0,0 +1,227 @@
/** Variables
---------------------------------------------------------*/
:root {
--dim-micro: 6px;
--dim-small: 12px;
--dim-medium: 24px;
--dim-mega: 48px;
--base-xs: 6px;
--base-s: 18px;
--base-m: 24px;
--base-l: 30px;
--base-xl: 42px;
--base-xxl: 60px;
--base-font-xs: 10px;
--base-font-s: 14px;
--base-font-m: 17px;
--base-font-l: 23px;
--base-font-xl: 60px;
}
/**
* 1. Apply a natural box layout
* model to all elements
*/
*,
*:after,
*:before {
box-sizing: border-box; /* 1 */
font: inherit;
margin: 0;
}
/** Document
---------------------------------------------------------*/
html {
height: 100%;
}
/** Body
---------------------------------------------------------*/
body {
height: 100%;
font: 300 17px/21px 'FiraSans';
background-color: var(--background);
color: var(--text-color);
padding: 0;
}
/** Tags
---------------------------------------------------------*/
a {
color: var(--highlight-color, inherit);
text-decoration: none;
font-style: italic;
}
hr {
height: 1px;
margin: var(--dim-small) 0 var(--dim-small) 0;
border: 0;
background:
var(--border-color,
var(--background-plus,
rgba(0,0,0,0.2)));
}
em {
font-weight: 500;
}
h1,
h2,
h3,
h4,
h5 {
font-weight: 300;
}
.alpha {
font-size: 23px;
font-weight: 200;
font-style: italic;
line-height: 26px;
margin: 6px 0 24px 0;
}
.beta {
font-size: 17px;
font-weight: 400;
margin: 32px 0 16px 0;
}
.gamma {
font-size: 14px;
text-transform: uppercase;
font-weight: normal;
margin: 32px 0 16px 0;
}
.milli {
font-size: 14px;
font-weight: 300;
}
.large {
font-size: 23px;
line-height: 26px;
}
.mega {
font-size: 60px;
line-height: 60px;
font-style: normal;
}
.giga {}
.kilo {}
/* alignment */
.l-align-right { float: right; }
.l-align-left { float: left; }
.l-align-center { text-align: center; }
.l-text-align-left { text-align: left; }
.l-text-align-right { text-align: right; }
.l-text-valign-top {
vertical-align: top;
}
.l-width-full {
width: 100%;
}
/** Flexbox
---------------------------------------------------------*/
.l-flex { display: flex; }
.l-flex-col { display: flex; flex-direction: column; }
.l-flex-col-v-align {
display: flex !important;
flex-direction: column;
justify-content: center;
}
.l-flex-v-align { align-items: center; }
.l-flex-h-align { justify-content: center; }
.l-flex-grow { flex: 1; }
/** Margin
---------------------------------------------------------*/
/* Top */
.l-mt-small { margin-top: var(--dim-small); }
.l-mt-med { margin-top: var(--dim-medium); }
/* Bottom */
.l-mb-small { margin-bottom: var(--dim-small); }
.l-mb-med { margin-bottom: var(--dim-medium); }
.l-mb-large {}
.l-mb-mega { margin-bottom: var(--dim-mega); }
/* Left */
.l-ml-small { margin-left: var(--dim-small); }
/* Right */
.l-mr-small { margin-right: var(--dim-small); }
/* Auto */
.l-mlr-auto { margin-left: auto; margin-right: auto; }
.l-m-auto { margin: auto }
/* Margin */
.l-m-small { margin: var(--dim-small) }
.l-m-med { margin: var(--dim-medium) }
.l-m-0 { margin: 0 }
/** Padding
---------------------------------------------------------*/
.l-p-small { padding: var(--dim-small); }
.l-pl-small { padding-left: var(--dim-small); }
.l-pr-small { padding-right: var(--dim-small); }
.l-plr-small {
padding-right: var(--dim-small);
padding-left: var(--dim-small);
}
.l-pb-small { padding-bottom: var(--dim-small); }
.l-pt-small { padding-top: var(--dim-small); }
.l-pt-mega { padding-top: var(--dim-mega); }
.l-pt-micro { padding-top: var(--dim-micro); }
.l-pb-micro { padding-bottom: var(--dim-micro); }
.l-p-0 { padding: 0 ;}
/** Helpers
---------------------------------------------------------*/
.l-clearfix:after {
content: "";
display: table;
clear: both;
}
.l-inline-block {
display: inline-block;
}
.is-hidden {
display: none !important;
}
.is-invisible {
visibility: hidden !important;
}

View file

@ -0,0 +1,16 @@
{
"name": "base",
"version": "0.0.0",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "base.css",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}

View file

@ -0,0 +1,25 @@
{
"name": "fast-list",
"version": "0.1.2",
"homepage": "https://github.com/gaia-components/fast-list",
"authors": [
"Etienne Segonzac <etienne@segonzac.info>",
"Wilson Page <wilsonpage@me.com>"
],
"description": "No, really.",
"main": "fast-list.js",
"keywords": [
"web",
"performance",
"virtual",
"list"
],
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}

View file

@ -0,0 +1,20 @@
{
"name": "fast-list/examples/sections",
"version": "0.1.0",
"homepage": "https://github.com/gaia-components/fast-list",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "index.html",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"dom-scheduler": "etiennesegonzac/dom-scheduler"
}
}

View file

@ -0,0 +1,134 @@
html, body {
padding: 0;
margin: 0;
font-family: sans-serif;
width: 100%;
height: 100%;
overflow: hidden;
}
h1 {
pointer-events: none;
}
#h1-after {
position: absolute;
width: 100%;
height: 4px;
left: 0;
top: 46px;
pointer-events: none;
background-color: #ff9500;
opacity: 0;
animation-duration: 0.25s;
animation-timing-function: ease-in;
animation-fill-mode: both;
-webkit-animation-duration: 0.25s;
-webkit-animation-timing-function: ease-in;
-webkit-animation-fill-mode: both;
}
[data-anim='reveal'] {
animation-name: reveal;
-webkit-animation-name: reveal;
}
@keyframes reveal {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-webkit-keyframes reveal {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
[data-anim='hide'] {
animation-name: hide;
-webkit-animation-name: hide;
}
@keyframes hide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@-webkit-keyframes hide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
button {
position: absolute;
display: block;
top: 0;
right: 0;
height: 50px;
border: 0;
border-radius: 0;
outline: 0;
background: none;
padding: 0 15px;
text-align: center;
text-decoration: underline;
font-size: 14px;
color: #ffffff;
-moz-appearance: none;
opacity: 1;
transition: transform 0.1s linear,
opacity 0.1s linear;
-webkit-transition: -webkit-transform 0.1s linear,
opacity 0.1s linear;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
button.transitioning {
opacity: 0.5;
transform: scale(0.6, 0.6);
-webkit-transform: scale(0.6, 0.6);
pointer-events: none;
}
ul {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
body > section {
position: absolute;
top: 50px;
width: 100%;
height: calc(100% - 50px);
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}

View file

@ -0,0 +1,21 @@
h1 {
position: absolute;
top: 0;
left: 0;
width: 100%;
margin: 0;
padding: 0;
background: #27c8c2;
text-align: center;
line-height: 50px;
font-weight: 300;
font-style: italic;
font-size: 24px;
color: #ffffff;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

View file

@ -0,0 +1,143 @@
ul {
list-style: none;
padding: 0 0 0 0;
margin: 0;
}
ul section {
margin: 0;
padding: 0;
}
ul h2 {
position: sticky;
position: -webkit-sticky;
top: 0px;
height: 32px;
margin: 0;
padding: 0 6px;
box-sizing: border-box;
font-size: 0.9em;
line-height: 32px;
background: #efefef;
border-bottom: 1px #bcbcbc solid;
z-index: 100;
}
ul .background {
position: absolute;
width: 100%;
background: linear-gradient(to bottom, transparent 0px, transparent 45px, #EDEDED 45px, #EDEDED 70px, transparent 20px),
linear-gradient(to bottom, transparent 0px, transparent 15px, #EDEDED 15px, #EDEDED 25px, transparent 20px),
linear-gradient(to top, #bcbcbc, #bcbcbc 1px, white 1px, white);
background-position: 20% 88px, 5% -88px, 0 -88px;
background-size: 90% 88px, 60% 88px, 100% 88px;
background-repeat: repeat-y;
z-index: 0;
}
ul.reordering .background {
background: linear-gradient(to top, #bcbcbc, #bcbcbc 1px, white 1px, white);
background-position: 0 -88px;
background-size: 100% 88px;
}
ul li {
width: 100%;
height: 88px;
margin: 0;
padding: 0;
box-sizing: border-box;
-moz-user-select: none;
background: white;
border-bottom: 1px #bcbcbc solid;
z-index: 10;
}
ul li h3 {
margin: 0;
padding: 9px 6px 3px 6px;
background: white;
color: #424242;
pointer-events: none;
}
ul li p {
margin: 0;
padding: 3px 6px 10px 6px;
background: white;
color: #797979;
pointer-events: none;
}
li.new {
opacity: 0;
}
li .overlay {
position: absolute;
top: 1px;
left: 0;
width: 100%;
height: calc(100% - 2px);
background: rgba(255, 255, 255, 0.7);
opacity: 0;
animation-duration: 0.2s;
animation-timing-function: ease-in;
animation-fill-mode: both;
-webkit-animation-duration: 0.2s;
-webkit-animation-timing-function: ease-in;
-webkit-animation-fill-mode: both;
pointer-events: none;
}
li.edit .overlay {
opacity: 1;
pointer-events: auto;
}
.overlay .cursor {
margin: 17px auto;
padding: 0;
width: 50px;
height: 50px;
text-align: center;
line-height: 50px;
font-size: 25px;
color: #0b0b0b;
border-radius: 8px;
background: rgba(0, 0, 0, 0.3);
-moz-user-select: none;
}
li.dragging {
transition: transform 0s linear;
-webkit-transition: -webkit-transform 0s linear;
}
li.dragging .overlay {
opacity: 0;
}
li[data-populated=false] {
opacity: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link rel="stylesheet" href="css/h1.css">
<link rel="stylesheet" href="css/ul.css">
<link rel="stylesheet" href="css/app.css">
<script src="bower_components/dom-scheduler/lib/dom-scheduler.js" defer></script>
<script src="fast-list.js" async></script>
<script src="js/bacon-source.js" async></script>
<script src="js/page.js" async></script>
<title>Dom Scheduler Demo</title>
</head>
<body class="theme-communications">
<h1>
Main List
</h1>
<div id="h1-after"></div>
<button>
Edit
</button>
<section>
<ul>
<section
><h2> </h2
><div class="background"
></div
></section>
<li
><h3> </h3
><p> </p
><div class="overlay"
><div class="cursor">↕︎</div></div
></li>
</ul>
</section>
</body>
</html>

View file

@ -0,0 +1,95 @@
(function() {
var debug = false;
var headerHeight = 50;
var maxItemCount = 28;
var endEvent = ('ontouchstart' in window) ? 'touchend' : 'mouseup';
window.addEventListener('load', function() {
var listContainer = document.querySelector('section');
var source = new BaconSource();
var list = new FastList(listContainer, source, scheduler);
var dialog = document.querySelector('gaia-dialog-alert');
function updateHeader() {
return scheduler.mutation(function() {
var h1 = document.querySelector('h1');
h1.textContent = 'Main List (' + source.fullLength() + ')';
h1.scrollTop; // flush
});
}
updateHeader();
function clearNewIndicator() {
var h1After = document.querySelector('#h1-after');
if (h1After.dataset.anim == 'reveal') {
scheduler.transition(function() {
h1After.dataset.anim = 'hide';
}, h1After, 'animationend');
}
}
listContainer.addEventListener('top-reached', clearNewIndicator);
function updateNewIndicator() {
var h1After = document.querySelector('#h1-after');
if (h1After.dataset.anim == 'reveal') {
return;
}
scheduler.transition(function() {
h1After.dataset.anim = 'reveal';
}, h1After, 'animationend');
}
listContainer.addEventListener('hidden-new-content', updateNewIndicator);
function openGaiaDialog(evt) {
scheduler.mutation(function() {
var detail = evt.detail;
var li = source.getRecordAt(detail.index);
dialog.textContent = li.title + ' item clicked!';
dialog.open(detail.clickEvt);
});
}
list.list.addEventListener('item-selected', openGaiaDialog);
function newContentHandler() {
var newContent = {
title: 'NEW Bacon ' + Date.now().toString().slice(7, -1),
body: 'Turkey BLT please.'
};
source.insertAtIndex(0, newContent);
list.insertedAtIndex(0);
updateHeader();
}
setInterval(newContentHandler, 15000);
window.addEventListener('new-content', newContentHandler);
window.pushNewContent = function() {
window.dispatchEvent(new CustomEvent('new-content'));
};
var button = document.querySelector('button');
button.addEventListener(endEvent, function() {
Promise.all([toggleTransitioning(), list.toggleEditMode()])
.then(updateText)
.then(toggleTransitioning);
});
function updateText(text) {
return scheduler.mutation(function() {
button.textContent = list.editing ? 'Exit' : 'Edit';
});
}
function toggleTransitioning() {
return scheduler.feedback(function() {
button.classList.toggle('transitioning');
}, button, 'transitionend');
}
});
})();

View file

@ -0,0 +1,175 @@
(function(exports) {
var listSize = 1042;
var sectionCount = 25;
var headerHeight = 32;
var itemHeight = 88;
function makeContent(prefix, name) {
return {
title: prefix + ' Bacon ipsum dolor ' +
Date.now().toString().slice(7, -1),
body: 'Turkey pig boudin ' + name.toLowerCase() +
' de mignon drums and all ' + prefix + '.'
}
}
exports.BaconSource = function BaconSource() {
this.sections = [];
this.contentMap = new Map();
this._cachedLength = null;
for (var i = 0; i < sectionCount; i++) {
this.sections.push('Section ' + i);
}
this.sections.forEach(function(name) {
var content = [];
for (var i = 0; i < (Math.random() * listSize); i++) {
content.push(makeContent(i, name));
}
this.contentMap.set(name, content);
}, this);
};
exports.BaconSource.prototype = {
populateItem: function(item, i) {
// Simulates the data source not being ready to populate
/*if (parseInt(Date.now() / 10) % 8 === 0) {*/
/*return new Promise(function(resolve, reject) {*/
/*setTimeout(resolve, Math.random() * 1000);*/
/*});*/
/*}*/
var title = item.firstChild;
var body = title.nextSibling;
var record = this.getRecordAt(i);
title.firstChild.data = record.title;
body.firstChild.data = record.body;
},
getSections: function() {
return this.contentMap.keys();
},
sectionHeaderHeight: function() {
return headerHeight;
},
fullSectionHeight: function(key) {
return this.contentMap.get(key).length * itemHeight;
},
fullSectionLength: function(key) {
return this.contentMap(key).length;
},
getRecordAt: function(index) {
for (var section of this.contentMap.values()) {
if (index < section.length) {
return section[index];
}
index -= section.length;
}
},
getSectionFor: function(index) {
for (var entry of this.contentMap.entries()) {
if (index < entry[1].length) {
return entry[0];
}
index -= entry[1].length;
}
},
indexAtPosition: function(pos) {
var index = 0;
for (var section of this.contentMap.values()) {
pos -= headerHeight;
var sectionHeight = section.length * itemHeight;
if (pos > sectionHeight) {
pos -= sectionHeight;
index += section.length;
continue;
}
for (var item of section) {
pos -= itemHeight;
index++;
if (pos <= 0 || index === this.fullLength() - 1) {
return index;
}
}
}
},
positionForIndex: function(index) {
var top = 0;
for (var section of this.contentMap.values()) {
top += headerHeight;
if (index < section.length) {
top += index * itemHeight;
return top;
}
index -= section.length;
top += section.length * itemHeight;
}
},
fullLength: function() {
if (this._cachedLength) {
return this._cachedLength;
}
var length = 0;
for (var section of this.contentMap.values()) {
length += section.length;
}
this._cachedLength = length;
return length;
},
itemHeight: function() {
return itemHeight;
},
fullHeight: function() {
var height = 0;
for (var section of this.contentMap.values()) {
height += headerHeight + section.length * itemHeight;
}
return height;
},
insertAtIndex: function(index, record, toSection) {
this._cachedLength = null;
for (var entry of this.contentMap.entries()) {
if (index < entry[1].length || entry[0] === toSection) {
return entry[1].splice(index, 0, record);
}
index -= entry[1].length;
}
},
replaceAtIndex: function(index, record) {
for (var section of this.contentMap.values()) {
if (index < section.length) {
return section.splice(index, 1, record);
}
index -= section.length;
}
},
removeAtIndex: function(index) {
this._cachedLength = null;
for (var section of this.contentMap.values()) {
if (index < section.length) {
return section.splice(index, 1)[0];
}
index -= section.length;
}
}
};
})(window);

View file

@ -0,0 +1,94 @@
(function() {
/*global BaconSource, FastList*/
'use strict';
var endEvent = ('ontouchstart' in window) ? 'touchend' : 'mouseup';
window.addEventListener('load', function() {
var listContainer = document.querySelector('section');
var source = new BaconSource();
var list = new FastList(listContainer, source);
var scheduler = FastList.scheduler;
function updateHeader() {
return scheduler.mutation(function() {
var h1 = document.querySelector('h1');
h1.textContent = 'Main List (' + source.fullLength() + ')';
h1.scrollTop; // flush
});
}
updateHeader();
function openAlert(evt) {
scheduler.mutation(function() {
var detail = evt.detail;
var li = source.getRecordAt(detail.index);
alert(li.title + ' item clicked!');
});
}
list.list.addEventListener('item-selected', openAlert);
function clearNewIndicator() {
var h1After = document.querySelector('#h1-after');
if (h1After.dataset.anim == 'reveal') {
scheduler.transition(function() {
h1After.dataset.anim = 'hide';
}, h1After, 'animationend');
}
}
listContainer.addEventListener('top-reached', clearNewIndicator);
function updateNewIndicator() {
var h1After = document.querySelector('#h1-after');
if (h1After.dataset.anim == 'reveal') {
return;
}
scheduler.transition(function() {
h1After.dataset.anim = 'reveal';
}, h1After, 'animationend');
}
listContainer.addEventListener('hidden-new-content', updateNewIndicator);
function newContentHandler() {
var newContent = {
title: 'NEW Bacon ' + Date.now().toString().slice(7, -1),
body: 'Turkey BLT please.'
};
source.insertAtIndex(0, newContent);
list.insertedAtIndex(0);
updateHeader();
}
setInterval(newContentHandler, 15000);
window.addEventListener('new-content', newContentHandler);
window.pushNewContent = function() {
window.dispatchEvent(new CustomEvent('new-content'));
};
var button = document.querySelector('button');
button.addEventListener(endEvent, function() {
Promise.all([toggleTransitioning(), list.toggleEditMode()])
.then(updateText)
.then(toggleTransitioning);
});
function updateText(text) {
return scheduler.mutation(function() {
button.textContent = list.editing ? 'Exit' : 'Edit';
});
}
function toggleTransitioning() {
return scheduler.feedback(function() {
button.classList.toggle('transitioning');
}, button, 'transitionend');
}
});
})();

View file

@ -0,0 +1,14 @@
{
"name": "fast-list-demo",
"description": "Faaaaasssst",
"launch_path": "/index.html",
"icons": {
"16": "/icons/icon16x16.png",
"48": "/icons/icon48x48.png",
"60": "/icons/icon60x60.png",
"128": "/icons/icon128x128.png"
},
"type": "certified",
"orientation": "default",
"permissions": {}
}

View file

@ -0,0 +1,84 @@
/* mini reset */
* { margin: 0; padding: 0; }
body,
html {
height: 100%;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
}
header,
footer {
height: 50px;
background: #eee;
}
#content {
position: relative;
flex: 1;
}
ul {
list-style: none;
padding: 0 0 0 0;
margin: 0;
}
ul section {
margin: 0;
padding: 0;
}
ul h2 {
display: none;
}
.background {
position: absolute;
z-index: 0;
width: 100%;
background: linear-gradient(to top, #bcbcbc, #bcbcbc 1px, white 1px, white);
background-position: 0 -88px;
background-size: 100% 88px;
background-repeat: repeat-y;
}
ul li {
width: 100%;
height: 88px;
margin: 0;
padding: 0;
box-sizing: border-box;
-moz-user-select: none;
background: white;
border-bottom: 1px #bcbcbc solid;
z-index: 10;
}
ul li h3 {
margin: 0;
padding: 9px 6px 3px 6px;
background: white;
color: #424242;
}
ul li p {
margin: 0;
padding: 3px 6px 10px 6px;
background: white;
color: #797979;
}

View file

@ -0,0 +1,22 @@
/*global DataSource, FastList*/
var content = document.getElementById('content');
var data = createDummyData(1000);
var dataSource = new DataSource(data);
var list = new FastList(content, dataSource);
function createDummyData(count) {
var result = [];
for (var i = 0; i < count; i++) {
result.push({
name: 'Song ' + i,
metadata: {
title: 'Song title ' + i,
artist: 'Song artist ' + i
}
});
}
return result;
}

View file

@ -0,0 +1,79 @@
(function(exports) {
var itemHeight = 88;
var debug = 0 ? (...args) => console.log('[DataSource]', ...args) : ()=>{};
exports.DataSource = function DataSource(items) {
this.items = items;
debug('initialized', items);
};
exports.DataSource.prototype = {
populateItem: function(item, i) {
debug('populate item', item);
var title = item.firstChild;
var body = title.nextSibling;
var record = this.getRecordAt(i);
title.firstChild.data = record.name;
body.firstChild.data = record.metadata.artist;
},
getSections() {
debug('get sections');
return [''];
},
sectionHeaderHeight() {
return 0;
},
fullSectionHeight() {
var result = this.items.length * itemHeight;
debug('full section height', result);
return result;
},
fullSectionLength() {
var result = this.items.length;
debug('full section length', result);
return result;
},
getSectionFor() {
return '';
},
getRecordAt: function(index) {
return this.items[index];
},
indexAtPosition: function(pos) {
return Math.floor(pos / itemHeight);
},
positionForIndex: function(index) {
return index * itemHeight;
},
fullLength: function() {
return this.items.length;
},
itemHeight: function() {
return itemHeight;
},
fullHeight: function() {
this.items.length * itemHeight;
},
insertAtIndex: function(index, record, toSection) {
return this.items.splice(index, 0, record);
},
replaceAtIndex: function(index, record) {
return this.items.splice(index, 1)[0];
}
};
})(window);

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link rel="stylesheet" href="app.css">
<title>fast-list: simple</title>
</head>
<body>
<header>Header</header>
<section id="content" style="height:100%;">
<ul>
<section
><h2> </h2
><div class="background"
></div
></section>
<li
><h3> </h3
><p> </p
></li>
</ul>
</section>
<footer>Footer</footer>
<!-- <script src="bower_components/dom-scheduler/lib/dom-scheduler.js"></script> -->
<script src="fast-list.js"></script>
<script src="data-source.js"></script>
<script src="app.js"></script>
</body>
</html>

View file

@ -0,0 +1,13 @@
{
"name": "simple-list-demo",
"description": "Sweet",
"launch_path": "/index.html",
"icons": {
"16": "/icons/icon16x16.png",
"48": "/icons/icon48x48.png",
"60": "/icons/icon60x60.png",
"128": "/icons/icon128x128.png"
},
"orientation": "default",
"permissions": {}
}

View file

@ -0,0 +1,935 @@
!(function(define){'use strict';define(function(require,exports,module){
var debug = 0 ? console.log.bind(console, '[FastList]') : function() {};
var startEvent = ('ontouchstart' in window) ? 'touchstart' : 'mousedown';
var moveEvent = ('ontouchstart' in window) ? 'touchmove' : 'mousemove';
var endEvent = ('ontouchstart' in window) ? 'touchend' : 'mouseup';
/**
* Use the dom-scheduler if it's around,
* else fallback to fake shim.
*
* @type {Object}
*/
var schedule = (window.scheduler && typeof window.scheduler.mutation === "function" ? window.scheduler : schedulerShim());
/**
* Exports
*/
exports = module.exports = FastList;
exports.scheduler = schedule;
function FastList(container, source, scheduler) {
this.editing = false;
container.style.overflowX = 'hidden';
container.style.overflowY = 'scroll';
this.container = container;
this.list = container.querySelector('ul');
this.source = source;
this.geometry = {
topPosition: 0,
forward: true,
viewportHeight: 0,
itemHeight: 0,
headerHeight: 0,
maxItemCount: 0,
switchWindow: 0
};
this._templateSection = this.list.querySelector('section');
this.geometry.headerHeight = source.sectionHeaderHeight();
this._templateSection.remove();
var template = this.list.querySelector('li');
template.style.position = 'absolute';
template.style.left = template.style.top = 0;
template.style.overflow = 'hidden';
template.style.willChange = 'transform';
this.geometry.itemHeight = template.offsetHeight;
template.remove();
this._template = template;
this.updateContainerGeometry();
if (debug.name) {
if (this.geometry.itemHeight !== source.itemHeight()) {
debug('Template height and source height are not the same.');
}
}
this._rendered = false;
this._previousTop = -1;
this._previousFast = false;
this._items = [];
this._itemsInDOM = [];
this.reorderingContext = {
item: null,
initialY: null,
identifier: null,
moveUp: null,
moveDown: null,
};
this.updateListHeight();
schedule.attachDirect(
this.container,
'scroll',
this.handleScroll.bind(this)
);
on(this.container, 'click', this);
on(window, 'resize', this);
schedule.mutation(function() {
this.updateSections();
this.render();
}.bind(this));
}
FastList.prototype = {
/* Rendering
========= */
updateContainerGeometry: function() {
var geo = this.geometry;
geo.viewportHeight = this.container.offsetHeight;
var itemPerScreen = geo.viewportHeight / geo.itemHeight;
// Taking into account the will-change budget multiplier from
// layout/base/nsDisplayList.cpp#1193
geo.maxItemCount = Math.floor(itemPerScreen * 2.4);
geo.switchWindow = Math.floor(itemPerScreen / 2);
debug('maxItemCount: ' + geo.maxItemCount);
},
// Returns true if we're fast scrolling
updateViewportGeometry: function() {
var geo = this.geometry;
var position = this.container.scrollTop;
// Don't compute velocity on first load
if (this._previousTop === -1) {
geo.topPosition = position;
}
this._previousTop = geo.topPosition;
geo.topPosition = position;
var distanceSinceLast = geo.topPosition - this._previousTop;
if ((distanceSinceLast > 0) && !geo.forward) {
geo.forward = true;
}
if ((distanceSinceLast < 0) && geo.forward) {
geo.forward = false;
}
if (geo.topPosition === 0 && this._previousTop !== 0) {
this.container.dispatchEvent(new CustomEvent('top-reached'));
}
var moved = Math.abs(distanceSinceLast);
var fastScrolling = this._previousFast;
if (!fastScrolling && moved > geo.viewportHeight * 2) {
fastScrolling = true;
}
if (fastScrolling && moved > 0 && moved < geo.viewportHeight * 0.5) {
fastScrolling = false;
}
this._previousFast = fastScrolling;
var onTop = position === 0;
var atBottom = position === this.source.fullHeight() - geo.viewportHeight;
return fastScrolling && !onTop && !atBottom;
},
render: function(reload, changedIndex) {
var source = this.source;
var items = this._items;
var itemsInDOM = this._itemsInDOM;
var geo = this.geometry;
var template = this._template;
var list = this.list;
var indices = computeIndices(this.source, this.geometry);
var criticalStart = indices.cStart;
var criticalEnd = indices.cEnd;
var startIndex = indices.start;
var endIndex = indices.end;
// Initial render generating all dom nodes
if (!this._rendered) {
this._rendered = true;
endIndex = Math.min(source.fullLength() - 1,
startIndex + this.geometry.maxItemCount - 1);
}
var recyclableItems = recycle(items, criticalStart, criticalEnd,
geo.forward ? endIndex : startIndex);
var findItemFor = function(index) {
var item;
if (recyclableItems.length > 0) {
var recycleIndex = recyclableItems.pop();
item = items[recycleIndex];
delete items[recycleIndex];
} else if (itemsInDOM.length < geo.maxItemCount){
item = template.cloneNode(true);
list.appendChild(item);
itemsInDOM.push(item);
} else {
console.warn('missing a cell');
return;
}
items[index] = item;
return item;
};
var renderItem = function(i) {
var item = items[i];
if (!item) {
item = findItemFor(i);
tryToPopulate(item, i, source, true);
item.classList.toggle('new', i === changedIndex);
} else if (reload) {
source.populateItem(item, i);
}
var section = source.getSectionFor(i);
placeItem(item, i, section, geo, source, reload);
};
if (geo.forward) {
for (var i = startIndex; i <= endIndex; ++i) {
renderItem(i);
}
} else {
for (var j = endIndex; j >= startIndex; --j) {
renderItem(j);
}
}
debugViewport(items, geo.forward, criticalStart, criticalEnd,
startIndex, endIndex);
},
/* Scrolling
========= */
handleScroll: function(evt) {
var fast = this.updateViewportGeometry();
if (fast) {
debug('[x] ---------- faaaaassssstttt');
} else {
this.render();
}
},
updateListHeight: function() {
return schedule.mutation((function() {
this.list.style.height = this.source.fullHeight() + 'px';
}).bind(this));
},
get scrollTop() {
return this.geometry.topPosition;
},
scrollInstantly: function(by) {
this.container.scrollTop += by;
this.updateViewportGeometry();
this.render();
},
reloadData: function() {
return schedule.mutation((function() {
this.updateSections();
this.render(true);
this.updateListHeight();
}).bind(this));
},
updateSections: function() {
var list = this.list;
var source = this.source;
var template = this._templateSection;
return schedule.mutation(function() {
var nodes = list.querySelectorAll('section');
for (var i = 0; i < nodes.length; i++) {
var toRemove = nodes[i];
toRemove.remove();
}
var headerHeight = source.sectionHeaderHeight();
for (var section of source.getSections()) {
var height = source.fullSectionHeight(section);
var sectionNode = template.cloneNode(true);
sectionNode.style.height = headerHeight + height + 'px';
var title = sectionNode.firstChild;
title.firstChild.data = section;
var background = title.nextSibling;
background.style.height = height + 'px';
list.appendChild(sectionNode);
}
});
},
/* Edit mode
========= */
toggleEditMode: function() {
this.editing = !this.editing;
if (this.editing) {
this._startTouchListeners();
} else {
this._stopTouchListeners();
}
return toggleEditClass(this.list,
this._itemsInDOM, this.editing);
},
_startTouchListeners: function() {
on(this.list, startEvent, this);
on(this.list, endEvent, this);
},
_stopTouchListeners: function() {
off(this.list, startEvent, this);
off(this.list, endEvent, this);
},
/* Reordering
---------- */
_reorderStart: function(evt) {
debug('reorder start');
var ctx = this.reorderingContext;
// Already tracking an item, bailing out
if (ctx.item) {
return;
}
var li = evt.target.parentNode.parentNode;
var touch = evt.touches && evt.touches[0] || evt;
ctx.item = li;
ctx.initialY = touch.pageY;
ctx.identifier = touch.identifier;
ctx.moveUp = new Set();
ctx.moveDown = new Set();
ctx.moveHandler = this._reorderMove.bind(this);
var listenToMove = (function() {
schedule.attachDirect(this.list, moveEvent, ctx.moveHandler);
}).bind(this);
setupForDragging(this.scheduler, li, true)
.then(listenToMove)
.then(toggleDraggingClass.bind(null, this.scheduler, li, true));
},
_reorderMove: function(evt) {
debug('reorder move');
var ctx = this.reorderingContext;
if (!ctx.item) {
return;
}
// Multi touch
if (evt.touches && evt.touches.length > 1) {
return;
}
var li = ctx.item;
var touch = evt.touches && evt.touches[0] || evt;
var position = touch.pageY;
var delta = position - ctx.initialY;
this._rearrange(delta);
tweakTransform(li, delta);
// TODO: scroll when close to the beginning/end of the viewport
},
_reorderEnd: function(evt) {
debug('reorder end');
var ctx = this.reorderingContext;
if (!ctx.item) {
return;
}
var li = ctx.item;
schedule.detachDirect(this.list, moveEvent, ctx.moveHandler);
var touch = evt.touches && evt.touches[0] || evt;
if (touch.identifier == ctx.identifier) {
var position = touch.pageY;
var delta = position - ctx.initialY;
computeChanges(ctx, this.geometry, this._itemsInDOM, delta);
}
Promise.all([
applyChanges(ctx, this.geometry, this._itemsInDOM),
moveInPlace(ctx, this.geometry)
]).then(this._reorderFinish.bind(this, li))
.then(toggleDraggingClass.bind(null, schedule, li, false));
},
_reorderFinish: function(li) {
var scheduler = this.scheduler;
return Promise.all([this._commitToDocument(),
setupForDragging(scheduler, li, false)]);
},
_rearrange: function(delta) {
var ctx = this.reorderingContext;
var upCount = ctx.moveUp.size;
var downCount = ctx.moveDown.size;
computeChanges(this.reorderingContext, this.geometry,
this._itemsInDOM, delta);
if (ctx.moveUp.size === upCount &&
ctx.moveDown.size === downCount) {
return;
}
applyChanges(this.reorderingContext,
this.geometry, this._itemsInDOM);
},
_commitToDocument: function() {
var ctx = this.reorderingContext;
var li = ctx.item;
var itemsInDOM = this._itemsInDOM;
var items = this._items;
var source = this.source;
return schedule.mutation((function() {
// Nothing to do
if (!ctx.moveUp && !ctx.moveDown) {
return;
}
var index = items.indexOf(li);
var section = li.dataset.section;
var newIndex = index + ctx.moveUp.size - ctx.moveDown.size;
items.splice(index, 1);
items.splice(newIndex, 0, li);
var c = source.removeAtIndex(index);
source.insertAtIndex(newIndex, c, section);
itemsInDOM.forEach(function(item) {
resetTransition(item);
resetTransform(item);
});
this.updateSections();
this.render();
ctx.item = null;
ctx.initialY = null;
ctx.identifier = null;
ctx.moveUp = null;
ctx.moveDown = null;
ctx.moveHandler = null;
}).bind(this));
},
/* External content changes
======================== */
insertedAtIndex: function(index) {
if (index !== 0) {
//TODO: support any point of insertion
return;
}
if (this.geometry.topPosition > this.geometry.itemHeight ||
this.editing) {
// No transition needed, just keep the scroll position
this._insertOnTop(true);
return;
}
var domItems = this._itemsInDOM;
var list = this.list;
list.classList.add('reordering');
pushDown(this.scheduler, domItems, this.geometry)
.then(this._insertOnTop.bind(this, false))
.then(cleanInlineStyles.bind(null, domItems))
.then(reveal.bind(null, this.list))
.then(function() {
list.classList.remove('reordering');
});
},
_insertOnTop: function(keepScrollPosition) {
return schedule.mutation((function() {
this._items.unshift(null);
delete this._items[0]; // keeping it sparse
this.updateSections();
if (keepScrollPosition) {
this.scrollInstantly(this.geometry.itemHeight);
this.container.dispatchEvent(new CustomEvent('hidden-new-content'));
} else {
this.render(false, 0);
}
this.updateListHeight();
}).bind(this));
},
handleEvent: function(evt) {
switch (evt.type) {
case 'resize':
this.geometry.viewportHeight = this.container.offsetHeight;
this.updateContainerGeometry();
break;
case startEvent:
if (!evt.target.classList.contains('cursor')) {
return;
}
evt.preventDefault();
this._reorderStart(evt);
break;
case endEvent:
if (!evt.target.classList.contains('cursor') &&
endEvent === 'touchend') {
return;
}
evt.preventDefault();
this._reorderEnd(evt);
break;
case 'click':
if (this.editing) {
break;
}
var li = evt.target;
var index = this._items.indexOf(li);
this.list.dispatchEvent(new CustomEvent('item-selected', {detail: {
index: index,
clickEvt: evt
}}));
break;
}
}
};
/* Internals
========= */
/* ASCII Art viewport debugging
---------------------------- */
function debugViewport(items, forward, cStart, cEnd, start, end) {
if (!debug.name) {
return;
}
var str = '[' + (forward ? 'v' : '^') + ']';
for (var i = 0; i < items.length; i++) {
if (i == start) {
str += '|';
}
if (i == cStart) {
str += '[';
}
if (items[i]) {
str += 'x';
} else {
str += '-';
}
if (i == cEnd) {
str += ']';
}
if (i == end) {
str += '|';
}
}
debug(str);
}
function computeIndices(source, geometry) {
var criticalStart = source.indexAtPosition(geometry.topPosition);
var criticalEnd = source.indexAtPosition(geometry.topPosition +
geometry.viewportHeight);
var canPrerender = geometry.maxItemCount -
(criticalEnd - criticalStart) - 1;
var before = geometry.switchWindow;
var after = canPrerender - before;
var lastIndex = source.fullLength() - 1;
var startIndex;
var endIndex;
if (geometry.forward) {
startIndex = Math.max(0, criticalStart - before);
endIndex = Math.min(lastIndex,
criticalEnd + after);
} else {
startIndex = Math.max(0, criticalStart - after);
endIndex = Math.min(lastIndex,
criticalEnd + before);
}
return {
cStart: criticalStart,
cEnd: criticalEnd,
start: startIndex,
end: endIndex
};
}
function recycle(items, start, end, action) {
var recyclableItems = [];
for (var i in items) {
if ((i < start) || (i > end)) {
recyclableItems.push(i);
}
}
// Put the items that are furthest away from the displayport edge
// at the end of the array.
recyclableItems.sort(function(a, b) {
return Math.abs(a - action) - Math.abs(b - action);
});
return recyclableItems;
}
function tryToPopulate(item, index, source, first) {
if (parseInt(item.dataset.index) !== index && !first) {
// The item was probably reused
return;
}
// TODO: should be in a mutation block when !first
var populateResult = source.populateItem(item, index);
if (populateResult instanceof Promise) {
item.dataset.populated = false;
populateResult.then(tryToPopulate.bind(null, item, index,
source));
return;
}
if (first) {
item.dataset.populated = true;
return;
}
// Revealing the populated item
item.style.transition = 'opacity 0.2s linear';
schedule.transition(function() {
item.dataset.populated = true;
}, item, 'transitionend').then(function() {
item.style.transition = '';
});
}
function placeItem(item, index, section, geometry, source, reload) {
if (parseInt(item.dataset.index) === index && !reload) {
// The item was probably reused
return;
}
item.dataset.position = source.positionForIndex(index);
item.dataset.index = index;
item.dataset.section = section;
var tweakedBy = parseInt(item.dataset.tweakDelta);
if (tweakedBy) {
tweakTransform(item, tweakedBy);
} else {
resetTransform(item);
}
}
function resetTransform(item) {
var position = parseInt(item.dataset.position);
item.style.webkitTransform =
item.style.transform = 'translate3d(0, ' + position + 'px, 0)';
item.dataset.tweakDelta = '';
}
function tweakTransform(item, delta) {
var position = parseInt(item.dataset.position) + delta;
item.style.webkitTransform =
item.style.transform = 'translate3d(0, ' + position + 'px, 0)';
item.dataset.tweakDelta = delta;
}
function resetTransition(item) {
item.style.transition = '';
item.style.webkitTransition = '';
}
function tweakAndTransition(item, tweak) {
return schedule.feedback(function() {
item.style.transition = 'transform 0.15s ease';
item.style.webkitTransition = '-webkit-transform 0.15s ease';
if (tweak === 0) {
setTimeout(resetTransform.bind(null, item));
return;
}
setTimeout(tweakTransform.bind(null, item, tweak));
}, item, 'transitionend')
.then(resetTransition.bind(null, item));
}
function pushDown(domItems, geometry) {
if (!domItems.length) {
return Promise.resolve();
}
return schedule.transition(function() {
for (var i = 0; i < domItems.length; i++) {
var item = domItems[i];
item.style.transition = 'transform 0.15s ease-in';
item.style.webkitTransition = '-webkit-transform 0.15s ease-in';
tweakTransform(item, geometry.itemHeight);
}
}, domItems[0], 'transitionend');
}
function reveal(list) {
var newEl = list.querySelector('li.new');
return schedule.transition(function() {
newEl.style.transition = 'opacity 0.25s ease-out';
newEl.style.webkitTransition = 'opacity 0.25s ease-out';
setTimeout(function() {
newEl.classList.remove('new');
});
}, newEl, 'transitionend').then(function() {
newEl.style.transition = '';
newEl.style.webkitTransition = '';
});
}
function cleanInlineStyles(domItems) {
return schedule.mutation(function() {
for (var i = 0; i < domItems.length; i++) {
var item = domItems[i];
item.style.transition = '';
item.style.webkitTransition = '';
resetTransform(item);
}
domItems[0] && domItems[0].scrollTop; // flushing
});
}
function toggleEditClass(list, domItems, editing) {
if (!domItems.length) {
return Promise.resolve();
}
return schedule.feedback(function() {
for (var i = 0; i < domItems.length; i++) {
var item = domItems[i];
var overlay = item.querySelector('.overlay');
overlay.dataset.anim = editing ? 'reveal' : 'hide';
item.classList.toggle('edit', editing);
}
}, list, 'animationend');
}
function setupForDragging(scheduler, item, on) {
return schedule.mutation(function() {
item.parentNode.classList.toggle('reordering', on);
item.style.zIndex = on ? '1000' : '';
item.style.boxShadow = on ? '0 0 3px 1px #bcbcbc' : '';
});
}
function toggleDraggingClass(scheduler, item, on) {
return schedule.feedback(function() {
var overlay = item.querySelector('.overlay');
overlay.dataset.anim = on ? 'hide' : 'reveal';
}, item, 'animationend');
}
function computeChanges(context, geometry, domItems, delta) {
if (!context.item) {
return;
}
var draggedItem = context.item;
var draggedOriginal = parseInt(draggedItem.dataset.position);
var draggedTweaked = draggedOriginal + delta;
for (var i = 0; i < domItems.length; i++) {
var item = domItems[i];
if (item === draggedItem) {
continue;
}
// TODO: support re-ordering accross sections
if (item.dataset.section !== draggedItem.dataset.section) {
continue;
}
var itemOriginal = parseInt(item.dataset.position);
var itemTweaked = itemOriginal + parseInt(item.dataset.tweakDelta);
if ((itemOriginal < draggedOriginal) && (draggedTweaked < itemOriginal)) {
context.moveDown.add(item);
} else {
if (draggedTweaked > itemTweaked) {
context.moveDown.delete(item);
}
}
if ((itemOriginal > draggedOriginal) && (draggedTweaked > itemOriginal)) {
context.moveUp.add(item);
} else {
if (draggedTweaked < itemTweaked) {
context.moveUp.delete(item);
}
}
}
}
function applyChanges(context, geometry, domItems) {
if (!context.item) {
return Promise.resolve();
}
var itemHeight = geometry.itemHeight;
var draggedItem = context.item;
var promises = [];
for (var i = 0; i < domItems.length; i++) {
var item = domItems[i];
if (item === draggedItem) {
continue;
}
// Reset
if (item.dataset.tweakDelta &&
!context.moveUp.has(item) && !context.moveDown.has(item)) {
promises.push(tweakAndTransition(item, 0));
}
// Moving down
if (context.moveDown.has(item)) {
if (parseInt(item.dataset.tweakDelta) !== itemHeight) {
promises.push(tweakAndTransition(item, itemHeight));
}
}
// Moving up
if (context.moveUp.has(item)) {
if (parseInt(item.dataset.tweakDelta) !== itemHeight * -1) {
promises.push(tweakAndTransition(item, itemHeight * -1));
}
}
}
return Promise.all(promises);
}
function moveInPlace(context, geometry) {
var li = context.item;
// We're already in place
if (!li.dataset.tweakDelta) {
return Promise.resolve();
}
var position = parseInt(li.dataset.position);
var taintedPosition = position + parseInt(li.dataset.tweakDelta);
var itemHeight = geometry.itemHeight;
var newPosition = position + context.moveUp.size * itemHeight -
context.moveDown.size * itemHeight;
var duration = (Math.abs(taintedPosition - newPosition) / itemHeight) * 150;
return schedule.feedback(function() {
li.style.transition = 'transform ' + duration + 'ms linear';
li.style.webkitTransition = '-webkit-transform ' + duration + 'ms linear';
setTimeout(tweakTransform.bind(null, li, (newPosition - position)));
}, li, 'transitionend');
}
/**
* Utils
*/
function schedulerShim() {
var raf = window.requestAnimationFrame;
return {
mutation: function(block) { return Promise.resolve(block()); },
transition: function(block, el, event, timeout) {
block();
return after(el, event, timeout || 500);
},
// Not sure if this should be different from .transition()
feedback: function(block, el, event, timeout) {
block();
return after(el, event, timeout || 500);
},
attachDirect: function(el, event, fn) {
fn._raffed = function(e) { raf(function() { fn(e); } ); };
on(el, event, fn._raffed);
},
detachDirect: function(el, event, fn) {
off(el, event, fn._raffed);
}
};
function after(target, event, timeout) {
return new Promise(function(resolve) {
var timer = timeout && setTimeout(cb, timeout);
on(target, event, cb);
function cb() {
off(target, event, cb);
clearTimeout(timer);
resolve();
}
});
}
}
// Shorthand
function on(el, name, fn) { el.addEventListener(name, fn); }
function off(el, name, fn) { el.removeEventListener(name, fn); }
});})((typeof define)[0]=='f'&&define.amd?define:(function(n,w){'use strict';
return(typeof module)[0]=='o'?function(c){c(require,exports,module);}:
function(c){var m={exports:{}};c(function(n){w[n];},m.exports,m);
w[n]=m.exports;};})('FastList',this));

View file

@ -0,0 +1,24 @@
{
"name": "fast-list",
"main": "fast-list.js",
"devDependencies": {
"jshint": "^2.7.0",
"karma": "^0.12.31",
"karma-firefox-launcher": "^0.1.4",
"karma-chrome-launcher": "^0.1.4",
"karma-mocha": "^0.1.10",
"karma-sinon-chai": "^0.3.0"
},
"scripts": {
"lint": "./node_modules/jshint/bin/jshint lib/*.js test/*.js",
"test": "./node_modules/karma/bin/karma start test/karma.conf --single-run",
"test-dev": "./node_modules/karma/bin/karma start test/karma.conf"
},
"repository": {
"type": "git",
"url": "git://github.com/gaia-components/fast-list.git"
},
"bugs": {
"url": "https://github.com/gaia-components/fast-list/issues"
}
}

View file

@ -0,0 +1,18 @@
{
"name": "gaia-component",
"version": "0.3.8",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-component.js",
"license": "MIT",
"ignore": [
"**/.*",
"README.md",
"package.json",
"node_modules",
"bower_components",
"test",
"tests"
]
}

View file

@ -0,0 +1,420 @@
/* globals define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Locals
*/
var textContent = Object.getOwnPropertyDescriptor(Node.prototype,
'textContent');
var innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
var removeAttribute = Element.prototype.removeAttribute;
var setAttribute = Element.prototype.setAttribute;
var noop = function() {};
/**
* Register a new component.
*
* @param {String} name
* @param {Object} props
* @return {constructor}
* @public
*/
exports.register = function(name, props) {
var baseProto = getBaseProto(props.extends);
var template = props.template || baseProto.templateString;
// Components are extensible by default but can be declared
// as non extensible as an optimization to avoid
// storing the template strings
var extensible = props.extensible = props.hasOwnProperty('extensible')?
props.extensible : true;
// Clean up
delete props.extends;
// Pull out CSS that needs to be in the light-dom
if (template) {
// Stores the string to be reprocessed when
// a new component extends this one
if (extensible && props.template) {
props.templateString = props.template;
}
var output = processCss(template, name);
props.template = document.createElement('template');
props.template.innerHTML = output.template;
props.lightCss = output.lightCss;
props.globalCss = props.globalCss || '';
props.globalCss += output.globalCss;
}
// Inject global CSS into the document,
// and delete as no longer needed
injectGlobalCss(props.globalCss);
delete props.globalCss;
// Merge base getter/setter attributes with the user's,
// then define the property descriptors on the prototype.
var descriptors = mixin(props.attrs || {}, base.descriptors);
// Store the orginal descriptors somewhere
// a little more private and delete the original
props._attrs = props.attrs;
delete props.attrs;
// Create the prototype, extended from base and
// define the descriptors directly on the prototype
var proto = createProto(baseProto, props);
Object.defineProperties(proto, descriptors);
// Register the custom-element and return the constructor
try {
return document.registerElement(name, { prototype: proto });
} catch (e) {
if (e.name !== 'NotSupportedError') {
throw e;
}
}
};
var base = {
properties: {
GaiaComponent: true,
attributeChanged: noop,
attached: noop,
detached: noop,
created: noop,
createdCallback: function() {
if (this.rtl) { addDirObserver(); }
injectLightCss(this);
this.created();
},
/**
* It is very common to want to keep object
* properties in-sync with attributes,
* for example:
*
* el.value = 'foo';
* el.setAttribute('value', 'foo');
*
* So we support an object on the prototype
* named 'attrs' to provide a consistent
* way for component authors to define
* these properties. When an attribute
* changes we keep the attr[name]
* up-to-date.
*
* @param {String} name
* @param {String||null} from
* @param {String||null} to
*/
attributeChangedCallback: function(name, from, to) {
var prop = toCamelCase(name);
if (this._attrs && this._attrs[prop]) { this[prop] = to; }
this.attributeChanged(name, from, to);
},
attachedCallback: function() { this.attached(); },
detachedCallback: function() { this.detached(); },
/**
* A convenient method for setting up
* a shadow-root using the defined template.
*
* @return {ShadowRoot}
*/
setupShadowRoot: function() {
if (!this.template) { return; }
var node = document.importNode(this.template.content, true);
this.createShadowRoot().appendChild(node);
return this.shadowRoot;
},
/**
* Sets an attribute internally
* and externally. This is so that
* we can style internal shadow-dom
* content.
*
* @param {String} name
* @param {String} value
*/
setAttr: function(name, value) {
var internal = this.shadowRoot.firstElementChild;
setAttribute.call(internal, name, value);
setAttribute.call(this, name, value);
},
/**
* Removes an attribute internally
* and externally. This is so that
* we can style internal shadow-dom
* content.
*
* @param {String} name
* @param {String} value
*/
removeAttr: function(name) {
var internal = this.shadowRoot.firstElementChild;
removeAttribute.call(internal, name);
removeAttribute.call(this, name);
}
},
descriptors: {
textContent: {
set: function(value) {
textContent.set.call(this, value);
if (this.lightStyle) { this.appendChild(this.lightStyle); }
},
get: function() {
return textContent.get();
}
},
innerHTML: {
set: function(value) {
innerHTML.set.call(this, value);
if (this.lightStyle) { this.appendChild(this.lightStyle); }
},
get: innerHTML.get
}
}
};
/**
* The default base prototype to use
* when `extends` is undefined.
*
* @type {Object}
*/
var defaultPrototype = createProto(HTMLElement.prototype, base.properties);
/**
* Returns a suitable prototype based
* on the object passed.
*
* @private
* @param {HTMLElementPrototype|undefined} proto
* @return {HTMLElementPrototype}
*/
function getBaseProto(proto) {
if (!proto) { return defaultPrototype; }
proto = proto.prototype || proto;
return !proto.GaiaComponent ?
createProto(proto, base.properties) : proto;
}
/**
* Extends the given proto and mixes
* in the given properties.
*
* @private
* @param {Object} proto
* @param {Object} props
* @return {Object}
*/
function createProto(proto, props) {
return mixin(Object.create(proto), props);
}
/**
* Detects presence of shadow-dom
* CSS selectors.
*
* @private
* @return {Boolean}
*/
var hasShadowCSS = (function() {
var div = document.createElement('div');
try { div.querySelector(':host'); div.querySelector('::content'); return true; }
catch (e) { return false; }
})();
/**
* Regexs used to extract shadow-css
*
* @type {Object}
*/
var regex = {
shadowCss: /(?:\:host|\:\:content)[^{]*\{[^}]*\}/g,
':host': /(?:\:host)/g,
':host()': /\:host\((.+)\)(?: \:\:content)?/g,
':host-context': /\:host-context\((.+)\)([^{,]+)?/g,
'::content': /(?:\:\:content)/g
};
/**
* Extracts the :host and ::content rules
* from the shadow-dom CSS and rewrites
* them to work from the <style scoped>
* injected at the root of the component.
*
* @private
* @return {String}
*/
function processCss(template, name) {
var globalCss = '';
var lightCss = '';
if (!hasShadowCSS) {
template = template.replace(regex.shadowCss, function(match) {
var hostContext = regex[':host-context'].exec(match);
if (hostContext) {
globalCss += match
.replace(regex['::content'], '')
.replace(regex[':host-context'], '$1 ' + name + '$2')
.replace(/ +/g, ' '); // excess whitespace
} else {
lightCss += match
.replace(regex[':host()'], name + '$1')
.replace(regex[':host'], name)
.replace(regex['::content'], name);
}
return '';
});
}
return {
template: template,
lightCss: lightCss,
globalCss: globalCss
};
}
/**
* Some CSS rules, such as @keyframes
* and @font-face don't work inside
* scoped or shadow <style>. So we
* have to put them into 'global'
* <style> in the head of the
* document.
*
* @private
* @param {String} css
*/
function injectGlobalCss(css) {
if (!css) {return;}
var style = document.createElement('style');
style.innerHTML = css.trim();
headReady().then(function() {
document.head.appendChild(style);
});
}
/**
* Resolves a promise once document.head is ready.
*
* @private
*/
function headReady() {
return new Promise(function(resolve) {
if (document.head) { return resolve(); }
window.addEventListener('load', function fn() {
window.removeEventListener('load', fn);
resolve();
});
});
}
/**
* The Gecko platform doesn't yet have
* `::content` or `:host`, selectors,
* without these we are unable to style
* user-content in the light-dom from
* within our shadow-dom style-sheet.
*
* To workaround this, we clone the <style>
* node into the root of the component,
* so our selectors are able to target
* light-dom content.
*
* @private
*/
function injectLightCss(el) {
if (hasShadowCSS) { return; }
el.lightStyle = document.createElement('style');
el.lightStyle.setAttribute('scoped', '');
el.lightStyle.innerHTML = el.lightCss;
el.appendChild(el.lightStyle);
}
/**
* Convert hyphen separated
* string to camel-case.
*
* Example:
*
* toCamelCase('foo-bar'); //=> 'fooBar'
*
* @private
* @param {Sring} string
* @return {String}
*/
function toCamelCase(string) {
return string.replace(/-(.)/g, function replacer(string, p1) {
return p1.toUpperCase();
});
}
/**
* Observer (singleton)
*
* @type {MutationObserver|undefined}
*/
var dirObserver;
/**
* Observes the document `dir` (direction)
* attribute and dispatches a global event
* when it changes.
*
* Components can listen to this event and
* make internal changes if need be.
*
* @private
*/
function addDirObserver() {
if (dirObserver) { return; }
dirObserver = new MutationObserver(onChanged);
dirObserver.observe(document.documentElement, {
attributeFilter: ['dir'],
attributes: true
});
function onChanged(mutations) {
document.dispatchEvent(new Event('dirchanged'));
}
}
/**
* Copy the values of all properties from
* source object `target` to a target object `source`.
* It will return the target object.
*
* @private
* @param {Object} target
* @param {Object} source
* @returns {Object}
*/
function mixin(target, source) {
for (var key in source) {
target[key] = source[key];
}
return target;
}
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-component',this));

View file

@ -0,0 +1,31 @@
{
"name": "gaia-dialog",
"version": "0.2.3",
"homepage": "https://github.com/gaia-components/gaia-dialog",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-dialog.js",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"examples",
"test",
"tests",
"README.md"
],
"devDependencies": {
"base": "gaia-components/base",
"pressed": "gaia-components/pressed",
"gaia-fonts": "gaia-components/gaia-fonts",
"gaia-theme": "gaia-components/gaia-theme",
"gaia-button": "gaia-components/gaia-button"
},
"dependencies": {
"gaia-text-input": "gaia-components/gaia-text-input",
"gaia-icons": "gaia-components/gaia-icons#^0.8.0",
"gaia-component": "gaia-components/gaia-component#^0.3.7"
}
}

View file

@ -0,0 +1,111 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-action', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog'),
submit: this.shadowRoot.querySelector('.submit'),
cancel: this.shadowRoot.querySelector('.cancel')
};
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.close());
},
template: `
<gaia-dialog>
<section>
<content select="h1"></content>
</section>
<content select="button"></content>
<button on-click="close" class="cancel">Cancel</button>
</gaia-dialog>
<style>
:host {
display: none;
}
:host[opened],
:host.animating {
display: block;
position: fixed;
width: 100%;
height: 100%;
}
/** Button
---------------------------------------------------------*/
::content section {
padding: 33px 16px;
}
::content button {
position: relative;
display: block;
width: 100%;
height: 50px;
margin: 0;
border: 0;
padding: 0rem 25px;
font: inherit;
background: var(--color-beta);
color: var(--color-epsilon);
transition: all 200ms;
transition-delay: 300ms;
}
::content button:after {
content: '';
display: block;
position: absolute;
height: 1px;
left: 6px;
right: 6px;
top: 49px;
background: #E7E7E7;
}
::content button:last-child:after {
display: none;
}
::content button:active {
background-color: var(--highlight-color);
color: #fff;
transition: none;
}
::content button:active:after {
background: var(--highlight-color);
transition: none;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-action',this));

View file

@ -0,0 +1,59 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-alert', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog')
};
this.els.dialog.addEventListener('closed',
GaiaDialogProto.hide.bind(this));
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
return this.els.dialog.close()
.then(GaiaDialogProto.hide.bind(this));
},
template: `
<gaia-dialog>
<section>
<p><content></content></p>
</section>
<div>
<button class="submit primary" on-click="close">Ok</button>
</div>
</gaia-dialog>
<style>
:host {
display: none;
position: fixed;
width: 100%;
height: 100%;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-alert',this));

View file

@ -0,0 +1,69 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-confirm', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog'),
submit: this.shadowRoot.querySelector('.submit'),
cancel: this.shadowRoot.querySelector('.cancel')
};
this.els.cancel.addEventListener('click', this.close.bind(this));
this.els.submit.addEventListener('click', this.close.bind(this));
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.close());
},
template: `
<gaia-dialog>
<section>
<p><content></content></p>
</section>
<fieldset>
<button class="cancel">Cancel</button>
<button class="submit danger">Confirm</button>
</fieldset>
</gaia-dialog>
<style>
:host {
display: none;
}
:host[opened],
:host.animating {
display: block;
position: fixed;
width: 100%;
height: 100%;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-confirm',this));

View file

@ -0,0 +1,92 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-menu', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog')
};
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.close());
},
template: `
<gaia-dialog>
<div class="items"><content select="button"></content></div>
</gaia-dialog>
<style>
:host {
display: none;
position: fixed;
width: 100%;
height: 100%;
}
::content > button {
position: relative;
display: block;
width: 100%;
height: 50px;
line-height: 51px;
margin: 0;
border: 0;
padding: 0rem 16px;
font: inherit;
font-style: italic;
text-align: start;
background: var(--color-beta);
color: var(--highlight-color);
}
::content > button[data-icon]:before {
width: 50px;
font-size: 22px;
vertical-align: middle;
}
/** Button Divider Line
---------------------------------------------------------*/
::content > button:after {
content: '';
display: block;
position: absolute;
height: 1px;
left: 6px;
right: 6px;
top: 49px;
background: #E7E7E7;
}
::content > button:last-of-type:after {
display: none;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-menu',this));

View file

@ -0,0 +1,83 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
require('gaia-text-input');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-prompt', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog'),
input: this.shadowRoot.querySelector('gaia-text-input'),
submit: this.shadowRoot.querySelector('.submit'),
cancel: this.shadowRoot.querySelector('.cancel')
};
this.els.dialog.addEventListener('opened', () => {
this.setAttribute('opened', '');
});
this.els.dialog.addEventListener('closed', () => {
this.removeAttribute('opened');
});
this.els.input.placeholder = this.firstChild.textContent;
this.els.cancel.addEventListener('click', this.close.bind(this));
this.els.submit.addEventListener('click', this.close.bind(this));
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.close());
},
template: `
<gaia-dialog>
<div><gaia-text-input></gaia-text-input></div>
<fieldset>
<button class="cancel">Cancel</button>
<button class="submit primary">Ok</button>
</fieldset>
</gaia-dialog>
<style>
:host {
display: none;
}
:host[opened],
:host.animating {
display: block;
position: fixed;
width: 100%;
height: 100%;
}
gaia-text-input {
margin: 16px !important;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-prompt',this));

View file

@ -0,0 +1,230 @@
/* global define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Dependencies
*/
var GaiaDialogProto = require('gaia-dialog').prototype;
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-dialog-select', {
created: function() {
this.setupShadowRoot();
this.els = {
dialog: this.shadowRoot.querySelector('gaia-dialog'),
submit: this.shadowRoot.querySelector('.submit'),
cancel: this.shadowRoot.querySelector('.cancel'),
list: this.shadowRoot.querySelector('ul')
};
this.multiple = this.hasAttribute('multiple');
this.els.list.addEventListener('click', this.onListClick.bind(this));
this.els.submit.addEventListener('click', this.close.bind(this));
this.els.cancel.addEventListener('click', this.close.bind(this));
},
open: function(e) {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.open(e));
},
close: function() {
return GaiaDialogProto.show.call(this)
.then(() => this.els.dialog.close());
},
onListClick: function(e) {
var el = e.target.closest('li');
var selected = el.getAttribute('aria-selected') === 'true';
if (this.multiple) { this.onChangeMultiple(el, selected); }
else { this.onChange(el, selected); }
},
onChange: function(el, selected) {
this.clearSelected();
if (!selected) { el.setAttribute('aria-selected', !selected); }
this.fireChange();
this.close();
},
onChangeMultiple: function(el, selected) {
el.setAttribute('aria-selected', !selected);
this.fireChange();
},
clearSelected: function() {
[].forEach.call(this.selectedOptions, function(option) {
option.removeAttribute('aria-selected');
});
},
fireChange: function() {
var e = new CustomEvent('change', { detail: { value: this.valueString }});
this.dispatchEvent(e);
},
attrs: {
multiple: {
get: function() { return !!this._multiple; },
set: function(value) {
value = value === '' ? true : value;
if (value === this._multiple) { return; }
if (!value) {
this._multiple = false;
this.removeAttribute('multiple');
} else {
this._multiple = true;
this.setAttribute('multiple', '');
}
}
},
options: {
get: function() { return this.querySelectorAll('li'); }
},
selectedOptions: {
get: function() {
return this.querySelectorAll('li[aria-selected="true"]');
}
},
selected: {
get: function() { return this.selectedOptions[0]; }
},
value: {
get: function() {
var selected = this.selectedOptions[0];
return selected && selected.getAttribute('value');
}
},
valueString: {
get: function() {
var selected = this.selected;
return selected && selected.textContent;
}
},
length: {
get: function() { return this.options.length; }
}
},
template: `
<gaia-dialog>
<content select="h1"></content>
<ul><content select="li"></content></ul>
<fieldset>
<button class="cancel">Cancel</button>
<button class="submit primary">Select</button>
</fieldset>
</gaia-dialog>
<style>
:host {
display: none;
position: fixed;
width: 100%;
height: 100%;
}
/** Title (duplicate from gaia-dialog)
---------------------------------------------------------*/
::content h1 {
padding: 16px;
font-size: 23px;
line-height: 26px;
font-weight: 200;
font-style: italic;
color: #858585;
}
/** List
---------------------------------------------------------*/
ul {
display: block;
padding: 0;
margin: 0;
}
/** Options
---------------------------------------------------------*/
::content li {
position: relative;
display: block;
padding: 16px;
text-align: start;
-moz-user-select: none;
cursor: pointer;
}
::content li[aria-selected='true'] {
color: #00CAF2;
font-weight: normal;
}
::content li:after {
content: '';
display: block;
position: absolute;
height: 1px;
left: 16px;
right: 16px;
bottom: 1px;
background: #E7E7E7;
}
::content li:last-of-type:after {
display: none
}
/** Buttons
---------------------------------------------------------*/
::host:not([multiple]) .submit {
display: none !important;
}
::host:not([multiple]) .cancel {
width: 100% !important;
}
::host:not([multiple]) .cancel:after {
display: none% !important;
}
/** Tick Icon
---------------------------------------------------------*/
::content li[aria-selected='true']:before {
content: 'tick';
position: absolute;
top: 50%; right: 20px;
display: block;
margin-top: -12px;
font-family: 'gaia-icons';
font-style: normal;
font-size: 14px;
font-weight: 500;
text-rendering: optimizeLegibility;
}
</style>`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog-select',this));

View file

@ -0,0 +1,475 @@
;(function(define){'use strict';define((require,exports,module) => {
/**
* Dependencies
*/
var component = require('gaia-component');
/**
* Simple logger (toggle 0)
*
* @type {Function}
*/
var debug = 0 ? console.log.bind(console) : () => {};
/**
* Use the dom-scheduler if it's around,
* else fallback to fake shim.
*
* @type {Object}
*/
var schedule = (window.scheduler && typeof window.scheduler.mutation === "function" ? window.scheduler : null) || {
mutation: block => Promise.resolve(block()),
transition: (block, el, event, timeout) => {
block();
return after(el, event, timeout || 500);
}
};
/**
* Exports
*/
module.exports = component.register('gaia-dialog', {
created() {
this.setupShadowRoot();
this.els = {
inner: this.shadowRoot.querySelector('.dialog-inner'),
background: this.shadowRoot.querySelector('.background'),
window: this.shadowRoot.querySelector('.window')
};
this.shadowRoot.addEventListener('click', e => this.onClick(e));
},
onClick(e) {
var el = e.target.closest('[on-click]');
if (!el) { return; }
debug('onClick');
var method = el.getAttribute('on-click');
if (typeof this[method] == 'function') { this[method](); }
},
open(options) {
if (this.isOpen) { return; }
debug('open dialog');
this.isOpen = true;
this.show()
.then(() => this.animateBackgroundIn(options))
.then(() => this.animateWindowIn())
.then(() => this.dispatch('opened'));
},
close(options) {
if (!this.isOpen) { return; }
debug('close dialog');
this.isOpen = false;
this.animateWindowOut()
.then(() => this.animateBackgroundOut())
.then(() => this.hide())
.then(() => this.dispatch('closed'));
},
animateBackgroundIn(options) {
if (options) { return this.animateBackgroundInFrom(options); }
var el = this.els.background;
return schedule.transition(() => {
debug('animate background in');
el.classList.remove('animate-out');
el.classList.add('animate-in');
}, el, 'animationend');
},
animateBackgroundOut() {
var el = this.els.background;
return schedule.transition(() => {
debug('animate background out');
el.classList.add('animate-out');
el.classList.remove('animate-in');
}, el, 'animationend')
.then(() => el.style = '');
},
animateBackgroundInFrom(pos) {
var el = this.els.background;
var scale = Math.sqrt(window.innerWidth * window.innerHeight) / 15;
var duration = scale * 9;
return schedule.mutation(() => {
el.classList.add('circular');
el.classList.remove('animate-out');
el.style.transform = `translate(${pos.clientX}px, ${pos.clientY}px)`;
el.style.transitionDuration = duration + 'ms';
el.offsetTop; // Hack, any ideas?
})
.then(() => {
return schedule.transition(() => {
debug('animate background in from', pos);
el.style.transform += ` scale(${scale})`;
el.style.opacity = 1;
}, el, 'transitionend', duration * 1.5);
});
},
show() {
return schedule.mutation(() => {
debug('show');
this.style.display = 'block';
});
},
hide() {
return schedule.mutation(() => {
debug('hide');
this.style.display = 'none';
});
},
animateWindowIn() {
var el = this.els.window;
return schedule.transition(() => {
debug('animate window in');
el.classList.add('animate-in');
el.classList.remove('animate-out');
}, el, 'animationend');
},
animateWindowOut() {
var el = this.els.window;
return schedule.transition(() => {
debug('animate window out');
el.classList.add('animate-out');
el.classList.remove('animate-in');
}, el, 'animationend');
},
dispatch(name) {
this.dispatchEvent(new CustomEvent(name));
},
attrs: {
opened: {
get: function() { return !!this.isOpen; },
set: function(value) {
value = value === '' || value;
if (!value) { this.close(); }
else { this.open(); }
}
}
},
template: `
<div class="dialog-inner">
<div class="background" on-click="close"></div>
<div class="window"><content></content></div>
</div>
<style>
::content * {
box-sizing: border-box;
font-weight: inherit;
font-size: inherit;
}
::content p,
::content h1,
::content h2,
::content h3,
::content h4,
::content button,
::content fieldset {
padding: 0;
margin: 0;
border: 0;
}
:host {
display: none;
position: fixed;
top: 0px; left: 0px;
width: 100%;
height: 100%;
z-index: 200;
font-style: italic;
text-align: center;
overflow: hidden;
}
/** Inner
---------------------------------------------------------*/
.dialog-inner {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
/** Background
---------------------------------------------------------*/
.background {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
opacity: 0;
background: rgba(199,199,199,0.85);
}
/**
* .circular
*/
.background.circular {
width: 40px;
height: 40px;
margin: -20px;
border-radius: 50%;
will-change: transform, opacity;
transition-property: opacity, transform;
transition-timing-function: linear;
}
/**
* .animate-in
*/
.background.animate-in {
animation-name: gaia-dialog-fade-in;
animation-duration: 260ms;
animation-fill-mode: forwards;
}
/**
* .animate-out
*/
.background.animate-out {
animation-name: gaia-dialog-fade-out;
animation-duration: 260ms;
animation-fill-mode: forwards;
opacity: 1;
}
/** Window
---------------------------------------------------------*/
.window {
position: relative;
width: 90%;
max-width: 350px;
margin: auto;
box-shadow: 0 1px 0 0px rgba(0,0,0,0.15);
background: var(--color-iota);
transition: opacity 300ms;
opacity: 0;
}
.window.animate-in {
animation-name: gaia-dialog-entrance;
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.275);
animation-fill-mode: forwards;
opacity: 1;
}
.window.animate-out {
animation-name: gaia-dialog-fade-out;
animation-duration: 150ms;
animation-timing-function: linear;
animation-fill-mode: forwards;
opacity: 1;
}
/** Title
---------------------------------------------------------*/
::content h1 {
padding: 16px;
font-size: 23px;
line-height: 26px;
font-weight: 200;
font-style: italic;
color: #858585;
}
::content strong {
font-weight: 700;
}
::content small {
font-size: 0.8em;
}
/** Section
---------------------------------------------------------*/
::content section {
padding: 33px 18px;
color: #858585;
}
::content section > *:not(:last-child) {
margin-bottom: 13px;
}
/** Paragraphs
---------------------------------------------------------*/
::content p {
text-align: -moz-start;
}
/** Buttons
---------------------------------------------------------*/
::content button {
position: relative;
display: block;
width: 100%;
height: 50px;
margin: 0;
border: 0;
padding: 0rem 16px;
cursor: pointer;
font: inherit;
background: var(--color-beta);
color: var(--color-epsilon);
transition: all 200ms;
transition-delay: 300ms;
border-radius: 0;
}
/**
* .primary
*/
::content button.primary {
color: var(--highlight-color);
}
/**
* .danger
*/
::content button.danger {
color: var(--color-destructive);
}
/**
* Disabled buttons
*/
::content button[disabled] {
color: var(--color-zeta);
}
/** Button Divider Line
---------------------------------------------------------*/
::content button:after {
content: '';
display: block;
position: absolute;
height: 1px;
left: 6px;
right: 6px;
top: 49px;
background: #E7E7E7;
}
::content button:last-of-type:after {
display: none;
}
::content button:active {
background-color: var(--highlight-color);
color: #fff;
transition: none;
}
::content button:active:after {
background: var(--highlight-color);
transition: none;
}
::content button[data-icon]:before {
float: left;
}
/** Fieldset (button group)
---------------------------------------------------------*/
::content fieldset {
overflow: hidden;
}
::content fieldset button {
position: relative;
float: left;
width: 50%;
}
::content fieldset button:after {
content: '';
display: block;
position: absolute;
top: 6px;
bottom: 6px;
right: 0px;
left: auto;
width: 1px;
height: calc(100% - 12px);
background: #e7e7e7;
transition: all 200ms;
transition-delay: 200ms;
}
</style>`,
globalCss: `
@keyframes gaia-dialog-entrance {
0% { transform: translateY(100px); }
100% { transform: translateY(0px); }
}
@keyframes gaia-dialog-fade-in {
0% { opacity: 0 }
100% { opacity: 1 }
}
@keyframes gaia-dialog-fade-out {
0% { opacity: 1 }
100% { opacity: 0 }
}`
});
/**
* Utils
*/
function after(target, event, timeout) {
return new Promise(resolve => {
var timer = timeout && setTimeout(cb, timeout);
target.addEventListener(event, cb);
function cb() {
target.removeEventListener(event, cb);
clearTimeout(timer);
resolve();
}
});
}
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-dialog',this));

View file

@ -0,0 +1,124 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>&lt;gaia-dialog&gt;</title>
<meta name="description" content="gaia-dialog demo" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" type="text/css" href="bower_components/base/base.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-theme/gaia-theme.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-fonts/style.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-icons/gaia-icons.css">
<script src="bower_components/gaia-theme/lib/gaia-theme-selector.js"></script>
<script src="bower_components/pressed/pressed.js"></script>
<script src="bower_components/gaia-component/gaia-component.js"></script>
<script src="bower_components/gaia-button/gaia-button.js"></script>
<script src="bower_components/gaia-text-input/gaia-text-input.js"></script>
<!-- <script src="bower_components/dom-scheduler/lib/dom-scheduler.js"></script> -->
<script src="gaia-dialog.js"></script>
<script src="gaia-dialog-alert.js"></script>
<script src="gaia-dialog-confirm.js"></script>
<script src="gaia-dialog-prompt.js"></script>
<script src="gaia-dialog-action.js"></script>
<script src="gaia-dialog-select.js"></script>
<script src="gaia-dialog-menu.js"></script>
<style>
body {
padding: 48px var(--base-m) 0 var(--base-m);
background-color: var(--background);
}
</style>
</head>
<body class="l-align-center">
<gaia-theme-selector></gaia-theme-selector>
<div id="buttons">
<div class="l-mb-small"><gaia-button data-for="gaia-dialog-alert">Alert</gaia-button></div>
<div class="l-mb-small"><gaia-button data-for="gaia-dialog-confirm">Confirm</gaia-button></div>
<div class="l-mb-small"><gaia-button data-for="gaia-dialog-prompt">Prompt</gaia-button></div>
<div class="l-mb-small"><gaia-button data-for="gaia-dialog-action">Action</gaia-button></div>
<div class="l-mb-small"><gaia-button data-for="gaia-dialog-select">Select</gaia-button></div>
<div class="l-mb-small"><gaia-button data-for="gaia-dialog-select[multiple]">Select Multiple</gaia-button></div>
<div class="l-mb-small"><gaia-button data-for="gaia-dialog-menu">Menu</gaia-button></div>
<div class="l-mb-small"><gaia-button data-for="#custom-dialog-1">Custom</gaia-button></div>
</div>
<div id="dialogs">
<gaia-dialog-alert>No SIM card is present</gaia-dialog-alert>
<gaia-dialog-confirm>Are you sure you want to delete this contact?</gaia-dialog-confirm>
<gaia-dialog-prompt>Device name</gaia-dialog-prompt>
<gaia-dialog-action>
<h1>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eu mollis tortor.</h1>
<button on-click="close">Action 1</button>
<button on-click="close">Action 2</button>
</gaia-dialog-action>
<gaia-dialog-select>
<h1>Ring tone</h1>
<li>Classic prism</li>
<li>Wallphone</li>
<li>Disco drive</li>
<li>Touchmatic</li>
<li>Classic courier</li>
<li>Hacker</li>
</gaia-dialog-select>
<gaia-dialog-select multiple>
<h1>Ring tone</h1>
<li>Classic prism</li>
<li>Wallphone</li>
<li>Disco drive</li>
<li>Touchmatic</li>
<li>Classic courier</li>
<li>Hacker</li>
</gaia-dialog-select>
<gaia-dialog-menu>
<button on-click="close" data-icon="firefox">Open in new window</button>
<button on-click="close" data-icon="firefox">Add to Home Screen</button>
<button on-click="close" data-icon="firefox">Share link</button>
<button on-click="close" data-icon="firefox">Settings</button>
</gaia-dialog-menu>
<gaia-dialog id="custom-dialog-1">
<section>
<h1>Application Download</h1>
<p>Do you want to download and install this application?</p>
</section>
<fieldset>
<button on-click="close">Cancel</button>
<button on-click="close">Action</button>
</fieldset>
</gaia-dialog>
<gaia-dialog>
<h1>Main Heading</h1>
<h2>Sub Heading</h2>
<hr></hr>
<p>Lorem ipsum dolor sit amet, consecutor adipisicing elit, sed do eiusod tempor incidenti ut la et dolore magna...</p>
<button on-click="close">Action</button>
<button on-click="close">Cancel</button>
</gaia-dialog>
<gaia-dialog>
<h1>Confirmation</h1>
<p>Lorem ipsum dolor sit amet, consecutor adipisicing elit, sed do eiusod tempor incidenti ut la et dolore magna...</p>
<hr>
<p>Lorem ipsum dolor sit amet, consecutor adipisicing elit, sed do eiusod tempor incidenti ut la et dolore magna...</p>
<button on-click="close">Action</button>
<button on-click="close">Action</button>
<button on-click="close">Cancel</button>
</gaia-dialog>
</div>
<script>
var buttons = document.querySelectorAll('gaia-button');
[].forEach.call(buttons, function(button) {
button.addEventListener('click', function(e) {
var dialog = document.querySelector(button.dataset.for);
dialog.open(e);
});
});
</script>
</body>
</html>

View file

@ -0,0 +1,27 @@
{
"name": "gaia-dialog",
"version": "0.2.3",
"homepage": "https://github.com/gaia-components/gaia-dialog",
"license": "MIT",
"main": "gaia-dialog.js",
"devDependencies": {
"babel": "^5.2.12",
"bower": "^1.4.1",
"browserify": "^10.1.0",
"jshint": "^2.7.0"
},
"scripts": {
"install": "bower install",
"lint": "jshint gaia-dialog*.js"
},
"repository": {
"type": "git",
"url": "git://github.com/gaia-components/gaia-dialog.git"
},
"bugs": {
"url": "https://github.com/gaia-components/gaia-dialog/issues"
},
"browser": {
"gaia-component": "./bower_components/gaia-component/gaia-component.js"
}
}

View file

@ -0,0 +1,16 @@
{
"name": "gaia-fonts",
"version": "0.2.1",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "style.css",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}

View file

@ -0,0 +1,54 @@
@font-face {
font-family: "FiraSans";
src: url("fonts/FiraSans-Light.woff") format("woff");
font-weight: 300; /* lighter */
}
@font-face {
font-family: "FiraSans";
src: url("fonts/FiraSans-LightItalic.woff") format("woff");
font-style: italic;
font-weight: 300; /* lighter */
}
@font-face {
font-family: "FiraSans";
src: url("fonts/FiraSans-Regular.woff") format("woff");
font-style: normal;
font-weight: normal; /* 400 */
}
@font-face {
font-family: "FiraSans";
src: url("fonts/FiraSans-RegularItalic.woff") format("woff");
font-style: italic;
font-weight: normal; /* 400 */
}
@font-face {
font-family: "FiraSans";
src: url("fonts/FiraSans-Medium.woff") format("woff");
font-style: italic;
font-weight: bold; /* 700 */
}
@font-face {
font-family: "FiraSans";
src: url("fonts/FiraSans-MediumItalic.woff") format("woff");
font-style: italic;
font-weight: bold; /* 700 */
}
@font-face {
font-family: "FiraSans";
src: url("fonts/FiraSans-Bold.woff") format("woff");
font-weight: 800; /* bolder */
}
@font-face {
font-family: "FiraSans";
src: url("fonts/FiraSans-BoldItalic.woff") format("woff");
font-style: italic;
font-weight: 800; /* bolder */
}

View file

@ -0,0 +1,118 @@
'use strict';
// Create a report about unused icons in a project.
var fs = require('fs');
var path = require('path');
var exec = require('child_process').exec;
var optimist = require('optimist');
var cli = optimist
.usage('Create a report about unused icons in a project.' + '\n' +
'Usage: $0 path/to/your/project/');
var projectDir = cli.argv._[0];
// No folder passed? Show help message then.
if (!projectDir) {
cli.showHelp();
process.exit(0);
}
// Check if the directory exist.
var stats = fs.lstatSync(projectDir);
if (!stats.isDirectory()) {
console.log('\x1b[31mPlease provide a valid folder.\x1b[0m\n');
cli.showHelp();
process.exit(0);
}
/**
* Get the list of all the icons in the font.
* @returns {Array}
*/
function getIcons() {
var icons = fs.readdirSync('./images/');
icons = icons.map(function(icon) {
return path.basename(icon, '.svg');
});
return icons;
}
/**
* Use a grep pattern to extract used icons from files.
* @param {string} grep
* @param {Function} callback
*/
function getUsedIcons(grep, callback) {
exec(grep, function(err, stdout, stderr) {
var usedIcons = stdout
.trim()
.split('\n')
.map(function(icon) {
// Take only the icon name.
return icon.split(/["']/)[1];
})
.filter(function(icon) {
// Remove non string items.
return icon && icon.match(/^[a-z0-9-]+$/);
});
callback(usedIcons);
if (err) {
console.log(err);
}
});
}
/**
* Get the list of unused icons in a folder. The patterns of extraction are:
* * data-icon="[^"]+"
* * .dataset.icon ?= ?.+
*/
function getUnusedIcons() {
var usedIcons = [];
var length = 2;
var cb = function(icons) {
usedIcons = usedIcons.concat(icons);
length--;
if (length > 0) {
return;
}
var allIcons = getIcons();
var unusedIcons = [];
allIcons.forEach(function(icon) {
if (usedIcons.indexOf(icon) === -1) {
unusedIcons.push(icon);
}
});
outputUnusedIcons(allIcons, unusedIcons);
};
getUsedIcons('grep -rhoP \'data-icon="[^"]+"\' ' + projectDir, cb);
getUsedIcons('grep -rhoP \'.dataset.icon ?= ?.+\' ' + projectDir, cb);
}
/**
* Display the list of unused icons to the screen.
* @param {Array.<string>} icons
* @param {Array.<string>} unusedIcons
*/
function outputUnusedIcons(icons, unusedIcons) {
console.log('\x1b[36mList of unused icons in %s (%d/%d):\x1b[0m',
projectDir, unusedIcons.length, icons.length);
unusedIcons.forEach(function(icon) {
console.log('* ' + icon);
});
}
getUnusedIcons();

View file

@ -0,0 +1,30 @@
{
"name": "gaia-icons",
"version": "0.8.0",
"homepage": "https://github.com/wilsonpage/gaia-icons",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": [
"gaia-icons.js",
"gaia-icons.css",
"fonts/gaia-icons.ttf",
"gaia-icons-embedded.css"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"templates",
"images",
"test",
"tests",
"index.html",
"create-svg-icon-set-file.py",
"Gruntfile.js",
"README.md",
"package.json"
]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,25 @@
/* Generated by grunt-webfont */
/* Based on https://github.com/endtwist/fontcustom/blob/master/lib/fontcustom/templates/fontcustom.css */
@font-face {
font-family: "gaia-icons";
src: url("fonts/gaia-icons.ttf") format("truetype");
font-weight: 500;
font-style: normal;
}
[data-icon]:before,
.ligature-icons {
font-family: "gaia-icons";
content: attr(data-icon);
display: inline-block;
font-weight: 500;
font-style: normal;
text-decoration: inherit;
text-transform: none;
text-rendering: optimizeLegibility;
font-size: 30px;
-webkit-font-smoothing: antialiased;
}

View file

@ -0,0 +1,38 @@
(function(define){define(function(require,exports,module){
/*jshint laxbreak:true*/
/**
* Exports
*/
var base = window.GAIA_ICONS_BASE_URL
|| window.COMPONENTS_BASE_URL
|| 'bower_components/';
if (!document.documentElement) {
window.addEventListener('load', load);
} else {
load();
}
function load() {
if (isLoaded()) { return; }
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = base + 'gaia-icons/gaia-icons.css';
document.head.appendChild(link);
exports.loaded = true;
}
function isLoaded() {
return exports.loaded ||
document.querySelector('link[href*=gaia-icons]') ||
document.documentElement.classList.contains('gaia-icons-loaded');
}
});})(typeof define=='function'&&define.amd?define
:(function(n,w){return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-icons',this));

View file

@ -0,0 +1,27 @@
{
"name": "gaia-text-input",
"version": "0.1.2",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-text-input.js",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"README.md"
],
"dependencies": {
"gaia-icons": "gaia-components/gaia-icons#~0.8.0",
"gaia-component": "gaia-components/gaia-component#~0.3.3"
},
"devDependencies": {
"gaia-fonts": "gaia-components/gaia-fonts#~0.2.1",
"gaia-theme": "gaia-components/gaia-theme#~0.4.3",
"base": "gaia-components/base",
"gaia-icons": "gaia-components/gaia-icons#master"
}
}

View file

@ -0,0 +1,216 @@
(function(define){define(function(require,exports,module){
/*jshint esnext:true*/
'use strict';
/**
* Dependencies
*/
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-text-input-multiline', {
created: function() {
this.setupShadowRoot();
this.els = {
inner: this.shadowRoot.querySelector('.inner'),
field: this.shadowRoot.querySelector('textarea')
};
this.type = this.getAttribute('type');
this.disabled = this.hasAttribute('disabled');
this.placeholder = this.getAttribute('placeholder');
this.required = this.getAttribute('required');
this.value = this.getAttribute('value');
},
clear: function(e) {
this.value = '';
},
attrs: {
placeholder: {
get: function() { return this.els.field.placeholder; },
set: function(value) { this.els.field.placeholder = value || ''; }
},
value: {
get: function() { return this.els.field.value; },
set: function(value) { this.els.field.value = value; }
},
required: {
get: function() { return this.els.field.required; },
set: function(value) { this.els.field.required = value; }
},
disabled: {
get: function() { return this.els.field.disabled; },
set: function(value) {
value = !!(value === '' || value);
this.els.field.disabled = value;
}
}
},
template: `<div class="inner">
<content select="label"></content>
<div class="fields">
<textarea></textarea>
<div class="focus focus-1"></div>
<div class="focus focus-2"></div>
</div>
</div>
<style>
/** Reset
---------------------------------------------------------*/
textarea {
box-sizing: border-box;
border: 0;
margin: 0;
padding: 0;
}
/** Host
---------------------------------------------------------*/
:host {
display: block;
margin-top: var(--base-m, 18px);
margin-bottom: var(--base-m, 18px);
}
/** Inner
---------------------------------------------------------*/
.inner {
height: 100%;
}
/** Label
---------------------------------------------------------*/
label {
font-size: 14px;
display: block;
margin: 0 0 4px 16px;
}
/**
* [disbled]
*/
[disabled] label {
opacity: 0.3;
}
/** Fields
---------------------------------------------------------*/
.fields {
box-sizing: border-box;
position: relative;
width: 100%;
height: 100%;
--gi-border-color:
var(--input-border-color,
var(--border-color,
var(--background-plus,
#e7e7e7)));
border-color:
var(--gi-border-color);
border:
var(--input-border,
var(--border,
1px solid var(--gi-border-color)));
}
/** Textarea
---------------------------------------------------------*/
textarea {
display: block;
width: 100%;
height: 100%;
min-height: 86px;
padding: 10px 16px;
font-size: inherit;
border: none;
margin: 0;
font: inherit;
resize: none;
/* dynamic */
color:
var(--text-color, #000);
background:
var(--text-input-background,
var(--input-background,
var(--background-minus,
#fff)));
}
/**
* [disabled]
*/
textarea[disabled] {
background: transparent;
}
/** Placeholder Text
---------------------------------------------------------*/
::-moz-placeholder {
font-style: italic;
color:
var(--input-placeholder-color, #909ca7);
}
/** Focus Bar
---------------------------------------------------------*/
.focus {
position: absolute;
bottom: 0px;
width: 100%;
height: 3px;
transition: all 200ms;
transform: scaleX(0);
visibility: hidden;
background: var(--highlight-color, #000);
}
/**
* input:focus
*/
:focus ~ .focus {
transform: scaleX(1);
transition-delay: 200ms;
visibility: visible;
}
.focus-2 {
top: 0;
bottom: auto;
}
</style>`
});
});})((function(n,w){return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:function(c){
var m={exports:{}},r=function(n){return w[n];};w[n]=c(r,m.exports,m)||m.exports;};})('gaia-text-input-multiline',this));

View file

@ -0,0 +1,279 @@
(function(define){define(function(require,exports,module){
/*jshint esnext:true*/
'use strict';
/**
* Dependencies
*/
var component = require('gaia-component');
/**
* Locals
*/
var DEFAULT_LENGTH = 4;
/**
* Exports
*/
module.exports = component.register('gaia-text-input-pin', {
created: function() {
this.setupShadowRoot();
this.els = {
inner: this.shadowRoot.querySelector('.inner'),
fields: this.shadowRoot.querySelector('.fields'),
input: this.shadowRoot.querySelector('input')
};
this.disabled = this.hasAttribute('disabled');
this.length = this.getAttribute('length') || DEFAULT_LENGTH;
this.addEventListener('keyup', () => this.updateCells());
},
updateCells: function() {
var l = this.els.input.value.length;
this.els.cells.forEach((cell, i) => {
cell.classList.toggle('populated', i < l);
cell.classList.toggle('focused', i == l);
});
},
onBackspace: function(e) {
var input = e.target;
var empty = !input.value;
var previous = input.previousElementSibling;
if (!empty && previous) {
previous.clear();
previous.focus();
} else if (empty && previous) {
previous.focus();
}
},
setupFields: function() {
this.els.fields.innerHTML = '';
this.els.cells = [];
for (var i = 0, l = this.length; i < l; i++) {
var el = document.createElement('div');
el.className = 'cell';
this.els.fields.appendChild(el);
this.els.cells.push(el);
}
},
attributeChangedCallback: function(attr, from, to) {
if (attrs[attr]) { this[attr] = to; }
},
clear: function(e) {
this.value = '';
},
/**
* Attributes
*/
attrs: {
length: {
get: function() { return this._length; },
set: function(value) {
value = Number(value);
this._length = value;
this.els.input.setAttribute('maxlength', this.length);
this.setupFields();
}
},
value: {
get: function() { return this.els.input.value; },
set: function(value) { this.els.input.value = value; }
},
disabled: {
get: function() { return this.els.input.disabled; },
set: function(value) {
value = !!(value === '' || value);
this.els.input.disabled = value;
}
}
},
template: `
<div class="inner">
<content select="label"></content>
<div class="container">
<input />
<div class="fields"></div>
</div>
</div>
<style>
/** Host
---------------------------------------------------------*/
:host {
display: block;
height: 40px;
margin-top: var(--base-m, 18px);
margin-bottom: var(--base-m, 18px);
font-size: 40px;
}
/** Inner
---------------------------------------------------------*/
.inner {
height: 100%;
}
/** Label
---------------------------------------------------------*/
label {
font-size: 14px;
display: block;
margin: 0 0 4px 16px;
}
/** Container
---------------------------------------------------------*/
.container {
position: relative;
z-index: 0;
height: 100%;
}
/** Input (hidden)
---------------------------------------------------------*/
input {
opacity: 0;
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
z-index: 1;
}
/** Fields
---------------------------------------------------------*/
.fields {
display: flex;
position: relative;
height: 100%;
margin-left: -1rem;
}
/**
* [disbled]
*/
[disabled] + .fields {
pointer-events: none;
}
/** Cell
---------------------------------------------------------*/
.cell {
position: relative;
height: 100%;
margin-left: 1rem;
flex: 1;
/* dynamic */
--gi-border-color:
var(--input-border-color,
var(--border-color,
var(--background-plus,
#e7e7e7)));
border-color:
var(--gi-border-color);
border:
var(--input-border,
var(--border,
1px solid var(--gi-border-color)));
background:
var(--text-input-background,
var(--input-background,
var(--background-minus,
#fff)));
}
/**
* [disbled]
*/
[disabled] + .fields .cell {
background: none;
}
/** Dot
---------------------------------------------------------*/
.cell::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 14px;
height: 14px;
margin: -7px;
border-radius: 50%;
opacity: 0;
/* dynamic */
background:
var(--text-color);
}
/**
* .populated
*/
.cell.populated::after {
opacity: 1;
}
.cell::before {
content: '';
position: absolute;
bottom: 0;
width: 100%;
height: 3px;
visibility: hidden;
background: var(--highlight-color, #000);
}
/**
* input:focus
*/
input:focus + .fields .cell::before {
visibility: visible;
}
</style>
`
});
});})((function(n,w){return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:function(c){
var m={exports:{}},r=function(n){return w[n];};w[n]=c(r,m.exports,m)||m.exports;};})('gaia-text-input-pin',this));

View file

@ -0,0 +1,317 @@
(function(define){define(function(require,exports,module){
/*jshint esnext:true*/
'use strict';
/**
* Dependencies
*/
var component = require('gaia-component');
require('gaia-icons'); // Load gaia-icons
/**
* Exports
*/
module.exports = component.register('gaia-text-input', {
extends: HTMLInputElement,
created: function() {
this.setupShadowRoot();
this.els = {
inner: this.shadowRoot.querySelector('.inner'),
input: this.shadowRoot.querySelector('input'),
clear: this.shadowRoot.querySelector('.clear')
};
this.type = this.getAttribute('type');
this.disabled = this.hasAttribute('disabled');
this.clearable = this.hasAttribute('clearable');
this.placeholder = this.getAttribute('placeholder');
this.required = this.getAttribute('required');
this.value = this.getAttribute('value');
// Don't take focus from the input field
this.els.clear.addEventListener('mousedown', (e) => e.preventDefault());
this.els.clear.addEventListener('click', e => this.clear(e));
},
clear: function(e) {
this.value = '';
},
focus: function() {
this.els.input.focus();
},
/**
* Attributes
*/
attrs: {
type: {
get: function() { return this.els.input.getAttribute('type'); },
set: function(value) {
if (!value) { return; }
this.els.inner.setAttribute('type', value);
this.els.input.setAttribute('type', value);
}
},
placeholder: {
get: function() { return this.field.placeholder; },
set: function(value) {
if (!value && value !== '') { return; }
this.els.input.placeholder = value;
}
},
clearable: {
get: function() { return this._clearable; },
set: function(value) {
var clearable = !!(value === '' || value);
if (clearable === this.clearable) { return; }
if (clearable) {
this.els.inner.setAttribute('clearable', '');
this.setAttribute('clearable', '');
} else {
this.els.inner.removeAttribute('clearable');
this.removeAttribute('clearable');
}
this._clearable = clearable;
}
},
value: {
get: function() { return this.els.input.value; },
set: function(value) { this.els.input.value = value; }
},
required: {
get: function() { return this.els.input.required; },
set: function(value) { this.els.input.required = value; }
},
maxlength: {
get: function() { return this.els.input.getAttribute('maxlength'); },
set: function(value) { this.els.input.setAttribute('maxlength', value); }
},
disabled: {
get: function() { return this.els.input.disabled; },
set: function(value) {
value = !!(value === '' || value);
this.els.input.disabled = value;
}
}
},
template: `
<div class="inner">
<content select="label"></content>
<div class="fields">
<input type="text"/>
<button class="clear" tabindex="-1"></button>
<div class="focus"></div>
</div>
</div>
<style>
:host {
display: block;
height: 40px;
margin-top: var(--base-m, 18px);
margin-bottom: var(--base-m, 18px);
}
/** Reset
---------------------------------------------------------*/
input,
button {
box-sizing: border-box;
border: 0;
margin: 0;
padding: 0;
}
/** Inner
---------------------------------------------------------*/
.inner {
height: 100%;
}
/** Label
---------------------------------------------------------*/
label {
font-size: 14px;
display: block;
margin: 0 0 4px 16px;
}
/**
* [disbled]
*/
[disabled] label {
opacity: 0.3;
}
/** Fields
---------------------------------------------------------*/
.fields {
position: relative;
width: 100%;
height: 100%;
--gi-border-color:
var(--input-border-color,
var(--border-color,
var(--background-plus,
#e7e7e7)));
border-color:
var(--gi-border-color);
border:
var(--input-border,
var(--border,
1px solid var(--gi-border-color)));
}
/**
* [type='search']
*/
[type='search'] .fields {
border-radius: 30px;
overflow: hidden;
}
/** Input Field
---------------------------------------------------------*/
input {
display: block;
width: 100%;
height: 100%;
border: none;
padding: 0 16px;
margin: 0;
font: inherit;
resize: none;
/* Dynamic */
color:
var(--text-color, #000);
background:
var(--text-input-background,
var(--input-background,
var(--background-minus,
#fff)));
}
/**
* [disabled]
*/
input[disabled] {
background: transparent;
}
/** Placeholder Text
---------------------------------------------------------*/
::-moz-placeholder {
font-style: italic;
color:
var(--input-placeholder-color, #909ca7);
}
/** Clear Button
---------------------------------------------------------*/
.clear {
display: none;
position: absolute;
top: 11px;
right: 10px;
width: 18px;
height: 18px;
padding: 0;
margin: 0;
border-radius: 50%;
opacity: 0;
color: #fff;
cursor: pointer;
background:
var(--input-clear-background, #999);
}
/**
* [clearable]
*/
[clearable] .clear {
display: block;
}
/**
* input:focus
*/
input:focus ~ .clear {
opacity: 1;
}
/** Clear Icon
---------------------------------------------------------*/
.clear:before {
font: normal 500 19px/16.5px 'gaia-icons';
content: 'close';
display: block;
text-rendering: optimizeLegibility;
}
/** Focus Bar
---------------------------------------------------------*/
.focus {
position: absolute;
bottom: 0px;
width: 100%;
height: 3px;
transition: all 200ms;
transform: scaleX(0);
visibility: hidden;
background: var(--highlight-color, #000);
}
/**
* input:focus
*/
:focus ~ .focus {
transform: scaleX(1);
transition-delay: 200ms;
visibility: visible;
}
</style>
`
});
});})((function(n,w){return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:function(c){
var m={exports:{}},r=function(n){return w[n];};w[n]=c(r,m.exports,m)||m.exports;};})('gaia-text-input',this));

View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>&lt;gaia-text-input&gt;</title>
<meta name="description" content="gaia-text-input demo">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<link rel="stylesheet" type="text/css" href="bower_components/base/base.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-icons/gaia-icons.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-theme/gaia-theme.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-fonts/style.css">
<script src="bower_components/gaia-theme/lib/gaia-theme-selector.js"></script>
<script src="bower_components/gaia-component/gaia-component.js"></script>
<script src="gaia-text-input.js"></script>
<script src="gaia-text-input-multiline.js"></script>
<script src="gaia-text-input-pin.js"></script>
<style type="text/css">
body {
padding: 48px 24px 24px;
}
</style>
</head>
<body>
<gaia-theme-selector></gaia-theme-selector>
<button id="btn">Focus</button>
<gaia-text-input id="field" placeholder="Default"></gaia-text-input>
<gaia-text-input placeholder="Search" type="search"></gaia-text-input>
<gaia-text-input placeholder="Email" type="email" clearable></gaia-text-input>
<gaia-text-input-multiline placeholder="Multiline"></gaia-text-input-multiline>
<gaia-text-input-pin length="4"></gaia-text-input-pin>
<gaia-text-input placeholder="Disabled" disabled></gaia-text-input>
<gaia-text-input placeholder="Search" type="search" disabled></gaia-text-input>
<gaia-text-input placeholder="Email" type="email" clearable disabled></gaia-text-input>
<gaia-text-input-multiline placeholder="Multiline" disabled></gaia-text-input-multiline>
<gaia-text-input-pin length="4" disabled></gaia-text-input-pin>
<!-- <gaia-text-input-password></gaia-text-input-password> -->
<script>
btn.onclick = e => field.focus();
</script>
</body>
</html>

View file

@ -0,0 +1,28 @@
{
"name": "gaia-text-input",
"homepage": "https://github.com/gaia-components/gaia-text-input",
"license": "ISC",
"main": "gaia-text-input.js",
"directories": {
"test": "test"
},
"devDependencies": {
"bower": "^1.3.5",
"karma": "^0.12.16",
"karma-firefox-launcher": "^0.1.3",
"karma-mocha": "^0.1.3",
"karma-sinon-chai": "^0.1.6",
"mozilla-download": "0.4.4"
},
"scripts": {
"test": "./node_modules/karma/bin/karma start test/karma.conf.js --single-run",
"test-dev": "./node_modules/karma/bin/karma start test/karma.conf.js"
},
"repository": {
"type": "git",
"url": "git://github.com/gaia-components/gaia-text-input.git"
},
"bugs": {
"url": "https://github.com/gaia-components/gaia-text-input/issues"
}
}

View file

@ -0,0 +1,26 @@
{
"name": "gaia-theme",
"version": "0.5.0",
"homepage": "https://github.com/gaia-components/gaia-theme",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"test",
"tests",
"index.html",
"README.md"
],
"devDependencies": {
"gaia-icons": "gaia-components/gaia-icons",
"gaia-header": "gaia-components/gaia-header",
"gaia-button": "gaia-components/gaia-button",
"gaia-switch": "gaia-components/gaia-switch",
"gaia-checkbox": "gaia-components/gaia-checkbox"
}
}

View file

@ -0,0 +1,244 @@
/*
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);
/** 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(--color-beta);
}
.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);
/** 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,111 @@
(function(define){'use strict';define(function(require,exports,module){
/*jshint esnext:true*/
/**
* Extend from the `HTMLElement` prototype
*
* @type {Object}
*/
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
this.createShadowRoot().innerHTML = template;
this.els = {
themes: this.shadowRoot.querySelector('.themes'),
rtl: this.shadowRoot.querySelector('.toggle-rtl')
};
this.els.rtl.addEventListener('click', this.onRtlClick.bind(this));
this.els.themes.addEventListener('click', this.onThemeClick.bind(this));
this.styleHack();
this.set(localStorage.theme || 'settings');
};
proto.styleHack = function() {
var style = this.shadowRoot.querySelector('style');
style.setAttribute('scoped', '');
this.appendChild(style.cloneNode(true));
};
proto.onThemeClick = function(e) {
var theme = e.target.value;
if (theme) { this.set(theme); }
};
proto.onRtlClick = function() {
document.dir = this.els.rtl.checked ? 'rtl' : 'ltr';
};
proto.set = function(theme) {
var radio = this.shadowRoot.querySelector('[value="' + theme + '"]');
document.body.classList.remove('theme-' + this.theme);
document.body.classList.add('theme-' + theme);
radio.checked = true;
this.theme = theme;
localStorage.theme = theme;
};
var template = `
<style>
* { margin: 0; padding: 0; }
gaia-theme-selector {
position: fixed;
top: 0; left: 0;
z-index: 101;
width: 100%;
height: 30px;
background: rgba(255,255,255,0.8);
color: #000;
font-size: 9px;
text-align: center;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
direction: ltr;
}
form {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
}
form label {
-moz-margin-start: 9px;
}
form input {
-moz-margin-end: 6px;
vertical-align: middle;
}
.rtl {
position: absolute;
top: 0;
left: 0;
height: 100%;
display: flex;
align-items: center;
}
</style>
<form>
<label class="rtl"><input type="checkbox" class="toggle-rtl" />RTL</label>
<span class="themes">
<label><input type="radio" name="theme" value="productivity"/>Prod</label>
<label><input type="radio" name="theme" value="communications"/>Comms</label>
<label><input type="radio" name="theme" value="media"/>Media</label>
<label><input type="radio" name="theme" value="settings"/>Settings</label>
</span>
</form>`;
document.registerElement('gaia-theme-selector', { prototype: proto });
});})((function(n,w){'use strict';return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:
function(c){var m={exports:{}},r=function(n){return w[n];};
w[n]=c(r,m.exports,m)||m.exports;};})('gaia-theme-selector',this));

View file

@ -0,0 +1,134 @@
html, body {
padding: 0;
margin: 0;
font-family: sans-serif;
width: 100%;
height: 100%;
overflow: hidden;
}
h1 {
pointer-events: none;
}
#h1-after {
position: absolute;
width: 100%;
height: 4px;
left: 0;
top: 46px;
pointer-events: none;
background-color: #ff9500;
opacity: 0;
animation-duration: 0.25s;
animation-timing-function: ease-in;
animation-fill-mode: both;
-webkit-animation-duration: 0.25s;
-webkit-animation-timing-function: ease-in;
-webkit-animation-fill-mode: both;
}
[data-anim='reveal'] {
animation-name: reveal;
-webkit-animation-name: reveal;
}
@keyframes reveal {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-webkit-keyframes reveal {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
[data-anim='hide'] {
animation-name: hide;
-webkit-animation-name: hide;
}
@keyframes hide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@-webkit-keyframes hide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
button {
position: absolute;
display: block;
top: 0;
right: 0;
height: 50px;
border: 0;
border-radius: 0;
outline: 0;
background: none;
padding: 0 15px;
text-align: center;
text-decoration: underline;
font-size: 14px;
color: #ffffff;
-moz-appearance: none;
opacity: 1;
transition: transform 0.1s linear,
opacity 0.1s linear;
-webkit-transition: -webkit-transform 0.1s linear,
opacity 0.1s linear;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
button.transitioning {
opacity: 0.5;
transform: scale(0.6, 0.6);
-webkit-transform: scale(0.6, 0.6);
pointer-events: none;
}
ul {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
body > section {
position: absolute;
top: 50px;
width: 100%;
height: calc(100% - 50px);
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}

View file

@ -0,0 +1,21 @@
h1 {
position: absolute;
top: 0;
left: 0;
width: 100%;
margin: 0;
padding: 0;
background: #27c8c2;
text-align: center;
line-height: 50px;
font-weight: 300;
font-style: italic;
font-size: 24px;
color: #ffffff;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

View file

@ -0,0 +1,143 @@
ul {
list-style: none;
padding: 0 0 0 0;
margin: 0;
}
ul section {
margin: 0;
padding: 0;
}
ul h2 {
position: sticky;
position: -webkit-sticky;
top: 0px;
height: 32px;
margin: 0;
padding: 0 6px;
box-sizing: border-box;
font-size: 0.9em;
line-height: 32px;
background: #efefef;
border-bottom: 1px #bcbcbc solid;
z-index: 100;
}
ul .background {
position: absolute;
width: 100%;
background: linear-gradient(to bottom, transparent 0px, transparent 45px, #EDEDED 45px, #EDEDED 70px, transparent 20px),
linear-gradient(to bottom, transparent 0px, transparent 15px, #EDEDED 15px, #EDEDED 25px, transparent 20px),
linear-gradient(to top, #bcbcbc, #bcbcbc 1px, white 1px, white);
background-position: 20% 88px, 5% -88px, 0 -88px;
background-size: 90% 88px, 60% 88px, 100% 88px;
background-repeat: repeat-y;
z-index: 0;
}
ul.reordering .background {
background: linear-gradient(to top, #bcbcbc, #bcbcbc 1px, white 1px, white);
background-position: 0 -88px;
background-size: 100% 88px;
}
ul li {
width: 100%;
height: 88px;
margin: 0;
padding: 0;
box-sizing: border-box;
-moz-user-select: none;
background: white;
border-bottom: 1px #bcbcbc solid;
z-index: 10;
}
ul li h3 {
margin: 0;
padding: 9px 6px 3px 6px;
background: white;
color: #424242;
pointer-events: none;
}
ul li p {
margin: 0;
padding: 3px 6px 10px 6px;
background: white;
color: #797979;
pointer-events: none;
}
li.new {
opacity: 0;
}
li .overlay {
position: absolute;
top: 1px;
left: 0;
width: 100%;
height: calc(100% - 2px);
background: rgba(255, 255, 255, 0.7);
opacity: 0;
animation-duration: 0.2s;
animation-timing-function: ease-in;
animation-fill-mode: both;
-webkit-animation-duration: 0.2s;
-webkit-animation-timing-function: ease-in;
-webkit-animation-fill-mode: both;
pointer-events: none;
}
li.edit .overlay {
opacity: 1;
pointer-events: auto;
}
.overlay .cursor {
margin: 17px auto;
padding: 0;
width: 50px;
height: 50px;
text-align: center;
line-height: 50px;
font-size: 25px;
color: #0b0b0b;
border-radius: 8px;
background: rgba(0, 0, 0, 0.3);
-moz-user-select: none;
}
li.dragging {
transition: transform 0s linear;
-webkit-transition: -webkit-transform 0s linear;
}
li.dragging .overlay {
opacity: 0;
}
li[data-populated=false] {
opacity: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link rel="stylesheet" href="css/h1.css">
<link rel="stylesheet" href="css/ul.css">
<link rel="stylesheet" href="css/app.css">
<!-- Component Style -->
<link rel="stylesheet" type="text/css" href="bower_components/base/base.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-theme/gaia-theme.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-fonts/style.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-icons/gaia-icons.css">
<!-- Dependencies -->
<script type="text/javascript" src="lib/dom-scheduler.js" defer></script>
<script src="bower_components/gaia-component/gaia-component.js" defer></script>
<script src="bower_components/gaia-button/gaia-button.js" defer></script>
<script src="bower_components/gaia-text-input/gaia-text-input.js" defer></script>
<!-- Libs -->
<script src="bower_components/gaia-dialog/gaia-dialog.js" defer></script>
<script src="bower_components/gaia-dialog/gaia-dialog-alert.js" defer></script>
<script src="bower_components/fast-list/fast-list.js" defer></script>
<!-- App -->
<script type="text/javascript" src="js/bacon-source.js" async></script>
<script type="text/javascript" src="js/app.js" async></script>
<title>Dom Scheduler Demo</title>
</head>
<body>
<h1>
Main List
</h1>
<div id="h1-after"></div>
<button>
Edit
</button>
<section>
<ul>
<section
><h2> </h2
><div class="background"
></div
></section>
<li
><h3> </h3
><p> </p
><div class="overlay"
><div class="cursor">↕︎</div></div
></li>
</ul>
</section>
<gaia-dialog-alert id="mine">List item clicked!</gaia-dialog-alert>
</body>
</html>

View file

@ -0,0 +1,95 @@
(function() {
var debug = false;
var headerHeight = 50;
var maxItemCount = 28;
var endEvent = ('ontouchstart' in window) ? 'touchend' : 'mouseup';
window.addEventListener('load', function() {
var listContainer = document.querySelector('section');
var source = new BaconSource();
var list = new FastList(listContainer, source);
var dialog = document.querySelector('gaia-dialog-alert');
function updateHeader() {
return scheduler.mutation(function() {
var h1 = document.querySelector('h1');
h1.textContent = 'Main List (' + source.fullLength() + ')';
h1.scrollTop; // flush
});
}
updateHeader();
function clearNewIndicator() {
var h1After = document.querySelector('#h1-after');
if (h1After.dataset.anim == 'reveal') {
scheduler.transition(function() {
h1After.dataset.anim = 'hide';
}, h1After, 'animationend');
}
}
listContainer.addEventListener('top-reached', clearNewIndicator);
function updateNewIndicator() {
var h1After = document.querySelector('#h1-after');
if (h1After.dataset.anim == 'reveal') {
return;
}
scheduler.transition(function() {
h1After.dataset.anim = 'reveal';
}, h1After, 'animationend');
}
listContainer.addEventListener('hidden-new-content', updateNewIndicator);
function openGaiaDialog(evt) {
scheduler.mutation(function() {
var detail = evt.detail;
var li = source.getRecordAt(detail.index);
dialog.textContent = li.title + ' item clicked!';
dialog.open(detail.clickEvt);
});
}
list.list.addEventListener('item-selected', openGaiaDialog);
function newContentHandler() {
var newContent = {
title: 'NEW Bacon ' + Date.now().toString().slice(7, -1),
body: 'Turkey BLT please.'
};
source.insertAtIndex(0, newContent);
list.insertedAtIndex(0);
updateHeader();
}
setInterval(newContentHandler, 15000);
window.addEventListener('new-content', newContentHandler);
window.pushNewContent = function() {
window.dispatchEvent(new CustomEvent('new-content'));
};
var button = document.querySelector('button');
button.addEventListener(endEvent, function() {
Promise.all([toggleTransitioning(), list.toggleEditMode()])
.then(updateText)
.then(toggleTransitioning);
});
function updateText(text) {
return scheduler.mutation(function() {
button.textContent = list.editing ? 'Exit' : 'Edit';
});
}
function toggleTransitioning() {
return scheduler.feedback(function() {
button.classList.toggle('transitioning');
}, button, 'transitionend');
}
});
})();

View file

@ -0,0 +1,175 @@
(function(exports) {
var listSize = 1042;
var sectionCount = 25;
var headerHeight = 32;
var itemHeight = 88;
function makeContent(prefix, name) {
return {
title: prefix + ' Bacon ipsum dolor ' +
Date.now().toString().slice(7, -1),
body: 'Turkey pig boudin ' + name.toLowerCase() +
' de mignon drums and all ' + prefix + '.'
}
}
exports.BaconSource = function BaconSource() {
this.sections = [];
this.contentMap = new Map();
this._cachedLength = null;
for (var i = 0; i < sectionCount; i++) {
this.sections.push('Section ' + i);
}
this.sections.forEach(function(name) {
var content = [];
for (var i = 0; i < (Math.random() * listSize); i++) {
content.push(makeContent(i, name));
}
this.contentMap.set(name, content);
}, this);
};
exports.BaconSource.prototype = {
populateItem: function(item, i) {
// Simulates the data source not being ready to populate
/*if (parseInt(Date.now() / 10) % 8 === 0) {*/
/*return new Promise(function(resolve, reject) {*/
/*setTimeout(resolve, Math.random() * 1000);*/
/*});*/
/*}*/
var title = item.firstChild;
var body = title.nextSibling;
var record = this.getRecordAt(i);
title.firstChild.data = record.title;
body.firstChild.data = record.body;
},
getSections: function() {
return this.contentMap.keys();
},
sectionHeaderHeight: function() {
return headerHeight;
},
fullSectionHeight: function(key) {
return this.contentMap.get(key).length * itemHeight;
},
fullSectionLength: function(key) {
return this.contentMap(key).length;
},
getRecordAt: function(index) {
for (var section of this.contentMap.values()) {
if (index < section.length) {
return section[index];
}
index -= section.length;
}
},
getSectionFor: function(index) {
for (var entry of this.contentMap.entries()) {
if (index < entry[1].length) {
return entry[0];
}
index -= entry[1].length;
}
},
indexAtPosition: function(pos) {
var index = 0;
for (var section of this.contentMap.values()) {
pos -= headerHeight;
var sectionHeight = section.length * itemHeight;
if (pos > sectionHeight) {
pos -= sectionHeight;
index += section.length;
continue;
}
for (var item of section) {
pos -= itemHeight;
index++;
if (pos <= 0 || index === this.fullLength() - 1) {
return index;
}
}
}
},
positionForIndex: function(index) {
var top = 0;
for (var section of this.contentMap.values()) {
top += headerHeight;
if (index < section.length) {
top += index * itemHeight;
return top;
}
index -= section.length;
top += section.length * itemHeight;
}
},
fullLength: function() {
if (this._cachedLength) {
return this._cachedLength;
}
var length = 0;
for (var section of this.contentMap.values()) {
length += section.length;
}
this._cachedLength = length;
return length;
},
itemHeight: function() {
return itemHeight;
},
fullHeight: function() {
var height = 0;
for (var section of this.contentMap.values()) {
height += headerHeight + section.length * itemHeight;
}
return height;
},
insertAtIndex: function(index, record, toSection) {
this._cachedLength = null;
for (var entry of this.contentMap.entries()) {
if (index < entry[1].length || entry[0] === toSection) {
return entry[1].splice(index, 0, record);
}
index -= entry[1].length;
}
},
replaceAtIndex: function(index, record) {
for (var section of this.contentMap.values()) {
if (index < section.length) {
return section.splice(index, 1, record);
}
index -= section.length;
}
},
removeAtIndex: function(index) {
this._cachedLength = null;
for (var section of this.contentMap.values()) {
if (index < section.length) {
return section.splice(index, 1)[0];
}
index -= section.length;
}
}
};
})(window);

View file

@ -0,0 +1,14 @@
{
"name": "dom-scheduler-demo",
"description": "A Hello World app",
"launch_path": "/index.html",
"icons": {
"16": "/icons/icon16x16.png",
"48": "/icons/icon48x48.png",
"60": "/icons/icon60x60.png",
"128": "/icons/icon128x128.png"
},
"type": "certified",
"orientation": "default",
"permissions": {}
}

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link rel="stylesheet" href="demo-app/css/h1.css">
<link rel="stylesheet" href="demo-app/css/ul.css">
<link rel="stylesheet" href="demo-app/css/app.css">
<script type="text/javascript" src="lib/dom-scheduler.js" async></script>
<script src="bower_components/fast-list/fast-list.js" defer></script>
<script type="text/javascript" src="demo-app/js/bacon-source.js" async></script>
<script type="text/javascript" src="page.js" async></script>
<title>Dom Scheduler Demo</title>
</head>
<body class="theme-communications">
<h1>
Main List
</h1>
<div id="h1-after"></div>
<button>
Edit
</button>
<section>
<ul>
<section
><h2> </h2
><div class="background"
></div
></section>
<li
><h3> </h3
><p> </p
><div class="overlay"
><div class="cursor">↕︎</div></div
></li>
</ul>
</section>
</body>
</html>

View file

@ -0,0 +1,338 @@
/* global performance */
(function(exports) {
'use strict';
var debug = false;
function log(str) {
if (!debug) {
return;
}
console.log('🎶 ', str);
}
// The naive mode just executes all blocks right away
// (useful for performance comparison).
var naive = false;
var naiveExec = function(block) {
block();
return Promise.resolve();
};
var directProtectionWindow = 360;
var Scheduler = function() {
this._directTracking = new Map();
this._ongoingTransitions = 0;
this._queuedTransitions = [];
this._flushing = false;
this._pendingMutations = [];
};
Scheduler.prototype = {
// *Direct* blocks should be used for direct manipulation use cases
// (touchevents, scrollevents...).
// They're exectuted in a requestAnimationFrame block and are protected
// from mutations. Request might be cancelled by subsequent direct blocks if
// the event loop gets too busy.
attachDirect: function(elm, evtType, block) {
var tracking = this._directTracking;
if (!tracking.get(elm)) {
tracking.set(elm, {});
}
var tElm = tracking.get(elm);
if (!(evtType in tElm)) {
tElm[evtType] = {
rafID: null,
protectionTimeout: null,
blocks: []
};
}
var tEvt = tElm[evtType];
if (!tEvt.blocks.length) {
elm.addEventListener(evtType, this);
}
tEvt.blocks.push(block);
},
detachDirect: function(elm, evtType, block) {
var tEvt = this._trackingFor(elm, evtType);
if (!tEvt) {
return;
}
if (tEvt.rafID) {
window.cancelAnimationFrame(tEvt.rafID);
}
if (tEvt.protectionTimeout) {
clearTimeout(tEvt.protectionTimeout);
tEvt.protectionTimeout = null;
this._stopProtectingDirect();
}
var toRemove = tEvt.blocks.indexOf(block);
if (toRemove === -1) {
log('wrong detach block');
return;
}
tEvt.blocks.splice(toRemove, 1);
if (!tEvt.blocks.length) {
elm.removeEventListener(evtType, this);
var tElm = this._directTracking.get(elm);
delete tElm[evtType];
if (Object.keys(tElm).length === 0) {
this._directTracking.delete(elm);
}
}
},
_trackingFor: function(elm, evtType) {
var tracking = this._directTracking;
if (!tracking.get(elm)) {
log('wrong detach element');
return;
}
var tElm = tracking.get(elm);
if (!(evtType in tElm)) {
log('wrong detach event');
return;
}
return tElm[evtType];
},
_shouldProtectDirect: function() {
var tracking = this._directTracking;
for (var elm of tracking.keys()) {
var tElm = tracking.get(elm);
for (var evtType in tElm) {
var tEvt = tElm[evtType];
if (tEvt.protectionTimeout !== null) {
return true;
}
}
}
return false;
},
_stopProtectingDirect: function() {
this._dequeueTransitions();
this._flushMutations();
if (debug) {
console.timeEnd('protecting');
}
},
handleEvent: function(evt) {
var tEvt = this._trackingFor(evt.currentTarget, evt.type);
if (!tEvt) {
return;
}
if (naive) {
tEvt.blocks.forEach(function(block) {
block(evt);
});
return;
}
if (tEvt.protectionTimeout) {
clearTimeout(tEvt.protectionTimeout);
} else {
if (debug) {
console.time('protecting');
}
}
tEvt.protectionTimeout = setTimeout((function() {
tEvt.protectionTimeout = null;
this._stopProtectingDirect();
}).bind(this), directProtectionWindow);
if (tEvt.rafID) {
window.cancelAnimationFrame(tEvt.rafID);
}
tEvt.rafID = window.requestAnimationFrame(function() {
tEvt.blocks.forEach(function(block) {
var startDate;
if (debug) {
startDate = performance.now();
}
block(evt);
if (debug) {
var blockDuration = performance.now() - startDate;
if (blockDuration > 16) {
log('Direct block took more than a frame (' +
blockDuration.toString() + 'ms)');
}
}
});
});
},
// *Feedbacks* blocks have a built in 'transitionend' wait mechanism.
// They're protected from mutation and will be delayed during a mutation
// flush.
// They have the same priority as `direct` blocks.
//
// -> Returns a promise fullfilled at the end of the transition for chaining
feedback: function(block, elm, evt, timeout) {
return this._transition(block, true, elm, evt, timeout);
},
// *Transitions* blocks have a built in 'transitionend' wait mechanism.
// They're protected from mutation and will be delayed during a mutation
// flush.
// They will also be delayed by `direct` blocks.
//
// -> Returns a promise fullfilled at the end of the transition for chaining
transition: function(block, elm, evt, timeout) {
return this._transition(block, false, elm, evt, timeout);
},
_transition: function(block, feedback, elm, evt, timeout) {
if (naive) {
return naiveExec(block);
}
timeout = timeout || 500;
return new Promise((function(resolve, reject) {
var content = (function() {
this._ongoingTransitions++;
block();
if (!elm || !evt) {
this._ongoingTransitions--;
resolve();
return;
}
if (debug) {
console.time('animating');
}
var finishTimeout;
var done = (function() {
clearTimeout(finishTimeout);
elm.removeEventListener(evt, done);
this._ongoingTransitions--;
if (debug) {
console.timeEnd('animating');
}
if (this._ongoingTransitions === 0) {
setTimeout(this._flushMutations.bind(this));
}
resolve();
}).bind(this);
elm.addEventListener(evt, done);
finishTimeout = setTimeout(function() {
done();
log('Transition block saved by a timeout of ' + timeout + ' ~ ' +
elm.style.transition);
}, timeout);
}).bind(this);
if (this._flushing || (this._shouldProtectDirect() && !feedback)) {
this._queuedTransitions.push(content);
} else {
content();
}
}).bind(this));
},
_dequeueTransitions: function() {
if (this._queuedTransitions.length === 0) {
return;
}
if (this._flushing || this._shouldProtectDirect()) {
return;
}
var transitions = this._queuedTransitions;
transitions.forEach(function(transition) {
transition();
});
this._queuedTransitions = [];
},
// *Mutations* blocks should be used to write to the DOM or perform
// actions requiring a reflow that are not direct manipulations.
// We shoud always aim for the document to be almost visually identical
// _before_ and _after_ a mutation block.
// Any big change in layout/size will cause a flash/jump.
//
// -> Returns a promise fullfilled after the reflow for chaining
mutation: function(block) {
if (naive) {
return naiveExec(block);
}
return new Promise((function(resolve, reject) {
if (this._shouldProtectDirect() || this._ongoingTransitions > 0) {
this._pendingMutations.push({
block: block,
resolve: resolve
});
} else {
Promise.resolve()
.then(block)
.then(resolve);
}
}).bind(this));
},
_flushMutations: function() {
if (this._pendingMutations.length === 0) {
return;
}
if (this._shouldProtectDirect() || this._ongoingTransitions > 0) {
return;
}
this._flushing = true;
var fulfilments =
this._pendingMutations
.map(function(obj) { return obj.resolve; });
var mutations = this._pendingMutations;
mutations.forEach(function(mutation) {
mutation.block();
});
this._pendingMutations = [];
this._flushing = false;
fulfilments.forEach(function(resolve) { resolve(); });
this._dequeueTransitions();
}
};
exports.DomScheduler = Scheduler;
// We only ever want there to be
// one instance of the scheduler
if (!exports.scheduler || typeof exports.scheduler.mutation !== 'function') Object.defineProperty(exports, 'scheduler', {value:new Scheduler(),configurable:true,writable:true});
})(window);

View file

@ -0,0 +1,24 @@
{
"name": "dom-scheduler",
"main": "lib/dom-scheduler.js",
"devDependencies": {
"jshint": "^2.7.0",
"karma": "^0.12.31",
"karma-firefox-launcher": "^0.1.4",
"karma-chrome-launcher": "^0.1.4",
"karma-mocha": "^0.1.10",
"karma-sinon-chai": "^0.3.0"
},
"scripts": {
"lint": "./node_modules/jshint/bin/jshint lib/dom-scheduler.js test/*.js",
"test": "./node_modules/karma/bin/karma start test/karma.conf --single-run",
"test-dev": "./node_modules/karma/bin/karma start test/karma.conf"
},
"repository": {
"type": "git",
"url": "git://github.com/etiennesegonzac/dom-scheduler.git"
},
"bugs": {
"url": "https://github.com/etiennesegonzac/dom-scheduler/issues"
}
}

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