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