diff --git a/bricks/icons/docs/eo-svg-icon.md b/bricks/icons/docs/eo-svg-icon.md
index 02ee1c94a..08b816cf6 100644
--- a/bricks/icons/docs/eo-svg-icon.md
+++ b/bricks/icons/docs/eo-svg-icon.md
@@ -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: |
+
+ style:
+ color: var(--palette-blue-6)
+ fontSize: 80px
+```
diff --git a/bricks/icons/src/shared/SvgCache.ts b/bricks/icons/src/shared/SvgCache.ts
index 831fc654d..c5478d31f 100644
--- a/bricks/icons/src/shared/SvgCache.ts
+++ b/bricks/icons/src/shared/SvgCache.ts
@@ -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,
@@ -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;
}
diff --git a/bricks/icons/src/svg-icon/index.spec.ts b/bricks/icons/src/svg-icon/index.spec.ts
index a48fd68c6..95f3c9454 100644
--- a/bricks/icons/src/svg-icon/index.spec.ts
+++ b/bricks/icons/src/svg-icon/index.spec.ts
@@ -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(
- `
+const svgContent = `
`.replace(/>\s+<")
- ),
+`.replace(/>\s+<");
+
+(global as any).fetch = jest.fn(() =>
+ Promise.resolve({
+ ok: true,
+ text: () => Promise.resolve(svgContent),
})
);
@@ -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 [
+ ,
+ ,
+ ]
+ `);
+ 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);
+ });
});
diff --git a/bricks/icons/src/svg-icon/index.ts b/bricks/icons/src/svg-icon/index.ts
index 58bfbfb68..8c53fb823 100644
--- a/bricks/icons/src/svg-icon/index.ts
+++ b/bricks/icons/src/svg-icon/index.ts
@@ -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";
@@ -14,6 +14,7 @@ const { defineElement, property, event } = createDecorators();
export interface SvgIconProps {
imgSrc?: string;
+ svgContent?: string;
noPublicRoot?: boolean;
}
@@ -23,6 +24,8 @@ class SvgIcon extends NextElement implements SvgIconProps {
/** 图标地址 */
@property() accessor imgSrc: string | undefined;
+ @property() accessor svgContent: string | undefined;
+
@property({
type: Boolean,
})
@@ -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;
+ }
}
+
// Currently React can't render mixed React Component and DOM nodes which are siblings,
// so we manually construct the DOM.
const nodes: Node[] = [];
diff --git a/shared/icons/README.md b/shared/icons/README.md
index b4a9c04e6..8d10a77c7 100644
--- a/shared/icons/README.md
+++ b/shared/icons/README.md
@@ -17,3 +17,9 @@ https://bricks.js.org/icons/
- 默认都是「字体图标」,和字体一样,本身不提供颜色,而是跟随页面文本颜色的设定(原始 SVG 文件仅使用一种颜色);
- 需要固定原始颜色(通常会使用多种颜色)的图标,需要放置在特定的分类下(以 `colored-` 开头),这些图标保留图标原始颜色,不能由消费端指定其他颜色;
- 可以让一个图标中的一部分内容带有透明度设置,变相实现类似「双色」的能力(一深一浅)。
+
+---
+
+不确定你的 SVG 图片转换为图标后是否符合预期?点击以下链接并导入 SVG 图片提前在线预览:
+
+https://bricks.js.org/playground/?mode=yaml&example=icons%2Feo-svg-icon%2Fsvgcontent&collapsed=1