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( const embossGroup = dom.createSvgElement(
Svg.G, Svg.G,
{ {'class': 'blocklyEmboss'},
'filter': `url(#${
this.workspace.getRenderer().getConstants().embossFilterId
})`,
},
this.svgRoot, this.svgRoot,
); );
this.tail = dom.createSvgElement( this.tail = dom.createSvgElement(

View File

@@ -85,6 +85,10 @@ let content = `
transition: transform .5s; transition: transform .5s;
} }
.blocklyEmboss {
filter: var(--blocklyEmbossFilter);
}
.blocklyTooltipDiv { .blocklyTooltipDiv {
background-color: #ffffc7; background-color: #ffffc7;
border: 1px solid #ddc; border: 1px solid #ddc;
@@ -138,6 +142,10 @@ let content = `
border-color: inherit; border-color: inherit;
} }
.blocklyHighlighted>.blocklyPath {
filter: var(--blocklyEmbossFilter);
}
.blocklyHighlightedConnectionPath { .blocklyHighlightedConnectionPath {
fill: none; fill: none;
stroke: #fc3; stroke: #fc3;
@@ -189,6 +197,7 @@ let content = `
} }
.blocklyDisabled>.blocklyPath { .blocklyDisabled>.blocklyPath {
fill: var(--blocklyDisabledPattern);
fill-opacity: .5; fill-opacity: .5;
stroke-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 rnd A random ID to append to the pattern's ID.
* @param gridOptions The object containing grid configuration. * @param gridOptions The object containing grid configuration.
* @param defs The root SVG element for this workspace's defs. * @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. * @returns The SVG element for the grid pattern.
* @internal * @internal
*/ */
@@ -217,6 +220,7 @@ export class Grid {
rnd: string, rnd: string,
gridOptions: GridOptions, gridOptions: GridOptions,
defs: SVGElement, defs: SVGElement,
injectionDiv?: HTMLElement,
): SVGElement { ): SVGElement {
/* /*
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse"> <pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
@@ -247,6 +251,17 @@ export class Grid {
// Edge 16 doesn't handle empty patterns // Edge 16 doesn't handle empty patterns
dom.createSvgElement(Svg.LINE, {}, gridPattern); 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; return gridPattern;
} }
} }

View File

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

View File

@@ -926,8 +926,18 @@ export class ConstantProvider {
* @param svg The root of the workspace's SVG. * @param svg The root of the workspace's SVG.
* @param tagName The name to use for the CSS style tag. * @param tagName The name to use for the CSS style tag.
* @param selector The CSS selector to use. * @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); this.injectCSS_(tagName, selector);
/* /*
@@ -1034,6 +1044,24 @@ export class ConstantProvider {
this.disabledPattern = disabledPattern; this.disabledPattern = disabledPattern;
this.createDebugFilter(); 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) { updateHighlighted(enable: boolean) {
if (enable) { if (enable) {
this.svgPath.setAttribute(
'filter',
'url(#' + this.constants.embossFilterId + ')',
);
this.setClass_('blocklyHighlighted', true); this.setClass_('blocklyHighlighted', true);
} else { } else {
this.svgPath.setAttribute('filter', 'none');
this.setClass_('blocklyHighlighted', false); this.setClass_('blocklyHighlighted', false);
} }
} }
@@ -206,12 +201,6 @@ export class PathObject implements IPathObject {
*/ */
protected updateDisabled_(disabled: boolean) { protected updateDisabled_(disabled: boolean) {
this.setClass_('blocklyDisabled', disabled); 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 svg The root of the workspace's SVG.
* @param theme The workspace theme object. * @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 * @internal
*/ */
createDom(svg: SVGElement, theme: Theme) { createDom(
svg: SVGElement,
theme: Theme,
injectionDivIfIsParent?: HTMLElement,
) {
this.constants_.createDom( this.constants_.createDom(
svg, svg,
this.name + '-' + theme.name, this.name + '-' + theme.name,
'.' + this.getClassName() + '.' + theme.getClassName(), '.' + this.getClassName() + '.' + theme.getClassName(),
injectionDivIfIsParent,
); );
} }
@@ -93,8 +103,17 @@ export class Renderer implements IRegistrable {
* *
* @param svg The root of the workspace's SVG. * @param svg The root of the workspace's SVG.
* @param theme The workspace theme object. * @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(); const previousConstants = this.getConstants();
previousConstants.dispose(); previousConstants.dispose();
this.constants_ = this.makeConstants_(); this.constants_ = this.makeConstants_();
@@ -105,7 +124,7 @@ export class Renderer implements IRegistrable {
this.constants_.randomIdentifier = previousConstants.randomIdentifier; this.constants_.randomIdentifier = previousConstants.randomIdentifier;
this.constants_.setTheme(theme); this.constants_.setTheme(theme);
this.constants_.init(); 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(); this.highlightConstants.init();
} }
override refreshDom(svg: SVGElement, theme: Theme) { override refreshDom(
super.refreshDom(svg, theme); svg: SVGElement,
theme: Theme,
injectionDiv: HTMLElement,
) {
super.refreshDom(svg, theme, injectionDiv);
this.getHighlightConstants().init(); this.getHighlightConstants().init();
} }

View File

@@ -675,8 +675,13 @@ export class ConstantProvider extends BaseConstantProvider {
return utilsColour.blend('#000', colour, 0.25) || colour; return utilsColour.blend('#000', colour, 0.25) || colour;
} }
override createDom(svg: SVGElement, tagName: string, selector: string) { override createDom(
super.createDom(svg, tagName, selector); svg: SVGElement,
tagName: string,
selector: string,
injectionDivIfIsParent?: HTMLElement,
) {
super.createDom(svg, tagName, selector, injectionDivIfIsParent);
/* /*
<defs> <defs>
... filters go here ... ... filters go here ...
@@ -795,6 +800,20 @@ export class ConstantProvider extends BaseConstantProvider {
); );
this.replacementGlowFilterId = replacementGlowFilter.id; this.replacementGlowFilterId = replacementGlowFilter.id;
this.replacementGlowFilter = replacementGlowFilter; 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) { override getCSS_(selector: string) {
@@ -873,7 +892,7 @@ export class ConstantProvider extends BaseConstantProvider {
// Disabled outline paths. // Disabled outline paths.
`${selector} .blocklyDisabled > .blocklyOutlinePath {`, `${selector} .blocklyDisabled > .blocklyOutlinePath {`,
`fill: url(#blocklyDisabledPattern${this.randomIdentifier})`, `fill: var(--blocklyDisabledPattern)`,
`}`, `}`,
// Insertion marker. // Insertion marker.
@@ -881,6 +900,15 @@ export class ConstantProvider extends BaseConstantProvider {
`fill-opacity: ${this.INSERTION_MARKER_OPACITY};`, `fill-opacity: ${this.INSERTION_MARKER_OPACITY};`,
`stroke: none;`, `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 (enable) {
if (!this.svgPathSelected) { if (!this.svgPathSelected) {
this.svgPathSelected = this.svgPath.cloneNode(true) as SVGElement; this.svgPathSelected = this.svgPath.cloneNode(true) as SVGElement;
this.svgPathSelected.setAttribute('fill', 'none'); this.svgPathSelected.classList.add('blocklyPathSelected');
this.svgPathSelected.setAttribute(
'filter',
'url(#' + this.constants.selectedGlowFilterId + ')',
);
this.svgRoot.appendChild(this.svgPathSelected); this.svgRoot.appendChild(this.svgPathSelected);
} }
} else { } else {
@@ -108,14 +104,6 @@ export class PathObject extends BasePathObject {
override updateReplacementFade(enable: boolean) { override updateReplacementFade(enable: boolean) {
this.setClass_('blocklyReplaceable', enable); 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) { 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. * The first parent div with 'injectionDiv' in the name, or null if not set.
* Access this with getInjectionDiv. * Access this with getInjectionDiv.
*/ */
private injectionDiv: Element | null = null; private injectionDiv: HTMLElement | null = null;
/** /**
* Last known position of the page scroll. * Last known position of the page scroll.
@@ -539,7 +539,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
*/ */
refreshTheme() { refreshTheme() {
if (this.svgGroup_) { 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. // 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. // Before the SVG canvas, scale the coordinates.
scale = this.scale; scale = this.scale;
} }
let ancestor: Element = element;
do { do {
// Loop through this block and every parent. // Loop through this block and every parent.
const xy = svgMath.getRelativeXY(element); const xy = svgMath.getRelativeXY(ancestor);
if (element === this.getCanvas() || element === this.getBubbleCanvas()) { if (
ancestor === this.getCanvas() ||
ancestor === this.getBubbleCanvas()
) {
// After the SVG canvas, don't scale the coordinates. // After the SVG canvas, don't scale the coordinates.
scale = 1; scale = 1;
} }
x += xy.x * scale; x += xy.x * scale;
y += xy.y * scale; y += xy.y * scale;
element = element.parentNode as SVGElement; ancestor = ancestor.parentNode as Element;
} while ( } while (
element && ancestor &&
element !== this.getParentSvg() && ancestor !== this.getParentSvg() &&
element !== this.getInjectionDiv() ancestor !== this.getInjectionDiv()
); );
return new Coordinate(x, y); 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. * @returns The first parent div with 'injectionDiv' in the name.
* @internal * @internal
*/ */
getInjectionDiv(): Element { getInjectionDiv(): HTMLElement {
// NB: it would be better to pass this in at createDom, but is more likely // NB: it would be better to pass this in at createDom, but is more likely
// to break existing uses of Blockly. // to break existing uses of Blockly.
if (!this.injectionDiv) { if (!this.injectionDiv) {
@@ -695,7 +704,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
while (element) { while (element) {
const classes = element.getAttribute('class') || ''; const classes = element.getAttribute('class') || '';
if ((' ' + classes + ' ').includes(' injectionDiv ')) { if ((' ' + classes + ' ').includes(' injectionDiv ')) {
this.injectionDiv = element; this.injectionDiv = element as HTMLElement;
break; break;
} }
element = element.parentNode as Element; element = element.parentNode as Element;
@@ -739,7 +748,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
* 'blocklyMutatorBackground'. * 'blocklyMutatorBackground'.
* @returns The workspace's SVG group. * @returns The workspace's SVG group.
*/ */
createDom(opt_backgroundClass?: string, injectionDiv?: Element): Element { createDom(opt_backgroundClass?: string, injectionDiv?: HTMLElement): Element {
if (!this.injectionDiv) { if (!this.injectionDiv) {
this.injectionDiv = injectionDiv ?? null; this.injectionDiv = injectionDiv ?? null;
} }
@@ -765,8 +774,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
); );
if (opt_backgroundClass === 'blocklyMainBackground' && this.grid) { if (opt_backgroundClass === 'blocklyMainBackground' && this.grid) {
this.svgBackground_.style.fill = this.svgBackground_.style.fill = 'var(--blocklyGridPattern)';
'url(#' + this.grid.getPatternId() + ')';
} else { } else {
this.themeManager_.subscribe( this.themeManager_.subscribe(
this.svgBackground_, this.svgBackground_,
@@ -823,7 +831,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
CursorClass && this.markerManager.setCursor(new CursorClass()); 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_; return this.svgGroup_;
} }