blob: c7100d1c2ef27fa261114af82af3288f8af48a6e [file] [log] [blame]
/*
* Copyright 2010 Google 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.
*/
package com.google.gwt.user.cellview.client;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.Cell.Context;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.cellview.client.HasDataPresenter.LoadingState;
import com.google.gwt.user.client.Event;
import com.google.gwt.view.client.CellPreviewEvent;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.SelectionModel;
import java.util.List;
import java.util.Set;
/**
* A single column list of cells.
*
* <p>
* <h3>Examples</h3>
* <dl>
* <dt>Trivial example</dt>
* <dd>{@example com.google.gwt.examples.cellview.CellListExample}</dd>
* <dt>ValueUpdater example</dt>
* <dd>{@example com.google.gwt.examples.cellview.CellListValueUpdaterExample}</dd>
* <dt>Key provider example</dt>
* <dd>{@example com.google.gwt.examples.view.KeyProviderExample}</dd>
* </dl>
* </p>
*
* @param <T> the data type of list items
*/
public class CellList<T> extends AbstractHasData<T> {
/**
* A ClientBundle that provides images for this widget.
*/
public interface Resources extends ClientBundle {
/**
* The background used for selected items.
*/
@ImageOptions(repeatStyle = RepeatStyle.Horizontal, flipRtl = true)
ImageResource cellListSelectedBackground();
/**
* The styles used in this widget.
*/
@Source(Style.DEFAULT_CSS)
Style cellListStyle();
}
/**
* Styles used by this widget.
*/
@ImportedWithPrefix("gwt-CellList")
public interface Style extends CssResource {
/**
* The path to the default CSS styles used by this resource.
*/
String DEFAULT_CSS = "com/google/gwt/user/cellview/client/CellList.css";
/**
* Applied to even items.
*/
String cellListEvenItem();
/**
* Applied to the keyboard selected item.
*/
String cellListKeyboardSelectedItem();
/**
* Applied to odd items.
*/
String cellListOddItem();
/**
* Applied to selected items.
*/
String cellListSelectedItem();
/**
* Applied to the widget.
*/
String cellListWidget();
}
interface Template extends SafeHtmlTemplates {
@Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\" style=\"outline:none;\" >{2}</div>")
SafeHtml div(int idx, String classes, SafeHtml cellContents);
@Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\" style=\"outline:none;\" tabindex=\"{2}\">{3}</div>")
SafeHtml divFocusable(int idx, String classes, int tabIndex,
SafeHtml cellContents);
@Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\" style=\"outline:none;\" tabindex=\"{2}\" accesskey=\"{3}\">{4}</div>")
SafeHtml divFocusableWithKey(int idx, String classes, int tabIndex,
char accessKey, SafeHtml cellContents);
}
/**
* The default page size.
*/
private static final int DEFAULT_PAGE_SIZE = 25;
private static Resources DEFAULT_RESOURCES;
private static final Template TEMPLATE = GWT.create(Template.class);
private static Resources getDefaultResources() {
if (DEFAULT_RESOURCES == null) {
DEFAULT_RESOURCES = GWT.create(Resources.class);
}
return DEFAULT_RESOURCES;
}
private final Cell<T> cell;
private boolean cellIsEditing;
private final Element childContainer;
private SafeHtml emptyListMessage = SafeHtmlUtils.fromSafeConstant("");
private final Element emptyMessageElem;
private final Style style;
private ValueUpdater<T> valueUpdater;
/**
* Construct a new {@link CellList}.
*
* @param cell the cell used to render each item
*/
public CellList(final Cell<T> cell) {
this(cell, getDefaultResources(), null);
}
/**
* Construct a new {@link CellList} with the specified {@link Resources}.
*
* @param cell the cell used to render each item
* @param resources the resources used for this widget
*/
public CellList(final Cell<T> cell, Resources resources) {
this(cell, resources, null);
}
/**
* Construct a new {@link CellList} with the specified {@link ProvidesKey key provider}.
*
* @param cell the cell used to render each item
* @param keyProvider an instance of ProvidesKey<T>, or null if the record
* object should act as its own key
*/
public CellList(final Cell<T> cell, ProvidesKey<T> keyProvider) {
this(cell, getDefaultResources(), keyProvider);
}
/**
* Construct a new {@link CellList} with the specified {@link Resources}
* and {@link ProvidesKey key provider}.
*
* @param cell the cell used to render each item
* @param resources the resources used for this widget
* @param keyProvider an instance of ProvidesKey<T>, or null if the record
* object should act as its own key
*/
public CellList(final Cell<T> cell, Resources resources, ProvidesKey<T> keyProvider) {
super(Document.get().createDivElement(), DEFAULT_PAGE_SIZE, keyProvider);
this.cell = cell;
this.style = resources.cellListStyle();
this.style.ensureInjected();
String widgetStyle = this.style.cellListWidget();
if (widgetStyle != null) {
// The widget style is null when used in CellBrowser.
addStyleName(widgetStyle);
}
// Create the DOM hierarchy.
childContainer = Document.get().createDivElement();
emptyMessageElem = Document.get().createDivElement();
showOrHide(emptyMessageElem, false);
DivElement outerDiv = getElement().cast();
outerDiv.appendChild(childContainer);
outerDiv.appendChild(emptyMessageElem);
// Sink events that the cell consumes.
CellBasedWidgetImpl.get().sinkEvents(this, cell.getConsumedEvents());
}
/**
* Get the message that is displayed when there is no data.
*
* @return the empty message
* @see #setEmptyListMessage(SafeHtml)
*/
public SafeHtml getEmptyListMessage() {
return emptyListMessage;
}
/**
* Get the {@link Element} for the specified index. If the element has not
* been created, null is returned.
*
* @param indexOnPage the index on the page
* @return the element, or null if it doesn't exists
* @throws IndexOutOfBoundsException if the index is outside of the current
* page
*/
public Element getRowElement(int indexOnPage) {
getPresenter().flush();
checkRowBounds(indexOnPage);
if (childContainer.getChildCount() > indexOnPage) {
return childContainer.getChild(indexOnPage).cast();
}
return null;
}
/**
* Set the message to display when there is no data.
*
* @param html the message to display when there are no results
* @see #getEmptyListMessage()
*/
public void setEmptyListMessage(SafeHtml html) {
this.emptyListMessage = html;
emptyMessageElem.setInnerHTML(html.asString());
}
/**
* Set the value updater to use when cells modify items.
*
* @param valueUpdater the {@link ValueUpdater}
*/
public void setValueUpdater(ValueUpdater<T> valueUpdater) {
this.valueUpdater = valueUpdater;
}
@Override
protected boolean dependsOnSelection() {
return cell.dependsOnSelection();
}
/**
* Called when a user action triggers selection.
*
* @param event the event that triggered selection
* @param value the value that was selected
* @param indexOnPage the index of the value on the page
* @deprecated use
* {@link #addCellPreviewHandler(com.google.gwt.view.client.CellPreviewEvent.Handler)}
* instead
*/
@Deprecated
protected void doSelection(Event event, T value, int indexOnPage) {
}
/**
* Fire an event to the cell.
*
* @param context the {@link Context} of the cell
* @param event the event that was fired
* @param parent the parent of the cell
* @param value the value of the cell
*/
protected void fireEventToCell(Context context, Event event, Element parent,
T value) {
Set<String> consumedEvents = cell.getConsumedEvents();
if (consumedEvents != null && consumedEvents.contains(event.getType())) {
boolean cellWasEditing = cell.isEditing(context, parent, value);
cell.onBrowserEvent(context, parent, value, event, valueUpdater);
cellIsEditing = cell.isEditing(context, parent, value);
if (cellWasEditing && !cellIsEditing) {
CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
public void execute() {
setFocus(true);
}
});
}
}
}
/**
* Return the cell used to render each item.
*/
protected Cell<T> getCell() {
return cell;
}
/**
* Get the parent element that wraps the cell from the list item. Override
* this method if you add structure to the element.
*
* @param item the row element that wraps the list item
* @return the parent element of the cell
*/
protected Element getCellParent(Element item) {
return item;
}
@Override
protected Element getChildContainer() {
return childContainer;
}
@Override
protected Element getKeyboardSelectedElement() {
// Do not use getRowElement() because that will flush the presenter.
int rowIndex = getKeyboardSelectedRow();
if (rowIndex >= 0 && childContainer.getChildCount() > rowIndex) {
return childContainer.getChild(rowIndex).cast();
}
return null;
}
@Override
protected boolean isKeyboardNavigationSuppressed() {
return cellIsEditing;
}
@Override
protected void onBlur() {
// Remove the keyboard selection style.
Element elem = getKeyboardSelectedElement();
if (elem != null) {
elem.removeClassName(style.cellListKeyboardSelectedItem());
}
}
@SuppressWarnings("deprecation")
@Override
protected void onBrowserEvent2(Event event) {
// Get the event target.
EventTarget eventTarget = event.getEventTarget();
if (!Element.is(eventTarget)) {
return;
}
final Element target = event.getEventTarget().cast();
// Forward the event to the cell.
String idxString = "";
Element cellTarget = target;
while ((cellTarget != null)
&& ((idxString = cellTarget.getAttribute("__idx")).length() == 0)) {
cellTarget = cellTarget.getParentElement();
}
if (idxString.length() > 0) {
// Select the item if the cell does not consume events. Selection occurs
// before firing the event to the cell in case the cell operates on the
// currently selected item.
String eventType = event.getType();
boolean isClick = "click".equals(eventType);
int idx = Integer.parseInt(idxString);
int indexOnPage = idx - getPageStart();
if (!isRowWithinBounds(indexOnPage)) {
// If the event causes us to page, then the index will be out of bounds.
return;
}
// Get the cell parent before doing selection in case the list is redrawn.
boolean isSelectionHandled = cell.handlesSelection()
|| KeyboardSelectionPolicy.BOUND_TO_SELECTION == getKeyboardSelectionPolicy();
Element cellParent = getCellParent(cellTarget);
T value = getVisibleItem(indexOnPage);
Context context = new Context(idx, 0, getValueKey(value));
CellPreviewEvent<T> previewEvent = CellPreviewEvent.fire(this, event,
this, context, value, cellIsEditing, isSelectionHandled);
if (isClick && !cellIsEditing && !isSelectionHandled) {
doSelection(event, value, indexOnPage);
}
// Focus on the cell.
if (isClick) {
/*
* If the selected element is natively focusable, then we do not want to
* steal focus away from it.
*/
boolean isFocusable = CellBasedWidgetImpl.get().isFocusable(target);
isFocused = isFocused || isFocusable;
getPresenter().setKeyboardSelectedRow(indexOnPage, !isFocusable, false);
}
// Fire the event to the cell if the list has not been refreshed.
if (!previewEvent.isCanceled()) {
fireEventToCell(context, event, cellParent, value);
}
}
}
@Override
protected void onFocus() {
// Add the keyboard selection style.
Element elem = getKeyboardSelectedElement();
if (elem != null) {
elem.addClassName(style.cellListKeyboardSelectedItem());
}
}
@Override
protected void renderRowValues(SafeHtmlBuilder sb, List<T> values, int start,
SelectionModel<? super T> selectionModel) {
String keyboardSelectedItem = " " + style.cellListKeyboardSelectedItem();
String selectedItem = " " + style.cellListSelectedItem();
String evenItem = style.cellListEvenItem();
String oddItem = style.cellListOddItem();
int keyboardSelectedRow = getKeyboardSelectedRow() + getPageStart();
int length = values.size();
int end = start + length;
for (int i = start; i < end; i++) {
T value = values.get(i - start);
boolean isSelected = selectionModel == null ? false
: selectionModel.isSelected(value);
StringBuilder classesBuilder = new StringBuilder();
classesBuilder.append(i % 2 == 0 ? evenItem : oddItem);
if (isSelected) {
classesBuilder.append(selectedItem);
}
SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
Context context = new Context(i, 0, getValueKey(value));
cell.render(context, value, cellBuilder);
if (i == keyboardSelectedRow) {
// This is the focused item.
if (isFocused) {
classesBuilder.append(keyboardSelectedItem);
}
char accessKey = getAccessKey();
if (accessKey != 0) {
sb.append(TEMPLATE.divFocusableWithKey(i, classesBuilder.toString(),
getTabIndex(), accessKey, cellBuilder.toSafeHtml()));
} else {
sb.append(TEMPLATE.divFocusable(i, classesBuilder.toString(),
getTabIndex(), cellBuilder.toSafeHtml()));
}
} else {
sb.append(TEMPLATE.div(i, classesBuilder.toString(),
cellBuilder.toSafeHtml()));
}
}
}
@Override
protected boolean resetFocusOnCell() {
int row = getKeyboardSelectedRow();
if (isRowWithinBounds(row)) {
Element rowElem = getKeyboardSelectedElement();
Element cellParent = getCellParent(rowElem);
T value = getVisibleItem(row);
Context context = new Context(row + getPageStart(), 0, getValueKey(value));
return cell.resetFocus(context, cellParent, value);
}
return false;
}
@Override
protected void setKeyboardSelected(int index, boolean selected,
boolean stealFocus) {
if (!isRowWithinBounds(index)) {
return;
}
Element elem = getRowElement(index);
if (!selected || isFocused || stealFocus) {
setStyleName(elem, style.cellListKeyboardSelectedItem(), selected);
}
setFocusable(elem, selected);
if (selected && stealFocus && !cellIsEditing) {
elem.focus();
onFocus();
}
}
/**
* @deprecated this method is never called by AbstractHasData, render the
* selected styles in
* {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
*/
@Override
@Deprecated
protected void setSelected(Element elem, boolean selected) {
setStyleName(elem, style.cellListSelectedItem(), selected);
}
@Override
void setLoadingState(LoadingState state) {
showOrHide(emptyMessageElem, state == LoadingState.EMPTY);
// TODO(jlabanca): Add a loading icon.
}
/**
* Show or hide an element.
*
* @param element the element
* @param show true to show, false to hide
*/
private void showOrHide(Element element, boolean show) {
if (show) {
element.getStyle().clearDisplay();
} else {
element.getStyle().setDisplay(Display.NONE);
}
}
}