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.input;
029
030import org.opencms.gwt.client.CmsCoreProvider;
031import org.opencms.gwt.client.I_CmsHasInit;
032import org.opencms.gwt.client.I_CmsHasResizeOnShow;
033import org.opencms.gwt.client.ui.CmsPushButton;
034import org.opencms.gwt.client.ui.CmsScrollPanel;
035import org.opencms.gwt.client.ui.I_CmsAutoHider;
036import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle;
037import org.opencms.gwt.client.ui.input.category.CmsDataValue;
038import org.opencms.gwt.client.ui.input.form.CmsWidgetFactoryRegistry;
039import org.opencms.gwt.client.ui.input.form.I_CmsFormWidgetFactory;
040import org.opencms.gwt.client.ui.tree.CmsTreeItem;
041import org.opencms.gwt.shared.CmsCategoryBean;
042import org.opencms.gwt.shared.CmsCategoryTreeEntry;
043import org.opencms.util.CmsStringUtil;
044
045import java.util.ArrayList;
046import java.util.Collection;
047import java.util.List;
048import java.util.Map;
049
050import com.google.common.base.Optional;
051import com.google.gwt.core.client.GWT;
052import com.google.gwt.dom.client.Style.Unit;
053import com.google.gwt.event.dom.client.ClickEvent;
054import com.google.gwt.event.dom.client.ClickHandler;
055import com.google.gwt.event.dom.client.DoubleClickEvent;
056import com.google.gwt.event.dom.client.DoubleClickHandler;
057import com.google.gwt.user.client.ui.Composite;
058import com.google.gwt.user.client.ui.FlowPanel;
059import com.google.gwt.user.client.ui.Panel;
060
061/**
062 * Basic category widget for forms.<p>
063 *
064 * @since 8.0.0
065 *
066 */
067public class CmsCategoryField extends Composite implements I_CmsFormWidget, I_CmsHasInit, I_CmsHasResizeOnShow {
068
069    /** Selection handler to handle check box click events and double clicks on the list items. */
070    protected abstract class A_SelectionHandler implements ClickHandler, DoubleClickHandler {
071
072        /** The reference to the checkbox. */
073        private CmsCheckBox m_checkBox;
074
075        /** The the select button, can be used instead of a double click to select and search. */
076        private CmsPushButton m_selectButton;
077
078        /**
079         * Constructor.<p>
080         *
081         * @param checkBox the item check box
082         */
083        protected A_SelectionHandler(CmsCheckBox checkBox) {
084
085            m_checkBox = checkBox;
086        }
087
088        /**
089         * @see com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event.dom.client.ClickEvent)
090         */
091        public void onClick(ClickEvent event) {
092
093            if (event.getSource().equals(m_selectButton)) {
094                m_checkBox.setChecked(true);
095                onSelectionChange();
096            } else if (event.getSource().equals(m_checkBox)) {
097                onSelectionChange();
098            }
099        }
100
101        /**
102         * @see com.google.gwt.event.dom.client.DoubleClickHandler#onDoubleClick(com.google.gwt.event.dom.client.DoubleClickEvent)
103         */
104        public void onDoubleClick(DoubleClickEvent event) {
105
106            m_checkBox.setChecked(true);
107            onSelectionChange();
108            event.stopPropagation();
109            event.preventDefault();
110        }
111
112        /**
113         * Sets the select button, can be used instead of a double click to select and search.<p>
114         *
115         * @param button the select button
116         */
117        public void setSelectButton(CmsPushButton button) {
118
119            m_selectButton = button;
120        }
121
122        /**
123         * Returns the check box.<p>
124         *
125         * @return the check box
126         */
127        protected CmsCheckBox getCheckBox() {
128
129            return m_checkBox;
130        }
131
132        /**
133         * Executed on selection change. Either when the check box was clicked or on double click on a list item.<p>
134         */
135        protected abstract void onSelectionChange();
136    }
137
138    /** The widget type identifier for this widget. */
139    private static final String WIDGET_TYPE = "categoryField";
140
141    /** The panel contains all the categories. */
142    FlowPanel m_categories = new FlowPanel();
143
144    /** The default rows set. */
145    int m_defaultHeight;
146
147    /** The root panel containing the other components of this widget. */
148    Panel m_panel = new FlowPanel();
149
150    /** The container for the text area. */
151    CmsScrollPanel m_scrollPanel = GWT.create(CmsScrollPanel.class);
152
153    /** The sife pathes of all added categories. */
154    private List<String> m_allSidePath = new ArrayList<String>();
155
156    /** The error display for this widget. */
157    private CmsErrorWidget m_error = new CmsErrorWidget();
158
159    /** The value if the parent should be selected with the children. */
160    private boolean m_selectParent;
161
162    /** The side path of the last added category. */
163    private String m_singleSidePath = "";
164
165    /** Count the numbers of values shown. */
166    private int m_valuesSet;
167
168    /**
169     * Category field widgets for ADE forms.<p>
170     */
171    public CmsCategoryField() {
172
173        super();
174        initWidget(m_panel);
175        m_panel.add(m_scrollPanel);
176        m_scrollPanel.getElement().getStyle().setHeight(50, Unit.PX);
177        m_scrollPanel.add(m_categories);
178
179        m_panel.add(m_error);
180        m_scrollPanel.addStyleName(I_CmsLayoutBundle.INSTANCE.generalCss().cornerAll());
181    }
182
183    /**
184     * Initializes this class.<p>
185     */
186    public static void initClass() {
187
188        // registers a factory for creating new instances of this widget
189        CmsWidgetFactoryRegistry.instance().registerFactory(WIDGET_TYPE, new I_CmsFormWidgetFactory() {
190
191            /**
192             * @see org.opencms.gwt.client.ui.input.form.I_CmsFormWidgetFactory#createWidget(java.util.Map, com.google.common.base.Optional)
193             */
194            public I_CmsFormWidget createWidget(Map<String, String> widgetParams, Optional<String> defaultValue) {
195
196                return new CmsCategoryField();
197            }
198        });
199    }
200
201    /**
202     * Checks if the given category is a parent category of any element of the given selection.<p>
203     *
204     * The selection might contain either category paths or site paths of categories.
205     *
206     * @param category a category path
207     * @param selection a set containing either category paths or category site paths
208     * @return true if the category is a parent category of any element of the given selection
209     */
210    public static boolean isParentCategoryOfSelected(String category, Collection<String> selection) {
211
212        category = normalizePath(category);
213        for (String selected : selection) {
214            selected = normalizePath(removeCategoryPrefix(selected));
215            if (selected.startsWith(category)) {
216                return true;
217            }
218        }
219        return false;
220    }
221
222    /**
223     * Adds leading/trailing slashes to a path if it doesn't already have them.
224     *
225     * @param path the path to normalize
226     * @return the normalized path
227     */
228    private static String normalizePath(String path) {
229
230        if (!path.startsWith("/")) {
231            path = "/" + path;
232        }
233        if (!path.endsWith("/")) {
234            path = path + "/";
235        }
236        return path;
237    }
238
239    /**
240     * Removes the category folder portion from the path of a category.
241     *
242     *  <p>If the argument doesn't have a category folder portion, it will be returned unchanged.
243     *
244     * @param selected a category site path (or in some cases just a category path)
245     * @return the category path (with the category folder stripped)
246     */
247    private static String removeCategoryPrefix(String selected) {
248
249        String globalFolder = "/system/categories/";
250        String localName = normalizePath(CmsCoreProvider.get().getCategoryBaseFolder());
251        String result = selected;
252        if (selected.startsWith(globalFolder)) {
253            result = selected.substring(globalFolder.length() - 1); // keep the slash
254        } else {
255            int namePos = selected.indexOf(localName);
256            if (namePos != -1) {
257                result = selected.substring((namePos + localName.length()) - 1);
258            }
259        }
260        return result;
261    }
262
263    /**
264     * Builds and shows the category tree.<p>
265     *
266     * @param treeEntries List of category entries
267     * @param selectedCategories a list of all selected categories
268     */
269    public void buildCategoryTree(List<CmsCategoryTreeEntry> treeEntries, Collection<String> selectedCategories) {
270
271        m_valuesSet = 0;
272        m_allSidePath.clear();
273        m_categories.removeFromParent();
274        m_categories.clear();
275
276        if ((treeEntries != null) && !treeEntries.isEmpty()) {
277            // add the first level and children
278            for (CmsCategoryTreeEntry category : treeEntries) {
279                // set the category tree item and add to list
280                CmsTreeItem treeItem;
281                boolean hasSelectedChildren = hasSelectedChildren(category.getChildren(), selectedCategories);
282                if (!category.getPath().isEmpty() || hasSelectedChildren) {
283                    if (m_selectParent || !hasSelectedChildren) {
284                        treeItem = buildTreeItem(category, selectedCategories, false);
285                        if (treeItem.isOpen()) {
286                            m_allSidePath.add(category.getSitePath());
287                        }
288                    } else {
289                        treeItem = buildTreeItem(category, selectedCategories, true);
290                    }
291                    if (treeItem.isOpen()) {
292                        m_singleSidePath = category.getSitePath();
293
294                        m_valuesSet++;
295                        addChildren(treeItem, category.getChildren(), selectedCategories);
296                    }
297                }
298            }
299        }
300        m_scrollPanel.add(m_categories);
301        m_scrollPanel.onResizeDescendant();
302
303    }
304
305    /**
306     * Returns the site path of all shown categories.<p>
307     *
308     * @return the site path of all shown categories
309     */
310    public List<String> getAllSitePath() {
311
312        return m_allSidePath;
313    }
314
315    /**
316     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getApparentValue()
317     */
318    public String getApparentValue() {
319
320        // TODO: Auto-generated method stub
321        return null;
322    }
323
324    /**
325     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getFieldType()
326     */
327    public FieldType getFieldType() {
328
329        return I_CmsFormWidget.FieldType.STRING;
330    }
331
332    /**
333     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getFormValue()
334     */
335    public Object getFormValue() {
336
337        // TODO: Auto-generated method stub
338        return null;
339    }
340
341    /**
342     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getFormValueAsString()
343     */
344    public String getFormValueAsString() {
345
346        // TODO: Auto-generated method stub
347        return null;
348    }
349
350    /**
351     * Returns the scroll panel of this widget.<p>
352     *
353     * @return the scroll panel
354     */
355    public CmsScrollPanel getScrollPanel() {
356
357        return m_scrollPanel;
358    }
359
360    /**
361     * Returns the site path of the last category.<p>
362     *
363     * @return the site path of the last category
364     */
365    public String getSingelSitePath() {
366
367        return m_singleSidePath;
368    }
369
370    /**
371     * Returns the count of values set to show.<p>
372     * @return the count of values set to show
373     */
374    public int getValuesSet() {
375
376        return m_valuesSet;
377    }
378
379    /**
380     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#isEnabled()
381     */
382    public boolean isEnabled() {
383
384        return false;
385    }
386
387    /**
388     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#reset()
389     */
390    public void reset() {
391
392        //TODO implement reset();
393    }
394
395    /**
396     * @see org.opencms.gwt.client.I_CmsHasResizeOnShow#resizeOnShow()
397     */
398    public void resizeOnShow() {
399
400        m_scrollPanel.onResizeDescendant();
401    }
402
403    /**
404     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setAutoHideParent(org.opencms.gwt.client.ui.I_CmsAutoHider)
405     */
406    public void setAutoHideParent(I_CmsAutoHider autoHideParent) {
407
408        // nothing to do
409    }
410
411    /**
412     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setEnabled(boolean)
413     */
414    public void setEnabled(boolean enabled) {
415
416        //TODO implement setEnabled;
417    }
418
419    /**
420     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setErrorMessage(java.lang.String)
421     */
422    public void setErrorMessage(String errorMessage) {
423
424        m_error.setText(errorMessage);
425    }
426
427    /**
428     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setFormValueAsString(java.lang.String)
429     */
430    public void setFormValueAsString(String value) {
431
432        // TODO: Auto-generated method stub
433
434    }
435
436    /**
437     * Sets the height of this category field.<p>
438     *
439     * @param height the height of this category field
440     */
441    public void setHeight(int height) {
442
443        m_defaultHeight = height;
444        m_scrollPanel.setHeight(m_defaultHeight + "px");
445        m_scrollPanel.setDefaultHeight(m_defaultHeight);
446        m_scrollPanel.onResizeDescendant();
447    }
448
449    /**
450     * Sets if the parent category should be selected with the child or not.
451     *
452     * @param value if the parent categories should be selected or not
453     * */
454    public void setParentSelection(boolean value) {
455
456        m_selectParent = value;
457    }
458
459    /**
460     * Sets the value of the widget.<p>
461     *
462     * @param value the new value
463     */
464    public void setSelected(Object value) {
465
466        // nothing to do
467    }
468
469    /**
470     * Set the selected categories.<p>
471     *
472     * @param newValue String of selected categories separated by '|'
473     */
474    public void setSelectedAsString(String newValue) {
475
476        setSelected(newValue);
477    }
478
479    /**
480     * @see com.google.gwt.user.client.ui.Composite#onAttach()
481     */
482    @Override
483    protected void onAttach() {
484
485        super.onAttach();
486    }
487
488    /**
489     * Adds children item to the category tree and select the categories.<p>
490     *
491     * @param parent the parent item
492     * @param children the list of children
493     * @param selectedCategories the list of categories to select
494     */
495    private void addChildren(
496        CmsTreeItem parent,
497        List<CmsCategoryTreeEntry> children,
498        Collection<String> selectedCategories) {
499
500        if (children != null) {
501            for (CmsCategoryTreeEntry child : children) {
502                // set the category tree item and add to parent tree item
503                CmsTreeItem treeItem;
504                boolean isPartofPath = false;
505                isPartofPath = isParentCategoryOfSelected(child.getPath(), selectedCategories);
506                if (isPartofPath) {
507                    m_singleSidePath = child.getSitePath();
508                    m_valuesSet++;
509                    if (m_selectParent || !hasSelectedChildren(child.getChildren(), selectedCategories)) {
510                        m_allSidePath.add(child.getSitePath());
511                        treeItem = buildTreeItem(child, selectedCategories, false);
512                    } else {
513                        treeItem = buildTreeItem(child, selectedCategories, true);
514                    }
515                    addChildren(treeItem, child.getChildren(), selectedCategories);
516                    parent.addChild(treeItem);
517                }
518
519            }
520        }
521    }
522
523    /**
524     * Builds a tree item for the given category.<p>
525     *
526     * @param category the category
527     * @param selectedCategories the selected categories
528     * @param inactive true if the value should be displayed inactive
529     *
530     * @return the tree item widget
531     */
532    private CmsTreeItem buildTreeItem(
533        CmsCategoryTreeEntry category,
534        Collection<String> selectedCategories,
535        boolean inactive) {
536
537        CmsDataValue categoryTreeItem = new CmsDataValue(
538            500,
539            4,
540            CmsCategoryBean.SMALL_ICON_CLASSES,
541            category.getTitle(),
542            category.getPath());
543        if (inactive) {
544            categoryTreeItem.setInactive();
545        }
546        categoryTreeItem.setTitle(
547            CmsStringUtil.isNotEmptyOrWhitespaceOnly(category.getDescription())
548            ? category.getDescription()
549            : category.getPath());
550        CmsTreeItem treeItem = new CmsTreeItem(false, categoryTreeItem);
551        treeItem.setId(category.getPath());
552        boolean isPartofPath = false;
553        isPartofPath = isParentCategoryOfSelected(category.getPath(), selectedCategories);
554        if (isPartofPath) {
555            m_categories.add(treeItem);
556            treeItem.setOpen(true);
557        }
558        return treeItem;
559    }
560
561    /**
562     * Checks if it has selected children.<p>
563     *
564     * @param children the children to check
565     * @param selectedCategories list of all selected categories
566     *
567     * @return true if it has selected children
568     * */
569    private boolean hasSelectedChildren(List<CmsCategoryTreeEntry> children, Collection<String> selectedCategories) {
570
571        boolean result = false;
572        if (children == null) {
573            return false;
574        }
575        for (CmsCategoryTreeEntry child : children) {
576            result = selectedCategories.contains(child.getSitePath());
577            if (result || hasSelectedChildren(child.getChildren(), selectedCategories)) {
578                return true;
579            }
580        }
581
582        return result;
583    }
584}