/** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Utilities for element styles. * These methods are not specific to Blockly, and could be factored out into * a JavaScript framework such as Closure. */ /** * Utilities for element styles. * These methods are not specific to Blockly, and could be factored out into * a JavaScript framework such as Closure. * @namespace Blockly.utils.style */ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.utils.style'); import {Coordinate} from './coordinate.js'; import {Rect} from './rect.js'; import {Size} from './size.js'; /** * Gets the height and width of an element. * Similar to Closure's goog.style.getSize * @param element Element to get size of. * @return Object with width/height properties. * @alias Blockly.utils.style.getSize */ export function getSize(element: Element): Size { return TEST_ONLY.getSizeInternal(element); } /** * Private version of getSize for stubbing in tests. */ function getSizeInternal(element: Element): Size { if (getStyle(element, 'display') !== 'none') { return getSizeWithDisplay(element); } // Evaluate size with a temporary element. // AnyDuringMigration because: Property 'style' does not exist on type // 'Element'. const style = (element as AnyDuringMigration).style; const originalDisplay = style.display; const originalVisibility = style.visibility; const originalPosition = style.position; style.visibility = 'hidden'; style.position = 'absolute'; style.display = 'inline'; const offsetWidth = (element as HTMLElement).offsetWidth; const offsetHeight = (element as HTMLElement).offsetHeight; style.display = originalDisplay; style.position = originalPosition; style.visibility = originalVisibility; return new Size(offsetWidth, offsetHeight); } /** * Gets the height and width of an element when the display is not none. * @param element Element to get size of. * @return Object with width/height properties. */ function getSizeWithDisplay(element: Element): Size { const offsetWidth = (element as HTMLElement).offsetWidth; const offsetHeight = (element as HTMLElement).offsetHeight; return new Size(offsetWidth, offsetHeight); } /** * Cross-browser pseudo get computed style. It returns the computed style where * available. If not available it tries the cascaded style value (IE * currentStyle) and in worst case the inline style value. It shouldn't be * called directly, see http://wiki/Main/ComputedStyleVsCascadedStyle for * discussion. * * Copied from Closure's goog.style.getStyle_ * * @param element Element to get style of. * @param style Property to get (must be camelCase, not CSS-style). * @return Style value. */ function getStyle(element: Element, style: string): string { // AnyDuringMigration because: Property 'style' does not exist on type // 'Element'. AnyDuringMigration because: Property 'style' does not exist on // type 'Element'. return getComputedStyle(element, style) || getCascadedStyle(element, style) || (element as AnyDuringMigration).style && (element as AnyDuringMigration).style[style]; } /** * Retrieves a computed style value of a node. It returns empty string if the * value cannot be computed (which will be the case in Internet Explorer) or * "none" if the property requested is an SVG one and it has not been * explicitly set (firefox and webkit). * * Copied from Closure's goog.style.getComputedStyle * * @param element Element to get style of. * @param property Property to get (camel-case). * @return Style value. * @alias Blockly.utils.style.getComputedStyle */ export function getComputedStyle(element: Element, property: string): string { if (document.defaultView && document.defaultView.getComputedStyle) { const styles = document.defaultView.getComputedStyle(element, null); if (styles) { // element.style[..] is undefined for browser specific styles // as 'filter'. return (styles as AnyDuringMigration)[property] || styles.getPropertyValue(property) || ''; } } return ''; } /** * Gets the cascaded style value of a node, or null if the value cannot be * computed (only Internet Explorer can do this). * * Copied from Closure's goog.style.getCascadedStyle * * @param element Element to get style of. * @param style Property to get (camel-case). * @return Style value. * @alias Blockly.utils.style.getCascadedStyle */ export function getCascadedStyle(element: Element, style: string): string { // AnyDuringMigration because: Property 'currentStyle' does not exist on type // 'Element'. AnyDuringMigration because: Property 'currentStyle' does not // exist on type 'Element'. return (element as AnyDuringMigration).currentStyle ? (element as AnyDuringMigration).currentStyle[style] : '' as string; } /** * Returns a Coordinate object relative to the top-left of the HTML document. * Similar to Closure's goog.style.getPageOffset * @param el Element to get the page offset for. * @return The page offset. * @alias Blockly.utils.style.getPageOffset */ export function getPageOffset(el: Element): Coordinate { const pos = new Coordinate(0, 0); const box = el.getBoundingClientRect(); const documentElement = document.documentElement; // Must add the scroll coordinates in to get the absolute page offset // of element since getBoundingClientRect returns relative coordinates to // the viewport. const scrollCoord = new Coordinate( window.pageXOffset || documentElement.scrollLeft, window.pageYOffset || documentElement.scrollTop); pos.x = box.left + scrollCoord.x; pos.y = box.top + scrollCoord.y; return pos; } /** * Calculates the viewport coordinates relative to the document. * Similar to Closure's goog.style.getViewportPageOffset * @return The page offset of the viewport. * @alias Blockly.utils.style.getViewportPageOffset */ export function getViewportPageOffset(): Coordinate { const body = document.body; const documentElement = document.documentElement; const scrollLeft = body.scrollLeft || documentElement.scrollLeft; const scrollTop = body.scrollTop || documentElement.scrollTop; return new Coordinate(scrollLeft, scrollTop); } /** * Shows or hides an element from the page. Hiding the element is done by * setting the display property to "none", removing the element from the * rendering hierarchy so it takes up no space. To show the element, the default * inherited display property is restored (defined either in stylesheets or by * the browser's default style rules). * Copied from Closure's goog.style.getViewportPageOffset * * @param el Element to show or hide. * @param isShown True to render the element in its default style, false to * disable rendering the element. * @alias Blockly.utils.style.setElementShown */ export function setElementShown(el: Element, isShown: AnyDuringMigration) { // AnyDuringMigration because: Property 'style' does not exist on type // 'Element'. (el as AnyDuringMigration).style.display = isShown ? '' : 'none'; } /** * Returns true if the element is using right to left (RTL) direction. * Copied from Closure's goog.style.isRightToLeft * * @param el The element to test. * @return True for right to left, false for left to right. * @alias Blockly.utils.style.isRightToLeft */ export function isRightToLeft(el: Element): boolean { return 'rtl' === getStyle(el, 'direction'); } /** * Gets the computed border widths (on all sides) in pixels * Copied from Closure's goog.style.getBorderBox * @param element The element to get the border widths for. * @return The computed border widths. * @alias Blockly.utils.style.getBorderBox */ export function getBorderBox(element: Element): Rect { const left = parseFloat(getComputedStyle(element, 'borderLeftWidth')); const right = parseFloat(getComputedStyle(element, 'borderRightWidth')); const top = parseFloat(getComputedStyle(element, 'borderTopWidth')); const bottom = parseFloat(getComputedStyle(element, 'borderBottomWidth')); return new Rect(top, bottom, left, right); } /** * Changes the scroll position of `container` with the minimum amount so * that the content and the borders of the given `element` become visible. * If the element is bigger than the container, its top left corner will be * aligned as close to the container's top left corner as possible. * Copied from Closure's goog.style.scrollIntoContainerView * * @param element The element to make visible. * @param container The container to scroll. If not set, then the document * scroll element will be used. * @param opt_center Whether to center the element in the container. * Defaults to false. * @alias Blockly.utils.style.scrollIntoContainerView */ export function scrollIntoContainerView( element: Element, container: Element, opt_center?: boolean) { const offset = getContainerOffsetToScrollInto(element, container, opt_center); container.scrollLeft = offset.x; container.scrollTop = offset.y; } /** * Calculate the scroll position of `container` with the minimum amount so * that the content and the borders of the given `element` become visible. * If the element is bigger than the container, its top left corner will be * aligned as close to the container's top left corner as possible. * Copied from Closure's goog.style.getContainerOffsetToScrollInto * * @param element The element to make visible. * @param container The container to scroll. If not set, then the document * scroll element will be used. * @param opt_center Whether to center the element in the container. * Defaults to false. * @return The new scroll position of the container. * @alias Blockly.utils.style.getContainerOffsetToScrollInto */ export function getContainerOffsetToScrollInto( element: Element, container: Element, opt_center?: boolean): Coordinate { // Absolute position of the element's border's top left corner. const elementPos = getPageOffset(element); // Absolute position of the container's border's top left corner. const containerPos = getPageOffset(container); const containerBorder = getBorderBox(container); // Relative pos. of the element's border box to the container's content box. const relX = elementPos.x - containerPos.x - containerBorder.left; const relY = elementPos.y - containerPos.y - containerBorder.top; // How much the element can move in the container, i.e. the difference between // the element's bottom-right-most and top-left-most position where it's // fully visible. const elementSize = getSizeWithDisplay(element); const spaceX = container.clientWidth - elementSize.width; const spaceY = container.clientHeight - elementSize.height; let scrollLeft = container.scrollLeft; let scrollTop = container.scrollTop; if (opt_center) { // All browsers round non-integer scroll positions down. scrollLeft += relX - spaceX / 2; scrollTop += relY - spaceY / 2; } else { // This formula was designed to give the correct scroll values in the // following cases: // - element is higher than container (spaceY < 0) => scroll down by relY // - element is not higher that container (spaceY >= 0): // - it is above container (relY < 0) => scroll up by abs(relY) // - it is below container (relY > spaceY) => scroll down by relY - spaceY // - it is in the container => don't scroll scrollLeft += Math.min(relX, Math.max(relX - spaceX, 0)); scrollTop += Math.min(relY, Math.max(relY - spaceY, 0)); } return new Coordinate(scrollLeft, scrollTop); } export const TEST_ONLY = { getSizeInternal, }