fix: a printed comment says which page it is anchored on
The list quoted its text but named no place, so on a five-page printout the entry was a needle. It now leads with the page, the format LibreOffice prints its own end-of-document comments in. Only the raster paths can say it — they print the editor's own pagination, the vector one re-paginates in the browser. commentsHeading gives way to commentLabels, since the page needs a word too.
This commit is contained in:
1 parent
29433e1b1d
commit
f51e3377e6
6 files changed
+57
-20
No files matched your search
+3
-3
@@ -942,7 +942,7 @@
|
||||
orientation: pageOrientation,
|
||||
pageFormat,
|
||||
numPages,
|
||||
commentsHeading: t().comments.title,
|
||||
commentLabels: { heading: t().comments.title, onPage: t().comments.onPage },
|
||||
printMarkup: printMarkup(),
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -969,7 +969,7 @@
|
||||
orientation: pageOrientation,
|
||||
pageFormat,
|
||||
numPages,
|
||||
commentsHeading: t().comments.title,
|
||||
commentLabels: { heading: t().comments.title, onPage: t().comments.onPage },
|
||||
printMarkup: printMarkup(),
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -1001,7 +1001,7 @@
|
||||
headerEvenDoc,
|
||||
footerEvenDoc,
|
||||
differentOddEven,
|
||||
commentsHeading: t().comments.title,
|
||||
commentLabels: { heading: t().comments.title, onPage: t().comments.onPage },
|
||||
printMarkup: printMarkup(),
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -80,6 +80,12 @@ before (no list, no CSS, no margin shift). The Review tab's **Print markup** tog
|
||||
`generateHTML`, where no layout exists yet: `markReviewBlocks` marks whole top-level
|
||||
blocks (CSS has no line box to hang a bar on, and only a top-level block's left edge is
|
||||
the text column's), so a long paragraph with a small change is marked whole.
|
||||
- **An entry names its page**, which with the quoted text is what makes it findable in a
|
||||
printout nobody can click — the format LibreOffice prints its own end-of-document
|
||||
comments in. Only the raster paths can say it (`withAnchorPages`): they print the
|
||||
editor's own pagination, while the vector path re-paginates in the browser and its entry
|
||||
simply has no page. The page comes off one `cycle`, so mixed section paper is out — the
|
||||
same limitation `Editor.svelte`'s own current-page readout has.
|
||||
- **The list is a page, not a flow, in the raster paths.** The live `.paper` is an already
|
||||
paginated layout nothing can flow into, so `renderCommentPages` lays the list out and
|
||||
rasters it on its own; a page starts at an entry boundary, since a raster sliced mid-line
|
||||
|
||||
+27
-10
@@ -12,7 +12,9 @@ import { columnPercents } from '../editor/extensions/tableView';
|
||||
import { effectiveOrderedDef, formatOrdinal } from '../utils/orderedListTypes';
|
||||
import { defaultBulletChar } from '../utils/bulletListTypes';
|
||||
import { deriveFilename } from './odt';
|
||||
import { BAR_STRIP_CM, commentListHtml, markReviewBlocks, printedComments, reviewPrintCss, type PrintedComment } from './reviewPrint';
|
||||
import { BAR_STRIP_CM, commentListHtml, markReviewBlocks, printedComments, reviewPrintCss, type CommentLabels, type PrintedComment } from './reviewPrint';
|
||||
|
||||
const DEFAULT_COMMENT_LABELS: CommentLabels = { heading: 'Comments', onPage: (n) => `Page ${n}` };
|
||||
import globalCss from '../../styles/global.css?inline';
|
||||
import editorCss from '../../styles/editor.css?inline';
|
||||
|
||||
@@ -29,8 +31,8 @@ export interface PdfOptions {
|
||||
orientation?: Orientation;
|
||||
pageFormat?: PageFormat;
|
||||
numPages?: number;
|
||||
/** Heading of the printed comment list; no comments, no extra pages. */
|
||||
commentsHeading?: string;
|
||||
/** Labels of the printed comment list; this module has no i18n of its own. */
|
||||
commentLabels?: CommentLabels;
|
||||
/** Whether the review markup prints at all (`storage/printMarkup`); default on. */
|
||||
printMarkup?: boolean;
|
||||
}
|
||||
@@ -269,6 +271,19 @@ function cropPageDataUrl(
|
||||
return tmp.toDataURL(mime, quality);
|
||||
}
|
||||
|
||||
// The page each comment sits on. Only the raster paths can say: they print the editor's
|
||||
// own pagination, while the vector path re-paginates in the browser.
|
||||
function withAnchorPages(list: PrintedComment[], source: HTMLElement, cycle: number): PrintedComment[] {
|
||||
const base = source.getBoundingClientRect();
|
||||
const scale = source.offsetWidth ? base.width / source.offsetWidth : 1;
|
||||
return list.map((c) => {
|
||||
const el = source.querySelector(`[data-comment="${CSS.escape(c.id)}"]`);
|
||||
if (!el) return c;
|
||||
const top = (el.getBoundingClientRect().top - base.top) / scale;
|
||||
return { ...c, page: Math.max(1, Math.floor(top / cycle) + 1) };
|
||||
});
|
||||
}
|
||||
|
||||
// The comment list as whole extra pages. The live .paper is an already paginated
|
||||
// layout the list cannot flow into, so it is laid out and rastered on its own; a page
|
||||
// starts at an entry boundary, because a raster sliced mid-line cuts the text in half.
|
||||
@@ -285,7 +300,7 @@ async function renderCommentPages(
|
||||
holder.setAttribute('data-pdf-export', '');
|
||||
holder.style.cssText = `position:fixed; left:-100000px; top:0; width:${contentW}px; background:#fff; color:#000;`;
|
||||
holder.innerHTML = `<style>${reviewPrintCss()}\n.comment-list { break-before: auto; }</style>`
|
||||
+ commentListHtml(list, opts.commentsHeading ?? 'Comments');
|
||||
+ commentListHtml(list, opts.commentLabels ?? DEFAULT_COMMENT_LABELS);
|
||||
document.body.appendChild(holder);
|
||||
try {
|
||||
await document.fonts.ready;
|
||||
@@ -327,7 +342,8 @@ export async function exportPdf(opts: PdfOptions): Promise<void> {
|
||||
const landscape = (opts.orientation ?? 'portrait') === 'landscape';
|
||||
try {
|
||||
const runs = collectRuns(clone);
|
||||
const comments = opts.printMarkup === false ? [] : printedComments(opts.source);
|
||||
const comments = opts.printMarkup === false
|
||||
? [] : withAnchorPages(printedComments(opts.source), opts.source, cycle);
|
||||
const extra = comments.length
|
||||
? await renderCommentPages(opts, comments, pageW, pageH, scale)
|
||||
: { images: [], runs: [] };
|
||||
@@ -383,7 +399,8 @@ export async function printRaster(opts: PdfOptions): Promise<void> {
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
const comments = opts.printMarkup === false ? [] : printedComments(opts.source);
|
||||
const comments = opts.printMarkup === false
|
||||
? [] : withAnchorPages(printedComments(opts.source), opts.source, cycle);
|
||||
if (comments.length) imgs = imgs.concat((await renderCommentPages(opts, comments, pageW, pageH, scale)).images);
|
||||
|
||||
const title = (opts.fileName ?? deriveFilename(opts.json)).replace(/\.(odt|pdf)$/i, '');
|
||||
@@ -446,8 +463,8 @@ export interface PrintPdfOptions {
|
||||
headerEvenDoc?: HfDoc;
|
||||
footerEvenDoc?: HfDoc;
|
||||
differentOddEven?: boolean;
|
||||
/** Heading of the printed comment list; no comments, no list. */
|
||||
commentsHeading?: string;
|
||||
/** Labels of the printed comment list; this module has no i18n of its own. */
|
||||
commentLabels?: CommentLabels;
|
||||
/** Whether the review markup prints at all (`storage/printMarkup`); default on. */
|
||||
printMarkup?: boolean;
|
||||
}
|
||||
@@ -622,7 +639,7 @@ export function printPdf(opts: PrintPdfOptions): void {
|
||||
headerEvenDoc: opts.headerEvenDoc ?? null,
|
||||
footerEvenDoc: opts.footerEvenDoc ?? null,
|
||||
differentOddEven: opts.differentOddEven ?? false,
|
||||
commentsHeading: opts.commentsHeading,
|
||||
commentLabels: opts.commentLabels,
|
||||
printMarkup: opts.printMarkup,
|
||||
};
|
||||
const title = (o.fileName ?? deriveFilename(o.json)).replace(/\.(odt|pdf)$/i, '');
|
||||
@@ -633,7 +650,7 @@ export function printPdf(opts: PrintPdfOptions): void {
|
||||
const markup = o.printMarkup !== false;
|
||||
const comments = markup ? printedComments(host) : [];
|
||||
const review = markup && markReviewBlocks(host);
|
||||
const list = comments.length ? commentListHtml(comments, o.commentsHeading ?? 'Comments') : '';
|
||||
const list = comments.length ? commentListHtml(comments, o.commentLabels ?? DEFAULT_COMMENT_LABELS) : '';
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.setAttribute('aria-hidden', 'true');
|
||||
|
||||
@@ -11,7 +11,14 @@ export const BAR_STRIP_CM = 0.32;
|
||||
|
||||
const COMMENT_COLOR = '#c88c00';
|
||||
|
||||
export type PrintedComment = { id: string; n: number; author: string; date: string; text: string; quote: string };
|
||||
export type PrintedComment = {
|
||||
id: string; n: number; author: string; date: string; text: string; quote: string;
|
||||
/** The page the anchor sits on, where the caller knows it (the raster paths do). */
|
||||
page?: number;
|
||||
};
|
||||
|
||||
/** The labels the list is printed with; this module has no i18n of its own. */
|
||||
export type CommentLabels = { heading: string; onPage: (n: number) => string };
|
||||
|
||||
const OPEN_COMMENT = '[data-comment]:not([data-comment-resolved="true"])';
|
||||
const MARKED = `${OPEN_COMMENT}, [data-insertion], [data-deletion]`;
|
||||
@@ -81,12 +88,17 @@ function when(iso: string): string {
|
||||
}
|
||||
|
||||
/** The comment bodies, after the document: what the margin bar cannot carry. */
|
||||
export function commentListHtml(list: PrintedComment[], heading: string): string {
|
||||
const rows = list.map((c) => `<li>`
|
||||
+ `<span class="cl-meta">${esc(c.author)}${c.date ? ` · ${esc(when(c.date))}` : ''}</span>`
|
||||
+ (c.quote.trim() ? `<span class="cl-quote">${esc(c.quote)}</span>` : '')
|
||||
+ `<span class="cl-text">${esc(c.text)}</span></li>`).join('');
|
||||
return `<section class="comment-list"><h2>${esc(heading)}</h2><ol>${rows}</ol></section>`;
|
||||
export function commentListHtml(list: PrintedComment[], labels: CommentLabels): string {
|
||||
const rows = list.map((c) => {
|
||||
// Page first, as LibreOffice prints its own end-of-document comments: with the
|
||||
// quote it is what makes an entry findable in a printout nobody can click.
|
||||
const meta = [c.page ? labels.onPage(c.page) : '', c.author, c.date ? when(c.date) : '']
|
||||
.filter(Boolean).map(esc).join(' · ');
|
||||
return `<li><span class="cl-meta">${meta}</span>`
|
||||
+ (c.quote.trim() ? `<span class="cl-quote">${esc(c.quote)}</span>` : '')
|
||||
+ `<span class="cl-text">${esc(c.text)}</span></li>`;
|
||||
}).join('');
|
||||
return `<section class="comment-list"><h2>${esc(labels.heading)}</h2><ol>${rows}</ol></section>`;
|
||||
}
|
||||
|
||||
/** The rules both print paths draw the bar and the list with. */
|
||||
|
||||
@@ -81,6 +81,7 @@ const de: Messages = {
|
||||
},
|
||||
comments: {
|
||||
title: 'Kommentare',
|
||||
onPage: (n: number) => `Seite ${n}`,
|
||||
empty: 'Noch keine Kommentare. Text markieren und einen hinzufügen.',
|
||||
edit: 'Bearbeiten',
|
||||
resolve: 'Erledigt',
|
||||
|
||||
@@ -84,6 +84,7 @@ const en = {
|
||||
},
|
||||
comments: {
|
||||
title: 'Comments',
|
||||
onPage: (n: number) => `Page ${n}`,
|
||||
empty: 'No comments yet. Select some text and add one.',
|
||||
edit: 'Edit',
|
||||
resolve: 'Resolve',
|
||||
|
||||
Reference in new issue
Block a user