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
package/README.md
CHANGED
|
@@ -23,6 +23,9 @@ Doc → Section → Column → Block // 仅四层,不允许 Block 再含
|
|
|
23
23
|
- **界面主题**:顶栏 **太阳 / 月亮 / 显示器** 图标切换浅色、深色、跟随系统(`prefers-color-scheme`);中间邮件画布仍为白纸以贴近成品
|
|
24
24
|
- **品牌色**:可选 `accentColor` / `setAccentColor`,覆盖强调色与选区色;可选顶栏拾色器
|
|
25
25
|
- **仅搭正文**:`ui.hideMailMeta` 隐藏主题 / Preheader 与顶栏「邮件设置」,保留版式宽度与全局样式
|
|
26
|
+
- **画布清空 / 重置**:顶栏「清空画布」「重置内容」;`presetDoc` 与 `initialDoc` 分离,编辑已保存邮件时重置仍回到业务预置模板
|
|
27
|
+
- **变量系统**:`setVariables` 注入占位符列表;顶栏 `{{ }}` 弹层支持插 key / 插元素 / 复制;`kind: 'link' | 'image'` 区分链接片段与图片块
|
|
28
|
+
- **点空白取消选中**:可选 `clearSelectionOnCanvasMargin`,点击画布灰色衬底或白底留白时提交内联编辑并清空选中
|
|
26
29
|
- 包体可控:核心 + MJML + CodeMirror + 富文本,gzip ≈ 586KB
|
|
27
30
|
|
|
28
31
|
## 仓库结构
|
|
@@ -67,12 +70,23 @@ const editor = new MailEditor({
|
|
|
67
70
|
variables: [{ key: 'user.name', label: '用户名', sample: '张三' }],
|
|
68
71
|
sections: [],
|
|
69
72
|
},
|
|
73
|
+
/**
|
|
74
|
+
* 顶栏「重置内容」恢复的目标(与 initialDoc 独立)。
|
|
75
|
+
* 编辑页打开已保存邮件时:initialDoc = 当前稿,presetDoc = 业务默认模板。
|
|
76
|
+
* 未传时与 initialDoc 合并结果一致。
|
|
77
|
+
*/
|
|
78
|
+
// presetDoc: defaultTemplatePartial,
|
|
70
79
|
/** 右栏控件形态等,见下文「UI 选项 ui」 */
|
|
71
80
|
// ui: { preferSliderControls: true, hideMailMeta: true },
|
|
72
81
|
// autoWrapSection: true(默认)— 把 Block 拖到 Section 之间空白处时
|
|
73
82
|
// 自动包一个一列 Section。设为 false 则强制只能拖入现有列内。
|
|
74
83
|
// 唯一块被删或拖走后,会去掉因此变空的 Section,无需再删一次壳子。
|
|
75
84
|
autoWrapSection: true,
|
|
85
|
+
/**
|
|
86
|
+
* 为 true 时:点击中栏灰色衬底、画布白底上未落到 Section/块的空白时,
|
|
87
|
+
* 提交内联编辑并清空选中,右栏回到文档级面板。默认 false。
|
|
88
|
+
*/
|
|
89
|
+
// clearSelectionOnCanvasMargin: true,
|
|
76
90
|
onChange: (doc) => console.log(doc),
|
|
77
91
|
/**
|
|
78
92
|
* 可选:见 README「图片资源 imageAssets」(uploadImage、内置 imageGallery、自管 pickImageFromGallery)。
|
|
@@ -85,6 +99,36 @@ const { mjml, html } = editor.export({ withSampleVariables: true });
|
|
|
85
99
|
|
|
86
100
|
// 整份替换文档(例如切换模板);会先失焦右栏避免旧值残留
|
|
87
101
|
// editor.setValue(nextDoc);
|
|
102
|
+
|
|
103
|
+
// 清空画布(仅 sections 置空,保留 meta / styles / variables;可 ⌘Z 撤销)
|
|
104
|
+
// editor.clearCanvas();
|
|
105
|
+
|
|
106
|
+
// 恢复为 presetDoc(或构造时 initialDoc)快照
|
|
107
|
+
// editor.resetToPreset();
|
|
108
|
+
|
|
109
|
+
// 异步加载默认模板后更新重置目标
|
|
110
|
+
// editor.setPresetDoc(defaultTemplatePartial);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 画布清空与重置
|
|
114
|
+
|
|
115
|
+
顶栏位于 **撤销 / 重做** 右侧(可用 `ui` 隐藏,见下表):
|
|
116
|
+
|
|
117
|
+
| 按钮 | 行为 |
|
|
118
|
+
|------|------|
|
|
119
|
+
| **清空画布** | 移除所有 Section / Block;`meta`、`styles`、`variables` 不变。记入撤销栈(⌘Z 可恢复)。 |
|
|
120
|
+
| **重置内容** | 整份替换为 **`presetDoc`** 快照(未传 `presetDoc` 时等同构造时的 `initialDoc` 合并结果)。**不**走撤销栈(与 `setValue` 相同,会清空 history)。 |
|
|
121
|
+
|
|
122
|
+
**宿主常见写法**:新建页 `initialDoc` 与 `presetDoc` 同为默认模板;编辑页 `initialDoc` 为接口返回的 `jsonContent`,`presetDoc` 仍为业务预置结构,避免「重置」把用户带回打开时的草稿。
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const editor = new MailEditor({
|
|
126
|
+
container: el,
|
|
127
|
+
blocks: allBlocks,
|
|
128
|
+
initialDoc: loadedFromApi, // 当前画布
|
|
129
|
+
presetDoc: businessDefaultTemplate, // 顶栏「重置内容」
|
|
130
|
+
onChange: (doc) => save(doc),
|
|
131
|
+
});
|
|
88
132
|
```
|
|
89
133
|
|
|
90
134
|
### 界面主题
|
|
@@ -120,6 +164,116 @@ editor.getTheme();
|
|
|
120
164
|
|------|------|
|
|
121
165
|
| `preferSliderControls?: boolean` | 为 `true` 时右栏数值、内边距、全局/组件字号等使用滑块等增强控件。默认 `false`。 |
|
|
122
166
|
| `hideMailMeta?: boolean` | 为 `true` 时:**不展示**右栏「主题」「Preheader」以及顶栏「邮件设置」按钮;右栏文档级面板改为 **「版式」(内容宽度)+「全局样式」**。`doc.meta.subject` / `preheader` 仍在数据模型中,导出 MJML 仍会生成 `<mj-title>`(可为空)、有值时才生成 `<mj-preview>`,适合发件主题由宿主系统单独维护的场景。 |
|
|
167
|
+
| `hideTopbarTitle?: boolean` | 隐藏顶栏左侧产品标题(嵌入宿主页时常用)。 |
|
|
168
|
+
| `hideTopbarMailSettings?: boolean` | 隐藏顶栏「邮件设置」按钮(与 `hideMailMeta` 叠加使用)。 |
|
|
169
|
+
| `hideTopbarFullscreen?: boolean` | 隐藏顶栏全屏按钮。 |
|
|
170
|
+
| `hideTopbarClearCanvas?: boolean` | 隐藏顶栏「清空画布」。 |
|
|
171
|
+
| `hideTopbarResetContent?: boolean` | 隐藏顶栏「重置内容」。 |
|
|
172
|
+
|
|
173
|
+
### 构造选项(画布行为)
|
|
174
|
+
|
|
175
|
+
| 字段 | 说明 |
|
|
176
|
+
|------|------|
|
|
177
|
+
| `autoWrapSection?: boolean` | 为 `true`(默认)时,把 Block 拖到 Section 之间空白处会自动包一列 Section;`false` 则只能拖入现有列内。 |
|
|
178
|
+
| `clearSelectionOnCanvasMargin?: boolean` | 为 `true` 时,点击中栏灰色衬底、画布白底留白(未点到 Section/Block)、空文档提示区等,会提交内联编辑并 `setSelection(null)`,右栏回到文档级面板。默认 `false`。嵌入宿主页且希望「点空白取消 focus」时开启。 |
|
|
179
|
+
|
|
180
|
+
### 变量系统
|
|
181
|
+
|
|
182
|
+
占位符用于导出 HTML / MJML 后由后端或发送服务替换。设计态通过 `Variable` 列表维护可选项。
|
|
183
|
+
|
|
184
|
+
#### 数据模型
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
interface Variable {
|
|
188
|
+
key: string; // Mustache 变量名,如 couponLink(不含 {{}})
|
|
189
|
+
label: string; // 弹层 / 下拉展示名
|
|
190
|
+
sample?: string; // 预览、export({ withSampleVariables: true }) 时的示例值
|
|
191
|
+
kind?: 'text' | 'link' | 'image'; // 默认 text
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
| `kind` | 含义 | 「插入元素」行为 |
|
|
196
|
+
|--------|------|----------------|
|
|
197
|
+
| `text`(默认) | 纯文本占位 | 与插 key 相同,写入 `{{key}}` |
|
|
198
|
+
| `link` | 链接类 | 插入 `<a href="{{key}}">{{key}}</a>`(href 与展示文本均为 token,不用 label) |
|
|
199
|
+
| `image` | 图片类 | 插入 image 块,`src` 为 `{{key}}` |
|
|
200
|
+
|
|
201
|
+
#### 注入与持久化
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
editor.setVariables([
|
|
205
|
+
{ key: 'username', label: '用户名', sample: '张三' },
|
|
206
|
+
{ key: 'couponLink', label: '优惠券链接', kind: 'link', sample: '#' },
|
|
207
|
+
{ key: 'couponImage', label: '优惠券图片', kind: 'image' },
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
editor.getVariables(); // 返回当前可用列表
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- 推荐宿主在构造后 **`setVariables`** 注入业务变量,而不是只写在 `initialDoc.variables` 里。
|
|
214
|
+
- 宿主调用 **`setValue` / `resetToPreset`** 恢复文档 JSON 后,已通过 `setVariables` 注入的列表**仍会写回** `doc.variables`,避免弹层显示「暂无可用变量」。
|
|
215
|
+
|
|
216
|
+
#### 插入 API
|
|
217
|
+
|
|
218
|
+
| 方法 | 说明 |
|
|
219
|
+
|------|------|
|
|
220
|
+
| `insertVariableKey(v)` | 插入 `{{key}}` 纯文本 |
|
|
221
|
+
| `insertVariableElement(v)` | `link` → 链接 HTML;`image` → 图片块;其余同 key |
|
|
222
|
+
| `insertVariable(v)` | 同 `insertVariableKey`(兼容旧名) |
|
|
223
|
+
|
|
224
|
+
插入位置优先级:
|
|
225
|
+
|
|
226
|
+
1. 当前**内联编辑**(双击文本块)→ 写入 contenteditable 光标处(打开顶栏变量弹层前会自动 `saveSelection`)
|
|
227
|
+
2. 编辑器内**聚焦的 input/textarea**(右栏属性等)
|
|
228
|
+
3. 当前**选中的 Block** → 追加到其主文本字段末尾
|
|
229
|
+
4. 以上皆无 → 在画布末尾新建 text 块
|
|
230
|
+
|
|
231
|
+
#### 顶栏弹层交互
|
|
232
|
+
|
|
233
|
+
顶栏 **`{{ }} 插入变量`** 打开列表,每行:
|
|
234
|
+
|
|
235
|
+
| 操作 | 行为 |
|
|
236
|
+
|------|------|
|
|
237
|
+
| **点击行** | 插入 `{{key}}` |
|
|
238
|
+
| **插入元素**(仅 `link` / `image`) | 调用 `insertVariableElement` |
|
|
239
|
+
| **复制** | 复制 token 到剪贴板并关闭弹层 |
|
|
240
|
+
|
|
241
|
+
#### 宿主侧工具函数
|
|
242
|
+
|
|
243
|
+
若主题、预览文案等字段在编辑器**外部**维护,可从 `@simple-mail/core` 导入:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import {
|
|
247
|
+
buildBodyVariableKeyInsert,
|
|
248
|
+
buildBodyVariableElementInsert,
|
|
249
|
+
buildLinkVariableHtml,
|
|
250
|
+
normalizeVariable,
|
|
251
|
+
tokenToVariableKey,
|
|
252
|
+
variablePlaceholder,
|
|
253
|
+
} from '@simple-mail/core';
|
|
254
|
+
|
|
255
|
+
// 纯 key
|
|
256
|
+
buildBodyVariableKeyInsert({ key: 'username', label: '用户名' });
|
|
257
|
+
// -> { content: '{{username}}', asHtml: false }
|
|
258
|
+
|
|
259
|
+
// 链接 / 图片片段(供 Monaco、Grapes 等宿主自管插入)
|
|
260
|
+
buildBodyVariableElementInsert(
|
|
261
|
+
{ key: 'couponLink', label: '优惠券链接', kind: 'link' },
|
|
262
|
+
{ linkColor: '#ff5a00' },
|
|
263
|
+
);
|
|
264
|
+
// link -> { content: '<a href="{{couponLink}}">...</a>', asHtml: true }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`buildBodyVariableInsert` 仍可用,内部按 kind 分发到 key / element,**新代码请用上述两个函数**。
|
|
268
|
+
|
|
269
|
+
#### 嵌入宿主时的入口分工(建议)
|
|
270
|
+
|
|
271
|
+
| 字段 | 建议入口 |
|
|
272
|
+
|------|----------|
|
|
273
|
+
| 发件主题、Preheader 等表单字段 | **宿主页**下拉 / 输入框旁「插入变量」 |
|
|
274
|
+
| 正文(已嵌入 `MailEditor`) | **编辑器顶栏** `{{ }}`(靠近光标,打开弹层前会保存选区) |
|
|
275
|
+
|
|
276
|
+
避免同一屏内正文出现两个变量入口;若宿主页仍保留正文入口,需自行在打开下拉前保存编辑器选区。
|
|
123
277
|
|
|
124
278
|
### 图片资源 `imageAssets`
|
|
125
279
|
|
|
@@ -217,8 +371,15 @@ onMounted(() => {
|
|
|
217
371
|
container: el.value!,
|
|
218
372
|
blocks: stable,
|
|
219
373
|
initialDoc: props.modelValue,
|
|
374
|
+
presetDoc: props.presetDoc,
|
|
375
|
+
ui: { hideMailMeta: true, hideTopbarTitle: true },
|
|
376
|
+
clearSelectionOnCanvasMargin: true,
|
|
220
377
|
onChange: (doc) => emit('update:modelValue', doc),
|
|
221
378
|
});
|
|
379
|
+
editor.setVariables([
|
|
380
|
+
{ key: 'username', label: '用户名' },
|
|
381
|
+
{ key: 'couponLink', label: '优惠券链接', kind: 'link' },
|
|
382
|
+
]);
|
|
222
383
|
});
|
|
223
384
|
onBeforeUnmount(() => editor?.destroy());
|
|
224
385
|
</script>
|
|
@@ -317,7 +478,12 @@ expandPaletteDrop?: (createBlock: (type: string) => Block) => Block[];
|
|
|
317
478
|
| 删除 | 选中后按 Delete/Backspace;或工具条 🗑 |
|
|
318
479
|
| 复制 | 工具条 ⎘ 图标 |
|
|
319
480
|
| 撤销 / 重做 | ⌘Z / ⌘⇧Z(顶栏按钮也行) |
|
|
320
|
-
|
|
|
481
|
+
| 清空画布 | 顶栏「清空画布」;移除全部 Section/Block,保留全局样式与变量(可撤销) |
|
|
482
|
+
| 重置内容 | 顶栏「重置内容」;恢复为 `presetDoc`(或构造时的 `initialDoc`) |
|
|
483
|
+
| 插入变量 key | 顶栏 `{{ }}` → **点击行**;或 `editor.insertVariableKey(v)` |
|
|
484
|
+
| 插入变量元素 | 顶栏 `{{ }}` → **插入元素**(仅 `link` / `image`);或 `editor.insertVariableElement(v)` |
|
|
485
|
+
| 复制变量 token | 顶栏 `{{ }}` → **复制** |
|
|
486
|
+
| 点空白取消选中 | 需 `clearSelectionOnCanvasMargin: true`;点击画布灰色衬底或白底留白 |
|
|
321
487
|
| 切换源码 / 设计 | 顶栏切换 |
|
|
322
488
|
| 导出 HTML | 顶栏右上 |
|
|
323
489
|
| 文档级设置(主题 / Preheader / 宽度 / 全局样式) | 未选中画布时右栏展示;`ui.hideMailMeta` 时无主题与 Preheader,且无顶栏「邮件设置」按钮 |
|
|
@@ -332,13 +498,18 @@ expandPaletteDrop?: (createBlock: (type: string) => Block) => Block[];
|
|
|
332
498
|
|
|
333
499
|
## 数据模型 速览
|
|
334
500
|
|
|
335
|
-
`EmailDoc` 为单一事实来源;设计 / 导出 / 宿主保存均围绕该 JSON
|
|
501
|
+
`EmailDoc` 为单一事实来源;设计 / 导出 / 宿主保存均围绕该 JSON。
|
|
502
|
+
|
|
503
|
+
- **`initialDoc`**:`Partial<EmailDoc>`,与默认空邮件合并,作为**首次进入画布**的内容。
|
|
504
|
+
- **`presetDoc`**:可选,与默认空邮件合并,作为顶栏 **「重置内容」** 的目标;未传时与 `initialDoc` 相同。编辑已保存邮件时应单独传入业务默认模板,勿与 `initialDoc` 混用。
|
|
505
|
+
- **`editor.setValue(doc)`**:运行期整份替换(如切换模板);会清空撤销栈;调用前会尽量失焦右栏输入。
|
|
506
|
+
- **`editor.clearCanvas()`** / **`editor.resetToPreset()`** / **`editor.setPresetDoc(partial)`**:与顶栏按钮等价,供宿主程序化调用。
|
|
336
507
|
|
|
337
508
|
```ts
|
|
338
509
|
interface EmailDoc {
|
|
339
510
|
version: '1';
|
|
340
511
|
meta: { subject: string; preheader?: string; width: number | string };
|
|
341
|
-
variables: { key: string; label: string; sample?: string }[];
|
|
512
|
+
variables: { key: string; label: string; sample?: string; kind?: 'text' | 'link' | 'image' }[];
|
|
342
513
|
styles: { backgroundColor; contentBackgroundColor; fontFamily; fontSize; color; linkColor; lineHeight };
|
|
343
514
|
sections: Section[]; // 顺序即视觉顺序
|
|
344
515
|
}
|
package/package.json
CHANGED
|
@@ -108,6 +108,9 @@ declare interface LogoProps {
|
|
|
108
108
|
align: 'left' | 'center' | 'right';
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** 画布预览用默认图标 URL(与 MJML `name` 映射一致;无内置图时返回 null) */
|
|
112
|
+
export declare function mjSocialDefaultIconSrc(network: string): string | null;
|
|
113
|
+
|
|
111
114
|
/** 生成 mj-social-element 的 name 属性(与 MJML 内置图标对齐;未知平台退化为 web)。 */
|
|
112
115
|
export declare function mjSocialElementName(network: string): string;
|
|
113
116
|
|
|
@@ -120,6 +123,15 @@ export declare function paddingQuad(p: {
|
|
|
120
123
|
paddingLeft: number;
|
|
121
124
|
}): string;
|
|
122
125
|
|
|
126
|
+
/** 画布 / renderPreview 社交图标 HTML(优先自定义 URL,其次 MJML 内置图,最后文字占位) */
|
|
127
|
+
export declare function renderSocialIconPreviewHtml(opts: {
|
|
128
|
+
network: string;
|
|
129
|
+
iconSize: number;
|
|
130
|
+
iconBorderRadius: number;
|
|
131
|
+
iconSrc?: string;
|
|
132
|
+
backgroundColor?: string;
|
|
133
|
+
}): string;
|
|
134
|
+
|
|
123
135
|
/** 社交平台与 MJML mj-social-element 的 name 对齐(含常用内置名)。 */
|
|
124
136
|
export declare const SOCIAL_NETWORK_OPTIONS: {
|
|
125
137
|
label: string;
|