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}