/**
* This file is part of Horaz.
*
* Horaz is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Horaz is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Horaz. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright Joe's App Factory UG (haftungsbeschränkt)
*/
package com.horaz.client.widgets;
import java.util.HashMap;
import java.util.Map;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.LIElement;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.UListElement;
import com.horaz.client.model.BaseModel;
import com.horaz.client.model.DataStore;
import com.horaz.client.model.events.FilterUpdatedEvent;
import com.horaz.client.model.events.FilterUpdatedListener;
import com.horaz.client.model.events.ModelAddedEvent;
import com.horaz.client.model.events.ModelAddedListener;
import com.horaz.client.model.events.ModelRemovedEvent;
import com.horaz.client.model.events.ModelRemovedListener;
import com.horaz.client.model.events.ModelUpdatedEvent;
import com.horaz.client.model.events.ModelUpdatedListener;
import com.horaz.client.util.RegExp;
import com.horaz.client.util.RegExp.Result;
import com.horaz.client.widgets.events.EventFactory;
import com.horaz.client.widgets.events.ItemApplyListener;
import com.horaz.client.widgets.events.ItemClickListener;
/**
* ListView
* Lists are used for data display, navigation, result lists, and data entry. It is possible to connect the ListView with a DataStore, so the data can be dynamically change (add, remove, update).
* If you use a DataStore, the ListView has to have to the attribute <b>data-template="TEMPLATE_ID"</b>.
* The template must be a container with placeholders in the syntax {#FIELD_NAME#}.
* When a list item will be created, the innerHTML of the placeholder container will be copied and
* the placeholders will be replaced with the values of the model for the corresponding field.
*
* @horaz.htmltag <ul data-role="listview" data-template="TEMPLATE_ID">
* @horaz.events itemApply, itemClick
* @see https://www.horaz-lang.com/dev-guide/listview
*/
abstract public class ListView<T extends BaseModel> extends BaseWidget<UListElement> {
private DataStore<T> dataStore;
private final Map<Long, T> modelsCache = new HashMap<Long, T>();
protected ListView(UListElement ulElm) {
super(ulElm);
}
native protected void _refresh(Element elm) /*-{
$wnd.jQuery(elm).listview('refresh');
}-*/;
/**
* register a item apply listener
* this is a hook that will be called when a <li> element was created.
* @param itemApplyListener
*/
public void addItemApplyListener(ItemApplyListener<T> itemApplyListener) {
EventFactory.delegateEventHandler(getElement(), "li", "itemApply", itemApplyListener);
}
/**
* register a item click listener
* @param itemClickListener
*/
public void addItemClickListener(ItemClickListener<T> itemClickListener) {
EventFactory.delegateEventHandler(getElement(), "li a", "click", itemClickListener);
}
abstract protected void createAllItems();
/**
* creates a new list item from a model
* notice: after that {@link #refresh()} has to be called.
* @param model
*/
protected void createNewItem(T model) {
String inner = generateItemInnerHTML(model);
LIElement newItem = Document.get().createLIElement();
newItem.setInnerHTML(inner);
newItem.setAttribute("data-modelid", String.valueOf(model.getModelId()));
// models cache for itemApply
modelsCache.put(model.getModelId(), model);
getElement().appendChild(newItem);
}
private String generateItemInnerHTML(T model) {
// create a new list item from template
String templateid = getElement().getAttribute("data-template");
if (templateid == null || templateid.isEmpty()) {
throw new IllegalStateException("attribute data-template has to be set.");
}
Element template = getElementById(templateid);
String inner = template.getInnerHTML();
// replace attributes
RegExp reg = RegExp.create("({#[^#]+#})", "g");
Result res = reg.match(inner);
for (String match : res.iterator()) {
Object value = model.getField(match.substring(2, match.length()-2));
String s = value==null?"":value.toString();
inner = inner.replace(match, s);
}
return inner;
}
/**
* @return datastore
*/
public DataStore<T> getDataStore() {
return dataStore;
}
public LIElement getListItem(T model) {
// find li-element
LIElement li = null;
long id = model.getModelId();
for (int i=0; i<getElement().getChildCount(); i++) {
Node node = getElement().getChild(i);
if (Element.is(node)) {
String attr = Element.as(node).getAttribute("data-modelid");
if (attr != null && !attr.isEmpty() && Integer.valueOf(attr) == id) {
li = (LIElement) Element.as(node);
break;
}
}
}
return li;
}
/**
* get the model behind a LI-Element. The li element must have to attribute data-modelid.
* @param el
* @return model or null
* @throws IllegalArgumentException if data-modelid is not set
*/
public T getModel(LIElement el) {
if (el.getAttribute("data-modelid") == null) {
throw new IllegalStateException("LI Element needs the attribute data-modelid");
}
int id = Integer.valueOf(el.getAttribute("data-modelid"));
return modelsCache.get((long) id);
}
// TODO place this is basewidget, better implementation
private boolean isInitFired() {
return getElement().getClassName().contains("ui-listview");
}
/**
* renders the list items
*/
protected void refresh() {
if (isInitFired()) {
_refresh(getElement());
}
}
/**
* remove li-element
* @param model
*/
private void removeItem(T model) {
// TODO for performance with jquery
long id = model.getModelId();
for (int i=0; i<getElement().getChildCount(); i++) {
Node node = getElement().getChild(i);
if (Element.is(node)) {
String attr = Element.as(node).getAttribute("data-modelid");
if (attr != null && !attr.isEmpty() && Integer.valueOf(attr) == id) {
getElement().removeChild(node);
break;
}
}
}
modelsCache.remove(id);
}
/**
* connect a datastore to the listview.
* The listview will automatically add/edit/remove the listitems, when the datastore changes
* @param dataStore
*/
public void setDataStore(DataStore<T> dataStore) {
this.dataStore = dataStore;
// TODO unregister old event handlers
// TODO remove dataStore (null)
// register event handler
this.dataStore.addModelAddedListener(new ModelAddedListener<T>() {
@Override
public void onModelAdded(ModelAddedEvent<T> event) {
createNewItem(event.getModel());
}
});
this.dataStore.addModelRemovedListener(new ModelRemovedListener<T>() {
@Override
public void onModelRemoved(ModelRemovedEvent<T> event) {
removeItem(event.getModel());
refresh();
}
});
this.dataStore.addModelUpdatedListener(new ModelUpdatedListener<T>() {
@Override
public void onModelUpdated(ModelUpdatedEvent<T> event) {
updateItem(event.getModel());
refresh();
}
});
this.dataStore.addFilterUpdatedListener(new FilterUpdatedListener() {
@Override
public void onFilterUpdated(FilterUpdatedEvent event) {
//TODO performance, filter existing elements
while (getElement().getChildCount()>0) {
getElement().removeChild(getElement().getChild(0));
}
// create a list item for each model
createAllItems();
_refresh(getElement());
}
});
}
private void updateItem(T model) {
LIElement li = getListItem(model);
long id = model.getModelId();
if (li == null) throw new IllegalStateException("could not find list item for model #"+id);
li.setClassName(""); // reset class names
String inner = generateItemInnerHTML(model);
li.setInnerHTML(inner);
}
}