From c91975db123f389b83d02388852838a03eca5d39 Mon Sep 17 00:00:00 2001 From: Matt Batchelder Date: Sat, 21 Feb 2026 22:52:38 -0500 Subject: [PATCH] Add column and row management features to comparison table block --- theme/blocks/editor.js | 139 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 10 deletions(-) diff --git a/theme/blocks/editor.js b/theme/blocks/editor.js index a28e9d4..f3154d9 100644 --- a/theme/blocks/editor.js +++ b/theme/blocks/editor.js @@ -1705,6 +1705,74 @@ reg('oribi/comparison-table', { var a = props.attributes, s = props.setAttributes; var cols = a.columns || []; var rows = a.rows || []; + + /* ── Column helpers ─────────────────────────────────────── */ + function updateCol(idx, val) { + var c = cols.slice(); c[idx] = val; s({ columns: c }); + } + function addCol() { + var newRows = rows.map(function(r) { + if (r.group) return r; + return Object.assign({}, r, { values: (r.values || []).concat([false]) }); + }); + s({ columns: cols.concat(['Plan']), rows: newRows }); + } + function removeCol(idx) { + var c = cols.slice(); c.splice(idx, 1); + var newRows = rows.map(function(r) { + if (r.group) return r; + var v = (r.values || []).slice(); v.splice(idx, 1); + return Object.assign({}, r, { values: v }); + }); + s({ columns: c, rows: newRows }); + } + + /* ── Row helpers ────────────────────────────────────────── */ + function updateRow(idx, key, val) { + var r = rows.slice(); + r[idx] = Object.assign({}, r[idx]); + r[idx][key] = val; + s({ rows: r }); + } + function updateCell(ri, ci, val) { + var r = rows.slice(); + r[ri] = Object.assign({}, r[ri]); + var v = (r[ri].values || []).slice(); + v[ci] = val; + r[ri].values = v; + s({ rows: r }); + } + function toggleCell(ri, ci) { + var val = (rows[ri].values || [])[ci]; + updateCell(ri, ci, val === true ? false : val === false ? true : true); + } + function switchCellToText(ri, ci) { updateCell(ri, ci, ''); } + function addFeatureRow() { + var vals = cols.map(function() { return false; }); + s({ rows: rows.concat([{ feature: 'New feature', values: vals }]) }); + } + function addGroupRow() { + s({ rows: rows.concat([{ group: 'New Group' }]) }); + } + function removeRow(idx) { + var r = rows.slice(); r.splice(idx, 1); s({ rows: r }); + } + function moveRow(idx, dir) { + var t = idx + dir; + if (t < 0 || t >= rows.length) return; + var r = rows.slice(); + var tmp = r[idx]; r[idx] = r[t]; r[t] = tmp; + s({ rows: r }); + } + + /* ── Inline styles for editor controls ──────────────────── */ + var inputStyle = { width: '100%', padding: '4px 6px', border: '1px solid #ddd', borderRadius: '3px', fontSize: '13px', background: 'transparent', boxSizing: 'border-box' }; + var thInputStyle = Object.assign({}, inputStyle, { fontWeight: 600, textAlign: 'center' }); + var smallBtnStyle = { background: 'none', border: 'none', cursor: 'pointer', padding: '2px 4px', fontSize: '11px', lineHeight: 1, opacity: 0.6, verticalAlign: 'middle' }; + var rowCtrlStyle = { whiteSpace: 'nowrap', width: '1%', padding: '4px', verticalAlign: 'middle', border: 'none', background: 'transparent' }; + var cellBtnStyle = { background: 'none', border: '1px solid #ddd', borderRadius: '3px', cursor: 'pointer', padding: '2px 6px', fontSize: '13px', lineHeight: '1.4', margin: '0 1px' }; + + /* ── Render ─────────────────────────────────────────────── */ return el(Frag, null, el(IC, null, el(PB, { title: 'Table Settings' }, @@ -1712,6 +1780,21 @@ reg('oribi/comparison-table', { { label: 'Normal', value: 'normal' }, { label: 'Alternate', value: 'alt' } ], onChange: function(v){s({variant:v});} }), el(TC, { label: 'Label', value: a.label, onChange: function(v){s({label:v});} }) + ), + el(PB, { title: 'Columns', initialOpen: false }, + cols.map(function(col, i) { + return el('div', { key: i, style: { display: 'flex', gap: '4px', marginBottom: '8px', alignItems: 'center' } }, + el(TC, { label: '', value: col, onChange: function(v){ updateCol(i, v); }, style: { flex: 1 } }), + el(Btn, { isDestructive: true, isSmall: true, onClick: function(){ removeCol(i); } }, '\u2715') + ); + }), + el(Btn, { isSecondary: true, isSmall: true, onClick: addCol }, '+ Add Column') + ), + el(PB, { title: 'Add Rows', initialOpen: false }, + el('div', { style: { display: 'flex', gap: '8px' } }, + el(Btn, { isSecondary: true, isSmall: true, onClick: addFeatureRow }, '+ Feature Row'), + el(Btn, { isSecondary: true, isSmall: true, onClick: addGroupRow }, '+ Group Row') + ) ) ), el('section', { className: a.variant === 'alt' ? 'section section-alt' : 'section' }, @@ -1726,26 +1809,62 @@ reg('oribi/comparison-table', { el('thead', null, el('tr', null, el('th', { className: 'comparison-feature-col' }, 'Feature'), - cols.map(function(col, i) { return el('th', { key: i }, col); }) + cols.map(function(col, i) { + return el('th', { key: i }, + el('input', { type: 'text', value: col, style: thInputStyle, onChange: function(e){ updateCol(i, e.target.value); } }) + ); + }), + el('th', { style: rowCtrlStyle }) ) ), el('tbody', null, - rows.map(function(row, i) { + rows.map(function(row, ri) { if (row.group) { - return el('tr', { key: i, className: 'comparison-group-row' }, - el('td', { colSpan: cols.length + 1 }, row.group) + return el('tr', { key: ri, className: 'comparison-group-row' }, + el('td', { colSpan: cols.length + 1 }, + el('input', { type: 'text', value: row.group, style: Object.assign({}, inputStyle, { fontWeight: 700 }), onChange: function(e){ updateRow(ri, 'group', e.target.value); } }) + ), + el('td', { style: rowCtrlStyle }, + el('button', { style: smallBtnStyle, onClick: function(){ moveRow(ri, -1); }, title: 'Move up' }, '\u25B2'), + el('button', { style: smallBtnStyle, onClick: function(){ moveRow(ri, 1); }, title: 'Move down' }, '\u25BC'), + el('button', { style: Object.assign({}, smallBtnStyle, { color: '#b00' }), onClick: function(){ removeRow(ri); }, title: 'Remove row' }, '\u2715') + ) ); } - return el('tr', { key: i }, - el('td', { className: 'comparison-feature-name' }, row.feature || ''), - (row.values || []).map(function(val, j) { - return el('td', { key: j, className: 'comparison-cell' }, - val === true ? '\u2713' : val === false ? '\u2014' : String(val) + return el('tr', { key: ri }, + el('td', { className: 'comparison-feature-name' }, + el('input', { type: 'text', value: row.feature || '', style: inputStyle, placeholder: 'Feature name\u2026', onChange: function(e){ updateRow(ri, 'feature', e.target.value); } }) + ), + (row.values || []).map(function(val, ci) { + if (typeof val === 'boolean') { + return el('td', { key: ci, className: 'comparison-cell', style: { textAlign: 'center' } }, + el('button', { style: Object.assign({}, cellBtnStyle, { color: val ? '#2e7d32' : '#c62828' }), onClick: function(){ toggleCell(ri, ci); }, title: 'Toggle \u2713/\u2717' }, + val ? '\u2713' : '\u2717' + ), + el('button', { style: Object.assign({}, smallBtnStyle, { fontSize: '10px' }), onClick: function(){ switchCellToText(ri, ci); }, title: 'Switch to text' }, 'Aa') + ); + } + return el('td', { key: ci, className: 'comparison-cell' }, + el('div', { style: { display: 'flex', alignItems: 'center', gap: '2px' } }, + el('input', { type: 'text', value: String(val), style: Object.assign({}, inputStyle, { flex: 1, minWidth: '60px' }), placeholder: 'Value\u2026', onChange: function(e){ updateCell(ri, ci, e.target.value); } }), + el('button', { style: Object.assign({}, smallBtnStyle, { color: '#2e7d32' }), onClick: function(){ updateCell(ri, ci, true); }, title: 'Set as \u2713' }, '\u2713'), + el('button', { style: Object.assign({}, smallBtnStyle, { color: '#c62828' }), onClick: function(){ updateCell(ri, ci, false); }, title: 'Set as \u2717' }, '\u2717') + ) ); - }) + }), + el('td', { style: rowCtrlStyle }, + el('button', { style: smallBtnStyle, onClick: function(){ moveRow(ri, -1); }, title: 'Move up' }, '\u25B2'), + el('button', { style: smallBtnStyle, onClick: function(){ moveRow(ri, 1); }, title: 'Move down' }, '\u25BC'), + el('button', { style: Object.assign({}, smallBtnStyle, { color: '#b00' }), onClick: function(){ removeRow(ri); }, title: 'Remove row' }, '\u2715') + ) ); }) ) + ), + el('div', { style: { display: 'flex', gap: '8px', marginTop: '12px', justifyContent: 'center' } }, + el(Btn, { isSecondary: true, isSmall: true, onClick: addFeatureRow }, '+ Feature Row'), + el(Btn, { isSecondary: true, isSmall: true, onClick: addGroupRow }, '+ Group Row'), + el(Btn, { isSecondary: true, isSmall: true, onClick: addCol }, '+ Column') ) ) )