mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-29 12:33:20 +00:00
Refine status item placement and inactive styling
This commit is contained in:
1 parent
68dc40ff05
commit
e7f7e62cfa
9 files changed
+345
-68
No files matched your search
@@ -161,7 +161,7 @@ export function projectReplicationStatusLabel(
|
||||
fastForwardCount > 0
|
||||
? fastForwardCount
|
||||
: undefined;
|
||||
const fastForwardSuffix = visibleFastForwardCount === undefined ? "" : " (⏩ " + visibleFastForwardCount + ")";
|
||||
const fastForwardLabel = visibleFastForwardCount === undefined ? "" : " (⏩ " + visibleFastForwardCount + ")";
|
||||
|
||||
const parts: StatusBarPart[] = [];
|
||||
if (networkStatus.trackedRequestCount > 0) {
|
||||
@@ -177,10 +177,6 @@ export function projectReplicationStatusLabel(
|
||||
parts.push({ key: "network", text: networkActivity });
|
||||
}
|
||||
parts.push({ key: "sync", text: "Sync: " + icon });
|
||||
parts.push({ key: "sent", text: " ↑ ", value: sent });
|
||||
if (pushRemaining.part !== undefined) parts.push(pushRemaining.part);
|
||||
parts.push({ key: "arrived", text: " ↓ ", value: arrived });
|
||||
if (pullRemaining.part !== undefined) parts.push(pullRemaining.part);
|
||||
if (visibleFastForwardCount !== undefined) {
|
||||
parts.push({
|
||||
key: "ff",
|
||||
@@ -190,6 +186,10 @@ export function projectReplicationStatusLabel(
|
||||
title: "Compared CouchDB feed entries",
|
||||
});
|
||||
}
|
||||
parts.push({ key: "sent", text: " ↑ ", value: sent });
|
||||
if (pushRemaining.part !== undefined) parts.push(pushRemaining.part);
|
||||
parts.push({ key: "arrived", text: " ↓ ", value: arrived });
|
||||
if (pullRemaining.part !== undefined) parts.push(pullRemaining.part);
|
||||
|
||||
return {
|
||||
icon,
|
||||
@@ -197,13 +197,13 @@ export function projectReplicationStatusLabel(
|
||||
networkActivity +
|
||||
"Sync: " +
|
||||
icon +
|
||||
fastForwardLabel +
|
||||
" ↑ " +
|
||||
sent +
|
||||
pushRemaining.text +
|
||||
" ↓ " +
|
||||
arrived +
|
||||
pullRemaining.text +
|
||||
fastForwardSuffix,
|
||||
pullRemaining.text,
|
||||
parts,
|
||||
};
|
||||
}
|
||||
@@ -126,8 +126,9 @@ describe("projectReplicationStatusLabel", () => {
|
||||
1200
|
||||
);
|
||||
expect(transferred.icon).toBe("⚡");
|
||||
expect(transferred.message).toBe("Sync: ⚡ ↑ 0 ↓ 7 (⏩ 1200)");
|
||||
expect(transferred.parts[transferred.parts.length - 1]).toMatchObject({
|
||||
expect(transferred.message).toBe("Sync: ⚡ (⏩ 1200) ↑ 0 ↓ 7");
|
||||
expect(transferred.parts.map((part) => part.key)).toEqual(["sync", "ff", "sent", "arrived"]);
|
||||
expect(transferred.parts[1]).toMatchObject({
|
||||
key: "ff",
|
||||
text: " (⏩ ",
|
||||
value: 1200,
|
||||
@@ -144,7 +145,7 @@ describe("projectReplicationStatusLabel", () => {
|
||||
1200
|
||||
);
|
||||
expect(idle.icon).toBe("⚡");
|
||||
expect(idle.message).toBe("Sync: ⚡ ↑ 0 ↓ 7 (⏩ 1200)");
|
||||
expect(idle.message).toBe("Sync: ⚡ (⏩ 1200) ↑ 0 ↓ 7");
|
||||
});
|
||||
|
||||
it("keeps the error icon ahead of an active CouchDB checking indicator", () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const INACTIVE_CLASS = "livesync-stat-inactive";
|
||||
|
||||
export type StatusBarPart = {
|
||||
key: string;
|
||||
text: string;
|
||||
@@ -22,7 +24,6 @@ type RenderedItem = {
|
||||
|
||||
export function createStatusBarRenderer(parent: HTMLElement): StatusBarRenderer {
|
||||
const items = new Map<string, RenderedItem>();
|
||||
const managedElements = new Set<HTMLSpanElement>();
|
||||
let disposed = false;
|
||||
|
||||
const setText = (node: Text, text: string) => {
|
||||
@@ -33,6 +34,13 @@ export function createStatusBarRenderer(parent: HTMLElement): StatusBarRenderer
|
||||
if (element.hidden !== hidden) element.hidden = hidden;
|
||||
};
|
||||
|
||||
const setInactive = (element: HTMLElement, inactive: boolean) => {
|
||||
if (element.classList.contains(INACTIVE_CLASS) === inactive) return;
|
||||
element.classList.toggle(INACTIVE_CLASS, inactive);
|
||||
if (inactive) element.setAttribute("aria-hidden", "true");
|
||||
else element.removeAttribute("aria-hidden");
|
||||
};
|
||||
|
||||
const setWidth = (element: HTMLElement, width: string) => {
|
||||
if (element.style.getPropertyValue("--livesync-stat-width") !== width) {
|
||||
element.setCssProps({ "--livesync-stat-width": width });
|
||||
@@ -58,7 +66,6 @@ export function createStatusBarRenderer(parent: HTMLElement): StatusBarRenderer
|
||||
|
||||
const item = { element, prefix, value, valueText, suffix, widestValueLength: 0 };
|
||||
items.set(key, item);
|
||||
managedElements.add(element);
|
||||
return item;
|
||||
};
|
||||
|
||||
@@ -89,20 +96,16 @@ export function createStatusBarRenderer(parent: HTMLElement): StatusBarRenderer
|
||||
};
|
||||
|
||||
const reorderIfNeeded = (visibleItems: readonly RenderedItem[]) => {
|
||||
const desiredOrder = visibleItems.map((item) => item.element);
|
||||
const visibleElements = new Set(desiredOrder);
|
||||
const currentOrder = Array.from(parent.childNodes).filter((node): node is HTMLSpanElement =>
|
||||
managedElements.has(node as HTMLSpanElement)
|
||||
visibleElements.has(node as HTMLSpanElement)
|
||||
);
|
||||
const visibleElements = new Set(visibleItems.map((item) => item.element));
|
||||
const desiredOrder = [
|
||||
...visibleItems.map((item) => item.element),
|
||||
...currentOrder.filter((element) => !visibleElements.has(element)),
|
||||
];
|
||||
if (
|
||||
currentOrder.length === desiredOrder.length &&
|
||||
currentOrder.every((node, index) => node === desiredOrder[index])
|
||||
) {
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
let reference: ChildNode | null = currentOrder[0] ?? null;
|
||||
for (const element of desiredOrder) {
|
||||
@@ -110,7 +113,7 @@ export function createStatusBarRenderer(parent: HTMLElement): StatusBarRenderer
|
||||
const childNodes = Array.from(parent.childNodes);
|
||||
const elementIndex = childNodes.indexOf(element);
|
||||
reference =
|
||||
childNodes.slice(elementIndex + 1).find((node) => managedElements.has(node as HTMLSpanElement)) ?? null;
|
||||
childNodes.slice(elementIndex + 1).find((node) => visibleElements.has(node as HTMLSpanElement)) ?? null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,17 +126,16 @@ export function createStatusBarRenderer(parent: HTMLElement): StatusBarRenderer
|
||||
for (const part of parts) {
|
||||
let item = items.get(part.key);
|
||||
if (!item) item = createItem(part.key);
|
||||
if (item.element.classList.contains(INACTIVE_CLASS)) item.widestValueLength = 0;
|
||||
updateItem(item, part);
|
||||
setHidden(item.element, false);
|
||||
setInactive(item.element, false);
|
||||
visibleItems.push(item);
|
||||
visibleKeys.add(part.key);
|
||||
}
|
||||
|
||||
for (const [key, item] of items) {
|
||||
if (visibleKeys.has(key)) continue;
|
||||
setHidden(item.element, true);
|
||||
setWidth(item.value, "");
|
||||
item.widestValueLength = 0;
|
||||
setInactive(item.element, true);
|
||||
}
|
||||
|
||||
reorderIfNeeded(visibleItems);
|
||||
@@ -145,7 +147,6 @@ export function createStatusBarRenderer(parent: HTMLElement): StatusBarRenderer
|
||||
if (item.element.parentNode === parent) parent.removeChild(item.element);
|
||||
}
|
||||
items.clear();
|
||||
managedElements.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -66,6 +66,33 @@ class TestElement {
|
||||
private currentClassName = "";
|
||||
private currentHidden = false;
|
||||
private currentTitle = "";
|
||||
private attributes = new Map<string, string>();
|
||||
readonly classList = {
|
||||
contains: (token: string): boolean => this.currentClassName.split(" ").includes(token),
|
||||
toggle: (token: string, force: boolean): boolean => {
|
||||
const tokens = this.currentClassName.split(" ").filter(Boolean);
|
||||
const present = tokens.includes(token);
|
||||
if (present !== force) {
|
||||
this.className = force
|
||||
? [...tokens, token].join(" ")
|
||||
: tokens.filter((item) => item !== token).join(" ");
|
||||
}
|
||||
return force;
|
||||
},
|
||||
};
|
||||
|
||||
getAttribute(name: string): string | null {
|
||||
return this.attributes.get(name) ?? null;
|
||||
}
|
||||
|
||||
setAttribute(name: string, value: string) {
|
||||
this.document.mutations++;
|
||||
this.attributes.set(name, value);
|
||||
}
|
||||
|
||||
removeAttribute(name: string) {
|
||||
if (this.attributes.delete(name)) this.document.mutations++;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly document: TestDocument,
|
||||
@@ -173,7 +200,9 @@ function findValue(item: TestElement): TestElement {
|
||||
}
|
||||
|
||||
function visibleItems(root: TestElement): TestElement[] {
|
||||
return root.children.filter((child) => !child.hidden && child.className.split(" ").includes("livesync-stat-item"));
|
||||
return root.children.filter(
|
||||
(child) => !child.classList.contains("livesync-stat-inactive") && child.classList.contains("livesync-stat-item")
|
||||
);
|
||||
}
|
||||
|
||||
describe("createStatusBarRenderer", () => {
|
||||
@@ -195,8 +224,10 @@ describe("createStatusBarRenderer", () => {
|
||||
expect(findValue(queue).className).toBe("livesync-stat-value");
|
||||
|
||||
renderer.render([{ key: "queue", text: " (queued: ", value: 3, suffix: ")" }]);
|
||||
expect(sync.hidden).toBe(true);
|
||||
expect(queue.hidden).toBe(false);
|
||||
expect(sync.classList.contains("livesync-stat-inactive")).toBe(true);
|
||||
expect(sync.hidden).toBe(false);
|
||||
expect(sync.getAttribute("aria-hidden")).toBe("true");
|
||||
expect(queue.classList.contains("livesync-stat-inactive")).toBe(false);
|
||||
expect(queue.textContent).toBe(" (queued: 3)");
|
||||
renderer.render([{ key: "queue", text: "Queue idle" }]);
|
||||
expect(queue.textContent).toBe("Queue idle");
|
||||
@@ -239,7 +270,7 @@ describe("createStatusBarRenderer", () => {
|
||||
renderer.dispose();
|
||||
});
|
||||
|
||||
it("resets numeric width while omitted and reuses the same item when it returns", () => {
|
||||
it("retains omitted content and width, then resets width when the same item returns", () => {
|
||||
const { root, parent } = createFixture();
|
||||
const renderer = createStatusBarRenderer(parent);
|
||||
|
||||
@@ -249,16 +280,49 @@ describe("createStatusBarRenderer", () => {
|
||||
expect(value.style.getPropertyValue("--livesync-stat-width")).toBe("4ch");
|
||||
|
||||
renderer.render([]);
|
||||
expect(item.hidden).toBe(true);
|
||||
expect(value.style.getPropertyValue("--livesync-stat-width")).toBe("");
|
||||
expect(item.classList.contains("livesync-stat-inactive")).toBe(true);
|
||||
expect(item.hidden).toBe(false);
|
||||
expect(item.getAttribute("aria-hidden")).toBe("true");
|
||||
expect(item.textContent).toBe(" 1234");
|
||||
expect(value.style.getPropertyValue("--livesync-stat-width")).toBe("4ch");
|
||||
|
||||
renderer.render([{ key: "count", text: " ", value: 5 }]);
|
||||
expect(findItem(root, "count")).toBe(item);
|
||||
expect(item.hidden).toBe(false);
|
||||
expect(item.classList.contains("livesync-stat-inactive")).toBe(false);
|
||||
expect(item.getAttribute("aria-hidden")).toBeNull();
|
||||
expect(value.style.getPropertyValue("--livesync-stat-width")).toBe("1ch");
|
||||
renderer.dispose();
|
||||
});
|
||||
|
||||
it("keeps inactive items in place when other items appear and does not repeat inactive mutations", () => {
|
||||
const { document, root, parent } = createFixture();
|
||||
const renderer = createStatusBarRenderer(parent);
|
||||
const first = { key: "first", text: "first " };
|
||||
const count = { key: "count", text: "count ", value: 1234 };
|
||||
const last = { key: "last", text: "last" };
|
||||
renderer.render([first, count, last]);
|
||||
const initialOrder = [...root.children];
|
||||
const omitted = findItem(root, "count");
|
||||
|
||||
renderer.render([first, last]);
|
||||
expect(root.children.map((item) => initialOrder.indexOf(item))).toEqual([0, 1, 2]);
|
||||
expect(visibleItems(root)).toEqual([initialOrder[0], initialOrder[2]]);
|
||||
const mutations = document.mutations;
|
||||
renderer.render([first, last]);
|
||||
expect(document.mutations).toBe(mutations);
|
||||
|
||||
const added = { key: "added", text: "added " };
|
||||
renderer.render([first, added, last]);
|
||||
expect(root.children.map((item) => item.textContent)).toEqual(["first ", "count 1234", "added ", "last"]);
|
||||
expect(omitted.textContent).toBe("count 1234");
|
||||
expect(findValue(omitted).style.getPropertyValue("--livesync-stat-width")).toBe("4ch");
|
||||
|
||||
renderer.render([first, count, added, last]);
|
||||
expect(findItem(root, "count")).toBe(omitted);
|
||||
expect(visibleItems(root)).toEqual(root.children);
|
||||
renderer.dispose();
|
||||
});
|
||||
|
||||
it("reorders existing nodes without replacing them", () => {
|
||||
const { root, parent } = createFixture();
|
||||
const renderer = createStatusBarRenderer(parent);
|
||||
|
||||
Reference in new issue
Block a user