mirror of
https://github.com/google/blockly.git
synced 2025-12-16 14:20:10 +01:00
206 lines
6.2 KiB
TypeScript
206 lines
6.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2023 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {getFocusManager} from './focus_manager.js';
|
|
import type {IFocusableNode} from './interfaces/i_focusable_node.js';
|
|
import {IRenderedElement} from './interfaces/i_rendered_element.js';
|
|
import * as layerNums from './layers.js';
|
|
import {Coordinate} from './utils/coordinate.js';
|
|
import * as dom from './utils/dom.js';
|
|
import {Svg} from './utils/svg.js';
|
|
import {WorkspaceSvg} from './workspace_svg.js';
|
|
|
|
/** @internal */
|
|
export class LayerManager {
|
|
/** The layer elements being dragged are appended to. */
|
|
private dragLayer: SVGGElement | undefined;
|
|
/** The layer elements being animated are appended to. */
|
|
private animationLayer: SVGGElement | undefined;
|
|
/** The layers elements not being dragged are appended to. */
|
|
private layers = new Map<number, SVGGElement>();
|
|
|
|
/** @internal */
|
|
constructor(private workspace: WorkspaceSvg) {
|
|
const injectionDiv = workspace.getInjectionDiv();
|
|
// `getInjectionDiv` is actually nullable. We hit this if the workspace
|
|
// is part of a flyout and the workspace the flyout is attached to hasn't
|
|
// been appended yet.
|
|
if (injectionDiv) {
|
|
this.dragLayer = this.createDragLayer(injectionDiv);
|
|
this.animationLayer = this.createAnimationLayer(injectionDiv);
|
|
}
|
|
|
|
// We construct these manually so we can add the css class for backwards
|
|
// compatibility.
|
|
const blockLayer = this.createLayer(layerNums.BLOCK);
|
|
dom.addClass(blockLayer, 'blocklyBlockCanvas');
|
|
const bubbleLayer = this.createLayer(layerNums.BUBBLE);
|
|
dom.addClass(bubbleLayer, 'blocklyBubbleCanvas');
|
|
}
|
|
|
|
private createDragLayer(injectionDiv: Element) {
|
|
const svg = dom.createSvgElement(Svg.SVG, {
|
|
'class': 'blocklyBlockDragSurface',
|
|
'xmlns': dom.SVG_NS,
|
|
'xmlns:html': dom.HTML_NS,
|
|
'xmlns:xlink': dom.XLINK_NS,
|
|
'version': '1.1',
|
|
});
|
|
injectionDiv.append(svg);
|
|
return dom.createSvgElement(Svg.G, {}, svg);
|
|
}
|
|
|
|
private createAnimationLayer(injectionDiv: Element) {
|
|
const svg = dom.createSvgElement(Svg.SVG, {
|
|
'class': 'blocklyAnimationLayer',
|
|
'xmlns': dom.SVG_NS,
|
|
'xmlns:html': dom.HTML_NS,
|
|
'xmlns:xlink': dom.XLINK_NS,
|
|
'version': '1.1',
|
|
});
|
|
injectionDiv.append(svg);
|
|
return dom.createSvgElement(Svg.G, {}, svg);
|
|
}
|
|
|
|
/**
|
|
* Appends the element to the animation layer. The animation layer doesn't
|
|
* move when the workspace moves, so e.g. delete animations don't move
|
|
* when a block delete triggers a workspace resize.
|
|
*
|
|
* @internal
|
|
*/
|
|
appendToAnimationLayer(elem: IRenderedElement) {
|
|
const currentTransform = this.dragLayer?.getAttribute('transform');
|
|
// Only update the current transform when appending, so animations don't
|
|
// move if the workspace moves.
|
|
if (currentTransform) {
|
|
this.animationLayer?.setAttribute('transform', currentTransform);
|
|
}
|
|
this.animationLayer?.appendChild(elem.getSvgRoot());
|
|
}
|
|
|
|
/**
|
|
* Translates layers when the workspace is dragged or zoomed.
|
|
*
|
|
* @internal
|
|
*/
|
|
translateLayers(newCoord: Coordinate, newScale: number) {
|
|
const translation = `translate(${newCoord.x}, ${newCoord.y}) scale(${newScale})`;
|
|
this.dragLayer?.setAttribute('transform', translation);
|
|
for (const [_, layer] of this.layers) {
|
|
layer.setAttribute('transform', translation);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves the given element to the drag layer, which exists on top of all other
|
|
* layers, and the drag surface.
|
|
*
|
|
* @internal
|
|
*/
|
|
moveToDragLayer(elem: IRenderedElement & IFocusableNode) {
|
|
this.dragLayer?.appendChild(elem.getSvgRoot());
|
|
|
|
if (elem.canBeFocused()) {
|
|
// Since moving the element to the drag layer will cause it to lose focus,
|
|
// ensure it regains focus (to ensure proper highlights & sent events).
|
|
getFocusManager().focusNode(elem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves the given element off of the drag layer.
|
|
*
|
|
* @internal
|
|
*/
|
|
moveOffDragLayer(elem: IRenderedElement & IFocusableNode, layerNum: number) {
|
|
this.append(elem, layerNum);
|
|
|
|
if (elem.canBeFocused()) {
|
|
// Since moving the element off the drag layer will cause it to lose focus,
|
|
// ensure it regains focus (to ensure proper highlights & sent events).
|
|
getFocusManager().focusNode(elem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Appends the given element to a layer. If the layer does not exist, it is
|
|
* created.
|
|
*
|
|
* @internal
|
|
*/
|
|
append(elem: IRenderedElement, layerNum: number) {
|
|
if (!this.layers.has(layerNum)) {
|
|
this.createLayer(layerNum);
|
|
}
|
|
const childElem = elem.getSvgRoot();
|
|
if (this.layers.get(layerNum)?.lastChild !== childElem) {
|
|
// Only append the child if it isn't already last (to avoid re-firing
|
|
// events like focused).
|
|
this.layers.get(layerNum)?.appendChild(childElem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a layer and inserts it at the proper place given the layer number.
|
|
*
|
|
* More positive layers exist later in the dom and are rendered ontop of
|
|
* less positive layers. Layers are added to the layer map as a side effect.
|
|
*/
|
|
private createLayer(layerNum: number): SVGGElement {
|
|
const parent = this.workspace.getSvgGroup();
|
|
const layer = dom.createSvgElement(Svg.G, {});
|
|
|
|
let inserted = false;
|
|
const sortedLayers = [...this.layers].sort((a, b) => a[0] - b[0]);
|
|
for (const [num, sib] of sortedLayers) {
|
|
if (layerNum < num) {
|
|
parent.insertBefore(layer, sib);
|
|
inserted = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!inserted) {
|
|
parent.appendChild(layer);
|
|
}
|
|
this.layers.set(layerNum, layer);
|
|
return layer;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the given element is a layer managed by the layer manager.
|
|
* False otherwise.
|
|
*
|
|
* @internal
|
|
*/
|
|
hasLayer(elem: SVGElement) {
|
|
return (
|
|
elem === this.dragLayer ||
|
|
new Set(this.layers.values()).has(elem as SVGGElement)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* We need to be able to access this layer explicitly for backwards
|
|
* compatibility.
|
|
*
|
|
* @internal
|
|
*/
|
|
getBlockLayer(): SVGGElement {
|
|
return this.layers.get(layerNums.BLOCK)!;
|
|
}
|
|
|
|
/**
|
|
* We need to be able to access this layer explicitly for backwards
|
|
* compatibility.
|
|
*
|
|
* @internal
|
|
*/
|
|
getBubbleLayer(): SVGGElement {
|
|
return this.layers.get(layerNums.BUBBLE)!;
|
|
}
|
|
}
|