@seliseblocks/mailcraft 0.2.13 → 0.2.14
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/package.json
CHANGED
package/src/core/editor-core.js
CHANGED
|
@@ -580,7 +580,7 @@ export class EditorCore {
|
|
|
580
580
|
* dragging through uniformly-formatted text rebuilds nothing at all.
|
|
581
581
|
*/
|
|
582
582
|
formatFingerprint(b) {
|
|
583
|
-
let s = b.id + '|' + this.currentTag() + '|' + (b
|
|
583
|
+
let s = b.id + '|' + this.currentTag() + '|' + this.selSize(b) + '|' + (this.state.linkDraft ? 1 : 0) + '|';
|
|
584
584
|
for (const cmd of ['bold', 'italic', 'underline', 'strikeThrough', 'superscript', 'subscript', 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 'insertUnorderedList', 'insertOrderedList']) {
|
|
585
585
|
let on = false;
|
|
586
586
|
try { on = document.queryCommandState(cmd); } catch { /* ignore */ }
|
|
@@ -622,6 +622,13 @@ export class EditorCore {
|
|
|
622
622
|
};
|
|
623
623
|
|
|
624
624
|
size(b, delta) {
|
|
625
|
+
// A non-collapsed selection inside a rich text block means the user is
|
|
626
|
+
// sizing a *run*, not the block -- every neighbouring control (bold,
|
|
627
|
+
// color, highlight) is selection-scoped, so a ± that rewrote the whole
|
|
628
|
+
// block's prop here read as broken. Only `text` can keep the resulting
|
|
629
|
+
// spans: a heading folds back through `textContent` (syncEdit), which
|
|
630
|
+
// would silently drop them, so it stays block-level.
|
|
631
|
+
if (b.type === 'text' && this.sizeSelection(b, delta)) return;
|
|
625
632
|
// Uncommitted inline formatting is folded into props by `setProp` below
|
|
626
633
|
// (`onFoldLiveEdit`), not here. This used to do its own fold, as a second
|
|
627
634
|
// commit: that read `editEl.innerHTML` unconditionally, so a second click
|
|
@@ -639,6 +646,114 @@ export class EditorCore {
|
|
|
639
646
|
this.setProp(b.id, 'size', Math.max(lo, Math.min(hi, cur + delta)));
|
|
640
647
|
}
|
|
641
648
|
|
|
649
|
+
/** Nearest inline px font-size walking up from `node` to the edited block's wrapper -- null when no run declares one (the block prop then owns the size). */
|
|
650
|
+
inlineSizeAt(node) {
|
|
651
|
+
let n = node && node.nodeType === 1 ? node : (node ? node.parentElement : null);
|
|
652
|
+
while (n && n !== this.editEl) {
|
|
653
|
+
const m = /^([\d.]+)px$/.exec((n.style && n.style.fontSize) || '');
|
|
654
|
+
if (m) return parseFloat(m[1]);
|
|
655
|
+
n = n.parentElement;
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** What the ± readout should show: the inline size at the selection when the caret sits inside a sized run, else the block's own size. Also part of `formatFingerprint`, so moving the caret across differently-sized runs refreshes the toolbar. */
|
|
661
|
+
selSize(b) {
|
|
662
|
+
const live = this.find(this.state.doc, b.id).block || b;
|
|
663
|
+
const base = Number(live.props.size) || 16;
|
|
664
|
+
if (b.type !== 'text' || this.state.editing !== b.id || !this.editEl) return base;
|
|
665
|
+
const sel = this.getSelection();
|
|
666
|
+
const node = sel && sel.rangeCount && this.editEl.contains(sel.anchorNode)
|
|
667
|
+
? sel.anchorNode
|
|
668
|
+
: (this.savedRange && this.editEl.contains(this.savedRange.startContainer) ? this.savedRange.startContainer : null);
|
|
669
|
+
const inline = node ? this.inlineSizeAt(node) : null;
|
|
670
|
+
return inline == null ? base : Math.round(inline);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Sizes just the selected run(s) of text by wrapping each selected text node
|
|
675
|
+
* in a `font-size` span (or restepping the span a previous click made --
|
|
676
|
+
* repeated ± must not nest one span per click). Wrapping happens at the text
|
|
677
|
+
* node, the innermost level, so the new size always outranks any inline size
|
|
678
|
+
* an imported ancestor carries. The change lives in the contenteditable like
|
|
679
|
+
* bold/italic do and folds into props through the same blur/commit path.
|
|
680
|
+
*
|
|
681
|
+
* Returns false when the click is not selection-scoped -- no live edit, a
|
|
682
|
+
* bare caret, or a selection covering the whole block. The last keeps
|
|
683
|
+
* select-all + ± behaving as the block-level master scale it always was
|
|
684
|
+
* (`syncRichContent` then *scales* mixed sizes instead of flattening them,
|
|
685
|
+
* and the saved `size` prop stays truthful).
|
|
686
|
+
*/
|
|
687
|
+
sizeSelection(b, delta) {
|
|
688
|
+
const root = this.editEl;
|
|
689
|
+
if (!root || !root.isConnected || this.state.editing !== b.id) return false;
|
|
690
|
+
const sel = this.getSelection();
|
|
691
|
+
// Same fallback discipline as `exec`: the live selection wins when it is
|
|
692
|
+
// inside the edited block; `savedRange` covers focus stolen by a control.
|
|
693
|
+
let src = sel && sel.rangeCount && root.contains(sel.anchorNode) && root.contains(sel.focusNode) ? sel.getRangeAt(0) : null;
|
|
694
|
+
if (!src && this.savedRange && root.contains(this.savedRange.startContainer) && root.contains(this.savedRange.endContainer)) src = this.savedRange;
|
|
695
|
+
if (!src || src.collapsed) return false;
|
|
696
|
+
const range = src.cloneRange();
|
|
697
|
+
const total = root.textContent.length;
|
|
698
|
+
if (charOffset(root, range.startContainer, range.startOffset) === 0
|
|
699
|
+
&& charOffset(root, range.endContainer, range.endOffset) === total) return false;
|
|
700
|
+
|
|
701
|
+
const [lo, hi] = SIZE_SPAN.text;
|
|
702
|
+
const live = this.find(this.state.doc, b.id).block || b;
|
|
703
|
+
const cur = this.inlineSizeAt(range.startContainer) || Number(live.props.size) || 16;
|
|
704
|
+
const next = Math.max(lo, Math.min(hi, Math.round(cur) + delta));
|
|
705
|
+
|
|
706
|
+
// Split the boundary text nodes so every text node intersecting the range
|
|
707
|
+
// is *fully* inside it; order matters when both ends share one node.
|
|
708
|
+
const endC = range.endContainer;
|
|
709
|
+
if (endC.nodeType === 3 && range.endOffset < endC.nodeValue.length) endC.splitText(range.endOffset);
|
|
710
|
+
const startC = range.startContainer;
|
|
711
|
+
if (startC.nodeType === 3 && range.startOffset > 0) {
|
|
712
|
+
const tail = startC.splitText(range.startOffset);
|
|
713
|
+
range.setStart(tail, 0);
|
|
714
|
+
if (endC === startC) range.setEnd(tail, tail.nodeValue.length);
|
|
715
|
+
}
|
|
716
|
+
const s = charOffset(root, range.startContainer, range.startOffset);
|
|
717
|
+
const e = charOffset(root, range.endContainer, range.endOffset);
|
|
718
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
719
|
+
const hits = [];
|
|
720
|
+
let pos = 0; let tn;
|
|
721
|
+
while ((tn = walker.nextNode())) {
|
|
722
|
+
const len = tn.nodeValue.length;
|
|
723
|
+
if (len && pos >= s && pos + len <= e) hits.push(tn);
|
|
724
|
+
pos += len;
|
|
725
|
+
if (pos >= e) break;
|
|
726
|
+
}
|
|
727
|
+
// Something non-text was selected (an image, say): still handled -- the
|
|
728
|
+
// click must not fall through and resize the whole block.
|
|
729
|
+
if (!hits.length) return true;
|
|
730
|
+
const wraps = hits.map((node) => {
|
|
731
|
+
const parent = node.parentElement;
|
|
732
|
+
if (parent && parent !== root && parent.tagName === 'SPAN' && parent.childNodes.length === 1) {
|
|
733
|
+
parent.style.fontSize = next + 'px';
|
|
734
|
+
return parent;
|
|
735
|
+
}
|
|
736
|
+
const span = document.createElement('span');
|
|
737
|
+
span.style.fontSize = next + 'px';
|
|
738
|
+
node.replaceWith(span);
|
|
739
|
+
span.appendChild(node);
|
|
740
|
+
return span;
|
|
741
|
+
});
|
|
742
|
+
// Reselect the runs so the next ± click steps from here, and cache the
|
|
743
|
+
// range the way `exec` does for controls that steal focus. Boundaries go
|
|
744
|
+
// *inside* the first/last wrap (each holds exactly one text node), so the
|
|
745
|
+
// selection anchor sits under the new span and `selSize` reads it for the
|
|
746
|
+
// toolbar readout.
|
|
747
|
+
const first = wraps[0].firstChild;
|
|
748
|
+
const last = wraps[wraps.length - 1].lastChild;
|
|
749
|
+
const r2 = document.createRange();
|
|
750
|
+
r2.setStart(first, 0);
|
|
751
|
+
r2.setEnd(last, last.nodeValue.length);
|
|
752
|
+
if (sel) { sel.removeAllRanges(); sel.addRange(r2); }
|
|
753
|
+
this.savedRange = r2.cloneRange();
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
|
|
642
757
|
hasCountdown() { return this.state.doc.rows.some((r) => r.cols.some((c) => c.blocks.some((b) => b.type === 'countdown'))); }
|
|
643
758
|
|
|
644
759
|
persist(doc, assets, chrome) {
|
package/src/render/block-body.js
CHANGED
|
@@ -158,8 +158,14 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
158
158
|
// `inline-table` so the wrapper's text-align still positions it; the
|
|
159
159
|
// `align` attribute is the same instruction for Word, which ignores the
|
|
160
160
|
// display value. A full-width button is a plain 100% table instead.
|
|
161
|
+
// `float:none` is load-bearing: browsers map `align="left|right"` on a
|
|
162
|
+
// table to a float presentational hint, which takes the pill out of
|
|
163
|
+
// flow -- the block collapses to its padding on the canvas and the
|
|
164
|
+
// button paints over the next block. The inline style outranks the
|
|
165
|
+
// hint everywhere floats work, and Word ignores CSS float, so the
|
|
166
|
+
// `align` attribute still does its one job there.
|
|
161
167
|
const table = el('table', {
|
|
162
|
-
display: p.full ? 'table' : 'inline-table', width: p.full ? '100%' : 'auto', borderCollapse: 'separate',
|
|
168
|
+
display: p.full ? 'table' : 'inline-table', width: p.full ? '100%' : 'auto', borderCollapse: 'separate', cssFloat: 'none',
|
|
163
169
|
}, { role: 'presentation', cellpadding: '0', cellspacing: '0', border: '0', align: p.full ? undefined : p.align });
|
|
164
170
|
const td = el('td', {
|
|
165
171
|
background: p.bg, borderRadius: p.radius + 'px', padding: p.py + 'px ' + p.px + 'px', textAlign: 'center',
|
package/src/render/rte.js
CHANGED
|
@@ -138,7 +138,10 @@ export function renderRte(core, b) {
|
|
|
138
138
|
if (b.type === 'text' || b.type === 'heading') r1.append(
|
|
139
139
|
sep(),
|
|
140
140
|
btn('minus', 'Smaller text', () => core.size(b, -1)),
|
|
141
|
-
|
|
141
|
+
// `selSize`, not `props.size`: with the caret inside a run the ± pair
|
|
142
|
+
// sized on its own (core `sizeSelection`), the readout shows that run's
|
|
143
|
+
// size -- the block prop no longer tells the whole story.
|
|
144
|
+
el('span', { fontFamily: 'var(--ed-font)', fontSize: '9.5px', color: 'var(--rte-muted)', minWidth: '32px', textAlign: 'center' }, { text: core.selSize(b) + 'px' }),
|
|
142
145
|
btn('plus', 'Larger text', () => core.size(b, 1)),
|
|
143
146
|
);
|
|
144
147
|
|