import type {FileRenderPlugin} from '../render/plugin.ts'; import {newRenderPlugin3DViewer} from '../render/plugins/3d-viewer.ts'; import {newRenderPluginPdfViewer} from '../render/plugins/pdf-viewer.ts'; import {registerGlobalInitFunc} from '../modules/observer.ts'; import {createElementFromHTML, showElem, toggleElemClass} from '../utils/dom.ts'; import {html} from '../utils/html.ts'; import {basename} from '../utils.ts'; const plugins: FileRenderPlugin[] = []; function initPluginsOnce(): void { if (plugins.length) return; plugins.push(newRenderPlugin3DViewer(), newRenderPluginPdfViewer()); } function findFileRenderPlugin(filename: string, mimeType: string): FileRenderPlugin | null { return plugins.find((plugin) => plugin.canHandle(filename, mimeType)) || null; } function showRenderRawFileButton(elFileView: HTMLElement, renderContainer: HTMLElement | null): void { const toggleButtons = elFileView.querySelector('.file-view-toggle-buttons')!; showElem(toggleButtons); const displayingRendered = Boolean(renderContainer); toggleElemClass(toggleButtons.querySelectorAll('.file-view-toggle-source'), 'active', !displayingRendered); // it may not exist toggleElemClass(toggleButtons.querySelector('.file-view-toggle-rendered')!, 'active', displayingRendered); // TODO: if there is only one button, hide it? } async function renderRawFileToContainer(container: HTMLElement, rawFileLink: string, mimeType: string) { const elViewRawPrompt = container.querySelector('.file-view-raw-prompt'); if (!rawFileLink || !elViewRawPrompt) throw new Error('unexpected file view container'); let rendered = false, errorMsg = ''; try { const plugin = findFileRenderPlugin(basename(rawFileLink), mimeType); if (plugin) { container.classList.add('is-loading'); container.setAttribute('data-render-name', plugin.name); // not used yet await plugin.render(container, rawFileLink); rendered = true; } } catch (e) { errorMsg = `${e}`; } finally { container.classList.remove('is-loading'); } if (rendered) { elViewRawPrompt.remove(); return; } // remove all children from the container, and only show the raw file link container.replaceChildren(elViewRawPrompt); if (errorMsg) { const elErrorMessage = createElementFromHTML(html`
`); elViewRawPrompt.insertAdjacentElement('afterbegin', elErrorMessage); } } function updateSidebarPosition(elFileView: HTMLElement, sidebar: HTMLElement): void { const fileHeader = elFileView.querySelector('.file-header'); const segment = elFileView.querySelector('.ui.bottom.segment'); if (!fileHeader || !segment) return; const headerRect = fileHeader.getBoundingClientRect(); const segmentRect = segment.getBoundingClientRect(); // Calculate top position: // - When file header is visible: align with file header top // - When file header scrolls above viewport: stick to top (12px) // - Ensure sidebar top doesn't go above segment top (when scrolling up) const minTop = 12; let topPos = Math.max(minTop, headerRect.top); topPos = Math.max(topPos, segmentRect.top); // Don't go above segment top // Dynamically calculate max-height so sidebar doesn't extend below segment bottom const availableHeight = Math.max(0, segmentRect.bottom - topPos); const cssMaxHeight = window.innerHeight - 140; // Match CSS calc(100vh - 140px) const maxHeight = Math.min(availableHeight, cssMaxHeight); // Hide sidebar if available height is too small if (maxHeight < 50) { sidebar.style.opacity = '0'; return; } sidebar.style.maxHeight = `${maxHeight}px`; sidebar.style.opacity = ''; sidebar.style.top = `${topPos}px`; // Position sidebar right next to the content const leftPos = segmentRect.right + 8; // 8px gap from content sidebar.style.left = `${leftPos}px`; sidebar.style.right = 'auto'; // Mark as positioned to show the sidebar (prevents flicker) sidebar.classList.add('sidebar-positioned'); } function initSidebarToggle(elFileView: HTMLElement): void { const toggleBtn = elFileView.querySelector('#toggle-sidebar-btn'); const sidebar = elFileView.querySelector