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:
371
LittleShop/wwwroot/lib/hammerjs/src/utils.js
Normal file
371
LittleShop/wwwroot/lib/hammerjs/src/utils.js
Normal file
@@ -0,0 +1,371 @@
|
||||
var VENDOR_PREFIXES = ['', 'webkit', 'Moz', 'MS', 'ms', 'o'];
|
||||
var TEST_ELEMENT = document.createElement('div');
|
||||
|
||||
var TYPE_FUNCTION = 'function';
|
||||
|
||||
var round = Math.round;
|
||||
var abs = Math.abs;
|
||||
var now = Date.now;
|
||||
|
||||
/**
|
||||
* set a timeout with a given scope
|
||||
* @param {Function} fn
|
||||
* @param {Number} timeout
|
||||
* @param {Object} context
|
||||
* @returns {number}
|
||||
*/
|
||||
function setTimeoutContext(fn, timeout, context) {
|
||||
return setTimeout(bindFn(fn, context), timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* if the argument is an array, we want to execute the fn on each entry
|
||||
* if it aint an array we don't want to do a thing.
|
||||
* this is used by all the methods that accept a single and array argument.
|
||||
* @param {*|Array} arg
|
||||
* @param {String} fn
|
||||
* @param {Object} [context]
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function invokeArrayArg(arg, fn, context) {
|
||||
if (Array.isArray(arg)) {
|
||||
each(arg, context[fn], context);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* walk objects and arrays
|
||||
* @param {Object} obj
|
||||
* @param {Function} iterator
|
||||
* @param {Object} context
|
||||
*/
|
||||
function each(obj, iterator, context) {
|
||||
var i;
|
||||
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (obj.forEach) {
|
||||
obj.forEach(iterator, context);
|
||||
} else if (obj.length !== undefined) {
|
||||
i = 0;
|
||||
while (i < obj.length) {
|
||||
iterator.call(context, obj[i], i, obj);
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
for (i in obj) {
|
||||
obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* wrap a method with a deprecation warning and stack trace
|
||||
* @param {Function} method
|
||||
* @param {String} name
|
||||
* @param {String} message
|
||||
* @returns {Function} A new function wrapping the supplied method.
|
||||
*/
|
||||
function deprecate(method, name, message) {
|
||||
var deprecationMessage = 'DEPRECATED METHOD: ' + name + '\n' + message + ' AT \n';
|
||||
return function() {
|
||||
var e = new Error('get-stack-trace');
|
||||
var stack = e && e.stack ? e.stack.replace(/^[^\(]+?[\n$]/gm, '')
|
||||
.replace(/^\s+at\s+/gm, '')
|
||||
.replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@') : 'Unknown Stack Trace';
|
||||
|
||||
var log = window.console && (window.console.warn || window.console.log);
|
||||
if (log) {
|
||||
log.call(window.console, deprecationMessage, stack);
|
||||
}
|
||||
return method.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* extend object.
|
||||
* means that properties in dest will be overwritten by the ones in src.
|
||||
* @param {Object} target
|
||||
* @param {...Object} objects_to_assign
|
||||
* @returns {Object} target
|
||||
*/
|
||||
var assign;
|
||||
if (typeof Object.assign !== 'function') {
|
||||
assign = function assign(target) {
|
||||
if (target === undefined || target === null) {
|
||||
throw new TypeError('Cannot convert undefined or null to object');
|
||||
}
|
||||
|
||||
var output = Object(target);
|
||||
for (var index = 1; index < arguments.length; index++) {
|
||||
var source = arguments[index];
|
||||
if (source !== undefined && source !== null) {
|
||||
for (var nextKey in source) {
|
||||
if (source.hasOwnProperty(nextKey)) {
|
||||
output[nextKey] = source[nextKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
};
|
||||
} else {
|
||||
assign = Object.assign;
|
||||
}
|
||||
|
||||
/**
|
||||
* extend object.
|
||||
* means that properties in dest will be overwritten by the ones in src.
|
||||
* @param {Object} dest
|
||||
* @param {Object} src
|
||||
* @param {Boolean} [merge=false]
|
||||
* @returns {Object} dest
|
||||
*/
|
||||
var extend = deprecate(function extend(dest, src, merge) {
|
||||
var keys = Object.keys(src);
|
||||
var i = 0;
|
||||
while (i < keys.length) {
|
||||
if (!merge || (merge && dest[keys[i]] === undefined)) {
|
||||
dest[keys[i]] = src[keys[i]];
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return dest;
|
||||
}, 'extend', 'Use `assign`.');
|
||||
|
||||
/**
|
||||
* merge the values from src in the dest.
|
||||
* means that properties that exist in dest will not be overwritten by src
|
||||
* @param {Object} dest
|
||||
* @param {Object} src
|
||||
* @returns {Object} dest
|
||||
*/
|
||||
var merge = deprecate(function merge(dest, src) {
|
||||
return extend(dest, src, true);
|
||||
}, 'merge', 'Use `assign`.');
|
||||
|
||||
/**
|
||||
* simple class inheritance
|
||||
* @param {Function} child
|
||||
* @param {Function} base
|
||||
* @param {Object} [properties]
|
||||
*/
|
||||
function inherit(child, base, properties) {
|
||||
var baseP = base.prototype,
|
||||
childP;
|
||||
|
||||
childP = child.prototype = Object.create(baseP);
|
||||
childP.constructor = child;
|
||||
childP._super = baseP;
|
||||
|
||||
if (properties) {
|
||||
assign(childP, properties);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* simple function bind
|
||||
* @param {Function} fn
|
||||
* @param {Object} context
|
||||
* @returns {Function}
|
||||
*/
|
||||
function bindFn(fn, context) {
|
||||
return function boundFn() {
|
||||
return fn.apply(context, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* let a boolean value also be a function that must return a boolean
|
||||
* this first item in args will be used as the context
|
||||
* @param {Boolean|Function} val
|
||||
* @param {Array} [args]
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function boolOrFn(val, args) {
|
||||
if (typeof val == TYPE_FUNCTION) {
|
||||
return val.apply(args ? args[0] || undefined : undefined, args);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* use the val2 when val1 is undefined
|
||||
* @param {*} val1
|
||||
* @param {*} val2
|
||||
* @returns {*}
|
||||
*/
|
||||
function ifUndefined(val1, val2) {
|
||||
return (val1 === undefined) ? val2 : val1;
|
||||
}
|
||||
|
||||
/**
|
||||
* addEventListener with multiple events at once
|
||||
* @param {EventTarget} target
|
||||
* @param {String} types
|
||||
* @param {Function} handler
|
||||
*/
|
||||
function addEventListeners(target, types, handler) {
|
||||
each(splitStr(types), function(type) {
|
||||
target.addEventListener(type, handler, false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* removeEventListener with multiple events at once
|
||||
* @param {EventTarget} target
|
||||
* @param {String} types
|
||||
* @param {Function} handler
|
||||
*/
|
||||
function removeEventListeners(target, types, handler) {
|
||||
each(splitStr(types), function(type) {
|
||||
target.removeEventListener(type, handler, false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* find if a node is in the given parent
|
||||
* @method hasParent
|
||||
* @param {HTMLElement} node
|
||||
* @param {HTMLElement} parent
|
||||
* @return {Boolean} found
|
||||
*/
|
||||
function hasParent(node, parent) {
|
||||
while (node) {
|
||||
if (node == parent) {
|
||||
return true;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* small indexOf wrapper
|
||||
* @param {String} str
|
||||
* @param {String} find
|
||||
* @returns {Boolean} found
|
||||
*/
|
||||
function inStr(str, find) {
|
||||
return str.indexOf(find) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* split string on whitespace
|
||||
* @param {String} str
|
||||
* @returns {Array} words
|
||||
*/
|
||||
function splitStr(str) {
|
||||
return str.trim().split(/\s+/g);
|
||||
}
|
||||
|
||||
/**
|
||||
* find if a array contains the object using indexOf or a simple polyFill
|
||||
* @param {Array} src
|
||||
* @param {String} find
|
||||
* @param {String} [findByKey]
|
||||
* @return {Boolean|Number} false when not found, or the index
|
||||
*/
|
||||
function inArray(src, find, findByKey) {
|
||||
if (src.indexOf && !findByKey) {
|
||||
return src.indexOf(find);
|
||||
} else {
|
||||
var i = 0;
|
||||
while (i < src.length) {
|
||||
if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) {
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* convert array-like objects to real arrays
|
||||
* @param {Object} obj
|
||||
* @returns {Array}
|
||||
*/
|
||||
function toArray(obj) {
|
||||
return Array.prototype.slice.call(obj, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* unique array with objects based on a key (like 'id') or just by the array's value
|
||||
* @param {Array} src [{id:1},{id:2},{id:1}]
|
||||
* @param {String} [key]
|
||||
* @param {Boolean} [sort=False]
|
||||
* @returns {Array} [{id:1},{id:2}]
|
||||
*/
|
||||
function uniqueArray(src, key, sort) {
|
||||
var results = [];
|
||||
var values = [];
|
||||
var i = 0;
|
||||
|
||||
while (i < src.length) {
|
||||
var val = key ? src[i][key] : src[i];
|
||||
if (inArray(values, val) < 0) {
|
||||
results.push(src[i]);
|
||||
}
|
||||
values[i] = val;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
if (!key) {
|
||||
results = results.sort();
|
||||
} else {
|
||||
results = results.sort(function sortUniqueArray(a, b) {
|
||||
return a[key] > b[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the prefixed property
|
||||
* @param {Object} obj
|
||||
* @param {String} property
|
||||
* @returns {String|Undefined} prefixed
|
||||
*/
|
||||
function prefixed(obj, property) {
|
||||
var prefix, prop;
|
||||
var camelProp = property[0].toUpperCase() + property.slice(1);
|
||||
|
||||
var i = 0;
|
||||
while (i < VENDOR_PREFIXES.length) {
|
||||
prefix = VENDOR_PREFIXES[i];
|
||||
prop = (prefix) ? prefix + camelProp : property;
|
||||
|
||||
if (prop in obj) {
|
||||
return prop;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* get a unique id
|
||||
* @returns {number} uniqueId
|
||||
*/
|
||||
var _uniqueId = 1;
|
||||
function uniqueId() {
|
||||
return _uniqueId++;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the window object of an element
|
||||
* @param {HTMLElement} element
|
||||
* @returns {DocumentView|Window}
|
||||
*/
|
||||
function getWindowForElement(element) {
|
||||
var doc = element.ownerDocument || element;
|
||||
return (doc.defaultView || doc.parentWindow || window);
|
||||
}
|
||||
Reference in New Issue
Block a user