2025-07-13 13:31:25 -06:00
|
|
|
import clsx from 'clsx';
|
2025-03-02 22:55:54 -07:00
|
|
|
import styles from './TooltipShared.module.css';
|
|
|
|
|
|
|
|
|
|
import type { display } from 'objdiff-wasm';
|
2025-07-13 13:31:25 -06:00
|
|
|
import React, { useMemo } from 'react';
|
2025-03-02 22:55:54 -07:00
|
|
|
import { Tooltip } from 'react-tooltip';
|
|
|
|
|
|
2025-07-13 13:31:25 -06:00
|
|
|
export type TooltipTriggerProps = {
|
2025-05-27 22:15:56 -06:00
|
|
|
'data-tooltip-id': string;
|
|
|
|
|
'data-tooltip-content': string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type TooltipCallback<T> = (content: T) => display.HoverItem[] | null;
|
|
|
|
|
|
2025-07-13 13:31:25 -06:00
|
|
|
export type TooltipProps<T> = Omit<
|
|
|
|
|
React.ComponentProps<typeof Tooltip>,
|
|
|
|
|
'id' | 'render'
|
|
|
|
|
> & {
|
|
|
|
|
callback: TooltipCallback<T>;
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-27 22:15:56 -06:00
|
|
|
export function createTooltip<T>(): {
|
2025-07-13 13:31:25 -06:00
|
|
|
Tooltip: React.FC<TooltipProps<T>>;
|
|
|
|
|
useTooltip: (content: T) => TooltipTriggerProps;
|
2025-05-27 22:15:56 -06:00
|
|
|
} {
|
|
|
|
|
const id = generateRandomString(10);
|
|
|
|
|
return {
|
2025-07-13 13:31:25 -06:00
|
|
|
Tooltip: ({ callback, className, ...props }) => (
|
|
|
|
|
<Tooltip
|
|
|
|
|
{...props}
|
|
|
|
|
id={id}
|
|
|
|
|
className={clsx(styles.tooltip, className)}
|
|
|
|
|
render={({ content }) => {
|
|
|
|
|
const items = useMemo(() => {
|
|
|
|
|
if (!content) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const parsedContent = JSON.parse(content) as T;
|
|
|
|
|
return callback(parsedContent);
|
|
|
|
|
}, [callback, content]);
|
2025-09-07 20:05:56 -06:00
|
|
|
if (!items || items.length === 0) {
|
2025-05-27 22:15:56 -06:00
|
|
|
return null;
|
|
|
|
|
}
|
2025-07-13 13:31:25 -06:00
|
|
|
return <TooltipContentMemo items={items} />;
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
useTooltip: (content: T) => ({
|
|
|
|
|
'data-tooltip-id': id,
|
|
|
|
|
'data-tooltip-content': JSON.stringify(content),
|
|
|
|
|
}),
|
2025-05-27 22:15:56 -06:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-02 22:55:54 -07:00
|
|
|
const TooltipContent = ({ items }: { items: display.HoverItem[] }) => {
|
|
|
|
|
const out = [];
|
|
|
|
|
for (const [i, item] of items.entries()) {
|
|
|
|
|
let inner: React.ReactNode;
|
|
|
|
|
switch (item.tag) {
|
2025-09-07 20:05:56 -06:00
|
|
|
case 'text': {
|
|
|
|
|
let labelClass: string | null = null;
|
|
|
|
|
let valueClass: string | null = null;
|
|
|
|
|
switch (item.val.color) {
|
|
|
|
|
case 'special':
|
|
|
|
|
labelClass = styles.labelColorSpecial;
|
|
|
|
|
break;
|
|
|
|
|
case 'insert':
|
|
|
|
|
labelClass = styles.labelColorInsert;
|
|
|
|
|
break;
|
|
|
|
|
case 'delete':
|
|
|
|
|
labelClass = styles.labelColorDelete;
|
|
|
|
|
break;
|
|
|
|
|
case 'emphasized':
|
|
|
|
|
valueClass = styles.valueColorEmphasized;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-03-02 22:55:54 -07:00
|
|
|
if (item.val.label) {
|
|
|
|
|
inner = (
|
|
|
|
|
<>
|
2025-09-07 20:05:56 -06:00
|
|
|
<span className={clsx(styles.hoverItemLabel, labelClass)}>
|
2025-03-02 22:55:54 -07:00
|
|
|
{item.val.label}
|
|
|
|
|
{': '}
|
|
|
|
|
</span>
|
2025-09-07 20:05:56 -06:00
|
|
|
<span className={clsx(styles.hoverItemValue, valueClass)}>
|
|
|
|
|
{item.val.value}
|
|
|
|
|
</span>
|
2025-03-02 22:55:54 -07:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
inner = (
|
2025-09-07 20:05:56 -06:00
|
|
|
<span className={clsx(styles.hoverItemValue, valueClass)}>
|
|
|
|
|
{item.val.value}
|
|
|
|
|
</span>
|
2025-03-02 22:55:54 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
break;
|
2025-09-07 20:05:56 -06:00
|
|
|
}
|
2025-03-02 22:55:54 -07:00
|
|
|
case 'separator':
|
|
|
|
|
inner = <hr className={styles.hoverItemSeparator} />;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
console.warn('Unhandled context item', item);
|
|
|
|
|
inner = null;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
out.push(
|
|
|
|
|
<div key={i} className={styles.hoverItem}>
|
|
|
|
|
{inner}
|
|
|
|
|
</div>,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return <div>{out}</div>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const TooltipContentMemo = React.memo(TooltipContent);
|
|
|
|
|
|
2025-05-27 22:15:56 -06:00
|
|
|
function generateRandomString(length: number): string {
|
|
|
|
|
const characters =
|
|
|
|
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
|
|
|
let result = '';
|
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
|
|
|
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|