mail-editor-pancake 0.0.7 → 0.0.8
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 +50 -1
- package/package.json +1 -1
- package/packages/blocks/dist/index.js +88 -89
- package/packages/blocks/dist/index.js.map +1 -1
- package/packages/blocks/src/text.ts +21 -14
- package/packages/core/dist/index.d.ts +45 -4
- package/packages/core/dist/index.js +2252 -1449
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/style.css +1 -1
- package/packages/core/src/editor/Canvas.ts +68 -34
- package/packages/core/src/editor/ColorPickerPopover.ts +291 -0
- package/packages/core/src/editor/Editor.ts +72 -2
- 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 +306 -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 +378 -25
- package/packages/core/src/index.ts +12 -1
- package/packages/core/src/store/store.ts +44 -3
- package/packages/core/src/types.ts +6 -0
- package/packages/core/src/utils/richTextCommand.ts +30 -0
- package/packages/core/src/utils/sectionLayout.ts +13 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { bindColorPickerInput } from './ColorPickerPopover';
|
|
1
2
|
import { h } from '../utils/dom';
|
|
3
|
+
import { FONT_WEIGHT_STEP_OPTIONS, normalizeFontWeightStep } from '../utils/fontWeightSteps';
|
|
2
4
|
import type { InlineEditor, SelectionState } from './InlineEditor';
|
|
3
5
|
|
|
4
6
|
/**
|
|
@@ -34,13 +36,6 @@ const FONT_FAMILIES = [
|
|
|
34
36
|
|
|
35
37
|
const FONT_SIZES = ['12px', '13px', '14px', '15px', '16px', '18px', '20px', '24px', '28px', '32px'];
|
|
36
38
|
|
|
37
|
-
const FONT_WEIGHTS = [
|
|
38
|
-
{ label: '常规', value: '400' },
|
|
39
|
-
{ label: '中等', value: '500' },
|
|
40
|
-
{ label: '半粗', value: '600' },
|
|
41
|
-
{ label: '加粗', value: '700' },
|
|
42
|
-
];
|
|
43
|
-
|
|
44
39
|
export class RichTextToolbar {
|
|
45
40
|
el: HTMLElement;
|
|
46
41
|
private opts: RichTextToolbarOptions;
|
|
@@ -115,14 +110,15 @@ export class RichTextToolbar {
|
|
|
115
110
|
this.btns.unlink.style.display = hasLink ? '' : 'none';
|
|
116
111
|
|
|
117
112
|
if (f.foreColor) {
|
|
118
|
-
const hex =
|
|
113
|
+
const hex = colorToHexInput(f.foreColor);
|
|
119
114
|
if (hex) this.inputColor.value = hex;
|
|
120
115
|
}
|
|
116
|
+
this.inputBgColor.value = f.backColor
|
|
117
|
+
? (colorToHexInput(f.backColor) ?? '#ffffff')
|
|
118
|
+
: '#ffffff';
|
|
121
119
|
if (f.fontName) this.selectFontFamily.value = matchFamily(f.fontName);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (px && FONT_SIZES.includes(px)) this.selectFontSize.value = px;
|
|
125
|
-
}
|
|
120
|
+
this.selectFontSize.value = matchFontSize(f.fontSize);
|
|
121
|
+
this.selectFontWeight.value = matchFontWeight(f.fontWeight);
|
|
126
122
|
|
|
127
123
|
const ed = this.editor;
|
|
128
124
|
requestAnimationFrame(() => {
|
|
@@ -212,7 +208,7 @@ export class RichTextToolbar {
|
|
|
212
208
|
title: '字号',
|
|
213
209
|
onchange: (e: Event) => {
|
|
214
210
|
const v = (e.target as HTMLSelectElement).value;
|
|
215
|
-
if (v) this.
|
|
211
|
+
if (v) this.editor?.applyFontSize(v);
|
|
216
212
|
},
|
|
217
213
|
});
|
|
218
214
|
{
|
|
@@ -234,7 +230,7 @@ export class RichTextToolbar {
|
|
|
234
230
|
title: '字重',
|
|
235
231
|
onchange: (e: Event) => {
|
|
236
232
|
const v = (e.target as HTMLSelectElement).value;
|
|
237
|
-
if (v) this.
|
|
233
|
+
if (v) this.editor?.applyFontWeight(v);
|
|
238
234
|
},
|
|
239
235
|
});
|
|
240
236
|
{
|
|
@@ -243,7 +239,7 @@ export class RichTextToolbar {
|
|
|
243
239
|
placeholder.textContent = '字重';
|
|
244
240
|
this.selectFontWeight.append(placeholder);
|
|
245
241
|
}
|
|
246
|
-
for (const w of
|
|
242
|
+
for (const w of FONT_WEIGHT_STEP_OPTIONS) {
|
|
247
243
|
const opt = document.createElement('option');
|
|
248
244
|
opt.value = w.value;
|
|
249
245
|
opt.textContent = w.label;
|
|
@@ -256,8 +252,15 @@ export class RichTextToolbar {
|
|
|
256
252
|
type: 'color',
|
|
257
253
|
title: '文字颜色',
|
|
258
254
|
value: '#433f3f',
|
|
259
|
-
oninput: (e: Event) => this.editor?.exec('foreColor', (e.target as HTMLInputElement).value),
|
|
260
255
|
}) as HTMLInputElement;
|
|
256
|
+
bindColorPickerInput(this.inputColor, {
|
|
257
|
+
layerRoot: this.opts.positionRoot,
|
|
258
|
+
liveCommit: true,
|
|
259
|
+
autoFocusHex: false,
|
|
260
|
+
onBeforeOpen: () => this.editor?.saveSelection(),
|
|
261
|
+
onCommit: (hex) => this.editor?.exec('foreColor', hex),
|
|
262
|
+
onClosed: () => this.editor?.refocus(),
|
|
263
|
+
});
|
|
261
264
|
|
|
262
265
|
// 文字背景色
|
|
263
266
|
this.inputBgColor = h('input', {
|
|
@@ -265,8 +268,15 @@ export class RichTextToolbar {
|
|
|
265
268
|
type: 'color',
|
|
266
269
|
title: '文字背景色',
|
|
267
270
|
value: '#fff7e6',
|
|
268
|
-
oninput: (e: Event) => this.editor?.exec('hiliteColor', (e.target as HTMLInputElement).value),
|
|
269
271
|
}) as HTMLInputElement;
|
|
272
|
+
bindColorPickerInput(this.inputBgColor, {
|
|
273
|
+
layerRoot: this.opts.positionRoot,
|
|
274
|
+
liveCommit: true,
|
|
275
|
+
autoFocusHex: false,
|
|
276
|
+
onBeforeOpen: () => this.editor?.saveSelection(),
|
|
277
|
+
onCommit: (hex) => this.editor?.exec('hiliteColor', hex),
|
|
278
|
+
onClosed: () => this.editor?.refocus(),
|
|
279
|
+
});
|
|
270
280
|
|
|
271
281
|
// 链接:插入链接(无链接时显示)
|
|
272
282
|
this.btns.link = this._iconBtn(
|
|
@@ -361,30 +371,6 @@ export class RichTextToolbar {
|
|
|
361
371
|
this.inputLink.value = '';
|
|
362
372
|
}
|
|
363
373
|
|
|
364
|
-
private _wrapStyle(prop: string, value: string) {
|
|
365
|
-
if (!this.editor) return;
|
|
366
|
-
// execCommand 没有"任意 inline style"命令;自己处理:把存档选区的内容包到一个 span。
|
|
367
|
-
this.editor.refocus(); // focus + restoreSelection
|
|
368
|
-
const sel = window.getSelection();
|
|
369
|
-
if (!sel || sel.rangeCount === 0) return;
|
|
370
|
-
const range = sel.getRangeAt(0);
|
|
371
|
-
if (range.collapsed) return; // 没选中文字,无意义
|
|
372
|
-
const span = document.createElement('span');
|
|
373
|
-
span.setAttribute('style', `${prop}:${value};`);
|
|
374
|
-
try {
|
|
375
|
-
span.appendChild(range.extractContents());
|
|
376
|
-
range.insertNode(span);
|
|
377
|
-
// 选中 span 内容方便连续操作
|
|
378
|
-
const newRange = document.createRange();
|
|
379
|
-
newRange.selectNodeContents(span);
|
|
380
|
-
sel.removeAllRanges();
|
|
381
|
-
sel.addRange(newRange);
|
|
382
|
-
this.editor.saveSelection();
|
|
383
|
-
} catch {
|
|
384
|
-
// 跨节点结构复杂时 extractContents 偶尔抛错,忽略并保持原样
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
374
|
/**
|
|
389
375
|
* 相对 `positionRoot` 定位:默认贴在**正文容器**上沿上方,避免压在选区文字上;
|
|
390
376
|
* 若顶端空间不足则贴在容器下沿下方。
|
|
@@ -475,9 +461,45 @@ function matchFamily(name: string): string {
|
|
|
475
461
|
return '';
|
|
476
462
|
}
|
|
477
463
|
|
|
478
|
-
function
|
|
479
|
-
if (
|
|
480
|
-
const m =
|
|
464
|
+
function matchFontSize(raw: string | null): string {
|
|
465
|
+
if (!raw) return '';
|
|
466
|
+
const m = raw.trim().match(/^(\d+(?:\.\d+)?)px$/i);
|
|
467
|
+
if (!m) return '';
|
|
468
|
+
const px = `${Math.round(parseFloat(m[1]))}px`;
|
|
469
|
+
if (FONT_SIZES.includes(px)) return px;
|
|
470
|
+
const num = parseFloat(px);
|
|
471
|
+
let best = FONT_SIZES[0];
|
|
472
|
+
let bestD = Infinity;
|
|
473
|
+
for (const s of FONT_SIZES) {
|
|
474
|
+
const d = Math.abs(parseFloat(s) - num);
|
|
475
|
+
if (d < bestD) {
|
|
476
|
+
bestD = d;
|
|
477
|
+
best = s;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return best;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function matchFontWeight(raw: string | null): string {
|
|
484
|
+
if (!raw) return '';
|
|
485
|
+
const step = normalizeFontWeightStep(raw);
|
|
486
|
+
return FONT_WEIGHT_STEP_OPTIONS.some((w) => w.value === step) ? step : '';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function colorToHexInput(color: string): string | null {
|
|
490
|
+
const s = color.trim();
|
|
491
|
+
if (!s) return null;
|
|
492
|
+
if (s.startsWith('#')) {
|
|
493
|
+
if (/^#[0-9a-f]{3}$/i.test(s)) {
|
|
494
|
+
const r = s[1];
|
|
495
|
+
const g = s[2];
|
|
496
|
+
const b = s[3];
|
|
497
|
+
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
|
498
|
+
}
|
|
499
|
+
if (/^#[0-9a-f]{6}$/i.test(s)) return s.toLowerCase();
|
|
500
|
+
return s;
|
|
501
|
+
}
|
|
502
|
+
const m = s.match(/^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i);
|
|
481
503
|
if (!m) return null;
|
|
482
504
|
const toHex = (n: string) => Number(n).toString(16).padStart(2, '0');
|
|
483
505
|
return `#${toHex(m[1])}${toHex(m[2])}${toHex(m[3])}`;
|