1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license http://www.gnu.org/licenses/gpl-2.0.html
4 @source: http://yukkurigames.com/yuu/
10 /** yT - yuu type creation
12 yT is a function like `Object.create`, but with support for
13 more powerful property descriptors (referred to as _extended
14 property descriptors (XPDs)_). Most standard JavaScript
15 property descriptors are valid XPDs, but XPDs allow shortcuts
16 to specify common descriptor patterns.
18 Equivalents for `Object.defineProperties` and
19 `Object.defineProperty` are also provided.
21 ## Extended Property Descriptors
23 Any standard descriptor that has a `get` function (called an
24 'accessor descriptor') or a `value` property (called a 'data
25 descriptor') is also valid XPD.
27 An extended descriptor that does not have either of these and
28 does not meet any of the other conditions below is equivalent
29 to a data descriptor with a `value` of itself, referred to as
30 a 'bare value descriptor'. For example, the following two XPDs
33 { x: 1 } { x: { value: 1 } }
35 (This means a useless descriptor like `{}` is interpreted as a
36 data descriptor with value `undefined` by `Object.create` but
37 a bare value descriptor with value `{}` by `yT`.)
39 In addition, extended descriptors have several other formats
40 which can be used to generate different kinds of idomatic
43 * `alias` - This property is a synonym for a different property.
44 Reads and writes to it will be mapped to reads and writes
45 to the aliased property.
46 { firstChild: { alias: "children[0]" } }
49 get: function () { return this.children[0]; },
50 set: function (v) { this.children[0] = v; }
53 * `proxy` - This property is a synonym for a method call
54 on a different object.
55 { start: { proxy: "engine.start" } }
57 { start: { value: function () {
58 return this.engine.start.apply(this.engine, arguments);
61 Using `alias` rather than `proxy` would result in the
62 wrong (non-engine) `this` argument being passed to the
65 * `aliasSynthetic` - Aliases don't work if one of the
66 properties in the lookup chain is a temporary variable.
67 For example, aliasing `x` to `position[0]` is no good if
68 `position` itself has a getter like `transform.slice(12)`
69 because the assignment to the returned value will have
72 `aliasSynthetic` can be used to capture the temporary,
73 assign to it, and then assign the whole temporary back.
74 { x: { aliasSynthetic: "position[0]" } }
75 Generates the same `get` as `alias`, but `set` is
77 var t = this.position;
82 `aliasSynthetic` assumes the next-to-last value is the
83 temporary, e.g. in `a.b.c.d`, `a.b.c` is the temporary.
84 If rather e.g. `a.b` is the temporary, you can separate
85 the `alias` and `synthetic` parts:
86 { x: { alias: "a.b.c.d", synthetic: "a.b" } }
88 * `swizzle` - Swizzling lets you treat separate properties
89 as one array property. For example if you have a color
90 class with individual r, g, and b properties,
91 { rgb: { swizzle: ["r", "g", "b"] } }
94 get: function () { return [this.r, this.g, this.b] },
102 Any descriptor may also have the `chainable` property set,
103 which generates a chainable setter function. Chainable data
104 descriptors are writable by default.
105 { x: { value: 0, chainable: true } }
107 { x: { value: 0, writable: true },
108 setX: { value: function (v) { this.x = v; return this; } }
113 An example of a simple 2D Point class using XPDs:
115 var Point = yT(Object, {
116 constructor: function (x, y) {
123 xy: { swizzle: "xy" },
124 yx: { swizzle: "yx" },
129 return Math.atan2(this.y, this.x);
131 set: function (angle) {
132 var magnitude = this.magnitude;
133 this.x = Math.cos(angle) * magnitude;
134 this.y = Math.sin(angle) * magnitude;
141 return Math.sqrt(this.x * this.x + this.y * this.y);
143 set: function (magnitude) {
144 var angle = this.angle;
145 this.x = Math.cos(angle) * magnitude;
146 this.y = Math.sin(angle) * magnitude;
153 var p = new Point(3, 0);
155 p.y = 4; p[1] == 4; // true
156 p.magnitude = 1; // normalize
157 p.xy = p.yx; // transpose
159 new Point().setMagnitude(m).setAngle(a);
160 // construct a point from an angle and magnitude in a
164 /* jshint -W054 */ // Function constructors are the whole point here.
166 function isFunction (o
) {
167 /** Check if a value is a function */
168 return Object
.prototype.toString
.call(o
) === '[object Function]';
171 function update (dst
, src
) {
172 /** Copy every enumerable key and its value from src to dst */
178 function rooted (path
) {
179 if (typeof path
=== "number")
180 return "[" + path
+ "]";
181 else if (path
[0] !== "." && path
[0] !== "[")
187 function chainableName (name
) {
188 return "set" + name
[0].toUpperCase() + name
.substring(1);
191 function chainableSetter (name
) {
194 "this" + rooted(name
) + " = value; " + "return this;");
197 function alias (path
, readonly
) {
200 ? { get: new Function("return this" + path
+ ";") }
201 : { get: new Function("return this" + path
+ ";"),
202 set: new Function("v", "return this" + path
+ " = v;") };
205 function proxy (path
) {
207 var prop
= path
.substr(0, path
.lastIndexOf("."));
210 "return this" + path
+ ".apply(this" + prop
+ ", arguments);")
214 function swizzle (props
) {
215 props
= Array
.prototype.map
.call(props
, function (p
) {
216 return "this" + rooted(p
);
218 var assignments
= props
.map(function (p
, i
) {
219 return p
+ " = x[" + i
+ "];";
222 get: new Function("return [" + props
.join(", ") + "];"),
223 set: new Function("x", assignments
.join("\n"))
227 function aliasSynthetic (path
) {
228 var idx
= Math
.max(path
.lastIndexOf("."), path
.lastIndexOf("["));
229 return synthetic(path
.substring(0, idx
), path
);
232 function synthetic (synthPath
, path
) {
233 synthPath
= rooted(synthPath
);
234 path
= rooted(path
).substring(synthPath
.length
);
236 get: new Function("return this" + synthPath
+ path
+ ";"),
237 set: new Function("v",
238 ["var t = this" + synthPath
+ ";",
239 "t" + path
+ " = v; ",
240 "this" + synthPath
+ " = t;",
245 function isAccessorDescriptor (pd
) {
246 /** Check if `pd` is a descriptor describing an accessor */
247 return pd
&& typeof pd
=== "object" && isFunction(pd
.get);
250 function isDataDescriptor (pd
) {
251 /** Check if `pd` is a descriptor describing data */
252 return pd
&& typeof pd
=== "object" && ("value" in pd
);
255 function isDescriptor (pd
) {
256 /** Check if `pd` is any kind of descriptor */
257 return isDataDescriptor(pd
) || isAccessorDescriptor(pd
);
260 function createDescriptors (name
, xpd
, pds
) {
261 if (xpd
.alias
!== undefined) {
262 if (xpd
.synthetic
!== undefined)
263 pds
[name
] = update(xpd
, synthetic(xpd
.alias
, xpd
.synthetic
));
265 pds
[name
] = update(xpd
, alias(xpd
.alias
, xpd
.readonly
));
266 } else if (xpd
.proxy
!== undefined) {
267 pds
[name
] = update(xpd
, proxy(xpd
.proxy
));
268 } else if (xpd
.swizzle
!== undefined) {
269 pds
[name
] = update(xpd
, swizzle(xpd
.swizzle
));
270 } else if (xpd
.aliasSynthetic
!== undefined) {
271 pds
[name
] = update(xpd
, aliasSynthetic(xpd
.aliasSynthetic
));
274 pds
[name
] = isDescriptor(xpd
) ? xpd
: { value
: xpd
};
276 pds
[chainableName(name
)] = { value
: chainableSetter(name
) };
277 if (isDataDescriptor(pds
[name
]) && !("writable" in pds
[name
]))
278 pds
[name
].writable
= true;
282 function xpdsToPds (xpds
) {
284 Object
.keys(xpds
).forEach(function (k
) {
285 createDescriptors(k
, xpds
[k
], pds
);
290 function T (parent
, xpds
) {
291 /** Create a new class described by XPDs
293 This function is similar to `Object.create` but supports
294 extended property descriptors as described above.
296 yT(parent, map of extended descriptors)
297 yT(map of extended descriptors)
298 // The parent is assumed to be Object
300 This returns a _function_ which acts as a constructor for
301 the provided type. If there is a property descriptor named
302 `constructor` it is returned; otherwise a new function
303 that calls the parent's.
305 `parent` may be either the parent constructor or a
312 parent
= isFunction(parent
) && parent
.prototype || parent
;
313 var pds
= xpdsToPds(xpds
);
314 var ctor
= pds
.constructor && pds
.constructor.value
;
315 if (!ctor
|| ctor
=== {}.constructor) {
317 ? function () { parent
.constructor.apply(this, arguments
); }
319 pds
.constructor = { value
: ctor
};
321 ctor
.prototype = Object
.create(parent
, pds
);
325 T
.defineProperties = function (o
, xpds
) {
326 /** Add properties described by XPDs to an object
328 This function is similar to`Object.defineProperties`,
329 but supports the same features as `yT`.
331 return Object
.defineProperties(o
, xpdsToPds(xpds
));
334 T
.defineProperty = function (o
, name
, xpd
) {
335 /** Add a property described by an XPD to an object
337 This function is similar to`Object.defineProperty`,
338 but supports the same features as `yT`.
342 return T
.defineProperties(o
, xpds
);
345 T
.getPropertyDescriptor = function (o
, name
) {
346 /** Look up a descriptor from `o`'s prototype chain */
349 v
= Object
.getOwnPropertyDescriptor(o
, name
);
350 o
= Object
.getPrototypeOf(o
);
355 T
.isAccessorDescriptor
= isAccessorDescriptor
;
356 T
.isDataDescriptor
= isDataDescriptor
;
357 T
.isDescriptor
= isDescriptor
;
363 }).call(typeof exports
=== "undefined" ? this : exports
,
364 typeof exports
=== "undefined" ? null : module
);