/** 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()); } });