29-Apr-05 (Created: 29-Apr-05) | More in 'laszlo'

laszlo: Source code of basetree.lzx

<library>

    <include href="base/treeselector.lzx" />
    <include href="base/basecomponent.lzx" />

    <!--==========-->
    <!-- BASETREE -->
    <!--==========-->
    <!--- An abstract base class to build tree controls. -->
    <class name="basetree" extends="basecomponent" focusable="false">

        <attribute name="defaultplacement" value="children" type="string"/>

        <!--- Check to see if this tree is open. Default is false. -->
        <attribute name="open" value="false" type="boolean" 
                   setter="_setOpen(open)"/>

        <!--- Flag to close other siblings when this tree is open. Default 
              is false. -->
        <attribute name="closesiblings" value="false" type="boolean" />

        <!--- Close all immediate children when this tree is closed. Default is
              false.-->
        <attribute name="closechildren" value="false" type="boolean" />

        <!--- Auto scroll if tree is clipped. Default is false. -->
        <attribute name="autoscroll" value="false" type="boolean" />

        <!--- Check to see if this tree is selected. Default is false, except
              for the root of a tree, which its selected attribute is set to
              true. -->
        <attribute name="selected" value="false" type="boolean"
                   setter="_setSelected(selected)" />

        <!--- Spacing to indent trees on the x-axis. Default is 10. -->
        <attribute name="xindent" value="10" type="number" />

        <!--- Spacing to indent trees on the y-axis. Default is 20. -->
        <attribute name="yindent" value="20" type="number" />

        <!--- Meaningful only with data replication. If true, it will
              recursively follow the datapath's children. Default is true. 
              @keywords final -->
        <attribute name="recurse" value="true" type="boolean" />

        <!--- Meaningful only in root tree. If false, the root item is invisible
              and its children are displayed. Default is true.
              @keywords final -->
        <attribute name="showroot" value="true" type="boolean" />

        <!--- Meaningful only in root. Whether to multiselect items. Default is
              false.
              @keywords final -->
        <attribute name="multiselect" value="false" type="boolean" />

        <!--- Meaningful only in root. Flag to toggle selected nodes. Default is
              false. 
              @keywords final -->
        <attribute name="toggleselected" value="false" type="boolean" />

        <!--- Meaningful only in root. Flag to select a tree on focus. Default
              is false.
              @keywords final -->
        <attribute name="focusselect" value="false" type="boolean" />

        <!--- If true, this tree is focused. Default is false.
              @keywords readonly -->
        <attribute name="focused" value="false" type="boolean" />

        <!--- Meaningful only in root. If focusselect is false and focusoverlay
              is true, then focus has a visual bracket overlay over the focused
              tree. Default is false. -->
        <attribute name="focusoverlay" value="false" type="boolean" />

        <!--- Layout for children. Default is "class: simplelayout; axis: y;
              spacing: 0". -->
        <attribute name="layout" value="class: simplelayout; axis: y; spacing: 0" />
        
        <!--- If true, this basetree is a leaf node. Default is false.-->
        <attribute name="isleaf" value="false" type="boolean" 
                   setter="_setIsLeaf(isleaf)" />

        <!--- Child subview number selected. Default to the first one. This will
              get set when key press moves up and down. 
              @keywords private -->
        <attribute name="_currentChild" value="0" type="number" />

        <!--- The last focused tree. Only available in the root.
              @keywords private -->
        <attribute name="_lastfocused" value="null" type="boolean" />

        <!--- The selection manager. Only available in the root. 
              @keywords private -->
        <attribute name="_selector" value="null" type="expression" />

        <!--- This event gets triggered whenever this tree is open. The open
              value of this tree is also sent. -->
        <attribute name="onopen" value="null" />
        <!--- This event gets triggered whenever this tree is selected. The
              value of the selection (true or false) is sent with this
              event.
              Note the args of this has changed from the previous release. -->
        <attribute name="onselected" value="null" />
        <!--- This event gets triggered whenever this tree is selected. This
              tree is sent with the event. The tree root also receives this
              event. -->
        <attribute name="onselect" value="null" />

        <!--- This event gets triggered whenever this tree is focused. The value
              of the focus (true or false) is sent with this event. -->
        <attribute name="onfocused" value="null" />
        <!--- This event gets triggered whenever the tree's focus is
              changed. This tree is sent with the event. The tree root also
              receives this event.-->
        <attribute name="onfocus" value="null" />

        <!--- @keywords private : reference to the defaultplacement view
              by default this is 'children' but it could be modified by
              subclass -->
        <attribute name="_children" value="null"/>
        <!--- @keywords private -->
        <method name="init">
        <![CDATA[
            if (this.datapath) {
                createChildTrees();
            }

            super.init();

            if ( this._children == null ) {
                this._children = this.searchSubnodes( "name" , this.defaultplacement );
            }

            if (this.isRoot()) {

                var focusItem = this.item;
                if ( ! this.showroot ) {
                    this.item.destroy();
                    this.item = null;
                    this.setAttribute("open", true);
                    this.children.setAttribute("x", 0);
                    this.children.setAttribute("y", 0);
                    var sv = this.children.subviews;
                    if (sv && sv[0].instanceOf(basetree)) {
                        focusItem = sv[0].item;
                    }
                }

                this._selector = new treeselector(this,
                    { multiselect: this.multiselect,
                      toggle: this.toggleselected });

                // Call this only after selector is created
                this.changeFocus(focusItem.parent);
            }

            // Make sure selector knows about me being selected
            if (this.selected) this._setSelected(true);


        ]]>
        </method>

        <!--- Method to recurse and create subtrees when replicating data. 
              @keywords private -->
        <method name="createChildTrees">
        <![CDATA[
            var count = this.datapath.getNodeCount();

            // Since text nodes 'count', skip if has text and only one
            if (this.datapath.getNodeText() != "" && count == 1 || count == 0) {
                return;
            }

            // Don't recurse if we have children.
            if (this.children.subviews) {
                this.recurse = false;
            }

            if (! this.recurse) return;

            // Replication manager overrides clone's _instanceAttrs, so we have
            // to redo them here.
            var args = {};
            for (var a in this._instanceAttrs) {
                if (a == 'id') continue;
                if (a == 'showroot') continue; // skip for non-root trees
                args[a] = this._instanceAttrs[a]
            }

            // Check to see if we have a datapath. Most likely, the clone won't
            // have a datapath, so use clone manager's datapath.
            if (this.datapath['xpath'] != null) {
                args.datapath = this.datapath.xpath;
            } else if (this.clonemanager.xpath != null) {
                args.datapath = this.clonemanager.xpath;
            } else {
                // couldn't find xpath for recursion
                return;
            }

            var c = this.getChildClass();
            if (c != null) {
                new global[c](this, args, null, true);
            }
        ]]>
        </method>

        <!--- Setter for open attribute. Leaf nodes are always closed.
              @param boolean o: if true, this tree is open
              @keywords private -->
        <method name="_setOpen" args="o">
        <![CDATA[
            if (_initcomplete && this.isleaf) {
                this.open = false;
                return;
            }

            if (this.closesiblings && ! this.isRoot()) {
                var siblings = parent.children.subviews;
                if (siblings['length'] != null) {
                    for (var i=0; i < siblings.length; i++) {
                        // .open may not have been created the first
                        // time through this loop
                        if (siblings[i]['open'] && siblings[i] != this) {
                            siblings[i].setAttribute("open", false);
                        }
                    }
                }
            }

            // Do this because datapaths only evaluate to strings
            if (!_initcomplete && typeof(o) == "string") {
                o = (o == "true" );
            } else if (o == null) {
                o = false;
            }

            this.open = o;
            if (!_initcomplete) return;

            if ( ! this.isRoot() ) {

                // Close other siblings.
                if (this.closesiblings) {
                    var siblings = parent._children.subviews;
                    for (var i=0; i < siblings.length; i++) {
                        if (siblings[i].open && siblings[i] != this) {
                            // don't want to re-enter this routine
                            siblings[i].open = false;
                            siblings[i].openChildren(false);
                            if (siblings[i].onopen) {
                                siblings[i].onopen.sendEvent(false);
                            }
                        }
                    }
                }
            }

            var sv = this._children.subviews;

            if (this.closechildren && sv) {
                for (var i=0; i < sv.length; i++) {
                    if (sv[i].open) {
                        sv[i].setAttribute("open", false);
                    }
                }
            }

            openChildren(o);
            if (this.onopen) this.onopen.sendEvent(o);
        ]]>
        </method>

        <!--- Calls selector to select this tree.
              @param boolean s: whether or not this tree is selected
              @keywords private -->
        <method name="_setSelected" args="s">
        <![CDATA[

            // Add tree to selector
            if (_initcomplete) {
                var r = this.getRoot();
                if (s) {
                    r._selector.select(this);
                } else {
                    r._selector.unselect(this);
                }
            } else {
                // Do this because datapaths only evaluate to strings
                if (typeof(s) == "string") {
                    s = (s == "true" );
                }
                this.setSelected(s);
            }
        ]]>
        </method>

        <!--- Setter for isleaf attribute. 
              @param boolean leaf: if true, this tree is a leaf.
              @keywords private -->
        <method name="_setIsLeaf" args="leaf">
            // do this because datapaths only evaluate to strings
            if (typeof(leaf) == "string") {
                leaf = (leaf == "true" );
            }
            this.isleaf = leaf;
        </method>

        <!--- Returns class to use for instantiating replicated tree children.
              If tree is leaf, return null, since we don't care to instantiate
              any more subtrees. Override this method to instantiate different
              classes. -->
        <method name="getChildClass">
            if (this.isleaf) return null;
            return this.classname;
        </method>

        <!--- Check to see if this is the root of the tree.
              @return Boolean: true if this tree is the root, otherwise
              false. -->
        <method name="isRoot">
            return ! parent.instanceOf(basetree);
        </method>

        <!--- Get the root of this tree. 
              @return basetree: the root of this tree. -->
        <method name="getRoot">
            var v = this;
            var p = v.parent;
            while (p.instanceOf(basetree)) {
                v = v.parent;
                p = v.parent;
            }
            return v;
        </method>

        <!--- Called when tree is selected using keyboard. Default
              action is to select the tree. -->
        <method name="keySelect">
            this.setAttribute("selected", true);
        </method>

        <!--- Get current tree selection.  
              @return Object: if multiselect is true, an array of basetrees,
              else the selected basetree. If none selected, returns null. -->
        <method name="getSelection">
            var root = this.getRoot() ;
            var selection = root._selector.getSelection();
            if (root._selector.multiselect) {
                return selection;
            } else if (selection.length == 0) {
               return null;
            } else {
               return selection[0];
            }
        </method>

        <!--- Called by selectionmanager when this is selected or
              unselected.
              @param Boolean s: whether tree is selected or not.
              @keywords private -->
        <method name="setSelected" args="s">
        <![CDATA[
            this.selected = s;
            var root = this.getRoot();
            if (root.onselect) root.onselect.sendEvent(this);
            if (this != root && this.onselect) this.onselect.sendEvent(this);
            if (this.onselected) this.onselected.sendEvent(s);
        ]]>           
        </method>

        <!--- Change the focus to new tree and unfocus the previous focused
              tree. If the focusselect for the tree is true, this method will also
              select the focused tree. 
              @param Basetree focusedTree: the tree to focus. If null, the
              current tree is focused. -->
        <method name="changeFocus" args="focusedTree">
        <![CDATA[
            if (focusedTree == null) focusedTree = this;

            var ftRoot = focusedTree.getRoot();

            // Remove last focused item's focus
            if (ftRoot._lastfocused) {
                ftRoot._lastfocused.setTreeFocus(false,ftRoot);                
            }

            // Set correct _currentChild settings.
            if (focusedTree != ftRoot) {
                var index = focusedTree.parent.getChildIndex(focusedTree);
                if (index != -1) {
                    focusedTree.parent.setAttribute("_currentChild", index);
                }
            }

            // See lastfocus to new focused tree
            focusedTree.setTreeFocus(true,ftRoot);

            // If focusselect, don't use focusoverlay.
            var useFocusOverlay = ftRoot.focusoverlay;
            if (ftRoot.focusselect) {
                useFocusOverlay = false;
            }
            LzFocus.setFocus(focusedTree.item, useFocusOverlay);
            ftRoot.setAttribute("_lastfocused", focusedTree);
            if (ftRoot.focusselect) focusedTree.setAttribute("selected", true);
            if (ftRoot.autoscroll) focusedTree.doAutoScroll(ftRoot);
        ]]>
        </method>

        <!--- Autoscroll if this tree is outside of scroll view.
              @param Basetree root: the root of this tree. 
              @keywords private -->
        <method name="doAutoScroll" args="root">
        <![CDATA[
            if (root.height > root.parent.height) {
                var relY = this.getAttributeRelative('y', root);
                if (relY < 0) {
                    root.setAttribute('y', root.y - relY);
                    return;
                }

                var delta = root.parent.height - relY - this.item.height;
                if (delta < 0) {
                    root.setAttribute('y', root.y + delta);
                }
            }
        ]]>
        </method>


        <!--- Set the focus of this tree. This will not onfocus the last focused
              tree. Use changeFocus() for to do that.
              @param Boolean focus: true you want the tree focused, else false.
              @keywords private
              -->
        <method name="setTreeFocus" args="focus,root">
        <![CDATA[
            this.item.setAttribute("focusable", focus);
            this.setAttribute("focused", focus);
            if (root == null) root = this.getRoot();
            if (root.onfocus) root.onfocus.sendEvent(this);
            if (this != root && this.onfocus) this.onfocus.sendEvent(this);
            //if (this.onfocused) this.onfocused.sendEvent(focus);
        ]]>
        </method>

        <!--- Get the child index of the child passed in.
              @param LzView child: a child view of the current tree.
              @return Number: the child index of the view. If not a child,
              returns -1.  -->
        <method name="getChildIndex" args="child">
        <![CDATA[
            if (children.subviews != null) {
                for (var i=0; i < children.subviews.length; i++) {
                    if (children.subviews[i] == child) {
                        return i;
                    }
                }
            }
            return -1;
        ]]>
        </method>

        <!--- Keyboard focus on parent. If no parent exists, keep focus on
              current tree. 
              @keywords private -->
        <method name="_focusParent">
        <![CDATA[
            // Make sure there's a parent to select.
            if (this.isRoot() || parent.item == null) return;

            this.setAttribute("_currentChild", 0);
            this.changeFocus(parent);
        ]]>
        </method>

        <!--- Keyboard focus on first child. If none exists, keep focus on
              current tree.
              @keywords private -->
        <method name="_focusFirstChild">
        <![CDATA[
            var n = 0;
            if (children.subviews && 
                children.subviews[n].instanceOf(basetree)) {
                this.setAttribute("_currentChild", n);
                this.changeFocus(children.subviews[n]);
            }
        ]]>
        </method>

        <!--- Keyboard focus on last child. If none exist, keep focus on current
              tree.
              @keywords private -->
        <method name="_focusLastChild">
        <![CDATA[
            var n = children.subviews.length - 1;
            if (children.subviews &&
                children.subviews[n].instanceOf(basetree)) {
                var last = children.subviews[n];
                if (last.open && last.children.subviews) {
                    var next = last.children.subviews.length -1;
                    if (last.children.subviews[next].instanceOf(basetree)) {
                        last._focusLastChild();
                        return;
                    }
                }                    
                this.setAttribute("_currentChild", n);
                this.changeFocus(children.subviews[n]);
            }
        ]]>
        </method>

        <!--- Keyboard focus on previous sibling. If we're the first sibling,
              calls _focusParent(). If none exists, keep focus on current
              tree.
              @keywords private -->
        <method name="_focusPreviousSibling">
        <![CDATA[
            // Make sure we're not root
            if (this.isRoot()) return;

            // if we're the first sibling, previous goes to parent
            if (parent._currentChild == 0) {
                this. _focusParent();
                return;
            }

            var prev = parent._currentChild - 1;
            parent.setAttribute("_currentChild", prev);

            // If previous sibling is open, select last child of that sibling
            var sibling = parent.children.subviews[prev]
            if (sibling.open && sibling.children.subviews && 
                sibling.children.subviews[0].instanceOf(basetree)) {
                sibling._focusLastChild();
            } else {
                this.changeFocus(sibling);
            }
        ]]>
        </method>

        <!--- Keyboard focus on next sibling. If we're not root, focus on
              parent's next sibling, else keep focus on current tree.
              @keywords private -->
        <method name="_focusNextSibling">
        <![CDATA[
            // Make sure we're not root
            if (this.isRoot()) return;

            var next = parent._currentChild + 1;
            if (next < parent.children.subviews.length) {
                parent.setAttribute("_currentChild", next);
                this.changeFocus(parent.children.subviews[next]);
            } else if (! this.isRoot()) {
                parent._focusNextSibling();
            }
        ]]>
        </method>

        <!--- @keyword private
              Map keyboard navigation for tree. 
              Space (32): call keySelect().
              Left (37): close tree if open, else focus on parent.
              Up (38): focus on previous sibling.
              Right (39): open tree if closed, else focus on first child. 
              Down (40): focus on first child if open, else focus next sibling.
              -->
        <method name="keyboardNavigate" args="kc">
        <![CDATA[
            if (kc == 32) { // space
                this.keySelect();
            } else if (kc == 37) { // left
                if (this.open) {
                    this.setAttribute("open", false);
                } else {
                    this._focusParent();
                }
            } else if (kc == 38) { // up
                this._focusPreviousSibling();
            } else if (kc == 39) { // right
                if (! this.open) {
                    this.setAttribute("open", true);
                } else {
                    this._focusFirstChild();
                }
            } else if (kc == 40) { // down
                if (this.open && 
                    this.children.subviews &&
                    this.children.subviews[0].instanceOf(basetree)) {
                    this._focusFirstChild();
                } else {
                    this._focusNextSibling();
                }
            }
        ]]>
        </method>

        <!-- View to place basetree node item. This is where the visual
             component of the tree goes. -->
        <view name="item">
            <!--- @keywords private -->
            <method event="onkeydown" args="kc">
                classroot.keyboardNavigate(kc);
            </method>
        </view>

        <method name="openChildren" args="o"> <![CDATA[
            var makevisible = ( o && children.subviews != null );
            children.setVisible(makevisible);
            ]]>
        </method>

        <!-- View to place child trees. -->
        <view name="children" x="${parent.xindent}" y="${parent.yindent}">
            <method name="init"> <![CDATA[
                setVisible(classroot.open && this.subviews != null );
                super.init();
                ]]>
            </method>
            <method event="onaddsubview">
            <![CDATA[
                // If this is the first one added, send event.
                if (this.subviews.length == 1 && classroot.open) {
                    setAttribute("visible", true);
                }
            ]]>
            </method>

        </view>

    </class> <!-- basetree -->

</library>
<!--                                                                       -->
<!-- Copyright 2002-2004 Laszlo Systems, Inc.  All Rights Reserved.        -->
<!-- Unauthorized use, duplication or distribution is strictly prohibited. -->
<!-- This software is the proprietary information of Laszlo Systems, Inc.  -->
<!-- Use is subject to license terms.                                      -->
<!--                                                                       -->
<!--                                                                       -->
<!-- Laszlo Presentation Server version 2.2.1 lps-2.2.1-001185-0001                     -->
<!--                                                                       -->