fable-editor 1.0.0 → 1.0.2

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.
@@ -136,7 +136,13 @@ const I18N = {
136
136
  insertimg: 'Insert',
137
137
  dropimage: 'Drop image here',
138
138
  uploading: 'Uploading…',
139
- uploadfailed: 'Upload failed — click to retry'
139
+ uploadfailed: 'Upload failed — click to retry',
140
+ template: 'Templates',
141
+ tplimgleft: 'Image left, text right',
142
+ tplimgright: 'Image right, text left',
143
+ tplimgtop: 'Image top, text below',
144
+ tplimgcenter: 'Text top, image center',
145
+ deltemplate: 'Delete template'
140
146
  },
141
147
  ar: {
142
148
  dir: 'rtl',
@@ -272,7 +278,13 @@ const I18N = {
272
278
  insertimg: 'إدراج',
273
279
  dropimage: 'أسقط الصورة هنا',
274
280
  uploading: 'جارٍ الرفع…',
275
- uploadfailed: 'فشل الرفع — انقر لإعادة المحاولة'
281
+ uploadfailed: 'فشل الرفع — انقر لإعادة المحاولة',
282
+ template: 'قوالب',
283
+ tplimgleft: 'صورة يسار، نص يمين',
284
+ tplimgright: 'صورة يمين، نص يسار',
285
+ tplimgtop: 'صورة أعلى، نص أسفل',
286
+ tplimgcenter: 'نص أعلى، صورة وسط',
287
+ deltemplate: 'حذف القالب'
276
288
  }
277
289
  };
278
290
  function getStrings(lang) {
@@ -284,7 +296,7 @@ function getStrings(lang) {
284
296
  const DEFAULT_TOOLBAR = 'undo redo | preview print | importword revhistory | fontfamily fontsize | fontsizeincrease fontsizedecrease | ' +
285
297
  'bold italic underline strikethrough | forecolor backcolor | alignleft aligncenter alignright alignjustify | ' +
286
298
  'bullist numlist | outdent indent | link blockquote changecase lineheight wordspacing | removeformat | blocks | ' +
287
- 'ltr rtl | quickimage quicktable charmap | fullscreen sourcecode';
299
+ 'ltr rtl | quickimage quicktable template charmap | fullscreen sourcecode';
288
300
  /** Transcribed from the editor's built-in menu keys so that `menubar: true` (the
289
301
  * default) renders identically to the hard-coded original. */
290
302
  const DEFAULT_MENUBAR = 'file edit view insert format tools table help';
@@ -475,6 +487,11 @@ const IC = {
475
487
  wordic: S('<path d="M6 3h8l5 5v13H6z"/><path d="M14 3v5h5"/><text x="8" y="17" font-size="8.5" stroke="none" fill="#222f3e" font-family="Arial" font-weight="bold">W</text>'),
476
488
  draftic: G('M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z'),
477
489
  trash: S('<path d="M5 7h14M10 7V5h4v2M7.5 7l1 13h7l1-13"/>'),
490
+ templateic: S('<rect x="4" y="4" width="16" height="16" rx="1.5"/><rect x="6.5" y="6.5" width="5" height="5" rx=".5"/><path d="M14 7.5h3.5M14 10h3.5M6.5 14h11M6.5 17h11"/>'),
491
+ tplleft: S('<rect x="3" y="5" width="8" height="8" rx=".8"/><path d="M13.5 6.5H21M13.5 9.5H21M13.5 12.5H21M3 15.5h18M3 18.5h18"/>'),
492
+ tplright: S('<rect x="13" y="5" width="8" height="8" rx=".8"/><path d="M3 6.5h7.5M3 9.5h7.5M3 12.5h7.5M3 15.5h18M3 18.5h18"/>'),
493
+ tpltop: S('<rect x="5" y="3" width="14" height="9" rx=".8"/><path d="M3 15h18M3 18h18M3 21h12"/>'),
494
+ tplcenter: S('<path d="M3 4h18M3 7h18M3 10h12"/><rect x="5" y="12.5" width="14" height="9" rx=".8"/>'),
478
495
  rotateleft: S('<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>'),
479
496
  rotateright: S('<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>'),
480
497
  fliphorizontal: S('<path d="M12 3v18"/><path d="M7 8l-4 4 4 4"/><path d="M17 8l4 4-4 4"/>'),
@@ -1364,6 +1381,19 @@ async function importDocxToHtml(input) {
1364
1381
  }
1365
1382
 
1366
1383
  const IMG_CORNERS = ['nw', 'ne', 'sw', 'se'];
1384
+ const TPL_LAYOUTS = ['img-left', 'img-right', 'img-top', 'img-center'];
1385
+ const TPL_LABEL = {
1386
+ 'img-left': 'tplimgleft',
1387
+ 'img-right': 'tplimgright',
1388
+ 'img-top': 'tplimgtop',
1389
+ 'img-center': 'tplimgcenter'
1390
+ };
1391
+ const TPL_ICON = {
1392
+ 'img-left': IC.tplleft,
1393
+ 'img-right': IC.tplright,
1394
+ 'img-top': IC.tpltop,
1395
+ 'img-center': IC.tplcenter
1396
+ };
1367
1397
  class FableEditor {
1368
1398
  static instanceCounter = 0;
1369
1399
  options;
@@ -1403,6 +1433,8 @@ class FableEditor {
1403
1433
  onImageUploadError;
1404
1434
  contentStyle;
1405
1435
  selCtx = null;
1436
+ tplActive = null;
1437
+ tplCtx = null;
1406
1438
  docInput;
1407
1439
  revisions = [];
1408
1440
  revTimer = null;
@@ -1561,12 +1593,17 @@ class FableEditor {
1561
1593
  this.positionTableHandles();
1562
1594
  this.positionImageHandles();
1563
1595
  this.positionImgPhCtx();
1596
+ this.positionTplCtx();
1564
1597
  });
1565
1598
  this.on(this.ed, 'keydown', (e) => {
1566
1599
  if (e.altKey && e.key === '0') {
1567
1600
  e.preventDefault();
1568
1601
  this.helpDlg();
1569
1602
  }
1603
+ if ((e.key === 'Delete' || e.key === 'Backspace') && this.phActive) {
1604
+ e.preventDefault();
1605
+ this.removeImgPlaceholder();
1606
+ }
1570
1607
  });
1571
1608
  this.on(this.ed, 'paste', (e) => this.handlePaste(e));
1572
1609
  this.on(this.ed, 'drop', (e) => {
@@ -1591,11 +1628,17 @@ class FableEditor {
1591
1628
  const ph = target.closest?.('.img-ph');
1592
1629
  if (ph && this.ed.contains(ph) && !this.options.readonly) {
1593
1630
  e.preventDefault();
1631
+ this.ed.focus();
1594
1632
  this.selectImgPlaceholder(ph);
1595
1633
  }
1596
1634
  else {
1597
1635
  this.clearImgPlaceholderSel();
1598
1636
  }
1637
+ const tpl = target.closest?.('.tpl');
1638
+ if (tpl && this.ed.contains(tpl) && !this.options.readonly)
1639
+ this.selectTemplate(tpl);
1640
+ else
1641
+ this.clearTplSel();
1599
1642
  const img = target.closest?.('img');
1600
1643
  if (img && this.ed.contains(img) && !this.options.readonly) {
1601
1644
  e.preventDefault();
@@ -1630,17 +1673,20 @@ class FableEditor {
1630
1673
  this.positionTableHandles();
1631
1674
  this.positionImageHandles();
1632
1675
  this.positionImgPhCtx();
1676
+ this.positionTplCtx();
1633
1677
  });
1634
1678
  this.onWin('scroll', () => {
1635
1679
  this.positionTableHandles();
1636
1680
  this.positionImageHandles();
1637
1681
  this.positionImgPhCtx();
1682
+ this.positionTplCtx();
1638
1683
  this.repositionSelToolbar();
1639
1684
  }, true);
1640
1685
  this.onWin('resize', () => {
1641
1686
  this.positionTableHandles();
1642
1687
  this.positionImageHandles();
1643
1688
  this.positionImgPhCtx();
1689
+ this.positionTplCtx();
1644
1690
  this.repositionSelToolbar();
1645
1691
  });
1646
1692
  this.on(this.imgInput, 'change', () => {
@@ -1675,6 +1721,7 @@ class FableEditor {
1675
1721
  this.imgHandles.includes(e.target) ||
1676
1722
  (this.imgCtx && this.imgCtx.contains(e.target)) ||
1677
1723
  (this.phCtx && this.phCtx.contains(e.target)) ||
1724
+ (this.tplCtx && this.tplCtx.contains(e.target)) ||
1678
1725
  (this.selCtx && this.selCtx.contains(e.target)) ||
1679
1726
  (this.openPop && this.openPop.contains(e.target)) ||
1680
1727
  (this.openSubEl && this.openSubEl.contains(e.target)))
@@ -1682,6 +1729,7 @@ class FableEditor {
1682
1729
  this.clearTableHandles();
1683
1730
  this.clearImageHandles();
1684
1731
  this.clearImgPlaceholderSel();
1732
+ this.clearTplSel();
1685
1733
  this.clearSelToolbar();
1686
1734
  });
1687
1735
  }
@@ -1707,6 +1755,7 @@ class FableEditor {
1707
1755
  this.clearTableHandles();
1708
1756
  this.clearImageHandles();
1709
1757
  this.clearImgPlaceholderSel();
1758
+ this.clearTplSel();
1710
1759
  this.clearSelToolbar();
1711
1760
  this.onChange();
1712
1761
  }
@@ -1768,6 +1817,7 @@ class FableEditor {
1768
1817
  this.clearTableHandles();
1769
1818
  this.clearImageHandles();
1770
1819
  this.clearImgPlaceholderSel();
1820
+ this.clearTplSel();
1771
1821
  this.clearSelToolbar();
1772
1822
  this.listeners.forEach((fn) => fn());
1773
1823
  this.listeners = [];
@@ -2612,6 +2662,7 @@ class FableEditor {
2612
2662
  rtl: () => this.tbtn(IC.rtl, this.t('rtl'), () => this.setDir('rtl'), 'rtl'),
2613
2663
  quickimage: () => this.tbtn(IC.image, this.t('quickimage'), () => this.insertImagePlaceholder()),
2614
2664
  quicktable: () => tableBtn,
2665
+ template: () => this.menuTbtn(IC.templateic, this.t('template'), (b) => this.templateMenu(b)),
2615
2666
  charmap: () => this.tbtn(IC.charic, this.t('charmap'), () => this.charMap()),
2616
2667
  fullscreen: () => this.tbtn(IC.fullscreen, this.t('fullscreen'), () => this.toggleFullscreen(), 'fullscreen'),
2617
2668
  sourcecode: () => this.tbtn(IC.srcic, this.t('sourcecode'), () => this.sourceDlg())
@@ -2683,6 +2734,7 @@ class FableEditor {
2683
2734
  this.mItem('link', IC.link, () => this.linkDlg()),
2684
2735
  this.mItem('image', IC.image, () => this.insertImagePlaceholder()),
2685
2736
  { label: this.t('inserttable'), icon: IC.tableic, action: () => this.tableGrid(this.menubar.querySelector('[data-menu-key="table"]') || this.menubar) },
2737
+ { label: this.t('template'), icon: IC.templateic, subBuild: (el) => this.buildTemplatePickInto(el) },
2686
2738
  this.mItem('hr', IC.hric, () => this.exec('insertHorizontalRule')),
2687
2739
  '|',
2688
2740
  this.mItem('charmap', IC.charic, () => this.charMap()),
@@ -2755,7 +2807,7 @@ class FableEditor {
2755
2807
  this.tablePropsDlg();
2756
2808
  }
2757
2809
  },
2758
- { label: this.t('deltable'), icon: IC.tabledelete, action: () => this.tableOp('deltable') }
2810
+ { label: this.t('deltable'), icon: IC.trash, action: () => this.tableOp('deltable') }
2759
2811
  ],
2760
2812
  help: [this.mItem('helpttl', IC.helpic, () => this.helpDlg(), 'Alt+0')]
2761
2813
  };
@@ -3286,10 +3338,13 @@ class FableEditor {
3286
3338
  this.imgInput.click();
3287
3339
  }
3288
3340
  /* ---------------------------------------------- image upload placeholder */
3341
+ imgPhHTML(id) {
3342
+ return `<div class="img-ph"${id ? ` id="${id}"` : ''} contenteditable="false">${IC.image}<span>${this.t('dropimage')}</span></div>`;
3343
+ }
3289
3344
  insertImagePlaceholder() {
3290
3345
  this.restoreSel();
3291
3346
  const id = 'img-ph-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
3292
- document.execCommand('insertHTML', false, `<div class="img-ph" id="${id}" contenteditable="false">${IC.image}<span>${this.t('dropimage')}</span></div>`);
3347
+ document.execCommand('insertHTML', false, this.imgPhHTML(id));
3293
3348
  const ph = this.ed.querySelector('#' + id);
3294
3349
  if (ph) {
3295
3350
  ph.removeAttribute('id');
@@ -3303,6 +3358,17 @@ class FableEditor {
3303
3358
  this.phActive?.classList.remove('active');
3304
3359
  this.phActive = null;
3305
3360
  }
3361
+ removeImgPlaceholder() {
3362
+ const ph = this.phActive;
3363
+ if (!ph)
3364
+ return;
3365
+ this.clearImgPlaceholderSel();
3366
+ /* the placeholder inside a template block IS the media slot — deleting
3367
+ it drops the slot entirely, leaving a text-only block */
3368
+ (ph.closest('.tpl-media') || ph).remove();
3369
+ this.refreshState();
3370
+ this.onChange();
3371
+ }
3306
3372
  selectImgPlaceholder(ph) {
3307
3373
  if (this.phActive === ph)
3308
3374
  return;
@@ -3323,7 +3389,7 @@ class FableEditor {
3323
3389
  el.append(this.ctxBtn(IC.uploadic, this.t('uploadimg'), () => {
3324
3390
  this.phUploadTarget = this.phActive;
3325
3391
  this.pickImage();
3326
- }), this.ctxBtn(IC.link, this.t('imagelink'), () => this.renderPhCtxUrlInput()));
3392
+ }), this.ctxBtn(IC.link, this.t('imagelink'), () => this.renderPhCtxUrlInput()), this.ctxSep(), this.ctxBtn(IC.trash, this.t('deleteimg'), () => this.removeImgPlaceholder()));
3327
3393
  this.positionImgPhCtx();
3328
3394
  }
3329
3395
  renderPhCtxUrlInput() {
@@ -3448,6 +3514,139 @@ class FableEditor {
3448
3514
  this.phCtx.style.left = cx + 'px';
3449
3515
  this.phCtx.style.top = cy + 'px';
3450
3516
  }
3517
+ /* ---------------------------------------------------------- templates */
3518
+ templateMenu(anchor) {
3519
+ this.popup(anchor, (el) => this.buildTemplatePickInto(el));
3520
+ }
3521
+ /** Miniature div-based mockup of a layout, mirroring what the block looks
3522
+ like in the editor: grey box = image slot, bars = heading/paragraph. */
3523
+ tplPreviewHTML(layout) {
3524
+ const img = '<span class="pv-img"></span>';
3525
+ const head = '<span class="pv-h"></span>';
3526
+ const line = (w) => `<span class="pv-l" style="width:${w}%"></span>`;
3527
+ const col = (inner) => `<span class="pv-col">${inner}</span>`;
3528
+ const row = (inner) => `<span class="pv-row">${inner}</span>`;
3529
+ const textCol = col(head + line(100) + line(88));
3530
+ if (layout === 'img-left')
3531
+ return `<span class="pv">${row(img + textCol)}${line(100)}${line(72)}</span>`;
3532
+ if (layout === 'img-right')
3533
+ return `<span class="pv">${row(textCol + img)}${line(100)}${line(72)}</span>`;
3534
+ if (layout === 'img-top')
3535
+ return `<span class="pv pv-c">${img}${head}${line(100)}${line(72)}</span>`;
3536
+ return `<span class="pv pv-c">${head}${line(100)}${line(72)}${img}</span>`;
3537
+ }
3538
+ buildTemplatePickInto(el, after) {
3539
+ const wrap = document.createElement('div');
3540
+ wrap.className = 'tplpick';
3541
+ TPL_LAYOUTS.forEach((layout) => {
3542
+ const tile = document.createElement('button');
3543
+ tile.type = 'button';
3544
+ tile.className = 'tpltile';
3545
+ tile.title = this.t(TPL_LABEL[layout]);
3546
+ tile.innerHTML = this.tplPreviewHTML(layout) + `<span class="tpllbl">${this.t(TPL_LABEL[layout])}</span>`;
3547
+ tile.addEventListener('click', () => {
3548
+ this.closePop();
3549
+ this.insertTemplate(layout);
3550
+ after?.();
3551
+ });
3552
+ wrap.appendChild(tile);
3553
+ });
3554
+ el.appendChild(wrap);
3555
+ }
3556
+ insertTemplate(layout) {
3557
+ this.restoreSel();
3558
+ const media = `<div class="tpl-media" contenteditable="false">${this.imgPhHTML()}</div>`;
3559
+ const text = `<div class="tpl-text"><h2>${this.t('heading')}</h2><p>${this.t('para')}</p></div>`;
3560
+ /* text-above-image layout keeps DOM order = visual order so the caret
3561
+ travels naturally; every other layout has the media slot first */
3562
+ const inner = layout === 'img-center' ? text + media : media + text;
3563
+ document.execCommand('insertHTML', false, `<div class="tpl tpl-${layout}">${inner}</div><p><br></p>`);
3564
+ this.saveSel();
3565
+ this.onChange();
3566
+ }
3567
+ clearTplSel() {
3568
+ this.tplCtx?.remove();
3569
+ this.tplCtx = null;
3570
+ this.tplActive?.classList.remove('tpl-selected');
3571
+ this.tplActive = null;
3572
+ }
3573
+ selectTemplate(tpl) {
3574
+ if (this.tplActive === tpl)
3575
+ return;
3576
+ this.clearTplSel();
3577
+ this.tplActive = tpl;
3578
+ tpl.classList.add('tpl-selected');
3579
+ this.tplCtx = document.createElement('div');
3580
+ this.tplCtx.className = 'imgctx tplctx';
3581
+ this.tplCtx.dir = this.dir();
3582
+ document.body.appendChild(this.tplCtx);
3583
+ this.renderTplCtxButtons();
3584
+ }
3585
+ renderTplCtxButtons() {
3586
+ const el = this.tplCtx;
3587
+ const tpl = this.tplActive;
3588
+ if (!el || !tpl)
3589
+ return;
3590
+ el.innerHTML = '';
3591
+ TPL_LAYOUTS.forEach((layout) => {
3592
+ const b = this.ctxBtn(TPL_ICON[layout], this.t(TPL_LABEL[layout]), () => this.setTplLayout(layout));
3593
+ b.classList.toggle('on', tpl.classList.contains('tpl-' + layout));
3594
+ el.appendChild(b);
3595
+ });
3596
+ el.append(this.ctxSep(), this.ctxBtn(IC.trash, this.t('deltemplate'), () => this.removeTemplate()));
3597
+ this.positionTplCtx();
3598
+ }
3599
+ setTplLayout(layout) {
3600
+ const tpl = this.tplActive;
3601
+ if (!tpl)
3602
+ return;
3603
+ TPL_LAYOUTS.forEach((l) => tpl.classList.remove('tpl-' + l));
3604
+ tpl.classList.add('tpl-' + layout);
3605
+ /* keep DOM order = visual order (see insertTemplate) */
3606
+ const media = tpl.querySelector(':scope > .tpl-media');
3607
+ const text = tpl.querySelector(':scope > .tpl-text');
3608
+ if (media && text) {
3609
+ if (layout === 'img-center')
3610
+ tpl.appendChild(media);
3611
+ else
3612
+ tpl.insertBefore(media, text);
3613
+ }
3614
+ this.renderTplCtxButtons();
3615
+ this.positionImgPhCtx();
3616
+ this.positionImageHandles();
3617
+ this.onChange();
3618
+ }
3619
+ removeTemplate() {
3620
+ const tpl = this.tplActive;
3621
+ if (!tpl)
3622
+ return;
3623
+ this.clearTplSel();
3624
+ if (this.phActive && tpl.contains(this.phActive))
3625
+ this.clearImgPlaceholderSel();
3626
+ if (this.imgActive && tpl.contains(this.imgActive))
3627
+ this.clearImageHandles();
3628
+ tpl.remove();
3629
+ this.refreshState();
3630
+ this.onChange();
3631
+ }
3632
+ positionTplCtx() {
3633
+ if (this.tplActive && !document.body.contains(this.tplActive)) {
3634
+ this.clearTplSel();
3635
+ return;
3636
+ }
3637
+ if (!this.tplActive || !this.tplCtx)
3638
+ return;
3639
+ const r = this.tplActive.getBoundingClientRect();
3640
+ const cw = this.tplCtx.offsetWidth;
3641
+ const ch = this.tplCtx.offsetHeight;
3642
+ let cx = r.left + scrollX + (r.width - cw) / 2;
3643
+ cx = Math.max(8 + scrollX, Math.min(cx, scrollX + innerWidth - cw - 8));
3644
+ let cy = r.top + scrollY - ch - 8;
3645
+ if (cy < this.ctxMinTop())
3646
+ cy = r.bottom + scrollY + 8;
3647
+ this.tplCtx.style.left = cx + 'px';
3648
+ this.tplCtx.style.top = cy + 'px';
3649
+ }
3451
3650
  pickWordDoc() {
3452
3651
  this.docInput.value = '';
3453
3652
  this.docInput.click();
@@ -3554,7 +3753,7 @@ class FableEditor {
3554
3753
  b.addEventListener('mouseleave', () => this.setOpTarget(null));
3555
3754
  return b;
3556
3755
  };
3557
- el.append(hl(this.ctxBtn(IC.rowbefore, this.t('rowabove'), () => this.tableOp('rowabove')), 'row'), hl(this.ctxBtn(IC.rowafter, this.t('rowbelow'), () => this.tableOp('rowbelow')), 'row'), hl(this.ctxBtn(IC.rowdelete, this.t('delrow'), () => this.tableOp('delrow')), 'row'), this.ctxSep(), hl(this.ctxBtn(IC.colbefore, this.t('colbefore'), () => this.tableOp('colbefore')), 'col'), hl(this.ctxBtn(IC.colafter, this.t('colafter'), () => this.tableOp('colafter')), 'col'), hl(this.ctxBtn(IC.coldelete, this.t('delcol'), () => this.tableOp('delcol')), 'col'), this.ctxSep(), this.ctxBtn(IC.tabledelete, this.t('deltable'), () => this.tableOp('deltable')), this.ctxBtn(IC.tableic, this.t('tablepropsttl'), () => this.tablePropsDlg()));
3756
+ el.append(hl(this.ctxBtn(IC.rowbefore, this.t('rowabove'), () => this.tableOp('rowabove')), 'row'), hl(this.ctxBtn(IC.rowafter, this.t('rowbelow'), () => this.tableOp('rowbelow')), 'row'), hl(this.ctxBtn(IC.rowdelete, this.t('delrow'), () => this.tableOp('delrow')), 'row'), this.ctxSep(), hl(this.ctxBtn(IC.colbefore, this.t('colbefore'), () => this.tableOp('colbefore')), 'col'), hl(this.ctxBtn(IC.colafter, this.t('colafter'), () => this.tableOp('colafter')), 'col'), hl(this.ctxBtn(IC.coldelete, this.t('delcol'), () => this.tableOp('delcol')), 'col'), this.ctxSep(), this.ctxBtn(IC.trash, this.t('deltable'), () => this.tableOp('deltable')), this.ctxBtn(IC.tableic, this.t('tablepropsttl'), () => this.tablePropsDlg()));
3558
3757
  document.body.appendChild(el);
3559
3758
  return el;
3560
3759
  }
@@ -3761,7 +3960,13 @@ class FableEditor {
3761
3960
  this.flipImage(img, 'y');
3762
3961
  rebuild();
3763
3962
  }), this.ctxSep(), mkBtn(IC.trash, this.t('deleteimg'), false, () => {
3764
- img.remove();
3963
+ /* inside a template media slot the image swaps back to an
3964
+ upload placeholder so the slot is not lost */
3965
+ const slot = img.closest('.tpl-media');
3966
+ if (slot && this.ed.contains(slot))
3967
+ img.outerHTML = this.imgPhHTML();
3968
+ else
3969
+ img.remove();
3765
3970
  this.clearImageHandles();
3766
3971
  this.onChange();
3767
3972
  }));