UNPKG

alpine-magic-helpers

Version:

A collection of magic properties and helper functions for use with Alpine.

708 lines (639 loc) 24.7 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, (global.AlpineMagicHelpers = global.AlpineMagicHelpers || {}, global.AlpineMagicHelpers.undo = factory())); }(this, (function () { 'use strict'; var checkForAlpine = function checkForAlpine() { if (!window.Alpine) { throw new Error('[Magic Helpers] Alpine is required for the magic helpers to function correctly.'); } if (!window.Alpine.version || !isValidVersion('2.5.0', window.Alpine.version)) { throw new Error('Invalid Alpine version. Please use Alpine version 2.5.0 or above'); } }; var updateOnMutation = function updateOnMutation(componentBeingObserved, callback) { if (!componentBeingObserved.getAttribute('x-bind:data-last-refresh')) { componentBeingObserved.setAttribute('x-bind:data-last-refresh', 'Date.now()'); } var observer = new MutationObserver(function (mutations) { for (var i = 0; i < mutations.length; i++) { var mutatedComponent = mutations[i].target.closest('[x-data]'); if (mutatedComponent && !mutatedComponent.isSameNode(componentBeingObserved)) continue; callback(); return; } }); observer.observe(componentBeingObserved, { attributes: true, childList: true, subtree: true }); }; // Borrowed from https://stackoverflow.com/a/54733755/1437789 var componentData = function componentData(component, properties) { var data = component.__x ? component.__x.getUnobservedData() : saferEval(component.getAttribute('x-data'), component); if (properties) { properties = Array.isArray(properties) ? properties : [properties]; return properties.reduce(function (object, key) { object[key] = data[key]; return object; }, {}); } return data; }; function isValidVersion(required, current) { var requiredArray = required.split('.'); var currentArray = current.split('.'); for (var i = 0; i < requiredArray.length; i++) { if (!currentArray[i] || parseInt(currentArray[i]) < parseInt(requiredArray[i])) { return false; } } return true; } function saferEval(expression, dataContext, additionalHelperVariables) { if (additionalHelperVariables === void 0) { additionalHelperVariables = {}; } if (typeof expression === 'function') { return expression.call(dataContext); } // eslint-disable-next-line no-new-func return new Function(['$data'].concat(Object.keys(additionalHelperVariables)), "var __alpine_result; with($data) { __alpine_result = " + expression + " }; return __alpine_result").apply(void 0, [dataContext].concat(Object.values(additionalHelperVariables))); } // Returns a dummy proxy that supports multiple levels of nesting and always prints/returns an empty string. function importOrderCheck() { // We only want to show the error once if (window.Alpine && !window.AlpineMagicHelpers.__fatal) { window.AlpineMagicHelpers.__fatal = setTimeout(function () { console.error('%c*** ALPINE MAGIC HELPER: Fatal Error! ***\n\n\n' + 'Alpine magic helpers need to be loaded before Alpine ' + 'to avoid errors when Alpine initialises its component. \n\n' + 'Make sure the helper script is included before Alpine in ' + 'your page when using the defer attribute', 'font-size: 14px'); }, 200); // We set a small timeout to make sure we flush all the Alpine noise first } } var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function createCommonjsModule(fn) { var module = { exports: {} }; return fn(module, module.exports), module.exports; } var deepDiff = createCommonjsModule(function (module, exports) { (function(root, factory) { // eslint-disable-line no-extra-semi var deepDiff = factory(root); // eslint-disable-next-line no-undef { // Node.js or ReactNative module.exports = deepDiff; } }(commonjsGlobal, function(root) { var validKinds = ['N', 'E', 'A', 'D']; // nodejs compatible on server side and in the browser. function inherits(ctor, superCtor) { ctor.super_ = superCtor; ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); } function Diff(kind, path) { Object.defineProperty(this, 'kind', { value: kind, enumerable: true }); if (path && path.length) { Object.defineProperty(this, 'path', { value: path, enumerable: true }); } } function DiffEdit(path, origin, value) { DiffEdit.super_.call(this, 'E', path); Object.defineProperty(this, 'lhs', { value: origin, enumerable: true }); Object.defineProperty(this, 'rhs', { value: value, enumerable: true }); } inherits(DiffEdit, Diff); function DiffNew(path, value) { DiffNew.super_.call(this, 'N', path); Object.defineProperty(this, 'rhs', { value: value, enumerable: true }); } inherits(DiffNew, Diff); function DiffDeleted(path, value) { DiffDeleted.super_.call(this, 'D', path); Object.defineProperty(this, 'lhs', { value: value, enumerable: true }); } inherits(DiffDeleted, Diff); function DiffArray(path, index, item) { DiffArray.super_.call(this, 'A', path); Object.defineProperty(this, 'index', { value: index, enumerable: true }); Object.defineProperty(this, 'item', { value: item, enumerable: true }); } inherits(DiffArray, Diff); function arrayRemove(arr, from, to) { var rest = arr.slice((to || from) + 1 || arr.length); arr.length = from < 0 ? arr.length + from : from; arr.push.apply(arr, rest); return arr; } function realTypeOf(subject) { var type = typeof subject; if (type !== 'object') { return type; } if (subject === Math) { return 'math'; } else if (subject === null) { return 'null'; } else if (Array.isArray(subject)) { return 'array'; } else if (Object.prototype.toString.call(subject) === '[object Date]') { return 'date'; } else if (typeof subject.toString === 'function' && /^\/.*\//.test(subject.toString())) { return 'regexp'; } return 'object'; } // http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ function hashThisString(string) { var hash = 0; if (string.length === 0) { return hash; } for (var i = 0; i < string.length; i++) { var char = string.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; } // Gets a hash of the given object in an array order-independent fashion // also object key order independent (easier since they can be alphabetized) function getOrderIndependentHash(object) { var accum = 0; var type = realTypeOf(object); if (type === 'array') { object.forEach(function (item) { // Addition is commutative so this is order indep accum += getOrderIndependentHash(item); }); var arrayString = '[type: array, hash: ' + accum + ']'; return accum + hashThisString(arrayString); } if (type === 'object') { for (var key in object) { if (object.hasOwnProperty(key)) { var keyValueString = '[ type: object, key: ' + key + ', value hash: ' + getOrderIndependentHash(object[key]) + ']'; accum += hashThisString(keyValueString); } } return accum; } // Non object, non array...should be good? var stringToHash = '[ type: ' + type + ' ; value: ' + object + ']'; return accum + hashThisString(stringToHash); } function deepDiff(lhs, rhs, changes, prefilter, path, key, stack, orderIndependent) { changes = changes || []; path = path || []; stack = stack || []; var currentPath = path.slice(0); if (typeof key !== 'undefined' && key !== null) { if (prefilter) { if (typeof (prefilter) === 'function' && prefilter(currentPath, key)) { return; } else if (typeof (prefilter) === 'object') { if (prefilter.prefilter && prefilter.prefilter(currentPath, key)) { return; } if (prefilter.normalize) { var alt = prefilter.normalize(currentPath, key, lhs, rhs); if (alt) { lhs = alt[0]; rhs = alt[1]; } } } } currentPath.push(key); } // Use string comparison for regexes if (realTypeOf(lhs) === 'regexp' && realTypeOf(rhs) === 'regexp') { lhs = lhs.toString(); rhs = rhs.toString(); } var ltype = typeof lhs; var rtype = typeof rhs; var i, j, k, other; var ldefined = ltype !== 'undefined' || (stack && (stack.length > 0) && stack[stack.length - 1].lhs && Object.getOwnPropertyDescriptor(stack[stack.length - 1].lhs, key)); var rdefined = rtype !== 'undefined' || (stack && (stack.length > 0) && stack[stack.length - 1].rhs && Object.getOwnPropertyDescriptor(stack[stack.length - 1].rhs, key)); if (!ldefined && rdefined) { changes.push(new DiffNew(currentPath, rhs)); } else if (!rdefined && ldefined) { changes.push(new DiffDeleted(currentPath, lhs)); } else if (realTypeOf(lhs) !== realTypeOf(rhs)) { changes.push(new DiffEdit(currentPath, lhs, rhs)); } else if (realTypeOf(lhs) === 'date' && (lhs - rhs) !== 0) { changes.push(new DiffEdit(currentPath, lhs, rhs)); } else if (ltype === 'object' && lhs !== null && rhs !== null) { for (i = stack.length - 1; i > -1; --i) { if (stack[i].lhs === lhs) { other = true; break; } } if (!other) { stack.push({ lhs: lhs, rhs: rhs }); if (Array.isArray(lhs)) { // If order doesn't matter, we need to sort our arrays if (orderIndependent) { lhs.sort(function (a, b) { return getOrderIndependentHash(a) - getOrderIndependentHash(b); }); rhs.sort(function (a, b) { return getOrderIndependentHash(a) - getOrderIndependentHash(b); }); } i = rhs.length - 1; j = lhs.length - 1; while (i > j) { changes.push(new DiffArray(currentPath, i, new DiffNew(undefined, rhs[i--]))); } while (j > i) { changes.push(new DiffArray(currentPath, j, new DiffDeleted(undefined, lhs[j--]))); } for (; i >= 0; --i) { deepDiff(lhs[i], rhs[i], changes, prefilter, currentPath, i, stack, orderIndependent); } } else { var akeys = Object.keys(lhs); var pkeys = Object.keys(rhs); for (i = 0; i < akeys.length; ++i) { k = akeys[i]; other = pkeys.indexOf(k); if (other >= 0) { deepDiff(lhs[k], rhs[k], changes, prefilter, currentPath, k, stack, orderIndependent); pkeys[other] = null; } else { deepDiff(lhs[k], undefined, changes, prefilter, currentPath, k, stack, orderIndependent); } } for (i = 0; i < pkeys.length; ++i) { k = pkeys[i]; if (k) { deepDiff(undefined, rhs[k], changes, prefilter, currentPath, k, stack, orderIndependent); } } } stack.length = stack.length - 1; } else if (lhs !== rhs) { // lhs is contains a cycle at this element and it differs from rhs changes.push(new DiffEdit(currentPath, lhs, rhs)); } } else if (lhs !== rhs) { if (!(ltype === 'number' && isNaN(lhs) && isNaN(rhs))) { changes.push(new DiffEdit(currentPath, lhs, rhs)); } } } function observableDiff(lhs, rhs, observer, prefilter, orderIndependent) { var changes = []; deepDiff(lhs, rhs, changes, prefilter, null, null, null, orderIndependent); if (observer) { for (var i = 0; i < changes.length; ++i) { observer(changes[i]); } } return changes; } function orderIndependentDeepDiff(lhs, rhs, changes, prefilter, path, key, stack) { return deepDiff(lhs, rhs, changes, prefilter, path, key, stack, true); } function accumulateDiff(lhs, rhs, prefilter, accum) { var observer = (accum) ? function (difference) { if (difference) { accum.push(difference); } } : undefined; var changes = observableDiff(lhs, rhs, observer, prefilter); return (accum) ? accum : (changes.length) ? changes : undefined; } function accumulateOrderIndependentDiff(lhs, rhs, prefilter, accum) { var observer = (accum) ? function (difference) { if (difference) { accum.push(difference); } } : undefined; var changes = observableDiff(lhs, rhs, observer, prefilter, true); return (accum) ? accum : (changes.length) ? changes : undefined; } function applyArrayChange(arr, index, change) { if (change.path && change.path.length) { var it = arr[index], i, u = change.path.length - 1; for (i = 0; i < u; i++) { it = it[change.path[i]]; } switch (change.kind) { case 'A': applyArrayChange(it[change.path[i]], change.index, change.item); break; case 'D': delete it[change.path[i]]; break; case 'E': case 'N': it[change.path[i]] = change.rhs; break; } } else { switch (change.kind) { case 'A': applyArrayChange(arr[index], change.index, change.item); break; case 'D': arr = arrayRemove(arr, index); break; case 'E': case 'N': arr[index] = change.rhs; break; } } return arr; } function applyChange(target, source, change) { if (typeof change === 'undefined' && source && ~validKinds.indexOf(source.kind)) { change = source; } if (target && change && change.kind) { var it = target, i = -1, last = change.path ? change.path.length - 1 : 0; while (++i < last) { if (typeof it[change.path[i]] === 'undefined') { it[change.path[i]] = (typeof change.path[i + 1] !== 'undefined' && typeof change.path[i + 1] === 'number') ? [] : {}; } it = it[change.path[i]]; } switch (change.kind) { case 'A': if (change.path && typeof it[change.path[i]] === 'undefined') { it[change.path[i]] = []; } applyArrayChange(change.path ? it[change.path[i]] : it, change.index, change.item); break; case 'D': delete it[change.path[i]]; break; case 'E': case 'N': it[change.path[i]] = change.rhs; break; } } } function revertArrayChange(arr, index, change) { if (change.path && change.path.length) { // the structure of the object at the index has changed... var it = arr[index], i, u = change.path.length - 1; for (i = 0; i < u; i++) { it = it[change.path[i]]; } switch (change.kind) { case 'A': revertArrayChange(it[change.path[i]], change.index, change.item); break; case 'D': it[change.path[i]] = change.lhs; break; case 'E': it[change.path[i]] = change.lhs; break; case 'N': delete it[change.path[i]]; break; } } else { // the array item is different... switch (change.kind) { case 'A': revertArrayChange(arr[index], change.index, change.item); break; case 'D': arr[index] = change.lhs; break; case 'E': arr[index] = change.lhs; break; case 'N': arr = arrayRemove(arr, index); break; } } return arr; } function revertChange(target, source, change) { if (target && source && change && change.kind) { var it = target, i, u; u = change.path.length - 1; for (i = 0; i < u; i++) { if (typeof it[change.path[i]] === 'undefined') { it[change.path[i]] = {}; } it = it[change.path[i]]; } switch (change.kind) { case 'A': // Array was modified... // it will be an array... revertArrayChange(it[change.path[i]], change.index, change.item); break; case 'D': // Item was deleted... it[change.path[i]] = change.lhs; break; case 'E': // Item was edited... it[change.path[i]] = change.lhs; break; case 'N': // Item is new... delete it[change.path[i]]; break; } } } function applyDiff(target, source, filter) { if (target && source) { var onChange = function (change) { if (!filter || filter(target, source, change)) { applyChange(target, source, change); } }; observableDiff(target, source, onChange); } } Object.defineProperties(accumulateDiff, { diff: { value: accumulateDiff, enumerable: true }, orderIndependentDiff: { value: accumulateOrderIndependentDiff, enumerable: true }, observableDiff: { value: observableDiff, enumerable: true }, orderIndependentObservableDiff: { value: orderIndependentDeepDiff, enumerable: true }, orderIndepHash: { value: getOrderIndependentHash, enumerable: true }, applyDiff: { value: applyDiff, enumerable: true }, applyChange: { value: applyChange, enumerable: true }, revertChange: { value: revertChange, enumerable: true }, isConflict: { value: function () { return typeof $conflict !== 'undefined'; }, enumerable: true } }); // hackish... accumulateDiff.DeepDiff = accumulateDiff; // ...but works with: // import DeepDiff from 'deep-diff' // import { DeepDiff } from 'deep-diff' // const DeepDiff = require('deep-diff'); // const { DeepDiff } = require('deep-diff'); if (root) { root.DeepDiff = accumulateDiff; } return accumulateDiff; })); }); importOrderCheck(); var history = new WeakMap(); var AlpineUndoMagicMethod = { start: function start() { var _this = this; checkForAlpine(); Alpine.addMagicProperty('track', function ($el) { return function (propertiesToWatch) { var _propertiesToWatch; propertiesToWatch = (_propertiesToWatch = propertiesToWatch) != null ? _propertiesToWatch : Object.keys(componentData($el)); propertiesToWatch = Array.isArray(propertiesToWatch) ? propertiesToWatch : [propertiesToWatch]; var initialState = JSON.stringify(componentData($el, propertiesToWatch)); updateOnMutation($el, function () { history.has($el.__x) || _this.store($el.__x, { props: propertiesToWatch, previous: initialState }); var fresh = componentData($el, history.get($el.__x).props); var previous = JSON.parse(history.get($el.__x).previous); var changes = deepDiff.DeepDiff.diff(previous, fresh, true); if (changes && changes.length) { changes = changes.filter(function (change) { return history.get($el.__x).props.some(function (prop) { return change.path.join('.').startsWith(prop); }); }); history.get($el.__x).previous = JSON.stringify(fresh); history.get($el.__x).changes.push(changes); $el.__x.updateElements($el); } }); }; }); Alpine.addMagicProperty('undo', function ($el, $clone) { return function () { if ($el !== $clone) { $el = _this.syncClone($el, $clone); } var changes = history.get($el.__x).changes.pop(); var previous = JSON.parse(history.get($el.__x).previous); changes && changes.forEach(function (change) { deepDiff.DeepDiff.revertChange(previous, componentData($el, history.get($el.__x).props), change); }); // This could probably be extracted to a utility method like updateComponentProperties() if (Object.keys(previous).length) { var newData = {}; Object.entries(previous).forEach(function (item) { newData[item[0]] = item[1]; }); $el.__x.$data = Object.assign($el.__x.$data, newData); } history.get($el.__x).previous = JSON.stringify(componentData($el, history.get($el.__x).props)); }; }); Alpine.addMagicProperty('history', function ($el, $clone) { if (!$clone.__x) return []; if ($el !== $clone) { $el = _this.syncClone($el, $clone); } return history.has($el.__x) ? history.get($el.__x) : []; }); }, store: function store(key, state) { history.set(key, Object.assign({ changes: [], get length() { return this.changes.length; } }, state)); return history.get(key); }, syncClone: function syncClone($el, $clone) { this.store($clone.__x, { props: history.get($el.__x).props, previous: history.get($el.__x).previous, changes: history.get($el.__x).changes }); return $clone; } }; var alpine = window.deferLoadingAlpine || function (alpine) { return alpine(); }; window.deferLoadingAlpine = function (callback) { alpine(callback); AlpineUndoMagicMethod.start(); }; return AlpineUndoMagicMethod; })));