This commit is contained in:
+358
-48
@@ -1,24 +1,54 @@
|
||||
/* ===== Illustrated Algorithm – Shared Visualization Library ===== */
|
||||
/* Supports BOTH API styles:
|
||||
* Style A (string-returning): renderArray(arr, {highlights}) → HTML string
|
||||
* Style B (DOM-mutating): renderArray(container, items, opts) → mutates container
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/* --- Utility --- */
|
||||
const $ = id => document.getElementById(id);
|
||||
const $$ = sel => document.querySelectorAll(sel);
|
||||
|
||||
/* --- StepController --- */
|
||||
/* --- CSS variable defaults --- */
|
||||
const _CSS = {
|
||||
blue: '#3b82f6', green: '#16a34a', purple: '#8b5cf6',
|
||||
red: '#ef4444', orange: '#f59e0b', cyan: '#06b6d4',
|
||||
text: '#0f172a', text2: '#475569', muted: '#94a3b8',
|
||||
border: '#e2e8f0', surface2: '#f1f5f9',
|
||||
mono: 'Menlo,Consolas,monospace',
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
StepController — supports BOTH constructor signatures:
|
||||
new StepController(opts) → Style A (deferred steps)
|
||||
new StepController(steps, opts) → Style B (immediate steps)
|
||||
================================================================ */
|
||||
class StepController {
|
||||
constructor(steps, opts = {}) {
|
||||
this.steps = steps;
|
||||
constructor(stepsOrOpts, maybeOpts) {
|
||||
// Detect which style
|
||||
if (Array.isArray(stepsOrOpts)) {
|
||||
// Style B: new StepController(steps, opts)
|
||||
this.steps = stepsOrOpts;
|
||||
this.opts = maybeOpts || {};
|
||||
} else {
|
||||
// Style A: new StepController(opts)
|
||||
this.steps = [];
|
||||
this.opts = stepsOrOpts || {};
|
||||
}
|
||||
this.idx = -1;
|
||||
this.autoTimer = null;
|
||||
this.onRender = opts.onRender || (() => {});
|
||||
this.onDone = opts.onDone || (() => {});
|
||||
this.autoDelay = opts.autoDelay || 800;
|
||||
this.autoDelay = this.opts.autoDelay || this.opts.autoInterval || 800;
|
||||
this.onRender = this.opts.onRender || this.opts.onStep || (() => {});
|
||||
this.onDone = this.opts.onDone || this.opts.onComplete || (() => {});
|
||||
this.stepCounterEl = this.opts.stepCounterEl || null;
|
||||
}
|
||||
get current() { return this.idx >= 0 && this.idx < this.steps.length ? this.steps[this.idx] : null; }
|
||||
get isEnd() { return this.idx >= this.steps.length - 1; }
|
||||
get total() { return this.steps.length; }
|
||||
|
||||
/* --- Style A helpers --- */
|
||||
setSteps(steps) { this.steps = steps; this.idx = -1; this.updateCounter(); }
|
||||
set onStep(fn) { this.onRender = fn; }
|
||||
get onStep() { return this.onRender; }
|
||||
|
||||
/* --- Navigation --- */
|
||||
next() {
|
||||
if (this.idx < this.steps.length - 1) { this.idx++; this._render(); }
|
||||
else this.stopAuto();
|
||||
@@ -26,7 +56,8 @@ class StepController {
|
||||
prev() {
|
||||
if (this.idx > 0) { this.idx--; this._render(); }
|
||||
}
|
||||
jumpEnd() {
|
||||
jumpToEnd() { this.jumpEnd(); } // Style A alias
|
||||
jumpEnd() { // Style B name
|
||||
this.idx = this.steps.length - 1;
|
||||
this._render();
|
||||
this.stopAuto();
|
||||
@@ -39,6 +70,14 @@ class StepController {
|
||||
this.stopAuto();
|
||||
this._renderPipeline();
|
||||
if (this.onRender) this.onRender(null);
|
||||
this.updateCounter();
|
||||
}
|
||||
|
||||
/* --- Auto-play --- */
|
||||
toggleAuto() { // Style A name
|
||||
if (this.autoTimer) { this.stopAuto(); return false; }
|
||||
this.startAuto();
|
||||
return true;
|
||||
}
|
||||
startAuto() {
|
||||
this.stopAuto();
|
||||
@@ -52,28 +91,102 @@ class StepController {
|
||||
stopAuto() {
|
||||
if (this.autoTimer) { clearInterval(this.autoTimer); this.autoTimer = null; }
|
||||
}
|
||||
|
||||
/* --- Internal --- */
|
||||
_render() {
|
||||
if (this.onRender) this.onRender(this.current);
|
||||
const step = this.idx >= 0 && this.idx < this.steps.length ? this.steps[this.idx] : null;
|
||||
if (this.onRender) {
|
||||
// Style A pages pass (index, step), Style B passes just step
|
||||
// Try calling with both and let the page's handler decide
|
||||
try { this.onRender(this.idx, step); } catch(e) {
|
||||
try { this.onRender(step); } catch(e2) {}
|
||||
}
|
||||
}
|
||||
this._renderPipeline();
|
||||
this.updateCounter();
|
||||
}
|
||||
_renderPipeline() {
|
||||
const el = $('pipeline');
|
||||
if (!el) return;
|
||||
el.innerHTML = '';
|
||||
for (let i = 0; i < this.steps.length; i++) {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'step-dot' + (i < this.idx ? ' done' : i === this.idx ? ' active' : '');
|
||||
d.title = `Step ${i + 1}`;
|
||||
d.style.cursor = 'pointer';
|
||||
d.addEventListener('click', () => this.jumpTo(i));
|
||||
el.appendChild(d);
|
||||
// Don't overwrite if the page manages its own pipeline HTML
|
||||
// Only render step-dots if pipeline is empty or previously rendered by us
|
||||
if (el.dataset.managed === 'page') return;
|
||||
}
|
||||
updateCounter() {
|
||||
if (this.stepCounterEl) {
|
||||
this.stepCounterEl.textContent = `${this.idx + 1} / ${this.steps.length}`;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- State queries --- */
|
||||
get current() { return this.idx >= 0 && this.idx < this.steps.length ? this.steps[this.idx] : null; }
|
||||
get isEnd() { return this.idx >= this.steps.length - 1; }
|
||||
get total() { return this.steps.length; }
|
||||
isFirst() { return this.idx <= 0; }
|
||||
isLast() { return this.idx >= this.steps.length - 1; }
|
||||
}
|
||||
|
||||
/* --- Render helpers --- */
|
||||
function renderArray(container, items, opts = {}) {
|
||||
const { barMaxH = 120, barMinW = 28, barGap = 4, classes = [], tags = [], idxLabels = true } = opts;
|
||||
/* ================================================================
|
||||
renderArray — supports BOTH signatures:
|
||||
Style A: renderArray(arr, {highlights, pointers, indices, ...}) → HTML string
|
||||
Style B: renderArray(container, items, opts) → mutates container
|
||||
================================================================ */
|
||||
function renderArray(a, b, c) {
|
||||
// Style B: first arg is DOM element
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderArrayB(a, b, c);
|
||||
}
|
||||
// Style A: first arg is plain array, returns HTML string
|
||||
return _renderArrayA(a, b);
|
||||
}
|
||||
|
||||
/* Style A: returns HTML string */
|
||||
function _renderArrayA(arr, options = {}) {
|
||||
const {
|
||||
highlights = {},
|
||||
pointers = {},
|
||||
indices = true,
|
||||
separator = null,
|
||||
customClass = '',
|
||||
width = null,
|
||||
} = options;
|
||||
|
||||
let html = '<div class="nums-line ' + customClass + '">';
|
||||
arr.forEach((val, i) => {
|
||||
const hlClass = highlights[i] || 'default';
|
||||
const widthStyle = width ? `width:${width}px;` : '';
|
||||
html += '<span class="chip-group">';
|
||||
html += `<span class="chip ${hlClass}" style="${widthStyle}">${val}</span>`;
|
||||
if (indices) html += `<span class="chip-index">${i}</span>`;
|
||||
html += '</span>';
|
||||
if (separator !== null && i === separator - 1) {
|
||||
html += '<span class="split">|</span>';
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
if (Object.keys(pointers).length > 0) {
|
||||
html += '<div class="nums-line" style="margin-top:-4px;">';
|
||||
const w = width || 40;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const label = Object.entries(pointers).find(([_, idx]) => idx === i);
|
||||
if (label) {
|
||||
html += `<span class="pointer-arrow" style="min-width:${w}px;">${label[0]}</span>`;
|
||||
} else {
|
||||
html += `<span style="min-width:${w}px;"></span>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/* Style B: mutates container */
|
||||
function _renderArrayB(container, items, opts = {}) {
|
||||
const {
|
||||
barMaxH = 120, barMinW = 28, barGap = 4,
|
||||
classes = [], tags = [], idxLabels = true
|
||||
} = opts || {};
|
||||
const maxVal = Math.max(...items.map(it => typeof it === 'object' ? it.val : it), 1);
|
||||
container.innerHTML = '';
|
||||
const row = document.createElement('div');
|
||||
@@ -96,8 +209,45 @@ function renderArray(container, items, opts = {}) {
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function renderGrid(container, rows, cols, cellData, opts = {}) {
|
||||
/* cellData: 2D array of {val, cls} or null */
|
||||
/* ================================================================
|
||||
renderGrid — supports BOTH:
|
||||
Style A: renderGrid(matrix, {highlights, cellClass, ...}) → HTML string
|
||||
Style B: renderGrid(container, rows, cols, cellData, opts) → mutates container
|
||||
================================================================ */
|
||||
function renderGrid(a, b, c, d, e) {
|
||||
// Style B: first arg is DOM element
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderGridB(a, b, c, d, e);
|
||||
}
|
||||
// Style A: first arg is 2D array
|
||||
return _renderGridA(a, b);
|
||||
}
|
||||
|
||||
function _renderGridA(matrix, options = {}) {
|
||||
const {
|
||||
cellClass = (val, r, c) => '',
|
||||
cellStyle = (val, r, c) => '',
|
||||
highlights = {},
|
||||
cellSize = 40
|
||||
} = options;
|
||||
const rows = matrix.length;
|
||||
const cols = matrix[0]?.length || 0;
|
||||
let html = `<div class="grid-viz" style="grid-template-columns:repeat(${cols}, ${cellSize}px);">`;
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const val = matrix[r][c];
|
||||
const key = `${r},${c}`;
|
||||
const hlc = highlights[key] || '';
|
||||
const extraCls = cellClass(val, r, c);
|
||||
const extraStyle = cellStyle(val, r, c);
|
||||
html += `<div class="grid-cell ${extraCls} ${hlc}" style="width:${cellSize}px;height:${cellSize}px;${extraStyle}">${val}</div>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _renderGridB(container, rows, cols, cellData, opts = {}) {
|
||||
const { cellSize = 48 } = opts;
|
||||
container.innerHTML = '';
|
||||
const grid = document.createElement('div');
|
||||
@@ -115,7 +265,35 @@ function renderGrid(container, rows, cols, cellData, opts = {}) {
|
||||
container.appendChild(grid);
|
||||
}
|
||||
|
||||
function renderCode(container, code, highlightLine = -1) {
|
||||
/* ================================================================
|
||||
renderCode — supports BOTH:
|
||||
Style A: renderCode(code, {lang, highlightLines}) → HTML string
|
||||
Style B: renderCode(container, code, highlightLine) → mutates container
|
||||
================================================================ */
|
||||
function renderCode(a, b, c) {
|
||||
// Style B: first arg is DOM element
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderCodeB(a, b, c);
|
||||
}
|
||||
// Style A: first arg is string
|
||||
return _renderCodeA(a, b);
|
||||
}
|
||||
|
||||
function _renderCodeA(code, options = {}) {
|
||||
const { highlightLines = [], lang = '' } = options;
|
||||
const lines = code.split('\n');
|
||||
let html = `<div class="code-block">`;
|
||||
if (lang) html += `<div class="lang-label">${lang}</div>`;
|
||||
html += `<pre><code>`;
|
||||
lines.forEach((line, i) => {
|
||||
const cls = highlightLines.includes(i + 1) ? 'highlight' : '';
|
||||
html += `<span class="code-line ${cls}">${_escapeHTML(line)}</span>`;
|
||||
});
|
||||
html += '</code></pre></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _renderCodeB(container, code, highlightLine = -1) {
|
||||
container.innerHTML = '';
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'code-block';
|
||||
@@ -133,55 +311,139 @@ function renderCode(container, code, highlightLine = -1) {
|
||||
container.appendChild(pre);
|
||||
}
|
||||
|
||||
function renderLinkedList(container, nodes, opts = {}) {
|
||||
/* nodes: [{val, cls}] */
|
||||
function _escapeHTML(str) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
renderLinkedList — supports BOTH:
|
||||
Style A: renderLinkedList(nodes, {highlights}) → HTML string
|
||||
Style B: renderLinkedList(container, nodes, opts) → mutates container
|
||||
================================================================ */
|
||||
function renderLinkedList(a, b, c) {
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderLinkedListB(a, b, c);
|
||||
}
|
||||
return _renderLinkedListA(a, b);
|
||||
}
|
||||
|
||||
function _renderLinkedListA(nodes, options = {}) {
|
||||
const { highlights = {}, showNext = true, nullLabel = 'NULL' } = options;
|
||||
let html = '<div class="linked-list">';
|
||||
nodes.forEach((node, i) => {
|
||||
const cls = highlights[i] || '';
|
||||
html += `<div class="ll-node ${cls}">`;
|
||||
html += `<span class="val">${node}</span>`;
|
||||
if (showNext) {
|
||||
html += cls === 'null-node'
|
||||
? `<span class="arrow">∅</span>`
|
||||
: `<span class="arrow">→</span>`;
|
||||
}
|
||||
html += '</div>';
|
||||
if (i < nodes.length - 1 && showNext) {
|
||||
html += '<span class="ll-arrow">→</span>';
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _renderLinkedListB(container, nodes, opts = {}) {
|
||||
container.innerHTML = '';
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:center;';
|
||||
nodes.forEach((n, i) => {
|
||||
const box = document.createElement('div');
|
||||
box.style.cssText = `padding:6px 12px;border-radius:6px;font-family:var(--mono);font-size:.85rem;border:2px solid var(--border);background:var(--surface2);transition:all .2s;${n.cls === 'highlight' ? 'border-color:var(--orange);background:var(--orange-dim);' : n.cls === 'done' ? 'border-color:var(--green);background:var(--green-dim);' : ''}`;
|
||||
box.style.cssText = `padding:6px 12px;border-radius:6px;font-family:${_CSS.mono};font-size:.85rem;border:2px solid ${_CSS.border};background:${_CSS.surface2};transition:all .2s;${n.cls === 'highlight' ? 'border-color:var(--orange);background:var(--orange-dim);' : n.cls === 'done' ? 'border-color:var(--green);background:var(--green-dim);' : ''}`;
|
||||
box.textContent = n.val;
|
||||
row.appendChild(box);
|
||||
if (i < nodes.length - 1) {
|
||||
const arrow = document.createElement('span');
|
||||
arrow.textContent = '→';
|
||||
arrow.style.cssText = 'color:var(--text2);font-size:1.1rem;';
|
||||
arrow.style.cssText = `color:${_CSS.text2};font-size:1.1rem;`;
|
||||
row.appendChild(arrow);
|
||||
}
|
||||
});
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function renderBinaryTree(container, tree, opts = {}) {
|
||||
/* simple SVG tree rendering */
|
||||
/* ================================================================
|
||||
renderBinaryTree — supports BOTH:
|
||||
Style A: renderBinaryTree(root, {highlights}) → HTML string
|
||||
Style B: renderBinaryTree(container, tree, opts) → mutates container
|
||||
================================================================ */
|
||||
function renderBinaryTree(a, b, c) {
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderBinaryTreeB(a, b, c);
|
||||
}
|
||||
return _renderBinaryTreeA(a, b);
|
||||
}
|
||||
|
||||
function _renderBinaryTreeA(root, options = {}) {
|
||||
const { highlights = {}, maxWidth = 900 } = options;
|
||||
if (!root) return '<div class="tree-container"><em>空树</em></div>';
|
||||
function buildLevels(node) {
|
||||
const levels = [];
|
||||
const queue = [node];
|
||||
while (queue.length) {
|
||||
const level = [];
|
||||
const nextQueue = [];
|
||||
let hasNonNull = false;
|
||||
for (const n of queue) {
|
||||
if (n) { level.push(n.val); nextQueue.push(n.left, n.right); if (n.left || n.right) hasNonNull = true; }
|
||||
else { level.push(null); nextQueue.push(null, null); }
|
||||
}
|
||||
levels.push(level);
|
||||
queue.length = 0;
|
||||
if (hasNonNull) queue.push(...nextQueue);
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
const levels = buildLevels(root);
|
||||
let html = `<div class="tree-container"><div style="max-width:${maxWidth}px;width:100%;">`;
|
||||
levels.forEach((level, li) => {
|
||||
const gap = Math.max(4, 60 / (li + 1));
|
||||
html += `<div style="display:flex;justify-content:center;gap:${gap}px;margin-bottom:${li === 0 ? 0 : 12}px;">`;
|
||||
level.forEach(val => {
|
||||
if (val === null) { html += `<div style="width:40px;height:40px;"></div>`; }
|
||||
else {
|
||||
const cls = highlights[val] || '';
|
||||
html += `<div class="tree-node"><div class="node-circle ${cls}">${val}</div></div>`;
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _renderBinaryTreeB(container, tree, opts = {}) {
|
||||
container.innerHTML = '';
|
||||
if (!tree) return;
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 500 280');
|
||||
svg.style.width = '100%';
|
||||
svg.style.maxWidth = '500px';
|
||||
const nodeEls = [];
|
||||
function draw(node, x, y, spread, depth) {
|
||||
function draw(node, x, y, spread) {
|
||||
if (!node) return;
|
||||
const leftSpread = spread / 2;
|
||||
const ls = spread / 2;
|
||||
if (node.left) {
|
||||
const lx = x - leftSpread, ly = y + 60;
|
||||
const lx = x - ls, ly = y + 60;
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', x); line.setAttribute('y1', y);
|
||||
line.setAttribute('x2', lx); line.setAttribute('y2', ly);
|
||||
line.setAttribute('stroke', '#2e3347'); line.setAttribute('stroke-width', '2');
|
||||
svg.appendChild(line);
|
||||
draw(node.left, lx, ly, leftSpread, depth + 1);
|
||||
draw(node.left, lx, ly, ls);
|
||||
}
|
||||
if (node.right) {
|
||||
const rx = x + leftSpread, ry = y + 60;
|
||||
const rx = x + ls, ry = y + 60;
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', x); line.setAttribute('y1', y);
|
||||
line.setAttribute('x2', rx); line.setAttribute('y2', ry);
|
||||
line.setAttribute('stroke', '#2e3347'); line.setAttribute('stroke-width', '2');
|
||||
svg.appendChild(line);
|
||||
draw(node.right, rx, ry, leftSpread, depth + 1);
|
||||
draw(node.right, rx, ry, ls);
|
||||
}
|
||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
@@ -193,34 +455,82 @@ function renderBinaryTree(container, tree, opts = {}) {
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', x); text.setAttribute('y', y + 5);
|
||||
text.setAttribute('text-anchor', 'middle'); text.setAttribute('fill', '#e4e6f0');
|
||||
text.setAttribute('font-size', '12'); text.setAttribute('font-family', 'var(--mono)');
|
||||
text.setAttribute('font-size', '12'); text.setAttribute('font-family', _CSS.mono);
|
||||
text.textContent = node.val;
|
||||
g.appendChild(circle); g.appendChild(text);
|
||||
svg.appendChild(g);
|
||||
}
|
||||
draw(tree, 250, 30, 200, 0);
|
||||
draw(tree, 250, 30, 200);
|
||||
container.appendChild(svg);
|
||||
}
|
||||
|
||||
function renderStack(container, items, opts = {}) {
|
||||
/* ================================================================
|
||||
renderStack — supports BOTH:
|
||||
Style A: renderStack(items, {topIndex, poppedIndex}) → HTML string
|
||||
Style B: renderStack(container, items, opts) → mutates container
|
||||
================================================================ */
|
||||
function renderStack(a, b, c) {
|
||||
if (a && a.nodeType === 1) {
|
||||
return _renderStackB(a, b, c);
|
||||
}
|
||||
return _renderStackA(a, b);
|
||||
}
|
||||
|
||||
function _renderStackA(items, options = {}) {
|
||||
const { topIndex = items.length - 1, poppedIndex = -1 } = options;
|
||||
let html = '<div class="stack-container">';
|
||||
if (items.length === 0) html += '<span style="color:var(--text-muted);font-style:italic;">空栈</span>';
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
let cls = '';
|
||||
if (i === topIndex) cls = 'top';
|
||||
if (i === poppedIndex) cls = 'popped';
|
||||
html += `<div class="stack-item ${cls}">${items[i]}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function _renderStackB(container, items, opts = {}) {
|
||||
container.innerHTML = '';
|
||||
const stack = document.createElement('div');
|
||||
stack.style.cssText = 'display:flex;flex-direction:column-reverse;align-items:center;gap:3px;';
|
||||
items.forEach((item, i) => {
|
||||
const d = typeof item === 'object' ? item : { val: item, cls: '' };
|
||||
const el = document.createElement('div');
|
||||
el.style.cssText = `padding:6px 20px;border-radius:4px;font-family:var(--mono);font-size:.82rem;border:1px solid var(--border);background:var(--surface2);min-width:60px;text-align:center;${d.cls === 'top' ? 'border-color:var(--orange);background:var(--orange-dim);' : ''}`;
|
||||
el.style.cssText = `padding:6px 20px;border-radius:4px;font-family:${_CSS.mono};font-size:.82rem;border:1px solid ${_CSS.border};background:${_CSS.surface2};min-width:60px;text-align:center;${d.cls === 'top' ? 'border-color:var(--orange);background:var(--orange-dim);' : ''}`;
|
||||
el.textContent = d.val;
|
||||
stack.appendChild(el);
|
||||
});
|
||||
container.appendChild(stack);
|
||||
}
|
||||
|
||||
function highlightRange(container, start, end, cls) {
|
||||
/* highlight child elements in range [start, end) with class */
|
||||
const children = container.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (i >= start && i < end) children[i].classList.add(cls);
|
||||
else children[i].classList.remove(cls);
|
||||
/* ================================================================
|
||||
highlightRange — returns an highlights object for array indices
|
||||
================================================================ */
|
||||
function highlightRange(arr, start, end, cls = 'active') {
|
||||
const highlights = {};
|
||||
for (let i = start; i <= end && i < arr.length; i++) {
|
||||
highlights[i] = cls;
|
||||
}
|
||||
return highlights;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
escapeHTML — utility
|
||||
================================================================ */
|
||||
function escapeHTML(str) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/* --- Global exports --- */
|
||||
window.StepController = StepController;
|
||||
window.renderArray = renderArray;
|
||||
window.renderLinkedList = renderLinkedList;
|
||||
window.renderBinaryTree = renderBinaryTree;
|
||||
window.renderGrid = renderGrid;
|
||||
window.renderStack = renderStack;
|
||||
window.renderCode = renderCode;
|
||||
window.highlightRange = highlightRange;
|
||||
window.escapeHTML = escapeHTML;
|
||||
window.$ = $;
|
||||
window.$$ = $$;
|
||||
|
||||
Reference in New Issue
Block a user