|
1 /* this source code form is subject to the terms of the mozilla public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 /** |
|
6 * ObservableObject |
|
7 * |
|
8 * An observable object is a JSON-like object that throws |
|
9 * events when its direct properties or properties of any |
|
10 * contained objects, are getting accessed or set. |
|
11 * |
|
12 * Inherits from EventEmitter. |
|
13 * |
|
14 * Properties: |
|
15 * ⬩ object: JSON-like object |
|
16 * |
|
17 * Events: |
|
18 * ⬩ "get" / path (array of property names) |
|
19 * ⬩ "set" / path / new value |
|
20 * |
|
21 * Example: |
|
22 * |
|
23 * let emitter = new ObservableObject({ x: { y: [10] } }); |
|
24 * emitter.on("set", console.log); |
|
25 * emitter.on("get", console.log); |
|
26 * let obj = emitter.object; |
|
27 * obj.x.y[0] = 50; |
|
28 * |
|
29 */ |
|
30 |
|
31 "use strict"; |
|
32 |
|
33 const EventEmitter = require("devtools/toolkit/event-emitter"); |
|
34 |
|
35 function ObservableObject(object = {}) { |
|
36 EventEmitter.decorate(this); |
|
37 let handler = new Handler(this); |
|
38 this.object = new Proxy(object, handler); |
|
39 handler._wrappers.set(this.object, object); |
|
40 handler._paths.set(object, []); |
|
41 } |
|
42 |
|
43 module.exports = ObservableObject; |
|
44 |
|
45 function isObject(x) { |
|
46 if (typeof x === "object") |
|
47 return x !== null; |
|
48 return typeof x === "function"; |
|
49 } |
|
50 |
|
51 function Handler(emitter) { |
|
52 this._emitter = emitter; |
|
53 this._wrappers = new WeakMap(); |
|
54 this._values = new WeakMap(); |
|
55 this._paths = new WeakMap(); |
|
56 } |
|
57 |
|
58 Handler.prototype = { |
|
59 wrap: function(target, key, value) { |
|
60 let path; |
|
61 if (!isObject(value)) { |
|
62 path = this._paths.get(target).concat(key); |
|
63 } else if (this._wrappers.has(value)) { |
|
64 path = this._paths.get(value); |
|
65 } else if (this._paths.has(value)) { |
|
66 path = this._paths.get(value); |
|
67 value = this._values.get(value); |
|
68 } else { |
|
69 path = this._paths.get(target).concat(key); |
|
70 this._paths.set(value, path); |
|
71 let wrapper = new Proxy(value, this); |
|
72 this._wrappers.set(wrapper, value); |
|
73 this._values.set(value, wrapper); |
|
74 value = wrapper; |
|
75 } |
|
76 return [value, path]; |
|
77 }, |
|
78 unwrap: function(target, key, value) { |
|
79 if (!isObject(value) || !this._wrappers.has(value)) { |
|
80 return [value, this._paths.get(target).concat(key)]; |
|
81 } |
|
82 return [this._wrappers.get(value), this._paths.get(target).concat(key)]; |
|
83 }, |
|
84 get: function(target, key) { |
|
85 let value = target[key]; |
|
86 let [wrapped, path] = this.wrap(target, key, value); |
|
87 this._emitter.emit("get", path, value); |
|
88 return wrapped; |
|
89 }, |
|
90 set: function(target, key, value) { |
|
91 let [wrapped, path] = this.unwrap(target, key, value); |
|
92 target[key] = value; |
|
93 this._emitter.emit("set", path, value); |
|
94 }, |
|
95 getOwnPropertyDescriptor: function(target, key) { |
|
96 let desc = Object.getOwnPropertyDescriptor(target, key); |
|
97 if (desc) { |
|
98 if ("value" in desc) { |
|
99 let [wrapped, path] = this.wrap(target, key, desc.value); |
|
100 desc.value = wrapped; |
|
101 this._emitter.emit("get", path, desc.value); |
|
102 } else { |
|
103 if ("get" in desc) { |
|
104 [desc.get] = this.wrap(target, "get "+key, desc.get); |
|
105 } |
|
106 if ("set" in desc) { |
|
107 [desc.set] = this.wrap(target, "set "+key, desc.set); |
|
108 } |
|
109 } |
|
110 } |
|
111 return desc; |
|
112 }, |
|
113 defineProperty: function(target, key, desc) { |
|
114 if ("value" in desc) { |
|
115 let [unwrapped, path] = this.unwrap(target, key, desc.value); |
|
116 desc.value = unwrapped; |
|
117 Object.defineProperty(target, key, desc); |
|
118 this._emitter.emit("set", path, desc.value); |
|
119 } else { |
|
120 if ("get" in desc) { |
|
121 [desc.get] = this.unwrap(target, "get "+key, desc.get); |
|
122 } |
|
123 if ("set" in desc) { |
|
124 [desc.set] = this.unwrap(target, "set "+key, desc.set); |
|
125 } |
|
126 Object.defineProperty(target, key, desc); |
|
127 } |
|
128 } |
|
129 }; |