UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

272 lines (271 loc) • 9.37 kB
/** * DevExtreme (esm/__internal/ui/scroll_view/scrollbar.js) * Version: 25.2.3 * Build date: Fri Dec 12 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import { move } from "../../../common/core/animation/translator"; import eventsEngine from "../../../common/core/events/core/events_engine"; import pointerEvents from "../../../common/core/events/pointer"; import { addNamespace } from "../../../common/core/events/utils/index"; import domAdapter from "../../../core/dom_adapter"; import $ from "../../../core/renderer"; import { deferRenderer } from "../../../core/utils/common"; import readyCallback from "../../../core/utils/ready_callbacks"; import { isPlainObject } from "../../../core/utils/type"; import Widget from "../../core/widget/widget"; const SCROLLBAR = "dxScrollbar"; const SCROLLABLE_SCROLLBAR_CLASS = "dx-scrollable-scrollbar"; const SCROLLABLE_SCROLLBAR_ACTIVE_CLASS = "dx-scrollable-scrollbar-active"; const SCROLLABLE_SCROLL_CLASS = "dx-scrollable-scroll"; const SCROLLABLE_SCROLL_CONTENT_CLASS = "dx-scrollable-scroll-content"; const HOVER_ENABLED_STATE = "dx-scrollbar-hoverable"; const HORIZONTAL = "horizontal"; const THUMB_MIN_SIZE = 15; const DEFAULT_SCALE_RATIO = 1; const DEFAULT_SIZE = 0; const MIN_CONTAINER_TO_CONTENT_RATIO = 1; let activeScrollbar = null; class Scrollbar extends Widget { _getDefaultOptions() { return Object.assign({}, super._getDefaultOptions(), { visible: false, activeStateEnabled: false, visibilityMode: "onScroll", containerSize: 0, contentSize: 0, expandable: true, scaleRatio: 1 }) } _init() { super._init(); this._isHovered = false } _initMarkup() { this._renderThumb(); super._initMarkup() } _render() { super._render(); this._renderDirection(); this._update(); this._attachPointerDownHandler(); this.option("hoverStateEnabled", this._isHoverMode()); const { hoverStateEnabled: hoverStateEnabled } = this.option(); this.$element().toggleClass(HOVER_ENABLED_STATE, hoverStateEnabled) } _renderThumb() { this._$thumb = $("<div>").addClass("dx-scrollable-scroll"); $("<div>").addClass("dx-scrollable-scroll-content").appendTo(this._$thumb); this.$element().addClass("dx-scrollable-scrollbar").append(this._$thumb) } isThumb($element) { return !!this.$element().find($element).length } _isHoverMode() { const { visibilityMode: visibilityMode, expandable: expandable } = this.option(); return ("onHover" === visibilityMode || "always" === visibilityMode) && expandable } _renderDirection() { const { direction: direction } = this.option(); this.$element().addClass(`dx-scrollbar-${direction}`); this._dimension = direction === HORIZONTAL ? "width" : "height"; this._prop = direction === HORIZONTAL ? "left" : "top" } _attachPointerDownHandler() { eventsEngine.on(this._$thumb, addNamespace(pointerEvents.down, SCROLLBAR), this.feedbackOn.bind(this)) } feedbackOn(e) { null === e || void 0 === e || e.preventDefault(); this.$element().addClass("dx-scrollable-scrollbar-active"); activeScrollbar = this } feedbackOff() { this.$element().removeClass("dx-scrollable-scrollbar-active"); activeScrollbar = null } cursorEnter() { this._isHovered = true; if (this._needScrollbar()) { this.option("visible", true) } } cursorLeave() { this._isHovered = false; this.option("visible", false) } _renderDimensions() { this._$thumb.css({ width: this.option("width"), height: this.option("height") }) } _toggleVisibility(visible) { const { visibilityMode: visibilityMode } = this.option(); if ("onScroll" === visibilityMode) { this._$thumb.css("opacity") } const adjustedVisible = this._adjustVisibility(visible); this.option().visible = adjustedVisible; this._$thumb.toggleClass("dx-state-invisible", !adjustedVisible) } _adjustVisibility(visible) { if (this._baseContainerToContentRatio && !this._needScrollbar()) { return false } const { visibilityMode: visibilityMode } = this.option(); let adjustedVisible = visible; switch (visibilityMode) { case "onScroll": default: break; case "onHover": adjustedVisible = adjustedVisible || !!this._isHovered; break; case "never": adjustedVisible = false; break; case "always": adjustedVisible = true } return adjustedVisible } moveTo(location) { if (this._isAlwaysHidden()) { return } let normalizedLocation = location; if (isPlainObject(location)) { normalizedLocation = location[this._prop] || 0 } const scrollBarLocation = {}; scrollBarLocation[this._prop] = this._calculateScrollBarPosition(normalizedLocation); move(this._$thumb, scrollBarLocation) } _calculateScrollBarPosition(location) { return -location * this._thumbRatio } _getSizes() { const { containerSize: containerSize, contentSize: contentSize, baseContainerSize: baseContainerSize, baseContentSize: baseContentSize } = this.option(); return { containerSize: Math.round(containerSize), contentSize: Math.round(contentSize), baseContainerSize: Math.round(baseContainerSize), baseContentSize: Math.round(baseContentSize) } } _update() { const { containerSize: containerSize, contentSize: contentSize } = this._getSizes(); let { baseContainerSize: baseContainerSize, baseContentSize: baseContentSize } = this._getSizes(); if (isNaN(baseContainerSize)) { baseContainerSize = containerSize; baseContentSize = contentSize } const { scaleRatio: scaleRatio } = this.option(); this._baseContainerToContentRatio = baseContentSize ? baseContainerSize / baseContentSize : baseContainerSize; this._realContainerToContentRatio = contentSize ? containerSize / contentSize : containerSize; const thumbSize = Math.round(Math.max(Math.round(containerSize * this._realContainerToContentRatio), 15)); this._thumbRatio = (containerSize - thumbSize) / (scaleRatio * (contentSize - containerSize)); this.option(this._dimension, thumbSize / scaleRatio); this.$element().css("display", this._needScrollbar() ? "" : "none") } _isAlwaysHidden() { const { visibilityMode: visibilityMode } = this.option(); return "never" === visibilityMode } _needScrollbar() { return !this._isAlwaysHidden() && this._baseContainerToContentRatio < 1 } containerToContentRatio() { return this._realContainerToContentRatio } _normalizeSize(size) { return isPlainObject(size) ? size[this._dimension] || 0 : size } _clean() { super._clean(); if (this === activeScrollbar) { activeScrollbar = null } eventsEngine.off(this._$thumb, `.${SCROLLBAR}`) } _optionChanged(args) { if (this._isAlwaysHidden()) { return } const { name: name, value: value } = args; switch (name) { case "containerSize": case "contentSize": this.option()[name] = this._normalizeSize(value); this._update(); break; case "baseContentSize": case "baseContainerSize": case "scaleRatio": this._update(); break; case "visibilityMode": case "direction": this._invalidate(); break; default: super._optionChanged(args) } } update() { deferRenderer((() => { if (this._adjustVisibility()) { this.option("visible", true) } }))() } } readyCallback.add((() => { eventsEngine.subscribeGlobal(domAdapter.getDocument(), addNamespace(pointerEvents.up, SCROLLBAR), (() => { if (activeScrollbar) { activeScrollbar.feedbackOff() } })) })); export default Scrollbar;