001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.gwt.client.ui.tree;
029
030import org.opencms.gwt.client.dnd.CmsDNDHandler.Orientation;
031import org.opencms.gwt.client.dnd.I_CmsDraggable;
032import org.opencms.gwt.client.dnd.I_CmsDropTarget;
033import org.opencms.gwt.client.ui.CmsList;
034import org.opencms.gwt.client.ui.CmsListItem;
035import org.opencms.gwt.client.ui.CmsToggleButton;
036import org.opencms.gwt.client.ui.I_CmsButton;
037import org.opencms.gwt.client.ui.I_CmsButton.ButtonStyle;
038import org.opencms.gwt.client.ui.I_CmsButton.Size;
039import org.opencms.gwt.client.ui.I_CmsListItem;
040import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle;
041import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle.I_CmsListTreeCss;
042import org.opencms.gwt.client.ui.input.CmsCheckBox;
043import org.opencms.gwt.client.util.CmsDomUtil;
044import org.opencms.gwt.client.util.CmsStyleVariable;
045
046import com.google.common.base.Function;
047import com.google.gwt.animation.client.Animation;
048import com.google.gwt.dom.client.Element;
049import com.google.gwt.event.dom.client.ClickEvent;
050import com.google.gwt.event.dom.client.ClickHandler;
051import com.google.gwt.user.client.ui.Label;
052import com.google.gwt.user.client.ui.Widget;
053
054/**
055 * List tree item implementation.<p>
056 *
057 * Implemented as:
058 * <pre>
059 * &lt;li class='listTreeItem listTreeItem*state*'>
060 *   &lt;span class='listTreeItemImage'>&lt;/span>
061 *   &lt;div class='listTreeItemContent'>...*content*&lt;/div>
062 *   &lt;ul class='listTreeItemChildren'>
063 *      *children*
064 *   &lt;/ul>
065 * &lt;/li>
066 * </pre>
067 *
068 * Where state can be <code>opened</code>, <code>closed</code> or <code>leaf</code>.<p>
069 *
070 * @since 8.0.0
071 */
072public class CmsTreeItem extends CmsListItem {
073
074    /** The duration of the animations. */
075    public static final int ANIMATION_DURATION = 200;
076
077    /** The CSS bundle used for this widget. */
078    private static final I_CmsListTreeCss CSS = I_CmsLayoutBundle.INSTANCE.listTreeCss();
079
080    /** The width of the opener. */
081    private static final int OPENER_WIDTH = 16;
082
083    /** The children list. */
084    protected CmsList<CmsTreeItem> m_children;
085
086    /** The element showing the open/close icon. */
087    protected CmsToggleButton m_opener;
088
089    /** Flag to indicate if drag'n drop is enabled. 3-states: if <code>null</code> the tree decides. */
090    private Boolean m_dropEnabled;
091
092    /** The style variable controlling this tree item's leaf/non-leaf state. */
093    private CmsStyleVariable m_leafStyleVar;
094
095    /** Flag to indicate if open or closed. */
096    private boolean m_open;
097
098    /** The item parent. */
099    private CmsTreeItem m_parentItem;
100
101    /** The style variable controlling this tree item's open/closed state. */
102    private CmsStyleVariable m_styleVar;
103
104    /** The tree reference. */
105    private CmsTree<CmsTreeItem> m_tree;
106
107    /**
108     * Creates a new list tree item containing a main widget and a check box.<p>
109     *
110     * @param showOpeners if true, show open/close icons
111     * @param checkbox the check box
112     * @param mainWidget the main widget
113     */
114    public CmsTreeItem(boolean showOpeners, CmsCheckBox checkbox, Widget mainWidget) {
115
116        this(showOpeners);
117        addMainWidget(mainWidget);
118        addCheckBox(checkbox);
119        initContent();
120        if (!showOpeners) {
121            hideOpeners();
122        }
123    }
124
125    /**
126     * Creates a new list tree item containing a main widget.<p>
127     *
128     * @param showOpeners if true, show open/close icons
129     * @param mainWidget the main widget
130     */
131    public CmsTreeItem(boolean showOpeners, Widget mainWidget) {
132
133        this(showOpeners);
134        addMainWidget(mainWidget);
135        initContent();
136        if (!showOpeners) {
137            hideOpeners();
138        }
139    }
140
141    /**
142     * Creates a new tree item with a 24px wide icon.<p>
143     *
144     * @param showOpeners if <code>true</code>, show open/close icons
145     * @param mainWidget the main widget
146     * @param icon the icon style name
147     */
148    public CmsTreeItem(boolean showOpeners, Widget mainWidget, String icon) {
149
150        this(showOpeners);
151        addMainWidget(mainWidget);
152        Label label = new Label();
153        label.addStyleName(icon);
154        addDecoration(label, 28, true);
155        initContent();
156        if (!showOpeners) {
157            hideOpeners();
158        }
159    }
160
161    /**
162     * Default constructor.<p>
163     *
164     * @param showOpeners if true, the opener icons should be shown
165     */
166    protected CmsTreeItem(boolean showOpeners) {
167
168        super();
169        m_styleVar = new CmsStyleVariable(this);
170        m_leafStyleVar = new CmsStyleVariable(this);
171        m_opener = createOpener();
172        addDecoration(m_opener, showOpeners ? OPENER_WIDTH : 0, true);
173        m_children = new CmsList<CmsTreeItem>();
174        m_children.setStyleName(CSS.listTreeItemChildren());
175        m_panel.add(m_children);
176        onChangeChildren();
177        m_open = true;
178        setOpen(false);
179    }
180
181    /**
182     * Returns the last opened item of a tree fragment.<p>
183     *
184     * @param item the tree item
185     * @param stopLevel the level to stop at, set -1 to go to the very last opened item
186     * @param requiresDropEnabled <code>true</code> if it is required the returned element to be drop enabled
187     *
188     * @return the last visible item of a tree fragment
189     */
190    protected static CmsTreeItem getLastOpenedItem(CmsTreeItem item, int stopLevel, boolean requiresDropEnabled) {
191
192        if (stopLevel != -1) {
193            // stop level is set
194            int currentLevel = getPathLevel(item.getPath());
195            if (currentLevel > stopLevel) {
196                // we are past the stop level, prevent further checks
197                stopLevel = -1;
198            } else if (currentLevel == stopLevel) {
199                // matches stop level
200                return item;
201            }
202        }
203        if (item.getChildCount() > 0) {
204            int childIndex = item.getChildCount() - 1;
205            CmsTreeItem child = item.getChild(childIndex);
206            if (requiresDropEnabled) {
207                while (!child.isDropEnabled()) {
208                    childIndex--;
209                    if (childIndex < 0) {
210                        return item;
211                    }
212                    child = item.getChild(childIndex);
213                }
214            }
215
216            if (child.isOpen()) {
217                return CmsTreeItem.getLastOpenedItem(child, stopLevel, requiresDropEnabled);
218            }
219        }
220        return item;
221    }
222
223    /**
224     * Method determining the path level by counting the number of '/'.<p>
225     * Example: '/xxx/xxx/' has a path-level of 2.<p>
226     *
227     * @param path the path to test
228     *
229     * @return the path level
230     */
231    protected static native int getPathLevel(String path)/*-{
232                return path.match(/\//g).length - 1;
233    }-*/;
234
235    /**
236     * Unsupported operation.<p>
237     *
238     * @see org.opencms.gwt.client.ui.CmsListItem#add(com.google.gwt.user.client.ui.Widget)
239     */
240    @Override
241    public void add(Widget w) {
242
243        throw new UnsupportedOperationException();
244    }
245
246    /**
247     * Adds a child list item.<p>
248     *
249     * @param item the child to add
250     *
251     * @see org.opencms.gwt.client.ui.CmsList#addItem(org.opencms.gwt.client.ui.I_CmsListItem)
252     */
253    public void addChild(CmsTreeItem item) {
254
255        m_children.addItem(item);
256        adopt(item);
257    }
258
259    /**
260     * @see com.google.gwt.user.client.ui.HasWidgets#clear()
261     */
262    public void clear() {
263
264        clearChildren();
265    }
266
267    /**
268     * Removes all children.<p>
269     *
270     * @see org.opencms.gwt.client.ui.CmsList#clearList()
271     */
272    public void clearChildren() {
273
274        for (int i = getChildCount(); i > 0; i--) {
275            removeChild(i - 1);
276        }
277    }
278
279    /**
280     * Closes all empty child entries.<p>
281     */
282    public void closeAllEmptyChildren() {
283
284        for (Widget child : m_children) {
285            if (child instanceof CmsTreeItem) {
286                CmsTreeItem item = (CmsTreeItem)child;
287                if (item.isOpen()) {
288                    if (item.getChildCount() == 0) {
289                        item.setOpen(false);
290                    } else {
291                        item.closeAllEmptyChildren();
292                    }
293                }
294            }
295        }
296    }
297
298    /**
299     * Returns the child tree item at the given position.<p>
300     *
301     * @param index the position
302     *
303     * @return the tree item
304     *
305     * @see org.opencms.gwt.client.ui.CmsList#getItem(int)
306     */
307    public CmsTreeItem getChild(int index) {
308
309        return m_children.getItem(index);
310    }
311
312    /**
313     * Returns the tree item with the given id.<p>
314     *
315     * @param itemId the id of the item to retrieve
316     *
317     * @return the tree item
318     *
319     * @see org.opencms.gwt.client.ui.CmsList#getItem(String)
320     */
321    public CmsTreeItem getChild(String itemId) {
322
323        CmsTreeItem result = m_children.getItem(itemId);
324        return result;
325    }
326
327    /**
328     * Helper method which gets the number of children.<p>
329     *
330     * @return the number of children
331     *
332     * @see org.opencms.gwt.client.ui.CmsList#getWidgetCount()
333     */
334    public int getChildCount() {
335
336        return m_children.getWidgetCount();
337    }
338
339    /**
340     * Returns the children of this list item.<p>
341     *
342     * @return the children list
343     */
344    public CmsList<? extends I_CmsListItem> getChildren() {
345
346        return m_children;
347    }
348
349    /**
350     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getDragHelper(I_CmsDropTarget)
351     */
352    @Override
353    public Element getDragHelper(I_CmsDropTarget target) {
354
355        // disable animation to get a drag helper without any visible children
356        boolean isAnimated = getTree().isAnimationEnabled();
357        getTree().setAnimationEnabled(false);
358        setOpen(false);
359        getTree().setAnimationEnabled(isAnimated);
360        return super.getDragHelper(target);
361    }
362
363    /**
364     * Returns the given item position.<p>
365     *
366     * @param item the item to get the position for
367     *
368     * @return the item position
369     */
370    public int getItemPosition(CmsTreeItem item) {
371
372        return m_children.getWidgetIndex(item);
373    }
374
375    /**
376     * Returns the parent item.<p>
377     *
378     * @return the parent item
379     */
380    public CmsTreeItem getParentItem() {
381
382        return m_parentItem;
383    }
384
385    /**
386     * @see org.opencms.gwt.client.ui.CmsListItem#getParentTarget()
387     */
388    @Override
389    public I_CmsDropTarget getParentTarget() {
390
391        return getTree();
392    }
393
394    /**
395     * Returns the path of IDs for the this item.<p>
396     *
397     * @return a path of IDs separated by slash
398     */
399    public String getPath() {
400
401        StringBuffer path = new StringBuffer("/");
402        CmsTreeItem current = this;
403        while (current != null) {
404            path.insert(0, current.getId()).insert(0, "/");
405            current = current.getParentItem();
406        }
407        String result = path.toString();
408        if (result.startsWith("//")) {
409            // This happens if the root item has an empty id.
410            // In that case, we cut off the first slash.
411            result = result.substring(1);
412        }
413        return result;
414    }
415
416    /**
417     * Gets the tree to which this tree item belongs, or null if it does not belong to a tree.<p>
418     *
419     * @return a tree or <code>null</code>
420     */
421    public CmsTree<CmsTreeItem> getTree() {
422
423        return m_tree;
424    }
425
426    /**
427     * Hides the open/close icons for this tree item and its descendants.<p>
428     */
429    public void hideOpeners() {
430
431        addStyleName(CSS.listTreeItemNoOpeners());
432    }
433
434    /**
435     * Inserts the given item at the given position.<p>
436     *
437     * @param item the item to insert
438     * @param position the position
439     *
440     * @see org.opencms.gwt.client.ui.CmsList#insertItem(org.opencms.gwt.client.ui.I_CmsListItem, int)
441     */
442    public void insertChild(CmsTreeItem item, int position) {
443
444        m_children.insert(item, position);
445        adopt(item);
446    }
447
448    /**
449     * Checks if dropping is enabled.<p>
450     *
451     * @return <code>true</code> if dropping is enabled
452     */
453    public boolean isDropEnabled() {
454
455        if (m_dropEnabled != null) {
456            return m_dropEnabled.booleanValue();
457        }
458        CmsTree<?> tree = getTree();
459        if (tree == null) {
460            return false;
461        }
462        return tree.isDropEnabled();
463    }
464
465    /**
466     * Checks if the item is open or closed.<p>
467     *
468     * @return <code>true</code> if open
469     */
470    public boolean isOpen() {
471
472        return m_open;
473    }
474
475    /**
476     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#onDragCancel()
477     */
478    @Override
479    public void onDragCancel() {
480
481        CmsTreeItem parent = getParentItem();
482        if (parent != null) {
483            parent.insertChild(this, parent.getItemPosition(this));
484        }
485        super.onDragCancel();
486    }
487
488    /**
489     * Removes an item from the list.<p>
490     *
491     * @param item the item to remove
492     *
493     * @return the removed item
494     *
495     * @see org.opencms.gwt.client.ui.CmsList#removeItem(org.opencms.gwt.client.ui.I_CmsListItem)
496     */
497    public CmsTreeItem removeChild(final CmsTreeItem item) {
498
499        item.setParentItem(null);
500        item.setTree(null);
501        if ((m_tree != null) && m_tree.isAnimationEnabled()) {
502            // could be null if already detached
503            // animate
504            (new Animation() {
505
506                /**
507                 * @see com.google.gwt.animation.client.Animation#onComplete()
508                 */
509                @Override
510                protected void onComplete() {
511
512                    super.onComplete();
513                    m_children.removeItem(item);
514                    onChangeChildren();
515                }
516
517                /**
518                 * @see com.google.gwt.animation.client.Animation#onUpdate(double)
519                 */
520                @Override
521                protected void onUpdate(double progress) {
522
523                    item.getElement().getStyle().setOpacity(1 - progress);
524                }
525            }).run(ANIMATION_DURATION);
526        } else {
527            m_children.removeItem(item);
528            onChangeChildren();
529        }
530        return item;
531    }
532
533    /**
534     * Removes the item identified by the given index from the list.<p>
535     *
536     * @param index the index of the item to remove
537     *
538     * @return the removed item
539     *
540     * @see org.opencms.gwt.client.ui.CmsList#remove(int)
541     */
542    public CmsTreeItem removeChild(int index) {
543
544        return removeChild(m_children.getItem(index));
545    }
546
547    /**
548     * Removes an item from the list.<p>
549     *
550     * @param itemId the id of the item to remove
551     *
552     * @return the removed item
553     *
554     * @see org.opencms.gwt.client.ui.CmsList#removeItem(String)
555     */
556    public CmsTreeItem removeChild(String itemId) {
557
558        return removeChild(m_children.getItem(itemId));
559    }
560
561    /**
562     * Removes the opener widget.<p>
563     */
564    public void removeOpener() {
565
566        removeDecorationWidget(m_opener, OPENER_WIDTH);
567    }
568
569    /**
570     * Positions the drag and drop placeholder as a sibling or descendant of this element.<p>
571     *
572     * @param x the cursor client x position
573     * @param y the cursor client y position
574     * @param placeholder the placeholder
575     * @param orientation the drag and drop orientation
576     *
577     * @return the placeholder index
578     */
579    public int repositionPlaceholder(int x, int y, Element placeholder, Orientation orientation) {
580
581        I_CmsDraggable draggable = null;
582        if (getTree().getDnDHandler() != null) {
583            draggable = getTree().getDnDHandler().getDraggable();
584        }
585        Element itemElement = getListItemWidget().getElement();
586        // check if the mouse pointer is within the height of the element
587        int top = CmsDomUtil.getRelativeY(y, itemElement);
588        int height = itemElement.getOffsetHeight();
589        int index;
590        String parentPath;
591        boolean isParentDndEnabled;
592        CmsTreeItem parentItem = getParentItem();
593        if (parentItem == null) {
594            index = getTree().getItemPosition(this);
595            parentPath = "/";
596            isParentDndEnabled = getTree().isRootDropEnabled();
597        } else {
598            index = parentItem.getItemPosition(this);
599            parentPath = getParentItem().getPath();
600            isParentDndEnabled = getParentItem().isDropEnabled();
601        }
602
603        if (top < height) {
604            // the mouse pointer is within the widget
605            int diff = x - getListItemWidget().getAbsoluteLeft();
606            if ((draggable != this) && isDropEnabled() && (diff > 0) && (diff < 32)) {
607                // over icon
608                getTree().setOpenTimer(this);
609                m_children.getElement().insertBefore(placeholder, m_children.getElement().getFirstChild());
610                getTree().setPlaceholderPath(getPath());
611                return 0;
612            }
613            getTree().cancelOpenTimer();
614
615            // In this case try to drop on the parent
616            if (!isParentDndEnabled) {
617                // we are not allowed to drop here
618                // keeping old position
619                return getTree().getPlaceholderIndex();
620            }
621            int originalPathLevel = -1;
622            if (draggable instanceof CmsTreeItem) {
623                originalPathLevel = getPathLevel(((CmsTreeItem)draggable).getPath()) - 1;
624            }
625            if (shouldInsertIntoSiblingList(originalPathLevel, parentItem, index)) {
626                @SuppressWarnings("null")
627                CmsTreeItem previousSibling = parentItem.getChild(index - 1);
628                if (previousSibling.isOpen()) {
629                    // insert as last into the last opened of the siblings tree fragment
630                    return CmsTreeItem.getLastOpenedItem(
631                        previousSibling,
632                        originalPathLevel,
633                        true).insertPlaceholderAsLastChild(placeholder);
634                }
635            }
636            // insert place holder at the parent before the current item
637            getElement().getParentElement().insertBefore(placeholder, getElement());
638            getTree().setPlaceholderPath(parentPath);
639            return index;
640        } else if ((draggable != this) && isOpen()) {
641            getTree().cancelOpenTimer();
642            // the mouse pointer is on children
643            for (int childIndex = 0; childIndex < getChildCount(); childIndex++) {
644                CmsTreeItem child = getChild(childIndex);
645                Element childElement = child.getElement();
646
647                boolean over = false;
648                switch (orientation) {
649                    case HORIZONTAL:
650                        over = CmsDomUtil.checkPositionInside(childElement, x, -1);
651                        break;
652                    case VERTICAL:
653                        over = CmsDomUtil.checkPositionInside(childElement, -1, y);
654                        break;
655                    case ALL:
656                    default:
657                        over = CmsDomUtil.checkPositionInside(childElement, x, y);
658                }
659                if (over) {
660                    return child.repositionPlaceholder(x, y, placeholder, orientation);
661                }
662            }
663        }
664        getTree().cancelOpenTimer();
665        // keeping old position
666        return getTree().getPlaceholderIndex();
667    }
668
669    /**
670     * Enables/disables dropping.<p>
671     *
672     * @param enabled <code>true</code> to enable, or <code>false</code> to disable
673     */
674    public void setDropEnabled(boolean enabled) {
675
676        if ((m_dropEnabled != null) && (m_dropEnabled.booleanValue() == enabled)) {
677            return;
678        }
679        m_dropEnabled = Boolean.valueOf(enabled);
680    }
681
682    /**
683     * Sets the tree item style to leaf, hiding the list opener.<p>
684     *
685     * @param isLeaf <code>true</code> to set to leaf style
686     */
687    public void setLeafStyle(boolean isLeaf) {
688
689        if (isLeaf) {
690            m_leafStyleVar.setValue(CSS.listTreeItemLeaf());
691        } else {
692            m_leafStyleVar.setValue(CSS.listTreeItemInternal());
693        }
694    }
695
696    /**
697     * Opens or closes this tree item (i.e. shows or hides its descendants).<p>
698     *
699     * @param open if <code>true</code>, open the tree item, else close it
700     */
701    public void setOpen(boolean open) {
702
703        setOpen(open, true);
704    }
705
706    /**
707     * Opens or closes this tree item (i.e. shows or hides its descendants).<p>
708     *
709     * @param open if <code>true</code>, open the tree item, else close it
710     * @param fireEvents true if the open/close events should be fired
711     */
712    public void setOpen(boolean open, boolean fireEvents) {
713
714        if (m_open == open) {
715            return;
716        }
717        m_open = open;
718        executeOpen(fireEvents);
719        CmsDomUtil.resizeAncestor(getParent());
720    }
721
722    /**
723     * Sets the parent item.<p>
724     *
725     * @param parentItem the parent item to set
726     */
727    public void setParentItem(CmsTreeItem parentItem) {
728
729        m_parentItem = parentItem;
730    }
731
732    /**
733     * Sets the tree to which this tree item belongs.<p>
734     *
735     * This is automatically called when this tree item or one of its ancestors is inserted into a tree.<p>
736     *
737     * @param tree the tree into which the item has been inserted
738     */
739    public void setTree(CmsTree<CmsTreeItem> tree) {
740
741        m_tree = tree;
742        for (Widget widget : m_children) {
743            if (widget instanceof CmsTreeItem) {
744                ((CmsTreeItem)widget).setTree(tree);
745            }
746        }
747    }
748
749    /**
750     * Shows the open/close icons for this tree item and its descendants.<p>
751     */
752    public void showOpeners() {
753
754        removeStyleName(CSS.listTreeItemNoOpeners());
755    }
756
757    /**
758     * Visits all nested tree items with the given visitor function.<p>
759     *
760     * @param visitor the visitor
761     */
762    public void visit(Function<CmsTreeItem, Boolean> visitor) {
763
764        visitor.apply(this);
765        for (Widget child : m_children) {
766            ((CmsTreeItem)child).visit(visitor);
767        }
768    }
769
770    /**
771     * Adopts the given item.<p>
772     *
773     * @param item the item to adopt
774     */
775    protected void adopt(final CmsTreeItem item) {
776
777        item.setParentItem(this);
778        item.setTree(m_tree);
779        onChangeChildren();
780        if ((m_tree != null) && m_tree.isAnimationEnabled()) {
781            // could be null if not yet attached
782            item.getElement().getStyle().setOpacity(0);
783            // animate
784            (new Animation() {
785
786                /**
787                 * @see com.google.gwt.animation.client.Animation#onUpdate(double)
788                 */
789                @Override
790                protected void onUpdate(double progress) {
791
792                    item.getElement().getStyle().setOpacity(progress);
793                }
794            }).run(ANIMATION_DURATION);
795        }
796    }
797
798    /**
799     * Creates the button for opening/closing this item.<p>
800     *
801     * @return a button
802     */
803    protected CmsToggleButton createOpener() {
804
805        final CmsToggleButton opener = new CmsToggleButton();
806        opener.setButtonStyle(ButtonStyle.FONT_ICON, null);
807        opener.setSize(Size.small);
808        opener.addStyleName(CSS.listTreeItemOpener());
809        opener.setUpFace("", I_CmsButton.TREE_PLUS);
810        opener.setDownFace("", I_CmsButton.TREE_MINUS);
811        opener.addClickHandler(new ClickHandler() {
812
813            /**
814             * @see com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event.dom.client.ClickEvent)
815             */
816            public void onClick(ClickEvent e) {
817
818                setOpen(opener.isDown());
819                e.stopPropagation();
820                e.preventDefault();
821            }
822        });
823        return opener;
824    }
825
826    /**
827     * Executes the open call.<p>
828     *
829     * @param fireEvents if true, open/close events will be fired
830     */
831    protected void executeOpen(boolean fireEvents) {
832
833        m_styleVar.setValue(m_open ? CSS.listTreeItemOpen() : CSS.listTreeItemClosed());
834        setLeafStyle(false);
835        m_children.getElement().getStyle().clearDisplay();
836        if (m_opener.isDown() != m_open) {
837            m_opener.setDown(m_open);
838        }
839        if (fireEvents) {
840            if (m_open) {
841                fireOpen();
842            } else {
843                fireClose();
844            }
845        }
846        // reset the leaf style according to the child count
847        setLeafStyle(0 == getChildCount());
848    }
849
850    /**
851     * Fires the close event.<p>
852     */
853    protected void fireClose() {
854
855        if (m_tree != null) {
856            m_tree.fireClose(this);
857        }
858    }
859
860    /**
861     * Fires the open event on the tree.<p>
862     */
863    protected void fireOpen() {
864
865        if (m_tree != null) {
866            m_tree.fireOpen(this);
867        }
868    }
869
870    /**
871     * Inserts the placeholder element as last child of the children list.
872     * Setting it's path as the current placeholder path and returning the new index.<p>
873     *
874     * @param placeholder the placeholder element
875     *
876     * @return the new index
877     */
878    protected int insertPlaceholderAsLastChild(Element placeholder) {
879
880        m_children.getElement().appendChild(placeholder);
881        getTree().setPlaceholderPath(getPath());
882        return getChildCount();
883    }
884
885    /**
886     * Helper method which is called when the list of children changes.<p>
887     */
888    protected void onChangeChildren() {
889
890        setLeafStyle(0 == getChildCount());
891    }
892
893    /**
894     * Determines if the draggable should be inserted into the previous siblings children list.<p>
895     *
896     * @param originalPathLevel the original path level
897     * @param parent the parent item
898     * @param index the current index
899     *
900     * @return <code>true</code> if the item should be inserted into the previous siblings children list
901     */
902    private boolean shouldInsertIntoSiblingList(int originalPathLevel, CmsTreeItem parent, int index) {
903
904        if ((index <= 0) || (parent == null)) {
905            return false;
906        }
907        return originalPathLevel != getPathLevel(parent.getPath());
908    }
909}