├── .gitignore ├── LICENSE.md ├── README.md ├── example ├── about.html ├── detail.html ├── index.html └── server.js ├── index.js ├── package.json ├── scroll-frame-head.js ├── scroll-frame.js └── test ├── mocha.opts └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | pids 10 | logs 11 | results 12 | /node_modules 13 | npm-debug.log 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Craig Spaeth , Art.sy, 2014 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scrollFrame 2 | 3 | Retain your scroll position between pages using an iframe. Especially helpful for infinite scrolling views. 4 | 5 | ![](http://www.explainxkcd.com/wiki/images/5/56/infinite_scrolling.png) 6 | 7 | ## Example 8 | 9 | Insert scroll-frame-head.js into your `` tag across all views. 10 | 11 | ````html 12 | 13 | 14 | 15 | 16 | 17 |

My detail page

18 | 19 | 20 | ```` 21 | 22 | Then insert scroll-frame.js into the pages where you have an infinite scrolling list that needs to retain scroll position. 23 | 24 | ````html 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ```` 36 | 37 | Finally use the `scrollFrame` function to indicate what links should retain their scroll position when clicked. 38 | 39 | ````javascript 40 | scrollFrame('#my-infinite-scrolling-list a'); 41 | ```` 42 | 43 | For a working example check out the [example folder](https://github.com/artsy/scroll-frame/tree/master/example) which you can [visit a live demo of here](http://artsy.github.io/scroll-frame/demo.html) or run yourself by cloning the project and running `npm run example`. 44 | 45 | ## How it Works 46 | 47 | scrollFrame will hijack the user's click for elements that match the query selector you pass in and instead of reloading the page it will append a modal-like iframe that sits on top of your viewport and points to the element's href. It then uses HTML5 history APIs to make the back-button function as expected. 48 | 49 | ## Caveats 50 | 51 | * scrollFrame will only open the next immediate page in an iframe (solving the simple use case of opening a detail page from an infinite scrolling list and then clicking back without losing your position). After clicking on a link inside the iframe the page refreshes to avoid going down a rabbit hole of stacked iframe modals and messy state. 52 | 53 | * Because scrollFrame uses HTML5 history APIs it does not work with older browsers and will simply not do anything when included. This should gracefully degrade as it'll just mean older browsers won't retain their scroll position. 54 | 55 | ## Additionally 56 | 57 | Scroll frame will add the `scroll-frame-loading` class to the `` so you can set a loading state while the iframe is loading the page. As an example you may want to do something like `body.scroll-frame-loading #scroll-frame-spinner { display: block }`. 58 | 59 | # License 60 | 61 | MIT 62 | -------------------------------------------------------------------------------- /example/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

About Kittens

8 |
More kittens!
9 |

Pour-over 8-bit yr, blog letterpress Vice vinyl. Photo booth 90's Schlitz, try-hard PBR&B McSweeney's gentrify. Twee semiotics literally 8-bit umami. Food truck deep v irony master cleanse Vice chambray. Squid crucifix Cosby sweater lomo mlkshk flannel chia Truffaut. +1 salvia food truck vinyl four loko bicycle rights. Gluten-free deep v iPhone Neutra.

10 |

Portland salvia Thundercats, sriracha fap pug leggings. Kogi sriracha put a bird on it ethnic. Williamsburg deep v before they sold out, pour-over tote bag Neutra Pinterest wayfarers typewriter paleo McSweeney's umami. Brooklyn sriracha narwhal VHS mlkshk, Williamsburg Pinterest. Drinking vinegar typewriter quinoa, bitters biodiesel Echo Park viral Helvetica banjo literally shabby chic. Artisan stumptown VHS cred, asymmetrical slow-carb fingerstache. Ugh wolf brunch fashion axe, try-hard squid Bushwick.

11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /example/detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Detail Page!

8 |
9 |
About Kittens
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 | 33 | 34 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 38 | 39 | 40 | 41 | 42 |
Loading...
43 |
44 | 45 | 46 | 47 | 48 | 71 |
72 | 73 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var fs = require('fs'); 3 | var app = module.exports = express(); 4 | app.use(express.static(__dirname)); 5 | app.use(express.static(process.cwd())); 6 | if (require.main != module) return; 7 | app.listen(4000, function() { 8 | console.log('Listening on 4000'); 9 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./scroll-frame'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scroll-frame", 3 | "version": "1.0.0", 4 | "description": "Retain your scroll position between pages using an iframe. Especially helpful for infinite scrolling views.", 5 | "keywords": [ 6 | "scroll", 7 | "infinite", 8 | "infinite scroll", 9 | "iframe", 10 | "scroll position", 11 | "retain scroll" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "http://github.com/artsy/scroll-frame.git" 16 | }, 17 | "author": { 18 | "name": "Craig Spaeth", 19 | "email": "craigspaeth@gmail.com", 20 | "url": "http://craigspaeth.com" 21 | }, 22 | "engines": { 23 | "node": ">= 0.10.x" 24 | }, 25 | "scripts": { 26 | "test": "mocha", 27 | "example": "node example/server.js" 28 | }, 29 | "dependencies": { 30 | }, 31 | "devDependencies": { 32 | "express": "*", 33 | "mocha": "*", 34 | "should": "*", 35 | "zombie": "*", 36 | "jquery-on-infinite-scroll": "*", 37 | "sinon": "*" 38 | } 39 | } -------------------------------------------------------------------------------- /scroll-frame-head.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | // Ignore for unsupported browsers 4 | if (!(window.history && window.history.pushState)) return; 5 | 6 | // If we're inside an iframe modal then send a message to the parent 7 | // indicating what the iframe's location is so that the parent can decide 8 | // not to go down a rabbit hole inside the iframe. 9 | if (parent && parent.postMessage) { 10 | // postMessage support for IE & other browsers 11 | if (window.MessageChannel && navigator.userAgent.indexOf('MSIE') > -1) { 12 | var m = new MessageChannel(); 13 | parent.postMessage({ 14 | href: location.href, 15 | scrollFrame: true 16 | }, "*", [m.port2]); 17 | } else { 18 | parent.postMessage({ 19 | href: location.href, 20 | scrollFrame: true 21 | }, location.origin); 22 | } 23 | } 24 | 25 | // When navigating another level deep scrollFrame will refresh the page. 26 | // Hitting the back button will halt when it gets to the popstate point 27 | // at which scrollFrame added the iframe modal. This will notice that and 28 | // make the full refresh instead. 29 | var firstPopStateTriggered; 30 | addEventListener('popstate', function(e) { 31 | if (firstPopStateTriggered && e.state && e.state.scrollFrame && 32 | !document.querySelector('.scroll-frame-iframe')) { 33 | location = e.state.href; 34 | } 35 | firstPopStateTriggered = true; 36 | }); 37 | })(); -------------------------------------------------------------------------------- /scroll-frame.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | // Ignore for unsupported browsers 4 | if (!(window.history && window.history.pushState)) return; 5 | 6 | // Main function that listens for clicks to the selector and opens the 7 | // href of the element in the iframe modal. 8 | // 9 | // @param {String} selector DOM query selector e.g. 'ul.list-items a' 10 | 11 | var scrollFrame = function(selector) { 12 | refreshOnNewIframePage(); 13 | document.addEventListener('click', function(e) { 14 | // Ignore ctrl/cmd/shift clicks, as well as middle clicks 15 | if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return; 16 | 17 | // Ignore if the element doesnt match our selector 18 | var els = document.querySelectorAll(selector); 19 | var elMatchesSelector = (window.Array || Array) // Hack for Zombie testing 20 | .prototype.filter.call(els, function(el) { 21 | return el == e.target || el.contains(e.target); 22 | }).length > 0 23 | if (!elMatchesSelector) return; 24 | 25 | // Get the href & open the iframe on that url 26 | var href = e.target.href || e.target.parentNode.href; 27 | if (href) { 28 | e.preventDefault(); 29 | openIframe(href); 30 | } 31 | }); 32 | } 33 | 34 | // Change pushState and open the iframe modal pointing to this url. 35 | // 36 | // @param {String} url 37 | 38 | var openIframe = function(url) { 39 | var prevHref = location.href; 40 | var prevTitle = document.title; 41 | 42 | // Change the history 43 | history.pushState({ scrollFrame: true, href: location.href }, '', url); 44 | 45 | // Create the wrapper & iframe modal 46 | var body = document.getElementsByTagName('body')[0]; 47 | var iOS = navigator.userAgent.match(/(iPad|iPhone|iPod)/g) ? true : false; 48 | var attributes = [ 49 | 'position: fixed', 'top: 0', 'left: 0','width: 100%', 'height: 100%', 50 | 'z-index: 10000000', 'background-color: white', 'border: 0' 51 | ]; 52 | 53 | //only add scrolling fix for ios devices 54 | if (iOS){ 55 | attributes.push('overflow-y: scroll'); 56 | attributes.push('-webkit-overflow-scrolling: touch'); 57 | } 58 | //create wrapper for iOS scroll fix 59 | var wrapper = document.createElement("div"); 60 | wrapper.setAttribute('style',attributes.join(';')); 61 | var iframe = document.createElement("iframe"); 62 | iframe.className = 'scroll-frame-iframe' 63 | iframe.setAttribute('style', [ 64 | 'width: 100%', 'height: 100%', 'position:absolute', 65 | 'border: 0' 66 | ].join(';')); 67 | 68 | // Lock the body from scrolling & hide the body's scroll bars. 69 | body.setAttribute('style', 'overflow: hidden;' + 70 | (body.getAttribute('style') || '')); 71 | 72 | // Add a class to the body while the iframe loads then append it 73 | body.className += ' scroll-frame-loading'; 74 | iframe.onload = function() { 75 | body.className = body.className.replace(' scroll-frame-loading', ''); 76 | document.title = iframe.contentDocument.title; 77 | } 78 | wrapper.appendChild(iframe); 79 | body.appendChild(wrapper); 80 | iframe.contentWindow.location.replace(url); 81 | 82 | // On back-button remove the wrapper 83 | var onPopState = function(e) { 84 | if (location.href != prevHref) return; 85 | wrapper.removeChild(iframe); 86 | document.title = prevTitle; 87 | body.removeChild(wrapper); 88 | body.setAttribute('style', 89 | body.getAttribute('style').replace('overflow: hidden;', '')); 90 | removeEventListener('popstate', onPopState); 91 | } 92 | addEventListener('popstate', onPopState); 93 | } 94 | 95 | // To keep iframes from stacking up inside of each other and potentially 96 | // getting into a very messy state we'll use messaging b/t iframes to 97 | // signal when we've dived more than a page deep inside of our iframe modal 98 | // and cause the page to do a full refresh instead. 99 | 100 | var refreshOnNewIframePage = function() { 101 | addEventListener('message', function(e) { 102 | if (!e.data.href) return; 103 | if (!e.data.scrollFrame == true) return; 104 | if (e.data.href == this.location.href) return; 105 | var body = document.getElementsByTagName('body')[0]; 106 | var html = document.getElementsByTagName('html')[0]; 107 | this.location.assign(e.data.href); 108 | }); 109 | } 110 | 111 | // Export for CommonJS & window global 112 | if (typeof module != 'undefined') { 113 | module.exports = scrollFrame; 114 | } else { 115 | window.scrollFrame = scrollFrame; 116 | } 117 | })(); 118 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --ui bdd 3 | --timeout 10000 -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var app = require('../example/server'); 2 | var Browser = require('zombie'); 3 | var sinon = require('sinon'); 4 | var server, browser; 5 | 6 | beforeEach(function(done) { 7 | server = app.listen(5000, function() { 8 | browser = new Browser(); 9 | browser.visit('http://localhost:5000', function() { 10 | done(); 11 | }); 12 | }); 13 | }); 14 | 15 | afterEach(function() { 16 | server.close(); 17 | }); 18 | 19 | describe('scrollFrame', function() { 20 | 21 | it('adds an iframe to the body when clicking a scoped link', function(done) { 22 | browser.wait(function() { 23 | browser.window.Array = Array; 24 | browser.window.Array.prototype.filter = function(cb) { 25 | return [function(){}] 26 | } 27 | browser.clickLink('li:nth-child(6) a', function() { 28 | browser.html().should 29 | .containEql('