1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 /** @class
  9 
 10   The Builder class makes it easy to create new chained-builder API's such as
 11   those provided by CoreQuery or jQuery.  Usually you will not create a new
 12   builder yourself, but you will often use instances of the Builder object to
 13   configure parts of the UI such as menus and views.
 14   
 15   # Anatomy of a Builder
 16   
 17   You can create a new Builder much like you would any other class in 
 18   SproutCore.  For example, you could create a new CoreQuery-type object with
 19   the following:
 20   
 21       SC.$ = SC.Builder.create({
 22         // methods you can call go here.
 23       });
 24   
 25   Unlike most classes in SproutCore, Builder objects are actually functions 
 26   that you can call to create new instances.  In the example above, to use 
 27   the builder, you must call it like a function:
 28   
 29       buildit = SC.$();
 30   
 31   If you define an init() method on a builder, it will be invoked wheneve the
 32   builder is called as a function, including any passed params.  Your init()
 33   method MUST return this, unlike regular SC objects.  i.e.
 34   
 35       SC.$ = SC.Builder.create({
 36         init: function(args) { 
 37           this.args = SC.A(args);
 38           return this;
 39         }
 40       });
 41       
 42       buildit = SC.$('a', 'b');
 43       buildit.args => ['a','b']
 44   
 45   In addition to defining a function like this, all builder objects also have
 46   an 'fn' property that contains a hash of all of the helper methods defined
 47   on the builder function.  Once a builder has been created, you can add 
 48   addition "plugins" for the builder by simply adding new methods to the
 49   fn property.
 50   
 51   # Writing Builder Functions
 52   
 53   All builders share a few things in common:
 54   
 55    * when a new builder is created, it's init() method will be called.  The default version of this method simply copies the passed parameters into the builder as content, but you can override this with anything you want.
 56    * the content the builder works on is stored as indexed properties (i.e. 0,1,2,3, like an array).  The builder should also have a length property if you want it treated like an array.
 57    *- Builders also maintain a stack of previous builder instances which you can pop off at any time.
 58   
 59   To get content back out of a builder once you are ready with it, you can
 60   call the method done().  This will return an array or a single object, if 
 61   the builder only works on a single item.
 62   
 63   You should write your methods using the getEach() iterator to work on your
 64   member objects.  All builders implement SC.Enumerable in the fn() method.
 65 
 66       CoreQuery = SC.Builder.create({
 67         ...
 68       }) ;
 69       
 70       CoreQuery = new SC.Builder(properties) {
 71         ...
 72       } ;
 73       
 74       CoreQuery2 = CoreQuery.extend() {
 75       }
 76   
 77   @constructor
 78 */
 79 SC.Builder = function (props) { return SC.Builder.create(props); };
 80 
 81 /** 
 82   Create a new builder object, applying the passed properties to the 
 83   builder's fn property hash.
 84   
 85   @param {Hash} properties
 86   @returns {SC.Builder}
 87 */
 88 SC.Builder.create = function create(props) { 
 89   
 90   // generate new fn with built-in properties and copy props
 91   var fn = SC.mixin(SC.beget(this.fn), props||{}) ;
 92   if (props.hasOwnProperty('toString')) fn.toString = props.toString;
 93   
 94   // generate new constructor and hook in the fn
 95   var construct = function() {
 96     var ret = SC.beget(fn); // NOTE: using closure here...
 97     
 98     // the defaultClass is usually this for this constructor. 
 99     // e.g. SC.View.build() -> this = SC.View
100     ret.defaultClass = this ;
101     ret.constructor = construct ;
102 
103     // now init the builder object.
104     return ret.init.apply(ret, arguments) ;
105   } ;
106   construct.fn = construct.prototype = fn ;
107 
108   // the create() method can be used to extend a new builder.
109   // eg. SC.View.buildCustom = SC.View.build.extend({ ...props... })
110   construct.extend = SC.Builder.create ;
111   construct.mixin = SC.Builder.mixin ;
112   
113   return construct; // return new constructor
114 } ;
115 
116 SC.Builder.mixin = function() {
117   var len = arguments.length, idx;
118   for(idx=0;idx<len;idx++) SC.mixin(this, arguments[idx]);
119   return this ;
120 };
121 
122 /** This is the default set of helper methods defined for new builders. */
123 SC.Builder.fn = {
124 
125   /** 
126     Default init method for builders.  This method accepts either a single
127     content object or an array of content objects and copies them onto the 
128     receiver.  You can override this to provide any kind of init behavior 
129     that you want.  Any parameters passed to the builder method will be 
130     forwarded to your init method.
131     
132     @returns {SC.Builder} receiver
133   */
134   init: function(content) {
135     if (content !== undefined) {
136       if (SC.typeOf(content) === SC.T_ARRAY) {
137         var loc=content.length;
138         while(--loc >= 0) {
139           this[loc] = content.objectAt ? content.objectAt(loc) : content[loc];
140         }
141         this.length = content.length ;
142       } else {
143         this[0] = content; this.length=1;
144       }
145     }
146     return this ;
147   },
148   
149   /** Return the number of elements in the matched set. */
150   size: function() { return this.length; },
151   
152   /** 
153     Take an array of elements and push it onto the stack (making it the
154     new matched set.)  The receiver will be saved so it can be popped later.
155     
156     @param {Object|Array} content
157     @returns {SC.Builder} new instance
158   */
159   pushStack: function() {
160     // Build a new CoreQuery matched element set
161     var ret = this.constructor.apply(this,arguments);
162 
163     // Add the old object onto the stack (as a reference)
164     ret.prevObject = this;
165 
166     // Return the newly-formed element set
167     return ret;
168   },
169 
170   /**
171     Returns the previous object on the stack so you can continue with that
172     transform.  If there is no previous item on the stack, an empty set will
173     be returned.
174   */
175   end: function() { 
176     return this.prevObject || this.constructor(); 
177   },
178   
179   // toString describes the builder
180   toString: function() { 
181     return "%@$(%@)".fmt(this.defaultClass.toString(), 
182       SC.A(this).invoke('toString').join(',')); 
183   },
184   
185   /** You can enhance the fn using this mixin method. */
186   mixin: SC.Builder.mixin
187   
188 };
189 
190 // Apply SC.Enumerable.  Whenever possible we want to use the Array version
191 // because it might be native code.
192 (function() {
193   var enumerable = SC.Enumerable, fn = SC.Builder.fn, key, value ;
194   for(key in enumerable) {
195     if (!enumerable.hasOwnProperty(key)) continue ;
196     value = Array.prototype[key] || enumerable[key];
197     fn[key] = value ;
198   }
199 })();
200 
201 
202 
203