/* eslint-env browser */
/* global CodeMirror */
// Lightweight CodeMirror editor skeleton
// This file initializes a CodeMirror instance on demand and exposes a simple API
const EditorSkeleton = (function() {
let editor = null;
let container = null;
let currentMode = 'javascript';
function createContainer() {
container = document.createElement('div');
container.id = 'editor-modal';
container.style.position = 'fixed';
container.style.top = '10%';
container.style.left = '50%';
container.style.transform = 'translateX(-50%)';
container.style.width = '80%';
container.style.maxWidth = '900px';
container.style.zIndex = 9999;
container.style.background = 'var(--card-bg, #fff)';
container.style.border = '1px solid rgba(0,0,0,0.08)';
container.style.boxShadow = '0 8px 24px rgba(2,6,23,0.12)';
container.style.padding = '0';
container.style.display = 'none';
const header = document.createElement('div');
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.padding = '8px 12px';
header.style.borderBottom = '1px solid rgba(0,0,0,0.06)';
const title = document.createElement('div');
title.textContent = 'Editor';
title.style.fontWeight = '600';
const controls = document.createElement('div');
controls.className = 'group-buttons';
const runBtn = document.createElement('button');
runBtn.textContent = 'Run';
runBtn.className = 'btn btn-primary btn-sm';
runBtn.addEventListener('click', () => {
const code = editor ? editor.getValue() : '';
runCodeInSandbox(code);
});
const stopBtn = document.createElement('button');
stopBtn.textContent = 'Stop';
stopBtn.className = 'btn btn-sm';
stopBtn.title = 'Stop execution';
stopBtn.addEventListener('click', () => {
stopExecution();
});
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
clearBtn.className = 'btn btn-sm';
clearBtn.title = 'Clear output';
clearBtn.addEventListener('click', () => {
clearOutput();
});
const closeBtn = document.createElement('button');
closeBtn.textContent = 'ปิด';
closeBtn.className = 'btn btn-sm';
closeBtn.addEventListener('click', () => hide());
controls.appendChild(runBtn);
controls.appendChild(stopBtn);
controls.appendChild(clearBtn);
controls.appendChild(closeBtn);
header.appendChild(title);
header.appendChild(controls);
const editorHost = document.createElement('div');
editorHost.id = 'editor-host';
const outputHost = document.createElement('div');
outputHost.id = 'editor-output';
outputHost.style.height = '20vh';
outputHost.style.borderTop = '1px solid rgba(0,0,0,0.06)';
outputHost.style.padding = '8px';
outputHost.style.overflow = 'auto';
container.appendChild(header);
container.appendChild(editorHost);
container.appendChild(outputHost);
document.body.appendChild(container);
}
function init(initialCode = '', mode = 'javascript') {
currentMode = mode || 'javascript';
if (!container) createContainer();
const host = document.getElementById('editor-host');
if (!host) return;
// If CodeMirror is available, initialize or update the editor
if (window.CodeMirror) {
if (!editor) {
editor = CodeMirror(host, {
value: initialCode,
mode: mode,
lineNumbers: true,
theme: 'default',
viewportMargin: Infinity,
});
} else {
editor.setValue(initialCode);
editor.setOption('mode', mode);
}
} else {
host.textContent = 'CodeMirror ไม่พร้อมใช้งาน';
}
}
function runCodeInSandbox(code) {
let outputHost = document.getElementById('editor-output');
if (!outputHost) return;
// create or reuse iframe
let iframe = document.getElementById('editor-run-iframe');
if (iframe) iframe.remove();
iframe = document.createElement('iframe');
iframe.id = 'editor-run-iframe';
iframe.style.border = 'none';
iframe.sandbox = 'allow-scripts';
// clear previous output and iframe
outputHost.innerHTML = '';
// helper: console override script to post messages to parent
const consoleOverride = `
<script>
(function(){
function safeStringify(v){ try { return JSON.stringify(v); } catch(e) { return String(v); } }
const origLog = console.log;
console.log = function(){
const args = Array.from(arguments).map(a => safeStringify(a)).join(' ');
parent.postMessage({type:'editor-log', msg: args}, '*');
origLog.apply(console, arguments);
};
window.addEventListener('error', function(ev){
parent.postMessage({type:'editor-error', msg: ev.message + ' at ' + (ev.filename || '') + ':' + (ev.lineno||'')}, '*');
});
})();
</script>`;
const safeUser = String(code);
let html = '';
const mode = (currentMode || '').toLowerCase();
// detect HTML-like input regardless of selected mode to avoid running HTML inside a <script>
const trimmed = String(code || '').trim();
const looksLikeHtml = trimmed.startsWith('<') || /<!doctype|<html|<head|<body/i.test(trimmed);
const effectiveMode = (mode === 'javascript' && looksLikeHtml) ? 'html' : mode;
if (effectiveMode === 'javascript') {
// Wrap JS in a simple HTML that overrides console
html = `<!doctype html><html><head><meta charset="utf-8">${consoleOverride}</head><body><script>try{${safeUser}}catch(e){parent.postMessage({type:'editor-error', msg: String(e)}, '*');}</script></body></html>`;
// keep iframe hidden for pure JS unless it writes to DOM; but show it so document writes are visible
iframe.style.display = 'none';
outputHost.style.background = '#0b1220';
outputHost.style.color = '#d1fae5';
} else if (effectiveMode === 'html' || effectiveMode === 'htmlmixed' || /<!doctype|<\/?html|<\/?head|<body/i.test(safeUser)) {
// Treat user code as full or partial HTML. Inject console override into head where possible.
let user = safeUser;
// If user contains <html or <head>, attempt to inject consoleOverride into head
if (/<head[^>]*>/i.test(user)) {
user = user.replace(/<head[^>]*>/i, match => match + consoleOverride);
html = user;
} else if (/<html[^>]*>/i.test(user)) {
// insert a head with consoleOverride after <html>
html = user.replace(/<html([^>]*)>/i, (m, g1) => `<html${g1}><head><meta charset="utf-8">${consoleOverride}</head>`);
} else {
// wrap the user's fragment
html = `<!doctype html><html><head><meta charset="utf-8">${consoleOverride}</head><body>${user}</body></html>`;
}
iframe.style.display = 'block';
iframe.style.width = '100%';
iframe.style.height = '18vh';
outputHost.style.background = '#FFFFFF';
} else {
// fallback: treat as plain text
html = `<!doctype html><html><head><meta charset="utf-8">${consoleOverride}</head><body><pre>${safeUser}</pre></body></html>`;
iframe.style.display = 'block';
iframe.style.width = '100%';
iframe.style.height = '18vh';
outputHost.style.background = '#FFFF00';
}
iframe.srcdoc = html;
outputHost.appendChild(iframe);
// Listen for messages from iframe
function onMessage(e) {
if (!e.data) return;
if (e.data.type === 'editor-log') {
appendOutput(e.data.msg);
} else if (e.data.type === 'editor-error') {
appendOutput('Error: ' + e.data.msg);
}
}
// ensure we don't attach multiple handlers
window.removeEventListener('message', window._editorRunListener || (() => {}));
window._editorRunListener = onMessage;
window.addEventListener('message', window._editorRunListener);
}
function clearOutput() {
const out = document.getElementById('editor-output');
if (!out) return;
// remove iframe if exists
const iframe = document.getElementById('editor-run-iframe');
if (iframe) iframe.remove();
// clear textual output
out.innerHTML = '';
}
function stopExecution() {
// remove iframe and message listener
const iframe = document.getElementById('editor-run-iframe');
if (iframe) iframe.remove();
if (window._editorRunListener) {
window.removeEventListener('message', window._editorRunListener);
delete window._editorRunListener;
}
// also clear any output
clearOutput();
}
function appendOutput(text) {
const out = document.getElementById('editor-output');
if (!out) return;
const line = document.createElement('div');
line.style.background = '#0b1220';
line.style.color = '#d1fae5';
line.textContent = text;
line.style.whiteSpace = 'pre-wrap';
out.appendChild(line);
}
function show() {
if (!container) createContainer();
container.style.display = 'block';
setTimeout(() => {
if (editor) editor.refresh();
}, 50);
}
function hide() {
if (container) container.style.display = 'none';
}
function getValue() {
return editor ? editor.getValue() : '';
}
function setValue(v) {
if (editor) editor.setValue(v);
}
return {
init,
show,
hide,
getValue,
setValue,
run: runCodeInSandbox,
};
})();
// Auto-wire: any element with data-action="open-editor" will open editor with code from data-code
window.addEventListener('click', (e) => {
const target = e.target.closest('[data-action="open-editor"]');
if (!target) return;
const code = target.getAttribute('data-code') || '';
const mode = target.getAttribute('data-mode') || 'javascript';
EditorSkeleton.init(code, mode);
EditorSkeleton.show();
});
// Expose for other scripts
window.EditorSkeleton = EditorSkeleton;