/**
* Provides a plugin, which adds support for a scroll indicator to ScrollView instances
*
* @module scrollview
* @submodule scrollview-scrollbars
*/
var getClassName = Y.ClassNameManager.getClassName,
_classNames,
Transition = Y.Transition,
NATIVE_TRANSITIONS = Transition.useNative,
SCROLLBAR = 'scrollbar',
SCROLLVIEW = 'scrollview',
VERTICAL_NODE = "verticalNode",
HORIZONTAL_NODE = "horizontalNode",
CHILD_CACHE = "childCache",
TOP = "top",
LEFT = "left",
WIDTH = "width",
HEIGHT = "height",
HORIZ_CACHE = "_sbh",
VERT_CACHE = "_sbv",
TRANSITION_PROPERTY = Y.ScrollView._TRANSITION.PROPERTY,
TRANSFORM = "transform",
TRANSLATE_X = "translateX(",
TRANSLATE_Y = "translateY(",
SCALE_X = "scaleX(",
SCALE_Y = "scaleY(",
SCROLL_X = "scrollX",
SCROLL_Y = "scrollY",
PX = "px",
CLOSE = ")",
PX_CLOSE = PX + CLOSE;
/**
* ScrollView plugin that adds scroll indicators to ScrollView instances
*
* @class ScrollViewScrollbars
* @namespace Plugin
* @extends Plugin.Base
* @constructor
*/
function ScrollbarsPlugin() {
ScrollbarsPlugin.superclass.constructor.apply(this, arguments);
}
ScrollbarsPlugin.CLASS_NAMES = {
showing: getClassName(SCROLLVIEW, SCROLLBAR, 'showing'),
scrollbar: getClassName(SCROLLVIEW, SCROLLBAR),
scrollbarV: getClassName(SCROLLVIEW, SCROLLBAR, 'vert'),
scrollbarH: getClassName(SCROLLVIEW, SCROLLBAR, 'horiz'),
scrollbarVB: getClassName(SCROLLVIEW, SCROLLBAR, 'vert', 'basic'),
scrollbarHB: getClassName(SCROLLVIEW, SCROLLBAR, 'horiz', 'basic'),
child: getClassName(SCROLLVIEW, 'child'),
first: getClassName(SCROLLVIEW, 'first'),
middle: getClassName(SCROLLVIEW, 'middle'),
last: getClassName(SCROLLVIEW, 'last')
};
_classNames = ScrollbarsPlugin.CLASS_NAMES;
/**
* The identity of the plugin
*
* @property NAME
* @type String
* @default 'pluginScrollViewScrollbars'
* @static
*/
ScrollbarsPlugin.NAME = 'pluginScrollViewScrollbars';
/**
* The namespace on which the plugin will reside.
*
* @property NS
* @type String
* @default 'scrollbars'
* @static
*/
ScrollbarsPlugin.NS = 'scrollbars';
/**
* HTML template for the scrollbar
*
* @property SCROLLBAR_TEMPLATE
* @type Object
* @static
*/
ScrollbarsPlugin.SCROLLBAR_TEMPLATE = [
'<div>',
'<span class="' + _classNames.child + ' ' + _classNames.first + '"></span>',
'<span class="' + _classNames.child + ' ' + _classNames.middle + '"></span>',
'<span class="' + _classNames.child + ' ' + _classNames.last + '"></span>',
'</div>'
].join('');
/**
* The default attribute configuration for the plugin
*
* @property ATTRS
* @type Object
* @static
*/
ScrollbarsPlugin.ATTRS = {
/**
* Vertical scrollbar node
*
* @attribute verticalNode
* @type Y.Node
*/
verticalNode: {
setter: '_setNode',
valueFn: '_defaultNode'
},
/**
* Horizontal scrollbar node
*
* @attribute horizontalNode
* @type Y.Node
*/
horizontalNode: {
setter: '_setNode',
valueFn: '_defaultNode'
}
};
Y.namespace("Plugin").ScrollViewScrollbars = Y.extend(ScrollbarsPlugin, Y.Plugin.Base, {
/**
* Designated initializer
*
* @method initializer
*/
initializer: function() {
this._host = this.get("host");
this.afterHostEvent('scrollEnd', this._hostScrollEnd);
this.afterHostMethod('scrollTo', this._update);
this.afterHostMethod('_uiDimensionsChange', this._hostDimensionsChange);
},
/**
* Set up the DOM nodes for the scrollbars. This method is invoked whenever the
* host's _uiDimensionsChange fires, giving us the opportunity to remove un-needed
* scrollbars, as well as add one if necessary.
*
* @method _hostDimensionsChange
* @protected
*/
_hostDimensionsChange: function() {
var host = this._host,
axis = host._cAxis,
scrollX = host.get(SCROLL_X),
scrollY = host.get(SCROLL_Y);
this._dims = host._getScrollDims();
if (axis && axis.y) {
this._renderBar(this.get(VERTICAL_NODE), true, 'vert');
}
if (axis && axis.x) {
this._renderBar(this.get(HORIZONTAL_NODE), true, 'horiz');
}
this._update(scrollX, scrollY);
Y.later(500, this, 'flash', true);
},
/**
* Handler for the scrollEnd event fired by the host. Default implementation flashes the scrollbar
*
* @method _hostScrollEnd
* @param {EventFacade} e The event facade.
* @protected
*/
_hostScrollEnd : function() {
var host = this._host,
scrollX = host.get(SCROLL_X),
scrollY = host.get(SCROLL_Y);
this.flash();
this._update(scrollX, scrollY);
},
/**
* Adds or removes a scrollbar node from the document.
*
* @method _renderBar
* @private
* @param {Node} bar The scrollbar node
* @param {boolean} add true, to add the node, false to remove it
*/
_renderBar: function(bar, add) {
var inDoc = bar.inDoc(),
bb = this._host._bb,
className = bar.getData("isHoriz") ? _classNames.scrollbarHB : _classNames.scrollbarVB;
if (add && !inDoc) {
bb.append(bar);
bar.toggleClass(className, this._basic);
this._setChildCache(bar);
} else if(!add && inDoc) {
bar.remove();
this._clearChildCache(bar);
}
},
/**
* Caches scrollbar child element information,
* to optimize _update implementation
*
* @method _setChildCache
* @private
* @param {Node} node
*/
_setChildCache : function(node) {
var c = node.get("children"),
fc = c.item(0),
mc = c.item(1),
lc = c.item(2),
size = node.getData("isHoriz") ? "offsetWidth" : "offsetHeight";
node.setStyle(TRANSITION_PROPERTY, TRANSFORM);
mc.setStyle(TRANSITION_PROPERTY, TRANSFORM);
lc.setStyle(TRANSITION_PROPERTY, TRANSFORM);
node.setData(CHILD_CACHE, {
fc : fc,
lc : lc,
mc : mc,
fcSize : fc && fc.get(size),
lcSize : lc && lc.get(size)
});
},
/**
* Clears child cache
*
* @method _clearChildCache
* @private
* @param {Node} node
*/
_clearChildCache : function(node) {
node.clearData(CHILD_CACHE);
},
/**
* Utility method, to move/resize either vertical or horizontal scrollbars
*
* @method _updateBar
* @private
*
* @param {Node} scrollbar The scrollbar node.
* @param {Number} current The current scroll position.
* @param {Number} duration The transition duration.
* @param {boolean} horiz true if horizontal, false if vertical.
*/
_updateBar : function(scrollbar, current, duration, horiz) {
var host = this._host,
basic = this._basic,
scrollbarSize = 0,
scrollbarPos = 1,
childCache = scrollbar.getData(CHILD_CACHE),
lastChild = childCache.lc,
middleChild = childCache.mc,
firstChildSize = childCache.fcSize,
lastChildSize = childCache.lcSize,
middleChildSize,
lastChildPosition,
transition,
translate,
scale,
dim,
dimOffset,
dimCache,
widgetSize,
contentSize;
if (horiz) {
dim = WIDTH;
dimOffset = LEFT;
dimCache = HORIZ_CACHE;
widgetSize = this._dims.offsetWidth;
contentSize = this._dims.scrollWidth;
translate = TRANSLATE_X;
scale = SCALE_X;
current = (current !== undefined) ? current : host.get(SCROLL_X);
} else {
dim = HEIGHT;
dimOffset = TOP;
dimCache = VERT_CACHE;
widgetSize = this._dims.offsetHeight;
contentSize = this._dims.scrollHeight;
translate = TRANSLATE_Y;
scale = SCALE_Y;
current = (current !== undefined) ? current : host.get(SCROLL_Y);
}
scrollbarSize = Math.floor(widgetSize * (widgetSize/contentSize));
scrollbarPos = Math.floor((current/(contentSize - widgetSize)) * (widgetSize - scrollbarSize));
if (scrollbarSize > widgetSize) {
scrollbarSize = 1;
}
if (scrollbarPos > (widgetSize - scrollbarSize)) {
scrollbarSize = scrollbarSize - (scrollbarPos - (widgetSize - scrollbarSize));
} else if (scrollbarPos < 0) {
scrollbarSize = scrollbarPos + scrollbarSize;
scrollbarPos = 0;
} else if (isNaN(scrollbarPos)) {
scrollbarPos = 0;
}
middleChildSize = (scrollbarSize - (firstChildSize + lastChildSize));
if (middleChildSize < 0) {
middleChildSize = 0;
}
if (middleChildSize === 0 && scrollbarPos !== 0) {
scrollbarPos = widgetSize - (firstChildSize + lastChildSize) - 1;
}
if (duration !== 0) {
// Position Scrollbar
transition = {
duration : duration
};
if (NATIVE_TRANSITIONS) {
transition.transform = translate + scrollbarPos + PX_CLOSE;
} else {
transition[dimOffset] = scrollbarPos + PX;
}
scrollbar.transition(transition);
} else {
if (NATIVE_TRANSITIONS) {
scrollbar.setStyle(TRANSFORM, translate + scrollbarPos + PX_CLOSE);
} else {
scrollbar.setStyle(dimOffset, scrollbarPos + PX);
}
}
// Resize Scrollbar Middle Child
if (this[dimCache] !== middleChildSize) {
this[dimCache] = middleChildSize;
if (middleChildSize > 0) {
if (duration !== 0) {
transition = {
duration : duration
};
if(NATIVE_TRANSITIONS) {
transition.transform = scale + middleChildSize + CLOSE;
} else {
transition[dim] = middleChildSize + PX;
}
middleChild.transition(transition);
} else {
if (NATIVE_TRANSITIONS) {
middleChild.setStyle(TRANSFORM, scale + middleChildSize + CLOSE);
} else {
middleChild.setStyle(dim, middleChildSize + PX);
}
}
// Position Last Child
if (!horiz || !basic) {
lastChildPosition = scrollbarSize - lastChildSize;
if(duration !== 0) {
transition = {
duration : duration
};
if (NATIVE_TRANSITIONS) {
transition.transform = translate + lastChildPosition + PX_CLOSE;
} else {
transition[dimOffset] = lastChildPosition;
}
lastChild.transition(transition);
} else {
if (NATIVE_TRANSITIONS) {
lastChild.setStyle(TRANSFORM, translate + lastChildPosition + PX_CLOSE);
} else {
lastChild.setStyle(dimOffset, lastChildPosition + PX);
}
}
}
}
}
},
/**
* AOP method, invoked after the host's _uiScrollTo method,
* to position and resize the scroll bars
*
* @method _update
* @param x {Number} The current scrollX value
* @param y {Number} The current scrollY value
* @param duration {Number} Number of ms of animation (optional) - used when snapping to bounds
* @param easing {String} Optional easing equation to use during the animation, if duration is set
* @protected
*/
_update: function(x, y, duration) {
var vNode = this.get(VERTICAL_NODE),
hNode = this.get(HORIZONTAL_NODE),
host = this._host,
axis = host._cAxis;
duration = (duration || 0)/1000;
if (!this._showing) {
this.show();
}
if (axis && axis.y && vNode && y !== null) {
this._updateBar(vNode, y, duration, false);
}
if (axis && axis.x && hNode && x !== null) {
this._updateBar(hNode, x, duration, true);
}
},
/**
* Show the scroll bar indicators
*
* @method show
* @param animated {Boolean} Whether or not to animate the showing
*/
show: function(animated) {
this._show(true, animated);
},
/**
* Hide the scroll bar indicators
*
* @method hide
* @param animated {Boolean} Whether or not to animate the hiding
*/
hide: function(animated) {
this._show(false, animated);
},
/**
* Internal hide/show implementation utility method
*
* @method _show
* @param {boolean} show Whether to show or hide the scrollbar
* @param {bolean} animated Whether or not to animate while showing/hide
* @protected
*/
_show : function(show, animated) {
var verticalNode = this.get(VERTICAL_NODE),
horizontalNode = this.get(HORIZONTAL_NODE),
duration = (animated) ? 0.6 : 0,
opacity = (show) ? 1 : 0,
transition;
this._showing = show;
if (this._flashTimer) {
this._flashTimer.cancel();
}
transition = {
duration : duration,
opacity : opacity
};
if (verticalNode && verticalNode._node) {
verticalNode.transition(transition);
}
if (horizontalNode && horizontalNode._node) {
horizontalNode.transition(transition);
}
},
/**
* Momentarily flash the scroll bars to indicate current scroll position
*
* @method flash
*/
flash: function() {
this.show(true);
this._flashTimer = Y.later(800, this, 'hide', true);
},
/**
* Setter for the verticalNode and horizontalNode attributes
*
* @method _setNode
* @param node {Node} The Y.Node instance for the scrollbar
* @param name {String} The attribute name
* @return {Node} The Y.Node instance for the scrollbar
*
* @protected
*/
_setNode: function(node, name) {
var horiz = (name === HORIZONTAL_NODE);
node = Y.one(node);
if (node) {
node.addClass(_classNames.scrollbar);
node.addClass( (horiz) ? _classNames.scrollbarH : _classNames.scrollbarV );
node.setData("isHoriz", horiz);
}
return node;
},
/**
* Creates default node instances for scrollbars
*
* @method _defaultNode
* @return {Node} The Y.Node instance for the scrollbar
*
* @protected
*/
_defaultNode: function() {
return Y.Node.create(ScrollbarsPlugin.SCROLLBAR_TEMPLATE);
},
_basic: Y.UA.ie && Y.UA.ie <= 8
});