This commit is contained in:
Mateusz Gruszczyński committed 2026-10-29 15:20:35 +01:00
1 parent d36a4b2451
commit 99a5700caa
8 files changed
+176 -56

No files matched your search

+97 -19
View File
@@ -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;