/*
* Copyright 2014 cruxframework.org.
*
* 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 org.cruxframework.crux.smartfaces.client.pager;
import org.cruxframework.crux.core.client.collection.Array;
import org.cruxframework.crux.core.client.collection.CollectionFactory;
import org.cruxframework.crux.core.client.dataprovider.pager.AbstractPager;
import org.cruxframework.crux.core.client.dataprovider.pager.PageEvent;
import org.cruxframework.crux.core.client.dataprovider.pager.Pageable;
import org.cruxframework.crux.core.client.dataprovider.pager.PageablePager;
import org.cruxframework.crux.core.client.dataprovider.pager.Pager;
import org.cruxframework.crux.core.client.utils.StringUtils;
import org.cruxframework.crux.smartfaces.client.backbone.common.FacesBackboneResourcesCommon;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
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.Node;
import com.google.gwt.event.dom.client.ScrollEvent;
import com.google.gwt.event.dom.client.ScrollHandler;
import com.google.gwt.event.logical.shared.AttachEvent;
import com.google.gwt.event.logical.shared.AttachEvent.Handler;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.ScrollPanel;
/**
* A {@link Pager} that change pages from a {@link Pageable} when user scrolls down the pager.
*
* @author Thiago da Rosa de Bustamante
*/
public class ScrollablePager<T> extends AbstractPager<T> implements PageablePager<T>
{
public static final String DEFAULT_STYLE_NAME = "faces-ScollablePager";
private int lastRequestedPage = 0;
private int lastScrollPos = 0;
private DivElement loadingElement;
private ScrollPanel scrollable;
private int startVisiblePage = 0;
private Array<ScrollingData> backingScrollingData = CollectionFactory.createArray();
private Array<ScrollingData> forwardScrollingData = CollectionFactory.createArray();
private IsWidget pagePanel;
private int scrollableHeight;
private int pagePanelHeight;
private int visibleWindowMinScrollTop;
private int visibleWindowMaxScrollTop;
public ScrollablePager()
{
FacesBackboneResourcesCommon.INSTANCE.css().ensureInjected();
scrollable = new ScrollPanel();
initWidget(scrollable);
setStyleName(DEFAULT_PAGER_STYLE_NAME);
addStyleName(DEFAULT_STYLE_NAME);
// Do not let the scrollable take tab focus.
scrollable.getElement().setTabIndex(-1);
// Handle scroll events.
scrollable.addScrollHandler(new ScrollHandler()
{
public void onScroll(ScrollEvent event)
{
// If scrolling up, ignore the event.
int oldScrollPos = lastScrollPos;
lastScrollPos = scrollable.getVerticalScrollPosition();
int maxScrollTop = pagePanelHeight - scrollableHeight;
if (oldScrollPos >= lastScrollPos)
{
maybeUpdateBackwardVisibleWindow();
return;
}
maybeUpdateForwardVisibleWindow();
if (lastScrollPos >= maxScrollTop)
{
if (hasNextPage() && isEnabled())
{
int nextRequestedPage = getCurrentPage() + 1;
if (lastRequestedPage != nextRequestedPage)
{
lastRequestedPage = nextRequestedPage;
PageEvent pageEvent = PageEvent.fire(ScrollablePager.this, nextRequestedPage);
if(!pageEvent.isCanceled())
{
nextPage(maxScrollTop);
}
}
}
}
}
});
}
private void nextPage(int scrollTop)
{
int currentPage = getDataProvider().getCurrentPage();
if (currentPage > 1)
{
updateForwardVisibleWindow(null, scrollTop);
}
nextPage();
}
private void maybeUpdateBackwardVisibleWindow()
{
if (visibleWindowMinScrollTop > lastScrollPos)
{
int size = backingScrollingData.size();
if (size > 0)
{
ScrollingData scrollingData = backingScrollingData.get(backingScrollingData.size() - 1);
if (scrollingData != null)
{
updateBackwardVisibleWindow(scrollingData);
backingScrollingData.remove(size - 1);
}
}
}
}
private void maybeUpdateForwardVisibleWindow()
{
if (visibleWindowMaxScrollTop < lastScrollPos)
{
int size = forwardScrollingData.size();
if (size > 0)
{
ScrollingData scrollingData = forwardScrollingData.get(forwardScrollingData.size() - 1);
if (scrollingData != null)
{
updateForwardVisibleWindow(scrollingData, scrollingData.scrollTop);
forwardScrollingData.remove(size - 1);
}
}
}
}
private void updateBackwardVisibleWindow(ScrollingData scrollingData)
{
restorePage(scrollingData);
int pageSize = getDataProvider().getPageSize();
int startRecord = startVisiblePage + (2*pageSize) -1;
startVisiblePage--;
ScrollingData pageScrollingData = hidePage(startRecord, lastScrollPos);
forwardScrollingData.add(pageScrollingData);
visibleWindowMinScrollTop -= scrollingData.size;
visibleWindowMaxScrollTop -= pageScrollingData.size;
}
private void updateForwardVisibleWindow(ScrollingData scrollingData, int scrollTop)
{
if (scrollingData != null)
{
restorePage(scrollingData);
}
int startRecord = startVisiblePage;
this.startVisiblePage++;
ScrollingData pageScrollingData = hidePage(startRecord, scrollTop);
backingScrollingData.add(pageScrollingData);
visibleWindowMinScrollTop = scrollingData != null?visibleWindowMinScrollTop+scrollingData.size:scrollTop;
visibleWindowMaxScrollTop = visibleWindowMinScrollTop + pageScrollingData.size;
}
private void restorePage(ScrollingData scrollingData)
{
int pageSize = scrollingData.children.size();
for (int i = 0; i < pageSize; i++)
{
pagePanel.asWidget().getElement().insertBefore(scrollingData.children.get(i), scrollingData.replacement);
}
scrollingData.replacement.removeFromParent();
}
private ScrollingData hidePage(int startRecord, int scrollTop)
{
int pageSize = getDataProvider().getPageSize();
ScrollingData scrollingData = new ScrollingData();
scrollingData.size = 0;
scrollingData.scrollTop = scrollTop;
for (int i = 0; i < pageSize; i++)
{
Element child = pagePanel.asWidget().getElement().getChild(startRecord).cast();
scrollingData.children.add(child);
scrollingData.size += child.getOffsetHeight();
child.removeFromParent();
}
scrollingData.replacement = Document.get().createDivElement();
scrollingData.replacement.setAttribute("style", "height: "+scrollingData.size+"px");
Element element = pagePanel.asWidget().getElement();
if (element.getChildCount() > startRecord)
{
Node refChild = element.getChild(startRecord);
pagePanel.asWidget().getElement().insertBefore(scrollingData.replacement, refChild);
}
else
{
pagePanel.asWidget().getElement().appendChild(scrollingData.replacement);
}
scrollable.setVerticalScrollPosition(lastScrollPos);
return scrollingData;
}
static class ScrollingData
{
int scrollTop;
int size;
Array<Element> children = CollectionFactory.createArray();
Element replacement;
}
@Override
public void initializeContentPanel(final Panel contentPanel)
{
setContentPanelHeight(contentPanel);
contentPanel.clear();
contentPanel.add(this);
}
@Override
public void setPageable(Pageable<T> pageable)
{
if (pageable != null)
{
pageable.setPager(this);
}
}
@Override
public void setStyleName(String style)
{
super.setStyleName(style);
addStyleName(FacesBackboneResourcesCommon.INSTANCE.css().facesScrollablePager());
}
@Override
public void setStyleName(String style, boolean add)
{
super.setStyleName(style, add);
if (!add)
{
addStyleName(FacesBackboneResourcesCommon.INSTANCE.css().facesScrollablePager());
}
}
@Override
public boolean supportsInfiniteScroll()
{
return true;
}
@Override
public void updatePagePanel(IsWidget pagePanel, boolean forward)
{
this.pagePanel = pagePanel;
scrollable.setWidget(pagePanel);
}
@Override
protected void hideLoading()
{
if (loadingElement != null)
{
loadingElement.removeFromParent();
loadingElement = null;
}
}
@Override
protected void onTransactionStarted(int startRecord)
{
super.onTransactionStarted(startRecord);
int pageSize = getDataProvider().getPageSize();
int index = startRecord + 1;
lastRequestedPage = (index / pageSize) + (index%pageSize==0?0:1);
}
@Override
protected void onUpdate()
{
scrollableHeight = scrollable.getOffsetHeight();
if (pagePanel != null)
{
pagePanelHeight = pagePanel.asWidget().getOffsetHeight();
}
else
{
pagePanelHeight = 0;
}
}
@Override
protected void setInteractionEnabled(boolean enabled)
{
super.setInteractionEnabled(enabled);
scrollable.setTouchScrollingDisabled(!enabled);
}
@Override
protected void showLoading()
{
if (loadingElement == null)
{
loadingElement = Document.get().createDivElement();
loadingElement.setClassName(getLoaderStyleName());
Document.get().getBody().appendChild(loadingElement);
}
}
private void defineContentPageHeightToPageHeight(final Panel contentPanel)
{
addAttachHandler(new Handler()
{
@Override
public void onAttachOrDetach(AttachEvent event)
{
if (event.isAttached())
{
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
int clientHeight = contentPanel.getElement().getClientHeight();
if (clientHeight > 0)
{
contentPanel.setHeight(clientHeight + "px");
}
}
});
}
}
});
}
private void setContentPanelHeight(final Panel contentPanel)
{
String height = contentPanel.getElement().getPropertyString("height");
if (StringUtils.isEmpty(height))
{
defineContentPageHeightToPageHeight(contentPanel);
}
}
}