mail-editor-pancake 0.0.0 → 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 +79 -6
- package/package.json +1 -1
- package/packages/blocks/dist/index.d.ts +55 -3
- package/packages/blocks/dist/index.js +267 -208
- package/packages/blocks/dist/index.js.map +1 -1
- package/packages/blocks/node_modules/.bin/resolve +21 -0
- package/packages/blocks/package.json +1 -0
- package/packages/blocks/src/button.ts +1 -0
- package/packages/blocks/src/hero.ts +2 -0
- package/packages/blocks/src/image.ts +1 -0
- package/packages/blocks/src/index.ts +19 -2
- package/packages/blocks/src/logo.ts +1 -0
- package/packages/blocks/src/social.ts +47 -22
- package/packages/blocks/src/socialGroup.ts +49 -22
- package/packages/blocks/src/socialShared.ts +39 -2
- package/packages/blocks/src/text.ts +25 -19
- package/packages/blocks/tsconfig.json +0 -3
- package/packages/blocks/vite.config.ts +2 -0
- package/packages/core/dist/index.d.ts +90 -7
- package/packages/core/dist/index.js +2505 -1462
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/style.css +1 -1
- package/packages/core/src/editor/Canvas.ts +107 -53
- package/packages/core/src/editor/ColorPickerPopover.ts +291 -0
- package/packages/core/src/editor/Editor.ts +129 -8
- package/packages/core/src/editor/ExportModal.ts +2 -1
- package/packages/core/src/editor/FocusBreadcrumb.ts +120 -0
- package/packages/core/src/editor/ImageGalleryModal.ts +3 -3
- package/packages/core/src/editor/InlineEditor.ts +308 -66
- package/packages/core/src/editor/LeftPanel.ts +1 -2
- package/packages/core/src/editor/PreviewModal.ts +77 -7
- package/packages/core/src/editor/RichTextToolbar.ts +66 -44
- package/packages/core/src/editor/RightPanel.ts +773 -171
- package/packages/core/src/editor/SourceView.ts +2 -1
- package/packages/core/src/editor/Topbar.ts +150 -5
- package/packages/core/src/editor/styles.css +644 -81
- package/packages/core/src/index.ts +18 -1
- package/packages/core/src/renderer/mjml.ts +2 -1
- package/packages/core/src/store/store.ts +44 -3
- package/packages/core/src/types.ts +34 -3
- package/packages/core/src/utils/fontWeightSteps.ts +31 -0
- package/packages/core/src/utils/lockedMjml.ts +2 -1
- package/packages/core/src/utils/richTextCommand.ts +30 -0
- package/packages/core/src/utils/sectionLayout.ts +13 -0
- package/playground/vanilla/src/main.ts +1 -1
|
@@ -2,18 +2,30 @@ import { EditorView, basicSetup } from 'codemirror';
|
|
|
2
2
|
import { html as cmHtml } from '@codemirror/lang-html';
|
|
3
3
|
import { EditorState } from '@codemirror/state';
|
|
4
4
|
import type { Registry } from '../registry/registry';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
findBlockLocation,
|
|
7
|
+
findSection,
|
|
8
|
+
isTwoColumnLayout,
|
|
9
|
+
setSectionLayout,
|
|
10
|
+
type Store,
|
|
11
|
+
type DocChangedDetail,
|
|
12
|
+
} from '../store/store';
|
|
6
13
|
import type {
|
|
7
14
|
Block,
|
|
8
15
|
BlockSchemaField,
|
|
9
16
|
EditorUiOptions,
|
|
10
17
|
EmailDoc,
|
|
11
18
|
Section,
|
|
19
|
+
SectionLayout,
|
|
12
20
|
} from '../types';
|
|
13
21
|
import { defaultSocialIconBackground } from '../socialDefaults';
|
|
22
|
+
import { bindColorPickerInput } from './ColorPickerPopover';
|
|
23
|
+
import { normalizeAccentHex } from '../utils/accentColor';
|
|
14
24
|
import { clear, h } from '../utils/dom';
|
|
15
25
|
import { metaWidthInputString, parseMetaWidthFromUserInput, parseSectionWidthFromUserInput, sectionWidthInputString } from '../utils/contentWidth';
|
|
26
|
+
import { FONT_WEIGHT_STEP_OPTIONS, normalizeFontWeightStep } from '../utils/fontWeightSteps';
|
|
16
27
|
import type { ImageAssetsHandlers, ImageFieldContext } from './imageAssets';
|
|
28
|
+
import { FocusBreadcrumb } from './FocusBreadcrumb';
|
|
17
29
|
import { openImageGalleryModal } from './ImageGalleryModal';
|
|
18
30
|
|
|
19
31
|
/** 与社交组 block 中每行元素结构一致(core 不依赖 blocks) */
|
|
@@ -47,6 +59,12 @@ function layoutWidthNumeric(raw: string, mode: 'px' | '%'): number {
|
|
|
47
59
|
export interface RightPanelOptions {
|
|
48
60
|
store: Store;
|
|
49
61
|
registry: Registry;
|
|
62
|
+
/** 聚焦路径根节点文案(文档级),默认「邮件」 */
|
|
63
|
+
docRootLabel?: string;
|
|
64
|
+
/** 点击路径中的 Section 层级 */
|
|
65
|
+
onFocusSection: (sectionId: string) => void;
|
|
66
|
+
/** 点击路径中的文档级层级(等同 Esc 从 Section 上浮) */
|
|
67
|
+
onFocusDocument: () => void;
|
|
50
68
|
/**
|
|
51
69
|
* 可选。配置后,schema 中 `type: 'image'` 的字段会显示「上传 / 图床」按钮并调用对应回调。
|
|
52
70
|
*/
|
|
@@ -64,6 +82,9 @@ type RightTab = 'props' | 'code';
|
|
|
64
82
|
export class RightPanel {
|
|
65
83
|
el: HTMLElement;
|
|
66
84
|
private opts: RightPanelOptions;
|
|
85
|
+
private headEl: HTMLElement;
|
|
86
|
+
private contentEl: HTMLElement;
|
|
87
|
+
private focusCrumb: FocusBreadcrumb;
|
|
67
88
|
private currentTab: RightTab = 'props';
|
|
68
89
|
private codeView: EditorView | null = null;
|
|
69
90
|
|
|
@@ -78,6 +99,17 @@ export class RightPanel {
|
|
|
78
99
|
constructor(opts: RightPanelOptions) {
|
|
79
100
|
this.opts = opts;
|
|
80
101
|
this.el = h('aside', { class: 'sm-panel sm-panel--right' });
|
|
102
|
+
this.focusCrumb = new FocusBreadcrumb({
|
|
103
|
+
store: opts.store,
|
|
104
|
+
registry: opts.registry,
|
|
105
|
+
docRootLabel: opts.docRootLabel ?? '邮件',
|
|
106
|
+
onFocusDocument: () => opts.onFocusDocument(),
|
|
107
|
+
onFocusSection: (id) => opts.onFocusSection(id),
|
|
108
|
+
});
|
|
109
|
+
this.headEl = h('div', { class: 'sm-panel__head' }, [this.focusCrumb.el]);
|
|
110
|
+
this.contentEl = h('div', { class: 'sm-panel__body' });
|
|
111
|
+
this.el.append(this.headEl, this.contentEl);
|
|
112
|
+
this._syncHeadVisibility();
|
|
81
113
|
this._render();
|
|
82
114
|
opts.store.subscribe((detail?: DocChangedDetail) => {
|
|
83
115
|
if (detail?.source === 'history') {
|
|
@@ -94,10 +126,17 @@ export class RightPanel {
|
|
|
94
126
|
});
|
|
95
127
|
opts.store.subscribeSelection(() => {
|
|
96
128
|
this.currentTab = 'props';
|
|
129
|
+
this._syncHeadVisibility();
|
|
97
130
|
this._render();
|
|
98
131
|
});
|
|
99
132
|
}
|
|
100
133
|
|
|
134
|
+
/** 文档级(无选中)时不展示面包屑,避免与「邮件设置」标题重复 */
|
|
135
|
+
private _syncHeadVisibility() {
|
|
136
|
+
const show = !!this.opts.store.selection;
|
|
137
|
+
this.headEl.hidden = !show;
|
|
138
|
+
}
|
|
139
|
+
|
|
101
140
|
/** 焦点在右栏可编辑控件上时不要整栏重渲染(避免输入/CodeMirror 失焦) */
|
|
102
141
|
private _shouldSkipRenderBecauseFocused(): boolean {
|
|
103
142
|
const ae = document.activeElement as HTMLElement | null;
|
|
@@ -143,20 +182,20 @@ export class RightPanel {
|
|
|
143
182
|
|
|
144
183
|
private _render() {
|
|
145
184
|
this._destroyCodeView();
|
|
146
|
-
clear(this.
|
|
185
|
+
clear(this.contentEl);
|
|
147
186
|
|
|
148
187
|
const sel = this.opts.store.selection;
|
|
149
188
|
const doc = this.opts.store.doc;
|
|
150
189
|
|
|
151
190
|
if (!sel) {
|
|
152
|
-
this.
|
|
191
|
+
this.contentEl.append(this._renderDocForm(doc));
|
|
153
192
|
return;
|
|
154
193
|
}
|
|
155
194
|
|
|
156
195
|
if (sel.kind === 'section') {
|
|
157
196
|
const section = findSection(doc, sel.sectionId);
|
|
158
197
|
if (!section) return;
|
|
159
|
-
this.
|
|
198
|
+
this.contentEl.append(this._renderSectionForm(section));
|
|
160
199
|
return;
|
|
161
200
|
}
|
|
162
201
|
|
|
@@ -167,8 +206,8 @@ export class RightPanel {
|
|
|
167
206
|
this._tabBtn('props', '属性'),
|
|
168
207
|
this._tabBtn('code', '代码'),
|
|
169
208
|
]);
|
|
170
|
-
this.
|
|
171
|
-
this.
|
|
209
|
+
this.contentEl.append(tabs);
|
|
210
|
+
this.contentEl.append(
|
|
172
211
|
h('div', { class: 'sm-empty-form' }, ['该组件可能已被删除,请重新选中画布中的块。']),
|
|
173
212
|
);
|
|
174
213
|
return;
|
|
@@ -179,11 +218,11 @@ export class RightPanel {
|
|
|
179
218
|
this._tabBtn('props', '属性'),
|
|
180
219
|
this._tabBtn('code', '代码'),
|
|
181
220
|
]);
|
|
182
|
-
this.
|
|
221
|
+
this.contentEl.append(tabs);
|
|
183
222
|
|
|
184
223
|
if (this.currentTab === 'props') {
|
|
185
224
|
if (loc.block.lockedMjml) {
|
|
186
|
-
this.
|
|
225
|
+
this.contentEl.append(
|
|
187
226
|
h('div', { class: 'sm-empty-form' }, [
|
|
188
227
|
'该组件已被代码模式锁定。',
|
|
189
228
|
h('div', { style: 'margin-top:8px;' }, [
|
|
@@ -205,14 +244,14 @@ export class RightPanel {
|
|
|
205
244
|
]),
|
|
206
245
|
);
|
|
207
246
|
} else if (def) {
|
|
208
|
-
this.
|
|
247
|
+
this.contentEl.append(this._renderBlockForm(loc.block, def.schema));
|
|
209
248
|
} else {
|
|
210
|
-
this.
|
|
249
|
+
this.contentEl.append(
|
|
211
250
|
h('div', { class: 'sm-empty-form' }, [`未注册的组件: ${loc.block.type}`]),
|
|
212
251
|
);
|
|
213
252
|
}
|
|
214
253
|
} else {
|
|
215
|
-
this.
|
|
254
|
+
this.contentEl.append(this._renderBlockCodeView(loc.block));
|
|
216
255
|
}
|
|
217
256
|
}
|
|
218
257
|
|
|
@@ -306,7 +345,7 @@ export class RightPanel {
|
|
|
306
345
|
'doc:styles.fontSize',
|
|
307
346
|
),
|
|
308
347
|
this._docFontWeightField(
|
|
309
|
-
doc.styles.fontWeight ?? '
|
|
348
|
+
doc.styles.fontWeight ?? '400',
|
|
310
349
|
(v) =>
|
|
311
350
|
this.opts.store.update((d) => {
|
|
312
351
|
d.styles.fontWeight = v;
|
|
@@ -351,6 +390,19 @@ export class RightPanel {
|
|
|
351
390
|
const a = section.attrs;
|
|
352
391
|
return h('form', { class: 'sm-form', onsubmit: (e: Event) => e.preventDefault() }, [
|
|
353
392
|
h('div', { class: 'sm-panel__title' }, ['Section 设置']),
|
|
393
|
+
...(isTwoColumnLayout(section.layout)
|
|
394
|
+
? [
|
|
395
|
+
this._twoColumnRatioField(
|
|
396
|
+
section.layout,
|
|
397
|
+
(layout) =>
|
|
398
|
+
this.opts.store.update((d) => {
|
|
399
|
+
const s = findSection(d, section.id);
|
|
400
|
+
if (s) setSectionLayout(s, layout);
|
|
401
|
+
}),
|
|
402
|
+
`section:${section.id}:layout`,
|
|
403
|
+
),
|
|
404
|
+
]
|
|
405
|
+
: []),
|
|
354
406
|
this._colorField(
|
|
355
407
|
'背景色',
|
|
356
408
|
a.backgroundColor ?? '',
|
|
@@ -360,6 +412,7 @@ export class RightPanel {
|
|
|
360
412
|
if (s) s.attrs.backgroundColor = v || undefined;
|
|
361
413
|
}),
|
|
362
414
|
`section:${section.id}:attrs.bg`,
|
|
415
|
+
'留空=透明',
|
|
363
416
|
),
|
|
364
417
|
this._layoutWidthField(
|
|
365
418
|
'区域宽度',
|
|
@@ -439,12 +492,16 @@ export class RightPanel {
|
|
|
439
492
|
private _field(field: BlockSchemaField, block: Block): HTMLElement {
|
|
440
493
|
const blockId = block.id;
|
|
441
494
|
const fp = `block:${blockId}:${field.key}`;
|
|
442
|
-
const
|
|
495
|
+
const def = this.opts.registry.get(block.type);
|
|
496
|
+
const defaults = def?.defaultProps as Record<string, unknown> | undefined;
|
|
497
|
+
const raw = (block.props as Record<string, unknown>)[field.key];
|
|
498
|
+
const value: unknown =
|
|
499
|
+
raw !== undefined && raw !== null ? raw : defaults?.[field.key];
|
|
443
500
|
const onChange = (v: any) => this._updateBlock(blockId, field.key, v);
|
|
444
501
|
|
|
445
502
|
switch (field.type) {
|
|
446
503
|
case 'textarea':
|
|
447
|
-
return this._textareaField(field.label, value ?? '', onChange, field.help, fp);
|
|
504
|
+
return this._textareaField(field.label, String(value ?? ''), onChange, field.help, fp);
|
|
448
505
|
case 'number':
|
|
449
506
|
return this._numberField(
|
|
450
507
|
field.label,
|
|
@@ -455,16 +512,54 @@ export class RightPanel {
|
|
|
455
512
|
field.step ?? 1,
|
|
456
513
|
fp,
|
|
457
514
|
);
|
|
458
|
-
case 'color':
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
515
|
+
case 'color': {
|
|
516
|
+
const rawColor = (block.props as Record<string, unknown>)[field.key];
|
|
517
|
+
const stored =
|
|
518
|
+
rawColor !== undefined && rawColor !== null ? String(rawColor).trim() : '';
|
|
519
|
+
return this._colorField(
|
|
520
|
+
field.label,
|
|
521
|
+
stored,
|
|
522
|
+
(v) => onChange(v.trim()),
|
|
523
|
+
fp,
|
|
524
|
+
field.placeholder ?? '',
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
case 'select': {
|
|
528
|
+
const isFw = field.key === 'fontWeight' || field.key === 'labelFontWeight';
|
|
529
|
+
const opts = field.options?.length
|
|
530
|
+
? field.options
|
|
531
|
+
: isFw
|
|
532
|
+
? FONT_WEIGHT_STEP_OPTIONS
|
|
533
|
+
: [];
|
|
534
|
+
if (isFw && field.inheritGlobal) {
|
|
535
|
+
const stored =
|
|
536
|
+
raw !== undefined && raw !== null ? String(raw).trim() : '';
|
|
537
|
+
return this._fontWeightInheritField(
|
|
538
|
+
field.label,
|
|
539
|
+
stored,
|
|
540
|
+
normalizeFontWeightStep(this.opts.store.doc.styles.fontWeight),
|
|
541
|
+
(v) => onChange(v),
|
|
542
|
+
fp,
|
|
543
|
+
opts,
|
|
544
|
+
field.help,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
const displayVal = isFw
|
|
548
|
+
? normalizeFontWeightStep(String(value ?? ''))
|
|
549
|
+
: String(value ?? '');
|
|
550
|
+
if (isFw || field.selectVariant === 'segmented') {
|
|
551
|
+
const el = this._segmentedSelectField(field.label, displayVal, opts, onChange, fp);
|
|
552
|
+
if (field.help) el.append(h('div', { class: 'sm-field__help' }, [field.help]));
|
|
553
|
+
return el;
|
|
554
|
+
}
|
|
555
|
+
return this._selectField(field.label, displayVal, opts, onChange, fp);
|
|
556
|
+
}
|
|
462
557
|
case 'switch':
|
|
463
558
|
return this._switchField(field.label, !!value, onChange, field.help, fp);
|
|
464
559
|
case 'image':
|
|
465
560
|
return this._imageField(
|
|
466
561
|
field.label,
|
|
467
|
-
value ?? '',
|
|
562
|
+
String(value ?? ''),
|
|
468
563
|
onChange,
|
|
469
564
|
field.placeholder ?? 'https://',
|
|
470
565
|
fp,
|
|
@@ -473,32 +568,33 @@ export class RightPanel {
|
|
|
473
568
|
case 'url':
|
|
474
569
|
case 'text': {
|
|
475
570
|
if (field.key === 'width' && this._preferSliderControls()) {
|
|
476
|
-
|
|
571
|
+
return this._layoutWidthField(
|
|
477
572
|
field.label,
|
|
478
573
|
String(value ?? ''),
|
|
479
574
|
(v) => onChange(v),
|
|
480
575
|
fp,
|
|
481
576
|
field.help,
|
|
482
577
|
);
|
|
483
|
-
if (field.placeholder && !field.help) {
|
|
484
|
-
el.append(h('div', { class: 'sm-field__help' }, [field.placeholder]));
|
|
485
|
-
}
|
|
486
|
-
return el;
|
|
487
578
|
}
|
|
488
|
-
if (field.key === 'fontSize' &&
|
|
489
|
-
const
|
|
579
|
+
if (field.key === 'fontSize' && field.inheritGlobal) {
|
|
580
|
+
const rawFs =
|
|
581
|
+
raw !== undefined && raw !== null ? String(raw).trim() : '';
|
|
582
|
+
return this._fontSizeSliderField(
|
|
490
583
|
field.label,
|
|
491
|
-
|
|
492
|
-
(v) => onChange(v),
|
|
584
|
+
rawFs,
|
|
585
|
+
(v) => onChange(v.trim()),
|
|
493
586
|
fp,
|
|
494
587
|
field.help,
|
|
588
|
+
this.opts.store.doc.styles.fontSize,
|
|
495
589
|
);
|
|
496
|
-
if (field.placeholder && !field.help) {
|
|
497
|
-
el.append(h('div', { class: 'sm-field__help' }, [field.placeholder]));
|
|
498
|
-
}
|
|
499
|
-
return el;
|
|
500
590
|
}
|
|
501
|
-
const row = this._textField(
|
|
591
|
+
const row = this._textField(
|
|
592
|
+
field.label,
|
|
593
|
+
String(value ?? ''),
|
|
594
|
+
onChange,
|
|
595
|
+
field.placeholder ?? '',
|
|
596
|
+
fp,
|
|
597
|
+
);
|
|
502
598
|
if (field.help) row.append(h('div', { class: 'sm-field__help' }, [field.help]));
|
|
503
599
|
return row;
|
|
504
600
|
}
|
|
@@ -528,16 +624,22 @@ export class RightPanel {
|
|
|
528
624
|
});
|
|
529
625
|
}, `${fp}:pad`, resetQuad);
|
|
530
626
|
}
|
|
531
|
-
case 'socialLinkList':
|
|
627
|
+
case 'socialLinkList': {
|
|
628
|
+
const rows = Array.isArray(value)
|
|
629
|
+
? (value as SocialLinkRow[])
|
|
630
|
+
: Array.isArray(defaults?.[field.key])
|
|
631
|
+
? (defaults[field.key] as SocialLinkRow[])
|
|
632
|
+
: [];
|
|
532
633
|
return this._socialLinkListField(
|
|
533
634
|
field.label,
|
|
534
|
-
|
|
635
|
+
rows,
|
|
535
636
|
field.options ?? [],
|
|
536
637
|
blockId,
|
|
537
638
|
field.key,
|
|
538
639
|
field.help,
|
|
539
640
|
fp,
|
|
540
641
|
);
|
|
642
|
+
}
|
|
541
643
|
default:
|
|
542
644
|
return h('div');
|
|
543
645
|
}
|
|
@@ -653,16 +755,18 @@ export class RightPanel {
|
|
|
653
755
|
...(focusPrefix ? { 'data-sm-focus': `${focusPrefix}:row:${i}:icon` } : {}),
|
|
654
756
|
oninput: (e: Event) => syncItem(i, { iconSrc: (e.target as HTMLInputElement).value }),
|
|
655
757
|
}),
|
|
656
|
-
h('input', {
|
|
657
|
-
class: 'sm-social-link-list__color sm-social-link-list__color--compact',
|
|
658
|
-
type: 'color',
|
|
659
|
-
title: '图标背景色',
|
|
660
|
-
value: bgDisplay.length >= 4 ? bgDisplay : bgResolved,
|
|
661
|
-
...(focusPrefix ? { 'data-sm-focus': `${focusPrefix}:row:${i}:bg` } : {}),
|
|
662
|
-
oninput: (e: Event) =>
|
|
663
|
-
syncItem(i, { backgroundColor: (e.target as HTMLInputElement).value }),
|
|
664
|
-
}),
|
|
665
758
|
);
|
|
759
|
+
const bgPick = h('input', {
|
|
760
|
+
class: 'sm-social-link-list__color sm-social-link-list__color--compact',
|
|
761
|
+
type: 'color',
|
|
762
|
+
value: bgDisplay.length >= 4 ? bgDisplay : bgResolved,
|
|
763
|
+
...(focusPrefix ? { 'data-sm-focus': `${focusPrefix}:row:${i}:bg` } : {}),
|
|
764
|
+
}) as HTMLInputElement;
|
|
765
|
+
bindColorPickerInput(bgPick, {
|
|
766
|
+
liveCommit: true,
|
|
767
|
+
onCommit: (hex) => syncItem(i, { backgroundColor: hex }),
|
|
768
|
+
});
|
|
769
|
+
extras.append(bgPick);
|
|
666
770
|
rowWrap.append(extras);
|
|
667
771
|
|
|
668
772
|
listEl.append(rowWrap);
|
|
@@ -886,21 +990,53 @@ export class RightPanel {
|
|
|
886
990
|
};
|
|
887
991
|
|
|
888
992
|
if (!this._preferSliderControls()) {
|
|
993
|
+
const inp = h('input', {
|
|
994
|
+
class: 'sm-input',
|
|
995
|
+
type: 'number',
|
|
996
|
+
value: String(value),
|
|
997
|
+
min,
|
|
998
|
+
max,
|
|
999
|
+
step,
|
|
1000
|
+
...(focusToken ? { 'data-sm-focus': focusToken } : {}),
|
|
1001
|
+
}) as HTMLInputElement;
|
|
1002
|
+
|
|
1003
|
+
const commitPlain = () => {
|
|
1004
|
+
const t = inp.value.trim();
|
|
1005
|
+
const fb = snap(value);
|
|
1006
|
+
if (t === '') {
|
|
1007
|
+
inp.value = String(fb);
|
|
1008
|
+
onChange(fb);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const v = Number(t);
|
|
1012
|
+
const s = snap(Number.isNaN(v) ? fb : v);
|
|
1013
|
+
inp.value = String(s);
|
|
1014
|
+
onChange(s);
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
/** min>0 时中途输入会被 snap 打断(如想输入 16 会先变成 10),仅失焦/Enter 提交 */
|
|
1018
|
+
if (min > 0) {
|
|
1019
|
+
inp.addEventListener('blur', commitPlain);
|
|
1020
|
+
inp.addEventListener('keydown', (e) => {
|
|
1021
|
+
if ((e as KeyboardEvent).key === 'Enter') inp.blur();
|
|
1022
|
+
});
|
|
1023
|
+
} else {
|
|
1024
|
+
inp.addEventListener('input', () => {
|
|
1025
|
+
const t = inp.value.trim();
|
|
1026
|
+
if (t === '') return;
|
|
1027
|
+
const v = Number(t);
|
|
1028
|
+
if (Number.isNaN(v)) return;
|
|
1029
|
+
onChange(snap(v));
|
|
1030
|
+
});
|
|
1031
|
+
inp.addEventListener('blur', commitPlain);
|
|
1032
|
+
inp.addEventListener('keydown', (e) => {
|
|
1033
|
+
if ((e as KeyboardEvent).key === 'Enter') commitPlain();
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
889
1037
|
return h('div', { class: 'sm-field' }, [
|
|
890
1038
|
h('label', { class: 'sm-field__label' }, [label]),
|
|
891
|
-
|
|
892
|
-
class: 'sm-input',
|
|
893
|
-
type: 'number',
|
|
894
|
-
value: String(value),
|
|
895
|
-
min,
|
|
896
|
-
max,
|
|
897
|
-
step,
|
|
898
|
-
...(focusToken ? { 'data-sm-focus': focusToken } : {}),
|
|
899
|
-
oninput: (e: Event) => {
|
|
900
|
-
const v = Number((e.target as HTMLInputElement).value);
|
|
901
|
-
if (!Number.isNaN(v)) onChange(v);
|
|
902
|
-
},
|
|
903
|
-
}),
|
|
1039
|
+
inp,
|
|
904
1040
|
]);
|
|
905
1041
|
}
|
|
906
1042
|
|
|
@@ -911,28 +1047,54 @@ export class RightPanel {
|
|
|
911
1047
|
max,
|
|
912
1048
|
step,
|
|
913
1049
|
value: String(v0),
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
num.value = String(s);
|
|
917
|
-
onChange(s);
|
|
918
|
-
},
|
|
919
|
-
});
|
|
1050
|
+
}) as HTMLInputElement;
|
|
1051
|
+
|
|
920
1052
|
const num = h('input', {
|
|
921
1053
|
class: 'sm-input',
|
|
922
1054
|
type: 'number',
|
|
923
|
-
value: String(v0),
|
|
924
1055
|
min,
|
|
925
1056
|
max,
|
|
926
1057
|
step,
|
|
1058
|
+
value: String(v0),
|
|
927
1059
|
...(focusToken ? { 'data-sm-focus': focusToken } : {}),
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1060
|
+
}) as HTMLInputElement;
|
|
1061
|
+
|
|
1062
|
+
const syncFromSlider = (n: number) => {
|
|
1063
|
+
const s = snap(n);
|
|
1064
|
+
range.value = String(s);
|
|
1065
|
+
num.value = String(s);
|
|
1066
|
+
onChange(s);
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
range.addEventListener('input', () => {
|
|
1070
|
+
syncFromSlider(Number(range.value));
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
num.addEventListener('input', () => {
|
|
1074
|
+
const t = num.value.trim();
|
|
1075
|
+
if (t === '') return;
|
|
1076
|
+
const raw = Number(t);
|
|
1077
|
+
if (Number.isNaN(raw)) return;
|
|
1078
|
+
range.value = String(snap(raw));
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
num.addEventListener('blur', () => {
|
|
1082
|
+
const t = num.value.trim();
|
|
1083
|
+
const fb = snap(Number(range.value));
|
|
1084
|
+
if (t === '') {
|
|
1085
|
+
syncFromSlider(fb);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const raw = Number(t);
|
|
1089
|
+
if (Number.isNaN(raw)) {
|
|
1090
|
+
syncFromSlider(fb);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
syncFromSlider(raw);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
num.addEventListener('keydown', (e) => {
|
|
1097
|
+
if ((e as KeyboardEvent).key === 'Enter') num.blur();
|
|
936
1098
|
});
|
|
937
1099
|
|
|
938
1100
|
return h('div', { class: 'sm-field' }, [
|
|
@@ -945,27 +1107,107 @@ export class RightPanel {
|
|
|
945
1107
|
value: string,
|
|
946
1108
|
onChange: (v: string) => void,
|
|
947
1109
|
focusPrefix?: string,
|
|
1110
|
+
placeholder = '#ffffff',
|
|
948
1111
|
) {
|
|
949
1112
|
const pickToken = focusPrefix ? `${focusPrefix}:pick` : undefined;
|
|
950
1113
|
const textToken = focusPrefix ? `${focusPrefix}:text` : undefined;
|
|
1114
|
+
const pick = h('input', {
|
|
1115
|
+
type: 'color',
|
|
1116
|
+
...(pickToken ? { 'data-sm-focus': pickToken } : {}),
|
|
1117
|
+
}) as HTMLInputElement;
|
|
1118
|
+
const text = h('input', {
|
|
1119
|
+
class: 'sm-input',
|
|
1120
|
+
type: 'text',
|
|
1121
|
+
placeholder,
|
|
1122
|
+
...(textToken ? { 'data-sm-focus': textToken } : {}),
|
|
1123
|
+
}) as HTMLInputElement;
|
|
1124
|
+
|
|
1125
|
+
let lastValid = value.trim();
|
|
1126
|
+
|
|
1127
|
+
const applyPickInherit = () => {
|
|
1128
|
+
pick.value = '#808080';
|
|
1129
|
+
pick.classList.add('sm-color-row__pick--inherit');
|
|
1130
|
+
pick.title = placeholder || '未设置';
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
const applyStored = (stored: string) => {
|
|
1134
|
+
const trimmed = stored.trim();
|
|
1135
|
+
text.value = trimmed;
|
|
1136
|
+
const hex = normalizeAccentHex(trimmed);
|
|
1137
|
+
if (hex) {
|
|
1138
|
+
pick.value = hex;
|
|
1139
|
+
pick.classList.remove('sm-color-row__pick--inherit');
|
|
1140
|
+
pick.title = '';
|
|
1141
|
+
} else {
|
|
1142
|
+
applyPickInherit();
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
applyStored(value);
|
|
1147
|
+
|
|
1148
|
+
const previewPick = (hex: string) => {
|
|
1149
|
+
text.value = hex;
|
|
1150
|
+
pick.value = hex;
|
|
1151
|
+
pick.classList.remove('sm-color-row__pick--inherit');
|
|
1152
|
+
pick.title = '';
|
|
1153
|
+
lastValid = hex;
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
const commitPick = (hex: string) => {
|
|
1157
|
+
previewPick(hex);
|
|
1158
|
+
onChange(hex);
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
bindColorPickerInput(pick, {
|
|
1162
|
+
getValue: () => text.value.trim() || pick.value,
|
|
1163
|
+
placeholder,
|
|
1164
|
+
allowClear: true,
|
|
1165
|
+
onPreview: previewPick,
|
|
1166
|
+
onCommit: commitPick,
|
|
1167
|
+
onClear: () => {
|
|
1168
|
+
applyStored('');
|
|
1169
|
+
lastValid = '';
|
|
1170
|
+
onChange('');
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
text.addEventListener('input', () => {
|
|
1175
|
+
const raw = text.value;
|
|
1176
|
+
const hex = normalizeAccentHex(raw);
|
|
1177
|
+
if (hex) {
|
|
1178
|
+
pick.value = hex;
|
|
1179
|
+
pick.classList.remove('sm-color-row__pick--inherit');
|
|
1180
|
+
pick.title = '';
|
|
1181
|
+
lastValid = hex;
|
|
1182
|
+
} else if (!raw.trim()) {
|
|
1183
|
+
applyPickInherit();
|
|
1184
|
+
lastValid = '';
|
|
1185
|
+
}
|
|
1186
|
+
onChange(raw);
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
text.addEventListener('blur', () => {
|
|
1190
|
+
const raw = text.value.trim();
|
|
1191
|
+
if (!raw) {
|
|
1192
|
+
applyStored('');
|
|
1193
|
+
lastValid = '';
|
|
1194
|
+
onChange('');
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const hex = normalizeAccentHex(raw);
|
|
1198
|
+
if (hex) {
|
|
1199
|
+
applyStored(hex);
|
|
1200
|
+
lastValid = hex;
|
|
1201
|
+
if (hex !== raw) onChange(hex);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
applyStored(lastValid);
|
|
1205
|
+
onChange(lastValid);
|
|
1206
|
+
});
|
|
1207
|
+
|
|
951
1208
|
return h('div', { class: 'sm-field' }, [
|
|
952
1209
|
h('label', { class: 'sm-field__label' }, [label]),
|
|
953
|
-
h('div', { class: 'sm-color-row' }, [
|
|
954
|
-
h('input', {
|
|
955
|
-
type: 'color',
|
|
956
|
-
value: value || '#ffffff',
|
|
957
|
-
...(pickToken ? { 'data-sm-focus': pickToken } : {}),
|
|
958
|
-
oninput: (e: Event) => onChange((e.target as HTMLInputElement).value),
|
|
959
|
-
}),
|
|
960
|
-
h('input', {
|
|
961
|
-
class: 'sm-input',
|
|
962
|
-
type: 'text',
|
|
963
|
-
value,
|
|
964
|
-
placeholder: '#ffffff',
|
|
965
|
-
...(textToken ? { 'data-sm-focus': textToken } : {}),
|
|
966
|
-
oninput: (e: Event) => onChange((e.target as HTMLInputElement).value),
|
|
967
|
-
}),
|
|
968
|
-
]),
|
|
1210
|
+
h('div', { class: 'sm-color-row' }, [pick, text]),
|
|
969
1211
|
]);
|
|
970
1212
|
}
|
|
971
1213
|
private _selectField(
|
|
@@ -992,6 +1234,43 @@ export class RightPanel {
|
|
|
992
1234
|
sel,
|
|
993
1235
|
]);
|
|
994
1236
|
}
|
|
1237
|
+
|
|
1238
|
+
/** select 平铺:与顶栏 sm-segmented 视觉一致,占满右栏宽度 */
|
|
1239
|
+
private _segmentedSelectField(
|
|
1240
|
+
label: string,
|
|
1241
|
+
value: string,
|
|
1242
|
+
options: { label: string; value: string }[],
|
|
1243
|
+
onChange: (v: string) => void,
|
|
1244
|
+
focusToken?: string,
|
|
1245
|
+
wrap = false,
|
|
1246
|
+
) {
|
|
1247
|
+
const group = h('div', {
|
|
1248
|
+
class: `sm-segmented sm-segmented--fill${wrap ? ' sm-segmented--wrap' : ''}`,
|
|
1249
|
+
role: 'group',
|
|
1250
|
+
'aria-label': label,
|
|
1251
|
+
});
|
|
1252
|
+
for (const o of options) {
|
|
1253
|
+
const active = o.value === value;
|
|
1254
|
+
group.append(
|
|
1255
|
+
h(
|
|
1256
|
+
'button',
|
|
1257
|
+
{
|
|
1258
|
+
type: 'button',
|
|
1259
|
+
class: `sm-segmented__item${active ? ' sm-segmented__item--active' : ''}`,
|
|
1260
|
+
'aria-pressed': active ? 'true' : 'false',
|
|
1261
|
+
title: o.label,
|
|
1262
|
+
...(focusToken ? { 'data-sm-focus': `${focusToken}:${o.value}` } : {}),
|
|
1263
|
+
onclick: () => onChange(o.value),
|
|
1264
|
+
},
|
|
1265
|
+
[o.label],
|
|
1266
|
+
),
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
return h('div', { class: 'sm-field' }, [
|
|
1270
|
+
h('label', { class: 'sm-field__label' }, [label]),
|
|
1271
|
+
group,
|
|
1272
|
+
]);
|
|
1273
|
+
}
|
|
995
1274
|
private _switchField(
|
|
996
1275
|
label: string,
|
|
997
1276
|
value: boolean,
|
|
@@ -1036,8 +1315,8 @@ export class RightPanel {
|
|
|
1036
1315
|
];
|
|
1037
1316
|
const emit = () => onChange([...current]);
|
|
1038
1317
|
|
|
1039
|
-
const makePlain = (v: number | undefined, idx: number) =>
|
|
1040
|
-
h('input', {
|
|
1318
|
+
const makePlain = (v: number | undefined, idx: number) => {
|
|
1319
|
+
const inp = h('input', {
|
|
1041
1320
|
class: 'sm-input',
|
|
1042
1321
|
type: 'number',
|
|
1043
1322
|
min: 0,
|
|
@@ -1045,12 +1324,32 @@ export class RightPanel {
|
|
|
1045
1324
|
value: String(v ?? 0),
|
|
1046
1325
|
title: titles[idx],
|
|
1047
1326
|
...(focusPrefix ? { 'data-sm-focus': `${focusPrefix}:${idx}` } : {}),
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1327
|
+
}) as HTMLInputElement;
|
|
1328
|
+
inp.addEventListener('input', () => {
|
|
1329
|
+
const t = inp.value.trim();
|
|
1330
|
+
if (t === '') return;
|
|
1331
|
+
const raw = Number(t);
|
|
1332
|
+
if (Number.isNaN(raw)) return;
|
|
1333
|
+
current[idx] = clampPad(raw);
|
|
1334
|
+
emit();
|
|
1335
|
+
});
|
|
1336
|
+
inp.addEventListener('blur', () => {
|
|
1337
|
+
const t = inp.value.trim();
|
|
1338
|
+
if (t === '') {
|
|
1339
|
+
inp.value = String(current[idx]);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const raw = Number(t);
|
|
1343
|
+
const c = clampPad(Number.isNaN(raw) ? current[idx] : raw);
|
|
1344
|
+
inp.value = String(c);
|
|
1345
|
+
current[idx] = c;
|
|
1346
|
+
emit();
|
|
1347
|
+
});
|
|
1348
|
+
inp.addEventListener('keydown', (e) => {
|
|
1349
|
+
if ((e as KeyboardEvent).key === 'Enter') inp.blur();
|
|
1053
1350
|
});
|
|
1351
|
+
return inp;
|
|
1352
|
+
};
|
|
1054
1353
|
|
|
1055
1354
|
const makeSliderRow = (v: number | undefined, idx: number) => {
|
|
1056
1355
|
const cur = clampPad(v ?? 0);
|
|
@@ -1061,13 +1360,15 @@ export class RightPanel {
|
|
|
1061
1360
|
step: 1,
|
|
1062
1361
|
value: String(cur),
|
|
1063
1362
|
title: titles[idx],
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1363
|
+
}) as HTMLInputElement;
|
|
1364
|
+
|
|
1365
|
+
range.addEventListener('input', () => {
|
|
1366
|
+
const c = clampPad(Number(range.value));
|
|
1367
|
+
current[idx] = c;
|
|
1368
|
+
num.value = String(c);
|
|
1369
|
+
emit();
|
|
1070
1370
|
});
|
|
1371
|
+
|
|
1071
1372
|
const num = h('input', {
|
|
1072
1373
|
class: 'sm-input',
|
|
1073
1374
|
type: 'number',
|
|
@@ -1076,16 +1377,38 @@ export class RightPanel {
|
|
|
1076
1377
|
value: String(cur),
|
|
1077
1378
|
title: titles[idx],
|
|
1078
1379
|
...(focusPrefix ? { 'data-sm-focus': `${focusPrefix}:${idx}` } : {}),
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1380
|
+
}) as HTMLInputElement;
|
|
1381
|
+
|
|
1382
|
+
num.addEventListener('input', () => {
|
|
1383
|
+
const t = num.value.trim();
|
|
1384
|
+
if (t === '') return;
|
|
1385
|
+
const rawN = Number(t);
|
|
1386
|
+
if (Number.isNaN(rawN)) return;
|
|
1387
|
+
const c = clampPad(rawN);
|
|
1388
|
+
range.value = String(c);
|
|
1389
|
+
current[idx] = c;
|
|
1390
|
+
emit();
|
|
1088
1391
|
});
|
|
1392
|
+
|
|
1393
|
+
num.addEventListener('blur', () => {
|
|
1394
|
+
const t = num.value.trim();
|
|
1395
|
+
if (t === '') {
|
|
1396
|
+
num.value = String(current[idx]);
|
|
1397
|
+
range.value = String(current[idx]);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const rawN = Number(t);
|
|
1401
|
+
const c = clampPad(Number.isNaN(rawN) ? current[idx] : rawN);
|
|
1402
|
+
num.value = String(c);
|
|
1403
|
+
range.value = String(c);
|
|
1404
|
+
current[idx] = c;
|
|
1405
|
+
emit();
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
num.addEventListener('keydown', (e) => {
|
|
1409
|
+
if ((e as KeyboardEvent).key === 'Enter') num.blur();
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1089
1412
|
return h('div', { class: 'sm-spacing-row' }, [
|
|
1090
1413
|
h('span', { class: 'sm-spacing-row__axis' }, [titles[idx]]),
|
|
1091
1414
|
range,
|
|
@@ -1126,6 +1449,188 @@ export class RightPanel {
|
|
|
1126
1449
|
]);
|
|
1127
1450
|
}
|
|
1128
1451
|
|
|
1452
|
+
/** 「继承全局」开关行:开=跟随邮件全局,关=块级固定值 */
|
|
1453
|
+
private _globalInheritSwitch(
|
|
1454
|
+
inheritOn: boolean,
|
|
1455
|
+
globalDisplay: string,
|
|
1456
|
+
onToggle: (inherit: boolean) => void,
|
|
1457
|
+
focusToken?: string,
|
|
1458
|
+
): { el: HTMLElement; setInherit: (on: boolean, display?: string) => void } {
|
|
1459
|
+
const input = h('input', {
|
|
1460
|
+
type: 'checkbox',
|
|
1461
|
+
class: 'sm-inherit-switch__input',
|
|
1462
|
+
role: 'switch',
|
|
1463
|
+
...(focusToken ? { 'data-sm-focus': focusToken } : {}),
|
|
1464
|
+
...(inheritOn ? { checked: true } : {}),
|
|
1465
|
+
}) as HTMLInputElement;
|
|
1466
|
+
const valueEl = h('span', {
|
|
1467
|
+
class: 'sm-inherit-switch__value',
|
|
1468
|
+
hidden: !inheritOn,
|
|
1469
|
+
}, [globalDisplay]);
|
|
1470
|
+
const el = h('div', { class: 'sm-inherit-switch' }, [
|
|
1471
|
+
h('label', { class: 'sm-inherit-switch__label' }, [
|
|
1472
|
+
input,
|
|
1473
|
+
h('span', { class: 'sm-inherit-switch__text' }, ['继承全局']),
|
|
1474
|
+
]),
|
|
1475
|
+
valueEl,
|
|
1476
|
+
]);
|
|
1477
|
+
input.addEventListener('change', () => onToggle(input.checked));
|
|
1478
|
+
const setInherit = (on: boolean, display?: string) => {
|
|
1479
|
+
input.checked = on;
|
|
1480
|
+
if (display !== undefined) valueEl.textContent = display;
|
|
1481
|
+
valueEl.hidden = !on;
|
|
1482
|
+
};
|
|
1483
|
+
return { el, setInherit };
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
private _fontWeightInheritField(
|
|
1487
|
+
label: string,
|
|
1488
|
+
stored: string,
|
|
1489
|
+
globalWeight: string,
|
|
1490
|
+
onChange: (v: string) => void,
|
|
1491
|
+
focusToken: string,
|
|
1492
|
+
options: { label: string; value: string }[],
|
|
1493
|
+
help?: string,
|
|
1494
|
+
): HTMLElement {
|
|
1495
|
+
let inheriting = !stored;
|
|
1496
|
+
const group = h('div', {
|
|
1497
|
+
class: 'sm-segmented sm-segmented--fill',
|
|
1498
|
+
role: 'group',
|
|
1499
|
+
'aria-label': label,
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
const syncGroup = () => {
|
|
1503
|
+
group.classList.toggle('sm-segmented--disabled', inheriting);
|
|
1504
|
+
const display = inheriting ? '' : normalizeFontWeightStep(stored || globalWeight);
|
|
1505
|
+
clear(group);
|
|
1506
|
+
for (const o of options) {
|
|
1507
|
+
const active = !inheriting && o.value === display;
|
|
1508
|
+
group.append(
|
|
1509
|
+
h(
|
|
1510
|
+
'button',
|
|
1511
|
+
{
|
|
1512
|
+
type: 'button',
|
|
1513
|
+
class: `sm-segmented__item${active ? ' sm-segmented__item--active' : ''}`,
|
|
1514
|
+
'aria-pressed': active ? 'true' : 'false',
|
|
1515
|
+
title: o.label,
|
|
1516
|
+
...(focusToken ? { 'data-sm-focus': `${focusToken}:${o.value}` } : {}),
|
|
1517
|
+
onclick: () => {
|
|
1518
|
+
inheriting = false;
|
|
1519
|
+
stored = o.value;
|
|
1520
|
+
inheritSwitch.setInherit(false);
|
|
1521
|
+
syncGroup();
|
|
1522
|
+
onChange(o.value);
|
|
1523
|
+
},
|
|
1524
|
+
},
|
|
1525
|
+
[o.label],
|
|
1526
|
+
),
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
const inheritSwitch = this._globalInheritSwitch(
|
|
1532
|
+
inheriting,
|
|
1533
|
+
globalWeight,
|
|
1534
|
+
(on) => {
|
|
1535
|
+
if (on) {
|
|
1536
|
+
inheriting = true;
|
|
1537
|
+
stored = '';
|
|
1538
|
+
inheritSwitch.setInherit(true, globalWeight);
|
|
1539
|
+
syncGroup();
|
|
1540
|
+
onChange('');
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
inheriting = false;
|
|
1544
|
+
stored = globalWeight;
|
|
1545
|
+
inheritSwitch.setInherit(false);
|
|
1546
|
+
syncGroup();
|
|
1547
|
+
onChange(globalWeight);
|
|
1548
|
+
},
|
|
1549
|
+
`${focusToken}:inherit`,
|
|
1550
|
+
);
|
|
1551
|
+
|
|
1552
|
+
syncGroup();
|
|
1553
|
+
|
|
1554
|
+
const wrap = h('div', { class: 'sm-field' }, [
|
|
1555
|
+
h('label', { class: 'sm-field__label' }, [label]),
|
|
1556
|
+
inheritSwitch.el,
|
|
1557
|
+
group,
|
|
1558
|
+
]);
|
|
1559
|
+
if (help) wrap.append(h('div', { class: 'sm-field__help' }, [help]));
|
|
1560
|
+
return wrap;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
private _fontSizeInheritTextField(
|
|
1564
|
+
label: string,
|
|
1565
|
+
stored: string,
|
|
1566
|
+
onChange: (v: string) => void,
|
|
1567
|
+
focusToken: string,
|
|
1568
|
+
globalFontSize: string,
|
|
1569
|
+
help?: string,
|
|
1570
|
+
): HTMLElement {
|
|
1571
|
+
const globalPx = this._snapFontSizeToPx(globalFontSize);
|
|
1572
|
+
let inheriting = !stored.trim();
|
|
1573
|
+
const input = h('input', {
|
|
1574
|
+
class: 'sm-input',
|
|
1575
|
+
type: 'text',
|
|
1576
|
+
...(focusToken ? { 'data-sm-focus': focusToken } : {}),
|
|
1577
|
+
}) as HTMLInputElement;
|
|
1578
|
+
|
|
1579
|
+
const syncInput = () => {
|
|
1580
|
+
input.disabled = inheriting;
|
|
1581
|
+
input.value = inheriting ? '' : stored;
|
|
1582
|
+
input.placeholder = inheriting ? '' : '如 16px';
|
|
1583
|
+
input.classList.toggle('sm-input--inherit-disabled', inheriting);
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
const inheritSwitch = this._globalInheritSwitch(
|
|
1587
|
+
inheriting,
|
|
1588
|
+
`${globalPx}px`,
|
|
1589
|
+
(on) => {
|
|
1590
|
+
if (on) {
|
|
1591
|
+
inheriting = true;
|
|
1592
|
+
stored = '';
|
|
1593
|
+
inheritSwitch.setInherit(true, `${globalPx}px`);
|
|
1594
|
+
syncInput();
|
|
1595
|
+
onChange('');
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
inheriting = false;
|
|
1599
|
+
stored = `${globalPx}px`;
|
|
1600
|
+
inheritSwitch.setInherit(false);
|
|
1601
|
+
syncInput();
|
|
1602
|
+
onChange(stored);
|
|
1603
|
+
},
|
|
1604
|
+
`${focusToken}:inherit`,
|
|
1605
|
+
);
|
|
1606
|
+
|
|
1607
|
+
syncInput();
|
|
1608
|
+
|
|
1609
|
+
input.addEventListener('input', () => {
|
|
1610
|
+
if (inheriting) return;
|
|
1611
|
+
stored = input.value;
|
|
1612
|
+
onChange(stored);
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
input.addEventListener('blur', () => {
|
|
1616
|
+
if (inheriting) return;
|
|
1617
|
+
const t = input.value.trim();
|
|
1618
|
+
if (!t) {
|
|
1619
|
+
stored = `${globalPx}px`;
|
|
1620
|
+
input.value = stored;
|
|
1621
|
+
onChange(stored);
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
const wrap = h('div', { class: 'sm-field' }, [
|
|
1626
|
+
h('label', { class: 'sm-field__label' }, [label]),
|
|
1627
|
+
inheritSwitch.el,
|
|
1628
|
+
input,
|
|
1629
|
+
]);
|
|
1630
|
+
if (help) wrap.append(h('div', { class: 'sm-field__help' }, [help]));
|
|
1631
|
+
return wrap;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1129
1634
|
private _snapFontSizeToPx(value: string): number {
|
|
1130
1635
|
const t = (value ?? '').trim();
|
|
1131
1636
|
const m = t.match(/^(\d+(\.\d+)?)/);
|
|
@@ -1139,49 +1644,128 @@ export class RightPanel {
|
|
|
1139
1644
|
onChange: (v: string) => void,
|
|
1140
1645
|
focusToken: string,
|
|
1141
1646
|
help?: string,
|
|
1647
|
+
globalFontSize?: string,
|
|
1142
1648
|
) {
|
|
1649
|
+
const inheritGlobal = globalFontSize !== undefined;
|
|
1143
1650
|
if (!this._preferSliderControls()) {
|
|
1651
|
+
if (inheritGlobal) {
|
|
1652
|
+
return this._fontSizeInheritTextField(
|
|
1653
|
+
label,
|
|
1654
|
+
value,
|
|
1655
|
+
onChange,
|
|
1656
|
+
focusToken,
|
|
1657
|
+
globalFontSize,
|
|
1658
|
+
help,
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1144
1661
|
return this._textField(label, value ?? '', onChange, '', focusToken);
|
|
1145
1662
|
}
|
|
1663
|
+
|
|
1146
1664
|
const min = 10;
|
|
1147
1665
|
const max = 48;
|
|
1148
1666
|
const step = 1;
|
|
1149
1667
|
const snap = (n: number) => Math.min(max, Math.max(min, Math.round(n)));
|
|
1150
|
-
const
|
|
1668
|
+
const globalPx = snap(this._snapFontSizeToPx(globalFontSize ?? value ?? '16px'));
|
|
1669
|
+
let stored = value.trim();
|
|
1670
|
+
let inheriting = inheritGlobal && !stored;
|
|
1151
1671
|
|
|
1672
|
+
const sliderRow = h('div', { class: 'sm-field__slider-row' });
|
|
1152
1673
|
const range = h('input', {
|
|
1153
1674
|
type: 'range',
|
|
1154
1675
|
min,
|
|
1155
1676
|
max,
|
|
1156
1677
|
step,
|
|
1157
|
-
|
|
1158
|
-
oninput: (e: Event) => {
|
|
1159
|
-
const s = snap(Number((e.target as HTMLInputElement).value));
|
|
1160
|
-
num.value = String(s);
|
|
1161
|
-
onChange(`${s}px`);
|
|
1162
|
-
},
|
|
1163
|
-
});
|
|
1678
|
+
}) as HTMLInputElement;
|
|
1164
1679
|
const num = h('input', {
|
|
1165
1680
|
class: 'sm-input',
|
|
1166
1681
|
type: 'number',
|
|
1167
1682
|
min,
|
|
1168
1683
|
max,
|
|
1169
1684
|
step,
|
|
1170
|
-
value: String(px),
|
|
1171
1685
|
'data-sm-focus': focusToken,
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1686
|
+
}) as HTMLInputElement;
|
|
1687
|
+
sliderRow.append(range, num);
|
|
1688
|
+
|
|
1689
|
+
const syncSlider = () => {
|
|
1690
|
+
sliderRow.classList.toggle('sm-field__slider-row--inherit', inheriting);
|
|
1691
|
+
num.disabled = inheriting;
|
|
1692
|
+
if (inheriting) {
|
|
1693
|
+
range.value = String(globalPx);
|
|
1694
|
+
num.value = '';
|
|
1695
|
+
} else {
|
|
1696
|
+
const s = snap(this._snapFontSizeToPx(stored || `${globalPx}px`));
|
|
1176
1697
|
range.value = String(s);
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1698
|
+
num.value = String(s);
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
let inheritSwitch: ReturnType<RightPanel['_globalInheritSwitch']> | null = null;
|
|
1703
|
+
if (inheritGlobal) {
|
|
1704
|
+
inheritSwitch = this._globalInheritSwitch(
|
|
1705
|
+
inheriting,
|
|
1706
|
+
`${globalPx}px`,
|
|
1707
|
+
(on) => {
|
|
1708
|
+
if (on) {
|
|
1709
|
+
inheriting = true;
|
|
1710
|
+
stored = '';
|
|
1711
|
+
inheritSwitch!.setInherit(true, `${globalPx}px`);
|
|
1712
|
+
syncSlider();
|
|
1713
|
+
onChange('');
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
inheriting = false;
|
|
1717
|
+
stored = `${globalPx}px`;
|
|
1718
|
+
inheritSwitch!.setInherit(false);
|
|
1719
|
+
syncSlider();
|
|
1720
|
+
onChange(stored);
|
|
1721
|
+
},
|
|
1722
|
+
`${focusToken}:inherit`,
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
syncSlider();
|
|
1727
|
+
|
|
1728
|
+
const leaveInherit = () => {
|
|
1729
|
+
if (!inheriting) return;
|
|
1730
|
+
inheriting = false;
|
|
1731
|
+
inheritSwitch?.setInherit(false);
|
|
1732
|
+
syncSlider();
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
range.addEventListener('input', () => {
|
|
1736
|
+
leaveInherit();
|
|
1737
|
+
const s = snap(Number(range.value));
|
|
1738
|
+
num.value = String(s);
|
|
1739
|
+
stored = `${s}px`;
|
|
1740
|
+
onChange(stored);
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
num.addEventListener('input', () => {
|
|
1744
|
+
if (inheriting) return;
|
|
1745
|
+
const rawN = Number(num.value);
|
|
1746
|
+
if (Number.isNaN(rawN)) return;
|
|
1747
|
+
range.value = String(snap(rawN));
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
num.addEventListener('blur', () => {
|
|
1751
|
+
if (inheriting) return;
|
|
1752
|
+
const t = num.value.trim();
|
|
1753
|
+
const fb = snap(Number(range.value));
|
|
1754
|
+
const s = snap(t === '' || Number.isNaN(Number(t)) ? fb : Number(t));
|
|
1755
|
+
stored = `${s}px`;
|
|
1756
|
+
num.value = String(s);
|
|
1757
|
+
range.value = String(s);
|
|
1758
|
+
onChange(stored);
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
num.addEventListener('keydown', (e) => {
|
|
1762
|
+
if ((e as KeyboardEvent).key === 'Enter') num.blur();
|
|
1180
1763
|
});
|
|
1181
1764
|
|
|
1182
1765
|
const wrap = h('div', { class: 'sm-field' }, [
|
|
1183
1766
|
h('label', { class: 'sm-field__label' }, [label]),
|
|
1184
|
-
|
|
1767
|
+
...(inheritSwitch ? [inheritSwitch.el] : []),
|
|
1768
|
+
sliderRow,
|
|
1185
1769
|
]);
|
|
1186
1770
|
if (help) wrap.append(h('div', { class: 'sm-field__help' }, [help]));
|
|
1187
1771
|
return wrap;
|
|
@@ -1238,6 +1822,8 @@ export class RightPanel {
|
|
|
1238
1822
|
const min = 1;
|
|
1239
1823
|
const displayN = mode === 'auto' ? 280 : layoutWidthNumeric(raw, mode === '%' ? '%' : 'px');
|
|
1240
1824
|
|
|
1825
|
+
const clampW = (n: number) => Math.max(min, Math.min(max, Math.round(n)));
|
|
1826
|
+
|
|
1241
1827
|
const range = h('input', {
|
|
1242
1828
|
type: 'range',
|
|
1243
1829
|
min,
|
|
@@ -1245,13 +1831,8 @@ export class RightPanel {
|
|
|
1245
1831
|
step: 1,
|
|
1246
1832
|
value: String(displayN),
|
|
1247
1833
|
disabled: mode === 'auto',
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
const c = Math.max(min, Math.min(max, Math.round(Number((e.target as HTMLInputElement).value))));
|
|
1251
|
-
num.value = String(c);
|
|
1252
|
-
onChange(mode === '%' ? `${c}%` : `${c}px`);
|
|
1253
|
-
},
|
|
1254
|
-
});
|
|
1834
|
+
}) as HTMLInputElement;
|
|
1835
|
+
|
|
1255
1836
|
const num = h('input', {
|
|
1256
1837
|
class: 'sm-input',
|
|
1257
1838
|
type: 'number',
|
|
@@ -1261,15 +1842,44 @@ export class RightPanel {
|
|
|
1261
1842
|
value: String(displayN),
|
|
1262
1843
|
disabled: mode === 'auto',
|
|
1263
1844
|
...(focusToken ? { 'data-sm-focus': focusToken } : {}),
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1845
|
+
}) as HTMLInputElement;
|
|
1846
|
+
|
|
1847
|
+
const emitW = (c: number) => {
|
|
1848
|
+
const v = clampW(c);
|
|
1849
|
+
range.value = String(v);
|
|
1850
|
+
num.value = String(v);
|
|
1851
|
+
onChange(mode === '%' ? `${v}%` : `${v}px`);
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1854
|
+
range.addEventListener('input', () => {
|
|
1855
|
+
if (mode === 'auto') return;
|
|
1856
|
+
emitW(Number(range.value));
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
num.addEventListener('input', () => {
|
|
1860
|
+
if (mode === 'auto') return;
|
|
1861
|
+
const t = num.value.trim();
|
|
1862
|
+
if (t === '') return;
|
|
1863
|
+
const rawN = Number(t);
|
|
1864
|
+
if (Number.isNaN(rawN)) return;
|
|
1865
|
+
range.value = String(clampW(rawN));
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
num.addEventListener('blur', () => {
|
|
1869
|
+
if (mode === 'auto') return;
|
|
1870
|
+
const t = num.value.trim();
|
|
1871
|
+
const fb = clampW(Number(range.value));
|
|
1872
|
+
if (t === '') {
|
|
1873
|
+
emitW(fb);
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
const rawN = Number(t);
|
|
1877
|
+
emitW(Number.isNaN(rawN) ? fb : rawN);
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
num.addEventListener('keydown', (e) => {
|
|
1881
|
+
if (mode === 'auto') return;
|
|
1882
|
+
if ((e as KeyboardEvent).key === 'Enter') num.blur();
|
|
1273
1883
|
});
|
|
1274
1884
|
|
|
1275
1885
|
const sliderRow = h(
|
|
@@ -1310,34 +1920,26 @@ export class RightPanel {
|
|
|
1310
1920
|
}
|
|
1311
1921
|
|
|
1312
1922
|
private _docFontWeightField(value: string, onChange: (v: string) => void, focusToken: string) {
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
[o.label],
|
|
1334
|
-
),
|
|
1335
|
-
);
|
|
1336
|
-
}
|
|
1337
|
-
return h('div', { class: 'sm-field' }, [
|
|
1338
|
-
h('label', { class: 'sm-field__label' }, ['字重']),
|
|
1339
|
-
row,
|
|
1340
|
-
]);
|
|
1923
|
+
const cur = normalizeFontWeightStep(value);
|
|
1924
|
+
return this._segmentedSelectField('字重', cur, FONT_WEIGHT_STEP_OPTIONS, onChange, focusToken);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
private _twoColumnRatioField(
|
|
1928
|
+
layout: SectionLayout,
|
|
1929
|
+
onChange: (layout: SectionLayout) => void,
|
|
1930
|
+
focusToken: string,
|
|
1931
|
+
) {
|
|
1932
|
+
return this._segmentedSelectField(
|
|
1933
|
+
'列比例',
|
|
1934
|
+
layout,
|
|
1935
|
+
[
|
|
1936
|
+
{ label: '1:1', value: '1-1' },
|
|
1937
|
+
{ label: '1:2', value: '1-2' },
|
|
1938
|
+
{ label: '2:1', value: '2-1' },
|
|
1939
|
+
],
|
|
1940
|
+
(v) => onChange(v as SectionLayout),
|
|
1941
|
+
focusToken,
|
|
1942
|
+
);
|
|
1341
1943
|
}
|
|
1342
1944
|
|
|
1343
1945
|
private _renderBlockCodeView(block: Block): HTMLElement {
|