Skip to content

Commit 5813141

Browse files
authored
fix: support configurable icon rendering strategy and fix complex ico… (#3328)
* fix: support configurable icon rendering strategy and fix complex icon misalignment * chore: 修复单测
1 parent 0e704bb commit 5813141

File tree

9 files changed

+55
-18
lines changed

9 files changed

+55
-18
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
🐛 Bugfix
1919
<!-- 如果没有相关的 issue 需要关闭,请删除这一行 -->
20-
- [ ] Solve the issue and close #1904
20+
- [ ] Solve the issue and close
2121

2222
🔧 Chore
2323

@@ -54,10 +54,6 @@
5454
<!-- If there is a related Issue/PR link -->
5555
<!-- 如果有相关的 Issue/PR 链接,请关联上 -->
5656

57-
<!-- close #0 -->
58-
<!-- ref #0 -->
59-
<!-- fix #0 -->
60-
6157
### 🔍 Self-Check before the merge
6258

6359
<!-- Please add test case, docs, and demos -->

packages/s2-core/__tests__/unit/common/icons/gui-icon-spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ describe('GuiIcon Tests', () => {
152152
y: 10,
153153
width: 16,
154154
height: 16,
155+
iconStrategy: 'path',
155156
});
156157

157158
// Path 模式下应该有 iconPathShapes
@@ -173,6 +174,7 @@ describe('GuiIcon Tests', () => {
173174
y: 0,
174175
width: 16,
175176
height: 16,
177+
iconStrategy: 'path',
176178
});
177179

178180
expect(icon.iconPathShapes.length).toBeGreaterThan(0);
@@ -190,6 +192,7 @@ describe('GuiIcon Tests', () => {
190192
width: 16,
191193
height: 16,
192194
cursor: 'pointer',
195+
iconStrategy: 'path',
193196
});
194197

195198
expect(icon.iconPathShapes.length).toBeGreaterThan(0);
@@ -231,6 +234,7 @@ describe('GuiIcon Tests', () => {
231234
width: 16,
232235
height: 16,
233236
fill: 'red',
237+
iconStrategy: 'path',
234238
});
235239

236240
expect(icon.iconPathShapes.length).toBeGreaterThan(0);
@@ -251,6 +255,7 @@ describe('GuiIcon Tests', () => {
251255
y: 0,
252256
width: 16,
253257
height: 16,
258+
iconStrategy: 'path',
254259
});
255260

256261
expect(icon.iconPathShapes.length).toBeGreaterThan(0);
@@ -262,6 +267,7 @@ describe('GuiIcon Tests', () => {
262267
y: 0,
263268
width: 16,
264269
height: 16,
270+
iconStrategy: 'path',
265271
});
266272

267273
expect(icon.name).toBe('Minus');
@@ -275,6 +281,7 @@ describe('GuiIcon Tests', () => {
275281
y: 0,
276282
width: 16,
277283
height: 16,
284+
iconStrategy: 'path',
278285
});
279286

280287
icon.updatePosition({ x: 50, y: 100 });
@@ -293,6 +300,7 @@ describe('GuiIcon Tests', () => {
293300
y: 0,
294301
width: 16,
295302
height: 16,
303+
iconStrategy: 'path',
296304
});
297305

298306
// 隐藏

packages/s2-core/src/cell/base-cell.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
SHAPE_ATTRS_MAP,
3131
SHAPE_STYLE_MAP,
3232
} from '../common/constant';
33-
import type { GuiIcon } from '../common/icons/gui-icon';
33+
import type { GuiIcon, GuiIconCfg } from '../common/icons/gui-icon';
3434
import {
3535
CellBorderPosition,
3636
CellClipBox,
@@ -766,12 +766,13 @@ export abstract class BaseCell<T extends SimpleBBox> extends Group {
766766
const position = this.getIconPosition();
767767
const { size } = this.getStyle()!.icon!;
768768

769-
const iconCfg = {
769+
const iconCfg: GuiIconCfg = {
770770
...position,
771771
name: attrs?.name!,
772772
width: size,
773773
height: size,
774774
fill: attrs?.fill,
775+
iconStrategy: this.spreadsheet.options.csp?.iconStrategy,
775776
};
776777

777778
if (this.conditionIconShape) {

packages/s2-core/src/cell/col-cell.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,7 @@ export class ColCell extends HeaderCell<ColHeaderConfig> {
588588
...iconConfig,
589589
name: 'ExpandColIcon',
590590
cursor: 'pointer',
591+
iconStrategy: this.spreadsheet.options.csp?.iconStrategy,
591592
});
592593

593594
icon.addEventListener('click', () => {

packages/s2-core/src/cell/corner-cell.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export class CornerCell extends HeaderCell<CornerHeaderConfig> {
104104
width: size,
105105
height: size,
106106
fill,
107+
iconStrategy: this.spreadsheet.options.csp?.iconStrategy,
107108
},
108109
isCollapsed,
109110
onClick: () => {

packages/s2-core/src/cell/header-cell.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ export abstract class HeaderCell<
251251
name,
252252
x,
253253
y,
254+
iconStrategy: this.spreadsheet.options.csp?.iconStrategy,
254255
});
255256

256257
icon.toggleVisibility(!defaultHide);
@@ -321,13 +322,14 @@ export abstract class HeaderCell<
321322
const y = iconPosition.y;
322323

323324
if (icon.isConditionIcon) {
324-
const iconCfg = {
325+
const iconCfg: GuiIconCfg = {
325326
x,
326327
y,
327328
name: icon.name,
328329
width: size,
329330
height: size,
330331
fill: icon.fill,
332+
iconStrategy: this.spreadsheet.options.csp?.iconStrategy,
331333
};
332334

333335
if (this.conditionIconShape) {

packages/s2-core/src/cell/row-cell.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export class RowCell extends HeaderCell<RowHeaderConfig> {
224224
width: size,
225225
height: size,
226226
fill,
227+
iconStrategy: this.spreadsheet.options.csp?.iconStrategy,
227228
},
228229
isCollapsed,
229230
onClick: () => {

packages/s2-core/src/common/icons/gui-icon.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,28 @@ const PathDataCache: Record<string, ParsedSvgData> = {};
2525
export interface GuiIconCfg extends Omit<ImageStyleProps, 'fill'> {
2626
readonly name: string;
2727
readonly fill?: string | null;
28+
/**
29+
* 图标渲染策略
30+
* @see S2BasicOptions.csp
31+
*/
32+
readonly iconStrategy?: 'blob' | 'path';
2833
}
2934

3035
/**
3136
* 从 SVG 字符串中解析 viewBox 和 path 数据
3237
* @see https://github.com/antvis/S2/issues/3125
3338
*/
3439
function parseSvgPaths(svg: string): ParsedSvgData | null {
40+
// 如果 SVG 包含 transform, g, rect 等不支持的复杂标签或属性,暂不使用 Path 模式
41+
// 避免渲染错位
42+
if (
43+
/transform|translate|scale|rotate|<g|<rect|<circle|<ellipse|<line|<polyline|<polygon|<text|<tspan/i.test(
44+
svg,
45+
)
46+
) {
47+
return null;
48+
}
49+
3550
// 提取 viewBox
3651
const viewBoxMatch = svg.match(/viewBox=["']([^"']+)["']/);
3752

@@ -289,20 +304,20 @@ export class GuiIcon extends Group {
289304
public isOnlineLink = (src: string) => /^(?:https?:)?(?:\/\/)/.test(src);
290305

291306
private render() {
292-
const { name, fill } = this.cfg;
307+
const { name, fill, iconStrategy } = this.cfg;
293308

294-
// 优先尝试 Path 模式 (完全绕过 CSP 限制)
295-
if (this.tryRenderAsPath(name, fill)) {
309+
// 如果指定了 path 模式,优先尝试 Path 模式 (完全绕过 CSP 限制)
310+
if (iconStrategy === 'path' && this.tryRenderAsPath(name, fill)) {
296311
this.usePathMode = true;
297312

298313
return;
299314
}
300315

301-
// 回退到 Image 模式
316+
// 默认或失败时回退到 Image 模式
302317
this.usePathMode = false;
303318
const attrs = clone(this.cfg);
304319
const image = new CustomImage(GuiIcon.type, {
305-
style: omit(attrs, 'fill'),
320+
style: omit(attrs, ['fill', 'iconStrategy']),
306321
});
307322

308323
this.iconImageShape = image;
@@ -312,7 +327,7 @@ export class GuiIcon extends Group {
312327
public reRender(cfg: GuiIconCfg) {
313328
this.name = cfg.name;
314329
this.cfg = cfg;
315-
const { name, fill } = this.cfg;
330+
const { name, fill, iconStrategy } = this.cfg;
316331

317332
// 清除旧的渲染
318333
if (this.usePathMode) {
@@ -323,8 +338,8 @@ export class GuiIcon extends Group {
323338
this.iconPathShapes = [];
324339
}
325340

326-
// 优先尝试 Path 模式
327-
if (this.tryRenderAsPath(name, fill)) {
341+
// 如果指定了 path 模式,优先尝试 Path 模式
342+
if (iconStrategy === 'path' && this.tryRenderAsPath(name, fill)) {
328343
this.usePathMode = true;
329344

330345
return;
@@ -336,11 +351,11 @@ export class GuiIcon extends Group {
336351

337352
if (!this.iconImageShape) {
338353
this.iconImageShape = new CustomImage(GuiIcon.type, {
339-
style: omit(attrs, 'fill'),
354+
style: omit(attrs, ['fill', 'iconStrategy']),
340355
});
341356
} else {
342357
this.iconImageShape.imgType = GuiIcon.type;
343-
batchSetStyle(this.iconImageShape, omit(attrs, 'fill'));
358+
batchSetStyle(this.iconImageShape, omit(attrs, ['fill', 'iconStrategy']));
344359
}
345360

346361
this.setImageAttrs({ name, fill });

packages/s2-core/src/common/interface/s2Options.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,18 @@ export interface S2BasicOptions<
312312
*/
313313
experimentalReuseCell?: boolean;
314314
};
315+
316+
/**
317+
* 安全策略配置
318+
*/
319+
csp?: {
320+
/**
321+
* 图标渲染策略
322+
* - blob: 使用 Blob URL 渲染 (默认, 兼容性好, 但严苛 CSP 环境可能报错)
323+
* - path: 优先使用矢量路径渲染 (CSP 友好, 但仅支持简单无变换图标)
324+
*/
325+
iconStrategy?: 'blob' | 'path';
326+
};
315327
}
316328

317329
// 设备,pc || mobile

0 commit comments

Comments
 (0)