ares-ide
Version:
A browser-based code editor and UI designer for Enyo 2 projects
351 lines (347 loc) • 11.5 kB
JavaScript
/**
_enyo.UiComponent_ implements a container strategy suitable for presentation
layers.
UiComponent itself is abstract. Concrete subkinds include
<a href="#enyo.Control">enyo.Control</a> (for HTML/DOM) and
<a href="#enyo.canvas.Control">enyo.canvas.Control</a>
(for Canvas contexts).
*/
enyo.kind({
name: "enyo.UiComponent",
kind: "enyo.Component",
published: {
//* The UiComponent that physically contains this component in the DOM
container: null,
/**
The UiComponent that owns this component for purposes of event
propagation
*/
parent: null,
/**
The UiComponent that will physically contain new items added by
calls to _createComponent_
*/
controlParentName: "client",
//* A kind used to manage the size and placement of child components
layoutKind: ""
},
handlers: {
onresize: "resizeHandler"
},
/**
When set, provides a control reference used to indicate where a
newly-created component should be added in the UiComponent's array of
children. This is typically used when dynamically creating children
(rather than at design time). If set to null, the new control will be
added at the beginning of the array; if set to a specific existing
control, the new control will be added before the specified control. If
left undefined, the default behavior is to add the new control at the
end of the array.
*/
addBefore: undefined,
//* @protected
protectedStatics: {
_resizeFlags: {showingOnly: true} // don't waterfall these events into hidden controls
},
// TODO-POST-2.3
// we will go ahead and do all of this but warn that it is deprecated
controllerChanged: function (p) {
var c = this.controller;
if (c) {
if (enyo.isString(c)) {
this.warn(
"the `controller` properties special handling has been deprecated, please use " +
"bindings to help resolve paths as this feature will be removed"
);
c = this.controller = enyo.getPath.call(c[0] == "."? this: enyo.global, c);
}
}
},
// END-TODO-POST-2.3
create: enyo.inherit(function (sup) {
return function() {
this.controls = this.controls || [];
this.children = this.children || [];
this.containerChanged();
sup.apply(this, arguments);
this.layoutKindChanged();
// TODO-POST-2.3
if (this.controller) {
this.notifyObservers("controller", null, this.controller);
}
// END-TODO-POST-2.3
};
}),
destroy: enyo.inherit(function (sup) {
return function() {
// Destroys all non-chrome controls (regardless of owner).
this.destroyClientControls();
// Removes us from our container.
this.setContainer(null);
// Destroys chrome controls owned by this.
sup.apply(this, arguments);
};
}),
importProps: enyo.inherit(function (sup) {
return function(inProps) {
sup.apply(this, arguments);
if (!this.owner) {
this.owner = enyo.master;
}
};
}),
// As implemented, _controlParentName_ only works to identify an owned
// control created via _createComponents_ (i.e., usually in our _components_
// block). To attach a _controlParent_ via other means, one must call
// _discoverControlParent_ or set _controlParent_ directly.
//
// We could call _discoverControlParent_ in _addComponent_, but it would
// cause a lot of useless checking.
createComponents: enyo.inherit(function (sup) {
return function() {
var results = sup.apply(this, arguments);
this.discoverControlParent();
return results;
};
}),
discoverControlParent: function() {
this.controlParent = this.$[this.controlParentName] || this.controlParent;
},
adjustComponentProps: enyo.inherit(function (sup) {
return function(inProps) {
// Components we create have us as a container by default.
inProps.container = inProps.container || this;
sup.apply(this, arguments);
};
}),
// containment
containerChanged: function(inOldContainer) {
if (inOldContainer) {
inOldContainer.removeControl(this);
}
if (this.container) {
this.container.addControl(this, this.addBefore);
}
},
// parentage
parentChanged: function(inOldParent) {
if (inOldParent && inOldParent != this.parent) {
inOldParent.removeChild(this);
}
},
//* @public
// Note: Oddly, a Control is considered a descendant of itself.
isDescendantOf: function(inAncestor) {
var p = this;
while (p && p!=inAncestor) {
p = p.parent;
}
return inAncestor && (p == inAncestor);
},
/**
Returns all controls.
*/
getControls: function() {
return this.controls;
},
/**
Returns all non-chrome controls.
*/
getClientControls: function() {
var results = [];
for (var i=0, cs=this.controls, c; (c=cs[i]); i++) {
if (!c.isChrome) {
results.push(c);
}
}
return results;
},
/**
Destroys "client controls", the same set of controls returned by
_getClientControls_.
*/
destroyClientControls: function() {
var c$ = this.getClientControls();
for (var i=0, c; (c=c$[i]); i++) {
c.destroy();
}
},
//* @protected
addControl: function(inControl, inBefore) {
// Called to add an already created control to the object's control list. It is
// not used to create controls and should likely not be called directly.
// It can be overridden to detect when controls are added.
if (inBefore !== undefined) {
var idx = (inBefore === null) ? 0 : this.indexOfControl(inBefore);
this.controls.splice(idx, 0, inControl);
} else {
this.controls.push(inControl);
}
// When we add a Control, we also establish a parent.
this.addChild(inControl, inBefore);
},
removeControl: function(inControl) {
// Called to remove a control from the object's control list. As with addControl it
// can be overridden to detect when controls are removed.
// When we remove a Control, we also remove it from its parent.
inControl.setParent(null);
return enyo.remove(inControl, this.controls);
},
indexOfControl: function(inControl) {
return enyo.indexOf(inControl, this.controls);
},
indexOfClientControl: function(inControl) {
return enyo.indexOf(inControl, this.getClientControls());
},
indexInContainer: function() {
return this.container.indexOfControl(this);
},
clientIndexInContainer: function() {
return this.container.indexOfClientControl(this);
},
controlAtIndex: function(inIndex) {
return this.controls[inIndex];
},
// children
addChild: function(inChild, inBefore) {
// if inBefore is undefined, add to the end of the child list.
// If it's null, add to front of list, otherwise add before the
// specified control.
//
// allow delegating the child to a different container
if (this.controlParent /*&& !inChild.isChrome*/) {
// this.controlParent might have a controlParent, and so on; seek the ultimate parent
this.controlParent.addChild(inChild, inBefore);
} else {
// NOTE: addChild drives setParent.
// It's the opposite for setContainer, where containerChanged (in Containable)
// drives addControl.
// Because of the way 'parent' is derived from 'container', this difference is
// helpful for implementing controlParent.
// By the same token, since 'parent' is derived from 'container', setParent is
// not intended to be called by client code. Therefore, the lack of parallelism
// should be private to this implementation.
// Set the child's parent property to this
inChild.setParent(this);
// track in children array
if (inBefore !== undefined) {
var idx = (inBefore === null) ? 0 : this.indexOfChild(inBefore);
this.children.splice(idx, 0, inChild);
} else {
this.children.push(inChild);
}
}
},
removeChild: function(inChild) {
return enyo.remove(inChild, this.children);
},
indexOfChild: function(inChild) {
return enyo.indexOf(inChild, this.children);
},
layoutKindChanged: function() {
if (this.layout) {
this.layout.destroy();
}
this.layout = enyo.createFromKind(this.layoutKind, this);
if (this.generated) {
this.render();
}
},
flow: function() {
if (this.layout) {
this.layout.flow();
}
},
// CAVEAT: currently we use the entry point for both
// post-render layout work *and* post-resize layout work.
reflow: function() {
if (this.layout) {
this.layout.reflow();
}
},
/**
Call after this control has been resized to allow it to process the size change.
To respond to a resize, override _resizeHandler_ instead.
*/
// syntactic sugar for 'waterfall("onresize")'
resized: function() {
this.waterfall("onresize", enyo.UiComponent._resizeFlags);
this.waterfall("onpostresize", enyo.UiComponent._resizeFlags);
},
//* @protected
resizeHandler: function() {
// FIXME: once we are in the business of reflowing layouts on resize, then we have an
// inside/outside problem: some scenarios will need to reflow before child
// controls reflow, and some will need to reflow after. Even more complex scenarios
// have circular dependencies, and can require multiple passes or other resolution.
// When we can rely on CSS to manage reflows we do not have these problems.
this.reflow();
},
/**
Sends a message to all my descendents.
*/
waterfallDown: function(name, event, sender) {
event = event || {};
// Note: Controls will generally be both in a $ hash and a child list somewhere.
// Attempt to avoid duplicated messages by sending only to components that are not
// UiComponent, as those components are guaranteed not to be in a child list.
// May cause a problem if there is a scenario where a UiComponent owns a pure
// Component that in turn owns Controls.
//
// waterfall to all pure components
for (var n in this.$) {
if (!(this.$[n] instanceof enyo.UiComponent)) {
this.$[n].waterfall(name, event, sender);
}
}
// waterfall to my children
for (var i=0, cs=this.children, c; (c=cs[i]); i++) {
// Do not send {showingOnly: true} events to hidden controls. This flag is set for resize events
// which are broadcast from within the framework. This saves a *lot* of unnecessary layout.
// TODO: Maybe remember that we did this, and re-send those messages on setShowing(true)?
// No obvious problems with it as-is, though
if (c.showing || !(event && event.showingOnly)) {
c.waterfall(name, event, sender);
}
}
},
getBubbleTarget: function() {
return this.bubbleTarget || this.parent || this.owner;
}
});
enyo.createFromKind = function(inKind, inParam) {
var Ctor = inKind && enyo.constructorForKind(inKind);
if (Ctor) {
return new Ctor(inParam);
}
};
//
// Default owner for ownerless UiComponents to allow notifying such UiComponents
// of important system events like window resize.
//
// NOTE: Ownerless UiComponents will not be garbage collected unless explicitly
// destroyed, as they will be referenced by _enyo.master_.
//
enyo.master = new enyo.Component({
name: "master",
notInstanceOwner: true,
eventFlags: {showingOnly: true}, // don't waterfall these events into hidden controls
getId: function() {
return '';
},
isDescendantOf: enyo.nop,
bubble: function(inEventName, inEvent) {
//enyo.log("master event: " + inEventName);
if (inEventName == "onresize") {
// Resize is special; waterfall this message.
// This works because master is a Component, so it waterfalls
// to its owned Components (i.e., master has no children).
enyo.master.waterfallDown("onresize", this.eventFlags);
enyo.master.waterfallDown("onpostresize", this.eventFlags);
} else {
// All other top-level events are sent only to interested Signal
// receivers.
enyo.Signals.send(inEventName, inEvent);
}
}
});