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.ui.components.editablegroup;
029
030import java.util.List;
031
032import com.google.common.base.Supplier;
033import com.google.common.collect.Lists;
034import com.vaadin.ui.AbstractComponent;
035import com.vaadin.ui.AbstractOrderedLayout;
036import com.vaadin.ui.Button;
037import com.vaadin.ui.Component;
038import com.vaadin.ui.Component.ErrorEvent;
039import com.vaadin.ui.Component.Event;
040import com.vaadin.ui.Component.Listener;
041import com.vaadin.ui.Layout;
042import com.vaadin.v7.ui.Label;
043
044/**
045 * Manages a group of widgets used as a multivalue input.<p>
046 *
047 * This class is not itself a widget, it just coordinates the other widgets actually used to display the multivalue widget group.
048 */
049public class CmsEditableGroup {
050
051    /**
052     * Empty handler which shows or hides an 'Add' button to add new rows, depending on whether the group is empty.
053     */
054    public static class AddButtonEmptyHandler implements CmsEditableGroup.I_EmptyHandler {
055
056        /** The 'Add' button. */
057        private Button m_addButton;
058
059        /** The group. */
060        private CmsEditableGroup m_group;
061
062        /**
063         * Creates a new instance.
064         *
065         * @param addButtonText the text for the Add button
066         */
067        public AddButtonEmptyHandler(String addButtonText) {
068
069            m_addButton = new Button(addButtonText);
070            m_addButton.addClickListener(evt -> {
071                Component component = m_group.getNewComponentFactory().get();
072                m_group.addRow(component);
073            });
074        }
075
076        /**
077         * @see org.opencms.ui.components.editablegroup.CmsEditableGroup.I_EmptyHandler#init(org.opencms.ui.components.editablegroup.CmsEditableGroup)
078         */
079        public void init(CmsEditableGroup group) {
080
081            m_group = group;
082        }
083
084        /**
085         * @see org.opencms.ui.components.editablegroup.CmsEditableGroup.I_EmptyHandler#setEmpty(boolean)
086         */
087        public void setEmpty(boolean empty) {
088
089            if (empty) {
090                m_group.getContainer().addComponent(m_addButton);
091            } else {
092                m_group.getContainer().removeComponent(m_addButton);
093            }
094        }
095
096    }
097
098    /**
099     * Default implementation for row builder.
100     */
101    public static class DefaultRowBuilder implements CmsEditableGroup.I_RowBuilder {
102
103        public I_CmsEditableGroupRow buildRow(CmsEditableGroup group, Component component) {
104
105            if (component instanceof Layout.MarginHandler) {
106                // Since the row is a HorizontalLayout with the edit buttons positioned next to the original
107                // widget, a margin on the widget causes it to be vertically offset from the buttons too much
108                Layout.MarginHandler marginHandler = (Layout.MarginHandler)component;
109                marginHandler.setMargin(false);
110            }
111            if (component instanceof AbstractComponent) {
112                component.addListener(group.getErrorListener());
113            }
114            I_CmsEditableGroupRow row = new CmsEditableGroupRow(group, component);
115            if (group.getRowCaption() != null) {
116                row.setCaption(group.getRowCaption());
117            }
118            return row;
119        }
120    }
121
122    /**
123     * Handles state changes when the group becomes empty/not empty.
124     */
125    public interface I_EmptyHandler {
126
127        /**
128         * Needs to be called initially with the group for which this is used.
129         *
130         * @param group the group
131         */
132        public void init(CmsEditableGroup group);
133
134        /**
135         * Called when the group changes from empty to not empty, or vice versa.
136         *
137         * @param empty true if the group is empty
138         */
139        public void setEmpty(boolean empty);
140    }
141
142    /**
143     * Interface for group row components that can have errors.
144     */
145    public interface I_HasError {
146
147        /**
148         * Check if there is an error.
149         *
150         * @return true if there is an error
151         */
152        public boolean hasEditableGroupError();
153    }
154
155    /**
156     * Builds editable group rows by wrapping other components.
157     */
158    public interface I_RowBuilder {
159
160        /**
161         * Builds a row for the given group by wrapping the given component.
162         *
163         * @param group the group
164         * @param component the component
165         * @return the new row
166         */
167        public I_CmsEditableGroupRow buildRow(CmsEditableGroup group, Component component);
168    }
169
170    /** The container in which to render the individual rows of the multivalue widget group. */
171    private AbstractOrderedLayout m_container;
172
173    /** Flag which controls whether the edit option is enabled. */
174    private boolean m_editEnabled;
175
176    private I_EmptyHandler m_emptyHandler;
177
178    /** The error label. */
179    private Label m_errorLabel = new Label();
180
181    /** The error listener. */
182    private Listener m_errorListener;
183
184    /** The error message. */
185    private String m_errorMessage;
186
187    /**Should the add option be hidden?*/
188    private boolean m_hideAdd;
189
190    /** Factory for creating new input fields. */
191    private Supplier<Component> m_newComponentFactory;
192
193    /** The builder to use for creating new rows. */
194    private I_RowBuilder m_rowBuilder = new DefaultRowBuilder();
195
196    /** The row caption. */
197    private String m_rowCaption;
198
199    /**
200     * Creates a new instance.<p>
201     *
202     * @param container the container in which to render the individual rows
203     * @param componentFactory the factory used to create new input fields
204     * @param placeholder the placeholder to display when there are no rows
205     */
206    public CmsEditableGroup(
207        AbstractOrderedLayout container,
208        Supplier<Component> componentFactory,
209        I_EmptyHandler emptyHandler) {
210
211        m_hideAdd = false;
212        m_emptyHandler = emptyHandler;
213        m_container = container;
214        m_newComponentFactory = componentFactory;
215        m_emptyHandler = emptyHandler;
216        m_emptyHandler.init(this);
217        m_errorListener = new Listener() {
218
219            private static final long serialVersionUID = 1L;
220
221            @SuppressWarnings("synthetic-access")
222            public void componentEvent(Event event) {
223
224                if (event instanceof ErrorEvent) {
225                    updateGroupValidation();
226                }
227            }
228        };
229        m_errorLabel.setValue(m_errorMessage);
230        m_errorLabel.addStyleName("o-editablegroup-errorlabel");
231        setErrorVisible(false);
232    }
233
234    /**
235     * Creates a new instance.<p>
236     *
237     * @param container the container in which to render the individual rows
238     * @param componentFactory the factory used to create new input fields
239     * @param addButtonCaption the caption for the button which is used to add a new row to an empty list
240     */
241    public CmsEditableGroup(
242        AbstractOrderedLayout container,
243        Supplier<Component> componentFactory,
244        String addButtonCaption) {
245
246        this(container, componentFactory, new AddButtonEmptyHandler(addButtonCaption));
247    }
248
249    /**
250     * Adds a row for the given component at the end of the group.
251     *
252     * @param component the component to wrap in the row to be added
253     */
254    public void addRow(Component component) {
255
256        Component actualComponent = component == null ? m_newComponentFactory.get() : component;
257        I_CmsEditableGroupRow row = m_rowBuilder.buildRow(this, actualComponent);
258        m_container.addComponent(row);
259        updatePlaceholder();
260        updateButtonBars();
261        updateGroupValidation();
262    }
263
264    /**
265     * Adds a new row after the given one.
266     *
267     * @param row the row after which a new one should be added
268     */
269    public void addRowAfter(I_CmsEditableGroupRow row) {
270
271        int index = m_container.getComponentIndex(row);
272        if (index >= 0) {
273            Component component = m_newComponentFactory.get();
274            I_CmsEditableGroupRow newRow = m_rowBuilder.buildRow(this, component);
275            m_container.addComponent(newRow, index + 1);
276        }
277        updatePlaceholder();
278        updateButtonBars();
279        updateGroupValidation();
280    }
281
282    /**
283     * Gets the row container.
284     *
285     * @return the row container
286     */
287    public AbstractOrderedLayout getContainer() {
288
289        return m_container;
290    }
291
292    /**
293     * Gets the error listener.
294     *
295     * @return t
296     */
297    public Listener getErrorListener() {
298
299        return m_errorListener;
300    }
301
302    /**
303     * Gets the factory used for creating new components.
304     *
305     * @return the factory used for creating new components
306     */
307    public Supplier<Component> getNewComponentFactory() {
308
309        return m_newComponentFactory;
310    }
311
312    /**
313     * Returns the row caption.<p>
314     *
315     * @return the row caption
316     */
317    public String getRowCaption() {
318
319        return m_rowCaption;
320    }
321
322    /**
323     * Gets all rows.
324     *
325     * @return the list of all rows
326     */
327    public List<I_CmsEditableGroupRow> getRows() {
328
329        List<I_CmsEditableGroupRow> result = Lists.newArrayList();
330        for (Component component : m_container) {
331            if (component instanceof I_CmsEditableGroupRow) {
332                result.add((I_CmsEditableGroupRow)component);
333            }
334        }
335        return result;
336    }
337
338    /**
339     * Initializes the multivalue group.<p>
340     */
341    public void init() {
342
343        m_container.removeAllComponents();
344        m_container.addComponent(m_errorLabel);
345        updatePlaceholder();
346    }
347
348    /**
349     * Moves the given row down.
350     *
351     * @param row the row to move
352     */
353    public void moveDown(I_CmsEditableGroupRow row) {
354
355        int index = m_container.getComponentIndex(row);
356        if ((index >= 0) && (index < (m_container.getComponentCount() - 1))) {
357            m_container.removeComponent(row);
358            m_container.addComponent(row, index + 1);
359        }
360        updateButtonBars();
361    }
362
363    /**
364     * Moves the given row up.
365     *
366     * @param row the row to move
367     */
368    public void moveUp(I_CmsEditableGroupRow row) {
369
370        int index = m_container.getComponentIndex(row);
371        if (index > 0) {
372            m_container.removeComponent(row);
373            m_container.addComponent(row, index - 1);
374        }
375        updateButtonBars();
376    }
377
378    public void onEdit(I_CmsEditableGroupRow row) {
379
380    }
381
382    /**
383     * Removes the given row.
384     *
385     * @param row the row to remove
386     */
387    public void remove(I_CmsEditableGroupRow row) {
388
389        int index = m_container.getComponentIndex(row);
390        if (index >= 0) {
391            m_container.removeComponent(row);
392        }
393        updatePlaceholder();
394        updateButtonBars();
395        updateGroupValidation();
396    }
397
398    public void removeAll() {
399
400        m_container.removeAllComponents();
401        updatePlaceholder();
402        updateButtonBars();
403        updateGroupValidation();
404
405    }
406
407    /**
408     * @see org.opencms.ui.components.editablegroup.I_CmsEditableGroup#setAddButtonVisible(boolean)
409     */
410    public void setAddButtonVisible(boolean visible) {
411
412        m_hideAdd = !visible;
413
414    }
415
416    /**
417     * Enables / disables edit button.
418     *
419     * @param enabled true if edit button should be enabled
420     */
421    public void setEditEnabled(boolean enabled) {
422
423        m_editEnabled = enabled;
424    }
425
426    /**
427     * Sets the error message.<p>
428     *
429     * @param errorMessage the error message
430     */
431    public void setErrorMessage(String errorMessage) {
432
433        m_errorMessage = errorMessage;
434        m_errorLabel.setValue(errorMessage != null ? errorMessage : "");
435    }
436
437    /**
438     * Sets the row builder.
439     *
440     * @param rowBuilder the row builder
441     */
442    public void setRowBuilder(I_RowBuilder rowBuilder) {
443
444        m_rowBuilder = rowBuilder;
445    }
446
447    /**
448     * Sets the row caption.<p>
449     *
450     * @param rowCaption the row caption to set
451     */
452    public void setRowCaption(String rowCaption) {
453
454        m_rowCaption = rowCaption;
455    }
456
457    /**
458     * Checks if the given group component has an error.<p>
459     *
460     * @param component the component to check
461     * @return true if the component has an error
462     */
463    protected boolean hasError(Component component) {
464
465        if (component instanceof AbstractComponent) {
466            if (((AbstractComponent)component).getComponentError() != null) {
467                return true;
468            }
469        }
470        if (component instanceof I_HasError) {
471            if (((I_HasError)component).hasEditableGroupError()) {
472                return true;
473            }
474
475        }
476        return false;
477    }
478
479    /**
480     * Shows or hides the error label.<p>
481     *
482     * @param hasError true if we have an error
483     */
484    private void setErrorVisible(boolean hasError) {
485
486        m_errorLabel.setVisible(hasError && (m_errorMessage != null));
487    }
488
489    /**
490     * Updates the button bars.<p>
491     */
492    private void updateButtonBars() {
493
494        List<I_CmsEditableGroupRow> rows = getRows();
495        int i = 0;
496        for (I_CmsEditableGroupRow row : rows) {
497            boolean first = i == 0;
498            boolean last = i == (rows.size() - 1);
499            row.getButtonBar().setFirstLast(first, last, m_hideAdd);
500            row.getButtonBar().getState().setEditEnabled(m_editEnabled);
501            i += 1;
502        }
503    }
504
505    /**
506     * Updates the visibility of the error label based on errors in the group components.<p>
507     */
508    private void updateGroupValidation() {
509
510        boolean hasError = false;
511        for (I_CmsEditableGroupRow row : getRows()) {
512            if (hasError(row.getComponent())) {
513                hasError = true;
514                break;
515            }
516        }
517        setErrorVisible(hasError);
518    }
519
520    /**
521     * Updates the button visibility.<p>
522     */
523    private void updatePlaceholder() {
524
525        boolean empty = getRows().size() == 0;
526        m_emptyHandler.setEmpty(empty);
527
528    }
529}