Repository: metafizzy/infinite-scroll Branch: master Commit: 38a464fbbbe4 Files: 85 Total size: 228.8 KB Directory structure: gitextract_pwag9y0f/ ├── .eslintrc.js ├── .github/ │ ├── contributing.md │ ├── issue_template.md │ └── workflows/ │ └── nodejs.yml ├── .gitignore ├── .nvmrc ├── LICENSE.txt ├── README.md ├── bin/ │ ├── build-dist.js │ └── version.js ├── bower.json ├── dist/ │ └── infinite-scroll.pkgd.js ├── js/ │ ├── button.js │ ├── core.js │ ├── history.js │ ├── index.js │ ├── page-load.js │ ├── scroll-watch.js │ └── status.js ├── package.json ├── sandbox/ │ ├── button-class.html │ ├── button-first.html │ ├── button-load.html │ ├── container-scroll.html │ ├── css/ │ │ ├── blog.css │ │ ├── loader-ellips.css │ │ └── masonry-images.css │ ├── element-scroll.html │ ├── html-init.html │ ├── jquery-plugin.html │ ├── js/ │ │ ├── masonry-images.js │ │ ├── scroll-loader.js │ │ ├── unsplash-masonry.js │ │ └── unsplash.js │ ├── masonry-images/ │ │ ├── index.html │ │ ├── page2.html │ │ ├── page3.html │ │ ├── page4.html │ │ └── page5.html │ ├── page/ │ │ ├── 2.html │ │ ├── 3.html │ │ ├── 4.html │ │ ├── 5.html │ │ └── 6.html │ ├── prefill.html │ ├── scroll-3.html │ ├── scroll-loader.html │ ├── unsplash-masonry.html │ └── unsplash.html └── test/ ├── _get-server.js ├── _with-page.js ├── check-last-page.js ├── dist-jquery.js ├── dist.js ├── history.js ├── html/ │ ├── _serial-t.js │ ├── dist-jquery.html │ ├── dist.html │ ├── history.html │ ├── outlayer.html │ ├── page/ │ │ ├── 2.html │ │ ├── 2.json │ │ ├── 3.html │ │ ├── 3.json │ │ ├── empty.html │ │ ├── fill.html │ │ ├── no-access.html │ │ ├── outlayer2.html │ │ └── outlayer3.html │ ├── page-index.html │ ├── page-load.html │ ├── path.html │ ├── prefill.html │ ├── scroll-watch-element.html │ ├── scroll-watch-window.html │ └── test.css ├── load-next-page-promise.js ├── outlayer.js ├── page-index.js ├── page-load-error.js ├── page-load-json.js ├── page-load.js ├── path.js ├── prefill.js └── scroll-watch.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ /* eslint-env node */ module.exports = { plugins: [ 'metafizzy' ], extends: 'plugin:metafizzy/browser', env: { browser: true, commonjs: true, }, parserOptions: { ecmaVersion: 2018, }, globals: { InfiniteScroll: 'readonly', Promise: 'readonly', QUnit: 'readonly', serialT: 'readonly', }, rules: { }, }; ================================================ FILE: .github/contributing.md ================================================ ## Submitting issues ### Reduced test case required All bug reports and problem issues require a [**reduced test case**](https://css-tricks.com/reduced-test-cases/) or **live URL**. + A reduced test case clearly demonstrates the bug or issue. + It contains the bare minimum HTML, CSS, and JavaScript required to demonstrate the bug. + A link to your production site is **not** a reduced test case. Create one by forking any one of the [CodePen demos](https://codepen.io/collection/DZejqa?grid_type=list&sort_by=item_created_at) from [the docs](https://infinite-scroll.com). + [Scroll & append](https://codepen.io/desandro/pen/yLaKLop) + [Scroll & append, vanilla JS](https://codepen.io/desandro/pen/vYXRYeY) Providing a reduced test case is the best way to get your issue addressed. They help you point out the problem. They help me verify and debug the problem. They help others understand the problem. Without a reduced test case or live URL, your issue may be closed. ## Pull requests Contributions are welcome! + **For typos and one-line fixes,** send those right in. + **For larger features,** open an issue before starting any significant work. Let's discuss to see how your feature fits within Infinite Scroll's vision. + **Check code style.** Run `npm run lint`. Spaces in brackets, semicolons, trailing commas. + **Do not edit `infinite-scroll.pkgd.js`.** Make your edits to source files in `js/`. + **Do not run the `dist` task to update `dist/` files.** I'll take care of this when I create a new release. Your code will be used as part of a commercial product if merged. By submitting a Pull Request, you are giving your consent for your code to be integrated into Infinite Scroll as part of a commercial product. ================================================ FILE: .github/issue_template.md ================================================ **Test case:** https://codepen.io/desandro/pen/yLaKLop ================================================ FILE: .github/workflows/nodejs.yml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [14.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm ci # - run: npm run dist - run: npm test env: CI: true ================================================ FILE: .gitignore ================================================ bower_components/ node_modules/ ================================================ FILE: .nvmrc ================================================ 14 ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) Metafizzy 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ================================================ FILE: README.md ================================================ # Infinite Scroll _Automatically add next page_ See [infinite-scroll.com](https://infinite-scroll.com) for complete docs and demos. ## Install ### Download - [infinite-scroll.pkgd.min.js](https://unpkg.com/infinite-scroll@5/dist/infinite-scroll.pkgd.min.js) minified, or - [infinite-scroll.pkgd.js](https://unpkg.com/infinite-scroll@5/dist/infinite-scroll.pkgd.js) un-minified ### CDN Link directly to Infinite Scroll files on [unpkg](https://unpkg.com). ``` html ``` ### Package managers npm: `npm install infinite-scroll` Yarn: `yarn add infinite-scroll` ## License Infinite Scroll v5 is licensed under the MIT License. Whereas earlier versions of Infinite Scroll were previously dual licensed for commercial and closed-source usage, Infinite Scroll v5 licensing no longer has this distinction. You are free to use Infinite Scroll v5 in commercial and closed-source applications. ## Usage Infinite Scroll works on a container element with its child item elements ``` html
...
...
...
...
``` ### Options ``` js let infScroll = new InfiniteScroll( '.container', { // defaults listed path: undefined, // REQUIRED. Determines the URL for the next page // Set to selector string to use the href of the next page's link // path: '.pagination__next' // Or set with {{#}} in place of the page number in the url // path: '/blog/page/{{#}}' // or set with function // path: function() { // return return '/articles/P' + ( ( this.loadCount + 1 ) * 10 ); // } append: undefined, // REQUIRED for appending content // Appends selected elements from loaded page to the container checkLastPage: true, // Checks if page has path selector element // Set to string if path is not set as selector string: // checkLastPage: '.pagination__next' prefill: false, // Loads and appends pages on intialization until scroll requirement is met. responseBody: 'text', // Sets the method used on the response. // Set to 'json' to load JSON. domParseResponse: true, // enables parsing response body into a DOM // disable to load flat text fetchOptions: undefined, // sets custom settings for the fetch() request // for setting headers, cors, or POST method // can be set to an object, or a function that returns an object outlayer: false, // Integrates Masonry, Isotope or Packery // Appended items will be added to the layout scrollThreshold: 400, // Sets the distance between the viewport to scroll area // for scrollThreshold event to be triggered. elementScroll: false, // Sets scroller to an element for overflow element scrolling loadOnScroll: true, // Loads next page when scroll crosses over scrollThreshold history: 'replace', // Changes the browser history and URL. // Set to 'push' to use history.pushState() // to create new history entries for each page change. historyTitle: true, // Updates the window title. Requires history enabled. hideNav: undefined, // Hides navigation element status: undefined, // Displays status elements indicating state of page loading: // .infinite-scroll-request, .infinite-scroll-load, .infinite-scroll-error // status: '.page-load-status' button: undefined, // Enables a button to load pages on click // button: '.load-next-button' onInit: undefined, // called on initialization // useful for binding events on init // onInit: function() { // this.on( 'append', function() {...}) // } debug: false, // Logs events and state changes to the console. }) ``` ## Browser support Infinite Scroll v4 supports Chrome 60+, Edge 79+, Firefox 55+, Safari 11+. For IE10 and Android 4 support, try [Infinite Scroll v3](https://v3.infinite-scroll.com/). ## Development This package is developed with Node.js v14 and npm v6. Manage Node and npm version with [nvm](https://github.com/nvm-sh/nvm). ``` sh nvm use ``` Install dependencies ``` sh npm install ``` Lint ``` sh npm run lint ``` Run tests ``` sh npm test ``` --- By [Metafizzy 🌈🐻](https://metafizzy.co) ================================================ FILE: bin/build-dist.js ================================================ const fs = require('fs'); const { execSync } = require('child_process'); const { minify } = require('terser'); const indexPath = 'js/index.js'; const distPath = 'dist/infinite-scroll.pkgd.js'; const distMinPath = 'dist/infinite-scroll.pkgd.min.js'; let indexContent = fs.readFileSync( `./${indexPath}`, 'utf8' ); // get file paths from index.js let cjsBlockRegex = /module\.exports = factory\([\w ,'.\-()/\n]+;/i; let cjsBlockMatch = indexContent.match( cjsBlockRegex ); let jsPaths = cjsBlockMatch[0].match( /require\('([.\-/\w]+)'\)/gi ); jsPaths = jsPaths.map( function( path ) { return path.replace( "require('.", 'js' ).replace( "')", '.js' ); } ); let paths = [ 'node_modules/jquery-bridget/jquery-bridget.js', 'node_modules/ev-emitter/ev-emitter.js', 'node_modules/fizzy-ui-utils/utils.js', ...jsPaths, 'node_modules/imagesloaded/imagesloaded.js', ]; // concatenate files execSync(`cat ${paths.join(' ')} > ${distPath}`); // add banner let banner = indexContent.split(' */')[0] + ' */\n\n'; banner = banner.replace( 'Infinite Scroll', 'Infinite Scroll PACKAGED' ); let distJsContent = fs.readFileSync( distPath, 'utf8' ); distJsContent = banner + distJsContent; fs.writeFileSync( distPath, distJsContent ); // minify ( async function() { let { code } = await minify( distJsContent, { mangle: true } ); fs.writeFileSync( distMinPath, code ); } )(); ================================================ FILE: bin/version.js ================================================ /* eslint-env node */ const fs = require('fs'); const path = require('path'); const { version } = require('../package.json'); function dir( file ) { return path.resolve( __dirname, file ); } let content = fs.readFileSync( dir('../js/index.js'), 'utf8' ); content = content.replace( /Infinite Scroll v[\w.-]+/, `Infinite Scroll v${version}` ); fs.writeFileSync( dir('../js/index.js'), content, 'utf8' ); ================================================ FILE: bower.json ================================================ { "name": "infinite-scroll", "authors": [ "David DeSandro " ], "description": "infinite scroll", "main": "js/index.js", "dependencies": { "ev-emitter": "^2.1.0", "fizzy-ui-utils": "^3.0.0" }, "devDependencies": {}, "keywords": [ "infinite scroll", "infinite", "scroll", "plugin" ], "license": "MIT", "homepage": "https://infinite-scroll.com", "ignore": [ "**/.*", "node_modules", "bower_components", "test", "tests", "sandbox", "package.json", "gulpfile.js" ] } ================================================ FILE: dist/infinite-scroll.pkgd.js ================================================ /*! * Infinite Scroll PACKAGED v5.0.0 * Automatically add next page * MIT License * https://infinite-scroll.com * Copyright 2018-2025 Metafizzy */ /** * Bridget makes jQuery widgets * v3.0.0 * MIT license */ ( function( window, factory ) { // module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( window, require('jquery'), ); } else { // browser global window.jQueryBridget = factory( window, window.jQuery, ); } }( window, function factory( window, jQuery ) { // ----- utils ----- // // helper function for logging errors // $.error breaks jQuery chaining let console = window.console; let logError = typeof console == 'undefined' ? function() {} : function( message ) { console.error( message ); }; // ----- jQueryBridget ----- // function jQueryBridget( namespace, PluginClass, $ ) { $ = $ || jQuery || window.jQuery; if ( !$ ) { return; } // add option method -> $().plugin('option', {...}) if ( !PluginClass.prototype.option ) { // option setter PluginClass.prototype.option = function( opts ) { if ( !opts ) return; this.options = Object.assign( this.options || {}, opts ); }; } // make jQuery plugin $.fn[ namespace ] = function( arg0, ...args ) { if ( typeof arg0 == 'string' ) { // method call $().plugin( 'methodName', { options } ) return methodCall( this, arg0, args ); } // just $().plugin({ options }) plainCall( this, arg0 ); return this; }; // $().plugin('methodName') function methodCall( $elems, methodName, args ) { let returnValue; let pluginMethodStr = `$().${namespace}("${methodName}")`; $elems.each( function( i, elem ) { // get instance let instance = $.data( elem, namespace ); if ( !instance ) { logError( `${namespace} not initialized.` + ` Cannot call method ${pluginMethodStr}` ); return; } let method = instance[ methodName ]; if ( !method || methodName.charAt( 0 ) == '_' ) { logError(`${pluginMethodStr} is not a valid method`); return; } // apply method, get return value let value = method.apply( instance, args ); // set return value if value is returned, use only first value returnValue = returnValue === undefined ? value : returnValue; } ); return returnValue !== undefined ? returnValue : $elems; } function plainCall( $elems, options ) { $elems.each( function( i, elem ) { let instance = $.data( elem, namespace ); if ( instance ) { // set options & init instance.option( options ); instance._init(); } else { // initialize new instance instance = new PluginClass( elem, options ); $.data( elem, namespace, instance ); } } ); } } // ----- ----- // return jQueryBridget; } ) ); /** * EvEmitter v2.0.0 * Lil' event emitter * MIT License */ ( function( global, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS - Browserify, Webpack module.exports = factory(); } else { // Browser globals global.EvEmitter = factory(); } }( typeof window != 'undefined' ? window : this, function() { function EvEmitter() {} let proto = EvEmitter.prototype; proto.on = function( eventName, listener ) { if ( !eventName || !listener ) return this; // set events hash let events = this._events = this._events || {}; // set listeners array let listeners = events[ eventName ] = events[ eventName ] || []; // only add once if ( !listeners.includes( listener ) ) { listeners.push( listener ); } return this; }; proto.once = function( eventName, listener ) { if ( !eventName || !listener ) return this; // add event this.on( eventName, listener ); // set once flag // set onceEvents hash let onceEvents = this._onceEvents = this._onceEvents || {}; // set onceListeners object let onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {}; // set flag onceListeners[ listener ] = true; return this; }; proto.off = function( eventName, listener ) { let listeners = this._events && this._events[ eventName ]; if ( !listeners || !listeners.length ) return this; let index = listeners.indexOf( listener ); if ( index != -1 ) { listeners.splice( index, 1 ); } return this; }; proto.emitEvent = function( eventName, args ) { let listeners = this._events && this._events[ eventName ]; if ( !listeners || !listeners.length ) return this; // copy over to avoid interference if .off() in listener listeners = listeners.slice( 0 ); args = args || []; // once stuff let onceListeners = this._onceEvents && this._onceEvents[ eventName ]; for ( let listener of listeners ) { let isOnce = onceListeners && onceListeners[ listener ]; if ( isOnce ) { // remove listener // remove before trigger to prevent recursion this.off( eventName, listener ); // unset once flag delete onceListeners[ listener ]; } // trigger listener listener.apply( this, args ); } return this; }; proto.allOff = function() { delete this._events; delete this._onceEvents; return this; }; return EvEmitter; } ) ); /** * Fizzy UI utils v3.0.0 * MIT license */ ( function( global, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( global ); } else { // browser global global.fizzyUIUtils = factory( global ); } }( this, function factory( global ) { let utils = {}; // ----- extend ----- // // extends objects utils.extend = function( a, b ) { return Object.assign( a, b ); }; // ----- modulo ----- // utils.modulo = function( num, div ) { return ( ( num % div ) + div ) % div; }; // ----- makeArray ----- // // turn element or nodeList into an array utils.makeArray = function( obj ) { // use object if already an array if ( Array.isArray( obj ) ) return obj; // return empty array if undefined or null. #6 if ( obj === null || obj === undefined ) return []; let isArrayLike = typeof obj == 'object' && typeof obj.length == 'number'; // convert nodeList to array if ( isArrayLike ) return [ ...obj ]; // array of single index return [ obj ]; }; // ----- removeFrom ----- // utils.removeFrom = function( ary, obj ) { let index = ary.indexOf( obj ); if ( index != -1 ) { ary.splice( index, 1 ); } }; // ----- getParent ----- // utils.getParent = function( elem, selector ) { while ( elem.parentNode && elem != document.body ) { elem = elem.parentNode; if ( elem.matches( selector ) ) return elem; } }; // ----- getQueryElement ----- // // use element as selector string utils.getQueryElement = function( elem ) { if ( typeof elem == 'string' ) { return document.querySelector( elem ); } return elem; }; // ----- handleEvent ----- // // enable .ontype to trigger from .addEventListener( elem, 'type' ) utils.handleEvent = function( event ) { let method = 'on' + event.type; if ( this[ method ] ) { this[ method ]( event ); } }; // ----- filterFindElements ----- // utils.filterFindElements = function( elems, selector ) { // make array of elems elems = utils.makeArray( elems ); return elems // check that elem is an actual element .filter( ( elem ) => elem instanceof HTMLElement ) .reduce( ( ffElems, elem ) => { // add elem if no selector if ( !selector ) { ffElems.push( elem ); return ffElems; } // filter & find items if we have a selector // filter if ( elem.matches( selector ) ) { ffElems.push( elem ); } // find children let childElems = elem.querySelectorAll( selector ); // concat childElems to filterFound array ffElems = ffElems.concat( ...childElems ); return ffElems; }, [] ); }; // ----- debounceMethod ----- // utils.debounceMethod = function( _class, methodName, threshold ) { threshold = threshold || 100; // original method let method = _class.prototype[ methodName ]; let timeoutName = methodName + 'Timeout'; _class.prototype[ methodName ] = function() { clearTimeout( this[ timeoutName ] ); let args = arguments; this[ timeoutName ] = setTimeout( () => { method.apply( this, args ); delete this[ timeoutName ]; }, threshold ); }; }; // ----- docReady ----- // utils.docReady = function( onDocReady ) { let readyState = document.readyState; if ( readyState == 'complete' || readyState == 'interactive' ) { // do async to allow for other scripts to run. metafizzy/flickity#441 setTimeout( onDocReady ); } else { document.addEventListener( 'DOMContentLoaded', onDocReady ); } }; // ----- htmlInit ----- // // http://bit.ly/3oYLusc utils.toDashed = function( str ) { return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) { return $1 + '-' + $2; } ).toLowerCase(); }; let console = global.console; // allow user to initialize classes via [data-namespace] or .js-namespace class // htmlInit( Widget, 'widgetName' ) // options are parsed from data-namespace-options utils.htmlInit = function( WidgetClass, namespace ) { utils.docReady( function() { let dashedNamespace = utils.toDashed( namespace ); let dataAttr = 'data-' + dashedNamespace; let dataAttrElems = document.querySelectorAll( `[${dataAttr}]` ); let jQuery = global.jQuery; [ ...dataAttrElems ].forEach( ( elem ) => { let attr = elem.getAttribute( dataAttr ); let options; try { options = attr && JSON.parse( attr ); } catch ( error ) { // log error, do not initialize if ( console ) { console.error( `Error parsing ${dataAttr} on ${elem.className}: ${error}` ); } return; } // initialize let instance = new WidgetClass( elem, options ); // make available via $().data('namespace') if ( jQuery ) { jQuery.data( elem, namespace, instance ); } } ); } ); }; // ----- ----- // return utils; } ) ); // core ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( window, require('ev-emitter'), require('fizzy-ui-utils'), ); } else { // browser global window.InfiniteScroll = factory( window, window.EvEmitter, window.fizzyUIUtils, ); } }( window, function factory( window, EvEmitter, utils ) { let jQuery = window.jQuery; // internal store of all InfiniteScroll intances let instances = {}; function InfiniteScroll( element, options ) { let queryElem = utils.getQueryElement( element ); if ( !queryElem ) { console.error( 'Bad element for InfiniteScroll: ' + ( queryElem || element ) ); return; } element = queryElem; // do not initialize twice on same element if ( element.infiniteScrollGUID ) { let instance = instances[ element.infiniteScrollGUID ]; instance.option( options ); return instance; } this.element = element; // options this.options = { ...InfiniteScroll.defaults }; this.option( options ); // add jQuery if ( jQuery ) { this.$element = jQuery( this.element ); } this.create(); } // defaults InfiniteScroll.defaults = { // path: null, // hideNav: null, // debug: false, }; // create & destroy methods InfiniteScroll.create = {}; InfiniteScroll.destroy = {}; let proto = InfiniteScroll.prototype; // inherit EvEmitter Object.assign( proto, EvEmitter.prototype ); // -------------------------- -------------------------- // // globally unique identifiers let GUID = 0; proto.create = function() { // create core // add id for InfiniteScroll.data let id = this.guid = ++GUID; this.element.infiniteScrollGUID = id; // expando instances[ id ] = this; // associate via id // properties this.pageIndex = 1; // default to first page this.loadCount = 0; this.updateGetPath(); // bail if getPath not set, or returns falsey #776 let hasPath = this.getPath && this.getPath(); if ( !hasPath ) { console.error('Disabling InfiniteScroll'); return; } this.updateGetAbsolutePath(); this.log( 'initialized', [ this.element.className ] ); this.callOnInit(); // create features for ( let method in InfiniteScroll.create ) { InfiniteScroll.create[ method ].call( this ); } }; proto.option = function( opts ) { Object.assign( this.options, opts ); }; // call onInit option, used for binding events on init proto.callOnInit = function() { let onInit = this.options.onInit; if ( onInit ) { onInit.call( this, this ); } }; // ----- events ----- // proto.dispatchEvent = function( type, event, args ) { this.log( type, args ); let emitArgs = event ? [ event ].concat( args ) : args; this.emitEvent( type, emitArgs ); // trigger jQuery event if ( !jQuery || !this.$element ) { return; } // namespace jQuery event type += '.infiniteScroll'; let $event = type; if ( event ) { // create jQuery event /* eslint-disable-next-line new-cap */ let jQEvent = jQuery.Event( event ); jQEvent.type = type; $event = jQEvent; } this.$element.trigger( $event, args ); }; let loggers = { initialized: ( className ) => `on ${className}`, request: ( path ) => `URL: ${path}`, load: ( response, path ) => `${response.title || ''}. URL: ${path}`, error: ( error, path ) => `${error}. URL: ${path}`, append: ( response, path, items ) => `${items.length} items. URL: ${path}`, last: ( response, path ) => `URL: ${path}`, history: ( title, path ) => `URL: ${path}`, pageIndex: function( index, origin ) { return `current page determined to be: ${index} from ${origin}`; }, }; // log events proto.log = function( type, args ) { if ( !this.options.debug ) return; let message = `[InfiniteScroll] ${type}`; let logger = loggers[ type ]; if ( logger ) message += '. ' + logger.apply( this, args ); console.log( message ); }; // -------------------------- methods used amoung features -------------------------- // proto.updateMeasurements = function() { this.windowHeight = window.innerHeight; let rect = this.element.getBoundingClientRect(); this.top = rect.top + window.scrollY; }; proto.updateScroller = function() { let elementScroll = this.options.elementScroll; if ( !elementScroll ) { // default, use window this.scroller = window; return; } // if true, set to element, otherwise use option this.scroller = elementScroll === true ? this.element : utils.getQueryElement( elementScroll ); if ( !this.scroller ) { throw new Error(`Unable to find elementScroll: ${elementScroll}`); } }; // -------------------------- page path -------------------------- // proto.updateGetPath = function() { let optPath = this.options.path; if ( !optPath ) { console.error(`InfiniteScroll path option required. Set as: ${optPath}`); return; } // function let type = typeof optPath; if ( type == 'function' ) { this.getPath = optPath; return; } // template string: '/pages/{{#}}.html' let templateMatch = type == 'string' && optPath.match('{{#}}'); if ( templateMatch ) { this.updateGetPathTemplate( optPath ); return; } // selector: '.next-page-selector' this.updateGetPathSelector( optPath ); }; proto.updateGetPathTemplate = function( optPath ) { // set getPath with template string this.getPath = () => { let nextIndex = this.pageIndex + 1; return optPath.replace( '{{#}}', nextIndex ); }; // get pageIndex from location // convert path option into regex to look for pattern in location // escape query (?) in url, allows for parsing GET parameters let regexString = optPath .replace( /(\\\?|\?)/, '\\?' ) .replace( '{{#}}', '(\\d\\d?\\d?)' ); let templateRe = new RegExp( regexString ); let match = location.href.match( templateRe ); if ( match ) { this.pageIndex = parseInt( match[1], 10 ); this.log( 'pageIndex', [ this.pageIndex, 'template string' ] ); } }; let pathRegexes = [ // WordPress & Tumblr - example.com/page/2 // Jekyll - example.com/page2 /^(.*?\/?page\/?)(\d\d?\d?)(.*?$)/, // Drupal - example.com/?page=1 /^(.*?\/?\?page=)(\d\d?\d?)(.*?$)/, // catch all, last occurence of a number /(.*?)(\d\d?\d?)(?!.*\d)(.*?$)/, ]; // try matching href to pathRegexes patterns let getPathParts = InfiniteScroll.getPathParts = function( href ) { if ( !href ) return; for ( let regex of pathRegexes ) { let match = href.match( regex ); if ( match ) { let [ , begin, index, end ] = match; return { begin, index, end }; } } }; proto.updateGetPathSelector = function( optPath ) { // parse href of link: '.next-page-link' let hrefElem = document.querySelector( optPath ); if ( !hrefElem ) { console.error(`Bad InfiniteScroll path option. Next link not found: ${optPath}`); return; } let href = hrefElem.getAttribute('href'); let pathParts = getPathParts( href ); if ( !pathParts ) { console.error(`InfiniteScroll unable to parse next link href: ${href}`); return; } let { begin, index, end } = pathParts; this.isPathSelector = true; // flag for checkLastPage() this.getPath = () => begin + ( this.pageIndex + 1 ) + end; // get pageIndex from href this.pageIndex = parseInt( index, 10 ) - 1; this.log( 'pageIndex', [ this.pageIndex, 'next link' ] ); }; proto.updateGetAbsolutePath = function() { let path = this.getPath(); // path doesn't start with http or / let isAbsolute = path.match( /^http/ ) || path.match( /^\// ); if ( isAbsolute ) { this.getAbsolutePath = this.getPath; return; } let { pathname } = location; // query parameter #829. example.com/?pg=2 let isQuery = path.match( /^\?/ ); // /foo/bar/index.html => /foo/bar let directory = pathname.substring( 0, pathname.lastIndexOf('/') ); let pathStart = isQuery ? pathname : directory + '/'; this.getAbsolutePath = () => pathStart + this.getPath(); }; // -------------------------- nav -------------------------- // // hide navigation InfiniteScroll.create.hideNav = function() { let nav = utils.getQueryElement( this.options.hideNav ); if ( !nav ) return; nav.style.display = 'none'; this.nav = nav; }; InfiniteScroll.destroy.hideNav = function() { if ( this.nav ) this.nav.style.display = ''; }; // -------------------------- destroy -------------------------- // proto.destroy = function() { this.allOff(); // remove all event listeners // call destroy methods for ( let method in InfiniteScroll.destroy ) { InfiniteScroll.destroy[ method ].call( this ); } delete this.element.infiniteScrollGUID; delete instances[ this.guid ]; // remove jQuery data. #807 if ( jQuery && this.$element ) { jQuery.removeData( this.element, 'infiniteScroll' ); } }; // -------------------------- utilities -------------------------- // // https://remysharp.com/2010/07/21/throttling-function-calls InfiniteScroll.throttle = function( fn, threshold ) { threshold = threshold || 200; let last, timeout; return function() { let now = +new Date(); let args = arguments; let trigger = () => { last = now; fn.apply( this, args ); }; if ( last && now < last + threshold ) { // hold on to it clearTimeout( timeout ); timeout = setTimeout( trigger, threshold ); } else { trigger(); } }; }; InfiniteScroll.data = function( elem ) { elem = utils.getQueryElement( elem ); let id = elem && elem.infiniteScrollGUID; return id && instances[ id ]; }; // set internal jQuery, for Webpack + jQuery v3 InfiniteScroll.setJQuery = function( jqry ) { jQuery = jqry; }; // -------------------------- setup -------------------------- // utils.htmlInit( InfiniteScroll, 'infinite-scroll' ); // add noop _init method for jQuery Bridget. #768 proto._init = function() {}; let { jQueryBridget } = window; if ( jQuery && jQueryBridget ) { jQueryBridget( 'infiniteScroll', InfiniteScroll, jQuery ); } // -------------------------- -------------------------- // return InfiniteScroll; } ) ); // page-load ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( window, require('./core'), ); } else { // browser global factory( window, window.InfiniteScroll, ); } }( window, function factory( window, InfiniteScroll ) { let proto = InfiniteScroll.prototype; Object.assign( InfiniteScroll.defaults, { // append: false, loadOnScroll: true, checkLastPage: true, responseBody: 'text', domParseResponse: true, // prefill: false, // outlayer: null, } ); InfiniteScroll.create.pageLoad = function() { this.canLoad = true; this.on( 'scrollThreshold', this.onScrollThresholdLoad ); this.on( 'load', this.checkLastPage ); if ( this.options.outlayer ) { this.on( 'append', this.onAppendOutlayer ); } }; proto.onScrollThresholdLoad = function() { if ( this.options.loadOnScroll ) this.loadNextPage(); }; let domParser = new DOMParser(); proto.loadNextPage = function() { if ( this.isLoading || !this.canLoad ) return; let { responseBody, domParseResponse, fetchOptions } = this.options; let path = this.getAbsolutePath(); this.isLoading = true; if ( typeof fetchOptions == 'function' ) fetchOptions = fetchOptions(); let fetchPromise = fetch( path, fetchOptions ) .then( ( response ) => { if ( !response.ok ) { let error = new Error( response.statusText ); this.onPageError( error, path, response ); return { response }; } return response[ responseBody ]().then( ( body ) => { let canDomParse = responseBody == 'text' && domParseResponse; if ( canDomParse ) { body = domParser.parseFromString( body, 'text/html' ); } if ( response.status == 204 ) { this.lastPageReached( body, path ); return { body, response }; } else { return this.onPageLoad( body, path, response ); } } ); } ) .catch( ( error ) => { this.onPageError( error, path ); } ); this.dispatchEvent( 'request', null, [ path, fetchPromise ] ); return fetchPromise; }; proto.onPageLoad = function( body, path, response ) { // done loading if not appending if ( !this.options.append ) { this.isLoading = false; } this.pageIndex++; this.loadCount++; this.dispatchEvent( 'load', null, [ body, path, response ] ); return this.appendNextPage( body, path, response ); }; proto.appendNextPage = function( body, path, response ) { let { append, responseBody, domParseResponse } = this.options; // do not append json let isDocument = responseBody == 'text' && domParseResponse; if ( !isDocument || !append ) return { body, response }; let items = body.querySelectorAll( append ); let promiseValue = { body, response, items }; // last page hit if no items. #840 if ( !items || !items.length ) { this.lastPageReached( body, path ); return promiseValue; } let fragment = getItemsFragment( items ); let appendReady = () => { this.appendItems( items, fragment ); this.isLoading = false; this.dispatchEvent( 'append', null, [ body, path, items, response ] ); return promiseValue; }; // TODO add hook for option to trigger appendReady if ( this.options.outlayer ) { return this.appendOutlayerItems( fragment, appendReady ); } else { return appendReady(); } }; proto.appendItems = function( items, fragment ) { if ( !items || !items.length ) return; // get fragment if not provided fragment = fragment || getItemsFragment( items ); refreshScripts( fragment ); this.element.appendChild( fragment ); }; function getItemsFragment( items ) { // add items to fragment let fragment = document.createDocumentFragment(); if ( items ) fragment.append( ...items ); return fragment; } // replace ================================================ FILE: sandbox/button-first.html ================================================ button first

wyDay logos

22 Mar 2017

wyDay logos

After seeing my work on Logo Pizza, Wyatt at wyDay employed my services to design a new set of logos for wyDay and its products, LimeLM and wyBuild. wyDay makes "premium software development tools for high-tech companies." A kindred spirit for Metafizzy!

wyDay logos before & after

wyDay already had a strong brand to work with. That LimeLM pirate with lime eye patch is a great visual — easily memorable. The wyDay and wyBuild logos were a bit generic, so I was offered a blank slate for them. The end result is a set of straight-forward, iconic emblems.

Mealio logo

8 Mar 2017

Mealio logo

If you eat and if you even remotely like food then Mealio is the right place for you. Mealio helps people solve the problem of constantly having to create a weekly meal plan by generating them based on the user's preferences. It's like Spotify for food.

I was tasked to design the Mealio logo. I delivered on an friendly & clever M monogram that evokes the approachable and helpful qualities of Mealio. The fork and knife fitting within the counters of the M say it all. Knife, fork, M = Mealio.

================================================ FILE: sandbox/button-load.html ================================================ button load

wyDay logos

22 Mar 2017

wyDay logos

After seeing my work on Logo Pizza, Wyatt at wyDay employed my services to design a new set of logos for wyDay and its products, LimeLM and wyBuild. wyDay makes "premium software development tools for high-tech companies." A kindred spirit for Metafizzy!

wyDay logos before & after

wyDay already had a strong brand to work with. That LimeLM pirate with lime eye patch is a great visual — easily memorable. The wyDay and wyBuild logos were a bit generic, so I was offered a blank slate for them. The end result is a set of straight-forward, iconic emblems.

Mealio logo

8 Mar 2017

Mealio logo

If you eat and if you even remotely like food then Mealio is the right place for you. Mealio helps people solve the problem of constantly having to create a weekly meal plan by generating them based on the user's preferences. It's like Spotify for food.

I was tasked to design the Mealio logo. I delivered on an friendly & clever M monogram that evokes the approachable and helpful qualities of Mealio. The fork and knife fitting within the counters of the M say it all. Knife, fork, M = Mealio.

================================================ FILE: sandbox/container-scroll.html ================================================ container scroll
================================================ FILE: sandbox/css/blog.css ================================================ body { line-height: 1.4; background: #EEE; color: #444; } .container { padding: 0 20px; max-width: 700px; margin: 0 auto; } .site-header { height: 150px; background: #FFF; } .post { margin: 60px 0; background: white; padding: 10px; } .post img { display: block; max-width: 100%; } .load-more-button { font-size: 20px; padding: 20px; } .site-footer { padding: 200px 0; background: #333; color: white; } .scroll-status { margin: 60px 0; display: none; } ================================================ FILE: sandbox/css/loader-ellips.css ================================================ /* loader-ellips ------------------------- */ .loader-ellips { font-size: 20px; position: relative; width: 4em; height: 1em; margin: 10px auto; } .loader-ellips__dot { display: block; width: 1em; height: 1em; border-radius: 0.5em; background: #333; position: absolute; animation-duration: 0.5s; animation-timing-function: ease; animation-iteration-count: infinite; } .loader-ellips__dot--1, .loader-ellips__dot--2 { left: 0; } .loader-ellips__dot--3 { left: 1.5em; } .loader-ellips__dot--4 { left: 3em; } @keyframes reveal { from { transform: scale(0.001); } to { transform: scale(1); } } @keyframes slide { to { transform: translateX(1.5em) } } .loader-ellips__dot--1 { animation-name: reveal; } .loader-ellips__dot--2, .loader-ellips__dot--3 { animation-name: slide; } .loader-ellips__dot--4 { animation-name: reveal; animation-direction: reverse; } ================================================ FILE: sandbox/css/masonry-images.css ================================================ .container { max-width: 1200px; margin: 0 auto; padding-bottom: 400px; } .grid { min-height: 600px; } .grid__col-sizer, .grid__item { width: 30%; } .grid__gutter-sizer { width: 3.33%; } .grid__item { float: left; margin-bottom: 30px; visibility: hidden;; /*hidden by default*/ } .grid.are-images-ready .grid__item { visibility: visible; } .grid__item img { display: block; max-width: 100%; } .scroll-status { display: none; } ================================================ FILE: sandbox/element-scroll.html ================================================ element scroll

wyDay logos

22 Mar 2017

wyDay logos

After seeing my work on Logo Pizza, Wyatt at wyDay employed my services to design a new set of logos for wyDay and its products, LimeLM and wyBuild. wyDay makes "premium software development tools for high-tech companies." A kindred spirit for Metafizzy!

wyDay logos before & after

wyDay already had a strong brand to work with. That LimeLM pirate with lime eye patch is a great visual — easily memorable. The wyDay and wyBuild logos were a bit generic, so I was offered a blank slate for them. The end result is a set of straight-forward, iconic emblems.

Mealio logo

8 Mar 2017

Mealio logo

If you eat and if you even remotely like food then Mealio is the right place for you. Mealio helps people solve the problem of constantly having to create a weekly meal plan by generating them based on the user's preferences. It's like Spotify for food.

I was tasked to design the Mealio logo. I delivered on an friendly & clever M monogram that evokes the approachable and helpful qualities of Mealio. The fork and knife fitting within the counters of the M say it all. Knife, fork, M = Mealio.

================================================ FILE: sandbox/html-init.html ================================================ html init

wyDay logos

22 Mar 2017

wyDay logos

After seeing my work on Logo Pizza, Wyatt at wyDay employed my services to design a new set of logos for wyDay and its products, LimeLM and wyBuild. wyDay makes "premium software development tools for high-tech companies." A kindred spirit for Metafizzy!

wyDay logos before & after

wyDay already had a strong brand to work with. That LimeLM pirate with lime eye patch is a great visual — easily memorable. The wyDay and wyBuild logos were a bit generic, so I was offered a blank slate for them. The end result is a set of straight-forward, iconic emblems.

Mealio logo

8 Mar 2017

Mealio logo

If you eat and if you even remotely like food then Mealio is the right place for you. Mealio helps people solve the problem of constantly having to create a weekly meal plan by generating them based on the user's preferences. It's like Spotify for food.

I was tasked to design the Mealio logo. I delivered on an friendly & clever M monogram that evokes the approachable and helpful qualities of Mealio. The fork and knife fitting within the counters of the M say it all. Knife, fork, M = Mealio.

Last page reached

================================================ FILE: sandbox/jquery-plugin.html ================================================ jquery plugin

wyDay logos

22 Mar 2017

wyDay logos

After seeing my work on Logo Pizza, Wyatt at wyDay employed my services to design a new set of logos for wyDay and its products, LimeLM and wyBuild. wyDay makes "premium software development tools for high-tech companies." A kindred spirit for Metafizzy!

wyDay logos before & after

wyDay already had a strong brand to work with. That LimeLM pirate with lime eye patch is a great visual — easily memorable. The wyDay and wyBuild logos were a bit generic, so I was offered a blank slate for them. The end result is a set of straight-forward, iconic emblems.

Mealio logo

8 Mar 2017

Mealio logo

If you eat and if you even remotely like food then Mealio is the right place for you. Mealio helps people solve the problem of constantly having to create a weekly meal plan by generating them based on the user's preferences. It's like Spotify for food.

I was tasked to design the Mealio logo. I delivered on an friendly & clever M monogram that evokes the approachable and helpful qualities of Mealio. The fork and knife fitting within the counters of the M say it all. Knife, fork, M = Mealio.

Last page reached

================================================ FILE: sandbox/js/masonry-images.js ================================================ /* globals Masonry, imagesLoaded */ let msnry = new Masonry( '.grid', { itemSelector: 'none', // select no images on init columnWidth: '.grid__col-sizer', gutter: '.grid__gutter-sizer', percentPosition: true, stagger: 30, visibleStyle: { transform: 'translateY(0)', opacity: 1, }, hiddenStyle: { transform: 'translateY(100px)', opacity: 0, }, } ); imagesLoaded( '.grid', function() { msnry.options.itemSelector = '.grid__item'; // select proper items document.querySelector('.grid').classList.add('are-images-ready'); let items = document.querySelectorAll('.grid__item'); msnry.appended( items ); } ); window.infScroll = new InfiniteScroll( '.grid', { path: '.pagination__next', append: '.grid__item', debug: true, outlayer: msnry, status: '.scroll-status', scrollThreshold: 1, } ); ================================================ FILE: sandbox/js/scroll-loader.js ================================================ let container = document.querySelector('.posts-container'); window.infScroll = new InfiniteScroll( container, { path: '.pagination__next', append: '.post', nav: '.pagination', status: '.scroll-status', debug: true, // history: false, } ); ================================================ FILE: sandbox/js/unsplash-masonry.js ================================================ /* globals Masonry, imagesLoaded */ let msnry = new Masonry( '.grid', { itemSelector: '.grid-item', columnWidth: '.grid-sizer', percentPosition: true, stagger: 30, visibleStyle: { transform: 'translateY(0)', opacity: 1 }, hiddenStyle: { transform: 'translateY(100px)', opacity: 0 }, } ); const clientId = '9ad80b14098bcead9c7de952435e937cc3723ae61084ba8e729adb642daf0251'; let infScroll = new InfiniteScroll( '.grid', { path: `https://api.unsplash.com/photos?client_id=${clientId}&page={{#}}`, responseBody: 'json', status: '.scroll-status', history: false, } ); let proxyDiv = document.createElement('div'); infScroll.on( 'load', function( data ) { // convert data into HTML let itemsHTML = data.map( getItem ).join(''); // get elements from HTML string proxyDiv.innerHTML = itemsHTML; let items = proxyDiv.querySelectorAll('.grid-item'); // append items after imagesLoaded imagesLoaded( items, function() { infScroll.appendItems( items ); msnry.appended( items ); } ); } ); // load first page infScroll.loadNextPage(); // ----- template ----- // let itemTemplateSrc = document.querySelector('#item-template').innerHTML; function getItem( photo ) { return microTemplate( itemTemplateSrc, photo ); } // micro templating, sort-of function microTemplate( src, data ) { // replace {{tags}} in source return src.replace( /\{\{([\w\-_.]+)\}\}/gi, function( match, key ) { // walk through objects to get value let value = data; key.split('.').forEach( ( part ) => { value = value[ part ]; } ); return value; } ); } ================================================ FILE: sandbox/js/unsplash.js ================================================ let container = document.querySelector('.container'); let unsplashID = '9ad80b14098bcead9c7de952435e937cc3723ae61084ba8e729adb642daf0251'; let infScroll = new InfiniteScroll( '.container', { path: `https://api.unsplash.com/photos?page={{#}}&client_id=${unsplashID}`, responseBody: 'json', history: false, } ); infScroll.on( 'load', function( data ) { let itemsHTML = data.map( getItem ).join(''); container.innerHTML += itemsHTML; } ); let itemTemplateSrc = document.querySelector('#item-template').innerHTML; function getItem( photo ) { return microTemplate( itemTemplateSrc, photo ); } // micro templating, sort-of function microTemplate( src, data ) { // replace {{tags}} in source return src.replace( /\{\{([\w\-_.]+)\}\}/gi, function( match, key ) { // walk through objects to get value let value = data; key.split('.').forEach( function( part ) { value = value[ part ]; } ); return value; } ); } // load first page infScroll.loadNextPage(); ================================================ FILE: sandbox/masonry-images/index.html ================================================ Masonry images

Masonry images

Next

No more pages to load

Last page loaded

================================================ FILE: sandbox/masonry-images/page2.html ================================================ Masonry images - page 2

Masonry images - page 2

Next

No more pages to load

Last page loaded

================================================ FILE: sandbox/masonry-images/page3.html ================================================ Masonry images - page 3

Masonry images - page 3

Next

No more pages to load

Last page loaded

================================================ FILE: sandbox/masonry-images/page4.html ================================================ Masonry images - page 4

Masonry images - page 4

Next

No more pages to load

Last page loaded

================================================ FILE: sandbox/masonry-images/page5.html ================================================ Masonry images - page 5

Masonry images - page 5

================================================ FILE: sandbox/page/2.html ================================================ Page 2

Sunset Spark logo

6 Mar 2017

Sunset Spark logo

Sunset Spark is a technology and science school in Sunset Park, Brooklyn, NY. They teach new American families all the subjects I would have devoured as a kid: robotics, creative coding, game development. Just take a peak at their Insta. I'm proud to have designed their new logo.

The double-S and bolt logo is a nice balance between approachable/fun and professional/serious. The bolt is front and center. The S's take a back seat formed by the bolt and the outlines.

================================================ FILE: sandbox/page/3.html ================================================ #3 page

Shepherd logo & brand

6 Dec 2016

Shepherd brand

Shepherd is a platform for business owners to build communities and peer groups so they can better face the challenges and opportunities they share (launching soon!). I designed the brand with supervision from Shepherd's lead, Ben. The main logo depicts two crossed shepherd crooks forming an S.

The alternate logo is a dog, an Australian shepherd. Shepherds still use dogs to herd and control their flocks. Dogs are a great symbol of companionship and loyalty, qualities central to Shepherd. Herding breeds like the Aussie and border collie are the smartest.

Erin loves Aussies. Even as her husband, I'll always be second place in her heart, behind her family Aussie, Tess. She was thrilled to find my screen like this:

Aussie desktop

Designing an Aussie logo was a fun challenge. Blue merle Aussies have striking coloring, with gray and black patchwork and tan points. My solution was to stylize the whole image: use a striped pattern for the patches, punch up the colors to blue and gold, and stick to an angular grid. It's a dog you won't forget.

Logo Pizza Delivered

2 Oct 2016

Logo Pizza

After shipping Flickity v2 thus wrapping 2016's huge development project, I didn't have it in me to write another line of code. I discussed my state of mind and motivation in this 3 min podcast.

New thing, shortsightedness on Bumpers

Looking to change things up, I started making logos.

Man, I love logos. A little piece of imagery that represents the ideal you want your project to be — that's design magic right there. I've been able to work on some great logo projects, but I've been itching to do more. Rather than wait for projects to come my way, I gave myself a project of my own: design 50 logos. 50 logos in 30 days.

Fifty logos is a lot. At least one or two a day for an entire month. I tried pushing myself: exploring different styles, subjects, and techniques. It was like design boot camp: working all those muscles you never use.

================================================ FILE: sandbox/page/4.html ================================================ 4th page

Fetchy Shiba logo comin' to get ya

18 Aug 2016

Fetchy logos

Fetchy takes YouTube videos and converts them to downloadable videos and audio files. Thank heavens. Pulling out videos to make clips and gifs has been difficult for too long. Fetchy is a great service that I consistently need. I was pumped to be tasked with designing its logo.

I kicked off the concepts with dogs, lots of dogs. Why over think it? I sketched out a variety of breeds, actions, and styles. I tried using the most meme-able breeds: corgis and pugs. To mix things up, I sketched a couple "f bird" concepts and more straight-forward monogram letters.

Look at that winged puppy. Too bad he didn't get used. Fly high, little guy.

CodePen showcase: Round 1

9 Aug 2016

Metafizzy's libraries have hundreds of CodePen demos. One for every feature, option, and behavior. But these demos are simplified examples. They don't do a good job of showing off what they're capable of. So I reached out to the true code artists of CodePen to see how they could make Isotope, Flickity, and Packery shine in the spotlight. Give 'em the old razzle-dazzle.

I'll be collecting these in the Metafizzy showcase CodePen collection, as well as individual collections for each library.

Kseso makes the most of Flickity and CSS, using is-selected classes to trigger transitions for each slide. Within each slide is a great mix of imagery and typography at that!

See the Pen Playing with Flickity by Kseso (@Kseso) on CodePen.

Jesse Shawl made a slide puzzle with Packery. I can't believe this actually works!

See the Pen Order the tiles by Jesse Shawl (@jshawl) on CodePen.

Gregor Adams took the Packery concept composed this 3D cube ballet. I love how the cubes align even with staggered animation.

See the Pen Pack(ev)ery thing by Gregor Adams (@pixelass) on CodePen.

But he didn't stop there. Gregor made another Packery demo, this time using simpler rectangles. Check out how it works when you resize it.

See the Pen Packery hackery by Gregor Adams (@pixelass) on CodePen.

Perhaps the best use of Isotope ever, Antoinette Janus makes sense of many characters and fusions in Steven Universe. I have a hard time remembering the difference between Sugilite and Sardonyx ;)

See the Pen Steven Universe x Isotope [Sponsored] by Antoinette Janus (@acjdesigns) on CodePen.

Bennett Feely makes a 3D, hovering Packery layout. It's melting my mind how this works.

See the Pen Packery layout with 3D blocks by Bennett Feely (@bennettfeely) on CodePen.

Katy DeCorah brings her ever-impressive style to make this captivating grid animation using Packery. This one is fun to resize.

See the Pen Geo scope by Katy DeCorah (@katydecorah) on CodePen.

It's a delight to see what creative coders can come up with.

We have some more artists lined up so stay tuned for Round 2!

================================================ FILE: sandbox/page/5.html ================================================ The fifth page

Flickity takes The Field

17 Dec 2015

The Field is a new outdoor lifestyle publication, or as founder Chris Stillitano puts it, "The Field is a place for good design and the great outdoors." It's also a great place to see Flickity in use.

The Field's beautiful photo galleries are made with Flickity. It's great to see how they've taken advantage of Flickity's customizable previous & buttons to minimally style and position them. The slide counters change on cellSelect.

The site has a keen design sense, utilizing different layouts for its various content types. So much care went into these photo galleries. Watch how images resize and center while resizing the browser, even with images of varying aspect ratios.

Fizzy bear branded

1 Dec 2015

Fizzy's got a brand new brand.

Metafizzy brand

The previous logotype was a beaut and served Metafizzy well. But it did have some issues (No fault of James' — these issues were all self-inflicted as I requested).

  • Legibility trouble: People would read "Meta fuzzy" or worse "Meta furry"
  • The two line treatment led to people spelling the name as two words "Meta Fizzy"
  • It did not reduce well. It didn't hold up at small sizes, like a Twitter avatar or favicon
================================================ FILE: sandbox/page/6.html ================================================ Page number 6

Triggering jQuery and vanilla JS events

3 Jul 2015

You can now bind to jQuery events in Isotope, Packery, and Masonry. The recent upgrades allow you to use standard jQuery event methods .on(), .off, and .one(), rather than using ugly plugin method syntax.

// previous plugin method syntax
// Isotope <= v2.2.0
$grid.isotope( 'on', 'layoutComplete', function() {...})

// standard jQuery event
// Isotope >= v2.2.1
$grid.on( 'layoutComplete', function() {...})

View Isotope layoutComplete demo on CodePen.

This feature is already in Flickity and Draggabilly. It was prime time to port it over to the layout libraries.

Dropping IE8 and 9 support

1 Jul 2015

2015 will be the last year Metafizzy supports IE8 and 9. In 2016, we'll release new major versions of our libraries that remove support for the Internet Explorers of yore.

The time has come. Global browser usage is especially low for both browsers. IE8 is at 2%. IE9 is at 1.5%.

IE8 and 9 were the last browsers to have significant feature gaps. Dropping their support will allow a lot of ugly code to be removed. Polyfill libraries can be removed like eventie, classie, and doc-ready.

If you still require IE8 and 9 support, previous versions will still be completely available to download and view documentation. The old versions will no longer get bug fixes or improved features, but you can continue using them as long as you like.

We've opened issues to track this change for each library. Follow along:

================================================ FILE: sandbox/prefill.html ================================================ prefill

wyDay logos

22 Mar 2017

wyDay logos

After seeing my work on Logo Pizza, Wyatt at wyDay employed my services to design a new set of logos for wyDay and its products, LimeLM and wyBuild. wyDay makes "premium software development tools for high-tech companies." A kindred spirit for Metafizzy!

wyDay logos before & after

wyDay already had a strong brand to work with. That LimeLM pirate with lime eye patch is a great visual — easily memorable. The wyDay and wyBuild logos were a bit generic, so I was offered a blank slate for them. The end result is a set of straight-forward, iconic emblems.

Mealio logo

8 Mar 2017

Mealio logo

If you eat and if you even remotely like food then Mealio is the right place for you. Mealio helps people solve the problem of constantly having to create a weekly meal plan by generating them based on the user's preferences. It's like Spotify for food.

I was tasked to design the Mealio logo. I delivered on an friendly & clever M monogram that evokes the approachable and helpful qualities of Mealio. The fork and knife fitting within the counters of the M say it all. Knife, fork, M = Mealio.

Last page reached

================================================ FILE: sandbox/scroll-3.html ================================================ scroll 3, then button (Facebook)

wyDay logos

22 Mar 2017

wyDay logos

After seeing my work on Logo Pizza, Wyatt at wyDay employed my services to design a new set of logos for wyDay and its products, LimeLM and wyBuild. wyDay makes "premium software development tools for high-tech companies." A kindred spirit for Metafizzy!

wyDay logos before & after

wyDay already had a strong brand to work with. That LimeLM pirate with lime eye patch is a great visual — easily memorable. The wyDay and wyBuild logos were a bit generic, so I was offered a blank slate for them. The end result is a set of straight-forward, iconic emblems.

Mealio logo

8 Mar 2017

Mealio logo

If you eat and if you even remotely like food then Mealio is the right place for you. Mealio helps people solve the problem of constantly having to create a weekly meal plan by generating them based on the user's preferences. It's like Spotify for food.

I was tasked to design the Mealio logo. I delivered on an friendly & clever M monogram that evokes the approachable and helpful qualities of Mealio. The fork and knife fitting within the counters of the M say it all. Knife, fork, M = Mealio.

================================================ FILE: sandbox/scroll-loader.html ================================================ scroll loader

wyDay logos

22 Mar 2017

wyDay logos

After seeing my work on Logo Pizza, Wyatt at wyDay employed my services to design a new set of logos for wyDay and its products, LimeLM and wyBuild. wyDay makes "premium software development tools for high-tech companies." A kindred spirit for Metafizzy!

wyDay logos before & after

wyDay already had a strong brand to work with. That LimeLM pirate with lime eye patch is a great visual — easily memorable. The wyDay and wyBuild logos were a bit generic, so I was offered a blank slate for them. The end result is a set of straight-forward, iconic emblems.

Mealio logo

8 Mar 2017

Mealio logo

If you eat and if you even remotely like food then Mealio is the right place for you. Mealio helps people solve the problem of constantly having to create a weekly meal plan by generating them based on the user's preferences. It's like Spotify for food.

I was tasked to design the Mealio logo. I delivered on an friendly & clever M monogram that evokes the approachable and helpful qualities of Mealio. The fork and knife fitting within the counters of the M say it all. Knife, fork, M = Mealio.

No more pages to load

Last page loaded

================================================ FILE: sandbox/unsplash-masonry.html ================================================ unsplash masonry

unsplash masonry

Loading photos from Unsplash API

No more pages to load

Last page loaded

================================================ FILE: sandbox/unsplash.html ================================================ unsplash

unsplash

Loading photos from Unsplash API

================================================ FILE: test/_get-server.js ================================================ // https://developer.mozilla.org/en-US/docs/Node_server_without_framework // https://stackoverflow.com/a/29046869 const http = require('http'); const url = require('url'); const fs = require('fs'); const path = require('path'); module.exports = function getServer() { return http.createServer( listener ); }; const mimeTypes = { '.ico': 'image/x-icon', '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.png': 'image/png', '.jpg': 'image/jpeg', '.wav': 'audio/wav', '.mp3': 'audio/mpeg', '.svg': 'image/svg+xml', '.pdf': 'application/pdf', '.doc': 'application/msword', }; function listener( req, res ) { // if ( req.url.includes('.html') ) { // console.log( `${req.method} ${req.url}` ); // } let parsedUrl = url.parse( req.url ); let pathname = `.${parsedUrl.pathname}`; let ext = path.parse( pathname ).ext; fs.exists( pathname, function( exist ) { if ( !exist ) { // if the file is not found, return 404 res.statusCode = 404; res.end(`File ${pathname} not found`); return; } // if is a directory search for index file matching the extention if ( fs.statSync( pathname ).isDirectory() ) { pathname += '/index' + ext; } // read file from file system fs.readFile( pathname, function( err, data ) { if ( err ) { res.statusCode = 500; res.end(`Error getting the file: ${err}.`); } else { // if the file is found, set Content-type and send data let mimeType = mimeTypes[ ext ] || 'text/plain'; res.setHeader( 'Content-type', mimeType ); res.end( data ); } } ); } ); } ================================================ FILE: test/_with-page.js ================================================ // https://github.com/avajs/ava/blob/v3.13.0/docs/recipes/puppeteer.md const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const getPort = require('get-port'); module.exports = async( t, run ) => { const port = await getPort({ port: getPort.makeRange( 9100, 9200 ) }); let server = getServer(); server.listen( port ); const browser = await puppeteer.launch(); const page = await browser.newPage(); try { await run( t, page ); } finally { await page.close(); await browser.close(); server.close(); } }; ================================================ FILE: test/check-last-page.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9005; let server, browser; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); } ); test.after( async function() { await browser.close(); server.close(); } ); async function withPage( t, run ) { const page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/page-load.html`); try { await run( t, page ); } finally { await page.close(); } } function getPageAssertions() { return async function() { let { infScroll, serialT } = window; // check that last doesn't trigger when not last page infScroll.on( 'last', onLast ); function onLast() { serialT.fail('last event should not trigger when not last page'); } await infScroll.loadNextPage().then( function() { infScroll.off( 'last', onLast ); } ); let promise = new Promise( function( resolve ) { infScroll.once( 'last', function() { serialT.pass(`last event triggered on ${infScroll.pageIndex}`); resolve( serialT.assertions ); } ); } ); // load page 3 infScroll.loadNextPage(); return promise; }; } // ------ tests ------ // test( 'checkLastPage: true', withPage, async function( t, page ) { await page.evaluate( function() { window.infScroll = new InfiniteScroll( '.container', { append: '.post', path: '.next-link', // checkLastPage: true, // true by default } ); } ); let assertions = await page.evaluate( getPageAssertions() ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test( 'checkLastPage: ".selector-string"', withPage, async function( t, page ) { await page.evaluate( function() { window.infScroll = new InfiniteScroll( '.container', { append: '.post', path: 'page/{{#}}.html', checkLastPage: '.next-link', } ); } ); let assertions = await page.evaluate( getPageAssertions() ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test( 'checkLastPage with empty page', withPage, async function( t, page ) { await page.evaluate( function() { window.infScroll = new InfiniteScroll( '.container', { // provide only page/2.html, then falsy path: function() { if ( this.pageIndex < 2 ) { return `page/${this.pageIndex + 1}.html`; } else { return 'page/empty.html'; } }, // checkLastPage: true, // true by default append: '.post', } ); } ); let assertions = await page.evaluate( getPageAssertions() ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test( 'checkLastPage with path: function() {}', withPage, async function( t, page ) { let assertions = await page.evaluate( function() { let infScroll = new InfiniteScroll( '.container', { // provide only page/2.html, then falsy path: function() { if ( this.pageIndex < 2 ) { return `page/${this.pageIndex + 1}.html`; } }, // checkLastPage: true, // true by default append: '.post', } ); // function returning falsey will trigger last right after pageLoad let promise = new Promise( function( resolve ) { infScroll.once( 'last', function() { serialT.is( infScroll.pageIndex, 2 ); resolve( serialT.assertions ); } ); } ); // load page 2 infScroll.loadNextPage(); return promise; } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); ================================================ FILE: test/dist-jquery.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9011; let server, browser, page; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/dist-jquery.html`); } ); test.after( async function() { page.close(); await browser.close(); server.close(); } ); // ------ tests ------ // test( 'dist jQuery', async( t ) => { let assertions = await page.evaluate( function() { const { jQuery } = window; let $container = document.querySelector('.container'); $container = jQuery('.container').infiniteScroll({ path: 'page/{{#}}.html', append: '.post', }); let infScroll = $container.data('infinite-scroll'); return $container.infiniteScroll('loadNextPage') .then( function( load ) { let { response, body, items } = load; serialT.true( response instanceof Response ); serialT.true( response.ok ); serialT.is( response.status, 200 ); serialT.true( body instanceof HTMLDocument ); serialT.true( items instanceof NodeList ); serialT.is( items.length, 2 ); serialT.true( items[0] == $container[0].children[1] ); serialT.true( items[1] == $container[0].children[2] ); serialT.is( infScroll.pageIndex, 2 ); serialT.is( infScroll.loadCount, 1 ); } ) .then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); ================================================ FILE: test/dist.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9010; let server, browser, page; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/dist.html`); } ); test.after( async function() { page.close(); await browser.close(); server.close(); } ); // ------ tests ------ // test( 'dist', async( t ) => { let assertions = await page.evaluate( function() { let $container = document.querySelector('.container'); let infScroll = new InfiniteScroll( $container, { path: 'page/{{#}}.html', append: '.post', } ); return infScroll.loadNextPage() .then( function( load ) { let { response, body, items } = load; serialT.true( response instanceof Response ); serialT.true( response.ok ); serialT.is( response.status, 200 ); serialT.true( body instanceof HTMLDocument ); serialT.true( items instanceof NodeList ); serialT.is( items.length, 2 ); serialT.true( items[0] == $container.children[1] ); serialT.true( items[1] == $container.children[2] ); serialT.is( infScroll.pageIndex, 2 ); serialT.is( infScroll.loadCount, 1 ); } ) .then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); ================================================ FILE: test/history.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9007; let server, browser, page; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/history.html`); } ); test.after( async function() { await page.close(); await browser.close(); server.close(); } ); // ------ tests ------ // test( 'history', async( t ) => { t.plan( 6 ); let assertions = await page.evaluate( function() { let $container = document.querySelector('.container'); let infScroll = new InfiniteScroll( $container, { path: 'page/{{#}}.html', append: '.post', scrollThreshold: false, // history: 'replace', // default historyTitle: true, } ); function getTop( $elem ) { return $elem.getBoundingClientRect().top + window.scrollY; } let page1Top = getTop( $container ) - 100; let page2Top, page3Top; // append next page, scroll to it to trigger history return new Promise( function( resolve ) { infScroll.once( 'append', function( response, _path, items ) { page2Top = getTop( items[0] ); // TODO this block should be its own promise, but can't get it to work infScroll.once( 'history', function( title, path ) { serialT.is( path, location.href, `2nd page history url changed to ${path}` ); serialT.is( title, document.title, `document title changed to ${title}` ); resolve(); } ); scrollTo( 0, page2Top - window.innerHeight / 4 ); } ); infScroll.loadNextPage(); } ) // scroll back up to top of page to trigger history on previous page .then( new Promise( function( resolve ) { infScroll.once( 'history', function( title, path ) { serialT.is( path, location.href, `1st page history, url changed to ${path}` ); serialT.is( title, document.title, `document title changed to ${title}` ); resolve(); } ); scrollTo( 0, page1Top ); } ) ) .then( new Promise( function( resolve ) { infScroll.once( 'append', function( response, _path, items ) { page3Top = getTop( items[0] ); resolve(); } ); infScroll.loadNextPage(); } ) ) .then( new Promise( function( resolve ) { infScroll.once( 'history', function( title, path ) { serialT.is( path, location.href, `3rd page history url changed to ${path}` ); serialT.is( title, document.title, `document title changed to ${title}` ); resolve(); } ); scrollTo( 0, page3Top - window.innerHeight / 4 ); } ) ) .then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); ================================================ FILE: test/html/_serial-t.js ================================================ // serialT works kinda like t, but to pass serialized assertions out of .evaluate // serialT.assertions will be passed out of .evaluate // https://github.com/avajs/ava/blob/master/docs/03-assertions.md#built-in-assertions const tKeys = [ 'pass', 'fail', 'assert', 'truthy', 'falsy', 'true', 'false', 'is', 'not', 'deepEqual', 'notDeepEqual', ]; window.serialT = tKeys.reduce( ( serializedT, method ) => { serializedT[ method ] = function( ...args ) { serializedT.assertions.push({ method, args }); }; return serializedT; }, { assertions: [] } ); ================================================ FILE: test/html/dist-jquery.html ================================================ Infinite Scroll test - dist jQuery

Infinite Scroll test - dist jQuery

page 1, post 1

Next

================================================ FILE: test/html/dist.html ================================================ Infinite Scroll test - dist

Infinite Scroll test - dist

page 1, post 1

Next

================================================ FILE: test/html/history.html ================================================ Infinite Scroll test - history

Infinite Scroll test - history

page 1, post 1
page 1, post 2
================================================ FILE: test/html/outlayer.html ================================================ Infinite Scroll test - page-load

Infinite Scroll test - outlayer

================================================ FILE: test/html/page/2.html ================================================ test page 2

test page 2

page 2, post 1
page 2, post 2
Page 3 ================================================ FILE: test/html/page/2.json ================================================ [ { "id": 2, "content": "venus" }, { "id": 3, "content": "mother earth" }, { "id": 4, "content": "mars" } ] ================================================ FILE: test/html/page/3.html ================================================ test page 3

test page 3

page 3, post 1
page 3, post 2
page 3, post 3
================================================ FILE: test/html/page/3.json ================================================ [ { "id": 5, "content": "jupiter" }, { "id": 6, "content": "saturn" }, { "id": 7, "content": "uranus" } ] ================================================ FILE: test/html/page/empty.html ================================================ empty page

empty page

================================================ FILE: test/html/page/fill.html ================================================
prefill post
================================================ FILE: test/html/page/no-access.html ================================================ test page no access

test page no access

no access page, post 1
no access page, post 2
================================================ FILE: test/html/page/outlayer2.html ================================================
================================================ FILE: test/html/page/outlayer3.html ================================================
================================================ FILE: test/html/page-index.html ================================================ Infinite Scroll test - page-index

Infinite Scroll test - page-index

page 4, post 1

Page 5

================================================ FILE: test/html/page-load.html ================================================ Infinite Scroll test - page-load

Infinite Scroll test - page-load

page 1, post 1

Next

================================================ FILE: test/html/path.html ================================================ Infinite Scroll test - path

Infinite Scroll test - path

Next

================================================ FILE: test/html/prefill.html ================================================ Infinite Scroll test - page-load

Infinite Scroll test - prefill

================================================ FILE: test/html/scroll-watch-element.html ================================================ Infinite Scroll test - scrollWatch element

Infinite Scroll test - scrollWatch element

page 1, post 1
page 1, post 2
page 1, post 3
page 1, post 4
page 1, post 5
================================================ FILE: test/html/scroll-watch-window.html ================================================ Infinite Scroll test - scrollWatch window

Infinite Scroll test - scrollWatch window

================================================ FILE: test/html/test.css ================================================ body { font-family: sans-serif; } .post { background: #EEE; margin-bottom: 10px; } .container { border: 1px solid #CCC; } .container--big-posts .post { height: 190px; /* 200px total height*/ } /* space for scrolling past */ .footer { background: #DDD; height: 600px; } ================================================ FILE: test/load-next-page-promise.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9002; let server, browser, page; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/page-load.html`); } ); test.after( async function() { page.close(); await browser.close(); server.close(); } ); // ------ tests ------ // test( 'loadNextPage promise', async( t ) => { let assertions = await page.evaluate( function() { let $container = document.querySelector('.container'); let infScroll = new InfiniteScroll( $container, { path: 'page/{{#}}.html', append: '.post', } ); return infScroll.loadNextPage() .then( function( load ) { let { response, body, items } = load; serialT.true( response instanceof Response ); serialT.true( response.ok ); serialT.is( response.status, 200 ); serialT.true( body instanceof HTMLDocument ); serialT.true( items instanceof NodeList ); serialT.is( items.length, 2 ); serialT.true( items[0] == $container.children[1] ); serialT.true( items[1] == $container.children[2] ); serialT.is( infScroll.pageIndex, 2 ); serialT.is( infScroll.loadCount, 1 ); } ) .then( () => infScroll.loadNextPage() ) // load page3.html .then( function( load ) { let { response, body, items } = load; serialT.true( response instanceof Response ); serialT.true( response.ok ); serialT.is( response.status, 200 ); serialT.true( body instanceof HTMLDocument ); serialT.true( items instanceof NodeList ); serialT.is( items.length, 3 ); serialT.true( items[0] == $container.children[3] ); serialT.true( items[1] == $container.children[4] ); serialT.is( infScroll.pageIndex, 3 ); serialT.is( infScroll.loadCount, 2 ); } ) .then( () => infScroll.loadNextPage() ) // try loading page4.html .then( function( load ) { let { response, body, items } = load; serialT.true( response instanceof Response ); serialT.false( response.ok ); serialT.is( response.status, 404 ); serialT.true( body === undefined ); serialT.true( items === undefined ); serialT.is( infScroll.pageIndex, 3 ); serialT.is( infScroll.loadCount, 2 ); } ) .then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); ================================================ FILE: test/outlayer.js ================================================ /* globals imagesLoaded, Masonry */ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9008; let server, browser; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); } ); test.after( async function() { await browser.close(); server.close(); } ); async function withPage( t, run ) { let page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/outlayer.html`); try { await run( t, page ); } finally { await page.close(); } } // ------ tests ------ // test( 'outlayer items', withPage, async( t, page ) => { let assertions = await page.evaluate( function() { let msnry, infScroll; let $container = document.querySelector('.container'); function checkItems( items ) { items.forEach( ( item, i ) => { let $itemElem = item.element; let { left, top } = $itemElem.style; serialT.truthy( $itemElem.parentNode === infScroll.element, `item ${i} has infScroll parent` ); serialT.truthy( left && top, `item ${i} has left & top style set` ); } ); } function loadNextPagePromise() { let promises = [ new Promise( ( resolve ) => msnry.once( 'layoutComplete', resolve ) ), new Promise( ( resolve ) => infScroll.once( 'load', resolve ) ), new Promise( ( resolve ) => infScroll.once( 'append', resolve ) ), ]; infScroll.loadNextPage(); return Promise.all( promises ); } return new Promise( function( resolve ) { imagesLoaded( $container, resolve ); } ) .then( () => { msnry = new Masonry( $container, { itemSelector: '.outlayer-item', transitionDuration: '0.1s', } ); infScroll = new InfiniteScroll( $container, { path: 'page/outlayer{{#}}.html', append: '.outlayer-item', outlayer: msnry, scrollThreshold: false, } ); return loadNextPagePromise(); } ) .then( ([ items ]) => { serialT.is( items.length, 8, '8 items laid out on page 2' ); checkItems( items ); return loadNextPagePromise(); } ) .then( ([ items ]) => { serialT.is( items.length, 10, '10 items laid out on page 3' ); checkItems( items ); } ) .then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test( 'outlayer none', withPage, async( t, page ) => { let assertions = await page.evaluate( function() { let msnry, infScroll; let $container = document.querySelector('.container'); return new Promise( function( resolve ) { imagesLoaded( $container, resolve ); } ) .then( () => { msnry = new Masonry( $container, { itemSelector: '.outlayer-item', transitionDuration: '0.1s', } ); infScroll = new InfiniteScroll( $container, { path: 'page/outlayer{{#}}.html', append: 'NONE', // prevent appending with faulty selector outlayer: msnry, scrollThreshold: false, } ); let promise = new Promise( ( resolve ) => { infScroll.once( 'load', () => { serialT.pass('load triggered but not append'); resolve(); } ); } ); infScroll.loadNextPage(); return promise; } ) .then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); ================================================ FILE: test/page-index.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9004; let server, browser; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); } ); test.after( async function() { await browser.close(); server.close(); } ); async function withPage( t, run ) { const page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/page-index.html`); try { await run( t, page ); } finally { await page.close(); } } // ------ tests ------ // test( 'pageIndex from path element', withPage, async function( t, page ) { let pageIndex = await page.evaluate( function() { let infScroll = new InfiniteScroll( '.container', { path: '.next-link', } ); return infScroll.pageIndex; } ); t.is( pageIndex, 4 ); } ); test( 'pageIndex from URL with {{#}}', withPage, async function( t, page ) { let pageIndex = await page.evaluate( function() { window.history.replaceState( null, document.title, 'page/7' ); let infScroll = new InfiniteScroll( '.container', { path: 'page/{{#}}', } ); return infScroll.pageIndex; } ); t.is( pageIndex, 7 ); } ); test( 'pageIndex from GET param with {{#}}', withPage, async function( t, page ) { let pageIndex = await page.evaluate( function() { window.history.replaceState( null, document.title, 'page?currPage=8' ); let infScroll = new InfiniteScroll( '.container', { path: 'page?currPage={{#}}', } ); return infScroll.pageIndex; } ); t.is( pageIndex, 8 ); } ); test( 'pageIndex from GET param with {{#}} and pre-escaped regexp', withPage, async function( t, page ) { let pageIndex = await page.evaluate( function() { window.history.replaceState( null, document.title, 'page?currPage=8' ); let infScroll = new InfiniteScroll( '.container', { path: 'page\\?currPage={{#}}', } ); return infScroll.pageIndex; } ); t.is( pageIndex, 8 ); } ); ================================================ FILE: test/page-load-error.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9003; let server, browser; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); } ); test.after( async function() { await browser.close(); server.close(); } ); async function withPage( t, run ) { let page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/page-load.html`); try { await run( t, page ); } finally { page.close(); } } // ------ tests ------ // test( 'pageLoad error 404', withPage, async( t, page ) => { t.plan( 2 ); let assertions = await page.evaluate( function() { let infScroll = new InfiniteScroll( '.container', { path: () => 'page/4.html', } ); infScroll.on( 'load', function() { serialT.fail('load event should not trigger'); } ); let eventPromises = Promise.all([ // request event new Promise( function( resolve ) { infScroll.on( 'request', function() { serialT.pass('request event'); resolve(); } ); } ), // error event new Promise( function( resolve ) { infScroll.on( 'error', function( error ) { serialT.true( Boolean( error ), 'error event, with error argument' ); resolve(); } ); } ), infScroll.loadNextPage(), ]); return eventPromises.then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test( 'pageLoad error 403', withPage, async( t, page ) => { // t.plan( 2 ); let assertions = await page.evaluate( function() { let infScroll = new InfiniteScroll( '.container', { path: () => 'page/no-access.html', } ); let eventPromises = Promise.all([ // request event new Promise( function( resolve ) { infScroll.on( 'request', function() { serialT.pass('request event'); resolve(); } ); } ), // load event // TODO this test is backwards // load event should NOT trigger, but it does in Puppeteer, but not in browser ?? new Promise( function( resolve ) { infScroll.on( 'load', function( body, path, response ) { serialT.truthy( body ); serialT.is( response.status, 200 ); resolve(); } ); } ), infScroll.loadNextPage(), ]); return eventPromises.then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test( 'pageLoad error no CORS', withPage, async( t, page ) => { t.plan( 2 ); let assertions = await page.evaluate( function() { let infScroll = new InfiniteScroll( '.container', { path: () => 'http://example.com', } ); let eventPromises = Promise.all([ // request event new Promise( function( resolve ) { infScroll.on( 'request', function() { serialT.pass('request event'); resolve(); } ); } ), // error event new Promise( function( resolve ) { infScroll.on( 'error', function( error ) { serialT.true( Boolean( error ), 'error event, with error argument' ); resolve(); } ); } ), ]); infScroll.on( 'load', function() { serialT.fail('load event should not trigger'); } ); infScroll.loadNextPage(); return eventPromises.then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); ================================================ FILE: test/page-load-json.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const page2Json = require('./html/page/2.json'); const page3Json = require('./html/page/3.json'); const port = 9009; let server, browser, page; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/page-load.html`); } ); test.after( async function() { page.close(); await browser.close(); server.close(); } ); // ------ tests ------ // test.serial( 'page-load: page/2.json', async( t ) => { let assertions = await page.evaluate( function( page2Data ) { let infScroll = window.infScroll = new InfiniteScroll( '.container', { path: 'page/{{#}}.json', responseBody: 'json', history: false, } ); let eventPromises = Promise.all([ // request event new Promise( function( resolve ) { infScroll.once( 'request', function( path ) { serialT.truthy( path.match('page/2.json') ); resolve(); } ); } ), // load event new Promise( function( resolve ) { infScroll.once( 'load', function( response, path ) { serialT.truthy( path.match('page/2.json') ); serialT.deepEqual( response, page2Data ); serialT.is( infScroll.loadCount, 1 ); serialT.is( infScroll.pageIndex, 2 ); resolve(); } ); } ), ]); // load page 2 infScroll.loadNextPage(); return eventPromises.then( () => serialT.assertions ); }, page2Json ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test.serial( 'page-load: page/3.json', async( t ) => { let assertions = await page.evaluate( function( page3Data ) { let infScroll = window.infScroll; let eventPromises = Promise.all([ // request event new Promise( function( resolve ) { infScroll.once( 'request', function( path ) { serialT.truthy( path.match('page/3.json') ); resolve(); } ); } ), // load event new Promise( function( resolve ) { infScroll.once( 'load', function( response, path ) { serialT.truthy( path.match('page/3.json') ); serialT.deepEqual( response, page3Data ); serialT.is( infScroll.loadCount, 2 ); serialT.is( infScroll.pageIndex, 3 ); resolve(); } ); } ), ]); // load page 2 infScroll.loadNextPage(); return eventPromises.then( () => serialT.assertions ); }, page3Json ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); ================================================ FILE: test/page-load.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9002; let server, browser, page; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/page-load.html`); } ); test.after( async function() { page.close(); await browser.close(); server.close(); } ); // ------ tests ------ // test.serial( 'page-load: page 2', async( t ) => { let assertions = await page.evaluate( function() { let $container = document.querySelector('.container'); let infScroll = new InfiniteScroll( $container, { path: '.next-link', append: '.post', } ); let eventPromises = Promise.all([ // request event new Promise( function( resolve ) { infScroll.once( 'request', function( path ) { serialT.truthy( path.match('page/2.html') ); resolve(); } ); } ), // load event new Promise( function( resolve ) { infScroll.once( 'load', function( response, path ) { serialT.is( response.nodeName, '#document' ); serialT.truthy( path.match('page/2.html') ); serialT.is( infScroll.loadCount, 1 ); serialT.is( infScroll.pageIndex, 2 ); resolve(); } ); } ), // append event new Promise( function( resolve ) { infScroll.once( 'append', function( response, path, items ) { serialT.is( response.nodeName, '#document' ); serialT.truthy( path.match('page/2.html') ); serialT.is( items.length, 2 ); serialT.truthy( $container.children[1] === items[0] ); // item0 appended serialT.truthy( $container.children[2] === items[1] ); // item1 appended // inline script executed serialT.truthy( window.page2InlineScriptLoaded ); resolve(); } ); } ), ]); // load page 2 infScroll.loadNextPage(); return eventPromises.then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test.serial( 'page-load: page 3', async( t ) => { let assertions = await page.evaluate( function() { serialT.assertions = []; // reset assertions let $container = document.querySelector('.container'); let infScroll = InfiniteScroll.data( $container ); let eventPromises = Promise.all([ // request event new Promise( function( resolve ) { infScroll.once( 'request', function( path ) { serialT.truthy( path.match('page/3.html') ); resolve(); } ); } ), // load event new Promise( function( resolve ) { infScroll.once( 'load', function( response, path ) { serialT.is( response.nodeName, '#document' ); serialT.truthy( path.match('page/3.html') ); serialT.is( infScroll.loadCount, 2 ); serialT.is( infScroll.pageIndex, 3 ); resolve(); } ); } ), // append event new Promise( function( resolve ) { infScroll.once( 'append', function( response, path, items ) { serialT.is( response.nodeName, '#document' ); serialT.truthy( path.match('page/3.html') ); serialT.is( items.length, 3 ); serialT.truthy( $container.children[3] === items[0] ); serialT.truthy( $container.children[4] === items[1] ); serialT.truthy( $container.children[5] === items[2] ); resolve(); } ); } ), // last event new Promise( function( resolve ) { infScroll.once( 'last', function( response, path ) { serialT.is( response.nodeName, '#document' ); serialT.truthy( path.match('page/3.html') ); resolve(); } ); } ), ]); // load page 3 infScroll.loadNextPage(); return eventPromises.then( () => serialT.assertions ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); ================================================ FILE: test/path.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9001; let server, browser; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); } ); test.after( async function() { await browser.close(); server.close(); } ); async function withPage( t, run ) { let page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/path.html`); try { await run( t, page ); } finally { page.close(); } } // ------ tests ------ // test( 'getPathParts', withPage, async( t, page ) => { let assertions = await page.evaluate( function() { let { serialT } = window; serialT.deepEqual( InfiniteScroll.getPathParts('https://example.com/page/2'), { begin: 'https://example.com/page/', index: '2', end: '', } ); serialT.deepEqual( InfiniteScroll.getPathParts('https://example.com/page44'), { begin: 'https://example.com/page', index: '44', end: '', } ); serialT.deepEqual( InfiniteScroll.getPathParts('page/5.html?dark_mode=2'), { begin: 'page/', index: '5', end: '.html?dark_mode=2', } ); serialT.deepEqual( InfiniteScroll.getPathParts('blog/?page=15&dark_mode=2'), { begin: 'blog/?page=', index: '15', end: '&dark_mode=2', } ); serialT.deepEqual( InfiniteScroll.getPathParts('blog?page=15&dark_mode=2'), { begin: 'blog?page=', index: '15', end: '&dark_mode=2', } ); let coasterUrlA = '/parks/coasters?page=2&ridden_in=2019&sort=order'; // #915 serialT.deepEqual( InfiniteScroll.getPathParts( coasterUrlA ), { begin: '/parks/coasters?page=', index: '2', end: '&ridden_in=2019&sort=order', } ); let coasterUrlB = '/parks/coasters?ridden_in=2019&sort=order&page=2'; // #915 serialT.deepEqual( InfiniteScroll.getPathParts( coasterUrlB ), { begin: '/parks/coasters?ridden_in=2019&sort=order&page=', index: '2', end: '', } ); let paginoUrl = 'https://example.com/?year=2020&pagino=15&dark_mode=true'; serialT.deepEqual( InfiniteScroll.getPathParts( paginoUrl ), { begin: 'https://example.com/?year=2020&pagino=', index: '15', end: '&dark_mode=true', } ); return serialT.assertions; } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test( 'path: function() {}', withPage, async( t, page ) => { let infScrollPath = await page.evaluate( function() { let infScroll = new InfiniteScroll( '.container', { path: function() { let nextIndex = this.loadCount + 1; return '/fn/page' + nextIndex + '.html'; }, } ); return infScroll.getPath(); } ); t.is( infScrollPath, '/fn/page1.html' ); } ); test( 'path: "string{{#}}"', withPage, async( t, page ) => { let infScrollPath = await page.evaluate( function() { let infScroll = new InfiniteScroll( '.container', { path: '/string/page{{#}}.html', } ); return infScroll.getPath(); } ); t.is( infScrollPath, '/string/page2.html' ); } ); test( 'path: ".selector-string"', withPage, async( t, page ) => { let infScrollPath = await page.evaluate( function() { let infScroll = new InfiniteScroll( '.container', { path: '.path-next-link', } ); return infScroll.getPath(); } ); t.is( infScrollPath, '/area51/selector/page10.html' ); } ); ================================================ FILE: test/prefill.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9006; let server, browser; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); } ); test.after( async function() { await browser.close(); server.close(); } ); async function withPage( t, run ) { let page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/prefill.html`); try { await run( t, page ); } finally { page.close(); } } // ------ tests ------ // test( 'prefill', withPage, async( t, page ) => { let assertions = await page.evaluate( function() { let { serialT, innerHeight } = window; // expected load count, each post is 200px tall let expLoadCount = Math.ceil( innerHeight/200 ); return new Promise( function( resolve ) { let infScroll = new InfiniteScroll( '.container--prefill-window', { path: () => 'page/fill.html', append: '.post', prefill: true, onInit: function() { this.on( 'append', onAppend ); }, } ); function onAppend() { serialT.pass(`prefill window appended post ${infScroll.loadCount}`); if ( infScroll.loadCount == expLoadCount ) { serialT.is( infScroll.loadCount, expLoadCount, `${expLoadCount} pages appended` ); resolve( serialT.assertions ); } } } ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test( 'prefill with elementScroll', withPage, async( t, page ) => { let assertions = await page.evaluate( function() { let { serialT } = window; // expected load count, each post is 200px tall, container is 500px tall let expLoadCount = 3; return new Promise( function( resolve ) { let infScroll = new InfiniteScroll( '.container--prefill-element', { path: () => 'page/fill.html', append: '.post', elementScroll: true, prefill: true, onInit: function() { this.on( 'append', onAppend ); }, } ); function onAppend() { serialT.pass(`prefill window appended post ${infScroll.loadCount}`); if ( infScroll.loadCount == expLoadCount ) { serialT.is( infScroll.loadCount, expLoadCount, `${expLoadCount} pages appended` ); resolve( serialT.assertions ); } } } ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); test( 'prefill, last hit', withPage, async( t, page ) => { let assertions = await page.evaluate( function() { let { serialT } = window; let expLoadCount = 2; return new Promise( function( resolve ) { let infScroll = new InfiniteScroll( '.container--prefill-element', { path: function() { return this.loadCount < expLoadCount ? 'page/fill.html' : false; }, append: '.post', elementScroll: true, prefill: true, onInit: function() { this.on( 'append', onAppend ); this.on( 'error', () => serialT.fail('error should not trigger') ); }, } ); function onAppend() { serialT.pass(`prefill window appended post ${infScroll.loadCount}`); if ( infScroll.loadCount == expLoadCount ) { serialT.is( infScroll.loadCount, expLoadCount, `${expLoadCount} pages appended` ); resolve( serialT.assertions ); } } } ); } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); } ); ================================================ FILE: test/scroll-watch.js ================================================ const test = require('ava'); const puppeteer = require('puppeteer'); const getServer = require('./_get-server.js'); const port = 9007; let server, browser; test.before( async function() { server = getServer(); server.listen( port ); browser = await puppeteer.launch(); } ); test.after( async function() { await browser.close(); server.close(); } ); // ------ tests ------ // test( 'scrollWatch window', async( t ) => { t.plan( 2 ); let page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/scroll-watch-window.html`); let assertions = await page.evaluate( function() { let $container = document.querySelector('.container'); let infScroll = new InfiniteScroll( $container, { path: 'page/{{#}}', scrollThreshold: 400, loadOnScroll: false, } ); let containerBottom; function updateContainerBottom() { let rect = $container.getBoundingClientRect(); containerBottom = rect.bottom + window.scrollY - window.innerHeight; } updateContainerBottom(); // add delay so that debounced event listener can trigger function waitForDebounce() { return new Promise( function( resolve ) { setTimeout( resolve, 300 ); } ); } function onScrollThresholdFail() { serialT.fail('scrollThreshold should not yet trigger'); } infScroll.on( 'scrollThreshold', onScrollThresholdFail ); let promises = waitForDebounce() // scroll up at top, nowhere near threshold. scrollThreshold should not trigger .then( () => scrollTo( 0, 100 ) ) .then( waitForDebounce ) // get close to bottom, but not over. scrollThreshold should not trigger .then( () => scrollTo( 0, containerBottom - 500 ) ) .then( waitForDebounce ) // scroll past threshold, trigger scrollThreshold .then( () => { infScroll.off( 'scrollThreshold', onScrollThresholdFail ); let promise = new Promise( function( resolve ) { infScroll.once( 'scrollThreshold', function() { serialT.pass('scrollThreshold triggered at bottom'); resolve(); } ); } ); scrollTo( 0, containerBottom - 300 ); return promise; } ) // scroll up, should not trigger scrollThreshold .then( () => { infScroll.on( 'scrollThreshold', onScrollThresholdFail ); scrollTo( 0, containerBottom - 500 ); } ) .then( waitForDebounce ) // resize element & trigger scrollThreshold .then( () => { infScroll.off( 'scrollThreshold', onScrollThresholdFail ); let promise = new Promise( function( resolve ) { infScroll.once( 'scrollThreshold', function() { serialT.pass('scrollThreshold triggered at new bottom'); resolve(); } ); } ); $container.style.height = '1000px'; updateContainerBottom(); scrollTo( 0, containerBottom - 300 ); return promise; } ) .then( () => serialT.assertions ); return promises; } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); page.close(); } ); test( 'scrollWatch element', async( t ) => { t.plan( 2 ); let page = await browser.newPage(); await page.goto(`http://localhost:${port}/test/html/scroll-watch-element.html`); let assertions = await page.evaluate( function() { let $container = document.querySelector('.container'); let infScroll = new InfiniteScroll( $container, { path: 'page/{{#}}', scrollThreshold: 200, elementScroll: true, loadOnScroll: false, } ); let containerBottom; function updateContainerBottom() { containerBottom = $container.scrollHeight - $container.clientHeight; } updateContainerBottom(); // add delay so that debounced event listener can trigger function waitForDebounce() { return new Promise( function( resolve ) { setTimeout( resolve, 300 ); } ); } function getPost() { let $post = document.createElement('div'); $post.className = 'post'; return $post; } function onScrollThresholdFail() { serialT.fail('scrollThreshold should not yet trigger'); } infScroll.on( 'scrollThreshold', onScrollThresholdFail ); let promises = waitForDebounce() // scroll up at top, nowhere near threshold. scrollThreshold should not trigger .then( () => { $container.scrollTop = 100; } ) .then( waitForDebounce ) // get close to bottom, but not over. scrollThreshold should not trigger .then( () => { $container.scrollTop = containerBottom - 300; } ) .then( waitForDebounce ) // scroll past threshold, trigger scrollThreshold .then( () => { infScroll.off( 'scrollThreshold', onScrollThresholdFail ); let promise = new Promise( function( resolve ) { infScroll.once( 'scrollThreshold', function() { serialT.pass('scrollThreshold triggered at bottom'); resolve(); } ); } ); $container.scrollTop = containerBottom - 150; return promise; } ) // scroll up, should not trigger scrollThreshold .then( () => { infScroll.on( 'scrollThreshold', onScrollThresholdFail ); $container.scrollTop = containerBottom - 400; } ) .then( waitForDebounce ) // resize element & trigger scrollThreshold .then( () => { infScroll.off( 'scrollThreshold', onScrollThresholdFail ); let promise = new Promise( function( resolve ) { infScroll.once( 'scrollThreshold', function() { serialT.pass('scrollThreshold triggered at new bottom'); resolve(); } ); } ); $container.append( getPost() ); $container.append( getPost() ); updateContainerBottom(); $container.scrollTop = containerBottom - 150; return promise; } ) .then( () => serialT.assertions ); return promises; } ); assertions.forEach( ({ method, args }) => t[ method ]( ...args ) ); page.close(); } );