devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
733 lines (632 loc) • 24 kB
JavaScript
"use strict";
var $ = require("../../core/renderer"),
eventsEngine = require("../../events/core/events_engine"),
support = require("../../core/utils/support"),
browser = require("../../core/utils/browser"),
commonUtils = require("../../core/utils/common"),
typeUtils = require("../../core/utils/type"),
extend = require("../../core/utils/extend").extend,
getPublicElement = require("../../core/utils/dom").getPublicElement,
windowUtils = require("../../core/utils/window"),
navigator = windowUtils.getNavigator(),
domAdapter = require("../../core/dom_adapter"),
devices = require("../../core/devices"),
registerComponent = require("../../core/component_registrator"),
DOMComponent = require("../../core/dom_component"),
selectors = require("../widget/selectors"),
eventUtils = require("../../events/utils"),
scrollEvents = require("./ui.events.emitter.gesture.scroll"),
simulatedStrategy = require("./ui.scrollable.simulated"),
NativeStrategy = require("./ui.scrollable.native"),
when = require("../../core/utils/deferred").when;
var SCROLLABLE = "dxScrollable",
SCROLLABLE_STRATEGY = "dxScrollableStrategy",
SCROLLABLE_CLASS = "dx-scrollable",
SCROLLABLE_DISABLED_CLASS = "dx-scrollable-disabled",
SCROLLABLE_CONTAINER_CLASS = "dx-scrollable-container",
SCROLLABLE_WRAPPER_CLASS = "dx-scrollable-wrapper",
SCROLLABLE_CONTENT_CLASS = "dx-scrollable-content",
VERTICAL = "vertical",
HORIZONTAL = "horizontal",
BOTH = "both";
var deviceDependentOptions = function deviceDependentOptions() {
return [{
device: function device() {
return !support.nativeScrolling;
},
options: {
/**
* @name dxScrollableOptions.useNative
* @publicName useNative
* @default false @for desktop
* @default true @for Mac
*/
useNative: false
}
}, {
device: function device(_device) {
return !devices.isSimulator() && devices.real().platform === "generic" && _device.platform === "generic";
},
options: {
/**
* @name dxScrollableOptions.bounceEnabled
* @publicName bounceEnabled
* @default false @for desktop
*/
bounceEnabled: false,
/**
* @name dxScrollableOptions.scrollByThumb
* @publicName scrollByThumb
* @default true @for desktop
*/
scrollByThumb: true,
/**
* @name dxScrollableOptions.scrollByContent
* @publicName scrollByContent
* @default false @for non-touch_devices
*/
scrollByContent: support.touch,
/**
* @name dxScrollableOptions.showScrollbar
* @publicName showScrollbar
* @default 'onHover' @for desktop
*/
showScrollbar: "onHover"
}
}];
};
/**
* @name dxScrollable
* @publicName dxScrollable
* @type object
* @inherits DOMComponent
* @namespace DevExpress.ui
* @hidden
*/
var Scrollable = DOMComponent.inherit({
_getDefaultOptions: function _getDefaultOptions() {
return extend(this.callBase(), {
/**
* @name dxScrollableOptions.disabled
* @publicName disabled
* @type boolean
* @default false
*/
disabled: false,
/**
* @name dxScrollableOptions.onScroll
* @publicName onScroll
* @extends Action
* @type function(e)
* @type_function_param1 e:object
* @type_function_param1_field4 jQueryEvent:jQuery.Event:deprecated(event)
* @type_function_param1_field5 event:event
* @type_function_param1_field6 scrollOffset:object
* @type_function_param1_field7 reachedLeft:boolean
* @type_function_param1_field8 reachedRight:boolean
* @type_function_param1_field9 reachedTop:boolean
* @type_function_param1_field10 reachedBottom:boolean
* @action
*/
onScroll: null,
/**
* @name dxScrollableOptions.direction
* @publicName direction
* @type Enums.ScrollDirection
* @default "vertical"
*/
direction: VERTICAL,
/**
* @name dxScrollableOptions.showScrollbar
* @publicName showScrollbar
* @type string
* @acceptValues 'onScroll'|'onHover'|'always'|'never'
* @default 'onScroll'
*/
showScrollbar: "onScroll",
/**
* @name dxScrollableOptions.useNative
* @publicName useNative
* @type boolean
* @default true
*/
useNative: true,
/**
* @name dxScrollableOptions.bounceEnabled
* @publicName bounceEnabled
* @type boolean
* @default true
*/
bounceEnabled: true,
/**
* @name dxScrollableOptions.scrollByContent
* @publicName scrollByContent
* @type boolean
* @default true
*/
scrollByContent: true,
/**
* @name dxScrollableOptions.scrollByThumb
* @publicName scrollByThumb
* @type boolean
* @default false
*/
scrollByThumb: false,
/**
* @name dxScrollableOptions.onUpdated
* @publicName onUpdated
* @extends Action
* @type function(e)
* @type_function_param1 e:object
* @type_function_param1_field4 jQueryEvent:jQuery.Event:deprecated(event)
* @type_function_param1_field5 event:event
* @type_function_param1_field6 scrollOffset:object
* @type_function_param1_field7 reachedLeft:boolean
* @type_function_param1_field8 reachedRight:boolean
* @type_function_param1_field9 reachedTop:boolean
* @type_function_param1_field10 reachedBottom:boolean
* @action
*/
onUpdated: null,
onStart: null,
onEnd: null,
onBounce: null,
onStop: null,
useSimulatedScrollbar: false,
useKeyboard: true,
inertiaEnabled: true,
pushBackValue: 0,
updateManually: false
});
},
_defaultOptionsRules: function _defaultOptionsRules() {
return this.callBase().concat(deviceDependentOptions(), [{
device: function device() {
return support.nativeScrolling && devices.real().platform === "android" && !browser.mozilla;
},
options: {
useSimulatedScrollbar: true
}
}, {
device: function device() {
return devices.real().platform === "ios";
},
options: {
pushBackValue: 1
}
}]);
},
_initOptions: function _initOptions(options) {
this.callBase(options);
if (!("useSimulatedScrollbar" in options)) {
this._setUseSimulatedScrollbar();
}
},
_setUseSimulatedScrollbar: function _setUseSimulatedScrollbar() {
if (!this.initialOption("useSimulatedScrollbar")) {
this.option("useSimulatedScrollbar", !this.option("useNative"));
}
},
_init: function _init() {
this.callBase();
this._initScrollableMarkup();
this._locked = false;
},
_visibilityChanged: function _visibilityChanged(visible) {
if (visible) {
this.update();
this._updateRtlPosition(this.option("rtlEnabled"));
this._savedScrollOffset && this.scrollTo(this._savedScrollOffset);
delete this._savedScrollOffset;
} else {
this._savedScrollOffset = this.scrollOffset();
}
},
_initScrollableMarkup: function _initScrollableMarkup() {
var $element = this.$element().addClass(SCROLLABLE_CLASS),
$container = this._$container = $("<div>").addClass(SCROLLABLE_CONTAINER_CLASS),
$wrapper = this._$wrapper = $("<div>").addClass(SCROLLABLE_WRAPPER_CLASS),
$content = this._$content = $("<div>").addClass(SCROLLABLE_CONTENT_CLASS);
if (domAdapter.hasDocumentProperty("onbeforeactivate") && browser.msie && browser.version < 12) {
eventsEngine.on($element, eventUtils.addNamespace("beforeactivate", SCROLLABLE), function (e) {
if (!$(e.target).is(selectors.focusable)) {
e.preventDefault();
}
});
}
$content.append($element.contents()).appendTo($container);
$container.appendTo($wrapper);
$wrapper.appendTo($element);
},
_dimensionChanged: function _dimensionChanged() {
this.update();
},
_attachNativeScrollbarsCustomizationCss: function _attachNativeScrollbarsCustomizationCss() {
// NOTE: Customize native scrollbars for dashboard team
if (devices.real().deviceType === "desktop" && !(navigator.platform.indexOf('Mac') > -1 && browser['webkit'])) {
this.$element().addClass("dx-scrollable-customizable-scrollbars");
}
},
_initMarkup: function _initMarkup() {
this.callBase();
this._renderDirection();
},
_render: function _render() {
this._renderStrategy();
this._attachNativeScrollbarsCustomizationCss();
this._attachEventHandlers();
this._renderDisabledState();
this._createActions();
this.update();
this.callBase();
this._updateRtlPosition(this.option("rtlEnabled"));
},
_updateRtlPosition: function _updateRtlPosition(rtl) {
var that = this;
this._updateBounds();
if (rtl && this.option("direction") !== VERTICAL) {
commonUtils.deferUpdate(function () {
var left = that.scrollWidth() - that.clientWidth();
commonUtils.deferRender(function () {
that.scrollTo({ left: left });
});
});
}
},
_updateBounds: function _updateBounds() {
this._strategy.updateBounds();
},
_attachEventHandlers: function _attachEventHandlers() {
var strategy = this._strategy;
var initEventData = {
getDirection: strategy.getDirection.bind(strategy),
validate: this._validate.bind(this),
isNative: this.option("useNative"),
scrollTarget: this._$container
};
eventsEngine.off(this._$wrapper, "." + SCROLLABLE);
eventsEngine.on(this._$wrapper, eventUtils.addNamespace(scrollEvents.init, SCROLLABLE), initEventData, this._initHandler.bind(this));
eventsEngine.on(this._$wrapper, eventUtils.addNamespace(scrollEvents.start, SCROLLABLE), strategy.handleStart.bind(strategy));
eventsEngine.on(this._$wrapper, eventUtils.addNamespace(scrollEvents.move, SCROLLABLE), strategy.handleMove.bind(strategy));
eventsEngine.on(this._$wrapper, eventUtils.addNamespace(scrollEvents.end, SCROLLABLE), strategy.handleEnd.bind(strategy));
eventsEngine.on(this._$wrapper, eventUtils.addNamespace(scrollEvents.cancel, SCROLLABLE), strategy.handleCancel.bind(strategy));
eventsEngine.on(this._$wrapper, eventUtils.addNamespace(scrollEvents.stop, SCROLLABLE), strategy.handleStop.bind(strategy));
eventsEngine.off(this._$container, "." + SCROLLABLE);
eventsEngine.on(this._$container, eventUtils.addNamespace("scroll", SCROLLABLE), strategy.handleScroll.bind(strategy));
},
_validate: function _validate(e) {
if (this._isLocked()) {
return false;
}
this._updateIfNeed();
return this._strategy.validate(e);
},
_initHandler: function _initHandler() {
var strategy = this._strategy;
strategy.handleInit.apply(strategy, arguments);
},
_renderDisabledState: function _renderDisabledState() {
this.$element().toggleClass(SCROLLABLE_DISABLED_CLASS, this.option("disabled"));
if (this.option("disabled")) {
this._lock();
} else {
this._unlock();
}
},
_renderDirection: function _renderDirection() {
this.$element().removeClass("dx-scrollable-" + HORIZONTAL).removeClass("dx-scrollable-" + VERTICAL).removeClass("dx-scrollable-" + BOTH).addClass("dx-scrollable-" + this.option("direction"));
},
_renderStrategy: function _renderStrategy() {
this._createStrategy();
this._strategy.render();
this.$element().data(SCROLLABLE_STRATEGY, this._strategy);
},
_createStrategy: function _createStrategy() {
this._strategy = this.option("useNative") ? new NativeStrategy(this) : new simulatedStrategy.SimulatedStrategy(this);
},
_createActions: function _createActions() {
this._strategy && this._strategy.createActions();
},
_clean: function _clean() {
this._strategy && this._strategy.dispose();
},
_optionChanged: function _optionChanged(args) {
switch (args.name) {
case "onStart":
case "onEnd":
case "onStop":
case "onUpdated":
case "onScroll":
case "onBounce":
this._createActions();
break;
case "direction":
this._resetInactiveDirection();
this._invalidate();
break;
case "useNative":
this._setUseSimulatedScrollbar();
this._invalidate();
break;
case "inertiaEnabled":
case "scrollByContent":
case "scrollByThumb":
case "bounceEnabled":
case "useKeyboard":
case "showScrollbar":
case "useSimulatedScrollbar":
case "pushBackValue":
this._invalidate();
break;
case "disabled":
this._renderDisabledState();
this._strategy && this._strategy.disabledChanged();
break;
case "updateManually":
break;
default:
this.callBase(args);
}
},
_resetInactiveDirection: function _resetInactiveDirection() {
var inactiveProp = this._getInactiveProp();
if (!inactiveProp || !windowUtils.hasWindow()) {
return;
}
var scrollOffset = this.scrollOffset();
scrollOffset[inactiveProp] = 0;
this.scrollTo(scrollOffset);
},
_getInactiveProp: function _getInactiveProp() {
var direction = this.option("direction");
if (direction === VERTICAL) {
return "left";
}
if (direction === HORIZONTAL) {
return "top";
}
},
_location: function _location() {
return this._strategy.location();
},
_normalizeLocation: function _normalizeLocation(location) {
if (typeUtils.isPlainObject(location)) {
var left = commonUtils.ensureDefined(location.left, location.x);
var top = commonUtils.ensureDefined(location.top, location.y);
return {
left: typeUtils.isDefined(left) ? -left : undefined,
top: typeUtils.isDefined(top) ? -top : undefined
};
} else {
var direction = this.option("direction");
return {
left: direction !== VERTICAL ? -location : undefined,
top: direction !== HORIZONTAL ? -location : undefined
};
}
},
_isLocked: function _isLocked() {
return this._locked;
},
_lock: function _lock() {
this._locked = true;
},
_unlock: function _unlock() {
if (!this.option("disabled")) {
this._locked = false;
}
},
_isDirection: function _isDirection(direction) {
var current = this.option("direction");
if (direction === VERTICAL) {
return current !== HORIZONTAL;
}
if (direction === HORIZONTAL) {
return current !== VERTICAL;
}
return current === direction;
},
_updateAllowedDirection: function _updateAllowedDirection() {
var allowedDirections = this._strategy._allowedDirections();
if (this._isDirection(BOTH) && allowedDirections.vertical && allowedDirections.horizontal) {
this._allowedDirectionValue = BOTH;
} else if (this._isDirection(HORIZONTAL) && allowedDirections.horizontal) {
this._allowedDirectionValue = HORIZONTAL;
} else if (this._isDirection(VERTICAL) && allowedDirections.vertical) {
this._allowedDirectionValue = VERTICAL;
} else {
this._allowedDirectionValue = null;
}
},
_allowedDirection: function _allowedDirection() {
return this._allowedDirectionValue;
},
_container: function _container() {
return this._$container;
},
$content: function $content() {
return this._$content;
},
/**
* @name dxScrollablemethods.content
* @publicName content()
* @return dxElement
*/
content: function content() {
return getPublicElement(this._$content);
},
/**
* @name dxScrollablemethods.scrollOffset
* @publicName scrollOffset()
* @return object
*/
scrollOffset: function scrollOffset() {
var location = this._location();
return {
top: -location.top,
left: -location.left
};
},
/**
* @name dxScrollablemethods.scrollTop
* @publicName scrollTop()
* @return numeric
*/
scrollTop: function scrollTop() {
return this.scrollOffset().top;
},
/**
* @name dxScrollablemethods.scrollLeft
* @publicName scrollLeft()
* @return numeric
*/
scrollLeft: function scrollLeft() {
return this.scrollOffset().left;
},
/**
* @name dxScrollablemethods.clientHeight
* @publicName clientHeight()
* @return numeric
*/
clientHeight: function clientHeight() {
return this._$container.height();
},
/**
* @name dxScrollablemethods.scrollHeight
* @publicName scrollHeight()
* @return numeric
*/
scrollHeight: function scrollHeight() {
return this.$content().outerHeight() - 2 * this._strategy.verticalOffset();
},
/**
* @name dxScrollablemethods.clientWidth
* @publicName clientWidth()
* @return numeric
*/
clientWidth: function clientWidth() {
return this._$container.width();
},
/**
* @name dxScrollablemethods.scrollWidth
* @publicName scrollWidth()
* @return numeric
*/
scrollWidth: function scrollWidth() {
return this.$content().outerWidth();
},
/**
* @name dxScrollablemethods.update
* @publicName update()
* @return Promise<void>
*/
update: function update() {
if (!this._strategy) {
return;
}
return when(this._strategy.update()).done(function () {
this._updateAllowedDirection();
}.bind(this));
},
/**
* @name dxScrollablemethods.scrollBy
* @publicName scrollBy(distance)
* @param1 distance:numeric
*/
/**
* @name dxScrollablemethods.scrollBy
* @publicName scrollBy(distanceObject)
* @param1 distanceObject:object
*/
scrollBy: function scrollBy(distance) {
distance = this._normalizeLocation(distance);
if (!distance.top && !distance.left) {
return;
}
this._updateIfNeed();
this._strategy.scrollBy(distance);
},
/**
* @name dxScrollablemethods.scrollTo
* @publicName scrollTo(targetLocation)
* @param1 targetLocation:numeric
*/
/**
* @name dxScrollablemethods.scrollTo
* @publicName scrollTo(targetLocationObject)
* @param1 targetLocation:object
*/
scrollTo: function scrollTo(targetLocation) {
targetLocation = this._normalizeLocation(targetLocation);
this._updateIfNeed();
var location = this._location();
var distance = this._normalizeLocation({
left: location.left - commonUtils.ensureDefined(targetLocation.left, location.left),
top: location.top - commonUtils.ensureDefined(targetLocation.top, location.top)
});
if (!distance.top && !distance.left) {
return;
}
this._strategy.scrollBy(distance);
},
/**
* @name dxScrollablemethods.scrollToElement
* @publicName scrollToElement(targetLocation)
* @param1 element:Node|jQuery
*/
scrollToElement: function scrollToElement(element, offset) {
offset = offset || {};
var $element = $(element);
var elementInsideContent = this.$content().find(element).length;
var elementIsInsideContent = $element.parents("." + SCROLLABLE_CLASS).length - $element.parents("." + SCROLLABLE_CONTENT_CLASS).length === 0;
if (!elementInsideContent || !elementIsInsideContent) {
return;
}
var scrollPosition = { top: 0, left: 0 };
var direction = this.option("direction");
if (direction !== VERTICAL) {
scrollPosition.left = this._scrollToElementPosition($element, HORIZONTAL, offset);
}
if (direction !== HORIZONTAL) {
scrollPosition.top = this._scrollToElementPosition($element, VERTICAL, offset);
}
this.scrollTo(scrollPosition);
},
_scrollToElementPosition: function _scrollToElementPosition($element, direction, offset) {
var isVertical = direction === VERTICAL;
var startOffset = (isVertical ? offset.top : offset.left) || 0;
var endOffset = (isVertical ? offset.bottom : offset.right) || 0;
var pushBackOffset = isVertical ? this._strategy.verticalOffset() : 0;
var elementPositionRelativeToContent = this._elementPositionRelativeToContent($element, isVertical ? 'top' : 'left');
var elementPosition = elementPositionRelativeToContent - pushBackOffset;
var elementSize = $element[isVertical ? 'outerHeight' : 'outerWidth']();
var scrollLocation = isVertical ? this.scrollTop() : this.scrollLeft();
var clientSize = isVertical ? this.clientHeight() : this.clientWidth();
var startDistance = scrollLocation - elementPosition + startOffset;
var endDistance = scrollLocation - elementPosition - elementSize + clientSize - endOffset;
if (startDistance <= 0 && endDistance >= 0) {
return scrollLocation;
}
return scrollLocation - (Math.abs(startDistance) > Math.abs(endDistance) ? endDistance : startDistance);
},
_elementPositionRelativeToContent: function _elementPositionRelativeToContent($element, prop) {
var result = 0;
while (this._hasScrollContent($element)) {
result += $element.position()[prop];
$element = $element.offsetParent();
}
return result;
},
_hasScrollContent: function _hasScrollContent($element) {
var $content = this.$content();
return $element.closest($content).length && !$element.is($content);
},
_updateIfNeed: function _updateIfNeed() {
if (!this.option("updateManually")) {
this.update();
}
}
});
registerComponent(SCROLLABLE, Scrollable);
module.exports = Scrollable;
module.exports.deviceDependentOptions = deviceDependentOptions;