@helping-ai-workflow/md2doc 2.11.0 → 2.11.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 +100 -11
- package/lib/editor/list-md.js +68 -1
- package/lib/md2doc.js +81 -7
- package/package.json +1 -1
package/lib/editor/client.js
CHANGED
|
@@ -1553,6 +1553,16 @@
|
|
|
1553
1553
|
el.className = 'ed-handle';
|
|
1554
1554
|
el.textContent = '⠿';
|
|
1555
1555
|
el.setAttribute('aria-label', '區塊選項');
|
|
1556
|
+
// v2.11.1: a <button> is a sequential focus stop, and there is one of
|
|
1557
|
+
// these plus one + standing immediately after EVERY block — so any Tab
|
|
1558
|
+
// that reaches the browser walks straight into gutter chrome, which is the
|
|
1559
|
+
// most jarring shape of the two escape classes fixed above. Both are
|
|
1560
|
+
// mouse-only affordances with no keyboard contract of their own (the ⠿
|
|
1561
|
+
// menu is opened by click; nothing here is reachable or operable by
|
|
1562
|
+
// keyboard today), so they are removed from the tab order rather than
|
|
1563
|
+
// given one they do not have. tabindex="-1" keeps them programmatically
|
|
1564
|
+
// and click-focusable, so `.ed-handle:focus { opacity: 1 }` still works.
|
|
1565
|
+
el.setAttribute('tabindex', '-1');
|
|
1556
1566
|
// Deliberately NOT wired with its own addEventListener here (see the
|
|
1557
1567
|
// paragraph above) — including for Final-review Finding 5a's mousedown
|
|
1558
1568
|
// preventDefault() (see wireBlockSelection()'s delegated 'mousedown'
|
|
@@ -1565,7 +1575,7 @@
|
|
|
1565
1575
|
return el;
|
|
1566
1576
|
}
|
|
1567
1577
|
|
|
1568
|
-
// The single shared ⠿ menu (spec §3.7: 轉換成 › /
|
|
1578
|
+
// The single shared ⠿ menu (spec §3.7: 轉換成 › / 建立副本 / 刪除 / MD 原始碼)
|
|
1569
1579
|
// — built once, moved into whichever block's DOM the user opened it on,
|
|
1570
1580
|
// same pattern as `selToolbar` elsewhere in this file. `gutterMenuBlockEl`
|
|
1571
1581
|
// names which block it's currently open for. Because the node is a
|
|
@@ -1609,7 +1619,7 @@
|
|
|
1609
1619
|
openConvertSubmenu(gutterMenuConvert);
|
|
1610
1620
|
});
|
|
1611
1621
|
|
|
1612
|
-
gutterMenuDuplicate = item('
|
|
1622
|
+
gutterMenuDuplicate = item('建立副本', 'Duplicate this block', (e) => {
|
|
1613
1623
|
e.stopPropagation();
|
|
1614
1624
|
const blockEl = gutterMenuBlockEl;
|
|
1615
1625
|
closeGutterMenu();
|
|
@@ -1745,6 +1755,8 @@
|
|
|
1745
1755
|
el.className = 'ed-insert';
|
|
1746
1756
|
el.textContent = '+';
|
|
1747
1757
|
el.setAttribute('aria-label', '插入區塊');
|
|
1758
|
+
// Not a tab stop — see buildGutterHandle() above for the whole reason.
|
|
1759
|
+
el.setAttribute('tabindex', '-1');
|
|
1748
1760
|
return el;
|
|
1749
1761
|
}
|
|
1750
1762
|
|
|
@@ -1944,7 +1956,7 @@
|
|
|
1944
1956
|
// the ids — see captureBlockIdentity()'s comment.
|
|
1945
1957
|
const identity = captureBlockIdentity(blockEl);
|
|
1946
1958
|
// S2 Task 7: the FOURTH and last call site of the hole 轉換 (Task 2), 刪除
|
|
1947
|
-
// and
|
|
1959
|
+
// and 建立副本 (Task 6) already closed, and the one that was latent only
|
|
1948
1960
|
// because a li had no + to press. Finding 5a's delegated mousedown
|
|
1949
1961
|
// preventDefault() names '.ed-insert' as well as '.ed-handle', so the
|
|
1950
1962
|
// burst survives the press and the commit that lands inside
|
|
@@ -1987,7 +1999,7 @@
|
|
|
1987
1999
|
//
|
|
1988
2000
|
// 1. THE INSERTION POINT IS THE END OF THE ANCHOR'S SUBTREE, not the
|
|
1989
2001
|
// anchor's own last line. This is the ruling §4.3 already made for
|
|
1990
|
-
//
|
|
2002
|
+
// 建立副本 (「副本插在該 block 整棵子樹之後」), and it is what makes every
|
|
1991
2003
|
// non-list kind safe here. Measured on ['# Doc','','- alpha',
|
|
1992
2004
|
// ' - child',' - grand','']: anchored on `child`,
|
|
1993
2005
|
// commitBlockInsertion() with the 段落 skeleton yields
|
|
@@ -2004,7 +2016,7 @@
|
|
|
2004
2016
|
// measured, '# Doc\n\n- alpha\n - child\n\n -\n' has a NESTED
|
|
2005
2017
|
// list with loose === true, so every item of it grows a <p>,
|
|
2006
2018
|
// serializeBlocks() pushes 'P' for each and the run degrades read-only
|
|
2007
|
-
// with no banner. Same fork
|
|
2019
|
+
// with no banner. Same fork 建立副本 hit in Task 6, and the same answer:
|
|
2008
2020
|
// route the li through its own run's re-serialization, which emits no
|
|
2009
2021
|
// blank at all, re-runs §3.8's renumbering, and — the point of carry 2
|
|
2010
2022
|
// — takes the new item's indent prefix from the serializer's own
|
|
@@ -2183,7 +2195,7 @@
|
|
|
2183
2195
|
if (!rec) { showBanner(DROPPED_GESTURE_MESSAGE, null, null); return; }
|
|
2184
2196
|
const kind = liveBlockEl.getAttribute('data-block-type');
|
|
2185
2197
|
|
|
2186
|
-
// §4.3's run-wide gate:
|
|
2198
|
+
// §4.3's run-wide gate: 轉換/建立副本/刪除/拖曳 all pass through
|
|
2187
2199
|
// listRunSupportsStructuralEdit() BEFORE any mutation, the same door
|
|
2188
2200
|
// Tab/Enter/checkbox already use. Its input is §3.4 rule 2's SCOPE, which
|
|
2189
2201
|
// is exactly what listRunOf() returns (the outermost run PLUS every
|
|
@@ -2272,7 +2284,7 @@
|
|
|
2272
2284
|
return document.querySelector('.ed-block[data-block-id="' + at.id + '"]');
|
|
2273
2285
|
}
|
|
2274
2286
|
|
|
2275
|
-
// S2 Task 6 —
|
|
2287
|
+
// S2 Task 6 — 建立副本 (§4.3).
|
|
2276
2288
|
//
|
|
2277
2289
|
// The copy is inserted after the block's ENTIRE SUBTREE, never after its own
|
|
2278
2290
|
// line. The spec records the measurement and it reproduces here:
|
|
@@ -2348,16 +2360,16 @@
|
|
|
2348
2360
|
}
|
|
2349
2361
|
}
|
|
2350
2362
|
|
|
2351
|
-
// The li half of
|
|
2363
|
+
// The li half of 建立副本. The copy is spliced into the run's own span and the
|
|
2352
2364
|
// WHOLE span is re-serialized over the run's line range — one commitRangeEdit,
|
|
2353
|
-
// therefore one undo op (§4.3:
|
|
2365
|
+
// therefore one undo op (§4.3: 建立副本與刪除均為單一 undo), no leading blank, and
|
|
2354
2366
|
// §3.8's renumbering falls out of the re-serialization ('1. alpha' duplicated
|
|
2355
2367
|
// gives '1. alpha / 2. alpha / 3. bravo', not '1. alpha / 1. alpha / 2.
|
|
2356
2368
|
// bravo').
|
|
2357
2369
|
async function duplicateListItem(liEl) {
|
|
2358
2370
|
const run = listRunOf(liEl);
|
|
2359
2371
|
if (!run.length) return;
|
|
2360
|
-
// §4.3's run-wide gate —
|
|
2372
|
+
// §4.3's run-wide gate — 轉換/建立副本/刪除/拖曳 each make this call for
|
|
2361
2373
|
// themselves; there is no shared helper. Its input is §3.4 rule 2's scope,
|
|
2362
2374
|
// which is exactly what listRunOf() returns (the outermost run PLUS every
|
|
2363
2375
|
// descendant of its members). A duplicate is NOT column-only (§4.1 修訂 2:
|
|
@@ -3773,7 +3785,24 @@
|
|
|
3773
3785
|
// wireBurstListeners() below), Shift+Enter inserts a <br> and snapshots
|
|
3774
3786
|
// it, Escape reverts, Ctrl+Z/Y drive the burst-local history.
|
|
3775
3787
|
function handleBurstKeydown(e, editEl) {
|
|
3776
|
-
if (!currentBurst || currentBurst.editEl !== editEl)
|
|
3788
|
+
if (!currentBurst || currentBurst.editEl !== editEl) {
|
|
3789
|
+
// v2.11.1 acceptance, escape class B. This bail is reachable with the
|
|
3790
|
+
// surface STILL FOCUSED and still `.ed-wys-armed`: resolveBurst() nulls
|
|
3791
|
+
// `currentBurst` without blurring (Ctrl+S is the everyday way in), and
|
|
3792
|
+
// the delegated handler's call site below `return`s unconditionally, so
|
|
3793
|
+
// nothing else in the document handler runs either. For every other key
|
|
3794
|
+
// that is the right answer — the surface is a plain contenteditable and
|
|
3795
|
+
// the browser's default IS the behaviour we want. Tab is the one key
|
|
3796
|
+
// whose default is not "insert something" but "walk the caret out of the
|
|
3797
|
+
// document": measured on 2.11.0 it moved focus to that same item's own +
|
|
3798
|
+
// button (Shift+Tab, to the previous block's ⠿). Spec §3.5 names this
|
|
3799
|
+
// outright — 必須 preventDefault(),否則 Tab 在 body 上是瀏覽器焦點巡覽.
|
|
3800
|
+
// Swallowed, not acted on: there is no burst to act within, and an
|
|
3801
|
+
// indent from a resolved burst would be a structural edit the user did
|
|
3802
|
+
// not ask for.
|
|
3803
|
+
if (e.key === 'Tab') e.preventDefault();
|
|
3804
|
+
return;
|
|
3805
|
+
}
|
|
3777
3806
|
// Task 8 (Phase 4): per-li burst — Enter / Shift+Enter / Tab / Shift+Tab
|
|
3778
3807
|
// are owned by handleLiKeydown() below (spec §4's key semantics for li
|
|
3779
3808
|
// surfaces, acceptance rows 1, 3, 5, 6, 7, 8). Every other key (Escape,
|
|
@@ -6462,6 +6491,28 @@
|
|
|
6462
6491
|
if (insertMenuBlockEl) closeInsertMenu();
|
|
6463
6492
|
if (e.target.closest(ED_LIGHTBOX_TARGETS)) return; // let the lightbox open, unchanged
|
|
6464
6493
|
let blockEl = e.target.closest('.ed-block');
|
|
6494
|
+
// v2.11.1: `.ed-block::before` (lib/md2doc.js's editModeLayoutCss) makes
|
|
6495
|
+
// the 40px gutter part of the block's HIT area so that hovering it keeps
|
|
6496
|
+
// the +/⠿ pair visible. That is a hover fix, and it must not become a
|
|
6497
|
+
// click fix by accident: before it, a click in the gutter band hit
|
|
6498
|
+
// main.content and meant "clicked outside any block" — which for a
|
|
6499
|
+
// DEGRADED block (blockquote, fenced code, an unsupported table) is the
|
|
6500
|
+
// difference between committing whatever was open and silently opening
|
|
6501
|
+
// that block's raw source editor from 20px away from it.
|
|
6502
|
+
//
|
|
6503
|
+
// Read only when the click landed on the block's OWN box (a click on any
|
|
6504
|
+
// descendant — the text surface, a marker, a checkbox, a gutter button —
|
|
6505
|
+
// is unaffected) and only when the event actually carries coordinates:
|
|
6506
|
+
// a synthesized `new MouseEvent('click', {bubbles:true})` and
|
|
6507
|
+
// `el.click()` both report clientX/clientY 0, which several scenarios in
|
|
6508
|
+
// test/editor-client-runtime.test.js use precisely because they mean
|
|
6509
|
+
// "the block itself", not "a point". `offsetX < 0` looks like the
|
|
6510
|
+
// tidier test and is NOT usable: for a synthesized event Chromium still
|
|
6511
|
+
// derives offsetX from clientX 0, so it comes back as minus the block's
|
|
6512
|
+
// whole left offset and every such click reads as a gutter click.
|
|
6513
|
+
if (blockEl && e.target === blockEl && (e.clientX || e.clientY)) {
|
|
6514
|
+
if (e.clientX < blockEl.getBoundingClientRect().left) blockEl = null;
|
|
6515
|
+
}
|
|
6465
6516
|
if (!blockEl) { await switchAwayFrom(); return; } // clicked outside any block
|
|
6466
6517
|
|
|
6467
6518
|
// Task 5: a table block is now armed exactly like paragraph/heading/
|
|
@@ -6638,6 +6689,44 @@
|
|
|
6638
6689
|
return;
|
|
6639
6690
|
}
|
|
6640
6691
|
|
|
6692
|
+
// v2.11.1 acceptance, escape class A: Tab with NOTHING focused. Every
|
|
6693
|
+
// branch above is keyed on the event target being some edit surface, and
|
|
6694
|
+
// after a commit / Escape / Ctrl+Z, or a click on a bullet marker or in
|
|
6695
|
+
// the block's own gutter, focus is on BODY and the target is BODY — so no
|
|
6696
|
+
// branch matched and the browser ran its own sequential focus navigation,
|
|
6697
|
+
// landing on whichever gutter <button> happens to come next in document
|
|
6698
|
+
// order. Spec §3.5: 必須 preventDefault(),否則 Tab 在 body 上是瀏覽器焦點
|
|
6699
|
+
// 巡覽. This is deliberately a silent no-op rather than "indent the block
|
|
6700
|
+
// nearest the caret": with no focus there is no caret, so there is no
|
|
6701
|
+
// block the key could mean.
|
|
6702
|
+
//
|
|
6703
|
+
// Scoped so a real control keeps its keyboard contract: the reader's own
|
|
6704
|
+
// search input and the raw editor's textarea (which returned above) are
|
|
6705
|
+
// still tabbable, and so is anything else the user has deliberately
|
|
6706
|
+
// focused. What is swallowed is Tab from inside a `.ed-block` and Tab with
|
|
6707
|
+
// no focus at all — the two states the editor puts the user in.
|
|
6708
|
+
if (e.key === 'Tab') {
|
|
6709
|
+
const inBlock = e.target && e.target.closest && e.target.closest('.ed-block');
|
|
6710
|
+
// A REAL control inside a block keeps its keyboard contract. The raw
|
|
6711
|
+
// source editor's own 完成/取消 buttons are the case that matters: Tab
|
|
6712
|
+
// out of its textarea is how a keyboard user reaches them (the textarea
|
|
6713
|
+
// itself returned above), and swallowing the next Tab would trap focus
|
|
6714
|
+
// on the button it just landed on. The ⠿ menu's buttons are the same
|
|
6715
|
+
// shape. The two GUTTER buttons are excluded from that exemption on
|
|
6716
|
+
// purpose — they are the chrome this fix exists to keep out of the tab
|
|
6717
|
+
// order, and buildGutterHandle()/buildGutterInsertButton() give them
|
|
6718
|
+
// tabindex="-1" for the same reason.
|
|
6719
|
+
const control = e.target && e.target.closest && e.target.closest(
|
|
6720
|
+
'button:not(.ed-handle):not(.ed-insert), input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])');
|
|
6721
|
+
const focused = document.activeElement;
|
|
6722
|
+
const nothingFocused = !focused || focused === document.body ||
|
|
6723
|
+
focused === document.documentElement;
|
|
6724
|
+
if ((inBlock && !control) || nothingFocused) {
|
|
6725
|
+
e.preventDefault();
|
|
6726
|
+
return;
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
|
|
6641
6730
|
if (e.key === 'Escape') {
|
|
6642
6731
|
e.preventDefault();
|
|
6643
6732
|
closeGutterMenu();
|
package/lib/editor/list-md.js
CHANGED
|
@@ -377,6 +377,61 @@
|
|
|
377
377
|
return n > 0 ? new Array(n + 1).join(' ') : '';
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
// ── The setext-underline hazard on an EMPTY bulleted item ───────────────
|
|
381
|
+
// An empty item is emitted as a BARE marker ('-', no trailing space): '- '
|
|
382
|
+
// lexes as a PARAGRAPH, which is why lib/editor/client.js's
|
|
383
|
+
// BLOCK_SKELETONS.list is a bare marker too, and that reasoning is not being
|
|
384
|
+
// undone here. But CommonMark gives that same bare '-' a SECOND reading, and
|
|
385
|
+
// which one wins is decided by the line ABOVE it: an EMPTY list item may not
|
|
386
|
+
// interrupt a paragraph, and a line consisting of nothing but '-' standing
|
|
387
|
+
// at an open paragraph's own content column is a SETEXT H2 UNDERLINE.
|
|
388
|
+
//
|
|
389
|
+
// That is exactly the line an indent produces. `- beta` + Enter + Tab wrote
|
|
390
|
+
//
|
|
391
|
+
// - alpha
|
|
392
|
+
// - beta
|
|
393
|
+
// -
|
|
394
|
+
//
|
|
395
|
+
// and marked (14.1.4) reads it back as `<li><h2>beta</h2></li>`: the new
|
|
396
|
+
// item is gone and the parent's text has been re-typed as a heading. Two
|
|
397
|
+
// ordinary keystrokes, silent content destruction, measured on 2.11.0.
|
|
398
|
+
//
|
|
399
|
+
// The hazard is POSITIONAL, not a property of the marker. It exists only
|
|
400
|
+
// where the previously emitted line is a paragraph at this line's own
|
|
401
|
+
// column, which inside a run means precisely "this item is the FIRST item of
|
|
402
|
+
// a deeper nesting" (`prev.indent < indent`) — the parent's own text (or its
|
|
403
|
+
// lazy continuation) is then the line immediately above, and a child's
|
|
404
|
+
// marker column IS the parent's content column by construction. An empty
|
|
405
|
+
// item that follows a SAME-level sibling is safe (the line above is a marker
|
|
406
|
+
// line, so '-' can only be another marker there), and so is one whose
|
|
407
|
+
// predecessor is deeper.
|
|
408
|
+
//
|
|
409
|
+
// Every other shape is left byte-identical:
|
|
410
|
+
// * ordered — '1.' is not a run of dashes, so it is not a setext underline;
|
|
411
|
+
// * task — a content-free task item never emits a marker line at all
|
|
412
|
+
// (it becomes `pending`, a same-line prefix), which is what the
|
|
413
|
+
// `head === indentPrefix + marker` test below detects;
|
|
414
|
+
// * non-empty items, and every empty item at top level.
|
|
415
|
+
//
|
|
416
|
+
// The escape is a U+200B ZERO WIDTH SPACE: real, non-whitespace content to
|
|
417
|
+
// the block lexer (so the line is a list item, not an underline) and nothing
|
|
418
|
+
// at all to a reader. It is the same trade-off client.js already documents
|
|
419
|
+
// for BLOCK_SKELETONS.paragraph. It never becomes part of the user's text:
|
|
420
|
+
// lib/md2doc.js's edit-mode list renderer renders a U+200B-only item as an
|
|
421
|
+
// EMPTY surface, so the next keystroke lands in an empty item, and the
|
|
422
|
+
// itemMd normalisation above takes the character back off on the way out.
|
|
423
|
+
const SETEXT_ESCAPE = '\u200b';
|
|
424
|
+
const SETEXT_ESCAPE_RE = /^\u200b+$/;
|
|
425
|
+
function escapeSetextHazard(firstOwnLine, ctx) {
|
|
426
|
+
if (firstOwnLine !== '') return firstOwnLine;
|
|
427
|
+
if (ctx.listType !== 'ul' || ctx.isTask) return firstOwnLine;
|
|
428
|
+
// A `pending` task prefix has already been joined onto `head`, so the line
|
|
429
|
+
// is not a bare run of dashes and needs nothing.
|
|
430
|
+
if (ctx.head !== ctx.indentPrefix + ctx.marker) return firstOwnLine;
|
|
431
|
+
if (!ctx.prev || ctx.indent <= ctx.prev.indent) return firstOwnLine;
|
|
432
|
+
return SETEXT_ESCAPE;
|
|
433
|
+
}
|
|
434
|
+
|
|
380
435
|
// `opts.carryOver` (spec §3.4, 多行 li 的旁觀者規則): a map of block id →
|
|
381
436
|
// that block's ORIGINAL source lines. A hard-wrapped item named there is NOT
|
|
382
437
|
// re-serialized; its own bytes are replayed with the column difference of its
|
|
@@ -536,6 +591,15 @@
|
|
|
536
591
|
const res = inlineMd.serializeInline({ childNodes: inlineKids });
|
|
537
592
|
itemMd = res.md;
|
|
538
593
|
res.unsupported.forEach((u) => innerUnsupported.push(u));
|
|
594
|
+
// The other half of the SETEXT ESCAPE applied at the emission site
|
|
595
|
+
// below: a U+200B is this serializer's own way of saying "empty item
|
|
596
|
+
// in a position where a bare marker would re-lex as a heading
|
|
597
|
+
// underline", so this serializer is also the one that takes it back
|
|
598
|
+
// off. Without it, an escaped item that later moves somewhere the
|
|
599
|
+
// escape is not needed (Shift+Tab back to the top level) would keep a
|
|
600
|
+
// zero-width character it never asked for, and the item would stop
|
|
601
|
+
// reading as empty to every `itemMd === ''` test in this function.
|
|
602
|
+
if (SETEXT_ESCAPE_RE.test(itemMd)) itemMd = '';
|
|
539
603
|
}
|
|
540
604
|
|
|
541
605
|
// Anything in the block that is neither chrome nor the text surface is
|
|
@@ -717,7 +781,10 @@
|
|
|
717
781
|
prev = { indent: indent, listType: listType };
|
|
718
782
|
return;
|
|
719
783
|
}
|
|
720
|
-
lines.push((head + ownLines[0]
|
|
784
|
+
lines.push((head + escapeSetextHazard(ownLines[0], {
|
|
785
|
+
head: head, indentPrefix: indentPrefix, marker: marker,
|
|
786
|
+
listType: listType, isTask: isTask, indent: indent, prev: prev,
|
|
787
|
+
})).replace(/[ \t]+$/, ''));
|
|
721
788
|
lineMeta.push({ blockId: blockId, indentPrefix: metaPrefix, marker: marker });
|
|
722
789
|
for (let k = 1; k < ownLines.length; k++) {
|
|
723
790
|
const body = ownLines[k].replace(/^[ \t]+/, '').replace(/[ \t]+$/, '');
|
package/lib/md2doc.js
CHANGED
|
@@ -284,6 +284,15 @@ function renderEditModeList(listToken, blocks, biRef, out) {
|
|
|
284
284
|
// but would render as a phantom blank line inside the item.
|
|
285
285
|
inner = marked.parser(ownTokens).trim();
|
|
286
286
|
}
|
|
287
|
+
// The receiving half of list-md.js's SETEXT ESCAPE (see
|
|
288
|
+
// escapeSetextHazard() there): an empty list item that is the first child
|
|
289
|
+
// of a deeper nesting is written to disk as `- <U+200B>`, because a bare
|
|
290
|
+
// `-` on that line is a setext H2 underline for the parent's own text and
|
|
291
|
+
// destroys both blocks. The zero-width space is the serializer's, not the
|
|
292
|
+
// user's, so it is taken back off here — the surface renders EMPTY, which
|
|
293
|
+
// is what keeps the next keystroke from landing next to an invisible
|
|
294
|
+
// character and writing it back out inside the user's own text.
|
|
295
|
+
if (/^\u200b+$/.test(inner)) inner = '';
|
|
287
296
|
const check = b.task
|
|
288
297
|
? `<span class="ed-li-check" data-checked="${b.checked ? 1 : 0}" role="checkbox" aria-checked="${!!b.checked}"></span>`
|
|
289
298
|
: '';
|
|
@@ -1078,8 +1087,9 @@ ${itemsHtml}
|
|
|
1078
1087
|
// (now reverted) "inset the row grip into the table" hack that ended up
|
|
1079
1088
|
// covering the first cell's text. 56px of content padding plus moving the
|
|
1080
1089
|
// gutter buttons out (⠿ to left:-36px, + to left:-54px) separates them
|
|
1081
|
-
// properly: the gutter pair occupies [contentLeft-
|
|
1082
|
-
//
|
|
1090
|
+
// properly: the gutter pair occupies [contentLeft-40, contentLeft-4]
|
|
1091
|
+
// (v2.11.1; it was [contentLeft-54, contentLeft-18] until spec §4.2's own
|
|
1092
|
+
// numbers were restored) and the 20px-wide row grip straddles contentLeft at
|
|
1083
1093
|
// [contentLeft-10, contentLeft+10] — an 8px gap, and the grip's inner half
|
|
1084
1094
|
// stays within the cell's own 14px padding, so it never touches cell text.
|
|
1085
1095
|
// (The padding was 48px until §4.2's hit-test conflict 1 was fixed; see the
|
|
@@ -1133,7 +1143,11 @@ ${itemsHtml}
|
|
|
1133
1143
|
|
|
1134
1144
|
8 more pixels of padding move the pair to [contentLeft-54,
|
|
1135
1145
|
contentLeft-18] = [splitterRight+2, splitterRight+20], i.e. entirely
|
|
1136
|
-
inside the content column with 2px of clearance.
|
|
1146
|
+
inside the content column with 2px of clearance. (v2.11.1 pulled the pair
|
|
1147
|
+
further in, to spec §4.2's own [contentLeft-40, contentLeft-4] — the
|
|
1148
|
+
clearance is 16px at this padding rather than 2px. The padding stays at
|
|
1149
|
+
56px: nothing asks for the text column to move, and this measurement is
|
|
1150
|
+
the one the §4.2 conflict-1 guard is written against.) The 20px table row grip
|
|
1137
1151
|
still straddles contentLeft at [contentLeft-10, contentLeft+10], so its
|
|
1138
1152
|
8px gap to ⠿ is unchanged. Guarded by the elementFromPoint(splitter.right
|
|
1139
1153
|
- 2, y) assertion §4.2 asks for, in
|
|
@@ -1210,8 +1224,66 @@ ${itemsHtml}
|
|
|
1210
1224
|
reaches a negative viewport x and never covers the splitter. The 20px
|
|
1211
1225
|
table row grip still straddles contentLeft at
|
|
1212
1226
|
[contentLeft-10, contentLeft+10] - an 8px gap, unchanged. */
|
|
1213
|
-
.
|
|
1214
|
-
|
|
1227
|
+
/* Stated ONCE, as tokens, because §4.2's three numbers are not independent:
|
|
1228
|
+
the gutter is exactly two buttons wide plus the deliberate right-hand
|
|
1229
|
+
breathing gap, and the hover zone below is exactly the gutter. Writing any
|
|
1230
|
+
of them as a second literal is how the corridor comes back. */
|
|
1231
|
+
:root {
|
|
1232
|
+
--ed-gutter-btn: 18px;
|
|
1233
|
+
--ed-gutter-gap: 4px;
|
|
1234
|
+
--ed-gutter-w: calc(var(--ed-gutter-btn) * 2 + var(--ed-gutter-gap));
|
|
1235
|
+
}
|
|
1236
|
+
.ed-handle {
|
|
1237
|
+
left: calc(-1 * (var(--ed-gutter-btn) + var(--ed-gutter-gap))); top: 0;
|
|
1238
|
+
width: var(--ed-gutter-btn);
|
|
1239
|
+
}
|
|
1240
|
+
.ed-insert {
|
|
1241
|
+
left: calc(-1 * var(--ed-gutter-w)); top: 0;
|
|
1242
|
+
width: var(--ed-gutter-btn);
|
|
1243
|
+
}
|
|
1244
|
+
/* v2.11.1: the gutter's HOVER ZONE, and it is the reason the pair could move
|
|
1245
|
+
back to spec §4.2's own numbers at all.
|
|
1246
|
+
|
|
1247
|
+
Both buttons are revealed by .ed-block:hover, and :hover is true only
|
|
1248
|
+
over the block's border box or over one of the buttons themselves. At
|
|
1249
|
+
-36/-54 that left the band [blockLeft-18, blockLeft) belonging to neither:
|
|
1250
|
+
measured at 1400x900, elementFromPoint() returned main.content for
|
|
1251
|
+
x 394..411, and a real pointer walking out of the text at 100 px/s held
|
|
1252
|
+
the ⠿ at opacity 0 for 16 consecutive frames (~270 ms) — the cursor is
|
|
1253
|
+
between the text and the ⠿ and the ⠿ is not there. Conforming to §4.2
|
|
1254
|
+
([contentLeft-40, contentLeft-4], the + and ⠿ flush, 4px of breathing
|
|
1255
|
+
room on the right) narrows that band to the 4px gap but does not close it:
|
|
1256
|
+
with the pair moved and this rule taken back out, a 2px-per-frame walk
|
|
1257
|
+
across the same row measured 0.06 / 0.49 / 0.53 at three of its 23 stops —
|
|
1258
|
+
a flicker instead of a disappearance, but still the ⠿ dimming under the
|
|
1259
|
+
cursor that is travelling to it. Geometry alone cannot close it, because §4.2's 4px gap is
|
|
1260
|
+
deliberate (5.3 item 3a's elementFromPoint must land ON the ⠿).
|
|
1261
|
+
|
|
1262
|
+
Two more holes have the same shape and the same cure: the buttons are 20px
|
|
1263
|
+
tall at top:0 while an li row is 24.75px, so the bottom ~4.75px of EVERY
|
|
1264
|
+
row is an empty gutter; and a 61.5px heading's vertical centre is 20px
|
|
1265
|
+
below the bottom of its own ⠿, so moving left from the middle of a heading
|
|
1266
|
+
never reached anything at all.
|
|
1267
|
+
|
|
1268
|
+
One absolutely-positioned pseudo-element spanning the whole gutter for the
|
|
1269
|
+
block's whole height makes :hover continuously true from the text out
|
|
1270
|
+
past the +, at every Y. Its width is the --ed-gutter-w token the pair's
|
|
1271
|
+
own offsets are built from, so the two cannot drift apart — if they do,
|
|
1272
|
+
the corridor comes straight back.
|
|
1273
|
+
.ed-block is already position:relative, and position: absolute keeps
|
|
1274
|
+
this out of the li row's flex flow. It is deliberately NOT
|
|
1275
|
+
pointer-events: none: that would stop it being hit-tested, which is the
|
|
1276
|
+
entire mechanism. It therefore also changes what a click in the band
|
|
1277
|
+
hits (main.content -> the block), which wireBlockSelection() in
|
|
1278
|
+
lib/editor/client.js compensates for explicitly — see the
|
|
1279
|
+
clientX-vs-block-rect guard just above its "clicked outside any block"
|
|
1280
|
+
branch. (No backticks in this comment: it lives inside a JS template
|
|
1281
|
+
literal.) */
|
|
1282
|
+
.ed-block::before {
|
|
1283
|
+
content: ""; position: absolute;
|
|
1284
|
+
left: calc(-1 * var(--ed-gutter-w)); top: 0;
|
|
1285
|
+
width: var(--ed-gutter-w); height: 100%;
|
|
1286
|
+
}`;
|
|
1215
1287
|
|
|
1216
1288
|
const html = `<!DOCTYPE html>
|
|
1217
1289
|
<html lang="en">
|
|
@@ -2039,7 +2111,7 @@ ${itemsHtml}
|
|
|
2039
2111
|
.ed-block:hover .ed-handle,
|
|
2040
2112
|
.ed-handle:focus { opacity: 1; }
|
|
2041
2113
|
.ed-handle:hover { background: rgba(0, 0, 0, 0.08); }
|
|
2042
|
-
/* The ⠿ handle's menu: 轉換成 › /
|
|
2114
|
+
/* The ⠿ handle's menu: 轉換成 › / 建立副本 / 刪除 / MD 原始碼 (spec §3.7).
|
|
2043
2115
|
Dark translucent panel, bordered rows — same visual language as
|
|
2044
2116
|
.ed-seltb below.
|
|
2045
2117
|
|
|
@@ -2086,7 +2158,9 @@ ${itemsHtml}
|
|
|
2086
2158
|
wording allows this ("left gutter, above or beside it").
|
|
2087
2159
|
|
|
2088
2160
|
S1 Task 5 (D6): edit mode now overrides BOTH buttons to sit side by side
|
|
2089
|
-
(+ at left:-
|
|
2161
|
+
(v2.11.1, spec §4.2's own numbers: + at left:-40px, ⠿ at left:-22px,
|
|
2162
|
+
both top:0, plus a .ed-block::before hover zone spanning the pair) — see
|
|
2163
|
+
editModeLayoutCss.
|
|
2090
2164
|
That became possible only once .content gained 48px of edit-mode padding;
|
|
2091
2165
|
the stacked geometry declared here is what any NON-edit render would use,
|
|
2092
2166
|
and those never emit .ed-block at all, so it is inert there. Kept rather
|