mail-editor-pancake 0.0.8 → 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 CHANGED
@@ -24,6 +24,8 @@ Doc → Section → Column → Block // 仅四层,不允许 Block 再含
24
24
  - **品牌色**:可选 `accentColor` / `setAccentColor`,覆盖强调色与选区色;可选顶栏拾色器
25
25
  - **仅搭正文**:`ui.hideMailMeta` 隐藏主题 / Preheader 与顶栏「邮件设置」,保留版式宽度与全局样式
26
26
  - **画布清空 / 重置**:顶栏「清空画布」「重置内容」;`presetDoc` 与 `initialDoc` 分离,编辑已保存邮件时重置仍回到业务预置模板
27
+ - **变量系统**:`setVariables` 注入占位符列表;顶栏 `{{ }}` 弹层支持插 key / 插元素 / 复制;`kind: 'link' | 'image'` 区分链接片段与图片块
28
+ - **点空白取消选中**:可选 `clearSelectionOnCanvasMargin`,点击画布灰色衬底或白底留白时提交内联编辑并清空选中
27
29
  - 包体可控:核心 + MJML + CodeMirror + 富文本,gzip ≈ 586KB
28
30
 
29
31
  ## 仓库结构
@@ -80,6 +82,11 @@ const editor = new MailEditor({
80
82
  // 自动包一个一列 Section。设为 false 则强制只能拖入现有列内。
81
83
  // 唯一块被删或拖走后,会去掉因此变空的 Section,无需再删一次壳子。
82
84
  autoWrapSection: true,
85
+ /**
86
+ * 为 true 时:点击中栏灰色衬底、画布白底上未落到 Section/块的空白时,
87
+ * 提交内联编辑并清空选中,右栏回到文档级面板。默认 false。
88
+ */
89
+ // clearSelectionOnCanvasMargin: true,
83
90
  onChange: (doc) => console.log(doc),
84
91
  /**
85
92
  * 可选:见 README「图片资源 imageAssets」(uploadImage、内置 imageGallery、自管 pickImageFromGallery)。
@@ -109,8 +116,8 @@ const { mjml, html } = editor.export({ withSampleVariables: true });
109
116
 
110
117
  | 按钮 | 行为 |
111
118
  |------|------|
112
- | **清空画布** | 移除所有 Section / Block;`meta`、`styles`、`variables` 不变。操作前浏览器 `confirm` 确认;记入撤销栈(⌘Z 可恢复)。 |
113
- | **重置内容** | 整份替换为 **`presetDoc`** 快照(未传 `presetDoc` 时等同构造时的 `initialDoc` 合并结果)。操作前 `confirm` 确认;**不**走撤销栈(与 `setValue` 相同,会清空 history)。 |
119
+ | **清空画布** | 移除所有 Section / Block;`meta`、`styles`、`variables` 不变。记入撤销栈(⌘Z 可恢复)。 |
120
+ | **重置内容** | 整份替换为 **`presetDoc`** 快照(未传 `presetDoc` 时等同构造时的 `initialDoc` 合并结果)。**不**走撤销栈(与 `setValue` 相同,会清空 history)。 |
114
121
 
115
122
  **宿主常见写法**:新建页 `initialDoc` 与 `presetDoc` 同为默认模板;编辑页 `initialDoc` 为接口返回的 `jsonContent`,`presetDoc` 仍为业务预置结构,避免「重置」把用户带回打开时的草稿。
116
123
 
@@ -163,6 +170,111 @@ editor.getTheme();
163
170
  | `hideTopbarClearCanvas?: boolean` | 隐藏顶栏「清空画布」。 |
164
171
  | `hideTopbarResetContent?: boolean` | 隐藏顶栏「重置内容」。 |
165
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
+ 避免同一屏内正文出现两个变量入口;若宿主页仍保留正文入口,需自行在打开下拉前保存编辑器选区。
277
+
166
278
  ### 图片资源 `imageAssets`
167
279
 
168
280
  编辑器**只把图片存成 props 里的 URL 字符串**(与 MJML `src` 一致),不负责对象存储落地。通过 `MailEditor` 的 **`imageAssets`** 合并配置。
@@ -259,8 +371,15 @@ onMounted(() => {
259
371
  container: el.value!,
260
372
  blocks: stable,
261
373
  initialDoc: props.modelValue,
374
+ presetDoc: props.presetDoc,
375
+ ui: { hideMailMeta: true, hideTopbarTitle: true },
376
+ clearSelectionOnCanvasMargin: true,
262
377
  onChange: (doc) => emit('update:modelValue', doc),
263
378
  });
379
+ editor.setVariables([
380
+ { key: 'username', label: '用户名' },
381
+ { key: 'couponLink', label: '优惠券链接', kind: 'link' },
382
+ ]);
264
383
  });
265
384
  onBeforeUnmount(() => editor?.destroy());
266
385
  </script>
@@ -361,7 +480,10 @@ expandPaletteDrop?: (createBlock: (type: string) => Block) => Block[];
361
480
  | 撤销 / 重做 | ⌘Z / ⌘⇧Z(顶栏按钮也行) |
362
481
  | 清空画布 | 顶栏「清空画布」;移除全部 Section/Block,保留全局样式与变量(可撤销) |
363
482
  | 重置内容 | 顶栏「重置内容」;恢复为 `presetDoc`(或构造时的 `initialDoc`) |
364
- | 插入变量 | 顶栏 `{{ }}`:编辑中插到光标处;否则插到聚焦输入框 |
483
+ | 插入变量 key | 顶栏 `{{ }}` → **点击行**;或 `editor.insertVariableKey(v)` |
484
+ | 插入变量元素 | 顶栏 `{{ }}` → **插入元素**(仅 `link` / `image`);或 `editor.insertVariableElement(v)` |
485
+ | 复制变量 token | 顶栏 `{{ }}` → **复制** |
486
+ | 点空白取消选中 | 需 `clearSelectionOnCanvasMargin: true`;点击画布灰色衬底或白底留白 |
365
487
  | 切换源码 / 设计 | 顶栏切换 |
366
488
  | 导出 HTML | 顶栏右上 |
367
489
  | 文档级设置(主题 / Preheader / 宽度 / 全局样式) | 未选中画布时右栏展示;`ui.hideMailMeta` 时无主题与 Preheader,且无顶栏「邮件设置」按钮 |
@@ -387,7 +509,7 @@ expandPaletteDrop?: (createBlock: (type: string) => Block) => Block[];
387
509
  interface EmailDoc {
388
510
  version: '1';
389
511
  meta: { subject: string; preheader?: string; width: number | string };
390
- variables: { key: string; label: string; sample?: string }[];
512
+ variables: { key: string; label: string; sample?: string; kind?: 'text' | 'link' | 'image' }[];
391
513
  styles: { backgroundColor; contentBackgroundColor; fontFamily; fontSize; color; linkColor; lineHeight };
392
514
  sections: Section[]; // 顺序即视觉顺序
393
515
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mail-editor-pancake",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "private": false,
5
5
  "description": "面向运营的轻量邮件可视化编辑器(monorepo 根)",
6
6
  "devDependencies": {
@@ -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;