mail-editor-pancake 0.0.7 → 0.0.9
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 +174 -3
- package/package.json +1 -1
- package/packages/blocks/dist/index.d.ts +12 -0
- package/packages/blocks/dist/index.js +186 -148
- package/packages/blocks/dist/index.js.map +1 -1
- package/packages/blocks/src/index.ts +2 -0
- package/packages/blocks/src/social.ts +8 -11
- package/packages/blocks/src/socialGroup.ts +8 -11
- package/packages/blocks/src/socialShared.ts +52 -0
- package/packages/blocks/src/text.ts +21 -14
- package/packages/core/dist/index.d.ts +99 -5
- package/packages/core/dist/index.js +2475 -1480
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/style.css +1 -1
- package/packages/core/src/editor/Canvas.ts +77 -34
- package/packages/core/src/editor/ColorPickerPopover.ts +291 -0
- package/packages/core/src/editor/Editor.ts +269 -32
- package/packages/core/src/editor/ExportModal.ts +2 -1
- package/packages/core/src/editor/FocusBreadcrumb.ts +120 -0
- package/packages/core/src/editor/InlineEditor.ts +316 -65
- package/packages/core/src/editor/LeftPanel.ts +1 -2
- package/packages/core/src/editor/RichTextToolbar.ts +66 -44
- package/packages/core/src/editor/RightPanel.ts +491 -74
- package/packages/core/src/editor/SourceView.ts +2 -1
- package/packages/core/src/editor/Topbar.ts +98 -7
- package/packages/core/src/editor/styles.css +427 -33
- package/packages/core/src/index.ts +23 -1
- package/packages/core/src/store/store.ts +44 -3
- package/packages/core/src/types.ts +8 -0
- package/packages/core/src/utils/richHtmlInsert.ts +16 -0
- package/packages/core/src/utils/richTextCommand.ts +30 -0
- package/packages/core/src/utils/sectionLayout.ts +13 -0
- package/packages/core/src/variables/index.ts +91 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { normalizeAccentHex } from '../utils/accentColor';
|
|
2
|
+
import { h } from '../utils/dom';
|
|
3
|
+
|
|
4
|
+
/** 邮件常用预设色 */
|
|
5
|
+
export const COLOR_PICKER_PALETTE = [
|
|
6
|
+
'#FFFFFF',
|
|
7
|
+
'#F5F6F8',
|
|
8
|
+
'#E6E8EC',
|
|
9
|
+
'#9AA0A6',
|
|
10
|
+
'#6B7280',
|
|
11
|
+
'#433F3F',
|
|
12
|
+
'#1F2328',
|
|
13
|
+
'#000000',
|
|
14
|
+
'#DC2626',
|
|
15
|
+
'#EA580C',
|
|
16
|
+
'#FF5A00',
|
|
17
|
+
'#CA8A04',
|
|
18
|
+
'#16A34A',
|
|
19
|
+
'#059669',
|
|
20
|
+
'#2563EB',
|
|
21
|
+
'#4F46E5',
|
|
22
|
+
'#7C3AED',
|
|
23
|
+
'#DB2777',
|
|
24
|
+
'#FEE2E2',
|
|
25
|
+
'#FFEDD5',
|
|
26
|
+
'#FEF3C7',
|
|
27
|
+
'#D1FAE5',
|
|
28
|
+
'#EEF2FF',
|
|
29
|
+
'#F3E8FF',
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export interface ColorPickerPopoverOptions {
|
|
33
|
+
anchor: HTMLElement;
|
|
34
|
+
layerRoot: HTMLElement;
|
|
35
|
+
value: string;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/** 显示「清除」并允许提交空值 */
|
|
38
|
+
allowClear?: boolean;
|
|
39
|
+
/** 每次改色立即 onCommit(富文本/顶栏) */
|
|
40
|
+
liveCommit?: boolean;
|
|
41
|
+
onPreview?: (hex: string) => void;
|
|
42
|
+
onCommit: (hex: string) => void;
|
|
43
|
+
onClear?: () => void;
|
|
44
|
+
/** 打开前调用(富文本:在失焦前保存选区) */
|
|
45
|
+
onBeforeOpen?: () => void;
|
|
46
|
+
/** 为 false 时不自动 focus 十六进制输入,避免抢编辑区选区 */
|
|
47
|
+
autoFocusHex?: boolean;
|
|
48
|
+
onClosed?: () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let activeTeardown: (() => void) | null = null;
|
|
52
|
+
|
|
53
|
+
export function isColorPickerOpen(): boolean {
|
|
54
|
+
return activeTeardown !== null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function closeActiveColorPicker(): void {
|
|
58
|
+
activeTeardown?.();
|
|
59
|
+
activeTeardown = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function positionLayer(layer: HTMLElement, anchor: HTMLElement, layerRoot: HTMLElement) {
|
|
63
|
+
const rect = anchor.getBoundingClientRect();
|
|
64
|
+
const rootRect = layerRoot.getBoundingClientRect();
|
|
65
|
+
layer.style.top = `${rect.bottom - rootRect.top + 4}px`;
|
|
66
|
+
layer.style.left = `${Math.max(4, rect.left - rootRect.left)}px`;
|
|
67
|
+
|
|
68
|
+
requestAnimationFrame(() => {
|
|
69
|
+
const popRect = layer.getBoundingClientRect();
|
|
70
|
+
if (popRect.right > rootRect.right - 8) {
|
|
71
|
+
layer.style.left = `${Math.max(4, parseFloat(layer.style.left) - (popRect.right - rootRect.right + 8))}px`;
|
|
72
|
+
}
|
|
73
|
+
if (popRect.bottom > window.innerHeight - 8) {
|
|
74
|
+
layer.style.top = `${rect.top - rootRect.top - popRect.height - 4}px`;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 点击色块网格缝隙等无焦点区域时,避免编辑区 blur 导致误提交 */
|
|
80
|
+
function guardInlineEditFocus(layer: HTMLElement) {
|
|
81
|
+
layer.addEventListener(
|
|
82
|
+
'mousedown',
|
|
83
|
+
(e) => {
|
|
84
|
+
const t = e.target as Element;
|
|
85
|
+
if (t.closest('input, button, select, textarea, a, [contenteditable]')) return;
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
},
|
|
88
|
+
true,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function syncNativeInput(native: HTMLInputElement, hex: string) {
|
|
93
|
+
const n = normalizeAccentHex(hex);
|
|
94
|
+
if (n) native.value = n;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 打开自定义色板(含关闭按钮);点击色卡触发,不再使用系统色板。 */
|
|
98
|
+
export function openColorPickerPopover(opts: ColorPickerPopoverOptions): void {
|
|
99
|
+
closeActiveColorPicker();
|
|
100
|
+
opts.onBeforeOpen?.();
|
|
101
|
+
|
|
102
|
+
const initialHex = normalizeAccentHex(opts.value);
|
|
103
|
+
let draft = initialHex ?? '#4F46E5';
|
|
104
|
+
let closed = false;
|
|
105
|
+
|
|
106
|
+
const layer = h('div', {
|
|
107
|
+
class: 'sm-color-picker-layer',
|
|
108
|
+
'data-sm-color-picker-layer': '',
|
|
109
|
+
});
|
|
110
|
+
const panel = h('div', {
|
|
111
|
+
class: 'sm-color-picker',
|
|
112
|
+
role: 'dialog',
|
|
113
|
+
'aria-label': '选择颜色',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const closeBtn = h(
|
|
117
|
+
'button',
|
|
118
|
+
{
|
|
119
|
+
class: 'sm-color-picker__close',
|
|
120
|
+
type: 'button',
|
|
121
|
+
title: '关闭',
|
|
122
|
+
'aria-label': '关闭',
|
|
123
|
+
},
|
|
124
|
+
['×'],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const head = h('div', { class: 'sm-color-picker__head' }, [
|
|
128
|
+
h('span', { class: 'sm-color-picker__title' }, ['选择颜色']),
|
|
129
|
+
closeBtn,
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
const native = h('input', {
|
|
133
|
+
class: 'sm-color-picker__native',
|
|
134
|
+
type: 'color',
|
|
135
|
+
'aria-label': '自定义颜色',
|
|
136
|
+
}) as HTMLInputElement;
|
|
137
|
+
|
|
138
|
+
const hexInput = h('input', {
|
|
139
|
+
class: 'sm-input sm-color-picker__hex',
|
|
140
|
+
type: 'text',
|
|
141
|
+
placeholder: opts.placeholder ?? '#RRGGBB',
|
|
142
|
+
}) as HTMLInputElement;
|
|
143
|
+
|
|
144
|
+
const applyDraft = (hex: string, commit: boolean) => {
|
|
145
|
+
const n = normalizeAccentHex(hex);
|
|
146
|
+
if (!n) return;
|
|
147
|
+
draft = n;
|
|
148
|
+
syncNativeInput(native, n);
|
|
149
|
+
hexInput.value = n;
|
|
150
|
+
opts.onPreview?.(n);
|
|
151
|
+
if (commit || opts.liveCommit) opts.onCommit(n);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
syncNativeInput(native, draft);
|
|
155
|
+
hexInput.value = initialHex ?? '';
|
|
156
|
+
|
|
157
|
+
const teardown = (commit: boolean) => {
|
|
158
|
+
if (closed) return;
|
|
159
|
+
closed = true;
|
|
160
|
+
activeTeardown = null;
|
|
161
|
+
document.removeEventListener('keydown', onKey, true);
|
|
162
|
+
document.removeEventListener('mousedown', onDoc, true);
|
|
163
|
+
layer.remove();
|
|
164
|
+
if (commit && !opts.liveCommit) {
|
|
165
|
+
const n = normalizeAccentHex(draft);
|
|
166
|
+
if (n) opts.onCommit(n);
|
|
167
|
+
}
|
|
168
|
+
opts.onClosed?.();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const swatchGrid = h('div', { class: 'sm-color-picker__swatches' });
|
|
172
|
+
for (const hex of COLOR_PICKER_PALETTE) {
|
|
173
|
+
const btn = h('button', {
|
|
174
|
+
class: 'sm-color-picker__swatch',
|
|
175
|
+
type: 'button',
|
|
176
|
+
title: `${hex}(双击确认)`,
|
|
177
|
+
style: `background:${hex}`,
|
|
178
|
+
'aria-label': hex,
|
|
179
|
+
});
|
|
180
|
+
btn.addEventListener('click', () => applyDraft(hex, false));
|
|
181
|
+
btn.addEventListener('dblclick', (e) => {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
applyDraft(hex, true);
|
|
184
|
+
teardown(true);
|
|
185
|
+
});
|
|
186
|
+
swatchGrid.append(btn);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
native.addEventListener('input', () => applyDraft(native.value, false));
|
|
190
|
+
|
|
191
|
+
hexInput.addEventListener('input', () => {
|
|
192
|
+
const n = normalizeAccentHex(hexInput.value);
|
|
193
|
+
if (n) applyDraft(n, false);
|
|
194
|
+
});
|
|
195
|
+
hexInput.addEventListener('keydown', (e) => {
|
|
196
|
+
const ke = e as KeyboardEvent;
|
|
197
|
+
if (ke.key === 'Enter') {
|
|
198
|
+
ke.preventDefault();
|
|
199
|
+
const n = normalizeAccentHex(hexInput.value);
|
|
200
|
+
if (n) {
|
|
201
|
+
applyDraft(n, true);
|
|
202
|
+
teardown(true);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const footKids: HTMLElement[] = [];
|
|
208
|
+
if (opts.allowClear) {
|
|
209
|
+
footKids.push(
|
|
210
|
+
h(
|
|
211
|
+
'button',
|
|
212
|
+
{
|
|
213
|
+
class: 'sm-btn sm-btn--ghost sm-color-picker__clear',
|
|
214
|
+
type: 'button',
|
|
215
|
+
onclick: () => {
|
|
216
|
+
opts.onClear?.();
|
|
217
|
+
teardown(false);
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
['清除'],
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
panel.append(head, swatchGrid, native, hexInput);
|
|
226
|
+
if (footKids.length) panel.append(h('div', { class: 'sm-color-picker__foot' }, footKids));
|
|
227
|
+
|
|
228
|
+
layer.append(panel);
|
|
229
|
+
guardInlineEditFocus(layer);
|
|
230
|
+
opts.layerRoot.append(layer);
|
|
231
|
+
positionLayer(layer, opts.anchor, opts.layerRoot);
|
|
232
|
+
|
|
233
|
+
closeBtn.addEventListener('click', (e) => {
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
teardown(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const onKey = (e: KeyboardEvent) => {
|
|
239
|
+
if (e.key === 'Escape') {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
e.stopPropagation();
|
|
242
|
+
if (initialHex) opts.onPreview?.(initialHex);
|
|
243
|
+
teardown(false);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const onDoc = (ev: MouseEvent) => {
|
|
248
|
+
const t = ev.target as Node;
|
|
249
|
+
if (layer.contains(t) || opts.anchor.contains(t)) return;
|
|
250
|
+
teardown(true);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
document.addEventListener('keydown', onKey, true);
|
|
254
|
+
setTimeout(() => document.addEventListener('mousedown', onDoc, true), 0);
|
|
255
|
+
|
|
256
|
+
activeTeardown = () => teardown(true);
|
|
257
|
+
if (opts.autoFocusHex !== false) hexInput.focus();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface BindColorPickerInputOptions
|
|
261
|
+
extends Omit<ColorPickerPopoverOptions, 'anchor' | 'layerRoot' | 'value'> {
|
|
262
|
+
layerRoot?: HTMLElement;
|
|
263
|
+
getValue?: () => string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** 拦截色卡点击,改为打开自定义色板。 */
|
|
267
|
+
export function bindColorPickerInput(
|
|
268
|
+
pick: HTMLInputElement,
|
|
269
|
+
opts: BindColorPickerInputOptions,
|
|
270
|
+
): void {
|
|
271
|
+
if (!pick.title) pick.title = '点击打开色板;预设色块可双击快速确认';
|
|
272
|
+
|
|
273
|
+
pick.addEventListener('mousedown', (e) => {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
opts.onBeforeOpen?.();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
pick.addEventListener('click', (e) => {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
e.stopPropagation();
|
|
281
|
+
const layerRoot =
|
|
282
|
+
opts.layerRoot ?? (pick.closest('.sm-root') as HTMLElement | null) ?? document.body;
|
|
283
|
+
const value = opts.getValue?.() ?? pick.value;
|
|
284
|
+
openColorPickerPopover({
|
|
285
|
+
...opts,
|
|
286
|
+
anchor: pick,
|
|
287
|
+
layerRoot,
|
|
288
|
+
value,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|