├── .gitignore ├── README.md ├── backlog.txt ├── deploy-standalone-demo ├── oegyscroll-demo ├── pom.xml ├── run └── src │ └── main │ ├── java │ └── org │ │ └── laughingpanda │ │ └── oegyscrolldemo │ │ ├── OegyScrollDemoApplication.java │ │ ├── OegyScrollDemoPage.html │ │ └── OegyScrollDemoPage.java │ └── webapp │ ├── WEB-INF │ └── web.xml │ └── images │ ├── loading-row.gif │ └── table-rows-striped.gif ├── oegyscroll ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── laughingpanda │ │ └── wicket │ │ ├── Block.java │ │ ├── LazyLoadScrollableList.java │ │ ├── PlaceHolder.java │ │ ├── ProxyDataProvider.java │ │ ├── RemainderBlock.java │ │ ├── RowDataView.java │ │ ├── ScrolledContentView.java │ │ ├── SublistDataProvider.java │ │ ├── oegyscroll-updater.js │ │ └── oegyscroll.js │ └── test │ ├── java │ └── org │ │ └── laughingpanda │ │ └── wicket │ │ ├── LazyLoadScrollableListSpec.java │ │ ├── LazyLoadScrollableListTestPage.html │ │ └── LazyLoadScrollableListTestPage.java │ └── js │ ├── jquery-1.3.2.js │ ├── qunit.css │ ├── qunit.js │ └── scrollertest.html ├── pom.xml └── standalone-demo ├── images ├── loading-row.gif └── table-rows-striped.gif └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .settings/ 3 | .classpath 4 | .project 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OegyScroll 2 | ========== 3 | 4 | From LaughingPanda 5 | ------------------ 6 | 7 | OegyScroll (or org.laughingpanda.LazyLoadScrollableList) is a Wicket 8 | component for displaying long lists of data in a scrollable view. The 9 | idea is that at first only the first 100 rows are loaded to the browser 10 | and more rows are loaded while you scroll. This way a list of 20000 11 | items can easily be shown in a browser, because the initial markup of 12 | the page contains only some hundreds of elements. Still, the page 13 | behaves as if all the data was loaded immediately. The user can freely 14 | scroll through the data and will only experience a short delay before 15 | the actual data appears. 16 | 17 | New: JQuery client-side library 18 | ------------------------------- 19 | 20 | Using the client-side library, you can enjoy OegyScrolling without 21 | Wicket, too. Have a look at [this humble demo page][demo] to get the 22 | idea! 23 | 24 | [demo]: http://juhajasatu.com/oegydemo/ 25 | 26 | How To Install It? 27 | ------------------ 28 | 29 | If you're using Maven 2, just add the following dependency block into your POM file: 30 | 31 | 32 | 33 | org.laughingpanda.oegyscroll 34 | oegyscroll 35 | 1.0-SNAPSHOT 36 | 37 | 38 | 39 | ..or you can just clone the source and build it yourself. 40 | 41 | Dependencies 42 | ------------ 43 | 44 | * Wicket 1.4 (can easily be backported to 1.3.x) 45 | * Tested on Firefox 3.0, IE 6.0, IE 7.0 46 | 47 | How To Use It? 48 | -------------- 49 | 50 | It's designed to be an easy replacement for [DataView][]. You supply 51 | your data using an implementation of [data provider][IDataProvider] such 52 | as [ListDataProvider][]. 53 | 54 | Like with DataView, you have to override the populateRow method where 55 | you add the components for each actual data row. 56 | 57 | You need some custom HTML in your markup too, where you specify the 58 | markup for the placeholders and the data rows. You should use CSS styles 59 | to ensure that the height of the placeholder equals row height times 60 | block size. 61 | 62 | Please have a look at the [OegyScroll demo][] and I'm sure you'll get 63 | the idea. If you do a full checkout of OegyScroll, you'll also get the 64 | runnable demo, which has a "run" script for running it in Jetty. 65 | 66 | [DataView]: http://wicket.apache.org/docs/1.4/org/apache/wicket/markup/repeater/data/DataView.html 67 | [IDataProvider]: http://wicket.apache.org/docs/1.4/org/apache/wicket/markup/repeater/data/IDataProvider.html 68 | [ListDataProvider]: http://wicket.apache.org/docs/1.4/org/apache/wicket/markup/repeater/data/ListDataProvider.html 69 | [OegyScroll demo]: https://github.com/reaktor/oegyscroll/tree/master/oegyscroll-demo 70 | 71 | How Does It Work? 72 | ----------------- 73 | 74 | It splits your data into blocks of 100 items (yes, you can specify block 75 | size too) and at first, only shows a bunch of rows (actually the 76 | remainder of rowcount divided by block size). For the rest of the rows, 77 | it puts placeholder components in the markup and replaces them with 78 | actual rows when the placeholder gets visible in the browser. The 79 | visibility check is done using a piece of javascript that is run once a 80 | second and checks if there are placeholders that should be replaced with 81 | data rows. The javascript invokes the onclick behaviour attached to the 82 | placeholder, and the behavior replaces the placeholder with row data 83 | using Ajax. 84 | 85 | Developers 86 | ---------- 87 | 88 | * Juha Paananen 89 | * Antti Viljakainen 90 | 91 | Version Control 92 | --------------- 93 | 94 | * Github: [https://github.com/reaktor/oegyscroll](https://github.com/reaktor/oegyscroll) 95 | * git: `git://github.com/reaktor/oegyscroll.git` 96 | 97 | 98 | License 99 | ------- 100 | 101 | Copyright © 2009 original author or authors 102 | 103 | OegyScroll is Licensed under the 104 | [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 105 | -------------------------------------------------------------------------------- /backlog.txt: -------------------------------------------------------------------------------- 1 | - IE testing for the new javascript implementation 2 | - Where to deploy the javascripts? 3 | - Make Wicket code append all the required attributes 4 | - Use "onscroll" event or equivalent where applicable 5 | -------------------------------------------------------------------------------- /deploy-standalone-demo: -------------------------------------------------------------------------------- 1 | DEST=valtone.com:juhajasatu.com/html 2 | JS_SOURCE=oegyscroll/src/main/java/org/laughingpanda/wicket 3 | scp -r standalone-demo/* $DEST/oegydemo 4 | scp $JS_SOURCE/oegyscroll.js $DEST/oegyscroll 5 | scp $JS_SOURCE/oegyscroll-updater.js $DEST/oegyscroll 6 | -------------------------------------------------------------------------------- /oegyscroll-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | org.laughingpanda.oegyscroll 4 | oegyscroll-demo 5 | war 6 | 1.15-SNAPSHOT 7 | oegyscroll-demo 8 | https://github.com/reaktor/oegyscroll 9 | 10 | 11 | org.laughingpanda.oegyscroll 12 | oegyscroll 13 | 1.15-SNAPSHOT 14 | 15 | 16 | 17 | oegyscroll-demo 18 | 19 | 20 | org.apache.maven.plugins 21 | maven-compiler-plugin 22 | 23 | 1.5 24 | 1.5 25 | 26 | 27 | 28 | 29 | 30 | src/main/java 31 | 32 | **/*.java 33 | 34 | 35 | 36 | 37 | 38 | 39 | Laughing Panda SCP 40 | Laughing Panda 41 | scpexe://maven.laughingpanda.org:/var/www/maven.laughingpanda.org/maven2 42 | 43 | 44 | Laughing Panda SCP 45 | Laughing Panda 46 | scpexe://maven.laughingpanda.org:/var/www/maven.laughingpanda.org/maven2/snapshots 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /oegyscroll-demo/run: -------------------------------------------------------------------------------- 1 | mvn org.mortbay.jetty:maven-jetty-plugin:6.1.14:run 2 | -------------------------------------------------------------------------------- /oegyscroll-demo/src/main/java/org/laughingpanda/oegyscrolldemo/OegyScrollDemoApplication.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.oegyscrolldemo; 2 | 3 | import org.apache.wicket.Page; 4 | import org.apache.wicket.protocol.http.WebApplication; 5 | 6 | public class OegyScrollDemoApplication extends WebApplication { 7 | @Override 8 | public Class getHomePage() { 9 | return OegyScrollDemoPage.class; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /oegyscroll-demo/src/main/java/org/laughingpanda/oegyscrolldemo/OegyScrollDemoPage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 |
16 | 17 |

OegyScroll Demo Page

18 | 19 |
20 |
21 | 22 | 23 | 24 | 27 | 28 | 29 | 32 | 33 | 34 |
25 |   26 |
30 | 31 |
35 |
36 |
37 |
38 | Refresh 39 | 40 | -------------------------------------------------------------------------------- /oegyscroll-demo/src/main/java/org/laughingpanda/oegyscrolldemo/OegyScrollDemoPage.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.oegyscrolldemo; 2 | 3 | import java.util.Iterator; 4 | 5 | import org.apache.wicket.ajax.AjaxRequestTarget; 6 | import org.apache.wicket.ajax.markup.html.AjaxLink; 7 | import org.apache.wicket.markup.html.WebMarkupContainer; 8 | import org.apache.wicket.markup.html.WebPage; 9 | import org.apache.wicket.markup.html.basic.Label; 10 | import org.apache.wicket.markup.repeater.data.IDataProvider; 11 | import org.apache.wicket.model.IModel; 12 | import org.apache.wicket.model.Model; 13 | import org.laughingpanda.wicket.LazyLoadScrollableList; 14 | 15 | public class OegyScrollDemoPage extends WebPage { 16 | private final class SampleDataProvider implements 17 | IDataProvider { 18 | private final int size; 19 | 20 | private SampleDataProvider(int size) { 21 | this.size = size; 22 | } 23 | 24 | public Iterator iterator(final int first, final int count) { 25 | return new Iterator() { 26 | int index = first; 27 | 28 | public boolean hasNext() { 29 | return index < first + count; 30 | } 31 | 32 | public String next() { 33 | try { 34 | return "Row " + index + " of sample data set"; 35 | } finally { 36 | index++; 37 | } 38 | } 39 | 40 | public void remove() { 41 | throw new UnsupportedOperationException(); 42 | }}; 43 | } 44 | 45 | public IModel model(String object) { 46 | return new Model(object); 47 | } 48 | 49 | public int size() { 50 | return size; 51 | } 52 | 53 | public void detach() { 54 | } 55 | } 56 | 57 | static int counter = 0; 58 | 59 | 60 | public OegyScrollDemoPage() { 61 | this(100000, 100); 62 | } 63 | 64 | public OegyScrollDemoPage(final int size, final int blockSize) { 65 | final LazyLoadScrollableList scroller = new LazyLoadScrollableList("djuizyScroller", new SampleDataProvider(size), blockSize) { 66 | @Override 67 | protected void populateRow(final WebMarkupContainer rowContainer, final int index, final String modelObject) { 68 | rowContainer.add(new Label("rowLabel", new Model(modelObject))); 69 | } 70 | }; 71 | add(scroller); 72 | 73 | add(new AjaxLink("refresh") { 74 | @Override 75 | public void onClick(final AjaxRequestTarget target) { 76 | target.addComponent(scroller); 77 | } 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /oegyscroll-demo/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Oegyscroll demo web application 7 | 8 | wicket.front 9 | org.apache.wicket.protocol.http.WicketFilter 10 | 11 | applicationClassName 12 | org.laughingpanda.oegyscrolldemo.OegyScrollDemoApplication 13 | 14 | 15 | 16 | wicket.front 17 | /* 18 | 19 | 20 | -------------------------------------------------------------------------------- /oegyscroll-demo/src/main/webapp/images/loading-row.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktor/oegyscroll/ad382e04e1da034cdadfdaf9cba790f655a1abe7/oegyscroll-demo/src/main/webapp/images/loading-row.gif -------------------------------------------------------------------------------- /oegyscroll-demo/src/main/webapp/images/table-rows-striped.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktor/oegyscroll/ad382e04e1da034cdadfdaf9cba790f655a1abe7/oegyscroll-demo/src/main/webapp/images/table-rows-striped.gif -------------------------------------------------------------------------------- /oegyscroll/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | org.laughingpanda.oegyscroll 4 | oegyscroll 5 | jar 6 | 1.15-SNAPSHOT 7 | oegyscroll 8 | https://github.com/reaktor/oegyscroll 9 | 10 | org.laughingpanda.oegyscroll 11 | oegyscroll-root 12 | 1.15-SNAPSHOT 13 | .. 14 | 15 | 16 | 17 | org.jdave 18 | jdave-core 19 | 1.1 20 | 21 | 22 | org.jdave 23 | jdave-wicket 24 | 1.1 25 | 26 | 27 | org.jdave 28 | jdave-junit4 29 | 1.1 30 | 31 | 32 | org.mockito 33 | mockito-all 34 | 1.8.3 35 | test 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-compiler-plugin 43 | 44 | 1.5 45 | 1.5 46 | 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-surefire-plugin 51 | 2.4.2 52 | 53 | 54 | **/*Spec.java 55 | 56 | 57 | 58 | 59 | maven-source-plugin 60 | 2.1.1 61 | 62 | 63 | attach-sources 64 | verify 65 | 66 | jar-no-fork 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | src/test/java 75 | **/*.java 76 | 77 | 78 | 79 | 80 | src/main/java 81 | **/*.java 82 | 83 | 84 | 85 | 86 | 87 | Laughing Panda SCP 88 | Laughing Panda 89 | scpexe://maven.laughingpanda.org:/var/www/maven.laughingpanda.org/maven2 90 | 91 | 92 | Laughing Panda SCP 93 | Laughing Panda 94 | scpexe://maven.laughingpanda.org:/var/www/maven.laughingpanda.org/maven2/snapshots 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /oegyscroll/src/main/java/org/laughingpanda/wicket/Block.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.wicket; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.apache.wicket.Component; 6 | import org.apache.wicket.ajax.AjaxRequestTarget; 7 | import org.apache.wicket.behavior.AttributeAppender; 8 | import org.apache.wicket.markup.repeater.Item; 9 | import org.apache.wicket.model.Model; 10 | 11 | class Block implements Serializable { 12 | private Component placeholder; 13 | private RowDataView rowDataView; 14 | private final int startIndex; 15 | private final int itemCount; 16 | private Item> blockListItem; 17 | private final LazyLoadScrollableList list; 18 | 19 | public Block(final int startIndex, final int itemCount, final LazyLoadScrollableList list) { 20 | this.startIndex = startIndex; 21 | this.itemCount = itemCount; 22 | this.list = list; 23 | } 24 | 25 | public void populate(final Item> item) { 26 | item.setOutputMarkupId(true); 27 | item.add(new AttributeAppender("class", true, new Model("block"), " ")); 28 | this.blockListItem = item; 29 | placeholder = new PlaceHolder(this); 30 | item.add(placeholder); 31 | rowDataView = new RowDataView("row", list, startIndex); 32 | item.add(rowDataView); 33 | int initialRow = list.getInitialRow(); 34 | if (initialRow >= startIndex && initialRow < startIndex + itemCount) { 35 | showRows(); 36 | } 37 | } 38 | 39 | protected void showRows() { 40 | placeholder.setVisible(false); 41 | rowDataView.setDataProvider(new SublistDataProvider(list.getDataProvider(), startIndex, itemCount)); 42 | } 43 | 44 | public void showRows(final AjaxRequestTarget target) { 45 | showRows(); 46 | target.addComponent(blockListItem); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /oegyscroll/src/main/java/org/laughingpanda/wicket/LazyLoadScrollableList.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.wicket; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import org.apache.wicket.ResourceReference; 8 | import org.apache.wicket.markup.html.IHeaderContributor; 9 | import org.apache.wicket.markup.html.IHeaderResponse; 10 | import org.apache.wicket.markup.html.WebMarkupContainer; 11 | import org.apache.wicket.markup.repeater.data.IDataProvider; 12 | 13 | public abstract class LazyLoadScrollableList extends WebMarkupContainer implements IHeaderContributor { 14 | private int remainder; 15 | private int blockCountExcludingRemainderBlock; 16 | private final IDataProvider dataProvider; 17 | private final int blockSize; 18 | private final List> blocks = new ArrayList>(); 19 | private boolean javaScriptInitialized; 20 | private int initialRow; 21 | 22 | public LazyLoadScrollableList(final String id, final IDataProvider dataProvider, final int blockSize) { 23 | super(id); 24 | this.blockSize = blockSize; 25 | this.dataProvider = dataProvider; 26 | add(new ScrolledContentView("scrolledContent", blocks)); 27 | setMarkupId("scroller" + System.identityHashCode(this)); 28 | setOutputMarkupId(true); 29 | } 30 | 31 | public int getInitialRow() { 32 | return initialRow; 33 | } 34 | 35 | public void setInitialRow(final int initialRow) { 36 | this.initialRow = initialRow; 37 | } 38 | 39 | @Override 40 | protected void onBeforeRender() { 41 | calculateBlockCountAndRemainder(); 42 | blocks.clear(); 43 | blocks.add(new RemainderBlock(remainder, this)); 44 | for (int i = 0; i < blockCountExcludingRemainderBlock; i++) { 45 | blocks.add(new Block(i * blockSize + remainder, blockSize, this)); 46 | } 47 | super.onBeforeRender(); 48 | } 49 | 50 | private void calculateBlockCountAndRemainder() { 51 | int rowCount = getDataProvider().size(); 52 | blockCountExcludingRemainderBlock = rowCount / blockSize; 53 | remainder = rowCount - blockCountExcludingRemainderBlock * blockSize; 54 | if (remainder == 0 && blockCountExcludingRemainderBlock > 0) { 55 | remainder = blockSize; 56 | blockCountExcludingRemainderBlock--; 57 | } 58 | } 59 | 60 | protected abstract void populateRow(final WebMarkupContainer rowContainer, final int index, final T modelObject); 61 | 62 | public IDataProvider getDataProvider() { 63 | return dataProvider; 64 | } 65 | 66 | public void renderHead(IHeaderResponse response) { 67 | if (!javaScriptInitialized) { 68 | addScrollableListJavascript(response); 69 | addContentLoaderInitializationJavascript(response); 70 | javaScriptInitialized = true; 71 | } 72 | } 73 | 74 | private void addScrollableListJavascript(IHeaderResponse response) { 75 | response.renderJavascriptReference(new ResourceReference(LazyLoadScrollableList.class, "oegyscroll-updater.js")); 76 | } 77 | 78 | private void addContentLoaderInitializationJavascript(IHeaderResponse response) { 79 | final String scrollerId = getMarkupId(); 80 | final String scrolledContentId = get("scrolledContent").getMarkupId(); 81 | response.renderOnDomReadyJavascript("if (!window.oegyscroll) { window.oegyscroll = {timers: []}};" + 82 | "window.oegyscroll.timers['"+scrollerId+"'] = new OegyScrollUpdater(\""+scrollerId+"\", \""+scrolledContentId+"\").scheduleScrollPositionUpdate();"); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /oegyscroll/src/main/java/org/laughingpanda/wicket/PlaceHolder.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.wicket; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.apache.wicket.AttributeModifier; 6 | import org.apache.wicket.ajax.AjaxEventBehavior; 7 | import org.apache.wicket.ajax.AjaxRequestTarget; 8 | import org.apache.wicket.markup.html.WebMarkupContainer; 9 | import org.apache.wicket.model.Model; 10 | 11 | class PlaceHolder extends WebMarkupContainer { 12 | public PlaceHolder(final Block block) { 13 | super("placeholder"); 14 | add(new AjaxEventBehavior("onclick") { 15 | @Override 16 | protected void onEvent(final AjaxRequestTarget target) { 17 | block.showRows(target); 18 | } 19 | }); 20 | add(new AttributeModifier("class", true, new Model("loader-placeholder"))); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /oegyscroll/src/main/java/org/laughingpanda/wicket/ProxyDataProvider.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.wicket; 2 | 3 | import java.util.Iterator; 4 | 5 | import org.apache.wicket.markup.repeater.data.EmptyDataProvider; 6 | import org.apache.wicket.markup.repeater.data.IDataProvider; 7 | import org.apache.wicket.model.IModel; 8 | 9 | class ProxyDataProvider implements IDataProvider { 10 | private IDataProvider dataProvider; 11 | 12 | public ProxyDataProvider() { 13 | this(new EmptyDataProvider()); 14 | } 15 | 16 | public ProxyDataProvider(final IDataProvider dataProvider) { 17 | super(); 18 | this.dataProvider = dataProvider; 19 | } 20 | 21 | public void setDataProvider(final IDataProvider dataProvider) { 22 | this.dataProvider = dataProvider; 23 | } 24 | 25 | public void detach() { 26 | dataProvider.detach(); 27 | } 28 | 29 | public Iterator iterator(final int i, final int j) { 30 | return dataProvider.iterator(i, j); 31 | } 32 | 33 | public IModel model(final T obj) { 34 | return dataProvider.model(obj); 35 | } 36 | 37 | public int size() { 38 | return dataProvider.size(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /oegyscroll/src/main/java/org/laughingpanda/wicket/RemainderBlock.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.wicket; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.apache.wicket.markup.repeater.Item; 6 | 7 | class RemainderBlock extends Block { 8 | public RemainderBlock(int remainder, LazyLoadScrollableList list) { 9 | super(0, remainder, list); 10 | } 11 | 12 | @Override 13 | public void populate(final Item> item) { 14 | super.populate(item); 15 | showRows(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /oegyscroll/src/main/java/org/laughingpanda/wicket/RowDataView.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.wicket; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.apache.wicket.AttributeModifier; 6 | import org.apache.wicket.markup.repeater.Item; 7 | import org.apache.wicket.markup.repeater.data.DataView; 8 | import org.apache.wicket.model.Model; 9 | 10 | class RowDataView extends DataView { 11 | private final LazyLoadScrollableList list; 12 | private final int offset; 13 | 14 | public RowDataView(final String id, LazyLoadScrollableList list, int offset) { 15 | super(id, new ProxyDataProvider()); 16 | this.list = list; 17 | this.offset = offset; 18 | } 19 | 20 | public void setDataProvider(final SublistDataProvider dataProvider) { 21 | ((ProxyDataProvider) getDataProvider()).setDataProvider(dataProvider); 22 | } 23 | 24 | @Override 25 | protected void populateItem(final Item item) { 26 | T modelObject = item.getModelObject(); 27 | item.add(new AttributeModifier("class", true, new Model("loaded-row"))); 28 | list.populateRow(item, offset + item.getIndex(), modelObject); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /oegyscroll/src/main/java/org/laughingpanda/wicket/ScrolledContentView.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.wicket; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | import org.apache.wicket.markup.html.WebMarkupContainer; 7 | import org.apache.wicket.markup.repeater.Item; 8 | import org.apache.wicket.markup.repeater.data.DataView; 9 | import org.apache.wicket.markup.repeater.data.ListDataProvider; 10 | 11 | class ScrolledContentView extends WebMarkupContainer { 12 | public ScrolledContentView(final String id, final List> blocks) { 13 | super(id); 14 | setOutputMarkupId(true); 15 | add(new DataView>("block", new ListDataProvider>(blocks)) { 16 | @Override 17 | protected void populateItem(Item> item) { 18 | Block block= item.getModelObject(); 19 | block.populate(item); 20 | } 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /oegyscroll/src/main/java/org/laughingpanda/wicket/SublistDataProvider.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.wicket; 2 | 3 | import java.util.Iterator; 4 | 5 | import org.apache.wicket.markup.repeater.data.IDataProvider; 6 | import org.apache.wicket.model.IModel; 7 | 8 | public class SublistDataProvider implements IDataProvider { 9 | private final IDataProvider dataProvider; 10 | private final int index; 11 | private final int count; 12 | 13 | public SublistDataProvider(final IDataProvider dataProvider, final int index, final int count) { 14 | super(); 15 | this.dataProvider = dataProvider; 16 | this.index = index; 17 | this.count = count; 18 | } 19 | 20 | public Iterator iterator(final int index, final int count) { 21 | return dataProvider.iterator(this.index + index, count); 22 | } 23 | 24 | public IModel model(final T obj) { 25 | return dataProvider.model(obj); 26 | } 27 | 28 | public int size() { 29 | return count; 30 | } 31 | 32 | public void detach() { 33 | dataProvider.detach(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /oegyscroll/src/main/java/org/laughingpanda/wicket/oegyscroll-updater.js: -------------------------------------------------------------------------------- 1 | function OegyScrollUpdater(scrollerId, scrolledContentId) { 2 | this.scrollerId = scrollerId; 3 | this.scrolledContentId = scrolledContentId; 4 | } 5 | 6 | OegyScrollUpdater.prototype.scheduleScrollPositionUpdate = function() { 7 | var self = this; 8 | return window.setInterval(function() { 9 | new OegyScrollUpdater(self.scrollerId, self.scrolledContentId).checkScrollPositionRepeatedly(); 10 | }, 1000); 11 | } 12 | 13 | OegyScrollUpdater.prototype.checkScrollPositionRepeatedly = function() { 14 | try { 15 | this.checkScrollPosition(); 16 | } catch (err) { 17 | this.error('Error occurred while updating scroller: ' + err); 18 | } 19 | } 20 | 21 | OegyScrollUpdater.prototype.checkScrollPosition = function() { 22 | if (this.scroller() && this.contentScrolled()) { 23 | this.refreshPlaceholders(); 24 | } 25 | } 26 | 27 | OegyScrollUpdater.prototype.scroller = function() { 28 | return this.elementById(document, this.scrollerId); 29 | } 30 | 31 | OegyScrollUpdater.prototype.contentScrolled = function() { 32 | return this.elementById(this.scroller(), this.scrolledContentId); 33 | } 34 | 35 | OegyScrollUpdater.prototype.refreshPlaceholders = function() { 36 | this.blokz = undefined; 37 | if (!this.getBlock(1)) return; 38 | for (blockIndex = this.getFirstVisibleBlock(); this.blockPosition(blockIndex) < this.scrollPos() + this.viewHeight() ; blockIndex++) { 39 | block = this.getBlock(blockIndex); 40 | placeholder = $(".loader-placeholder", $(block)).get(0); 41 | if (placeholder) { 42 | placeholder.onclick(); 43 | } 44 | } 45 | } 46 | 47 | OegyScrollUpdater.prototype.viewHeight = function() { 48 | return this.scroller().offsetHeight; 49 | } 50 | 51 | OegyScrollUpdater.prototype.scrollPos = function() { 52 | return this.scroller().scrollTop; 53 | } 54 | 55 | OegyScrollUpdater.prototype.blockHeight = function() { 56 | return this.getBlock(1).offsetHeight; 57 | } 58 | 59 | OegyScrollUpdater.prototype.getBlock = function(index) { 60 | if (!this.blokz) { 61 | this.blokz = $(".block", this.contentScrolled()); 62 | } 63 | return this.blokz.get(index); 64 | } 65 | 66 | OegyScrollUpdater.prototype.blockPosition = function(index) { 67 | block = this.getBlock(index); 68 | if (block) 69 | return block.offsetTop; 70 | } 71 | 72 | OegyScrollUpdater.prototype.getFirstVisibleBlock = function() { 73 | return this.getFirstVisibleBlockStartingFrom(this.guessFirstVisibleBlock()); 74 | } 75 | 76 | OegyScrollUpdater.prototype.getFirstVisibleBlockStartingFrom = function(from) { 77 | while (from > 0 && (!this.getBlock(from) || this.scrollPos() < this.blockPosition(from))) { 78 | from -= 1; 79 | } 80 | while (this.scrollPos() > this.blockPosition(from) + this.blockHeight()) { 81 | from += 1; 82 | } 83 | return from; 84 | } 85 | 86 | OegyScrollUpdater.prototype.guessFirstVisibleBlock = function () { 87 | return Math.floor(Math.abs((this.scrollPos() - this.blockPosition(0)) / this.blockHeight())); 88 | } 89 | 90 | OegyScrollUpdater.prototype.error = function(text) { 91 | if (typeof(window["console"]) != "undefined") { 92 | console.log(text); 93 | } 94 | } 95 | 96 | OegyScrollUpdater.prototype.elementById = function(parent, id) { 97 | var element = $("#" + id, $(parent)).get(0); 98 | if (!element) { 99 | this.error('Element ' + id + ' not found from page.'); 100 | } 101 | return element; 102 | } -------------------------------------------------------------------------------- /oegyscroll/src/main/java/org/laughingpanda/wicket/oegyscroll.js: -------------------------------------------------------------------------------- 1 | function OegyScroll(blockSize, height, rowHeight, id) { 2 | this.height = height; 3 | this.rowHeight = rowHeight; 4 | this.blockSize = blockSize; 5 | this.id = (id) ? id : "scroller"; 6 | this.contentId = "content"; 7 | } 8 | OegyScroll.prototype.createBlock = function (contentCreator) { 9 | var block = $("").addClass("block"); 10 | contentCreator(block); 11 | block.appendTo(this.contentArea); 12 | return block; 13 | } 14 | OegyScroll.prototype.createPlaceholder = function (id, content) { 15 | return $(''+content+''); 16 | }; 17 | OegyScroll.prototype.generateMarkup = function() { 18 | this.main = $('
'); 19 | this.main.attr("id", this.id); 20 | this.scrollable = $('
').appendTo(this.main); 21 | this.contentArea = $('
').appendTo(this.scrollable); 22 | } 23 | OegyScroll.prototype.createRow = function (content) { 24 | return $('' + content + ''); 25 | } 26 | OegyScroll.prototype.createPlaceHolderBlock = function(id, content, onclickFunction) { 27 | var placeHolder = this.createPlaceholder(id, content); 28 | var block = this.createBlock(function(block){ 29 | placeHolder.appendTo(block) 30 | }); 31 | placeHolder.get(0).onclick=onclickFunction; 32 | return block; 33 | } 34 | OegyScroll.prototype.checkInit = function() { 35 | if (!this.main) { 36 | this.main=$("#" + this.id); 37 | if (this.main.size() > 0) { 38 | this.contentArea=$("#" + this.contentId, this.main); 39 | } else { 40 | this.generateMarkup(); 41 | } 42 | } 43 | } 44 | OegyScroll.prototype.appendTo=function(body) { 45 | this.checkInit(); 46 | this.main.appendTo(body); 47 | } 48 | OegyScroll.prototype.init = function(rowCount, rowFetcher, placeHolderContent) { 49 | this.initFetchByBlock(rowCount, function(oegy, block, start, end) { 50 | for (row = start; row < end; row++) { 51 | oegy.createRow(rowFetcher(row)).attr("id", "row-" + (row+1)).appendTo(block); 52 | } 53 | }, placeHolderContent); 54 | } 55 | OegyScroll.prototype.initFetchByBlock = function(rowCount, blockFetcher, placeHolderContent) { 56 | this.checkInit(); 57 | this.contentArea.empty(); 58 | var oegy = this; 59 | this.remainder = rowCount % this.blockSize; 60 | if (this.remainder == 0) this.remainder = this.blockSize; 61 | this.createBlock(function(block) { 62 | oegy.appendRowsToBlock(block, 0, oegy.remainder, blockFetcher); 63 | }); 64 | this.blockCount = (rowCount - this.remainder) / this.blockSize; 65 | for (i = 0; i < this.blockCount; i++) { 66 | this.createAutomaticPlaceholder(i, blockFetcher, placeHolderContent); 67 | } 68 | } 69 | OegyScroll.prototype.createAutomaticPlaceholder = function(blockNumber, blockFetcher, content) { 70 | var oegy = this; 71 | this.createPlaceHolderBlock('placeholder-' + blockNumber, content, function() { 72 | oegy.replacePlaceholderWithData($(this), blockNumber, blockFetcher); 73 | }); 74 | } 75 | OegyScroll.prototype.replacePlaceholderWithData = function(placeholder, blockNumber, blockFetcher) { 76 | block = placeholder.parent(); 77 | offset = blockNumber * this.blockSize; 78 | start = offset + this.remainder; 79 | end = offset + this.blockSize + this.remainder; 80 | this.appendRowsToBlock(block, start, end, blockFetcher); 81 | placeholder.remove(); 82 | } 83 | OegyScroll.prototype.appendRowsToBlock = function(block, start, end, blockFetcher) { 84 | blockFetcher(this, block, start, end); 85 | } 86 | OegyScroll.prototype.scrollTo = function(yPos) { 87 | this.main.attr('scrollTop', yPos); 88 | }; 89 | OegyScroll.prototype.refresh = function() { 90 | this.updater().checkScrollPosition(); 91 | } 92 | OegyScroll.prototype.autorefresh = function() { 93 | this.updater().checkScrollPositionRepeatedly(); 94 | } 95 | OegyScroll.prototype.updater = function() { 96 | return new OegyScrollUpdater(this.id, this.contentId); 97 | } -------------------------------------------------------------------------------- /oegyscroll/src/test/java/org/laughingpanda/wicket/LazyLoadScrollableListSpec.java: -------------------------------------------------------------------------------- 1 | package org.laughingpanda.wicket; 2 | 3 | import static java.util.Collections.emptyList; 4 | import static org.mockito.Matchers.any; 5 | import static org.mockito.Mockito.verify; 6 | import static org.mockito.Mockito.verifyNoMoreInteractions; 7 | 8 | import java.io.Serializable; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | import org.apache.wicket.ResourceReference; 14 | import org.apache.wicket.ajax.markup.html.AjaxLink; 15 | import org.apache.wicket.markup.html.IHeaderResponse; 16 | import org.apache.wicket.markup.html.WebMarkupContainer; 17 | import org.apache.wicket.markup.html.basic.Label; 18 | import org.apache.wicket.model.IModel; 19 | import org.junit.runner.RunWith; 20 | import org.mockito.Mockito; 21 | 22 | import jdave.junit4.JDaveRunner; 23 | import jdave.wicket.ComponentSpecification; 24 | 25 | @RunWith(JDaveRunner.class) 26 | public class LazyLoadScrollableListSpec extends ComponentSpecification { 27 | List testData = emptyList(); 28 | 29 | private void createTestData(final int rowCount) { 30 | testData = new ArrayList(rowCount); 31 | for (int i = 0; i < rowCount; i++) { 32 | testData.add(new String("row" + i)); 33 | } 34 | } 35 | int blockSize = 4; 36 | 37 | @SuppressWarnings("unchecked") 38 | @Override 39 | protected LazyLoadScrollableListTestPage newComponent(final String id, final IModel model) { 40 | return new LazyLoadScrollableListTestPage(testData, blockSize); 41 | } 42 | 43 | public class AnyScroller { 44 | public LazyLoadScrollableListTestPage create() { 45 | return startComponent(); 46 | } 47 | 48 | public void hasFixedMarkupId() { 49 | specify(getScroller().getMarkupId(), must.equal("scroller" + System.identityHashCode(getScroller()))); 50 | } 51 | 52 | public void hasOutputMarkupIdSetToTrue() { 53 | specify(getScroller().getOutputMarkupId()); 54 | } 55 | } 56 | 57 | public class AnyScrollerWithData { 58 | public LazyLoadScrollableListTestPage create() { 59 | blockSize = 5; 60 | createTestData(7); 61 | return startComponent(); 62 | } 63 | 64 | public void setsClassAttributeOfBlockElements() { 65 | String attribute = wicket.getTagByWicketId("block").getAttribute("class"); 66 | specify(attribute, must.equal("markup-class-for-block block")); 67 | } 68 | } 69 | 70 | public class WhenDatasetSizeIsZero { 71 | public LazyLoadScrollableListTestPage create() { 72 | testData = Arrays.asList(); 73 | return startComponent(); 74 | } 75 | 76 | public void noRowsAreRendered() { 77 | specify(getRenderedRows().size(), 0); 78 | } 79 | 80 | public void thereIsOnePlaceholderButItIsInvisible() { 81 | specify(getPlaceholders().size(), 1); 82 | specify(getPlaceholders().get(0).isVisible(), false); 83 | } 84 | } 85 | 86 | public class WhenDatasetIsLessThanBlockSize { 87 | public LazyLoadScrollableListTestPage create() { 88 | blockSize = 4; 89 | createTestData(3); 90 | return startComponent(); 91 | } 92 | 93 | public void allRowsAreRendered() { 94 | specify(getRenderedRows().size(), 3); 95 | } 96 | 97 | public void placeHolderIsHidden() { 98 | specify(getPlaceholders().get(0).isVisible(), false); 99 | } 100 | 101 | public void providesIndexForRows() { 102 | verifyIndexes(); 103 | } 104 | } 105 | 106 | public class WhenDatasetIsEqualToBlockSize { 107 | public LazyLoadScrollableListTestPage create() { 108 | blockSize = 4; 109 | createTestData(blockSize); 110 | return startComponent(); 111 | } 112 | 113 | public void allRowsAreRendered() { 114 | specify(getRenderedRows().size(), 4); 115 | } 116 | 117 | public void placeHolderIsHidden() { 118 | specify(getPlaceholders().get(0).isVisible(), false); 119 | } 120 | 121 | public void providesIndexForRows() { 122 | verifyIndexes(); 123 | } 124 | } 125 | 126 | public abstract class WhenDataSetIsLargerThanBlockSize { 127 | public void placeHolderForFirstBlockIsHidden() { 128 | specify(getPlaceholders().get(0).isVisible(), false); 129 | } 130 | 131 | public void placeHoldersForOtherBlocksAreShown() { 132 | specify(getPlaceholders().get(1).isVisible()); 133 | } 134 | 135 | public void providesConsecutiveIndexesForRowsOnDifferentBlocks() { 136 | showSecondBlock(); 137 | verifyIndexes(); 138 | } 139 | } 140 | 141 | public class WhenDatasetIsTwoTimesBlockSize extends WhenDataSetIsLargerThanBlockSize { 142 | public LazyLoadScrollableListTestPage create() { 143 | blockSize = 4; 144 | createTestData(8); 145 | return startComponent(); 146 | } 147 | 148 | public void firstBlockIsRendered() { 149 | specify(getRenderedRows().size(), 4); 150 | } 151 | 152 | public void secondBlockIsRenderedWhenClicked() { 153 | showSecondBlock(); 154 | specify(getPlaceholders().get(1).isVisible(), false); 155 | specify(getRenderedRows().size(), 8); 156 | } 157 | } 158 | 159 | public class WhenDataSetIsBlockSizePlus1 { 160 | public LazyLoadScrollableListTestPage create() { 161 | blockSize = 5; 162 | createTestData(6); 163 | return startComponent(); 164 | } 165 | 166 | public void remainderBlockIsRendered() { 167 | specify(getRenderedRows().size(), 1); 168 | } 169 | 170 | public void secondBlockIsRenderedWhenClicked() { 171 | showSecondBlock(); 172 | specify(getPlaceholders().get(1).isVisible(), false); 173 | specify(getRenderedRows().size(), 6); 174 | } 175 | } 176 | 177 | public class WhenDataSetIsBlockSizePlus2 { 178 | public LazyLoadScrollableListTestPage create() { 179 | blockSize = 5; 180 | createTestData(7); 181 | return startComponent(); 182 | } 183 | 184 | public void remainderBlockIsRendered() { 185 | specify(getRenderedRows().size(), 2); 186 | } 187 | 188 | public void secondBlockIsRenderedWhenClicked() { 189 | showSecondBlock(); 190 | specify(getPlaceholders().get(1).isVisible(), false); 191 | specify(getRenderedRows().size(), 7); 192 | } 193 | } 194 | 195 | public class WhenSettingInitialRow { 196 | public LazyLoadScrollableListTestPage create() { 197 | blockSize = 5; 198 | createTestData(6); 199 | return startComponent(); 200 | } 201 | 202 | public void blockContainingInitialRowIsRendered() { 203 | getScroller().setInitialRow(blockSize); 204 | wicket.executeAjaxEvent(selectFirst(AjaxLink.class, "refresh").from(context), "onclick"); 205 | specify(getRenderedRows().size(), 6); 206 | } 207 | } 208 | 209 | public class WhenUpdatingListUsingAjax { 210 | public LazyLoadScrollableListTestPage create() { 211 | createTestData(2); 212 | return startComponent(); 213 | } 214 | 215 | public void contentIsUpdated() { 216 | wicket.executeAjaxEvent(selectFirst(AjaxLink.class, "replaceData").from(context), "onclick"); 217 | String newTextOnFirstRow = getTextOnRow(0); 218 | specify(newTextOnFirstRow, must.equal("lol")); 219 | } 220 | 221 | private String getTextOnRow(final int row) { 222 | return getRenderedRows().get(row).getDefaultModelObjectAsString(); 223 | } 224 | } 225 | 226 | public class RendersJavascript { 227 | IHeaderResponse response = Mockito.mock(IHeaderResponse.class); 228 | DummyScroller dummyScroller = new DummyScroller(); 229 | 230 | public void onFirstRendering() { 231 | render(); 232 | verify(response).renderJavascriptReference(any(ResourceReference.class)); 233 | verify(response).renderOnDomReadyJavascript(any(String.class)); 234 | } 235 | 236 | public void notTwice() { 237 | onFirstRendering(); 238 | render(); 239 | verifyNoMoreInteractions(response); 240 | } 241 | 242 | private void render() { 243 | dummyScroller.renderHead(response); 244 | } 245 | } 246 | 247 | @SuppressWarnings("unchecked") 248 | private List getPlaceholders() { 249 | return selectAll(PlaceHolder.class).from(context); 250 | } 251 | 252 | private List