feat: Phase 2.5 - Variant Collections Spreadsheet Editor
Replaces JSON textarea with professional Excel-like spreadsheet interface for managing product variant properties. Features: - Handsontable 14.6.1 spreadsheet component - Property presets (Size, Color, Material, Storage, Custom) - Inline cell editing with Tab/Enter navigation - Context menu for add/remove rows and columns - Keyboard shortcuts (Ctrl+D delete, Ctrl+Enter save, Ctrl+Z undo) - Mobile touch gestures (swipe to delete rows) - Automatic JSON serialization on form submit - Form validation before saving - Comprehensive user guide documentation Files Changed: - LittleShop/package.json: NPM package management setup - LittleShop/wwwroot/js/variant-editor.js: 400-line spreadsheet editor module - LittleShop/wwwroot/lib/handsontable/: Handsontable library (Community Edition) - LittleShop/wwwroot/lib/hammerjs/: Hammer.js touch gesture library - LittleShop/Areas/Admin/Views/VariantCollections/Edit.cshtml: Spreadsheet UI integration - VARIANT_COLLECTIONS_USER_GUIDE.md: Complete user guide (18+ pages) Technical Details: - Excel-like editing experience (no more manual JSON editing) - Mobile-first responsive design - Browser compatibility: Chrome 90+, Firefox 88+, Edge 90+, Safari 14+ - Touch-optimized for mobile administration - Automatic data validation and error handling
This commit is contained in:
57
LittleShop/wwwroot/lib/hammerjs/src/input/mouse.js
Normal file
57
LittleShop/wwwroot/lib/hammerjs/src/input/mouse.js
Normal file
@@ -0,0 +1,57 @@
|
||||
var MOUSE_INPUT_MAP = {
|
||||
mousedown: INPUT_START,
|
||||
mousemove: INPUT_MOVE,
|
||||
mouseup: INPUT_END
|
||||
};
|
||||
|
||||
var MOUSE_ELEMENT_EVENTS = 'mousedown';
|
||||
var MOUSE_WINDOW_EVENTS = 'mousemove mouseup';
|
||||
|
||||
/**
|
||||
* Mouse events input
|
||||
* @constructor
|
||||
* @extends Input
|
||||
*/
|
||||
function MouseInput() {
|
||||
this.evEl = MOUSE_ELEMENT_EVENTS;
|
||||
this.evWin = MOUSE_WINDOW_EVENTS;
|
||||
|
||||
this.pressed = false; // mousedown state
|
||||
|
||||
Input.apply(this, arguments);
|
||||
}
|
||||
|
||||
inherit(MouseInput, Input, {
|
||||
/**
|
||||
* handle mouse events
|
||||
* @param {Object} ev
|
||||
*/
|
||||
handler: function MEhandler(ev) {
|
||||
var eventType = MOUSE_INPUT_MAP[ev.type];
|
||||
|
||||
// on start we want to have the left mouse button down
|
||||
if (eventType & INPUT_START && ev.button === 0) {
|
||||
this.pressed = true;
|
||||
}
|
||||
|
||||
if (eventType & INPUT_MOVE && ev.which !== 1) {
|
||||
eventType = INPUT_END;
|
||||
}
|
||||
|
||||
// mouse must be down
|
||||
if (!this.pressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventType & INPUT_END) {
|
||||
this.pressed = false;
|
||||
}
|
||||
|
||||
this.callback(this.manager, eventType, {
|
||||
pointers: [ev],
|
||||
changedPointers: [ev],
|
||||
pointerType: INPUT_TYPE_MOUSE,
|
||||
srcEvent: ev
|
||||
});
|
||||
}
|
||||
});
|
||||
88
LittleShop/wwwroot/lib/hammerjs/src/input/pointerevent.js
Normal file
88
LittleShop/wwwroot/lib/hammerjs/src/input/pointerevent.js
Normal file
@@ -0,0 +1,88 @@
|
||||
var POINTER_INPUT_MAP = {
|
||||
pointerdown: INPUT_START,
|
||||
pointermove: INPUT_MOVE,
|
||||
pointerup: INPUT_END,
|
||||
pointercancel: INPUT_CANCEL,
|
||||
pointerout: INPUT_CANCEL
|
||||
};
|
||||
|
||||
// in IE10 the pointer types is defined as an enum
|
||||
var IE10_POINTER_TYPE_ENUM = {
|
||||
2: INPUT_TYPE_TOUCH,
|
||||
3: INPUT_TYPE_PEN,
|
||||
4: INPUT_TYPE_MOUSE,
|
||||
5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816
|
||||
};
|
||||
|
||||
var POINTER_ELEMENT_EVENTS = 'pointerdown';
|
||||
var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel';
|
||||
|
||||
// IE10 has prefixed support, and case-sensitive
|
||||
if (window.MSPointerEvent && !window.PointerEvent) {
|
||||
POINTER_ELEMENT_EVENTS = 'MSPointerDown';
|
||||
POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointer events input
|
||||
* @constructor
|
||||
* @extends Input
|
||||
*/
|
||||
function PointerEventInput() {
|
||||
this.evEl = POINTER_ELEMENT_EVENTS;
|
||||
this.evWin = POINTER_WINDOW_EVENTS;
|
||||
|
||||
Input.apply(this, arguments);
|
||||
|
||||
this.store = (this.manager.session.pointerEvents = []);
|
||||
}
|
||||
|
||||
inherit(PointerEventInput, Input, {
|
||||
/**
|
||||
* handle mouse events
|
||||
* @param {Object} ev
|
||||
*/
|
||||
handler: function PEhandler(ev) {
|
||||
var store = this.store;
|
||||
var removePointer = false;
|
||||
|
||||
var eventTypeNormalized = ev.type.toLowerCase().replace('ms', '');
|
||||
var eventType = POINTER_INPUT_MAP[eventTypeNormalized];
|
||||
var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType;
|
||||
|
||||
var isTouch = (pointerType == INPUT_TYPE_TOUCH);
|
||||
|
||||
// get index of the event in the store
|
||||
var storeIndex = inArray(store, ev.pointerId, 'pointerId');
|
||||
|
||||
// start and mouse must be down
|
||||
if (eventType & INPUT_START && (ev.button === 0 || isTouch)) {
|
||||
if (storeIndex < 0) {
|
||||
store.push(ev);
|
||||
storeIndex = store.length - 1;
|
||||
}
|
||||
} else if (eventType & (INPUT_END | INPUT_CANCEL)) {
|
||||
removePointer = true;
|
||||
}
|
||||
|
||||
// it not found, so the pointer hasn't been down (so it's probably a hover)
|
||||
if (storeIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update the event in the store
|
||||
store[storeIndex] = ev;
|
||||
|
||||
this.callback(this.manager, eventType, {
|
||||
pointers: store,
|
||||
changedPointers: [ev],
|
||||
pointerType: pointerType,
|
||||
srcEvent: ev
|
||||
});
|
||||
|
||||
if (removePointer) {
|
||||
// remove from the store
|
||||
store.splice(storeIndex, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
68
LittleShop/wwwroot/lib/hammerjs/src/input/singletouch.js
Normal file
68
LittleShop/wwwroot/lib/hammerjs/src/input/singletouch.js
Normal file
@@ -0,0 +1,68 @@
|
||||
var SINGLE_TOUCH_INPUT_MAP = {
|
||||
touchstart: INPUT_START,
|
||||
touchmove: INPUT_MOVE,
|
||||
touchend: INPUT_END,
|
||||
touchcancel: INPUT_CANCEL
|
||||
};
|
||||
|
||||
var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart';
|
||||
var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel';
|
||||
|
||||
/**
|
||||
* Touch events input
|
||||
* @constructor
|
||||
* @extends Input
|
||||
*/
|
||||
function SingleTouchInput() {
|
||||
this.evTarget = SINGLE_TOUCH_TARGET_EVENTS;
|
||||
this.evWin = SINGLE_TOUCH_WINDOW_EVENTS;
|
||||
this.started = false;
|
||||
|
||||
Input.apply(this, arguments);
|
||||
}
|
||||
|
||||
inherit(SingleTouchInput, Input, {
|
||||
handler: function TEhandler(ev) {
|
||||
var type = SINGLE_TOUCH_INPUT_MAP[ev.type];
|
||||
|
||||
// should we handle the touch events?
|
||||
if (type === INPUT_START) {
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
var touches = normalizeSingleTouches.call(this, ev, type);
|
||||
|
||||
// when done, reset the started state
|
||||
if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) {
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
this.callback(this.manager, type, {
|
||||
pointers: touches[0],
|
||||
changedPointers: touches[1],
|
||||
pointerType: INPUT_TYPE_TOUCH,
|
||||
srcEvent: ev
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @this {TouchInput}
|
||||
* @param {Object} ev
|
||||
* @param {Number} type flag
|
||||
* @returns {undefined|Array} [all, changed]
|
||||
*/
|
||||
function normalizeSingleTouches(ev, type) {
|
||||
var all = toArray(ev.touches);
|
||||
var changed = toArray(ev.changedTouches);
|
||||
|
||||
if (type & (INPUT_END | INPUT_CANCEL)) {
|
||||
all = uniqueArray(all.concat(changed), 'identifier', true);
|
||||
}
|
||||
|
||||
return [all, changed];
|
||||
}
|
||||
98
LittleShop/wwwroot/lib/hammerjs/src/input/touch.js
Normal file
98
LittleShop/wwwroot/lib/hammerjs/src/input/touch.js
Normal file
@@ -0,0 +1,98 @@
|
||||
var TOUCH_INPUT_MAP = {
|
||||
touchstart: INPUT_START,
|
||||
touchmove: INPUT_MOVE,
|
||||
touchend: INPUT_END,
|
||||
touchcancel: INPUT_CANCEL
|
||||
};
|
||||
|
||||
var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel';
|
||||
|
||||
/**
|
||||
* Multi-user touch events input
|
||||
* @constructor
|
||||
* @extends Input
|
||||
*/
|
||||
function TouchInput() {
|
||||
this.evTarget = TOUCH_TARGET_EVENTS;
|
||||
this.targetIds = {};
|
||||
|
||||
Input.apply(this, arguments);
|
||||
}
|
||||
|
||||
inherit(TouchInput, Input, {
|
||||
handler: function MTEhandler(ev) {
|
||||
var type = TOUCH_INPUT_MAP[ev.type];
|
||||
var touches = getTouches.call(this, ev, type);
|
||||
if (!touches) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.callback(this.manager, type, {
|
||||
pointers: touches[0],
|
||||
changedPointers: touches[1],
|
||||
pointerType: INPUT_TYPE_TOUCH,
|
||||
srcEvent: ev
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @this {TouchInput}
|
||||
* @param {Object} ev
|
||||
* @param {Number} type flag
|
||||
* @returns {undefined|Array} [all, changed]
|
||||
*/
|
||||
function getTouches(ev, type) {
|
||||
var allTouches = toArray(ev.touches);
|
||||
var targetIds = this.targetIds;
|
||||
|
||||
// when there is only one touch, the process can be simplified
|
||||
if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) {
|
||||
targetIds[allTouches[0].identifier] = true;
|
||||
return [allTouches, allTouches];
|
||||
}
|
||||
|
||||
var i,
|
||||
targetTouches,
|
||||
changedTouches = toArray(ev.changedTouches),
|
||||
changedTargetTouches = [],
|
||||
target = this.target;
|
||||
|
||||
// get target touches from touches
|
||||
targetTouches = allTouches.filter(function(touch) {
|
||||
return hasParent(touch.target, target);
|
||||
});
|
||||
|
||||
// collect touches
|
||||
if (type === INPUT_START) {
|
||||
i = 0;
|
||||
while (i < targetTouches.length) {
|
||||
targetIds[targetTouches[i].identifier] = true;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// filter changed touches to only contain touches that exist in the collected target ids
|
||||
i = 0;
|
||||
while (i < changedTouches.length) {
|
||||
if (targetIds[changedTouches[i].identifier]) {
|
||||
changedTargetTouches.push(changedTouches[i]);
|
||||
}
|
||||
|
||||
// cleanup removed touches
|
||||
if (type & (INPUT_END | INPUT_CANCEL)) {
|
||||
delete targetIds[changedTouches[i].identifier];
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!changedTargetTouches.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [
|
||||
// merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel'
|
||||
uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true),
|
||||
changedTargetTouches
|
||||
];
|
||||
}
|
||||
95
LittleShop/wwwroot/lib/hammerjs/src/input/touchmouse.js
Normal file
95
LittleShop/wwwroot/lib/hammerjs/src/input/touchmouse.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Combined touch and mouse input
|
||||
*
|
||||
* Touch has a higher priority then mouse, and while touching no mouse events are allowed.
|
||||
* This because touch devices also emit mouse events while doing a touch.
|
||||
*
|
||||
* @constructor
|
||||
* @extends Input
|
||||
*/
|
||||
|
||||
var DEDUP_TIMEOUT = 2500;
|
||||
var DEDUP_DISTANCE = 25;
|
||||
|
||||
function TouchMouseInput() {
|
||||
Input.apply(this, arguments);
|
||||
|
||||
var handler = bindFn(this.handler, this);
|
||||
this.touch = new TouchInput(this.manager, handler);
|
||||
this.mouse = new MouseInput(this.manager, handler);
|
||||
|
||||
this.primaryTouch = null;
|
||||
this.lastTouches = [];
|
||||
}
|
||||
|
||||
inherit(TouchMouseInput, Input, {
|
||||
/**
|
||||
* handle mouse and touch events
|
||||
* @param {Hammer} manager
|
||||
* @param {String} inputEvent
|
||||
* @param {Object} inputData
|
||||
*/
|
||||
handler: function TMEhandler(manager, inputEvent, inputData) {
|
||||
var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH),
|
||||
isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE);
|
||||
|
||||
if (isMouse && inputData.sourceCapabilities && inputData.sourceCapabilities.firesTouchEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
// when we're in a touch event, record touches to de-dupe synthetic mouse event
|
||||
if (isTouch) {
|
||||
recordTouches.call(this, inputEvent, inputData);
|
||||
} else if (isMouse && isSyntheticEvent.call(this, inputData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.callback(manager, inputEvent, inputData);
|
||||
},
|
||||
|
||||
/**
|
||||
* remove the event listeners
|
||||
*/
|
||||
destroy: function destroy() {
|
||||
this.touch.destroy();
|
||||
this.mouse.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
function recordTouches(eventType, eventData) {
|
||||
if (eventType & INPUT_START) {
|
||||
this.primaryTouch = eventData.changedPointers[0].identifier;
|
||||
setLastTouch.call(this, eventData);
|
||||
} else if (eventType & (INPUT_END | INPUT_CANCEL)) {
|
||||
setLastTouch.call(this, eventData);
|
||||
}
|
||||
}
|
||||
|
||||
function setLastTouch(eventData) {
|
||||
var touch = eventData.changedPointers[0];
|
||||
|
||||
if (touch.identifier === this.primaryTouch) {
|
||||
var lastTouch = {x: touch.clientX, y: touch.clientY};
|
||||
this.lastTouches.push(lastTouch);
|
||||
var lts = this.lastTouches;
|
||||
var removeLastTouch = function() {
|
||||
var i = lts.indexOf(lastTouch);
|
||||
if (i > -1) {
|
||||
lts.splice(i, 1);
|
||||
}
|
||||
};
|
||||
setTimeout(removeLastTouch, DEDUP_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
function isSyntheticEvent(eventData) {
|
||||
var x = eventData.srcEvent.clientX, y = eventData.srcEvent.clientY;
|
||||
for (var i = 0; i < this.lastTouches.length; i++) {
|
||||
var t = this.lastTouches[i];
|
||||
var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y);
|
||||
if (dx <= DEDUP_DISTANCE && dy <= DEDUP_DISTANCE) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user