2
 
Copyright (c) 2008, Yahoo! Inc. All rights reserved.
 
3
 
Code licensed under the BSD License:
 
4
 
http://developer.yahoo.net/yui/license.txt
 
7
 
YUI.add('attribute', function(Y) {
 
10
 
     * Managed Attribute Provider
 
15
 
     * Maintain state for a collection of items.  Individual properties 
 
16
 
     * are stored in hash tables.  This is instead of having state objects 
 
17
 
     * for each item in the collection.  For large collections, especially 
 
18
 
     * changing ones, this approach may perform better.
 
23
 
    Y.State = function() { 
 
35
 
         * Add an item with all of the properties in the supplied object.
 
37
 
         * @param name {string} identifier for this attribute
 
38
 
         * @param o hash of attributes
 
40
 
        add: function(name, o) {
 
41
 
            Y.each(o, function(v, k) {
 
46
 
                this.data[k][name] = v;
 
51
 
         * Remove entire item, or optionally specified fields
 
53
 
         * @param name {string} name of attribute
 
54
 
         * @param o {string|object|array} single key or collection of keys to delete
 
56
 
        remove: function(name, o) {
 
59
 
                    if (d[key] && (name in d[key])) {
 
64
 
            if (Y.Lang.isString(o)) {
 
67
 
                Y.each(o || d, function(v, k) {
 
68
 
                    if(Y.Lang.isString(k)) {
 
79
 
         * For a given item, gets an attribute.  If key is not
 
80
 
         * supplied, a disposable object with all attributes is 
 
81
 
         * returned.  Use of the latter option makes sense when
 
82
 
         * working with single items, but not if object explosion
 
83
 
         * might cause gc problems.
 
85
 
         * @param name {string} name of attribute
 
86
 
         * @param key {string} optional attribute to get
 
87
 
         * @return either the value of the supplied key or an object with
 
90
 
        // get: function(name, key, val) {
 
91
 
        get: function(name, key) {
 
96
 
                return (d[key] && name in d[key]) ?  d[key][name] : undefined;
 
98
 
                Y.each(d, function(v, k) {
 
110
 
        // figure out what kind of functionality we may need here
 
112
 
        // get a list of items and values for a given key
 
113
 
        // get a list of items where a key has the supplied value
 
115
 
        list: function(key, val) {
 
116
 
            var o = {}, d = this.data, test = !L.isUndefined(val);
 
118
 
            Y.each(this, function(v, k) {
 
121
 
                if (key && k !== key) {
 
123
 
                // verify value.  note, v will be the item names, so this
 
124
 
                // isn't working ... need to iterate v items
 
125
 
                } else if (test && v !== val) {
 
139
 
     * Managed Attribute Provider
 
151
 
        READ_ONLY = "readOnly",
 
152
 
        WRITE_ONCE = "writeOnce",
 
153
 
        VALIDATOR = "validator",
 
159
 
     * Attribute provides managed attribute support. 
 
162
 
     * The class is designed to be augmented onto a host class, 
 
163
 
     * and allows the host to support get/set methods for attributes,
 
164
 
     * initial configuration support and attribute change events.
 
166
 
     * <p>Attributes added to the host can:</p>
 
168
 
     *     <li>Be defined as read-only.</li>
 
169
 
     *     <li>Be defined as write-once.</li>
 
170
 
     *     <li>Be defined with a set function, used to manipulate
 
171
 
     *     values passed to Attribute's set method, before they are stored.</li>
 
172
 
     *     <li>Be defined with a validator function, to validate values before they are stored.</li>
 
173
 
     *     <li>Be defined with a get function, which can be used to manipulate stored values,
 
174
 
     *     before they are returned by Attribute's get method.</li>
 
175
 
     *     <li>Specify if and how they should be cloned on 'get' (see <a href="#property_CLONE">Attribute.CLONE</a> for supported clone modes).</li>
 
178
 
     * <p>See the <a href="#method_addAtt">addAtt</a> method, for details about how to add attributes with
 
179
 
     * a specific configuration</p>
 
184
 
    function Attribute() {
 
185
 
        Y.Event.Target.call(this, {emitFacade:true});
 
186
 
        this._conf = this._conf || new Y.State();
 
187
 
        Y.log('att constructor called', 'info', 'attribute');
 
192
 
     * Constants for clone formats supported by Attribute.
 
195
 
     * By default attribute values returned by the get method
 
196
 
     * are not cloned. However setting the attribute's "clone"
 
200
 
     *     <dt>Attribute.CLONE.DEEP</dt>
 
201
 
     *     <dd>Will result in a deep cloned value being returned
 
202
 
     *        (using YUI's clone method). This can be expensive for complex
 
205
 
     *     <dt>Attribute.CLONE.SHALLOW</dt>
 
206
 
     *     <dd>Will result in a shallow cloned value being returned
 
207
 
     *        (using YUI's merge method).
 
209
 
     *     <dt>Attribute.CLONE.IMMUTABLE</dt>
 
210
 
     *     <dd>Will result in a deep cloned value being returned
 
211
 
     *         when using the get method. Additionally users will
 
212
 
     *         not be able to set sub values of the attribute 
 
213
 
     *         using the complex attribute notation (obj.set("x.y.z, 5)).
 
214
 
     *         However the value of the attribute can be changed, making
 
215
 
     *         it different from a READONLY attribute.
 
217
 
     *     <dt>Attribute.CLONE.NONE</dt>
 
219
 
     *         The value will not be cloned, resulting in a reference
 
220
 
     *         to the stored value being passed back, if the value is an object.
 
221
 
     *         This is the default behavior.
 
237
 
    CLONE_ENUM = Attribute.CLONE;
 
239
 
    Attribute.prototype = {
 
242
 
         * Adds an attribute, with the provided configuration to the host object. Intended
 
243
 
         * to be used by the host object to setup it's set of available attributes.
 
246
 
         * The config argument object literal supports the following optional properties:
 
249
 
         *    <dt>value <Any></dt>
 
250
 
         *    <dd>The initial value to set on the attribute</dd>
 
251
 
         *    <dt>readOnly <Boolean></dt>
 
252
 
         *    <dd>Whether or not the attribute is read only. Attributes having readOnly set to true
 
253
 
         *        cannot be set by invoking the set method.</dd>
 
254
 
         *    <dt>writeOnce <Boolean></dt>
 
255
 
         *    <dd>Whether or not the attribute is "write once". Attributes having writeOnce set to true, 
 
256
 
         *        can only have their values set once, be it through the default configuration, 
 
257
 
         *        constructor configuration arguments, or by invoking set.</dd>
 
258
 
         *    <dt>set <Function></dt>
 
259
 
         *    <dd>The setter function to be invoked (within the context of the host object) before 
 
260
 
         *        the attribute is stored by a call to the set method. The value returned by the 
 
261
 
         *        set function will be the finally stored value.</dd>
 
262
 
         *    <dt>get <Function></dt>
 
263
 
         *    <dd>The getter function to be invoked (within the context of the host object) before
 
264
 
         *    the stored values is returned to a user invoking the get method for the attribute.
 
265
 
         *    The value returned by the get function is the final value which will be returned to the 
 
266
 
         *    user when they invoke get.</dd>
 
267
 
         *    <dt>validator <Function></dt>
 
268
 
         *    <dd>The validator function which is invoked prior to setting the stored value. Returning
 
269
 
         *    false from the validator function will prevent the value from being stored</dd>
 
270
 
         *    <dt>clone <int></dt>
 
271
 
         *    <dd>If and how the value returned by a call to the get method, should be de-referenced from
 
272
 
         *    the stored value. By default values are not cloned, and hence a call to get will return
 
273
 
         *    a reference to the stored value. See Attribute.CLONE for more details about the clone 
 
274
 
         *    options available</dd>
 
279
 
         * @param {String} name The attribute key
 
280
 
         * @param {Object} config (optional) An object literal specifying the configuration for the attribute.
 
281
 
         * <strong>NOTE:</strong> The config object is modified when adding an attribute, 
 
282
 
         * so if you need to protect the original values, you will need to merge or clone the object.
 
285
 
        addAtt: function(name, config) {
 
286
 
            Y.log('adding attribute: ' + name, 'info', 'attribute');
 
287
 
            var value, hasValue = (VALUE in config);
 
289
 
            if (config[READ_ONLY] && !hasValue) { Y.log('readOnly attribute: ' + name + ', added without an initial value. Value will be set on intial call to set', 'warn', 'attribute');}
 
292
 
                value = config.value;
 
296
 
            config.initValue = value;
 
297
 
            this._conf.add(name, config);
 
300
 
                this.set(name, value);
 
305
 
         * Resets the given attribute or all attributes to the initial value.
 
308
 
         * @param {String} name optional An attribute to reset.  If omitted, all attributes are reset
 
310
 
        reset: function(name) {
 
312
 
                this.set(name, this._conf.data['initValue'][name]);
 
314
 
                var initVals = this._conf.data['initValue'];
 
315
 
                Y.each(initVals, function(v, n) {
 
322
 
         * Removes an attribute.
 
325
 
         * @param {String} name The attribute key
 
327
 
        removeAtt: function(name) {
 
328
 
            this._conf.remove(name);
 
332
 
         * Returns the current value of the attribute. If the attribute
 
333
 
         * has been configured with a 'get' handler, this method will delegate
 
334
 
         * to the 'get' handler to obtain the value of the attribute.
 
335
 
         * The 'get' handler will be passed the current value of the attribute 
 
336
 
         * as the only argument.
 
340
 
         * @param {String} key The attribute whose value will be returned. If
 
341
 
         * the value of the attribute is an Object, dot notation can be used to
 
342
 
         * obtain the value of a property of the object (e.g. <code>get("x.y.z")</code>)
 
344
 
         * @return {Any} The current value of the attribute
 
346
 
        get: function(name) {
 
348
 
            var conf = this._conf,
 
354
 
            if (name.indexOf(DOT) !== -1) {
 
355
 
                path = name.split(DOT);
 
359
 
            val = conf.get(name, VALUE);
 
360
 
            getFn = conf.get(name, GET);
 
361
 
            clone = conf.get(name, CLONE);
 
363
 
            val = (clone) ? this._cloneAttVal(val, clone) : val;
 
364
 
            val = (getFn) ? getFn.call(this, val) : val;
 
365
 
            val = (path) ? this._getSubAttVal(path, val) : val;
 
371
 
         * Allows setting of readOnly/writeOnce attributes.
 
376
 
         * @return {Object} Reference to the host object
 
378
 
        _set: function(name, val, opts) {
 
379
 
            return this.set(name, val, opts, true);
 
383
 
         * Sets the value of an attribute.
 
388
 
         * @param {String} name The name of the attribute. Note, if the 
 
389
 
         * value of the attribute is an Object, dot notation can be used
 
390
 
         * to set the value of a property within the object 
 
391
 
         * (e.g. <code>set("x.y.z", 5)</code>), if the attribute has not
 
392
 
         * been declared as an immutable attribute (see <a href="#property_CLONE">Attribute.CLONE</a>).
 
394
 
         * @param {Any} value The value to apply to the attribute
 
396
 
         * @param {Object} opts Optional event data. This object will be mixed into
 
397
 
         * the event facade passed as the first argument to subscribers 
 
398
 
         * of attribute change events
 
400
 
         * @return {Object} Reference to the host object
 
402
 
        set: function(name, val, opts, privateSet) {
 
403
 
            var conf = this._conf,
 
408
 
                initialSet = (!data.value || !(name in data.value));
 
410
 
            if (name.indexOf(DOT) !== -1) {
 
412
 
                path = name.split(DOT);
 
416
 
            if (path && conf.get(name, CLONE) === CLONE_ENUM.IMMUTABLE) {
 
417
 
                Y.log('set ' + name + ' failed; Attribute is IMMUTABLE. Setting a sub value is not permitted', 'info', 'attribute');
 
421
 
            if (!initialSet && !privateSet) {
 
422
 
                if (conf.get(name, WRITE_ONCE)) {
 
423
 
                    Y.log('set ' + name + ' failed; Attribute is writeOnce', 'info', 'attribute');
 
426
 
                if (conf.get(name, READ_ONLY)) {
 
427
 
                    Y.log('set ' + name + ' failed; Attribute is readOnly', 'info', 'attribute');
 
432
 
            if (!conf.get(name)) {
 
433
 
                //Y.log('Set called with unconfigured attribute. Adding a new attribute: ' + name, 'info', 'attribute');
 
434
 
                Y.log('set ' + name + ' failed; Attribute is not configured', 'info', 'attribute');
 
438
 
            currVal = this.get(name);
 
441
 
               val = this._setSubAttVal(path, Y.clone(currVal), val);
 
442
 
               if (val === undefined) {
 
443
 
                   // Path not valid, don't set anything.
 
444
 
                   Y.log('set ' + strPath + 'failed; attribute sub path is invalid', 'error', 'attribute');
 
449
 
            this._fireAttChange(name, currVal, val, name, strPath, opts);
 
456
 
         * Alias for the Event.Target <a href="Event.Target.html#method_subscribe">subscribe</a> method.
 
459
 
         * <p>Subscribers using this method to listen for attribute change events will be notified just
 
460
 
         * <strong>before</strong> the state of the attribute has been modified, and before the default handler has been
 
463
 
         * <p>The <a href="Event.Target.html#method_after">after</a> method, inherited from Event Target, can be used by subscribers
 
464
 
         * who wish to be notified <strong>after</strong> the attribute's value has changed.</p>
 
466
 
         * @param {String} type The event type. For attribute change events, the event type is "[Attribute Name]Change", e.g.
 
467
 
         * for the attribute "enabled", the event type will be "enabledChange".
 
468
 
         * @param {Function} fn The subscribed function to invoke
 
469
 
         * @param {Object} context Optional execution context
 
470
 
         * @param {Any*} args* 0..n additional arguments to append to supply to the subscribed function when the event fires.
 
472
 
         * @return {Event.Handle} The handle object for unsubscribing the subscriber from the event.
 
475
 
            return this.subscribe.apply(this, arguments);
 
479
 
         * Default handler implementation for set events
 
483
 
         * @param {Event.Facade} e The event object for the custom event
 
485
 
        _defAttSet : function(e) {
 
486
 
            var conf = this._conf,
 
490
 
                valFn  = conf.get(name, VALIDATOR),
 
491
 
                setFn = conf.get(name, SET);
 
494
 
                retVal = setFn.call(this, val);
 
495
 
                if (retVal !== undefined) {
 
496
 
                    Y.log('attribute: ' + name + ' modified by setter', 'info', 'attribute');
 
497
 
                    val = retVal; // setter can change value
 
501
 
            if (!valFn || valFn.call(this, val)) {
 
502
 
                conf.add(name, { value: val });
 
503
 
                e.newVal = conf.get(name, VALUE);
 
505
 
                // Prevent "after" listeners from being 
 
506
 
                // invoked since nothing changed.
 
507
 
                e.stopImmediatePropagation();
 
512
 
         * Retrieves the sub value at the provided path,
 
513
 
         * from the value object provided.
 
515
 
         * @method _getSubAttVal
 
518
 
         * @param {Array} path  A path array, specifying the object traversal path
 
519
 
         *                      from which to obtain the sub value.
 
520
 
         * @param {Object} val  The object from which to extract the property value
 
521
 
         * @return {Any} The value stored in the path or undefined if not found.
 
523
 
        _getSubAttVal: function (path, val) {
 
524
 
            var pl = path.length,
 
528
 
                for (i = 0; val !== undefined && i < pl; ++i) {
 
537
 
         * Sets the sub value at the provided path on the value object.
 
538
 
         * Returns the modified value object, or undefined if the path is invalid.
 
540
 
         * @method _setSubAttVal
 
543
 
         * @param {Array} path  A path array, specifying the object traversal path
 
544
 
         *                      at which to set the sub value.
 
545
 
         * @param {Object} val  The object on which to set the sub value.
 
546
 
         * @param {Any} subval  The sub value to set.
 
547
 
         * @return {Object}     The modified object, with the new sub value set, or 
 
548
 
         *                      undefined, if the path was invalid.
 
550
 
        _setSubAttVal: function(path, val, subval) {
 
552
 
            var leafIdx = path.length-1,
 
559
 
                for (i = 0; o !== undefined && i < leafIdx; ++i) {
 
563
 
                // Not preventing new properties from being added
 
564
 
                if (o !== undefined /* && o[path[i]] !== undefined */) {
 
575
 
         * Sets multiple attribute values.
 
578
 
         * @param {Object} atts  A hash of attributes: name/value pairs
 
580
 
        setAtts: function(atts) {
 
581
 
            for (var att in atts) {
 
582
 
                if ( O.owns(atts, att) ) {
 
583
 
                    this.set(att, atts[att]);
 
589
 
         * Gets multiple attribute values.
 
592
 
         * @param {Array} Optional. An array of attribute names, whose values are required. If omitted, all attribute values are
 
594
 
         * @return {Object} A hash of attributes: name/value pairs
 
596
 
        getAtts: function(atts) {
 
597
 
            var o = {}, i, l, att;
 
598
 
            atts = atts || O.keys(this._conf.data[VALUE]);
 
600
 
            for (i = 0, l = atts.length; i < l; i++) {
 
601
 
                // Go through get, to retrieve massaged values and honor cloning
 
603
 
                o[att] = this.get(att); 
 
610
 
         * Configures attributes, and sets initial values
 
615
 
         * @param {Object} cfg Attribute configuration object literal
 
616
 
         * @param {Object} initValues Name/value hash of initial values to apply
 
618
 
        _initAtts : function(cfg, initValues) {
 
626
 
                values = this._splitAttVals(initValues);
 
629
 
                    if (O.owns(atts, att)) {
 
630
 
                        attCfg = Y.merge(atts[att]);
 
631
 
                        value = this._initAttVal(att, attCfg, values);
 
632
 
                        if (value !== undefined) {
 
633
 
                            attCfg.value = value;
 
636
 
                        this.addAtt(att, attCfg);
 
643
 
         * Utility to split out regular attribute values
 
644
 
         * from complex attribute values, so that complex
 
645
 
         * attributes can be keyed by top level attribute name.
 
647
 
         * @method _splitAttrValues
 
648
 
         * @param {Object} valueHash Name/value hash of initial values
 
650
 
         * @return {Object} Object literal with 2 properties - "simple" and "complex",
 
651
 
         * containing simple and complex attribute values respectively keyed 
 
652
 
         * by attribute the top level attribute name.
 
655
 
        _splitAttVals: function(valueHash) {
 
662
 
            for (var k in valueHash) {
 
663
 
                if (O.owns(valueHash, k)) {
 
664
 
                    if (k.indexOf(DOT) !== -1) {
 
667
 
                        v = subvals[attr] = subvals[attr] || [];
 
673
 
                        vals[k] = valueHash[k];
 
677
 
            return { simple:vals, complex:subvals };
 
681
 
         * Returns the initial value of the given attribute from
 
682
 
         * either the default configuration provided, or the 
 
683
 
         * over-ridden value if it exists in the initValues 
 
686
 
         * @param {String} att Attribute name
 
687
 
         * @param {Object} cfg Default attribute configuration
 
689
 
         * @param {Object} initVales Initial attribute values, provided 
 
692
 
         * @return {Any} Initial value of the attribute.
 
694
 
         * @method _initAttVal
 
697
 
        _initAttVal : function(att, cfg, initValues) {
 
699
 
            var hasVal = (VALUE in cfg),
 
700
 
                val = (cfg.valueFn) ? cfg.valueFn.call(this) : cfg.value,
 
709
 
            if (!cfg[READ_ONLY] && initValues) {
 
711
 
                simple = initValues.simple;
 
712
 
                if (simple && O.owns(simple, att)) {
 
717
 
                // Complex Attributes
 
718
 
                complex = initValues.complex;
 
719
 
                if (complex && O.owns(complex, att)) {
 
721
 
                    subvals = complex[att];
 
722
 
                    for (i = 0, l = subvals.length; i < l; ++i) {
 
723
 
                        path = subvals[i].path;
 
724
 
                        subval = subvals[i].value;
 
725
 
                        val = this._setSubAttVal(path, val, subval);
 
735
 
         * Clone utility method, which will 
 
736
 
         * clone the provided value using YUI's 
 
737
 
         * merge, or clone utilities based
 
738
 
         * on the clone type provided. See <a href="#property_CLONE">Attribute.CLONE</a>
 
741
 
         * @method _cloneAttVal
 
743
 
         * @param {Any} val Value to clone
 
744
 
         * @param {int} type Clone type to use, See the CLONE property
 
745
 
         * @return {Any} The cloned copy of the object, based on the provided type.
 
747
 
        _cloneAttVal : function(val, type) {
 
749
 
                case CLONE_ENUM.SHALLOW:
 
752
 
                case CLONE_ENUM.DEEP:
 
753
 
                case CLONE_ENUM.IMMUTABLE:
 
761
 
         * Utility method to help setup the event payload and 
 
762
 
         * fire the attribute change event.
 
764
 
         * @method _fireAttChange
 
766
 
         * @param {String} type The event name
 
767
 
         * @param {Any} currVal The current value of the attribute
 
768
 
         * @param {Any} newVal The new value of the attribute
 
769
 
         * @param {String} attrName The name of the attribute
 
770
 
         * @param {String} strFullPath The full path of the property being changed, 
 
771
 
         * if this is a sub-attribute value being change
 
772
 
         * @param {Object} opts Any additional event data to mix into the attribute change event's event facade.
 
774
 
        _fireAttChange: function(type, currVal, newVal, attrName, strFullPath, opts) {
 
775
 
            type = type + CHANGE;
 
777
 
            // TODO: Publishing temporarily, while we address event bubbling/queuing
 
778
 
            this.publish(type, {queuable:false, defaultFn:this._defAttSet, silent:true});
 
785
 
                subAttrName: strFullPath
 
796
 
    Y.mix(Attribute, Y.Event.Target, false, null, 1);
 
798
 
    Y.Attribute = Attribute;
 
802
 
}, '3.0.0pr2' ,{requires:['event']});