mail-editor-pancake 0.2.7 → 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 +8 -4
- package/packages/blocks/vite.config.ts +1 -1
- package/packages/core/dist/index.d.ts +96 -1
- package/packages/core/dist/index.js +4349 -3594
- 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 +559 -21
- package/packages/core/src/editor/ColorPickerPopover.ts +2 -2
- package/packages/core/src/editor/Editor.ts +381 -41
- 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 +142 -126
- 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 +5 -10
- 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 +2 -2
- 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,25 +1,36 @@
|
|
|
1
|
+
import type { SimpleMailT } from '../i18n';
|
|
1
2
|
import type { EmailDoc } from '../types';
|
|
2
|
-
import {
|
|
3
|
-
|
|
3
|
+
import {
|
|
4
|
+
type SelectionClipboardEnvelope,
|
|
5
|
+
parseDocClipboard,
|
|
6
|
+
parseSelectionClipboard,
|
|
7
|
+
} from '../utils/docClipboard';
|
|
8
|
+
import { clear, h } from '../utils/dom';
|
|
4
9
|
import { richTextExecCommand } from '../utils/richTextCommand';
|
|
5
10
|
import { Modal } from './Modal';
|
|
6
|
-
import type { SimpleMailT } from '../i18n';
|
|
7
11
|
|
|
8
12
|
export interface ImportDocModalOptions {
|
|
9
13
|
/** 读取剪贴板初始内容(失败时返回空字符串) */
|
|
10
14
|
readClipboard?: () => Promise<string>;
|
|
11
15
|
onApply: (doc: EmailDoc) => void;
|
|
16
|
+
/** 局部设计稿(simple-mail/selection)回调:追加到画布而非覆盖 */
|
|
17
|
+
onApplySelection?: (envelope: SelectionClipboardEnvelope) => void;
|
|
12
18
|
t: SimpleMailT;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
|
-
* 导入设计稿:展示 JSON
|
|
22
|
+
* 导入设计稿:展示 JSON 文本区,支持从剪贴板填入。
|
|
23
|
+
* - 整稿(simple-mail/doc 或裸 EmailDoc):覆盖当前画布
|
|
24
|
+
* - 局部(simple-mail/selection):追加到当前画布(按钮与提示自动切换)
|
|
17
25
|
*/
|
|
18
26
|
export class ImportDocModal {
|
|
19
27
|
private modal: Modal;
|
|
20
28
|
private textarea: HTMLTextAreaElement;
|
|
21
29
|
private hintEl: HTMLElement;
|
|
30
|
+
private applyBtn: HTMLButtonElement;
|
|
22
31
|
private opts: ImportDocModalOptions;
|
|
32
|
+
/** 当前输入识别为局部设计稿时为 true */
|
|
33
|
+
private selectionMode = false;
|
|
23
34
|
|
|
24
35
|
constructor(opts: ImportDocModalOptions) {
|
|
25
36
|
this.opts = opts;
|
|
@@ -44,6 +55,7 @@ export class ImportDocModal {
|
|
|
44
55
|
placeholder: t('importDoc.placeholder'),
|
|
45
56
|
rows: '14',
|
|
46
57
|
}) as HTMLTextAreaElement;
|
|
58
|
+
this.textarea.addEventListener('input', () => this._syncMode());
|
|
47
59
|
|
|
48
60
|
this.modal.body.append(this.hintEl, this.textarea);
|
|
49
61
|
|
|
@@ -57,7 +69,7 @@ export class ImportDocModal {
|
|
|
57
69
|
{ class: 'sm-btn', type: 'button', onclick: () => void this._fillFromClipboard() },
|
|
58
70
|
[t('importDoc.pasteFromClipboard')],
|
|
59
71
|
);
|
|
60
|
-
|
|
72
|
+
this.applyBtn = h(
|
|
61
73
|
'button',
|
|
62
74
|
{
|
|
63
75
|
class: 'sm-btn sm-btn--primary',
|
|
@@ -65,8 +77,8 @@ export class ImportDocModal {
|
|
|
65
77
|
onclick: () => this._apply(),
|
|
66
78
|
},
|
|
67
79
|
[t('importDoc.apply')],
|
|
68
|
-
);
|
|
69
|
-
this.modal.footer.append(cancelBtn, pasteBtn, applyBtn);
|
|
80
|
+
) as HTMLButtonElement;
|
|
81
|
+
this.modal.footer.append(cancelBtn, pasteBtn, this.applyBtn);
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
async open(parent?: HTMLElement) {
|
|
@@ -75,6 +87,7 @@ export class ImportDocModal {
|
|
|
75
87
|
this.modal.open(parent);
|
|
76
88
|
const initial = (await this.opts.readClipboard?.()) ?? '';
|
|
77
89
|
if (initial.trim()) this.textarea.value = initial;
|
|
90
|
+
this._syncMode();
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
private async _fillFromClipboard() {
|
|
@@ -85,6 +98,28 @@ export class ImportDocModal {
|
|
|
85
98
|
}
|
|
86
99
|
this.textarea.value = text;
|
|
87
100
|
this._setHint(this.opts.t('importDoc.clipboardFilled'), false);
|
|
101
|
+
this._syncMode();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 根据输入内容切换「覆盖整稿 / 追加局部」两种模式(按钮文案与提示)。 */
|
|
105
|
+
private _syncMode() {
|
|
106
|
+
const selection = parseSelectionClipboard(this.textarea.value);
|
|
107
|
+
const next = selection != null && typeof this.opts.onApplySelection === 'function';
|
|
108
|
+
if (next === this.selectionMode) return;
|
|
109
|
+
this.selectionMode = next;
|
|
110
|
+
clear(this.applyBtn);
|
|
111
|
+
this.applyBtn.append(this.opts.t(next ? 'importDoc.applyAppend' : 'importDoc.apply'));
|
|
112
|
+
this.hintEl.classList.remove('sm-import-doc__hint--error');
|
|
113
|
+
clear(this.hintEl);
|
|
114
|
+
if (next) {
|
|
115
|
+
this.hintEl.append(this.opts.t('importDoc.hintSelection'));
|
|
116
|
+
} else {
|
|
117
|
+
this.hintEl.append(
|
|
118
|
+
this.opts.t('importDoc.hint1'),
|
|
119
|
+
this.opts.t('importDoc.hint2'),
|
|
120
|
+
this.opts.t('importDoc.hint3'),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
88
123
|
}
|
|
89
124
|
|
|
90
125
|
private _apply() {
|
|
@@ -93,6 +128,14 @@ export class ImportDocModal {
|
|
|
93
128
|
this._setHint(this.opts.t('importDoc.empty'), true);
|
|
94
129
|
return;
|
|
95
130
|
}
|
|
131
|
+
if (this.selectionMode) {
|
|
132
|
+
const selection = parseSelectionClipboard(raw);
|
|
133
|
+
if (selection) {
|
|
134
|
+
this.opts.onApplySelection?.(selection);
|
|
135
|
+
this.modal.close();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
96
139
|
const doc = parseDocClipboard(raw);
|
|
97
140
|
if (!doc) {
|
|
98
141
|
this._setHint(this.opts.t('importDoc.invalid'), true);
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import type { GlobalStyles } from '../types';
|
|
2
|
+
import {
|
|
3
|
+
LIST_INDENT_DEFAULT_PX,
|
|
4
|
+
type ListIndentValue,
|
|
5
|
+
setListIndent as applyListIndentToElement,
|
|
6
|
+
findListAtSelection,
|
|
7
|
+
getListIndent,
|
|
8
|
+
resolveGlobalListIndentPx,
|
|
9
|
+
} from '../utils/emailListStyles';
|
|
1
10
|
/**
|
|
2
11
|
* 把任意 DOM 元素临时变成 contenteditable,并暴露提交/取消/选区事件。
|
|
3
12
|
*
|
|
@@ -10,41 +19,32 @@
|
|
|
10
19
|
* - 选区变化通过 `onSelectionChange` 实时回调,便于浮动工具条同步按钮状态/位置。
|
|
11
20
|
*/
|
|
12
21
|
import { normalizeFontWeightStep } from '../utils/fontWeightSteps';
|
|
13
|
-
import {
|
|
14
|
-
findListAtSelection,
|
|
15
|
-
getListIndent,
|
|
16
|
-
LIST_INDENT_DEFAULT_PX,
|
|
17
|
-
resolveGlobalListIndentPx,
|
|
18
|
-
setListIndent as applyListIndentToElement,
|
|
19
|
-
type ListIndentValue,
|
|
20
|
-
} from '../utils/emailListStyles';
|
|
21
|
-
import type { GlobalStyles } from '../types';
|
|
22
22
|
import {
|
|
23
23
|
applyListCommandForSelection,
|
|
24
24
|
detectListFormats,
|
|
25
25
|
findListItem,
|
|
26
|
-
getListItemsInRange,
|
|
27
26
|
getCaretTextOffsetAtRangeEnd,
|
|
27
|
+
getListItemsInRange,
|
|
28
28
|
insertSelectionBoundaryMarkers,
|
|
29
|
-
mergeListItemOnBackspace,
|
|
30
29
|
mergeAdjacentSameTypeLists,
|
|
30
|
+
mergeListItemOnBackspace,
|
|
31
31
|
mergeNextListItemOnDelete,
|
|
32
32
|
removeSelectionBoundaryMarkers,
|
|
33
33
|
restoreCaretAfterListMutation,
|
|
34
34
|
restoreCaretToEndOfListInSelectionBounds,
|
|
35
35
|
splitListItemOnEnter,
|
|
36
36
|
} from '../utils/inlineListEditing';
|
|
37
|
+
import {
|
|
38
|
+
getRichHtmlPlainText,
|
|
39
|
+
hasRichHtmlLineBreak,
|
|
40
|
+
isRichHtmlEditorSeedOnly,
|
|
41
|
+
isRichHtmlEffectivelyEmpty,
|
|
42
|
+
} from '../utils/richHtmlEmpty';
|
|
37
43
|
import {
|
|
38
44
|
richTextExecCommand,
|
|
39
45
|
richTextQueryCommandState,
|
|
40
46
|
richTextQueryCommandValue,
|
|
41
47
|
} from '../utils/richTextCommand';
|
|
42
|
-
import {
|
|
43
|
-
isRichHtmlEffectivelyEmpty,
|
|
44
|
-
isRichHtmlEditorSeedOnly,
|
|
45
|
-
hasRichHtmlLineBreak,
|
|
46
|
-
getRichHtmlPlainText,
|
|
47
|
-
} from '../utils/richHtmlEmpty';
|
|
48
48
|
import {
|
|
49
49
|
convertFontTags,
|
|
50
50
|
firstTextNode,
|
|
@@ -145,11 +145,7 @@ export class InlineEditor {
|
|
|
145
145
|
const text = (this.el.textContent ?? '').replace(/\u200b/g, '');
|
|
146
146
|
if (text.length > 0) {
|
|
147
147
|
value = sanitized;
|
|
148
|
-
} else if (
|
|
149
|
-
this.initiallyEmpty &&
|
|
150
|
-
!this.edited &&
|
|
151
|
-
isRichHtmlEditorSeedOnly(sanitized)
|
|
152
|
-
) {
|
|
148
|
+
} else if (this.initiallyEmpty && !this.edited && isRichHtmlEditorSeedOnly(sanitized)) {
|
|
153
149
|
value = '';
|
|
154
150
|
} else if (hasRichHtmlLineBreak(sanitized) && !this.hadTextInSession) {
|
|
155
151
|
// 从未有过文本,仅换行(含单个 <br>)视为有效
|
|
@@ -209,9 +205,7 @@ export class InlineEditor {
|
|
|
209
205
|
const isListCmd = command === 'insertOrderedList' || command === 'insertUnorderedList';
|
|
210
206
|
if (isListCmd && sel?.rangeCount) {
|
|
211
207
|
const range = sel.getRangeAt(0);
|
|
212
|
-
const itemsInRange = range.collapsed
|
|
213
|
-
? []
|
|
214
|
-
: getListItemsInRange(range, this.el);
|
|
208
|
+
const itemsInRange = range.collapsed ? [] : getListItemsInRange(range, this.el);
|
|
215
209
|
const savedOffset = getCaretTextOffsetAtRangeEnd(this.el, range);
|
|
216
210
|
const boundaryMarkers = insertSelectionBoundaryMarkers(range);
|
|
217
211
|
this.suppressListMerge = true;
|
|
@@ -514,8 +508,7 @@ export class InlineEditor {
|
|
|
514
508
|
el.classList.remove('is-empty');
|
|
515
509
|
|
|
516
510
|
const initial = this.opts.initialValue ?? '';
|
|
517
|
-
const initiallyEmpty =
|
|
518
|
-
mode === 'plain' ? !initial.trim() : isRichHtmlEffectivelyEmpty(initial);
|
|
511
|
+
const initiallyEmpty = mode === 'plain' ? !initial.trim() : isRichHtmlEffectivelyEmpty(initial);
|
|
519
512
|
this.initiallyEmpty = initiallyEmpty;
|
|
520
513
|
this.edited = false;
|
|
521
514
|
this.hadTextInSession =
|
|
@@ -625,7 +618,8 @@ export class InlineEditor {
|
|
|
625
618
|
const active = document.activeElement as HTMLElement | null;
|
|
626
619
|
if (active && (active === el || el.contains(active))) return;
|
|
627
620
|
if (isColorPickerOpen()) return;
|
|
628
|
-
if (active?.closest?.('.sm-floating-toolbar, .sm-color-picker, .sm-color-picker-layer'))
|
|
621
|
+
if (active?.closest?.('.sm-floating-toolbar, .sm-color-picker, .sm-color-picker-layer'))
|
|
622
|
+
return;
|
|
629
623
|
if (active?.closest?.('.sm-topbar, .sm-popover, .sm-panel--variable-picker')) return;
|
|
630
624
|
// 从左栏拖入 / 画布内 Sortable 排序:mousedown 会先 blur,若在此时提交会触发画布重渲染并拆掉 drop 目标
|
|
631
625
|
if (active?.closest?.('.sm-panel--left')) return;
|
|
@@ -787,7 +781,7 @@ export class InlineEditor {
|
|
|
787
781
|
|
|
788
782
|
const inline = resolveInlineFormatsAtSelection(this.el, sel);
|
|
789
783
|
|
|
790
|
-
|
|
784
|
+
const foreColor = inline.foreColor ?? safeQueryValue('foreColor');
|
|
791
785
|
let backColor = inline.backColor;
|
|
792
786
|
if (
|
|
793
787
|
!backColor &&
|
|
@@ -805,8 +799,7 @@ export class InlineEditor {
|
|
|
805
799
|
const listIndentGlobalPx = this.opts.globalStyles
|
|
806
800
|
? resolveGlobalListIndentPx(this.opts.globalStyles)
|
|
807
801
|
: LIST_INDENT_DEFAULT_PX;
|
|
808
|
-
const listIndentEffectivePx =
|
|
809
|
-
listIndentPx != null ? listIndentPx : listIndentGlobalPx;
|
|
802
|
+
const listIndentEffectivePx = listIndentPx != null ? listIndentPx : listIndentGlobalPx;
|
|
810
803
|
|
|
811
804
|
const state: SelectionState = {
|
|
812
805
|
hasSelection: true,
|
|
@@ -839,9 +832,7 @@ export class InlineEditor {
|
|
|
839
832
|
/** 焦点在浮动工具条上时沿用上一帧选区,避免点开链接框 / 颜色等控件后工具条消失。 */
|
|
840
833
|
private _emitSelectionOrKeepToolbar(fallback: null) {
|
|
841
834
|
if (
|
|
842
|
-
fallback === null &&
|
|
843
|
-
this.lastSelectionState &&
|
|
844
|
-
isColorPickerOpen() ||
|
|
835
|
+
(fallback === null && this.lastSelectionState && isColorPickerOpen()) ||
|
|
845
836
|
document.activeElement?.closest?.(
|
|
846
837
|
'.sm-floating-toolbar, .sm-color-picker, .sm-color-picker-layer',
|
|
847
838
|
)
|
|
@@ -870,7 +861,8 @@ export class InlineEditor {
|
|
|
870
861
|
|
|
871
862
|
/* ----------------------------- helpers ----------------------------- */
|
|
872
863
|
|
|
873
|
-
function findAncestorAnchor(
|
|
864
|
+
function findAncestorAnchor(start: Node | null, root: HTMLElement): HTMLAnchorElement | null {
|
|
865
|
+
let node = start;
|
|
874
866
|
while (node && node !== root) {
|
|
875
867
|
if (node.nodeType === 1 && (node as HTMLElement).tagName === 'A') {
|
|
876
868
|
return node as HTMLAnchorElement;
|
|
@@ -913,7 +905,11 @@ function resolveInlineFormatsAtSelection(
|
|
|
913
905
|
if (!host) return { fontSize: null, fontWeight: null, foreColor: null, backColor: null };
|
|
914
906
|
|
|
915
907
|
let fontWeight: string | null = null;
|
|
916
|
-
for (
|
|
908
|
+
for (
|
|
909
|
+
let el: HTMLElement | null = host;
|
|
910
|
+
el && root.contains(el) && el !== root;
|
|
911
|
+
el = el.parentElement
|
|
912
|
+
) {
|
|
917
913
|
if (el.style.fontWeight) fontWeight = el.style.fontWeight;
|
|
918
914
|
}
|
|
919
915
|
|
|
@@ -1033,7 +1029,11 @@ function resolveUnderlineAtSelection(root: HTMLElement, sel: Selection): boolean
|
|
|
1033
1029
|
|
|
1034
1030
|
/** 光标处文字色(foreColor / 链接局部 color;queryCommandValue 在 <a> 内常不准) */
|
|
1035
1031
|
function resolveForeColorAtSelection(root: HTMLElement, host: HTMLElement): string | null {
|
|
1036
|
-
for (
|
|
1032
|
+
for (
|
|
1033
|
+
let el: HTMLElement | null = host;
|
|
1034
|
+
el && root.contains(el) && el !== root;
|
|
1035
|
+
el = el.parentElement
|
|
1036
|
+
) {
|
|
1037
1037
|
const inline = el.style.color?.trim();
|
|
1038
1038
|
if (inline) return inline;
|
|
1039
1039
|
|
|
@@ -1055,7 +1055,11 @@ function readColorFromStyleAttr(styleAttr: string | null): string | null {
|
|
|
1055
1055
|
|
|
1056
1056
|
/** 光标处文字背景/高亮色(hiliteColor 的 queryCommandValue 在 Chrome 常为空,需读 DOM) */
|
|
1057
1057
|
function resolveBackColorAtSelection(root: HTMLElement, host: HTMLElement): string | null {
|
|
1058
|
-
for (
|
|
1058
|
+
for (
|
|
1059
|
+
let el: HTMLElement | null = host;
|
|
1060
|
+
el && root.contains(el) && el !== root;
|
|
1061
|
+
el = el.parentElement
|
|
1062
|
+
) {
|
|
1059
1063
|
const inline = el.style.backgroundColor?.trim();
|
|
1060
1064
|
if (inline && !isTransparentColor(inline)) return inline;
|
|
1061
1065
|
|
|
@@ -1103,7 +1107,7 @@ function isTransparentColor(c: string): boolean {
|
|
|
1103
1107
|
if (s === 'rgba(0, 0, 0, 0)' || s === 'rgba(0,0,0,0)') return true;
|
|
1104
1108
|
const rgba = s.match(/^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s/]+([\d.]+))?\s*\)$/);
|
|
1105
1109
|
if (rgba) {
|
|
1106
|
-
const a = rgba[4] !== undefined ? parseFloat(rgba[4]) : 1;
|
|
1110
|
+
const a = rgba[4] !== undefined ? Number.parseFloat(rgba[4]) : 1;
|
|
1107
1111
|
return a === 0;
|
|
1108
1112
|
}
|
|
1109
1113
|
return false;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import Sortable from 'sortablejs';
|
|
2
|
+
import type { SimpleMailT } from '../i18n';
|
|
2
3
|
import type { Registry } from '../registry/registry';
|
|
3
4
|
import type { BlockDefinition, SectionLayout } from '../types';
|
|
4
5
|
import { clear, h } from '../utils/dom';
|
|
5
6
|
import { paletteDropIsSectionLevel } from '../utils/paletteDrop';
|
|
6
|
-
import type { SimpleMailT } from '../i18n';
|
|
7
7
|
|
|
8
8
|
export interface LeftPanelOptions {
|
|
9
9
|
registry: Registry;
|
|
@@ -173,9 +173,7 @@ export class LeftPanel {
|
|
|
173
173
|
'div',
|
|
174
174
|
{
|
|
175
175
|
class:
|
|
176
|
-
sourceGroup === 'sections'
|
|
177
|
-
? 'sm-block-card sm-block-card--section'
|
|
178
|
-
: 'sm-block-card',
|
|
176
|
+
sourceGroup === 'sections' ? 'sm-block-card sm-block-card--section' : 'sm-block-card',
|
|
179
177
|
'data-source-group': sourceGroup,
|
|
180
178
|
'data-block-type': def.type,
|
|
181
179
|
title: this._blockCardTitle(def),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import type { SimpleMailT } from '../i18n';
|
|
1
2
|
import type { Registry } from '../registry/registry';
|
|
2
3
|
import { renderDoc } from '../renderer';
|
|
3
4
|
import type { Store } from '../store/store';
|
|
4
5
|
import { h } from '../utils/dom';
|
|
5
6
|
import { Modal } from './Modal';
|
|
6
|
-
import type { SimpleMailT } from '../i18n';
|
|
7
7
|
|
|
8
8
|
export type PreviewDevice = 'pc' | 'pad' | 'phone';
|
|
9
9
|
|
|
@@ -98,9 +98,7 @@ export class PreviewModal {
|
|
|
98
98
|
for (const k of Object.keys(this.deviceBtns) as PreviewDevice[]) {
|
|
99
99
|
this.deviceBtns[k].classList.toggle('sm-segmented__item--active', k === d);
|
|
100
100
|
}
|
|
101
|
-
requestAnimationFrame(() =>
|
|
102
|
-
requestAnimationFrame(() => this._scheduleFitPreviewHeight()),
|
|
103
|
-
);
|
|
101
|
+
requestAnimationFrame(() => requestAnimationFrame(() => this._scheduleFitPreviewHeight()));
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
private _render() {
|
|
@@ -130,10 +128,7 @@ export class PreviewModal {
|
|
|
130
128
|
const body = doc?.body;
|
|
131
129
|
if (!root || !body) return;
|
|
132
130
|
const raw = Math.max(root.scrollHeight, body.scrollHeight);
|
|
133
|
-
const maxPx = Math.max(
|
|
134
|
-
240,
|
|
135
|
-
window.innerHeight - PreviewModal.VIEWPORT_RESERVED_Y,
|
|
136
|
-
);
|
|
131
|
+
const maxPx = Math.max(240, window.innerHeight - PreviewModal.VIEWPORT_RESERVED_Y);
|
|
137
132
|
const minPx = 200;
|
|
138
133
|
const next = Math.min(Math.max(Math.ceil(raw), minPx), maxPx);
|
|
139
134
|
this.viewport.style.height = `${next}px`;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SimpleMailT } from '../i18n';
|
|
2
2
|
import { h } from '../utils/dom';
|
|
3
|
-
import { FONT_WEIGHT_STEP_OPTIONS, normalizeFontWeightStep } from '../utils/fontWeightSteps';
|
|
4
3
|
import { LIST_INDENT_PRESETS_PX, type ListIndentValue } from '../utils/emailListStyles';
|
|
4
|
+
import { FONT_WEIGHT_STEP_OPTIONS, normalizeFontWeightStep } from '../utils/fontWeightSteps';
|
|
5
|
+
import { bindColorPickerInput } from './ColorPickerPopover';
|
|
5
6
|
import type { InlineEditor, SelectionState } from './InlineEditor';
|
|
6
|
-
import type { SimpleMailT } from '../i18n';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* 富文本浮动工具条。
|
|
@@ -31,8 +31,16 @@ export interface RichTextToolbarOptions {
|
|
|
31
31
|
const FONT_FAMILIES = [
|
|
32
32
|
{ labelKey: 'toolbar.fontDefault', label: 'Default', value: '' },
|
|
33
33
|
{ label: 'Inter', value: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif' },
|
|
34
|
-
{
|
|
35
|
-
|
|
34
|
+
{
|
|
35
|
+
labelKey: 'toolbar.fontPingFang',
|
|
36
|
+
label: 'PingFang SC',
|
|
37
|
+
value: '"PingFang SC", "Helvetica Neue", Arial, sans-serif',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
labelKey: 'toolbar.fontMicrosoftYaHei',
|
|
41
|
+
label: 'Microsoft YaHei',
|
|
42
|
+
value: '"Microsoft YaHei", "Segoe UI", sans-serif',
|
|
43
|
+
},
|
|
36
44
|
{ labelKey: 'toolbar.fontSimSun', label: 'SimSun', value: 'SimSun, "Songti SC", serif' },
|
|
37
45
|
{ label: 'Helvetica', value: '"Helvetica Neue", Arial, sans-serif' },
|
|
38
46
|
{ label: 'Arial', value: 'Arial, sans-serif' },
|
|
@@ -144,9 +152,7 @@ export class RichTextToolbar {
|
|
|
144
152
|
const hex = colorToHexInput(f.foreColor);
|
|
145
153
|
if (hex) this.inputColor.value = hex;
|
|
146
154
|
}
|
|
147
|
-
this.inputBgColor.value = f.backColor
|
|
148
|
-
? (colorToHexInput(f.backColor) ?? '#ffffff')
|
|
149
|
-
: '#ffffff';
|
|
155
|
+
this.inputBgColor.value = f.backColor ? (colorToHexInput(f.backColor) ?? '#ffffff') : '#ffffff';
|
|
150
156
|
if (f.fontName) this.selectFontFamily.value = matchFamily(f.fontName);
|
|
151
157
|
this.selectFontSize.value = matchFontSize(f.fontSize);
|
|
152
158
|
this.selectFontWeight.value = matchFontWeight(f.fontWeight);
|
|
@@ -173,9 +179,14 @@ export class RichTextToolbar {
|
|
|
173
179
|
this.btns.bold = this._btn('B', t('toolbar.bold'), exec('bold'), { fontWeight: '700' });
|
|
174
180
|
this.btns.italic = this._btn('I', t('toolbar.italic'), exec('italic'), { fontStyle: 'italic' });
|
|
175
181
|
// 链接内的下划线归 <a> 管,走专用实现;非链接文本回落到 execCommand
|
|
176
|
-
this.btns.underline = this._btn(
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
this.btns.underline = this._btn(
|
|
183
|
+
'U',
|
|
184
|
+
t('toolbar.underline'),
|
|
185
|
+
() => this.editor?.toggleUnderline(),
|
|
186
|
+
{
|
|
187
|
+
textDecoration: 'underline',
|
|
188
|
+
},
|
|
189
|
+
);
|
|
179
190
|
this.btns.strikethrough = this._btn('S', t('toolbar.strikethrough'), exec('strikeThrough'), {
|
|
180
191
|
textDecoration: 'line-through',
|
|
181
192
|
});
|
|
@@ -585,13 +596,13 @@ function matchFontSize(raw: string | null): string {
|
|
|
585
596
|
if (!raw) return '';
|
|
586
597
|
const m = raw.trim().match(/^(\d+(?:\.\d+)?)px$/i);
|
|
587
598
|
if (!m) return '';
|
|
588
|
-
const px = `${Math.round(parseFloat(m[1]))}px`;
|
|
599
|
+
const px = `${Math.round(Number.parseFloat(m[1]))}px`;
|
|
589
600
|
if (FONT_SIZES.includes(px)) return px;
|
|
590
|
-
const num = parseFloat(px);
|
|
601
|
+
const num = Number.parseFloat(px);
|
|
591
602
|
let best = FONT_SIZES[0];
|
|
592
|
-
let bestD =
|
|
603
|
+
let bestD = Number.POSITIVE_INFINITY;
|
|
593
604
|
for (const s of FONT_SIZES) {
|
|
594
|
-
const d = Math.abs(parseFloat(s) - num);
|
|
605
|
+
const d = Math.abs(Number.parseFloat(s) - num);
|
|
595
606
|
if (d < bestD) {
|
|
596
607
|
bestD = d;
|
|
597
608
|
best = s;
|