@helping-ai-workflow/md2doc 3.13.0 → 3.13.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/lib/editor/client.js +659 -14
- package/lib/editor/inline-md.js +45 -1
- package/lib/editor/list-md.js +8 -1
- package/lib/md2doc.js +423 -74
- package/lib/theme/runtime.js +279 -27
- package/lib/theme/tokens.js +3 -0
- package/package.json +1 -1
package/lib/editor/client.js
CHANGED
|
@@ -1110,7 +1110,9 @@
|
|
|
1110
1110
|
let switching = null;
|
|
1111
1111
|
function switchAwayFrom() {
|
|
1112
1112
|
if (switching) return switching;
|
|
1113
|
-
|
|
1113
|
+
// v3.13.1 T1: a held-key replay that waited on this resolution resumes
|
|
1114
|
+
// once it is over (scheduleFlightFlush() is a no-op with nothing held).
|
|
1115
|
+
switching = resolveOpenSession().finally(() => { switching = null; scheduleFlightFlush(); });
|
|
1114
1116
|
return switching;
|
|
1115
1117
|
}
|
|
1116
1118
|
|
|
@@ -1685,6 +1687,11 @@
|
|
|
1685
1687
|
// through explicitly. Passed positionally here and through
|
|
1686
1688
|
// applyRenderResult() rather than folded into `j` (which is the
|
|
1687
1689
|
// server's response payload, not render-context the CALLER derived).
|
|
1690
|
+
// v3.13.1 R11: the selection right before the DOM is replaced — a click,
|
|
1691
|
+
// drag or double-click into another block during this request has
|
|
1692
|
+
// already put it there. The focusin re-resolve and the flight replay
|
|
1693
|
+
// restore it, anchor and focus both.
|
|
1694
|
+
lastRenderSel = selectionSnapshotIn(activeFlightSurface());
|
|
1688
1695
|
return applyRenderResult(j, editRange, scrollY, declaredRange);
|
|
1689
1696
|
}
|
|
1690
1697
|
|
|
@@ -2664,14 +2671,457 @@
|
|
|
2664
2671
|
// refusal, and commitListStructure()'s no-range exit) pass nothing, which
|
|
2665
2672
|
// falls back to a full re-render by contract — patch can only compare
|
|
2666
2673
|
// renderer output, never DOM the user has already touched.
|
|
2674
|
+
//
|
|
2675
|
+
// v3.13.1 T1: every render is also a "commit in flight" for the input guard
|
|
2676
|
+
// below — see `commitFlight`. A render that lands re-points every held
|
|
2677
|
+
// entry's tag at the surface that now holds the same source.
|
|
2667
2678
|
async function safeRerenderAll(editRange) {
|
|
2679
|
+
const blocksBefore = blocks;
|
|
2680
|
+
const lineMap = pendingFlightLineMap;
|
|
2681
|
+
pendingFlightLineMap = null;
|
|
2682
|
+
beginCommitFlight();
|
|
2683
|
+
let ok = false;
|
|
2668
2684
|
try {
|
|
2669
|
-
|
|
2685
|
+
ok = await rerenderAll(editRange);
|
|
2686
|
+
return ok;
|
|
2670
2687
|
} catch (e) {
|
|
2671
2688
|
showError('這次修改沒有套用:發生未預期的錯誤(' + describeFailure(e) +
|
|
2672
2689
|
')。文件維持修改前的樣子。', null, null);
|
|
2673
2690
|
return false;
|
|
2691
|
+
} finally {
|
|
2692
|
+
if (ok) reanchorFlightBuffer(blocksBefore, editRange, lineMap);
|
|
2693
|
+
// A failed render: nothing places focus afterwards (commitListStructure()
|
|
2694
|
+
// skips its focusBlockAtLine(), and a failed blur-commit refocuses the
|
|
2695
|
+
// block it could not commit), so the opening run replays where it was
|
|
2696
|
+
// typed. Measured before round 3: edit A, click B@5, `ZZ`, render 500 —
|
|
2697
|
+
// `ZZ` went to the start of A.
|
|
2698
|
+
else pinFirstFlightRun(null);
|
|
2699
|
+
endCommitFlight();
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// ── v3.13.1 T1: the commit-in-flight input guard ──────────────────────
|
|
2704
|
+
// A commit awaits /api/render and then rebuilds the surfaces it covers from
|
|
2705
|
+
// `lines`. Anything typed into a surface while that request is out lands in
|
|
2706
|
+
// DOM the render is about to throw away. Measured before this (audit
|
|
2707
|
+
// 2026-10-10): Enter then an immediate `type('new')` in a tight list lost
|
|
2708
|
+
// every character in Chromium and wrote an extra empty `-` item; with the
|
|
2709
|
+
// render held 800 ms, 100% loss in both engines.
|
|
2710
|
+
//
|
|
2711
|
+
// While a flight is active (flightActive(): a commit in flight, or held
|
|
2712
|
+
// entries still waiting to replay), what is typed into an armed surface is held in
|
|
2713
|
+
// `flightBuffer` instead of reaching the DOM — text, paste, and the keys
|
|
2714
|
+
// that edit or move: Enter, Tab, Backspace/Delete (and their word/line
|
|
2715
|
+
// chords), arrows, Home/End. Escape drops everything held (R12: Escape
|
|
2716
|
+
// means "throw away what I have not committed", in flight or not). Once
|
|
2717
|
+
// the render has landed and its caller has placed focus, the entries are
|
|
2718
|
+
// replayed in order, each through the path the live input would have taken
|
|
2719
|
+
// (ruling R13): text through the browser's own insertText (which replaces a
|
|
2720
|
+
// selection and rebalances whitespace; insertTextAtCaret() is the fallback),
|
|
2721
|
+
// paste through routePaste(), keys as a keydown through the
|
|
2722
|
+
// document handler — so ↑/↓ can still cross blocks and a table cell still
|
|
2723
|
+
// owns its keys — and, when no handler claims the key, its native default
|
|
2724
|
+
// (execCommand deletion, Selection.modify movement).
|
|
2725
|
+
//
|
|
2726
|
+
// WHERE an entry replays (fix rounds 1-3):
|
|
2727
|
+
// * A "continuation" entry (pos === null) replays at the selection the
|
|
2728
|
+
// replay has reached — wherever focus is once the caller placed it, or
|
|
2729
|
+
// wherever an earlier replayed key moved it (a held ArrowDown that walks
|
|
2730
|
+
// to the next block takes the keys typed after it there). A held key
|
|
2731
|
+
// never moves the DOM selection; a pointer press does, so the press
|
|
2732
|
+
// first PINS the run of continuation entries that opened the flight to
|
|
2733
|
+
// the selection it is about to move away from (pinFirstFlightRun(); a
|
|
2734
|
+
// failed render pins it too, since then no caller places focus). Every
|
|
2735
|
+
// other run already starts at a positioned entry. Measured before round
|
|
2736
|
+
// 3: click B@5, `xy`, click Omega, `Z` replayed `xy` at B's start.
|
|
2737
|
+
// * A "positioned" entry is the first one held after a pointer press into
|
|
2738
|
+
// a surface during the flight: it carries a tag (surface, block id,
|
|
2739
|
+
// table cell index) and the full selection (anchor and focus offsets),
|
|
2740
|
+
// restored before it replays. Offsets are shifted past the held text
|
|
2741
|
+
// that will be inserted before them in the same surface by the time it
|
|
2742
|
+
// replays (heldShiftBefore()). The tag is re-pointed when a render
|
|
2743
|
+
// replaces its surface: by the committed run's own line map for an item
|
|
2744
|
+
// of a list run the commit rewrote (an item the split pushed down a
|
|
2745
|
+
// line, the provisional item), otherwise by its block's start line,
|
|
2746
|
+
// shifted by the commit's line delta when it lay below the edited range.
|
|
2747
|
+
// Measured before round 2: Enter, `ab`, click `charlie`, `Z` saved
|
|
2748
|
+
// `- abZ` with charlie untouched.
|
|
2749
|
+
// * A pointer press during the flight with nothing typed after it becomes
|
|
2750
|
+
// a { kind: 'focus' } entry, so the click is not lost to the commit's
|
|
2751
|
+
// own focusBlockAtLine().
|
|
2752
|
+
//
|
|
2753
|
+
// A failed render keeps the buffer too: rerenderAll() leaves `.content`
|
|
2754
|
+
// untouched on failure, every surface is still connected, and the replay
|
|
2755
|
+
// lands where it was typed.
|
|
2756
|
+
let commitFlight = 0;
|
|
2757
|
+
const flightBuffer = [];
|
|
2758
|
+
// A pointer press into a surface during a flight: the next held entry is
|
|
2759
|
+
// positioned (see above).
|
|
2760
|
+
let flightSelMoved = false;
|
|
2761
|
+
// Set while an entry replays: the replay's own keydown / insertText must
|
|
2762
|
+
// not be held again.
|
|
2763
|
+
let flightReplaying = false;
|
|
2764
|
+
function flightActive() {
|
|
2765
|
+
return !flightReplaying && (commitFlight > 0 || flightBuffer.length > 0);
|
|
2766
|
+
}
|
|
2767
|
+
// The selection right before the latest render replaced the DOM (R11).
|
|
2768
|
+
let lastRenderSel = null;
|
|
2769
|
+
// Where continuation entries go when the render took focus away and no
|
|
2770
|
+
// caller put it anywhere (e.g. a Ctrl+S commit of the focused block).
|
|
2771
|
+
let flightFallback = null;
|
|
2772
|
+
// commitListStructure() -> safeRerenderAll(): the run's line map.
|
|
2773
|
+
let pendingFlightLineMap = null;
|
|
2774
|
+
const FLIGHT_DROPPED_MESSAGE = '重新整理期間打的字沒有地方放,已略過';
|
|
2775
|
+
|
|
2776
|
+
function beginCommitFlight() {
|
|
2777
|
+
commitFlight++;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
// Not a synchronous decrement. The caller's continuation after
|
|
2781
|
+
// `await safeRerenderAll(...)` — focusBlockAtLine(), an arrow walk's
|
|
2782
|
+
// landing — is a microtask, so it runs before this timer and focus is
|
|
2783
|
+
// placed before the replay. Keys that arrive between the render landing
|
|
2784
|
+
// and the timer are still held, behind the earlier ones.
|
|
2785
|
+
function endCommitFlight() {
|
|
2786
|
+
setTimeout(() => {
|
|
2787
|
+
commitFlight--;
|
|
2788
|
+
if (!commitFlight) flushFlightBuffer();
|
|
2789
|
+
}, 0);
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// A replay that had to wait for a focus change resumes from here (the
|
|
2793
|
+
// focusin handlers call it once their burst is open).
|
|
2794
|
+
function scheduleFlightFlush() {
|
|
2795
|
+
if (flightBuffer.length && !commitFlight) setTimeout(flushFlightBuffer, 0);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
function flightSurfaceOf(node) {
|
|
2799
|
+
const el = node && (node.nodeType === 1 ? node : node.parentElement);
|
|
2800
|
+
return el && el.closest ? el.closest('.ed-wys-armed, .ed-wys-cell') : null;
|
|
2801
|
+
}
|
|
2802
|
+
function activeFlightSurface() {
|
|
2803
|
+
return flightSurfaceOf(document.activeElement);
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// Selection points as character offsets inside a surface, counted over
|
|
2807
|
+
// text nodes only — the same count pointInSurface() walks back.
|
|
2808
|
+
function offsetInSurface(surface, node, offset) {
|
|
2809
|
+
const r = document.createRange();
|
|
2810
|
+
r.selectNodeContents(surface);
|
|
2811
|
+
r.setEnd(node, offset);
|
|
2812
|
+
return r.toString().length;
|
|
2813
|
+
}
|
|
2814
|
+
function pointInSurface(surface, offset) {
|
|
2815
|
+
let left = offset;
|
|
2816
|
+
let lastText = null;
|
|
2817
|
+
const walker = document.createTreeWalker(surface, NodeFilter.SHOW_TEXT);
|
|
2818
|
+
for (let t = walker.nextNode(); t; t = walker.nextNode()) {
|
|
2819
|
+
if (left <= t.data.length) return [t, left];
|
|
2820
|
+
left -= t.data.length;
|
|
2821
|
+
lastText = t;
|
|
2822
|
+
}
|
|
2823
|
+
return lastText ? [lastText, lastText.data.length] : [surface, 0];
|
|
2824
|
+
}
|
|
2825
|
+
// { surface, anchor, focus } for the selection in `surface`; offsets are
|
|
2826
|
+
// null when the selection is not inside it.
|
|
2827
|
+
function selectionSnapshotIn(surface) {
|
|
2828
|
+
if (!surface) return null;
|
|
2829
|
+
const sel = window.getSelection();
|
|
2830
|
+
if (!sel.rangeCount || !surface.contains(sel.anchorNode) || !surface.contains(sel.focusNode)) {
|
|
2831
|
+
return { surface, anchor: null, focus: null };
|
|
2832
|
+
}
|
|
2833
|
+
return { surface,
|
|
2834
|
+
anchor: offsetInSurface(surface, sel.anchorNode, sel.anchorOffset),
|
|
2835
|
+
focus: offsetInSurface(surface, sel.focusNode, sel.focusOffset) };
|
|
2836
|
+
}
|
|
2837
|
+
function restoreSelection(surface, anchor, focus) {
|
|
2838
|
+
if (anchor === null || anchor === undefined) return;
|
|
2839
|
+
const a = pointInSurface(surface, anchor);
|
|
2840
|
+
const f = pointInSurface(surface, focus === null || focus === undefined ? anchor : focus);
|
|
2841
|
+
window.getSelection().setBaseAndExtent(a[0], a[1], f[0], f[1]);
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function flightTagOf(snap) {
|
|
2845
|
+
if (!snap || !snap.surface) return null;
|
|
2846
|
+
const blockEl = snap.surface.closest('.ed-block');
|
|
2847
|
+
const isCell = snap.surface.classList.contains('ed-wys-cell');
|
|
2848
|
+
return {
|
|
2849
|
+
surface: snap.surface,
|
|
2850
|
+
blockId: blockEl ? blockEl.getAttribute('data-block-id') : null,
|
|
2851
|
+
cellIndex: isCell && blockEl
|
|
2852
|
+
? Array.prototype.indexOf.call(blockEl.querySelectorAll('.ed-wys-cell'), snap.surface) : null,
|
|
2853
|
+
anchor: snap.anchor, focus: snap.focus,
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
// The committed run's lines after the commit: the serializer's own
|
|
2858
|
+
// line -> block map (`lineMeta`, one entry per emitted line), keyed by the
|
|
2859
|
+
// block ids the run had before it. The provisional item has no id.
|
|
2860
|
+
function flightLineMapOf(lineMeta, startLine) {
|
|
2861
|
+
const map = { ids: {}, provisional: null };
|
|
2862
|
+
(lineMeta || []).forEach((m, k) => {
|
|
2863
|
+
if (m.blank) return;
|
|
2864
|
+
if (m.blockId === null || m.blockId === undefined) {
|
|
2865
|
+
if (map.provisional === null) map.provisional = startLine + k;
|
|
2866
|
+
} else if (!(m.blockId in map.ids)) {
|
|
2867
|
+
map.ids[m.blockId] = startLine + k;
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2870
|
+
return map;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// How far held text will have pushed `offset` in `surface` by the time an
|
|
2874
|
+
// entry positioned there replays: the text of every run pinned (or, for
|
|
2875
|
+
// the opening run, typed) in the same surface at or before that offset.
|
|
2876
|
+
function heldShiftBefore(surface, offset) {
|
|
2877
|
+
let pin = null;
|
|
2878
|
+
let shift = 0;
|
|
2879
|
+
flightBuffer.forEach((en, i) => {
|
|
2880
|
+
if (en.pos || i === 0) pin = en.pos || en.hint;
|
|
2881
|
+
if (en.kind !== 'text' || !pin || pin.surface !== surface || pin.anchor === null) return;
|
|
2882
|
+
if (Math.min(pin.anchor, pin.focus === null ? pin.anchor : pin.focus) <= offset) shift += en.data.length;
|
|
2883
|
+
});
|
|
2884
|
+
return shift;
|
|
2885
|
+
}
|
|
2886
|
+
function shiftedTag(tag) {
|
|
2887
|
+
if (!tag || tag.anchor === null) return tag;
|
|
2888
|
+
tag.anchor += heldShiftBefore(tag.surface, tag.anchor);
|
|
2889
|
+
if (tag.focus !== null) tag.focus += heldShiftBefore(tag.surface, tag.focus);
|
|
2890
|
+
return tag;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// Pins the run of continuation entries that opened this flight (if it is
|
|
2894
|
+
// still unpinned) to `snap`, or to its own first entry's hint — where it
|
|
2895
|
+
// was typed — when there is no usable selection.
|
|
2896
|
+
function pinFirstFlightRun(snap) {
|
|
2897
|
+
const first = flightBuffer[0];
|
|
2898
|
+
if (!first || first.pos) return;
|
|
2899
|
+
const tag = snap && snap.anchor !== null ? flightTagOf(snap) : null;
|
|
2900
|
+
first.pos = tag || (first.hint ? Object.assign({}, first.hint) : null);
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
function holdFlightEntry(surface, entry) {
|
|
2904
|
+
entry.pos = null;
|
|
2905
|
+
entry.hint = flightTagOf(selectionSnapshotIn(surface));
|
|
2906
|
+
if (flightSelMoved) {
|
|
2907
|
+
flightSelMoved = false;
|
|
2908
|
+
entry.pos = shiftedTag(flightTagOf(selectionSnapshotIn(surface)));
|
|
2909
|
+
}
|
|
2910
|
+
const last = flightBuffer[flightBuffer.length - 1];
|
|
2911
|
+
if (entry.kind === 'text' && !entry.pos && last && last.kind === 'text') {
|
|
2912
|
+
last.data += entry.data;
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
flightBuffer.push(entry);
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
// After a render lands: re-point every tag whose surface it replaced.
|
|
2919
|
+
function reanchorFlightBuffer(before, editRange, lineMap) {
|
|
2920
|
+
if (flightSelMoved && lastRenderSel) {
|
|
2921
|
+
flightSelMoved = false;
|
|
2922
|
+
flightBuffer.push({ kind: 'focus', pos: shiftedTag(flightTagOf(lastRenderSel)) });
|
|
2923
|
+
}
|
|
2924
|
+
flightFallback = lastRenderSel && !lastRenderSel.surface.isConnected
|
|
2925
|
+
? flightTagOf(lastRenderSel) : null;
|
|
2926
|
+
const remap = (pos) => {
|
|
2927
|
+
if (!pos) return;
|
|
2928
|
+
if (pos.surface && pos.surface.isConnected) {
|
|
2929
|
+
const own = pos.surface.closest('.ed-block');
|
|
2930
|
+
pos.blockId = own ? own.getAttribute('data-block-id') : pos.blockId;
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
let line = null;
|
|
2934
|
+
if (lineMap) {
|
|
2935
|
+
if (pos.blockId === null) line = lineMap.provisional;
|
|
2936
|
+
else if (pos.blockId in lineMap.ids) line = lineMap.ids[pos.blockId];
|
|
2937
|
+
}
|
|
2938
|
+
if (line === null && pos.blockId !== null) {
|
|
2939
|
+
const rec = (before || []).find((b) => b.id === Number(pos.blockId));
|
|
2940
|
+
if (rec) {
|
|
2941
|
+
line = rec.startLine;
|
|
2942
|
+
if (editRange && line > editRange.endLine) line += editRange.delta;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
pos.surface = null;
|
|
2946
|
+
if (line === null) return;
|
|
2947
|
+
const nb = blocks.find((b) => b.startLine === line);
|
|
2948
|
+
const nEl = nb ? document.querySelector('.ed-block[data-block-id="' + nb.id + '"]') : null;
|
|
2949
|
+
if (!nEl) return;
|
|
2950
|
+
let surf = pos.cellIndex !== null && pos.cellIndex !== undefined
|
|
2951
|
+
? nEl.querySelectorAll('.ed-wys-cell')[pos.cellIndex] || null
|
|
2952
|
+
: blockContentEl(nEl);
|
|
2953
|
+
if (surf && !surf.matches('.ed-wys-armed, .ed-wys-cell')) surf = null;
|
|
2954
|
+
pos.surface = surf;
|
|
2955
|
+
pos.blockId = String(nb.id);
|
|
2956
|
+
};
|
|
2957
|
+
flightBuffer.forEach((en) => { remap(en.pos); remap(en.hint); });
|
|
2958
|
+
remap(flightFallback);
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// A deletion chord's input type, and a caret chord's movement, per
|
|
2962
|
+
// platform (Ctrl on Linux/Windows, Alt and Cmd on a Mac).
|
|
2963
|
+
const FLIGHT_IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform);
|
|
2964
|
+
function deletionForKey(e) {
|
|
2965
|
+
const word = FLIGHT_IS_MAC ? (e.altKey && !e.metaKey && !e.ctrlKey) : (e.ctrlKey && !e.metaKey && !e.altKey);
|
|
2966
|
+
const line = FLIGHT_IS_MAC && e.metaKey && !e.altKey && !e.ctrlKey;
|
|
2967
|
+
const plain = !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
2968
|
+
if (e.key === 'Backspace') {
|
|
2969
|
+
return plain ? 'deleteContentBackward' : word ? 'deleteWordBackward' : line ? 'deleteSoftLineBackward' : null;
|
|
2970
|
+
}
|
|
2971
|
+
if (e.key === 'Delete') {
|
|
2972
|
+
return plain ? 'deleteContentForward' : word ? 'deleteWordForward' : line ? 'deleteSoftLineForward' : null;
|
|
2973
|
+
}
|
|
2974
|
+
return null;
|
|
2975
|
+
}
|
|
2976
|
+
function caretMoveForKey(e) {
|
|
2977
|
+
const word = FLIGHT_IS_MAC ? (e.altKey && !e.metaKey && !e.ctrlKey) : (e.ctrlKey && !e.metaKey && !e.altKey);
|
|
2978
|
+
const plain = !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
2979
|
+
const extend = !!e.shiftKey;
|
|
2980
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
2981
|
+
if (!plain && !word) return null;
|
|
2982
|
+
return { dir: e.key === 'ArrowLeft' ? 'backward' : 'forward', unit: word ? 'word' : 'character', extend };
|
|
2983
|
+
}
|
|
2984
|
+
if (!plain) return null;
|
|
2985
|
+
if (e.key === 'Home' || e.key === 'End') return { dir: e.key === 'Home' ? 'backward' : 'forward', unit: 'lineboundary', extend };
|
|
2986
|
+
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') return { dir: e.key === 'ArrowUp' ? 'backward' : 'forward', unit: 'line', extend };
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
function isHeldTyping(en) {
|
|
2990
|
+
return en.kind === 'text' || en.kind === 'paste' || en.kind === 'delete' ||
|
|
2991
|
+
(en.kind === 'key' && !caretMoveForKey(en));
|
|
2992
|
+
}
|
|
2993
|
+
function dropHeldTyping() {
|
|
2994
|
+
const kept = [];
|
|
2995
|
+
let carry = null;
|
|
2996
|
+
flightBuffer.forEach((en) => {
|
|
2997
|
+
if (isHeldTyping(en)) {
|
|
2998
|
+
if (en.pos) carry = en.pos;
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
if (en.pos) carry = null;
|
|
3002
|
+
else if (carry) { en.pos = carry; carry = null; }
|
|
3003
|
+
kept.push(en);
|
|
3004
|
+
});
|
|
3005
|
+
flightBuffer.length = 0;
|
|
3006
|
+
kept.forEach((en) => flightBuffer.push(en));
|
|
3007
|
+
}
|
|
3008
|
+
function flightHoldsKey(e) {
|
|
3009
|
+
if ((e.key === 'Enter' || e.key === 'Tab') && !e.ctrlKey && !e.metaKey && !e.altKey) return true;
|
|
3010
|
+
return !!(deletionForKey(e) || caretMoveForKey(e));
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// The native default of a deletion, against the restored selection: a
|
|
3014
|
+
// selection is deleted whole; a collapsed caret deletes one character, or
|
|
3015
|
+
// is first extended by a word / to the line boundary.
|
|
3016
|
+
function nativeFlightDeletion(t, target) {
|
|
3017
|
+
const sel = window.getSelection();
|
|
3018
|
+
if (!sel.rangeCount) return;
|
|
3019
|
+
const back = /Backward$/.test(t);
|
|
3020
|
+
const unit = /Word/.test(t) ? 'word' : (/Line/.test(t) ? 'lineboundary' : null);
|
|
3021
|
+
if (sel.isCollapsed && unit) {
|
|
3022
|
+
if (!sel.modify) return;
|
|
3023
|
+
sel.modify('extend', back ? 'backward' : 'forward', unit);
|
|
3024
|
+
// Nothing to delete in that direction: a collapsed 'delete' would take
|
|
3025
|
+
// a character instead.
|
|
3026
|
+
if (sel.isCollapsed) return;
|
|
3027
|
+
}
|
|
3028
|
+
snapBurstIfActive(target, 'replay-pre');
|
|
3029
|
+
document.execCommand(sel.isCollapsed && !back ? 'forwardDelete' : 'delete');
|
|
3030
|
+
snapBurstIfActive(target, 'replay');
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
function replayFlightEntry(item, target) {
|
|
3034
|
+
if (item.kind === 'text') {
|
|
3035
|
+
// The browser's own insertText, not insertTextAtCaret(): it replaces a
|
|
3036
|
+
// selection the same way, and it also rebalances the whitespace around
|
|
3037
|
+
// the caret the way live typing does (measured: a held Ctrl+Backspace
|
|
3038
|
+
// leaves `ab` + NBSP, and only the native insertion of the next `e`
|
|
3039
|
+
// turns that back into `ab e`).
|
|
3040
|
+
snapBurstIfActive(target, 'replay-pre');
|
|
3041
|
+
if (/[\r\n]/.test(item.data) || !document.execCommand('insertText', false, item.data)) {
|
|
3042
|
+
insertTextAtCaret(item.data);
|
|
3043
|
+
}
|
|
3044
|
+
snapBurstIfActive(target, 'replay');
|
|
3045
|
+
} else if (item.kind === 'paste') {
|
|
3046
|
+
plainPasteArmed = item.plainOnly;
|
|
3047
|
+
routePaste(target, item.dt);
|
|
3048
|
+
} else if (item.kind === 'delete') {
|
|
3049
|
+
nativeFlightDeletion(item.inputType, target);
|
|
3050
|
+
} else if (item.kind === 'key') {
|
|
3051
|
+
const ev = new KeyboardEvent('keydown', { key: item.key, shiftKey: item.shiftKey,
|
|
3052
|
+
ctrlKey: item.ctrlKey, altKey: item.altKey, metaKey: item.metaKey, bubbles: true, cancelable: true });
|
|
3053
|
+
target.dispatchEvent(ev);
|
|
3054
|
+
if (ev.defaultPrevented) return;
|
|
3055
|
+
const del = deletionForKey(item);
|
|
3056
|
+
if (del) { nativeFlightDeletion(del, target); return; }
|
|
3057
|
+
const move = caretMoveForKey(item);
|
|
3058
|
+
const sel = window.getSelection();
|
|
3059
|
+
if (move && sel.modify) sel.modify(move.extend ? 'extend' : 'move', move.dir, move.unit);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
function flushFlightBuffer() {
|
|
3064
|
+
if (commitFlight || switching) return;
|
|
3065
|
+
let dropped = false;
|
|
3066
|
+
// Replay in order. A replayed key, or the focus change to a positioned
|
|
3067
|
+
// entry's surface, can start a new commit (or wait on one); the rest
|
|
3068
|
+
// waits and resumes from its endCommitFlight() or the focusin handlers.
|
|
3069
|
+
while (flightBuffer.length && !commitFlight && !switching) {
|
|
3070
|
+
const item = flightBuffer[0];
|
|
3071
|
+
let target = null;
|
|
3072
|
+
if (item.pos) {
|
|
3073
|
+
target = item.pos.surface && item.pos.surface.isConnected ? item.pos.surface : null;
|
|
3074
|
+
if (!target) { flightBuffer.shift(); dropped = true; continue; }
|
|
3075
|
+
if (activeFlightSurface() !== target) {
|
|
3076
|
+
target.focus();
|
|
3077
|
+
if (commitFlight || switching) break;
|
|
3078
|
+
}
|
|
3079
|
+
restoreSelection(target, item.pos.anchor, item.pos.focus);
|
|
3080
|
+
} else {
|
|
3081
|
+
target = activeFlightSurface();
|
|
3082
|
+
const fb = flightFallback;
|
|
3083
|
+
if (!target && fb && fb.surface && fb.surface.isConnected) {
|
|
3084
|
+
fb.surface.focus();
|
|
3085
|
+
if (commitFlight || switching) break;
|
|
3086
|
+
restoreSelection(fb.surface, fb.anchor, fb.focus);
|
|
3087
|
+
flightFallback = null;
|
|
3088
|
+
target = activeFlightSurface();
|
|
3089
|
+
}
|
|
3090
|
+
if (!target) { flightBuffer.shift(); dropped = true; continue; }
|
|
3091
|
+
}
|
|
3092
|
+
flightBuffer.shift();
|
|
3093
|
+
if (item.kind !== 'focus') {
|
|
3094
|
+
flightReplaying = true;
|
|
3095
|
+
try { replayFlightEntry(item, target); } finally { flightReplaying = false; }
|
|
3096
|
+
}
|
|
2674
3097
|
}
|
|
3098
|
+
if (!flightBuffer.length && !commitFlight) flightFallback = null;
|
|
3099
|
+
if (dropped) showNotice(FLIGHT_DROPPED_MESSAGE);
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// An IME composition cannot be cancelled (its beforeinput is not
|
|
3103
|
+
// cancelable), so the committed text is already in the DOM when
|
|
3104
|
+
// compositionend reports it. It is taken back out when it sits right
|
|
3105
|
+
// before the caret, which leaves the buffer its only owner — a render that
|
|
3106
|
+
// keeps this surface (a failed one, or a patch that does not cover it)
|
|
3107
|
+
// would otherwise show it twice once the replay lands. Returns false when
|
|
3108
|
+
// it cannot be found there; the caller holds it anyway, and the render
|
|
3109
|
+
// that replaces the surface is what removes the DOM copy.
|
|
3110
|
+
function takeBackComposedText(data) {
|
|
3111
|
+
const sel = window.getSelection();
|
|
3112
|
+
if (!data || !sel.rangeCount) return false;
|
|
3113
|
+
const r = sel.getRangeAt(0);
|
|
3114
|
+
if (!r.collapsed || r.startContainer.nodeType !== 3) return false;
|
|
3115
|
+
const node = r.startContainer;
|
|
3116
|
+
const start = r.startOffset - data.length;
|
|
3117
|
+
if (start < 0 || node.data.slice(start, r.startOffset) !== data) return false;
|
|
3118
|
+
node.deleteData(start, data.length);
|
|
3119
|
+
const c = document.createRange();
|
|
3120
|
+
c.setStart(node, start);
|
|
3121
|
+
c.collapse(true);
|
|
3122
|
+
sel.removeAllRanges();
|
|
3123
|
+
sel.addRange(c);
|
|
3124
|
+
return true;
|
|
2675
3125
|
}
|
|
2676
3126
|
|
|
2677
3127
|
function autoSize(ta) {
|
|
@@ -7579,11 +8029,19 @@
|
|
|
7579
8029
|
commitMd = runMd;
|
|
7580
8030
|
commitStart = range.startLine;
|
|
7581
8031
|
commitEnd = range.endLine;
|
|
8032
|
+
// v3.13.1 T1 fix round 3: the whole run is rewritten — including any
|
|
8033
|
+
// provisional item still in it, which then gets a line of its own and
|
|
8034
|
+
// pushes the items after it down one. Keys held for those items find
|
|
8035
|
+
// them by this map (see flightLineMapOf()). Measured without it: a
|
|
8036
|
+
// failed Enter render, then `ab` replayed into the new item, then the
|
|
8037
|
+
// blur commit of this run — `Z` held for charlie landed in `ab`.
|
|
8038
|
+
pendingFlightLineMap = flightLineMapOf(lineMeta, range.startLine);
|
|
7582
8039
|
}
|
|
7583
8040
|
const liCommitResult = (commitMd === '')
|
|
7584
8041
|
? commitRangeRemoval({ lines, blocks, stack }, commitStart, commitEnd)
|
|
7585
8042
|
: commitRangeEdit({ lines, blocks, stack }, commitStart, commitEnd, commitMd);
|
|
7586
8043
|
if (liCommitResult.op === null) {
|
|
8044
|
+
pendingFlightLineMap = null;
|
|
7587
8045
|
endBurstWithoutResolve();
|
|
7588
8046
|
return true;
|
|
7589
8047
|
}
|
|
@@ -7637,7 +8095,13 @@
|
|
|
7637
8095
|
// commit '#'.repeat(depth) + ' ' with nothing after the space. Every
|
|
7638
8096
|
// non-heading burst's `depth` is null (blockDepthOf() only computes it for
|
|
7639
8097
|
// 'heading'), so it takes the plain result.md branch.
|
|
7640
|
-
|
|
8098
|
+
// v3.13.1 T2 (F-1): a paragraph's text is escaped where it would read as
|
|
8099
|
+
// a block marker (`## `, `- `, `> `, `1. ` …), or the next render turns
|
|
8100
|
+
// the paragraph into that block. A heading's text sits after its own
|
|
8101
|
+
// `# ` and needs none; a table's rows START with `|`, so its markdown
|
|
8102
|
+
// must never reach the escape — hence the blockType test, not just depth.
|
|
8103
|
+
const newText = burst.depth === null
|
|
8104
|
+
? (burst.blockType === 'paragraph' ? inlineMd.escapeBlockStarts(result.md) : result.md) :
|
|
7641
8105
|
(result.md === '' ? '#'.repeat(burst.depth) : '#'.repeat(burst.depth) + ' ' + result.md);
|
|
7642
8106
|
// S1 removed the pre-per-li "a whole-list burst that serialized to ''"
|
|
7643
8107
|
// branch that lived here alongside the dead 'list' serializer arm above.
|
|
@@ -8354,7 +8818,7 @@
|
|
|
8354
8818
|
async function commitListStructure(runEls, focusStartLine, caretToEnd, opts) {
|
|
8355
8819
|
const span = runEls || [];
|
|
8356
8820
|
const presetRange = opts && opts.presetRange;
|
|
8357
|
-
const { md } = serializeListEls(span, { carryOver: (opts && opts.carryOver) || null });
|
|
8821
|
+
const { md, lineMeta } = serializeListEls(span, { carryOver: (opts && opts.carryOver) || null });
|
|
8358
8822
|
// The run's line range is read back off its own li blocks' ids — which
|
|
8359
8823
|
// requires at least one li to still BE there. A caller whose mutation
|
|
8360
8824
|
// removed the run's last item therefore captures the range BEFORE mutating
|
|
@@ -8383,6 +8847,10 @@
|
|
|
8383
8847
|
endLine: Math.max(result.op.startLine - 1, result.op.endLine),
|
|
8384
8848
|
delta: result.op.after.length - result.op.before.length }
|
|
8385
8849
|
: undefined;
|
|
8850
|
+
// v3.13.1 T1 fix round 2: where each item of the run will start once this
|
|
8851
|
+
// commit lands, so a key held for an item of this very run — one the
|
|
8852
|
+
// split pushed down a line, or the provisional item itself — finds it.
|
|
8853
|
+
if (result.op !== null) pendingFlightLineMap = flightLineMapOf(lineMeta, range.startLine);
|
|
8386
8854
|
const ok = await safeRerenderAll(editRange);
|
|
8387
8855
|
if (!ok) {
|
|
8388
8856
|
lines = rollbackFailedRender({ lines, stack }, result, prevLines);
|
|
@@ -9131,6 +9599,28 @@
|
|
|
9131
9599
|
//
|
|
9132
9600
|
// Returns the new block element, or null when the caret is not inside
|
|
9133
9601
|
// `blockEl`'s own surface (nothing mutated).
|
|
9602
|
+
// v3.13.1 T1: the item splitListItemAtCaret() just built is a plain div
|
|
9603
|
+
// until the commit's render replaces it — not editable, so the caret stayed
|
|
9604
|
+
// in the OLD item and anything typed before the render landed went there.
|
|
9605
|
+
// Arm it the way armEditables() arms an li (the same two writes), focus it
|
|
9606
|
+
// and put the caret at its start, where the render's own
|
|
9607
|
+
// focusBlockAtLine() will put it again.
|
|
9608
|
+
// The focusin this raises starts no burst: Enter's burst is still current
|
|
9609
|
+
// here, so that handler's `if (currentBurst) return;` returns.
|
|
9610
|
+
function armProvisionalLi(newLi) {
|
|
9611
|
+
const textEl = liTextEl(newLi);
|
|
9612
|
+
if (!textEl) return;
|
|
9613
|
+
textEl.setAttribute('contenteditable', 'true');
|
|
9614
|
+
textEl.classList.add('ed-wys-armed');
|
|
9615
|
+
textEl.focus();
|
|
9616
|
+
const range = document.createRange();
|
|
9617
|
+
range.selectNodeContents(textEl);
|
|
9618
|
+
range.collapse(true);
|
|
9619
|
+
const sel = window.getSelection();
|
|
9620
|
+
sel.removeAllRanges();
|
|
9621
|
+
sel.addRange(range);
|
|
9622
|
+
}
|
|
9623
|
+
|
|
9134
9624
|
function splitListItemAtCaret(blockEl) {
|
|
9135
9625
|
const sel = window.getSelection();
|
|
9136
9626
|
if (!sel.rangeCount) return null;
|
|
@@ -10018,7 +10508,14 @@
|
|
|
10018
10508
|
// splitListItemAtCaret() keeps `li` itself in place (only `newLi` is a
|
|
10019
10509
|
// freshly-inserted sibling) and never touches the focused node's
|
|
10020
10510
|
// attachment.
|
|
10021
|
-
|
|
10511
|
+
// v3.13.1 T1: the new item is armed and focused inside the same span, so
|
|
10512
|
+
// the old item's focusout stays suppressed and the next keystroke is
|
|
10513
|
+
// already aimed at the new item while the commit below is in flight.
|
|
10514
|
+
const newLi = mutateListRun(() => {
|
|
10515
|
+
const n = splitListItemAtCaret(li);
|
|
10516
|
+
if (n) armProvisionalLi(n);
|
|
10517
|
+
return n;
|
|
10518
|
+
});
|
|
10022
10519
|
if (!newLi) return true;
|
|
10023
10520
|
run = listRunOf(newLi);
|
|
10024
10521
|
// `li` had its text CUT IN TWO in the DOM; replaying its source would put
|
|
@@ -17550,6 +18047,38 @@
|
|
|
17550
18047
|
e.target.classList.contains('ed-raw');
|
|
17551
18048
|
if (inTextarea) return; // Ctrl+Enter/Esc handled by openRawEditor()'s own per-instance listener
|
|
17552
18049
|
|
|
18050
|
+
// v3.13.1 T1: a key that edits or moves, pressed while a commit is in
|
|
18051
|
+
// flight, would act on DOM the render is about to replace. Held, in order
|
|
18052
|
+
// with the text around it, and replayed by flushFlightBuffer() through
|
|
18053
|
+
// this same handler (see `commitFlight`). An IME's own keys (isComposing)
|
|
18054
|
+
// are left alone. Escape drops everything held (R12) and then carries on
|
|
18055
|
+
// to its usual meaning. A standing table cell range keeps its own keys —
|
|
18056
|
+
// its Delete clears the range, which a held character delete would not.
|
|
18057
|
+
if (flightActive() && !e.isComposing && !cellRangeBurst()) {
|
|
18058
|
+
const flightSurface = flightSurfaceOf(e.target);
|
|
18059
|
+
if (flightSurface && e.key === 'Escape') { flightBuffer.length = 0; flightSelMoved = false; }
|
|
18060
|
+
// Ctrl/Cmd+Z with keys held: the most recent thing to undo is that
|
|
18061
|
+
// typing, which never reached the document — dropping it IS the undo,
|
|
18062
|
+
// and the commit in flight stays (the next Ctrl+Z, with nothing held,
|
|
18063
|
+
// undoes it as usual). Measured before: the undo reverted the Enter
|
|
18064
|
+
// underneath the held keys, and they replayed into the next item
|
|
18065
|
+
// (`abccharlie`). Held navigation (and a click's focus entry) is not
|
|
18066
|
+
// typing and stays; a dropped entry's position passes to the next kept
|
|
18067
|
+
// entry of its run.
|
|
18068
|
+
if (flightSurface && flightBuffer.some(isHeldTyping) && (e.ctrlKey || e.metaKey) && !e.shiftKey &&
|
|
18069
|
+
!e.altKey && (e.key === 'z' || e.key === 'Z')) {
|
|
18070
|
+
e.preventDefault();
|
|
18071
|
+
dropHeldTyping();
|
|
18072
|
+
return;
|
|
18073
|
+
}
|
|
18074
|
+
if (flightSurface && flightHoldsKey(e)) {
|
|
18075
|
+
e.preventDefault();
|
|
18076
|
+
holdFlightEntry(flightSurface, { kind: 'key', key: e.key, shiftKey: e.shiftKey,
|
|
18077
|
+
ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey });
|
|
18078
|
+
return;
|
|
18079
|
+
}
|
|
18080
|
+
}
|
|
18081
|
+
|
|
17553
18082
|
// Task 5: a table cell (class 'ed-wys-cell') owns its own Enter/Tab/
|
|
17554
18083
|
// Escape/Ctrl+Z/Ctrl+Y contract, materially different from paragraph/
|
|
17555
18084
|
// heading/list (Enter is an UNCONDITIONAL <br>, Tab moves the active
|
|
@@ -17790,7 +18319,7 @@
|
|
|
17790
18319
|
// top of each listener below — see handleTableCellFocusIn() above.
|
|
17791
18320
|
document.addEventListener('focusin', async (e) => {
|
|
17792
18321
|
const cellEl = e.target && e.target.closest && e.target.closest('.ed-wys-cell');
|
|
17793
|
-
if (cellEl) { await handleTableCellFocusIn(cellEl); return; }
|
|
18322
|
+
if (cellEl) { await handleTableCellFocusIn(cellEl); scheduleFlightFlush(); return; }
|
|
17794
18323
|
const editEl = e.target && e.target.closest && e.target.closest('.ed-wys-armed');
|
|
17795
18324
|
if (!editEl) return;
|
|
17796
18325
|
if (currentBurst && currentBurst.editEl === editEl) return; // already tracking
|
|
@@ -17801,7 +18330,17 @@
|
|
|
17801
18330
|
// switchAwayFrom()'s single-flight `switching`. Await it before
|
|
17802
18331
|
// starting a new burst so the two never race a concurrent commit.
|
|
17803
18332
|
if (switching) await switching;
|
|
17804
|
-
|
|
18333
|
+
// v3.13.1 T1 fix round 2: focus may have moved on while this waited — an
|
|
18334
|
+
// arrow walk lands on the NEXT block from inside that same resolution, and
|
|
18335
|
+
// its own focusin is queued behind this one. Starting a burst here would
|
|
18336
|
+
// open it on a surface that no longer has focus and leave the one that
|
|
18337
|
+
// does without one: measured on base and head, Enter, `ab`, ArrowDown, `Z`
|
|
18338
|
+
// put `Z` in charlie's DOM, and the blur that followed committed nothing.
|
|
18339
|
+
if (document.body.contains(editEl) && !editEl.contains(document.activeElement)) {
|
|
18340
|
+
scheduleFlightFlush();
|
|
18341
|
+
return;
|
|
18342
|
+
}
|
|
18343
|
+
if (currentBurst) { scheduleFlightFlush(); return; } // a concurrent focusin already won the race
|
|
17805
18344
|
let liveEditEl = editEl;
|
|
17806
18345
|
if (!document.body.contains(editEl)) {
|
|
17807
18346
|
// The awaited resolution above committed successfully and swapped the
|
|
@@ -17813,9 +18352,19 @@
|
|
|
17813
18352
|
liveEditEl = liveBlockEl ? blockContentEl(liveBlockEl) : null;
|
|
17814
18353
|
if (!liveEditEl || !liveEditEl.classList.contains('ed-wys-armed')) return;
|
|
17815
18354
|
liveEditEl.focus();
|
|
18355
|
+
// v3.13.1 R11: focus() alone puts the caret at the surface's start, not
|
|
18356
|
+
// where the click that brought focus here put it. Measured on base and
|
|
18357
|
+
// head, both engines: edit A, click mid-B, type — the characters landed
|
|
18358
|
+
// at B's start; a drag or double-click selection collapsed. The
|
|
18359
|
+
// selection was read just before the render replaced B (lastRenderSel).
|
|
18360
|
+
if (lastRenderSel && lastRenderSel.surface === editEl) {
|
|
18361
|
+
restoreSelection(liveEditEl, lastRenderSel.anchor, lastRenderSel.focus);
|
|
18362
|
+
}
|
|
18363
|
+
scheduleFlightFlush();
|
|
17816
18364
|
return;
|
|
17817
18365
|
}
|
|
17818
18366
|
startBurst(liveEditEl);
|
|
18367
|
+
scheduleFlightFlush();
|
|
17819
18368
|
});
|
|
17820
18369
|
|
|
17821
18370
|
document.addEventListener('focusout', (e) => {
|
|
@@ -17956,6 +18505,79 @@
|
|
|
17956
18505
|
return currentBurst.history;
|
|
17957
18506
|
}
|
|
17958
18507
|
|
|
18508
|
+
// v3.13.1 T1: a pointer press into a surface during a flight moves the
|
|
18509
|
+
// selection the user means the next held key for (see `flightSelMoved`).
|
|
18510
|
+
// A press anywhere else — the toolbar, the margin — is not one.
|
|
18511
|
+
// Capture phase: this runs before the press moves the selection, which is
|
|
18512
|
+
// what pins the opening run to where it was typed (see pinFirstFlightRun()).
|
|
18513
|
+
document.addEventListener('pointerdown', (e) => {
|
|
18514
|
+
if (!flightActive()) return;
|
|
18515
|
+
pinFirstFlightRun(selectionSnapshotIn(activeFlightSurface()));
|
|
18516
|
+
if (flightSurfaceOf(e.target)) flightSelMoved = true;
|
|
18517
|
+
}, true);
|
|
18518
|
+
|
|
18519
|
+
// v3.13.1 T1: the input half of the commit-in-flight guard (see
|
|
18520
|
+
// `commitFlight`). Registered ahead of the native-format listener below,
|
|
18521
|
+
// both in the capture phase, so a held keystroke never reaches it.
|
|
18522
|
+
//
|
|
18523
|
+
// `isComposing` insertText is skipped: an engine that reports a
|
|
18524
|
+
// composition's commit that way also reports it to compositionend below,
|
|
18525
|
+
// and the text must be held once.
|
|
18526
|
+
function flightSurfaceOfEvent(e) {
|
|
18527
|
+
return flightActive() ? flightSurfaceOf(e.target) : null;
|
|
18528
|
+
}
|
|
18529
|
+
// The text compositionend just held. An engine that inserts a
|
|
18530
|
+
// composition's commit AFTER compositionend (WebKit's
|
|
18531
|
+
// insertFromComposition) would put it in the DOM a second time; that
|
|
18532
|
+
// insertion is cancelled, or taken back out on its input event when it
|
|
18533
|
+
// cannot be cancelled. Cleared on the next task: the follow-up arrives in
|
|
18534
|
+
// the same one or not at all.
|
|
18535
|
+
let composedHeld = null;
|
|
18536
|
+
let composedTakeBack = null;
|
|
18537
|
+
document.addEventListener('beforeinput', (e) => {
|
|
18538
|
+
if (composedHeld !== null && e.data === composedHeld &&
|
|
18539
|
+
(e.inputType === 'insertFromComposition' || e.inputType === 'insertText')) {
|
|
18540
|
+
composedHeld = null;
|
|
18541
|
+
if (e.cancelable) e.preventDefault(); else composedTakeBack = e.data;
|
|
18542
|
+
return;
|
|
18543
|
+
}
|
|
18544
|
+
const surface = flightSurfaceOfEvent(e);
|
|
18545
|
+
if (!surface) return;
|
|
18546
|
+
const t = e.inputType;
|
|
18547
|
+
if ((t === 'insertText' || t === 'insertReplacementText') && !e.isComposing) {
|
|
18548
|
+
// insertReplacementText (autocorrect, spellcheck) carries its text in
|
|
18549
|
+
// dataTransfer in some engines and in `data` in others.
|
|
18550
|
+
const data = e.data != null ? e.data
|
|
18551
|
+
: (e.dataTransfer && e.dataTransfer.getData('text/plain')) || '';
|
|
18552
|
+
e.preventDefault();
|
|
18553
|
+
if (data) holdFlightEntry(surface, { kind: 'text', data });
|
|
18554
|
+
return;
|
|
18555
|
+
}
|
|
18556
|
+
// A deletion that did not come from a held key (the keyboard ones are
|
|
18557
|
+
// held at keydown — WebKit fires no beforeinput at all for Backspace on
|
|
18558
|
+
// an empty surface, measured): a context-menu delete, say.
|
|
18559
|
+
if (/^delete/.test(t) && t !== 'deleteByCut' && t !== 'deleteByDrag' && t !== 'deleteCompositionText') {
|
|
18560
|
+
e.preventDefault();
|
|
18561
|
+
holdFlightEntry(surface, { kind: 'delete', inputType: t });
|
|
18562
|
+
}
|
|
18563
|
+
}, true);
|
|
18564
|
+
document.addEventListener('input', (e) => {
|
|
18565
|
+
if (composedTakeBack !== null && e.data === composedTakeBack) takeBackComposedText(e.data);
|
|
18566
|
+
composedTakeBack = null;
|
|
18567
|
+
}, true);
|
|
18568
|
+
// Known limit: a composition still open when the flight ends is left to
|
|
18569
|
+
// the browser — its compositionend arrives with no flight to hold it, and
|
|
18570
|
+
// if the render replaced the surface mid-composition, what the engine does
|
|
18571
|
+
// with the rest of it is its own.
|
|
18572
|
+
document.addEventListener('compositionend', (e) => {
|
|
18573
|
+
const surface = flightSurfaceOfEvent(e);
|
|
18574
|
+
if (!surface || !e.data) return;
|
|
18575
|
+
takeBackComposedText(e.data);
|
|
18576
|
+
holdFlightEntry(surface, { kind: 'text', data: e.data });
|
|
18577
|
+
composedHeld = e.data;
|
|
18578
|
+
setTimeout(() => { composedHeld = null; }, 0);
|
|
18579
|
+
}, true);
|
|
18580
|
+
|
|
17959
18581
|
// Capture phase, matching the phase the ordering above was measured in.
|
|
17960
18582
|
document.addEventListener('beforeinput', (e) => {
|
|
17961
18583
|
if (!NATIVE_FORMAT_INPUT_TYPES.has(e.inputType)) return;
|
|
@@ -18022,10 +18644,35 @@
|
|
|
18022
18644
|
// wave field is not — this is here so the boundary is one predicate rather
|
|
18023
18645
|
// than a property of which targets those two selectors happen to match.
|
|
18024
18646
|
if (waveEditorIsModal()) return;
|
|
18025
|
-
|
|
18026
|
-
|
|
18647
|
+
// v3.13.1 T1 fix round 1: a paste while a commit is in flight is held like
|
|
18648
|
+
// typing (measured before: lost outright) and replayed through
|
|
18649
|
+
// routePaste(), the path a live paste takes. The clipboard is readable
|
|
18650
|
+
// only during the event, so its flavours are copied out here.
|
|
18651
|
+
const flightSurface = flightSurfaceOfEvent(e);
|
|
18652
|
+
if (flightSurface) {
|
|
18027
18653
|
e.preventDefault();
|
|
18028
|
-
|
|
18654
|
+
holdFlightEntry(flightSurface, { kind: 'paste',
|
|
18655
|
+
dt: snapshotClipboard(e.clipboardData || window.clipboardData), plainOnly: plainPasteArmed });
|
|
18656
|
+
plainPasteArmed = false;
|
|
18657
|
+
return;
|
|
18658
|
+
}
|
|
18659
|
+
const target = e.target && e.target.closest &&
|
|
18660
|
+
(e.target.closest('.ed-wys-cell') || e.target.closest('.ed-wys-armed'));
|
|
18661
|
+
if (!target) return;
|
|
18662
|
+
e.preventDefault();
|
|
18663
|
+
routePaste(target, e.clipboardData || window.clipboardData);
|
|
18664
|
+
});
|
|
18665
|
+
function snapshotClipboard(dt) {
|
|
18666
|
+
const data = {};
|
|
18667
|
+
['text/plain', 'text/html'].forEach((k) => {
|
|
18668
|
+
try { data[k] = dt ? dt.getData(k) || '' : ''; } catch (err) { data[k] = ''; }
|
|
18669
|
+
});
|
|
18670
|
+
const files = dt && dt.files ? Array.prototype.slice.call(dt.files) : [];
|
|
18671
|
+
return { files, types: Object.keys(data).filter((k) => data[k]), getData: (k) => data[k] || '' };
|
|
18672
|
+
}
|
|
18673
|
+
function routePaste(target, dt) {
|
|
18674
|
+
const cellEl = target.closest('.ed-wys-cell');
|
|
18675
|
+
if (cellEl) {
|
|
18029
18676
|
const grid = dt ? clipboardGrid(dt) : null;
|
|
18030
18677
|
if (grid && pasteGridIntoTable(cellEl, grid)) return;
|
|
18031
18678
|
snapBurstIfActive(cellEl.closest('table'), 'paste-pre');
|
|
@@ -18033,10 +18680,8 @@
|
|
|
18033
18680
|
snapBurstIfActive(cellEl.closest('table'), 'paste');
|
|
18034
18681
|
return;
|
|
18035
18682
|
}
|
|
18036
|
-
const editEl =
|
|
18683
|
+
const editEl = target.closest('.ed-wys-armed');
|
|
18037
18684
|
if (!editEl) return;
|
|
18038
|
-
e.preventDefault();
|
|
18039
|
-
const dt = e.clipboardData || window.clipboardData;
|
|
18040
18685
|
// v3.1.0 追加 2: paste-md.js decides what the clipboard actually holds
|
|
18041
18686
|
// (text/html -> markdown, an image, or plain text) and this routes the
|
|
18042
18687
|
// answer. The TABLE-CELL branch above deliberately keeps its plain-text
|
|
@@ -18044,7 +18689,7 @@
|
|
|
18044
18689
|
// pasted into one cell would be a nested structure the serializer has no
|
|
18045
18690
|
// way to represent.
|
|
18046
18691
|
handleArmedPaste(dt, editEl);
|
|
18047
|
-
}
|
|
18692
|
+
}
|
|
18048
18693
|
|
|
18049
18694
|
// Task 5: hover-edge insert bubbles — one delegated, rAF-throttled
|
|
18050
18695
|
// mousemove listener (never a per-block/per-boundary listener) drives
|