diff --git a/frontend/src/charts/ScatterPlot.svelte b/frontend/src/charts/ScatterPlot.svelte
index 6f28723fd..a0e1009c8 100644
--- a/frontend/src/charts/ScatterPlot.svelte
+++ b/frontend/src/charts/ScatterPlot.svelte
@@ -10,7 +10,7 @@
import { scatterplotScale } from "./helpers";
import type { ScatterPlotDatum } from "./scatterplot";
import type { TooltipFindNode } from "./tooltip";
- import { positionedTooltip } from "./tooltip";
+ import { domHelpers, positionedTooltip } from "./tooltip";
export let data: ScatterPlotDatum[];
export let width: number;
@@ -51,7 +51,7 @@
);
function tooltipText(d: ScatterPlotDatum) {
- return `${d.description}${day(d.date)}`;
+ return [domHelpers.t(d.description), domHelpers.em(day(d.date))];
}
const tooltipFindNode: TooltipFindNode = (xPos, yPos) => {
diff --git a/frontend/src/charts/Treemap.svelte b/frontend/src/charts/Treemap.svelte
index c75b79f71..1efddd558 100644
--- a/frontend/src/charts/Treemap.svelte
+++ b/frontend/src/charts/Treemap.svelte
@@ -11,7 +11,7 @@
AccountHierarchyDatum,
AccountHierarchyNode,
} from "./hierarchy";
- import { followingTooltip } from "./tooltip";
+ import { domHelpers, followingTooltip } from "./tooltip";
export let data: AccountHierarchyNode;
export let width: number;
@@ -35,9 +35,12 @@
const val = d.value ?? 0;
const rootValue = root.value || 1;
- return `${$ctx.amount(val, currency)} (${formatPercentage(
- val / rootValue
- )})${d.data.account}`;
+ return [
+ domHelpers.t(
+ `${$ctx.amount(val, currency)} (${formatPercentage(val / rootValue)})`
+ ),
+ domHelpers.em(d.data.account),
+ ];
}
function setVisibility(
diff --git a/frontend/src/charts/bar.ts b/frontend/src/charts/bar.ts
index 4180b47c8..5256ce018 100644
--- a/frontend/src/charts/bar.ts
+++ b/frontend/src/charts/bar.ts
@@ -7,6 +7,8 @@ import type { Result } from "../lib/result";
import { array, date, number, object, record } from "../lib/validation";
import type { ChartContext } from "./context";
+import type { TooltipContent } from "./tooltip";
+import { domHelpers } from "./tooltip";
export interface BarChartDatumValue {
currency: string;
@@ -38,7 +40,11 @@ export interface BarChart {
/** Whether this chart contains any stacks (or is just a single account). */
hasStackedData: boolean;
};
- tooltipText: (c: FormatterContext, d: BarChartDatum, e: string) => string;
+ tooltipText: (
+ c: FormatterContext,
+ d: BarChartDatum,
+ e: string
+ ) => TooltipContent;
}
const bar_validator = array(
@@ -91,24 +97,31 @@ export function bar(
type: "barchart" as const,
data: { accounts, bar_groups, stacks, hasStackedData },
tooltipText: (c, d, e) => {
- let text = "";
+ const content: TooltipContent = [];
if (e === "") {
d.values.forEach((a) => {
- text += c.amount(a.value, a.currency);
- if (a.budget) {
- text += ` / ${c.amount(a.budget, a.currency)}`;
- }
- text += "
";
+ content.push(
+ domHelpers.t(
+ a.budget
+ ? `${c.amount(a.value, a.currency)} / ${c.amount(
+ a.budget,
+ a.currency
+ )}`
+ : c.amount(a.value, a.currency)
+ )
+ );
+ content.push(domHelpers.br());
});
} else {
- text += `${e}`;
+ content.push(domHelpers.em(e));
d.values.forEach((a) => {
const value = d.account_balances[e]?.[a.currency] ?? 0;
- text += `${c.amount(value, a.currency)}
`;
+ content.push(domHelpers.t(`${c.amount(value, a.currency)}`));
+ content.push(domHelpers.br());
});
}
- text += `${d.label}`;
- return text;
+ content.push(domHelpers.em(d.label));
+ return content;
},
});
}
diff --git a/frontend/src/charts/context.ts b/frontend/src/charts/context.ts
index 9adcdfae7..f493a0dd3 100644
--- a/frontend/src/charts/context.ts
+++ b/frontend/src/charts/context.ts
@@ -2,7 +2,7 @@ import type { Readable } from "svelte/store";
import { derived } from "svelte/store";
import { currentDateFormat } from "../format";
-import { conversion, operating_currency } from "../stores";
+import { conversion, currencies, operating_currency } from "../stores";
export type ChartContext = {
currencies: string[];
@@ -13,17 +13,12 @@ export type ChartContext = {
* The list of operating currencies, adding in the current conversion currency.
*/
const operatingCurrenciesWithConversion = derived(
- [operating_currency, conversion],
- ([operating_currency_val, conversion_val]) => {
- if (
- !conversion_val ||
- ["at_cost", "at_value", "units"].includes(conversion_val) ||
- operating_currency_val.includes(conversion_val)
- ) {
- return operating_currency_val;
- }
- return [...operating_currency_val, conversion_val];
- }
+ [operating_currency, currencies, conversion],
+ ([operating_currency_val, currencies_val, conversion_val]) =>
+ currencies_val.includes(conversion_val) &&
+ !operating_currency_val.includes(conversion_val)
+ ? [...operating_currency_val, conversion_val]
+ : operating_currency_val
);
export const chartContext: Readable = derived(
diff --git a/frontend/src/charts/line.ts b/frontend/src/charts/line.ts
index 222e11dcc..086505b59 100644
--- a/frontend/src/charts/line.ts
+++ b/frontend/src/charts/line.ts
@@ -12,6 +12,9 @@ import {
tuple,
} from "../lib/validation";
+import type { TooltipContent } from "./tooltip";
+import { domHelpers } from "./tooltip";
+
export interface LineChartDatum {
name: string;
date: Date;
@@ -26,7 +29,7 @@ export type LineChartData = {
export interface LineChart {
type: "linechart";
data: LineChartData[];
- tooltipText: (c: FormatterContext, d: LineChartDatum) => string;
+ tooltipText: (c: FormatterContext, d: LineChartDatum) => TooltipContent;
}
const balances_validator = array(object({ date, balance: record(number) }));
@@ -57,8 +60,10 @@ export function balances(json: unknown): Result {
return ok({
type: "linechart" as const,
data,
- tooltipText: (c, d) =>
- `${c.amount(d.value, d.name)}${day(d.date)}`,
+ tooltipText: (c, d) => [
+ domHelpers.t(c.amount(d.value, d.name)),
+ domHelpers.em(day(d.date)),
+ ],
});
}
@@ -82,8 +87,9 @@ export function commodities(
return ok({
type: "linechart" as const,
data: [{ name: label, values }],
- tooltipText(c, d) {
- return `1 ${base} = ${c.amount(d.value, quote)}${day(d.date)}`;
- },
+ tooltipText: (c, d) => [
+ domHelpers.t(`1 ${base} = ${c.amount(d.value, quote)}`),
+ domHelpers.em(day(d.date)),
+ ],
});
}
diff --git a/frontend/src/charts/tooltip.ts b/frontend/src/charts/tooltip.ts
index ac8dcf5d8..b9f792e26 100644
--- a/frontend/src/charts/tooltip.ts
+++ b/frontend/src/charts/tooltip.ts
@@ -19,6 +19,19 @@ const hide = (): void => {
t.style.opacity = "0";
};
+/** Some small utilities to create tooltip contents. */
+export const domHelpers = {
+ br: () => document.createElement("br"),
+ em: (content: string) => {
+ const em = document.createElement("em");
+ em.textContent = content;
+ return em;
+ },
+ t: (text: string) => document.createTextNode(text),
+};
+
+export type TooltipContent = (HTMLElement | Text)[];
+
/**
* Svelte action to have the given element act on mouse to show a tooltip.
*
@@ -27,8 +40,8 @@ const hide = (): void => {
*/
export function followingTooltip(
node: SVGElement,
- text: () => string
-): { destroy: () => void; update: (t: () => string) => void } {
+ text: () => TooltipContent
+): { destroy: () => void; update: (t: () => TooltipContent) => void } {
let getter = text;
/** Event listener to have the tooltip follow the mouse. */
function followMouse(event: MouseEvent): void {
@@ -39,14 +52,14 @@ export function followingTooltip(
}
node.addEventListener("mouseenter", () => {
const t = tooltip();
- t.innerHTML = getter();
+ t.replaceChildren(...getter());
});
node.addEventListener("mousemove", followMouse);
node.addEventListener("mouseleave", hide);
return {
destroy: hide,
- update(t: () => string): void {
+ update(t: () => TooltipContent): void {
getter = t;
},
};
@@ -56,7 +69,7 @@ export function followingTooltip(
export type TooltipFindNode = (
x: number,
y: number
-) => [number, number, string] | undefined;
+) => [number, number, TooltipContent] | undefined;
/**
* Svelte action to have the given element act on mouse to show a tooltip.
@@ -78,7 +91,7 @@ export function positionedTooltip(
const [x, y, content] = res;
const t = tooltip();
t.style.opacity = "1";
- t.innerHTML = content;
+ t.replaceChildren(...content);
t.style.left = `${window.scrollX + x + matrix.e}px`;
t.style.top = `${window.scrollY + y + matrix.f - 15}px`;
} else {
diff --git a/frontend/src/keyboard-shortcuts.ts b/frontend/src/keyboard-shortcuts.ts
index d8d5e4f2a..005f46e95 100644
--- a/frontend/src/keyboard-shortcuts.ts
+++ b/frontend/src/keyboard-shortcuts.ts
@@ -10,7 +10,7 @@ function showTooltip(target: HTMLElement): () => void {
target.classList.remove("hidden");
}
tooltip.className = "keyboard-tooltip";
- tooltip.innerHTML = target.getAttribute("data-key") || "";
+ tooltip.textContent = target.getAttribute("data-key") ?? "";
document.body.appendChild(tooltip);
const parentCoords = target.getBoundingClientRect();
// Padded 10px to the left if there is space or centered otherwise
diff --git a/frontend/src/sidebar/index.ts b/frontend/src/sidebar/index.ts
index b83c0fe67..199a981c0 100644
--- a/frontend/src/sidebar/index.ts
+++ b/frontend/src/sidebar/index.ts
@@ -26,7 +26,7 @@ export function initSidebar(): void {
errorCountEl.classList.toggle("hidden", errorCount_val === 0);
const span = errorCountEl.querySelector("span");
if (span) {
- span.innerHTML = `${errorCount_val}`;
+ span.textContent = `${errorCount_val}`;
}
});
}