/**
|
|
Slideable is a control that can be dragged either horizontally or vertically
|
|
between a minimum and a maximum value. When released from dragging, a
|
|
Slideable will animate to its minimum or maximum position, depending on the
|
|
direction of the drag.
|
|
|
|
The *min* value specifies a position left of or above the initial position,
|
|
to which the Slideable may be dragged.
|
|
The *max* value specifies a position right of or below the initial position,
|
|
to which the Slideable may be dragged.
|
|
The *value* property specifies the current position of the Slideable,
|
|
between the minimum and maximum positions.
|
|
|
|
*min*, *max*, and *value* may be specified in units of "px" or "%".
|
|
|
|
The *axis* property controls whether the Slideable slides left-right (h) or
|
|
up-down (v).
|
|
|
|
The following control is placed 90% off the screen to the right, and slides
|
|
to its natural position.
|
|
|
|
{kind: "enyo.Slideable", value: -90, min: -90, unit: "%",
|
|
classes: "enyo-fit", style: "width: 300px;",
|
|
components: [
|
|
{content: "stuff"}
|
|
]
|
|
}
|
|
*/
|
|
enyo.kind({
|
|
name: "enyo.Slideable",
|
|
kind: "Control",
|
|
published: {
|
|
//* Direction of sliding; can be "h" or "v"
|
|
axis: "h",
|
|
//* A value between min and max to position the Slideable
|
|
value: 0,
|
|
//* Unit for min, max, and value; can be "px" or "%"
|
|
unit: "px",
|
|
//* A minimum value to slide to
|
|
min: 0,
|
|
//* A maximum value to slide to
|
|
max: 0,
|
|
accelerated: "auto",
|
|
//* Set to false to prevent the Slideable from dragging with elasticity past its min/max value.
|
|
overMoving: true,
|
|
//* Set to false to disable dragging.
|
|
draggable: true
|
|
},
|
|
events: {
|
|
//* Fires when the Slideable finishes animating.
|
|
onAnimateFinish: "",
|
|
onChange: ""
|
|
},
|
|
// Set to true to prevent a drag from bubbling beyond the Slideable.
|
|
preventDragPropagation: false,
|
|
//* @protected
|
|
tools: [
|
|
{kind: "Animator", onStep: "animatorStep", onEnd: "animatorComplete"}
|
|
],
|
|
handlers: {
|
|
ondragstart: "dragstart",
|
|
ondrag: "drag",
|
|
ondragfinish: "dragfinish"
|
|
},
|
|
kDragScalar: 1,
|
|
dragEventProp: "dx",
|
|
unitModifier: false,
|
|
canTransform: false,
|
|
//* @protected
|
|
create: function() {
|
|
this.inherited(arguments);
|
|
this.acceleratedChanged();
|
|
this.transformChanged();
|
|
this.axisChanged();
|
|
this.valueChanged();
|
|
this.addClass("enyo-slideable");
|
|
},
|
|
initComponents: function() {
|
|
this.createComponents(this.tools);
|
|
this.inherited(arguments);
|
|
},
|
|
rendered: function() {
|
|
this.inherited(arguments);
|
|
this.canModifyUnit();
|
|
this.updateDragScalar();
|
|
},
|
|
resizeHandler: function() {
|
|
this.inherited(arguments);
|
|
this.updateDragScalar();
|
|
},
|
|
canModifyUnit: function() {
|
|
if (!this.canTransform) {
|
|
var b = this.getInitialStyleValue(this.hasNode(), this.boundary);
|
|
// If inline style of "px" exists, while unit is "%"
|
|
if (b.match(/px/i) && (this.unit === "%")) {
|
|
// Set unitModifier - used to over-ride "%"
|
|
this.unitModifier = this.getBounds()[this.dimension];
|
|
}
|
|
}
|
|
},
|
|
getInitialStyleValue: function(inNode, inBoundary) {
|
|
var s = enyo.dom.getComputedStyle(inNode);
|
|
if (s) {
|
|
return s.getPropertyValue(inBoundary);
|
|
} else if (inNode && inNode.currentStyle) {
|
|
return inNode.currentStyle[inBoundary];
|
|
}
|
|
return "0";
|
|
},
|
|
updateBounds: function(inValue, inDimensions) {
|
|
var inBounds = {};
|
|
inBounds[this.boundary] = inValue;
|
|
this.setBounds(inBounds, this.unit);
|
|
|
|
this.setInlineStyles(inValue, inDimensions);
|
|
},
|
|
updateDragScalar: function() {
|
|
if (this.unit == "%") {
|
|
var d = this.getBounds()[this.dimension];
|
|
this.kDragScalar = d ? 100 / d : 1;
|
|
|
|
if (!this.canTransform) {
|
|
this.updateBounds(this.value, 100);
|
|
}
|
|
}
|
|
},
|
|
transformChanged: function() {
|
|
this.canTransform = enyo.dom.canTransform();
|
|
},
|
|
acceleratedChanged: function() {
|
|
if (!(enyo.platform.android > 2)) {
|
|
enyo.dom.accelerate(this, this.accelerated);
|
|
}
|
|
},
|
|
axisChanged: function() {
|
|
var h = this.axis == "h";
|
|
this.dragMoveProp = h ? "dx" : "dy";
|
|
this.shouldDragProp = h ? "horizontal" : "vertical";
|
|
this.transform = h ? "translateX" : "translateY";
|
|
this.dimension = h ? "width" : "height";
|
|
this.boundary = h ? "left" : "top";
|
|
},
|
|
setInlineStyles: function(inValue, inDimensions) {
|
|
var inBounds = {};
|
|
|
|
if (this.unitModifier) {
|
|
inBounds[this.boundary] = this.percentToPixels(inValue, this.unitModifier);
|
|
inBounds[this.dimension] = this.unitModifier;
|
|
this.setBounds(inBounds);
|
|
} else {
|
|
if (inDimensions) {
|
|
inBounds[this.dimension] = inDimensions;
|
|
} else {
|
|
inBounds[this.boundary] = inValue;
|
|
}
|
|
this.setBounds(inBounds, this.unit);
|
|
}
|
|
},
|
|
valueChanged: function(inLast) {
|
|
var v = this.value;
|
|
if (this.isOob(v) && !this.isAnimating()) {
|
|
this.value = this.overMoving ? this.dampValue(v) : this.clampValue(v);
|
|
}
|
|
// FIXME: android cannot handle nested compositing well so apply acceleration only if needed
|
|
// desktop chrome doesn't like this code path so avoid...
|
|
if (enyo.platform.android > 2) {
|
|
if (this.value) {
|
|
if (inLast === 0 || inLast === undefined) {
|
|
enyo.dom.accelerate(this, this.accelerated);
|
|
}
|
|
} else {
|
|
enyo.dom.accelerate(this, false);
|
|
}
|
|
}
|
|
|
|
// If platform supports transforms
|
|
if (this.canTransform) {
|
|
enyo.dom.transformValue(this, this.transform, this.value + this.unit);
|
|
// else update inline styles
|
|
} else {
|
|
this.setInlineStyles(this.value, false);
|
|
}
|
|
this.doChange();
|
|
},
|
|
getAnimator: function() {
|
|
return this.$.animator;
|
|
},
|
|
isAtMin: function() {
|
|
return this.value <= this.calcMin();
|
|
},
|
|
isAtMax: function() {
|
|
return this.value >= this.calcMax();
|
|
},
|
|
calcMin: function() {
|
|
return this.min;
|
|
},
|
|
calcMax: function() {
|
|
return this.max;
|
|
},
|
|
clampValue: function(inValue) {
|
|
var min = this.calcMin();
|
|
var max = this.calcMax();
|
|
return Math.max(min, Math.min(inValue, max));
|
|
},
|
|
dampValue: function(inValue) {
|
|
return this.dampBound(this.dampBound(inValue, this.min, 1), this.max, -1);
|
|
},
|
|
dampBound: function(inValue, inBoundary, inSign) {
|
|
var v = inValue;
|
|
if (v * inSign < inBoundary * inSign) {
|
|
v = inBoundary + (v - inBoundary) / 4;
|
|
}
|
|
return v;
|
|
},
|
|
percentToPixels: function(value, dimension) {
|
|
return Math.floor((dimension / 100) * value);
|
|
},
|
|
pixelsToPercent: function(value) {
|
|
var boundary = this.unitModifier ? this.getBounds()[this.dimension] : this.container.getBounds()[this.dimension];
|
|
return (value / boundary) * 100;
|
|
},
|
|
// dragging
|
|
shouldDrag: function(inEvent) {
|
|
return this.draggable && inEvent[this.shouldDragProp];
|
|
},
|
|
isOob: function(inValue) {
|
|
return inValue > this.calcMax() || inValue < this.calcMin();
|
|
},
|
|
dragstart: function(inSender, inEvent) {
|
|
if (this.shouldDrag(inEvent)) {
|
|
inEvent.preventDefault();
|
|
this.$.animator.stop();
|
|
inEvent.dragInfo = {};
|
|
this.dragging = true;
|
|
this.drag0 = this.value;
|
|
this.dragd0 = 0;
|
|
return this.preventDragPropagation;
|
|
}
|
|
},
|
|
drag: function(inSender, inEvent) {
|
|
if (this.dragging) {
|
|
inEvent.preventDefault();
|
|
var d = this.canTransform ? inEvent[this.dragMoveProp] * this.kDragScalar : this.pixelsToPercent(inEvent[this.dragMoveProp]);
|
|
var v = this.drag0 + d;
|
|
var dd = d - this.dragd0;
|
|
this.dragd0 = d;
|
|
if (dd) {
|
|
inEvent.dragInfo.minimizing = dd < 0;
|
|
}
|
|
this.setValue(v);
|
|
return this.preventDragPropagation;
|
|
}
|
|
},
|
|
dragfinish: function(inSender, inEvent) {
|
|
if (this.dragging) {
|
|
this.dragging = false;
|
|
this.completeDrag(inEvent);
|
|
inEvent.preventTap();
|
|
return this.preventDragPropagation;
|
|
}
|
|
},
|
|
completeDrag: function(inEvent) {
|
|
if (this.value !== this.calcMax() && this.value != this.calcMin()) {
|
|
this.animateToMinMax(inEvent.dragInfo.minimizing);
|
|
}
|
|
},
|
|
// animation
|
|
isAnimating: function() {
|
|
return this.$.animator.isAnimating();
|
|
},
|
|
play: function(inStart, inEnd) {
|
|
this.$.animator.play({
|
|
startValue: inStart,
|
|
endValue: inEnd,
|
|
node: this.hasNode()
|
|
});
|
|
},
|
|
//* @public
|
|
//* Animates to the given value.
|
|
animateTo: function(inValue) {
|
|
this.play(this.value, inValue);
|
|
},
|
|
//* Animates to the minimum value.
|
|
animateToMin: function() {
|
|
this.animateTo(this.calcMin());
|
|
},
|
|
//* Animates to the maximum value.
|
|
animateToMax: function() {
|
|
this.animateTo(this.calcMax());
|
|
},
|
|
//* @protected
|
|
animateToMinMax: function(inMin) {
|
|
if (inMin) {
|
|
this.animateToMin();
|
|
} else {
|
|
this.animateToMax();
|
|
}
|
|
},
|
|
animatorStep: function(inSender) {
|
|
this.setValue(inSender.value);
|
|
return true;
|
|
},
|
|
animatorComplete: function(inSender) {
|
|
this.doAnimateFinish(inSender);
|
|
return true;
|
|
},
|
|
//* @public
|
|
//* Toggles between min and max with animation.
|
|
toggleMinMax: function() {
|
|
this.animateToMinMax(!this.isAtMin());
|
|
}
|
|
});
|