feat: Redesign variant editor with preset buttons
Major UX improvements based on user feedback: - Replaced auto-populated columns with preset shortcut buttons - Quick Add buttons for Size, Color, Material, Storage - Custom button for user-defined property names - Double-click column headers to rename - Rename column option in context menu - Starts with single empty column instead of defaults - Improved usage instructions in UI - Cache-busting version updated to force reload This design is more flexible and less confusing than auto-generating columns.
This commit is contained in:
parent
b53597f250
commit
abe01cb8a0
@ -42,11 +42,11 @@
|
|||||||
<div class="form-text mt-2">
|
<div class="form-text mt-2">
|
||||||
<strong><i class="fas fa-info-circle"></i> How to use:</strong>
|
<strong><i class="fas fa-info-circle"></i> How to use:</strong>
|
||||||
<ul class="mb-1">
|
<ul class="mb-1">
|
||||||
<li><strong>Click column headers</strong> to select property type (Size, Color, Material, Storage, or Custom)</li>
|
<li><strong>Quick Add buttons:</strong> Click preset buttons (Size, Color, Material, Storage) to add pre-populated columns</li>
|
||||||
<li><strong>Click cells</strong> to edit values directly</li>
|
<li><strong>Custom button:</strong> Click Custom to add a column with your own name</li>
|
||||||
<li><strong>Right-click</strong> for menu to add/remove rows and columns</li>
|
<li><strong>Double-click headers:</strong> Double-click column headers to rename them</li>
|
||||||
|
<li><strong>Right-click menu:</strong> Add/remove rows and columns, rename columns, clear values</li>
|
||||||
<li><strong>Keyboard shortcuts:</strong> Tab to move right, Enter to move down, Ctrl+D to delete, Ctrl+Enter to save</li>
|
<li><strong>Keyboard shortcuts:</strong> Tab to move right, Enter to move down, Ctrl+D to delete, Ctrl+Enter to save</li>
|
||||||
<li><strong>Mobile:</strong> Swipe rows left/right to delete</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<small class="text-muted">Changes are automatically saved when you click "Create Collection" below.</small>
|
<small class="text-muted">Changes are automatically saved when you click "Create Collection" below.</small>
|
||||||
</div>
|
</div>
|
||||||
@ -67,14 +67,14 @@
|
|||||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||||
|
|
||||||
<!-- Handsontable Spreadsheet Library -->
|
<!-- Handsontable Spreadsheet Library -->
|
||||||
<link rel="stylesheet" href="~/lib/handsontable/handsontable.full.min.css?v=20251113" />
|
<link rel="stylesheet" href="~/lib/handsontable/handsontable.full.min.css?v=20251113b" />
|
||||||
<script src="~/lib/handsontable/handsontable.full.min.js?v=20251113"></script>
|
<script src="~/lib/handsontable/handsontable.full.min.js?v=20251113b"></script>
|
||||||
|
|
||||||
<!-- Hammer.js for Touch Gestures -->
|
<!-- Hammer.js for Touch Gestures -->
|
||||||
<script src="~/lib/hammerjs/hammer.min.js?v=20251113"></script>
|
<script src="~/lib/hammerjs/hammer.min.js?v=20251113b"></script>
|
||||||
|
|
||||||
<!-- Variant Editor Module -->
|
<!-- Variant Editor Module -->
|
||||||
<script src="~/js/variant-editor.js?v=20251113"></script>
|
<script src="~/js/variant-editor.js?v=20251113b"></script>
|
||||||
|
|
||||||
<!-- Initialize Variant Editor -->
|
<!-- Initialize Variant Editor -->
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -43,11 +43,11 @@
|
|||||||
<div class="form-text mt-2">
|
<div class="form-text mt-2">
|
||||||
<strong><i class="fas fa-info-circle"></i> How to use:</strong>
|
<strong><i class="fas fa-info-circle"></i> How to use:</strong>
|
||||||
<ul class="mb-1">
|
<ul class="mb-1">
|
||||||
<li><strong>Click column headers</strong> to select property type (Size, Color, Material, Storage, or Custom)</li>
|
<li><strong>Quick Add buttons:</strong> Click preset buttons (Size, Color, Material, Storage) to add pre-populated columns</li>
|
||||||
<li><strong>Click cells</strong> to edit values directly</li>
|
<li><strong>Custom button:</strong> Click Custom to add a column with your own name</li>
|
||||||
<li><strong>Right-click</strong> for menu to add/remove rows and columns</li>
|
<li><strong>Double-click headers:</strong> Double-click column headers to rename them</li>
|
||||||
|
<li><strong>Right-click menu:</strong> Add/remove rows and columns, rename columns, clear values</li>
|
||||||
<li><strong>Keyboard shortcuts:</strong> Tab to move right, Enter to move down, Ctrl+D to delete, Ctrl+Enter to save</li>
|
<li><strong>Keyboard shortcuts:</strong> Tab to move right, Enter to move down, Ctrl+D to delete, Ctrl+Enter to save</li>
|
||||||
<li><strong>Mobile:</strong> Swipe rows left/right to delete</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<small class="text-muted">Changes are automatically saved when you click "Save Changes" below.</small>
|
<small class="text-muted">Changes are automatically saved when you click "Save Changes" below.</small>
|
||||||
</div>
|
</div>
|
||||||
@ -78,14 +78,14 @@
|
|||||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||||
|
|
||||||
<!-- Handsontable Spreadsheet Library -->
|
<!-- Handsontable Spreadsheet Library -->
|
||||||
<link rel="stylesheet" href="~/lib/handsontable/handsontable.full.min.css?v=20251113" />
|
<link rel="stylesheet" href="~/lib/handsontable/handsontable.full.min.css?v=20251113b" />
|
||||||
<script src="~/lib/handsontable/handsontable.full.min.js?v=20251113"></script>
|
<script src="~/lib/handsontable/handsontable.full.min.js?v=20251113b"></script>
|
||||||
|
|
||||||
<!-- Hammer.js for Touch Gestures -->
|
<!-- Hammer.js for Touch Gestures -->
|
||||||
<script src="~/lib/hammerjs/hammer.min.js?v=20251113"></script>
|
<script src="~/lib/hammerjs/hammer.min.js?v=20251113b"></script>
|
||||||
|
|
||||||
<!-- Variant Editor Module -->
|
<!-- Variant Editor Module -->
|
||||||
<script src="~/js/variant-editor.js?v=20251113"></script>
|
<script src="~/js/variant-editor.js?v=20251113b"></script>
|
||||||
|
|
||||||
<!-- Initialize Variant Editor -->
|
<!-- Initialize Variant Editor -->
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -6,8 +6,9 @@
|
|||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Property presets (Size, Color, Material, Storage, Custom)
|
* - Property presets (Size, Color, Material, Storage, Custom)
|
||||||
|
* - Preset shortcut buttons for quick column addition
|
||||||
* - Inline cell editing with Tab navigation
|
* - Inline cell editing with Tab navigation
|
||||||
* - Add/remove rows and columns dynamically
|
* - Add/remove rows and columns via context menu
|
||||||
* - Keyboard shortcuts (Ctrl+D delete, Ctrl+Enter save)
|
* - Keyboard shortcuts (Ctrl+D delete, Ctrl+Enter save)
|
||||||
* - Mobile-friendly touch gestures
|
* - Mobile-friendly touch gestures
|
||||||
* - Automatic JSON serialization
|
* - Automatic JSON serialization
|
||||||
@ -19,8 +20,8 @@ class VariantEditor {
|
|||||||
this.hot = null;
|
this.hot = null;
|
||||||
this.initialData = this.parseInitialData(initialData);
|
this.initialData = this.parseInitialData(initialData);
|
||||||
this.options = {
|
this.options = {
|
||||||
minRows: 3,
|
minRows: 5,
|
||||||
minCols: 2,
|
minCols: 1,
|
||||||
maxCols: 10,
|
maxCols: 10,
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
@ -30,8 +31,7 @@ class VariantEditor {
|
|||||||
'Size': ['XS', 'S', 'M', 'L', 'XL', 'XXL'],
|
'Size': ['XS', 'S', 'M', 'L', 'XL', 'XXL'],
|
||||||
'Color': ['Black', 'White', 'Red', 'Blue', 'Green', 'Yellow', 'Pink', 'Purple', 'Gray', 'Brown'],
|
'Color': ['Black', 'White', 'Red', 'Blue', 'Green', 'Yellow', 'Pink', 'Purple', 'Gray', 'Brown'],
|
||||||
'Material': ['Cotton', 'Polyester', 'Silk', 'Wool', 'Leather', 'Denim', 'Linen'],
|
'Material': ['Cotton', 'Polyester', 'Silk', 'Wool', 'Leather', 'Denim', 'Linen'],
|
||||||
'Storage': ['16GB', '32GB', '64GB', '128GB', '256GB', '512GB', '1TB'],
|
'Storage': ['16GB', '32GB', '64GB', '128GB', '256GB', '512GB', '1TB']
|
||||||
'Custom': null // Freeform text
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
@ -41,13 +41,16 @@ class VariantEditor {
|
|||||||
* Parse initial JSON data into spreadsheet format
|
* Parse initial JSON data into spreadsheet format
|
||||||
*/
|
*/
|
||||||
parseInitialData(jsonString) {
|
parseInitialData(jsonString) {
|
||||||
|
// If no data, start with empty grid
|
||||||
if (!jsonString || jsonString.trim() === '' || jsonString === '[]') {
|
if (!jsonString || jsonString.trim() === '' || jsonString === '[]') {
|
||||||
return {
|
return {
|
||||||
properties: ['Size', 'Color'],
|
properties: ['Property 1'],
|
||||||
values: [
|
values: [
|
||||||
['S', 'Black'],
|
[null],
|
||||||
['M', 'White'],
|
[null],
|
||||||
['L', 'Red']
|
[null],
|
||||||
|
[null],
|
||||||
|
[null]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -63,7 +66,7 @@ class VariantEditor {
|
|||||||
const propertyValues = properties.map(p => p.values);
|
const propertyValues = properties.map(p => p.values);
|
||||||
|
|
||||||
// Determine max number of rows needed
|
// Determine max number of rows needed
|
||||||
const maxRows = Math.max(...propertyValues.map(v => Array.isArray(v) ? v.length : 0), 1);
|
const maxRows = Math.max(...propertyValues.map(v => Array.isArray(v) ? v.length : 0), this.options.minRows);
|
||||||
|
|
||||||
// Build grid data
|
// Build grid data
|
||||||
const gridData = [];
|
const gridData = [];
|
||||||
@ -83,7 +86,7 @@ class VariantEditor {
|
|||||||
values: gridData
|
values: gridData
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to parse initial data, using defaults:', e);
|
console.warn('Failed to parse initial data, using empty grid:', e);
|
||||||
return this.parseInitialData('');
|
return this.parseInitialData('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,28 +100,59 @@ class VariantEditor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hot = new Handsontable(this.container, {
|
// Create preset buttons toolbar
|
||||||
|
this.createPresetToolbar();
|
||||||
|
|
||||||
|
// Create spreadsheet container
|
||||||
|
const spreadsheetDiv = document.createElement('div');
|
||||||
|
spreadsheetDiv.id = this.container.id + '-grid';
|
||||||
|
spreadsheetDiv.style.marginTop = '10px';
|
||||||
|
this.container.appendChild(spreadsheetDiv);
|
||||||
|
|
||||||
|
this.hot = new Handsontable(spreadsheetDiv, {
|
||||||
data: this.initialData.values,
|
data: this.initialData.values,
|
||||||
colHeaders: this.initialData.properties,
|
colHeaders: this.initialData.properties,
|
||||||
rowHeaders: true,
|
rowHeaders: true,
|
||||||
minRows: this.options.minRows,
|
minRows: this.options.minRows,
|
||||||
minSpareRows: 1,
|
minSpareRows: 1,
|
||||||
minCols: this.options.minCols,
|
minCols: this.options.minCols,
|
||||||
minSpareCols: 1,
|
minSpareCols: 0,
|
||||||
maxCols: this.options.maxCols,
|
maxCols: this.options.maxCols,
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
items: {
|
items: {
|
||||||
'row_above': {},
|
'row_above': {
|
||||||
'row_below': {},
|
name: 'Insert row above'
|
||||||
|
},
|
||||||
|
'row_below': {
|
||||||
|
name: 'Insert row below'
|
||||||
|
},
|
||||||
'separator1': '---------',
|
'separator1': '---------',
|
||||||
'remove_row': {},
|
'remove_row': {
|
||||||
|
name: 'Delete row'
|
||||||
|
},
|
||||||
'separator2': '---------',
|
'separator2': '---------',
|
||||||
'col_left': {},
|
'col_left': {
|
||||||
'col_right': {},
|
name: 'Insert column left'
|
||||||
|
},
|
||||||
|
'col_right': {
|
||||||
|
name: 'Insert column right'
|
||||||
|
},
|
||||||
'separator3': '---------',
|
'separator3': '---------',
|
||||||
'remove_col': {},
|
'remove_col': {
|
||||||
|
name: 'Delete column'
|
||||||
|
},
|
||||||
'separator4': '---------',
|
'separator4': '---------',
|
||||||
'clear_column': {},
|
'rename_column': {
|
||||||
|
name: 'Rename column...',
|
||||||
|
callback: (key, selection) => {
|
||||||
|
const col = selection[0].start.col;
|
||||||
|
this.showRenameDialog(col);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'separator5': '---------',
|
||||||
|
'clear_column': {
|
||||||
|
name: 'Clear column values'
|
||||||
|
},
|
||||||
'undo': {},
|
'undo': {},
|
||||||
'redo': {}
|
'redo': {}
|
||||||
}
|
}
|
||||||
@ -128,23 +162,18 @@ class VariantEditor {
|
|||||||
undo: true,
|
undo: true,
|
||||||
autoWrapRow: true,
|
autoWrapRow: true,
|
||||||
autoWrapCol: true,
|
autoWrapCol: true,
|
||||||
enterMoves: { row: 0, col: 1 }, // Tab behavior: move right
|
enterMoves: { row: 1, col: 0 }, // Enter moves down
|
||||||
tabMoves: { row: 0, col: 1 },
|
tabMoves: { row: 0, col: 1 }, // Tab moves right
|
||||||
fillHandle: {
|
fillHandle: {
|
||||||
autoInsertRow: true
|
autoInsertRow: true
|
||||||
},
|
},
|
||||||
stretchH: 'all',
|
stretchH: 'all',
|
||||||
licenseKey: 'non-commercial-and-evaluation', // Community Edition
|
licenseKey: 'non-commercial-and-evaluation', // Community Edition
|
||||||
|
|
||||||
// Enable column header editing for property names
|
// Double-click header to rename
|
||||||
afterGetColHeader: (col, TH) => {
|
|
||||||
TH.classList.add('editable-header');
|
|
||||||
},
|
|
||||||
|
|
||||||
// Make headers clickable to change property type
|
|
||||||
afterOnCellMouseDown: (event, coords) => {
|
afterOnCellMouseDown: (event, coords) => {
|
||||||
if (coords.row === -1 && coords.col >= 0) {
|
if (coords.row === -1 && coords.col >= 0 && event.detail === 2) {
|
||||||
this.showPropertyTypeMenu(coords.col, event);
|
this.showRenameDialog(coords.col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -159,104 +188,128 @@ class VariantEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show property type selection menu
|
* Create preset shortcut buttons toolbar
|
||||||
*/
|
*/
|
||||||
showPropertyTypeMenu(colIndex, event) {
|
createPresetToolbar() {
|
||||||
const currentHeader = this.hot.getColHeader(colIndex);
|
const toolbar = document.createElement('div');
|
||||||
const menu = document.createElement('div');
|
toolbar.className = 'variant-preset-toolbar';
|
||||||
menu.className = 'property-type-menu';
|
toolbar.style.marginBottom = '10px';
|
||||||
menu.style.position = 'absolute';
|
toolbar.style.display = 'flex';
|
||||||
menu.style.left = event.clientX + 'px';
|
toolbar.style.gap = '8px';
|
||||||
menu.style.top = event.clientY + 'px';
|
toolbar.style.flexWrap = 'wrap';
|
||||||
menu.style.zIndex = 10000;
|
|
||||||
menu.style.background = 'white';
|
|
||||||
menu.style.border = '1px solid #ccc';
|
|
||||||
menu.style.borderRadius = '4px';
|
|
||||||
menu.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
|
|
||||||
menu.style.padding = '8px 0';
|
|
||||||
menu.style.minWidth = '150px';
|
|
||||||
|
|
||||||
const presetOptions = Object.keys(this.propertyPresets);
|
const label = document.createElement('strong');
|
||||||
presetOptions.push('Custom...');
|
label.textContent = 'Quick Add: ';
|
||||||
|
label.style.marginRight = '8px';
|
||||||
|
label.style.alignSelf = 'center';
|
||||||
|
toolbar.appendChild(label);
|
||||||
|
|
||||||
presetOptions.forEach(preset => {
|
// Add preset buttons
|
||||||
const option = document.createElement('div');
|
Object.keys(this.propertyPresets).forEach(presetName => {
|
||||||
option.className = 'property-type-option';
|
const button = document.createElement('button');
|
||||||
option.textContent = preset === 'Custom...' ? '✏️ ' + preset : preset;
|
button.type = 'button';
|
||||||
option.style.padding = '8px 16px';
|
button.className = 'btn btn-sm btn-outline-primary';
|
||||||
option.style.cursor = 'pointer';
|
button.innerHTML = `<i class="fas fa-plus"></i> ${presetName}`;
|
||||||
|
button.style.fontSize = '12px';
|
||||||
|
|
||||||
if (preset === currentHeader) {
|
button.addEventListener('click', () => {
|
||||||
option.style.background = '#e3f2fd';
|
this.addPresetColumn(presetName);
|
||||||
option.style.fontWeight = 'bold';
|
|
||||||
}
|
|
||||||
|
|
||||||
option.addEventListener('mouseenter', () => {
|
|
||||||
option.style.background = '#f5f5f5';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
option.addEventListener('mouseleave', () => {
|
toolbar.appendChild(button);
|
||||||
if (preset !== currentHeader) {
|
|
||||||
option.style.background = 'white';
|
|
||||||
} else {
|
|
||||||
option.style.background = '#e3f2fd';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
option.addEventListener('click', () => {
|
// Add custom column button
|
||||||
if (preset === 'Custom...') {
|
const customButton = document.createElement('button');
|
||||||
const customName = prompt('Enter custom property name:', currentHeader);
|
customButton.type = 'button';
|
||||||
if (customName) {
|
customButton.className = 'btn btn-sm btn-outline-secondary';
|
||||||
this.setPropertyType(colIndex, customName, null);
|
customButton.innerHTML = `<i class="fas fa-pencil-alt"></i> Custom`;
|
||||||
}
|
customButton.style.fontSize = '12px';
|
||||||
} else {
|
|
||||||
this.setPropertyType(colIndex, preset, this.propertyPresets[preset]);
|
customButton.addEventListener('click', () => {
|
||||||
}
|
this.addCustomColumn();
|
||||||
document.body.removeChild(menu);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
menu.appendChild(option);
|
toolbar.appendChild(customButton);
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(menu);
|
this.container.appendChild(toolbar);
|
||||||
|
|
||||||
// Close menu when clicking outside
|
|
||||||
const closeMenu = (e) => {
|
|
||||||
if (!menu.contains(e.target)) {
|
|
||||||
document.body.removeChild(menu);
|
|
||||||
document.removeEventListener('click', closeMenu);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setTimeout(() => document.addEventListener('click', closeMenu), 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set property type for a column
|
* Add a preset column to the spreadsheet
|
||||||
*/
|
*/
|
||||||
setPropertyType(colIndex, propertyName, presetValues) {
|
addPresetColumn(presetName) {
|
||||||
|
const presetValues = this.propertyPresets[presetName];
|
||||||
|
const currentColCount = this.hot.countCols();
|
||||||
|
|
||||||
|
// Insert new column
|
||||||
|
this.hot.alter('insert_col_end', currentColCount, 1);
|
||||||
|
|
||||||
// Update column header
|
// Update column header
|
||||||
|
const headers = this.hot.getColHeaders();
|
||||||
|
headers[currentColCount] = presetName;
|
||||||
this.hot.updateSettings({
|
this.hot.updateSettings({
|
||||||
colHeaders: this.hot.getSettings().colHeaders.map((header, index) => {
|
colHeaders: headers
|
||||||
return index === colIndex ? propertyName : header;
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If preset has values, populate the column
|
// Populate with preset values
|
||||||
if (presetValues && Array.isArray(presetValues)) {
|
if (presetValues && Array.isArray(presetValues)) {
|
||||||
const data = this.hot.getData();
|
|
||||||
presetValues.forEach((value, rowIndex) => {
|
presetValues.forEach((value, rowIndex) => {
|
||||||
if (rowIndex < data.length) {
|
if (rowIndex < this.hot.countRows()) {
|
||||||
this.hot.setDataAtCell(rowIndex, colIndex, value);
|
this.hot.setDataAtCell(rowIndex, currentColCount, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a custom column to the spreadsheet
|
||||||
|
*/
|
||||||
|
addCustomColumn() {
|
||||||
|
const customName = prompt('Enter column name:', 'Custom Property');
|
||||||
|
if (!customName || customName.trim() === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentColCount = this.hot.countCols();
|
||||||
|
|
||||||
|
// Insert new column
|
||||||
|
this.hot.alter('insert_col_end', currentColCount, 1);
|
||||||
|
|
||||||
|
// Update column header
|
||||||
|
const headers = this.hot.getColHeaders();
|
||||||
|
headers[currentColCount] = customName.trim();
|
||||||
|
this.hot.updateSettings({
|
||||||
|
colHeaders: headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show rename dialog for column
|
||||||
|
*/
|
||||||
|
showRenameDialog(colIndex) {
|
||||||
|
const currentHeader = this.hot.getColHeader(colIndex);
|
||||||
|
const newName = prompt('Rename column:', currentHeader);
|
||||||
|
|
||||||
|
if (newName && newName.trim() !== '') {
|
||||||
|
const headers = this.hot.getColHeaders();
|
||||||
|
headers[colIndex] = newName.trim();
|
||||||
|
this.hot.updateSettings({
|
||||||
|
colHeaders: headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup keyboard shortcuts
|
* Setup keyboard shortcuts
|
||||||
*/
|
*/
|
||||||
setupKeyboardShortcuts() {
|
setupKeyboardShortcuts() {
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// Only process if the spreadsheet is focused
|
||||||
|
if (!this.container.contains(document.activeElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Ctrl+D or Cmd+D: Delete selected rows/columns
|
// Ctrl+D or Cmd+D: Delete selected rows/columns
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -314,20 +367,24 @@ class VariantEditor {
|
|||||||
// Process each column
|
// Process each column
|
||||||
for (let col = 0; col < headers.length; col++) {
|
for (let col = 0; col < headers.length; col++) {
|
||||||
const propertyName = headers[col];
|
const propertyName = headers[col];
|
||||||
if (!propertyName || propertyName.trim() === '') continue;
|
|
||||||
|
// Skip empty column names
|
||||||
|
if (!propertyName || propertyName.trim() === '' || propertyName.startsWith('Property ')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Collect non-null values from this column
|
// Collect non-null values from this column
|
||||||
const values = [];
|
const values = [];
|
||||||
for (let row = 0; row < data.length; row++) {
|
for (let row = 0; row < data.length; row++) {
|
||||||
const cellValue = data[row][col];
|
const cellValue = data[row][col];
|
||||||
if (cellValue !== null && cellValue !== undefined && cellValue !== '') {
|
if (cellValue !== null && cellValue !== undefined && cellValue !== '') {
|
||||||
values.push(cellValue);
|
values.push(String(cellValue).trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create property object
|
// Create property object
|
||||||
const property = {
|
const property = {
|
||||||
name: propertyName,
|
name: propertyName.trim(),
|
||||||
values: values.length > 0 ? values : null // null means freeform
|
values: values.length > 0 ? values : null // null means freeform
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -336,7 +393,7 @@ class VariantEditor {
|
|||||||
|
|
||||||
const jsonString = JSON.stringify(properties, null, 2);
|
const jsonString = JSON.stringify(properties, null, 2);
|
||||||
|
|
||||||
// Update hidden input or textarea
|
// Update hidden input
|
||||||
const hiddenInput = document.getElementById('PropertiesJson');
|
const hiddenInput = document.getElementById('PropertiesJson');
|
||||||
if (hiddenInput) {
|
if (hiddenInput) {
|
||||||
hiddenInput.value = jsonString;
|
hiddenInput.value = jsonString;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user