fix: Create CSS vars for SVG patterns. (#8671)

This commit is contained in:
John Nesky
2024-12-02 13:34:05 -08:00
committed by GitHub
parent af5905a3e6
commit 4230956244
11 changed files with 149 additions and 55 deletions

View File

@@ -106,11 +106,7 @@ export abstract class Bubble implements IBubble, ISelectable {
);
const embossGroup = dom.createSvgElement(
Svg.G,
{
'filter': `url(#${
this.workspace.getRenderer().getConstants().embossFilterId
})`,
},
{'class': 'blocklyEmboss'},
this.svgRoot,
);
this.tail = dom.createSvgElement(

View File

@@ -85,6 +85,10 @@ let content = `
transition: transform .5s;
}
.blocklyEmboss {
filter: var(--blocklyEmbossFilter);
}
.blocklyTooltipDiv {
background-color: #ffffc7;
border: 1px solid #ddc;
@@ -138,6 +142,10 @@ let content = `
border-color: inherit;
}
.blocklyHighlighted>.blocklyPath {
filter: var(--blocklyEmbossFilter);
}
.blocklyHighlightedConnectionPath {
fill: none;
stroke: #fc3;
@@ -189,6 +197,7 @@ let content = `
}
.blocklyDisabled>.blocklyPath {
fill: var(--blocklyDisabledPattern);
fill-opacity: .5;
stroke-opacity: .5;
}

View File

@@ -210,6 +210,9 @@ export class Grid {
* @param rnd A random ID to append to the pattern's ID.
* @param gridOptions The object containing grid configuration.
* @param defs The root SVG element for this workspace's defs.
* @param injectionDiv The div containing the parent workspace and all related
* workspaces and block containers. CSS variables representing SVG patterns
* will be scoped to this container.
* @returns The SVG element for the grid pattern.
* @internal
*/
@@ -217,6 +220,7 @@ export class Grid {
rnd: string,
gridOptions: GridOptions,
defs: SVGElement,
injectionDiv?: HTMLElement,
): SVGElement {
/*
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
@@ -247,6 +251,17 @@ export class Grid {
// Edge 16 doesn't handle empty patterns
dom.createSvgElement(Svg.LINE, {}, gridPattern);
}
if (injectionDiv) {
// Add CSS variables scoped to the injection div referencing the created
// patterns so that CSS can apply the patterns to any element in the
// injection div.
injectionDiv.style.setProperty(
'--blocklyGridPattern',
`url(#${gridPattern.id})`,
);
}
return gridPattern;
}
}

View File

@@ -89,7 +89,7 @@ export function inject(
* @param options Dictionary of options.
* @returns Newly created SVG image.
*/
function createDom(container: Element, options: Options): SVGElement {
function createDom(container: HTMLElement, options: Options): SVGElement {
// Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying
// out content in RTL mode. Therefore Blockly forces the use of LTR,
// then manually positions content in RTL as needed.
@@ -132,7 +132,12 @@ function createDom(container: Element, options: Options): SVGElement {
// https://neil.fraser.name/news/2015/11/01/
const rnd = String(Math.random()).substring(2);
options.gridPattern = Grid.createDom(rnd, options.gridOptions, defs);
options.gridPattern = Grid.createDom(
rnd,
options.gridOptions,
defs,
container,
);
return svg;
}
@@ -144,7 +149,7 @@ function createDom(container: Element, options: Options): SVGElement {
* @returns Newly created main workspace.
*/
function createMainWorkspace(
injectionDiv: Element,
injectionDiv: HTMLElement,
svg: SVGElement,
options: Options,
): WorkspaceSvg {

View File

@@ -926,8 +926,18 @@ export class ConstantProvider {
* @param svg The root of the workspace's SVG.
* @param tagName The name to use for the CSS style tag.
* @param selector The CSS selector to use.
* @param injectionDivIfIsParent The div containing the parent workspace and
* all related workspaces and block containers, if this renderer is for the
* parent workspace. CSS variables representing SVG patterns will be scoped
* to this container. Child workspaces should not override the CSS variables
* created by the parent and thus do not need access to the injection div.
*/
createDom(svg: SVGElement, tagName: string, selector: string) {
createDom(
svg: SVGElement,
tagName: string,
selector: string,
injectionDivIfIsParent?: HTMLElement,
) {
this.injectCSS_(tagName, selector);
/*
@@ -1034,6 +1044,24 @@ export class ConstantProvider {
this.disabledPattern = disabledPattern;
this.createDebugFilter();
if (injectionDivIfIsParent) {
// If this renderer is for the parent workspace, add CSS variables scoped
// to the injection div referencing the created patterns so that CSS can
// apply the patterns to any element in the injection div.
injectionDivIfIsParent.style.setProperty(
'--blocklyEmbossFilter',
`url(#${this.embossFilterId})`,
);
injectionDivIfIsParent.style.setProperty(
'--blocklyDisabledPattern',
`url(#${this.disabledPatternId})`,
);
injectionDivIfIsParent.style.setProperty(
'--blocklyDebugFilter',
`url(#${this.debugFilterId})`,
);
}
}
/**

View File

@@ -173,13 +173,8 @@ export class PathObject implements IPathObject {
updateHighlighted(enable: boolean) {
if (enable) {
this.svgPath.setAttribute(
'filter',
'url(#' + this.constants.embossFilterId + ')',
);
this.setClass_('blocklyHighlighted', true);
} else {
this.svgPath.setAttribute('filter', 'none');
this.setClass_('blocklyHighlighted', false);
}
}
@@ -206,12 +201,6 @@ export class PathObject implements IPathObject {
*/
protected updateDisabled_(disabled: boolean) {
this.setClass_('blocklyDisabled', disabled);
if (disabled) {
this.svgPath.setAttribute(
'fill',
'url(#' + this.constants.disabledPatternId + ')',
);
}
}
/**

View File

@@ -78,13 +78,23 @@ export class Renderer implements IRegistrable {
*
* @param svg The root of the workspace's SVG.
* @param theme The workspace theme object.
* @param injectionDivIfIsParent The div containing the parent workspace and
* all related workspaces and block containers, if this renderer is for the
* parent workspace. CSS variables representing SVG patterns will be scoped
* to this container. Child workspaces should not override the CSS variables
* created by the parent and thus do not need access to the injection div.
* @internal
*/
createDom(svg: SVGElement, theme: Theme) {
createDom(
svg: SVGElement,
theme: Theme,
injectionDivIfIsParent?: HTMLElement,
) {
this.constants_.createDom(
svg,
this.name + '-' + theme.name,
'.' + this.getClassName() + '.' + theme.getClassName(),
injectionDivIfIsParent,
);
}
@@ -93,8 +103,17 @@ export class Renderer implements IRegistrable {
*
* @param svg The root of the workspace's SVG.
* @param theme The workspace theme object.
* @param injectionDivIfIsParent The div containing the parent workspace and
* all related workspaces and block containers, if this renderer is for the
* parent workspace. CSS variables representing SVG patterns will be scoped
* to this container. Child workspaces should not override the CSS variables
* created by the parent and thus do not need access to the injection div.
*/
refreshDom(svg: SVGElement, theme: Theme) {
refreshDom(
svg: SVGElement,
theme: Theme,
injectionDivIfIsParent?: HTMLElement,
) {
const previousConstants = this.getConstants();
previousConstants.dispose();
this.constants_ = this.makeConstants_();
@@ -105,7 +124,7 @@ export class Renderer implements IRegistrable {
this.constants_.randomIdentifier = previousConstants.randomIdentifier;
this.constants_.setTheme(theme);
this.constants_.init();
this.createDom(svg, theme);
this.createDom(svg, theme, injectionDivIfIsParent);
}
/**

View File

@@ -50,8 +50,12 @@ export class Renderer extends BaseRenderer {
this.highlightConstants.init();
}
override refreshDom(svg: SVGElement, theme: Theme) {
super.refreshDom(svg, theme);
override refreshDom(
svg: SVGElement,
theme: Theme,
injectionDiv: HTMLElement,
) {
super.refreshDom(svg, theme, injectionDiv);
this.getHighlightConstants().init();
}

View File

@@ -675,8 +675,13 @@ export class ConstantProvider extends BaseConstantProvider {
return utilsColour.blend('#000', colour, 0.25) || colour;
}
override createDom(svg: SVGElement, tagName: string, selector: string) {
super.createDom(svg, tagName, selector);
override createDom(
svg: SVGElement,
tagName: string,
selector: string,
injectionDivIfIsParent?: HTMLElement,
) {
super.createDom(svg, tagName, selector, injectionDivIfIsParent);
/*
<defs>
... filters go here ...
@@ -795,6 +800,20 @@ export class ConstantProvider extends BaseConstantProvider {
);
this.replacementGlowFilterId = replacementGlowFilter.id;
this.replacementGlowFilter = replacementGlowFilter;
if (injectionDivIfIsParent) {
// If this renderer is for the parent workspace, add CSS variables scoped
// to the injection div referencing the created patterns so that CSS can
// apply the patterns to any element in the injection div.
injectionDivIfIsParent.style.setProperty(
'--blocklySelectedGlowFilter',
`url(#${this.selectedGlowFilterId})`,
);
injectionDivIfIsParent.style.setProperty(
'--blocklyReplacementGlowFilter',
`url(#${this.replacementGlowFilterId})`,
);
}
}
override getCSS_(selector: string) {
@@ -873,7 +892,7 @@ export class ConstantProvider extends BaseConstantProvider {
// Disabled outline paths.
`${selector} .blocklyDisabled > .blocklyOutlinePath {`,
`fill: url(#blocklyDisabledPattern${this.randomIdentifier})`,
`fill: var(--blocklyDisabledPattern)`,
`}`,
// Insertion marker.
@@ -881,6 +900,15 @@ export class ConstantProvider extends BaseConstantProvider {
`fill-opacity: ${this.INSERTION_MARKER_OPACITY};`,
`stroke: none;`,
`}`,
`${selector} .blocklySelected>.blocklyPath.blocklyPathSelected {`,
`fill: none;`,
`filter: var(--blocklySelectedGlowFilter);`,
`}`,
`${selector} .blocklyReplaceable>.blocklyPath {`,
`filter: var(--blocklyReplacementGlowFilter);`,
`}`,
];
}
}

View File

@@ -91,11 +91,7 @@ export class PathObject extends BasePathObject {
if (enable) {
if (!this.svgPathSelected) {
this.svgPathSelected = this.svgPath.cloneNode(true) as SVGElement;
this.svgPathSelected.setAttribute('fill', 'none');
this.svgPathSelected.setAttribute(
'filter',
'url(#' + this.constants.selectedGlowFilterId + ')',
);
this.svgPathSelected.classList.add('blocklyPathSelected');
this.svgRoot.appendChild(this.svgPathSelected);
}
} else {
@@ -108,14 +104,6 @@ export class PathObject extends BasePathObject {
override updateReplacementFade(enable: boolean) {
this.setClass_('blocklyReplaceable', enable);
if (enable) {
this.svgPath.setAttribute(
'filter',
'url(#' + this.constants.replacementGlowFilterId + ')',
);
} else {
this.svgPath.removeAttribute('filter');
}
}
override updateShapeForInputHighlight(conn: Connection, enable: boolean) {

View File

@@ -225,7 +225,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
* The first parent div with 'injectionDiv' in the name, or null if not set.
* Access this with getInjectionDiv.
*/
private injectionDiv: Element | null = null;
private injectionDiv: HTMLElement | null = null;
/**
* Last known position of the page scroll.
@@ -539,7 +539,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
*/
refreshTheme() {
if (this.svgGroup_) {
this.renderer.refreshDom(this.svgGroup_, this.getTheme());
const isParentWorkspace = this.options.parentWorkspace === null;
this.renderer.refreshDom(
this.svgGroup_,
this.getTheme(),
isParentWorkspace ? this.getInjectionDiv() : undefined,
);
}
// Update all blocks in workspace that have a style name.
@@ -636,20 +641,24 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
// Before the SVG canvas, scale the coordinates.
scale = this.scale;
}
let ancestor: Element = element;
do {
// Loop through this block and every parent.
const xy = svgMath.getRelativeXY(element);
if (element === this.getCanvas() || element === this.getBubbleCanvas()) {
const xy = svgMath.getRelativeXY(ancestor);
if (
ancestor === this.getCanvas() ||
ancestor === this.getBubbleCanvas()
) {
// After the SVG canvas, don't scale the coordinates.
scale = 1;
}
x += xy.x * scale;
y += xy.y * scale;
element = element.parentNode as SVGElement;
ancestor = ancestor.parentNode as Element;
} while (
element &&
element !== this.getParentSvg() &&
element !== this.getInjectionDiv()
ancestor &&
ancestor !== this.getParentSvg() &&
ancestor !== this.getInjectionDiv()
);
return new Coordinate(x, y);
}
@@ -687,7 +696,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
* @returns The first parent div with 'injectionDiv' in the name.
* @internal
*/
getInjectionDiv(): Element {
getInjectionDiv(): HTMLElement {
// NB: it would be better to pass this in at createDom, but is more likely
// to break existing uses of Blockly.
if (!this.injectionDiv) {
@@ -695,7 +704,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
while (element) {
const classes = element.getAttribute('class') || '';
if ((' ' + classes + ' ').includes(' injectionDiv ')) {
this.injectionDiv = element;
this.injectionDiv = element as HTMLElement;
break;
}
element = element.parentNode as Element;
@@ -739,7 +748,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
* 'blocklyMutatorBackground'.
* @returns The workspace's SVG group.
*/
createDom(opt_backgroundClass?: string, injectionDiv?: Element): Element {
createDom(opt_backgroundClass?: string, injectionDiv?: HTMLElement): Element {
if (!this.injectionDiv) {
this.injectionDiv = injectionDiv ?? null;
}
@@ -765,8 +774,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
);
if (opt_backgroundClass === 'blocklyMainBackground' && this.grid) {
this.svgBackground_.style.fill =
'url(#' + this.grid.getPatternId() + ')';
this.svgBackground_.style.fill = 'var(--blocklyGridPattern)';
} else {
this.themeManager_.subscribe(
this.svgBackground_,
@@ -823,7 +831,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
CursorClass && this.markerManager.setCursor(new CursorClass());
this.renderer.createDom(this.svgGroup_, this.getTheme());
const isParentWorkspace = this.options.parentWorkspace === null;
this.renderer.createDom(
this.svgGroup_,
this.getTheme(),
isParentWorkspace ? this.getInjectionDiv() : undefined,
);
return this.svgGroup_;
}