v0.2.97
This commit is contained in:
1 parent
d36a4b2451
commit
99a5700caa
8 files changed
+176
-56
No files matched your search
+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) {
|
||||
if (!frame || !canEditDocument()) 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>
|
||||
<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 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-reset>${escapeHtml(t("editor.layout.natural", {}, "Natural"))}</button>
|
||||
</div>`;
|
||||
@@ -1030,6 +1105,7 @@ export function startNoteEditor(adapter) {
|
||||
resize.setAttribute("aria-label", t("editor.resizeImage", {}, "Resize image"));
|
||||
resize.title = t("editor.dragResize", {}, "Drag to resize");
|
||||
frame.append(tools, resize);
|
||||
requestAnimationFrame(() => positionImageTools(frame));
|
||||
}
|
||||
|
||||
function updateImageFrameAlias(frame, patch) {
|
||||
@@ -1069,36 +1145,26 @@ export function startNoteEditor(adapter) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
const rect = frame.getBoundingClientRect();
|
||||
const image = frame.querySelector("img");
|
||||
const storedWidth = Number(frame.dataset.imageWidth);
|
||||
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 ratio = imageFrameAspectRatio(frame);
|
||||
const keepAspectRatio = frame.querySelector("[data-image-aspect-lock]")?.checked !== false;
|
||||
const parentWidth = frame.parentElement?.getBoundingClientRect().width || preview.clientWidth;
|
||||
const maxWidth = Math.max(1, Math.min(10000, Math.round(parentWidth)));
|
||||
const minWidth = Math.min(80, maxWidth);
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startWidth = Math.max(minWidth, Math.round(rect.width));
|
||||
const startHeight = Math.max(1, Math.round(rect.height));
|
||||
let width = startWidth;
|
||||
let height = Math.max(1, Math.round(width / ratio));
|
||||
let height = startHeight;
|
||||
|
||||
handle.setPointerCapture(event.pointerId);
|
||||
frame.classList.add("is-resizing", "markdown-alias-image--sized");
|
||||
const move = moveEvent => {
|
||||
width = Math.max(minWidth, Math.min(maxWidth, Math.round(startWidth + moveEvent.clientX - startX)));
|
||||
height = Math.max(1, Math.min(10000, Math.round(width / ratio)));
|
||||
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);
|
||||
height = keepAspectRatio
|
||||
? Math.max(1, Math.min(10000, Math.round(width / ratio)))
|
||||
: Math.max(1, Math.min(10000, Math.round(startHeight + moveEvent.clientY - startY)));
|
||||
previewImageFrameSize(frame, width, height);
|
||||
};
|
||||
const finish = () => {
|
||||
frame.classList.remove("is-resizing");
|
||||
@@ -1269,8 +1335,10 @@ export function startNoteEditor(adapter) {
|
||||
preview.addEventListener("load", invalidatePreviewPositions, true);
|
||||
window.addEventListener("resize", invalidatePreviewPositions);
|
||||
window.addEventListener("resize", () => {
|
||||
repositionSelectedImageTools();
|
||||
if (virtualLayers.active) virtualLayers.sync();
|
||||
});
|
||||
window.addEventListener("scroll", repositionSelectedImageTools, { passive: true, capture: true });
|
||||
document.fonts?.addEventListener?.("loadingdone", invalidatePreviewPositions);
|
||||
function previewLinePositions() {
|
||||
const height = preview.scrollHeight, width = preview.clientWidth;
|
||||
@@ -1845,6 +1913,16 @@ export function startNoteEditor(adapter) {
|
||||
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 => {
|
||||
const frame = event.target instanceof Element ? event.target.closest(".markdown-alias-image") : null;
|
||||
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) {
|
||||
collapseHeadingGaps(root);
|
||||
markShortKeepBlocks(root);
|
||||
groupAtomicVisuals(root);
|
||||
groupHeadingsWithLead(root);
|
||||
markAtomicTextBlocks(root);
|
||||
}
|
||||
|
||||
async function ensurePrintableMermaid(root, darkMode = false) {
|
||||
@@ -446,8 +453,17 @@ function alignFooterToLastPage(doc, pageHeightMm, marginMm) {
|
||||
const minGap = 4 * pxPerMm;
|
||||
if (!Number.isFinite(pageContentHeight) || pageContentHeight <= footerHeight || footerHeight <= 0) return;
|
||||
|
||||
const measuredFill = measureLastPageFill(doc, documentRoot, pageContentHeight);
|
||||
const positionOnPage = measuredFill ?? fallbackLastPageFill(documentRoot, spacer, pageContentHeight);
|
||||
// The multicolumn pagination probe is only needed when the document contains
|
||||
// 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 spacerHeight = available >= footerHeight + minGap + bottomGuard
|
||||
@@ -464,7 +480,7 @@ function fitOversizedKeepUnits(doc, pageHeightMm, marginMm) {
|
||||
const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * 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");
|
||||
let rect = unit.getBoundingClientRect();
|
||||
if (rect.height <= maxHeight) return;
|
||||
@@ -659,25 +675,13 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
||||
break-after: avoid-page;
|
||||
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 {
|
||||
break-inside: avoid-page;
|
||||
page-break-inside: avoid;
|
||||
break-inside: auto;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
.pdf-content p,
|
||||
.pdf-content li,
|
||||
.pdf-content dd { orphans: 3; widows: 3; }
|
||||
.pdf-content p {
|
||||
break-inside: avoid-page;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.pdf-content ul,
|
||||
.pdf-content ol,
|
||||
@@ -693,8 +697,8 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
||||
page-break-inside: auto;
|
||||
}
|
||||
.pdf-content li {
|
||||
break-inside: avoid-page;
|
||||
page-break-inside: avoid;
|
||||
break-inside: auto;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
.pdf-content li:has(> ul),
|
||||
.pdf-content li:has(> ol) {
|
||||
@@ -736,31 +740,38 @@ function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, darkMode) {
|
||||
-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
|
||||
* previous page before moving the real block to the next page.
|
||||
* Making the keep unit an atomic inline-block prevents that ghost
|
||||
* fragment while preserving book-like pagination.
|
||||
* Chromium can paint a thin fragment of a block at the bottom of the
|
||||
* previous page before moving the actual block to the next page.
|
||||
* Atomic inline-blocks prevent that paint fragment instead of relying
|
||||
* only on break-inside: avoid, which triggers the Chromium artifact.
|
||||
*/
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
break-inside: avoid-page;
|
||||
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 > :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;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
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 > h2,
|
||||
.pdf-content .pdf-heading-lead > h3 { margin-bottom: .18em; }
|
||||
|
||||
Reference in new issue
Block a user