diff --git a/templates/reader.templ b/templates/reader.templ index b645770..14ea006 100644 --- a/templates/reader.templ +++ b/templates/reader.templ @@ -139,7 +139,13 @@ templ Reader(user User, metadata ReaderMetadata) { :style="'background-color:' + c" :title="selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'" :aria-label="'Highlight color ' + c" - @click="selectionPopover.mode === 'create' ? createHighlight(c) : (selectionPopover.color = c, saveHighlightChanges())" + @click="selectionPopover.noteOpen + ? (selectionPopover.mode === 'create' + ? (selectionPopover.color = c) + : (selectionPopover.color = c, saveHighlightChanges(true))) + : (selectionPopover.mode === 'create' + ? createHighlight(c) + : (selectionPopover.color = c, saveHighlightChanges()))" >
diff --git a/templates/reader_templ.go b/templates/reader_templ.go index 14ffcdc..1f40f7b 100644 --- a/templates/reader_templ.go +++ b/templates/reader_templ.go @@ -50,14 +50,14 @@ func Reader(user User, metadata ReaderMetadata) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"overscroll-none\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\"><title>") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 30, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 30, Col: 26} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -67,7 +67,7 @@ func Reader(user User, metadata ReaderMetadata) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var3 = []any{"theme-" + user.Theme + " h-screen overflow-hidden"} + var templ_7745c5c3_Var3 = []any{"theme-" + user.Theme + " h-screen overflow-hidden overscroll-none"} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -79,7 +79,7 @@ func Reader(user User, metadata ReaderMetadata) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(readerInitExpr(metadata)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 40, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 40, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4) if templ_7745c5c3_Err != nil { @@ -92,7 +92,7 @@ func Reader(user User, metadata ReaderMetadata) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 1, Col: 0} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 1, Col: 0} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5) if templ_7745c5c3_Err != nil { @@ -122,7 +122,7 @@ func Reader(user User, metadata ReaderMetadata) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><!-- Selection popover (highlights) --><div id=\"selection-popover\" x-show=\"selectionPopover.open\" x-transition:enter=\"transition ease-out duration-150\" x-transition:enter-start=\"opacity-0 scale-95\" x-transition:enter-end=\"opacity-100 scale-100\" x-transition:leave=\"transition ease-in duration-100\" x-transition:leave-start=\"opacity-100 scale-100\" x-transition:leave-end=\"opacity-0 scale-95\" :style=\"'left:' + selectionPopover.x + 'px; top:' + selectionPopover.y + 'px'\" class=\"reader-popover\" role=\"dialog\" aria-label=\"Highlight selection\" @pointerdown.stop><div class=\"flex items-center gap-2\"><template x-for=\"c in highlightColors\" :key=\"c\"><button type=\"button\" class=\"color-dot\" :class=\"selectionPopover.color === c ? 'selected' : ''\" :style=\"'background-color:' + c\" :title=\"selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'\" :aria-label=\"'Highlight color ' + c\" @click=\"selectionPopover.mode === 'create' ? createHighlight(c) : (selectionPopover.color = c, saveHighlightChanges())\"></button></template><div class=\"w-px h-5 reader-sep\"></div><button type=\"button\" class=\"reader-popover-btn\" title=\"Note\" @click=\"selectionPopover.noteOpen = true\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 4 13 L 4 16 L 7 16 L 14.5 8.5 L 11.5 5.5 L 4 13 M 12.5 4.5 L 15.5 7.5\"></path></svg></button> <button type=\"button\" class=\"reader-popover-btn\" title=\"Copy text\" @click=\"copySelectionText()\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 6 6 V 3 H 16 V 13 H 13 M 3 6 H 13 V 16 H 3 Z\"></path></svg></button><template x-if=\"selectionPopover.mode === 'edit'\"><button type=\"button\" class=\"reader-popover-btn danger\" title=\"Delete highlight\" @click=\"deleteHighlightById(selectionPopover.id)\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 5 6 H 15 L 14 17 H 6 Z M 8 6 V 4 H 12 V 6 M 4 6 H 16\"></path></svg></button></template></div><div x-show=\"selectionPopover.noteOpen\" class=\"mt-2\"><textarea x-model=\"selectionPopover.note\" rows=\"3\" class=\"reader-note-input\" placeholder=\"Note…\"></textarea><div class=\"flex gap-2 mt-1\"><button type=\"button\" class=\"flex-1 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700\" @click=\"selectionPopover.mode === 'create' ? createHighlight(selectionPopover.color) : saveHighlightChanges()\" x-text=\"selectionPopover.mode === 'create' ? 'Highlight with note' : 'Save note'\">Save</button> <button type=\"button\" class=\"flex-1 py-1 text-xs border rounded hover:opacity-80\" style=\"border-color: var(--border);\" @click=\"selectionPopover.noteOpen = false\">Cancel</button></div></div></div><!-- Annotations drawer (right): highlights / notes / bookmarks --><div id=\"annotations-drawer\" x-show=\"bookmarksOpen\" x-transition:enter=\"transition-transform duration-200 ease-out\" x-transition:enter-start=\"translate-x-full\" x-transition:enter-end=\"translate-x-0\" x-transition:leave=\"transition-transform duration-150 ease-in\" x-transition:leave-start=\"translate-x-0\" x-transition:leave-end=\"translate-x-full\" class=\"reader-drawer right\" role=\"dialog\" aria-label=\"Annotations\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><!-- Selection popover (highlights) --><div id=\"selection-popover\" x-show=\"selectionPopover.open\" x-transition:enter=\"transition ease-out duration-150\" x-transition:enter-start=\"opacity-0 scale-95\" x-transition:enter-end=\"opacity-100 scale-100\" x-transition:leave=\"transition ease-in duration-100\" x-transition:leave-start=\"opacity-100 scale-100\" x-transition:leave-end=\"opacity-0 scale-95\" :style=\"'left:' + selectionPopover.x + 'px; top:' + selectionPopover.y + 'px'\" class=\"reader-popover\" role=\"dialog\" aria-label=\"Highlight selection\" @pointerdown.stop><div class=\"flex items-center gap-2\"><template x-for=\"c in highlightColors\" :key=\"c\"><button type=\"button\" class=\"color-dot\" :class=\"selectionPopover.color === c ? 'selected' : ''\" :style=\"'background-color:' + c\" :title=\"selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'\" :aria-label=\"'Highlight color ' + c\" @click=\"selectionPopover.noteOpen\n\t\t\t\t\t\t\t? (selectionPopover.mode === 'create'\n\t\t\t\t\t\t\t\t? (selectionPopover.color = c)\n\t\t\t\t\t\t\t\t: (selectionPopover.color = c, saveHighlightChanges(true)))\n\t\t\t\t\t\t\t: (selectionPopover.mode === 'create'\n\t\t\t\t\t\t\t\t? createHighlight(c)\n\t\t\t\t\t\t\t\t: (selectionPopover.color = c, saveHighlightChanges()))\"></button></template><div class=\"w-px h-5 reader-sep\"></div><button type=\"button\" class=\"reader-popover-btn\" title=\"Note\" @click=\"selectionPopover.noteOpen = true\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 4 13 L 4 16 L 7 16 L 14.5 8.5 L 11.5 5.5 L 4 13 M 12.5 4.5 L 15.5 7.5\"></path></svg></button> <button type=\"button\" class=\"reader-popover-btn\" title=\"Copy text\" @click=\"copySelectionText()\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 6 6 V 3 H 16 V 13 H 13 M 3 6 H 13 V 16 H 3 Z\"></path></svg></button><template x-if=\"selectionPopover.mode === 'edit'\"><button type=\"button\" class=\"reader-popover-btn danger\" title=\"Delete highlight\" @click=\"deleteHighlightById(selectionPopover.id)\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 5 6 H 15 L 14 17 H 6 Z M 8 6 V 4 H 12 V 6 M 4 6 H 16\"></path></svg></button></template></div><div x-show=\"selectionPopover.noteOpen\" class=\"mt-2\"><textarea x-model=\"selectionPopover.note\" rows=\"3\" class=\"reader-note-input\" placeholder=\"Note…\"></textarea><div class=\"flex gap-2 mt-1\"><button type=\"button\" class=\"flex-1 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700\" @click=\"selectionPopover.mode === 'create' ? createHighlight(selectionPopover.color) : saveHighlightChanges()\" x-text=\"selectionPopover.mode === 'create' ? 'Highlight with note' : 'Save note'\">Save</button> <button type=\"button\" class=\"flex-1 py-1 text-xs border rounded hover:opacity-80\" style=\"border-color: var(--border);\" @click=\"selectionPopover.noteOpen = false\">Cancel</button></div></div></div><!-- Annotations drawer (right): highlights / notes / bookmarks --><div id=\"annotations-drawer\" x-show=\"bookmarksOpen\" x-transition:enter=\"transition-transform duration-200 ease-out\" x-transition:enter-start=\"translate-x-full\" x-transition:enter-end=\"translate-x-0\" x-transition:leave=\"transition-transform duration-150 ease-in\" x-transition:leave-start=\"translate-x-0\" x-transition:leave-end=\"translate-x-full\" class=\"reader-drawer right\" role=\"dialog\" aria-label=\"Annotations\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -174,7 +174,7 @@ func ReaderChrome(metadata ReaderMetadata) templ.Component { var templ_7745c5c3_Var7 templ.SafeURL templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 292, Col: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 298, Col: 63} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -187,7 +187,7 @@ func ReaderChrome(metadata ReaderMetadata) templ.Component { var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 295, Col: 95} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 301, Col: 95} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -229,7 +229,7 @@ func drawerHeader(title string) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 555, Col: 35} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 561, Col: 35} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index a5797ed..a934fcd 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -730,6 +730,20 @@ document.addEventListener("alpine:init", () => { this.scheduleSelectionPopover(doc, index); }); } + // Clicks in the book dismiss the popover in ANY mode and for ANY + // content doc (EPUB sections and fixed-layout pages alike): iframe + // events never bubble to the host document (so the host + // outside-click dismiss never sees them). Clicking a painted + // highlight still works: this hides, then foliate's + // show-annotation re-opens it in edit mode. + doc.addEventListener( + "pointerdown", + () => { + if (this.selectionPopover.open) + this.hideSelectionPopover(); + }, + { passive: true }, + ); // Text selection → highlight popover (reflowable EPUB only; // fixed-layout highlight overlays are a later milestone). if (!this.isFixedLayout) { @@ -759,21 +773,6 @@ document.addEventListener("alpine:init", () => { (e: PointerEvent) => setTimeout(() => checkSelection(e), 0), { passive: true }, ); - // Clicks in the book dismiss the popover in ANY mode: iframe - // events never bubble to the host document (so the host - // outside-click dismiss never sees them), and the collapsed - // check above only covers create mode — edit mode had no - // outside-click path at all, leaving Esc as the only way out. - // Clicking a painted highlight still works: this hides, then - // foliate's show-annotation re-opens it in edit mode. - doc.addEventListener( - "pointerdown", - () => { - if (this.selectionPopover.open) - this.hideSelectionPopover(); - }, - { passive: true }, - ); doc.addEventListener( "keyup", (ev: KeyboardEvent) => { @@ -1627,6 +1626,9 @@ document.addEventListener("alpine:init", () => { const p = this.selectionPopover; const token = getToken(); if (!token || !this.mediaItemId || (!p.cfi && p.pdfPage < 0)) return; + // The note editor writes into p.note live; "Highlight with note" + // creates the highlight AND its note in one POST. + const note = p.note.trim(); const pdfAnchor = p.pdfPage >= 0 ? JSON.stringify({ v: 1, page: p.pdfPage, rects: p.pdfRects }) @@ -1647,7 +1649,7 @@ document.addEventListener("alpine:init", () => { epubcfi_start: p.pdfPage >= 0 ? pdfAnchor : p.cfi, epubcfi_end: p.pdfPage >= 0 ? pdfAnchor : p.cfiEnd, color, - note_text: "", + note_text: note, percentage_start: this.lastRelocateDetail?.fraction ?? 0, }), }, @@ -1676,7 +1678,7 @@ document.addEventListener("alpine:init", () => { ? "" : this.toRangeCfi(p.cfi, p.cfiEnd, p.text) || p.cfi, color, - note: "", + note, id: row.id, }); // Clear the DOM selection: the gesture is consumed, and it is @@ -1692,7 +1694,7 @@ document.addEventListener("alpine:init", () => { /* ignore highlight errors */ } }, - async saveHighlightChanges() { + async saveHighlightChanges(keepNoteOpen = false) { const p = this.selectionPopover; const token = getToken(); if (!token || !this.mediaItemId || !p.id) return; @@ -1756,7 +1758,9 @@ document.addEventListener("alpine:init", () => { id: p.id, }); } - p.noteOpen = false; + // Color tweaks made while the note editor is open keep the + // editor open — only the explicit save/close paths collapse it. + if (!keepNoteOpen) p.noteOpen = false; } catch (_e) { /* ignore highlight errors */ }