Creating an Infinite Scroll Masonry Block Without Views
The Views Infinite Scroll module provides a way to apply infinite scroll to the output of a view, but if you want to apply infinite scroll to custom block content, you're out of luck. I found myself in this position while developing a recently-launched site for The Salmon Project, which I'll use as a loose reference point as I walk you through my solution to applying both Masonry and Infinite Scroll to custom block content.
Overview
- Creating a block to hold our paged content that can be placed on a page
- Generating a paged content array to return as block content
- Applying Masonry to the paged content block
- Applying Infinite Scroll to the paged content block
- Creating an Infinite Scroll trigger
Creating a block to hold paged content
The first step is creating a block to hold our custom paged view that can be placed on a page. To create the block, we'll use a hook_block_info()
followed by a hook_block_view()
.
<span class="cp"><?php</span><span class="k">function</span> <span class="nf">my_module_block_info</span><span class="p">()</span> <span class="p">{</span> <span class="nv">$block</span><span class="p">[</span><span class="s1">'masonry_content'</span><span class="p">]</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span> <span class="s1">'info'</span> <span class="o">=></span> <span class="s1">'Masonry content'</span><span class="p">,</span> <span class="p">);</span> <span class="k">return</span> <span class="nv">$block</span><span class="p">}</span><span class="k">function</span> <span class="nf">my_module_block_view</span><span class="p">(</span><span class="nv">$delta</span> <span class="o">=</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$block</span> <span class="o">=</span> <span class="k">array</span><span class="p">();</span> <span class="nv">$block</span><span class="p">[</span><span class="s1">'subject'</span><span class="p">]</span> <span class="o">=</span> <span class="s1">''</span><span class="p">;</span> <span class="nv">$block</span><span class="p">[</span><span class="s1">'content'</span><span class="p">]</span> <span class="o">=</span> <span class="nx">my_module_masonry_content</span><span class="p">();</span><span class="p">}</span>
Here, we've defined which function we'll be generating our paged view from; namely my_module_masonry_content
.
Generating a paged content array
Within our my_module_masonry_content
function, we'll create a paged view of nodes. To do so, we'll use an EntityFieldQuery with the "pager" property, which causes the results of the query to be returned as a pager.
<span class="cp"><?php</span><span class="nv">$query</span><span class="o">-></span><span class="na">pager</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
The argument passed to the pager function determines how many results at a time will be returned; this is analogous to setting the number of items to be shown per page in a view – keep this bit in mind, as we'll return to it later when implementing the infinite scroll.
Now we'll add some query conditions and, finally, execute the query.
These conditions are, of course, site specific, but I'm including them as an example for thoroughness. For help constructing your EntityFieldQuery query, see the EntityFieldQuery api documentation page.
<span class="cp"><?php</span><span class="nv">$query</span><span class="o">-></span><span class="na">fieldCondition</span><span class="p">(</span><span class="s1">'field_example'</span><span class="p">,</span> <span class="s1">'value'</span><span class="p">,</span> <span class="k">array</span><span class="p">(</span><span class="s1">'value1'</span><span class="p">,</span> <span class="s1">'value2'</span><span class="p">),</span> <span class="s1">'IN'</span><span class="p">);</span><span class="nv">$query</span><span class="o">-></span><span class="na">propertyCondition</span><span class="p">(</span><span class="s1">'status'</span><span class="p">,</span> <span class="s1">'1'</span><span class="p">);</span><span class="nv">$results</span> <span class="o">=</span> <span class="nv">$query</span><span class="o">.</span><span class="nx">execute</span><span class="p">();</span>
In this example, I've requested all nodes that have a field_example
value of value1
or value2
and are published (i.e. node status property is equal to 1).
Once we have our results set, we will need to create a renderable array to return as the block content.
<span class="cp"><?php</span><span class="k">foreach</span> <span class="p">(</span><span class="nv">$node_result</span><span class="p">[</span><span class="s1">'node'</span><span class="p">]</span> <span class="k">as</span> <span class="nv">$row</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$node</span> <span class="o">=</span> <span class="nx">entity_load_single</span><span class="p">(</span><span class="s1">'node'</span><span class="p">,</span> <span class="nv">$row</span><span class="o">-></span><span class="na">nid</span><span class="p">);</span> <span class="nv">$output</span><span class="p">[]</span> <span class="o">=</span> <span class="nx">node_view</span><span class="p">(</span><span class="nv">$node</span><span class="p">,</span> <span class="s1">'category_term_page'</span><span class="p">);</span><span class="p">}</span>
Then we'll apply pager theming to the output by explicitly adding it to the returned renderable array.
<span class="cp"><?php</span><span class="nv">$output</span><span class="p">[</span><span class="s1">'pager'</span><span class="p">]</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span><span class="s1">'#theme'</span> <span class="o">=></span> <span class="s1">'pager'</span><span class="p">);</span>
We then return our output array and get to the JavaScript…
Applying Masonry to the paged content block
To apply Masonry to the paged block content, we'll need some JavaScript so let's create a new JavaScript file within our module's js directory (js/my_module.js
). This file will depend on the Masonry JavaScript library so we'll need to load it in addition to our new, custom JavaScript file.
Loading the required libraries
For optimal performance, we only want to load our JavaScript when the masonry_content
block is present. To conditionally load the JavaScript we'll use a hook_block_view_alter()
.
<span class="cp"><?php</span><span class="k">function</span> <span class="nf">my_module_block_view_alter</span><span class="p">(</span><span class="o">&</span><span class="nv">$data</span><span class="p">,</span> <span class="nv">$block</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// only load libraries if masonry_content block is present</span> <span class="k">if</span> <span class="p">(</span><span class="nv">$block</span><span class="o">-></span><span class="na">module</span> <span class="o">==</span> <span class="s1">'my_module'</span> <span class="o">&&</span> <span class="nv">$block</span><span class="o">-></span><span class="na">delta</span> <span class="o">==</span> <span class="s1">'masonry_content'</span><span class="p">)</span> <span class="nv">$module_path</span> <span class="o">=</span> <span class="nx">drupal_get_path</span><span class="p">(</span><span class="s1">'moduel'</span><span class="p">,</span> <span class="s1">'my_module'</span><span class="p">);</span> <span class="nv">$data</span><span class="p">[</span><span class="s1">'content'</span><span class="p">][</span><span class="s1">'#attached'</span><span class="p">][</span><span class="s1">'js'</span><span class="p">][]</span> <span class="o">=</span> <span class="nv">$module_path</span> <span class="o">.</span> <span class="s1">'/js/my_module.js'</span><span class="p">;</span> <span class="nv">$masonry_path</span> <span class="o">=</span> <span class="nx">libraries_get_path</span><span class="p">(</span><span class="s1">'masonry'</span><span class="p">);</span> <span class="nv">$data</span><span class="p">[</span><span class="s1">'content'</span><span class="p">][</span><span class="s1">'#attached'</span><span class="p">][</span><span class="s1">'js'</span><span class="p">][]</span> <span class="o">=</span> <span class="nv">$masonry_path</span> <span class="o">.</span> <span class="s1">'/dist/masonry.pkgd.min.js'</span><span class="p">;</span> <span class="p">}</span><span class="p">}</span>
Now that we've got our required JavaScripts, let's take a look at how to apply Masonry to our paged content block.
<span class="c1">// within my_module.js</span><span class="kd">var</span> <span class="nx">container</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">'#my-module-masonry-content);</span><span class="s1">container.masonry({</span><span class="s1"> // Masonry options</span><span class="s1"> itemSelector: '</span><span class="err">#</span><span class="nx">my</span><span class="o">-</span><span class="nx">module</span><span class="o">-</span><span class="nx">masonry</span><span class="o">-</span><span class="nx">content</span> <span class="nx">article</span><span class="p">.</span><span class="nx">node</span><span class="err">'</span><span class="p">});</span>
In this case, we're selecting the <div> that has our block id and the node items within that <div>.
Doing this will apply Masonry once to the items present after the initial page load, but since we are going to be loading more items via the pager, we'll need to re-apply Masonry after those new items are loaded. We haven't defined the "change" action yet, but will later when implementing the infinite scroll JavaScript.
<span class="c1">// necessary to apply masonry to new items pulled in from infinite_scroll.js</span><span class="nx">container</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="s1">'change'</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="nx">container</span><span class="p">.</span><span class="nx">masonry</span><span class="p">(</span><span class="s1">'reloadItems'</span><span class="p">);</span> <span class="nx">container</span><span class="p">.</span><span class="nx">masonry</span><span class="p">()</span> <span class="p">});</span><span class="p">});</span>
Now that we've got Masonry applied to our block content, let's move on to getting the infinite scroll behavior in place.
Applying Infinite Scroll to the paged content block
To pull new items into our content block (to which Masonry is being applied) we'll leverage the Autopager library. This means we'll need to add Autopager to the list of JavaScript to be loaded conditionally when our block is present.
Loading more required libraries
Again, we'll use drupal_get_path()
and libraries_get_path()
to retrieve more required JavaScript from within our hook_block_view_alter()
.
<span class="cp"><?php</span><span class="nv">$autopager_path</span> <span class="o">=</span> <span class="nx">libraries_get_path</span><span class="p">(</span><span class="s1">'autopager'</span><span class="p">);</span><span class="nv">$data</span><span class="p">[</span><span class="s1">'content'</span><span class="p">][</span><span class="s1">'#attached'</span><span class="p">][</span><span class="s1">'js'</span><span class="p">][]</span> <span class="o">=</span> <span class="nv">$autopager_path</span> <span class="o">.</span> <span class="s1">'/jquery.autopager-1.0.0.js'</span><span class="p">;</span>
Now that we've got Autopager and my_module_infinite_scroll.js
loaded, let's apply the infinite scroll…
The first thing we need to to is define the parameters that will be passed to Autopager.
<span class="cp"><?php</span><span class="c1">// Make sure that autopager plugin is loaded</span><span class="k">if</span><span class="p">(</span><span class="err">$</span><span class="o">.</span><span class="nx">autopager</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// define autopager parameters</span> <span class="k">var</span> <span class="nx">content_selector</span> <span class="o">=</span> <span class="s1">'#my-module-masonry-content'</span><span class="p">;</span> <span class="k">var</span> <span class="nx">items_selector</span> <span class="o">=</span> <span class="nx">content_selector</span> <span class="o">+</span> <span class="s1">'article.node'</span><span class="p">;</span> <span class="k">var</span> <span class="nx">next_selector</span> <span class="o">=</span> <span class="s1">'.pager-next a'</span><span class="p">;</span> <span class="k">var</span> <span class="nx">pager_selector</span> <span class="o">=</span> <span class="s1">'.pager'</span>
Notice that $content_selector
matches the selector we used to apply Masonry to each piece of block content. This is because Autopager will, behind the scenes, retrieve more content similar to what's already on the page when we click the "next" link.
The $next_selector
and $pager_selector
selectors are what target the "next" and "1, 2, 3…" links our pager exposes and, not incidentally, what Autopager uses to retrieve the next set of content. Recall from above our query returns 3 nodes at a time so the "next" link will cause 3 more nodes to be shown.
Though necessary to retrieve more content, we don't want to see these pager links so let's hide them.
<span class="nx">$</span><span class="p">(</span><span class="nx">pager_selector</span><span class="p">).</span><span class="nx">hide</span><span class="p">();</span>
…and now create our Autopager handler.
<span class="kd">var</span> <span class="nx">handle</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">autopager</span><span class="p">({</span> <span class="nx">autoLoad</span><span class="o">:</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">appendTo</span><span class="o">:</span> <span class="nx">content_selector</span><span class="p">,</span> <span class="nx">content</span><span class="o">:</span> <span class="nx">items_selector</span><span class="p">,</span> <span class="nx">link</span><span class="o">:</span> <span class="nx">next_selector</span><span class="p">,</span> <span class="nx">load</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="nx">$</span><span class="p">(</span><span class="nx">content_selector</span><span class="p">).</span><span class="nx">trigger</span><span class="p">(</span><span class="s1">'change'</span><span class="p">);</span> <span class="p">}</span><span class="p">});</span>
The $(content_selector).trigger('change')
bit is a key component of this snippet because the "change" action is what we are using to apply Masonry to new items added to our block (see the container.bind('change'…
bit above).
Triggering the infinite scroll function
The Autopager handler we just defined acts as our gas with respect to the infinite scroll action, but we also need a brake. The following snippet, taken from views_infinite_scroll.js
uses some fancy math to determine when the user has hit page bottom and only calls handle.autopager('load')
when this is the case, effectively acting as the brake.
<span class="c1">// Trigger autoload if content height is less than doc height already</span><span class="kd">var</span> <span class="nx">prev_content_height</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="nx">content_selector</span><span class="p">).</span><span class="nx">height</span><span class="p">();</span><span class="k">do</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">last</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="nx">items_selector</span><span class="p">).</span><span class="nx">filter</span><span class="p">(</span><span class="s1">':last'</span><span class="p">);</span> <span class="k">if</span><span class="p">(</span><span class="nx">last</span><span class="p">.</span><span class="nx">offset</span><span class="p">().</span><span class="nx">top</span> <span class="o">+</span> <span class="nx">last</span><span class="p">.</span><span class="nx">height</span><span class="p">()</span> <span class="o"><</span> <span class="nx">$</span><span class="p">(</span><span class="nb">document</span><span class="p">).</span><span class="nx">scrollTop</span><span class="p">()</span> <span class="o">+</span> <span class="nx">$</span><span class="p">(</span><span class="nb">window</span><span class="p">).</span><span class="nx">height</span><span class="p">())</span> <span class="p">{</span> <span class="nx">last</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="nx">items_selector</span><span class="p">).</span><span class="nx">filter</span><span class="p">(</span><span class="s1">':last'</span><span class="p">);</span> <span class="nx">handle</span><span class="p">.</span><span class="nx">autopager</span><span class="p">(</span><span class="s1">'load'</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">break</span><span class="p">;</span> <span class="p">}</span><span class="p">}</span><span class="k">while</span> <span class="p">(</span><span class="nx">$</span><span class="p">(</span><span class="nx">content_selector</span><span class="p">).</span><span class="nx">height</span><span class="p">()</span> <span class="o">></span> <span class="nx">prev_content_height</span><span class="p">);</span>
You'll notice on The Salmon Project site I am not using infinite scroll; instead I opted for a "View more" button to trigger the autopager('load')
action and some logic in the function bound to the "change" action to hide said button.
Regardless of the method you choose as a trigger, all the method needs to do is call Autopager's load function (analogous to hitting the hidden "next" pager link) to load more content.
And there you have it, an infinite scroll masonry block that loads 3 more nodes each time the user hits page bottom without the use of the Views module.