@helping-ai-workflow/md2doc 3.0.0 → 3.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.
- package/lib/editor/client.js +370 -102
- package/lib/editor/table-md.js +411 -24
- package/lib/md2doc.js +123 -78
- package/package.json +2 -2
package/lib/editor/client.js
CHANGED
|
@@ -1667,6 +1667,57 @@
|
|
|
1667
1667
|
// function did by returning early.
|
|
1668
1668
|
function computeIndentClamp(spanEls, opBlockEl, opOldIndent, opts) {
|
|
1669
1669
|
if (!indentClamp || !spanEls || !spanEls.length) return null;
|
|
1670
|
+
// v3.0.2 — the structural answer to the stale-`run` pattern that v3.0.1
|
|
1671
|
+
// point-fixed at three call sites, and that both reviewers asked to have
|
|
1672
|
+
// fixed at the call convention instead. There are FIVE call sites, not
|
|
1673
|
+
// three (3483 batch delete, 3812 convert, 5778 batch Tab/Shift+Tab, 6028
|
|
1674
|
+
// caret Tab/Shift+Tab — both indent directions share ONE call site each,
|
|
1675
|
+
// batch vs. caret, not one call site per direction — and the drop's
|
|
1676
|
+
// compute at 8516), and every one of them hands over a `listRunOf()`
|
|
1677
|
+
// result in document order. Two ways that goes stale, both of which
|
|
1678
|
+
// produced a confident wrong number rather than a refusal: a member
|
|
1679
|
+
// DETACHED (getAttribute still answers on a detached node) and a span
|
|
1680
|
+
// still attached but no longer in the run's document order (indexOf()
|
|
1681
|
+
// then reports an opIndex for a layout that no longer exists, so §3.4
|
|
1682
|
+
// rule 2's scope starts in the wrong place). Re-deriving the span here
|
|
1683
|
+
// instead of checking it was rejected: it would quietly change the
|
|
1684
|
+
// contract from "clamp this span" to "clamp the run containing this
|
|
1685
|
+
// op", and the drop is the one caller subtle enough that taking that
|
|
1686
|
+
// expressiveness away is not free.
|
|
1687
|
+
//
|
|
1688
|
+
// It does NOT throw. This file's defensive style (safeRerenderAll at
|
|
1689
|
+
// ~990) is that the editor must not die on one path's failure. But the
|
|
1690
|
+
// refusal is NOT free either: at the drop (8516) a null clamp feeds
|
|
1691
|
+
// spanIndentsAreAnchored() the RAW reordered indents, and that refuses
|
|
1692
|
+
// 1425 of 4067 legal drops where the clamped answer refuses only 14 (the
|
|
1693
|
+
// clamp rescues the other 1411; the measured numbers are recorded at
|
|
1694
|
+
// line ~371). So a false positive here costs the user up to 1411 legal
|
|
1695
|
+
// batch drags, replaced by a banner —
|
|
1696
|
+
// which is why the assert is worth this cost even though the batch-
|
|
1697
|
+
// delete, convert and drop sites each have a scenario whose correct
|
|
1698
|
+
// answer needs a real reclamp (a wrongly-firing assert breaks them
|
|
1699
|
+
// visibly), while neither Tab call site (5778 batch, 6028 caret) has a
|
|
1700
|
+
// scenario that reclamps a TRAILING sibling — see
|
|
1701
|
+
// test/editor-client-runtime.test.js for the per-site detail.
|
|
1702
|
+
const fresh = listRunOf(spanEls[0]);
|
|
1703
|
+
if (fresh.length !== spanEls.length ||
|
|
1704
|
+
fresh.some((el, i) => el !== spanEls[i])) {
|
|
1705
|
+
// Minor 5: the length pair alone is silent on the reordered-but-same-
|
|
1706
|
+
// length case (the ONLY case where an index is diagnostic — a length
|
|
1707
|
+
// mismatch is already self-explanatory), so report the first index at
|
|
1708
|
+
// which the two runs disagree too.
|
|
1709
|
+
const minLen = Math.min(fresh.length, spanEls.length);
|
|
1710
|
+
let badIndex = -1;
|
|
1711
|
+
for (let i = 0; i < minLen; i++) {
|
|
1712
|
+
if (fresh[i] !== spanEls[i]) { badIndex = i; break; }
|
|
1713
|
+
}
|
|
1714
|
+
if (badIndex < 0 && fresh.length !== spanEls.length) badIndex = minLen;
|
|
1715
|
+
console.error('computeIndentClamp: stale span — the run has ' + fresh.length +
|
|
1716
|
+
' members, the caller passed ' + spanEls.length +
|
|
1717
|
+
', first differing at index ' + badIndex +
|
|
1718
|
+
'; refusing rather than clamping against a layout that no longer exists');
|
|
1719
|
+
return null;
|
|
1720
|
+
}
|
|
1670
1721
|
const opEls = Array.isArray(opBlockEl) ? opBlockEl : [opBlockEl];
|
|
1671
1722
|
const opIndex = [];
|
|
1672
1723
|
for (let k = 0; k < opEls.length; k++) {
|
|
@@ -2009,6 +2060,30 @@
|
|
|
2009
2060
|
'<path d="M6.6 7.2v4.2"/><path d="M9.4 7.2v4.2"/>',
|
|
2010
2061
|
// MD 原始碼 — angle brackets: source, not prose.
|
|
2011
2062
|
code: '<path d="M6 3.6L2 8l4 4.4"/><path d="M10 3.6L14 8l-4 4.4"/>',
|
|
2063
|
+
// ── v3.0.1: one icon per 轉換成 target ────────────────────────────────
|
|
2064
|
+
// Keys are CONVERT_TARGETS[].id verbatim; openConvertSubmenu() looks them
|
|
2065
|
+
// up by that id, so a rename on either side must be made on both.
|
|
2066
|
+
// 文字 — a paragraph pilcrow, drawn rather than typed so it inherits the
|
|
2067
|
+
// same stroke weight as every sibling icon.
|
|
2068
|
+
text: '<path d="M9 2.5H6.2a2.7 2.7 0 0 0 0 5.4H9"/><path d="M9 2.5v11"/><path d="M11.8 2.5v11"/>',
|
|
2069
|
+
// 標題 1–6 — an "H" plus the level, as two strokes and a numeral bar set.
|
|
2070
|
+
h1: '<path d="M3 3v10"/><path d="M8 3v10"/><path d="M3 8h5"/><path d="M11.5 6.5l1.5-1v7.5"/>',
|
|
2071
|
+
h2: '<path d="M3 3v10"/><path d="M8 3v10"/><path d="M3 8h5"/><path d="M11 6.6a1.6 1.6 0 0 1 2.7 1.1c0 1.4-2.7 2.6-2.7 5.3h3"/>',
|
|
2072
|
+
h3: '<path d="M3 3v10"/><path d="M8 3v10"/><path d="M3 8h5"/><path d="M11 5.8h2.7l-1.6 2.6a1.8 1.8 0 1 1-1.3 3"/>',
|
|
2073
|
+
h4: '<path d="M3 3v10"/><path d="M8 3v10"/><path d="M3 8h5"/><path d="M13 5.8v7.2"/><path d="M13 10.7h-2.6l2.2-4.9"/>',
|
|
2074
|
+
h5: '<path d="M3 3v10"/><path d="M8 3v10"/><path d="M3 8h5"/><path d="M13.5 5.8h-2.6v2.8a1.9 1.9 0 1 1-.5 3.6"/>',
|
|
2075
|
+
h6: '<path d="M3 3v10"/><path d="M8 3v10"/><path d="M3 8h5"/><path d="M13.4 6.2a1.9 1.9 0 0 0-3 1.5v3.2a1.8 1.8 0 1 0 3.1-1.3 1.8 1.8 0 0 0-3.1 1.3"/>',
|
|
2076
|
+
// 項目符號列表 — three dots with three rules.
|
|
2077
|
+
ul: '<circle cx="3.2" cy="4.2" r="1"/><circle cx="3.2" cy="8" r="1"/><circle cx="3.2" cy="11.8" r="1"/>' +
|
|
2078
|
+
'<path d="M6.4 4.2h7.4"/><path d="M6.4 8h7.4"/><path d="M6.4 11.8h7.4"/>',
|
|
2079
|
+
// 編號列表 — the same three rules led by 1/2/3 strokes.
|
|
2080
|
+
ol: '<path d="M2.2 3.4l1-.6v2.8"/><path d="M2 8.2a1 1 0 0 1 1.7.7c0 .9-1.7 1.6-1.7 2.4h1.9"/>' +
|
|
2081
|
+
'<path d="M2.1 11.4h1.6l-1 1.1a1 1 0 1 1-.7 1.2"/>' +
|
|
2082
|
+
'<path d="M6.4 4.2h7.4"/><path d="M6.4 8.6h7.4"/><path d="M6.4 12.9h7.4"/>',
|
|
2083
|
+
// 待辦清單 — a checked box.
|
|
2084
|
+
task: '<rect x="2.2" y="2.6" width="10.8" height="10.8" rx="2.4"/><path d="M5.2 8.2l2.1 2.1 3.8-4"/>',
|
|
2085
|
+
// 引用 — a quote bar plus two shortened rules.
|
|
2086
|
+
quote: '<path d="M3 3.4v9.2"/><path d="M6.4 5.4h7.2"/><path d="M6.4 8h7.2"/><path d="M6.4 10.6h4.6"/>',
|
|
2012
2087
|
};
|
|
2013
2088
|
function menuIconMarkup(name) {
|
|
2014
2089
|
return '<svg class="ed-menu-icon" viewBox="0 0 16 16" width="16" height="16" ' +
|
|
@@ -2191,7 +2266,13 @@
|
|
|
2191
2266
|
const b = document.createElement('button');
|
|
2192
2267
|
b.type = 'button';
|
|
2193
2268
|
b.className = 'ed-handle-menu-btn';
|
|
2194
|
-
|
|
2269
|
+
// v3.0.1: the submenu carries icons too. Without them its labels start
|
|
2270
|
+
// at the button's own padding while the parent menu's start past a 16px
|
|
2271
|
+
// icon, so the two levels' text does not share a vertical line — the
|
|
2272
|
+
// exact defect the user reported. `.ed-handle-menu-btn` is already a
|
|
2273
|
+
// flex row with a gap, so an icon plus a text node is all it takes.
|
|
2274
|
+
b.innerHTML = menuIconMarkup(t.id);
|
|
2275
|
+
b.appendChild(document.createTextNode(t.label));
|
|
2195
2276
|
b.setAttribute('aria-label', 'Convert to ' + t.id);
|
|
2196
2277
|
b.addEventListener('click', (e) => {
|
|
2197
2278
|
e.stopPropagation();
|
|
@@ -4186,7 +4267,16 @@
|
|
|
4186
4267
|
// S1 removed the pre-per-li 'list' branch that lived here: blockmap has not
|
|
4187
4268
|
// emitted type:'list' blocks since Phase 4, so no startBurst() call could
|
|
4188
4269
|
// produce blockType === 'list' and the branch was already dead code.
|
|
4189
|
-
|
|
4270
|
+
// v3.0.1: hand the serializer the table's CURRENT source lines so it can
|
|
4271
|
+
// reuse the author's own column widths instead of flattening the whole
|
|
4272
|
+
// table to minimal form over a one-cell edit. extractBlockSource() is the
|
|
4273
|
+
// same helper the raw editor prefills from, so "what the file says" has
|
|
4274
|
+
// exactly one definition.
|
|
4275
|
+
const tableBlockRec = burst.blockType === 'table'
|
|
4276
|
+
? blocks.find((b) => b.id === burst.blockId) : null;
|
|
4277
|
+
const result = burst.blockType === 'table'
|
|
4278
|
+
? tableMd.serializeTable(burst.editEl,
|
|
4279
|
+
tableBlockRec ? extractBlockSource(lines, tableBlockRec) : null)
|
|
4190
4280
|
: inlineMd.serializeInline(burst.editEl);
|
|
4191
4281
|
if (result.unsupported.length > 0) {
|
|
4192
4282
|
// Degrade-never-lose (same contract as Phase 2's openWysiwygEditor()
|
|
@@ -4452,6 +4542,15 @@
|
|
|
4452
4542
|
window.__edTestForceRerender = function () { return rerenderAll(); };
|
|
4453
4543
|
window.__edTestTruncateTo = function (n) { lines = lines.slice(0, n); };
|
|
4454
4544
|
|
|
4545
|
+
// v3.0.2: computeIndentClamp() lives past this file's node early-exit
|
|
4546
|
+
// (module.exports at line ~509 ships the pure core only) and nothing on
|
|
4547
|
+
// window reaches it, so the stale-span assert added to it in v3.0.2 had
|
|
4548
|
+
// no way to be tested at all. This is the seventh __edTest seam and the
|
|
4549
|
+
// only production surface that change adds.
|
|
4550
|
+
window.__edTestClampProbe = function (spanEls, opBlockEl, opOldIndent, opts) {
|
|
4551
|
+
return computeIndentClamp(spanEls, opBlockEl, opOldIndent, opts);
|
|
4552
|
+
};
|
|
4553
|
+
|
|
4455
4554
|
// ── S3 Task 4: the entry and exit GESTURES ─────────────────────────────
|
|
4456
4555
|
// §4.4 entries: (a) press inside a block and drag across its boundary;
|
|
4457
4556
|
// (b) Shift+Click; (c) Shift+↑↓. Exits: Escape (Task 3's keydown prologue)
|
|
@@ -5468,16 +5567,29 @@
|
|
|
5468
5567
|
function indentListItem(blockEl) {
|
|
5469
5568
|
const self = liAttrs(blockEl);
|
|
5470
5569
|
if (!self) return false;
|
|
5471
|
-
// Rule (d): the first item of a LIST has nothing above it to nest under.
|
|
5472
|
-
// Without this the §3.4 clamp would happily read the previous list's last
|
|
5473
|
-
// item as "the previous block" and indent this one underneath it, merging
|
|
5474
|
-
// two lists the user never asked to join. Pre-S1 this fell out of
|
|
5475
|
-
// `previousElementSibling` being null inside the item's own <ul>.
|
|
5476
|
-
if (self.listStart) return false;
|
|
5477
5570
|
const all = allBlockEls();
|
|
5478
5571
|
const i = all.indexOf(blockEl);
|
|
5479
5572
|
if (i < 0) return false;
|
|
5480
5573
|
const prev = liAttrs(all[i - 1]);
|
|
5574
|
+
// Rule (d), narrowed (v3.0.1). The old rule was `if (self.listStart)
|
|
5575
|
+
// return false` — the first item of a LIST has nothing above it to nest
|
|
5576
|
+
// under. That over-refused: a bullet outdented to indent 0 directly
|
|
5577
|
+
// BELOW an ordered item is a listStart (a ul following an ol) whose
|
|
5578
|
+
// previous block is a perfectly good parent, and the user could never
|
|
5579
|
+
// indent it back. The half that still has to be refused is the one the
|
|
5580
|
+
// old rule was really aimed at: two list tokens the user kept apart with
|
|
5581
|
+
// a BLANK LINE. Indenting the second one makes listRunOf() span both,
|
|
5582
|
+
// and re-serializing that span swallows the blank line — a rewrite of
|
|
5583
|
+
// lines nobody touched, which is the one thing this editor must never do.
|
|
5584
|
+
// Contiguity is a fact about the FILE, so it is read off `blocks`, not
|
|
5585
|
+
// guessed from the DOM.
|
|
5586
|
+
if (self.listStart) {
|
|
5587
|
+
if (!prev) return false;
|
|
5588
|
+
const prevRec = blockRecOf(all[i - 1]);
|
|
5589
|
+
const selfRec = blockRecOf(blockEl);
|
|
5590
|
+
if (!prevRec || !selfRec) return false;
|
|
5591
|
+
if (prevRec.endLine + 1 !== selfRec.startLine) return false;
|
|
5592
|
+
}
|
|
5481
5593
|
const max = prev ? prev.indent + 1 : 0;
|
|
5482
5594
|
const next = Math.min(self.indent + 1, max);
|
|
5483
5595
|
if (next === self.indent) return false;
|
|
@@ -5554,22 +5666,39 @@
|
|
|
5554
5666
|
|
|
5555
5667
|
// How far ONE member may move, as a magnitude (0 or 1), never a signed
|
|
5556
5668
|
// delta. `dir` is +1 (Tab) or -1 (Shift+Tab). The ceiling restates
|
|
5557
|
-
// indentListItem()'s own single-item rule
|
|
5558
|
-
//
|
|
5559
|
-
//
|
|
5560
|
-
//
|
|
5561
|
-
//
|
|
5562
|
-
//
|
|
5563
|
-
//
|
|
5669
|
+
// indentListItem()'s own single-item rule, v3.0.1's narrowed list-start
|
|
5670
|
+
// clause included: a listStart member still refuses UNLESS the block
|
|
5671
|
+
// immediately before it (by file line, via blockRecOf — same contiguity
|
|
5672
|
+
// check, same code shape as indentListItem()'s, kept in that shape
|
|
5673
|
+
// deliberately so the two stay in visible agreement) is a contiguous list
|
|
5674
|
+
// item, in which case it has real head-room like any other item. Before
|
|
5675
|
+
// v3.0.1 this was unconditional (`if (self.listStart) return 0;`) —
|
|
5676
|
+
// batch Tab on a listStart block was refused outright, matching
|
|
5677
|
+
// indentListItem()'s then-unconditional refusal. Narrowing ONE of the two
|
|
5678
|
+
// without the other silently reintroduces the single-item defect on the
|
|
5679
|
+
// batch axis: a listStart item directly below a contiguous ordered item,
|
|
5680
|
+
// selected as part of a batch, would stay refused even though the very
|
|
5681
|
+
// same block indents fine on its own. The floor is outdentListItem()'s
|
|
5682
|
+
// (indent 0 cannot rise), untouched by this narrowing. Both read PRE-move
|
|
5683
|
+
// indents, which is §3.4's global convention ("the operated block's
|
|
5684
|
+
// indent" always means the value before the operation), and it is what
|
|
5685
|
+
// makes the minimum below well-defined: every member is measured against
|
|
5686
|
+
// the document as it stands, not against members already moved.
|
|
5564
5687
|
function memberIndentHeadroom(el, dir) {
|
|
5565
5688
|
const self = liAttrs(el);
|
|
5566
5689
|
if (!self) return 0;
|
|
5567
5690
|
if (dir < 0) return self.indent > 0 ? 1 : 0;
|
|
5568
|
-
if (self.listStart) return 0;
|
|
5569
5691
|
const all = allBlockEls();
|
|
5570
5692
|
const i = all.indexOf(el);
|
|
5571
5693
|
if (i < 0) return 0;
|
|
5572
5694
|
const prev = liAttrs(all[i - 1]);
|
|
5695
|
+
if (self.listStart) {
|
|
5696
|
+
if (!prev) return 0;
|
|
5697
|
+
const prevRec = blockRecOf(all[i - 1]);
|
|
5698
|
+
const selfRec = blockRecOf(el);
|
|
5699
|
+
if (!prevRec || !selfRec) return 0;
|
|
5700
|
+
if (prevRec.endLine + 1 !== selfRec.startLine) return 0;
|
|
5701
|
+
}
|
|
5573
5702
|
const max = prev ? prev.indent + 1 : 0;
|
|
5574
5703
|
return Math.max(0, Math.min(1, max - self.indent));
|
|
5575
5704
|
}
|
|
@@ -5623,7 +5752,30 @@
|
|
|
5623
5752
|
// segment delta for the blocks below instead of N independent clamps, and
|
|
5624
5753
|
// its rule 1 then walks the operated blocks in document order so each is
|
|
5625
5754
|
// measured against the member above it that the same pass just settled.
|
|
5626
|
-
|
|
5755
|
+
//
|
|
5756
|
+
// v3.0.1 (review round 1 follow-up): `run` above is batchRunOf()'s
|
|
5757
|
+
// PRE-mutation snapshot — the exact same staleness handleLiKeydown()'s
|
|
5758
|
+
// single-item Tab branch had before its own v3.0.1 fix (see that
|
|
5759
|
+
// call's comment). For a single-member batch on a listStart item whose
|
|
5760
|
+
// ceiling memberIndentHeadroom() has just narrowed (a bullet directly
|
|
5761
|
+
// below a contiguous ordered item), the pre-mutation run is `[liEls[0]]`
|
|
5762
|
+
// alone — batchRunOf() never walked backward past the list-start
|
|
5763
|
+
// boundary, because before this fix nothing could ever cross it. With
|
|
5764
|
+
// no anchor IN the span, applyIndentClamp()'s rule 1 has nothing to
|
|
5765
|
+
// bound the indent the forEach above just wrote against, and clamps it
|
|
5766
|
+
// straight back to 0 — silently undoing the write, MEASURED directly:
|
|
5767
|
+
// narrowing memberIndentHeadroom() alone left the covering test RED.
|
|
5768
|
+
// A FRESH listRunOf(liEls[0]) call here — taken after the write above —
|
|
5769
|
+
// walks back from the NEW indent and finds the real parent, exactly
|
|
5770
|
+
// like the post-mutation `liveRun = listRunOf(liEls[0])` a few lines
|
|
5771
|
+
// below (the COMMIT span) already does; using the same fresh call for
|
|
5772
|
+
// the clamp keeps the two consistent, same reasoning as
|
|
5773
|
+
// handleLiKeydown()'s Tab branch. Provably identical to the old stale
|
|
5774
|
+
// `run` for every case where no member was ever a list-start
|
|
5775
|
+
// (listRunOf() slices by INDEX RANGE, and neither the range's
|
|
5776
|
+
// start/end index nor the elements between them shift when only
|
|
5777
|
+
// already-run-member indents change).
|
|
5778
|
+
applyIndentClamp(listRunOf(liEls[0]), liEls, oldIndent);
|
|
5627
5779
|
});
|
|
5628
5780
|
// Re-derived AFTER the mutation, like the caret Tab's: an indent change can
|
|
5629
5781
|
// move a block between runs.
|
|
@@ -5854,7 +6006,26 @@
|
|
|
5854
6006
|
const oldIndent = Number(li.getAttribute('data-indent')) || 0;
|
|
5855
6007
|
const changed = mutateListRun(() => {
|
|
5856
6008
|
if (!(e.shiftKey ? outdentListItem(li) : indentListItem(li))) return false;
|
|
5857
|
-
|
|
6009
|
+
// v3.0.1: `run` above is a PRE-mutation snapshot, captured while `li`
|
|
6010
|
+
// still had its OLD indent. That is fine for outdentListItem() — the
|
|
6011
|
+
// clamp's model rereads every element's CURRENT data-indent off the
|
|
6012
|
+
// DOM, so it self-corrects regardless of which elements the snapshot
|
|
6013
|
+
// names — but indentListItem() can now move a rule-(d) list-start
|
|
6014
|
+
// item (a bullet directly below an ordered item, say) whose own
|
|
6015
|
+
// pre-mutation run is `[li]` alone: nothing shallower ever preceded
|
|
6016
|
+
// it, because runBlocksOf() stops at a list-start by design. With no
|
|
6017
|
+
// anchor IN the span, applyIndentClamp()'s rule 1 has nothing to
|
|
6018
|
+
// bound the indent this call just wrote against, and clamps it
|
|
6019
|
+
// straight back to 0 — silently undoing the write. A FRESH
|
|
6020
|
+
// listRunOf(li) call here — taken after the write above — walks back
|
|
6021
|
+
// from li's NEW indent and finds the real parent, exactly like the
|
|
6022
|
+
// post-mutation `run = listRunOf(li)` a few lines below (the COMMIT
|
|
6023
|
+
// span) already does; using the same fresh call for the clamp keeps
|
|
6024
|
+
// the two consistent. Provably identical to the old stale `run` for
|
|
6025
|
+
// every case where li was never a list-start (listRunOf() slices by
|
|
6026
|
+
// INDEX RANGE, and neither the range's start/end index nor the
|
|
6027
|
+
// elements between them shift when only li's own indent changes).
|
|
6028
|
+
applyIndentClamp(listRunOf(li), li, oldIndent);
|
|
5858
6029
|
return true;
|
|
5859
6030
|
});
|
|
5860
6031
|
if (!changed) return true;
|
|
@@ -6426,8 +6597,12 @@
|
|
|
6426
6597
|
function tableIdentityOf(tableEl) {
|
|
6427
6598
|
const headerRow = tableEl ? headerRowOf(tableEl) : null;
|
|
6428
6599
|
if (!headerRow) return null;
|
|
6429
|
-
|
|
6430
|
-
|
|
6600
|
+
// The two delimiters are written as ESCAPES, not as literal control
|
|
6601
|
+
// bytes. They used to be literal, which made grep classify this whole
|
|
6602
|
+
// 9643-line file as binary and SILENTLY TRUNCATE its output — a real
|
|
6603
|
+
// investigation was misled by it. The runtime value is identical.
|
|
6604
|
+
return allRowsOf(tableEl).length + '\x01' +
|
|
6605
|
+
Array.prototype.slice.call(headerRow.cells).map((c) => c.textContent).join('\x00');
|
|
6431
6606
|
}
|
|
6432
6607
|
async function ensureTableBurstOpen(tableEl) {
|
|
6433
6608
|
if (currentBurst && currentBurst.blockType === 'table' && currentBurst.editEl === tableEl) return tableEl;
|
|
@@ -6907,19 +7082,20 @@
|
|
|
6907
7082
|
// listener (wired near the bottom of this file) that already drives
|
|
6908
7083
|
// updateTableInsertBubbles() — see its own comment for the coalescing
|
|
6909
7084
|
// contract this reuses.
|
|
6910
|
-
// Geometry, per axis (they are
|
|
7085
|
+
// Geometry, per axis (v3.0.1: they are mirror images again):
|
|
6911
7086
|
// COLUMN grip: its CENTRELINE coincides with the table's TOP border, so
|
|
6912
7087
|
// its hit rect straddles that border by half its own height on each
|
|
6913
7088
|
// side (P0-a).
|
|
6914
|
-
// ROW grip: its
|
|
6915
|
-
//
|
|
6916
|
-
//
|
|
6917
|
-
//
|
|
6918
|
-
//
|
|
6919
|
-
//
|
|
6920
|
-
//
|
|
6921
|
-
//
|
|
6922
|
-
//
|
|
7089
|
+
// ROW grip: its CENTRELINE coincides with the table's LEFT border, so
|
|
7090
|
+
// its hit rect straddles that border by half its own width on each
|
|
7091
|
+
// side — same shape as the column grip, restored in v3.0.1 (spec §4.2
|
|
7092
|
+
// hit-test conflict 2).
|
|
7093
|
+
//
|
|
7094
|
+
// History: why the row axis briefly broke the symmetry, and how that was
|
|
7095
|
+
// undone. The block's own gutter ⠿ hangs outside the content box, and a
|
|
7096
|
+
// table block's own table starts at that same left edge, so a row grip
|
|
7097
|
+
// centred on the table's left border occupied [blockLeft-10, blockLeft+10]
|
|
7098
|
+
// and overlapped the ⠿'s right 6px. Both are position: fixed / absolutely
|
|
6923
7099
|
// positioned overlays and the grip is a document.body child at z-index 7,
|
|
6924
7100
|
// so the grip won that hit test whenever it was showing — which is
|
|
6925
7101
|
// whenever the pointer had recently been over a table cell. MEASURED on
|
|
@@ -6930,20 +7106,32 @@
|
|
|
6930
7106
|
// 402..407. S4 makes the ⠿ a drag handle, so those 6px would have started
|
|
6931
7107
|
// a table ROW drag instead of a block move.
|
|
6932
7108
|
//
|
|
6933
|
-
//
|
|
7109
|
+
// S4 T1 (v2.12.0) fixed that by INSETTING the grip instead — its left edge
|
|
7110
|
+
// on the table's left border, entirely inside the table, no more straddle.
|
|
7111
|
+
// That broke the row/column symmetry (the column grip still straddles the
|
|
7112
|
+
// table's top border, because nothing of the page's own chrome lives up
|
|
7113
|
+
// there) and was reported as a v3.0.1 visual regression. An even earlier
|
|
7114
|
+
// revision (v2.10.1) had already tried an inset like it and been reverted,
|
|
6934
7115
|
// because a 20px grip inside a cell whose left padding is 14px sits on
|
|
6935
|
-
// that cell's TEXT
|
|
6936
|
-
//
|
|
6937
|
-
//
|
|
6938
|
-
//
|
|
6939
|
-
//
|
|
6940
|
-
//
|
|
6941
|
-
//
|
|
6942
|
-
//
|
|
6943
|
-
//
|
|
6944
|
-
//
|
|
6945
|
-
//
|
|
6946
|
-
//
|
|
7116
|
+
// that cell's TEXT — S4 T1 avoided repeating that by widening the first
|
|
7117
|
+
// column's own edit-mode padding to clear the inset grip.
|
|
7118
|
+
//
|
|
7119
|
+
// v3.0.1 undoes the inset rather than re-tuning it: the row grip goes back
|
|
7120
|
+
// to straddling the border (this file's change, see updateTableEdgeGrips()
|
|
7121
|
+
// below), and the ⠿/+ pair itself moves further LEFT instead (a shift
|
|
7122
|
+
// token declared alongside the gutter's own tokens in lib/md2doc.js) — the
|
|
7123
|
+
// overlap is removed at its SOURCE, not by moving the grip out of the
|
|
7124
|
+
// ⠿'s way. That also means the first column's edit-mode padding widening
|
|
7125
|
+
// is gone: the grip's straddling half (10px) fits inside the column's
|
|
7126
|
+
// default 14px padding on its own, so v2.10.1's original defect does not
|
|
7127
|
+
// come back. Neither the ⠿'s right edge nor the grip's left edge is
|
|
7128
|
+
// written down as a literal on either side of the gap between them — see
|
|
7129
|
+
// the shift-token comment in lib/md2doc.js's edit-mode :root block, which
|
|
7130
|
+
// this file deliberately does not name (test/editor-client.test.js forbids
|
|
7131
|
+
// the retired hover-gutter class name as a bare substring and the token
|
|
7132
|
+
// name contains it) — so the two cannot drift apart the way v2.11.0's pair
|
|
7133
|
+
// position once did (the CSS comment claiming "an 8px gap" was describing
|
|
7134
|
+
// that stale position, which v2.11.1 replaced).
|
|
6947
7135
|
// test/editor-client-runtime.test.js asserts BOTH directions: every pixel
|
|
6948
7136
|
// of a block's ⠿ answers that ⠿ (row grip hidden and showing), and the row
|
|
6949
7137
|
// grip's own centre answers the row grip.
|
|
@@ -6962,6 +7150,19 @@
|
|
|
6962
7150
|
// reported overlap corner AND verifies the z-index ordering directly —
|
|
6963
7151
|
// re-verify it if either the grip's or bubble's z-index ever changes.
|
|
6964
7152
|
|
|
7153
|
+
// --ed-te-grip-row-w, read once and cached. The token is the single source
|
|
7154
|
+
// of truth for the grip's width (lib/md2doc.js declares it); parsing it here
|
|
7155
|
+
// means the JS never carries a second copy of the number.
|
|
7156
|
+
let gripRowWidthCache = null;
|
|
7157
|
+
function gripRowWidthPx() {
|
|
7158
|
+
if (gripRowWidthCache !== null) return gripRowWidthCache;
|
|
7159
|
+
const raw = getComputedStyle(document.documentElement)
|
|
7160
|
+
.getPropertyValue('--ed-te-grip-row-w');
|
|
7161
|
+
const n = parseFloat(raw);
|
|
7162
|
+
gripRowWidthCache = Number.isFinite(n) && n > 0 ? n : 20;
|
|
7163
|
+
return gripRowWidthCache;
|
|
7164
|
+
}
|
|
7165
|
+
|
|
6965
7166
|
function buildTableGrip(cls, ariaLabel) {
|
|
6966
7167
|
const b = document.createElement('button');
|
|
6967
7168
|
b.type = 'button';
|
|
@@ -6998,6 +7199,40 @@
|
|
|
6998
7199
|
let gripColTableEl = null;
|
|
6999
7200
|
let gripColIndex = null;
|
|
7000
7201
|
|
|
7202
|
+
// v3.0.2 — the gutter keep-alive for the table row grip. Held in its own
|
|
7203
|
+
// module-level reference, NOT read back off `gripRowTableEl`, because
|
|
7204
|
+
// hideTableGrips() below nulls that at its own line 7246-7248: a teardown
|
|
7205
|
+
// that went looking for the block through it would find null exactly when
|
|
7206
|
+
// it needs to clear the class, and rerenderAll()'s hideTableGrips() call
|
|
7207
|
+
// reaches it with the table already detached.
|
|
7208
|
+
let gutterKeepEl = null;
|
|
7209
|
+
|
|
7210
|
+
function setGutterKeep(blockEl) {
|
|
7211
|
+
const next = blockEl || null;
|
|
7212
|
+
if (gutterKeepEl === next) return;
|
|
7213
|
+
if (gutterKeepEl) gutterKeepEl.classList.remove('ed-keep-lit');
|
|
7214
|
+
gutterKeepEl = next;
|
|
7215
|
+
if (gutterKeepEl) gutterKeepEl.classList.add('ed-keep-lit');
|
|
7216
|
+
}
|
|
7217
|
+
|
|
7218
|
+
function clearGutterKeep() { setGutterKeep(null); }
|
|
7219
|
+
|
|
7220
|
+
// The block whose gutter the CURRENTLY SHOWN row grip is covering, or null
|
|
7221
|
+
// when there is nothing to keep lit. Two conditions, both load-bearing:
|
|
7222
|
+
//
|
|
7223
|
+
// • gripRowTableEl / gripRowEl may be null while a grip is nonetheless
|
|
7224
|
+
// visible — a header-only table takes the else branch at 7466-7470 and
|
|
7225
|
+
// nulls both, then the column grip is shown unconditionally just
|
|
7226
|
+
// below. Both callers of this function are reached in that state.
|
|
7227
|
+
// • only the HEADER row's grip shares a Y with the ⠿. Keeping the
|
|
7228
|
+
// gutter lit for a body row's grip would light the ⠿ in a place the
|
|
7229
|
+
// defect never occurred — a behaviour change beyond the fix.
|
|
7230
|
+
function headerGripBlock() {
|
|
7231
|
+
if (!gripRowTableEl || !gripRowEl) return null;
|
|
7232
|
+
if (gripRowEl !== headerRowOf(gripRowTableEl)) return null;
|
|
7233
|
+
return gripRowTableEl.closest('.ed-block');
|
|
7234
|
+
}
|
|
7235
|
+
|
|
7001
7236
|
function hideTableGrips() {
|
|
7002
7237
|
rowGrip.hidden = true;
|
|
7003
7238
|
colGrip.hidden = true;
|
|
@@ -7012,6 +7247,7 @@
|
|
|
7012
7247
|
gripRowEl = null;
|
|
7013
7248
|
gripColTableEl = null;
|
|
7014
7249
|
gripColIndex = null;
|
|
7250
|
+
clearGutterKeep();
|
|
7015
7251
|
}
|
|
7016
7252
|
|
|
7017
7253
|
// Bug fix (user acceptance) — history: grips were originally BOTH
|
|
@@ -7025,16 +7261,18 @@
|
|
|
7025
7261
|
// gripCenter(), which jump straight to the grip's own coordinates) could
|
|
7026
7262
|
// ever land on it; a real mouse gesture could not.
|
|
7027
7263
|
//
|
|
7028
|
-
// Current geometry (
|
|
7029
|
-
//
|
|
7030
|
-
//
|
|
7031
|
-
//
|
|
7032
|
-
//
|
|
7033
|
-
//
|
|
7034
|
-
//
|
|
7035
|
-
//
|
|
7036
|
-
//
|
|
7037
|
-
//
|
|
7264
|
+
// Current geometry (v3.0.1 restored the row axis to mirror the column
|
|
7265
|
+
// axis — see the "Notion-style row/column grip handles" section comment
|
|
7266
|
+
// above for the S4 T1 detour into an inset row grip and how it was
|
|
7267
|
+
// undone): BOTH grips straddle their own border again, the COLUMN grip
|
|
7268
|
+
// half above / half below the table's TOP border and the ROW grip half
|
|
7269
|
+
// outside / half inside the table's LEFT border. The corridor-crossing
|
|
7270
|
+
// bug described above therefore applies to BOTH axes' straddling shape in
|
|
7271
|
+
// principle, and pointInColGripZone() still covers the column's
|
|
7272
|
+
// outside-the-border corridor exactly as before. pointInRowGripZone()
|
|
7273
|
+
// deliberately does NOT get an equivalent corridor formula back, even
|
|
7274
|
+
// though the straddle is back — see its own comment for why the padded
|
|
7275
|
+
// grip rect alone is still sufficient.
|
|
7038
7276
|
//
|
|
7039
7277
|
// Review fix (Important, first pass over-permissive): the first version of
|
|
7040
7278
|
// this fix kept a grip visible while the pointer was ANYWHERE within the
|
|
@@ -7047,30 +7285,28 @@
|
|
|
7047
7285
|
// SPECIFIC shown grip's own anchor (pointInRowGripZone()/
|
|
7048
7286
|
// pointInColGripZone() below) instead of the whole table.
|
|
7049
7287
|
//
|
|
7050
|
-
// What each keep-zone covers TODAY
|
|
7051
|
-
//
|
|
7052
|
-
//
|
|
7053
|
-
//
|
|
7054
|
-
//
|
|
7055
|
-
//
|
|
7056
|
-
//
|
|
7057
|
-
//
|
|
7058
|
-
//
|
|
7059
|
-
//
|
|
7060
|
-
//
|
|
7061
|
-
//
|
|
7062
|
-
//
|
|
7063
|
-
//
|
|
7064
|
-
//
|
|
7065
|
-
//
|
|
7066
|
-
//
|
|
7067
|
-
//
|
|
7068
|
-
//
|
|
7069
|
-
//
|
|
7070
|
-
//
|
|
7071
|
-
//
|
|
7072
|
-
// at another row's height" alone is true of any geometry once the
|
|
7073
|
-
// corridor is gone.
|
|
7288
|
+
// What each keep-zone covers TODAY:
|
|
7289
|
+
// pointInColGripZone() keeps the ORIGINAL corridor shape: the union of
|
|
7290
|
+
// (the column grip's own padded rect) and (the straight strip between
|
|
7291
|
+
// the grip's own TOP edge and the table's top border, x clamped to the
|
|
7292
|
+
// anchor COLUMN's own horizontal extent, padded). The column grip
|
|
7293
|
+
// straddles the top border, so that corridor is real.
|
|
7294
|
+
// pointInRowGripZone() is the padded grip rect ALONE, no corridor term —
|
|
7295
|
+
// kept that way through the v3.0.1 straddle restoration on purpose,
|
|
7296
|
+
// not left behind by accident. A pointer travelling from inside a body
|
|
7297
|
+
// cell to the row grip's own centre (which sits exactly on the
|
|
7298
|
+
// table's left border) never has to leave the table's horizontal
|
|
7299
|
+
// extent on the way there, so the padded rect alone already covers
|
|
7300
|
+
// every REAL (non-teleporting) approach this file's own test suite
|
|
7301
|
+
// exercises — see "table grips: the row grip straddles the table's
|
|
7302
|
+
// left border again…" in test/editor-client-runtime.test.js. That is
|
|
7303
|
+
// narrower than the column axis's guarantee (nothing approaches the
|
|
7304
|
+
// row grip from further out along the table's own left edge the way a
|
|
7305
|
+
// pointer can approach the column grip from above it), which is why
|
|
7306
|
+
// the two functions are allowed to stay asymmetric even though the
|
|
7307
|
+
// grips' shapes are not. If a future report finds a real pointer path
|
|
7308
|
+
// that does need a corridor term here, that is a new, separate defect
|
|
7309
|
+
// — not evidence this reasoning was wrong when it was written.
|
|
7074
7310
|
// A pointer outside either grip's own zone is a genuine exit and still
|
|
7075
7311
|
// hides the grip via hideTableGrips(), same as before. Neither fix touches
|
|
7076
7312
|
// either grip's size or z-index, so the click-priority guarantee (bubble
|
|
@@ -7090,16 +7326,32 @@
|
|
|
7090
7326
|
function pointInRowGripZone(x, y) {
|
|
7091
7327
|
if (rowGrip.hidden || !gripRowTableEl || !gripRowEl ||
|
|
7092
7328
|
!document.body.contains(gripRowEl) || !document.body.contains(gripRowTableEl)) return false;
|
|
7093
|
-
//
|
|
7094
|
-
//
|
|
7095
|
-
//
|
|
7096
|
-
//
|
|
7097
|
-
//
|
|
7098
|
-
//
|
|
7099
|
-
//
|
|
7100
|
-
//
|
|
7101
|
-
//
|
|
7102
|
-
//
|
|
7329
|
+
// v3.0.1 恢復跨界:row grip 的中心線重新落在表格左邊界上(不再是
|
|
7330
|
+
// S4 T1 那個左緣頂在邊界上、整個縮進表格內側的做法)。與 ⠿ 的重疊
|
|
7331
|
+
// 改在源頭解決——往左再位移的是頁面 gutter 的 ⠿/+ 那一對按鈕本身
|
|
7332
|
+
// (多移動一個位移 token,宣告在 lib/md2doc.js 的編輯模式 :root 區
|
|
7333
|
+
// 塊,此檔案依慣例不直接引用其名稱),不是移動 grip。
|
|
7334
|
+
//
|
|
7335
|
+
// 走廊公式沒有跟著補回來,是刻意維持的範圍決定,不是漏掉:
|
|
7336
|
+
// TE_GRIP_ZONE_PAD_PX=4 的 padded rect 涵蓋 [tableLeft-14,
|
|
7337
|
+
// tableLeft+14](grip 本身 20px 寬、跨在邊界上是 [tableLeft-10,
|
|
7338
|
+
// tableLeft+10],外加 4px 緩衝)。指標從儲存格內側走向 grip 中心
|
|
7339
|
+
// (正好落在 tableLeft)的整段路徑,x 一律 >= tableLeft,從未離開
|
|
7340
|
+
// 表格的水平範圍,所以「儲存格 -> grip」這條測試套件唯一驗證過的
|
|
7341
|
+
// 真實(非瞬移)路徑,本來就不需要走廊公式。哪天真的量到一條需要
|
|
7342
|
+
// 走廊的真實路徑(例如從表格左側、gutter 那一側靠近),那是新的、
|
|
7343
|
+
// 獨立的缺陷,不代表這裡當初的推論錯了。
|
|
7344
|
+
//
|
|
7345
|
+
// 誠實記一筆巧合,不要誤讀成保證:⠿ 的右緣恰好落在 tableLeft-14,
|
|
7346
|
+
// 與這個 padded rect 的左緣相觸、不重疊——這是 TE_GRIP_ZONE_PAD_PX
|
|
7347
|
+
// 這個寫死的 4,與 lib/md2doc.js 那個 4px 呼吸間距 token 兩個各自
|
|
7348
|
+
// 獨立宣告出來的巧合,不是誰推導誰的保證。任一邊數值改動,觸點都
|
|
7349
|
+
// 可能變成 1px 縫隙或重疊,沒有任何型別檢查會抓到。這是既有狀態,
|
|
7350
|
+
// 不是本次修正引入或修掉的東西。
|
|
7351
|
+
//
|
|
7352
|
+
// 對使用者可見的差別:離開表格左側不再等於立刻收掉 row grip——padded
|
|
7353
|
+
// rect 本身就已經探出表格邊界 14px,所以指標要離開這整個 padded rect
|
|
7354
|
+
// (水平或垂直)才會真的收掉,錨定那一列的高度也不例外。
|
|
7103
7355
|
return pointInPaddedRect(x, y, rowGrip.getBoundingClientRect(), TE_GRIP_ZONE_PAD_PX);
|
|
7104
7356
|
}
|
|
7105
7357
|
|
|
@@ -7141,7 +7393,15 @@
|
|
|
7141
7393
|
// whatever was last shown untouched instead; hideTableGrips() (called
|
|
7142
7394
|
// from table-leave, burst-end, and drag-start elsewhere) already covers
|
|
7143
7395
|
// every path that actually needs to clear it.
|
|
7144
|
-
if (target && target.closest && (target.closest('.ed-te-grip-row') || target.closest('.ed-te-grip-col')))
|
|
7396
|
+
if (target && target.closest && (target.closest('.ed-te-grip-row') || target.closest('.ed-te-grip-col'))) {
|
|
7397
|
+
// v3.0.2: only the ROW grip is the one that covers the ⠿'s approach.
|
|
7398
|
+
// Testing the hit rather than reusing the branch condition matters —
|
|
7399
|
+
// the column grip sits above the table's top border, nowhere near the
|
|
7400
|
+
// ⠿, and lighting the gutter from there is the behaviour change this
|
|
7401
|
+
// fix is scoped to avoid.
|
|
7402
|
+
setGutterKeep(target.closest('.ed-te-grip-row') ? headerGripBlock() : null);
|
|
7403
|
+
return;
|
|
7404
|
+
}
|
|
7145
7405
|
const blockEl = target && target.closest && target.closest('.ed-block[data-block-type="table"]');
|
|
7146
7406
|
const tableEl = blockEl ? blockContentEl(blockEl) : null;
|
|
7147
7407
|
const cellEl = (tableEl && target && target.closest) ? target.closest('th, td') : null;
|
|
@@ -7154,10 +7414,15 @@
|
|
|
7154
7414
|
// comment for why not the whole table). Keep that grip up while still
|
|
7155
7415
|
// in its zone; only actually hide once the pointer has left both
|
|
7156
7416
|
// zones entirely.
|
|
7157
|
-
|
|
7417
|
+
const inRowZone = pointInRowGripZone(x, y);
|
|
7418
|
+
if (inRowZone || pointInColGripZone(x, y)) {
|
|
7419
|
+
setGutterKeep(inRowZone ? headerGripBlock() : null);
|
|
7420
|
+
return;
|
|
7421
|
+
}
|
|
7158
7422
|
hideTableGrips();
|
|
7159
7423
|
return;
|
|
7160
7424
|
}
|
|
7425
|
+
clearGutterKeep();
|
|
7161
7426
|
|
|
7162
7427
|
const tableRect = tableEl.getBoundingClientRect();
|
|
7163
7428
|
const rowEl = cellEl.parentElement;
|
|
@@ -7167,10 +7432,11 @@
|
|
|
7167
7432
|
// Row grip: every row, including the header — the first row of a
|
|
7168
7433
|
// markdown table IS the header, so any row must be draggable to the
|
|
7169
7434
|
// top to become it. ONE position rule for all of them (user acceptance:
|
|
7170
|
-
// 「grip 位置都一樣」) — its
|
|
7171
|
-
//
|
|
7172
|
-
//
|
|
7173
|
-
//
|
|
7435
|
+
// 「grip 位置都一樣」) — its CENTRELINE on the table's left border, i.e.
|
|
7436
|
+
// straddling it half outside / half inside the table, restored in
|
|
7437
|
+
// v3.0.1 (spec §4.2 hit-test conflict 2 — see the geometry note above
|
|
7438
|
+
// buildTableGrip() for the S4 T1 detour into an inset grip and how it
|
|
7439
|
+
// was undone), vertically centred on its own row; no
|
|
7174
7440
|
// header special case. The
|
|
7175
7441
|
// one exception is a header-only table (no body rows): its single row
|
|
7176
7442
|
// is thead's only row, and dragging it away would empty the thead —
|
|
@@ -7183,16 +7449,18 @@
|
|
|
7183
7449
|
// Fallback height matches .ed-te-grip-row's own CSS height exactly
|
|
7184
7450
|
// (28) — offsetHeight reads 0 while `hidden` (display: none) is still
|
|
7185
7451
|
// true on the FIRST show of a hover session, before the
|
|
7186
|
-
// `hidden = false` assignment below takes effect.
|
|
7187
|
-
// fallback any more, and that is the point: the left edge is the
|
|
7188
|
-
// table's own border, so the grip's WIDTH is not an input to its
|
|
7189
|
-
// position at all. It used to be (`tableRect.left - gw / 2`), which
|
|
7190
|
-
// meant the CSS width and this literal `20` had to agree for the
|
|
7191
|
-
// gutter clearance to hold — exactly the drift §4.2 conflict 2 is
|
|
7192
|
-
// about. The width now lives in one place only, the
|
|
7193
|
-
// --ed-te-grip-row-w token in lib/md2doc.js.
|
|
7452
|
+
// `hidden = false` assignment below takes effect.
|
|
7194
7453
|
const gh = rowGrip.offsetHeight || 28;
|
|
7195
|
-
|
|
7454
|
+
// v3.0.1: back to STRADDLING the table's left border (the S4 T1 inset
|
|
7455
|
+
// was a workaround for the ⠿ hit-test overlap; the ⠿/+ pair has
|
|
7456
|
+
// since moved further left via a CSS shift token in lib/md2doc.js,
|
|
7457
|
+
// which removes the overlap at its source). The width is read from the
|
|
7458
|
+
// token rather than written as a literal here: offsetWidth reads 0
|
|
7459
|
+
// while `hidden` is still true on the first show of a hover session,
|
|
7460
|
+
// and a hard-coded fallback is exactly the CSS/JS drift the token
|
|
7461
|
+
// exists to prevent.
|
|
7462
|
+
const gw = rowGrip.offsetWidth || gripRowWidthPx();
|
|
7463
|
+
rowGrip.style.left = (tableRect.left - gw / 2) + 'px';
|
|
7196
7464
|
rowGrip.style.top = (r.top + r.height / 2 - gh / 2) + 'px';
|
|
7197
7465
|
rowGrip.hidden = false;
|
|
7198
7466
|
} else {
|