1 if (!("MBX" in window)) {
  2     /** @namespace
  3         @ignore
  4     */
  5     MBX = {};
  6 }
  7 
  8 /** this is the view part of the MVC framework
  9     handle updates and creations and deletes of registered
 10     objects and classes
 11     @namespace
 12 */
 13 MBX.JsView = (function () {
 14     /**
 15         @memberof MBX.JsView
 16         @namespace
 17     */
 18     var self = {};
 19     
 20     var jsElementClass = '.js_updateable';
 21     
 22     /**
 23         Grab give a model - get a css... pass in "." as the second arg to give it css-style return
 24         @param {JsModel} model the model you want the css for
 25         @param {String} prePend any string you want prepended to the return (to avoid "." + MBX.JsView.modelCSS(model))
 26         @returns {String} a css class for a model
 27         @name MBX.JsView.modelCSS
 28         @function
 29     */
 30     self.modelCSS = function (model, prePend) {
 31         prePend = prePend || "";
 32         return prePend + model.modelName.toLowerCase();
 33     };
 34     
 35     /** given an instance of a model, give the recommended instance class 
 36         @param {JsModel#instance} instance the JsModel instance
 37         @param {String} prePend any string you want prepended to the return (to avoid "." + MBX.JsView.modelCSS(model))
 38         @returns {String} a long-ish css class for a specific instance
 39         @name MBX.JsView.cssForInstance
 40         @function
 41     */
 42     self.cssForInstance = function (instance, prePend) {
 43         prePend = prePend || "";
 44         return prePend + self.modelCSS(instance.parentClass) + "_" + instance.primaryKey().toLowerCase().gsub(/[^\w\-]/, "_");
 45     };
 46     
 47     /** @class A single view instance
 48         @constructor
 49         @name JsView
 50         @example
 51             MBX.JsView.create({
 52                 model: MBX.DesktopUpload,
 53                 onCreate: function (upload) {
 54                     //create the upload
 55                 },
 56                 onChange: function (upload) {
 57                     // any upload changes
 58                 },
 59                 onDestroy: function (upload) {
 60                     // handle destroys
 61                 }
 62             });
 63     */
 64     var View = function (opts) {
 65         opts = opts || {};
 66         Object.extend(this, opts);
 67         
 68         if (this.model) {
 69             this._subscribeToEvents();
 70         }
 71         
 72         if (typeof this.initialize == 'function') {
 73             this.initialize();
 74         }
 75     };
 76     
 77     View.prototype = /** @lends JsView */{
 78         active: true,
 79         
 80         /** reactivate event listening on this view
 81         */
 82         activate: function () {
 83             this.active = true;
 84         },
 85         
 86         /** quiets all events on this view, your callbacks will
 87             not get called
 88         */
 89         deactivate: function () {
 90             this.active = false;
 91         },
 92         /** helper function to spit out divs
 93             @param {String} className the classes you want to add to the div
 94             @param {Object} opts (optional) any opts to pass to new Element
 95             @returns div DOM element
 96         */
 97         div: function (className, opts) {
 98             opts = opts || {};
 99             Object.extend(opts, { className: className });
100             return new Element('div', opts);
101         },
102         
103         /** helper function to spit out imageTag
104             @param {String} src the url for the image
105             @param {Object} opts (optional) any opts to pass to new Element
106             @returns img DOM element
107         */
108         imageTag: function (src, opts) {
109             opts = opts || {};
110             Object.extend(opts, { src: src });
111             return new Element("img", opts);
112         },
113         
114         /** Anytime an instance changes on the model you are observing,
115             JsView will fire a function with the object and the key
116             @params {JsModel#instance} object the instance that changed
117             @params {String} key the key that changed
118         */
119         _onInstanceChange: function (evt) {
120             if (this.active && typeof this.onInstanceChange == 'function') {
121                 this.onInstanceChange(evt.object, evt.key);
122             }
123         },
124         
125         _onInstanceCreate: function (evt) {
126             if (this.active && typeof this.onInstanceCreate == 'function') {
127                 this.onInstanceCreate(evt.object);
128             }
129         },
130         
131         _onInstanceDestroy: function (evt) {
132             if (this.active && typeof this.onInstanceDestroy == 'function') {
133                 this.onInstanceDestroy(evt.object);
134             }
135         },
136         
137         _onAttributeChange: function (evt) {
138             if (this.active && typeof this.onAttributeChange == 'function') {
139                 this.onAttributeChange(evt.key);
140             }
141         },
142         
143         /** subscribe to change, create, destroy events
144             We assume you don't need your UI elements to update at the expense of user interaction
145             hence the defer: true
146         */
147         _subscribeToEvents: function () {
148             var changeEvent = this.model.Event.changeInstance;
149             var newEvent = this.model.Event.newInstance;
150             var destroyEvent = this.model.Event.destroyInstance;
151             var attributeEvent = this.model.Event.changeAttribute;
152             var defer = this.looselyCoupled;
153             
154             this.eventSubscriptions = [];
155             this.eventSubscriptions.push(MBX.EventHandler.subscribe(MBX, changeEvent, this._onInstanceChange.bind(this), {defer: defer}));
156             this.eventSubscriptions.push(MBX.EventHandler.subscribe(MBX, newEvent, this._onInstanceCreate.bind(this), {defer: defer}));
157             this.eventSubscriptions.push(MBX.EventHandler.subscribe(MBX, destroyEvent, this._onInstanceDestroy.bind(this), {defer: defer}));
158             this.eventSubscriptions.push(MBX.EventHandler.subscribe(MBX, attributeEvent, this._onAttributeChange.bind(this), {defer: defer}));
159         },
160         
161         /** return the CSS class for the model associated with this view
162             @param {String} prePend any string to prepend to the model
163             @see MBX.JsView.modelCSS
164         */
165         modelCSS: function (prePend) {
166             return MBX.JsView.modelCSS(this.model, prePend);
167         },
168         
169         /** given an instance of a model, give the recommended instance class
170             @see MBX.JsView.cssForInstance
171         */
172         cssForInstance: function (instance, prePend) {
173             return MBX.JsView.cssForInstance(instance, prePend);
174         },
175         
176         /** given an instance, return the relevent elements
177             @param {JsModel#instance} instance the instance you want to find elements
178             @returns {Array} an array of extended dom elements that match the relevent class names
179         */
180         elementsFromInstance: function (instance) {
181             return $$(this.modelCSS(".") + " " + cssForInstance("."));
182         },
183         
184         /** return all the various collections of dom elements that have the appropriate classes
185             @returns {Array} array of extended dom elements that match collection CSS
186         */
187         domCollections: function () {
188             var findString = [jsElementClass, this.modelCSS("."), '.collection'].join('');
189             return $$(findString);
190         }
191         
192     };
193     
194     /** create a new view handler... specify a model and some
195         functions and some great magic happens.
196         
197         If your view listens to a model, but you are not dependent on real-time updates,
198         you can add the option "looselyCoupled: true" and all updates will be done with
199         setTimeout, which will be a performance enhancement.
200         
201         @name MBX.JsView.create
202         @function
203         @param {Object} opts the various options specified for a view
204         @example
205             MBX.JsView.create({
206                 model: MBX.DesktopUpload,
207                 looselyCoupled: false, // false is the default
208                 onCreate: function (upload) {
209                     //create the upload
210                 },
211                 onChange: function (upload) {
212                     // any upload changes
213                 },
214                 onDestroy: function (upload) {
215                     // handle destroys
216                 }
217             });
218     */
219     self.create = function (opts) {
220         return new View(opts);
221     };
222     
223     /**
224         call extend() to add methods and/or attributes to ALL views
225         @param {Object} methsAndAttrs
226         @name MBX.JsView.extend
227         @function
228     */
229     self.extend = function (methsAndAttrs) {
230         methsAndAttrs = methsAndAttrs || {};
231         Object.extend(View.prototype, methsAndAttrs);
232     };
233     
234     /** Added to prototype elements when a view is created
235         allows you to observe particular keys of a model object
236         @name MBX.JsView.updatesOn
237         @function
238         @param {JsModel#instance} obj the model instance to watch
239         @param {String} key the key to watch
240         @param {Object} opts handler or preProcess functions to execute
241         @returns {DOM element} the dom element that was called (for chaining)
242     */
243     self.updatesOn = function (element, obj, key, opts) {
244         if (!obj || !key) {
245             throw new Error("You must specify a key or an object with updatesOn");
246         }
247         element = $(element);
248         opts = opts || {};
249         
250         var changeHandler = function (evt) {
251             var content = obj.get(key);
252             if (opts.preProcess) {
253                 content = opts.preProcess(obj, content, key);
254             }
255             if (opts.handler) {
256                 opts.handler(obj, element, content);
257             } else {
258                 element.update(content);
259             }
260         };
261         
262         var sub = MBX.EventHandler.subscribe(obj, key + "_changed", changeHandler);
263         
264         element.__JsViewSubscriptions = element.__JsViewSubscriptions || [];       
265         element.__JsViewSubscriptions.push(sub);
266         
267         obj.__JsViewSubscriptions = obj.__JsViewSubscriptions || [];
268         obj.__JsViewSubscriptions.push(sub);
269         
270         if (opts.updateNow) {
271             changeHandler();
272         }
273         
274         return element;
275     };
276     
277     /** stop updating this instance, all event handlers are removed
278         Preferred method of accessing this function is through extended elements
279         @name MBX.JsView.stopUpdating
280         @function
281         @param {DOM Element} element the element to stop observing - this will usually be filled for you by prototype
282         @returns {DOM Element} element for chaining
283         @example
284           $("el").stopUpdating();
285     */
286     self.stopUpdating = function (element) {
287         element = $(element);
288         if (element.__JsViewSubscriptions) {
289             while (element.__JsViewSubscriptions.length > 0) {
290                 MBX.EventHandler.unsubscribe(element.__JsViewSubscriptions.pop());
291             }
292         }
293         return element;
294     };
295     
296     /** attach a JsModel#instance to this DOM element
297         Preferred method of accessing this function is through extended elements
298         @name MBX.JsView.assignInstance
299         @function
300         @example
301           $("el").assignInstance(instance);
302           $("el").getInstance();  // == instance
303     */
304     self.assignInstance = function (element, instance) {
305         element = $(element);
306         element.__JsViewMvcInstance = instance;
307         element.addClassName(MBX.JsView.modelCSS(instance.parentClass));
308         element.addClassName(MBX.JsView.cssForInstance(instance));
309         return element;
310     };
311     
312     /** fetch a JsModel#instance from this DOM element
313         Preferred method of accessing this function is through extended elements
314         @name MBX.JsView.getInstance
315         @function
316         @example
317           $("el").assignInstance(instance);
318           $("el").getInstance();  // == instance
319     */
320     self.getInstance = function (element, instance) {
321         return element.__JsViewMvcInstance;
322     };
323     
324     Element.addMethods({
325         updatesOn: self.updatesOn,
326         stopUpdating: self.stopUpdating,
327         assignInstance: self.assignInstance,
328         getInstance: self.getInstance
329     });
330     
331     return self;
332 })();
333