treesap
Version:
AI Agent Framework
305 lines (263 loc) • 10.3 kB
JavaScript
// SimpleLivePreview component JavaScript
import { sidebarStore } from '/signals/SidebarSignal.js';
class SimpleLivePreviewManager {
constructor(id = 'simple-preview') {
this.id = id;
// DOM elements
this.container = document.getElementById(id);
this.hideSidebarBtn = document.getElementById(`${id}-hide-sidebar-btn`);
this.hideSidebarIcon = document.getElementById(`${id}-hide-sidebar-icon`);
this.floatingHideSidebarBtn = document.getElementById(`${id}-floating-hide-sidebar-btn`);
this.floatingHideSidebarIcon = document.getElementById(`${id}-floating-hide-sidebar-icon`);
this.refreshBtn = document.getElementById(`${id}-refresh-btn`);
this.urlInput = document.getElementById(`${id}-url-input`);
this.loadBtn = document.getElementById(`${id}-load-btn`);
this.iframe = document.getElementById(`${id}-iframe`);
// Get preview port from iframe data attribute
this.previewPort = this.iframe?.getAttribute('data-preview-port') || 5173;
// Reference to the sidebar store
this.store = sidebarStore;
this.init();
}
init() {
console.log('Initializing SimpleLivePreview:', this.id);
console.log('Elements found:', {
container: !!this.container,
urlInput: !!this.urlInput,
loadBtn: !!this.loadBtn,
iframe: !!this.iframe
});
// Set up event listeners
this.setupEventListeners();
// Subscribe to sidebar store changes
this.subscribeToStore();
}
setupEventListeners() {
// Hide sidebar toggle (both sidebar and floating button)
this.hideSidebarBtn?.addEventListener('click', () => this.store.toggle());
this.floatingHideSidebarBtn?.addEventListener('click', () => this.store.toggle());
// Refresh button
this.refreshBtn?.addEventListener('click', () => this.refreshIframe());
// URL navigation
this.loadBtn?.addEventListener('click', (e) => {
e.preventDefault();
this.loadUrl();
});
this.urlInput?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.loadUrl();
}
});
// Prevent form submission if input is in a form
this.urlInput?.addEventListener('submit', (e) => {
e.preventDefault();
this.loadUrl();
});
// Listen for events from Sidebar component
document.addEventListener('preview:refresh', () => this.refreshIframe());
document.addEventListener('preview:loadUrl', (e) => {
if (e.detail && e.detail.path !== undefined) {
this.loadUrlFromPath(e.detail.path);
}
});
// Legacy: Listen for sidebar state changes (for backward compatibility)
document.addEventListener('sidebar:stateChanged', (e) => {
if (e.detail) {
this.handleSidebarStateChange(e.detail);
}
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Cmd/Ctrl + R for refresh
if ((e.metaKey || e.ctrlKey) && e.key === 'r' && this.iframe && this.iframe.closest(`#${this.id}`)) {
e.preventDefault();
this.refreshIframe();
}
});
// Handle iframe load errors (for X-Frame-Options violations)
if (this.iframe) {
// Store the current src to detect external navigation
let lastSrc = this.iframe.src;
this.iframe.addEventListener('error', (e) => {
console.log('Iframe load error, possibly due to X-Frame-Options');
this.handleIframeError();
});
// Monitor src changes to catch navigation
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
const newSrc = this.iframe.src;
console.log('Iframe src changed from', lastSrc, 'to', newSrc);
// Check if it's an external URL
if (newSrc && (newSrc.startsWith('http://') || newSrc.startsWith('https://'))) {
const localServerUrl = `http://localhost:${this.previewPort}`;
if (!newSrc.startsWith(localServerUrl)) {
console.log('Detected external navigation, redirecting to new tab:', newSrc);
// Prevent the navigation by restoring the previous src
this.iframe.src = lastSrc;
// Open in new tab
window.open(newSrc, '_blank');
return;
}
}
lastSrc = newSrc;
}
});
});
observer.observe(this.iframe, { attributes: true, attributeFilter: ['src'] });
// Also listen for load events to detect if content failed to load
this.iframe.addEventListener('load', () => {
try {
// Try to access the iframe's location - this will throw if blocked by X-Frame-Options
const iframeUrl = this.iframe.contentWindow?.location?.href;
if (!iframeUrl || iframeUrl === 'about:blank') {
// Might be a blocked frame
setTimeout(() => this.checkIframeContent(), 100);
}
} catch (error) {
console.log('Cannot access iframe content, likely blocked by security policy');
this.handleIframeError();
}
});
}
}
subscribeToStore() {
// Subscribe to sidebar store changes
this.store.shouldShowFloatingButton.subscribe(() => this.updateFloatingButton());
this.store.isOpen.subscribe(() => this.updateFloatingButton());
// Initial update
this.updateFloatingButton();
}
updateFloatingButton() {
const floatingBtn = this.floatingHideSidebarBtn;
const floatingIcon = this.floatingHideSidebarIcon;
const shouldShow = this.store.shouldShowFloatingButton.value;
if (floatingBtn) {
if (shouldShow) {
// Show floating button when sidebar is closed
floatingBtn.style.display = 'flex';
// Update icon and title
if (floatingIcon) {
floatingIcon.setAttribute('icon', 'ph:sidebar-simple-fill');
}
floatingBtn.setAttribute('title', 'Show Sidebar');
} else {
// Hide floating button when sidebar is open
floatingBtn.style.display = 'none';
}
}
}
toggleSidebarVisibility() {
// Use the store to toggle
this.store.toggle();
}
refreshIframe() {
if (this.iframe) {
console.log('Refreshing iframe...');
// Force reload by adding timestamp
const url = new URL(this.iframe.src);
url.searchParams.set('_t', Date.now().toString());
this.iframe.src = url.toString();
}
}
loadUrl() {
if (this.urlInput && this.iframe) {
const path = this.urlInput.value.trim();
this.loadUrlFromPath(path);
}
}
loadUrlFromPath(path) {
if (this.iframe) {
console.log('loadUrlFromPath called with path:', path);
// Check if it's an external URL (starts with http:// or https://)
if (path.startsWith('http://') || path.startsWith('https://')) {
// Check if it's NOT our local server
const localServerUrl = `http://localhost:${this.previewPort}`;
if (!path.startsWith(localServerUrl)) {
// Open external URLs in a new tab
console.log('Opening external URL in new tab:', path);
window.open(path, '_blank');
// Clear the input if it exists
if (this.urlInput) {
this.urlInput.value = '';
}
return;
}
}
const baseUrl = `http://localhost:${this.previewPort}`;
const newUrl = path ? baseUrl + '/' + path.replace(/^\//, '') : baseUrl;
console.log('Loading URL in iframe:', newUrl);
this.iframe.src = newUrl;
// Update input if it exists
if (this.urlInput) {
this.urlInput.value = path;
}
}
}
handleSidebarStateChange(state) {
// Legacy method for backward compatibility
console.log('Legacy sidebar state changed:', state);
// The floating button is now handled by updateFloatingButton()
}
handleIframeError() {
// Get the current iframe src and open it in a new tab if it's external
if (this.iframe && this.iframe.src) {
const src = this.iframe.src;
if (src.startsWith('http://') || src.startsWith('https://')) {
if (!src.startsWith(`http://localhost:${this.previewPort}`)) {
console.log('Opening blocked external URL in new tab:', src);
window.open(src, '_blank');
// Reset iframe to local server
this.iframe.src = `http://localhost:${this.previewPort}`;
// Clear input if it exists
if (this.urlInput) {
this.urlInput.value = '';
}
}
}
}
}
checkIframeContent() {
// Additional check for iframe content accessibility
if (this.iframe) {
try {
const doc = this.iframe.contentDocument;
if (!doc || doc.body.innerHTML === '') {
this.handleIframeError();
}
} catch (error) {
this.handleIframeError();
}
}
}
destroy() {
// Clean up event listeners if needed
// (Optional since Sapling handles cleanup)
}
}
// Auto-initialize when script loads
console.log('SimpleLivePreview.js loaded, looking for preview containers...');
function initializeSimpleLivePreview() {
// Look for all sapling-islands and find the ones with SimpleLivePreview content
const saplingIslands = document.querySelectorAll('sapling-island');
for (const island of saplingIslands) {
// Look for a div with an iframe that has data-preview-port
const previewDiv = island.querySelector('div[id] iframe[data-preview-port]');
if (previewDiv) {
const parentDiv = previewDiv.closest('div[id]');
if (parentDiv && parentDiv.id) {
console.log('Found SimpleLivePreview component with ID:', parentDiv.id);
new SimpleLivePreviewManager(parentDiv.id);
}
}
}
}
// Initialize immediately since Sapling islands are ready
initializeSimpleLivePreview();
// Make available globally
window.SimpleLivePreviewManager = SimpleLivePreviewManager;
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
// Cleanup handled by Sapling automatically
});