/*
* Ext GWT - Ext for GWT
* Copyright(c) 2007, 2008, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
package com.extjs.gxt.ui.client.widget;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import com.extjs.gxt.ui.client.Events;
import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.core.CompositeElement;
import com.extjs.gxt.ui.client.core.DomQuery;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.core.XTemplate;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.ModelProcessor;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.ListViewEvent;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.store.StoreEvent;
import com.extjs.gxt.ui.client.store.StoreListener;
import com.extjs.gxt.ui.client.util.Util;
import com.extjs.gxt.ui.client.widget.tips.QuickTip;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
/**
* A mechanism for displaying data using custom layout templates. ListView uses
* an {@link XTemplate} as its internal templating mechanism.
* <p>
* <b>In order to use these features, an {@link #setItemSelector(String)} must
* be provided for the ListView to determine what nodes it will be working
* with.</b>
* </p>
*
* <dt><b>Events:</b></dt>
*
* <dd><b>Select</b> : ListViewEvent(listView, event)<br>
* <div>Fires when a template node is clicked.</div>
* <ul>
* <li>listView : this</li>
* <li>event : the dom event</li>
* <li>index : the index of the target node</li>
* </ul>
* </dd>
*
* <dd><b>DoubleClick</b> : ListViewEvent(listView, index, element, event)<br>
* <div>Fires when a template node is double clicked.</div>
* <ul>
* <li>listView : this</li>
* <li>index : the index of the target node</li>
* <li>element : the target nodet</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* <dd><b>ContextMenu</b> : ListViewEvent(listView, index, element, event)<br>
* <div>Fires when a template node is right clicked.</div>
* <ul>
* <li>listView : this</li>
* <li>index : the index of the target node</li>
* <li>element : the target node</li>
* <li>event : the dom event</li>
* </ul>
* </dd>
*
* </dt>
*/
public class ListView<M extends ModelData> extends BoxComponent {
protected ListStore<M> store;
private String selectStyle = "x-view-item-sel";
private String overStyle = "x-view-item-over";
private String itemSelector = ".x-view-item";
private boolean selectOnHover;
private CompositeElement all;
private ListViewSelectionModel<M> sm;
private XTemplate template;
private boolean initial;
private StoreListener storeListener;
private String displayProperty = "text";
private String loadingText;
private Element overElement;
private ModelProcessor<M> modelProcessor;
/**
* Creates a new view.
*/
public ListView() {
initComponent();
setSelectionModel(new ListViewSelectionModel<M>());
all = new CompositeElement();
baseStyle = "x-view";
focusable = true;
new QuickTip(this);
}
/**
* Creates a new view.
*/
public ListView(ListStore<M> store) {
this();
setStore(store);
}
/**
* Creates a new template list.
*
* @param template the template
*/
public ListView(ListStore<M> store, XTemplate template) {
this(store);
this.template = template;
}
/**
* Returns the matching element.
*
* @param element the element or any child element
* @return the parent element
*/
public Element findElement(Element element) {
return fly(element).findParentElement(itemSelector, 5);
}
/**
* Returns the element's index.
*
* @param element the element or any child element
* @return the element index or -1 if no match
*/
public int findElementIndex(Element element) {
Element elem = findElement(element);
if (elem != null) {
return indexOf(elem);
}
return -1;
}
/**
* Returns the display property.
*
* @return the display property
*/
public String getDisplayProperty() {
return displayProperty;
}
/**
* Returns the element at the given index.
*
* @param index the index
* @return the element
*/
public Element getElement(int index) {
return all.getElement(index);
}
/**
* Returns all of the child elements.
*
* @return the elements
*/
public List<Element> getElements() {
return all.getElements();
}
/**
* Returns the number of models in the view.
*
* @return the count
*/
public int getItemCount() {
return store.getCount();
}
/**
* Returns the item selector.
*
* @return the selector
*/
public String getItemSelector() {
return itemSelector;
}
public String getLoadingText() {
return loadingText;
}
/**
* Returns the model processor.
*
* @return the model processor
*/
public ModelProcessor<M> getModelProcessor() {
return modelProcessor;
}
/**
* Returns the over style.
*
* @return the over style
*/
public String getOverStyle() {
return overStyle;
}
/**
* Returns the view's selection model.
*
* @return the selection model
*/
public ListViewSelectionModel<M> getSelectionModel() {
return sm;
}
/**
* Returns true if select on hover is enabled.
*
* @return the select on hover state
*/
public boolean getSelectOnOver() {
return selectOnHover;
}
/**
* Returns the select style.
*
* @return the select style
*/
public String getSelectStyle() {
return selectStyle;
}
/**
* Returns the combo's stote.
*
* @return the store
*/
public ListStore<M> getStore() {
return store;
}
/**
* Returns the list's template.
*
* @return the template
*/
public XTemplate getTemplate() {
return template;
}
/**
* Reuturns the index of the element.
*
* @param element the element
* @return the index
*/
public int indexOf(Element element) {
if (element.getPropertyString("viewIndex") != null) {
return element.getPropertyInt("viewIndex");
}
return all.indexOf(element);
}
/**
* Moves the current selections down one level.
*/
public void moveSelectedDown() {
List<M> sel = getSelectionModel().getSelectedItems();
Collections.sort(sel, new Comparator<M>() {
public int compare(M o1, M o2) {
return store.indexOf(o1) < store.indexOf(o2) ? 1 : 0;
}
});
for (M m : sel) {
int idx = store.indexOf(m);
if (idx < (store.getCount() - 1)) {
store.remove(m);
store.insert(m, idx + 1);
}
}
getSelectionModel().select(sel);
}
/**
* Moves the current selections up one level.
*/
public void moveSelectedUp() {
List<M> sel = getSelectionModel().getSelectedItems();
Collections.sort(sel, new Comparator<M>() {
public int compare(M o1, M o2) {
return store.indexOf(o1) > store.indexOf(o2) ? 1 : 0;
}
});
for (M m : sel) {
int idx = store.indexOf(m);
if (idx > 0) {
store.remove(m);
store.insert(m, idx - 1);
}
}
getSelectionModel().select(sel);
}
public void onComponentEvent(ComponentEvent ce) {
super.onComponentEvent(ce);
ListViewEvent le = (ListViewEvent) ce;
switch (ce.type) {
case Event.ONMOUSEOVER:
onMouseOver(le);
break;
case Event.ONMOUSEOUT:
onMouseOut(le);
break;
case Event.ONMOUSEDOWN:
onMouseDown(ce);
break;
}
}
/**
* Refreshes the view by reloading the data from the store and re-rendering
* the template.
*/
public void refresh() {
if (!rendered) {
return;
}
el().setInnerHtml("");
List models = store.getModels();
if (models.size() < 1) {
all.removeAll();
return;
}
template.overwrite(getElement(), Util.getJsObjects(collectData(models, 0),
template.getMaxDepth()));
all = new CompositeElement(el().select(itemSelector));
updateIndexes(0, -1);
fireEvent(Events.Refresh);
}
/**
* Refreshes an individual node's data from the store.
*
* @param index the items data index in the store
*/
public void refreshNode(int index) {
onUpdate(store.getAt(index), index);
}
/**
* Sets the display property. Applies when using the default template for each
* item's text.
*
* @param displayProperty the display property
*/
public void setDisplayProperty(String displayProperty) {
this.displayProperty = displayProperty;
}
/**
* This is a required setting. A simple CSS selector (e.g. div.some-class or
* span:first-child) that will be used to determine what nodes this DataView
* will be working with (defaults to 'x-view-item').
*
* @param itemSelector the item selector
*/
public void setItemSelector(String itemSelector) {
this.itemSelector = itemSelector;
}
/**
* Sets the text loading text to be displayed during a load request.
*
* @param loadingText the loading text
*/
public void setLoadingText(String loadingText) {
this.loadingText = loadingText;
}
/**
* Sets the view's model processor. The model processor can be used to provide
* "formatted" properties to the XTemplate used to render the view.
*
* @see ModelProcessor
* @param modelProcessor
*/
public void setModelProcessor(ModelProcessor<M> modelProcessor) {
this.modelProcessor = modelProcessor;
}
/**
* Sets the style name to apply on mouse over.
*
* @param overStyle the over style
*/
public void setOverStyle(String overStyle) {
this.overStyle = overStyle;
}
/**
* Sets the selection model.
*
* @param sm the selection model
*/
public void setSelectionModel(ListViewSelectionModel<M> sm) {
if (this.sm != null) {
this.sm.bindList(null);
}
this.sm = sm;
if (sm != null) {
sm.bindList(this);
}
}
/**
* True to select the item when mousing over a element (defaults to false).
*
* @param selectOnHover true to select on mouse over
*/
public void setSelectOnOver(boolean selectOnHover) {
this.selectOnHover = selectOnHover;
}
/**
* The style to be applied to each selected item (defaults to
* 'x-view-item-sel').
*
* @param selectStyle the select style
*/
public void setSelectStyle(String selectStyle) {
this.selectStyle = selectStyle;
}
/**
* Sets the template fragment to be used for the text of each listview item.
*
* <pre><code>
* listview.setSimpleTemplate("{abbr} {name}");
* </code></pre>
*
* @param html the html used only for the text of each item in the list
*/
public void setSimpleTemplate(String html) {
assertPreRender();
html = "<tpl for=\".\"><div class=x-view-item>" + html + "</div></tpl>";
template = XTemplate.create(html);
}
/**
* Changes the data store bound to this view and refreshes it.
*
* @param store the store to bind this view
*/
public void setStore(ListStore store) {
if (!initial && this.store != null) {
this.store.removeStoreListener(storeListener);
}
if (store != null) {
store.addStoreListener(storeListener);
}
this.store = store;
sm.bindList(this);
if (store != null && isRendered()) {
refresh();
}
}
/**
* Sets the view's template.
*
* @param html the HTML fragment
*/
public void setTemplate(String html) {
setTemplate(XTemplate.create(html));
}
/**
* Sets the view's template.
*
* @param template the template
*/
public void setTemplate(XTemplate template) {
this.template = template;
}
protected List collectData(List<M> models, int startIndex) {
List<M> list = new ArrayList<M>();
for (int i = 0, len = models.size(); i < len; i++) {
list.add(prepareData(models.get(i)));
}
return list;
}
@Override
protected ComponentEvent createComponentEvent(Event event) {
ListViewEvent e = new ListViewEvent(this, event);
e.index = -1;
if (event != null) {
El el = e.getTarget(itemSelector, 10);
if (el != null) {
int idx = indexOf(el.dom);
if (idx != -1) {
e.index = indexOf(el.dom);
e.element = all.getElement(e.index);
}
}
}
return e;
}
protected void focusItem(int index) {
Element elem = all.getElement(index);
if (elem != null) {
fly(elem).scrollIntoView(getElement(), false);
}
focus();
}
protected void initComponent() {
storeListener = new StoreListener() {
@Override
public void storeAdd(StoreEvent se) {
onAdd(se.models, se.index);
}
@Override
public void storeBeforeDataChanged(StoreEvent se) {
onBeforeLoad();
}
@Override
public void storeClear(StoreEvent se) {
refresh();
}
@Override
public void storeDataChanged(StoreEvent se) {
refresh();
}
@Override
public void storeFilter(StoreEvent se) {
refresh();
}
@Override
public void storeRemove(StoreEvent se) {
onRemove(se.model, se.index);
}
@Override
public void storeSort(StoreEvent se) {
refresh();
}
@Override
public void storeUpdate(StoreEvent se) {
onUpdate((M) se.model, se.index);
}
};
}
protected void onAdd(List<M> models, int index) {
Element[] nodes = bufferRender(models);
all.insert(nodes, index);
if (rendered) {
el().insertChild(nodes, index);
updateIndexes(index, -1);
}
}
protected void onBeforeLoad() {
if (loadingText != null) {
if (rendered) {
el().setInnerHtml("<div class='loading-indicator'>" + loadingText + "</div>");
}
all.removeAll();
}
}
protected void onMouseDown(ComponentEvent be) {
El el = be.getTarget(itemSelector, 10);
if (el != null) {
fireEvent(Events.Select, be);
}
}
protected void onMouseOut(ListViewEvent ce) {
if (overElement != null) {
if (!ce.within(overElement, true)) {
fly(overElement).removeStyleName(overStyle);
overElement = null;
}
}
}
protected void onMouseOver(ListViewEvent ce) {
if (ce.index != -1) {
if (selectOnHover) {
sm.select(ce.index);
} else {
Element e = all.getElement(ce.index);
if (e != null && e != overElement) {
fly(e).addStyleName(overStyle);
overElement = e;
}
}
}
}
protected void onRemove(ModelData data, int index) {
if (all != null) {
Element e = getElement(index);
if (e != null) {
fly(e).removeStyleName(overStyle);
if (overElement == e) {
overElement = null;
}
getSelectionModel().deselect(index);
fly(e).removeFromParent();
all.remove(index);
updateIndexes(index, -1);
}
}
}
protected void onRender(Element target, int index) {
super.onRender(target, index);
setElement(DOM.createDiv(), target, index);
el().setStyleAttribute("overflow", "auto");
el().setStyleAttribute("padding", "0px");
if (!GXT.isIE) {
el().setTabIndex(0);
}
if (template == null) {
template = XTemplate.create("<tpl for=\".\"><div class='x-view-item'>{" + displayProperty
+ "}</div></tpl>");
}
if (store != null && store.getCount() > 0) {
refresh();
}
disableTextSelection(true);
sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS);
}
protected void onSelectChange(M model, boolean select) {
if (rendered && all != null) {
int index = store.indexOf(model);
if (index != -1) {
if (select) {
fly(all.getElement(index)).addStyleName(selectStyle);
} else {
fly(all.getElement(index)).removeStyleName(selectStyle);
}
fly(all.getElement(index)).removeStyleName(overStyle);
}
}
}
protected void onUpdate(M model, int index) {
Element original = all.getElement(index);
List list = Util.createList(model);
Element node = bufferRender(list)[0];
all.replaceElement(original, node);
}
protected M prepareData(M model) {
if (modelProcessor != null) {
return modelProcessor.prepareData(model);
}
return model;
}
private Element[] bufferRender(List<M> models) {
Element div = DOM.createDiv();
template.overwrite(div,
Util.getJsObjects(collectData((List) models, 0), template.getMaxDepth()));
return DomQuery.select(itemSelector, div);
}
private void updateIndexes(int startIndex, int endIndex) {
List<Element> elems = all.getElements();
endIndex = endIndex == -1 ? elems.size() - 1 : endIndex;
for (int i = startIndex; i <= endIndex; i++) {
elems.get(i).setPropertyInt("viewIndex", i);
}
}
}