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