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.
@@ -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 = rgbToHex(f.foreColor);
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
- if (f.fontSize) {
123
- const px = f.fontSize.match(/\d+px/)?.[0];
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._wrapStyle('font-size', v);
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._wrapStyle('font-weight', v);
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 FONT_WEIGHTS) {
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 rgbToHex(rgb: string): string | null {
479
- if (rgb.startsWith('#')) return rgb;
480
- const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
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])}`;