// ===== Blocks Module - Hierarchical Block Layout with Folding
// - Inline footer (closing line stays in same div as opener)
// - Double-click to focus a subtree
// - In focused view: no ellipsis; wrap long code so blocks expand; show input boxes
// - In full view: condensed single-line with ellipsis; plain text only
// - Tight indent (10px per depth)
// - Full-height colored strip tied to block height and content type
// - Folding: Containers can be collapsed/expanded with a toggle button
// - String/Number inputs with colored underlines (ONLY in focused mode)
// =================================================
document.addEventListener('DOMContentLoaded', () => {
const { editor } = window.editorAPI || {};
if (!editor) {
console.warn('Editor not found, blocks functionality may be limited');
return;
}
// DOM
const blocksBtn = document.getElementById("blocksBtn");
const blocksOverlay = document.getElementById("blocksOverlay");
const blocksClose = document.getElementById("blocksClose");
const blocksCanvas = document.getElementById("blocksCanvas");
// ===== Config =====
const BASE_TAB_PX = 10; // tighter indentation
const BASE_FONT_PX = 12;
const CONTENT_TYPE_COLORS = {
script: '#ff6b6b', // Red for <script> tags
div: '#4CAF50', // Green for <div> tags
function: '#2196F3', // Blue for functions
variable: '#9C27B0', // Purple for variables
line: '#007acc', // Default blue for other lines
container: '#007acc' // Default for containers
};
// ===== View state =====
let rootModel = null; // full document tree
let currentRoot = null; // current subtree being displayed
const viewStack = []; // for Back
// ===== Overlay =====
function openOverlay() {
blocksOverlay.classList.add("open");
parseAndRenderBlocks();
}
function closeOverlay() {
blocksOverlay.classList.remove("open");
if (blocksBtn) blocksBtn.focus();
}
if (blocksBtn) blocksBtn.addEventListener("click", openOverlay);
if (blocksClose) blocksClose.addEventListener("click", closeOverlay);
window.addEventListener("keydown", (e) => {
if (blocksOverlay.classList.contains("open") && e.key === "Escape") closeOverlay();
});
// ===== Parse & Render Entrypoint =====
function forceFoldComputation(session) {
const foldWidgets = session.foldWidgets || {};
const lines = session.getDocument().getAllLines();
let computedCount = 0;
// Iterate through all lines to compute fold widgets
for (let row = 0; row < lines.length; row++) {
if (!(row in foldWidgets) || foldWidgets[row] == null) {
const widget = session.getFoldWidget(row);
foldWidgets[row] = widget; // Update foldWidgets directly
if (widget) computedCount++;
}
}
// Log the number of computed fold widgets
console.log(`Computed ${computedCount} fold widgets for ${lines.length} lines`, foldWidgets);
// Warn if fold widgets are still incomplete
const missingFolds = lines.length - Object.keys(foldWidgets).length;
if (missingFolds > 0) {
console.warn(`${missingFolds} lines missing fold widgets, may fall back to indentation parsing`);
}
session.foldWidgets = foldWidgets; // Ensure session.foldWidgets is updated
// Force editor re-render to ensure fold widgets are applied
session.bgTokenizer.start(0);
}
function parseAndRenderBlocks() {
const session = editor.getSession();
const lines = session.getDocument().getAllLines();
// Force fold computation
forceFoldComputation(session);
const foldWidgets = session.foldWidgets;
rootModel = foldWidgets && Object.keys(foldWidgets).length
? buildTreeWithFolds(lines, foldWidgets, session)
: buildTreeWithIndentation(lines);
// Debug: Log the parsed tree
console.log('Parsed tree:', JSON.stringify(rootModel, (key, value) => {
if (key === 'children') return value.map(child => ({
id: child.id,
text: child.text,
depth: child.depth,
type: child.type,
footerText: child.footerText
}));
return value;
}, 2));
currentRoot = rootModel;
renderTree(currentRoot);
}
// ===== Tree model =====
// Node: { id, text, depth, type:'root'|'container'|'line', contentType:'script'|'div'|'function'|'variable'|'line', children:[], startRow?, endRow?, footerText?, collapsed? }
let nextId = 1;
function makeNode(props) {
return Object.assign(
{ id: nextId++, text: '', depth: 0, type: 'line', contentType: detectContentType(props.text), children: [], collapsed: false },
props
);
}
// Helper to detect content type
function detectContentType(text) {
if (!text) return 'line';
const trimmed = text.trim();
if (trimmed.startsWith('<script') || trimmed.includes('<script>')) return 'script';
if (trimmed.startsWith('<div') || trimmed.includes('<div>')) return 'div';
if (trimmed.includes('function ') || trimmed.match(/function\s+\w+\s*\(/)) return 'function';
if (trimmed.match(/^(let|const|var)\s+\w+/)) return 'variable';
return 'line';
}
// --- Fold-based parser (Ace) ---
function buildTreeWithFolds(lines, foldWidgets, session) {
nextId = 1;
const root = makeNode({ type: 'root', depth: 0, text: 'ROOT' });
const stack = [{ node: root, endRow: null }];
for (let row = 0; row < lines.length; row++) {
const raw = lines[row];
const trimmed = raw.trim();
if (!trimmed) continue;
// Skip comments unless they contain @editIdentifiers@
if (trimmed.startsWith('<!--') && !trimmed.includes('@editIdentifiers@')) {
console.log(`Ignoring comment at row ${row}: ${trimmed}`);
continue;
}
// Clean up stack for containers that have ended
while (stack.length > 1 && stack[stack.length - 1].endRow !== null && stack[stack.length - 1].endRow < row) {
console.log(`Popping node at row ${row}:`, stack[stack.length - 1]);
stack.pop();
}
// Check if this row is the closing tag for the current container
if (stack.length > 1 && stack[stack.length - 1].endRow === row) {
const level = stack[stack.length - 1];
const parent = stack[stack.length - 2].node;
const opener = level.node.text.trim();
// Validate closing tag matches opening tag
if ((opener.match(/^<(div|script|style|body|html|head)(?:\s|>)/) && trimmed.match(new RegExp(`^</${opener.match(/^<(div|script|style|body|html|head)/)?.[1]}>`))) ||
(opener.includes('{') && trimmed.includes('}'))) {
level.node.footerText = trimmed;
console.log(`Assigned footerText at row ${row}: ${trimmed} to node ID ${level.node.id}`);
} else {
console.warn(`Mismatch at row ${row}: expected closing tag for ${opener}, got ${trimmed}`);
parent.children.push(makeNode({
text: trimmed,
depth: parent.depth + 1,
type: 'line'
}));
}
stack.pop();
continue;
}
const fw = foldWidgets[row];
if (fw === 'start') {
const range = session.getFoldWidgetRange(row);
if (!range) {
console.warn(`No fold range for start at row ${row}: ${trimmed}`);
const parent = stack[stack.length - 1].node;
parent.children.push(makeNode({
text: trimmed,
depth: parent.depth + 1,
type: 'line'
}));
continue;
}
const endRow = range.end.row;
// Handle single-line blocks
if (endRow === row) {
const closingMatch = trimmed.match(/<\/(div|script|style|body|html|head)>$/) || trimmed.match(/}$/);
if (closingMatch) {
const parent = stack[stack.length - 1].node;
const containerNode = makeNode({
text: trimmed.replace(closingMatch[0], '').trim(),
depth: parent.depth + 1,
type: 'container',
startRow: row,
endRow,
footerText: closingMatch[0],
collapsed: false
});
parent.children.push(containerNode);
console.log(`Single-line container at row ${row}:`, containerNode);
continue;
}
}
const parent = stack[stack.length - 1].node;
const containerNode = makeNode({
text: trimmed,
depth: parent.depth + 1,
type: 'container',
startRow: row,
endRow,
collapsed: false
});
parent.children.push(containerNode);
stack.push({ node: containerNode, endRow });
console.log(`Pushed container at row ${row}:`, containerNode);
} else {
const parent = stack[stack.length - 1].node;
parent.children.push(makeNode({
text: trimmed,
depth: parent.depth + 1,
type: 'line'
}));
}
}
// Ensure no unclosed containers remain in the stack
while (stack.length > 1) {
console.warn('Unclosed container detected:', stack[stack.length - 1]);
stack.pop();
}
return root;
}
// --- Indentation fallback parser ---
function buildTreeWithIndentation(lines) {
nextId = 1;
const root = makeNode({ type: 'root', depth: 0, text: 'ROOT' });
const stack = [{ node: root, indent: -1 }];
lines.forEach((raw, row) => {
const trimmed = raw.trim();
if (!trimmed) return;
// Skip comments unless they contain @editIdentifiers@
if (trimmed.startsWith('<!--') && !trimmed.includes('@editIdentifiers@')) {
console.log(`Ignoring comment at row ${row}: ${trimmed}`);
return;
}
const indentWidth = raw.length - raw.replace(/^\s*/, '').length;
const currentIndent = Math.floor(indentWidth / 2);
// Clean up stack based on indentation
while (stack.length > 1 && stack[stack.length - 1].indent >= currentIndent) {
console.log(`Popping node at row ${row} due to indentation ${currentIndent}:`, stack[stack.length - 1]);
stack.pop();
}
const parent = stack[stack.length - 1].node;
const isClosingTag = trimmed.match(/^<\/(div|script|style|body|html|head)>/);
const lastContainer = stack[stack.length - 1].node.type === 'container' ? stack[stack.length - 1].node : null;
if (lastContainer && isClosingTag) {
const opener = lastContainer.text.trim();
const closingTagName = isClosingTag[1];
if (opener.match(new RegExp(`^<${closingTagName}(?:\\s|>|$)`)) ||
(opener.includes('{') && trimmed.includes('}'))) {
lastContainer.footerText = trimmed;
console.log(`Assigned footerText at row ${row}: ${trimmed} to node ID ${lastContainer.id}`);
stack.pop();
return;
}
}
const looksOpen = trimmed.includes('{') && !trimmed.includes('}') ||
trimmed.match(/^<(div|script|style|body|html|head)(?:\s|>)/);
if (looksOpen) {
const containerNode = makeNode({
text: trimmed,
depth: parent.depth + 1,
type: 'container',
collapsed: false
});
parent.children.push(containerNode);
stack.push({ node: containerNode, indent: currentIndent });
console.log(`Pushed container at row ${row}:`, containerNode);
} else {
parent.children.push(makeNode({
text: trimmed,
depth: parent.depth + 1,
type: 'line'
}));
}
});
return root;
}
// ===== String/Number/Text/Identifier Input Detection =====
function parseTextWithInputs(text) {
const container = document.createDocumentFragment();
console.log('parseTextWithInputs called with:', text);
const editIdentifiersMatch = text.match(/(?:<!--.*?@editIdentifiers(?:\.([^@\s]+))?@.*?-->|\/\/.*?@editIdentifiers(?:\.([^@\s]+))?@)/);
const shouldEditIdentifiers = editIdentifiersMatch !== null;
const specificIdentifiers = editIdentifiersMatch && (editIdentifiersMatch[1] || editIdentifiersMatch[2])
? (editIdentifiersMatch[1] || editIdentifiersMatch[2]).split('.')
: null;
console.log('Edit identifiers:', shouldEditIdentifiers, 'Specific:', specificIdentifiers);
const cleanText = text
.replace(/<!--.*?@editIdentifiers(?:\.[^@\s]+)?@.*?-->/g, '')
.replace(/\/\/.*?@editIdentifiers(?:\.[^@\s]+)?@.*$/gm, '');
if (!cleanText.trim()) {
return container;
}
const workingText = cleanText;
const stringRegex = /(['"`])((?:\\.|(?!\1)[^\\])*?)\1/g;
const numberRegex = /\b\d+\.?\d*\b/g;
const htmlTextRegex = />([^<]+)</g;
const identifierRegex = /\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g;
let lastIndex = 0;
const matches = [];
let match;
while ((match = stringRegex.exec(workingText)) !== null) {
matches.push({
type: 'string',
value: match[0],
start: match.index,
end: match.index + match[0].length
});
}
htmlTextRegex.lastIndex = 0;
while ((match = htmlTextRegex.exec(workingText)) !== null) {
const textContent = match[1].trim();
if (textContent && textContent.length > 0) {
const textStart = match.index + 1;
matches.push({
type: 'text',
value: textContent,
start: textStart,
end: textStart + textContent.length
});
}
}
numberRegex.lastIndex = 0;
while ((match = numberRegex.exec(workingText)) !== null) {
const isInsideOther = matches.some(token =>
match.index >= token.start && match.index < token.end
);
if (!isInsideOther) {
matches.push({
type: 'number',
value: match[0],
start: match.index,
end: match.index + match[0].length
});
}
}
if (shouldEditIdentifiers) {
identifierRegex.lastIndex = 0;
const identifierCounts = new Map();
while ((match = identifierRegex.exec(workingText)) !== null) {
const identifier = match[0];
const reservedWords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'true', 'false', 'null', 'undefined', 'this', 'new', 'class', 'extends', 'import', 'export', 'from', 'default', 'async', 'await', 'try', 'catch', 'throw', 'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'break', 'continue', 'case', 'switch', 'do', 'with'];
if (reservedWords.includes(identifier)) {
continue;
}
const isInsideOther = matches.some(token =>
match.index >= token.start && match.index < token.end
);
if (isInsideOther) {
continue;
}
const currentCount = identifierCounts.get(identifier) || 0;
identifierCounts.set(identifier, currentCount + 1);
let shouldInclude = false;
if (specificIdentifiers) {
const targetIndex = specificIdentifiers.findIndex((spec, idx) => {
const [targetName, targetOccurrence] = spec.split('.');
const occurrenceNum = targetOccurrence ? parseInt(targetOccurrence, 10) : 0;
return targetName === identifier && currentCount === occurrenceNum;
});
shouldInclude = targetIndex !== -1;
} else {
shouldInclude = true;
}
if (shouldInclude) {
matches.push({
type: 'identifier',
value: identifier,
start: match.index,
end: match.index + identifier.length
});
}
}
}
console.log('Found matches:', matches);
matches.sort((a, b) => a.start - b.start);
if (matches.length === 0) {
container.appendChild(document.createTextNode(workingText));
return container;
}
matches.forEach(token => {
if (token.start > lastIndex) {
const textBefore = workingText.substring(lastIndex, token.start);
container.appendChild(document.createTextNode(textBefore));
}
let inputElement;
if (token.type === 'text') {
inputElement = document.createElement('textarea');
inputElement.value = token.value;
inputElement.style.resize = 'none';
inputElement.style.overflow = 'hidden';
inputElement.style.minHeight = '20px';
inputElement.style.height = '20px';
inputElement.style.verticalAlign = 'top';
inputElement.rows = 1;
const autoResize = () => {
inputElement.style.height = 'auto';
inputElement.style.height = `${inputElement.scrollHeight}px`;
};
setTimeout(autoResize, 0);
inputElement.addEventListener('input', autoResize);
inputElement.style.borderBottom = '2px solid #2196F3';
inputElement.style.backgroundColor = 'rgba(33, 150, 243, 0.1)';
console.log('Created textarea for:', token.value);
} else {
inputElement = document.createElement('input');
inputElement.type = 'text';
inputElement.value = token.value;
inputElement.style.width = `${Math.max(token.value.length + 1, 3)}ch`;
if (token.type === 'string') {
inputElement.style.borderBottom = '2px solid #4CAF50';
inputElement.style.backgroundColor = 'rgba(76, 175, 80, 0.1)';
console.log('Created string input for:', token.value);
} else if (token.type === 'number') {
inputElement.style.borderBottom = '2px solid #FF9800';
inputElement.style.backgroundColor = 'rgba(255, 152, 0, 0.1)';
console.log('Created number input for:', token.value);
} else if (token.type === 'identifier') {
inputElement.style.borderBottom = '2px solid #9C27B0';
inputElement.style.backgroundColor = 'rgba(156, 39, 176, 0.1)';
console.log('Created identifier input for:', token.value);
}
inputElement.addEventListener('input', (e) => {
e.target.style.width = `${Math.max(e.target.value.length + 1, 3)}ch`;
});
}
inputElement.style.border = 'none';
inputElement.style.outline = 'none';
inputElement.style.fontFamily = 'inherit';
inputElement.style.fontSize = 'inherit';
inputElement.style.color = 'inherit';
inputElement.style.padding = '0';
inputElement.style.margin = '0';
inputElement.addEventListener('focus', (e) => {
e.stopPropagation();
});
container.appendChild(inputElement);
lastIndex = token.end;
});
if (lastIndex < workingText.length) {
const remainingText = workingText.substring(lastIndex);
container.appendChild(document.createTextNode(remainingText));
}
return container;
}
// ===== Rendering =====
function renderTree(rootNode) {
blocksCanvas.innerHTML = '';
const isFocused = rootNode !== rootModel;
const headerRow = document.createElement('div');
headerRow.style.display = 'flex';
headerRow.style.alignItems = 'center';
headerRow.style.gap = '8px';
headerRow.style.padding = '8px 0';
if (isFocused) {
const backBtn = document.createElement('button');
backBtn.textContent = '← Back';
backBtn.style.padding = '6px 10px';
backBtn.style.border = '1px solid #bbb';
backBtn.style.borderRadius = '6px';
backBtn.style.background = '#fff';
backBtn.style.cursor = 'pointer';
backBtn.addEventListener('click', () => {
if (viewStack.length) {
currentRoot = viewStack.pop();
renderTree(currentRoot);
}
});
headerRow.appendChild(backBtn);
const crumb = document.createElement('div');
crumb.textContent = displayPath(rootNode);
crumb.style.fontFamily = 'system-ui, sans-serif';
crumb.style.fontSize = '12px';
crumb.style.color = '#666';
headerRow.appendChild(crumb);
}
blocksCanvas.appendChild(headerRow);
const container = document.createElement('div');
container.className = 'blocks-container';
blocksCanvas.appendChild(container);
rootNode.children.forEach(child => {
container.appendChild(renderNode(child, isFocused));
});
}
function displayPath(node) {
const names = [];
let n = node;
while (n && n.type !== 'root') {
names.push(n.text);
n = findParent(rootModel, n.id);
}
return names.reverse().join(' / ');
}
function findParent(root, childId) {
const stack = [root];
while (stack.length) {
const cur = stack.pop();
if (!cur.children) continue;
for (const c of cur.children) {
if (c.id === childId) return cur;
stack.push(c);
}
}
return null;
}
function renderNode(node, isFocused) {
if (node.type === 'container') {
const { blockEl, childContainer } = createContainerBlock(node.text, node.depth, node.id, isFocused, node.collapsed);
if (!node.collapsed) {
node.children.forEach(ch => {
childContainer.appendChild(renderNode(ch, isFocused));
});
}
if (node.footerText && !node.collapsed) {
const footer = document.createElement('div');
footer.className = 'block-footer';
footer.style.marginTop = '4px';
footer.style.opacity = '0.9';
footer.style.fontStyle = 'italic';
footer.style.marginLeft = '0'; // Align with header
if (isFocused) {
footer.appendChild(parseTextWithInputs(node.footerText));
} else {
footer.appendChild(document.createTextNode(node.footerText));
}
applyTextStyles(footer, isFocused);
blockEl.appendChild(footer);
}
return blockEl;
} else {
return createLineBlock(node.text, node.depth, node.id, isFocused);
}
}
// ===== Focus (double-click) =====
function focusOn(nodeId) {
const node = findById(rootModel, nodeId);
if (!node) return;
viewStack.push(currentRoot);
currentRoot = cloneAsFocusedRoot(node);
renderTree(currentRoot);
}
function findById(root, id) {
const stack = [root];
while (stack.length) {
const n = stack.pop();
if (n.id === id) return n;
if (n.children) stack.push(...n.children);
}
return null;
}
function cloneAsFocusedRoot(node) {
const focused = makeNode({
id: node.id,
text: node.text,
depth: 0,
type: 'root',
contentType: node.contentType,
children: [],
collapsed: false
});
if (node.type === 'container') {
const cloneContainer = deepCloneNode(node, node.depth - 1);
focused.children.push(cloneContainer);
} else {
focused.children.push(deepCloneNode(node, node.depth - 1));
}
return focused;
}
function deepCloneNode(node, depthOffset) {
const cloned = makeNode({
id: node.id,
text: node.text,
depth: Math.max(1, node.depth - depthOffset),
type: node.type,
contentType: node.contentType,
children: [],
collapsed: node.collapsed
});
if (node.footerText) cloned.footerText = node.footerText;
if (node.children && node.children.length) {
cloned.children = node.children.map(ch => deepCloneNode(ch, depthOffset));
}
return cloned;
}
// ===== Block constructors =====
function createContainerBlock(headerText, depth, nodeId, isFocused, collapsed) {
const node = findById(rootModel, nodeId);
const blockEl = document.createElement('div');
blockEl.className = 'code-block container-block';
applyBlockBaseStyles(blockEl, depth, node ? node.contentType : 'container');
blockEl.dataset.nodeId = String(nodeId);
const headerWrapper = document.createElement('div');
headerWrapper.style.display = 'flex';
headerWrapper.style.alignItems = 'center';
headerWrapper.style.gap = '6px';
const toggleBtn = document.createElement('button');
toggleBtn.textContent = collapsed ? '+' : '−';
toggleBtn.style.width = '20px';
toggleBtn.style.height = '20px';
toggleBtn.style.padding = '0';
toggleBtn.style.border = '1px solid #bbb';
toggleBtn.style.borderRadius = '4px';
toggleBtn.style.background = '#fff';
toggleBtn.style.cursor = 'pointer';
toggleBtn.style.fontFamily = 'monospace';
toggleBtn.style.fontSize = '14px';
toggleBtn.style.lineHeight = '20px';
toggleBtn.style.textAlign = 'center';
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
const node = findById(rootModel, nodeId);
if (node) {
node.collapsed = !node.collapsed;
renderTree(currentRoot);
}
});
headerWrapper.appendChild(toggleBtn);
const header = document.createElement('div');
header.className = 'block-header';
if (isFocused) {
header.appendChild(parseTextWithInputs(headerText));
} else {
header.appendChild(document.createTextNode(headerText));
}
header.style.fontWeight = '600';
applyTextStyles(header, isFocused);
headerWrapper.appendChild(header);
blockEl.appendChild(headerWrapper);
const childContainer = document.createElement('div');
childContainer.className = 'fold-children';
childContainer.style.marginTop = '4px';
blockEl.appendChild(childContainer);
blockEl.addEventListener('dblclick', (e) => {
e.stopPropagation();
focusOn(Number(blockEl.dataset.nodeId));
});
return { blockEl, childContainer };
}
function createLineBlock(text, depth, nodeId, isFocused) {
const node = findById(rootModel, nodeId);
const block = document.createElement('div');
block.className = 'code-block line-block';
applyBlockBaseStyles(block, depth, node ? node.contentType : 'line');
block.dataset.nodeId = String(nodeId);
const span = document.createElement('span');
if (isFocused) {
span.appendChild(parseTextWithInputs(text));
} else {
span.appendChild(document.createTextNode(text));
}
applyTextStyles(span, isFocused);
block.appendChild(span);
block.addEventListener('dblclick', (e) => {
e.stopPropagation();
focusOn(Number(block.dataset.nodeId));
});
return block;
}
// ===== Shared styling (white blocks + full-height colored strip) =====
function applyBlockBaseStyles(el, depth, contentType) {
el.style.position = 'relative';
el.style.border = '1px solid #ddd';
el.style.margin = '2px 0';
el.style.padding = '6px 10px 6px 16px';
el.style.fontFamily = "'Courier New', monospace";
el.style.fontSize = `${BASE_FONT_PX}px`;
el.style.lineHeight = '1.35';
el.style.marginLeft = `${depth * BASE_TAB_PX}px`;
el.style.minHeight = '20px';
el.style.boxSizing = 'border-box';
el.style.borderRadius = '4px';
el.style.backgroundColor = '#ffffff';
el.style.cursor = 'zoom-in';
const strip = document.createElement('div');
strip.className = 'block-strip';
strip.style.position = 'absolute';
strip.style.left = '10px';
strip.style.top = '4px';
strip.style.bottom = '4px';
strip.style.width = '4px';
strip.style.backgroundColor = CONTENT_TYPE_COLORS[contentType] || CONTENT_TYPE_COLORS.line;
strip.style.borderRadius = '2px';
el.appendChild(strip);
el.addEventListener('mouseenter', () => { el.style.backgroundColor = '#f9fbff'; });
el.addEventListener('mouseleave', () => { el.style.backgroundColor = '#ffffff'; });
}
function applyTextStyles(el, isFocused) {
el.style.color = '#2d2d2d';
el.style.display = 'block';
if (isFocused) {
el.style.whiteSpace = 'pre-wrap';
el.style.overflow = 'visible';
el.style.textOverflow = 'clip';
el.style.wordBreak = 'break-word';
} else {
el.style.whiteSpace = 'nowrap';
el.style.overflow = 'hidden';
el.style.textOverflow = 'ellipsis';
}
}
// ===== Auto-refresh while open =====
let refreshTimeout;
editor.on('change', () => {
clearTimeout(refreshTimeout);
refreshTimeout = setTimeout(() => {
if (blocksOverlay.classList.contains('open')) {
const wasFocused = currentRoot && currentRoot !== rootModel;
const focusId = wasFocused ? currentRoot.children?.[0]?.id : null;
const collapsedStates = extractCollapsedStates(rootModel);
// Force fold computation before parsing
forceFoldComputation(editor.getSession());
parseAndRenderBlocks();
applyCollapsedStates(rootModel, collapsedStates);
if (wasFocused && focusId != null) {
const node = findById(rootModel, focusId);
if (node) {
currentRoot = cloneAsFocusedRoot(node);
renderTree(currentRoot);
}
}
}
}, 350);
});
// Helper to extract collapsed states before re-parsing
function extractCollapsedStates(root) {
const states = {};
const stack = [root];
while (stack.length) {
const node = stack.pop();
if (node.type === 'container' && node.collapsed) {
states[node.id] = true;
}
if (node.children) stack.push(...node.children);
}
return states;
}
// Helper to apply collapsed states after re-parsing
function applyCollapsedStates(root, states) {
const stack = [root];
while (stack.length) {
const node = stack.pop();
if (node.type === 'container' && states[node.id]) {
node.collapsed = true;
}
if (node.children) stack.push(...node.children);
}
}
// ===== Shortcut to open =====
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'B') {
e.preventDefault();
openOverlay();
}
});
// ===== Export API =====
window.blocksAPI = { openOverlay, closeOverlay, parseAndRenderBlocks };
console.log("Blocks module (focused unwrap with folding and color-coded tags) loaded");
});