From ff1cb10b35bf21a1d978c220a958f1c2d2ac7002 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 11 Aug 2023 15:19:37 -0700 Subject: [PATCH] chore: Migrate the content-highlight plugin to Typescript. (#1855) * chore: Rename index.js to index.ts * fix: Explicitly declare instance variables * fix: Add types and visibility modifiers to methods * fix: Remove underscores from names of private methods * fix: Clean up some comments * fix: Remove trailing underscores * chore: Clean up code a bit * chore: Format/lint * chore: Use camel case instead of constant case to satisfy the linter --- plugins/content-highlight/src/index.js | 286 ------------------------ plugins/content-highlight/src/index.ts | 254 +++++++++++++++++++++ plugins/content-highlight/tsconfig.json | 13 ++ 3 files changed, 267 insertions(+), 286 deletions(-) delete mode 100644 plugins/content-highlight/src/index.js create mode 100644 plugins/content-highlight/src/index.ts create mode 100644 plugins/content-highlight/tsconfig.json diff --git a/plugins/content-highlight/src/index.js b/plugins/content-highlight/src/index.js deleted file mode 100644 index 369932d08e..0000000000 --- a/plugins/content-highlight/src/index.js +++ /dev/null @@ -1,286 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview A plugin that highlights the content on the workspace. - */ - -import * as Blockly from 'blockly'; - -/** - * List of events that cause a change in content area size. - * @const - * @private - */ -const CONTENT_CHANGE_EVENTS_ = [ - Blockly.Events.VIEWPORT_CHANGE, - Blockly.Events.BLOCK_MOVE, - Blockly.Events.BLOCK_DELETE, -]; - -/** - * The default padding to use for the content highlight in workspace units. - * @type {number} - * @private - */ -const DEFAULT_PADDING_ = 10; - -/** - * Length of opacity change transition in seconds. - * @type {number} - * @const - */ -const ANIMATION_TIME = 0.25; - -/** - * A plugin that highlights the area where content exists on the workspace. - */ -export class ContentHighlight { - /** - * Constructor for the content highlight plugin. - * @param {!Blockly.WorkspaceSvg} workspace The workspace that the plugin will - * be added to. - */ - constructor(workspace) { - /** - * The workspace. - * @type {!Blockly.WorkspaceSvg} - * @protected - */ - this.workspace_ = workspace; - - /** - * The width of the highlight rectangle in workspace units. - * @type {number} - * @private - */ - this.width_ = 0; - - /** - * The height of the highlight rectangle in workspace units. - * @type {number} - * @private - */ - this.height_ = 0; - - /** - * The top offset of the highlight rectangle in pixels. - * @type {number} - * @private - */ - this.top_ = 0; - - /** - * The left offset of the highlight rectangle in pixels. - * @type {number} - * @private - */ - this.left_ = 0; - - /** - * The last scale value applied on the content highlight. - * @type {number} - * @private - */ - this.lastScale_ = 1; - - /** - * The cached content metrics for the workspace in workspace units. - * @type {!Blockly.MetricsManager.ContainerRegion|undefined} - * @private - */ - this.cachedContentMetrics_ = undefined; - } - - /** - * Initializes the plugin. - * @param {number=} padding The padding to use for the content area highlight - * rectangle, in workspace units. - */ - init(padding) { - padding = Number(padding); - /** - * The padding to use around the content area. - * @const {number} - */ - this.padding_ = isNaN(padding) ? DEFAULT_PADDING_ : padding; - - /** @type {SVGElement} */ - this.svgGroup_ = Blockly.utils.dom.createSvgElement( - Blockly.utils.Svg.G, - {'class': 'contentAreaHighlight'}, null); - - const rnd = String(Math.random()).substring(2); - const mask = Blockly.utils.dom.createSvgElement( - new Blockly.utils.Svg('mask'), { - 'id': 'contentAreaHighlightMask' + rnd, - }, this.svgGroup_); - Blockly.utils.dom.createSvgElement( - Blockly.utils.Svg.RECT, { - 'x': 0, - 'y': 0, - 'width': '100%', - 'height': '100%', - 'fill': 'white', - }, mask); - this.rect_ = Blockly.utils.dom.createSvgElement( - Blockly.utils.Svg.RECT, { - 'x': 0, - 'y': 0, - 'rx': Blockly.Bubble.BORDER_WIDTH, - 'ry': Blockly.Bubble.BORDER_WIDTH, - 'fill': 'black', - }, mask); - this.background_ = Blockly.utils.dom.createSvgElement( - Blockly.utils.Svg.RECT, { - 'x': 0, - 'y': 0, - 'width': '100%', - 'height': '100%', - 'mask': 'url(#contentAreaHighlightMask' + rnd + ')', - }, this.svgGroup_); - - this.applyColor_(); - const metricsManager = this.workspace_.getMetricsManager(); - this.cachedContentMetrics_ = metricsManager.getContentMetrics(true); - this.resize_(this.cachedContentMetrics_); - const absoluteMetrics = metricsManager.getAbsoluteMetrics(); - this.position_(this.cachedContentMetrics_, absoluteMetrics); - - // Apply transition animation for opacity changes. - this.svgGroup_.style.transition = 'opacity ' + ANIMATION_TIME + 's'; - - const parentSvg = this.workspace_.getParentSvg(); - if (parentSvg.firstChild) { - parentSvg.insertBefore(this.svgGroup_, parentSvg.firstChild); - } else { - parentSvg.appendChild(this.svgGroup_); - } - - this.onChangeWrapper_ = this.onChange_.bind(this); - this.workspace_.addChangeListener(this.onChangeWrapper_); - } - - /** - * Disposes of content highlight. - * Unlinks from all DOM elements and remove all event listeners - * to prevent memory leaks. - */ - dispose() { - if (this.svgGroup_) { - Blockly.utils.dom.removeNode(this.svgGroup_); - } - if (this.onChangeWrapper_) { - this.workspace_.removeChangeListener(this.onChangeWrapper_); - } - } - - /** - * Handles events triggered on the workspace. - * @param {!Blockly.Events.Abstract} event The event. - * @private - */ - onChange_(event) { - if (event.type === Blockly.Events.THEME_CHANGE) { - this.applyColor_(); - } else if (CONTENT_CHANGE_EVENTS_.indexOf(event.type) !== -1) { - const metricsManager = this.workspace_.getMetricsManager(); - if (event.type !== Blockly.Events.VIEWPORT_CHANGE) { - // The content metrics change when it's not a viewport change event. - this.cachedContentMetrics_ = metricsManager.getContentMetrics(true); - this.resize_(this.cachedContentMetrics_); - } - const absoluteMetrics = metricsManager.getAbsoluteMetrics(); - this.position_(this.cachedContentMetrics_, absoluteMetrics); - } else if (event.type === Blockly.Events.BLOCK_DRAG) { - this.handleBlockDrag_(/** @type {!Blockly.Events.BlockDrag} */ event); - } else if (event.type === Blockly.Events.BLOCK_CHANGE) { - // Resizes the content highlight when it is a block change event - const metricsManager = this.workspace_.getMetricsManager(); - this.cachedContentMetrics_ = metricsManager.getContentMetrics(true); - this.resize_(this.cachedContentMetrics_); - } - } - - /** - * Changes opacity of the highlight based on what kind of block drag event - * is passed. - * @param {!Blockly.Events.BlockDrag} event The BlockDrag event. - * @private - */ - handleBlockDrag_(event) { - const opacity = event.isStart ? '0' : '1'; - this.svgGroup_.setAttribute('opacity', opacity); - } - - /** - * Applies the color fill for the highlight based on the current theme. - * @private - */ - applyColor_() { - const theme = this.workspace_.getTheme(); - const bgColor = - theme.getComponentStyle('workspaceBackgroundColour') || '#ffffff'; - - const colorDarkened = - Blockly.utils.colour.blend('#000', bgColor, 0.1); - const colorLightened = - Blockly.utils.colour.blend('#fff', bgColor, 0.1); - const color = (bgColor === '#ffffff' || bgColor === '#fff') ? - colorDarkened : colorLightened; - this.background_.setAttribute('fill', color); - } - - /** - * Resizes the content highlight. - * @param {!Blockly.MetricsManager.ContainerRegion} contentMetrics The content - * metrics for the workspace in workspace coordinates. - * @private - */ - resize_(contentMetrics) { - const width = contentMetrics.width ? contentMetrics.width + - 2 * this.padding_ : 0; - const height = contentMetrics.height ? contentMetrics.height + - 2 * this.padding_ : 0; - if (width !== this.width_) { - this.width_ = width; - this.rect_.setAttribute('width', width); - } - if (height !== this.height_) { - this.height_ = height; - this.rect_.setAttribute('height', height); - } - } - - /** - * Positions the highlight on the workspace based on the workspace metrics. - * @param {!Blockly.MetricsManager.ContainerRegion} contentMetrics The content - * metrics for the workspace in workspace coordinates. - * @param {!Blockly.MetricsManager.AbsoluteMetrics} absoluteMetrics The - * absolute metrics for the workspace. - * @private - */ - position_(contentMetrics, absoluteMetrics) { - // Compute top/left manually to avoid unnecessary extra computation. - const viewTop = -this.workspace_.scrollY; - const viewLeft = -this.workspace_.scrollX; - const scale = this.workspace_.scale; - const top = absoluteMetrics.top + contentMetrics.top * scale - viewTop - - this.padding_ * scale; - const left = absoluteMetrics.left + contentMetrics.left * scale - viewLeft - - this.padding_ * scale; - - if (top !== this.top_ || left !== this.left_ || - this.lastScale_ !== scale) { - this.top_ = top; - this.left_ = left; - this.lastScale_ = scale; - this.rect_.setAttribute( - 'transform', - 'translate(' + left + ',' + top + ') scale(' + scale +')'); - } - } -} diff --git a/plugins/content-highlight/src/index.ts b/plugins/content-highlight/src/index.ts new file mode 100644 index 0000000000..97823c0771 --- /dev/null +++ b/plugins/content-highlight/src/index.ts @@ -0,0 +1,254 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview A plugin that highlights the content on the workspace. + */ + +import * as Blockly from 'blockly'; + +/** + * List of events that cause a change in content area size. + */ +const contentChangeEvents = [ + Blockly.Events.VIEWPORT_CHANGE, + Blockly.Events.BLOCK_MOVE, + Blockly.Events.BLOCK_DELETE, +]; + +/** + * The default padding to use for the content highlight in workspace units. + */ +const defaultPadding = 10; + +/** + * Length of opacity change transition in seconds. + */ +const animationTime = 0.25; + +/** + * A plugin that highlights the area where content exists on the workspace. + */ +export class ContentHighlight { + /** + * The width of the highlight rectangle in workspace units. + */ + private width = 0; + + /** + * The height of the highlight rectangle in workspace units. + */ + private height = 0; + + /** + * The top offset of the highlight rectangle in pixels. + */ + private top = 0; + + /** + * The left offset of the highlight rectangle in pixels. + */ + private left = 0; + + /** + * The last scale value applied on the content highlight. + */ + private lastScale = 1; + + /** + * The cached content metrics for the workspace in workspace units. + */ + private cachedContentMetrics?: Blockly.MetricsManager.ContainerRegion; + + /** + * The padding to use around the content area. + */ + private padding = defaultPadding; + + private svgGroup?: SVGGElement; + private rect?: SVGRectElement; + private background?: SVGRectElement; + private onChangeWrapper?: () => void; + + /** + * Constructor for the content highlight plugin. + * @param workspace The workspace that the plugin will be added to. + */ + constructor(protected workspace: Blockly.WorkspaceSvg) {} + + /** + * Initializes the plugin. + * @param padding The padding to use for the content area highlight + * rectangle, in workspace units. + */ + init(padding?: number) { + this.padding = padding || defaultPadding; + + /** @type {SVGElement} */ + this.svgGroup = Blockly.utils.dom.createSvgElement( + Blockly.utils.Svg.G, {'class': 'contentAreaHighlight'}, null); + + const rnd = String(Math.random()).substring(2); + const mask = + Blockly.utils.dom.createSvgElement(new Blockly.utils.Svg('mask'), { + 'id': 'contentAreaHighlightMask' + rnd, + }, this.svgGroup); + Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT, { + 'x': 0, + 'y': 0, + 'width': '100%', + 'height': '100%', + 'fill': 'white', + }, mask); + this.rect = Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT, { + 'x': 0, + 'y': 0, + 'rx': Blockly.Bubble.BORDER_WIDTH, + 'ry': Blockly.Bubble.BORDER_WIDTH, + 'fill': 'black', + }, mask); + this.background = + Blockly.utils.dom.createSvgElement(Blockly.utils.Svg.RECT, { + 'x': 0, + 'y': 0, + 'width': '100%', + 'height': '100%', + 'mask': `url(#contentAreaHighlightMask${rnd})`, + }, this.svgGroup); + + this.applyColor(); + const metricsManager = this.workspace.getMetricsManager(); + this.cachedContentMetrics = metricsManager.getContentMetrics(true); + this.resize(this.cachedContentMetrics); + const absoluteMetrics = metricsManager.getAbsoluteMetrics(); + this.position(this.cachedContentMetrics, absoluteMetrics); + + // Apply transition animation for opacity changes. + this.svgGroup.style.transition = `opacity ${animationTime}s`; + + const parentSvg = this.workspace.getParentSvg(); + if (parentSvg.firstChild) { + parentSvg.insertBefore(this.svgGroup, parentSvg.firstChild); + } else { + parentSvg.appendChild(this.svgGroup); + } + + this.onChangeWrapper = this.onChange.bind(this); + this.workspace.addChangeListener(this.onChangeWrapper); + } + + /** + * Disposes of content highlight. + * Unlinks from all DOM elements and remove all event listeners + * to prevent memory leaks. + */ + dispose() { + if (this.svgGroup) { + Blockly.utils.dom.removeNode(this.svgGroup); + } + if (this.onChangeWrapper) { + this.workspace.removeChangeListener(this.onChangeWrapper); + } + } + + /** + * Handles events triggered on the workspace. + * @param event The event. + */ + private onChange(event: Blockly.Events.Abstract) { + if (event.type === Blockly.Events.THEME_CHANGE) { + this.applyColor(); + } else if (contentChangeEvents.indexOf(event.type) !== -1) { + const metricsManager = this.workspace.getMetricsManager(); + if (event.type !== Blockly.Events.VIEWPORT_CHANGE) { + // The content metrics change when it's not a viewport change event. + this.cachedContentMetrics = metricsManager.getContentMetrics(true); + this.resize(this.cachedContentMetrics); + } + const absoluteMetrics = metricsManager.getAbsoluteMetrics(); + this.position(this.cachedContentMetrics, absoluteMetrics); + } else if (event.type === Blockly.Events.BLOCK_DRAG) { + this.handleBlockDrag(event as Blockly.Events.BlockDrag); + } else if (event.type === Blockly.Events.BLOCK_CHANGE) { + // Resizes the content highlight when it is a block change event + const metricsManager = this.workspace.getMetricsManager(); + this.cachedContentMetrics = metricsManager.getContentMetrics(true); + this.resize(this.cachedContentMetrics); + } + } + + /** + * Changes opacity of the highlight based on what kind of block drag event + * is passed. + * @param event The BlockDrag event. + */ + private handleBlockDrag(event: Blockly.Events.BlockDrag) { + const opacity = event.isStart ? '0' : '1'; + this.svgGroup.setAttribute('opacity', opacity); + } + + /** + * Applies the color fill for the highlight based on the current theme. + */ + private applyColor() { + const theme = this.workspace.getTheme(); + const bgColor = + theme.getComponentStyle('workspaceBackgroundColour') || '#ffffff'; + + const colorDarkened = Blockly.utils.colour.blend('#000', bgColor, 0.1); + const colorLightened = Blockly.utils.colour.blend('#fff', bgColor, 0.1); + const color = (bgColor === '#ffffff' || bgColor === '#fff') ? + colorDarkened : + colorLightened; + this.background.setAttribute('fill', color); + } + + /** + * Resizes the content highlight. + * @param contentMetrics The content metrics for the workspace in workspace + * coordinates. + */ + private resize(contentMetrics: Blockly.MetricsManager.ContainerRegion) { + const width = + contentMetrics.width ? contentMetrics.width + 2 * this.padding : 0; + const height = + contentMetrics.height ? contentMetrics.height + 2 * this.padding : 0; + if (width !== this.width) { + this.width = width; + this.rect.setAttribute('width', `${width}`); + } + if (height !== this.height) { + this.height = height; + this.rect.setAttribute('height', `${height}`); + } + } + + /** + * Positions the highlight on the workspace based on the workspace metrics. + * @param contentMetrics The content metrics for the workspace in workspace + * coordinates. + * @param absoluteMetrics The absolute metrics for the workspace. + */ + private position(contentMetrics: Blockly.MetricsManager.ContainerRegion, + absoluteMetrics: Blockly.MetricsManager.AbsoluteMetrics) { + // Compute top/left manually to avoid unnecessary extra computation. + const viewTop = -this.workspace.scrollY; + const viewLeft = -this.workspace.scrollX; + const scale = this.workspace.scale; + const top = absoluteMetrics.top + contentMetrics.top * scale - viewTop - + this.padding * scale; + const left = absoluteMetrics.left + contentMetrics.left * scale - viewLeft - + this.padding * scale; + + if (top !== this.top || left !== this.left || this.lastScale !== scale) { + this.top = top; + this.left = left; + this.lastScale = scale; + this.rect.setAttribute('transform', + `translate(${left}, ${top}) scale(${scale})`); + } + } +} diff --git a/plugins/content-highlight/tsconfig.json b/plugins/content-highlight/tsconfig.json new file mode 100644 index 0000000000..343d69216d --- /dev/null +++ b/plugins/content-highlight/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "outDir": "build", + "target": "es6", + "allowJs": true, + "sourceMap": true, + "moduleResolution": "nodenext", + "lib": ["es2021", "dom"] + }, + "include": [ + "src", "test" + ], +}