editor-skeleton.js

9.78 KB
23/10/2025 01:38
JS
editor-skeleton.js
/* 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;