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;
029
030import org.opencms.gwt.client.Messages;
031import org.opencms.gwt.client.dnd.CmsDNDHandler;
032import org.opencms.gwt.client.dnd.I_CmsDragHandle;
033import org.opencms.gwt.client.dnd.I_CmsDraggable;
034import org.opencms.gwt.client.dnd.I_CmsDropTarget;
035import org.opencms.gwt.client.ui.I_CmsButton.ButtonStyle;
036import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle;
037import org.opencms.gwt.client.ui.input.CmsCheckBox;
038import org.opencms.gwt.client.ui.input.category.CmsDataValue;
039import org.opencms.gwt.client.util.CmsDomUtil;
040
041import java.util.Collections;
042import java.util.Iterator;
043import java.util.LinkedList;
044import java.util.List;
045
046import com.google.common.base.Optional;
047import com.google.gwt.dom.client.Element;
048import com.google.gwt.dom.client.Style;
049import com.google.gwt.dom.client.Style.Position;
050import com.google.gwt.dom.client.Style.Unit;
051import com.google.gwt.dom.client.Style.Visibility;
052import com.google.gwt.user.client.DOM;
053import com.google.gwt.user.client.ui.Composite;
054import com.google.gwt.user.client.ui.RootPanel;
055import com.google.gwt.user.client.ui.Widget;
056
057/**
058 * List item which uses a float panel for layout.<p>
059 *
060 * @since 8.0.0
061 */
062public class CmsListItem extends Composite implements I_CmsListItem {
063
064    /** The move handle. */
065    public class MoveHandle extends CmsPushButton implements I_CmsDragHandle {
066
067        /** The draggable. */
068        private CmsListItem m_draggable;
069
070        /**
071         * Constructor.<p>
072         *
073         * @param draggable the draggable
074         */
075        MoveHandle(CmsListItem draggable) {
076
077            setImageClass(I_CmsButton.MOVE_SMALL);
078            setButtonStyle(ButtonStyle.FONT_ICON, null);
079            setTitle(Messages.get().key(Messages.GUI_TOOLBAR_MOVE_TO_0));
080            addStyleName(MOVE_HANDLE_MARKER_CLASS);
081            m_draggable = draggable;
082        }
083
084        /**
085         * @see org.opencms.gwt.client.dnd.I_CmsDragHandle#getDraggable()
086         */
087        public I_CmsDraggable getDraggable() {
088
089            return m_draggable;
090        }
091
092    }
093
094    /** The CSS class to mark the move handle. */
095    public static final String MOVE_HANDLE_MARKER_CLASS = "cmsMoveHandle";
096
097    /** The width of a checkbox. */
098    private static final int CHECKBOX_WIDTH = 20;
099
100    /** The checkbox of this list item, or null if there is no checkbox. */
101    protected CmsCheckBox m_checkbox;
102
103    /** The panel which contains both the decorations (checkbox, etc.) and the main widget. */
104    protected CmsSimpleDecoratedPanel m_decoratedPanel;
105
106    /** A list of decoration widgets which is used to initialize {@link CmsListItem#m_decoratedPanel}. */
107    protected LinkedList<Widget> m_decorationWidgets = new LinkedList<Widget>();
108
109    /** The decoration width which should be used to initialize {@link CmsListItem#m_decoratedPanel}. */
110    protected int m_decorationWidth;
111
112    /** The logical id, it is not the HTML id. */
113    protected String m_id;
114
115    /** The list item widget, if this widget has one. */
116    protected CmsListItemWidget m_listItemWidget;
117
118    /** The main widget of the list item. */
119    protected Widget m_mainWidget;
120
121    /** This widgets panel. */
122    protected CmsFlowPanel m_panel;
123
124    /** The drag'n drop place holder element. */
125    protected Element m_placeholder;
126
127    /** The provisional drag parent. */
128    protected Element m_provisionalParent;
129
130    /** Arbitrary data belonging to the list item. */
131    private Object m_data;
132
133    /** The class to set on the DND helper. */
134    private String m_dndHelperClass;
135
136    /** The class to set on the DND parent. */
137    private String m_dndParentClass;
138
139    /** The drag helper. */
140    private Element m_helper;
141
142    /** The move handle. */
143    private MoveHandle m_moveHandle;
144
145    /** The offset delta. */
146    private Optional<int[]> m_offsetDelta = Optional.absent();
147
148    /** Indicating this box has a reduced height. */
149    private boolean m_smallView;
150
151    /**
152     * Default constructor.<p>
153     */
154    public CmsListItem() {
155
156        m_panel = new CmsFlowPanel("li");
157        m_panel.setStyleName(I_CmsLayoutBundle.INSTANCE.listTreeCss().listTreeItem());
158        initWidget(m_panel);
159    }
160
161    /**
162     * Default constructor.<p>
163     *
164     * @param checkBox the checkbox
165     * @param widget the widget to use
166     */
167    public CmsListItem(CmsCheckBox checkBox, CmsListItemWidget widget) {
168
169        this();
170        initContent(checkBox, widget);
171    }
172
173    /**
174     * Default constructor.<p>
175     *
176     * @param widget the widget to use
177     */
178    public CmsListItem(CmsListItemWidget widget) {
179
180        this();
181        initContent(widget);
182    }
183
184    /**
185     * @see org.opencms.gwt.client.ui.I_CmsListItem#add(com.google.gwt.user.client.ui.Widget)
186     */
187    public void add(Widget w) {
188
189        throw new UnsupportedOperationException();
190    }
191
192    /**
193     * Adds a decoration widget to the list item.<p>
194     *
195     * @param widget the widget
196     * @param width the widget width
197     */
198    public void addDecorationWidget(Widget widget, int width) {
199
200        addDecoration(widget, width, false);
201        initContent();
202    }
203
204    /**
205     * Gets the checkbox of this list item.<p>
206     *
207     * This method will return a checkbox if this list item has one, or null if it doesn't.
208     *
209     * @return a check box or null
210     */
211    public CmsCheckBox getCheckBox() {
212
213        return m_checkbox;
214    }
215
216    /**
217     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getCursorOffsetDelta()
218     */
219    public Optional<int[]> getCursorOffsetDelta() {
220
221        return m_offsetDelta;
222    }
223
224    /**
225     * Gets the data belonging to the list item.<p>
226     *
227     * @return the data belonging to the list item
228     */
229    @SuppressWarnings("unchecked")
230    public <T> T getData() {
231
232        return (T)m_data;
233    }
234
235    /**
236     * Returns the decoration widgets of this list item.<p>
237     *
238     * @return the decoration widgets
239     */
240    public List<Widget> getDecorationWidgets() {
241
242        return Collections.unmodifiableList(m_decorationWidgets);
243    }
244
245    /**
246     * Gets the class for the DND helper.<p>
247     *
248     * @return the class for the DND helper
249     */
250    public String getDndHelperClass() {
251
252        return m_dndHelperClass;
253    }
254
255    /**
256     * Gets the class for the DND parent.<p>
257     *
258     * @return the class for the DND parent
259     */
260    public String getDndParentClass() {
261
262        return m_dndParentClass;
263    }
264
265    /**
266     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getDragHelper(I_CmsDropTarget)
267     */
268    public Element getDragHelper(I_CmsDropTarget target) {
269
270        if (m_helper == null) {
271            if (m_listItemWidget != null) {
272                m_listItemWidget.setAdditionalInfoVisible(false);
273                Iterator<Widget> buttonIterator = m_listItemWidget.getButtonPanel().iterator();
274                while (buttonIterator.hasNext()) {
275                    Widget button = buttonIterator.next();
276                    if (button != m_moveHandle) {
277                        button.getElement().getStyle().setVisibility(Visibility.HIDDEN);
278                    }
279                }
280            }
281            int oldMoveHandleLeft = moveHandleLeft(getElement());
282            int oldElemLeft = getElement().getAbsoluteLeft();
283            //int oldMoveHandleLeft = m_moveHandle.getAbsoluteLeft();
284            m_helper = CmsDomUtil.clone(getElement());
285            if (m_dndHelperClass != null) {
286                m_helper.addClassName(m_dndHelperClass);
287            }
288            // remove all decorations
289            List<com.google.gwt.dom.client.Element> elems = CmsDomUtil.getElementsByClass(
290                I_CmsLayoutBundle.INSTANCE.floatDecoratedPanelCss().decorationBox(),
291                CmsDomUtil.Tag.div,
292                m_helper);
293            for (com.google.gwt.dom.client.Element elem : elems) {
294                elem.removeFromParent();
295            }
296
297            // we append the drag helper to the body to prevent any kind of issues
298            // (ie when the parent is styled with overflow:hidden)
299            // and we put it additionally inside a absolute positioned provisional parent
300            // ON the original parent for the eventual animation when releasing
301            Element parentElement = getElement().getParentElement();
302            if (parentElement == null) {
303                parentElement = target.getElement();
304            }
305            int elementTop = getElement().getAbsoluteTop();
306            int parentTop = parentElement.getAbsoluteTop();
307            m_provisionalParent = DOM.createElement(parentElement.getTagName());
308            if (m_dndParentClass != null) {
309                m_provisionalParent.addClassName(m_dndParentClass);
310            }
311            RootPanel.getBodyElement().appendChild(m_provisionalParent);
312
313            m_provisionalParent.getStyle().setWidth(parentElement.getOffsetWidth(), Unit.PX);
314            m_provisionalParent.appendChild(m_helper);
315
316            m_provisionalParent.addClassName(I_CmsLayoutBundle.INSTANCE.generalCss().clearStyles());
317            m_provisionalParent.addClassName(
318                org.opencms.gwt.client.ui.css.I_CmsLayoutBundle.INSTANCE.generalCss().opencms());
319            Style style = m_helper.getStyle();
320            style.setWidth(m_helper.getOffsetWidth(), Unit.PX);
321            // the dragging class will set position absolute
322            m_helper.addClassName(I_CmsLayoutBundle.INSTANCE.listItemWidgetCss().dragging());
323            style.setTop(elementTop - parentTop, Unit.PX);
324            m_provisionalParent.getStyle().setPosition(Position.ABSOLUTE);
325            m_provisionalParent.getStyle().setTop(parentTop, Unit.PX);
326            m_provisionalParent.getStyle().setLeft(parentElement.getAbsoluteLeft(), Unit.PX);
327            int newMoveHandleLeft = moveHandleLeft(m_helper);
328            int newElemLeft = m_helper.getAbsoluteLeft();
329            m_offsetDelta = Optional.fromNullable(
330                new int[] {((newMoveHandleLeft - oldMoveHandleLeft) + oldElemLeft) - newElemLeft, 0});
331            m_provisionalParent.getStyle().setZIndex(I_CmsLayoutBundle.INSTANCE.constants().css().zIndexDND());
332        }
333        // ensure mouse out
334        if (m_listItemWidget != null) {
335            m_listItemWidget.forceMouseOut();
336        }
337        CmsDomUtil.ensureMouseOut(this);
338        return m_helper;
339    }
340
341    /**
342     * @see org.opencms.gwt.client.ui.I_CmsListItem#getId()
343     */
344    public String getId() {
345
346        return m_id;
347    }
348
349    /**
350     * Returns the list item widget of this list item, or null if this item doesn't have a list item widget.<p>
351     *
352     * @return a list item widget or null
353     */
354    public CmsListItemWidget getListItemWidget() {
355
356        if ((m_mainWidget == null) || !(m_mainWidget instanceof CmsListItemWidget)) {
357            return null;
358        }
359        return (CmsListItemWidget)m_mainWidget;
360    }
361
362    /**
363     * Returns the main widget.<p>
364     *
365     * @return the main widget
366     */
367    public Widget getMainWidget() {
368
369        return m_mainWidget;
370    }
371
372    /**
373     * Returns the move handle.<p>
374     *
375     * @return the move handle
376     */
377    public I_CmsDragHandle getMoveHandle() {
378
379        return m_moveHandle;
380    }
381
382    /**
383     * Returns the parent list.<p>
384     *
385     * @return the parent list
386     */
387    @SuppressWarnings("unchecked")
388    public CmsList<CmsListItem> getParentList() {
389
390        Widget parent = getParent();
391        if (parent == null) {
392            return null;
393        }
394        return (CmsList<CmsListItem>)parent;
395    }
396
397    /**
398     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getParentTarget()
399     */
400    public I_CmsDropTarget getParentTarget() {
401
402        return getParentList();
403    }
404
405    /**
406     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#getPlaceholder(I_CmsDropTarget)
407     */
408    public Element getPlaceholder(I_CmsDropTarget target) {
409
410        if (m_placeholder == null) {
411            if (m_listItemWidget != null) {
412                m_listItemWidget.setAdditionalInfoVisible(false);
413            }
414            m_placeholder = cloneForPlaceholder(this);
415        }
416        return m_placeholder;
417    }
418
419    /**
420     * Initializes the move handle with the given drag and drop handler and adds it to the list item widget.<p>
421     *
422     * This method will not work for list items that don't have a list-item-widget.<p>
423     *
424     * @param dndHandler the drag and drop handler
425     *
426     * @return <code>true</code> if initialization was successful
427     */
428    public boolean initMoveHandle(CmsDNDHandler dndHandler) {
429
430        return initMoveHandle(dndHandler, false);
431    }
432
433    /**
434     * Initializes the move handle with the given drag and drop handler and adds it to the list item widget.<p>
435     *
436     * This method will not work for list items that don't have a list-item-widget.<p>
437     *
438     * @param dndHandler the drag and drop handler
439     *
440     * @param addFirst if true, adds the move handle as first child
441     *
442     * @return <code>true</code> if initialization was successful
443     */
444    public boolean initMoveHandle(CmsDNDHandler dndHandler, boolean addFirst) {
445
446        if (m_moveHandle != null) {
447            return true;
448        }
449        if (m_listItemWidget == null) {
450            return false;
451        }
452        m_moveHandle = new MoveHandle(this);
453        if (addFirst) {
454            m_listItemWidget.addButtonToFront(m_moveHandle);
455        } else {
456            m_listItemWidget.addButton(m_moveHandle);
457        }
458
459        m_moveHandle.addMouseDownHandler(dndHandler);
460        return true;
461    }
462
463    /**
464     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#onDragCancel()
465     */
466    public void onDragCancel() {
467
468        clearDrag();
469    }
470
471    /**
472     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#onDrop(org.opencms.gwt.client.dnd.I_CmsDropTarget)
473     */
474    public void onDrop(I_CmsDropTarget target) {
475
476        clearDrag();
477    }
478
479    /**
480     * @see org.opencms.gwt.client.dnd.I_CmsDraggable#onStartDrag(org.opencms.gwt.client.dnd.I_CmsDropTarget)
481     */
482    public void onStartDrag(I_CmsDropTarget target) {
483
484        CmsDomUtil.ensureMouseOut(getMoveHandle().getElement());
485        setVisible(false);
486    }
487
488    /**
489     * Sets the data for this list item.<p>
490     *
491     * @param data the data to set
492     */
493    public void setData(Object data) {
494
495        m_data = data;
496    }
497
498    /**
499     * Sets the class for the DND helper.<p>
500     *
501     * @param dndHelperClass the class for the DND helper
502     */
503    public void setDndHelperClass(String dndHelperClass) {
504
505        m_dndHelperClass = dndHelperClass;
506    }
507
508    /**
509     * Sets the class for the DND parent.<p>
510     *
511     * @param dndParentClass the class for the DND parent
512     */
513    public void setDndParentClass(String dndParentClass) {
514
515        m_dndParentClass = dndParentClass;
516    }
517
518    /**
519     * @see org.opencms.gwt.client.ui.I_CmsListItem#setId(java.lang.String)
520     */
521    public void setId(String id) {
522
523        CmsList<CmsListItem> parentList = getParentList();
524        if (parentList != null) {
525            parentList.changeId(this, id);
526        }
527        m_id = id;
528    }
529
530    /**
531     * Sets the decoration style to fit with the small view of list items.<p>
532     *
533     * @param smallView true if the decoration has to fit with the small view of list items
534     */
535    public void setSmallView(boolean smallView) {
536
537        m_smallView = smallView;
538        if (m_smallView) {
539            m_decoratedPanel.addDecorationBoxStyle(
540                I_CmsLayoutBundle.INSTANCE.floatDecoratedPanelCss().decorationBoxSmall());
541        }
542    }
543
544    /**
545     * @see org.opencms.gwt.client.ui.I_CmsTruncable#truncate(java.lang.String, int)
546     */
547    public void truncate(String textMetricsPrefix, int widgetWidth) {
548
549        boolean hasDataValue = m_mainWidget instanceof CmsDataValue;
550        for (Widget widget : m_panel) {
551            if (!(widget instanceof I_CmsTruncable)) {
552                continue;
553            }
554            int width = widgetWidth - 4; // just to be on the safe side
555            if (widget instanceof CmsList<?>) {
556                if (hasDataValue) {
557                    width = widgetWidth;
558                } else {
559                    width -= 25; // 25px left margin
560                }
561            }
562            ((I_CmsTruncable)widget).truncate(textMetricsPrefix, width);
563        }
564    }
565
566    /**
567     * Adds a check box to this list item.<p>
568     *
569     * @param checkbox the check box
570     */
571    protected void addCheckBox(CmsCheckBox checkbox) {
572
573        assert m_checkbox == null;
574        m_checkbox = checkbox;
575        addDecoration(m_checkbox, CHECKBOX_WIDTH, false);
576    }
577
578    /**
579     * Helper method for adding a decoration widget and updating the decoration width accordingly.<p>
580     *
581     * @param widget the decoration widget to add
582     * @param width the intended width of the decoration widget
583     * @param first if true, inserts the widget at the front of the decorations, else at the end.
584     */
585    protected void addDecoration(Widget widget, int width, boolean first) {
586
587        m_decorationWidgets.add(widget);
588        m_decorationWidth += width;
589    }
590
591    /**
592     * Adds the main widget to the list item.<p>
593     *
594     * In most cases, the widget will be a list item widget. If this is the case, then further calls to {@link CmsListItem#getListItemWidget()} will
595     * return the widget which was passed as a parameter to this method. Otherwise, the method will return null.<p>
596     *
597     * @param widget the main content widget
598     */
599    protected void addMainWidget(Widget widget) {
600
601        assert m_mainWidget == null;
602        assert m_listItemWidget == null;
603        if (widget instanceof CmsListItemWidget) {
604            m_listItemWidget = (CmsListItemWidget)widget;
605        }
606        m_mainWidget = widget;
607    }
608
609    /**
610     * Clones the given item to be used as a place holder.<p>
611     *
612     * @param listItem the item to clone
613     *
614     * @return the cloned item
615     */
616    protected Element cloneForPlaceholder(CmsListItem listItem) {
617
618        Element clone = CmsDomUtil.clone(listItem.getElement());
619        clone.addClassName(I_CmsLayoutBundle.INSTANCE.dragdropCss().dragPlaceholder());
620
621        // remove hoverbar
622        List<Element> elems = CmsDomUtil.getElementsByClass(
623            I_CmsLayoutBundle.INSTANCE.listItemWidgetCss().buttonPanel(),
624            CmsDomUtil.Tag.div,
625            clone);
626        for (com.google.gwt.dom.client.Element elem : elems) {
627            elem.removeFromParent();
628        }
629
630        return clone;
631    }
632
633    /**
634     * This internal helper method creates the actual contents of the widget by combining the decorators and the main widget.<p>
635     */
636    protected void initContent() {
637
638        if (m_decoratedPanel != null) {
639            m_decoratedPanel.removeFromParent();
640        }
641        m_decoratedPanel = new CmsSimpleDecoratedPanel(m_decorationWidth, m_mainWidget, m_decorationWidgets);
642        m_panel.insert(m_decoratedPanel, 0);
643        setSmallView(m_smallView);
644    }
645
646    /**
647     * This method is a convenience method which sets the checkbox and main widget of this widget, and then calls {@link CmsListItem#initContent()}.<p>
648     *
649     * @param checkbox the checkbox to add
650     * @param mainWidget the mainWidget to add
651     */
652    protected void initContent(CmsCheckBox checkbox, Widget mainWidget) {
653
654        addCheckBox(checkbox);
655        addMainWidget(mainWidget);
656        initContent();
657    }
658
659    /**
660     * This method is a convenience method which sets the main widget of this widget, and then calls {@link CmsListItem#initContent()}.<p>
661     *
662     * @param mainWidget the main widget to add
663     */
664    protected void initContent(Widget mainWidget) {
665
666        addMainWidget(mainWidget);
667        initContent();
668    }
669
670    /**
671     * Gets the left edge of the move handle located in the element.<p>
672     *
673     * @param elem the element to search in
674     *
675     * @return the left edge of the move handle
676     */
677    protected int moveHandleLeft(Element elem) {
678
679        return CmsDomUtil.getElementsByClass(MOVE_HANDLE_MARKER_CLASS, elem).get(0).getAbsoluteLeft();
680    }
681
682    /**
683     * Removes a decoration widget.<p>
684     *
685     * @param widget the widget to remove
686     * @param width the widget width
687     */
688    protected void removeDecorationWidget(Widget widget, int width) {
689
690        if ((widget != null) && m_decorationWidgets.remove(widget)) {
691            m_decorationWidth -= width;
692            initContent();
693        }
694    }
695
696    /**
697     * Called when a drag operation for this widget is stopped.<p>
698     */
699    private void clearDrag() {
700
701        if (m_listItemWidget != null) {
702            Iterator<Widget> buttonIterator = m_listItemWidget.getButtonPanel().iterator();
703            while (buttonIterator.hasNext()) {
704                Widget button = buttonIterator.next();
705                button.getElement().getStyle().clearVisibility();
706            }
707        }
708        if (m_helper != null) {
709            m_helper.removeFromParent();
710            m_helper = null;
711        }
712        if (m_provisionalParent != null) {
713            m_provisionalParent.removeFromParent();
714            m_provisionalParent = null;
715        }
716        setVisible(true);
717
718    }
719}