Skip to content

Commit

Permalink
feat(): support svg icon with content
Browse files Browse the repository at this point in the history
  • Loading branch information
weareoutman committed Sep 5, 2024
1 parent 846ca27 commit 7831bfb
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 56 deletions.
59 changes: 59 additions & 0 deletions bricks/icons/docs/eo-svg-icon.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,62 @@ properties:
fontSize: 32px
color: var(--palette-green-6)
```
## Examples
### SvgContent
```yaml preview
- brick: h2
properties:
textContent: 预览 SVG 图标
- brick: p
properties:
textContent: 选择一张 SVG 图片,并预览它生成的图标。注意在右上角切换主题模式,以便验证其在深色和浅色主题下都能按预期显示。
- brick: eo-upload-file
properties:
style:
width: 300px
uploadDraggable: true
accept: image/svg+xml
draggableUploadTip: 选择一张 SVG 图片
events:
change:
- if: <% EVENT.detail?.length %>
useProvider: basic.set-timeout
args:
- 0
- <% EVENT.detail[EVENT.detail.length - 1].file.text() %>
callback:
success:
target: eo-svg-icon
properties:
svgContent: <% EVENT.detail %>
- brick: eo-svg-icon
properties:
svgContent: |
<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>编组 11</title>
<g id="设计稿" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="节点A-2-5备份-8" transform="translate(-328.000000, -737.000000)">
<g id="编组-10" transform="translate(118.000000, 165.000000)">
<g id="编组-4备份-4" transform="translate(193.000000, 572.000000)">
<g id="编组-11" transform="translate(17.000000, 0.000000)">
<g id="背景区域" opacity="0" fill="#D8D8D8" fill-rule="nonzero">
<rect id="矩形" x="0" y="0" width="50" height="50"></rect>
</g>
<g id="图形" transform="translate(1.000000, 6.163443)" fill="#0A2B7C">
<polygon id="路径-7" points="23.9669422 38.1353143 23.9669422 29.8266835 47.9338844 14.6547388 47.9338844 23.2092327"></polygon>
<polygon id="路径-7备份-2" fill-opacity="0.85" transform="translate(11.983471, 26.895027) scale(-1, 1) translate(-11.983471, -26.895027) " points="0 38.1353143 0 29.8266835 23.9669421 15.6547388 23.9669421 24.2092327"></polygon>
<path d="M23.9669422,0 L47.9338844,14.6547388 L23.9669422,29.8266835 L1.00000079e-07,15.6547388 L23.9669422,0 Z M23.9673365,4.33655702 L18.9974043,7.59974204 L20.337264,8.45100769 L23.0933942,6.64076695 L23.2261771,11.8075118 C22.4431552,11.9182441 21.7020662,12.170893 21.1011309,12.5654584 C20.5003317,12.9599343 20.1214346,13.442546 19.9628255,13.9499068 L11.9620118,13.9494705 L15.1512478,11.8560703 L13.8113883,11.0048047 L8.40928812,14.5517449 L13.7687265,17.9568075 L15.0652306,17.1055419 L11.9926688,15.1533419 L19.9935217,15.1537047 C20.1779914,15.6610209 20.5814534,16.1435869 21.2022938,16.5380315 C21.8229927,16.9323858 22.576475,17.1849755 23.3646777,17.2958002 L23.496299,22.4624466 L20.424669,20.5106045 L19.128165,21.3618701 L24.4876034,24.7669328 L29.8897035,21.2199925 L28.5498439,20.3687269 L25.3604896,22.4624466 L25.2300188,17.2958002 C26.0125772,17.1849755 26.753195,16.9323858 27.353809,16.5380315 C27.9545602,16.1435869 28.3334447,15.6610209 28.4920764,15.1537047 L36.4924832,15.1533419 L33.7362922,16.9633805 L35.0761518,17.8146462 L40.0456518,14.5517449 L34.6862134,11.1466823 L33.3897093,11.9979479 L36.4618261,13.9494705 L28.4614624,13.9499068 C28.277013,13.442546 27.8735362,12.9599343 27.252646,12.5654584 C26.6316152,12.170893 25.8776587,11.9182441 25.0889971,11.8075118 L24.9575848,6.64076695 L28.0302709,8.59288531 L29.3267749,7.74161965 L23.9673365,4.33655702 Z M24.2222536,13.0198498 L24.2686283,13.0206385 L24.310389,13.021765 L24.2961026,13.0213211 L24.332064,13.0225173 C24.4214655,13.025918 24.5107068,13.032499 24.5993376,13.0422603 L24.717571,13.0570049 L24.7458348,13.0612184 L24.7620823,13.0638997 L24.808418,13.0715605 L24.8557462,13.0797582 L24.8851354,13.0852727 L24.8990801,13.0879796 L24.9421553,13.0967143 C25.2888525,13.1692964 25.6161875,13.2932426 25.8949055,13.4685526 C26.1229872,13.6120132 26.2978043,13.7768447 26.4190955,13.9527995 L26.4699892,14.0325704 L26.4832809,14.057277 L26.4982578,14.0839427 L26.5109209,14.1080604 L26.5199248,14.1265614 C26.5254477,14.138165 26.5307529,14.1498003 26.5358403,14.1614648 L26.546423,14.1869811 L26.563102,14.2299733 L26.5910295,14.3195001 L26.6116481,14.418722 L26.615975,14.4474502 L26.6179046,14.4600468 L26.6206387,14.4859081 L26.623486,14.5251143 L26.6248477,14.5808085 L26.6237746,14.6125418 L26.6226511,14.642908 L26.6203365,14.6657847 L26.6196274,14.6784234 L26.6153171,14.713884 C26.6025534,14.8051692 26.5769741,14.8957538 26.5379685,14.9844763 L26.529032,15.0058042 L26.518991,15.0271362 C26.411493,15.2495371 26.22202,15.4586651 25.9500842,15.6353699 C25.6951549,15.8010239 25.3944807,15.9208164 25.0727442,15.9947474 L25.0613171,15.9973518 L25.0459899,16.0003057 L25.012051,16.0080986 L24.9471906,16.0210773 L24.8909707,16.0309492 L24.8605368,16.0359287 L24.8323339,16.0409065 L24.7757397,16.0492401 L24.8247293,16.0416744 L24.767418,16.0501013 L24.7433063,16.0535982 L24.662295,16.0631782 L24.613779,16.068035 C24.5300964,16.0758496 24.4458175,16.080819 24.3613239,16.0829431 L24.295361,16.0840231 L24.2378011,16.0840231 L24.1769159,16.083066 L24.1396266,16.0815108 L24.100709,16.0804835 L24.0682402,16.0784465 L24.1144784,16.0810643 L24.0346347,16.0765313 L24.0154301,16.0753822 L23.9598691,16.0716334 L23.9187708,16.0680439 C23.8914939,16.0655094 23.8642646,16.0626756 23.8370958,16.0595423 L23.7780001,16.051 L23.7643085,16.0501013 L23.6987902,16.0409146 L23.6420427,16.0316946 L23.5791148,16.0203682 C23.560729,16.0168872 23.5423918,16.0132636 23.5239103,16.009457 C23.1732022,15.9371995 22.8419505,15.8125039 22.5603328,15.6353699 C22.3496398,15.5028466 22.1843995,15.3520868 22.0648176,15.1911685 L21.9897669,15.0792361 L21.9790116,15.0606179 L21.9602504,15.0267657 L21.9382766,14.9840933 L21.9253017,14.9557641 C21.9006523,14.9011373 21.880798,14.8458481 21.8657454,14.7901707 L21.8467689,14.7063976 L21.8402864,14.6657847 L21.8381181,14.6512291 L21.8353091,14.6253817 L21.831407,14.5765357 L21.830385,14.5364324 L21.830385,14.5251097 L21.8311226,14.4941815 L21.8326564,14.4599905 L21.8416888,14.3779384 L21.8598976,14.2868559 L21.8866874,14.1965427 L21.8909345,14.1839168 L21.8994958,14.1615008 L21.9061319,14.1446878 L21.9136171,14.126633 L21.9327316,14.0839427 L21.938808,14.0715224 C22.0467416,13.8510861 22.2353648,13.6438627 22.5051541,13.4685526 C22.7627252,13.301182 23.0669941,13.1806283 23.3925031,13.1068916 L23.4670001,13.09 L23.4817097,13.0876267 L23.4710001,13.089 L23.4984197,13.083752 L23.5082747,13.0818146 L23.5221297,13.0798772 L23.5520905,13.0747722 L23.5246363,13.0797351 L23.5614613,13.0730927 L23.5857954,13.0689903 L23.6218247,13.062441 L23.6280001,13.061 L23.6783459,13.0548429 L23.8115662,13.0388027 C23.8561753,13.0342675 23.9009675,13.0305436 23.9458846,13.027631 L24.0688997,13.021765 L24.1088084,13.0206344 L24.15514,13.0198498 L24.2222536,13.0198498 Z" id="形状结合" fill-opacity="0.65"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
style:
color: var(--palette-blue-6)
fontSize: 80px
```
102 changes: 61 additions & 41 deletions bricks/icons/src/shared/SvgCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,65 @@ interface ResolveIconOptions {
replaceSource?(source: string): string;
}

export function constructSvgElement(
content: string,
retryable: false,
options?: ResolveIconOptions
): SVGSVGElement | null;
export function constructSvgElement(
content: string,
retryable: true,
options?: ResolveIconOptions
): SVGResult;
export function constructSvgElement(
content: string,
retryable: boolean,
options?: ResolveIconOptions
): SVGResult | null {
const div = document.createElement("div");
div.innerHTML = content;

const svg = div.firstElementChild;
if (svg?.tagName?.toLowerCase() !== "svg")
return retryable ? CACHEABLE_ERROR : null;

if (!parser) parser = new DOMParser();
const doc = parser.parseFromString(svg.outerHTML, "text/html");

const svgEl = doc.body.querySelector("svg");
if (!svgEl) return retryable ? CACHEABLE_ERROR : null;

const titles = svgEl.querySelectorAll("title");
for (const title of titles) {
title.remove();
}

if (options?.currentColor) {
const colorProps = [
"color",
"fill",
"stroke",
"stop-color",
"flood-color",
"lighting-color",
];
for (const prop of colorProps) {
const elements = svgEl.querySelectorAll(
`[${prop}]:not([${prop}="none"])`
);
for (const e of elements) {
if (!belongToMask(e, svgEl)) {
e.setAttribute(prop, "currentColor");
}
}
}
}

svgEl.setAttribute("width", "1em");
svgEl.setAttribute("height", "1em");
return document.adoptNode(svgEl);
}

/** Given a URL, this function returns the resulting SVG element or an appropriate error symbol. */
async function resolveIcon(
url: string,
Expand All @@ -29,47 +88,8 @@ async function resolveIcon(
}

try {
const div = document.createElement("div");
div.innerHTML = await fileData.text();

const svg = div.firstElementChild;
if (svg?.tagName?.toLowerCase() !== "svg") return CACHEABLE_ERROR;

if (!parser) parser = new DOMParser();
const doc = parser.parseFromString(svg.outerHTML, "text/html");

const svgEl = doc.body.querySelector("svg");
if (!svgEl) return CACHEABLE_ERROR;

const titles = svgEl.querySelectorAll("title");
for (const title of titles) {
title.remove();
}

if (options?.currentColor) {
const colorProps = [
"color",
"fill",
"stroke",
"stop-color",
"flood-color",
"lighting-color",
];
for (const prop of colorProps) {
const elements = svgEl.querySelectorAll(
`[${prop}]:not([${prop}="none"])`
);
for (const e of elements) {
if (!belongToMask(e, svgEl)) {
e.setAttribute(prop, "currentColor");
}
}
}
}

svgEl.setAttribute("width", "1em");
svgEl.setAttribute("height", "1em");
return document.adoptNode(svgEl);
const content = await fileData.text();
return constructSvgElement(content, true, options);
} catch {
return CACHEABLE_ERROR;
}
Expand Down
75 changes: 67 additions & 8 deletions bricks/icons/src/svg-icon/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import { describe, test, expect } from "@jest/globals";
import "./index.js";
import type { SvgIcon } from "./index.js";

(global as any).fetch = jest.fn(() =>
Promise.resolve({
ok: true,
text: () =>
Promise.resolve(
`<?xml version="1.0" encoding="UTF-8"?>
const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
<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">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-1804.000000, -58.000000)" stroke="#595959">
Expand All @@ -17,8 +12,12 @@ import type { SvgIcon } from "./index.js";
</g>
</g>
</g>
</svg>`.replace(/>\s+</g, "><")
),
</svg>`.replace(/>\s+</g, "><");

(global as any).fetch = jest.fn(() =>
Promise.resolve({
ok: true,
text: () => Promise.resolve(svgContent),
})
);

Expand Down Expand Up @@ -105,4 +104,64 @@ describe("eo-svg-icon", () => {
(element as any)._render();
expect(fetch).not.toBeCalled();
});

test("use svg content", async () => {
const element = document.createElement("eo-svg-icon") as SvgIcon;
element.svgContent = svgContent;

expect(element.shadowRoot).toBeFalsy();
document.body.appendChild(element);
expect(element.shadowRoot).toBeTruthy();
await (global as any).flushPromises();
expect(element.shadowRoot?.childNodes).toMatchInlineSnapshot(`
NodeList [
<style>
icons.shadow.css
</style>,
<svg
height="1em"
version="1.1"
viewBox="0 0 15 17"
width="1em"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g
fill="none"
fill-rule="evenodd"
stroke="none"
stroke-width="1"
>
<g
stroke="currentColor"
transform="translate(-1804.000000, -58.000000)"
>
<g
transform="translate(1805.000000, 59.000000)"
>
<circle
cx="6.512"
cy="3.552"
r="3.552"
/>
<path
d="M10.448,8.184 Z"
stroke-linecap="square"
/>
</g>
</g>
</g>
</svg>,
]
`);
document.body.removeChild(element);
expect(element.shadowRoot?.childNodes.length).toBe(0);

// Re-connect
document.body.appendChild(element);
await (global as any).flushPromises();
expect(element.shadowRoot?.childNodes.length).toBe(2);
document.body.removeChild(element);
expect(element.shadowRoot?.childNodes.length).toBe(0);
});
});
22 changes: 15 additions & 7 deletions bricks/icons/src/svg-icon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type EventEmitter,
} from "@next-core/element";
import { wrapLocalBrick } from "@next-core/react-element";
import { getIcon } from "../shared/SvgCache.js";
import { constructSvgElement, getIcon } from "../shared/SvgCache.js";
import { getImageUrl } from "../shared/getImageUrl.js";
import type { IconEvents, IconEventsMapping } from "../shared/interfaces.js";
import sharedStyleText from "../shared/icons.shadow.css";
Expand All @@ -14,6 +14,7 @@ const { defineElement, property, event } = createDecorators();

export interface SvgIconProps {
imgSrc?: string;
svgContent?: string;
noPublicRoot?: boolean;
}

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

@property() accessor svgContent: string | undefined;

@property({
type: Boolean,
})
Expand Down Expand Up @@ -60,13 +63,18 @@ class SvgIcon extends NextElement implements SvgIconProps {
if (!this.isConnected || !this.shadowRoot) {
return;
}
const url = getImageUrl(this.imgSrc, this.noPublicRoot);

const svg = await getIcon(url, { currentColor: true });
if (url !== getImageUrl(this.imgSrc, this.noPublicRoot)) {
// The icon has changed during `await getIcon(...)`
return;
let svg: SVGElement | null = null;
if (this.svgContent) {
svg = constructSvgElement(this.svgContent, false, { currentColor: true });
} else {
const url = getImageUrl(this.imgSrc, this.noPublicRoot);
svg = await getIcon(url, { currentColor: true });
if (url !== getImageUrl(this.imgSrc, this.noPublicRoot)) {
// The icon has changed during `await getIcon(...)`
return;

Check warning on line 74 in bricks/icons/src/svg-icon/index.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L74 was not covered by tests
}
}

// Currently React can't render mixed React Component and DOM nodes which are siblings,
// so we manually construct the DOM.
const nodes: Node[] = [];
Expand Down

0 comments on commit 7831bfb

Please sign in to comment.