chore(right_pane): more advanced expand/collapse

This commit is contained in:
Elian Doran 2025-12-18 14:57:42 +02:00
parent 5dacfd3ac6
commit ea3222cf12
No known key found for this signature in database
2 changed files with 149 additions and 61 deletions

View File

@ -1,8 +1,9 @@
import { dayjs } from "@triliumnext/commons";
import type { ViewMode, ViewScope } from "./link.js";
import FNote from "../entities/fnote";
import { snapdom } from "@zumer/snapdom";
import FNote from "../entities/fnote";
import type { ViewMode, ViewScope } from "./link.js";
const SVG_MIME = "image/svg+xml";
export const isShare = !window.glob;
@ -113,9 +114,9 @@ function formatDateISO(date: Date) {
export function formatDateTime(date: Date, userSuppliedFormat?: string): string {
if (userSuppliedFormat?.trim()) {
return dayjs(date).format(userSuppliedFormat);
} else {
return `${formatDate(date)} ${formatTime(date)}`;
}
}
return `${formatDate(date)} ${formatTime(date)}`;
}
function localNowDateTime() {
@ -191,9 +192,9 @@ export function formatSize(size: number | null | undefined) {
if (size < 1024) {
return `${size} KiB`;
} else {
return `${Math.round(size / 102.4) / 10} MiB`;
}
}
return `${Math.round(size / 102.4) / 10} MiB`;
}
function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
@ -297,18 +298,18 @@ function formatHtml(html: string) {
let indent = "\n";
const tab = "\t";
let i = 0;
let pre: { indent: string; tag: string }[] = [];
const pre: { indent: string; tag: string }[] = [];
html = html
.replace(new RegExp("<pre>([\\s\\S]+?)?</pre>"), function (x) {
.replace(new RegExp("<pre>([\\s\\S]+?)?</pre>"), (x) => {
pre.push({ indent: "", tag: x });
return "<--TEMPPRE" + i++ + "/-->";
return `<--TEMPPRE${ i++ }/-->`;
})
.replace(new RegExp("<[^<>]+>[^<]?", "g"), function (x) {
.replace(new RegExp("<[^<>]+>[^<]?", "g"), (x) => {
let ret;
const tagRegEx = /<\/?([^\s/>]+)/.exec(x);
let tag = tagRegEx ? tagRegEx[1] : "";
let p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x);
const tag = tagRegEx ? tagRegEx[1] : "";
const p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x);
if (p) {
const pInd = parseInt(p[1]);
@ -318,24 +319,22 @@ function formatHtml(html: string) {
if (["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"].indexOf(tag) >= 0) {
// self closing tag
ret = indent + x;
} else if (x.indexOf("</") < 0) {
//open tag
if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length);
else ret = indent + x;
!p && (indent += tab);
} else {
if (x.indexOf("</") < 0) {
//open tag
if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length);
else ret = indent + x;
!p && (indent += tab);
} else {
//close tag
indent = indent.substr(0, indent.length - 1);
if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length);
else ret = indent + x;
}
//close tag
indent = indent.substr(0, indent.length - 1);
if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length);
else ret = indent + x;
}
return ret;
});
for (i = pre.length; i--;) {
html = html.replace("<--TEMPPRE" + i + "/-->", pre[i].tag.replace("<pre>", "<pre>\n").replace("</pre>", pre[i].indent + "</pre>"));
html = html.replace(`<--TEMPPRE${ i }/-->`, pre[i].tag.replace("<pre>", "<pre>\n").replace("</pre>", `${pre[i].indent }</pre>`));
}
return html.charAt(0) === "\n" ? html.substr(1, html.length - 1) : html;
@ -364,11 +363,11 @@ type dynamicRequireMappings = {
export function dynamicRequire<T extends keyof dynamicRequireMappings>(moduleName: T): Awaited<dynamicRequireMappings[T]>{
if (typeof __non_webpack_require__ !== "undefined") {
return __non_webpack_require__(moduleName);
} else {
// explicitly pass as string and not as expression to suppress webpack warning
// 'Critical dependency: the request of a dependency is an expression'
return require(`${moduleName}`);
}
}
// explicitly pass as string and not as expression to suppress webpack warning
// 'Critical dependency: the request of a dependency is an expression'
return require(`${moduleName}`);
}
function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string) {
@ -509,8 +508,8 @@ export function escapeRegExp(str: string) {
function areObjectsEqual(...args: unknown[]) {
let i;
let l;
let leftChain: Object[];
let rightChain: Object[];
let leftChain: object[];
let rightChain: object[];
function compare2Objects(x: unknown, y: unknown) {
let p;
@ -695,9 +694,9 @@ async function downloadAsSvg(nameWithoutExtension: string, svgSource: string | S
try {
const result = await snapdom(element, {
backgroundColor: "transparent",
scale: 2
});
backgroundColor: "transparent",
scale: 2
});
triggerDownload(`${nameWithoutExtension}.svg`, result.url);
} finally {
cleanup();
@ -733,9 +732,9 @@ async function downloadAsPng(nameWithoutExtension: string, svgSource: string | S
try {
const result = await snapdom(element, {
backgroundColor: "transparent",
scale: 2
});
backgroundColor: "transparent",
scale: 2
});
const pngImg = await result.toPng();
await triggerDownload(`${nameWithoutExtension}.png`, pngImg.src);
} finally {
@ -763,11 +762,11 @@ export function getSizeFromSvg(svgContent: string) {
return {
width: parseFloat(width),
height: parseFloat(height)
}
} else {
console.warn("SVG export error", svgDocument.documentElement);
return null;
}
};
}
console.warn("SVG export error", svgDocument.documentElement);
return null;
}
/**
@ -896,9 +895,9 @@ export function mapToKeyValueArray<K extends string | number | symbol, V>(map: R
export function getErrorMessage(e: unknown) {
if (e && typeof e === "object" && "message" in e && typeof e.message === "string") {
return e.message;
} else {
return "Unknown error";
}
}
return "Unknown error";
}
/**
@ -913,6 +912,12 @@ export function handleRightToLeftPlacement<T extends string>(placement: T) {
return placement;
}
export function clamp(value: number, min: number, max: number) {
if (value < min) return min;
if (value > max) return max;
return value;
}
export default {
reloadFrontendApp,
restartDesktopApp,

View File

@ -7,6 +7,7 @@ import { useEffect, useRef } from "preact/hooks";
import options from "../../services/options";
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer";
import { clamp } from "../../services/utils";
import HighlightsList from "./HighlightsList";
import TableOfContents from "./TableOfContents";
@ -64,31 +65,113 @@ export default function RightPanelContainer() {
const children = Array.from(rightPaneEl?.querySelectorAll(":scope > .card") ?? []);
const pos = children.indexOf(cardEl);
if (pos === -1) return;
const sizes = splitInstance.getSizes();
const sizes = splitInstance.getSizes(); // percentages
const COLLAPSED_SIZE = 0; // keep your current behavior; consider a small min later
// Choose recipients/donors: nearest expanded panes first; if none, all except pos.
const recipients = getRecipientsByDistance(sizes, pos, COLLAPSED_SIZE);
const fallback = getExpandedIndices(sizes, pos, -Infinity); // all other panes
const targets = recipients.length ? recipients : fallback;
if (!expanded) {
const sizeBeforeCollapse = sizes[pos];
sizesBeforeCollapse.current.set(cardEl, sizeBeforeCollapse);
sizes[pos] = 0;
const itemToExpand = pos > 0 ? pos - 1 : pos + 1;
if (sizes[itemToExpand] > COLLAPSED_SIZE) {
sizes[itemToExpand] += sizeBeforeCollapse;
}
// Collapse
sizes[pos] = COLLAPSED_SIZE;
// Give freed space to other panes
const freed = sizeBeforeCollapse - COLLAPSED_SIZE;
distributeInto(sizes, targets, freed);
} else {
const itemToExpand = pos > 0 ? pos - 1 : pos + 1;
const sizeBeforeCollapse = sizesBeforeCollapse.current.get(cardEl) ?? 50;
const want = sizesBeforeCollapse.current.get(cardEl) ?? 50;
if (sizes[itemToExpand] > COLLAPSED_SIZE) {
sizes[itemToExpand] -= sizeBeforeCollapse;
}
sizes[pos] = sizeBeforeCollapse;
// Take space back from other panes to expand this one
const took = takeFrom(sizes, targets, want);
sizes[pos] = COLLAPSED_SIZE + took; // if donors couldn't provide all, expand partially
}
console.log("Set sizes to ", sizes);
// Optional: tiny cleanup to avoid negatives / floating drift
for (let i = 0; i < sizes.length; i++) sizes[i] = clamp(sizes[i], 0, 100);
// Normalize to sum to 100 (Split.js likes this)
const sum = sizes.reduce((a, b) => a + b, 0);
if (sum > 0) {
for (let i = 0; i < sizes.length; i++) sizes[i] = (sizes[i] / sum) * 100;
}
splitInstance.setSizes(sizes);
},
}
}}>
{items}
</RightPanelContext.Provider>
</div>
);
}
function getExpandedIndices(sizes, skipIndex, COLLAPSED_SIZE) {
const idxs = [];
for (let i = 0; i < sizes.length; i++) {
if (i === skipIndex) continue;
if (sizes[i] > COLLAPSED_SIZE) idxs.push(i);
}
return idxs;
}
// Prefer nearby panes (VS Code-ish). Falls back to "all expanded panes".
function getRecipientsByDistance(sizes, pos, COLLAPSED_SIZE) {
const recipients = [];
for (let d = 1; d < sizes.length; d++) {
const left = pos - d;
const right = pos + d;
if (left >= 0 && sizes[left] > COLLAPSED_SIZE) recipients.push(left);
if (right < sizes.length && sizes[right] > COLLAPSED_SIZE) recipients.push(right);
}
return recipients;
}
// Distribute `amount` into `recipients` proportionally to their current sizes.
function distributeInto(sizes, recipients, amount) {
if (amount === 0 || recipients.length === 0) return;
const total = recipients.reduce((sum, i) => sum + sizes[i], 0);
if (total <= 0) {
// equal split fallback
const delta = amount / recipients.length;
recipients.forEach(i => (sizes[i] += delta));
return;
}
recipients.forEach(i => {
const share = (sizes[i] / total) * amount;
sizes[i] += share;
});
}
// Take `amount` out of `donors` proportionally, without driving anyone below 0.
// Returns how much was actually taken.
function takeFrom(sizes, donors, amount) {
if (amount <= 0 || donors.length === 0) return 0;
// max each donor can contribute (dont go below 0 here; you can change min if you want)
const caps = donors.map(i => ({ i, cap: Math.max(0, sizes[i]) }));
let remaining = amount;
// iterative proportional take with caps
for (let iter = 0; iter < 5 && remaining > 1e-9; iter++) {
const active = caps.filter(x => x.cap > 1e-9);
if (active.length === 0) break;
const total = active.reduce((s, x) => s + sizes[x.i], 0) || active.length;
for (const x of active) {
const weight = total === active.length ? 1 / active.length : (sizes[x.i] / total);
const want = remaining * weight;
const took = Math.min(x.cap, want);
sizes[x.i] -= took;
x.cap -= took;
remaining -= took;
if (remaining <= 1e-9) break;
}
}
return amount - remaining;
}