feat: a later section's header and footer can be edited
The zones already imported, rendered and exported per section, but only section 1 was editable state — the rest were marked read-only. Editing now targets the section of the page it starts on: a double-click edits the zone under the pointer, the Layout-panel buttons the current page's. zoneKey/writeZone route the single live editor's read and its write-back to either the six per-zone props or the extraHfSections entry.
This commit is contained in:
1 parent
9d85ddb213
commit
aad4e7a3e4
8 files changed
+54
-45
No files matched your search
+3
-2
@@ -70,7 +70,8 @@
|
||||
let footerEvenDoc: HfDoc = $state(loadHfDoc('footer', 'even'));
|
||||
let differentOddEven: boolean = $state(loadDifferentOddEven());
|
||||
let hfDistances: HfDistances = $state(loadHfDistances());
|
||||
// Sections past the first: imported and exported, not editable (see HeaderFooterLayer).
|
||||
// Sections past the first; the layer edits them in place, section 1 stays the
|
||||
// per-zone state above.
|
||||
let extraHfSections: HfSet[] = $state(loadExtraHfSections());
|
||||
let hfEditor: Editor | null = $state(null);
|
||||
let hfActive: HfZone | null = $state(null);
|
||||
@@ -1045,7 +1046,7 @@
|
||||
bind:hfTick
|
||||
{hfDistances}
|
||||
{tabIntervalCm}
|
||||
{extraHfSections}
|
||||
bind:extraHfSections
|
||||
{zoom}
|
||||
onZoom={setZoom}
|
||||
{showFormattingMarks}
|
||||
|
||||
@@ -57,8 +57,11 @@ In dev builds a **Debug** button (`App.svelte`) downloads a JSON snapshot combin
|
||||
|
||||
A body block carrying `sectionBreak` (pageBreak.ts) opens a section; `HeaderFooterLayer`
|
||||
picks that section's `HfSet` per page from `sectionStartPages`, which `pageBreaks.ts`
|
||||
reports on `pm-pagecount`. Section 1 is the app's own editable state, the rest ride along
|
||||
from the file (`extraHfSections`, persisted whole) and are read-only.
|
||||
reports on `pm-pagecount`. Section 1 is the app's six per-zone states, the rest live in
|
||||
`extraHfSections` (bound up to `App`, persisted whole). Editing targets the section of the
|
||||
page it starts on, so a double-click edits the zone under the pointer and the Layout-panel
|
||||
buttons the current page's; `zoneKey`/`writeZone` route the one live editor's read and
|
||||
write-back to either side. Page geometry is still document-wide.
|
||||
|
||||
Word's "different first page" is per section, so a later section's first page shows its
|
||||
own variant. `Editor.svelte` publishes each section's zone reaches as `--pb-section-reach`
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
headerFirstDoc = $bindable(null), footerFirstDoc = $bindable(null), differentFirstPage = false,
|
||||
headerEvenDoc = $bindable(null), footerEvenDoc = $bindable(null), differentOddEven = false,
|
||||
hfEditor = $bindable(null), hfActive = $bindable(null), hfTick = $bindable(0),
|
||||
extraHfSections = [],
|
||||
extraHfSections = $bindable([]),
|
||||
}: {
|
||||
editor: Editor | null; tick: number; currentPage: number; numPages: number; zoom: number;
|
||||
onZoom?: (zoom: number) => void;
|
||||
@@ -853,7 +853,7 @@
|
||||
{orientation}
|
||||
{pageFormat}
|
||||
{hfDistances}
|
||||
{extraHfSections}
|
||||
bind:extraHfSections
|
||||
{sectionStartPages}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
hfEditor = $bindable(),
|
||||
hfActive = $bindable(),
|
||||
hfTick = $bindable(),
|
||||
extraHfSections = [],
|
||||
extraHfSections = $bindable([]),
|
||||
sectionStartPages = [],
|
||||
}: {
|
||||
headerDoc: HfDoc;
|
||||
@@ -114,7 +114,7 @@
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// Section 1 is the app's own editable state; the rest ride along from the file.
|
||||
// Section 1 is the app's own editable state; the rest live in extraHfSections.
|
||||
let sets = $derived<HfSet[]>([
|
||||
{
|
||||
header: headerDoc, footer: footerDoc,
|
||||
@@ -192,11 +192,26 @@
|
||||
let pendingPage: number | null = null;
|
||||
let liveZone: HfZone | null = null;
|
||||
|
||||
// The doc for a zone + which variant is being edited.
|
||||
function zoneDoc(zone: HfZone, variant: HfVariant): HfDoc {
|
||||
if (variant === 'first') return zone === 'header' ? headerFirstDoc : footerFirstDoc;
|
||||
if (variant === 'even') return zone === 'header' ? headerEvenDoc : footerEvenDoc;
|
||||
return zone === 'header' ? headerDoc : footerDoc;
|
||||
// Which HfSet field a zone + variant is.
|
||||
type ZoneKey = 'header' | 'footer' | 'headerFirst' | 'footerFirst' | 'headerEven' | 'footerEven';
|
||||
const zoneKey = (zone: HfZone, variant: HfVariant): ZoneKey =>
|
||||
(zone + (variant === 'first' ? 'First' : variant === 'even' ? 'Even' : '')) as ZoneKey;
|
||||
|
||||
function zoneDoc(index: number, zone: HfZone, variant: HfVariant): HfDoc {
|
||||
return (sets[index] ?? sets[0])[zoneKey(zone, variant)];
|
||||
}
|
||||
// Section 1's zones are separate bindable props; a later section's live in the array,
|
||||
// replaced whole so the reassignment reaches App (which persists it).
|
||||
function writeZone(index: number, zone: HfZone, variant: HfVariant, doc: HfDoc): void {
|
||||
const key = zoneKey(zone, variant);
|
||||
if (index > 0) {
|
||||
extraHfSections = extraHfSections.map((s, i) => (i === index - 1 ? { ...s, [key]: doc } : s));
|
||||
} else if (key === 'header') headerDoc = doc;
|
||||
else if (key === 'footer') footerDoc = doc;
|
||||
else if (key === 'headerFirst') headerFirstDoc = doc;
|
||||
else if (key === 'footerFirst') footerFirstDoc = doc;
|
||||
else if (key === 'headerEven') headerEvenDoc = doc;
|
||||
else footerEvenDoc = doc;
|
||||
}
|
||||
function emptyDoc(): HfDoc {
|
||||
return { type: 'doc', content: [{ type: 'paragraph' }] };
|
||||
@@ -229,28 +244,21 @@
|
||||
editingPage = pendingPage ?? currentPage;
|
||||
pendingPage = null;
|
||||
liveZone = zone;
|
||||
// Which variant this edit session targets — fixed for its lifetime (the edited page's
|
||||
// variant; App ends the edit when a flag toggles).
|
||||
const editingVariant = variantFor(editingPage);
|
||||
// Which section and variant this edit session targets — fixed for its lifetime (the
|
||||
// edited page's; App ends the edit when a flag toggles).
|
||||
const editingIndex = sectionOf(editingPage);
|
||||
const editingVariant = variantFor(editingPage, editingIndex);
|
||||
const ed = new Editor({
|
||||
element: mount,
|
||||
extensions: hfExtensions(zone === 'header' ? t().hf.headerPlaceholder : t().hf.footerPlaceholder),
|
||||
content: (zoneDoc(zone, editingVariant) ?? emptyDoc()) as Content,
|
||||
content: (zoneDoc(editingIndex, zone, editingVariant) ?? emptyDoc()) as Content,
|
||||
// No autofocus: its scrollIntoView nudges the page so the just-clicked zone
|
||||
// appears to jump. Focus the zone explicitly without scrolling instead.
|
||||
onTransaction: () => {
|
||||
hfTick++;
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
const json = editor.getJSON() as HfDoc;
|
||||
if (editingVariant === 'first') {
|
||||
if (zone === 'header') headerFirstDoc = json;
|
||||
else footerFirstDoc = json;
|
||||
} else if (editingVariant === 'even') {
|
||||
if (zone === 'header') headerEvenDoc = json;
|
||||
else footerEvenDoc = json;
|
||||
} else if (zone === 'header') headerDoc = json;
|
||||
else footerDoc = json;
|
||||
writeZone(editingIndex, zone, editingVariant, editor.getJSON() as HfDoc);
|
||||
},
|
||||
editorProps: {
|
||||
handleKeyDown: (_view, event) => {
|
||||
@@ -296,14 +304,12 @@
|
||||
{#each ['header', 'footer'] as const as zone}
|
||||
{#if !(hfActive === zone && editingPage === p)}
|
||||
{@const html = zoneHtml(zone, p)}
|
||||
{@const locked = sectionOf(p) > 0}
|
||||
<div
|
||||
class="hf-zone hf-{zone}"
|
||||
class:hf-empty={!html && !locked}
|
||||
class:hf-locked={locked}
|
||||
class:hf-empty={!html}
|
||||
data-hf-label={zone === 'header' ? t().hf.addHeaderHint : t().hf.addFooterHint}
|
||||
style={boxStyle(zoneBox(zone, p))}
|
||||
ondblclick={() => { if (!locked) startEdit(zone, p); }}
|
||||
ondblclick={() => startEdit(zone, p)}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
use:patchFields={[p, numPages, html]}
|
||||
@@ -317,12 +323,16 @@
|
||||
|
||||
{#if hfActive}
|
||||
{@const box = activeZoneBox(hfActive, editingPage)}
|
||||
{@const variant = variantFor(editingPage)}
|
||||
{@const index = sectionOf(editingPage)}
|
||||
{@const variant = variantFor(editingPage, index)}
|
||||
<div class="hf-zone hf-{hfActive} hf-active" style={boxStyle(box) + ` --hf-tb-offset: ${-activeTrailingPx}px;`} bind:this={liveMount}></div>
|
||||
<div class="hf-tag" style="top: {box.top}px; left: {box.left}px;">
|
||||
{hfActive === 'header'
|
||||
? (variant === 'first' ? t().hf.firstPageHeader : variant === 'even' ? t().hf.evenPageHeader : t().hf.headerLabel)
|
||||
: (variant === 'first' ? t().hf.firstPageFooter : variant === 'even' ? t().hf.evenPageFooter : t().hf.footerLabel)}
|
||||
: (variant === 'first' ? t().hf.firstPageFooter : variant === 'even' ? t().hf.evenPageFooter : t().hf.footerLabel)}{sets.length >
|
||||
1
|
||||
? ` · ${t().hf.section} ${index + 1}`
|
||||
: ''}
|
||||
</div>
|
||||
<div class="hf-bar" style="top: {box.top}px; left: {box.left + box.width}px;">
|
||||
<span class="hf-bar-label">{t().hf.insert}</span>
|
||||
@@ -365,11 +375,6 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* A section past the first carries the file's zones and has no editing UI yet. */
|
||||
.hf-locked {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Empty zone: invisible until hovered, then show a faint double-click hint. */
|
||||
.hf-empty::before {
|
||||
content: attr(data-hf-label);
|
||||
|
||||
@@ -393,6 +393,7 @@ const de: Messages = {
|
||||
footerPlaceholder: 'Fußzeile…',
|
||||
headerLabel: 'Kopfzeile',
|
||||
footerLabel: 'Fußzeile',
|
||||
section: 'Abschnitt',
|
||||
firstPageHeader: 'Kopfzeile erste Seite',
|
||||
firstPageFooter: 'Fußzeile erste Seite',
|
||||
evenPageHeader: 'Kopfzeile gerade Seiten',
|
||||
|
||||
@@ -397,6 +397,7 @@ const en = {
|
||||
footerPlaceholder: 'Footer…',
|
||||
headerLabel: 'Header',
|
||||
footerLabel: 'Footer',
|
||||
section: 'Section',
|
||||
firstPageHeader: 'First page header',
|
||||
firstPageFooter: 'First page footer',
|
||||
evenPageHeader: 'Even page header',
|
||||
|
||||
@@ -7,8 +7,7 @@ export type HfVariant = 'default' | 'first' | 'even';
|
||||
export type HfDoc = { type: 'doc'; content?: unknown[] } | null;
|
||||
|
||||
// One section's zones. A document has one per section (a body block carrying
|
||||
// `sectionBreak` starts the next one); section 1 is the app's own editable state,
|
||||
// the rest ride along from the file.
|
||||
// `sectionBreak` starts the next one).
|
||||
export type HfSet = {
|
||||
header: HfDoc;
|
||||
footer: HfDoc;
|
||||
@@ -32,8 +31,8 @@ export function hfSetIsEmpty(s: HfSet): boolean {
|
||||
&& hfIsEmpty(s.headerEven) && hfIsEmpty(s.footerEven);
|
||||
}
|
||||
|
||||
// Sections past the first, in order — imported and exported but not editable, so they
|
||||
// are persisted whole rather than per zone like section 1's.
|
||||
// Sections past the first, in order. Persisted whole rather than per zone like
|
||||
// section 1's, whose six docs each have their own key.
|
||||
const EXTRA_KEY = 'odf-editor-hf-sections';
|
||||
|
||||
export function loadExtraHfSections(): HfSet[] {
|
||||
|
||||
@@ -95,10 +95,9 @@ LibreOffice's 48, with 62 line-level differences, most of them the justification
|
||||
one is rendered: a line box avoids a float's whole *margin* box, so applying the
|
||||
vertical one as a top margin makes dead space where Word flows text — measured, the
|
||||
thesis' figure page went from 131mm off to 187mm and the document grew a page.
|
||||
- **A later section's header/footer cannot be edited.** Its zones import, render and
|
||||
export, but the Layout panel still edits section 1 only — the layer marks the others
|
||||
read-only (`hf-locked`). Page geometry stays document-wide too, so a file whose
|
||||
sections disagree on margins keeps the last one's.
|
||||
- **Page geometry is document-wide.** Headers and footers are per section (each is
|
||||
editable in place), but margins, orientation and format are not — a file whose sections
|
||||
disagree on them keeps the last one's.
|
||||
- **Line height follows the paragraph, not the line.** The block's CSS strut applies to
|
||||
every line; a word processor takes each line's own runs and the paragraph mark only on
|
||||
the last. The mark's font rides the block (`blockFontSize.ts`), and a paragraph whose
|
||||
|
||||
Reference in new issue
Block a user