v0.2.97
This commit is contained in:
1 parent
d36a4b2451
commit
99a5700caa
8 files changed
+176
-56
No files matched your search
Generated
+1
-1
@@ -2674,7 +2674,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.96"
|
version = "0.2.97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.96"
|
version = "0.2.97"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.94"
|
rust-version = "1.94"
|
||||||
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
RustPad is a collaborative Markdown editor with standalone notes and workspaces.
|
RustPad is a collaborative Markdown editor with standalone notes and workspaces.
|
||||||
|
|
||||||
|
## Release 0.2.97 — proportional image resize and stable PDF pagination
|
||||||
|
|
||||||
|
- Image resize controls keep the source image proportions by default. Editing width automatically updates height and editing height updates width.
|
||||||
|
- The image toolbar adds a **Keep proportions** checkbox; disabling it allows independent width/height edits and free two-axis drag resizing.
|
||||||
|
- Drag resizing now uses the image's natural aspect ratio when available instead of preserving an accidentally distorted stored size.
|
||||||
|
- PDF pagination renders movable text blocks as atomic print fragments, eliminating Chromium's thin "ghost" text artifact when headings, paragraphs, blockquotes, or definition entries move to the next page.
|
||||||
|
- Oversized text blocks still fall back to normal page splitting so long content is never clipped or forced off-page.
|
||||||
|
|
||||||
## Release 0.2.96 — PDF layout, inline rename and PDF tags
|
## Release 0.2.96 — PDF layout, inline rename and PDF tags
|
||||||
|
|
||||||
- Pressing Enter while renaming a note title no longer bubbles to the header copy-link shortcut.
|
- Pressing Enter while renaming a note title no longer bubbles to the header copy-link shortcut.
|
||||||
|
|||||||
@@ -458,6 +458,7 @@
|
|||||||
"editor.insertEmoji": "Insert emoji",
|
"editor.insertEmoji": "Insert emoji",
|
||||||
"editor.italic": "Italic",
|
"editor.italic": "Italic",
|
||||||
"editor.keyboardShortcuts": "Keyboard shortcuts",
|
"editor.keyboardShortcuts": "Keyboard shortcuts",
|
||||||
|
"editor.keepAspectRatio": "Keep proportions",
|
||||||
"editor.layout.auto": "Auto",
|
"editor.layout.auto": "Auto",
|
||||||
"editor.layout.center": "Center",
|
"editor.layout.center": "Center",
|
||||||
"editor.layout.left": "Left",
|
"editor.layout.left": "Left",
|
||||||
|
|||||||
@@ -458,6 +458,7 @@
|
|||||||
"editor.insertEmoji": "Wstaw emoji",
|
"editor.insertEmoji": "Wstaw emoji",
|
||||||
"editor.italic": "Kursywa",
|
"editor.italic": "Kursywa",
|
||||||
"editor.keyboardShortcuts": "Skróty klawiaturowe",
|
"editor.keyboardShortcuts": "Skróty klawiaturowe",
|
||||||
|
"editor.keepAspectRatio": "Zachowaj proporcje",
|
||||||
"editor.layout.auto": "Automatycznie",
|
"editor.layout.auto": "Automatycznie",
|
||||||
"editor.layout.center": "Wyśrodkowanie",
|
"editor.layout.center": "Wyśrodkowanie",
|
||||||
"editor.layout.left": "Do lewej",
|
"editor.layout.left": "Do lewej",
|
||||||
|
|||||||
+27
-6
@@ -2858,14 +2858,16 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-alias-tools {
|
.image-alias-tools {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
z-index: 4;
|
z-index: 1000;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
|
||||||
left: 8px;
|
left: 8px;
|
||||||
|
width: max-content;
|
||||||
|
max-width: min(720px, calc(100vw - 24px));
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
@@ -2912,11 +2914,30 @@ dialog::backdrop {
|
|||||||
font-size: .72rem;
|
font-size: .72rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-alias-size input {
|
.image-alias-size input[type="number"] {
|
||||||
width: 66px;
|
width: 66px;
|
||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-alias-size .image-alias-aspect {
|
||||||
|
gap: 5px;
|
||||||
|
padding: 0 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-alias-size .image-alias-aspect input {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: transparent;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.image-alias-resize {
|
.image-alias-resize {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
@@ -2939,8 +2960,8 @@ dialog::backdrop {
|
|||||||
@media(max-width:680px) {
|
@media(max-width:680px) {
|
||||||
.image-alias-tools {
|
.image-alias-tools {
|
||||||
top: 4px;
|
top: 4px;
|
||||||
right: 4px;
|
|
||||||
left: 4px;
|
left: 4px;
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+97
-19
@@ -1001,6 +1001,80 @@ export function startNoteEditor(adapter) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function imageFrameAspectRatio(frame) {
|
||||||
|
const image = frame?.querySelector("img");
|
||||||
|
if (image?.naturalWidth > 0 && image?.naturalHeight > 0) return image.naturalWidth / image.naturalHeight;
|
||||||
|
const dimensions = imageFrameDimensions(frame);
|
||||||
|
return Math.max(.01, dimensions.width / Math.max(1, dimensions.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampImageDimension(value) {
|
||||||
|
const number = Math.round(Number(value));
|
||||||
|
if (!Number.isFinite(number)) return null;
|
||||||
|
return Math.max(1, Math.min(10000, number));
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncImageSizeInput(frame, changedInput) {
|
||||||
|
const tools = frame?.querySelector(".image-alias-tools");
|
||||||
|
const lock = tools?.querySelector("[data-image-aspect-lock]");
|
||||||
|
if (!tools || !lock?.checked || !(changedInput instanceof HTMLInputElement)) return;
|
||||||
|
|
||||||
|
const widthInput = tools.querySelector("[data-image-width-input]");
|
||||||
|
const heightInput = tools.querySelector("[data-image-height-input]");
|
||||||
|
const ratio = imageFrameAspectRatio(frame);
|
||||||
|
const changed = clampImageDimension(changedInput.value);
|
||||||
|
if (!changed || !Number.isFinite(ratio) || ratio <= 0) return;
|
||||||
|
|
||||||
|
if (changedInput === widthInput && heightInput) heightInput.value = String(clampImageDimension(changed / ratio));
|
||||||
|
if (changedInput === heightInput && widthInput) widthInput.value = String(clampImageDimension(changed * ratio));
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewImageFrameSize(frame, width, height) {
|
||||||
|
frame.dataset.imageWidth = String(width);
|
||||||
|
frame.dataset.imageHeight = String(height);
|
||||||
|
frame.style.setProperty("--image-width", `${width}px`);
|
||||||
|
frame.style.setProperty("--image-height", `${height}px`);
|
||||||
|
frame.style.setProperty("--image-aspect", `${width} / ${height}`);
|
||||||
|
const widthInput = frame.querySelector("[data-image-width-input]");
|
||||||
|
const heightInput = frame.querySelector("[data-image-height-input]");
|
||||||
|
if (widthInput) widthInput.value = String(width);
|
||||||
|
if (heightInput) heightInput.value = String(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
let keepImageAspectRatio = true;
|
||||||
|
|
||||||
|
function positionImageTools(frame) {
|
||||||
|
const tools = frame?.querySelector(".image-alias-tools");
|
||||||
|
if (!tools || !frame?.isConnected) return;
|
||||||
|
|
||||||
|
// Keep the floating toolbar completely inside the visual viewport.
|
||||||
|
// Using fixed positioning prevents the toolbar itself from increasing the
|
||||||
|
// document scroll width when the selected image is narrow or near an edge.
|
||||||
|
const margin = 12;
|
||||||
|
const viewport = window.visualViewport;
|
||||||
|
const viewportLeft = viewport?.offsetLeft || 0;
|
||||||
|
const viewportWidth = viewport?.width || document.documentElement.clientWidth || window.innerWidth;
|
||||||
|
const availableWidth = Math.max(1, Math.floor(viewportWidth - margin * 2));
|
||||||
|
const frameRect = frame.getBoundingClientRect();
|
||||||
|
|
||||||
|
tools.style.position = "fixed";
|
||||||
|
tools.style.maxWidth = `${availableWidth}px`;
|
||||||
|
tools.style.left = `${viewportLeft + margin}px`;
|
||||||
|
tools.style.top = `${Math.round(frameRect.top + 8)}px`;
|
||||||
|
|
||||||
|
const toolsRect = tools.getBoundingClientRect();
|
||||||
|
const minLeft = viewportLeft + margin;
|
||||||
|
const maxLeft = viewportLeft + viewportWidth - margin - toolsRect.width;
|
||||||
|
const preferredLeft = frameRect.left + Math.min(8, Math.max(0, frameRect.width - toolsRect.width) / 2);
|
||||||
|
const clampedLeft = Math.max(minLeft, Math.min(Math.max(minLeft, maxLeft), preferredLeft));
|
||||||
|
tools.style.left = `${Math.floor(clampedLeft)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function repositionSelectedImageTools() {
|
||||||
|
const frame = selectedImageFrame();
|
||||||
|
if (frame) positionImageTools(frame);
|
||||||
|
}
|
||||||
|
|
||||||
function selectImageFrame(frame) {
|
function selectImageFrame(frame) {
|
||||||
if (!frame || !canEditDocument()) return;
|
if (!frame || !canEditDocument()) return;
|
||||||
if (selectedImageFrame() === frame && frame.querySelector(".image-alias-tools")) return;
|
if (selectedImageFrame() === frame && frame.querySelector(".image-alias-tools")) return;
|
||||||
@@ -1021,6 +1095,7 @@ export function startNoteEditor(adapter) {
|
|||||||
<label>W<input type="number" min="1" max="10000" step="1" value="${dimensions.width}" data-image-width-input aria-label="${escapeHtml(t("files.imageWidth", {}, "Image width"))}"></label>
|
<label>W<input type="number" min="1" max="10000" step="1" value="${dimensions.width}" data-image-width-input aria-label="${escapeHtml(t("files.imageWidth", {}, "Image width"))}"></label>
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
<label>H<input type="number" min="1" max="10000" step="1" value="${dimensions.height}" data-image-height-input aria-label="${escapeHtml(t("files.imageHeight", {}, "Image height"))}"></label>
|
<label>H<input type="number" min="1" max="10000" step="1" value="${dimensions.height}" data-image-height-input aria-label="${escapeHtml(t("files.imageHeight", {}, "Image height"))}"></label>
|
||||||
|
<label class="image-alias-aspect"><input type="checkbox" data-image-aspect-lock${keepImageAspectRatio ? " checked" : ""}>${escapeHtml(t("editor.keepAspectRatio", {}, "Keep proportions"))}</label>
|
||||||
<button type="button" data-image-size-apply>${escapeHtml(t("common.set", {}, "Set"))}</button>
|
<button type="button" data-image-size-apply>${escapeHtml(t("common.set", {}, "Set"))}</button>
|
||||||
<button type="button" data-image-size-reset>${escapeHtml(t("editor.layout.natural", {}, "Natural"))}</button>
|
<button type="button" data-image-size-reset>${escapeHtml(t("editor.layout.natural", {}, "Natural"))}</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -1030,6 +1105,7 @@ export function startNoteEditor(adapter) {
|
|||||||
resize.setAttribute("aria-label", t("editor.resizeImage", {}, "Resize image"));
|
resize.setAttribute("aria-label", t("editor.resizeImage", {}, "Resize image"));
|
||||||
resize.title = t("editor.dragResize", {}, "Drag to resize");
|
resize.title = t("editor.dragResize", {}, "Drag to resize");
|
||||||
frame.append(tools, resize);
|
frame.append(tools, resize);
|
||||||
|
requestAnimationFrame(() => positionImageTools(frame));
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateImageFrameAlias(frame, patch) {
|
function updateImageFrameAlias(frame, patch) {
|
||||||
@@ -1069,36 +1145,26 @@ export function startNoteEditor(adapter) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
const rect = frame.getBoundingClientRect();
|
const rect = frame.getBoundingClientRect();
|
||||||
const image = frame.querySelector("img");
|
const ratio = imageFrameAspectRatio(frame);
|
||||||
const storedWidth = Number(frame.dataset.imageWidth);
|
const keepAspectRatio = frame.querySelector("[data-image-aspect-lock]")?.checked !== false;
|
||||||
const storedHeight = Number(frame.dataset.imageHeight);
|
|
||||||
const ratio = storedWidth > 0 && storedHeight > 0
|
|
||||||
? storedWidth / storedHeight
|
|
||||||
: image?.naturalWidth > 0 && image?.naturalHeight > 0
|
|
||||||
? image.naturalWidth / image.naturalHeight
|
|
||||||
: Math.max(.01, rect.width / Math.max(1, rect.height));
|
|
||||||
const parentWidth = frame.parentElement?.getBoundingClientRect().width || preview.clientWidth;
|
const parentWidth = frame.parentElement?.getBoundingClientRect().width || preview.clientWidth;
|
||||||
const maxWidth = Math.max(1, Math.min(10000, Math.round(parentWidth)));
|
const maxWidth = Math.max(1, Math.min(10000, Math.round(parentWidth)));
|
||||||
const minWidth = Math.min(80, maxWidth);
|
const minWidth = Math.min(80, maxWidth);
|
||||||
const startX = event.clientX;
|
const startX = event.clientX;
|
||||||
|
const startY = event.clientY;
|
||||||
const startWidth = Math.max(minWidth, Math.round(rect.width));
|
const startWidth = Math.max(minWidth, Math.round(rect.width));
|
||||||
|
const startHeight = Math.max(1, Math.round(rect.height));
|
||||||
let width = startWidth;
|
let width = startWidth;
|
||||||
let height = Math.max(1, Math.round(width / ratio));
|
let height = startHeight;
|
||||||
|
|
||||||
handle.setPointerCapture(event.pointerId);
|
handle.setPointerCapture(event.pointerId);
|
||||||
frame.classList.add("is-resizing", "markdown-alias-image--sized");
|
frame.classList.add("is-resizing", "markdown-alias-image--sized");
|
||||||
const move = moveEvent => {
|
const move = moveEvent => {
|
||||||
width = Math.max(minWidth, Math.min(maxWidth, Math.round(startWidth + moveEvent.clientX - startX)));
|
width = Math.max(minWidth, Math.min(maxWidth, Math.round(startWidth + moveEvent.clientX - startX)));
|
||||||
height = Math.max(1, Math.min(10000, Math.round(width / ratio)));
|
height = keepAspectRatio
|
||||||
frame.dataset.imageWidth = String(width);
|
? Math.max(1, Math.min(10000, Math.round(width / ratio)))
|
||||||
frame.dataset.imageHeight = String(height);
|
: Math.max(1, Math.min(10000, Math.round(startHeight + moveEvent.clientY - startY)));
|
||||||
frame.style.setProperty("--image-width", `${width}px`);
|
previewImageFrameSize(frame, width, height);
|
||||||
frame.style.setProperty("--image-height", `${height}px`);
|
|
||||||
frame.style.setProperty("--image-aspect", `${width} / ${height}`);
|
|
||||||
const widthInput = frame.querySelector("[data-image-width-input]");
|
|
||||||
const heightInput = frame.querySelector("[data-image-height-input]");
|
|
||||||
if (widthInput) widthInput.value = String(width);
|
|
||||||
if (heightInput) heightInput.value = String(height);
|
|
||||||
};
|
};
|
||||||
const finish = () => {
|
const finish = () => {
|
||||||
frame.classList.remove("is-resizing");
|
frame.classList.remove("is-resizing");
|
||||||
@@ -1269,8 +1335,10 @@ export function startNoteEditor(adapter) {
|
|||||||
preview.addEventListener("load", invalidatePreviewPositions, true);
|
preview.addEventListener("load", invalidatePreviewPositions, true);
|
||||||
window.addEventListener("resize", invalidatePreviewPositions);
|
window.addEventListener("resize", invalidatePreviewPositions);
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
|
repositionSelectedImageTools();
|
||||||
if (virtualLayers.active) virtualLayers.sync();
|
if (virtualLayers.active) virtualLayers.sync();
|
||||||
});
|
});
|
||||||
|
window.addEventListener("scroll", repositionSelectedImageTools, { passive: true, capture: true });
|
||||||
document.fonts?.addEventListener?.("loadingdone", invalidatePreviewPositions);
|
document.fonts?.addEventListener?.("loadingdone", invalidatePreviewPositions);
|
||||||
function previewLinePositions() {
|
function previewLinePositions() {
|
||||||
const height = preview.scrollHeight, width = preview.clientWidth;
|
const height = preview.scrollHeight, width = preview.clientWidth;
|
||||||
@@ -1845,6 +1913,16 @@ export function startNoteEditor(adapter) {
|
|||||||
updateImageFrameAlias(frame, { width: null, height: null });
|
updateImageFrameAlias(frame, { width: null, height: null });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
preview.addEventListener("input", event => {
|
||||||
|
if (!(event.target instanceof HTMLInputElement)) return;
|
||||||
|
if (!event.target.matches("[data-image-width-input], [data-image-height-input]")) return;
|
||||||
|
const frame = event.target.closest(".markdown-alias-image");
|
||||||
|
if (frame) syncImageSizeInput(frame, event.target);
|
||||||
|
});
|
||||||
|
preview.addEventListener("change", event => {
|
||||||
|
if (!(event.target instanceof HTMLInputElement) || !event.target.matches("[data-image-aspect-lock]")) return;
|
||||||
|
keepImageAspectRatio = event.target.checked;
|
||||||
|
});
|
||||||
preview.addEventListener("keydown", event => {
|
preview.addEventListener("keydown", event => {
|
||||||
const frame = event.target instanceof Element ? event.target.closest(".markdown-alias-image") : null;
|
const frame = event.target instanceof Element ? event.target.closest(".markdown-alias-image") : null;
|
||||||
if (!frame) return;
|
if (!frame) return;
|
||||||
|
|||||||
+40
-29
@@ -317,11 +317,18 @@ function groupHeadingsWithLead(root) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markAtomicTextBlocks(root) {
|
||||||
|
root.querySelectorAll("h1, h2, h3, h4, h5, h6, p, blockquote, dt, dd").forEach(node => {
|
||||||
|
if (!node.closest(".pdf-keep-unit")) node.classList.add("pdf-atomic-text");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function preparePrintPagination(root) {
|
function preparePrintPagination(root) {
|
||||||
collapseHeadingGaps(root);
|
collapseHeadingGaps(root);
|
||||||
markShortKeepBlocks(root);
|
markShortKeepBlocks(root);
|
||||||
groupAtomicVisuals(root);
|
groupAtomicVisuals(root);
|
||||||
groupHeadingsWithLead(root);
|
groupHeadingsWithLead(root);
|
||||||
|
markAtomicTextBlocks(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensurePrintableMermaid(root, darkMode = false) {
|
async function ensurePrintableMermaid(root, darkMode = false) {
|
||||||
@@ -446,8 +453,17 @@ function alignFooterToLastPage(doc, pageHeightMm, marginMm) {
|
|||||||
const minGap = 4 * pxPerMm;
|
const minGap = 4 * pxPerMm;
|
||||||
if (!Number.isFinite(pageContentHeight) || pageContentHeight <= footerHeight || footerHeight <= 0) return;
|
if (!Number.isFinite(pageContentHeight) || pageContentHeight <= footerHeight || footerHeight <= 0) return;
|
||||||
|
|
||||||
const measuredFill = measureLastPageFill(doc, documentRoot, pageContentHeight);
|
// The multicolumn pagination probe is only needed when the document contains
|
||||||
const positionOnPage = measuredFill ?? fallbackLastPageFill(documentRoot, spacer, pageContentHeight);
|
// an explicit PDF page break. For ordinary flowing documents Chromium's
|
||||||
|
// column layout can report the sentinel at the end of a full column, which
|
||||||
|
// incorrectly makes the footer look as if it no longer fits and creates an
|
||||||
|
// otherwise empty extra page containing only the footer.
|
||||||
|
const fallbackFill = fallbackLastPageFill(documentRoot, spacer, pageContentHeight);
|
||||||
|
const hasExplicitPageBreak = Boolean(doc.querySelector(".pdf-content .pdf-page-break"));
|
||||||
|
const measuredFill = hasExplicitPageBreak
|
||||||
|
? measureLastPageFill(doc, documentRoot, pageContentHeight)
|
||||||
|
: null;
|
||||||
|
const positionOnPage = measuredFill ?? fallbackFill;
|
||||||
const available = Math.max(0, pageContentHeight - positionOnPage);
|
const available = Math.max(0, pageContentHeight - positionOnPage);
|
||||||
|
|
||||||
const spacerHeight = available >= footerHeight + minGap + bottomGuard
|
const spacerHeight = available >= footerHeight + minGap + bottomGuard
|
||||||
@@ -464,7 +480,7 @@ function fitOversizedKeepUnits(doc, pageHeightMm, marginMm) {
|
|||||||
const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * pxPerMm;
|
const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * pxPerMm;
|
||||||
const maxHeight = Math.max(40, pageContentHeight - Math.max(2, pxPerMm));
|
const maxHeight = Math.max(40, pageContentHeight - Math.max(2, pxPerMm));
|
||||||
|
|
||||||
root.querySelectorAll(".pdf-keep-unit").forEach(unit => {
|
root.querySelectorAll(".pdf-keep-unit, .pdf-keep-short, .pdf-atomic-text").forEach(unit => {
|
||||||
unit.classList.remove("pdf-keep-unit--oversized");
|
unit.classList.remove("pdf-keep-unit--oversized");
|
||||||
let rect = unit.getBoundingClientRect();
|
let rect = unit.getBoundingClientRect();
|
||||||
if (rect.height <= maxHeight) return;
|
if (rect.height <= maxHeight) return;
|
||||||
@@ -659,25 +675,13 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
|||||||
break-after: avoid-page;
|
break-after: avoid-page;
|
||||||
page-break-after: avoid;
|
page-break-after: avoid;
|
||||||
}
|
}
|
||||||
.pdf-content h1,
|
|
||||||
.pdf-content h2,
|
|
||||||
.pdf-content h3,
|
|
||||||
.pdf-content h4,
|
|
||||||
.pdf-content h5,
|
|
||||||
.pdf-content h6,
|
|
||||||
.pdf-content blockquote,
|
|
||||||
.pdf-content dd,
|
|
||||||
.pdf-content .footnotes li {
|
.pdf-content .footnotes li {
|
||||||
break-inside: avoid-page;
|
break-inside: auto;
|
||||||
page-break-inside: avoid;
|
page-break-inside: auto;
|
||||||
}
|
}
|
||||||
.pdf-content p,
|
.pdf-content p,
|
||||||
.pdf-content li,
|
.pdf-content li,
|
||||||
.pdf-content dd { orphans: 3; widows: 3; }
|
.pdf-content dd { orphans: 3; widows: 3; }
|
||||||
.pdf-content p {
|
|
||||||
break-inside: avoid-page;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-content ul,
|
.pdf-content ul,
|
||||||
.pdf-content ol,
|
.pdf-content ol,
|
||||||
@@ -693,8 +697,8 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
|||||||
page-break-inside: auto;
|
page-break-inside: auto;
|
||||||
}
|
}
|
||||||
.pdf-content li {
|
.pdf-content li {
|
||||||
break-inside: avoid-page;
|
break-inside: auto;
|
||||||
page-break-inside: avoid;
|
page-break-inside: auto;
|
||||||
}
|
}
|
||||||
.pdf-content li:has(> ul),
|
.pdf-content li:has(> ul),
|
||||||
.pdf-content li:has(> ol) {
|
.pdf-content li:has(> ol) {
|
||||||
@@ -736,31 +740,38 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
|||||||
-webkit-box-decoration-break: clone;
|
-webkit-box-decoration-break: clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-content .pdf-keep-unit {
|
.pdf-content .pdf-keep-unit,
|
||||||
|
.pdf-content .pdf-atomic-text,
|
||||||
|
.pdf-content .pdf-keep-short {
|
||||||
/*
|
/*
|
||||||
* Chromium can leave a 1-2 px fragment of an avoided block on the
|
* Chromium can paint a thin fragment of a block at the bottom of the
|
||||||
* previous page before moving the real block to the next page.
|
* previous page before moving the actual block to the next page.
|
||||||
* Making the keep unit an atomic inline-block prevents that ghost
|
* Atomic inline-blocks prevent that paint fragment instead of relying
|
||||||
* fragment while preserving book-like pagination.
|
* only on break-inside: avoid, which triggers the Chromium artifact.
|
||||||
*/
|
*/
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
break-inside: avoid-page;
|
break-inside: avoid-page;
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
|
.pdf-content dd.pdf-atomic-text {
|
||||||
|
width: calc(100% - 1.5em);
|
||||||
|
max-width: calc(100% - 1.5em);
|
||||||
|
}
|
||||||
.pdf-content .pdf-keep-unit > :first-child { margin-top: 0 !important; }
|
.pdf-content .pdf-keep-unit > :first-child { margin-top: 0 !important; }
|
||||||
.pdf-content .pdf-keep-unit > :last-child { margin-bottom: 0 !important; }
|
.pdf-content .pdf-keep-unit > :last-child { margin-bottom: 0 !important; }
|
||||||
.pdf-content .pdf-keep-unit.pdf-keep-unit--oversized {
|
.pdf-content .pdf-keep-unit.pdf-keep-unit--oversized,
|
||||||
|
.pdf-content .pdf-atomic-text.pdf-keep-unit--oversized,
|
||||||
|
.pdf-content .pdf-keep-short.pdf-keep-unit--oversized {
|
||||||
display: block;
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-width: none;
|
||||||
break-inside: auto;
|
break-inside: auto;
|
||||||
page-break-inside: auto;
|
page-break-inside: auto;
|
||||||
}
|
}
|
||||||
.pdf-content .pdf-keep-short {
|
|
||||||
break-inside: avoid-page !important;
|
|
||||||
page-break-inside: avoid !important;
|
|
||||||
}
|
|
||||||
.pdf-content .pdf-heading-lead > h1,
|
.pdf-content .pdf-heading-lead > h1,
|
||||||
.pdf-content .pdf-heading-lead > h2,
|
.pdf-content .pdf-heading-lead > h2,
|
||||||
.pdf-content .pdf-heading-lead > h3 { margin-bottom: .18em; }
|
.pdf-content .pdf-heading-lead > h3 { margin-bottom: .18em; }
|
||||||
|
|||||||
Reference in new issue
Block a user