Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infinite scrolling in a container. #281

Open
wants to merge 1 commit into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ www/.last_published
www/_site/
node_modules
rails-demo
.jekyll-metadata

# Dist folder is only for deployment to bower/npm/etc...
# Dist should contain unversioned files as bower/npm/etc is what does the versioning for these files
Expand Down
83 changes: 66 additions & 17 deletions src/intercooler.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ var Intercooler = Intercooler || (function() {
'ic-style-src', 'ic-attr-src', 'ic-prepend-from', 'ic-append-from', 'ic-action'],
function(elt){ return fixICAttributeName(elt) });

var _scrollHandler = null;
var _UUID = 1;
var _readyHandlers = [];

Expand Down Expand Up @@ -141,7 +140,9 @@ var Intercooler = Intercooler || (function() {
}
triggerEvent(elt, "log.ic", [msg, level, elt]);
if (level == "ERROR") {
if (window.console) {
if (typeof(console.error) === 'function') {
console.error("Intercooler Error : " + msg);
} else if (window.console) {
window.console.log("Intercooler Error : " + msg);
}
var errorUrl = closestAttrValue($('body'), 'ic-post-errors-to');
Expand Down Expand Up @@ -176,21 +177,57 @@ var Intercooler = Intercooler || (function() {
return "[" + fixICAttributeName(attribute) + "]";
}

function initScrollHandler() {
if (_scrollHandler == null) {
_scrollHandler = function() {
$(getICAttributeSelector("ic-trigger-on='scrolled-into-view'")).each(function() {
var _this = $(this);
if (isScrolledIntoView(getTriggeredElement(_this)) && _this.data('ic-scrolled-into-view-loaded') != true) {
_this.data('ic-scrolled-into-view-loaded', true);
fireICRequest(_this);
}
});
};
$(window).scroll(_scrollHandler);
function addScrollHandler(elt) {
var _scrollContainer = scrollContainer(elt)
if (_scrollContainer == null) _scrollContainer = $(window)

// Determine best possible handling of scrolling
var handlertype = 'calculated';
if (typeof(elt.isInView) === 'function')
handlertype = 'isinview';
// TODO: For modern browsers, support for IntersectionObserver should be added

log(elt, "handlertype: " + handlertype, 'DEBUG')

if (elt.data('ic-has-scroll-handler') != true) {
log(elt, 'Adding scroll handler to ' + elt, 'DEBUG')
switch(handlertype) {
case 'calculated':
var _scrollHandler = function() {
$(getICAttributeSelector("ic-trigger-on='scrolled-into-view'")).each(function() { // TODO: Why the "each()"?
var _this = $(this);
if (isScrolledIntoView_calculated(getTriggeredElement(_this)) && _this.data('ic-scrolled-into-view-loaded') != true) {
_this.data('ic-scrolled-into-view-loaded', true);
fireICRequest(_this);
}
});
};
_scrollContainer.scroll(_scrollHandler)
break;
case 'isinview':
var _scrollHandler = function() {
$(getICAttributeSelector("ic-trigger-on='scrolled-into-view'")).each(function() { // TODO: Why the "each()"?
var _this = $(this);
if (isScrolledIntoView_isInView(getTriggeredElement(_this)) && _this.data('ic-scrolled-into-view-loaded') != true) {
_this.data('ic-scrolled-into-view-loaded', true);
fireICRequest(_this);
}
});
};
_scrollContainer.scroll(_scrollHandler)
break;
}
elt.data('ic-has-scroll-handler', true)
}
}

function scrollContainer(elt) {
var _scrollContainer = $(elt).closest('.ic-scroll-container')
if (_scrollContainer == null) _scrollContainer = $(window)
return _scrollContainer
}


function currentUrl() {
return window.location.pathname + window.location.search + window.location.hash;
}
Expand Down Expand Up @@ -1108,9 +1145,9 @@ var Intercooler = Intercooler || (function() {
if (getICAttribute(elt, 'ic-trigger-on') == 'load') {
fireICRequest(elt);
} else if (getICAttribute(elt, 'ic-trigger-on') == 'scrolled-into-view') {
initScrollHandler();
addScrollHandler(elt);
setTimeout(function() {
triggerEvent($(window), 'scroll');
triggerEvent(scrollContainer(elt), 'scroll');
}, 100); // Trigger a scroll in case element is already viewable
} else {
var triggerOn = getICAttribute(elt, 'ic-trigger-on').split(" ");
Expand Down Expand Up @@ -1290,8 +1327,18 @@ var Intercooler = Intercooler || (function() {
// Utilities
//============================================================----

function isScrolledIntoView(elem) {
function isScrolledIntoView_isInView(elem) {
elem = $(elem);
var _scrollContainer = scrollContainer(elem)

var inview = elem.isInView(_scrollContainer, {partial: true, direction: "vertical"})
log(elem, 'isScrolledIntoView: ' + inview, 'DEBUG')
return inview
}

function isScrolledIntoView_calculated(elem) {
elem = $(elem);

if (elem.height() == 0 && elem.width() == 0) {
return false;
}
Expand All @@ -1305,6 +1352,7 @@ var Intercooler = Intercooler || (function() {
&& (elemBottom <= docViewBottom) && (elemTop >= docViewTop));
}


function maybeScrollToTarget(elt, target) {
if (closestAttrValue(elt, 'ic-scroll-to-target') != "false" &&
(closestAttrValue(elt, 'ic-scroll-to-target') == 'true' ||
Expand Down Expand Up @@ -1978,6 +2026,7 @@ var Intercooler = Intercooler || (function() {

function init() {
var elt = $('body');
log(elt, 'Intercooler.init', 'DEBUG')
processNodes(elt);
fireReadyStuff(elt);
if(_history) {
Expand Down
18 changes: 18 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
This folder contains the unit tests for Intercooler.js

### Generating the tests
The original tests are contained in `unit_tests.html`. Versions of this file for older versions of jQuery and for Zepto can be generated from there.

Run `ruby gen_tests.rb`.

### Adding new tests
Edit the file `unit_tests.html`, then regenerate the tests

The unit tests are built using the QUnit framework: https://qunitjs.com/

### Running the tests
Open the test file in a browser from the local file system.

Or, from the `test` directory, run `ruby -run -e httpd .. -p 9000`.

Then, go to http://127.0.0.1:9000/test and pick the test you want to run.
26 changes: 25 additions & 1 deletion test/unit_tests.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="A Javascript-optional AJAX library featuring declarative, REST-ful bindings">
<meta name="author" content="">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">

<title>Intercooler.js</title>

Expand All @@ -23,6 +26,8 @@

<!--BEGIN HEADER-->
<script src="lib/jquery-3.1.1.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery.documentsize@1.2.5/dist/jquery.documentsize.min.js" integrity="sha256-9Psfu7Qmn9nczYC13Sbg+pf66mg/YgSSd//pmZ36SB0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery.isinview@1.0.6/dist/jquery.isinview.min.js" integrity="sha256-5FMd/bsHKHteo4x/343J+eFUHnAh0mytkfdYC1/1VIQ=" crossorigin="anonymous"></script>
<script type="text/javascript" src="lib/jquery.mockjax-2.2.1.js"></script>
<!--END HEADER-->

Expand Down Expand Up @@ -86,7 +91,7 @@
QUnit.test(name, function (assert) {
var done = assert.async();
beforeRequest(assert);
var delay = timeout || 100;
var delay = timeout || 1000;
setTimeout(function () {
afterRequest(assert);
done();
Expand Down Expand Up @@ -2587,6 +2592,25 @@ <h1>Content</h1>
});
</script>

<div id="scrolling-viewport" class="ic-scroll-container" style="height: 50px; overflow-y: auto; border-style: solid; border-width: 2px; border-color: black">
<div id="scrollrow1">Content of scroll row #1</div>
<div id="scrollrow2" ic-trigger-on="scrolled-into-view" ic-target="#scrolling-viewport" ic-append-from="/scroll-row">Content of scroll row #2</div>
</div>

<script>
$(function() {
$(window).on('log.ic', function(evt, msg, level, elt){
console.log(msg);
});
})
intercoolerTest("test scrolled-into-view functionality",
function(assert) {
$.mockjax({ url: "/scroll-row", responseText:"<div>Loaded scroll row #3</div>"});
assert.equal(true, $("#scrollrow2").data("ic-has-scroll-handler"));
},
function(assert) {
});
</script>


</div>
Expand Down
132 changes: 121 additions & 11 deletions www/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ <h3>Table Of Contents</h3>
<li><a href="#transitions">CSS Element Transitions</a></li>
<li><a href="#client-side">Client Side Tools</a></li>
<li><a href="#history">History</a></li>
<li><a href="#progressive_enhancement">Progressive Enhancement</a></li>
<li><a href="#sse">Server Sent Events <sub>BETA</sub></a></li>
<li><a href="#requests">Anatomy of an Intercooler Request</a></li>
<li><a href="#responses">Anatomy of an Intercooler Response</a></li>
Expand Down Expand Up @@ -109,21 +110,13 @@ <h2>Intercooler in a Nutshell</h2>

<h2>Installing Intercooler</h2>

<p>Intercooler is just another javascript library, and can be either installed locally with your web
application,
loaded off our CDN (generously donated by <a href="https://www.maxcdn.com/">MaxCDN</a>).</p>

<pre style="width:90%">
&lt;script src="https://code.jquery.com/jquery-3.1.1.min.js">&lt;/script>
&lt;script src="https://intercoolerreleases-leaddynocom.netdna-ssl.com/intercooler-1.2.2.min.js">&lt;/script>
</pre>
<p>Intercooler is just another javascript library. You can download and install the latest version from
the <a href="download.html">release</a> page.</p>

<p>If you are using <a href="http://bower.io/">Bower</a>, the package name for Intercooler is
<code>intercooler-js</code>.</p>

<p>Intercooler depends on JQuery, version 1.10.0 or higher.</p>

<p>You can always grab the latest code from the <a href="download.html">Downloads</a> page.</p>
<p>Intercooler depends on JQuery, version 1.10.0 or higher, and is compatible with JQuery 1, 2 and 3.</p>

</section>

Expand Down Expand Up @@ -296,6 +289,16 @@ <h3>Special Events</h3>
This can be useful for implementing UI patterns such as infinite scroll or lazy image loading.
</p>

<p>
<b>Note: </b>Out of the box, Intercooler supports <code>scrolled-into-view</code> for layouts where the scrolling viewport is the same height as the entire window.
Loading two additional libraries ahead of loading Intercooler.js automatically gives it support for scrolling in smaller viewports (there is no flag, the mere presence of the libraries triggers the enhanced behavior).
A CSS class called <code>ic-scroll-container</code> is used to indicate to Intercooler which parent element is the viewport. When no parent element is tagged with <code>ic-scroll-container</code> the viewport defaults to
the entire window (same behavior as regular Intercooler).
<div class="live-demo">
See the examples section for a <a href="examples/infinitescroll-viewport.html">working example</a>.
</div>
</p>

</section>

<section>
Expand Down Expand Up @@ -579,6 +582,14 @@ <h3>Disabling Elements</h3>
<p>In the above demos you will see that the button greys out during
the request, which is due to Bootstrap's handling of this CSS class.</p>

<h3>The <code>ic-global-indicator</code> Attribute</h3>

<p>
The <code><a href="/attributes/ic-global-indicator.html">ic-global-indicator</a></code> attribute is similar to the <code>ic-indicator</code> attribute, but
will be shown in addition to any local indicators. This can be used to implement a site-wide progress indicator.
</p>


</section>

<section>
Expand Down Expand Up @@ -855,6 +866,64 @@ <h3>Client-Side Actions</h3>
</blockquote>
</div>

<h3>Switch Class</h3>

<p>Sometimes you may want to switch a class between siblings in a DOM without replacing the HTML. A
common situation where this comes up is in tabbed UIs, where the target is within the tabbed UI, but the
tabs themselves are not replaced.</p>

<p>Intercooler has an attribute, <code><a href="/attributes/ic-switch-class.html">ic-switch-class</a></code> that
enabled this pattern. It is placed on the parent element and the value is the name of the class that will be
switched to the element causing the intercooler request.</p>

<p>Below is an example of a tabbed UI using this technique. Note that the tabs are not replaced, but the
<code>active</code> class is switched between them as they are clicked.</p>

<pre>
&lt;ul class="nav nav-tabs" ic-target="#content" ic-switch-class="active">
&lt;li class="active">&lt;a ic-get-from="/tab1">Tab1&lt;/a>&lt;/li>
&lt;li>&lt;a ic-get-from="/tab2">Tab2&lt;/a>&lt;/li>
&lt;li>&lt;a ic-get-from="/tab3">Tab3&lt;/a>&lt;/li>
&lt;/ul>
&lt;div id="content">
Pick a tab
&lt;/div>
</pre>

<div class="live-demo">

<script>
(function () {
var i = 0;
$.mockjax({'url': '/tab1', 'responseText': "Tab 1"});
$.mockjax({'url': '/tab2', 'responseText': "Tab 2"});
$.mockjax({'url': '/tab3', 'responseText': "Tab 3"});
})();
</script>

<ul class="nav nav-tabs" ic-target="#content" ic-switch-class="active">
<li class="active"><a ic-get-from="/tab1">Tab1</a></li>
<li><a ic-get-from="/tab2">Tab2</a></li>
<li><a ic-get-from="/tab3">Tab3</a></li>
</ul>
<div id="content">
Tab 1
</div>

</div>

<pre>
&lt;a ic-action="slideToggle" ic-target="#chesterton-quote">Toggle Chesterton!&lt;/a>
</pre>

<div class="live-demo">
<a class="btn btn-primary" ic-action="slideToggle" ic-target="#chesterton-quote">Toggle Chesterton!</a>
<blockquote id="chesterton-quote" style="display: none">
“Without education, we are in a horrible and deadly danger of taking educated people seriously.”<br/>
--GK Chesterton
</blockquote>
</div>

</section>

<section>
Expand Down Expand Up @@ -926,6 +995,47 @@ <h3>Conditionally Updating The Location/History</h3>
use
the history support.</p>

</section>

<section>
<a class="anchor" id="progressive_enhancement"></a>

<h2>Progressive Enhancement<i class=""></i></h2>

<p>Intercooler provides a mechanism for
<a href="https://en.wikipedia.org/wiki/Progressive_enhancement">progressive enhancement</a>. The
<code><a href="/attributes/ic-enhance.html">ic-enhance</a></code> attribute can be set to <code>true</code>
on an element, all child anchor tags and form tags will be converted to their equivalent intercooler
implementations.</p>

<p>Anchor tags (links) will be converted to <code>ic-get-from</code> with <code>ic-push-url</code> set to true.</p>

<p>Forms will be converted to the intercooler equivalent action (e.g. a POST form will convert to <code>ic-post-to</code>)</p>

<p>Commonly you will have an <code>ic-target</code> set up at the top level of your DOM, paired with a
<code>ic-enhance</code> attribute. You can differentiate on the server side between normal and AJAX requests
by looking for the intercooler headers.</p>

<p>Here is an example:</p>

<pre>
&lt;div ic-enhance="true">
&lt;a href="/enhancement_example">Click Me!&lt;/a>
&lt;/div>
</pre>

<div class="live-demo">
<script>
$.mockjax({
url: '/enhancement_example',
responseText: "Link was clicked!"
});
</script>
<div ic-enhance="true">
<a href="/enhancement_example">Click Me!</a>
</div>
</div>

</section>

<section>
Expand Down