-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLiveEditor.astro
More file actions
362 lines (317 loc) · 13.7 KB
/
LiveEditor.astro
File metadata and controls
362 lines (317 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
---
import { Edit, ChevronsDownUp } from "lucide-astro";
interface Props {
class?: string;
title?: string;
initialHtml?: string;
initialCss?: string;
initialJs?: string;
localStorageKey?: string;
withCodepenButton?: boolean;
expandedEditors?: boolean; // New prop
}
const {
title = "Untitled",
initialHtml = "",
initialCss = "",
initialJs = "",
class: className = "",
localStorageKey,
withCodepenButton = false,
expandedEditors = false,
} = Astro.props;
// Important! This component is not compatible with Astro's view transitions:
// when navigating to this page after a view transition, the editor will not be initialized.
---
<form
action={withCodepenButton ? "https://codepen.io/pen/define" : undefined}
method="POST"
target="_blank"
class={`live-editor live-editor--static ${className}`}
data-storage-key={localStorageKey}
data-expanded-editors={expandedEditors ? "true" : "false"}
>
<input
class="codepen-data"
type="hidden"
name="data"
value={JSON.stringify({
title: title,
html: initialHtml,
css: initialCss,
js: initialJs,
})}
/>
<div class={`grid grid-rows-5 flex-1 w-full h-full border border-gray-700`}>
<div class="row-span-2 flex flex-col bg-gray-900 border-b border-gray-700 max-w-full editors-container">
<div class="flex justify-between bg-gray-700">
<div class="flex flex-row">
<button type="button" class="tab active" data-target="html">HTML</button>
<button type="button" class="tab" data-target="css">CSS</button>
<button type="button" class="tab" data-target="js">JS</button>
</div>
{
withCodepenButton && (
<button class="codepen-btn" type="submit">
Export to CodePen
<svg
width="24px"
height="24px"
class="inline"
viewBox="0 -0.5 25 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<>
<g stroke-width="0" />
<g stroke-linecap="round" stroke-linejoin="round" />
<g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.5 12L13 14.333V19L20 14.333V9.667L13 5V9.667L16.5 12Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M6.41598 9.04293C6.07132 8.81319 5.60568 8.90635 5.37593 9.25102C5.14619 9.59568 5.23935 10.0613 5.58402 10.2911L6.41598 9.04293ZM12.584 14.9571C12.9287 15.1868 13.3943 15.0936 13.6241 14.749C13.8538 14.4043 13.7606 13.9387 13.416 13.7089L12.584 14.9571ZM6.75 9.667C6.75 9.25279 6.41421 8.917 6 8.917C5.58579 8.917 5.25 9.25279 5.25 9.667H6.75ZM5.25 14.333C5.25 14.7472 5.58579 15.083 6 15.083C6.41421 15.083 6.75 14.7472 6.75 14.333H5.25ZM5.58395 9.04298C5.23932 9.27275 5.1462 9.73841 5.37598 10.083C5.60575 10.4277 6.07141 10.5208 6.41605 10.291L5.58395 9.04298ZM13.416 5.62402C13.7607 5.39425 13.8538 4.92859 13.624 4.58395C13.3942 4.23932 12.9286 4.1462 12.584 4.37598L13.416 5.62402ZM13.416 10.2911C13.7606 10.0613 13.8538 9.59568 13.6241 9.25102C13.3943 8.90635 12.9287 8.81319 12.584 9.04293L13.416 10.2911ZM5.58402 13.7089C5.23935 13.9387 5.14619 14.4043 5.37593 14.749C5.60568 15.0936 6.07132 15.1868 6.41598 14.9571L5.58402 13.7089ZM6.41605 13.709C6.07141 13.4792 5.60575 13.5723 5.37598 13.917C5.1462 14.2616 5.23932 14.7272 5.58395 14.957L6.41605 13.709ZM12.584 19.624C12.9286 19.8538 13.3942 19.7607 13.624 19.416C13.8538 19.0714 13.7607 18.6058 13.416 18.376L12.584 19.624ZM20.416 10.2911C20.7606 10.0613 20.8538 9.59568 20.6241 9.25102C20.3943 8.90635 19.9287 8.81319 19.584 9.04293L20.416 10.2911ZM16.5 12L16.084 11.3759C15.8753 11.515 15.75 11.7492 15.75 12C15.75 12.2508 15.8753 12.485 16.084 12.6241L16.5 12ZM19.584 14.9571C19.9287 15.1868 20.3943 15.0936 20.6241 14.749C20.8538 14.4043 20.7606 13.9387 20.416 13.7089L19.584 14.9571ZM5.58402 10.2911L12.584 14.9571L13.416 13.7089L6.41598 9.04293L5.58402 10.2911ZM5.25 9.667V14.333H6.75V9.667H5.25ZM6.41605 10.291L13.416 5.62402L12.584 4.37598L5.58395 9.04298L6.41605 10.291ZM12.584 9.04293L5.58402 13.7089L6.41598 14.9571L13.416 10.2911L12.584 9.04293ZM5.58395 14.957L12.584 19.624L13.416 18.376L6.41605 13.709L5.58395 14.957ZM19.584 9.04293L16.084 11.3759L16.916 12.6241L20.416 10.2911L19.584 9.04293ZM16.084 12.6241L19.584 14.9571L20.416 13.7089L16.916 11.3759L16.084 12.6241Z"
fill="currentColor"
/>
</g>
</>
</svg>
</button>
)
}
</div>
<div class="html-editor editor active">{initialHtml}</div>
<div class="css-editor editor hidden">{initialCss}</div>
<div class="js-editor editor hidden">{initialJs}</div>
</div>
<div class={`${expandedEditors ? "row-span-3" : "row-span-5"} flex flex-col bg-black preview-container`}>
<div class="bg-gray-700 text-white text-sm font-bold px-4 py-3 flex justify-between items-center">
<h3>
<div class="flex flex-row gap-2">
<span class="preview-title" contenteditable="false" tabindex={"0"}>
{title}
</span>
<span hidden class="preview-title-separator">-</span>
<span hidden class="text-gray-400">Preview</span>
</div>
</h3>
<div>
<button title="Toggle editors" type="button" class="toggle-edit-btn text-gray-400 hover:text-white px-2">
<ChevronsDownUp size={16} class={expandedEditors ? "" : "hidden"} />
<Edit size={16} class={expandedEditors ? "hidden" : ""} />
</button>
</div>
</div>
<iframe class="flex-1 border-none" aria-label="Preview"></iframe>
</div>
</div>
</form>
<script>
import { css } from "@codemirror/lang-css";
import { html } from "@codemirror/lang-html";
import { javascript } from "@codemirror/lang-javascript";
import type { Extension } from "@codemirror/state";
import { vscodeDark } from "@uiw/codemirror-theme-vscode";
import { EditorView, basicSetup } from "codemirror";
import { debounce } from "lodash-es";
function createEditor(editorContainer: Element, selector: string, langExtension: Extension) {
const editorElement = editorContainer.querySelector(selector)!;
const initialCode = editorElement.textContent || "";
editorElement.textContent = "";
const codeEditor = new EditorView({
doc: initialCode,
extensions: [basicSetup, langExtension, vscodeDark, EditorView.lineWrapping],
parent: editorElement,
});
return codeEditor;
}
function setupEditor(editorContainer: Element) {
editorContainer.classList.remove("live-editor--static");
const jsonDataInput = editorContainer.querySelector(".codepen-data") as HTMLInputElement;
jsonDataInput.addEventListener("input", (e) => {
console.log((e.target as HTMLInputElement).value);
});
const htmlEditor = createEditor(
editorContainer,
".html-editor",
html({
autoCloseTags: true,
matchClosingTags: true,
}),
);
const cssEditor = createEditor(editorContainer, ".css-editor", css());
const jsEditor = createEditor(editorContainer, ".js-editor", javascript());
const preview = editorContainer.querySelector("iframe") as HTMLIFrameElement;
const editorsContainer = editorContainer.querySelector(".editors-container") as HTMLElement;
const previewContainer = editorContainer.querySelector(".preview-container") as HTMLElement;
const toggleEditBtn = editorContainer.querySelector(".toggle-edit-btn") as HTMLButtonElement;
const previewTitle = editorContainer.querySelector(".preview-title") as HTMLHeadingElement;
const storageKey = editorContainer.getAttribute("data-storage-key");
// Load saved content from localStorage if storageKey is provided
if (storageKey) {
const savedContent = localStorage.getItem(storageKey);
if (savedContent) {
const { html, css, js, title } = JSON.parse(savedContent);
htmlEditor.dispatch({
changes: { from: 0, to: htmlEditor.state.doc.length, insert: html },
});
cssEditor.dispatch({
changes: { from: 0, to: cssEditor.state.doc.length, insert: css },
});
jsEditor.dispatch({
changes: { from: 0, to: jsEditor.state.doc.length, insert: js },
});
previewTitle.textContent = title;
}
}
const saveToLocalStorage = storageKey
? debounce((data: string) => {
localStorage.setItem(storageKey, data);
}, 400)
: (_data: string) => {};
// Set initial editors visibility
const initialExpandedState = editorContainer.getAttribute("data-expanded-editors") === "true";
editorsContainer.style.display = initialExpandedState ? "flex" : "none";
updateToggleButtonIcon(initialExpandedState);
previewTitle.addEventListener("click", () => {
const isExpanded = editorsContainer.style.display !== "none";
if (isExpanded) {
previewTitle.contentEditable = "true";
}
});
toggleEditBtn.addEventListener("click", () => {
const isExpanded = editorsContainer.style.display !== "none";
editorsContainer.style.display = isExpanded ? "none" : "flex";
previewTitle.contentEditable = isExpanded ? "false" : "true";
updateToggleButtonIcon(!isExpanded);
if (!isExpanded) {
previewContainer.classList.toggle("row-span-3", isExpanded);
previewTitle.focus();
const range = document.createRange();
range.selectNodeContents(previewTitle);
range.collapse(false); // This moves the cursor to the end
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
} else {
previewContainer.classList.toggle("row-span-5", isExpanded);
}
});
previewTitle.addEventListener("blur", () => {
updateJsonData();
previewTitle.contentEditable = "false";
});
previewTitle.addEventListener(
"keydown",
debounce((e: KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
previewTitle.blur();
return;
}
updateJsonData();
}, 200),
);
function updateToggleButtonIcon(isExpanded: boolean) {
if (isExpanded) {
toggleEditBtn.querySelector("svg:first-child")!.classList.remove("hidden");
toggleEditBtn.querySelector("svg:last-child")!.classList.add("hidden");
} else {
toggleEditBtn.querySelector("svg:first-child")!.classList.add("hidden");
toggleEditBtn.querySelector("svg:last-child")!.classList.remove("hidden");
}
}
function updateJsonData() {
const htmlContent = htmlEditor.state.doc.toString();
const cssContent = cssEditor.state.doc.toString();
const jsContent = jsEditor.state.doc.toString();
const currentTitle = previewTitle.textContent || "";
const jsonData = JSON.stringify({
title: currentTitle,
html: htmlContent,
css: cssContent,
js: jsContent,
});
jsonDataInput.value = jsonData;
saveToLocalStorage(jsonData);
}
function updatePreview() {
const htmlContent = htmlEditor.state.doc.toString();
const cssContent = cssEditor.state.doc.toString();
const jsContent = jsEditor.state.doc.toString();
const content = `<html>
<head><style>${cssContent}</style></head>
<body>
${htmlContent}
<script type="module">${jsContent}<\/script>
</body>
</html>`;
preview.srcdoc = content.trim();
updateJsonData();
}
htmlEditor.dom.addEventListener("keyup", debounce(updatePreview, 200));
cssEditor.dom.addEventListener("keyup", debounce(updatePreview, 200));
jsEditor.dom.addEventListener("keyup", debounce(updatePreview, 200));
// Tab switching logic
const tabs = editorContainer.querySelectorAll(".tab");
const editors = editorContainer.querySelectorAll(".editor");
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
const target = tab.getAttribute("data-target");
tabs.forEach((t) => t.classList.remove("active"));
editors.forEach((e) => e.classList.add("hidden"));
tab.classList.add("active");
editorContainer.querySelector(`.${target}-editor`)?.classList.remove("hidden");
});
});
updatePreview();
}
document.querySelectorAll(".live-editor").forEach(setupEditor);
</script>
<style is:global>
@import "https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css";
@import "https://fonts.googleapis.com/css2?family=Fira+Code&display=swap";
.cm-editor .cm-scroller {
font-family: "Fira Code", monospace;
font-size: 14px;
flex-grow: 1;
}
.cm-editor .cm-scroller,
.cm-editor .cm-gutters {
@apply bg-gray-900;
}
.tab {
@apply text-white text-sm font-bold px-4 py-3 cursor-pointer h-auto;
}
.codepen-btn {
@apply text-gray-400 hover:text-white text-sm font-bold px-4 py-3 cursor-pointer h-auto;
}
.tab.active {
@apply bg-gray-600;
}
.editor {
@apply flex-1 max-h-full max-w-full overflow-auto;
}
.live-editor--static {
opacity: 0.3;
cursor: not-allowed;
* {
cursor: not-allowed;
pointer-events: none;
}
}
.preview-title[contenteditable="true"] {
@apply outline-none border-b border-dashed border-white;
}
.toggle-edit-btn {
@apply text-gray-400 hover:text-white text-sm font-bold cursor-pointer;
}
</style>