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}`; } }); }