feat(files): add HTML preview support in file viewer (#772)
- Add isHtmlFile() helper to detect .html/.htm files - Add htmlViewMode state for preview/edit toggle - Add localStorage persistence for HTML view mode preference - Show PreviewToggleButton for HTML files - Render HTML in sandboxed iframe with srcdoc - Inject base tag for relative CSS/JS/image paths to work - Sandbox permissions: allow-scripts, same-origin, forms - Reset view mode when switching files
This commit is contained in:
committed by
GitHub
parent
f032df2298
commit
744b29c492
@@ -263,6 +263,12 @@ const isMarkdownFile = (path: string): boolean => {
|
||||
return ext === 'md' || ext === 'markdown';
|
||||
};
|
||||
|
||||
const isHtmlFile = (path: string): boolean => {
|
||||
if (!path) return false;
|
||||
const ext = path.toLowerCase().split('.').pop();
|
||||
return ext === 'html' || ext === 'htm';
|
||||
};
|
||||
|
||||
interface FileRowProps {
|
||||
node: FileNode;
|
||||
isExpanded: boolean;
|
||||
@@ -466,6 +472,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
|
||||
const [textViewMode, setTextViewMode] = React.useState<'view' | 'edit'>('edit');
|
||||
const [mdViewMode, setMdViewMode] = React.useState<'preview' | 'edit'>('edit');
|
||||
const [htmlViewMode, setHtmlViewMode] = React.useState<'preview' | 'edit'>('edit');
|
||||
|
||||
const lightTheme = React.useMemo(
|
||||
() => availableThemes.find((theme) => theme.metadata.id === lightThemeId) ?? getDefaultTheme(false),
|
||||
@@ -1632,8 +1639,9 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
const canCopyPath = Boolean(selectedFile && displaySelectedPath.length > 0);
|
||||
const canEdit = Boolean(selectedFile && !isSelectedImage && files.writeFile && fileContent.length <= MAX_VIEW_CHARS);
|
||||
const isMarkdown = Boolean(selectedFile?.path && isMarkdownFile(selectedFile.path));
|
||||
const isHtml = Boolean(selectedFile?.path && isHtmlFile(selectedFile.path));
|
||||
const isTextFile = Boolean(selectedFile && !isSelectedImage);
|
||||
const canUseShikiFileView = isTextFile && !isMarkdown;
|
||||
const canUseShikiFileView = isTextFile && !isMarkdown && !(isHtml && htmlViewMode === 'preview');
|
||||
const staticLanguageExtension = React.useMemo(
|
||||
() => (selectedFilePath ? languageByExtension(selectedFilePath) : null),
|
||||
[selectedFilePath],
|
||||
@@ -1669,6 +1677,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
|
||||
React.useEffect(() => {
|
||||
setTextViewMode('edit');
|
||||
setHtmlViewMode('edit');
|
||||
}, [selectedFile?.path]);
|
||||
|
||||
const MD_VIEWER_MODE_KEY = 'openchamber:files:md-viewer-mode';
|
||||
@@ -1699,6 +1708,34 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return mdViewMode;
|
||||
}, [mdViewMode]);
|
||||
|
||||
const HTML_VIEWER_MODE_KEY = 'openchamber:files:html-viewer-mode';
|
||||
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(HTML_VIEWER_MODE_KEY);
|
||||
if (stored === 'preview') {
|
||||
setHtmlViewMode('preview');
|
||||
} else if (stored === 'edit') {
|
||||
setHtmlViewMode('edit');
|
||||
}
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
}, []);
|
||||
|
||||
const saveHtmlViewMode = React.useCallback((mode: 'preview' | 'edit') => {
|
||||
setHtmlViewMode(mode);
|
||||
try {
|
||||
localStorage.setItem(HTML_VIEWER_MODE_KEY, mode);
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getHtmlViewMode = React.useCallback((): 'preview' | 'edit' => {
|
||||
return htmlViewMode;
|
||||
}, [htmlViewMode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!pendingFileNavigation || !root) {
|
||||
return;
|
||||
@@ -2235,10 +2272,16 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{isMarkdown && (
|
||||
{(isMarkdown || isHtmlFile(selectedFile?.path ?? '')) && (
|
||||
<PreviewToggleButton
|
||||
currentMode={getMdViewMode()}
|
||||
onToggle={() => saveMdViewMode(getMdViewMode() === 'preview' ? 'edit' : 'preview')}
|
||||
currentMode={isMarkdown ? getMdViewMode() : getHtmlViewMode()}
|
||||
onToggle={() => {
|
||||
if (isHtmlFile(selectedFile?.path ?? '')) {
|
||||
saveHtmlViewMode(getHtmlViewMode() === 'preview' ? 'edit' : 'preview');
|
||||
} else {
|
||||
saveMdViewMode(getMdViewMode() === 'preview' ? 'edit' : 'preview');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -2561,6 +2604,21 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
) : selectedFile && isHtml && htmlViewMode === 'preview' ? (
|
||||
<div className="h-full overflow-hidden">
|
||||
<iframe
|
||||
srcDoc={(() => {
|
||||
// Inject base tag for relative paths (CSS/JS/images) to work
|
||||
const basePath = selectedFile.path.substring(0, selectedFile.path.lastIndexOf('/') + 1);
|
||||
if (!basePath) return fileContent;
|
||||
const baseTag = `<base href="${runtime.isDesktop ? basePath : basePath}">`;
|
||||
return fileContent.replace(/<head([^>]*)>/i, `<head$1>${baseTag}`);
|
||||
})()}
|
||||
className="w-full h-full border-none"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
title="HTML Preview"
|
||||
/>
|
||||
</div>
|
||||
) : selectedFile && canUseShikiFileView && textViewMode === 'view' ? (
|
||||
renderShikiFileView(selectedFile, draftContent)
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user