UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

733 lines (632 loc) • 24 kB
"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;