const { createApp } = Vue; Prism.manual = true; createApp({ data() { return { token: localStorage.getItem("token"), showLogin: false, showShareModal: false, loginEmail: "", loginPassword: "", title: "", html: ` Bin Code `, css: "", js: "", shareUrl: "", currentShareId: null, isDragging: false, startX: null, startWidth: null, containerWidth: null, editorWidth: "50%", minWidth: 250, maxWidth: null, // Tab state activeTab: "html", tabs: [ { id: "html", label: "HTML", icon: '', language: "markup", }, { id: "css", label: "CSS", icon: '', language: "css", }, { id: "js", label: "JavaScript", icon: '', language: "javascript", }, ], extraIcons: [ { visible: ``, }, { hidden: ``, }, { console: ` ` } ], highlightedCode: { html: "", css: "", js: "", }, showEditor: true, showPreview: true, isExecutionPaused: false, updateTimer: null, showConsole: true, playIcon: ``, pauseIcon: ``, }; }, computed: { displayCode() { return { html: this.highlightedCode.html || this.html, css: this.highlightedCode.css || this.css, js: this.highlightedCode.js || this.js, }; }, currentCode: { get() { return this[this.activeTab]; }, set(value) { this[this.activeTab] = value; this.highlightCode(value, this.activeTab); }, }, previewWidth() { if ( typeof this.editorWidth === "string" && this.editorWidth.endsWith("%") ) { return 100 - parseInt(this.editorWidth) + "%"; } return `calc(100% - ${this.editorWidth}px)`; }, currentLanguage() { const tab = this.tabs.find((t) => t.id === this.activeTab); return tab ? tab.language : "markup"; }, }, watch: { html: { handler() { this.schedulePreviewUpdate(); }, }, css: { handler() { this.schedulePreviewUpdate(); }, }, js: { handler() { this.schedulePreviewUpdate(); }, }, }, created() { this.updatePreviewDebounced = this.debounce(this.updatePreview, 50); const urlParams = new URLSearchParams(window.location.search); const shareId = urlParams.get("share"); if (shareId) { this.loadSharedSnippet(shareId); } }, mounted() { this.loadPrismTheme(); this.initThemeListener(); this.initializeLayout(); document.addEventListener("keydown", this.handleKeyboardShortcuts); // initialize syntax highlighting this.highlightCode(this.html, "html"); this.highlightCode(this.css, "css"); this.highlightCode(this.js, "js"); // ensure iframe isloaded before updating the preview const preview = document.getElementById("preview-frame"); if (preview) { preview.onload = () => { this.updatePreview(); }; } }, methods: { loadPrismTheme() { const existingTheme = document.querySelector('link[data-prism-theme]'); if (existingTheme) { existingTheme.remove(); } const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; const link = document.createElement('link'); link.rel = 'stylesheet'; link.setAttribute('data-prism-theme', ''); if (isDarkMode) { link.href = './libraries/prism/theme/prism-vsc-dark-plus.min.css'; } else { link.href = './libraries/prism/theme/prism.min.css'; } document.head.appendChild(link); this.rehighlightCode(); }, rehighlightCode() { this.highlightCode(this.html, 'html'); this.highlightCode(this.css, 'css'); this.highlightCode(this.js, 'js'); }, initThemeListener() { window.matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', () => { this.loadPrismTheme(); }); }, handleScroll(event) { const textarea = event.target; const pre = textarea.nextElementSibling; if (pre) { pre.scrollTop = textarea.scrollTop; pre.scrollLeft = textarea.scrollLeft } }, toggleConsole() { this.showConsole = !this.showConsole; this.updatePreview(); }, schedulePreviewUpdate() { if (this.updateTimer) { clearTimeout(this.updateTimer); } if (!this.isExecutionPaused) { this.updateTimer = setTimeout(() => { this.updatePreview(); }, 500); } }, toggleExecution() { this.isExecutionPaused = !this.isExecutionPaused; if (!this.isExecutionPaused) { // resume execution this.updatePreview(); } }, toggleEditor() { this.showEditor = !this.showEditor; }, togglePreview() { this.showPreview = !this.showPreview; if (!this.showPreview) { this.editorWidth = "100%"; } else { this.editorWidth = "50%"; } }, highlightCode(code, tab) { const languageMap = { html: "markup", css: "css", js: "javascript", }; const language = languageMap[tab]; if (!language) return; // run highlighting in a requestAnimationFrame to avoid blocking the main thread requestAnimationFrame(() => { try { this.highlightedCode[tab] = Prism.highlight( code || "", Prism.languages[language], language ); } catch (error) { console.error("Highlighting error:", error); this.highlightedCode[tab] = code || ""; } }); }, handleKeydown(event) { if (event.key === "Tab") { event.preventDefault(); const textarea = event.target; const start = textarea.selectionStart; const end = textarea.selectionEnd; const spaces = " "; const value = this.currentCode; const beforeCursor = value.substring(0, start); const afterCursor = value.substring(end); if (event.shiftKey) { // TODO: Shift + Tab undo 2 space } else { // Handle Tab (indent) this.currentCode = beforeCursor + spaces + afterCursor; this.$nextTick(() => { textarea.selectionStart = textarea.selectionEnd = start + spaces.length; }); } } }, handleInput(event) { const value = event.target.value; this[this.activeTab] = value; this.highlightCode(value, this.activeTab); }, debounce(fn, delay) { let timeoutId; return function (...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn.apply(this, args), delay); }; }, handleKeyboardShortcuts(e) { // handle Ctrl/Cmd + number for tab switching if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) { const num = parseInt(e.key); if (num >= 1 && num <= this.tabs.length) { this.activeTab = this.tabs[num - 1].id; e.preventDefault(); } } // handle Ctrl/Cmd + S for save if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") { e.preventDefault(); this.saveSnippet(); } }, initializeLayout() { const container = document.querySelector(".editor-container"); this.containerWidth = container.offsetWidth; this.maxWidth = this.containerWidth - this.minWidth; this.updateLayout(); }, updateLayout() { const editorGroup = document.querySelector(".editor-group"); const preview = document.querySelector(".preview"); if (editorGroup && preview) { editorGroup.style.width = this.editorWidth; preview.style.width = this.previewWidth; } }, updatePreview() { if (this.isExecutionPaused) return; const preview = document.getElementById("preview-frame"); if (!preview) return; // create a new iframe to replace the existing one const newFrame = document.createElement("iframe"); newFrame.id = "preview-frame"; // replace the old frame with the new one preview.parentNode.replaceChild(newFrame, preview); // write content to the new frame const doc = newFrame.contentDocument; doc.open(); const content = ` ${this.html || ""} ${this.showConsole ? `` : ''} `; try { doc.write(content); doc.close(); } catch (error) { console.error("Preview update error:", error); } }, switchTab(tabId) { this.activeTab = tabId; // re-highlight code when switching tabs this.$nextTick(() => { this.highlightCode(this[tabId], tabId); const editor = document.querySelector(".editor-content textarea"); if (editor) editor.focus(); }); }, async login() { try { const response = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: this.loginEmail, password: this.loginPassword, }), }); if (!response.ok) throw new Error("Login failed"); const data = await response.json(); this.token = data.token; localStorage.setItem("token", data.token); this.showLogin = false; } catch (error) { alert("Login failed"); } }, logout() { this.token = null; localStorage.removeItem("token"); }, async saveSnippet() { if (!this.token) { this.showLogin = true; return; } try { const response = await fetch("/api/snippets", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.token}`, }, body: JSON.stringify({ title: this.title || "Untitled", html: this.html, css: this.css, js: this.js, }), }); if (!response.ok) throw new Error("Failed to save snippet"); const data = await response.json(); this.currentShareId = data.shareId; this.shareUrl = `${window.location.origin}/?share=${data.shareId}`; this.showShareModal = true; } catch (error) { alert("Failed to save snippet"); } }, async loadSharedSnippet(shareId) { try { const response = await fetch(`/api/snippets/share/${shareId}`); if (!response.ok) throw new Error("Failed to load snippet"); const data = await response.json(); this.title = data.title; this.html = data.html; this.css = data.css; this.js = data.js; this.rehighlightCode(); this.updatePreview(); } catch (error) { alert("Failed to load shared snippet"); } }, async copyShareUrl() { try { await navigator.clipboard.writeText(this.shareUrl); } catch (error) { alert("Failed to copy URL"); } }, }, }).mount("#app");