mail-editor-pancake 0.0.9 → 0.1.1
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 +461 -303
- package/package.json +2 -2
- package/packages/blocks/dist/index.js +153 -150
- package/packages/blocks/dist/index.js.map +1 -1
- package/packages/blocks/src/html.ts +11 -3
- package/packages/blocks/src/text.ts +8 -3
- package/packages/core/dist/index.d.ts +220 -10
- package/packages/core/dist/index.js +3963 -2250
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/style.css +1 -1
- package/packages/core/package.json +1 -1
- package/packages/core/src/editor/BlockCodeModal.ts +84 -2
- package/packages/core/src/editor/Canvas.ts +182 -26
- package/packages/core/src/editor/Editor.ts +414 -68
- package/packages/core/src/editor/ExportModal.ts +1 -1
- package/packages/core/src/editor/ImportDocModal.ts +129 -0
- package/packages/core/src/editor/InlineEditor.ts +238 -24
- package/packages/core/src/editor/LeftPanel.ts +55 -14
- package/packages/core/src/editor/Modal.ts +20 -1
- package/packages/core/src/editor/PreviewModal.ts +2 -2
- package/packages/core/src/editor/RichTextToolbar.ts +125 -18
- package/packages/core/src/editor/RightPanel.ts +165 -7
- package/packages/core/src/editor/Topbar.ts +306 -132
- package/packages/core/src/editor/VariablePickerPanel.ts +145 -0
- package/packages/core/src/editor/styles.css +410 -52
- package/packages/core/src/index.ts +32 -2
- package/packages/core/src/renderer/index.ts +9 -1
- package/packages/core/src/renderer/mjml.ts +4 -5
- package/packages/core/src/store/store.ts +13 -2
- package/packages/core/src/types.ts +65 -2
- package/packages/core/src/utils/docClipboard.ts +89 -0
- package/packages/core/src/utils/dynamicVariantHtml.ts +45 -0
- package/packages/core/src/utils/dynamicVariantKey.ts +24 -0
- package/packages/core/src/utils/dynamicVariantSection.ts +98 -0
- package/packages/core/src/utils/emailListStyles.ts +188 -0
- package/packages/core/src/utils/inlineListEditing.ts +774 -0
- package/packages/core/src/utils/lockedMjml.ts +146 -12
- package/packages/core/src/utils/modalSize.ts +27 -0
- package/packages/core/src/utils/richHtmlEmpty.ts +43 -0
- package/packages/core/src/utils/sectionLayout.ts +2 -2
- package/packages/core/src/variables/index.ts +3 -4
- package/playground/vanilla/src/main.ts +26 -1
|
@@ -7,11 +7,26 @@ import {
|
|
|
7
7
|
pruneSectionIfEmpty,
|
|
8
8
|
type Store,
|
|
9
9
|
} from '../store/store';
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
Block,
|
|
12
|
+
Column,
|
|
13
|
+
EditorUiOptions,
|
|
14
|
+
EmailDoc,
|
|
15
|
+
PaletteDropResult,
|
|
16
|
+
RenderContext,
|
|
17
|
+
Section,
|
|
18
|
+
SectionAttrs,
|
|
19
|
+
SectionLayout,
|
|
20
|
+
} from '../types';
|
|
21
|
+
import { getSectionDynamicVariantKey } from '../utils/dynamicVariantSection';
|
|
11
22
|
import { blockButtonWidthCss, docContentWidthCss } from '../utils/contentWidth';
|
|
23
|
+
import { globalListIndentCssVarValue } from '../utils/emailListStyles';
|
|
12
24
|
import { normalizeFontWeightStep } from '../utils/fontWeightSteps';
|
|
13
25
|
import { clear, escapeHtml, h } from '../utils/dom';
|
|
14
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
defaultPaddingForLockedBlock,
|
|
28
|
+
resolveLockedMjmlCanvasContent,
|
|
29
|
+
} from '../utils/lockedMjml';
|
|
15
30
|
import { layoutHumanLabel } from '../utils/sectionLayout';
|
|
16
31
|
import { BlockCodeModal } from './BlockCodeModal';
|
|
17
32
|
import { InlineEditor, type SelectionState } from './InlineEditor';
|
|
@@ -34,6 +49,8 @@ export interface CanvasOptions {
|
|
|
34
49
|
* 应传入 MailEditor 根节点;若挂到 `document.body`,`var(--sm-code-bg)` 等会失效,弹框背景会变透明。
|
|
35
50
|
*/
|
|
36
51
|
layerRoot: HTMLElement;
|
|
52
|
+
/** 与 MailEditor.opts.ui 对齐 */
|
|
53
|
+
ui?: EditorUiOptions;
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
/**
|
|
@@ -62,6 +79,9 @@ export class Canvas {
|
|
|
62
79
|
private pendingRender = false;
|
|
63
80
|
/** 设计模式下禁用画布内超链接导航(预览弹框 iframe 不在此 DOM 内,不受影响) */
|
|
64
81
|
private linkNavSuppression = new AbortController();
|
|
82
|
+
/** clearSelectionOnCanvasMargin:记录本次指针是否从编辑区内按下(区分拖选松手 vs 点空白退出) */
|
|
83
|
+
private clearSelectionGestureTracking = new AbortController();
|
|
84
|
+
private pointerGestureStartedInInlineEdit = false;
|
|
65
85
|
private blockCodeModal: BlockCodeModal;
|
|
66
86
|
|
|
67
87
|
constructor(opts: CanvasOptions) {
|
|
@@ -78,6 +98,7 @@ export class Canvas {
|
|
|
78
98
|
|
|
79
99
|
this._bindDesignModeLinkSuppression();
|
|
80
100
|
if (opts.clearSelectionOnCanvasMargin) {
|
|
101
|
+
this._bindClearSelectionGestureTracking();
|
|
81
102
|
this._bindClearSelectionOnCanvasWhitespace();
|
|
82
103
|
this._bindClearSelectionOnCanvasMargin();
|
|
83
104
|
}
|
|
@@ -88,7 +109,13 @@ export class Canvas {
|
|
|
88
109
|
|
|
89
110
|
this._render();
|
|
90
111
|
|
|
91
|
-
opts.store.subscribe(() => {
|
|
112
|
+
opts.store.subscribe((detail) => {
|
|
113
|
+
// 编辑态下若走了文档级 undo/redo,必须丢弃未提交 DOM,否则失焦提交会与 store 脱节(整节消失等)
|
|
114
|
+
if (this.editingBlockId && detail?.source === 'history') {
|
|
115
|
+
this.abortInlineEdit();
|
|
116
|
+
this._render();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
92
119
|
if (this.editingBlockId) {
|
|
93
120
|
this.pendingRender = true;
|
|
94
121
|
return;
|
|
@@ -112,6 +139,30 @@ export class Canvas {
|
|
|
112
139
|
this.inlineEditor?.commit();
|
|
113
140
|
}
|
|
114
141
|
|
|
142
|
+
get isInlineEditing(): boolean {
|
|
143
|
+
return this.editingBlockId != null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 放弃未提交的内联编辑(不写入 store)。
|
|
148
|
+
* 用于文档级撤销/重做:编辑期间画布不重绘,若直接 undo 会导致 DOM 与 doc 不一致。
|
|
149
|
+
*/
|
|
150
|
+
abortInlineEdit() {
|
|
151
|
+
if (!this.editingBlockId) return;
|
|
152
|
+
const id = this.editingBlockId;
|
|
153
|
+
this.editingBlockId = null;
|
|
154
|
+
this.pendingRender = false;
|
|
155
|
+
if (this.inlineEditor) {
|
|
156
|
+
this.inlineEditor.destroy(false);
|
|
157
|
+
this.inlineEditor = null;
|
|
158
|
+
}
|
|
159
|
+
this.opts.toolbar.detach();
|
|
160
|
+
const blockEl = this.inner.querySelector(
|
|
161
|
+
`.sm-block[data-id="${cssEscape(id)}"]`,
|
|
162
|
+
) as HTMLElement | null;
|
|
163
|
+
blockEl?.classList.remove('is-editing');
|
|
164
|
+
}
|
|
165
|
+
|
|
115
166
|
/** 强制重绘(内联编辑中会延迟到 commit 后) */
|
|
116
167
|
refresh() {
|
|
117
168
|
if (this.editingBlockId) {
|
|
@@ -123,11 +174,46 @@ export class Canvas {
|
|
|
123
174
|
|
|
124
175
|
destroy() {
|
|
125
176
|
this.linkNavSuppression.abort();
|
|
177
|
+
this.clearSelectionGestureTracking.abort();
|
|
126
178
|
this._exitEditing(false);
|
|
127
179
|
this._destroySortables();
|
|
128
180
|
this.blockCodeModal.destroy();
|
|
129
181
|
}
|
|
130
182
|
|
|
183
|
+
/**
|
|
184
|
+
* clearSelectionOnCanvasMargin:mousedown 在编辑区内、mouseup 落在灰底/留白时,
|
|
185
|
+
* 浏览器仍可能对 mouseup 目标派发 click;需与 Section padding 的拖选抑制同理。
|
|
186
|
+
*/
|
|
187
|
+
private _bindClearSelectionGestureTracking() {
|
|
188
|
+
const opts = { capture: true, signal: this.clearSelectionGestureTracking.signal } as const;
|
|
189
|
+
document.addEventListener(
|
|
190
|
+
'mousedown',
|
|
191
|
+
(e: MouseEvent) => {
|
|
192
|
+
if (e.button !== 0) {
|
|
193
|
+
this.pointerGestureStartedInInlineEdit = false;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!this.editingBlockId) {
|
|
197
|
+
this.pointerGestureStartedInInlineEdit = false;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const editingEl = this.inner.querySelector('.sm-inline-editing');
|
|
201
|
+
if (!editingEl) {
|
|
202
|
+
this.pointerGestureStartedInInlineEdit = false;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const t = e.target;
|
|
206
|
+
if (!(t instanceof Node)) {
|
|
207
|
+
this.pointerGestureStartedInInlineEdit = false;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const node = t.nodeType === Node.TEXT_NODE ? t.parentNode : t;
|
|
211
|
+
this.pointerGestureStartedInInlineEdit = !!(node && editingEl.contains(node));
|
|
212
|
+
},
|
|
213
|
+
opts,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
131
217
|
/**
|
|
132
218
|
* 点击画布白底上未落到 Section 内的区域(如 min-height 下方留白、空文档提示)时清空选中,
|
|
133
219
|
* Section/Block 上的点击会在冒泡到此前被 stopPropagation,不会误清。
|
|
@@ -139,6 +225,7 @@ export class Canvas {
|
|
|
139
225
|
const el = t instanceof Element ? t : t.parentElement;
|
|
140
226
|
if (!el) return;
|
|
141
227
|
if (el === this.inner || el.closest('.sm-empty-doc')) {
|
|
228
|
+
if (this._shouldSuppressClearSelectionFromTextDrag()) return;
|
|
142
229
|
this.commitInlineEdit();
|
|
143
230
|
this.opts.store.setSelection(null);
|
|
144
231
|
}
|
|
@@ -153,6 +240,7 @@ export class Canvas {
|
|
|
153
240
|
if (!this.el.contains(t)) return;
|
|
154
241
|
if (this.inner.contains(t)) return;
|
|
155
242
|
if (this.addBar.contains(t)) return;
|
|
243
|
+
if (this._shouldSuppressClearSelectionFromTextDrag()) return;
|
|
156
244
|
this.commitInlineEdit();
|
|
157
245
|
this.opts.store.setSelection(null);
|
|
158
246
|
});
|
|
@@ -202,6 +290,10 @@ export class Canvas {
|
|
|
202
290
|
this.inner.style.color = doc.styles.color;
|
|
203
291
|
this.inner.style.lineHeight = doc.styles.lineHeight ?? '1.25';
|
|
204
292
|
this.inner.style.setProperty('--sm-editor-link-color', doc.styles.linkColor);
|
|
293
|
+
this.inner.style.setProperty(
|
|
294
|
+
'--sm-list-indent-default',
|
|
295
|
+
globalListIndentCssVarValue(doc.styles),
|
|
296
|
+
);
|
|
205
297
|
|
|
206
298
|
if (!doc.sections.length) {
|
|
207
299
|
this.inner.append(
|
|
@@ -244,7 +336,13 @@ export class Canvas {
|
|
|
244
336
|
}px ${a.paddingLeft ?? 0}px`;
|
|
245
337
|
|
|
246
338
|
const layoutShort = layoutHumanLabel(section.layout);
|
|
247
|
-
const
|
|
339
|
+
const dvKey =
|
|
340
|
+
this.opts.ui?.enableDynamicVariantKey === true
|
|
341
|
+
? getSectionDynamicVariantKey(section)
|
|
342
|
+
: undefined;
|
|
343
|
+
const sectionChip = dvKey
|
|
344
|
+
? `动态变量 ${sectionIndex + 1} · {{${dvKey}}}`
|
|
345
|
+
: `区块 ${sectionIndex + 1} · ${layoutShort}`;
|
|
248
346
|
|
|
249
347
|
const sw = blockButtonWidthCss(a.width);
|
|
250
348
|
const box =
|
|
@@ -256,7 +354,7 @@ export class Canvas {
|
|
|
256
354
|
});
|
|
257
355
|
|
|
258
356
|
const wrap = h('div', {
|
|
259
|
-
class: 'sm-section'
|
|
357
|
+
class: `sm-section${dvKey ? ' sm-section--dynamic-variant' : ''}`,
|
|
260
358
|
title: `${sectionChip}。子组件铺满列内时:按 Esc 或 Alt+点击块可选中本节。`,
|
|
261
359
|
style: `padding:${padding};${a.backgroundColor ? `background:${a.backgroundColor};` : ''}${box}`,
|
|
262
360
|
});
|
|
@@ -389,23 +487,24 @@ export class Canvas {
|
|
|
389
487
|
const def = this.opts.registry.get(block.type);
|
|
390
488
|
let inner = '';
|
|
391
489
|
if (block.lockedMjml) {
|
|
392
|
-
|
|
393
|
-
|
|
490
|
+
const isHtmlLike = block.type === 'html' || def?.inlineEditable?.mode === 'html';
|
|
491
|
+
const padding = defaultPaddingForLockedBlock(
|
|
492
|
+
block.type,
|
|
493
|
+
block.lockedMjml,
|
|
494
|
+
block.props as {
|
|
394
495
|
paddingTop?: number;
|
|
395
496
|
paddingRight?: number;
|
|
396
497
|
paddingBottom?: number;
|
|
397
498
|
paddingLeft?: number;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
});
|
|
405
|
-
inner = `<div class="sm-html-content">${frag}</div>`;
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
const resolved = resolveLockedMjmlCanvasContent(block.lockedMjml, padding);
|
|
502
|
+
if (resolved.mode === 'preview') {
|
|
503
|
+
const wrapClass = isHtmlLike ? 'sm-html-content' : 'sm-locked-preview';
|
|
504
|
+
inner = `<div class="${wrapClass}">${resolved.html}</div>`;
|
|
406
505
|
} else {
|
|
407
506
|
inner = `<div style="padding:8px;color:#92400e;background:#fffbeb;border:1px dashed #f59e0b;font-family:monospace;font-size:11px;white-space:pre-wrap;">${escapeHtml(
|
|
408
|
-
|
|
507
|
+
resolved.source,
|
|
409
508
|
)}</div>`;
|
|
410
509
|
}
|
|
411
510
|
} else if (def?.renderPreview) {
|
|
@@ -520,7 +619,13 @@ export class Canvas {
|
|
|
520
619
|
});
|
|
521
620
|
});
|
|
522
621
|
|
|
523
|
-
if (
|
|
622
|
+
if (block.lockedMjml) {
|
|
623
|
+
el.addEventListener('dblclick', (e) => {
|
|
624
|
+
e.stopPropagation();
|
|
625
|
+
e.preventDefault();
|
|
626
|
+
this.blockCodeModal.open(block.id, displayName);
|
|
627
|
+
});
|
|
628
|
+
} else if (def?.inlineEditable) {
|
|
524
629
|
el.addEventListener('dblclick', (e) => {
|
|
525
630
|
e.stopPropagation();
|
|
526
631
|
this._enterEditing(block);
|
|
@@ -533,8 +638,7 @@ export class Canvas {
|
|
|
533
638
|
private _renderAddBar(): HTMLElement {
|
|
534
639
|
const layouts: { layout: SectionLayout; label: string }[] = [
|
|
535
640
|
{ layout: '1', label: '一列' },
|
|
536
|
-
{ layout: '1-1', label: '
|
|
537
|
-
{ layout: '1-1-1', label: '三列' },
|
|
641
|
+
{ layout: '1-1', label: '多列' },
|
|
538
642
|
];
|
|
539
643
|
const bar = h('div', { class: 'sm-add-section-bar' }, [h('span', {}, ['+ 添加 Section:'])]);
|
|
540
644
|
for (const l of layouts) {
|
|
@@ -646,6 +750,7 @@ export class Canvas {
|
|
|
646
750
|
mode: def.inlineEditable.mode,
|
|
647
751
|
multiline: !!def.inlineEditable.multiline,
|
|
648
752
|
placeholder: def.inlineEditable.placeholder,
|
|
753
|
+
globalStyles: this.opts.store.doc.styles,
|
|
649
754
|
onCommit: (value) => {
|
|
650
755
|
const propKey = def.inlineEditable!.propKey;
|
|
651
756
|
const editingId = block.id;
|
|
@@ -702,12 +807,33 @@ export class Canvas {
|
|
|
702
807
|
if (this.editingBlockId) this.commitInlineEdit();
|
|
703
808
|
}
|
|
704
809
|
|
|
705
|
-
private
|
|
810
|
+
private _resolvePaletteDrop(blockType: string): PaletteDropResult {
|
|
706
811
|
const def = this.opts.registry.get(blockType);
|
|
707
812
|
if (def?.expandPaletteDrop) {
|
|
708
|
-
|
|
813
|
+
const out = def.expandPaletteDrop((t) => this.opts.registry.createBlock(t));
|
|
814
|
+
if (Array.isArray(out)) return { blocks: out };
|
|
815
|
+
return out;
|
|
709
816
|
}
|
|
710
|
-
return [this.opts.registry.createBlock(blockType)];
|
|
817
|
+
return { blocks: [this.opts.registry.createBlock(blockType)] };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
private _applySectionAttrs(target: Section, partial?: Partial<SectionAttrs>) {
|
|
821
|
+
if (!partial) return;
|
|
822
|
+
const { meta, ...rest } = partial;
|
|
823
|
+
Object.assign(target.attrs, rest);
|
|
824
|
+
if (meta && typeof meta === 'object') {
|
|
825
|
+
target.attrs.meta = { ...(target.attrs.meta ?? {}), ...meta };
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** 未开启 `enableDynamicVariantKey` 时剥离 palette 上的动态变量节属性 */
|
|
830
|
+
private _paletteSectionAttrsForDrop(
|
|
831
|
+
sectionAttrs?: Partial<SectionAttrs>,
|
|
832
|
+
): Partial<SectionAttrs> | undefined {
|
|
833
|
+
if (!sectionAttrs) return undefined;
|
|
834
|
+
if (this.opts.ui?.enableDynamicVariantKey === true) return sectionAttrs;
|
|
835
|
+
const { dynamicVariantKey: _dv, ...rest } = sectionAttrs;
|
|
836
|
+
return Object.keys(rest).length ? rest : undefined;
|
|
711
837
|
}
|
|
712
838
|
|
|
713
839
|
private _handleSectionAdd(e: Sortable.SortableEvent) {
|
|
@@ -731,8 +857,9 @@ export class Canvas {
|
|
|
731
857
|
// 路径 B:左栏内容/自定义卡片拖到 sections 之间 → 自动裹一列 Section
|
|
732
858
|
if (sourceGroup === 'blocks' && blockType) {
|
|
733
859
|
item.parentElement?.removeChild(item);
|
|
734
|
-
const blocks = this.
|
|
860
|
+
const { blocks, sectionAttrs } = this._resolvePaletteDrop(blockType);
|
|
735
861
|
const newSection = createSection('1');
|
|
862
|
+
this._applySectionAttrs(newSection, this._paletteSectionAttrsForDrop(sectionAttrs));
|
|
736
863
|
newSection.columns[0].blocks.splice(newIndex, 0, ...blocks);
|
|
737
864
|
this.opts.store.update((d) => {
|
|
738
865
|
d.sections.splice(newIndex, 0, newSection);
|
|
@@ -797,7 +924,29 @@ export class Canvas {
|
|
|
797
924
|
|
|
798
925
|
if (sourceGroup === 'blocks' && blockType) {
|
|
799
926
|
item.parentElement?.removeChild(item);
|
|
800
|
-
const
|
|
927
|
+
const drop = this._resolvePaletteDrop(blockType);
|
|
928
|
+
const paletteSectionAttrs = this._paletteSectionAttrsForDrop(drop.sectionAttrs);
|
|
929
|
+
if (paletteSectionAttrs?.dynamicVariantKey) {
|
|
930
|
+
const newSection = createSection('1');
|
|
931
|
+
this._applySectionAttrs(newSection, paletteSectionAttrs);
|
|
932
|
+
newSection.columns[0].blocks.push(...drop.blocks);
|
|
933
|
+
this.opts.store.update((d) => {
|
|
934
|
+
const secIdx = d.sections.findIndex((s) => s.id === sectionId);
|
|
935
|
+
const insertAt = secIdx >= 0 ? secIdx + 1 : d.sections.length;
|
|
936
|
+
d.sections.splice(insertAt, 0, newSection);
|
|
937
|
+
});
|
|
938
|
+
const head = drop.blocks[0];
|
|
939
|
+
if (head) {
|
|
940
|
+
this.opts.store.setSelection({
|
|
941
|
+
kind: 'block',
|
|
942
|
+
sectionId: newSection.id,
|
|
943
|
+
columnIndex: 0,
|
|
944
|
+
blockId: head.id,
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const blocks = drop.blocks;
|
|
801
950
|
this.opts.store.update((d) => {
|
|
802
951
|
const sec = d.sections.find((s) => s.id === sectionId);
|
|
803
952
|
if (!sec) return;
|
|
@@ -893,9 +1042,16 @@ export class Canvas {
|
|
|
893
1042
|
}
|
|
894
1043
|
|
|
895
1044
|
/**
|
|
896
|
-
*
|
|
897
|
-
*
|
|
1045
|
+
* 内联编辑中从编辑区拖选文字,若 mouseup 落在 Section padding / 灰底 / 留白,
|
|
1046
|
+
* 浏览器仍可能对 mouseup 目标派发 click;选区仍在编辑区内时不应提交或改选中。
|
|
898
1047
|
*/
|
|
1048
|
+
private _shouldSuppressClearSelectionFromTextDrag(): boolean {
|
|
1049
|
+
return (
|
|
1050
|
+
this.pointerGestureStartedInInlineEdit &&
|
|
1051
|
+
this._shouldSuppressSectionSelectFromTextInteraction()
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
899
1055
|
private _shouldSuppressSectionSelectFromTextInteraction(): boolean {
|
|
900
1056
|
if (!this.editingBlockId) return false;
|
|
901
1057
|
const editingEl = this.inner.querySelector('.sm-inline-editing');
|