UNPKG

can-define

Version:

Create observable objects with JS dot operator compatibility

790 lines (722 loc) 21.3 kB
"use strict"; var Construct = require("can-construct"); var define = require("can-define"); var make = define.make; var queues = require("can-queues"); var addTypeEvents = require("can-event-queue/type/type"); var ObservationRecorder = require("can-observation-recorder"); var canLog = require("can-log"); var canLogDev = require("can-log/dev/dev"); var defineHelpers = require("../define-helpers/define-helpers"); var assign = require("can-assign"); var diff = require("can-diff/list/list"); var ns = require("can-namespace"); var canReflect = require("can-reflect"); var canSymbol = require("can-symbol"); var singleReference = require("can-single-reference"); var splice = [].splice; var runningNative = false; var identity = function(x) { return x; }; // symbols aren't enumerable ... we'd need a version of Object that treats them that way var localOnPatchesSymbol = "can.patches"; var makeFilterCallback = function(props) { return function(item) { for (var prop in props) { if (item[prop] !== props[prop]) { return false; } } return true; }; }; var onKeyValue = define.eventsProto[canSymbol.for("can.onKeyValue")]; var offKeyValue = define.eventsProto[canSymbol.for("can.offKeyValue")]; var getSchemaSymbol = canSymbol.for("can.getSchema"); var inSetupSymbol = canSymbol.for("can.initializing"); function getSchema() { var definitions = this.prototype._define.definitions; var schema = { type: "list", keys: {} }; schema = define.updateSchemaKeys(schema, definitions); if(schema.keys["#"]) { schema.values = definitions["#"].Type; delete schema.keys["#"]; } return schema; } /** @add can-define/list/list */ var DefineList = Construct.extend("DefineList", /** @static */ { setup: function(base) { if (DefineList) { addTypeEvents(this); var prototype = this.prototype; var result = define(prototype, prototype, base.prototype._define); define.makeDefineInstanceKey(this, result); var itemsDefinition = result.definitions["#"] || result.defaultDefinition; if (itemsDefinition) { if (itemsDefinition.Type) { this.prototype.__type = make.set.Type("*", itemsDefinition.Type, identity); } else if (itemsDefinition.type) { this.prototype.__type = make.set.type("*", itemsDefinition.type, identity); } } this[getSchemaSymbol] = getSchema; } } }, /** @prototype */ { // setup for only dynamic DefineMap instances setup: function(items) { if (!this._define) { Object.defineProperty(this, "_define", { enumerable: false, value: { definitions: { length: { type: "number" }, _length: { type: "number" } } } }); Object.defineProperty(this, "_data", { enumerable: false, value: {} }); } define.setup.call(this, {}, false); Object.defineProperty(this, "_length", { enumerable: false, configurable: true, writable: true, value: 0 }); if (items) { this.splice.apply(this, [ 0, 0 ].concat(canReflect.toArray(items))); } }, __type: define.types.observable, _triggerChange: function(attr, how, newVal, oldVal) { var index = +attr; // `batchTrigger` direct add and remove events... // Make sure this is not nested and not an expando if ( !isNaN(index)) { var itemsDefinition = this._define.definitions["#"]; var patches, dispatched; if (how === 'add') { if (itemsDefinition && typeof itemsDefinition.added === 'function') { ObservationRecorder.ignore(itemsDefinition.added).call(this, newVal, index); } patches = [{type: "splice", insert: newVal, index: index, deleteCount: 0}]; dispatched = { type: how, action: "splice", insert: newVal, index: index, deleteCount: 0, patches: patches }; //!steal-remove-start if(process.env.NODE_ENV !== 'production') { dispatched.reasonLog = [ canReflect.getName(this), "added", newVal, "at", index ]; } //!steal-remove-end this.dispatch(dispatched, [ newVal, index ]); } else if (how === 'remove') { if (itemsDefinition && typeof itemsDefinition.removed === 'function') { ObservationRecorder.ignore(itemsDefinition.removed).call(this, oldVal, index); } patches = [{type: "splice", index: index, deleteCount: oldVal.length}]; dispatched = { type: how, patches: patches, action: "splice", index: index, deleteCount: oldVal.length, target: this }; //!steal-remove-start if(process.env.NODE_ENV !== 'production') { dispatched.reasonLog = [ canReflect.getName(this), "remove", oldVal, "at", index ]; } //!steal-remove-end this.dispatch(dispatched, [ oldVal, index ]); } else { this.dispatch(how, [ newVal, index ]); } } else { this.dispatch({ type: "" + attr, target: this }, [ newVal, oldVal ]); } }, get: function(index) { if (arguments.length) { if(isNaN(index)) { ObservationRecorder.add(this, index); } else { ObservationRecorder.add(this, "length"); } return this[index]; } else { return canReflect.unwrap(this, Map); } }, set: function(prop, value) { // if we are setting a single value if (typeof prop !== "object") { // We want change events to notify using integers if we're // setting an integer index. Note that <float> % 1 !== 0; prop = isNaN(+prop) || (prop % 1) ? prop : +prop; if (typeof prop === "number") { // Check to see if we're doing a .attr() on an out of // bounds index property. if (typeof prop === "number" && prop > this._length - 1) { var newArr = new Array((prop + 1) - this._length); newArr[newArr.length - 1] = value; this.push.apply(this, newArr); return newArr; } this.splice(prop, 1, value); } else { var defined = defineHelpers.defineExpando(this, prop, value); if (!defined) { this[prop] = value; } } } // otherwise we are setting multiple else { //!steal-remove-start if(process.env.NODE_ENV !== 'production') { canLogDev.warn('can-define/list/list.prototype.set is deprecated; please use can-define/list/list.prototype.assign or can-define/list/list.prototype.update instead'); } //!steal-remove-end //we are deprecating this in #245 if (canReflect.isListLike(prop)) { if (value) { this.replace(prop); } else { canReflect.assignList(this, prop); } } else { canReflect.assignMap(this, prop); } } return this; }, assign: function(prop) { if (canReflect.isListLike(prop)) { canReflect.assignList(this, prop); } else { canReflect.assignMap(this, prop); } return this; }, update: function(prop) { if (canReflect.isListLike(prop)) { canReflect.updateList(this, prop); } else { canReflect.updateMap(this, prop); } return this; }, assignDeep: function(prop) { if (canReflect.isListLike(prop)) { canReflect.assignDeepList(this, prop); } else { canReflect.assignDeepMap(this, prop); } return this; }, updateDeep: function(prop) { if (canReflect.isListLike(prop)) { canReflect.updateDeepList(this, prop); } else { canReflect.updateDeepMap(this, prop); } return this; }, _items: function() { var arr = []; this._each(function(item) { arr.push(item); }); return arr; }, _each: function(callback) { for (var i = 0, len = this._length; i < len; i++) { callback(this[i], i); } }, splice: function(index, howMany) { var args = canReflect.toArray(arguments), added = [], i, len, listIndex, allSame = args.length > 2, oldLength = this._length; index = index || 0; // converting the arguments to the right type for (i = 0, len = args.length - 2; i < len; i++) { listIndex = i + 2; args[listIndex] = this.__type(args[listIndex], listIndex); added.push(args[listIndex]); // Now lets check if anything will change if (this[i + index] !== args[listIndex]) { allSame = false; } } // if nothing has changed, then return if (allSame && this._length <= added.length) { return added; } // default howMany if not provided if (howMany === undefined) { howMany = args[1] = this._length - index; } runningNative = true; var removed = splice.apply(this, args); runningNative = false; queues.batch.start(); if (howMany > 0) { // tears down bubbling this._triggerChange("" + index, "remove", undefined, removed); } if (args.length > 2) { this._triggerChange("" + index, "add", added, removed); } this.dispatch('length', [ this._length, oldLength ]); queues.batch.stop(); return removed; }, /** */ serialize: function() { return canReflect.serialize(this, Map); } } ); for(var prop in define.eventsProto) { Object.defineProperty(DefineList.prototype, prop, { enumerable:false, value: define.eventsProto[prop], writable: true }); } var eventsProtoSymbols = ( "getOwnPropertySymbols" in Object && typeof canSymbol("symbol") === "symbol" ) ? Object.getOwnPropertySymbols(define.eventsProto) : [canSymbol.for("can.onKeyValue"), canSymbol.for("can.offKeyValue")]; eventsProtoSymbols.forEach(function(sym) { Object.defineProperty(DefineList.prototype, sym, { configurable: true, enumerable:false, value: define.eventsProto[sym], writable: true }); }); // Converts to an `array` of arguments. var getArgs = function(args) { return args[0] && Array.isArray(args[0]) ? args[0] : canReflect.toArray(args); }; // Create `push`, `pop`, `shift`, and `unshift` canReflect.eachKey({ push: "length", unshift: 0 }, // Adds a method // `name` - The method name. // `where` - Where items in the `array` should be added. function(where, name) { var orig = [][name]; DefineList.prototype[name] = function() { // Get the items being added. var args = [], // Where we are going to add items. len = where ? this._length : 0, i = arguments.length, res, val; // Go through and convert anything to a `map` that needs to be converted. while (i--) { val = arguments[i]; args[i] = this.__type(val, i); } // Call the original method. runningNative = true; res = orig.apply(this, args); runningNative = false; if (!this.comparator || args.length) { queues.batch.start(); this._triggerChange("" + len, "add", args, undefined); this.dispatch('length', [ this._length, len ]); queues.batch.stop(); } return res; }; }); canReflect.eachKey({ pop: "length", shift: 0 }, // Creates a `remove` type method function(where, name) { var orig = [][name]; DefineList.prototype[name] = function() { if (!this._length) { // For shift and pop, we just return undefined without // triggering events. return undefined; } var args = getArgs(arguments), len = where && this._length ? this._length - 1 : 0, oldLength = this._length ? this._length : 0, res; // Call the original method. runningNative = true; res = orig.apply(this, args); runningNative = false; // Create a change where the args are // `len` - Where these items were removed. // `remove` - Items removed. // `undefined` - The new values (there are none). // `res` - The old, removed values (should these be unbound). queues.batch.start(); this._triggerChange("" + len, "remove", undefined, [ res ]); this.dispatch('length', [ this._length, oldLength ]); queues.batch.stop(); return res; }; }); canReflect.eachKey({ "map": 3, "filter": 3, "reduce": 4, "reduceRight": 4, "every": 3, "some": 3 }, function a(fnLength, fnName) { DefineList.prototype[fnName] = function() { var self = this; var args = [].slice.call(arguments, 0); var callback = args[0]; var thisArg = args[fnLength - 1] || self; if (typeof callback === "object") { callback = makeFilterCallback(callback); } args[0] = function() { var cbArgs = [].slice.call(arguments, 0); // use .get(index) to ensure observation added. // the arguments are (item, index) or (result, item, index) cbArgs[fnLength - 3] = self.get(cbArgs[fnLength - 2]); return callback.apply(thisArg, cbArgs); }; var ret = Array.prototype[fnName].apply(this, args); if(fnName === "map") { return new DefineList(ret); } else if(fnName === "filter") { return new self.constructor(ret); } else { return ret; } }; }); assign(DefineList.prototype, { includes: (function(){ var arrayIncludes = Array.prototype.includes; if(arrayIncludes){ return function includes() { return arrayIncludes.apply(this, arguments); }; } else { return function includes() { throw new Error("DefineList.prototype.includes must have Array.prototype.includes available. Please add a polyfill to this environment."); }; } })(), indexOf: function(item, fromIndex) { for (var i = fromIndex || 0, len = this.length; i < len; i++) { if (this.get(i) === item) { return i; } } return -1; }, lastIndexOf: function(item, fromIndex) { fromIndex = typeof fromIndex === "undefined" ? this.length - 1: fromIndex; for (var i = fromIndex; i >= 0; i--) { if (this.get(i) === item) { return i; } } return -1; }, join: function() { ObservationRecorder.add(this, "length"); return [].join.apply(this, arguments); }, reverse: function() { // this shouldn't be observable var list = [].reverse.call(this._items()); return this.replace(list); }, slice: function() { // tells computes to listen on length for changes. ObservationRecorder.add(this, "length"); var temp = Array.prototype.slice.apply(this, arguments); return new this.constructor(temp); }, concat: function() { var args = []; // Go through each of the passed `arguments` and // see if it is list-like, an array, or something else canReflect.eachIndex(arguments, function(arg) { if (canReflect.isListLike(arg)) { // If it is list-like we want convert to a JS array then // pass each item of the array to this.__type var arr = Array.isArray(arg) ? arg : canReflect.toArray(arg); arr.forEach(function(innerArg) { args.push(this.__type(innerArg)); }, this); } else { // If it is a Map, Object, or some primitive // just pass arg to this.__type args.push(this.__type(arg)); } }, this); // We will want to make `this` list into a JS array // as well (We know it should be list-like), then // concat with our passed in args, then pass it to // list constructor to make it back into a list return new this.constructor(Array.prototype.concat.apply(canReflect.toArray(this), args)); }, forEach: function(cb, thisarg) { var item; for (var i = 0, len = this.length; i < len; i++) { item = this.get(i); if (cb.call(thisarg || item, item, i, this) === false) { break; } } return this; }, replace: function(newList) { var patches = diff(this, newList); queues.batch.start(); for (var i = 0, len = patches.length; i < len; i++) { this.splice.apply(this, [ patches[i].index, patches[i].deleteCount ].concat(patches[i].insert)); } queues.batch.stop(); return this; }, sort: function(compareFunction) { var sorting = Array.prototype.slice.call(this); Array.prototype.sort.call(sorting, compareFunction); this.splice.apply(this, [0,sorting.length].concat(sorting) ); return this; } }); // Add necessary event methods to this object. for (var prop in define.eventsProto) { DefineList[prop] = define.eventsProto[prop]; Object.defineProperty(DefineList.prototype, prop, { enumerable: false, value: define.eventsProto[prop], writable: true }); } Object.defineProperty(DefineList.prototype, "length", { get: function() { if (!this[inSetupSymbol]) { ObservationRecorder.add(this, "length"); } return this._length; }, set: function(newVal) { if (runningNative) { this._length = newVal; return; } // Don't set _length if: // - null or undefined // - a string that doesn't convert to number // - already the length being set if (newVal == null || isNaN(+newVal) || newVal === this._length) { return; } if (newVal > this._length - 1) { var newArr = new Array(newVal - this._length); this.push.apply(this, newArr); } else { this.splice(newVal); } }, enumerable: true }); DefineList.prototype.attr = function(prop, value) { canLog.warn("DefineMap::attr shouldn't be called"); if (arguments.length === 0) { return this.get(); } else if (prop && typeof prop === "object") { return this.set.apply(this, arguments); } else if (arguments.length === 1) { return this.get(prop); } else { return this.set(prop, value); } }; DefineList.prototype.item = function(index, value) { if (arguments.length === 1) { return this.get(index); } else { return this.set(index, value); } }; DefineList.prototype.items = function() { canLog.warn("DefineList::get should should be used instead of DefineList::items"); return this.get(); }; var defineListProto = { // type "can.isMoreListLikeThanMapLike": true, "can.isMapLike": true, "can.isListLike": true, "can.isValueLike": false, // get/set "can.getKeyValue": DefineList.prototype.get, "can.setKeyValue": DefineList.prototype.set, // Called for every reference to a property in a template // if a key is a numerical index then translate to length event "can.onKeyValue": function(key, handler, queue) { var translationHandler; if (isNaN(key)) { return onKeyValue.apply(this, arguments); } else { translationHandler = function() { handler(this[key]); }; //!steal-remove-start if(process.env.NODE_ENV !== 'production') { Object.defineProperty(translationHandler, "name", { value: "translationHandler(" + key + ")::" + canReflect.getName(this) + ".onKeyValue('length'," + canReflect.getName(handler) + ")", }); } //!steal-remove-end singleReference.set(handler, this, translationHandler, key); return onKeyValue.call(this, 'length', translationHandler, queue); } }, // Called when a property reference is removed "can.offKeyValue": function(key, handler, queue) { var translationHandler; if ( isNaN(key)) { return offKeyValue.apply(this, arguments); } else { translationHandler = singleReference.getAndDelete(handler, this, key); return offKeyValue.call(this, 'length', translationHandler, queue); } }, "can.deleteKeyValue": function(prop) { // convert string key to number index if key can be an integer: // isNaN if prop isn't a numeric representation // (prop % 1) if numeric representation is a float // In both of the above cases, leave as string. prop = isNaN(+prop) || (prop % 1) ? prop : +prop; if(typeof prop === "number") { this.splice(prop, 1); } else if(prop === "length" || prop === "_length") { return; // length must not be deleted } else { this.set(prop, undefined); } return this; }, // shape get/set "can.assignDeep": function(source){ queues.batch.start(); canReflect.assignList(this, source); queues.batch.stop(); }, "can.updateDeep": function(source){ queues.batch.start(); this.replace(source); queues.batch.stop(); }, // observability "can.keyHasDependencies": function(key) { return !!(this._computed && this._computed[key] && this._computed[key].compute); }, "can.getKeyDependencies": function(key) { var ret; if(this._computed && this._computed[key] && this._computed[key].compute) { ret = {}; ret.valueDependencies = new Set(); ret.valueDependencies.add(this._computed[key].compute); } return ret; }, /*"can.onKeysAdded": function(handler,queue) { this[canSymbol.for("can.onKeyValue")]("add", handler,queue); }, "can.onKeysRemoved": function(handler,queue) { this[canSymbol.for("can.onKeyValue")]("remove", handler,queue); },*/ "can.splice": function(index, deleteCount, insert){ this.splice.apply(this, [index, deleteCount].concat(insert)); }, "can.onPatches": function(handler,queue){ this[canSymbol.for("can.onKeyValue")](localOnPatchesSymbol, handler,queue); }, "can.offPatches": function(handler,queue) { this[canSymbol.for("can.offKeyValue")](localOnPatchesSymbol, handler,queue); } }; //!steal-remove-start if(process.env.NODE_ENV !== 'production') { defineListProto["can.getName"] = function() { return canReflect.getName(this.constructor) + "[]"; }; } //!steal-remove-end canReflect.assignSymbols(DefineList.prototype, defineListProto); canReflect.setKeyValue(DefineList.prototype, canSymbol.iterator, function() { var index = -1; if(typeof this.length !== "number") { this.length = 0; } return { next: function() { index++; return { value: this[index], done: index >= this.length }; }.bind(this) }; }); //!steal-remove-start if(process.env.NODE_ENV !== 'production') { // call `list.log()` to log all event changes // pass `key` to only log the matching event, e.g: `list.log("add")` DefineList.prototype.log = defineHelpers.log; } //!steal-remove-end define.DefineList = DefineList; module.exports = ns.DefineList = DefineList;