mail-editor-pancake 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -4
- package/package.json +1 -1
- package/packages/blocks/dist/index.js +275 -263
- package/packages/blocks/dist/index.js.map +1 -1
- package/packages/blocks/package.json +1 -3
- package/packages/blocks/src/button.ts +1 -1
- package/packages/blocks/src/hero.ts +6 -3
- package/packages/blocks/src/i18n.ts +10 -11
- package/packages/blocks/src/image.ts +2 -1
- package/packages/blocks/src/index.ts +5 -3
- package/packages/blocks/src/social.ts +3 -3
- package/packages/blocks/src/socialGroup.ts +4 -4
- package/packages/blocks/src/socialShared.ts +23 -3
- package/packages/blocks/src/text.ts +1 -2
- package/packages/blocks/vite.config.ts +1 -1
- package/packages/core/dist/index.d.ts +96 -1
- package/packages/core/dist/index.js +4346 -3591
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/style.css +1 -1
- package/packages/core/package.json +1 -3
- package/packages/core/src/editor/BlockCodeModal.ts +6 -7
- package/packages/core/src/editor/Canvas.ts +558 -20
- package/packages/core/src/editor/ColorPickerPopover.ts +2 -2
- package/packages/core/src/editor/Editor.ts +379 -39
- package/packages/core/src/editor/ExportModal.ts +7 -9
- package/packages/core/src/editor/FocusBreadcrumb.ts +3 -3
- package/packages/core/src/editor/ImportDocModal.ts +50 -7
- package/packages/core/src/editor/InlineEditor.ts +43 -39
- package/packages/core/src/editor/LeftPanel.ts +2 -4
- package/packages/core/src/editor/Modal.ts +1 -1
- package/packages/core/src/editor/PreviewModal.ts +3 -8
- package/packages/core/src/editor/RichTextToolbar.ts +26 -15
- package/packages/core/src/editor/RightPanel.ts +141 -125
- package/packages/core/src/editor/SourceView.ts +5 -3
- package/packages/core/src/editor/Topbar.ts +79 -34
- package/packages/core/src/editor/VariablePickerPanel.ts +3 -5
- package/packages/core/src/editor/imageAssets.ts +1 -1
- package/packages/core/src/editor/styles.css +258 -86
- package/packages/core/src/i18n/index.ts +139 -41
- package/packages/core/src/index.ts +11 -1
- package/packages/core/src/renderer/index.ts +3 -6
- package/packages/core/src/renderer/mjml.ts +4 -9
- package/packages/core/src/store/store.ts +2 -6
- package/packages/core/src/types.ts +3 -3
- package/packages/core/src/utils/accentColor.ts +3 -3
- package/packages/core/src/utils/docClipboard.ts +107 -2
- package/packages/core/src/utils/dynamicVariantHtml.ts +1 -1
- package/packages/core/src/utils/emailListStyles.ts +1 -1
- package/packages/core/src/utils/fontWeightSteps.ts +7 -6
- package/packages/core/src/utils/html.ts +3 -2
- package/packages/core/src/utils/inlineListEditing.ts +19 -35
- package/packages/core/src/utils/link.ts +3 -6
- package/packages/core/src/utils/lockedMjml.ts +1 -1
- package/packages/core/src/utils/modalSize.ts +4 -3
- package/packages/core/src/utils/richHtmlEmpty.ts +3 -1
- package/packages/core/src/variables/index.ts +4 -5
- package/packages/core/vite.config.ts +1 -1
- package/playground/vanilla/index.html +17 -0
- package/playground/vanilla/src/main.ts +152 -123
- package/playground/vanilla/vite.config.ts +5 -2
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import Sortable from 'sortablejs';
|
|
2
|
+
import type { SimpleMailT } from '../i18n';
|
|
2
3
|
import type { Registry } from '../registry/registry';
|
|
3
4
|
import {
|
|
5
|
+
type Store,
|
|
4
6
|
createSection,
|
|
5
7
|
findBlockLocation,
|
|
6
8
|
findSection,
|
|
7
9
|
pruneSectionIfEmpty,
|
|
8
|
-
type Store,
|
|
9
10
|
} from '../store/store';
|
|
10
11
|
import type {
|
|
11
12
|
Block,
|
|
13
|
+
BlockDefinition,
|
|
14
|
+
BlockSchemaField,
|
|
12
15
|
Column,
|
|
13
16
|
EditorUiOptions,
|
|
14
17
|
EmailDoc,
|
|
@@ -18,21 +21,19 @@ import type {
|
|
|
18
21
|
SectionAttrs,
|
|
19
22
|
SectionLayout,
|
|
20
23
|
} from '../types';
|
|
21
|
-
import { getSectionDynamicVariantKey } from '../utils/dynamicVariantSection';
|
|
22
24
|
import { blockButtonWidthCss, docContentWidthCss } from '../utils/contentWidth';
|
|
25
|
+
import { clear, escapeHtml, h } from '../utils/dom';
|
|
26
|
+
import { getSectionDynamicVariantKey } from '../utils/dynamicVariantSection';
|
|
23
27
|
import { globalListIndentCssVarValue } from '../utils/emailListStyles';
|
|
24
28
|
import { normalizeFontWeightStep } from '../utils/fontWeightSteps';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
defaultPaddingForLockedBlock,
|
|
28
|
-
resolveLockedMjmlCanvasContent,
|
|
29
|
-
} from '../utils/lockedMjml';
|
|
30
|
-
import { layoutHumanLabel } from '../utils/sectionLayout';
|
|
29
|
+
import { defaultPaddingForLockedBlock, resolveLockedMjmlCanvasContent } from '../utils/lockedMjml';
|
|
31
30
|
import { paletteDropHasSectionLayout, resolvePaletteDropResult } from '../utils/paletteDrop';
|
|
31
|
+
import { layoutHumanLabel } from '../utils/sectionLayout';
|
|
32
32
|
import { BlockCodeModal } from './BlockCodeModal';
|
|
33
|
+
import { openImageGalleryModal } from './ImageGalleryModal';
|
|
33
34
|
import { InlineEditor, type SelectionState } from './InlineEditor';
|
|
34
35
|
import type { RichTextToolbar } from './RichTextToolbar';
|
|
35
|
-
import type {
|
|
36
|
+
import type { ImageAssetsHandlers } from './imageAssets';
|
|
36
37
|
|
|
37
38
|
export interface CanvasOptions {
|
|
38
39
|
store: Store;
|
|
@@ -53,6 +54,16 @@ export interface CanvasOptions {
|
|
|
53
54
|
layerRoot: HTMLElement;
|
|
54
55
|
/** 与 MailEditor.opts.ui 对齐 */
|
|
55
56
|
ui?: EditorUiOptions;
|
|
57
|
+
/** 与 MailEditor.opts.imageAssets 对齐:启用图库(showGallery)时,双击图片类块可直接打开图库换图 */
|
|
58
|
+
imageAssets?: ImageAssetsHandlers;
|
|
59
|
+
/** Section / Block 工具条「复制设计稿」回调(写局部 JSON 到剪贴板,供其他画布追加) */
|
|
60
|
+
onCopySelectionDesign?: (target: { sectionId: string } | { blockId: string }) => void;
|
|
61
|
+
/** 「选择节」批量模式动作(由 Editor 实现:写剪贴板 / 下载 JSON / 批量删除);块为散选、节为整选,二者可混合 */
|
|
62
|
+
onSectionSelectCopy?: (sectionIds: string[], blockIds: string[]) => void;
|
|
63
|
+
onSectionSelectExport?: (sectionIds: string[], blockIds: string[]) => void;
|
|
64
|
+
onSectionSelectRemove?: (sectionIds: string[], blockIds: string[]) => void;
|
|
65
|
+
/** 进入 / 退出节选择模式时通知宿主同步 UI(如顶栏按钮高亮);画布内「完成」按钮与 ⌘/Ctrl+点击直达均会触发 */
|
|
66
|
+
onSectionSelectModeChange?: (active: boolean) => void;
|
|
56
67
|
t: SimpleMailT;
|
|
57
68
|
}
|
|
58
69
|
|
|
@@ -86,6 +97,26 @@ export class Canvas {
|
|
|
86
97
|
private clearSelectionGestureTracking = new AbortController();
|
|
87
98
|
private pointerGestureStartedInInlineEdit = false;
|
|
88
99
|
private blockCodeModal: BlockCodeModal;
|
|
100
|
+
/** 节选择模式(批量复制 / 导出 / 删除):点击 Section 勾选而非单选 */
|
|
101
|
+
private sectionSelectActive = false;
|
|
102
|
+
/** 完整选中的节(点击节空白 / 选择圈命中) */
|
|
103
|
+
private checkedSectionIds = new Set<string>();
|
|
104
|
+
/** 散选的块(所属节未被完整选中;不变式由 _normalizeCheckedSets 维护) */
|
|
105
|
+
private checkedBlockIds = new Set<string>();
|
|
106
|
+
/**
|
|
107
|
+
* 区间锚点:最近一次点击的命中(节或块)。Shift+点击以它与目标为界做同类区间:
|
|
108
|
+
* 节↔节 = 节区间;同列块↔块 = 列内区间;跨类 / 跨列退化为单点切换。
|
|
109
|
+
*/
|
|
110
|
+
private sectionCheckAnchor: {
|
|
111
|
+
kind: 'section' | 'block';
|
|
112
|
+
id: string;
|
|
113
|
+
sectionId: string;
|
|
114
|
+
columnIndex: number;
|
|
115
|
+
} | null = null;
|
|
116
|
+
private sectionSelectBar: HTMLElement | null = null;
|
|
117
|
+
private sectionSelectCountEl: HTMLElement | null = null;
|
|
118
|
+
private sectionSelectAllBtn: HTMLButtonElement | null = null;
|
|
119
|
+
private sectionSelectActionBtns: HTMLButtonElement[] = [];
|
|
89
120
|
|
|
90
121
|
constructor(opts: CanvasOptions) {
|
|
91
122
|
this.opts = opts;
|
|
@@ -101,6 +132,10 @@ export class Canvas {
|
|
|
101
132
|
});
|
|
102
133
|
|
|
103
134
|
this._bindDesignModeLinkSuppression();
|
|
135
|
+
// 节选择模式下 Shift+点击 = 区间勾选:阻止浏览器默认的 Shift 扩展文本选区
|
|
136
|
+
this.inner.addEventListener('mousedown', (e: MouseEvent) => {
|
|
137
|
+
if (this.sectionSelectActive && e.shiftKey) e.preventDefault();
|
|
138
|
+
});
|
|
104
139
|
if (opts.clearSelectionOnCanvasMargin) {
|
|
105
140
|
this._bindClearSelectionGestureTracking();
|
|
106
141
|
this._bindClearSelectionOnCanvasWhitespace();
|
|
@@ -179,6 +214,7 @@ export class Canvas {
|
|
|
179
214
|
destroy() {
|
|
180
215
|
this.linkNavSuppression.abort();
|
|
181
216
|
this.clearSelectionGestureTracking.abort();
|
|
217
|
+
this.exitSectionSelectMode();
|
|
182
218
|
this._exitEditing(false);
|
|
183
219
|
this._destroySortables();
|
|
184
220
|
this.blockCodeModal.destroy();
|
|
@@ -244,6 +280,7 @@ export class Canvas {
|
|
|
244
280
|
if (!this.el.contains(t)) return;
|
|
245
281
|
if (this.inner.contains(t)) return;
|
|
246
282
|
if (this.addBar.contains(t)) return;
|
|
283
|
+
if (this.sectionSelectBar?.contains(t)) return;
|
|
247
284
|
if (this._shouldSuppressClearSelectionFromTextDrag()) return;
|
|
248
285
|
this.commitInlineEdit();
|
|
249
286
|
this.opts.store.setSelection(null);
|
|
@@ -300,11 +337,7 @@ export class Canvas {
|
|
|
300
337
|
);
|
|
301
338
|
|
|
302
339
|
if (!doc.sections.length) {
|
|
303
|
-
this.inner.append(
|
|
304
|
-
h('div', { class: 'sm-empty-doc' }, [
|
|
305
|
-
this.opts.t('canvas.emptyHint'),
|
|
306
|
-
]),
|
|
307
|
-
);
|
|
340
|
+
this.inner.append(h('div', { class: 'sm-empty-doc' }, [this.opts.t('canvas.emptyHint')]));
|
|
308
341
|
} else {
|
|
309
342
|
for (let si = 0; si < doc.sections.length; si++) {
|
|
310
343
|
this.inner.append(this._renderSection(doc.sections[si], doc, si));
|
|
@@ -330,6 +363,11 @@ export class Canvas {
|
|
|
330
363
|
});
|
|
331
364
|
this.sortableRefs.push(top);
|
|
332
365
|
|
|
366
|
+
if (this.sectionSelectActive) {
|
|
367
|
+
this._setSortablesDisabled(true);
|
|
368
|
+
this._syncSectionChecks();
|
|
369
|
+
}
|
|
370
|
+
|
|
333
371
|
this._syncSelection();
|
|
334
372
|
}
|
|
335
373
|
|
|
@@ -340,7 +378,9 @@ export class Canvas {
|
|
|
340
378
|
}px ${a.paddingLeft ?? 0}px`;
|
|
341
379
|
|
|
342
380
|
const layoutShort =
|
|
343
|
-
section.layout === '1'
|
|
381
|
+
section.layout === '1'
|
|
382
|
+
? this.opts.t('rightPanel.layout.oneColumn')
|
|
383
|
+
: layoutHumanLabel(section.layout);
|
|
344
384
|
const dvKey =
|
|
345
385
|
this.opts.ui?.enableDynamicVariantKey === true
|
|
346
386
|
? getSectionDynamicVariantKey(section)
|
|
@@ -354,7 +394,9 @@ export class Canvas {
|
|
|
354
394
|
|
|
355
395
|
const sw = blockButtonWidthCss(a.width);
|
|
356
396
|
const box =
|
|
357
|
-
sw != null
|
|
397
|
+
sw != null
|
|
398
|
+
? `max-width:${sw};width:100%;margin-left:auto;margin-right:auto;box-sizing:border-box;`
|
|
399
|
+
: '';
|
|
358
400
|
|
|
359
401
|
const host = h('div', {
|
|
360
402
|
class: 'sm-section-host',
|
|
@@ -392,6 +434,21 @@ export class Canvas {
|
|
|
392
434
|
},
|
|
393
435
|
[iconCopy()],
|
|
394
436
|
),
|
|
437
|
+
this.opts.onCopySelectionDesign
|
|
438
|
+
? h(
|
|
439
|
+
'button',
|
|
440
|
+
{
|
|
441
|
+
class: 'sm-tool-btn',
|
|
442
|
+
type: 'button',
|
|
443
|
+
title: this.opts.t('canvas.copySectionDesignTitle'),
|
|
444
|
+
onclick: (e: Event) => {
|
|
445
|
+
e.stopPropagation();
|
|
446
|
+
this.opts.onCopySelectionDesign?.({ sectionId: section.id });
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
[iconCopyDesign()],
|
|
450
|
+
)
|
|
451
|
+
: null,
|
|
395
452
|
h(
|
|
396
453
|
'button',
|
|
397
454
|
{
|
|
@@ -419,10 +476,27 @@ export class Canvas {
|
|
|
419
476
|
|
|
420
477
|
wrap.append(cols);
|
|
421
478
|
|
|
422
|
-
|
|
479
|
+
const check = h('div', { class: 'sm-section-check', 'aria-hidden': 'true' }, [
|
|
480
|
+
iconCheck(),
|
|
481
|
+
iconMinus(),
|
|
482
|
+
]);
|
|
483
|
+
host.append(wrap, toolbar, check);
|
|
423
484
|
|
|
424
485
|
host.addEventListener('click', (e) => {
|
|
486
|
+
if (this.sectionSelectActive) {
|
|
487
|
+
e.stopPropagation();
|
|
488
|
+
if (e.shiftKey) this._shiftClickSection(section.id);
|
|
489
|
+
else this._toggleSectionFull(section.id);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
425
492
|
if ((e.target as HTMLElement).closest('.sm-block')) return;
|
|
493
|
+
/** ⌘/Ctrl+点击节空白:直达节选择模式并勾选该节(macOS ⌘ / Windows·Linux Ctrl) */
|
|
494
|
+
if (e.metaKey || e.ctrlKey) {
|
|
495
|
+
e.preventDefault();
|
|
496
|
+
e.stopPropagation();
|
|
497
|
+
this._enterSectionSelectModeWith(section.id);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
426
500
|
// 文本拖选时在 Section padding 等区域松开会冒泡 click,不应误切成 Section 选中
|
|
427
501
|
if (!e.altKey && this._shouldSuppressSectionSelectFromTextInteraction()) return;
|
|
428
502
|
e.stopPropagation();
|
|
@@ -596,6 +670,21 @@ export class Canvas {
|
|
|
596
670
|
},
|
|
597
671
|
[iconCopy()],
|
|
598
672
|
),
|
|
673
|
+
this.opts.onCopySelectionDesign
|
|
674
|
+
? h(
|
|
675
|
+
'button',
|
|
676
|
+
{
|
|
677
|
+
class: 'sm-tool-btn',
|
|
678
|
+
type: 'button',
|
|
679
|
+
title: this.opts.t('canvas.copyBlockDesignTitle'),
|
|
680
|
+
onclick: (e: Event) => {
|
|
681
|
+
e.stopPropagation();
|
|
682
|
+
this.opts.onCopySelectionDesign?.({ blockId: block.id });
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
[iconCopyDesign()],
|
|
686
|
+
)
|
|
687
|
+
: null,
|
|
599
688
|
h(
|
|
600
689
|
'button',
|
|
601
690
|
{
|
|
@@ -615,6 +704,12 @@ export class Canvas {
|
|
|
615
704
|
el.append(blockToolbar, content);
|
|
616
705
|
|
|
617
706
|
el.addEventListener('click', (e) => {
|
|
707
|
+
if (this.sectionSelectActive) {
|
|
708
|
+
e.stopPropagation();
|
|
709
|
+
if (e.shiftKey) this._shiftClickBlock(section.id, columnIndex, block.id);
|
|
710
|
+
else this._toggleBlockCheck(section.id, columnIndex, block.id);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
618
713
|
if (this.editingBlockId === block.id) return; // 编辑中不抢焦点
|
|
619
714
|
/** Alt(mac Option):穿透选中父级 Section,解决单列内边距为 0 时块铺满无法点到 Section */
|
|
620
715
|
if (e.altKey) {
|
|
@@ -622,6 +717,13 @@ export class Canvas {
|
|
|
622
717
|
this.opts.store.setSelection({ kind: 'section', sectionId: section.id });
|
|
623
718
|
return;
|
|
624
719
|
}
|
|
720
|
+
/** ⌘/Ctrl+点击块:直达选择模式并勾选该块(不带走同节其他块) */
|
|
721
|
+
if (e.metaKey || e.ctrlKey) {
|
|
722
|
+
e.preventDefault();
|
|
723
|
+
e.stopPropagation();
|
|
724
|
+
this._enterSectionSelectModeWithBlock(section.id, columnIndex, block.id);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
625
727
|
e.stopPropagation();
|
|
626
728
|
this.opts.store.setSelection({
|
|
627
729
|
kind: 'block',
|
|
@@ -642,6 +744,18 @@ export class Canvas {
|
|
|
642
744
|
e.stopPropagation();
|
|
643
745
|
this._enterEditing(block);
|
|
644
746
|
});
|
|
747
|
+
} else {
|
|
748
|
+
// 启用图库时:双击图片类块(含 type: 'image' 字段,如 image / logo / hero)直接打开图库换图,
|
|
749
|
+
// 与右栏「图库」按钮共用同一份 adapter 与权限配置
|
|
750
|
+
const galleryField = this._galleryImageField(def);
|
|
751
|
+
if (galleryField) {
|
|
752
|
+
el.addEventListener('dblclick', (e) => {
|
|
753
|
+
if (this.sectionSelectActive) return; // 批量选择模式下双击仍只作勾选
|
|
754
|
+
e.stopPropagation();
|
|
755
|
+
e.preventDefault();
|
|
756
|
+
this._openGalleryForBlock(block, galleryField);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
645
759
|
}
|
|
646
760
|
|
|
647
761
|
return el;
|
|
@@ -677,7 +791,53 @@ export class Canvas {
|
|
|
677
791
|
/* ----------------------------- 内联编辑 -------------------------------- */
|
|
678
792
|
|
|
679
793
|
/** 空文档时:在画布白底区域双击,插入一列 Section + 空文本并进入编辑 */
|
|
794
|
+
/** 图库启用(showGallery 且配置了 imageGallery / pickImageFromGallery)时返回块内第一个 `type: 'image'` 字段,作为双击换图入口 */
|
|
795
|
+
private _galleryImageField(def: BlockDefinition | undefined): BlockSchemaField | null {
|
|
796
|
+
const assets = this.opts.imageAssets;
|
|
797
|
+
if (!assets || assets.showGallery !== true) return null;
|
|
798
|
+
if (!assets.imageGallery && !assets.pickImageFromGallery) return null;
|
|
799
|
+
return def?.schema.find((f) => f.type === 'image') ?? null;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* 双击图片类块打开图库换图,选中后写入 field 对应的 props 字段
|
|
804
|
+
* (image.src / logo.src / hero.backgroundUrl…)。
|
|
805
|
+
* 优先内置图库弹层(与右栏「图库」按钮同一 adapter / 权限),
|
|
806
|
+
* 宿主自管场景走 pickImageFromGallery 回调。
|
|
807
|
+
*/
|
|
808
|
+
private _openGalleryForBlock(block: Block, field: BlockSchemaField) {
|
|
809
|
+
const assets = this.opts.imageAssets;
|
|
810
|
+
if (!assets) return;
|
|
811
|
+
const propKey = field.key;
|
|
812
|
+
const currentUrl = String((block.props as Record<string, unknown>)[propKey] ?? '');
|
|
813
|
+
const applyUrl = (url: string | null) => {
|
|
814
|
+
const value = url == null ? '' : String(url).trim();
|
|
815
|
+
if (!value) return;
|
|
816
|
+
const blockId = block.id;
|
|
817
|
+
this.opts.store.update((d) => {
|
|
818
|
+
const loc = findBlockLocation(d, blockId);
|
|
819
|
+
if (loc) (loc.block.props as Record<string, unknown>)[propKey] = value;
|
|
820
|
+
});
|
|
821
|
+
};
|
|
822
|
+
if (assets.imageGallery) {
|
|
823
|
+
openImageGalleryModal({
|
|
824
|
+
adapter: assets.imageGallery,
|
|
825
|
+
permissions: assets.permissions,
|
|
826
|
+
onPick: (url) => applyUrl(url),
|
|
827
|
+
t: this.opts.t,
|
|
828
|
+
parent: this.opts.layerRoot,
|
|
829
|
+
});
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
assets
|
|
833
|
+
.pickImageFromGallery?.({ blockId: block.id, propKey, currentUrl })
|
|
834
|
+
.then(applyUrl, (e) => {
|
|
835
|
+
console.error('[simple-mail] pickImageFromGallery', e);
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
680
839
|
private _onEmptyCanvasDblClick(e: MouseEvent) {
|
|
840
|
+
if (this.sectionSelectActive) return;
|
|
681
841
|
if (this.opts.store.doc.sections.length > 0) return;
|
|
682
842
|
const t = e.target as HTMLElement | null;
|
|
683
843
|
if (!t || !this.inner.contains(t)) return;
|
|
@@ -711,6 +871,7 @@ export class Canvas {
|
|
|
711
871
|
|
|
712
872
|
/** 空列双击:插入正文组件(content 为空)并直接进入编辑 */
|
|
713
873
|
private _insertEmptyTextAndEdit(sectionId: string, columnIndex: number) {
|
|
874
|
+
if (this.sectionSelectActive) return;
|
|
714
875
|
const reg = this.opts.registry;
|
|
715
876
|
if (!reg.get('text')?.inlineEditable) return;
|
|
716
877
|
const newBlock = reg.createBlock('text');
|
|
@@ -731,6 +892,7 @@ export class Canvas {
|
|
|
731
892
|
}
|
|
732
893
|
|
|
733
894
|
private _enterEditing(block: Block) {
|
|
895
|
+
if (this.sectionSelectActive) return;
|
|
734
896
|
if (this.editingBlockId === block.id) return;
|
|
735
897
|
this._exitEditing(true);
|
|
736
898
|
|
|
@@ -866,7 +1028,9 @@ export class Canvas {
|
|
|
866
1028
|
return !!attrs?.dynamicVariantKey;
|
|
867
1029
|
}
|
|
868
1030
|
|
|
869
|
-
private _firstBlockInSection(
|
|
1031
|
+
private _firstBlockInSection(
|
|
1032
|
+
section: Section,
|
|
1033
|
+
): { block: Block; columnIndex: number } | undefined {
|
|
870
1034
|
for (let columnIndex = 0; columnIndex < section.columns.length; columnIndex += 1) {
|
|
871
1035
|
const block = section.columns[columnIndex]?.blocks[0];
|
|
872
1036
|
if (block) return { block, columnIndex };
|
|
@@ -1028,6 +1192,359 @@ export class Canvas {
|
|
|
1028
1192
|
});
|
|
1029
1193
|
}
|
|
1030
1194
|
|
|
1195
|
+
/* ------------------------- 节选择模式(批量操作) ------------------------- */
|
|
1196
|
+
|
|
1197
|
+
get isSectionSelectMode(): boolean {
|
|
1198
|
+
return this.sectionSelectActive;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** 进入节选择模式:屏蔽单选 / 拖拽 / 内联编辑,点击块勾块、点击节空白/圈勾整节,可批量复制 / 导出 / 删除 */
|
|
1202
|
+
enterSectionSelectMode() {
|
|
1203
|
+
if (this.sectionSelectActive) return;
|
|
1204
|
+
this.commitInlineEdit();
|
|
1205
|
+
this.sectionSelectActive = true;
|
|
1206
|
+
this.checkedSectionIds.clear();
|
|
1207
|
+
this.checkedBlockIds.clear();
|
|
1208
|
+
this.sectionCheckAnchor = null;
|
|
1209
|
+
this.opts.store.setSelection(null);
|
|
1210
|
+
this.el.classList.add('sm-section-select-mode');
|
|
1211
|
+
this._setSortablesDisabled(true);
|
|
1212
|
+
this._renderSectionSelectBar();
|
|
1213
|
+
this._syncSectionChecks();
|
|
1214
|
+
this.opts.onSectionSelectModeChange?.(true);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/** 退出节选择模式,恢复常规画布交互 */
|
|
1218
|
+
exitSectionSelectMode() {
|
|
1219
|
+
if (!this.sectionSelectActive) return;
|
|
1220
|
+
this.sectionSelectActive = false;
|
|
1221
|
+
this.checkedSectionIds.clear();
|
|
1222
|
+
this.checkedBlockIds.clear();
|
|
1223
|
+
this.sectionCheckAnchor = null;
|
|
1224
|
+
this.el.classList.remove('sm-section-select-mode');
|
|
1225
|
+
this.sectionSelectBar?.remove();
|
|
1226
|
+
this.sectionSelectBar = null;
|
|
1227
|
+
this.sectionSelectCountEl = null;
|
|
1228
|
+
this.sectionSelectAllBtn = null;
|
|
1229
|
+
this.sectionSelectActionBtns = [];
|
|
1230
|
+
this._setSortablesDisabled(false);
|
|
1231
|
+
this._syncSectionChecks();
|
|
1232
|
+
this.opts.onSectionSelectModeChange?.(false);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** 点节(空白 / 选择圈):未全选 → 整节选中;已全选 → 清空该节(取消其内散选块) */
|
|
1236
|
+
private _toggleSectionFull(id: string) {
|
|
1237
|
+
const sec = findSection(this.opts.store.doc, id);
|
|
1238
|
+
if (!sec) return;
|
|
1239
|
+
if (this.checkedSectionIds.has(id)) {
|
|
1240
|
+
this.checkedSectionIds.delete(id);
|
|
1241
|
+
} else {
|
|
1242
|
+
for (const c of sec.columns) {
|
|
1243
|
+
for (const b of c.blocks) this.checkedBlockIds.delete(b.id);
|
|
1244
|
+
}
|
|
1245
|
+
this.checkedSectionIds.add(id);
|
|
1246
|
+
}
|
|
1247
|
+
this.sectionCheckAnchor = { kind: 'section', id, sectionId: id, columnIndex: -1 };
|
|
1248
|
+
this._syncSectionChecks();
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* 点块:切换该块选中态,不带走同节其他块。
|
|
1253
|
+
* 整节选中时点其中一块 = 取消该块,其余块退散为逐块选中(半选)。
|
|
1254
|
+
*/
|
|
1255
|
+
private _toggleBlockCheck(sectionId: string, columnIndex: number, blockId: string) {
|
|
1256
|
+
const sec = findSection(this.opts.store.doc, sectionId);
|
|
1257
|
+
if (!sec) return;
|
|
1258
|
+
if (this.checkedSectionIds.has(sectionId)) {
|
|
1259
|
+
this.checkedSectionIds.delete(sectionId);
|
|
1260
|
+
for (const c of sec.columns) {
|
|
1261
|
+
for (const b of c.blocks) if (b.id !== blockId) this.checkedBlockIds.add(b.id);
|
|
1262
|
+
}
|
|
1263
|
+
} else if (this.checkedBlockIds.has(blockId)) {
|
|
1264
|
+
this.checkedBlockIds.delete(blockId);
|
|
1265
|
+
} else {
|
|
1266
|
+
this.checkedBlockIds.add(blockId);
|
|
1267
|
+
}
|
|
1268
|
+
this.sectionCheckAnchor = { kind: 'block', id: blockId, sectionId, columnIndex };
|
|
1269
|
+
this._normalizeCheckedSets();
|
|
1270
|
+
this._syncSectionChecks();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/** Shift+点击节:锚也是节时做节区间(替换全部勾选,Finder 语义);否则退化为单点切换 */
|
|
1274
|
+
private _shiftClickSection(targetId: string) {
|
|
1275
|
+
const ids = this.opts.store.doc.sections.map((s) => s.id);
|
|
1276
|
+
const targetIdx = ids.indexOf(targetId);
|
|
1277
|
+
if (targetIdx < 0) return;
|
|
1278
|
+
const anchor = this.sectionCheckAnchor;
|
|
1279
|
+
const anchorIdx = anchor?.kind === 'section' ? ids.indexOf(anchor.id) : -1;
|
|
1280
|
+
if (anchorIdx < 0) {
|
|
1281
|
+
this._toggleSectionFull(targetId);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const from = Math.min(anchorIdx, targetIdx);
|
|
1285
|
+
const to = Math.max(anchorIdx, targetIdx);
|
|
1286
|
+
this.checkedSectionIds = new Set(ids.slice(from, to + 1));
|
|
1287
|
+
this.checkedBlockIds.clear();
|
|
1288
|
+
this._syncSectionChecks();
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** Shift+点击块:锚是同节同列的块时做列内区间(替换全部勾选);跨类 / 跨列退化为单点切换 */
|
|
1292
|
+
private _shiftClickBlock(sectionId: string, columnIndex: number, blockId: string) {
|
|
1293
|
+
const sec = findSection(this.opts.store.doc, sectionId);
|
|
1294
|
+
const col = sec?.columns[columnIndex];
|
|
1295
|
+
if (!sec || !col) return;
|
|
1296
|
+
const ids = col.blocks.map((b) => b.id);
|
|
1297
|
+
const targetIdx = ids.indexOf(blockId);
|
|
1298
|
+
if (targetIdx < 0) return;
|
|
1299
|
+
const anchor = this.sectionCheckAnchor;
|
|
1300
|
+
const anchorIdx =
|
|
1301
|
+
anchor?.kind === 'block' &&
|
|
1302
|
+
anchor.sectionId === sectionId &&
|
|
1303
|
+
anchor.columnIndex === columnIndex
|
|
1304
|
+
? ids.indexOf(anchor.id)
|
|
1305
|
+
: -1;
|
|
1306
|
+
if (anchorIdx < 0) {
|
|
1307
|
+
this._toggleBlockCheck(sectionId, columnIndex, blockId);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const from = Math.min(anchorIdx, targetIdx);
|
|
1311
|
+
const to = Math.max(anchorIdx, targetIdx);
|
|
1312
|
+
this.checkedSectionIds.clear();
|
|
1313
|
+
this.checkedBlockIds = new Set(ids.slice(from, to + 1));
|
|
1314
|
+
this._syncSectionChecks();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* ⌘/Ctrl+点击直达:进入选择模式并整节勾选(macOS ⌘ / Windows·Linux Ctrl,
|
|
1319
|
+
* 与文件管理器「加选」一致的手势,省去先点顶栏入口的往返)。
|
|
1320
|
+
*/
|
|
1321
|
+
private _enterSectionSelectModeWith(sectionId: string) {
|
|
1322
|
+
this.enterSectionSelectMode();
|
|
1323
|
+
this.checkedSectionIds.add(sectionId);
|
|
1324
|
+
this.sectionCheckAnchor = { kind: 'section', id: sectionId, sectionId, columnIndex: -1 };
|
|
1325
|
+
this._syncSectionChecks();
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/** ⌘/Ctrl+点击块直达:进入选择模式并勾选该块(锚为块,后续可 Shift 列内区间) */
|
|
1329
|
+
private _enterSectionSelectModeWithBlock(
|
|
1330
|
+
sectionId: string,
|
|
1331
|
+
columnIndex: number,
|
|
1332
|
+
blockId: string,
|
|
1333
|
+
) {
|
|
1334
|
+
this.enterSectionSelectMode();
|
|
1335
|
+
this.checkedBlockIds.add(blockId);
|
|
1336
|
+
this.sectionCheckAnchor = { kind: 'block', id: blockId, sectionId, columnIndex };
|
|
1337
|
+
this._syncSectionChecks();
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** 节的勾选三态:无选 / 半选(部分散选块)/ 全选(整节) */
|
|
1341
|
+
private _sectionCheckState(section: Section): 'none' | 'partial' | 'full' {
|
|
1342
|
+
if (this.checkedSectionIds.has(section.id)) return 'full';
|
|
1343
|
+
let total = 0;
|
|
1344
|
+
let hit = 0;
|
|
1345
|
+
for (const c of section.columns) {
|
|
1346
|
+
for (const b of c.blocks) {
|
|
1347
|
+
total++;
|
|
1348
|
+
if (this.checkedBlockIds.has(b.id)) hit++;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (total === 0 || hit === 0) return 'none';
|
|
1352
|
+
return hit === total ? 'full' : 'partial';
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* 勾选集归一化,维护两条不变式:
|
|
1357
|
+
* 1. 失效 id(节/块被删)及时清理
|
|
1358
|
+
* 2. 节内全部块散选时折叠为整节选中;散选块的所属节被整选时清掉该块
|
|
1359
|
+
*/
|
|
1360
|
+
private _normalizeCheckedSets() {
|
|
1361
|
+
const sections = this.opts.store.doc.sections;
|
|
1362
|
+
const aliveSectionIds = new Set(sections.map((s) => s.id));
|
|
1363
|
+
for (const id of [...this.checkedSectionIds]) {
|
|
1364
|
+
if (!aliveSectionIds.has(id)) this.checkedSectionIds.delete(id);
|
|
1365
|
+
}
|
|
1366
|
+
const aliveBlockIds = new Set<string>();
|
|
1367
|
+
for (const s of sections) {
|
|
1368
|
+
for (const c of s.columns) for (const b of c.blocks) aliveBlockIds.add(b.id);
|
|
1369
|
+
}
|
|
1370
|
+
for (const id of [...this.checkedBlockIds]) {
|
|
1371
|
+
if (!aliveBlockIds.has(id)) this.checkedBlockIds.delete(id);
|
|
1372
|
+
}
|
|
1373
|
+
for (const s of sections) {
|
|
1374
|
+
if (this.checkedSectionIds.has(s.id)) {
|
|
1375
|
+
for (const c of s.columns) {
|
|
1376
|
+
for (const b of c.blocks) this.checkedBlockIds.delete(b.id);
|
|
1377
|
+
}
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
const total = s.columns.reduce((n, c) => n + c.blocks.length, 0);
|
|
1381
|
+
if (total === 0) continue;
|
|
1382
|
+
const hit = s.columns.reduce(
|
|
1383
|
+
(n, c) => n + c.blocks.filter((b) => this.checkedBlockIds.has(b.id)).length,
|
|
1384
|
+
0,
|
|
1385
|
+
);
|
|
1386
|
+
if (hit === total) {
|
|
1387
|
+
this.checkedSectionIds.add(s.id);
|
|
1388
|
+
for (const c of s.columns) {
|
|
1389
|
+
for (const b of c.blocks) this.checkedBlockIds.delete(b.id);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
private _toggleSelectAllSections() {
|
|
1396
|
+
const doc = this.opts.store.doc;
|
|
1397
|
+
if (doc.sections.length > 0 && this.checkedSectionIds.size === doc.sections.length) {
|
|
1398
|
+
this.checkedSectionIds.clear();
|
|
1399
|
+
this.checkedBlockIds.clear();
|
|
1400
|
+
} else {
|
|
1401
|
+
this.checkedSectionIds = new Set(doc.sections.map((s) => s.id));
|
|
1402
|
+
this.checkedBlockIds.clear();
|
|
1403
|
+
}
|
|
1404
|
+
this._syncSectionChecks();
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/** 同步勾选态:归一化 → 节三态圈 / 块高亮 → 计数 / 全选 / 动作按钮可用性 */
|
|
1408
|
+
private _syncSectionChecks() {
|
|
1409
|
+
this._normalizeCheckedSets();
|
|
1410
|
+
const doc = this.opts.store.doc;
|
|
1411
|
+
const sectionById = new Map(doc.sections.map((s) => [s.id, s]));
|
|
1412
|
+
|
|
1413
|
+
for (const host of this.inner.querySelectorAll<HTMLElement>('.sm-section-host')) {
|
|
1414
|
+
host.classList.remove('is-check-selected', 'is-check-partial');
|
|
1415
|
+
const id = host.getAttribute('data-id') ?? '';
|
|
1416
|
+
const sec = sectionById.get(id);
|
|
1417
|
+
if (!sec) continue;
|
|
1418
|
+
const state = this._sectionCheckState(sec);
|
|
1419
|
+
if (state === 'full') host.classList.add('is-check-selected');
|
|
1420
|
+
else if (state === 'partial') host.classList.add('is-check-partial');
|
|
1421
|
+
}
|
|
1422
|
+
for (const el of this.inner.querySelectorAll('.sm-block.is-check-selected')) {
|
|
1423
|
+
el.classList.remove('is-check-selected');
|
|
1424
|
+
}
|
|
1425
|
+
for (const id of this.checkedBlockIds) {
|
|
1426
|
+
this.inner
|
|
1427
|
+
.querySelector(`.sm-block[data-id="${cssEscape(id)}"]`)
|
|
1428
|
+
?.classList.add('is-check-selected');
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const secCount = this.checkedSectionIds.size;
|
|
1432
|
+
const blockCount = this.checkedBlockIds.size;
|
|
1433
|
+
if (this.sectionSelectCountEl) {
|
|
1434
|
+
this.sectionSelectCountEl.textContent =
|
|
1435
|
+
secCount > 0 && blockCount > 0
|
|
1436
|
+
? this.opts.t('sectionSelect.countMixed', { sections: secCount, blocks: blockCount })
|
|
1437
|
+
: blockCount > 0
|
|
1438
|
+
? this.opts.t('sectionSelect.countBlocks', { count: blockCount })
|
|
1439
|
+
: this.opts.t('sectionSelect.count', { count: secCount });
|
|
1440
|
+
}
|
|
1441
|
+
if (this.sectionSelectAllBtn) {
|
|
1442
|
+
const all = doc.sections.length > 0 && secCount === doc.sections.length;
|
|
1443
|
+
this.sectionSelectAllBtn.textContent = this.opts.t(
|
|
1444
|
+
all ? 'sectionSelect.unselectAll' : 'sectionSelect.selectAll',
|
|
1445
|
+
);
|
|
1446
|
+
this.sectionSelectAllBtn.disabled = doc.sections.length === 0;
|
|
1447
|
+
}
|
|
1448
|
+
for (const btn of this.sectionSelectActionBtns) {
|
|
1449
|
+
btn.disabled = secCount + blockCount === 0;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
private _setSortablesDisabled(disabled: boolean) {
|
|
1454
|
+
for (const s of this.sortableRefs) {
|
|
1455
|
+
try {
|
|
1456
|
+
s.option('disabled', disabled);
|
|
1457
|
+
} catch {
|
|
1458
|
+
/* sortable 已销毁时忽略 */
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
private _renderSectionSelectBar() {
|
|
1464
|
+
const t = this.opts.t;
|
|
1465
|
+
this.sectionSelectCountEl = h('span', { class: 'sm-section-select-bar__count' }, [
|
|
1466
|
+
t('sectionSelect.count', { count: 0 }),
|
|
1467
|
+
]);
|
|
1468
|
+
this.sectionSelectAllBtn = h(
|
|
1469
|
+
'button',
|
|
1470
|
+
{
|
|
1471
|
+
class: 'sm-section-select-bar__btn',
|
|
1472
|
+
type: 'button',
|
|
1473
|
+
onclick: () => this._toggleSelectAllSections(),
|
|
1474
|
+
},
|
|
1475
|
+
[t('sectionSelect.selectAll')],
|
|
1476
|
+
) as HTMLButtonElement;
|
|
1477
|
+
const copyBtn = h(
|
|
1478
|
+
'button',
|
|
1479
|
+
{
|
|
1480
|
+
class: 'sm-section-select-bar__btn',
|
|
1481
|
+
type: 'button',
|
|
1482
|
+
onclick: () => this._runSectionSelectAction('copy'),
|
|
1483
|
+
},
|
|
1484
|
+
[t('sectionSelect.copyDesign')],
|
|
1485
|
+
) as HTMLButtonElement;
|
|
1486
|
+
const exportBtn = h(
|
|
1487
|
+
'button',
|
|
1488
|
+
{
|
|
1489
|
+
class: 'sm-section-select-bar__btn',
|
|
1490
|
+
type: 'button',
|
|
1491
|
+
onclick: () => this._runSectionSelectAction('export'),
|
|
1492
|
+
},
|
|
1493
|
+
[t('sectionSelect.exportJson')],
|
|
1494
|
+
) as HTMLButtonElement;
|
|
1495
|
+
const deleteBtn = h(
|
|
1496
|
+
'button',
|
|
1497
|
+
{
|
|
1498
|
+
class: 'sm-section-select-bar__btn sm-section-select-bar__btn--danger',
|
|
1499
|
+
type: 'button',
|
|
1500
|
+
onclick: () => this._runSectionSelectAction('remove'),
|
|
1501
|
+
},
|
|
1502
|
+
[t('common.delete')],
|
|
1503
|
+
) as HTMLButtonElement;
|
|
1504
|
+
const doneBtn = h(
|
|
1505
|
+
'button',
|
|
1506
|
+
{
|
|
1507
|
+
class: 'sm-section-select-bar__btn sm-section-select-bar__btn--primary',
|
|
1508
|
+
type: 'button',
|
|
1509
|
+
onclick: () => this.exitSectionSelectMode(),
|
|
1510
|
+
},
|
|
1511
|
+
[t('sectionSelect.done')],
|
|
1512
|
+
) as HTMLButtonElement;
|
|
1513
|
+
this.sectionSelectActionBtns = [copyBtn, exportBtn, deleteBtn];
|
|
1514
|
+
this.sectionSelectBar = h('div', { class: 'sm-section-select-bar', role: 'toolbar' }, [
|
|
1515
|
+
this.sectionSelectCountEl,
|
|
1516
|
+
this.sectionSelectAllBtn,
|
|
1517
|
+
copyBtn,
|
|
1518
|
+
exportBtn,
|
|
1519
|
+
deleteBtn,
|
|
1520
|
+
doneBtn,
|
|
1521
|
+
]);
|
|
1522
|
+
this.el.append(this.sectionSelectBar);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
private _runSectionSelectAction(kind: 'copy' | 'export' | 'remove') {
|
|
1526
|
+
const sectionIds = [...this.checkedSectionIds];
|
|
1527
|
+
const blockIds = [...this.checkedBlockIds];
|
|
1528
|
+
if (!sectionIds.length && !blockIds.length) return;
|
|
1529
|
+
if (kind === 'remove') {
|
|
1530
|
+
const t = this.opts.t;
|
|
1531
|
+
const msg =
|
|
1532
|
+
sectionIds.length > 0 && blockIds.length > 0
|
|
1533
|
+
? t('sectionSelect.deleteConfirmMixed', {
|
|
1534
|
+
sections: sectionIds.length,
|
|
1535
|
+
blocks: blockIds.length,
|
|
1536
|
+
})
|
|
1537
|
+
: blockIds.length > 0
|
|
1538
|
+
? t('sectionSelect.deleteConfirmBlocks', { count: blockIds.length })
|
|
1539
|
+
: t('sectionSelect.deleteConfirm', { count: sectionIds.length });
|
|
1540
|
+
if (!window.confirm(msg)) return;
|
|
1541
|
+
this.opts.onSectionSelectRemove?.(sectionIds, blockIds);
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (kind === 'copy') this.opts.onSectionSelectCopy?.(sectionIds, blockIds);
|
|
1545
|
+
else this.opts.onSectionSelectExport?.(sectionIds, blockIds);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1031
1548
|
/* -------------------------------- 操作 ---------------------------------- */
|
|
1032
1549
|
|
|
1033
1550
|
private _removeSection(id: string) {
|
|
@@ -1125,8 +1642,7 @@ export class Canvas {
|
|
|
1125
1642
|
/* ---------------------------------- utils --------------------------------- */
|
|
1126
1643
|
|
|
1127
1644
|
function layoutFlexStyle(_layout: SectionLayout, columnGap?: number): string {
|
|
1128
|
-
const gap =
|
|
1129
|
-
columnGap != null && columnGap > 0 ? `gap:${Math.min(columnGap, 200)}px;` : '';
|
|
1645
|
+
const gap = columnGap != null && columnGap > 0 ? `gap:${Math.min(columnGap, 200)}px;` : '';
|
|
1130
1646
|
return `display:flex;flex-wrap:nowrap;width:100%;${gap}`;
|
|
1131
1647
|
}
|
|
1132
1648
|
|
|
@@ -1169,6 +1685,12 @@ function iconTrash() {
|
|
|
1169
1685
|
'<path d="M5 6h10M8 6V4h4v2M6 6l1 10h6l1-10" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linejoin="round"/>',
|
|
1170
1686
|
);
|
|
1171
1687
|
}
|
|
1688
|
+
/** 「复制设计稿」:双层文档图形,与顶栏 iconCopyDoc 保持一致 */
|
|
1689
|
+
function iconCopyDesign() {
|
|
1690
|
+
return svg(
|
|
1691
|
+
'<rect x="6.5" y="6.5" width="9" height="11" rx="1.2" stroke="currentColor" stroke-width="1.4" fill="none"/><rect x="4.5" y="3.5" width="9" height="11" rx="1.2" stroke="currentColor" stroke-width="1.4" fill="var(--sm-surface, #fff)"/>',
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1172
1694
|
function iconEdit() {
|
|
1173
1695
|
return svg(
|
|
1174
1696
|
'<path d="M4 14l8-8 2 2-8 8H4v-2z" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linejoin="round"/>',
|
|
@@ -1179,3 +1701,19 @@ function iconCode() {
|
|
|
1179
1701
|
'<path d="M7 7l-3 3 3 3M13 7l3 3-3 3" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
1180
1702
|
);
|
|
1181
1703
|
}
|
|
1704
|
+
/** 勾选(节选择模式气泡):全选态 */
|
|
1705
|
+
function iconCheck() {
|
|
1706
|
+
const el = svg(
|
|
1707
|
+
'<path d="M4.5 10.5l3.4 3.4 7.6-7.8" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
1708
|
+
);
|
|
1709
|
+
el.classList.add('sm-section-check__icon-check');
|
|
1710
|
+
return el;
|
|
1711
|
+
}
|
|
1712
|
+
/** 减号(节选择模式气泡):半选态(节内部分块被散选) */
|
|
1713
|
+
function iconMinus() {
|
|
1714
|
+
const el = svg(
|
|
1715
|
+
'<path d="M5 10h10" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round"/>',
|
|
1716
|
+
);
|
|
1717
|
+
el.classList.add('sm-section-check__icon-minus');
|
|
1718
|
+
return el;
|
|
1719
|
+
}
|