/** * @license * Copyright 2011 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Object representing a pair of scrollbars. */ 'use strict'; /** * Object representing a pair of scrollbars. * @class */ goog.module('Blockly.ScrollbarPair'); const dom = goog.require('Blockly.utils.dom'); const eventUtils = goog.require('Blockly.Events.utils'); /* eslint-disable-next-line no-unused-vars */ const {Metrics} = goog.requireType('Blockly.utils.Metrics'); const {Scrollbar} = goog.require('Blockly.Scrollbar'); const {Svg} = goog.require('Blockly.utils.Svg'); /* eslint-disable-next-line no-unused-vars */ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg'); /** * Class for a pair of scrollbars. Horizontal and vertical. */ const ScrollbarPair = class { /** * @param {!WorkspaceSvg} workspace Workspace to bind the scrollbars to. * @param {boolean=} addHorizontal Whether to add a horizontal scrollbar. * Defaults to true. * @param {boolean=} addVertical Whether to add a vertical scrollbar. Defaults * to true. * @param {string=} opt_class A class to be applied to these scrollbars. * @param {number=} opt_margin The margin to apply to these scrollbars. * @alias Blockly.ScrollbarPair */ constructor(workspace, addHorizontal, addVertical, opt_class, opt_margin) { /** * The workspace this scrollbar pair is bound to. * @type {!WorkspaceSvg} * @private */ this.workspace_ = workspace; addHorizontal = addHorizontal === undefined ? true : addHorizontal; addVertical = addVertical === undefined ? true : addVertical; const isPair = addHorizontal && addVertical; if (addHorizontal) { this.hScroll = new Scrollbar(workspace, true, isPair, opt_class, opt_margin); } if (addVertical) { this.vScroll = new Scrollbar(workspace, false, isPair, opt_class, opt_margin); } if (isPair) { this.corner_ = dom.createSvgElement( Svg.RECT, { 'height': Scrollbar.scrollbarThickness, 'width': Scrollbar.scrollbarThickness, 'class': 'blocklyScrollbarBackground', }, null); dom.insertAfter(this.corner_, workspace.getBubbleCanvas()); } /** * Previously recorded metrics from the workspace. * @type {?Metrics} * @private */ this.oldHostMetrics_ = null; } /** * Dispose of this pair of scrollbars. * Unlink from all DOM elements to prevent memory leaks. * @suppress {checkTypes} */ dispose() { dom.removeNode(this.corner_); this.corner_ = null; this.workspace_ = null; this.oldHostMetrics_ = null; if (this.hScroll) { this.hScroll.dispose(); this.hScroll = null; } if (this.vScroll) { this.vScroll.dispose(); this.vScroll = null; } } /** * Recalculate both of the scrollbars' locations and lengths. * Also reposition the corner rectangle. */ resize() { // Look up the host metrics once, and use for both scrollbars. const hostMetrics = this.workspace_.getMetrics(); if (!hostMetrics) { // Host element is likely not visible. return; } // Only change the scrollbars if there has been a change in metrics. let resizeH = false; let resizeV = false; if (!this.oldHostMetrics_ || this.oldHostMetrics_.viewWidth !== hostMetrics.viewWidth || this.oldHostMetrics_.viewHeight !== hostMetrics.viewHeight || this.oldHostMetrics_.absoluteTop !== hostMetrics.absoluteTop || this.oldHostMetrics_.absoluteLeft !== hostMetrics.absoluteLeft) { // The window has been resized or repositioned. resizeH = true; resizeV = true; } else { // Has the content been resized or moved? if (!this.oldHostMetrics_ || this.oldHostMetrics_.scrollWidth !== hostMetrics.scrollWidth || this.oldHostMetrics_.viewLeft !== hostMetrics.viewLeft || this.oldHostMetrics_.scrollLeft !== hostMetrics.scrollLeft) { resizeH = true; } if (!this.oldHostMetrics_ || this.oldHostMetrics_.scrollHeight !== hostMetrics.scrollHeight || this.oldHostMetrics_.viewTop !== hostMetrics.viewTop || this.oldHostMetrics_.scrollTop !== hostMetrics.scrollTop) { resizeV = true; } } if (resizeH || resizeV) { try { eventUtils.disable(); if (this.hScroll && resizeH) { this.hScroll.resize(hostMetrics); } if (this.vScroll && resizeV) { this.vScroll.resize(hostMetrics); } } finally { eventUtils.enable(); } this.workspace_.maybeFireViewportChangeEvent(); } if (this.hScroll && this.vScroll) { // Reposition the corner square. if (!this.oldHostMetrics_ || this.oldHostMetrics_.viewWidth !== hostMetrics.viewWidth || this.oldHostMetrics_.absoluteLeft !== hostMetrics.absoluteLeft) { this.corner_.setAttribute('x', this.vScroll.position.x); } if (!this.oldHostMetrics_ || this.oldHostMetrics_.viewHeight !== hostMetrics.viewHeight || this.oldHostMetrics_.absoluteTop !== hostMetrics.absoluteTop) { this.corner_.setAttribute('y', this.hScroll.position.y); } } // Cache the current metrics to potentially short-cut the next resize event. this.oldHostMetrics_ = hostMetrics; } /** * Returns whether scrolling horizontally is enabled. * @return {boolean} True if horizontal scroll is enabled. */ canScrollHorizontally() { return !!this.hScroll; } /** * Returns whether scrolling vertically is enabled. * @return {boolean} True if vertical scroll is enabled. */ canScrollVertically() { return !!this.vScroll; } /** * Record the origin of the workspace that the scrollbar is in, in pixels * relative to the injection div origin. This is for times when the scrollbar * is used in an object whose origin isn't the same as the main workspace * (e.g. in a flyout.) * @param {number} x The x coordinate of the scrollbar's origin, in CSS * pixels. * @param {number} y The y coordinate of the scrollbar's origin, in CSS * pixels. * @package */ setOrigin(x, y) { if (this.hScroll) { this.hScroll.setOrigin(x, y); } if (this.vScroll) { this.vScroll.setOrigin(x, y); } } /** * Set the handles of both scrollbars. * @param {number} x The horizontal content displacement, relative to the view * in pixels. * @param {number} y The vertical content displacement, relative to the view * in * pixels. * @param {boolean} updateMetrics Whether to update metrics on this set call. * Defaults to true. */ set(x, y, updateMetrics) { // This function is equivalent to: // this.hScroll.set(x); // this.vScroll.set(y); // However, that calls setMetrics twice which causes a chain of // getAttribute->setAttribute->getAttribute resulting in an extra layout // pass. Combining them speeds up rendering. if (this.hScroll) { this.hScroll.set(x, false); } if (this.vScroll) { this.vScroll.set(y, false); } if (updateMetrics || updateMetrics === undefined) { // Update metrics. const xyRatio = {}; if (this.hScroll) { xyRatio.x = this.hScroll.getRatio_(); } if (this.vScroll) { xyRatio.y = this.vScroll.getRatio_(); } this.workspace_.setMetrics(xyRatio); } } /** * Set the handle of the horizontal scrollbar to be at a certain position in * CSS pixels relative to its parents. * @param {number} x Horizontal scroll value. */ setX(x) { if (this.hScroll) { this.hScroll.set(x, true); } } /** * Set the handle of the vertical scrollbar to be at a certain position in * CSS pixels relative to its parents. * @param {number} y Vertical scroll value. */ setY(y) { if (this.vScroll) { this.vScroll.set(y, true); } } /** * Set whether this scrollbar's container is visible. * @param {boolean} visible Whether the container is visible. */ setContainerVisible(visible) { if (this.hScroll) { this.hScroll.setContainerVisible(visible); } if (this.vScroll) { this.vScroll.setContainerVisible(visible); } } /** * If any of the scrollbars are visible. Non-paired scrollbars may disappear * when they aren't needed. * @return {boolean} True if visible. */ isVisible() { let isVisible = false; if (this.hScroll) { isVisible = this.hScroll.isVisible(); } if (this.vScroll) { isVisible = isVisible || this.vScroll.isVisible(); } return isVisible; } /** * Recalculates the scrollbars' locations within their path and length. * This should be called when the contents of the workspace have changed. * @param {!Metrics} hostMetrics A data structure describing all * the required dimensions, possibly fetched from the host object. */ resizeContent(hostMetrics) { if (this.hScroll) { this.hScroll.resizeContentHorizontal(hostMetrics); } if (this.vScroll) { this.vScroll.resizeContentVertical(hostMetrics); } } /** * Recalculates the scrollbars' locations on the screen and path length. * This should be called when the layout or size of the window has changed. * @param {!Metrics} hostMetrics A data structure describing all * the required dimensions, possibly fetched from the host object. */ resizeView(hostMetrics) { if (this.hScroll) { this.hScroll.resizeViewHorizontal(hostMetrics); } if (this.vScroll) { this.vScroll.resizeViewVertical(hostMetrics); } } }; exports.ScrollbarPair = ScrollbarPair;