feat: add zoom glider in bottom toolbar
This commit is contained in:
1 parent
0147da6e03
commit
b7ab64948d
2 files changed
+129
-12
No files matched your search
+117
-1
@@ -14,6 +14,12 @@
|
||||
let themeMode: ThemeMode = $state(loadTheme());
|
||||
let themeOpen = $state(false);
|
||||
let toolbarExpanded = $state(loadToolbarExpanded());
|
||||
let zoom = $state(Math.max(50, Math.min(200, parseInt(localStorage.getItem('odf-editor-zoom') ?? '100', 10))));
|
||||
|
||||
function setZoom(value: number) {
|
||||
zoom = Math.max(50, Math.min(200, value));
|
||||
localStorage.setItem('odf-editor-zoom', String(zoom));
|
||||
}
|
||||
|
||||
function selectTheme(m: ThemeMode) {
|
||||
themeMode = m;
|
||||
@@ -113,9 +119,24 @@
|
||||
{#if toolbarExpanded}
|
||||
<ToolbarExpanded {editor} {tick} />
|
||||
{/if}
|
||||
<EditorComponent bind:editor bind:tick bind:currentPage bind:numPages />
|
||||
<EditorComponent bind:editor bind:tick bind:currentPage bind:numPages {zoom} />
|
||||
<footer class="statusbar">
|
||||
<span>Page {currentPage} of {numPages}</span>
|
||||
<div class="zoom-controls">
|
||||
<button class="zoom-btn" onclick={() => setZoom(zoom - 10)} disabled={zoom <= 50} title="Zoom out">−</button>
|
||||
<input
|
||||
type="range"
|
||||
class="zoom-slider"
|
||||
min="50"
|
||||
max="200"
|
||||
step="1"
|
||||
value={zoom}
|
||||
oninput={(e) => setZoom(parseInt((e.target as HTMLInputElement).value, 10))}
|
||||
title="Zoom"
|
||||
/>
|
||||
<button class="zoom-btn" onclick={() => setZoom(zoom + 10)} disabled={zoom >= 200} title="Zoom in">+</button>
|
||||
<button class="zoom-pct" onclick={() => setZoom(100)} title="Reset to 100%">{zoom}%</button>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
@@ -265,4 +286,99 @@
|
||||
user-select: none;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.zoom-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: color 0.1s, background 0.1s;
|
||||
}
|
||||
|
||||
.zoom-btn:hover:not(:disabled) {
|
||||
color: var(--color-text);
|
||||
background: var(--color-btn-hover);
|
||||
}
|
||||
|
||||
.zoom-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.zoom-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-border);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.zoom-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.zoom-slider:hover::-webkit-slider-thumb,
|
||||
.zoom-slider:focus::-webkit-slider-thumb {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.zoom-slider::-moz-range-thumb {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.zoom-slider:hover::-moz-range-thumb,
|
||||
.zoom-slider:focus::-moz-range-thumb {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.zoom-pct {
|
||||
min-width: 36px;
|
||||
padding: 0 3px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.75rem;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: color 0.1s, background 0.1s;
|
||||
}
|
||||
|
||||
.zoom-pct:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-btn-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -5,8 +5,8 @@
|
||||
import { saveDocument, loadDocument } from '../storage/autosave';
|
||||
import '../../styles/editor.css';
|
||||
|
||||
let { editor = $bindable(), tick = $bindable(0), currentPage = $bindable(1), numPages = $bindable(1) }: {
|
||||
editor: Editor | null; tick: number; currentPage: number; numPages: number;
|
||||
let { editor = $bindable(), tick = $bindable(0), currentPage = $bindable(1), numPages = $bindable(1), zoom = 100 }: {
|
||||
editor: Editor | null; tick: number; currentPage: number; numPages: number; zoom: number;
|
||||
} = $props();
|
||||
|
||||
let element: HTMLDivElement;
|
||||
@@ -20,15 +20,17 @@
|
||||
|
||||
const editorRect = editorContainer.getBoundingClientRect();
|
||||
const tiptapRect = tiptap.getBoundingClientRect();
|
||||
// getBoundingClientRect and coordsAtPos return zoomed viewport pixels;
|
||||
// divide by zoom factor to convert to document coordinates before comparing with CYCLE.
|
||||
const zoomFactor = zoom / 100;
|
||||
|
||||
// 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));
|
||||
const cursorInDoc = (cursorMidY - tiptapRect.top) / zoomFactor;
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInDoc) / CYCLE) + 1));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
@@ -36,9 +38,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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));
|
||||
const visibleTopInDoc = (editorRect.top - tiptapRect.top) / zoomFactor;
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, visibleTopInDoc) / CYCLE) + 1));
|
||||
}
|
||||
|
||||
function onPageCount(e: Event) {
|
||||
@@ -64,8 +65,8 @@
|
||||
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));
|
||||
const cursorInDoc = ((coords.top + coords.bottom) / 2 - tiptap.getBoundingClientRect().top) / (zoom / 100);
|
||||
currentPage = Math.max(1, Math.min(numPages, Math.floor(Math.max(0, cursorInDoc) / CYCLE) + 1));
|
||||
} catch { /* ignore */ }
|
||||
},
|
||||
onUpdate: ({ editor: e }) => {
|
||||
@@ -85,5 +86,5 @@
|
||||
</script>
|
||||
|
||||
<div class="editor" bind:this={editorContainer}>
|
||||
<div bind:this={element} class="paper"></div>
|
||||
<div bind:this={element} class="paper" style="zoom: {zoom / 100}"></div>
|
||||
</div>
|
||||
Reference in new issue
Block a user