1 /* 2 * Copyright (C) 2008-2009 WaveMaker Software, Inc. 3 * 4 * This file is part of the WaveMaker Client Runtime. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 dojo.provide("wm.base.Component"); 19 dojo.require('wm.base.Object'); 20 21 /** 22 Base class for all palette objects. 23 <br><br>Component: 24 <ul> 25 <li>can own components, and can itself be owned.</li> 26 <li>ensures all owned components have distinct names.</li> 27 <li>can be identified by a globally unique string id.</li> 28 <li>sends notification messages (via id) when it's values change</li> 29 <li>can read or write it's properties to a stream</li> 30 @name wm.Component 31 @class 32 @extends wm.Object 33 */ 34 dojo.declare("wm.Component", wm.Object, { 35 /** @lends wm.Component.prototype */ 36 /** 37 Name of this object. 38 Must be unique to it's owner. 39 @type String 40 @example this.label1.setValue("name", "titleLabel"); 41 */ 42 name: '', 43 /** 44 Name of this object. 45 Must be unique to it's owner. 46 @type String 47 @example newButton.setValue("owner", this); 48 */ 49 owner: null, 50 //======================================================= 51 // Construction 52 //======================================================= 53 /** 54 Component constructor optionally takes a set of properties to initialize on 55 the new instance. 56 @example 57 var foo = new wm.Component({name: "foo"}); 58 @param {Object} inProperties Properties to initialize on the new instance. 59 May be ommitted. 60 */ 61 constructor: function(inProps) { 62 this.$ = this.components = {}; 63 this._connections = []; 64 this._subscriptions = []; 65 this._designee = this; 66 }, 67 postscript: function(inProps) { 68 this.create(inProps); 69 wm.Component.add(this); 70 }, 71 create: function(inProps){ 72 this.prepare(inProps); 73 this.build(); 74 this.init(); 75 if (this._designer) 76 wm.fire(this, "designCreate"); 77 if (!this._loading) 78 this.postInit(); 79 }, 80 /** 81 Remove this component from the system and clean up 82 all resources. 83 */ 84 destroy: function() { 85 this._disconnect(); 86 this._unsubscribe(); 87 wm.fire(this, "designDestroy"); 88 var comps = []; 89 for (var n in this.components) 90 comps.push(this.components[n]); 91 for(var i=0, c; (c=comps[i]); i++) 92 c.destroy(); 93 wm.Component.remove(this); 94 this.setOwner(null); 95 }, 96 prepare: function(inProps) { 97 this.readProps(inProps); 98 dojo.mixin(this, inProps); 99 if (!this.flags) 100 this.flags = {}; 101 this.setOwner(this.owner); 102 }, 103 readProps: function(inProps) { 104 }, 105 build: function() { 106 }, 107 init: function() { 108 }, 109 postInit: function() { 110 this.valueChanged("", this); 111 }, 112 loaded: function() { 113 this._loading = false; 114 this.postInit(); 115 }, 116 toString: function() { 117 return '[' + this.declaredClass + ']'; 118 }, 119 //======================================================= 120 // FIXME: deprecated, remove asap 121 //======================================================= 122 // Get a named component by ascending owner chain 123 getComponent: function(inName) { 124 return this.components[inName] || this.owner && this.owner.getComponent(inName); 125 }, 126 //======================================================= 127 // Design Support 128 //======================================================= 129 isDesignLoaded: function() { 130 return (window.studio && (this.isOwnedBy(studio.page) || this == studio.page || this.isOwnedBy(studio.application) || !this.isOwnedBy(app))); 131 }, 132 getPath: function() { 133 // FIXME: hack, at least move studio awareness to design-only code 134 var p = ''; 135 if (this.isDesignLoaded()) { 136 p = "projects/" + studio.project.projectName + "/"; 137 } 138 return p; 139 }, 140 //======================================================= 141 // Ownership 142 //======================================================= 143 addComponent: function(inComponent) { 144 var n = inComponent.name; 145 //if (this.components[n]) 146 // wm.logging && console.debug('Duplicate object name "' + n + '" in owner ' + this); 147 this.components[n] = inComponent; 148 }, 149 removeComponent: function(inComponent) { 150 var n = inComponent.name; 151 if (this.components[n] == inComponent) 152 delete this.components[n]; 153 }, 154 setOwner: function(inOwner) { 155 if (this.owner) 156 this.owner.removeComponent(this); 157 this.owner = inOwner; 158 if (this.owner) { 159 this.owner.addComponent(this); 160 if (!this._designer) 161 this._designer = this.owner._designer; 162 this.updateId(); 163 } 164 }, 165 isOwnedBy: function(inOwner) { 166 var o = this.owner; 167 while (o) { 168 if (o == inOwner) 169 return true; 170 o = o.owner; 171 } 172 }, 173 qualifyName: function(inName) { 174 inName = this.name + '_' + inName; 175 if (window.studio && (window.studio.page == this.owner || window.studio.application == this.owner)) 176 return inName; 177 return this.owner ? this.owner.qualifyName(inName) : inName; 178 }, 179 getUniqueName: function(inName) { 180 return wm.findUniqueName(inName, [this, this.components]); 181 }, 182 //======================================================= 183 // Name & Id 184 //======================================================= 185 setName: function(inName) { 186 if (!inName) 187 return; 188 wm.Component.remove(this); 189 this.owner.removeComponent(this); 190 this.name = inName; 191 this.owner.addComponent(this); 192 this.updateId(); 193 wm.Component.add(this); 194 }, 195 updateId: function() { 196 this.id = this.makeId(); 197 }, 198 // make a streamable id 199 // an id is fully qualified within its root 200 makeId: function(inName) { 201 inName = this.name + (inName ? (this.name ? "." : "") + inName : ""); 202 return this.owner ? this.owner.getId(inName) : inName; 203 }, 204 /** 205 Return a string that can identify a name as a child of 206 this component in the namespace of the root object. 207 @see <a href="#getRoot">getRoot</a> 208 @param {String} inName The name to qualify. 209 @returns {String} The qualified id string. 210 */ 211 getId: function(inName) { 212 return this.makeId(inName); 213 }, 214 // get the root object that owns this component and under which its id is qualified 215 getRoot: function() { 216 return this.owner ? this.owner.getRoot() : null; 217 }, 218 // get the root portion of the runtime id 219 getRootId: function() { 220 var r = this.getRoot(); 221 r = r ? r.getRuntimeId() : ""; 222 return r ? r + (r.charAt(r.length-1) == "." ? "" : ".") : ""; 223 }, 224 /** 225 Return a string that can globally identify a name 226 as a child of this component. 227 @param {String} inName The name to qualify. 228 @returns {String} The qualified id string. 229 */ 230 // make a globally unique runtime id 231 getRuntimeId: function(inName) { 232 var r = this.getRootId() + this.getId(inName); 233 return r; 234 }, 235 // get a value under root using an id 236 getValueById: function(inId) { 237 var r = this.getRoot(); 238 r = r && r.getValue(inId); 239 return r && r._wmNull ? app.getValue(inId) : r; 240 }, 241 //======================================================= 242 // Utility 243 //======================================================= 244 connect: function() { 245 this._connections.push(dojo.connect.apply(dojo, arguments)); 246 }, 247 connectEvents: function(inObject, inEvents) { 248 this._connections = this._connections.concat(wm.connectEvents(this, inObject, inEvents)); 249 }, 250 _disconnect: function(inNode, inEvents) { 251 dojo.forEach(this._connections, dojo.disconnect); 252 }, 253 subscribe: function() { 254 this._subscriptions.push(dojo.subscribe.apply(dojo, arguments)); 255 }, 256 _unsubscribe: function() { 257 dojo.forEach(this._subscriptions, dojo.unsubscribe); 258 }, 259 //======================================================= 260 // Properties 261 //======================================================= 262 isEventProp: function(n) { 263 return dojo.isFunction(this._designee[n]) && (n.slice(0,2)=="on"); 264 }, 265 _getProp: function(n) { 266 if (this.isEventProp(n)) 267 return this.eventBindings[n] || ""; 268 // do we need this? 269 var g = this._getPropWorker(this._designee, n, "get"); 270 if (g) 271 return g.call(this, n); 272 return n in this._designee ? this._designee[n] : this.components[n]; 273 }, 274 _setProp: function(n, v) { 275 if (this.isEventProp(n)) 276 this.setEvent(n, v); 277 else { 278 // do we need this? 279 var s = this._getPropWorker(this._designee, n, "set"); 280 if (s) 281 s.call(this, v); 282 else if (n in this._designee) 283 this._designee[n] = v; 284 } 285 }, 286 //======================================================= 287 // Values 288 //======================================================= 289 // id-based notification 290 valueChanged: function(inProp, inValue) { 291 dojo.publish(this.getRuntimeId(inProp) + "-changed", [inValue, this]); 292 }, 293 //======================================================= 294 // Streaming In 295 //======================================================= 296 _create: function(ctor, props) { 297 var p = ctor.prototype; 298 if (ctor == wm.Component || p instanceof wm.Component) 299 return new ctor(props); 300 console.debug("Component._create: ignoring unknown component type: ", p); 301 //throw ("Page._create: unknown component type: " + p); 302 }, 303 adjustChildProps: function(inCtor, inProps) { 304 dojo.mixin(inProps, {owner: this}); 305 }, 306 /** 307 Create a component as a child of this component. 308 @param inName {String} Name of the new component (may be altered to ensure uniqueness). 309 @param inType {String} Type of component to create (note, a string, not a constructor). 310 @param inProps {Object} Hash of properties to pass to the new components <a href="#constructor">constructor</a>. 311 @param inEvents {Object} Name/value pairs that match events in the new component to functions in the owner. 312 @param inChildren {Object} Name/value pairs that describe child components to create on this object. 313 @param inOwner {Object} Optional. Override automatic value for "owner". 314 @example 315 this.panel1.createComponent("custom", "wm.Panel", { 316 // properties 317 height: "3em", 318 }, { 319 // events 320 onclick: "click" // connect onclick event to owner's "click" function 321 }, { 322 // children 323 // name: [ "[type]", { [properties] }, { [events] }, { [children] } ] 324 spacer1: [ "wm.Spacer", { width: "300px" } ], 325 content: [ "wm.Label", { width: "1flex" } ], 326 spacer2: [ "wm.Spacer", { width: "300px" } ] 327 }); 328 */ 329 createComponent: function(inName, inType, inProps, inEvents, inChildren, inOwner) { 330 var ctor = dojo.getObject(inType); 331 if (!ctor) throw('Component type "' + inType + '" is not available.'); 332 // 333 var props = dojo.mixin({_designer: this._designer, _loading: true}, inProps); 334 this.adjustChildProps(ctor, props); 335 if (inOwner) 336 props.owner = inOwner; 337 // 338 // FIXME: avoid unique names if owner root is loading... 339 // fix to prevent extra components in application children 340 // FIXME: or owner itself is loading (avoids copy/paste sub-components duplication) 341 props.name = props.owner.getRoot()._loading || props.owner._loading ? inName : props.owner.getUniqueName(inName); 342 // 343 var w = this._create(ctor, props); 344 try{ 345 if (inEvents) 346 w.owner.makeEvents(inEvents, w); 347 if (inChildren) 348 w.createComponents(inChildren); 349 }finally{ 350 w.loaded(); 351 } 352 return w; 353 }, 354 createComponents: function(inComponents, inOwner) { 355 var result = []; 356 for (var i in inComponents) { 357 var c = inComponents[i]; 358 result.push(this.createComponent(i, c[0], c[1], c[2], c[3], inOwner)); 359 } 360 return result; 361 }, 362 _eventArgs: function(c, a) { 363 var args = [ c ]; 364 for (var i=0,l=a.length; i<l; i++){args.push(a[i])}; 365 return args; 366 }, 367 makeEvents: function(inEvents, inComponent) { 368 var e, n, f; 369 for (n in inEvents) { 370 // name of the source 371 f = inEvents[n]; 372 // the target 373 e = this[f] || f; 374 if (this._designer) 375 // if designing, note the eventBinding 376 wm.fire(inComponent, "setProp", [n, f]); 377 else 378 // otherwise, connect the named event 379 this.connect(inComponent._eventSource||inComponent, n, this.makeEvent(e, f)); 380 } 381 }, 382 makeEvent: function(inHandler, inName) { 383 return dojo.isFunction(inHandler) ? this._makeEvent(inName) : this._makeComponentEvent(inHandler); 384 }, 385 _makeEvent: function(inName) { 386 var self = this; 387 return function() { 388 self[inName].apply(self, self._eventArgs(this, arguments)); 389 } 390 }, 391 _makeComponentEvent: function(inHandler) { 392 var self = this; 393 // FIXME: experimental: can call a method on a component 394 return function() { 395 // inHandler could be a component 396 // or a (string) Id of a component 397 // or a (string) Id of a component + a dotted method suffix 398 var c = inHandler instanceof wm.Component ? inHandler : self.getValueById(inHandler); 399 if (c instanceof wm.Component) 400 wm.fire(c, "update"); 401 // call a method on a component 402 else if (dojo.isString(inHandler)) { 403 var o = inHandler.split('.'); 404 if (o.length > 1) { 405 var m = o.pop(); 406 c = self.getValueById(o); 407 if (c && c[m]) 408 c[m](); 409 } 410 } 411 } 412 }, 413 readComponents: function(inComponents) { 414 var c = dojo.fromJson(inComponents); 415 return this.createComponents(c); 416 } 417 }); 418 419 //======================================================= 420 // Class Properties 421 //======================================================= 422 dojo.mixin(wm.Component, { 423 //======================================================= 424 // Component registry 425 //======================================================= 426 /** @lends wm.Component */ 427 byId: {}, 428 add: function(inComponent){ 429 wm.Component.byId[inComponent.getRuntimeId()] = inComponent; 430 }, 431 remove: function(inComponent){ 432 delete wm.Component.byId[inComponent.getRuntimeId()]; 433 }, 434 property: { 435 } 436 }); 437 438 //window.$$ = wm.Component.byId; 439