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.I_CmsHasInit;
031import org.opencms.gwt.client.ui.I_CmsAutoHider;
032import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle;
033import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle.I_CmsFilterSelectCss;
034import org.opencms.gwt.client.ui.input.form.CmsWidgetFactoryRegistry;
035import org.opencms.gwt.client.ui.input.form.I_CmsFormWidgetFactory;
036
037import java.util.HashMap;
038import java.util.LinkedHashMap;
039import java.util.Map;
040
041import com.google.common.base.Optional;
042import com.google.gwt.dom.client.InputElement;
043import com.google.gwt.user.client.Event;
044import com.google.gwt.user.client.Timer;
045import com.google.gwt.user.client.ui.FocusPanel;
046import com.google.gwt.user.client.ui.TextBox;
047
048import elemental2.dom.Element.FocusOptionsType;
049import elemental2.dom.HTMLInputElement;
050import jsinterop.base.Js;
051
052/**
053 * Select box that allows client-side filtering for its options.
054 *
055 * <p>Filtering is done by a case-insensitive substring test on the user-readable select option texts.
056 */
057public class CmsFilterSelectBox extends A_CmsSelectBox<CmsLabelSelectCell> implements I_CmsHasInit {
058
059    /** The widget type identifier. */
060    public static final String WIDGET_TYPE = "filterselect";
061
062    /** The CSS bundle. */
063    private static final I_CmsFilterSelectCss FILTERSELECT_CSS = I_CmsLayoutBundle.INSTANCE.filterSelectCss();
064
065    /** The text box used for filtering. */
066    private TextBox m_filterBox;
067
068    /** The currently active timer that, when it fires, will update the filtering. */
069    private Timer m_filterTimer;
070
071    /** The cached items. */
072    private LinkedHashMap<String, String> m_cachedItems;
073
074    /** A map of titles for the select options which should  be displayed on mouseover. */
075    private Map<String, String> m_titles = new HashMap<String, String>();
076
077    /** The last known filter box text. */
078    private String m_lastFilterText;
079
080    /**
081     * Creates a new instance.
082     */
083    public CmsFilterSelectBox() {
084
085        addStyleName(FILTERSELECT_CSS.filterSelect());
086        sinkEvents(Event.ONMOUSEWHEEL);
087    }
088
089    /**
090     * Creates a new instance.
091     *
092     * @param options the select options
093     */
094    public CmsFilterSelectBox(Map<String, String> options) {
095
096        this();
097        setItems(options);
098    }
099
100    /**
101     * Initializes this class.<p>
102     */
103    public static void initClass() {
104
105        CmsWidgetFactoryRegistry.instance().registerFactory(WIDGET_TYPE, new I_CmsFormWidgetFactory() {
106
107            /**
108             * @see org.opencms.gwt.client.ui.input.form.I_CmsFormWidgetFactory#createWidget(java.util.Map, com.google.common.base.Optional)
109             */
110            public I_CmsFormWidget createWidget(Map<String, String> widgetParams, Optional<String> defaultValue) {
111
112                return new CmsFilterSelectBox(new LinkedHashMap<>(widgetParams));
113            }
114        });
115
116    }
117
118    /**
119     * @see org.opencms.gwt.client.ui.input.A_CmsSelectBox#addOption(org.opencms.gwt.client.ui.input.A_CmsSelectCell)
120     */
121    @Override
122    public void addOption(CmsLabelSelectCell cell) {
123
124        super.addOption(cell);
125        m_cachedItems = null;
126    }
127
128    /**
129     * @see org.opencms.gwt.client.ui.input.A_CmsSelectBox#displayingAbove()
130     */
131    @Override
132    public boolean displayingAbove() {
133
134        return false;
135    }
136
137    /**
138     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#getApparentValue()
139     */
140    public String getApparentValue() {
141
142        return getFormValueAsString();
143
144    }
145
146    /**
147     * Gets the selection items as a map (the values are map keys, and the labels are the corresponding map values).
148     *
149     * @return the selection items as a map
150     */
151    public LinkedHashMap<String, String> getItems() {
152
153        if (m_cachedItems == null) {
154            m_cachedItems = new LinkedHashMap<>();
155            for (Map.Entry<String, CmsLabelSelectCell> entry : m_selectCells.entrySet()) {
156                CmsLabelSelectCell cell = entry.getValue();
157                m_cachedItems.put(cell.getValue(), cell.getText());
158            }
159        }
160        return m_cachedItems;
161    }
162
163    /**
164     * Gets the opener.
165     *
166     * @return the opener
167     */
168    public FocusPanel getOpener() {
169
170        return m_opener;
171    }
172
173    /**
174     * @see org.opencms.gwt.client.ui.input.A_CmsSelectBox#onBrowserEvent(com.google.gwt.user.client.Event)
175     */
176    @Override
177    public void onBrowserEvent(Event event) {
178
179        if (event.getTypeInt() == Event.ONMOUSEWHEEL) {
180            event.preventDefault();
181            event.stopPropagation();
182        } else {
183            super.onBrowserEvent(event);
184        }
185    }
186
187    /**
188     * @see org.opencms.gwt.client.ui.input.I_CmsFormWidget#setAutoHideParent(org.opencms.gwt.client.ui.I_CmsAutoHider)
189     */
190    public void setAutoHideParent(I_CmsAutoHider autoHideParent) {
191
192        // nothing to do
193
194    }
195
196    /**
197     * @see org.opencms.gwt.client.ui.input.A_CmsSelectBox#setEnabled(boolean)
198     */
199    @Override
200    public void setEnabled(boolean enabled) {
201
202        super.setEnabled(enabled);
203        m_filterBox.setEnabled(enabled);
204    }
205
206    /**
207     * Sets the select options.
208     *
209     * @param options the select options
210     */
211    public void setItems(Map<String, String> options) {
212
213        clearItems();
214        for (Map.Entry<String, String> entry : options.entrySet()) {
215            String title = m_titles.get(entry.getKey());
216            addOption(new CmsLabelSelectCell(entry.getKey(), entry.getValue().trim(), title));
217        }
218    }
219
220    /**
221     * Sets the title for a select option.
222     *
223     * @param key the select option key
224     * @param title the title
225     */
226    public void setTitle(String key, String title) {
227
228        m_titles.put(key, title);
229
230    }
231
232    /**
233     * @see org.opencms.gwt.client.ui.input.A_CmsSelectBox#close()
234     */
235    @Override
236    protected void close() {
237
238        super.close();
239
240        // Use a timer for the case where the select box is currently opened, the text box has focus, and the user clicks on the text box again
241        Timer blurTimer = new Timer() {
242
243            @SuppressWarnings("synthetic-access")
244            @Override
245            public void run() {
246
247                InputElement inputElem = m_filterBox.getElement().cast();
248                inputElem.blur();
249            }
250        };
251        blurTimer.schedule(0);
252        String text = getOptionText(getFormValueAsString());
253        m_lastFilterText = text;
254        m_filterBox.setValue(text);
255        HTMLInputElement input = Js.cast(m_filterBox.getElement());
256        input.scrollLeft = 0;
257    }
258
259    /**
260     * @see org.opencms.gwt.client.ui.input.A_CmsSelectBox#createUnknownOption(java.lang.String)
261     */
262    @Override
263    protected CmsLabelSelectCell createUnknownOption(String value) {
264
265        return new CmsLabelSelectCell(value, value);
266    }
267
268    /**
269     * Updates the visibility of select options based on the given filter string.
270     *
271     * <p>An option matches the filter if the display text contains the filter string as a substring, without regard
272     * for case.
273     *
274     * @param filter the filter string
275     */
276    protected void filterCells(String filter) {
277
278        String lowerCaseFilter = null;
279        if (filter != null) {
280            lowerCaseFilter = filter.toLowerCase();
281        }
282        for (Map.Entry<String, CmsLabelSelectCell> entry : m_selectCells.entrySet()) {
283            boolean show = (filter == null) || entry.getValue().getText().toLowerCase().contains(lowerCaseFilter);
284            entry.getValue().setVisible(show);
285        }
286    }
287
288    /**
289     * @see org.opencms.gwt.client.ui.input.A_CmsSelectBox#initOpener()
290     */
291    @Override
292    protected void initOpener() {
293
294        m_filterBox = new TextBox();
295        m_filterBox.addStyleName(FILTERSELECT_CSS.filterInput());
296        m_opener.setWidget(m_filterBox);
297        // when the user types very fast, we don't want to update the filtering
298        // after every keypress, so we 'debounce' the event handling using a timer
299        m_filterBox.addKeyDownHandler(event -> {
300            if (m_filterTimer != null) {
301                m_filterTimer.cancel();
302            }
303            m_filterTimer = new Timer() {
304
305                @SuppressWarnings("synthetic-access")
306                @Override
307                public void run() {
308
309                    m_filterTimer = null;
310                    if (m_openClose.isDown()) {
311                        String newInputValue = m_filterBox.getValue();
312                        if (!newInputValue.equals(m_lastFilterText)) {
313                            m_lastFilterText = newInputValue;
314                            filterCells(newInputValue);
315                        }
316
317                    }
318
319                }
320            };
321            m_filterTimer.schedule(150);
322        });
323    }
324
325    /**
326     * @see org.opencms.gwt.client.ui.input.A_CmsSelectBox#open()
327     */
328    @Override
329    protected void open() {
330
331        filterCells(null); // reset the filter before opening the select box
332        super.open();
333        HTMLInputElement input = Js.cast(m_filterBox.getElement());
334        // if the content of the input is long, the browser 'helpfully' scrolls to the right when we
335        // focus it and select the text. We don't want that (the left part is more relevant to the user),
336        // so we set a special option to prevent that.
337        FocusOptionsType options = FocusOptionsType.create();
338        options.setPreventScroll(true);
339        input.focus(options);
340        // by selecting the content of the text box, the user can still see the previous value,
341        // but can start filtering immediately because the text they type replaces the selected value
342        input.select();
343        CmsLabelSelectCell cell = m_selectCells.get(getFormValueAsString());
344        if (cell != null) {
345            cell.getElement().scrollIntoView();
346        }
347        input.scrollLeft = 0;
348    }
349
350    /**
351     * @see org.opencms.gwt.client.ui.input.A_CmsSelectBox#truncateOpener(java.lang.String, int)
352     */
353    @Override
354    protected void truncateOpener(String prefix, int width) {
355
356        // not using truncation
357    }
358
359    /**
360     * @see org.opencms.gwt.client.ui.input.A_CmsSelectBox#updateOpener(java.lang.String)
361     */
362    @Override
363    protected void updateOpener(String newValue) {
364
365        String text = getOptionText(newValue);
366        m_filterBox.setValue(text);
367        m_lastFilterText = text;
368        String title = m_titles.get(newValue);
369        if (title == null) {
370            title = text;
371        }
372        m_filterBox.setTitle(title);
373    }
374
375    /**
376     * Gets the user-readable text for the option.
377     *
378     * @param key the key for the option
379     * @return the user-readable text for the option
380     */
381    private String getOptionText(String key) {
382
383        CmsLabelSelectCell cell = m_selectCells.get(key);
384        if (cell != null) {
385            return cell.getText();
386        }
387        return "";
388    }
389
390}