named-properties-tracker.js
1 "use strict"; 2 // https://heycam.github.io/webidl/#idl-named-properties 3 4 const IS_NAMED_PROPERTY = Symbol("is named property"); 5 const TRACKER = Symbol("named property tracker"); 6 7 /** 8 * Create a new NamedPropertiesTracker for the given `object`. 9 * 10 * Named properties are used in DOM to let you lookup (for example) a Node by accessing a property on another object. 11 * For example `window.foo` might resolve to an image element with id "foo". 12 * 13 * This tracker is a workaround because the ES6 Proxy feature is not yet available. 14 * 15 * @param {Object} object Object used to write properties to 16 * @param {Object} objectProxy Object used to check if a property is already defined 17 * @param {Function} resolverFunc Each time a property is accessed, this function is called to determine the value of 18 * the property. The function is passed 3 arguments: (object, name, values). 19 * `object` is identical to the `object` parameter of this `create` function. 20 * `name` is the name of the property. 21 * `values` is a function that returns a Set with all the tracked values for this name. The order of these 22 * values is undefined. 23 * 24 * @returns {NamedPropertiesTracker} 25 */ 26 exports.create = function (object, objectProxy, resolverFunc) { 27 if (object[TRACKER]) { 28 throw Error("A NamedPropertiesTracker has already been created for this object"); 29 } 30 31 const tracker = new NamedPropertiesTracker(object, objectProxy, resolverFunc); 32 object[TRACKER] = tracker; 33 return tracker; 34 }; 35 36 exports.get = function (object) { 37 if (!object) { 38 return null; 39 } 40 41 return object[TRACKER] || null; 42 }; 43 44 function NamedPropertiesTracker(object, objectProxy, resolverFunc) { 45 this.object = object; 46 this.objectProxy = objectProxy; 47 this.resolverFunc = resolverFunc; 48 this.trackedValues = new Map(); // Map<Set<value>> 49 } 50 51 function newPropertyDescriptor(tracker, name) { 52 const emptySet = new Set(); 53 54 function getValues() { 55 return tracker.trackedValues.get(name) || emptySet; 56 } 57 58 const descriptor = { 59 enumerable: true, 60 configurable: true, 61 get() { 62 return tracker.resolverFunc(tracker.object, name, getValues); 63 }, 64 set(value) { 65 Object.defineProperty(tracker.object, name, { 66 enumerable: true, 67 configurable: true, 68 writable: true, 69 value 70 }); 71 } 72 }; 73 74 descriptor.get[IS_NAMED_PROPERTY] = true; 75 descriptor.set[IS_NAMED_PROPERTY] = true; 76 return descriptor; 77 } 78 79 /** 80 * Track a value (e.g. a Node) for a specified name. 81 * 82 * Values can be tracked eagerly, which means that not all tracked values *have* to appear in the output. The resolver 83 * function that was passed to the output may filter the value. 84 * 85 * Tracking the same `name` and `value` pair multiple times has no effect 86 * 87 * @param {String} name 88 * @param {*} value 89 */ 90 NamedPropertiesTracker.prototype.track = function (name, value) { 91 if (name === undefined || name === null || name === "") { 92 return; 93 } 94 95 let valueSet = this.trackedValues.get(name); 96 if (!valueSet) { 97 valueSet = new Set(); 98 this.trackedValues.set(name, valueSet); 99 } 100 101 valueSet.add(value); 102 103 if (name in this.objectProxy) { 104 // already added our getter or it is not a named property (e.g. "addEventListener") 105 return; 106 } 107 108 const descriptor = newPropertyDescriptor(this, name); 109 Object.defineProperty(this.object, name, descriptor); 110 }; 111 112 /** 113 * Stop tracking a previously tracked `name` & `value` pair, see track(). 114 * 115 * Untracking the same `name` and `value` pair multiple times has no effect 116 * 117 * @param {String} name 118 * @param {*} value 119 */ 120 NamedPropertiesTracker.prototype.untrack = function (name, value) { 121 if (name === undefined || name === null || name === "") { 122 return; 123 } 124 125 const valueSet = this.trackedValues.get(name); 126 if (!valueSet) { 127 // the value is not present 128 return; 129 } 130 131 if (!valueSet.delete(value)) { 132 // the value was not present 133 return; 134 } 135 136 if (valueSet.size === 0) { 137 this.trackedValues.delete(name); 138 } 139 140 if (valueSet.size > 0) { 141 // other values for this name are still present 142 return; 143 } 144 145 // at this point there are no more values, delete the property 146 147 const descriptor = Object.getOwnPropertyDescriptor(this.object, name); 148 149 if (!descriptor || !descriptor.get || descriptor.get[IS_NAMED_PROPERTY] !== true) { 150 // Not defined by NamedPropertyTracker 151 return; 152 } 153 154 // note: delete puts the object in dictionary mode. 155 // if this turns out to be a performance issue, maybe add: 156 // https://github.com/petkaantonov/bluebird/blob/3e36fc861ac5795193ba37935333eb6ef3716390/src/util.js#L177 157 delete this.object[name]; 158 };