feat: a footnotes and endnotes options dialog
Numbering format, start value, restart, position, prefix/suffix and the two styles per class, plus the separator's length, thickness, spacing, alignment and colour. The separator rides :root custom properties, so editor.css draws the line and pageBreaks.ts reserves its band from the same values. A settings change is no document change, so it carries a resync meta — without it the numbering kept the format it was typed under.
This commit is contained in:
1 parent
3703cea1f2
commit
1aa74c0a9f
11 files changed
+381
-13
No files matched your search
@@ -40,6 +40,7 @@
|
||||
import UiLanguagePicker from './lib/components/UiLanguagePicker.svelte';
|
||||
import AboutDialog from './lib/components/AboutDialog.svelte';
|
||||
import StyleManagerDialog from './lib/components/StyleManagerDialog.svelte';
|
||||
import NoteOptionsDialog from './lib/components/NoteOptionsDialog.svelte';
|
||||
import { t } from './lib/i18n/i18n.svelte';
|
||||
import { withShortcut } from './lib/i18n/shortcut';
|
||||
import { DEFAULT_SHORTCUTS, matchesEvent, shortcutHint } from './lib/editor/shortcuts';
|
||||
@@ -56,6 +57,7 @@
|
||||
let aboutOpen = $state(false);
|
||||
let styleManagerOpen = $state(false);
|
||||
let styleManagerFamily = $state<StyleFamily>('paragraph');
|
||||
let noteOptionsOpen = $state(false);
|
||||
|
||||
function openStyleManager(family: StyleFamily) {
|
||||
styleManagerFamily = family;
|
||||
@@ -201,6 +203,7 @@
|
||||
saveSpacingModel(spacingModel);
|
||||
});
|
||||
|
||||
|
||||
$effect(() => {
|
||||
savePageRtl(pageRtl);
|
||||
});
|
||||
@@ -862,6 +865,7 @@
|
||||
{hfActive}
|
||||
onManageStyles={openStyleManager}
|
||||
onManageTableStyles={() => openStyleManager('table')}
|
||||
onNoteOptions={() => (noteOptionsOpen = true)}
|
||||
onEditZone={(zone) => (hfActive = zone)}
|
||||
onFind={openFind}
|
||||
{namePlaceholder}
|
||||
@@ -1096,6 +1100,7 @@
|
||||
onEditZone={(zone) => (hfActive = zone)}
|
||||
onDebugDump={handleDebugDump}
|
||||
onManageTableStyles={() => openStyleManager('table')}
|
||||
onNoteOptions={() => (noteOptionsOpen = true)}
|
||||
/>
|
||||
</div>
|
||||
{@render toolbarScrollbar()}
|
||||
@@ -1212,6 +1217,7 @@
|
||||
<!-- One instance for every entry point (styles gallery, insert-table menu): the
|
||||
callers only say which family to land on. -->
|
||||
<StyleManagerDialog bind:open={styleManagerOpen} family={styleManagerFamily} editor={activeEditor} />
|
||||
<NoteOptionsDialog bind:open={noteOptionsOpen} />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
import { DEFAULT_TAB_INTERVAL_CM } from '../storage/tabInterval';
|
||||
import { type Orientation } from '../storage/pageOrientation';
|
||||
import { type SpacingModel } from '../storage/spacingModel';
|
||||
import { applyNoteVars } from '../storage/noteSettings';
|
||||
import { noteSettings } from '../storage/notes.svelte';
|
||||
import { RESYNC_NOTES } from '../editor/extensions/notes';
|
||||
import { applyPageSizeVars, type PageFormat } from '../storage/pageFormat';
|
||||
import { DEFAULT_HF_DISTANCES, hfIsEmpty, hfUsesChapterField, type HfDoc, type HfZone, type HfDistances, type HfSet } from '../storage/headerFooter';
|
||||
import { FORCE_PAGE_RECALC, pageOfElement, readVerticalMargins, type TableBreakBand } from '../editor/extensions/pageBreaks';
|
||||
@@ -261,6 +264,25 @@
|
||||
});
|
||||
});
|
||||
|
||||
// The separator's own band is part of the space pagination reserves for a page's
|
||||
// notes, so a change to it has to re-measure — and the line is drawn from the same
|
||||
// custom properties.
|
||||
$effect(() => {
|
||||
applyNoteVars(noteSettings());
|
||||
const ed = editor;
|
||||
if (!ed) return;
|
||||
requestAnimationFrame(() => {
|
||||
if (ed.view.dom.isConnected) {
|
||||
// The numbering follows the settings too, and nothing about them is a
|
||||
// document change the sync pass would otherwise see.
|
||||
ed.view.dispatch(ed.state.tr
|
||||
.setMeta('addToHistory', false)
|
||||
.setMeta(RESYNC_NOTES, true)
|
||||
.setMeta(FORCE_PAGE_RECALC, true));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Bundled fonts (Liberation Serif, Carlito, …) load with font-display:swap, so text
|
||||
// can reflow after pagination first runs (e.g. imported content in a not-yet-loaded
|
||||
// font). Re-paginate when fonts finish so page positions and the HF band settle.
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
DEFAULT_NOTE_SETTINGS, type NoteKind, type NoteNumFormat, type NoteRestart, type NoteSettings,
|
||||
} from '../storage/noteSettings';
|
||||
import { noteSettings, setNoteSettings } from '../storage/notes.svelte';
|
||||
import { styleSheet } from '../styles/sheet.svelte';
|
||||
import { formatOrdinal } from '../utils/orderedListTypes';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
// LibreOffice's Tools ▸ Footnotes and Endnotes. Everything here round-trips to ODF's
|
||||
// text:notes-configuration + style:footnote-sep and to Word's w:footnotePr.
|
||||
let { open = $bindable(false) }: { open?: boolean } = $props();
|
||||
|
||||
let dialogEl = $state<HTMLDialogElement | null>(null);
|
||||
let kind = $state<NoteKind>('footnote');
|
||||
|
||||
$effect(() => {
|
||||
const el = dialogEl;
|
||||
if (!el) return;
|
||||
if (open && !el.open) el.showModal();
|
||||
else if (!open && el.open) el.close();
|
||||
});
|
||||
|
||||
const FORMATS: NoteNumFormat[] = ['1', 'i', 'I', 'a', 'A'];
|
||||
const RESTARTS: NoteRestart[] = ['document', 'page', 'chapter'];
|
||||
const ALIGNS = ['left', 'center', 'right'] as const;
|
||||
|
||||
const cls = $derived(noteSettings()[kind]);
|
||||
const sep = $derived(noteSettings().separator);
|
||||
// Three running values, so a format reads as a sequence rather than a letter.
|
||||
const preview = (f: NoteNumFormat) => [0, 1, 2].map((i) => formatOrdinal(i + 1, f)).join(', ');
|
||||
|
||||
function patch(next: Partial<NoteSettings[NoteKind]>) {
|
||||
const s = noteSettings();
|
||||
setNoteSettings({ ...s, [kind]: { ...s[kind], ...next } });
|
||||
}
|
||||
|
||||
function patchSep(next: Partial<NoteSettings['separator']>) {
|
||||
const s = noteSettings();
|
||||
setNoteSettings({ ...s, separator: { ...s.separator, ...next } });
|
||||
}
|
||||
|
||||
const num = (e: Event) => parseFloat((e.currentTarget as HTMLInputElement).value.replace(',', '.'));
|
||||
</script>
|
||||
|
||||
<dialog
|
||||
bind:this={dialogEl}
|
||||
onclose={() => (open = false)}
|
||||
onclick={(e) => e.target === dialogEl && (open = false)}
|
||||
aria-label={t().notesDialog.title}
|
||||
>
|
||||
<div class="body">
|
||||
<h2>{t().notesDialog.title}</h2>
|
||||
|
||||
<div class="tabs" role="tablist">
|
||||
{#each ['footnote', 'endnote'] as const as k}
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={kind === k}
|
||||
class:selected={kind === k}
|
||||
onclick={() => (kind = k)}
|
||||
>{k === 'footnote' ? t().notesDialog.tabFootnotes : t().notesDialog.tabEndnotes}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>{t().notesDialog.numbering}</legend>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.format}</span>
|
||||
<select value={cls.numFormat} onchange={(e) => patch({ numFormat: e.currentTarget.value as NoteNumFormat })}>
|
||||
{#each FORMATS as f}<option value={f}>{preview(f)}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.startAt}</span>
|
||||
<input type="number" min="1" max="9999" value={cls.startAt}
|
||||
onchange={(e) => { const v = num(e); if (v >= 1) patch({ startAt: Math.round(v) }); }} />
|
||||
</label>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.restart}</span>
|
||||
<select value={cls.restart} onchange={(e) => patch({ restart: e.currentTarget.value as NoteRestart })}>
|
||||
{#each RESTARTS as r}
|
||||
<option value={r}>{r === 'page' ? t().notesDialog.restartPage : r === 'chapter' ? t().notesDialog.restartChapter : t().notesDialog.restartDocument}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{#if kind === 'footnote'}
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.position}</span>
|
||||
<select value={cls.position} onchange={(e) => patch({ position: e.currentTarget.value === 'document' ? 'document' : 'page' })}>
|
||||
<option value="page">{t().notesDialog.positionPage}</option>
|
||||
<option value="document">{t().notesDialog.positionDocument}</option>
|
||||
</select>
|
||||
</label>
|
||||
{/if}
|
||||
<div class="row">
|
||||
<span>{t().notesDialog.affix}</span>
|
||||
<input class="affix" value={cls.prefix} placeholder={t().notesDialog.prefix}
|
||||
onchange={(e) => patch({ prefix: e.currentTarget.value })} />
|
||||
<input class="affix" value={cls.suffix} placeholder={t().notesDialog.suffix}
|
||||
onchange={(e) => patch({ suffix: e.currentTarget.value })} />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>{t().notesDialog.styles}</legend>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.bodyStyle}</span>
|
||||
<select value={cls.bodyStyle} onchange={(e) => patch({ bodyStyle: e.currentTarget.value })}>
|
||||
{#each Object.keys(styleSheet().paragraph) as name}<option value={name}>{name}</option>{/each}
|
||||
{#if !styleSheet().paragraph[cls.bodyStyle]}<option value={cls.bodyStyle}>{cls.bodyStyle}</option>{/if}
|
||||
</select>
|
||||
</label>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.citationStyle}</span>
|
||||
<select value={cls.citationStyle} onchange={(e) => patch({ citationStyle: e.currentTarget.value })}>
|
||||
{#each Object.keys(styleSheet().character) as name}<option value={name}>{name}</option>{/each}
|
||||
{#if !styleSheet().character[cls.citationStyle]}<option value={cls.citationStyle}>{cls.citationStyle}</option>{/if}
|
||||
</select>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<!-- The separator is a footnote-only thing: the endnote list opens its own page. -->
|
||||
{#if kind === 'footnote'}
|
||||
<fieldset>
|
||||
<legend>{t().notesDialog.separator}</legend>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.sepWidth}</span>
|
||||
<input type="number" min="0" max="100" step="1" value={sep.relWidthPercent}
|
||||
onchange={(e) => { const v = num(e); if (v >= 0 && v <= 100) patchSep({ relWidthPercent: v }); }} />
|
||||
</label>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.sepWeight}</span>
|
||||
<input type="number" min="0" max="20" step="0.25" value={sep.weightPt}
|
||||
onchange={(e) => { const v = num(e); if (v >= 0) patchSep({ weightPt: v }); }} />
|
||||
</label>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.sepAbove}</span>
|
||||
<input type="number" min="0" max="5" step="0.05" value={sep.spaceAboveCm}
|
||||
onchange={(e) => { const v = num(e); if (v >= 0) patchSep({ spaceAboveCm: v }); }} />
|
||||
</label>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.sepBelow}</span>
|
||||
<input type="number" min="0" max="5" step="0.05" value={sep.spaceBelowCm}
|
||||
onchange={(e) => { const v = num(e); if (v >= 0) patchSep({ spaceBelowCm: v }); }} />
|
||||
</label>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.sepAlign}</span>
|
||||
<select value={sep.align} onchange={(e) => patchSep({ align: e.currentTarget.value as 'left' })}>
|
||||
{#each ALIGNS as a}<option value={a}>{t().align[a]}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="row">
|
||||
<span>{t().notesDialog.sepColor}</span>
|
||||
<input type="color" value={sep.color} onchange={(e) => patchSep({ color: e.currentTarget.value })} />
|
||||
</label>
|
||||
</fieldset>
|
||||
{/if}
|
||||
|
||||
<div class="actions">
|
||||
<button class="reset" onclick={() => setNoteSettings(DEFAULT_NOTE_SETTINGS)}>{t().notesDialog.reset}</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="primary" onclick={() => (open = false)}>{t().common.close}</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
dialog {
|
||||
/* The global reset zeroes every margin, which also takes the auto centring a
|
||||
modal <dialog> gets by default. */
|
||||
margin: auto;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
dialog::backdrop { background: rgba(0, 0, 0, 0.35); }
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 460px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
padding: 18px 20px 16px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
h2 { font-size: 1rem; }
|
||||
|
||||
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--color-border); }
|
||||
.tabs button {
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: none;
|
||||
color: var(--color-text-muted);
|
||||
padding: 6px 12px;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tabs button.selected { border-bottom-color: var(--color-primary); color: var(--color-text); }
|
||||
|
||||
fieldset { border: 1px solid var(--color-border); border-radius: var(--radius); padding: 8px 10px 10px; }
|
||||
legend { color: var(--color-text-muted); padding: 0 4px; }
|
||||
|
||||
.row { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
|
||||
.row > span { flex: 1; color: var(--color-text-muted); }
|
||||
|
||||
input, select {
|
||||
height: 26px;
|
||||
width: 150px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
padding: 0 6px;
|
||||
font: inherit;
|
||||
}
|
||||
input[type='color'] { width: 44px; padding: 2px; }
|
||||
.affix { width: 68px; }
|
||||
|
||||
.actions { display: flex; align-items: center; gap: 8px; }
|
||||
.spacer { flex: 1; }
|
||||
|
||||
.actions button {
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
padding: 5px 14px;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.actions .reset {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.actions .reset:hover { background: var(--color-btn-hover); }
|
||||
</style>
|
||||
@@ -30,8 +30,8 @@
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
import { shortcutHint, type ShortcutId } from '../editor/shortcuts';
|
||||
|
||||
let { editor, tick, showFormattingMarks = $bindable(), showRuler = $bindable(true), pageMargins = $bindable(DEFAULT_MARGINS), pageOrientation = $bindable<Orientation>('portrait'), pageFormat = $bindable<PageFormat>('A4'), hfDistances = $bindable(DEFAULT_HF_DISTANCES), differentFirstPage = $bindable(false), differentOddEven = $bindable(false), hfActive = null, onEditZone, onDebugDump, onManageTableStyles }:
|
||||
{ editor: Editor | null; tick: number; showFormattingMarks: boolean; showRuler?: boolean; pageMargins?: PageMargins; pageOrientation?: Orientation; pageFormat?: PageFormat; hfDistances?: HfDistances; differentFirstPage?: boolean; differentOddEven?: boolean; hfActive?: 'header' | 'footer' | null; onEditZone?: (zone: 'header' | 'footer') => void; onDebugDump?: () => void; onManageTableStyles?: () => void } = $props();
|
||||
let { editor, tick, showFormattingMarks = $bindable(), showRuler = $bindable(true), pageMargins = $bindable(DEFAULT_MARGINS), pageOrientation = $bindable<Orientation>('portrait'), pageFormat = $bindable<PageFormat>('A4'), hfDistances = $bindable(DEFAULT_HF_DISTANCES), differentFirstPage = $bindable(false), differentOddEven = $bindable(false), hfActive = null, onEditZone, onDebugDump, onManageTableStyles, onNoteOptions }:
|
||||
{ editor: Editor | null; tick: number; showFormattingMarks: boolean; showRuler?: boolean; pageMargins?: PageMargins; pageOrientation?: Orientation; pageFormat?: PageFormat; hfDistances?: HfDistances; differentFirstPage?: boolean; differentOddEven?: boolean; hfActive?: 'header' | 'footer' | null; onEditZone?: (zone: 'header' | 'footer') => void; onDebugDump?: () => void; onManageTableStyles?: () => void; onNoteOptions?: () => void } = $props();
|
||||
|
||||
const PAGE_FORMATS = Object.keys(PAGE_FORMAT_CM) as PageFormat[];
|
||||
|
||||
@@ -1749,6 +1749,18 @@
|
||||
<path d="M2 13.5h9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => onNoteOptions?.()}
|
||||
disabled={!onNoteOptions}
|
||||
title={t().notesDialog.title}
|
||||
aria-label={t().notesDialog.title}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M2 3h8M2 6h6M2 9h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<path d="M2 12.5h5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<path d="M12 10.5a2 2 0 1 1 0 4 2 2 0 0 1 0-4z" stroke="currentColor" stroke-width="1.3" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="link-wrap" use:linkClickOutside>
|
||||
<button
|
||||
class:active={isLink}
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
hfActive = null,
|
||||
onManageStyles,
|
||||
onManageTableStyles,
|
||||
onNoteOptions,
|
||||
onEditZone,
|
||||
onFind,
|
||||
namePlaceholder = '',
|
||||
@@ -74,6 +75,7 @@
|
||||
hfActive?: HfZone | null;
|
||||
onManageStyles?: (family: StyleFamily) => void;
|
||||
onManageTableStyles?: (family: StyleFamily) => void;
|
||||
onNoteOptions?: () => void;
|
||||
onEditZone?: (zone: HfZone) => void;
|
||||
onFind?: (mode: 'find' | 'replace') => void;
|
||||
namePlaceholder?: string;
|
||||
@@ -284,7 +286,7 @@
|
||||
{:else if tab === 'layout'}
|
||||
<LayoutTab {editor} {tick} {hfActive} bind:pageMargins bind:pageOrientation bind:pageFormat onParagraphDialog={() => (paragraphDialogOpen = true)} />
|
||||
{:else if tab === 'references'}
|
||||
<ReferencesTab {editor} {tick} {hfActive} />
|
||||
<ReferencesTab {editor} {tick} {hfActive} {onNoteOptions} />
|
||||
{:else if tab === 'review'}
|
||||
<ReviewTab {editor} {tick} {documentLanguage} {onLanguage} />
|
||||
{:else if tab === 'view'}
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
import { t } from '../../../i18n/i18n.svelte';
|
||||
import { shortcutHint } from '../../../editor/shortcuts';
|
||||
|
||||
let { editor, tick, hfActive = null }: {
|
||||
let { editor, tick, hfActive = null, onNoteOptions }: {
|
||||
editor: Editor | null;
|
||||
tick: number;
|
||||
hfActive?: HfZone | null;
|
||||
onNoteOptions?: () => void;
|
||||
} = $props();
|
||||
|
||||
// The document holds at most one, so the first hit is it.
|
||||
@@ -79,6 +80,13 @@
|
||||
disabled={!editor || !!hfActive}
|
||||
onclick={() => editor?.chain().focus().insertNote('endnote').run()}
|
||||
/>
|
||||
<RibbonButton
|
||||
icon="tocLevels"
|
||||
label={t().ribbon.noteOptions}
|
||||
title={t().notesDialog.title}
|
||||
disabled={!onNoteOptions}
|
||||
onclick={() => onNoteOptions?.()}
|
||||
/>
|
||||
</RibbonGroup>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -210,6 +210,10 @@ export function noteLabels(refs: NoteRefInfo[], settings: NoteSettings): Map<str
|
||||
|
||||
const notesKey = new PluginKey('notes');
|
||||
|
||||
// Meta asking for a renumber without a document change: the numbering follows the
|
||||
// settings, and changing those in the dialog edits nothing the sync pass would see.
|
||||
export const RESYNC_NOTES = 'resyncNotes';
|
||||
|
||||
export interface NotesOptions {
|
||||
settings: () => NoteSettings;
|
||||
}
|
||||
@@ -301,7 +305,7 @@ export const Notes = Extension.create<NotesOptions>({
|
||||
// both at once would need positions from a document this transaction is still
|
||||
// rewriting.
|
||||
appendTransaction(trs, _oldState, newState) {
|
||||
if (!trs.some((tr) => tr.docChanged)) return null;
|
||||
if (!trs.some((tr) => tr.docChanged || tr.getMeta(RESYNC_NOTES))) return null;
|
||||
return syncStructure(newState) ?? syncNumbers(newState, getSettings());
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -462,6 +462,35 @@ const de: Messages = {
|
||||
noLeader: 'Kein Füllzeichen',
|
||||
defaultInterval: 'Standardtabstopps',
|
||||
},
|
||||
notesDialog: {
|
||||
title: 'Fuß- und Endnoten',
|
||||
tabFootnotes: 'Fußnoten',
|
||||
tabEndnotes: 'Endnoten',
|
||||
numbering: 'Nummerierung',
|
||||
format: 'Format',
|
||||
startAt: 'Beginnen bei',
|
||||
restart: 'Nummerierung neu beginnen',
|
||||
restartDocument: 'Fortlaufend',
|
||||
restartPage: 'Pro Seite',
|
||||
restartChapter: 'Pro Abschnitt',
|
||||
position: 'Position',
|
||||
positionPage: 'Seitenende',
|
||||
positionDocument: 'Dokumentende',
|
||||
affix: 'Vor / nach der Nummer',
|
||||
prefix: 'Davor',
|
||||
suffix: 'Danach',
|
||||
styles: 'Vorlagen',
|
||||
bodyStyle: 'Notentext',
|
||||
citationStyle: 'Notenanker',
|
||||
separator: 'Trennlinie',
|
||||
sepWidth: 'Länge (% der Textbreite)',
|
||||
sepWeight: 'Dicke (pt)',
|
||||
sepAbove: 'Abstand oben (cm)',
|
||||
sepBelow: 'Abstand unten (cm)',
|
||||
sepAlign: 'Ausrichtung',
|
||||
sepColor: 'Farbe',
|
||||
reset: 'Auf LibreOffice-Standard zurücksetzen',
|
||||
},
|
||||
ribbon: {
|
||||
tabs: {
|
||||
file: 'Datei',
|
||||
|
||||
@@ -466,6 +466,35 @@ const en = {
|
||||
noLeader: 'No leader',
|
||||
defaultInterval: 'Default tab stops',
|
||||
},
|
||||
notesDialog: {
|
||||
title: 'Footnotes and Endnotes',
|
||||
tabFootnotes: 'Footnotes',
|
||||
tabEndnotes: 'Endnotes',
|
||||
numbering: 'Numbering',
|
||||
format: 'Format',
|
||||
startAt: 'Start at',
|
||||
restart: 'Restart numbering',
|
||||
restartDocument: 'Continuous',
|
||||
restartPage: 'Each page',
|
||||
restartChapter: 'Each section',
|
||||
position: 'Position',
|
||||
positionPage: 'Bottom of page',
|
||||
positionDocument: 'End of document',
|
||||
affix: 'Before / after the number',
|
||||
prefix: 'Before',
|
||||
suffix: 'After',
|
||||
styles: 'Styles',
|
||||
bodyStyle: 'Note text',
|
||||
citationStyle: 'Note anchor',
|
||||
separator: 'Separator line',
|
||||
sepWidth: 'Length (% of the text width)',
|
||||
sepWeight: 'Thickness (pt)',
|
||||
sepAbove: 'Space above (cm)',
|
||||
sepBelow: 'Space below (cm)',
|
||||
sepAlign: 'Alignment',
|
||||
sepColor: 'Color',
|
||||
reset: 'Reset to LibreOffice defaults',
|
||||
},
|
||||
ribbon: {
|
||||
tabs: {
|
||||
file: 'File',
|
||||
|
||||
@@ -133,3 +133,16 @@ export function loadNoteSettings(): NoteSettings {
|
||||
export function saveNoteSettings(s: NoteSettings): void {
|
||||
localStorage.setItem(KEY, JSON.stringify(s));
|
||||
}
|
||||
|
||||
// The separator's geometry on :root, where editor.css draws the line from it and
|
||||
// pageBreaks.ts reads back the band it has to reserve. The three lengths are
|
||||
// registered (@property), so they resolve to px there.
|
||||
export function applyNoteVars(s: NoteSettings): void {
|
||||
const root = document.documentElement.style;
|
||||
root.setProperty('--note-sep-above', `${s.separator.spaceAboveCm}cm`);
|
||||
root.setProperty('--note-sep-below', `${s.separator.spaceBelowCm}cm`);
|
||||
root.setProperty('--note-sep-weight', `${s.separator.weightPt}pt`);
|
||||
root.setProperty('--note-sep-rel-width', `${s.separator.relWidthPercent}%`);
|
||||
root.setProperty('--note-sep-color', s.separator.color);
|
||||
root.setProperty('--note-sep-align', s.separator.align);
|
||||
}
|
||||
@@ -284,11 +284,6 @@
|
||||
@property --note-sep-above { syntax: '<length>'; inherits: true; initial-value: 0.1cm; }
|
||||
@property --note-sep-weight { syntax: '<length>'; inherits: true; initial-value: 0.5pt; }
|
||||
@property --note-sep-below { syntax: '<length>'; inherits: true; initial-value: 0.1cm; }
|
||||
.paper .tiptap {
|
||||
--note-sep-rel-width: 25%;
|
||||
--note-sep-color: #000000;
|
||||
--note-sep-align: left;
|
||||
}
|
||||
.paper .tiptap .note-ref {
|
||||
/* style:text-position="super 58%" on LibreOffice's Footnote anchor style. */
|
||||
vertical-align: super;
|
||||
@@ -334,10 +329,12 @@
|
||||
nothing. The reservation above still uses the true weight, as a hairline border
|
||||
does — the line is a pixel wider than it measures, not the space it sits in. */
|
||||
height: max(1px, var(--note-sep-weight));
|
||||
background-image: linear-gradient(var(--note-sep-color), var(--note-sep-color));
|
||||
background-size: var(--note-sep-rel-width) 100%;
|
||||
/* The two non-length values carry their own default: only a registered property
|
||||
takes one from @property, and the dialog writes all of them onto :root. */
|
||||
background-image: linear-gradient(var(--note-sep-color, #000), var(--note-sep-color, #000));
|
||||
background-size: var(--note-sep-rel-width, 25%) 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: var(--note-sep-align) top;
|
||||
background-position: var(--note-sep-align, left) top;
|
||||
}
|
||||
|
||||
/* Formulas (formula.ts). MathML needs a font with an OpenType MATH table or Chromium
|
||||
|
||||
Reference in new issue
Block a user