Home | History | Annotate | Download | only in base
      1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 base.require('base.events');
      8 
      9 base.exportTo('base', function() {
     10   /**
     11    * Fires a property change event on the target.
     12    * @param {EventTarget} target The target to dispatch the event on.
     13    * @param {string} propertyName The name of the property that changed.
     14    * @param {*} newValue The new value for the property.
     15    * @param {*} oldValue The old value for the property.
     16    */
     17   function dispatchPropertyChange(target, propertyName, newValue, oldValue,
     18                                   opt_bubbles, opt_cancelable) {
     19     var e = new base.Event(propertyName + 'Change',
     20                            opt_bubbles, opt_cancelable);
     21     e.propertyName = propertyName;
     22     e.newValue = newValue;
     23     e.oldValue = oldValue;
     24 
     25     var error;
     26     e.throwError = function(err) {  // workaround CR 239648
     27       error = err;
     28     };
     29 
     30     target.dispatchEvent(e);
     31     if (error)
     32       throw error;
     33   }
     34 
     35   function setPropertyAndDispatchChange(obj, propertyName, newValue) {
     36     var privateName = propertyName + '_';
     37     var oldValue = obj[propertyName];
     38     obj[privateName] = newValue;
     39     if (oldValue !== newValue)
     40       base.dispatchPropertyChange(obj, propertyName,
     41           newValue, oldValue, true, false);
     42   }
     43 
     44   /**
     45    * Converts a camelCase javascript property name to a hyphenated-lower-case
     46    * attribute name.
     47    * @param {string} jsName The javascript camelCase property name.
     48    * @return {string} The equivalent hyphenated-lower-case attribute name.
     49    */
     50   function getAttributeName(jsName) {
     51     return jsName.replace(/([A-Z])/g, '-$1').toLowerCase();
     52   }
     53 
     54   /* Creates a private name unlikely to collide with object properties names
     55    * @param {string} name The defineProperty name
     56    * @return {string} an obfuscated name
     57    */
     58   function getPrivateName(name) {
     59     return name + '_base_';
     60   }
     61 
     62   /**
     63    * The kind of property to define in {@code defineProperty}.
     64    * @enum {number}
     65    * @const
     66    */
     67   var PropertyKind = {
     68     /**
     69      * Plain old JS property where the backing data is stored as a 'private'
     70      * field on the object.
     71      */
     72     JS: 'js',
     73 
     74     /**
     75      * The property backing data is stored as an attribute on an element.
     76      */
     77     ATTR: 'attr',
     78 
     79     /**
     80      * The property backing data is stored as an attribute on an element. If the
     81      * element has the attribute then the value is true.
     82      */
     83     BOOL_ATTR: 'boolAttr'
     84   };
     85 
     86   /**
     87    * Helper function for defineProperty that returns the getter to use for the
     88    * property.
     89    * @param {string} name The name of the property.
     90    * @param {base.PropertyKind} kind The kind of the property.
     91    * @return {function():*} The getter for the property.
     92    */
     93   function getGetter(name, kind) {
     94     switch (kind) {
     95       case PropertyKind.JS:
     96         var privateName = getPrivateName(name);
     97         return function() {
     98           return this[privateName];
     99         };
    100       case PropertyKind.ATTR:
    101         var attributeName = getAttributeName(name);
    102         return function() {
    103           return this.getAttribute(attributeName);
    104         };
    105       case PropertyKind.BOOL_ATTR:
    106         var attributeName = getAttributeName(name);
    107         return function() {
    108           return this.hasAttribute(attributeName);
    109         };
    110     }
    111   }
    112 
    113   /**
    114    * Helper function for defineProperty that returns the setter of the right
    115    * kind.
    116    * @param {string} name The name of the property we are defining the setter
    117    *     for.
    118    * @param {base.PropertyKind} kind The kind of property we are getting the
    119    *     setter for.
    120    * @param {function(*):void=} opt_setHook A function to run after the property
    121    *     is set, but before the propertyChange event is fired.
    122    * @param {boolean=} opt_bubbles Whether the event bubbles or not.
    123    * @param {boolean=} opt_cancelable Whether the default action of the event
    124    *     can be prevented.
    125    * @return {function(*):void} The function to use as a setter.
    126    */
    127   function getSetter(name, kind, opt_setHook, opt_bubbles, opt_cancelable) {
    128     switch (kind) {
    129       case PropertyKind.JS:
    130         var privateName = getPrivateName(name);
    131         return function(value) {
    132           var oldValue = this[privateName];
    133           if (value !== oldValue) {
    134             this[privateName] = value;
    135             if (opt_setHook)
    136               opt_setHook.call(this, value, oldValue);
    137             dispatchPropertyChange(this, name, value, oldValue,
    138                 opt_bubbles, opt_cancelable);
    139           }
    140         };
    141 
    142       case PropertyKind.ATTR:
    143         var attributeName = getAttributeName(name);
    144         return function(value) {
    145           var oldValue = this.getAttribute(attributeName);
    146           if (value !== oldValue) {
    147             if (value == undefined)
    148               this.removeAttribute(attributeName);
    149             else
    150               this.setAttribute(attributeName, value);
    151             if (opt_setHook)
    152               opt_setHook.call(this, value, oldValue);
    153             dispatchPropertyChange(this, name, value, oldValue,
    154                 opt_bubbles, opt_cancelable);
    155           }
    156         };
    157 
    158       case PropertyKind.BOOL_ATTR:
    159         var attributeName = getAttributeName(name);
    160         return function(value) {
    161           var oldValue = (this.getAttribute(attributeName) === name);
    162           if (value !== oldValue) {
    163             if (value)
    164               this.setAttribute(attributeName, name);
    165             else
    166               this.removeAttribute(attributeName);
    167             if (opt_setHook)
    168               opt_setHook.call(this, value, oldValue);
    169             dispatchPropertyChange(this, name, value, oldValue,
    170                 opt_bubbles, opt_cancelable);
    171           }
    172         };
    173     }
    174   }
    175 
    176   /**
    177    * Defines a property on an object. When the setter changes the value a
    178    * property change event with the type {@code name + 'Change'} is fired.
    179    * @param {!Object} obj The object to define the property for.
    180    * @param {string} name The name of the property.
    181    * @param {base.PropertyKind=} opt_kind What kind of underlying storage to
    182    * use.
    183    * @param {function(*):void=} opt_setHook A function to run after the
    184    *     property is set, but before the propertyChange event is fired.
    185    * @param {boolean=} opt_bubbles Whether the event bubbles or not.
    186    * @param {boolean=} opt_cancelable Whether the default action of the event
    187    *     can be prevented.
    188    */
    189   function defineProperty(obj, name, opt_kind, opt_setHook,
    190                           opt_bubbles, opt_cancelable) {
    191     console.error("Don't use base.defineProperty");
    192     if (typeof obj == 'function')
    193       obj = obj.prototype;
    194 
    195     var kind = opt_kind || PropertyKind.JS;
    196 
    197     if (!obj.__lookupGetter__(name))
    198       obj.__defineGetter__(name, getGetter(name, kind));
    199 
    200     if (!obj.__lookupSetter__(name))
    201       obj.__defineSetter__(name, getSetter(name, kind, opt_setHook,
    202           opt_bubbles, opt_cancelable));
    203   }
    204 
    205   return {
    206     PropertyKind: PropertyKind,
    207     defineProperty: defineProperty,
    208     dispatchPropertyChange: dispatchPropertyChange,
    209     setPropertyAndDispatchChange: setPropertyAndDispatchChange
    210   };
    211 });
    212