UNPKG

@schoolbelle/common

Version:

1,031 lines (1,021 loc) 32.3 kB
import { Subject } from 'rxjs'; import { forEach, isArray, isPlainObject, get as get$1, set as set$1, unset, isEqual, cloneDeep, orderBy, isEmpty, values, defaults } from 'lodash-es'; import { __extends, __assign, __read } from 'tslib'; import { get, set, cloneDeep as cloneDeep$1, defaults as defaults$1, isEmpty as isEmpty$1 } from 'lodash'; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @param {?} ob * @param {?=} delimiter * @param {?=} current_key_path * @param {?=} paths * @return {?} */ function getAllLeafPaths(ob, delimiter, current_key_path, paths) { if (delimiter === void 0) { delimiter = '.'; } if (current_key_path === void 0) { current_key_path = []; } if (paths === void 0) { paths = []; } forEach(ob, (/** * @param {?} val * @param {?} key * @return {?} */ function (val, key) { key = isArray(ob) ? "" + key : key; // an instance of Class such as a File instance is not a plain object. if (isPlainObject(val) || isArray(val)) { paths = getAllLeafPaths(val, delimiter, current_key_path.concat([key]), paths); } else { paths.push(current_key_path.concat([key]).join(delimiter)); } })); return paths; } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * calculate the difference btwn two objects * and return object with key being path and value being an object of to and from ; * ex> say before is {a:{b:{c:1}}} and current is {a:{b:{c:2}}} * returned value will be {'a.b.c':{to:2, from:1}} * * @param {?} current * @param {?} before * @return {?} */ function getAllLeafChanges(current, before) { /** @type {?} */ var diff = {}; /** @type {?} */ var allLeafPathsOfCurrent = getAllLeafPaths(current); /** @type {?} */ var allLeafPathsOfBefore = getAllLeafPaths(before); allLeafPathsOfCurrent.forEach((/** * @param {?} path * @return {?} */ function (path) { /** @type {?} */ var v_from_origin = get(before, path); /** @type {?} */ var v_from_current = get(current, path); if (isEmptyStringOrNullOrUndefined(v_from_current) === true && isEmptyStringOrNullOrUndefined(v_from_origin) === true) return; if (v_from_current !== v_from_origin) { set(diff, [path, 'to'], v_from_current); } })); allLeafPathsOfBefore.forEach((/** * @param {?} path * @return {?} */ function (path) { /** @type {?} */ var v_from_origin = get(before, path); /** @type {?} */ var v_from_current = get(current, path); if (isEmptyStringOrNullOrUndefined(v_from_current) === true && isEmptyStringOrNullOrUndefined(v_from_origin) === true) return; if (v_from_current !== v_from_origin) { set(diff, [path, 'from'], v_from_origin); } })); /** * [START file type usage extension] * * * class Foo { * a = "a" * b = "b" * } * const before = {branch1:{a:"a", b:"b"}, branch2:"c"}; // getAllLeafPaths will return ["branch1.a", "branch1.b", "branch2"] * const current = {branch1:new Foo(), , branch2:"c"} // getAllLeafPaths will return ["branch1", "branch2"] * * result will be : * 1. missing to or from in the "diff" * 2. sub-paths of a class instance in the "diff" * * We have to : * 1. Fill in the missing part of data * 2. Remove all sub-paths of a class instance * * @type {?} */ var paths = Object.keys(diff); Object.entries(diff) .forEach((/** * @param {?} __0 * @return {?} */ function (_a) { var _b = __read(_a, 2), path = _b[0], update = _b[1]; if (update.to !== null && typeof update.to === 'object') { // fill the missing data, from. update.from = get(before, path); // delete sub-paths of a class instance such as "branch1.a", "branch1.b" paths.filter((/** * @param {?} p * @return {?} */ function (p) { return p.startsWith(path) && p !== path; })).forEach((/** * @param {?} p * @return {?} */ function (p) { delete diff[p]; })); } else if (update.from !== null && typeof update.from === 'object') { // fill the missing data, to. update.to = get(current, path); // delete sub-paths of a class instance such as "branch1.a", "branch1.b" paths.filter((/** * @param {?} p * @return {?} */ function (p) { return p.startsWith(path) && p !== path; })).forEach((/** * @param {?} p * @return {?} */ function (p) { delete diff[p]; })); } })); // [END file type usage extension] return diff; } /** * @param {?} v * @return {?} */ function isEmptyStringOrNullOrUndefined(v) { return v === '' || v === null || v === undefined; } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @param {?} val * @return {?} */ function shakeDeadLeaves(val) { if (val === null) return null; if (val === undefined) return null; // if (val instanceof File || val instanceof Blob) return val; // instance of File or Blob if (val && typeof val === 'object' && !isPlainObject(val)) return val; // instance of a class including File, Blob, and many other if (val && typeof val === 'object' && Object.keys(val).length === 0) return null; // {} if (typeof val !== 'object') return val; else { for (var key in val) { val[key] = shakeDeadLeaves(val[key]); if (val[key] === null) delete val[key]; } if (Object.keys(val).length === 0) return null; else return val; } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ var ObjectChangeTracker = /** @class */ (function () { function ObjectChangeTracker(root, pathFromRoot) { if (root === void 0) { root = undefined; } if (pathFromRoot === void 0) { pathFromRoot = []; } this.contentUpdateEvent = new Subject(); /** * this is where it keep its model. * The only root tracker keeps it. * Child trackers send all changes to their root tracker and the root tracker writes all changes to this property. * */ this._value = {}; this._origin = {}; this._to = {}; this._from = {}; this._trackers = new Map(); this._root = root || this; this._pathFromRoot = pathFromRoot; if (this.root !== this) { this.trackers.set(this.pathFromRoot.join('.'), this); } } Object.defineProperty(ObjectChangeTracker.prototype, "root", { get: /** * @return {?} */ function () { if (this._root === this) return this; else return this._root.root; }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTracker.prototype, "pathFromRoot", { get: /** * @return {?} */ function () { if (this._root === this) return []; else { return this._root.pathFromRoot.concat(this._pathFromRoot); } }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTracker.prototype, "onContentUpdate", { get: /** * @return {?} */ function () { return this.contentUpdateEvent.asObservable(); }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTracker.prototype, "value", { /** * return the whole model if root or else "branch" of model. * Watch out for modifying this data cause it may return reference to the model itself. */ get: /** * return the whole model if root or else "branch" of model. * Watch out for modifying this data cause it may return reference to the model itself. * @return {?} */ function () { if (this._root === this) return this._value; else return get$1(this.root._value, this.pathFromRoot); }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTracker.prototype, "isChanged", { get: /** * @return {?} */ function () { /** @type {?} */ var to = this.to; /** @type {?} */ var from = this.from; return ((to && (typeof to === 'object' && Object.keys(to).length !== 0)) || (from && (typeof from === 'object' && Object.keys(from).length !== 0)) || !isEqual(to, from)); }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTracker.prototype, "origin", { get: /** * @return {?} */ function () { if (this.root === this) return this._origin; else return get$1(this.root.origin, this.pathFromRoot); }, set: /** * @param {?} v * @return {?} */ function (v) { if (typeof v === 'undefined') v = null; if (this.root === this) this._origin = v; else set$1(this.root.origin, this.pathFromRoot, v); }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTracker.prototype, "to", { get: /** * @return {?} */ function () { if (this.root === this) return this._to; else return get$1(this.root.to, this.pathFromRoot); }, set: /** * @param {?} v * @return {?} */ function (v) { if (this.root === this) this._to = v || {}; else { if (v === null) { unset(this.root.to, this.pathFromRoot); this.root.to = shakeDeadLeaves(this.root.to); } else set$1(this.root.to, this.pathFromRoot, v); } }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTracker.prototype, "from", { get: /** * @return {?} */ function () { if (this.root === this) return this._from; else return get$1(this.root.from, this.pathFromRoot); }, set: /** * @param {?} v * @return {?} */ function (v) { if (this.root === this) this._from = v || {}; else { if (v === null) { unset(this.root.from, this.pathFromRoot); this.root.from = shakeDeadLeaves(this.root.from); } else set$1(this.root.from, this.pathFromRoot, v); } }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTracker.prototype, "parent", { get: /** * @return {?} */ function () { if (this.root === this) return this; else return this.root.getChildTracker(this.pathFromRoot.slice(0, -1)); }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTracker.prototype, "trackers", { get: /** * @return {?} */ function () { if (this.root === this) return this._trackers; else return this.root.trackers; }, enumerable: true, configurable: true }); /** * @param {?} path * @return {?} */ ObjectChangeTracker.prototype.get = /** * @param {?} path * @return {?} */ function (path) { if (typeof path === 'string') path = path.split('.').filter((/** * @param {?} step * @return {?} */ function (step) { return step; })); path = this.pathFromRoot.concat(path); /** @type {?} */ var value; if (path.length === 0) value = this.root._value; else { value = get$1(this.root._value, path); } // array data type handling if (!isEmpty(value) && typeof value === 'object' && !Array.isArray(value)) { /** @type {?} */ var keys = Object.keys(value); if (typeof value["" + 0] !== "undefined" && typeof value["" + (keys.length - 1)] !== "undefined" && !keys.find((/** * @param {?} key * @return {?} */ function (key) { return parseInt("" + key) === NaN; }))) { return values(value); } else { return value; } } return value; // if (path.length === 0) return cloneDeep(this.root._value); // else { // return cloneDeep(get(this.root._value, path)); // } }; /** * @param {?} path * @param {?} value * @param {?=} options * @return {?} */ ObjectChangeTracker.prototype.set = /** * @param {?} path * @param {?} value * @param {?=} options * @return {?} */ function (path, value, options) { if (options === void 0) { options = undefined; } options = defaults(options, { push: 'up', updateDiff: true }); if (typeof value === 'undefined') value = null; if (typeof path === 'string') path = path.split('.').filter((/** * @param {?} step * @return {?} */ function (step) { return step; })); path = this.pathFromRoot.concat(path); this.updateData(path, value); if (options.updateOrigin) this.updateOrigin(path, value); if (options.updateDiff) this.updateDiff(); if (options.push !== 'none') this.notifyChangeThruTree(path, options); }; /** * @param {?=} origin * @param {?=} options * @return {?} */ ObjectChangeTracker.prototype.load = /** * @param {?=} origin * @param {?=} options * @return {?} */ function (origin, options) { if (origin === void 0) { origin = {}; } if (options === void 0) { options = undefined; } if (this.root !== this) throw new Error('load is only allowed for root'); options = defaults(options, { push: 'up', updateDiff: true }); this.origin = cloneDeep(origin); this.updateData([], origin); if (options.updateDiff) this.updateDiff(); if (options.push !== 'none') this.notifyChangeThruTree([], options); }; /** * @protected * @return {?} */ ObjectChangeTracker.prototype.updateDiff = /** * @protected * @return {?} */ function () { /** @type {?} */ var origin = this.origin; /** @type {?} */ var current = this.value; /** @type {?} */ var to = {}; /** @type {?} */ var from = {}; /** @type {?} */ var changes = getAllLeafChanges(current, origin); for (var path in changes) { set$1(to, path, changes[path].to); set$1(from, path, changes[path].from); } shakeDeadLeaves(to); shakeDeadLeaves(from); this.to = isEmpty(to) ? null : to; this.from = isEmpty(from) ? null : from; }; /** * * @param path * @param v_n * set 'null' means unset * unset value will return as 'undefined' * */ /** * * @protected * @param {?} path * @param {?} v_n * set 'null' means unset * unset value will return as 'undefined' * * @return {?} */ ObjectChangeTracker.prototype.updateData = /** * * @protected * @param {?} path * @param {?} v_n * set 'null' means unset * unset value will return as 'undefined' * * @return {?} */ function (path, v_n) { /** @type {?} */ var v_o = get$1(this._value, path); if (isEqual(v_n, v_o)) return; if (v_n === null) { if (path.length === 0) this.root._value = v_n; else unset(this.root._value, path); } else { if (path.length === 0) this.root._value = cloneDeep(v_n); else set$1(this.root._value, path, cloneDeep(v_n)); } }; /** * @protected * @param {?} path * @param {?} v_n * @return {?} */ ObjectChangeTracker.prototype.updateOrigin = /** * @protected * @param {?} path * @param {?} v_n * @return {?} */ function (path, v_n) { /** @type {?} */ var v_o = get$1(this._origin, path); if (isEqual(v_n, v_o)) return; if (v_n === null) { if (path.length === 0) this.root._origin = v_n; else unset(this.root._origin, path); } else { if (path.length === 0) this.root._origin = v_n instanceof File ? v_n : cloneDeep(v_n); else set$1(this.root._origin, path, v_n instanceof File ? v_n : cloneDeep(v_n)); } }; /** * @param {?=} paths * @param {?=} options * @return {?} */ ObjectChangeTracker.prototype.notifyChangeThruTree = /** * @param {?=} paths * @param {?=} options * @return {?} */ function (paths, options) { var _this = this; if (paths === void 0) { paths = []; } if (options === void 0) { options = undefined; } options = __assign({ push: 'up' }, options); /** @type {?} */ var tracker = paths.length === 0 ? this : this.trackers.get(paths.join('.')); if (['down', 'both'].includes(options.push)) { orderBy(Array.from(this.trackers.keys()), (/** * @param {?} key * @return {?} */ function (key) { return key.split('.').length; }), 'desc') .filter((/** * @param {?} key * @return {?} */ function (key) { return key.match(paths.join('.')); })) .map((/** * @param {?} key * @return {?} */ function (key) { return _this.trackers.get(key); })) .filter((/** * @param {?} childTracker * @return {?} */ function (childTracker) { return childTracker; })) .forEach((/** * @param {?} childTracker * @return {?} */ function (childTracker) { return childTracker.contentUpdateEvent.next({ to: childTracker.to, from: childTracker.from }); })); } if (!['none'].includes(options.push) && tracker) tracker.contentUpdateEvent.next({ to: tracker.to, from: tracker.from }); if (['up', 'both'].includes(options.push)) { paths.map((/** * @param {?} path * @param {?} index * @return {?} */ function (path, index) { return paths.slice(0, paths.length - index).join('.'); })) .map((/** * @param {?} subpaths * @return {?} */ function (subpaths) { return _this.trackers.get(subpaths); })) .filter((/** * @param {?} childTracker * @return {?} */ function (childTracker) { return childTracker; })) .forEach((/** * @param {?} childTracker * @return {?} */ function (childTracker) { childTracker.contentUpdateEvent.next({ to: childTracker.to, from: childTracker.from }); })); this.root.contentUpdateEvent.next({ to: this.root.to, from: this.root.from }); } }; /** * @param {?} path * @return {?} */ ObjectChangeTracker.prototype.getChildTracker = /** * @param {?} path * @return {?} */ function (path) { if (typeof path === 'string') path = path.split('.').filter((/** * @param {?} step * @return {?} */ function (step) { return step; })); /** @type {?} */ var fullpath = this.pathFromRoot.concat(path); if (fullpath.length === 0) { return this; } else { /** @type {?} */ var childRef = this.trackers.get(fullpath.join('.')); if (!childRef) { childRef = new ObjectChangeTracker(this, path); this.trackers.set(fullpath.join('/'), childRef); } return childRef; } }; /** * @param {?=} path * @return {?} */ ObjectChangeTracker.prototype.destroyTracker = /** * @param {?=} path * @return {?} */ function (path) { if (path === void 0) { path = []; } if (typeof path === 'string') path = path.split('.').filter((/** * @param {?} step * @return {?} */ function (step) { return step; })); path = this.pathFromRoot.concat(path); /** @type {?} */ var tracker; if (path.length === 0) tracker = this; else tracker = this.trackers.get(path.join('.')); if (tracker) { tracker.contentUpdateEvent.complete(); this.trackers.delete(path.join('.')); } }; return ObjectChangeTracker; }()); /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ var DEFAULT_MAX_SIZE = 100; var ObjectChangeTrackerWithHistory = /** @class */ (function (_super) { __extends(ObjectChangeTrackerWithHistory, _super); function ObjectChangeTrackerWithHistory(root, pathFromRoot) { if (root === void 0) { root = undefined; } if (pathFromRoot === void 0) { pathFromRoot = []; } var _this = _super.call(this, root, pathFromRoot) || this; _this._trackers = new Map(); /** * maximum length of history stack */ _this.MAX_SIZE = DEFAULT_MAX_SIZE; /** * current position in the history stack */ _this.position = 0; /** * history stack */ _this.stack = []; return _this; } Object.defineProperty(ObjectChangeTrackerWithHistory.prototype, "root", { get: /** * @return {?} */ function () { if (this._root === this) return this; else return this._root.root; }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTrackerWithHistory.prototype, "parent", { get: /** * @return {?} */ function () { if (this.root === this) return this; else return this.root.getChildTracker(this.pathFromRoot.slice(0, -1)); }, enumerable: true, configurable: true }); Object.defineProperty(ObjectChangeTrackerWithHistory.prototype, "trackers", { get: /** * @return {?} */ function () { if (this.root === this) return this._trackers; else return this.root.trackers; }, enumerable: true, configurable: true }); /** * @param {?} path * @return {?} */ ObjectChangeTrackerWithHistory.prototype.getChildTracker = /** * @param {?} path * @return {?} */ function (path) { if (typeof path === 'string') path = path.split('.').filter((/** * @param {?} step * @return {?} */ function (step) { return step; })); /** @type {?} */ var fullpath = this.pathFromRoot.concat(path); if (fullpath.length === 0) { return this; } else { /** @type {?} */ var childRef = this.trackers.get(fullpath.join('.')); if (!childRef) { childRef = new ObjectChangeTrackerWithHistory(this, path); this.trackers.set(fullpath.join('/'), childRef); } return childRef; } }; /** * * @param data * @param MAX_SIZE * * let's say you make 3 changes, undo 2 times, and enter then another change. * v denotes the current position in history stack. * [d1 d2 d3 v] // 3 changes (pos is 3) * [d1 v d2 d3] // 2 undo's (pos is 1) * [d1 d4 v] // a new change (pos is 2) */ /** * * @return {?} */ ObjectChangeTrackerWithHistory.prototype.save = /** * * @return {?} */ function () { if (this.root !== this) throw new Error('save is only allowed for root'); // 0. get diff /** @type {?} */ var before = this.before; /** @type {?} */ var current = this.value; /** @type {?} */ var diff = getAllLeafChanges(current, before) // 1. Pushing a new item should remove the history items that stand after the current position, if there is(are). ; // 1. Pushing a new item should remove the history items that stand after the current position, if there is(are). if (this.stack.length > this.position) this.stack.splice(this.position); // 2. Push to the stack. this.stack.push(diff); // 3. Rotate the list if it grows too big. if (this.stack.length > this.MAX_SIZE) this.stack.splice(0, this.stack.length - this.MAX_SIZE); // 4. move up the position if possible. if (this.position < this.MAX_SIZE) this.position++; // 5. update before for (var path in diff) { set(this.before, path, diff[path].to); } this.updateDiff(); }; /** * @param {?} path * @param {?} value * @param {?=} options * @return {?} */ ObjectChangeTrackerWithHistory.prototype.set = /** * @param {?} path * @param {?} value * @param {?=} options * @return {?} */ function (path, value, options) { if (options === void 0) { options = undefined; } options = defaults$1(options, { saveToHistory: true }); _super.prototype.set.call(this, path, value, options); if (options.saveToHistory) this.root.save(); }; /** * reset history related properties. */ /** * reset history related properties. * @protected * @return {?} */ ObjectChangeTrackerWithHistory.prototype.reset = /** * reset history related properties. * @protected * @return {?} */ function () { this.before = null; this.position = 0; this.stack.splice(0, this.stack.length); }; /** * @param {?=} origin * @param {?=} options * @return {?} */ ObjectChangeTrackerWithHistory.prototype.load = /** * @param {?=} origin * @param {?=} options * @return {?} */ function (origin, options) { if (origin === void 0) { origin = {}; } if (options === void 0) { options = undefined; } _super.prototype.load.call(this, origin, options); this.reset(); this.before = cloneDeep$1(origin); }; /** * @return {?} */ ObjectChangeTrackerWithHistory.prototype.undo = /** * @return {?} */ function () { if (this.root !== this) throw new Error('undo is only allowed for root'); if (this.position <= 0) return false; // decrement position this.position--; // get the target history snapshot /** @type {?} */ var diff = this.stack[this.position]; // update current and before value /** @type {?} */ var current = this._value || {}; /** @type {?} */ var before = this.before || {}; // recalculate current and before for (var path in diff) { set(current, path, diff[path].from); // go back, model! set(before, path, diff[path].from); // update before } shakeDeadLeaves(current); shakeDeadLeaves(before); // store current and before this._value = isEmpty$1(current) ? null : current; this.before = isEmpty$1(before) ? null : before; // update diff this.updateDiff(); // notify for (var path in diff) { this.notifyChangeThruTree(path.split('.'), { push: 'up' }); } return true; }; /** * @return {?} */ ObjectChangeTrackerWithHistory.prototype.redo = /** * @return {?} */ function () { if (this.root !== this) throw new Error('redo is only allowed for root'); if (this.position >= this.stack.length) return false; // increment position this.position++; // get the target history snapshot /** @type {?} */ var diff = this.stack[this.position - 1]; // recalculate current and before /** @type {?} */ var current = this._value || {}; /** @type {?} */ var before = this.before || {}; for (var path in diff) { set(current, path, diff[path].to); // go forward, model! set(before, path, diff[path].to); // update before } shakeDeadLeaves(current); shakeDeadLeaves(before); // store current and before this._value = isEmpty$1(current) ? null : current; this.before = isEmpty$1(before) ? null : before; // update diff this.updateDiff(); // notify for (var path in diff) { this.notifyChangeThruTree(path.split('.')); } return true; }; return ObjectChangeTrackerWithHistory; }(ObjectChangeTracker)); /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ export { ObjectChangeTracker, ObjectChangeTrackerWithHistory, getAllLeafPaths, shakeDeadLeaves }; //# sourceMappingURL=schoolbelle-common-object-change-tracker.js.map