fix: the whole margin band opens the header/footer
A double-click only reached the zone box (~28px of a 76px band), so the part nearest the page edge did nothing; a hit rect now covers the band edge to edge. The trailing-break discount no longer applies to an empty zone, where that break is the caret line itself — the placeholder sat a line below the typed text and jumped up on the first keystroke.
This commit is contained in:
1 parent
5c0ac978f4
commit
e2ef65d8cd
2 files changed
+45
-2
No files matched your search
@@ -155,6 +155,19 @@
|
||||
if (zone === 'footer') return { ...b, top: b.top + b.height - height, height };
|
||||
return { ...b, height };
|
||||
}
|
||||
// Double-click target: the whole margin band the zone sits in, edge to edge, not just
|
||||
// the zone box — the band above the body is the header's, as it is in LibreOffice.
|
||||
function hitBox(zone: HfZone, page: number) {
|
||||
const box = boxOf(page);
|
||||
const z = zoneBox(zone, page);
|
||||
const m = marginsOf(page);
|
||||
if (zone === 'header') {
|
||||
const bottom = Math.max(box.top + cmToPx(m.top), z.top + z.height);
|
||||
return { top: box.top, left: box.left, width: box.width, height: bottom - box.top };
|
||||
}
|
||||
const top = Math.min(box.top + box.height - cmToPx(m.bottom), z.top);
|
||||
return { top, left: box.left, width: box.width, height: box.top + box.height - top };
|
||||
}
|
||||
const boxStyle = (b: { top: number; left: number; width: number; height: number }) =>
|
||||
`top: ${b.top}px; left: ${b.left}px; width: ${b.width}px; height: ${b.height}px;`;
|
||||
|
||||
@@ -431,7 +444,9 @@
|
||||
// A rendered trailing break adds one caret line past the real content; measure that
|
||||
// line height (the CSS var below shifts the editor down so it overflows the anchor).
|
||||
const p = tt?.querySelector('p') as HTMLElement | null;
|
||||
const tb = p?.querySelector(':scope > br.ProseMirror-trailingBreak') as HTMLElement | null;
|
||||
// Only past real content: in an empty zone that break is the caret line itself, and
|
||||
// discounting it would drop the placeholder a line below the anchored edge.
|
||||
const tb = p && p.childNodes.length > 1 ? (p.querySelector(':scope > br.ProseMirror-trailingBreak') as HTMLElement | null) : null;
|
||||
const lineH = p ? parseFloat(getComputedStyle(p).lineHeight) : 0;
|
||||
activeTrailingPx = tb && getComputedStyle(tb).display !== 'none' && Number.isFinite(lineH) ? lineH : 0;
|
||||
});
|
||||
@@ -473,6 +488,9 @@
|
||||
<div class="hf-layer">
|
||||
{#each pages as p}
|
||||
{#each ['header', 'footer'] as const as zone}
|
||||
{#if interactive}
|
||||
<div class="hf-hit" style={boxStyle(hitBox(zone, p))} ondblclick={() => startEdit(zone, p)} role="button" tabindex="-1"></div>
|
||||
{/if}
|
||||
{#if !(interactive && hfActive === zone && editingPage === p)}
|
||||
{@const html = zoneHtml(zone, p)}
|
||||
{@const total = html.includes('data-page-field="count"') ? numPages : 0}
|
||||
@@ -584,6 +602,13 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* The margin band around a zone, double-clickable like the zone itself. Painted
|
||||
before it, so the zone keeps the events inside its own box. */
|
||||
.hf-hit {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Empty zone: invisible until hovered, then show a faint double-click hint. */
|
||||
.hf-empty::before {
|
||||
content: attr(data-hf-label);
|
||||
@@ -596,7 +621,8 @@
|
||||
.hf-footer.hf-empty::before {
|
||||
margin-top: auto;
|
||||
}
|
||||
.hf-empty:hover::before {
|
||||
.hf-empty:hover::before,
|
||||
.hf-hit:hover + .hf-zone.hf-empty::before {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,23 @@ try {
|
||||
const isDocx = saved[0] === 0x50 && saved[1] === 0x4b && saved.includes('word/document.xml');
|
||||
check(isDocx, `Save As (.docx) without a picker downloads a DOCX (${download.suggestedFilename()}, ${saved.length} bytes)`);
|
||||
|
||||
// The whole margin band is the zone's double-click target, and an empty zone's
|
||||
// placeholder sits where the first typed character lands — not a line below it.
|
||||
await page.keyboard.press('Escape');
|
||||
const hit = page.locator('.hf-hit').nth(1);
|
||||
const hitH = (await hit.boundingBox()).height;
|
||||
await hit.dblclick({ position: { x: 8, y: hitH - 4 } });
|
||||
await page.waitForSelector('.hf-active.hf-footer .tiptap', { timeout: 5000 });
|
||||
check(true, 'a double-click in the bottom-left page corner opens the footer');
|
||||
const lineTop = () => page.evaluate(() => document.querySelector('.hf-active .tiptap p').getBoundingClientRect().top);
|
||||
const emptyTop = await lineTop();
|
||||
await page.keyboard.type('x');
|
||||
const typedTop = await lineTop();
|
||||
check(Math.abs(emptyTop - typedTop) < 1,
|
||||
`the footer placeholder sits on the typed line (${emptyTop.toFixed(1)} → ${typedTop.toFixed(1)})`);
|
||||
await page.keyboard.press('Backspace');
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// The header/footer switches live in the ribbon's Insert tab. A ticked "different
|
||||
// first page" makes page 1 a second zone, so what is typed there lands beside the
|
||||
// running one — and the distance travels to the app's own storage.
|
||||
|
||||
Reference in new issue
Block a user