- Device breakpoint toggles (mobile 375px / tablet 768px / desktop 100%) - Editor theme selector with 6 themes (VS Dark/Light, High Contrast, Monokai, Dracula, GitHub Dark) - Starter template gallery with 8 pre-built templates (Todo, API Fetch, CSS Animation, etc.) - Code autocomplete with DOM/React type definitions and snippet completions - Devtools panels: console, network, elements, performance - Code formatter (Prettier), diff view, and linter integration
91 lines
3.1 KiB
JavaScript
91 lines
3.1 KiB
JavaScript
import { registerClearHandler } from './devtools.js';
|
|
|
|
function renderNode(node, depth, maxExpand) {
|
|
if (node.type === 'text') {
|
|
const text = node.text.trim();
|
|
if (!text) return '';
|
|
return `<div class="el-node"><span class="el-text">${escapeHtml(text)}</span></div>`;
|
|
}
|
|
|
|
const hasChildren = node.children && node.children.length > 0;
|
|
const expanded = depth < maxExpand;
|
|
|
|
let attrs = '';
|
|
if (node.attrs) {
|
|
for (const a of node.attrs) {
|
|
attrs += ` <span class="el-attr-name">${escapeHtml(a.name)}</span>=<span class="el-attr-value">"${escapeHtml(a.value)}"</span>`;
|
|
}
|
|
}
|
|
|
|
let html = '<div class="el-node">';
|
|
html += `<div class="el-node-header" data-has-children="${hasChildren}">`;
|
|
html += `<span class="el-toggle">${hasChildren ? (expanded ? '▼' : '▶') : ' '}</span>`;
|
|
html += `<span><<span class="el-tag">${escapeHtml(node.tag)}</span>${attrs}></span>`;
|
|
|
|
// Inline short text content
|
|
if (hasChildren && node.children.length === 1 && node.children[0].type === 'text' && node.children[0].text.trim().length < 60) {
|
|
html += `<span class="el-text">${escapeHtml(node.children[0].text.trim())}</span>`;
|
|
html += `<span></<span class="el-tag">${escapeHtml(node.tag)}</span>></span>`;
|
|
html += '</div></div>';
|
|
return html;
|
|
}
|
|
|
|
html += '</div>';
|
|
|
|
if (hasChildren) {
|
|
html += `<div class="el-children ${expanded ? 'expanded' : ''}">`;
|
|
for (const child of node.children) {
|
|
html += renderNode(child, depth + 1, maxExpand);
|
|
}
|
|
html += `<div style="padding-left:16px"></<span class="el-tag">${escapeHtml(node.tag)}</span>></div>`;
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
function bindToggleHandlers(container) {
|
|
container.querySelectorAll('.el-node-header[data-has-children="true"]').forEach(header => {
|
|
header.addEventListener('click', () => {
|
|
const children = header.nextElementSibling;
|
|
if (!children || !children.classList.contains('el-children')) return;
|
|
const toggle = header.querySelector('.el-toggle');
|
|
const isExpanded = children.classList.contains('expanded');
|
|
children.classList.toggle('expanded');
|
|
toggle.textContent = isExpanded ? '▶' : '▼';
|
|
});
|
|
});
|
|
}
|
|
|
|
let lastTree = null;
|
|
|
|
function render(tree) {
|
|
lastTree = tree;
|
|
const out = document.getElementById('elements-output');
|
|
if (!tree) {
|
|
out.innerHTML = '<div style="padding:12px;color:var(--text-dim);font-size:11px;">No elements captured. Run your code to see the DOM tree.</div>';
|
|
return;
|
|
}
|
|
out.innerHTML = renderNode(tree, 0, 2);
|
|
bindToggleHandlers(out);
|
|
}
|
|
|
|
export function clearElements() {
|
|
lastTree = null;
|
|
document.getElementById('elements-output').innerHTML = '';
|
|
}
|
|
|
|
export function initElements() {
|
|
registerClearHandler('elements', clearElements);
|
|
|
|
window.addEventListener('message', (e) => {
|
|
if (!e.data || e.data.type !== 'devtools' || e.data.tab !== 'elements') return;
|
|
if (e.data.tree) render(e.data.tree);
|
|
});
|
|
}
|