/*
* Ext GWT - Ext for GWT
* Copyright(c) 2007-2009, 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.GXT;
import com.extjs.gxt.ui.client.core.CompositeElement;
import com.extjs.gxt.ui.client.core.DomQuery;
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.Events;
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.dom.client.NodeList;
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>
*
* <dl>
* <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 node</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>
* </dl>
*
* <dl>
* <dt>Inherited Events:</dt>
* <dd>BoxComponent Move</dd>
* <dd>BoxComponent Resize</dd>
* <dd>Component Enable</dd>
* <dd>Component Disable</dd>
* <dd>Component BeforeHide</dd>
* <dd>Component Hide</dd>
* <dd>Component BeforeShow</dd>
* <dd>Component Show</dd>
* <dd>Component Attach</dd>
* <dd>Component Detach</dd>
* <dd>Component BeforeRender</dd>
* <dd>Component Render</dd>
* <dd>Component BrowserEvent</dd>
* <dd>Component BeforeStateRestore</dd>
* <dd>Component StateRestore</dd>
* <dd>Component BeforeStateSave</dd>
* <dd>Component SaveState</dd>
* </dl>
*/
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<M> 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;
}
/**
* Returns the view's loading text.
*
* @return the loading text
*/
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 store.
*
* @return the store
*/
public ListStore<M> getStore() {
return store;
}
/**
* Returns the list's template.
*
* @return the template
*/
public XTemplate getTemplate() {
return template;
}
/**
* Returns 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, false);
}
/**
* 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, false);
}
@SuppressWarnings("unchecked")
public void onComponentEvent(ComponentEvent ce) {
super.onComponentEvent(ce);
ListViewEvent le = (ListViewEvent) ce;
switch (ce.getEventTypeInt()) {
case Event.ONMOUSEOVER:
onMouseOver(le);
break;
case Event.ONMOUSEOUT:
onMouseOut(le);
break;
case Event.ONMOUSEDOWN:
onMouseDown(le);
break;
case Event.ONDBLCLICK:
if (le.getIndex() != -1) {
onDoubleClick(le);
}
break;
case Event.ONCLICK:
if (le.getIndex() != -1) {
onClick(le);
}
}
}
/**
* Refreshes the view by reloading the data from the store and re-rendering
* the template.
*/
@SuppressWarnings("unchecked")
public void refresh() {
if (!rendered) {
return;
}
el().setInnerHtml("");
repaint();
List models = store.getModels();
if (models.size() < 1) {
all.removeAll();
return;
}
template.overwrite(getElement(), Util.getJsObjects((List) collectData(models, 0),
template.getMaxDepth()));
all = new CompositeElement(Util.toElementArray(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<M> 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<M> 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) {
return new ListViewEvent<M>(this, event);
}
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<M>() {
@Override
public void storeAdd(StoreEvent<M> se) {
onAdd(se.getModels(), se.getIndex());
}
@Override
public void storeBeforeDataChanged(StoreEvent<M> se) {
onBeforeLoad();
}
@Override
public void storeClear(StoreEvent<M> se) {
refresh();
}
@Override
public void storeDataChanged(StoreEvent<M> se) {
refresh();
}
@Override
public void storeFilter(StoreEvent<M> se) {
refresh();
}
@Override
public void storeRemove(StoreEvent<M> se) {
onRemove(se.getModel(), se.getIndex());
}
@Override
public void storeSort(StoreEvent<M> se) {
refresh();
}
@Override
public void storeUpdate(StoreEvent<M> se) {
onUpdate(se.getModel(), se.getIndex());
}
};
}
protected void onAdd(List<M> models, int index) {
NodeList<Element> nodes = bufferRender(models);
Element[] e = Util.toElementArray(nodes);
all.insert(e, index);
if (rendered) {
el().insertChild(e, index);
updateIndexes(index, -1);
}
}
protected void onBeforeLoad() {
if (loadingText != null) {
if (rendered) {
el().setInnerHtml("<div class='loading-indicator'>" + loadingText + "</div>");
}
all.removeAll();
}
}
protected void onClick(ListViewEvent<M> e) {
}
protected void onDoubleClick(ListViewEvent<M> e) {
fireEvent(Events.DoubleClick, e);
}
protected void onMouseDown(ListViewEvent<M> e) {
if (e.getIndex() != -1) {
fireEvent(Events.Select, e);
}
}
protected void onMouseOut(ListViewEvent<M> ce) {
if (overElement != null) {
if (!ce.within(overElement, true)) {
fly(overElement).removeStyleName(overStyle);
overElement = null;
}
}
}
protected void onMouseOver(ListViewEvent<M> ce) {
if (ce.getIndex() != -1) {
if (selectOnHover) {
sm.select(ce.getIndex(), false);
} else {
Element e = all.getElement(ce.getIndex());
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 && index < all.getCount()) {
if (select) {
fly(all.getElement(index)).addStyleName(selectStyle);
} else {
fly(all.getElement(index)).removeStyleName(selectStyle);
}
fly(all.getElement(index)).removeStyleName(overStyle);
}
}
}
@SuppressWarnings("unchecked")
protected void onUpdate(M model, int index) {
Element original = all.getElement(index);
List list = Util.createList(model);
Element node = bufferRender(list).getItem(0);
all.replaceElement(original, node);
if (fly(original).hasStyleName(selectStyle)) {
fly(node).addStyleName(selectStyle);
}
el().insertChild(node, index);
el().removeChild(original);
}
protected M prepareData(M model) {
if (modelProcessor != null) {
return modelProcessor.prepareData(model);
}
return model;
}
@SuppressWarnings("unchecked")
private NodeList<Element> bufferRender(List<M> models) {
Element div = DOM.createDiv();
template.overwrite(div, Util.getJsObjects((List) 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);
}
}
}