blob: 7bb7d4054b14bea3a33baf57618fae10fe90af70 [file] [log] [blame]
/**
* @file 最简单的有滚动条的panel
* 现只支持纵向滚动条。支持“anchor”。其他功能需求需要时候再加。
* anchor 名称只允许是字母和数字和$、-、_
* @author sushuang(sushuang@baidu.com)
* @date 2014-05
*/
define(function (require) {
var $ = require('jquery');
var lib = require('../lib');
var Component = require('./Component');
var toInt = lib.toInt;
require('jqueryui');
require('jquerymousewheel');
// Constants
var MAIN_CSS = 'cpt-sclpn';
var CON_CSS = MAIN_CSS + '-con';
var CON_V_SCROLL_CSS = MAIN_CSS + '-con-vscl';
var V_BAR_CSS = MAIN_CSS + '-vsclbar';
var V_BAR_THUMB_CSS = MAIN_CSS + '-vsclbar-thumb';
var DEFAULT_CON_ATTR = 'cpt-sclpn-con';
var DEFAULT_ANCHOR_ATTR = 'cpt-sclpn-anchor';
/**
* 模板中的声明方式:
* <div data-cpt="
* type: 'scrollPanel',
* viewModel: {css: 'some-css-class'}">
* <div data-cpt-sclpn-con>
* ...内容...
* </div>
* </div>
*
* 内部Componnet可直接使用
* var updater = this.getAncestorLocalOb('LOCAL_OB_SCROLL_RESIZE');
* updater && updater({});
* 来触发大小变更后的刷新。
*/
var ScrollPanel = Component.extend({
_define: {
viewModel: function () {
return {
conAttr: DEFAULT_CON_ATTR,
anchorAttr: DEFAULT_ANCHOR_ATTR,
updater: createUpdater() // 可外部传入
};
},
viewModelPublic: ['conAttr', 'anchorAttr', 'updater'],
suppressConstructSub: true, // scrollPanel 本身不拥有、不初始化子component。
css: MAIN_CSS
},
/**
* @override
*/
_init: function () {
var viewModel = this._viewModel();
var $main = this.$el();
var updater = viewModel.updater;
// 使内部Component可以触发ScrollPanel的refresh
this.localOb('LOCAL_OB_SCROLL_RESIZE', updater._refresher);
this.$el(
'con', $main.find('[data-' + viewModel.conAttr + ']')
).css({position: 'absolute', top: 0}).addClass(CON_CSS);
this._initVScrollBar();
this._initWheel();
// refresh的依赖
this._disposable(updater._refresher.subscribe(this._refresh, this));
// position的依赖
this._disposable(updater._pos.subscribe(this._applyPosObj, this));
// TODO
// 和updater绑定,输出当前位置值。后续有需求再加。
$(window).on(this._event('resize'), $.proxy(this._refresh, this));
},
/**
* @override
*/
_dispose: function () {
$(window).off(this._event());
this.$el('con').off('mousewheel');
},
/**
* @private
*/
_initVScrollBar: function () {
var $vsclbar = $([
'<div class="', V_BAR_CSS, '">',
'<div class="', V_BAR_THUMB_CSS, '">',
'</div>',
'</div>'
].join('')).appendTo(this.$el());
this.$el('vsclbar', $vsclbar);
this.$el(
'vsclbarThumb', $vsclbar.find('.' + V_BAR_THUMB_CSS)
).draggable({
containment: 'parent',
axis: 'y',
scroll: false,
drag: $.proxy(onThumbDrag, this)
});
var that = this;
function onThumbDrag(event, ui) {
that._updatePositionByThumb(ui.position.top);
}
},
/**
* @private
*/
_initWheel: function () {
this.$el('con').on('mousewheel', onMouseWheel);
var that = this;
function onMouseWheel(event) {
// var deltaY = Math.max(window.devicePixelRatio || 1, 1) * 4;
// 实时取devicePixelRatio,因为可能在不同显示器中切换。
// var deltaY = 4 / (window.devicePixelRatio || 1);
// event.deltaY不随devicePixelRatio变化
var deltaY = 1;
// if (event.deltaY) {// 触摸板时deltaY可能为0
// that._moveThumb(-event.deltaY / Math.abs(event.deltaY) * deltaY);
that._moveThumb(-event.deltaY * deltaY);
// }
event.preventDefault();
event.stopPropagation();
}
},
/**
* 设置content height的scale。
* 使用场景如:内容区域使用了css3的transform: scale(...)
*
* @public
* @param {number} scale
*/
setContentHeightScale: function (scale) {
this._contentHeightSale = scale;
},
/**
* @public
*/
refresh: function () {
this._viewModel().updater.refresh();
},
/**
* @private
*/
_refresh: function () {
var $vsclbar = this.$el('vsclbar');
var $con = this.$el('con');
var $thumb = this.$el('vsclbarThumb');
var mainHeight = this.$el().height();
var conHeight = this._getContentHeight();
// 缓存
if (!this._outerCached) {
this._thumbVOuter =
toInt($thumb.css('padding-top'))
+ toInt($thumb.css('padding-bottom'))
+ toInt($thumb.css('border-top-width'))
+ toInt($thumb.css('border-bottom-width'))
+ toInt($thumb.css('margin-top'))
+ toInt($thumb.css('margin-bottom'));
this._outerCached = true;
}
if (conHeight && conHeight > mainHeight) {
this._scrollDisabled = false;
$vsclbar.show();
$con.addClass(CON_V_SCROLL_CSS);
// 因为现实滚动条会影响布局,所以重新计算尺寸
conHeight = this._getContentHeight();
mainHeight = this.$el().height();
// 计算thumb的尺寸
var vsclbarHeight = $vsclbar.height();
// 先不设thumb最小的尺寸。后续需要再说。
var vsclbarThumbHeight = toInt(vsclbarHeight * (mainHeight / conHeight));
$thumb.css(
'height', (vsclbarThumbHeight - this._thumbVOuter) + 'px'
);
// 缓存
this._mainHeight = mainHeight;
this._conHeight = conHeight;
this._vsclbarHeight = vsclbarHeight;
this._vsclbarThumbHeight = vsclbarThumbHeight;
// 因为内容大小可能变化了,故重新设定滚动条和内容的位置
this._setPosition(null, false);
}
else {
this._scrollDisabled = true;
$vsclbar.hide();
$con.removeClass(CON_V_SCROLL_CSS);
this._setPosition(0, false);
}
},
/**
* @private
*/
_getContentHeight: function () {
var conHeight = this.$el('con').outerHeight();
if (this._contentHeightSale != null) {
conHeight = conHeight * this._contentHeightSale;
}
return conHeight;
},
/**
* @private
*/
_moveThumb: function (offset) {
if (this._scrollDisabled) {
return;
}
var $thumb = this.$el('vsclbarThumb');
var currTop = toInt($thumb.css('top')) || 0;
this._setThumb(currTop + offset);
},
/**
* @private
*/
_setThumb: function (nextTop, suppressUpdatePosition) {
var topMax = this._vsclbarHeight - this._vsclbarThumbHeight;
if (nextTop > topMax) {
nextTop = topMax;
}
if (nextTop < 0) {
nextTop = 0;
}
var $thumb = this.$el('vsclbarThumb');
$thumb.css('top', toInt(nextTop) + 'px');
!suppressUpdatePosition && this._updatePositionByThumb(nextTop);
},
/**
* @private
*/
_setPosition: function (nextTop, suppressUpdateThumb) {
var $con = this.$el('con');
if (nextTop == null) {
// 使用当前的位置
nextTop = -(toInt($con.css('top')) || 0);
}
var diff = this._conHeight - this._mainHeight;
nextTop < 0 && (nextTop = 0);
nextTop > diff && (nextTop = diff);
$con.css('top', -toInt(nextTop) + 'px');
!suppressUpdateThumb && this._updateThumbByPosition(nextTop);
},
/**
* @private
*/
_updatePositionByThumb: function (nextThumbTop) {
this._setPosition(
toInt(this._conHeight * nextThumbTop / this._vsclbarHeight),
true
);
},
/**
* @private
*/
_updateThumbByPosition: function (nextTop) {
this._setThumb(
toInt(nextTop * this._vsclbarHeight / this._conHeight),
true
);
},
/**
* @private
*/
_applyPosObj: function (pos) {
// 暂只支持anchor
var $con = this.$el('con');
var $anchor = $con.find(
'*[data-' + this._viewModel().anchorAttr + '=' + pos.anchor + ']'
);
if ($anchor.length) {
var nextTop = $anchor.offset().top - $con.offset().top;
this._setThumb(
toInt(this._vsclbarHeight * nextTop / this._conHeight)
);
}
}
});
/**
* 产生一个更新器。写成这样只是为了便于传递到外部,作为此scrollPanel的操作器。
*
* @public
* @static
* @return {Object} updater
*/
var createUpdater = ScrollPanel.createUpdater = function () {
/**
* 内含 left 和 top 域,{number}类型。
*
*/
var updater = lib.ob({});
/**
* @private
*/
updater._pos = lib.ob({});
/**
* @private
*/
updater._refresher = lib.ob();
$.extend(updater, updaterMethods);
return updater;
};
var updaterMethods = {
/**
* 内容大小改变时调用
*
* @public
*/
refresh: function () {
// 没有使用延迟刷新,因为那会导致页面跳动。
this._refresher({});
},
/**
* 设置位置。(暂不支持得到位置,暂不支持px值设置,因为现在没这需要)
*
* @public
* @param {Object} value
* @param {string=} value.anchor 指定转移到的anchor
* @return {boolean=} value.head true则表示移动到头(暂不支持)
* @return {boolean=} value.tail true则表示移动到尾(暂不支持)
*/
position: function (value) {
// 因为是对象,所以每次都触发change事件。
this._pos(value);
}
};
return ScrollPanel;
});