Skip to content

Commit 3d07875

Browse files
committed
feat(): support svg icon with content
1 parent 846ca27 commit 3d07875

File tree

3 files changed

+143
-56
lines changed

3 files changed

+143
-56
lines changed

bricks/icons/src/shared/SvgCache.ts

Lines changed: 61 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,65 @@ interface ResolveIconOptions {
1414
replaceSource?(source: string): string;
1515
}
1616

17+
export function constructSvgElement(
18+
content: string,
19+
retryable: false,
20+
options?: ResolveIconOptions
21+
): SVGSVGElement | null;
22+
export function constructSvgElement(
23+
content: string,
24+
retryable: true,
25+
options?: ResolveIconOptions
26+
): SVGResult;
27+
export function constructSvgElement(
28+
content: string,
29+
retryable: boolean,
30+
options?: ResolveIconOptions
31+
): SVGResult | null {
32+
const div = document.createElement("div");
33+
div.innerHTML = content;
34+
35+
const svg = div.firstElementChild;
36+
if (svg?.tagName?.toLowerCase() !== "svg")
37+
return retryable ? CACHEABLE_ERROR : null;
38+
39+
if (!parser) parser = new DOMParser();
40+
const doc = parser.parseFromString(svg.outerHTML, "text/html");
41+
42+
const svgEl = doc.body.querySelector("svg");
43+
if (!svgEl) return retryable ? CACHEABLE_ERROR : null;
44+
45+
const titles = svgEl.querySelectorAll("title");
46+
for (const title of titles) {
47+
title.remove();
48+
}
49+
50+
if (options?.currentColor) {
51+
const colorProps = [
52+
"color",
53+
"fill",
54+
"stroke",
55+
"stop-color",
56+
"flood-color",
57+
"lighting-color",
58+
];
59+
for (const prop of colorProps) {
60+
const elements = svgEl.querySelectorAll(
61+
`[${prop}]:not([${prop}="none"])`
62+
);
63+
for (const e of elements) {
64+
if (!belongToMask(e, svgEl)) {
65+
e.setAttribute(prop, "currentColor");
66+
}
67+
}
68+
}
69+
}
70+
71+
svgEl.setAttribute("width", "1em");
72+
svgEl.setAttribute("height", "1em");
73+
return document.adoptNode(svgEl);
74+
}
75+
1776
/** Given a URL, this function returns the resulting SVG element or an appropriate error symbol. */
1877
async function resolveIcon(
1978
url: string,
@@ -29,47 +88,8 @@ async function resolveIcon(
2988
}
3089

3190
try {
32-
const div = document.createElement("div");
33-
div.innerHTML = await fileData.text();
34-
35-
const svg = div.firstElementChild;
36-
if (svg?.tagName?.toLowerCase() !== "svg") return CACHEABLE_ERROR;
37-
38-
if (!parser) parser = new DOMParser();
39-
const doc = parser.parseFromString(svg.outerHTML, "text/html");
40-
41-
const svgEl = doc.body.querySelector("svg");
42-
if (!svgEl) return CACHEABLE_ERROR;
43-
44-
const titles = svgEl.querySelectorAll("title");
45-
for (const title of titles) {
46-
title.remove();
47-
}
48-
49-
if (options?.currentColor) {
50-
const colorProps = [
51-
"color",
52-
"fill",
53-
"stroke",
54-
"stop-color",
55-
"flood-color",
56-
"lighting-color",
57-
];
58-
for (const prop of colorProps) {
59-
const elements = svgEl.querySelectorAll(
60-
`[${prop}]:not([${prop}="none"])`
61-
);
62-
for (const e of elements) {
63-
if (!belongToMask(e, svgEl)) {
64-
e.setAttribute(prop, "currentColor");
65-
}
66-
}
67-
}
68-
}
69-
70-
svgEl.setAttribute("width", "1em");
71-
svgEl.setAttribute("height", "1em");
72-
return document.adoptNode(svgEl);
91+
const content = await fileData.text();
92+
return constructSvgElement(content, true, options);
7393
} catch {
7494
return CACHEABLE_ERROR;
7595
}

bricks/icons/src/svg-icon/index.spec.ts

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@ import { describe, test, expect } from "@jest/globals";
22
import "./index.js";
33
import type { SvgIcon } from "./index.js";
44

5-
(global as any).fetch = jest.fn(() =>
6-
Promise.resolve({
7-
ok: true,
8-
text: () =>
9-
Promise.resolve(
10-
`<?xml version="1.0" encoding="UTF-8"?>
5+
const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
116
<svg width="15px" height="17px" viewBox="0 0 15 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
127
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
138
<g transform="translate(-1804.000000, -58.000000)" stroke="#595959">
@@ -17,8 +12,12 @@ import type { SvgIcon } from "./index.js";
1712
</g>
1813
</g>
1914
</g>
20-
</svg>`.replace(/>\s+</g, "><")
21-
),
15+
</svg>`.replace(/>\s+</g, "><");
16+
17+
(global as any).fetch = jest.fn(() =>
18+
Promise.resolve({
19+
ok: true,
20+
text: () => Promise.resolve(svgContent),
2221
})
2322
);
2423

@@ -105,4 +104,64 @@ describe("eo-svg-icon", () => {
105104
(element as any)._render();
106105
expect(fetch).not.toBeCalled();
107106
});
107+
108+
test("use svg content", async () => {
109+
const element = document.createElement("eo-svg-icon") as SvgIcon;
110+
element.svgContent = svgContent;
111+
112+
expect(element.shadowRoot).toBeFalsy();
113+
document.body.appendChild(element);
114+
expect(element.shadowRoot).toBeTruthy();
115+
await (global as any).flushPromises();
116+
expect(element.shadowRoot?.childNodes).toMatchInlineSnapshot(`
117+
NodeList [
118+
<style>
119+
icons.shadow.css
120+
</style>,
121+
<svg
122+
height="1em"
123+
version="1.1"
124+
viewBox="0 0 15 17"
125+
width="1em"
126+
xmlns="http://www.w3.org/2000/svg"
127+
xmlns:xlink="http://www.w3.org/1999/xlink"
128+
>
129+
<g
130+
fill="none"
131+
fill-rule="evenodd"
132+
stroke="none"
133+
stroke-width="1"
134+
>
135+
<g
136+
stroke="currentColor"
137+
transform="translate(-1804.000000, -58.000000)"
138+
>
139+
<g
140+
transform="translate(1805.000000, 59.000000)"
141+
>
142+
<circle
143+
cx="6.512"
144+
cy="3.552"
145+
r="3.552"
146+
/>
147+
<path
148+
d="M10.448,8.184 Z"
149+
stroke-linecap="square"
150+
/>
151+
</g>
152+
</g>
153+
</g>
154+
</svg>,
155+
]
156+
`);
157+
document.body.removeChild(element);
158+
expect(element.shadowRoot?.childNodes.length).toBe(0);
159+
160+
// Re-connect
161+
document.body.appendChild(element);
162+
await (global as any).flushPromises();
163+
expect(element.shadowRoot?.childNodes.length).toBe(2);
164+
document.body.removeChild(element);
165+
expect(element.shadowRoot?.childNodes.length).toBe(0);
166+
});
108167
});

bricks/icons/src/svg-icon/index.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
type EventEmitter,
66
} from "@next-core/element";
77
import { wrapLocalBrick } from "@next-core/react-element";
8-
import { getIcon } from "../shared/SvgCache.js";
8+
import { constructSvgElement, getIcon } from "../shared/SvgCache.js";
99
import { getImageUrl } from "../shared/getImageUrl.js";
1010
import type { IconEvents, IconEventsMapping } from "../shared/interfaces.js";
1111
import sharedStyleText from "../shared/icons.shadow.css";
@@ -14,6 +14,7 @@ const { defineElement, property, event } = createDecorators();
1414

1515
export interface SvgIconProps {
1616
imgSrc?: string;
17+
svgContent?: string;
1718
noPublicRoot?: boolean;
1819
}
1920

@@ -23,6 +24,8 @@ class SvgIcon extends NextElement implements SvgIconProps {
2324
/** 图标地址 */
2425
@property() accessor imgSrc: string | undefined;
2526

27+
@property() accessor svgContent: string | undefined;
28+
2629
@property({
2730
type: Boolean,
2831
})
@@ -60,13 +63,18 @@ class SvgIcon extends NextElement implements SvgIconProps {
6063
if (!this.isConnected || !this.shadowRoot) {
6164
return;
6265
}
63-
const url = getImageUrl(this.imgSrc, this.noPublicRoot);
64-
65-
const svg = await getIcon(url, { currentColor: true });
66-
if (url !== getImageUrl(this.imgSrc, this.noPublicRoot)) {
67-
// The icon has changed during `await getIcon(...)`
68-
return;
66+
let svg: SVGElement | null = null;
67+
if (this.svgContent) {
68+
svg = constructSvgElement(this.svgContent, false, { currentColor: true });
69+
} else {
70+
const url = getImageUrl(this.imgSrc, this.noPublicRoot);
71+
svg = await getIcon(url, { currentColor: true });
72+
if (url !== getImageUrl(this.imgSrc, this.noPublicRoot)) {
73+
// The icon has changed during `await getIcon(...)`
74+
return;
75+
}
6976
}
77+
7078
// Currently React can't render mixed React Component and DOM nodes which are siblings,
7179
// so we manually construct the DOM.
7280
const nodes: Node[] = [];

0 commit comments

Comments
 (0)