haxe/ui/containers/Collapsible.hx
package haxe.ui.containers;
import haxe.ui.animation.AnimationBuilder;
import haxe.ui.behaviours.DataBehaviour;
import haxe.ui.components.Image;
import haxe.ui.components.Label;
import haxe.ui.containers.Box;
import haxe.ui.containers.VBox;
import haxe.ui.core.Component;
import haxe.ui.core.CompositeBuilder;
import haxe.ui.core.InteractiveComponent;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.layouts.HorizontalLayout;
import haxe.ui.layouts.VerticalLayout;
@:composite(Events, CollapsibleBuilder, CollapsibleLayout)
class Collapsible extends Box {
@:clonable @:behaviour(TextBehaviour) public var text:String;
@:clonable @:behaviour(CollapsedBehaviour, true) public var collapsed:Bool;
}
//***********************************************************************************************************
// Behaviours
//***********************************************************************************************************
@:dox(hide) @:noCompletion
private class TextBehaviour extends DataBehaviour {
private override function validateData() {
var label = _component.findComponent("collapsible-label", Label);
if (label != null) {
label.text = _value;
}
}
}
@:dox(hide) @:noCompletion
private class CollapsedBehaviour extends DataBehaviour {
private override function validateData() {
var header = _component.findComponent("collapsible-header");
var icon = _component.findComponent("collapsible-icon");
var label = _component.findComponent("collapsible-label");
if (_value == true) {
header.swapClass("collapsed", "expanded");
icon.swapClass("collapsed", "expanded");
label.swapClass("collapsed", "expanded");
} else {
header.swapClass("expanded", "collapsed");
icon.swapClass("expanded", "collapsed");
label.swapClass("expanded", "collapsed");
}
var content = _component.findComponent("collapsible-content", Component);
if (content != null) {
// TODO: think about moving this to css animations... yuk.
if (_component.animatable) {
if (_value) {
//content.opacity = 0;
var cy = content.height;
var autoHeight = content.autoHeight;
var animation = new AnimationBuilder(content, .3, "ease");
animation.setPosition(0, "height", cy, true);
animation.setPosition(100, "height", 0, true);
/*
animation.setPosition(0, "opacity", 1, true);
animation.setPosition(100, "opacity", 0, true);
*/
animation.onComplete = function() {
if (autoHeight) {
@:privateAccess content._height = null;
}
content.hidden = _value;
//_component.dispatch(new UIEvent(UIEvent.CHANGE));
}
animation.play();
} else {
content.hidden = _value;
//content.opacity = 0;
var cy = content.height;
var autoHeight = content.autoHeight;
var animation = new AnimationBuilder(content, .3, "ease");
animation.setPosition(0, "height", 0, true);
animation.setPosition(100, "height", cy, true);
/*
animation.setPosition(0, "opacity", 0, true);
animation.setPosition(100, "opacity", 1, true);
*/
animation.onComplete = function() {
if (autoHeight) {
@:privateAccess content._height = null;
}
//_component.dispatch(new UIEvent(UIEvent.CHANGE));
}
animation.play();
}
} else {
content.hidden = _value;
//_component.dispatch(new UIEvent(UIEvent.CHANGE));
}
}
}
}
//***********************************************************************************************************
// Events
//***********************************************************************************************************
private class Events extends haxe.ui.events.Events {
private var _collapsible:Collapsible;
private function new(collapsible:Collapsible) {
super(collapsible);
_collapsible = collapsible;
}
public override function register() {
var header = _collapsible.findComponent("collapsible-header");
if (header != null && header.hasEvent(MouseEvent.CLICK, onHeaderClicked) == false) {
header.registerEvent(MouseEvent.CLICK, onHeaderClicked);
}
}
public override function unregister() {
var header = _collapsible.findComponent("collapsible-header");
if (header != null) {
header.unregisterEvent(MouseEvent.CLICK, onHeaderClicked);
}
}
private function onHeaderClicked(event:MouseEvent) {
var interactives = _collapsible.findComponentsUnderPoint(event.screenX, event.screenY, InteractiveComponent);
if (interactives.length != 0) {
return;
}
_collapsible.collapsed = !_collapsible.collapsed;
}
}
//***********************************************************************************************************
// Composite Builder
//***********************************************************************************************************
@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
class CollapsibleBuilder extends CompositeBuilder {
private var _collapsible:Collapsible;
private var _header:HBox;
private var _content:VBox;
private var _originalAnimatable:Bool = false;
private function new(collapsible:Collapsible) {
super(collapsible);
_collapsible = collapsible;
// we'll start off as non-animatable so things dont animate at the start of the component creation
_originalAnimatable = _collapsible.animatable;
_collapsible.animatable = false;
_component.recursivePointerEvents = false;
_header = new HBox();
_header.percentWidth = 100;
_header.id = "collapsible-header";
_header.addClass("collapsible-header");
_header.recursivePointerEvents = false;
var icon = new Image();
icon.addClass("collapsible-icon");
icon.id = "collapsible-icon";
_header.addComponent(icon);
var label = new Label();
label.addClass("collapsible-label");
label.id = "collapsible-label";
label.text = "Collapsible";
_header.addComponent(label);
_collapsible.addComponent(_header);
_content = new VBox();
_content.addClass("collapsible-content");
_content.id = "collapsible-content";
_content.scriptAccess = false;
_content.hide();
_collapsible.addComponent(_content);
_collapsible.registerInternalEvents(true);
}
public override function onReady() {
super.onReady();
_collapsible.animatable = _originalAnimatable;
}
public override function addComponent(child:Component):Component {
if ((child is Header)) {
child.horizontalAlign = "right";
return _header.addComponent(child);
}
if (child != _header && child != _content) {
return _content.addComponent(child);
}
return null;
}
public override function removeComponent(child:Component, dispose:Bool = true, invalidate:Bool = true):Component {
if ((child is Header)) {
return _header.removeComponent(child, dispose, invalidate);
}
if (child != _header && child != _content) {
return _content.removeComponent(child, dispose, invalidate);
}
return null;
}
public function calculateDepth():Int {
var depth = 0;
var parent = _collapsible.parentComponent;
// TODO: better way to do this??
while (parent != null) {
if (parent.hasClass("collapsible-content") && parent.parentComponent != null && (parent.parentComponent is Collapsible)) {
depth++;
}
parent = parent.parentComponent;
}
return depth;
}
}
@:dox(hide) @:noCompletion
@:access(haxe.ui.core.Component)
class CollapsibleLayout extends VerticalLayout {
private override function repositionChildren() {
super.repositionChildren();
var depth = 0;
var parent = component.parentComponent;
// TODO: better way to do this??
while (parent != null) {
if (parent.hasClass("collapsible-content") && parent.parentComponent != null && parent.parentComponent is Collapsible) {
depth++;
}
parent = parent.parentComponent;
}
var builder = cast(component._compositeBuilder, CollapsibleBuilder);
var depth = builder.calculateDepth();
var header = findComponent("collapsible-header");
var offset = 0;
if (depth == 0) {
offset = 5;
}
header.paddingLeft = offset + (depth * calcIndentSize());
var content = findComponent("collapsible-content", false);
applyIndent(content, depth);
}
public function calcIndentSize():Float {
return 16;
}
private function applyIndent(content:Component, depth:Int) {
if (depth == 0) {
return;
}
for (c in content.childComponents) {
if (c is Collapsible) {
continue;
}
c.marginLeft = (depth + 1) * 16;
}
}
}