aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2024-04-27 23:40:42 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2024-04-27 23:40:42 +0200
commit85a3ee2bcaf9a77b9d3751c5a18bdb6f6d20ad49 (patch)
tree19c0850d14ae6afae85515847163810ea25be711 /subprojects
parentrfactor(frontend): scroll to top on initialization (diff)
downloadrefinery-85a3ee2bcaf9a77b9d3751c5a18bdb6f6d20ad49.tar.gz
refinery-85a3ee2bcaf9a77b9d3751c5a18bdb6f6d20ad49.tar.zst
refinery-85a3ee2bcaf9a77b9d3751c5a18bdb6f6d20ad49.zip
refactor(frontend): fix icon placement in Safari
Also affected WebKitGTK
Diffstat (limited to 'subprojects')
-rw-r--r--subprojects/frontend/src/graph/GraphArea.tsx2
-rw-r--r--subprojects/frontend/src/graph/GraphTheme.tsx58
-rw-r--r--subprojects/frontend/src/graph/SVGIcons.tsx41
-rw-r--r--subprojects/frontend/src/graph/export/exportDiagram.tsx65
-rw-r--r--subprojects/frontend/src/graph/icons.tsx29
-rw-r--r--subprojects/frontend/src/graph/postProcessSVG.ts20
6 files changed, 111 insertions, 104 deletions
diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx
index b5d93aef..70ae3bc5 100644
--- a/subprojects/frontend/src/graph/GraphArea.tsx
+++ b/subprojects/frontend/src/graph/GraphArea.tsx
@@ -12,6 +12,7 @@ import { useResizeDetector } from 'react-resize-detector';
12 12
13import DotGraphVisualizer from './DotGraphVisualizer'; 13import DotGraphVisualizer from './DotGraphVisualizer';
14import type GraphStore from './GraphStore'; 14import type GraphStore from './GraphStore';
15import SVGIcons from './SVGIcons';
15import VisibilityPanel from './VisibilityPanel'; 16import VisibilityPanel from './VisibilityPanel';
16import ZoomCanvas from './ZoomCanvas'; 17import ZoomCanvas from './ZoomCanvas';
17import ExportPanel from './export/ExportPanel'; 18import ExportPanel from './export/ExportPanel';
@@ -38,6 +39,7 @@ function GraphArea({ graph }: { graph: GraphStore }): JSX.Element {
38 position="relative" 39 position="relative"
39 ref={ref} 40 ref={ref}
40 > 41 >
42 <SVGIcons />
41 <ZoomCanvas> 43 <ZoomCanvas>
42 {(fitZoom) => ( 44 {(fitZoom) => (
43 <DotGraphVisualizer 45 <DotGraphVisualizer
diff --git a/subprojects/frontend/src/graph/GraphTheme.tsx b/subprojects/frontend/src/graph/GraphTheme.tsx
index 50a003e0..bdc01b78 100644
--- a/subprojects/frontend/src/graph/GraphTheme.tsx
+++ b/subprojects/frontend/src/graph/GraphTheme.tsx
@@ -4,9 +4,6 @@
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
6 6
7import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw';
8import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw';
9import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw';
10import { 7import {
11 alpha, 8 alpha,
12 styled, 9 styled,
@@ -16,8 +13,6 @@ import {
16import { lch } from 'd3-color'; 13import { lch } from 'd3-color';
17import { range } from 'lodash-es'; 14import { range } from 'lodash-es';
18 15
19import svgURL from '../utils/svgURL';
20
21import obfuscateColor from './obfuscateColor'; 16import obfuscateColor from './obfuscateColor';
22 17
23function createEdgeColor( 18function createEdgeColor(
@@ -69,32 +64,16 @@ function createTypeHashStyles(
69 return result; 64 return result;
70} 65}
71 66
72function iconStyle(
73 svg: string,
74 color: string,
75 noEmbedIcons?: boolean,
76): CSSObject {
77 if (noEmbedIcons) {
78 return {
79 fill: color,
80 };
81 }
82 return {
83 maskImage: svgURL(svg),
84 background: color,
85 };
86}
87
88export function createGraphTheme({ 67export function createGraphTheme({
89 theme, 68 theme,
90 colorNodes, 69 colorNodes,
91 hexTypeHashes, 70 hexTypeHashes,
92 noEmbedIcons, 71 useOpacity,
93}: { 72}: {
94 theme: Theme; 73 theme: Theme;
95 colorNodes: boolean; 74 colorNodes: boolean;
96 hexTypeHashes: string[]; 75 hexTypeHashes: string[];
97 noEmbedIcons?: boolean; 76 useOpacity?: boolean;
98}): CSSObject { 77}): CSSObject {
99 const shadowAlapha = theme.palette.mode === 'dark' ? 0.32 : 0.24; 78 const shadowAlapha = theme.palette.mode === 'dark' ? 0.32 : 0.24;
100 79
@@ -120,13 +99,15 @@ export function createGraphTheme({
120 '.node-INDIVIDUAL .node-outline': { 99 '.node-INDIVIDUAL .node-outline': {
121 strokeWidth: 2, 100 strokeWidth: 2,
122 }, 101 },
123 '.node-shadow.node-bg': noEmbedIcons 102 '.node-shadow.node-bg': useOpacity
124 ? { 103 ? {
125 // Inkscape can't handle opacity in exported SVG. 104 // Inkscape can't handle RGBA in exported SVG.
126 fill: theme.palette.text.primary, 105 fill: theme.palette.text.primary,
127 opacity: shadowAlapha, 106 opacity: shadowAlapha,
128 } 107 }
129 : { 108 : {
109 // But using `opacity` with the transition animation leads to flashing shadows,
110 // so we still use RGBA whenever possible.
130 fill: alpha(theme.palette.text.primary, shadowAlapha), 111 fill: alpha(theme.palette.text.primary, shadowAlapha),
131 }, 112 },
132 '.node-exists-UNKNOWN .node-outline': { 113 '.node-exists-UNKNOWN .node-outline': {
@@ -147,24 +128,15 @@ export function createGraphTheme({
147 }, 128 },
148 ...createEdgeColor('UNKNOWN', theme.palette.text.secondary, 'none'), 129 ...createEdgeColor('UNKNOWN', theme.palette.text.secondary, 'none'),
149 ...createEdgeColor('ERROR', theme.palette.error.main), 130 ...createEdgeColor('ERROR', theme.palette.error.main),
150 ...(noEmbedIcons 131 '.icon-TRUE': {
151 ? {} 132 fill: theme.palette.text.primary,
152 : { 133 },
153 '.icon': { 134 '.icon-UNKNOWN': {
154 maskSize: '12px 12px', 135 fill: theme.palette.text.secondary,
155 maskPosition: '50% 50%', 136 },
156 maskRepeat: 'no-repeat', 137 '.icon-ERROR': {
157 width: '100%', 138 fill: theme.palette.error.main,
158 height: '100%', 139 },
159 },
160 }),
161 '.icon-TRUE': iconStyle(labelSVG, theme.palette.text.primary, noEmbedIcons),
162 '.icon-UNKNOWN': iconStyle(
163 labelOutlinedSVG,
164 theme.palette.text.secondary,
165 noEmbedIcons,
166 ),
167 '.icon-ERROR': iconStyle(cancelSVG, theme.palette.error.main, noEmbedIcons),
168 'text.label-UNKNOWN': { 140 'text.label-UNKNOWN': {
169 fill: theme.palette.text.secondary, 141 fill: theme.palette.text.secondary,
170 }, 142 },
diff --git a/subprojects/frontend/src/graph/SVGIcons.tsx b/subprojects/frontend/src/graph/SVGIcons.tsx
new file mode 100644
index 00000000..fa3484b1
--- /dev/null
+++ b/subprojects/frontend/src/graph/SVGIcons.tsx
@@ -0,0 +1,41 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { styled } from '@mui/material/styles';
8import { useCallback } from 'react';
9
10import icons from './icons';
11
12export const SVG_NS = 'http://www.w3.org/2000/svg';
13
14const SVGIconsHolder = styled('div', {
15 name: 'SVGIcons-Holder',
16})({
17 position: 'absolute',
18 top: 0,
19 left: 0,
20 width: 0,
21 height: 0,
22 visibility: 'hidden',
23});
24
25export default function SVGIcons(): JSX.Element {
26 const addNodes = useCallback((element: HTMLDivElement | null) => {
27 if (element === null) {
28 return;
29 }
30 const svgElement = document.createElementNS(SVG_NS, 'svg');
31 const defs = document.createElementNS(SVG_NS, 'defs');
32 svgElement.appendChild(defs);
33 icons.forEach((value) => {
34 const importedValue = document.importNode(value, true);
35 importedValue.id = `refinery-${importedValue.id}`;
36 defs.appendChild(importedValue);
37 });
38 element.replaceChildren(svgElement);
39 }, []);
40 return <SVGIconsHolder ref={addNodes} />;
41}
diff --git a/subprojects/frontend/src/graph/export/exportDiagram.tsx b/subprojects/frontend/src/graph/export/exportDiagram.tsx
index 52d19aa0..73b40fea 100644
--- a/subprojects/frontend/src/graph/export/exportDiagram.tsx
+++ b/subprojects/frontend/src/graph/export/exportDiagram.tsx
@@ -12,9 +12,6 @@ import normalFontURL from '@fontsource/open-sans/files/open-sans-latin-400-norma
12import boldFontURL from '@fontsource/open-sans/files/open-sans-latin-700-normal.woff2?url'; 12import boldFontURL from '@fontsource/open-sans/files/open-sans-latin-700-normal.woff2?url';
13import variableItalicFontURL from '@fontsource-variable/open-sans/files/open-sans-latin-wght-italic.woff2?url'; 13import variableItalicFontURL from '@fontsource-variable/open-sans/files/open-sans-latin-wght-italic.woff2?url';
14import variableFontURL from '@fontsource-variable/open-sans/files/open-sans-latin-wght-normal.woff2?url'; 14import variableFontURL from '@fontsource-variable/open-sans/files/open-sans-latin-wght-normal.woff2?url';
15import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw';
16import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw';
17import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw';
18import type { Theme } from '@mui/material/styles'; 15import type { Theme } from '@mui/material/styles';
19import { nanoid } from 'nanoid'; 16import { nanoid } from 'nanoid';
20 17
@@ -22,6 +19,7 @@ import { darkTheme, lightTheme } from '../../theme/ThemeProvider';
22import { copyBlob, saveBlob } from '../../utils/fileIO'; 19import { copyBlob, saveBlob } from '../../utils/fileIO';
23import type GraphStore from '../GraphStore'; 20import type GraphStore from '../GraphStore';
24import { createGraphTheme } from '../GraphTheme'; 21import { createGraphTheme } from '../GraphTheme';
22import icons from '../icons';
25import { SVG_NS } from '../postProcessSVG'; 23import { SVG_NS } from '../postProcessSVG';
26 24
27import type ExportSettingsStore from './ExportSettingsStore'; 25import type ExportSettingsStore from './ExportSettingsStore';
@@ -31,24 +29,6 @@ const PNG_CONTENT_TYPE = 'image/png';
31const SVG_CONTENT_TYPE = 'image/svg+xml'; 29const SVG_CONTENT_TYPE = 'image/svg+xml';
32const EXPORT_ID = 'export-image'; 30const EXPORT_ID = 'export-image';
33 31
34const ICONS: Map<string, Element> = new Map();
35
36function importSVG(svgSource: string, className: string): void {
37 const parser = new DOMParser();
38 const svgDocument = parser.parseFromString(svgSource, SVG_CONTENT_TYPE);
39 const root = svgDocument.children[0];
40 if (root === undefined) {
41 return;
42 }
43 root.id = className;
44 root.classList.add(className);
45 ICONS.set(className, root);
46}
47
48importSVG(labelSVG, 'icon-TRUE');
49importSVG(labelOutlinedSVG, 'icon-UNKNOWN');
50importSVG(cancelSVG, 'icon-ERROR');
51
52function fixIDs(id: string, svgDocument: XMLDocument) { 32function fixIDs(id: string, svgDocument: XMLDocument) {
53 const idMap = new Map<string, string>(); 33 const idMap = new Map<string, string>();
54 let i = 0; 34 let i = 0;
@@ -202,7 +182,7 @@ function appendStyles(
202 theme, 182 theme,
203 colorNodes, 183 colorNodes,
204 hexTypeHashes, 184 hexTypeHashes,
205 noEmbedIcons: true, 185 useOpacity: true,
206 }); 186 });
207 const sheet = { 187 const sheet = {
208 insert(rule) { 188 insert(rule) {
@@ -216,42 +196,25 @@ function appendStyles(
216 styleElement.innerHTML = rules.join(''); 196 styleElement.innerHTML = rules.join('');
217} 197}
218 198
219function fixForeignObjects( 199function fixIcons(
220 id: string, 200 id: string,
221 svgDocument: XMLDocument, 201 svgDocument: XMLDocument,
222 svg: SVGSVGElement, 202 svg: SVGSVGElement,
223): void { 203): void {
224 const foreignObjects: SVGForeignObjectElement[] = []; 204 const prefix = `refinery-${id}-`;
225 svg 205 const hrefPrefix = `#${prefix}`;
226 .querySelectorAll('foreignObject') 206 svg.querySelectorAll('use').forEach((use) => {
227 .forEach((object) => foreignObjects.push(object)); 207 const href = use.getAttribute('href');
228 foreignObjects.forEach((object) => { 208 if (href === null) {
229 const useElement = svgDocument.createElementNS(SVG_NS, 'use'); 209 return;
230 let x = Number(object.getAttribute('x') ?? '0'); 210 }
231 let y = Number(object.getAttribute('y') ?? '0'); 211 use.setAttribute('href', href.replace(/^#refinery-/, hrefPrefix));
232 const width = Number(object.getAttribute('width') ?? '0');
233 const height = Number(object.getAttribute('height') ?? '0');
234 const size = Math.min(width, height);
235 x += (width - size) / 2;
236 y += (height - size) / 2;
237 useElement.setAttribute('x', String(x));
238 useElement.setAttribute('y', String(y));
239 useElement.setAttribute('width', String(size));
240 useElement.setAttribute('height', String(size));
241 useElement.id = object.id;
242 object.children[0]?.classList?.forEach((className) => {
243 useElement.classList.add(className);
244 if (ICONS.has(className)) {
245 useElement.setAttribute('href', `#refinery-${id}-${className}`);
246 }
247 });
248 object.replaceWith(useElement);
249 }); 212 });
250 const defs = svgDocument.createElementNS(SVG_NS, 'defs'); 213 const defs = svgDocument.createElementNS(SVG_NS, 'defs');
251 svg.prepend(defs); 214 svg.prepend(defs);
252 ICONS.forEach((value) => { 215 icons.forEach((value) => {
253 const importedValue = svgDocument.importNode(value, true); 216 const importedValue = svgDocument.importNode(value, true);
254 importedValue.id = `refinery-${id}-${importedValue.id}`; 217 importedValue.id = `${prefix}${importedValue.id}`;
255 defs.appendChild(importedValue); 218 defs.appendChild(importedValue);
256 }); 219 });
257} 220}
@@ -398,7 +361,7 @@ export default async function exportDiagram(
398 addBackground(svgDocument, copyOfSVG, theme); 361 addBackground(svgDocument, copyOfSVG, theme);
399 } 362 }
400 363
401 fixForeignObjects(id, svgDocument, copyOfSVG); 364 fixIcons(id, svgDocument, copyOfSVG);
402 365
403 const { colorNodes } = graph; 366 const { colorNodes } = graph;
404 let fontsCSS = ''; 367 let fontsCSS = '';
diff --git a/subprojects/frontend/src/graph/icons.tsx b/subprojects/frontend/src/graph/icons.tsx
new file mode 100644
index 00000000..4f4407a0
--- /dev/null
+++ b/subprojects/frontend/src/graph/icons.tsx
@@ -0,0 +1,29 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw';
8import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw';
9import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw';
10
11const icons: Map<string, Element> = new Map();
12
13export default icons;
14
15function importSVG(svgSource: string, className: string): void {
16 const parser = new DOMParser();
17 const svgDocument = parser.parseFromString(svgSource, 'image/svg+xml');
18 const root = svgDocument.children[0];
19 if (root === undefined) {
20 return;
21 }
22 root.id = className;
23 root.classList.add(className);
24 icons.set(className, root);
25}
26
27importSVG(labelSVG, 'icon-TRUE');
28importSVG(labelOutlinedSVG, 'icon-UNKNOWN');
29importSVG(cancelSVG, 'icon-ERROR');
diff --git a/subprojects/frontend/src/graph/postProcessSVG.ts b/subprojects/frontend/src/graph/postProcessSVG.ts
index bf990f3a..97130012 100644
--- a/subprojects/frontend/src/graph/postProcessSVG.ts
+++ b/subprojects/frontend/src/graph/postProcessSVG.ts
@@ -144,14 +144,14 @@ function replaceImages(node: SVGGElement) {
144 } 144 }
145 const width = image.getAttribute('width')?.replace('px', '') ?? ''; 145 const width = image.getAttribute('width')?.replace('px', '') ?? '';
146 const height = image.getAttribute('height')?.replace('px', '') ?? ''; 146 const height = image.getAttribute('height')?.replace('px', '') ?? '';
147 const foreign = document.createElementNS(SVG_NS, 'foreignObject'); 147 const use = document.createElementNS(SVG_NS, 'use');
148 foreign.setAttribute('x', image.getAttribute('x') ?? ''); 148 use.setAttribute('x', image.getAttribute('x') ?? '');
149 foreign.setAttribute('y', image.getAttribute('y') ?? ''); 149 use.setAttribute('y', image.getAttribute('y') ?? '');
150 foreign.setAttribute('width', width); 150 use.setAttribute('width', width);
151 foreign.setAttribute('height', height); 151 use.setAttribute('height', height);
152 const div = document.createElement('div'); 152 const iconName = `icon-${href.replace('#', '')}`;
153 div.classList.add('icon', `icon-${href.replace('#', '')}`); 153 use.setAttribute('href', `#refinery-${iconName}`);
154 foreign.appendChild(div); 154 use.classList.add('icon', iconName);
155 const sibling = image.nextElementSibling; 155 const sibling = image.nextElementSibling;
156 // Since dot doesn't respect the `id` attribute on table cells with a single image, 156 // Since dot doesn't respect the `id` attribute on table cells with a single image,
157 // compute the ID based on the ID of the next element (the label). 157 // compute the ID based on the ID of the next element (the label).
@@ -160,9 +160,9 @@ function replaceImages(node: SVGGElement) {
160 sibling.tagName.toLowerCase() === 'g' && 160 sibling.tagName.toLowerCase() === 'g' &&
161 sibling.id !== '' 161 sibling.id !== ''
162 ) { 162 ) {
163 foreign.id = `${sibling.id},icon`; 163 use.id = `${sibling.id},icon`;
164 } 164 }
165 image.parentNode?.replaceChild(foreign, image); 165 image.parentNode?.replaceChild(use, image);
166 }); 166 });
167} 167}
168 168