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.acacia.client.widgets;
029
030import org.opencms.acacia.client.CmsAttributeHandler;
031import org.opencms.acacia.client.css.I_CmsWidgetsLayoutBundle;
032import org.opencms.acacia.client.ui.CmsAttributeValueView;
033import org.opencms.acacia.shared.CmsEntity;
034import org.opencms.ade.contenteditor.client.CmsContentEditor;
035import org.opencms.ade.contenteditor.client.I_CmsEntityChangeListener;
036import org.opencms.ade.contenteditor.client.css.I_CmsLayoutBundle;
037import org.opencms.gwt.client.ui.input.CmsSelectBox;
038import org.opencms.util.CmsStringUtil;
039
040import java.util.Arrays;
041import java.util.LinkedHashMap;
042import java.util.List;
043
044import com.google.common.collect.Maps;
045import com.google.gwt.dom.client.Element;
046import com.google.gwt.event.dom.client.FocusEvent;
047import com.google.gwt.event.dom.client.FocusHandler;
048import com.google.gwt.event.logical.shared.ValueChangeEvent;
049import com.google.gwt.event.logical.shared.ValueChangeHandler;
050import com.google.gwt.event.shared.HandlerRegistration;
051import com.google.gwt.user.client.Window;
052import com.google.gwt.user.client.ui.Composite;
053import com.google.gwt.user.client.ui.Widget;
054
055/**
056 * Select widget which uses other values from the content as select options.<p>
057 *
058 * This works as follows: The widget is given a configuration consisting of three pipe-separated OpenCms content value paths.
059 * The first path is used to select a set of nested content values. The second and third paths are relative to the first path
060 * and are used to select a select option and a select option display text from the nested contents matching the first path.
061 * Note that if you omit indexes on a component of the first path, all indexes will be matched.
062 * You can also use relative paths for the first path. Like '../NestedNode' or '../../OtherNode'.<p>
063 *
064 * The widget attaches event listeners to the editor so it can dynamically update the list of select options when the content changes.
065 */
066public class CmsDependentSelectWidget extends Composite implements I_CmsEditWidget, I_CmsHasDisplayDirection {
067
068    /** The global select box. */
069    protected CmsSelectBox m_selectBox = new CmsSelectBox();
070
071    /** Value of the activation. */
072    private boolean m_active = true;
073
074    /** Path components of the base path. */
075    private String[] m_basePath;
076
077    /** Path components of the path used to select the 'nice name' for the select option. */
078    private String[] m_descriptionPath;
079
080    /** The last value set through the setValue method. This is not necessarily the current widget value. */
081    private String m_externalValue;
082
083    /** Path components of the path used to select the option value. */
084    private String[] m_valuePath;
085
086    /**
087     * Creates a new widget instance.<p>
088     *
089     * @param configuration the widget configuration
090     */
091    public CmsDependentSelectWidget(String configuration) {
092
093        List<String> listConfig = CmsStringUtil.splitAsList(configuration, "|");
094        if (listConfig.size() == 0) {
095            Window.alert("Illegal dependent select widget configuration: " + configuration);
096        }
097        if (listConfig.size() == 1) {
098            m_basePath = splitPath(listConfig.get(0));
099            m_valuePath = splitPath("VALUE");
100            m_descriptionPath = m_valuePath;
101        } else if (listConfig.size() == 2) {
102            m_basePath = splitPath(listConfig.get(0));
103            m_valuePath = splitPath(listConfig.get(1));
104            m_descriptionPath = m_valuePath;
105        } else if (listConfig.size() >= 3) {
106            m_basePath = splitPath(listConfig.get(0));
107            m_valuePath = splitPath(listConfig.get(1));
108            m_descriptionPath = splitPath(listConfig.get(2));
109        }
110
111        // Place the check above the box using a vertical panel.
112        m_selectBox.addStyleName(I_CmsWidgetsLayoutBundle.INSTANCE.widgetCss().selectBoxPanel());
113        m_selectBox.setPopupResize(false);
114        // add some styles to parts of the selectbox.
115        m_selectBox.getOpener().addStyleName(I_CmsWidgetsLayoutBundle.INSTANCE.widgetCss().selectBoxSelected());
116        m_selectBox.getSelectorPopup().addStyleName(I_CmsLayoutBundle.INSTANCE.globalWidgetCss().selectBoxPopup());
117        m_selectBox.addValueChangeHandler(new ValueChangeHandler<String>() {
118
119            public void onValueChange(ValueChangeEvent<String> event) {
120
121                fireChangeEvent();
122
123            }
124
125        });
126
127        update(CmsContentEditor.getEntity());
128        initWidget(m_selectBox);
129    }
130
131    /**
132     * @see com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.google.gwt.event.dom.client.FocusHandler)
133     */
134    public HandlerRegistration addFocusHandler(FocusHandler handler) {
135
136        return addDomHandler(handler, FocusEvent.getType());
137    }
138
139    /**
140     * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler)
141     */
142    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) {
143
144        return addHandler(handler, ValueChangeEvent.getType());
145    }
146
147    /**
148     * Represents a value change event.<p>
149     * Please edit the blog entry text.
150     */
151    public void fireChangeEvent() {
152
153        ValueChangeEvent.fire(this, m_selectBox.getFormValueAsString());
154
155    }
156
157    /**
158     * @see org.opencms.acacia.client.widgets.I_CmsHasDisplayDirection#getDisplayingDirection()
159     */
160    public Direction getDisplayingDirection() {
161
162        return m_selectBox.displayingAbove() ? Direction.above : Direction.below;
163    }
164
165    /**
166     * @see com.google.gwt.user.client.ui.HasValue#getValue()
167     */
168    public String getValue() {
169
170        return m_selectBox.getFormValueAsString();
171    }
172
173    /**
174     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#isActive()
175     */
176    public boolean isActive() {
177
178        return m_active;
179    }
180
181    /**
182     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#onAttachWidget()
183     */
184    public void onAttachWidget() {
185
186        super.onAttach();
187    }
188
189    /**
190     * @see com.google.gwt.user.client.ui.Widget#onLoad()
191     */
192    @Override
193    public void onLoad() {
194
195        update(CmsContentEditor.getEntity());
196
197        CmsContentEditor.addEntityChangeListener(new I_CmsEntityChangeListener() {
198
199            public void onEntityChange(CmsEntity entity) {
200
201                update(CmsContentEditor.getEntity());
202            }
203        }, null);
204    }
205
206    /**
207     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#owns(com.google.gwt.dom.client.Element)
208     */
209    public boolean owns(Element element) {
210
211        return getElement().isOrHasChild(element);
212    }
213
214    /**
215     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setActive(boolean)
216     */
217    public void setActive(boolean active) {
218
219        // check if value change. If not do nothing.
220        if (m_active == active) {
221            return;
222        }
223        // set new value.
224        m_active = active;
225        // set the new value to the selectbox.
226        m_selectBox.setEnabled(active);
227        // fire change event if necessary.
228        if (active) {
229            fireChangeEvent();
230        }
231
232    }
233
234    /**
235     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setName(java.lang.String)
236     */
237    public void setName(String name) {
238
239        // no input field so nothing to do
240
241    }
242
243    /**
244     * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object)
245     */
246    public void setValue(String value) {
247
248        setValue(value, false);
249
250    }
251
252    /**
253     * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object, boolean)
254     */
255    public void setValue(String value, boolean fireEvents) {
256
257        m_selectBox.setFormValueAsString(value);
258        m_externalValue = value;
259        if (fireEvents) {
260            fireChangeEvent();
261        }
262
263    }
264
265    /**
266     * Updates the select options from the given entity.<p>
267     *
268     * @param entity a top-level content entity
269     */
270    public void update(CmsEntity entity) {
271
272        List<Object> baseObjects = CmsEntity.getValuesForPath(entity, getAbsolutePath(m_basePath));
273        LinkedHashMap<String, String> options = Maps.newLinkedHashMap();
274        for (Object baseObject : baseObjects) {
275            List<Object> valueValues = CmsEntity.getValuesForPath(baseObject, m_valuePath);
276            List<Object> descriptionValues = CmsEntity.getValuesForPath(baseObject, m_descriptionPath);
277            if (valueValues.size() > 0) {
278                String value = (String)valueValues.get(0);
279                String description = value;
280                if (descriptionValues.size() > 0) {
281                    description = (String)descriptionValues.get(0);
282                }
283                options.put(value, description);
284            }
285        }
286        replaceOptions(options);
287    }
288
289    /**
290     * Returns the absolute path elements.
291     * In case off a relative path indicated by leading '..' elements, the local value path will be used as a starting point.
292     * Otherwise the given path elements will be returned.<p>
293     *
294     * @param pathElements the path elements
295     *
296     * @return the absolute path elements
297     */
298    private String[] getAbsolutePath(String[] pathElements) {
299
300        String[] result = pathElements;
301        if (pathElements[0].equals("..")) {
302            Widget parent = getParent();
303            CmsAttributeValueView valueView = null;
304            while ((parent != null) && !(parent instanceof CmsAttributeValueView)) {
305                parent = parent.getParent();
306            }
307            if (parent instanceof CmsAttributeValueView) {
308                valueView = (CmsAttributeValueView)parent;
309            }
310            if (valueView != null) {
311                CmsAttributeHandler handler = valueView.getHandler();
312                String localPath = handler.getSimplePath(-1);
313                int pathIndex = 0;
314                while (pathElements[pathIndex].equals("..")) {
315                    pathIndex++;
316                    localPath = localPath.substring(0, localPath.lastIndexOf("/"));
317                }
318                String[] localPathElements = splitPath(localPath);
319                // join the to arrays
320                result = Arrays.copyOf(localPathElements, (localPathElements.length + pathElements.length) - pathIndex);
321                System.arraycopy(
322                    pathElements,
323                    pathIndex,
324                    result,
325                    localPathElements.length,
326                    pathElements.length - pathIndex);
327            }
328        }
329        return result;
330    }
331
332    /**
333     * Replaces the select options with the given options.<p>
334     *
335     * @param options the map of select options (keys are option values, values are option descriptions)
336     */
337    private void replaceOptions(LinkedHashMap<String, String> options) {
338
339        String oldValue = m_selectBox.getFormValueAsString();
340        for (String additionalValue : new String[] {oldValue, m_externalValue}) {
341            if (!options.containsKey(additionalValue)) {
342                options.put(additionalValue, additionalValue);
343            }
344        }
345        if (options.containsKey("")) {
346            options.put(
347                "",
348                org.opencms.gwt.client.Messages.get().key(
349                    org.opencms.gwt.client.Messages.GUI_SELECTBOX_EMPTY_SELECTION_0));
350        }
351        m_selectBox.setItems(options);
352        m_selectBox.setFormValueAsString(oldValue);
353    }
354
355    /**
356     * Splits a path into components.<p>
357     *
358     * @param path the path to split
359     * @return the path components
360     */
361    private String[] splitPath(String path) {
362
363        path = path.replaceAll("^/", "").replaceAll("/$", "");
364        return path.split("/");
365    }
366
367}