mirror of
https://github.com/zadam/trilium.git
synced 2026-01-08 23:54:29 +01:00
chore(right_pane): more advanced expand/collapse
This commit is contained in:
parent
5dacfd3ac6
commit
ea3222cf12
@ -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,
|
||||
|
||||
@ -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 (don’t 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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user