/*
* #%L
* carewebframework
* %%
* Copyright (C) 2008 - 2016 Regenstrief Institute, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This Source Code Form is also subject to the terms of the Health-Related
* Additional Disclaimer of Warranty and Limitation of Liability available at
*
* http://www.carewebframework.org/licensing/disclaimer.
*
* #L%
*/
package org.carewebframework.ui.wonderbar;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.zkoss.json.JSONAware;
import org.zkoss.text.MessageFormats;
import org.zkoss.util.resource.Labels;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.out.AuInvoke;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.sys.ContentRenderer;
import org.zkoss.zul.impl.InputElement;
/**
* This is the main ZK component for the wonder bar. It supports both server-side searching for
* searching through large lists of items and client/browser-side searching for smaller lists of
* items.
*
* @param <T> Type returned by search provider.
*/
public class Wonderbar<T> extends InputElement {
/**
* Controls behavior of client-side searches.
*/
public enum MatchMode implements JSONAware {
ANY_ORDER, // May match any any order
SAME_ORDER, // Must match in same order, not necessarily contiguous
ADJACENT, // Must match in same contiguous order, not necessarily at start
FROM_START; // Must match in same order at the start
@Override
public String toJSONString() {
return Integer.toString(ordinal());
}
};
private static final long serialVersionUID = 1L;
private final String MESSAGE_TOO_MANY = Labels.getLabel("cwf.wonderbar.items.truncated",
"<< More than {0} items were found for the current input. Please enter more characters. >>");
private boolean changeOnOKOnly;
private WonderbarDefaults defaultItems;
private WonderbarItems items;
private boolean isClient;
private int maxSearchResults;
private int minSearchCharacters = 3;
private IWonderbarSearchProvider<T> searchProvider;
private IWonderbarItemRenderer<T> renderer;
private boolean selectFirstItem = true;
private boolean openOnFocus;
private WonderbarItem selectedItem;
private final WonderbarGroup truncItem;
private int clientThreshold = 100;
private MatchMode clientMatchMode = MatchMode.ANY_ORDER;
static {
addClientEvent(Wonderbar.class, WonderbarSelectEvent.ON_WONDERBAR_SELECT, CE_IMPORTANT | CE_NON_DEFERRABLE);
addClientEvent(Wonderbar.class, WonderbarSearchEvent.ON_WONDERBAR_SEARCH, CE_IMPORTANT | CE_NON_DEFERRABLE);
addClientEvent(Wonderbar.class, Events.ON_FOCUS, CE_DUPLICATE_IGNORE);
addClientEvent(Wonderbar.class, Events.ON_BLUR, CE_DUPLICATE_IGNORE);
addClientEvent(Wonderbar.class, Events.ON_ERROR, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
}
public Wonderbar() {
super();
truncItem = new WonderbarGroup();
truncItem.setZclass("cwf-wonderbar-item-more");
setMaxSearchResults(100);
}
@Override
public void renderProperties(ContentRenderer renderer) throws IOException {
super.renderProperties(renderer);
renderer.render("_minLength", minSearchCharacters);
renderer.render("_maxResults", maxSearchResults);
renderer.render("_openOnFocus", openOnFocus);
renderer.render("_skipTab", changeOnOKOnly);
renderer.render("_selectFirst", selectFirstItem);
renderer.render("_clientMode", isClient);
renderer.render("_matchMode", clientMatchMode);
renderer.render("_truncItem", truncItem);
}
@Override
public String getZclass() {
return _zclass == null ? "cwf-wonderbar" : _zclass;
}
/**
* Processes the onWonderbarSelect and onWonderbarSearch events.
*/
@Override
public void service(AuRequest request, boolean everError) {
String cmd = request.getCommand();
if (cmd.equals(WonderbarSelectEvent.ON_WONDERBAR_SELECT)) {
WonderbarSelectEvent event = WonderbarSelectEvent.getSelectEvent(request);
if (selectedItem != event.getSelectedItem()) {
selectedItem = event.getSelectedItem();
Events.postEvent(event);
}
} else if (cmd.equals(WonderbarSearchEvent.ON_WONDERBAR_SEARCH)) {
WonderbarSearchEvent event = WonderbarSearchEvent.getSearchEvent(request);
Events.postEvent(event);
} else {
super.service(request, everError);
}
}
/**
* Validates attempt to add a child.
*/
@Override
public void beforeChildAdded(Component child, Component insertBefore) {
super.beforeChildAdded(child, insertBefore);
if (child instanceof WonderbarDefaults) {
if (defaultItems != null && defaultItems != child) {
throw new UiException("Default items already specified.");
}
} else if (child instanceof WonderbarItems) {
if (items != null && items != child) {
throw new UiException("Items already specified.");
}
} else {
throw new UiException("Unsupported child for Wonderbar: " + child);
}
}
/**
* Updates defaultItems and items when a child is added.
*/
@Override
public boolean insertBefore(Component child, Component refChild) {
if (child instanceof WonderbarDefaults) {
if (super.insertBefore(child, refChild)) {
defaultItems = (WonderbarDefaults) child;
return true;
}
} else if (child instanceof WonderbarItems) {
if (super.insertBefore(child, refChild)) {
items = (WonderbarItems) child;
return true;
}
} else {
return super.insertBefore(child, refChild);
}
return false;
}
/**
* Updates defaultItems and items when a child is removed.
*/
@Override
public void beforeChildRemoved(Component child) {
super.beforeChildRemoved(child);
if (defaultItems == child) {
defaultItems = null;
} else if (items == child) {
items = null;
}
}
@Override
protected Object coerceFromString(String value) throws WrongValueException {
return value == null ? "" : value;
}
@Override
protected String coerceToString(Object value) {
return value == null ? "" : value.toString();
}
/**
* Clear the current selection.
*/
public void clear() {
setText("");
initItems(null, false, false);
}
/**
* Invokes a search. Supports both client- and server-side searching.
*
* @param term Term to search for.
*/
public void doSearch(String term) {
doSearch(term, false);
}
/**
* Invokes a search. Supports both client- and server-side searching.
*
* @param term Term to search for.
* @param fromClient If true, request came from client.
*/
protected void doSearch(String term, boolean fromClient) {
term = term == null ? "" : term.trim();
if (!fromClient) {
setText(term);
invoke("search", term);
focus();
} else {
IWonderbarServerSearchProvider<T> provider = (IWonderbarServerSearchProvider<T>) getSearchProvider();
if (provider != null) {
List<T> hits = new ArrayList<>();
boolean tooMany = !provider.getSearchResults(term, maxSearchResults, hits);
initItems(hits, tooMany, false);
invoke("_serverResponse", term);
}
}
}
/**
* Handles the client request for a server-based search.
*
* @param event The search event.
*/
public void onWonderbarSearch(WonderbarSearchEvent event) {
doSearch(event.getTerm(), true);
}
/**
* Open the drop down.
*/
public void open() {
invoke("_open");
}
/**
* Close the drop down.
*/
public void close() {
invoke("_close");
}
/**
* Invokes a function on the client.
*
* @param fnc Function name.
* @param args Function arguments.
*/
private void invoke(String fnc, Object... args) {
response(new AuInvoke(this, fnc, args));
}
@Override
public String getWidgetClass() {
return "wonderbar.ext.Wonderbar";
}
@Override
protected boolean isChildable() {
return true;
}
public WonderbarDefaults getDefaultItems() {
return this.defaultItems;
}
@Override
public void onPageAttached(Page newpage, Page oldpage) {
super.onPageAttached(newpage, oldpage);
truncItem.setPage(newpage);
}
/**
* @return true if this wonderbar is currently running in client mode, false if server mode
*/
public boolean isClientMode() {
return this.isClient;
}
protected void setClientMode(boolean value) {
if (!(value ? IWonderbarClientSearchProvider.class : IWonderbarServerSearchProvider.class)
.isInstance(searchProvider)) {
throw new IllegalStateException("Search provider not compatible with selected mode.");
}
this.isClient = value;
smartUpdate("_clientMode", value);
initItems(searchProvider.getDefaultItems(), false, true);
initItems(value ? ((IWonderbarClientSearchProvider<T>) searchProvider).getAllItems() : null, false, false);
}
/**
* Initializes the searchable item list.
*
* @param list The list of searchable items. Replaces the existing list. A null value clears the
* list.
* @param tooMany If true, search was truncated because of too many results.
* @param defaults If true, initialize the default items.
*/
private void initItems(List<T> list, boolean tooMany, boolean defaults) {
WonderbarItems parent = defaults ? defaultItems : items;
if (list != null) {
if (parent != null) {
parent.clear();
} else {
parent = defaults ? new WonderbarDefaults() : new WonderbarItems();
appendChild(parent);
}
for (T data : list) {
WonderbarItem item = new WonderbarItem();
parent.appendChild(item);
if (renderer != null) {
renderer.render(item, data, parent.getChildren().size() - 1);
} else {
item.setLabel(data.toString());
item.setValue(data.toString());
item.setData(data);
}
}
if (tooMany) {
parent.appendChild(truncItem);
}
} else if (parent != null) {
parent.clear();
}
selectedItem = null;
}
/**
* Returns true if item selection occurs only when the enter key is pressed, or false if item
* selection also occurs when tabbing away from the component.
*
* @return Change-on-OK setting.
*/
public boolean getChangeOnOKOnly() {
return changeOnOKOnly;
}
/**
* Sets item selection behavior. Set to true if item selection occurs only when the enter key is
* pressed, or false if item selection also occurs when tabbing away from the component.
*
* @param value Change-on-OK setting.
*/
public void setChangeOnOKOnly(boolean value) {
this.changeOnOKOnly = value;
smartUpdate("_skipTab", value);
}
/**
* Returns true if the wonder bar menu should appear when the component receives focus.
*
* @return The open-on-focus setting.
*/
public boolean getOpenOnFocus() {
return openOnFocus;
}
/**
* Set to true if the wonder bar menu should appear when the component receives focus.
*
* @param value The open-on-focus setting.
*/
public void setOpenOnFocus(boolean value) {
this.openOnFocus = value;
smartUpdate("_openOnFocus", value);
}
/**
* Returns the maximum search results to be returned by the search provider.
*
* @return The max number of search results to return.
*/
public int getMaxSearchResults() {
return this.maxSearchResults;
}
/**
* Sets the maximum search results to be returned by the search provider.
*
* @param value The max number of search results to return.
*/
public void setMaxSearchResults(int value) {
this.maxSearchResults = value;
smartUpdate("_maxResults", value);
truncItem.setLabel(MessageFormats.format(MESSAGE_TOO_MANY, new Object[] { value }));
}
/**
* Returns the minimum number of characters that must be typed before search results are
* displayed.
*
* @return Minimum search characters.
*/
public int getMinSearchCharacters() {
return this.minSearchCharacters;
}
/**
* Set the minimum number of characters that must be typed before search results are displayed
*
* @param value Minimum search characters.
*/
public void setMinSearchCharacters(int value) {
this.minSearchCharacters = value;
smartUpdate("_minLength", value);
}
/**
* For search providers that support both client- and server-side searching, this parameter
* determines which search strategy is employed based on the total number of searchable items.
* When the number of searchable items exceeds this threshold, the server-based strategy is
* employed.
*
* @return The client search threshold.
*/
public int getClientThreshold() {
return clientThreshold;
}
/**
* For search providers that support both client- and server-side searching, this parameter
* determines which search strategy is employed based on the total number of searchable items.
* When the number of searchable items exceeds this threshold, the server-based strategy is
* employed.
*
* @param value The client search threshold.
*/
public void setClientThreshold(int value) {
if (this.clientThreshold != value) {
this.clientThreshold = value;
if (searchProvider != null) {
init(false);
}
}
}
/**
* Returns the match mode to be used for client-based searches.
*
* @return The client match mode.
*/
public MatchMode getClientMatchMode() {
return clientMatchMode;
}
/**
* Sets the match mode to be used by the client. Does not affect server-based searching.
*
* @param clientMatchMode The client match mode.
*/
public void setClientMatchMode(MatchMode clientMatchMode) {
this.clientMatchMode = clientMatchMode;
smartUpdate("_matchMode", clientMatchMode);
}
public String getValue() throws WrongValueException {
return getText();
}
public void setValue(String value) throws WrongValueException {
setText(value);
}
/**
* Returns the currently selected item.
*
* @return The currently selected item.
*/
public WonderbarItem getSelectedItem() {
return selectedItem;
}
/**
* Returns the data associated with the currently selected item.
*
* @return Data of the currently selected item.
*/
public Object getSelectedData() {
return selectedItem == null ? null : selectedItem.getData();
}
/**
* Selects the specified item on the client and fires an onWonderbarSelect event.
*
* @param selectedItem The item to be selected.
*/
public void setSelectedItem(WonderbarItem selectedItem) {
setSelectedItem(selectedItem, true);
}
/**
* Selects the specified item on the client and fires an onWonderbarSelect event.
*
* @param selectedItem The item to be selected.
* @param fireEvent If true, an onWonderbarSelect event will be fired.
*/
public void setSelectedItem(WonderbarItem selectedItem, boolean fireEvent) {
if (selectedItem != this.selectedItem) {
if (selectedItem != null && (selectedItem.getParent() == null || selectedItem.getParent().getParent() != this)) {
throw new UiException("Item does not belong to this parent.");
}
this.selectedItem = selectedItem;
invoke("_selectItem", selectedItem);
if (fireEvent) {
Events.postEvent(WonderbarSelectEvent.ON_WONDERBAR_SELECT, this, null);
}
}
}
/**
* Creates a wonderbar item and sets it as the current selection.
*
* @param label Label for the item.
* @param data Data for the item.
*/
public void setSelectedItem(String label, Object data) {
setSelectedItem(label, data, true);
}
/**
* Creates a wonderbar item and sets it as the current selection.
*
* @param label Label for the item.
* @param data Data for the item.
* @param fireEvent If true, an onWonderbarSelect event will be fired.
*/
public void setSelectedItem(String label, Object data, boolean fireEvent) {
WonderbarItem item = new WonderbarItem(label, null, data);
if (this.items == null) {
appendChild(new WonderbarItems());
}
this.items.appendChild(item);
setSelectedItem(item, fireEvent);
}
/**
* Returns the search provider.
*
* @return The search provider.
*/
public IWonderbarSearchProvider<T> getSearchProvider() {
return this.searchProvider;
}
/**
* Set the search provider. This must be an instance of IWonderbarServerSearchProvider,
* IWonderbarClientSearchProvider, or both.
*
* @param searchProvider The search provider.
*/
public void setSearchProvider(IWonderbarSearchProvider<T> searchProvider) {
this.searchProvider = searchProvider;
init(true);
}
/**
* Renderer for searchable and default items.
*
* @return The item renderer.
*/
public IWonderbarItemRenderer<T> getItemRenderer() {
return renderer;
}
/**
* Sets the renderer for searchable and default items. If none is specified, a default renderer
* will be used.
*
* @param renderer The item renderer.
*/
public void setItemRenderer(IWonderbarItemRenderer<T> renderer) {
this.renderer = renderer;
}
/**
* Initializes the component.
*
* @param force If true, forces initialization even if mode does not change.
*/
private void init(boolean force) {
boolean isclient = searchProvider instanceof IWonderbarClientSearchProvider;
boolean isserver = searchProvider instanceof IWonderbarServerSearchProvider;
if (isclient && isserver) {
isclient = ((IWonderbarClientSearchProvider<T>) searchProvider).getAllItems().size() <= getClientThreshold();
}
if (force || isclient != isClient) {
setClientMode(isclient);
}
}
/**
* Returns true if the first selectable item in the wonder bar menu should be selected by
* default.
*
* @return The select first item setting.
*/
public boolean isSelectFirstItem() {
return selectFirstItem;
}
/**
* Set to true if the first selectable item in the wonder bar menu should be selected by
* default.
*
* @param value The select first item setting.
*/
public void setSelectFirstItem(boolean value) {
this.selectFirstItem = value;
smartUpdate("_selectFirst", value);
}
}