feat: add bottom toolbar to show page number
This commit is contained in:
1 parent
166cadde86
commit
87be83efd3
5 files changed
+85
-4
No files matched your search
@@ -29,6 +29,7 @@
|
||||
- Shadow edges at page boundaries for depth effect
|
||||
- Dynamic page snapping (editor always shows full pages)
|
||||
- Print-ready @media print styles with @page A4 sizing
|
||||
- add bottom toolbar to show page number
|
||||
|
||||
### Not yet implemented
|
||||
- ODT import / open existing file
|
||||
|
||||
+24
-1
@@ -8,6 +8,8 @@
|
||||
|
||||
let editor: Editor | null = $state(null);
|
||||
let tick: number = $state(0);
|
||||
let currentPage: number = $state(1);
|
||||
let numPages: number = $state(1);
|
||||
|
||||
let themeMode: ThemeMode = $state(loadTheme());
|
||||
let themeOpen = $state(false);
|
||||
@@ -111,7 +113,10 @@
|
||||
{#if toolbarExpanded}
|
||||
<ToolbarExpanded {editor} {tick} />
|
||||
{/if}
|
||||
<EditorComponent bind:editor bind:tick />
|
||||
<EditorComponent bind:editor bind:tick bind:currentPage bind:numPages />
|
||||
<footer class="statusbar">
|
||||
<span>Page {currentPage} of {numPages}</span>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@@ -242,4 +247,22 @@
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.statusbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
background: var(--color-toolbar-bg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
user-select: none;
|
||||
z-index: 50;
|
||||
}
|
||||
</style>
|
||||
@@ -5,9 +5,46 @@
|
||||
import { saveDocument, loadDocument } from '../storage/autosave';
|
||||
import '../../styles/editor.css';
|
||||
|
||||
let { editor = $bindable(), tick = $bindable(0) }: { editor: Editor | null; tick: number } = $props();
|
||||
let { editor = $bindable(), tick = $bindable(0), currentPage = $bindable(1), numPages = $bindable(1) }: {
|
||||
editor: Editor | null; tick: number; currentPage: number; numPages: number;
|
||||
} = $props();
|
||||
|
||||
let element: HTMLDivElement;
|
||||
let editorContainer: HTMLDivElement;
|
||||
|
||||
const CYCLE = 1143; // PAGE_HEIGHT + PAGE_GAP, must match pageBreaks.ts
|
||||
|
||||
function updateCurrentPage() {
|
||||
const tiptap = element?.querySelector('.tiptap') as HTMLElement | null;
|
||||
if (!tiptap || !editorContainer) return;
|
||||
|
||||
const editorRect = editorContainer.getBoundingClientRect();
|
||||
const tiptapRect = tiptap.getBoundingClientRect();
|
||||
|
||||
// If the cursor is visible in the editor viewport, show its page
|
||||
if (editor) {
|
||||
try {
|
||||
const coords = editor.view.coordsAtPos(editor.state.selection.head);
|
||||
const cursorMidY = (coords.top + coords.bottom) / 2;
|
||||
if (cursorMidY >= editorRect.top && cursorMidY <= editorRect.bottom) {
|
||||
const cursorInTiptap = cursorMidY - tiptapRect.top;
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInTiptap) / CYCLE) + 1));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// coordsAtPos can fail during editor teardown — fall through
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor not visible — use the top of the visible scroll area
|
||||
const visibleTopInTiptap = editorRect.top - tiptapRect.top;
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, visibleTopInTiptap) / CYCLE) + 1));
|
||||
}
|
||||
|
||||
function onPageCount(e: Event) {
|
||||
numPages = (e as CustomEvent<{ numPages: number }>).detail.numPages;
|
||||
updateCurrentPage();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const saved = loadDocument();
|
||||
@@ -19,17 +56,34 @@
|
||||
onTransaction: () => {
|
||||
tick++;
|
||||
},
|
||||
onSelectionUpdate: ({ editor: e }) => {
|
||||
// Use the editor instance passed by TipTap directly — avoids any Svelte
|
||||
// prop-reactivity timing issues. TipTap always auto-scrolls the cursor
|
||||
// into view before firing this, so no visibility check is needed here.
|
||||
const tiptap = element?.querySelector('.tiptap') as HTMLElement | null;
|
||||
if (!tiptap) return;
|
||||
try {
|
||||
const coords = e.view.coordsAtPos(e.state.selection.head);
|
||||
const cursorInTiptap = ((coords.top + coords.bottom) / 2) - tiptap.getBoundingClientRect().top;
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInTiptap) / CYCLE) + 1));
|
||||
} catch { /* ignore */ }
|
||||
},
|
||||
onUpdate: ({ editor: e }) => {
|
||||
saveDocument(e.getJSON());
|
||||
},
|
||||
});
|
||||
|
||||
element.addEventListener('pm-pagecount', onPageCount);
|
||||
editorContainer.addEventListener('scroll', updateCurrentPage);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor?.destroy();
|
||||
element?.removeEventListener('pm-pagecount', onPageCount);
|
||||
editorContainer?.removeEventListener('scroll', updateCurrentPage);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="editor">
|
||||
<div class="editor" bind:this={editorContainer}>
|
||||
<div bind:this={element} class="paper"></div>
|
||||
</div>
|
||||
@@ -141,6 +141,9 @@ export const PageBreaks = Extension.create({
|
||||
const targetHeight = numPages * CYCLE - PAGE_GAP; // = N*PAGE_HEIGHT + (N-1)*PAGE_GAP
|
||||
dom.style.minHeight = `${targetHeight}px`;
|
||||
|
||||
// Notify the Svelte layer of the current page count
|
||||
dom.dispatchEvent(new CustomEvent('pm-pagecount', { bubbles: true, detail: { numPages } }));
|
||||
|
||||
isUpdating = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
padding: 2rem 1rem 3rem; /* extra bottom padding clears the fixed status bar */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user