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.css.I_CmsWidgetsLayoutBundle; 031import org.opencms.gwt.client.ui.input.CmsSelectBox; 032import org.opencms.gwt.shared.attributeselect.I_CmsAttributeSelectData; 033import org.opencms.gwt.shared.attributeselect.I_CmsAttributeSelectData.AttributeDefinition; 034import org.opencms.gwt.shared.attributeselect.I_CmsAttributeSelectData.Option; 035import org.opencms.gwt.shared.attributeselect.I_CmsAttributeSelectData.OptionWithAttributes; 036import org.opencms.util.CmsStringUtil; 037 038import java.util.HashMap; 039import java.util.LinkedHashMap; 040import java.util.Map; 041import java.util.Objects; 042 043import com.google.common.collect.HashMultimap; 044import com.google.gwt.dom.client.Element; 045import com.google.gwt.event.dom.client.FocusEvent; 046import com.google.gwt.event.dom.client.FocusHandler; 047import com.google.gwt.event.logical.shared.ValueChangeEvent; 048import com.google.gwt.event.logical.shared.ValueChangeHandler; 049import com.google.gwt.event.shared.HandlerRegistration; 050import com.google.gwt.user.client.ui.Composite; 051import com.google.gwt.user.client.ui.FlowPanel; 052import com.google.gwt.user.client.ui.Label; 053 054/** 055 * An attribute select widget acts as a select widget and consists of several attribute filter select boxes and one main select box, such 056 * that choosing values from the attribute filters restricts the available options in the main select box to those which 057 * have a matching value for every filter attribute. 058 * 059 * <p>All data related to the options and filter attributes must be passed into the constructor, this widget does not use any RPC calls. 060 */ 061public class CmsAttributeSelectWidget extends Composite implements I_CmsEditWidget { 062 063 /** 064 * Class representing a pair of an attribute name and value, for use as a key in the option index. 065 */ 066 class IndexKey { 067 068 /** The attribute name. */ 069 private String m_name; 070 071 /** The attribute value. */ 072 private String m_value; 073 074 /** 075 * Creates a new instance. 076 * 077 * @param name the attribute name 078 * @param value the attribute value 079 */ 080 public IndexKey(String name, String value) { 081 082 m_name = name; 083 m_value = value; 084 } 085 086 /** 087 * @see java.lang.Object#equals(java.lang.Object) 088 */ 089 @Override 090 public boolean equals(Object o) { 091 092 if (!(o instanceof IndexKey)) { 093 return false; 094 } 095 IndexKey other = (IndexKey)o; 096 return other.m_name.equals(m_name) && other.m_value.equals(m_value); 097 } 098 099 /** 100 * @see java.lang.Object#hashCode() 101 */ 102 @Override 103 public int hashCode() { 104 105 return (31 * m_name.hashCode()) + m_value.hashCode(); 106 } 107 } 108 109 /** The panel containing everything else. */ 110 protected FlowPanel m_root = new FlowPanel(); 111 112 /** Tracks if the widget is active. */ 113 private boolean m_active = true; 114 115 /** Map of attribute definitions by name. */ 116 private Map<String, AttributeDefinition> m_attributeDefinitions = new HashMap<>(); 117 118 /** Map of attribute select boxes by attribute name. */ 119 private Map<String, CmsSelectBox> m_attributeSelects = new HashMap<>(); 120 121 /** Value set from the outside. */ 122 private String m_externalValue; 123 124 /** The main select box for actually choosing the widget value. */ 125 private CmsSelectBox m_mainSelect; 126 127 /** An index for quickly locating all options with a given attribute value. */ 128 private HashMultimap<IndexKey, String> m_optionIndex = HashMultimap.create(); 129 130 /** Map of all options. */ 131 private LinkedHashMap<String, OptionWithAttributes> m_options = new LinkedHashMap<>(); 132 133 /** 134 * Creates a new instance. 135 * 136 * @param data the widget data 137 */ 138 public CmsAttributeSelectWidget(I_CmsAttributeSelectData data) { 139 140 initWidget(m_root); 141 for (AttributeDefinition attrDef : data.getAttributeDefinitions()) { 142 m_attributeDefinitions.put(attrDef.getName(), attrDef); 143 CmsSelectBox selectBox = new CmsSelectBox(); 144 LinkedHashMap<String, String> selectBoxOptions = new LinkedHashMap<>(); 145 for (Option option : attrDef.getOptions()) { 146 selectBoxOptions.put(option.getValue(), option.getLabel()); 147 if (option.getHelpText() != null) { 148 selectBox.setTitle(option.getValue(), option.getHelpText()); 149 } 150 } 151 selectBox.setItems(selectBoxOptions); 152 // set the value before adding the change handler, since we already call handleFilterChange() below to initialize the main select box 153 selectBox.setFormValueAsString(getDefaultOption(attrDef)); 154 addFilterLine(attrDef.getLabel(), selectBox); 155 m_attributeSelects.put(attrDef.getName(), selectBox); 156 selectBox.addValueChangeHandler(event -> handleFilterChange()); 157 } 158 159 m_mainSelect = new CmsSelectBox(); 160 m_root.add(m_mainSelect); 161 LinkedHashMap<String, String> mainOptions = new LinkedHashMap<>(); 162 for (OptionWithAttributes option : data.getOptions()) { 163 if (option.getHelpText() != null) { 164 // we only need to set all the help texts once, they will be preserved during option changes 165 m_mainSelect.setTitle(option.getValue(), option.getHelpText()); 166 } 167 m_options.put(option.getValue(), option); 168 mainOptions.put(option.getValue(), option.getLabel()); 169 for (String attribute : option.getAttributes().keySet()) { 170 for (String attrValue : option.getAttributes().get(attribute)) { 171 m_optionIndex.put(new IndexKey(attribute, attrValue), option.getValue()); 172 } 173 } 174 } 175 m_mainSelect.addValueChangeHandler(event -> fireChangeEvent()); 176 handleFilterChange(); 177 } 178 179 /** 180 * Adds the focus handler. 181 * 182 * @param handler the handler 183 * @return the handler registration 184 * @see com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.google.gwt.event.dom.client.FocusHandler) 185 */ 186 public HandlerRegistration addFocusHandler(FocusHandler handler) { 187 188 return addDomHandler(handler, FocusEvent.getType()); 189 } 190 191 /** 192 * Adds the value change handler. 193 * 194 * @param handler the handler 195 * @return the handler registration 196 * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler) 197 */ 198 public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) { 199 200 return addHandler(handler, ValueChangeEvent.getType()); 201 } 202 203 /** 204 * Represents a value change event.<p> 205 * 206 */ 207 public void fireChangeEvent() { 208 209 String val = m_mainSelect.getFormValueAsString(); 210 if (val != null) { 211 ValueChangeEvent.fire(this, val); 212 } 213 214 } 215 216 /** 217 * @see com.google.gwt.user.client.ui.HasValue#getValue() 218 */ 219 public String getValue() { 220 221 String value = m_mainSelect.getFormValueAsString(); 222 return value; 223 } 224 225 /** 226 * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#isActive() 227 */ 228 public boolean isActive() { 229 230 return m_active; 231 } 232 233 /** 234 * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#onAttachWidget() 235 */ 236 public void onAttachWidget() { 237 238 super.onAttach(); 239 240 } 241 242 /** 243 * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#owns(com.google.gwt.dom.client.Element) 244 */ 245 public boolean owns(Element element) { 246 247 return false; 248 } 249 250 /** 251 * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setActive(boolean) 252 */ 253 public void setActive(boolean active) { 254 255 if (active == m_active) { 256 // Trying to set one value while initializing the widget can result in a different value being set. 257 // But at that time the event handler for the widget may not yet be set up correctly, so fireChangeEvent 258 // does nothing. So we have to fire the event here in setActive (which is called during widget 259 // initialization, after the change handler is set up). 260 if (active && !Objects.equals(getValue(), m_externalValue)) { 261 fireChangeEvent(); 262 } 263 return; 264 } 265 m_active = active; 266 m_mainSelect.setEnabled(active); 267 for (CmsSelectBox attrSelect : m_attributeSelects.values()) { 268 attrSelect.setEnabled(active); 269 } 270 if (active) { 271 fireChangeEvent(); 272 } 273 } 274 275 /** 276 * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setName(java.lang.String) 277 */ 278 public void setName(String name) { 279 280 // do nothing 281 282 } 283 284 /** 285 * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object) 286 */ 287 public void setValue(String value) { 288 289 setValue(value, false); 290 291 } 292 293 /** 294 * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setValue(java.lang.String, boolean) 295 */ 296 public void setValue(String value, boolean fireEvent) { 297 298 m_externalValue = value; 299 OptionWithAttributes option = m_options.get(value); 300 if (option != null) { 301 for (String optionAttribute : option.getAttributes().keySet()) { 302 m_attributeSelects.get(optionAttribute).setFormValue( 303 option.getAttributes().get(optionAttribute).get(0), 304 false); 305 } 306 handleFilterChange(); 307 m_mainSelect.setFormValue(value, false); 308 } else { 309 for (AttributeDefinition attrDef : m_attributeDefinitions.values()) { 310 CmsSelectBox select = m_attributeSelects.get(attrDef.getName()); 311 // the editor initializes new widgets with the empty value, so we want to use the default option instead 312 // the neutral option for the attribute in that case. 313 String attrSelectValue = CmsStringUtil.isEmpty(value) 314 ? getDefaultOption(attrDef) 315 : getNeutralOption(attrDef); 316 select.setFormValue(attrSelectValue, false); 317 } 318 handleFilterChange(); 319 m_mainSelect.setFormValue(value, false); 320 } 321 if (fireEvent) { 322 fireChangeEvent(); 323 } 324 } 325 326 /** 327 * Gets the currently selected attribute filters. 328 * 329 * @return a map with attribute names as its keys and attribute values as its values 330 */ 331 protected Map<String, String> getFilterAttributes() { 332 333 Map<String, String> result = new HashMap<>(); 334 for (Map.Entry<String, CmsSelectBox> entry : m_attributeSelects.entrySet()) { 335 String filterValue = entry.getValue().getFormValueAsString(); 336 result.put(entry.getKey(), filterValue); 337 } 338 return result; 339 } 340 341 /** 342 * Changes the set of available options in the main select box to those which match the currently 343 * selected attribute filters. 344 */ 345 protected void handleFilterChange() { 346 347 Map<String, String> attributes = getFilterAttributes(); 348 Map<String, String> newMainOptions = new LinkedHashMap<>(); 349 350 // first generate a map of all options, then reduce the keys for each chosen filter using the option index 351 352 for (Map.Entry<String, OptionWithAttributes> option : m_options.entrySet()) { 353 newMainOptions.put(option.getKey(), option.getValue().getLabel()); 354 } 355 for (Map.Entry<String, String> filterEntry : attributes.entrySet()) { 356 newMainOptions.keySet().retainAll( 357 m_optionIndex.get(new IndexKey(filterEntry.getKey(), filterEntry.getValue()))); 358 } 359 m_mainSelect.setItems(newMainOptions); 360 fireChangeEvent(); 361 } 362 363 /** 364 * Adds a new line with an attribute filter select box and a label. 365 * 366 * @param label the label 367 * @param selectBox the select box 368 */ 369 private void addFilterLine(String label, CmsSelectBox selectBox) { 370 371 FlowPanel filterLine = new FlowPanel(); 372 filterLine.add(new Label(label)); 373 filterLine.add(selectBox); 374 filterLine.addStyleName(I_CmsWidgetsLayoutBundle.INSTANCE.widgetCss().attributeFilterLine()); 375 m_root.add(filterLine); 376 377 } 378 379 /** 380 * Gets the default option for an attribute. 381 * @param attrDef the attribute 382 * @return the default option 383 */ 384 private String getDefaultOption(AttributeDefinition attrDef) { 385 386 if (attrDef.getDefaultOption() != null) { 387 return attrDef.getDefaultOption(); 388 } 389 390 return attrDef.getOptions().get(0).getValue(); 391 392 } 393 394 /** 395 * Gets the filter attribute value to use if no other filter attribute value can be used. 396 * 397 * @param attrDef the attribute definition 398 * 399 * @return the neutral option 400 */ 401 private String getNeutralOption(AttributeDefinition attrDef) { 402 403 if (attrDef.getNeutralOption() != null) { 404 return attrDef.getNeutralOption(); 405 } 406 return attrDef.getOptions().get(0).getValue(); 407 } 408 409}