mirror of https://github.com/buster-so/buster.git
pass down ids and update pdf generation logic
This commit is contained in:
parent
3745dc6ac2
commit
5c6bfdf96a
|
@ -24,6 +24,7 @@ interface ReportEditorProps {
|
|||
onValueChange?: (value: ReportElements) => void;
|
||||
useFixedToolbarKit?: boolean;
|
||||
onReady?: (editor: IReportEditor) => void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export type IReportEditor = TPlateEditor<Value, AnyPluginConfig>;
|
||||
|
@ -40,6 +41,7 @@ export const ReportEditor = React.memo(
|
|||
{
|
||||
value,
|
||||
placeholder,
|
||||
id,
|
||||
onValueChange,
|
||||
onReady,
|
||||
variant = 'default',
|
||||
|
@ -89,7 +91,7 @@ export const ReportEditor = React.memo(
|
|||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<ThemeWrapper>
|
||||
<ThemeWrapper id={id}>
|
||||
<Plate editor={editor} readOnly={readOnly} onValueChange={onValueChangePreflight}>
|
||||
<EditorContainer
|
||||
variant={variant}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { THEME_RESET_COLORS, THEME_RESET_STYLE } from '@/styles/theme-reset';
|
|||
|
||||
interface ThemeWrapperProps extends React.ComponentProps<'div'> {
|
||||
defaultTheme?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,10 +44,11 @@ const CSS_VARIABLES_THEME = Object.entries(THEME_RESET_COLORS.light).reduce(
|
|||
|
||||
const EDITOR_THEME = { ...CSS_VARIABLES_THEME, ...THEME_RESET_STYLE };
|
||||
|
||||
export function ThemeWrapper({ children, className, defaultTheme }: ThemeWrapperProps) {
|
||||
export function ThemeWrapper({ children, className, defaultTheme, id }: ThemeWrapperProps) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id={id}
|
||||
style={EDITOR_THEME}
|
||||
className={cn(
|
||||
'themes-wrapper h-full w-full overflow-visible bg-transparent antialiased',
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { BaseAlignKit } from './plugins/align-base-kit';
|
||||
import { BaseBasicBlocksKit } from './plugins/basic-blocks-base-kit';
|
||||
import { BaseBasicMarksKit } from './plugins/basic-marks-base-kit';
|
||||
import { BusterStreamKit } from './plugins/buster-stream-kit';
|
||||
import { BaseCalloutKit } from './plugins/callout-base-kit';
|
||||
import { BaseCodeBlockKit } from './plugins/code-block-base-kit';
|
||||
import { BaseColumnKit } from './plugins/column-base-kit';
|
||||
|
@ -42,5 +43,6 @@ export const BaseEditorKit = [
|
|||
...BaseLineHeightKit,
|
||||
...BaseCommentKit,
|
||||
...BaseSuggestionKit,
|
||||
...MarkdownKit
|
||||
...MarkdownKit,
|
||||
...BusterStreamKit
|
||||
];
|
||||
|
|
|
@ -2,52 +2,51 @@ import { EditorStatic } from '../elements/EditorStatic';
|
|||
import type { PlateEditor } from 'platejs/react';
|
||||
import { createSlateEditor, serializeHtml } from 'platejs';
|
||||
|
||||
// Build a complete HTML document string for export, rasterizing
|
||||
// metric elements (which may contain canvas) into <img> tags AND
|
||||
// inlining computed CSS styles so no external CSS is required.
|
||||
// Build a complete HTML document string for export, inlining
|
||||
// computed CSS styles so no external CSS is required. Additionally,
|
||||
// snapshot <canvas> elements inside metrics to <img> tags to avoid
|
||||
// blank canvases in the exported HTML.
|
||||
export const buildExportHtml = async (editor: PlateEditor): Promise<string> => {
|
||||
// Prefer using the live editor DOM to inline computed styles
|
||||
const liveRoot = document.querySelector('[contenteditable="true"]') as HTMLElement | null;
|
||||
|
||||
// Create images for each metric element from the LIVE DOM (if available)
|
||||
const { default: html2canvas } = await import('html2canvas-pro');
|
||||
const metricDataUrls: string[] = [];
|
||||
const liveMetricFigures = liveRoot
|
||||
? (Array.from(
|
||||
liveRoot.querySelectorAll('[data-export-metric] [data-metric-figure]')
|
||||
) as HTMLElement[])
|
||||
: [];
|
||||
|
||||
for (const figureEl of liveMetricFigures) {
|
||||
try {
|
||||
const canvas = await html2canvas(figureEl, { backgroundColor: null });
|
||||
metricDataUrls.push(canvas.toDataURL('image/png'));
|
||||
} catch (e) {
|
||||
console.error('Failed to rasterize metric element for HTML export', e);
|
||||
metricDataUrls.push('');
|
||||
}
|
||||
}
|
||||
|
||||
let contentHtml = '';
|
||||
|
||||
if (liveRoot) {
|
||||
// Clone live DOM subtree and inline computed styles
|
||||
const clonedRoot = liveRoot.cloneNode(true) as HTMLElement;
|
||||
|
||||
// Replace metric figures with <img> tags in the clone
|
||||
const clonedMetricFigures = Array.from(
|
||||
clonedRoot.querySelectorAll('[data-export-metric] [data-metric-figure]')
|
||||
);
|
||||
clonedMetricFigures.forEach((node, index) => {
|
||||
const dataUrl = metricDataUrls[index];
|
||||
if (!dataUrl) return;
|
||||
const img = document.createElement('img');
|
||||
img.src = dataUrl;
|
||||
img.alt = 'Metric';
|
||||
(img.style as CSSStyleDeclaration).width = '100%';
|
||||
(img.style as CSSStyleDeclaration).height = 'auto';
|
||||
node.replaceWith(img);
|
||||
});
|
||||
// Snapshot metric canvases from the LIVE DOM and replace in the clone
|
||||
const liveMetricCanvases = Array.from(
|
||||
liveRoot.querySelectorAll('[data-export-metric] canvas')
|
||||
) as HTMLCanvasElement[];
|
||||
const cloneMetricCanvases = Array.from(
|
||||
clonedRoot.querySelectorAll('[data-export-metric] canvas')
|
||||
) as HTMLCanvasElement[];
|
||||
|
||||
for (let i = 0; i < Math.min(liveMetricCanvases.length, cloneMetricCanvases.length); i++) {
|
||||
const liveCanvas = liveMetricCanvases[i];
|
||||
const cloneCanvas = cloneMetricCanvases[i];
|
||||
try {
|
||||
// Use the raw canvas data to preserve exact rendering
|
||||
const dataUrl = liveCanvas.toDataURL('image/png');
|
||||
if (dataUrl) {
|
||||
const img = document.createElement('img');
|
||||
img.src = dataUrl;
|
||||
img.alt = 'Metric';
|
||||
const computed = window.getComputedStyle(liveCanvas);
|
||||
const width = computed.getPropertyValue('width');
|
||||
const height = computed.getPropertyValue('height');
|
||||
if (width) (img.style as CSSStyleDeclaration).width = width;
|
||||
if (height) (img.style as CSSStyleDeclaration).height = height;
|
||||
(img.style as CSSStyleDeclaration).display = 'block';
|
||||
cloneCanvas.replaceWith(img);
|
||||
}
|
||||
} catch (e) {
|
||||
// If canvas is tainted or fails, leave the canvas as-is
|
||||
console.error('Failed to snapshot metric canvas for HTML export', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Inline computed styles by pairing original and clone nodes in order
|
||||
const originals = [liveRoot, ...Array.from(liveRoot.querySelectorAll('*'))];
|
||||
|
@ -73,19 +72,25 @@ export const buildExportHtml = async (editor: PlateEditor): Promise<string> => {
|
|||
clone.removeAttribute('spellcheck');
|
||||
}
|
||||
|
||||
// Wrap content with a fixed width container for consistency
|
||||
// Wrap content with a fixed width, centered container for consistency
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.setAttribute(
|
||||
'style',
|
||||
'width: 850px; max-width: 850px; min-width: 850px; margin: 0 auto;'
|
||||
[
|
||||
'width: 816px',
|
||||
'max-width: 816px',
|
||||
'min-width: 816px',
|
||||
'margin: 24px auto',
|
||||
'background: #ffffff',
|
||||
'padding: 40px',
|
||||
'box-sizing: border-box'
|
||||
].join('; ')
|
||||
);
|
||||
wrapper.appendChild(clonedRoot);
|
||||
contentHtml = wrapper.outerHTML;
|
||||
} else {
|
||||
// Fallback: serialize using static editor and include minimal inline styles
|
||||
const BaseEditorKit = await import('../editor-base-kit').then(
|
||||
(module) => module.BaseEditorKit
|
||||
);
|
||||
const BaseEditorKit = await import('../editor-base-kit').then((module) => module.BaseEditorKit);
|
||||
|
||||
const editorStatic = createSlateEditor({
|
||||
plugins: BaseEditorKit,
|
||||
|
@ -97,23 +102,22 @@ export const buildExportHtml = async (editor: PlateEditor): Promise<string> => {
|
|||
props: { style: { padding: '0 calc(50% - 350px)', paddingBottom: '' } }
|
||||
});
|
||||
|
||||
// Replace metric figures in serialized HTML
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = serializedHtml;
|
||||
const clonedMetricFiguresFallback = Array.from(
|
||||
container.querySelectorAll('[data-export-metric] [data-metric-figure]')
|
||||
// Wrap the serialized HTML to match the centered, fixed-width layout
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.setAttribute(
|
||||
'style',
|
||||
[
|
||||
'width: 816px',
|
||||
'max-width: 816px',
|
||||
'min-width: 816px',
|
||||
'margin: 24px auto',
|
||||
'background: #ffffff',
|
||||
'padding: 40px',
|
||||
'box-sizing: border-box'
|
||||
].join('; ')
|
||||
);
|
||||
clonedMetricFiguresFallback.forEach((node, index) => {
|
||||
const dataUrl = metricDataUrls[index];
|
||||
if (!dataUrl) return;
|
||||
const img = document.createElement('img');
|
||||
img.src = dataUrl;
|
||||
img.alt = 'Metric';
|
||||
(img.style as CSSStyleDeclaration).width = '100%';
|
||||
(img.style as CSSStyleDeclaration).height = 'auto';
|
||||
node.replaceWith(img);
|
||||
});
|
||||
contentHtml = container.innerHTML;
|
||||
wrapper.innerHTML = serializedHtml;
|
||||
contentHtml = wrapper.outerHTML;
|
||||
}
|
||||
|
||||
// Build a minimal HTML document without external CSS
|
||||
|
@ -124,7 +128,10 @@ export const buildExportHtml = async (editor: PlateEditor): Promise<string> => {
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<style>
|
||||
@media print { body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
|
||||
body { margin: 0; background: #f5f5f5; }
|
||||
@media print {
|
||||
body { background: #fff; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -136,5 +143,3 @@ export const buildExportHtml = async (editor: PlateEditor): Promise<string> => {
|
|||
};
|
||||
|
||||
export default buildExportHtml;
|
||||
|
||||
|
||||
|
|
|
@ -12,11 +12,13 @@ import { ReportEditor, type IReportEditor } from '@/components/ui/report/ReportE
|
|||
import { registerReportEditor, unregisterReportEditor } from './editorRegistry';
|
||||
import { ReportEditorSkeleton } from '@/components/ui/report/ReportEditorSkeleton';
|
||||
|
||||
export const PAGE_CONTROLLER_ID = (reportId: string) => `report-page-controller-${reportId}`;
|
||||
|
||||
export const ReportPageController: React.FC<{
|
||||
reportId: string;
|
||||
readOnly?: boolean;
|
||||
className?: string;
|
||||
}> = ({ reportId, readOnly = false, className = '' }) => {
|
||||
}> = React.memo(({ reportId, readOnly = false, className = '' }) => {
|
||||
const { data: report } = useGetReport({ reportId, versionNumber: undefined });
|
||||
// const editor = useRef<AppReportRef>(null);
|
||||
|
||||
|
@ -54,6 +56,7 @@ export const ReportPageController: React.FC<{
|
|||
|
||||
return (
|
||||
<div
|
||||
id={PAGE_CONTROLLER_ID(reportId)}
|
||||
className={cn(
|
||||
'h-full space-y-1.5 overflow-y-auto pt-9 sm:px-[max(64px,calc(50%-350px))]',
|
||||
className
|
||||
|
@ -76,6 +79,7 @@ export const ReportPageController: React.FC<{
|
|||
readOnly={readOnly || !report}
|
||||
className="px-0!"
|
||||
onReady={onReady}
|
||||
id={PAGE_CONTROLLER_ID(reportId)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -83,4 +87,6 @@ export const ReportPageController: React.FC<{
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ReportPageController.displayName = 'ReportPageController';
|
||||
|
|
Loading…
Reference in New Issue