Skip to content

Commit 759e04f

Browse files
committed
feature: display playground files preview in tabs
1 parent e8d09d3 commit 759e04f

File tree

7 files changed

+297
-176
lines changed

7 files changed

+297
-176
lines changed

web/landing/assets/controllers/playground_controller.js

Lines changed: 1 addition & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
import { Controller } from "@hotwired/stimulus"
2-
import Prism from 'prismjs'
3-
import 'prismjs/components/prism-markup-templating.min.js'
4-
import 'prismjs/components/prism-php.min.js'
5-
import 'prismjs/components/prism-json.min.js'
6-
import 'prismjs/components/prism-csv.min.js'
72

83
export default class extends Controller {
94
static outlets = ["wasm", "code-editor", "turnstile", "playground-output"]
10-
static targets = ["loadingMessage", "loadingBar", "loadingPercent", "navigation", "editor", "outputContainer", "storageIndicator", "filePreviewContainer", "filePreviewTitle", "filePreviewContent", "actionSpinner"]
5+
static targets = ["loadingMessage", "loadingBar", "loadingPercent", "navigation", "editor", "outputContainer", "storageIndicator", "actionSpinner"]
116
static values = {
127
packageIcon: String,
138
linkIcon: String
149
}
1510

16-
#currentPreviewFile = null
17-
1811
connect() {
1912
this.#log('Connecting playground controller')
2013
}
@@ -142,118 +135,6 @@ export default class extends Controller {
142135
await Promise.all(promises)
143136
}
144137

145-
async previewFile(event) {
146-
const filePath = event.currentTarget.dataset.filePath
147-
if (!filePath) {
148-
this.#log('No file path found')
149-
return
150-
}
151-
152-
if (!this.hasWasmOutlet) {
153-
this.#log('WASM outlet not found')
154-
return
155-
}
156-
157-
try {
158-
const fullPath = `/workspace${filePath}`
159-
this.#log('Reading file:', fullPath)
160-
161-
const result = await this.wasmOutlet.readFile(fullPath)
162-
163-
if (!result.success || !result.content) {
164-
if (this.hasPlaygroundOutputOutlet) {
165-
this.playgroundOutputOutlet.show({ content: `Failed to read file: ${filePath}`, type: 'error' })
166-
}
167-
return
168-
}
169-
170-
const content = result.content
171-
172-
this.#currentPreviewFile = { path: filePath, content: content }
173-
174-
if (this.hasFilePreviewTitleTarget) {
175-
this.filePreviewTitleTarget.textContent = `File Preview: ${filePath}`
176-
}
177-
178-
if (this.hasFilePreviewContentTarget) {
179-
const extension = filePath.split('.').pop().toLowerCase()
180-
const languageMap = {
181-
'php': 'php',
182-
'json': 'json',
183-
'csv': 'csv',
184-
'xml': 'markup',
185-
'phar': 'php'
186-
}
187-
188-
const language = languageMap[extension] || 'none'
189-
190-
this.filePreviewContentTarget.textContent = content
191-
this.filePreviewContentTarget.className = `language-${language}`
192-
193-
Prism.highlightElement(this.filePreviewContentTarget)
194-
}
195-
196-
if (this.hasFilePreviewContainerTarget) {
197-
this.filePreviewContainerTarget.style.display = 'block'
198-
this.filePreviewContainerTarget.scrollIntoView({ behavior: 'smooth', block: 'start' })
199-
}
200-
201-
this.#log('File preview loaded:', filePath)
202-
} catch (error) {
203-
this.#log('Error reading file:', error)
204-
if (this.hasPlaygroundOutputOutlet) {
205-
this.playgroundOutputOutlet.show({ content: `Error reading file: ${error.message}`, type: 'error' })
206-
}
207-
}
208-
}
209-
210-
closeFilePreview(event) {
211-
if (event) {
212-
event.preventDefault()
213-
}
214-
215-
if (this.hasFilePreviewContainerTarget) {
216-
this.filePreviewContainerTarget.style.display = 'none'
217-
}
218-
219-
this.#currentPreviewFile = null
220-
}
221-
222-
downloadPreviewFile(event) {
223-
if (event) {
224-
event.preventDefault()
225-
}
226-
227-
if (!this.#currentPreviewFile) {
228-
this.#log('No file to download')
229-
return
230-
}
231-
232-
try {
233-
const { path, content } = this.#currentPreviewFile
234-
const fileName = path.split('/').pop()
235-
236-
const blob = new Blob([content], { type: 'text/plain' })
237-
const url = URL.createObjectURL(blob)
238-
const a = document.createElement('a')
239-
a.href = url
240-
a.download = fileName
241-
document.body.appendChild(a)
242-
a.click()
243-
document.body.removeChild(a)
244-
URL.revokeObjectURL(url)
245-
246-
if (this.hasPlaygroundOutputOutlet) {
247-
this.playgroundOutputOutlet.show({ content: `Downloaded: ${fileName}`, type: 'success' })
248-
}
249-
} catch (error) {
250-
this.#log('Error downloading file:', error)
251-
if (this.hasPlaygroundOutputOutlet) {
252-
this.playgroundOutputOutlet.show({ content: `Error downloading file: ${error.message}`, type: 'error' })
253-
}
254-
}
255-
}
256-
257138
#showContentAfterLoading() {
258139
if (this.hasNavigationTarget) {
259140
this.navigationTarget.style.display = 'grid'
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
import { EditorView, basicSetup } from "codemirror"
3+
import { EditorState } from "@codemirror/state"
4+
import { php } from "@codemirror/lang-php"
5+
import { json } from "@codemirror/lang-json"
6+
import { xml } from "@codemirror/lang-xml"
7+
import { flowThemeExtension } from "../codemirror/themes/theme-flow.js"
8+
9+
export default class extends Controller {
10+
static outlets = ["wasm", "code-editor"]
11+
static targets = ["tabBar", "codeTab", "previewTab", "previewTabName", "codePanel", "previewPanel", "downloadBtn"]
12+
static values = {
13+
activeTab: { type: String, default: 'code' },
14+
previewFile: { type: String, default: '' }
15+
}
16+
17+
#previewEditor = null
18+
#debug = false
19+
20+
connect() {
21+
this.#debug = this.application.debug
22+
this.#log('Connecting tabs controller')
23+
}
24+
25+
disconnect() {
26+
if (this.#previewEditor) {
27+
this.#previewEditor.destroy()
28+
this.#previewEditor = null
29+
}
30+
}
31+
32+
switchToCode(event) {
33+
if (event) {
34+
event.preventDefault()
35+
event.stopPropagation()
36+
}
37+
38+
this.activeTabValue = 'code'
39+
this.#updateTabUI()
40+
}
41+
42+
switchToPreview(event) {
43+
if (event) {
44+
event.preventDefault()
45+
event.stopPropagation()
46+
}
47+
48+
if (!this.previewFileValue) {
49+
return
50+
}
51+
52+
this.activeTabValue = 'preview'
53+
this.#updateTabUI()
54+
}
55+
56+
async openFile(event) {
57+
const filePath = event.currentTarget.dataset.filePath
58+
if (!filePath) {
59+
this.#log('No file path found')
60+
return
61+
}
62+
63+
if (!this.hasWasmOutlet) {
64+
this.#log('WASM outlet not found')
65+
return
66+
}
67+
68+
this.#log('Opening file:', filePath)
69+
70+
try {
71+
const fullPath = `/workspace${filePath}`
72+
const result = await this.wasmOutlet.readFile(fullPath)
73+
74+
if (!result.success || result.content === null || result.content === undefined) {
75+
this.#log('Failed to read file:', filePath)
76+
return
77+
}
78+
79+
const content = typeof result.content === 'string'
80+
? result.content
81+
: new TextDecoder().decode(result.content)
82+
83+
this.previewFileValue = filePath
84+
this.#setPreviewContent(content, filePath)
85+
this.activeTabValue = 'preview'
86+
this.#updateTabUI()
87+
88+
this.#log('File opened:', filePath)
89+
} catch (error) {
90+
this.#log('Error opening file:', error)
91+
}
92+
}
93+
94+
closePreview(event) {
95+
if (event) {
96+
event.preventDefault()
97+
event.stopPropagation()
98+
}
99+
100+
this.previewFileValue = ''
101+
this.activeTabValue = 'code'
102+
this.#updateTabUI()
103+
}
104+
105+
downloadPreviewFile(event) {
106+
if (event) {
107+
event.preventDefault()
108+
}
109+
110+
if (!this.previewFileValue || !this.#previewEditor) {
111+
this.#log('No file to download')
112+
return
113+
}
114+
115+
try {
116+
const content = this.#previewEditor.state.doc.toString()
117+
const fileName = this.previewFileValue.split('/').pop()
118+
119+
const blob = new Blob([content], { type: 'text/plain' })
120+
const url = URL.createObjectURL(blob)
121+
const a = document.createElement('a')
122+
a.href = url
123+
a.download = fileName
124+
document.body.appendChild(a)
125+
a.click()
126+
document.body.removeChild(a)
127+
URL.revokeObjectURL(url)
128+
129+
this.#log('Downloaded:', fileName)
130+
} catch (error) {
131+
this.#log('Error downloading file:', error)
132+
}
133+
}
134+
135+
#setPreviewContent(content, filePath) {
136+
if (!this.hasPreviewPanelTarget) {
137+
return
138+
}
139+
140+
const extension = filePath.split('.').pop().toLowerCase()
141+
const languageExtension = this.#getLanguageExtension(extension)
142+
143+
if (this.#previewEditor) {
144+
this.#previewEditor.destroy()
145+
this.#previewEditor = null
146+
this.previewPanelTarget.innerHTML = ''
147+
}
148+
149+
this.#createPreviewEditor(content, languageExtension)
150+
151+
if (this.hasPreviewTabNameTarget) {
152+
this.previewTabNameTarget.textContent = filePath.split('/').pop()
153+
}
154+
}
155+
156+
#createPreviewEditor(content, languageExtension) {
157+
const extensions = [
158+
basicSetup,
159+
languageExtension || [],
160+
flowThemeExtension,
161+
EditorState.readOnly.of(true),
162+
EditorView.editable.of(false)
163+
].flat()
164+
165+
this.#previewEditor = new EditorView({
166+
state: EditorState.create({
167+
doc: content,
168+
extensions: extensions
169+
}),
170+
parent: this.previewPanelTarget
171+
})
172+
}
173+
174+
#getLanguageExtension(extension) {
175+
const languageMap = {
176+
'php': php(),
177+
'phar': php(),
178+
'json': json(),
179+
'xml': xml()
180+
}
181+
182+
return languageMap[extension] || null
183+
}
184+
185+
#updateTabUI() {
186+
const isCode = this.activeTabValue === 'code'
187+
const hasPreview = !!this.previewFileValue
188+
189+
if (this.hasCodeTabTarget) {
190+
this.codeTabTarget.classList.toggle('active', isCode)
191+
}
192+
193+
if (this.hasPreviewTabTarget) {
194+
this.previewTabTarget.classList.toggle('active', !isCode && hasPreview)
195+
this.previewTabTarget.style.display = hasPreview ? 'flex' : 'none'
196+
}
197+
198+
if (this.hasDownloadBtnTarget) {
199+
this.downloadBtnTarget.style.display = hasPreview && !isCode ? 'block' : 'none'
200+
}
201+
202+
if (this.hasCodePanelTarget) {
203+
this.codePanelTarget.style.display = isCode ? 'block' : 'none'
204+
}
205+
206+
if (this.hasPreviewPanelTarget) {
207+
this.previewPanelTarget.style.display = isCode ? 'none' : 'block'
208+
}
209+
}
210+
211+
#log(...args) {
212+
if (this.#debug) {
213+
console.log('[PlaygroundTabs]', ...args)
214+
}
215+
}
216+
}

web/landing/assets/controllers/playground_workspace_controller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export default class extends Controller {
169169
} else {
170170
html += `
171171
<li class="file-tree-item file clickable" style="padding-left: ${indent}px"
172-
data-action="click->playground#previewFile"
172+
data-action="click->playground-tabs#openFile"
173173
data-file-path="${entry.path}">
174174
<img src="${this.fileIconValue}" class="icon" width="16" height="16" alt="">
175175
<span>${entry.name}</span>

0 commit comments

Comments
 (0)