      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