View Javadoc
1   /**
2    * This file Copyright (c) 2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.admincentral.findbar.layout;
35  
36  import info.magnolia.i18nsystem.SimpleTranslator;
37  import info.magnolia.icons.MagnoliaIcons;
38  import info.magnolia.ui.theme.ResurfaceTheme;
39  
40  import java.time.LocalDate;
41  import java.time.format.DateTimeFormatter;
42  import java.time.format.FormatStyle;
43  import java.util.List;
44  import java.util.Locale;
45  
46  import org.apache.commons.collections4.CollectionUtils;
47  import org.vaadin.addons.ComboBoxMultiselect;
48  
49  import com.vaadin.addon.daterangefield.DateRangeField;
50  import com.vaadin.ui.Alignment;
51  import com.vaadin.ui.Button;
52  import com.vaadin.ui.Component;
53  import com.vaadin.ui.CssLayout;
54  import com.vaadin.ui.HorizontalLayout;
55  import com.vaadin.ui.Label;
56  import com.vaadin.ui.VerticalLayout;
57  
58  
59  /**
60   * Filter options layout for {@link info.magnolia.admincentral.findbar.FindBar}.
61   */
62  public class FilterLayout extends CssLayout {
63  
64      private final DateRangeField dateField;
65      private final List<ComboBoxMultiselect<String>> fields;
66  
67      private final HorizontalLayout periscopeFilterOptions;
68      private final SimpleTranslator i18n;
69      private CssLayout globalTokenLayout;
70      private Label titleLabel;
71  
72      public FilterLayout(DateRangeField dateField, SimpleTranslator i18n, List<ComboBoxMultiselect<String>> fields) {
73          this.setStyleName("periscope-filter-wrapper");
74          this.setVisible(true);
75  
76          this.dateField = dateField;
77          this.i18n = i18n;
78          this.fields = fields;
79  
80          VerticalLayout periscopeFilter = new VerticalLayout();
81          periscopeFilter.setWidth(100, Unit.PERCENTAGE);
82          periscopeFilter.setMargin(false);
83          periscopeFilter.setSpacing(false);
84          periscopeFilter.addStyleName("periscope-filter");
85  
86          HorizontalLayout periscopeFilterHeader = initPeriscopeFilterHeader();
87  
88          this.periscopeFilterOptions = initPeriscopeFilterOptions();
89  
90          this.globalTokenLayout = initGlobalTokenLayout();
91  
92          this.fields.forEach(field -> this.periscopeFilterOptions.addComponent(initFieldLayout(field)));
93  
94          VerticalLayout editedFieldLayout = initRangeDateFieldLayout(dateField);
95          this.periscopeFilterOptions.addComponent(editedFieldLayout);
96  
97          this.periscopeFilterOptions.forEach(component -> component.addStyleName("filter-section"));
98  
99          periscopeFilter.addComponents(periscopeFilterHeader, periscopeFilterOptions, globalTokenLayout);
100         this.addComponent(periscopeFilter);
101     }
102 
103     private HorizontalLayout initPeriscopeFilterHeader() {
104         HorizontalLayout periscopeFilterHeader = new HorizontalLayout();
105         periscopeFilterHeader.setStyleName("periscope-filter-header-wrapper");
106         periscopeFilterHeader.setWidth(100, Unit.PERCENTAGE);
107         periscopeFilterHeader.setDefaultComponentAlignment(Alignment.BOTTOM_LEFT);
108 
109         titleLabel = new Label(i18n.translate("findbar.suggestions"));
110         titleLabel.setStyleName("periscope-filter-header-title");
111 
112         Label spacer = new Label();
113 
114         Button expandFilterOptionsButton = new Button(MagnoliaIcons.SEARCH_RESULT_FILTERS);
115         expandFilterOptionsButton.addStyleName(ResurfaceTheme.BUTTON_ICON);
116 
117         periscopeFilterHeader.addComponents(titleLabel, spacer, expandFilterOptionsButton);
118         periscopeFilterHeader.setComponentAlignment(expandFilterOptionsButton, Alignment.BOTTOM_RIGHT);
119         periscopeFilterHeader.setExpandRatio(titleLabel, 0);
120         periscopeFilterHeader.setExpandRatio(spacer, 1f);
121         periscopeFilterHeader.setExpandRatio(expandFilterOptionsButton, 0);
122 
123         expandFilterOptionsButton.addClickListener(click -> {
124             boolean optionsVisible = !periscopeFilterOptions.isVisible(); // negation because property represents new state after click
125             boolean noFiltersSet = fields.stream().allMatch(field -> CollectionUtils.isEmpty(field.getValue()))
126                     && dateField.getBeginDateField().getValue() == null
127                     && dateField.getEndDateField().getValue() == null;
128 
129             periscopeFilterOptions.setVisible(optionsVisible);
130             globalTokenLayout.removeAllComponents();
131             globalTokenLayout.setVisible(!optionsVisible && !noFiltersSet);
132             if (!optionsVisible && !noFiltersSet) {
133                 this.fields.forEach(field -> populateTokenLayout(globalTokenLayout, field));
134                 populateDateTokenLayout(globalTokenLayout, dateField);
135             }
136 
137             if (optionsVisible) {
138                 expandFilterOptionsButton.addStyleName("active");
139             } else {
140                 expandFilterOptionsButton.removeStyleName("active");
141             }
142         });
143         return periscopeFilterHeader;
144     }
145 
146     private CssLayout initGlobalTokenLayout() {
147         CssLayout globalTokenLayout = new CssLayout() {
148 
149             @Override
150             public void removeComponent(Component c) {
151                 super.removeComponent(c);
152                 if (getComponentCount() == 0) {
153                     setVisible(false);
154                 }
155             }
156         };
157 
158         globalTokenLayout.setStyleName("periscope-filter-global-token-layout");
159         globalTokenLayout.setWidth(100, Unit.PERCENTAGE);
160         globalTokenLayout.setVisible(false);
161         return globalTokenLayout;
162     }
163 
164     private HorizontalLayout initPeriscopeFilterOptions() {
165         HorizontalLayout periscopeFilterOptions = new HorizontalLayout();
166         periscopeFilterOptions.setSpacing(false);
167         periscopeFilterOptions.setStyleName("periscope-filter-options-wrapper");
168         periscopeFilterOptions.setWidth(100, Unit.PERCENTAGE);
169         periscopeFilterOptions.setVisible(false);
170         return periscopeFilterOptions;
171     }
172 
173     private VerticalLayout initFieldLayout(ComboBoxMultiselect<String> field) {
174         VerticalLayout fieldLayout = new VerticalLayout();
175         fieldLayout.setMargin(false);
176         field.setStyleGenerator(item -> "periscope-filter-item");
177         field.setWidth(100, Unit.PERCENTAGE);
178         field.setPlaceholder(i18n.translate("findbar.filter.placeholder"));
179         field.setPageLength(5);
180         CssLayout tokensLayout = new CssLayout();
181         tokensLayout.setStyleName("token-layout");
182         field.addValueChangeListener(event -> {
183             tokensLayout.removeAllComponents();
184             populateTokenLayout(tokensLayout, field);
185         });
186         fieldLayout.addComponents(field, tokensLayout);
187         return fieldLayout;
188     }
189 
190     private VerticalLayout initRangeDateFieldLayout(DateRangeField field) {
191         VerticalLayout fieldLayout = new VerticalLayout();
192         fieldLayout.setMargin(false);
193         field.setWidth(100, Unit.PERCENTAGE);
194         field.setStyleName("date-range-field");
195         field.getBeginDateField().setPlaceholder(i18n.translate("findbar.filter.date.begin"));
196         field.getEndDateField().setPlaceholder(i18n.translate("findbar.filter.date.end"));
197         LocalDate today = LocalDate.now();
198         field.addShortcut(i18n.translate("findbar.filter.date.today"), item -> item.setRange(today, today));
199         field.addShortcut(i18n.translate("findbar.filter.date.yesterday"), item -> item.setRange(today.minusDays(1), today.minusDays(1)));
200         field.addShortcut(i18n.translate("findbar.filter.date.pastweek"), item -> item.setRange(today.minusWeeks(1), today));
201         field.addShortcut(i18n.translate("findbar.filter.date.pastmonth"), item -> item.setRange(today.minusMonths(1), today));
202         field.addShortcut(i18n.translate("findbar.filter.date.pastyear"), item -> item.setRange(today.minusYears(1), today));
203         field.addShortcutSeparator();
204         field.addShortcut(i18n.translate("findbar.filter.clear"), DateRangeField::clear);
205         CssLayout tokensLayout = new CssLayout();
206         tokensLayout.setStyleName("token-layout");
207         field.addValueChangeListener(event -> {
208             tokensLayout.removeAllComponents();
209             populateDateTokenLayout(tokensLayout, field);
210         });
211         fieldLayout.addComponents(field, tokensLayout);
212         return fieldLayout;
213     }
214 
215     private void populateTokenLayout(CssLayout privateTokenLayout, ComboBoxMultiselect<String> field) {
216         field.getValue().forEach(token -> {
217             Button tokenButton = new Button(token, click -> {
218                 privateTokenLayout.removeComponent(click.getButton());
219                 field.deselect(click.getButton().getCaption());
220             });
221             tokenButton.setIcon(MagnoliaIcons.CLOSE);
222             tokenButton.addStyleName("periscope-token-button");
223             privateTokenLayout.addComponent(tokenButton);
224         });
225     }
226 
227     private void populateDateTokenLayout(CssLayout dateTokenLayout, DateRangeField dateRangeField) {
228         if (dateRangeField.getBeginDate() == null && dateRangeField.getEndDate() == null) {
229             return;
230         }
231         String label;
232         if (dateRangeField.getBeginDate() != null && dateRangeField.getEndDate() != null) {
233             label = dateRangeField.getBeginDate().format(createTimeFormatter(dateRangeField.getBeginDateField().getLocale()));
234             label += " - ";
235             label += dateRangeField.getEndDate().format(createTimeFormatter(dateRangeField.getEndDateField().getLocale()));
236         } else if (dateRangeField.getBeginDate() != null) {
237             label = "> ";
238             label += dateRangeField.getBeginDate().format(createTimeFormatter(dateRangeField.getBeginDateField().getLocale()));
239         } else {
240             label = "< ";
241             label += dateRangeField.getEndDate().format(createTimeFormatter(dateRangeField.getEndDateField().getLocale()));
242         }
243         Button tokenButton = new Button(label, click -> {
244             dateTokenLayout.removeComponent(click.getButton());
245             dateRangeField.clear();
246         });
247         tokenButton.setIcon(MagnoliaIcons.CLOSE);
248         tokenButton.addStyleName("periscope-token-button");
249         dateTokenLayout.addComponent(tokenButton);
250     }
251 
252     private DateTimeFormatter createTimeFormatter(Locale locale) {
253         DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
254         if (locale != null) {
255             return dateTimeFormatter.withLocale(locale);
256         }
257         return dateTimeFormatter;
258     }
259 
260     /**
261      * Update global token layout.
262      */
263     public void updateGlobalTokenLayout() {
264         globalTokenLayout.removeAllComponents();
265         fields.forEach(field -> populateTokenLayout(globalTokenLayout, field));
266         populateDateTokenLayout(globalTokenLayout, dateField);
267         globalTokenLayout.setVisible(!periscopeFilterOptions.isVisible() && globalTokenLayout.getComponentCount() > 0);
268     }
269 
270     public void updateTitleLabel(boolean areResultsSuggestions, int count) {
271         // We want grid to have 'Suggestions' as title, if those are not suggestions, then we want the result collection count.
272         if (!areResultsSuggestions) {
273             titleLabel.setValue(i18n.translate("findbar.results", count));
274         } else {
275             titleLabel.setValue(i18n.translate("findbar.suggestions"));
276         }
277     }
278 }