Files
blockly/core/widgetdiv.ts
Maribeth Bottorff ec72e8bb4d chore: add api extractor configuration and fix some associated problems (#6388)
* chore: add configuration for api extractor

* fix: remove extra param names

* chore: private to internal

* remove unrestricted

* chore: remove double backticks

* chore: remove fileoverview and export * as

* chore: return to returns

* chore: fix backslashes and angle brackets in tsdoc

* chore: final to sealed

* chore: ignore to internal

* chore: fix link tags

* chore: add api-extractor configuration

* chore: add unrecognized tag names

* chore: remove tsdoc-metadata

* fix: correct index.d.ts

* chore: fix connection link
2022-09-01 11:39:05 -07:00

253 lines
7.8 KiB
TypeScript

/**
* @license
* Copyright 2013 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* A div that floats on top of Blockly. This singleton contains
* temporary HTML UI widgets that the user is currently interacting with.
* E.g. text input areas, colour pickers, context menus.
*
* @namespace Blockly.WidgetDiv
*/
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.WidgetDiv');
import * as common from './common.js';
import * as dom from './utils/dom.js';
import type {Rect} from './utils/rect.js';
import type {Size} from './utils/size.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/** The object currently using this container. */
let owner: unknown = null;
/** Optional cleanup function set by whichever object uses the widget. */
let dispose: (() => void)|null = null;
/** A class name representing the current owner's workspace renderer. */
let rendererClassName = '';
/** A class name representing the current owner's workspace theme. */
let themeClassName = '';
/** The HTML container for popup overlays (e.g. editor widgets). */
let containerDiv: HTMLDivElement|null;
/**
* Returns the HTML container for editor widgets.
*
* @returns The editor widget container.
* @alias Blockly.WidgetDiv.getDiv
*/
export function getDiv(): HTMLDivElement|null {
return containerDiv;
}
/**
* Allows unit tests to reset the div. Do not use outside of tests.
*
* @param newDiv The new value for the DIV field.
* @alias Blockly.WidgetDiv.testOnly_setDiv
* @internal
*/
export function testOnly_setDiv(newDiv: HTMLDivElement|null) {
containerDiv = newDiv;
}
/**
* Create the widget div and inject it onto the page.
*
* @alias Blockly.WidgetDiv.createDom
*/
export function createDom() {
if (containerDiv) {
return; // Already created.
}
containerDiv = document.createElement('div') as HTMLDivElement;
containerDiv.className = 'blocklyWidgetDiv';
const container = common.getParentContainer() || document.body;
container.appendChild(containerDiv);
}
/**
* Initialize and display the widget div. Close the old one if needed.
*
* @param newOwner The object that will be using this container.
* @param rtl Right-to-left (true) or left-to-right (false).
* @param newDispose Optional cleanup function to be run when the widget is
* closed.
* @alias Blockly.WidgetDiv.show
*/
export function show(newOwner: unknown, rtl: boolean, newDispose: () => void) {
hide();
owner = newOwner;
dispose = newDispose;
const div = containerDiv;
if (!div) return;
div.style.direction = rtl ? 'rtl' : 'ltr';
div.style.display = 'block';
const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg;
rendererClassName = mainWorkspace.getRenderer().getClassName();
themeClassName = mainWorkspace.getTheme().getClassName();
dom.addClass(div, rendererClassName);
dom.addClass(div, themeClassName);
}
/**
* Destroy the widget and hide the div.
*
* @alias Blockly.WidgetDiv.hide
*/
export function hide() {
if (!isVisible()) {
return;
}
owner = null;
const div = containerDiv;
if (!div) return;
div.style.display = 'none';
div.style.left = '';
div.style.top = '';
dispose && dispose();
dispose = null;
div.textContent = '';
if (rendererClassName) {
dom.removeClass(div, rendererClassName);
rendererClassName = '';
}
if (themeClassName) {
dom.removeClass(div, themeClassName);
themeClassName = '';
}
(common.getMainWorkspace() as WorkspaceSvg).markFocused();
}
/**
* Is the container visible?
*
* @returns True if visible.
* @alias Blockly.WidgetDiv.isVisible
*/
export function isVisible(): boolean {
return !!owner;
}
/**
* Destroy the widget and hide the div if it is being used by the specified
* object.
*
* @param oldOwner The object that was using this container.
* @alias Blockly.WidgetDiv.hideIfOwner
*/
export function hideIfOwner(oldOwner: unknown) {
if (owner === oldOwner) {
hide();
}
}
/**
* Set the widget div's position and height. This function does nothing clever:
* it will not ensure that your widget div ends up in the visible window.
*
* @param x Horizontal location (window coordinates, not body).
* @param y Vertical location (window coordinates, not body).
* @param height The height of the widget div (pixels).
*/
function positionInternal(x: number, y: number, height: number) {
containerDiv!.style.left = x + 'px';
containerDiv!.style.top = y + 'px';
containerDiv!.style.height = height + 'px';
}
/**
* Position the widget div based on an anchor rectangle.
* The widget should be placed adjacent to but not overlapping the anchor
* rectangle. The preferred position is directly below and aligned to the left
* (LTR) or right (RTL) side of the anchor.
*
* @param viewportBBox The bounding rectangle of the current viewport, in window
* coordinates.
* @param anchorBBox The bounding rectangle of the anchor, in window
* coordinates.
* @param widgetSize The size of the widget that is inside the widget div, in
* window coordinates.
* @param rtl Whether the workspace is in RTL mode. This determines horizontal
* alignment.
* @alias Blockly.WidgetDiv.positionWithAnchor
* @internal
*/
export function positionWithAnchor(
viewportBBox: Rect, anchorBBox: Rect, widgetSize: Size, rtl: boolean) {
const y = calculateY(viewportBBox, anchorBBox, widgetSize);
const x = calculateX(viewportBBox, anchorBBox, widgetSize, rtl);
if (y < 0) {
positionInternal(x, 0, widgetSize.height + y);
} else {
positionInternal(x, y, widgetSize.height);
}
}
/**
* Calculate an x position (in window coordinates) such that the widget will not
* be offscreen on the right or left.
*
* @param viewportBBox The bounding rectangle of the current viewport, in window
* coordinates.
* @param anchorBBox The bounding rectangle of the anchor, in window
* coordinates.
* @param widgetSize The dimensions of the widget inside the widget div.
* @param rtl Whether the Blockly workspace is in RTL mode.
* @returns A valid x-coordinate for the top left corner of the widget div, in
* window coordinates.
*/
function calculateX(
viewportBBox: Rect, anchorBBox: Rect, widgetSize: Size,
rtl: boolean): number {
if (rtl) {
// Try to align the right side of the field and the right side of widget.
const widgetLeft = anchorBBox.right - widgetSize.width;
// Don't go offscreen left.
const x = Math.max(widgetLeft, viewportBBox.left);
// But really don't go offscreen right:
return Math.min(x, viewportBBox.right - widgetSize.width);
} else {
// Try to align the left side of the field and the left side of widget.
// Don't go offscreen right.
const x = Math.min(anchorBBox.left, viewportBBox.right - widgetSize.width);
// But left is more important, because that's where the text is.
return Math.max(x, viewportBBox.left);
}
}
/**
* Calculate a y position (in window coordinates) such that the widget will not
* be offscreen on the top or bottom.
*
* @param viewportBBox The bounding rectangle of the current viewport, in window
* coordinates.
* @param anchorBBox The bounding rectangle of the anchor, in window
* coordinates.
* @param widgetSize The dimensions of the widget inside the widget div.
* @returns A valid y-coordinate for the top left corner of the widget div, in
* window coordinates.
*/
function calculateY(
viewportBBox: Rect, anchorBBox: Rect, widgetSize: Size): number {
// Flip the widget vertically if off the bottom.
// The widget could go off the top of the window, but it would also go off
// the bottom. The window is just too small.
if (anchorBBox.bottom + widgetSize.height >= viewportBBox.bottom) {
// The bottom of the widget is at the top of the field.
return anchorBBox.top - widgetSize.height;
} else {
// The top of the widget is at the bottom of the field.
return anchorBBox.bottom;
}
}