Commit 23777a77 authored by Eric Eastwood's avatar Eric Eastwood

Add loading state, options.preload = false - v0.2.3

parent 357250a4
# v0.2.3 - 2015-8-31
- `options.preload` defaults to `false`. Instead, we load the iframe after the "Open Chat" button is clicked and the aside is slid into place. This is to avoid the unnecessary strain to the Gitter servers for people who never click the open chat button, etc.
- Add `.is-loading` state for when the iframe hasn't embedded yet but we are working on it. We don't add the iframe exactly on click because that causes jank in the slide in animation.
# v0.2.2 - 2015-8-27
- `options.room` defaults to `undefined` and will throw an error if no room is specified
......
......@@ -2,7 +2,7 @@
Gitter embed widget
# Latest version: 0.2.2
# Latest version: 0.2.3
### [Changelog](https://github.com/gitterHQ/sidecar/blob/master/CHANGELOG.md)
......@@ -52,6 +52,8 @@ You can also override these options individually on the container:
- Default: `null`
- `options.useStyles`: Whether to embed some pre-made CSS styles to the page
- Default: `true`
- `preload`: Whether the Gitter chat iframe should be loaded in when the chat embed instance is created(this is the page load for default embed)
- Defaut: `false`
### Window Options:
......
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -44,7 +44,12 @@
</style>
</head>
<body>
<iframe src="http://marionettejs.com/"></iframe>
<style>
body {
background-color: #999999;
}
</style>
<!-- <iframe src="http://marionettejs.com/"></iframe> -->
<script>
((window.___gitter = {}).chat = {}).options = {
room: 'marionettejs/backbone.marionette'
......
{
"name": "gitter-sidecar",
"version": "0.2.2",
"version": "0.2.3",
"description": "",
"main": "index.js",
"scripts": {
......@@ -18,6 +18,7 @@
"css-loader": "^0.16.0",
"csswring": "^3.0.5",
"postcss": "^4.1.16",
"postcss-css-variables": "^0.4.0",
"postcss-loader": "^0.5.1",
"postcss-nested": "^0.3.2",
"postcss-plugin-context": "^1.2.0",
......
......@@ -10,7 +10,14 @@
fill: currentColor;
}
/*
* States:
* `.is-collapsed`
* `.is-loading`
*/
.gitter-chat-embed {
--background-color: #ffffff;
z-index: 100;
position: fixed;
top: 0;
......@@ -21,15 +28,31 @@
display: flex;
flex-direction: row;
background-color: var(--background-color);
border-left: 1px solid #333;
box-shadow: -12px 0 18px 0 rgba(50, 50, 50, 0.3);
transition: transform 0.3s cubic-bezier(0.16, 0.22, 0.22, 1.7);
&.is-collapsed {
&.is-collapsed:not(.is-loading) {
transform: translateX(110%);
}
/* Add some "extension" so that there isn't a gap
* when we translate(via animation) more than 100% */
&:after {
content: '';
z-index: -1;
position: absolute;
top: 0;
left: 100%;
bottom: 0;
right: -100%;
background-color: var(--background-color);
}
& > iframe {
flex: 1;
width: 100%;
......@@ -37,8 +60,44 @@
border: 0;
}
}
.gitter-chat-embed-loading-wrapper {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: none;
/* main axis */
justify-content: center;
/* cross axis */
align-items: center;
.is-loading & {
display: flex;
}
}
.gitter-chat-embed-loading-indicator {
color: rgba(0, 0, 0, 0.75);
animation: spin 2s infinite linear;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359.9deg);
}
}
.gitter-chat-embed-action-bar {
position: absolute;
top: 0;
......@@ -49,10 +108,10 @@
color: #3a3133;
color: rgba(58, 49, 51, 0.65);
&:hover {
color: rgba(58, 49, 51, 1);
}
}
.gitter-chat-embed-action-bar-item {
......
......@@ -11,7 +11,7 @@ import chatCss from '../css/chat.css';
let concat = function(...args) {
return args.reduce(function(result, item) {
// If array-like
if(item.length && !Array.isArray(item)) {
if(item && item.length && !Array.isArray(item)) {
item = Array.prototype.slice.call(item);
}
......@@ -89,8 +89,12 @@ let gitterSvgSprites = `
<symbol id="gitter-shape-external-link" viewBox="0 0 1792 1792">
<path d="M1408 928v320q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h704q14 0 23 9t9 23v64q0 14-9 23t-23 9h-704q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-320q0-14 9-23t23-9h64q14 0 23 9t9 23zm384-864v512q0 26-19 45t-45 19-45-19l-176-176-652 652q-10 10-23 10t-23-10l-114-114q-10-10-10-23t10-23l652-652-176-176q-19-19-19-45t19-45 45-19h512q26 0 45 19t19 45z"/>
</symbol>
<symbol id="gitter-shape-spinner" viewBox="0 0 1792 1792">
<path d="M526 1394q0 53-37.5 90.5t-90.5 37.5q-52 0-90-38t-38-90q0-53 37.5-90.5t90.5-37.5 90.5 37.5 37.5 90.5zm498 206q0 53-37.5 90.5t-90.5 37.5-90.5-37.5-37.5-90.5 37.5-90.5 90.5-37.5 90.5 37.5 37.5 90.5zm-704-704q0 53-37.5 90.5t-90.5 37.5-90.5-37.5-37.5-90.5 37.5-90.5 90.5-37.5 90.5 37.5 37.5 90.5zm1202 498q0 52-38 90t-90 38q-53 0-90.5-37.5t-37.5-90.5 37.5-90.5 90.5-37.5 90.5 37.5 37.5 90.5zm-964-996q0 66-47 113t-113 47-113-47-47-113 47-113 113-47 113 47 47 113zm1170 498q0 53-37.5 90.5t-90.5 37.5-90.5-37.5-37.5-90.5 37.5-90.5 90.5-37.5 90.5 37.5 37.5 90.5zm-640-704q0 80-56 136t-136 56-136-56-56-136 56-136 136-56 136 56 56 136zm530 206q0 93-66 158.5t-158 65.5q-93 0-158.5-65.5t-65.5-158.5q0-92 65.5-158t158.5-66q92 0 158 66t66 158z"/>
</symbol>
</defs>
</svg>`;
</svg>
`;
let embedGitterSvgSprites = function() {
let elementStore = new ElementStore();
......@@ -109,13 +113,7 @@ let embedGitterSvgSprites = function() {
let embedGitterChat = function(opts) {
let elementStore = new ElementStore();
let containers = coerceIntoElementsArray(opts.container || (() => {
let container = elementStore.createElement('aside');
container.classList.add('gitter-chat-embed');
document.body.appendChild(container);
return container;
})());
let containers = opts.container;
containers.forEach((container) => {
let containerOpts = getDataOptionsFromElement(opts, container);
......@@ -134,10 +132,7 @@ let embedGitterChat = function(opts) {
});
return {
containers,
elementStore
};
return elementStore;
};
......@@ -157,7 +152,7 @@ let defaults = {
activation: null,
// Whether to preload the gitter chat iframe.
// We preload the chat so there isn't any jank when the chat opens
preload: true,
preload: false,
// Whether to embed a `<style>` tag with some pre-made CSS
useStyles: true,
......@@ -169,6 +164,7 @@ let defaults = {
//showLeftMenu: false
};
// Make the defaults a little more immutable
Object.keys(defaults).forEach((key) => {
Object.defineProperty(defaults, key, {
value: defaults[key],
......@@ -177,25 +173,38 @@ Object.keys(defaults).forEach((key) => {
});
});
// Keep some stuff behind symbols so people "can't" access the private data
const DEFAULTS = Symbol();
const OPTIONS = Symbol();
const ELEMENTSTORE = Symbol();
const INIT = Symbol();
const CONTAINERS = Symbol();
const ISEMBEDDED = Symbol();
const EMBEDCHATONCE = Symbol();
class chatEmbed {
const TOGGLECONTAINERS = Symbol();
class chatEmbed {
constructor(options = {}) {
this[ELEMENTSTORE] = new ElementStore();
this[DEFAULTS] = defaults;
this[DEFAULTS] = objectAssign({}, defaults);
// Coerce into array of dom elements on what they pass in
if(options.container) {
options.container = coerceIntoElementsArray(options.container);
}
// Otherwise create our own default container
else {
this[DEFAULTS].container = coerceIntoElementsArray((() => {
let container = this[ELEMENTSTORE].createElement('aside');
container.classList.add('gitter-chat-embed');
// Start out collapsed
container.classList.add('is-collapsed');
document.body.appendChild(container);
return container;
})());
}
this[OPTIONS] = objectAssign({}, this[DEFAULTS], options);
......@@ -210,11 +219,22 @@ class chatEmbed {
this[ELEMENTSTORE] = this[ELEMENTSTORE].concat(embedGitterSvgSprites());
}
let containers = opts.container;
containers.forEach((container) => {
let loadingIndicatorElement = this[ELEMENTSTORE].createElement('div');
loadingIndicatorElement.classList.add('gitter-chat-embed-loading-wrapper');
loadingIndicatorElement.innerHTML = `
<svg class=" gitter-chat-embed-loading-indicator gitter-icon"><use xlink:href="#gitter-shape-spinner"></use></svg>
`;
// Prepend
container.insertBefore(loadingIndicatorElement, container.firstChild);
});
if(opts.preload) {
this.toggleChat(false);
}
if(opts.showChatByDefault) {
this.toggleChat(true);
}
......@@ -239,7 +259,7 @@ class chatEmbed {
e.preventDefault();
});
this[CONTAINERS].forEach((container) => {
opts.container.forEach((container) => {
container.on('gitter-chat-toggle', (e) => {
let isChatOpen = e.detail.state;
// Toggle the visibiltiy of the activation element
......@@ -254,16 +274,18 @@ class chatEmbed {
}
[EMBEDCHATONCE]() {
if(!this[CONTAINERS]) {
let embedResult = embedGitterChat(this[OPTIONS]);
this[CONTAINERS] = embedResult.containers;
this[ELEMENTSTORE] = this[ELEMENTSTORE].concat(embedResult.elementStore);
if(!this[ISEMBEDDED]) {
let opts = this[OPTIONS];
let embedResult = embedGitterChat(this[OPTIONS]);
this[ELEMENTSTORE] = this[ELEMENTSTORE].concat(embedResult);
this[CONTAINERS].forEach((container) => {
let containers = opts.container;
containers.forEach((container) => {
let actionBar = this[ELEMENTSTORE].createElement('div');
actionBar.classList.add('gitter-chat-embed-action-bar');
// Prepend
container.insertBefore(actionBar, container.firstChild);
let collapseActionElement = this[ELEMENTSTORE].createElement('button');
......@@ -299,19 +321,20 @@ class chatEmbed {
});
}
this[ISEMBEDDED] = true;
}
[TOGGLECONTAINERS](state) {
let opts = this[OPTIONS];
// Public API
toggleChat(state) {
this[EMBEDCHATONCE]();
if(!this[CONTAINERS]) {
if(!opts.container) {
console.warn('Gitter Sidecar: No chat embed elements to toggle visibility on');
}
coerceIntoElementsArray(this[CONTAINERS]).forEach(function(container) {
let containers = opts.container;
containers.forEach((container) => {
container.classList.toggle('is-collapsed', !state);
let event = new CustomEvent('gitter-chat-toggle', {
......@@ -323,6 +346,38 @@ class chatEmbed {
});
}
// Public API
toggleChat(state) {
let opts = this[OPTIONS];
// We delay the embed to make sure the animation can go jank free
// if it isn't already embedded
if(state && !this[ISEMBEDDED]) {
let containers = opts.container;
// Start the loading spinner
containers.forEach((container) => {
container.classList.add('is-loading');
});
setTimeout(() => {
this[EMBEDCHATONCE]();
this[TOGGLECONTAINERS](state);
// Remove the loading spinner
containers.forEach((container) => {
container.classList.remove('is-loading');
});
}, 300/* TODO change to transition/animation end, see for robust transition/animation end code: https://github.com/MadLittleMods/jquery-carouselss */);
}
// But we still want people to embed no matter what state :)
// For example `options.preload`, should load the chat but not show it
else {
this[EMBEDCHATONCE]();
this[TOGGLECONTAINERS](state);
}
}
destroy() {
this[ELEMENTSTORE].destroy();
}
......
......@@ -5,6 +5,22 @@ var context = require('postcss-plugin-context');
var autoprefixer = require('autoprefixer');
var csswring = require('csswring');
var nested = require('postcss-nested');
var cssvariables = require('postcss-css-variables');
var borderBox = postcss.plugin('postcss-border-box', function (opts) {
opts = opts || {};
return function(css) {
css.eachRule(function(rule) {
var decl = postcss.decl({
prop: 'box-sizing',
value: 'border-box'
});
rule.prepend(decl);
});
};
});
module.exports = {
entry: './src/index.js',
......@@ -31,19 +47,9 @@ module.exports = {
postcss: function () {
return [
nested(),
cssvariables(),
context({
'border-box': postcss.plugin('postcss-border-box', function (opts) {
opts = opts || {};
return function(css) {
css.eachRule(function(rule) {
var decl = postcss.decl({
prop: 'box-sizing',
value: 'border-box'
});
rule.prepend(decl);
});
};
})()
'border-box': borderBox
}),
autoprefixer(),
csswring()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment