foam-framework
Version:
MVC metaprogramming framework
408 lines (390 loc) • 13.1 kB
JavaScript
/**
* @license
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
CLASS({
package: 'foam.ui',
name: 'SlidePanel',
extends: 'foam.ui.View',
requires: [
'foam.input.touch.GestureTarget'
],
imports: [
'clearTimeout',
'document',
'gestureManager',
'setTimeout'
],
constants: {
ANIMATION_DELAY: 150,
LEFT: {
panelX: function(x) {
return this.parentWidth - x - this.panelWidth;
},
invPanelX: function(x) {
return x - this.parentWidth + this.panelWidth;
},
mainX: function() {
return this.parentWidth - this.mainWidth;
},
dragDir: -1
},
RIGHT: {
panelX: function(x) {
return x;
},
invPanelX: function(x) {
return x;
},
mainX: function() {
return 0;
},
dragDir: 1
},
CLOSED: {
name: 'CLOSED',
layout: function() {
return [ this.parentWidth - this.stripWidth, this.minPanelWidth, this.stripWidth ];
},
onResize: function() {
if ( this.parentWidth > this.minWidth + this.minPanelWidth )
this.state = this.EXPANDED;
},
toggle: function() { this.open(); },
open: function() { this.state = this.OPEN; },
over: true
},
EXPANDED: {
name: 'EXPANDED',
layout: function() {
var extraWidth = this.parentWidth - this.minWidth - this.minPanelWidth;
var panelWidth = this.minPanelWidth + extraWidth * this.panelRatio;
return [ this.parentWidth - panelWidth, panelWidth, panelWidth ];
},
onResize: function() {
if ( this.parentWidth < this.minWidth + this.minPanelWidth )
this.state = this.CLOSED;
}
},
OPEN: {
name: 'OPEN',
layout: function() {
return [ this.parentWidth - this.stripWidth, this.minPanelWidth, this.minPanelWidth ];
},
onResize: function() {
if ( this.parentWidth > this.minWidth + this.minPanelWidth )
this.state = this.OPEN_EXPANDED;
},
close: function() { this.state = this.CLOSED; },
toggle: function() { this.close(); },
over: true
},
OPEN_EXPANDED: {
name: 'OPEN_EXPANDED',
layout: function() { return this.EXPANDED.layout.call(this); },
onResize: function() {
if ( this.parentWidth < this.minWidth + this.minPanelWidth )
this.state = this.OPEN;
}
}
},
help: 'A controller that shows a main view with a small strip of the ' +
'secondary view visible at the right edge. This "panel" can be dragged ' +
'by a finger or mouse pointer to any position from its small strip to ' +
'fully exposed. If the containing view is wide enough, both panels ' +
'will always be visible.',
properties: [
{
name: 'side',
adapt: function(_, side) {
return side === 'left' ? this.LEFT : side === 'right' ? this.RIGHT : side ;
},
lazyFactory: function() { return this.LEFT; }
},
{
name: 'state',
postSet: function(oldState, newState) {
var layout = this.state.layout.call(this);
if ( oldState === newState && ! this.af_ ) {
this.currentLayout = layout;
} else {
this.desiredLayout = layout;
}
}
},
{
name: 'currentLayout',
postSet: function(_, layout) {
this.panelWidth = Math.max(layout[1], this.minPanelWidth);
this.panelX = Math.min(this.parentWidth-this.stripWidth, this.parentWidth-layout[2]);
this.mainWidth = Math.max(layout[0], this.panelX);
}
},
{
name: 'desiredLayout',
postSet: function(_, layout) {
if ( ! this.currentLayout ) {
this.currentLayout = layout;
return;
}
var startLayout = this.currentLayout;
var start = Date.now();
var end = start + this.ANIMATION_DELAY;
var animate = function() {
var now = Date.now();
var p = (now-start) / (end-start);
if ( p < 1 ) {
var mainWidth = this.currentLayout = [
startLayout[0] * ( 1 - p ) + layout[0] * p,
startLayout[1] * ( 1 - p ) + layout[1] * p,
startLayout[2] * ( 1 - p ) + layout[2] * p
];
if ( this.af_ ) this.X.cancelAnimationFrame(this.af_);
this.af_ = this.X.requestAnimationFrame(animate);
} else {
this.currentLayout = layout;
this.af_ = null;
}
}.bind(this);
animate();
}
},
{ model_: 'ViewFactoryProperty', name: 'mainView' },
{ model_: 'ViewFactoryProperty', name: 'panelView' },
{
model_: 'IntProperty',
name: 'minWidth',
defaultValueFn: function() {
var e = this.main$();
return e ? toNum(this.X.window.getComputedStyle(e).width) : 300;
}
},
{
model_: 'IntProperty',
name: 'mainWidth',
model_: 'IntProperty',
hidden: true,
help: 'Set internally by the resize handler',
postSet: function(_, x) {
this.main$().style.width = x + 'px';
var x = this.side.mainX.call(this);
this.main$().style.webkitTransform = 'translate3d(' + x + 'px, 0,0)';
this.main$().style.MozTransform = 'translate3d(' + x + 'px, 0,0)';
}
},
{
model_: 'IntProperty',
name: 'panelWidth',
model_: 'IntProperty',
hidden: true,
help: 'Set internally by the resize handler',
postSet: function(_, x) {
this.panel$().style.width = (x+2) + 'px';
// if the panel has an onResize() method (maybe it's another SlidePanel), then call it.
this.panelView_ && this.panelView_.onResize && this.panelView_.onResize();
}
},
{
model_: 'IntProperty',
name: 'minPanelWidth',
defaultValueFn: function() {
if ( this.panelView && this.panelView.minWidth )
return this.panelView.minWidth + (this.panelView.stripWidth || 0);
var e = this.panel$();
return e ? toNum(this.X.window.getComputedStyle(e).width) : 250;
}
},
{
model_: 'IntProperty',
name: 'parentWidth',
help: 'A pseudoproperty that returns the current width (CSS pixels) of the containing element',
lazyFactory: function() {
return toNum(this.X.window.getComputedStyle(this.$.parentNode).width);
}
},
{
model_: 'IntProperty',
name: 'stripWidth',
help: 'The width in (CSS) pixels of the minimal visible strip of panel',
defaultValue: 30
},
{
model_: 'FloatProperty',
name: 'panelRatio',
help: 'The ratio (0-1) of the total width occupied by the panel, when ' +
'the containing element is wide enough for expanded view.',
defaultValue: 0.5
},
{
model_: 'IntProperty',
name: 'panelX',
postSet: function(oldX, x) {
if ( this.currentLayout ) this.currentLayout[2] = this.parentWidth-x;
if ( oldX !== x ) this.dir_ = oldX.compareTo(x);
x = this.side.panelX.call(this, x);
this.panel$().style.webkitTransform = 'translate3d(' + x + 'px, 0,0)';
this.panel$().style.MozTransform = 'translate3d(' + x + 'px, 0,0)';
}
},
{
name: 'dragGesture',
hidden: true,
transient: true,
lazyFactory: function() {
return this.GestureTarget.create({
containerID: this.id + '-panel',
handler: this,
gesture: 'drag'
});
}
},
{
name: 'tapGesture',
hidden: true,
transient: true,
lazyFactory: function() {
return this.GestureTarget.create({
containerID: this.id + '-panel',
handler: this,
gesture: 'tap'
});
}
}
],
templates: [
function CSS() {/*
.SlidePanel .left-shadow {
background: linear-gradient(to left, rgba(0,0,0,0.2) 0%,
rgba(0,0,0,0) 100%);
height: 100%;
left: -8px;
position: absolute;
width: 8px;
}
.SlidePanel .right-shadow {
background: linear-gradient(to right, rgba(0,0,0,0.2) 0%,
rgba(0,0,0,0) 100%);
height: 100%;
right: -8px;
position: absolute;
width: 8px;
top: 0;
}
*/},
function toHTML() {/*
<div id="%%id" style="display: inline-block;position: relative;" class="SlidePanel">
<div id="%%id-main" class="main">
<div style="width:0;position:absolute;"></div>
<%= this.mainView({ data$: this.data$ }) %>
</div>
<div id="%%id-panel" class="panel" style="position: absolute; top: 0; left: -1;">
<% if ( this.side === this.RIGHT ) { %> <div id="%%id-shadow" class="left-shadow"></div> <% } %>
<%= (this.panelView_ = this.panelView({ data$: this.data$ })) %>
<% if ( this.side === this.LEFT ) { %> <div id="%%id-shadow" class="right-shadow"></div> <% } %>
</div>
</div>
*/}
],
methods: {
initHTML: function() {
// Check if panel should be initially expanded
this.CLOSED.onResize.call(this);
if ( ! this.state ) this.state = this.CLOSED;
if (this.gestureManager) {
this.gestureManager.install(this.dragGesture);
this.gestureManager.install(this.tapGesture);
}
// Resize first, then init the outer view, and finally the panel view.
this.X.window.addEventListener('resize', this.onResize);
this.main$().addEventListener('click', this.onMainFocus);
this.main$().addEventListener('DOMFocusIn', this.onMainFocus);
this.panel$().addEventListener('DOMFocusIn', this.onPanelFocus);
this.initChildren(); // We didn't call SUPER(), so we have to do this here.
},
interpolate: function(state1, state2) {
var layout1 = state1.layout.call(this);
var layout2 = state2.layout.call(this);
return [
layout1[0] * this.progress + layout2[0] * ( 1 - this.progress ),
layout1[1] * this.progress + layout2[1] * ( 1 - this.progress ),
layout1[2] * this.progress + layout2[2] * ( 1 - this.progress ),
];
},
main$: function() { return this.X.$(this.id + '-main'); },
panel$: function() { return this.X.$(this.id + '-panel'); },
shadow$: function() { return this.X.$(this.id + '-shadow'); },
open: function() { this.state.open && this.state.open.call(this); },
close: function() { this.state.close && this.state.close.call(this); },
toggle: function() { this.state.toggle && this.state.toggle.call(this); }
},
listeners: [
{
name: 'onPanelFocus',
isMerged: 1,
code: function(e) { this.open(); }
},
{
name: 'onMainFocus',
isMerged: 1,
code: function(e) { this.close(); }
},
{
name: 'onResize',
isFramed: true,
code: function(e) {
this.clearProperty('parentWidth');
if ( ! this.$ ) return;
this.state.onResize.call(this);
this.shadow$().style.display = this.state.over ? 'inline' : 'none';
this.state = this.state;
}
},
{
name: 'tapClick',
code: function() { this.toggle(); }
},
{
name: 'dragStart',
code: function(point) {
if ( this.state === this.EXPANDED || this.state === this.OPEN_EXPANDED ) return;
// Otherwise, bind panelX to the absolute X.
var self = this;
var originalX = this.panelX;
Events.map(point.x$, this.panelX$, function(x) {
x = this.side.invPanelX.call(this, originalX + this.side.dragDir * point.totalX);
// Bound it between its left and right limits: full open and just the
// strip.
if ( x <= this.parentWidth - this.panelWidth )
return this.parentWidth - this.panelWidth;
if ( x >= this.parentWidth - this.stripWidth )
return this.parentWidth - this.stripWidth;
return x;
}.bind(this));
}
},
{
name: 'dragEnd',
code: function(point) {
var currentLayout = this.currentLayout;
if ( this.af_ ) this.X.cancelAnimationFrame(this.af_);
this.af_ = null;
if ( this.dir_ < 0 ) this.close(); else this.open();
var layout = this.state.layout.call(this);
this.currentLayout = currentLayout;
this.desiredLayout = layout;
}
}
]
});