UNPKG

10.9 kBJavaScriptView Raw
1"use strict";
2var ObservationRecorder = require('can-observation-recorder');
3var dev = require('can-log/dev/dev');
4var canSymbol = require("can-symbol");
5var canReflect = require("can-reflect");
6var canReflectPromise = require("can-reflect-promise");
7
8var getValueSymbol = canSymbol.for("can.getValue");
9var setValueSymbol = canSymbol.for("can.setValue");
10
11var isValueLikeSymbol = canSymbol.for("can.isValueLike");
12var peek = ObservationRecorder.ignore(canReflect.getKeyValue.bind(canReflect));
13var observeReader;
14var isPromiseLike = ObservationRecorder.ignore(function isPromiseLike(value){
15 return typeof value === "object" && value && typeof value.then === "function";
16});
17
18var bindName = Function.prototype.bind;
19//!steal-remove-start
20if (process.env.NODE_ENV !== 'production') {
21 bindName = function(source){
22 var fn = Function.prototype.bind.call(this, source);
23 Object.defineProperty(fn, "name", {
24 value: canReflect.getName(source) + "."+canReflect.getName(this)
25 });
26 return fn;
27 };
28}
29//!steal-remove-end
30
31var isAt = function(index, reads) {
32 var prevRead = reads[index-1];
33 return prevRead && prevRead.at;
34};
35
36var readValue = function(value, index, reads, options, state, prev){
37 // if the previous read is AT false ... we shouldn't be doing this;
38 var usedValueReader;
39 do {
40
41 usedValueReader = false;
42 for(var i =0, len = observeReader.valueReaders.length; i < len; i++){
43 if( observeReader.valueReaders[i].test(value, index, reads, options) ) {
44 value = observeReader.valueReaders[i].read(value, index, reads, options, state, prev);
45 //usedValueReader = true;
46 }
47 }
48 } while(usedValueReader);
49
50 return value;
51};
52
53var specialRead = {index: true, key: true, event: true, element: true, viewModel: true};
54
55var checkForObservableAndNotify = function(options, state, getObserves, value, index){
56 if(options.foundObservable && !state.foundObservable) {
57 if(ObservationRecorder.trapsCount()) {
58 ObservationRecorder.addMany( getObserves() );
59 options.foundObservable(value, index);
60 state.foundObservable = true;
61 }
62 }
63};
64
65var objHasKeyAtIndex = function(obj, reads, index) {
66 return !!(
67 reads && reads.length &&
68 canReflect.hasKey(obj, reads[index].key)
69 );
70};
71
72observeReader = {
73 // there are things that you need to evaluate when you get them back as a property read
74 // for example a compute or a function you might need to call to get the next value to
75 // actually check
76 // - readCompute - can be set to `false` to prevent reading an ending compute. This is used by component to get a
77 // compute as a delegate. In 3.0, this should be removed and force people to write "{@prop} change"
78 // - callMethodsOnObservables - this is an overwrite ... so normal methods won't be called, but observable ones will.
79 // - executeAnonymousFunctions - call a function if it's found, defaults to true
80 // - proxyMethods - if the last read is a method, return a function so `this` will be correct.
81 // - args - arguments to call functions with.
82 //
83 // Callbacks
84 // - earlyExit - called if a value could not be found
85 // - foundObservable - called when an observable value is found
86 read: function (parent, reads, options) {
87 options = options || {};
88 var state = {
89 foundObservable: false
90 };
91 var getObserves;
92 if(options.foundObservable) {
93 getObserves = ObservationRecorder.trap();
94 }
95
96 // `cur` is the current value.
97 var cur = readValue(parent, 0, reads, options, state),
98 type,
99 // `prev` is the object we are reading from.
100 prev,
101 // `foundObs` did we find an observable.
102 readLength = reads.length,
103 i = 0,
104 last,
105 parentHasKey;
106
107 checkForObservableAndNotify(options, state, getObserves, parent, 0);
108
109 while( i < readLength ) {
110 prev = cur;
111 // try to read the property
112 for(var r=0, readersLength = observeReader.propertyReaders.length; r < readersLength; r++) {
113 var reader = observeReader.propertyReaders[r];
114 if(reader.test(cur)) {
115 cur = reader.read(cur, reads[i], i, options, state);
116 break; // there can be only one reading of a property
117 }
118 }
119 checkForObservableAndNotify(options, state, getObserves, prev, i);
120 last = cur;
121 i = i+1;
122 // read the value if it is a compute or function
123 cur = readValue(cur, i, reads, options, state, prev);
124
125 checkForObservableAndNotify(options, state, getObserves, prev, i-1);
126
127 type = typeof cur;
128 // early exit if need be
129 if (i < reads.length && (cur === null || cur === undefined )) {
130 parentHasKey = objHasKeyAtIndex(prev, reads, i - 1);
131 if (options.earlyExit && !parentHasKey) {
132 options.earlyExit(prev, i - 1, cur);
133 }
134 // return undefined so we know this isn't the right value
135 return {
136 value: undefined,
137 parent: prev,
138 parentHasKey: parentHasKey,
139 foundLastParent: false
140 };
141 }
142
143 }
144
145 parentHasKey = objHasKeyAtIndex(prev, reads, reads.length - 1);
146 // if we don't have a value, exit early.
147 if (cur === undefined && !parentHasKey) {
148 if (options.earlyExit) {
149 options.earlyExit(prev, i - 1);
150 }
151 }
152 return {
153 value: cur,
154 parent: prev,
155 parentHasKey: parentHasKey,
156 foundLastParent: true
157 };
158 },
159 get: function(parent, reads, options){
160 return observeReader.read(parent, observeReader.reads(reads), options || {}).value;
161 },
162 valueReadersMap: {},
163 // an array of types that might have a value inside them like functions
164 // value readers check the current value
165 // and get a new value from it
166 // ideally they would keep calling until
167 // none of these passed
168 valueReaders: [
169 {
170 name: "function",
171 // if this is a function before the last read and its not a constructor function
172 test: function(value){
173 return value && canReflect.isFunctionLike(value) && !canReflect.isConstructorLike(value);
174 },
175 read: function(value, i, reads, options, state, prev){
176 if(options.callMethodsOnObservables && canReflect.isObservableLike(prev) && canReflect.isMapLike(prev)) {
177 dev.warn("can-stache-key: read() called with `callMethodsOnObservables: true`.");
178
179 return value.apply(prev, options.args || []);
180 }
181
182 return options.proxyMethods !== false ? bindName.call(value, prev) : value;
183 }
184 },
185 {
186 name: "isValueLike",
187 // compute value reader
188 test: function(value, i, reads, options) {
189 return value && value[getValueSymbol] && value[isValueLikeSymbol] !== false && (options.foundAt || !isAt(i, reads) );
190 },
191 read: function(value, i, reads, options){
192 if(options.readCompute === false && i === reads.length ) {
193 return value;
194 }
195 return canReflect.getValue(value);
196 },
197 write: function(base, newVal){
198 if(base[setValueSymbol]) {
199 base[setValueSymbol](newVal);
200 } else if(base.set) {
201 base.set(newVal);
202 } else {
203 base(newVal);
204 }
205 }
206 }],
207 propertyReadersMap: {},
208 // an array of things that might have a property
209 propertyReaders: [
210 {
211 name: "map",
212 test: function(value){
213 // the first time we try reading from a promise, set it up for
214 // special reflections.
215 if(canReflect.isPromise(value) ||
216 isPromiseLike(value)) {
217 canReflectPromise(value);
218 }
219
220 return canReflect.isObservableLike(value) && canReflect.isMapLike(value);
221 },
222 read: function(value, prop){
223 var res = canReflect.getKeyValue(value, prop.key);
224 if(res !== undefined) {
225 return res;
226 } else {
227 return value[prop.key];
228 }
229 },
230 write: canReflect.setKeyValue
231 },
232
233 // read a normal object
234 {
235 name: "object",
236 // this is the default
237 test: function(){return true;},
238 read: function(value, prop, i, options){
239 if(value == null) {
240 return undefined;
241 } else {
242 if(typeof value === "object") {
243 if(prop.key in value) {
244 return value[prop.key];
245 }
246 // TODO: remove in 5.0.
247 //!steal-remove-start
248 if (process.env.NODE_ENV !== 'production') {
249 if( prop.at && specialRead[prop.key] && ( ("@"+prop.key) in value)) {
250 options.foundAt = true;
251 dev.warn("Use %"+prop.key+" in place of @"+prop.key+".");
252 return undefined;
253 }
254 }
255 //!steal-remove-end
256 } else {
257 return value[prop.key];
258 }
259 }
260 },
261 write: function(base, prop, newVal){
262 var propValue = base[prop];
263 // if newVal is observable object, lets try to update
264 if(newVal != null && typeof newVal === "object" && canReflect.isMapLike(propValue) ) {
265 dev.warn("can-stache-key: Merging data into \"" + prop + "\" because its parent is non-observable");
266 canReflect.update(propValue, newVal);
267 } else if(propValue != null && propValue[setValueSymbol] !== undefined){
268 canReflect.setValue(propValue, newVal);
269 } else {
270 base[prop] = newVal;
271 }
272 }
273 }
274 ],
275 reads: function(keyArg) {
276 var key = ""+keyArg;
277 var keys = [];
278 var last = 0;
279 var at = false;
280 if( key.charAt(0) === "@" ) {
281 last = 1;
282 at = true;
283 }
284 var keyToAdd = "";
285 for(var i = last; i < key.length; i++) {
286 var character = key.charAt(i);
287 if(character === "." || character === "@") {
288 if( key.charAt(i -1) !== "\\" ) {
289 keys.push({
290 key: keyToAdd,
291 at: at
292 });
293 at = character === "@";
294 keyToAdd = "";
295 } else {
296 keyToAdd = keyToAdd.substr(0,keyToAdd.length - 1) + ".";
297 }
298 } else {
299 keyToAdd += character;
300 }
301 }
302 keys.push({
303 key: keyToAdd,
304 at: at
305 });
306
307 return keys;
308 },
309 // This should be able to set a property similar to how read works.
310 write: function(parent, key, value, options) {
311 var keys = typeof key === "string" ? observeReader.reads(key) : key;
312 var last;
313
314 options = options || {};
315 if(keys.length > 1) {
316 last = keys.pop();
317 parent = observeReader.read(parent, keys, options).value;
318 keys.push(last);
319 } else {
320 last = keys[0];
321 }
322 if(!parent) {
323 return;
324 }
325 var keyValue = peek(parent, last.key);
326 // here's where we need to figure out the best way to write
327
328 // if property being set points at a compute, set the compute
329 if( observeReader.valueReadersMap.isValueLike.test(keyValue, keys.length - 1, keys, options) ) {
330 observeReader.valueReadersMap.isValueLike.write(keyValue, value, options);
331 } else {
332 if(observeReader.valueReadersMap.isValueLike.test(parent, keys.length - 1, keys, options) ) {
333 parent = parent[getValueSymbol]();
334 }
335 if(observeReader.propertyReadersMap.map.test(parent)) {
336 observeReader.propertyReadersMap.map.write(parent, last.key, value, options);
337 }
338 else if(observeReader.propertyReadersMap.object.test(parent)) {
339 observeReader.propertyReadersMap.object.write(parent, last.key, value, options);
340 if(options.observation) {
341 options.observation.update();
342 }
343 }
344 }
345 }
346};
347observeReader.propertyReaders.forEach(function(reader){
348 observeReader.propertyReadersMap[reader.name] = reader;
349});
350observeReader.valueReaders.forEach(function(reader){
351 observeReader.valueReadersMap[reader.name] = reader;
352});
353observeReader.set = observeReader.write;
354
355module.exports = observeReader;