UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

479 lines (478 loc) • 16.6 kB
/** * DevExtreme (esm/__internal/ui/scroll_view/m_scrollable.js) * Version: 24.2.7 * Build date: Mon Apr 28 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import _extends from "@babel/runtime/helpers/esm/extends"; import eventsEngine from "../../../common/core/events/core/events_engine"; import scrollEvents from "../../../common/core/events/gesture/emitter.gesture.scroll"; import { addNamespace } from "../../../common/core/events/utils/index"; import registerComponent from "../../../core/component_registrator"; import devices from "../../../core/devices"; import { getPublicElement } from "../../../core/element"; import $ from "../../../core/renderer"; import browser from "../../../core/utils/browser"; import { ensureDefined, noop } from "../../../core/utils/common"; import { when } from "../../../core/utils/deferred"; import { getHeight, getOuterHeight, getOuterWidth, getWidth } from "../../../core/utils/size"; import { isDefined, isPlainObject } from "../../../core/utils/type"; import { hasWindow } from "../../../core/utils/window"; import DOMComponent from "../../core/widget/dom_component"; import { getElementLocationInternal } from "../../ui/scroll_view/utils/get_element_location_internal"; import supportUtils from "../../core/utils/m_support"; import { deviceDependentOptions } from "./m_scrollable.device"; import NativeStrategy from "./m_scrollable.native"; import { SimulatedStrategy } from "./m_scrollable.simulated"; const SCROLLABLE = "dxScrollable"; const SCROLLABLE_STRATEGY = "dxScrollableStrategy"; const SCROLLABLE_CLASS = "dx-scrollable"; const SCROLLABLE_DISABLED_CLASS = "dx-scrollable-disabled"; const SCROLLABLE_CONTAINER_CLASS = "dx-scrollable-container"; const SCROLLABLE_WRAPPER_CLASS = "dx-scrollable-wrapper"; const SCROLLABLE_CONTENT_CLASS = "dx-scrollable-content"; const VERTICAL = "vertical"; const HORIZONTAL = "horizontal"; const BOTH = "both"; class Scrollable extends DOMComponent { _getDefaultOptions() { return _extends({}, super._getDefaultOptions(), { disabled: false, onScroll: null, direction: VERTICAL, showScrollbar: "onScroll", useNative: true, bounceEnabled: true, scrollByContent: true, scrollByThumb: false, onUpdated: null, onStart: null, onEnd: null, onBounce: null, useSimulatedScrollbar: false, useKeyboard: true, inertiaEnabled: true, updateManually: false, _onVisibilityChanged: noop }) } _defaultOptionsRules() { return super._defaultOptionsRules().concat(deviceDependentOptions(), [{ device: () => supportUtils.nativeScrolling && "android" === devices.real().platform && !browser.mozilla, options: { useSimulatedScrollbar: true } }]) } _initOptions(options) { super._initOptions(options); if (!("useSimulatedScrollbar" in options)) { this._setUseSimulatedScrollbar() } } _setUseSimulatedScrollbar() { if (!this.initialOption("useSimulatedScrollbar")) { this.option("useSimulatedScrollbar", !this.option("useNative")) } } _init() { super._init(); this._initScrollableMarkup(); this._locked = false } _visibilityChanged(visible) { if (visible) { this.update(); this._updateRtlPosition(); this._savedScrollOffset && this.scrollTo(this._savedScrollOffset); delete this._savedScrollOffset; const { _onVisibilityChanged: onVisibilityChanged } = this.option(); null === onVisibilityChanged || void 0 === onVisibilityChanged || onVisibilityChanged(this) } else { this._savedScrollOffset = this.scrollOffset() } } _initScrollableMarkup() { const $element = this.$element().addClass("dx-scrollable"); const $container = $("<div>").addClass("dx-scrollable-container"); const $wrapper = $("<div>").addClass("dx-scrollable-wrapper"); const $content = $("<div>").addClass("dx-scrollable-content"); this._$container = $container; this._$wrapper = $wrapper; this._$content = $content; $content.append($element.contents()).appendTo($container); $container.appendTo($wrapper); $wrapper.appendTo($element) } _dimensionChanged() { this.update(); this._updateRtlPosition() } _initMarkup() { super._initMarkup(); this._renderDirection() } _render() { this._renderStrategy(); this._attachEventHandlers(); this._renderDisabledState(); this._createActions(); this.update(); super._render(); this._updateRtlPosition(true) } _updateRtlPosition(needInitializeRtlConfig) { this._strategy.updateRtlPosition(needInitializeRtlConfig) } _getMaxOffset() { const { scrollWidth: scrollWidth, clientWidth: clientWidth, scrollHeight: scrollHeight, clientHeight: clientHeight } = $(this.container()).get(0); return { left: scrollWidth - clientWidth, top: scrollHeight - clientHeight } } _attachEventHandlers() { const strategy = this._strategy; const 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, addNamespace(scrollEvents.init, SCROLLABLE), initEventData, this._initHandler.bind(this)); eventsEngine.on(this._$wrapper, addNamespace(scrollEvents.start, SCROLLABLE), strategy.handleStart.bind(strategy)); eventsEngine.on(this._$wrapper, addNamespace(scrollEvents.move, SCROLLABLE), strategy.handleMove.bind(strategy)); eventsEngine.on(this._$wrapper, addNamespace(scrollEvents.end, SCROLLABLE), strategy.handleEnd.bind(strategy)); eventsEngine.on(this._$wrapper, addNamespace(scrollEvents.cancel, SCROLLABLE), strategy.handleCancel.bind(strategy)); eventsEngine.on(this._$wrapper, addNamespace(scrollEvents.stop, SCROLLABLE), strategy.handleStop.bind(strategy)); eventsEngine.off(this._$container, `.${SCROLLABLE}`); eventsEngine.on(this._$container, addNamespace("scroll", SCROLLABLE), strategy.handleScroll.bind(strategy)) } _validate(e) { if (this._isLocked()) { return false } this._updateIfNeed(); return this._moveIsAllowed(e) } _moveIsAllowed(e) { return this._strategy.validate(e) } handleMove(e) { this._strategy.handleMove(e) } _prepareDirections(value) { this._strategy._prepareDirections(value) } _initHandler() { const strategy = this._strategy; strategy.handleInit.apply(strategy, arguments) } _renderDisabledState() { const { disabled: disabled } = this.option(); this.$element().toggleClass("dx-scrollable-disabled", disabled); if (this.option("disabled")) { this._lock() } else { this._unlock() } } _renderDirection() { const { direction: direction } = this.option(); this.$element().removeClass(`dx-scrollable-${HORIZONTAL}`).removeClass(`dx-scrollable-${VERTICAL}`).removeClass(`dx-scrollable-${BOTH}`).addClass(`dx-scrollable-${direction}`) } _renderStrategy() { this._createStrategy(); this._strategy.render(); this.$element().data(SCROLLABLE_STRATEGY, this._strategy) } _createStrategy() { this._strategy = this.option("useNative") ? new NativeStrategy(this) : new SimulatedStrategy(this) } _createActions() { var _this$_strategy; null === (_this$_strategy = this._strategy) || void 0 === _this$_strategy || _this$_strategy.createActions() } _clean() { var _this$_strategy2; null === (_this$_strategy2 = this._strategy) || void 0 === _this$_strategy2 || _this$_strategy2.dispose() } _optionChanged(args) { var _this$_strategy3; switch (args.name) { case "onStart": case "onEnd": 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 "scrollByThumb": case "bounceEnabled": case "useKeyboard": case "showScrollbar": case "useSimulatedScrollbar": this._invalidate(); break; case "disabled": this._renderDisabledState(); null === (_this$_strategy3 = this._strategy) || void 0 === _this$_strategy3 || _this$_strategy3.disabledChanged(); break; case "updateManually": case "scrollByContent": case "_onVisibilityChanged": break; case "width": super._optionChanged(args); this._updateRtlPosition(); break; default: super._optionChanged(args) } } _resetInactiveDirection() { const inactiveProp = this._getInactiveProp(); if (!inactiveProp || !hasWindow()) { return } const scrollOffset = this.scrollOffset(); scrollOffset[inactiveProp] = 0; this.scrollTo(scrollOffset) } _getInactiveProp() { const { direction: direction } = this.option(); if (direction === VERTICAL) { return "left" } if (direction === HORIZONTAL) { return "top" } } _location() { return this._strategy.location() } _normalizeLocation(location) { if (isPlainObject(location)) { const left = ensureDefined(location.left, location.x); const top = ensureDefined(location.top, location.y); return { left: isDefined(left) ? -left : void 0, top: isDefined(top) ? -top : void 0 } } const { direction: direction } = this.option(); return { left: direction !== VERTICAL ? -location : void 0, top: direction !== HORIZONTAL ? -location : void 0 } } _isLocked() { return this._locked } _lock() { this._locked = true } _unlock() { if (!this.option("disabled")) { this._locked = false } } _isDirection(direction) { const { direction: current } = this.option(); if (direction === VERTICAL) { return current !== HORIZONTAL } if (direction === HORIZONTAL) { return current !== VERTICAL } return current === direction } _updateAllowedDirection() { const 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() { return this._allowedDirectionValue } $content() { return this._$content } content() { return getPublicElement(this._$content) } container() { return getPublicElement(this._$container) } scrollOffset() { return this._strategy._getScrollOffset() } _isRtlNativeStrategy() { const { useNative: useNative, rtlEnabled: rtlEnabled } = this.option(); return useNative && rtlEnabled } scrollTop() { return this.scrollOffset().top } scrollLeft() { return this.scrollOffset().left } clientHeight() { return getHeight(this._$container) } scrollHeight() { return getOuterHeight(this.$content()) } clientWidth() { return getWidth(this._$container) } scrollWidth() { return getOuterWidth(this.$content()) } update() { if (!this._strategy) { return } return when(this._strategy.update()).done((() => { this._updateAllowedDirection() })) } scrollBy(distance) { distance = this._normalizeLocation(distance); if (!distance.top && !distance.left) { return } this._updateIfNeed(); this._strategy.scrollBy(distance) } scrollTo(targetLocation) { if (!hasWindow()) { return } targetLocation = this._normalizeLocation(targetLocation); this._updateIfNeed(); let location = this._location(); const { useNative: useNative } = this.option(); if (!useNative) { const strategy = this._strategy; targetLocation = strategy._applyScaleRatio(targetLocation); location = strategy._applyScaleRatio(location) } if (this._isRtlNativeStrategy()) { location.left -= this._getMaxOffset().left } const distance = this._normalizeLocation({ left: location.left - ensureDefined(targetLocation.left, location.left), top: location.top - ensureDefined(targetLocation.top, location.top) }); if (!distance.top && !distance.left) { return } this._strategy.scrollBy(distance) } scrollToElement(element, offset) { const $element = $(element); const elementInsideContent = this.$content().find(element).length; const elementIsInsideContent = $element.parents(".dx-scrollable").length - $element.parents(".dx-scrollable-content").length === 0; if (!elementInsideContent || !elementIsInsideContent) { return } const scrollPosition = { top: 0, left: 0 }; const { direction: direction } = this.option(); if (direction !== VERTICAL) { scrollPosition.left = this.getScrollElementPosition($element, HORIZONTAL, offset) } if (direction !== HORIZONTAL) { scrollPosition.top = this.getScrollElementPosition($element, VERTICAL, offset) } this.scrollTo(scrollPosition) } getScrollElementPosition($element, direction, offset) { const scrollOffset = this.scrollOffset(); return getElementLocationInternal($element.get(0), direction, $(this.container()).get(0), scrollOffset, offset) } _updateIfNeed() { if (!this.option("updateManually")) { this.update() } } _useTemplates() { return false } isRenovated() { return !!Scrollable.IS_RENOVATED_WIDGET } } registerComponent(SCROLLABLE, Scrollable); export default Scrollable;