├── .editorconfig ├── .github └── workflows │ ├── cs-lint.yml │ └── integrations.yml ├── .gitignore ├── .phpcs.xml.dist ├── README.md ├── bin └── install-wp-tests.sh ├── composer.json ├── images └── 1x1.trans.gif ├── js ├── jquery.sonar.js ├── jquery.sonar.min.js └── lazy-load.js ├── lazy-load.php ├── phpunit.xml.dist ├── readme.txt ├── tests ├── bootstrap.php ├── lazy-load-testcase.php └── test-process-image.php └── wpcom-helper.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # It is based on https://core.trac.wordpress.org/browser/trunk/.editorconfig. 3 | # See https://editorconfig.org for more information about the standard. 4 | 5 | # WordPress Coding Standards 6 | # https://make.wordpress.org/core/handbook/coding-standards/ 7 | 8 | root = true 9 | 10 | [*] 11 | charset = utf-8 12 | end_of_line = lf 13 | insert_final_newline = true 14 | trim_trailing_whitespace = true 15 | indent_style = tab 16 | 17 | [*.yml] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [*.txt] 25 | end_of_line = crlf 26 | -------------------------------------------------------------------------------- /.github/workflows/cs-lint.yml: -------------------------------------------------------------------------------- 1 | name: CS & Lint 2 | 3 | on: 4 | # Run on all pushes and on all pull requests. 5 | # Prevent the "push" build from running when there are only irrelevant changes. 6 | push: 7 | paths-ignore: 8 | - "**.md" 9 | pull_request: 10 | # Allow manually triggering the workflow. 11 | workflow_dispatch: 12 | 13 | jobs: 14 | checkcs: 15 | name: "Basic CS and QA checks" 16 | runs-on: ubuntu-latest 17 | 18 | env: 19 | XMLLINT_INDENT: " " 20 | 21 | steps: 22 | - name: Setup PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: "7.4" 26 | coverage: none 27 | tools: cs2pr 28 | 29 | # Show PHP lint violations inline in the file diff. 30 | # @link https://github.com/marketplace/actions/xmllint-problem-matcher 31 | - name: Register PHP lint violations to appear as file diff comments 32 | uses: korelstar/phplint-problem-matcher@v1 33 | 34 | # Show XML violations inline in the file diff. 35 | # @link https://github.com/marketplace/actions/xmllint-problem-matcher 36 | - name: Register XML violations to appear as file diff comments 37 | uses: korelstar/xmllint-problem-matcher@v1 38 | 39 | - name: Checkout code 40 | uses: actions/checkout@v2 41 | 42 | # Validate the composer.json file. 43 | # @link https://getcomposer.org/doc/03-cli.md#validate 44 | - name: Validate Composer installation 45 | run: composer validate --no-check-all 46 | 47 | # Install dependencies and handle caching in one go. 48 | # @link https://github.com/marketplace/actions/install-composer-dependencies 49 | - name: Install Composer dependencies 50 | uses: ramsey/composer-install@v1 51 | 52 | # Lint PHP. 53 | - name: Lint PHP against parse errors 54 | run: composer lint-ci | cs2pr 55 | 56 | # Needed as runs-on: system doesn't have xml-lint by default. 57 | # @link https://github.com/marketplace/actions/xml-lint 58 | - name: Lint phpunit.xml.dist 59 | uses: ChristophWurst/xmllint-action@v1 60 | with: 61 | xml-file: ./phpunit.xml.dist 62 | xml-schema-file: ./vendor/phpunit/phpunit/phpunit.xsd 63 | 64 | # Check the code-style consistency of the PHP files. 65 | # - name: Check PHP code style 66 | # continue-on-error: true 67 | # run: vendor/bin/phpcs --report-full --report-checkstyle=./phpcs-report.xml 68 | 69 | # - name: Show PHPCS results in PR 70 | # run: cs2pr ./phpcs-report.xml 71 | -------------------------------------------------------------------------------- /.github/workflows/integrations.yml: -------------------------------------------------------------------------------- 1 | name: Run PHPUnit 2 | 3 | on: 4 | # Run on all pushes and on all pull requests. 5 | # Prevent the "push" build from running when there are only irrelevant changes. 6 | push: 7 | paths-ignore: 8 | - "**.md" 9 | pull_request: 10 | # Allow manually triggering the workflow. 11 | workflow_dispatch: 12 | 13 | jobs: 14 | test: 15 | name: WP ${{ matrix.wordpress }} on PHP ${{ matrix.php }} 16 | # Ubuntu-20.x includes MySQL 8.0, which causes `caching_sha2_password` issues with PHP < 7.4 17 | # https://www.php.net/manual/en/mysqli.requirements.php 18 | # TODO: change to ubuntu-latest when we no longer support PHP < 7.4 19 | runs-on: ubuntu-18.04 20 | 21 | env: 22 | WP_VERSION: ${{ matrix.wordpress }} 23 | 24 | strategy: 25 | matrix: 26 | wordpress: ["5.5", "5.6", "5.7"] 27 | php: ["5.6", "7.0", "7.1", "7.2", "7.3", "7.4"] 28 | include: 29 | - php: "8.0" 30 | # Ignore platform requirements, so that PHPUnit 7.5 can be installed on PHP 8.0 (and above). 31 | composer-options: "--ignore-platform-reqs" 32 | extensions: pcov 33 | ini-values: pcov.directory=., "pcov.exclude=\"~(vendor|tests)~\"" 34 | coverage: pcov 35 | exclude: 36 | - php: "8.0" 37 | wordpress: "5.5" 38 | fail-fast: false 39 | 40 | steps: 41 | - name: Checkout code 42 | uses: actions/checkout@v2 43 | 44 | - name: Setup PHP ${{ matrix.php }} 45 | uses: shivammathur/setup-php@v2 46 | with: 47 | php-version: ${{ matrix.php }} 48 | extensions: ${{ matrix.extensions }} 49 | ini-values: ${{ matrix.ini-values }} 50 | coverage: ${{ matrix.coverage }} 51 | 52 | - name: Setup problem matchers for PHP 53 | run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" 54 | 55 | # Setup PCOV since we're using PHPUnit < 8 which has it integrated. Requires PHP 7.1. 56 | # Ignore platform reqs to make it install on PHP 8. 57 | # https://github.com/krakjoe/pcov-clobber 58 | - name: Setup PCOV 59 | if: ${{ matrix.php == 8.0 }} 60 | run: | 61 | composer require pcov/clobber --ignore-platform-reqs 62 | vendor/bin/pcov clobber 63 | 64 | - name: Setup Problem Matchers for PHPUnit 65 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 66 | 67 | - name: Install Composer dependencies 68 | uses: ramsey/composer-install@v1 69 | with: 70 | composer-options: "${{ matrix.composer-options }}" 71 | 72 | - name: Start MySQL Service 73 | run: sudo systemctl start mysql.service 74 | 75 | - name: Prepare environment for integration tests 76 | run: composer prepare-ci 77 | 78 | - name: Run integration tests (single site) 79 | if: ${{ matrix.php != 8.0 }} 80 | run: composer test 81 | - name: Run integration tests (single site with code coverage) 82 | if: ${{ matrix.php == 8.0 }} 83 | run: composer coverage-ci 84 | - name: Run integration tests (multisite) 85 | run: composer test-ms 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor 3 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Custom ruleset for lazy-load plugin. 4 | 5 | 6 | 7 | 8 | 9 | . 10 | 12 | /vendor/ 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lazy Load (WordPress Plugin) 2 | 3 | > ⚠️ **This plugin is deprecated** 4 | > 5 | > This plugin will not receive future updates. 6 | > 7 | > Lazy Loading functionality is now [built-in to WordPress](https://make.wordpress.org/core/2020/07/14/lazy-loading-images-in-5-5/) (and further enhanced via [plugins like Jetpack](https://jetpack.com/support/lazy-images/)). We recommend using the core and Jetpack functionality instead for better performance. 8 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 29 | WP_BRANCH=${WP_VERSION%\-*} 30 | WP_TESTS_TAG="branches/$WP_BRANCH" 31 | 32 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 33 | WP_TESTS_TAG="branches/$WP_VERSION" 34 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 35 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 36 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 37 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 38 | else 39 | WP_TESTS_TAG="tags/$WP_VERSION" 40 | fi 41 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 42 | WP_TESTS_TAG="trunk" 43 | else 44 | # http serves a single offer, whereas https serves multiple. we only want one 45 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 46 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 47 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 48 | if [[ -z "$LATEST_VERSION" ]]; then 49 | echo "Latest WordPress version could not be found" 50 | exit 1 51 | fi 52 | WP_TESTS_TAG="tags/$LATEST_VERSION" 53 | fi 54 | set -ex 55 | 56 | install_wp() { 57 | 58 | if [ -d $WP_CORE_DIR ]; then 59 | return; 60 | fi 61 | 62 | mkdir -p $WP_CORE_DIR 63 | 64 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 65 | mkdir -p $TMPDIR/wordpress-trunk 66 | rm -rf $TMPDIR/wordpress-trunk/* 67 | svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress 68 | mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR 69 | else 70 | if [ $WP_VERSION == 'latest' ]; then 71 | local ARCHIVE_NAME='latest' 72 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 73 | # https serves multiple offers, whereas http serves single. 74 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 75 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 76 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 77 | LATEST_VERSION=${WP_VERSION%??} 78 | else 79 | # otherwise, scan the releases and get the most up to date minor version of the major release 80 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 81 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 82 | fi 83 | if [[ -z "$LATEST_VERSION" ]]; then 84 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 85 | else 86 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 87 | fi 88 | else 89 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 90 | fi 91 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 92 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 93 | fi 94 | 95 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 96 | } 97 | 98 | install_test_suite() { 99 | # portable in-place argument for both GNU sed and Mac OSX sed 100 | if [[ $(uname -s) == 'Darwin' ]]; then 101 | local ioption='-i.bak' 102 | else 103 | local ioption='-i' 104 | fi 105 | 106 | # set up testing suite if it doesn't yet exist 107 | if [ ! -d $WP_TESTS_DIR ]; then 108 | # set up testing suite 109 | mkdir -p $WP_TESTS_DIR 110 | rm -rf $WP_TESTS_DIR/{includes,data} 111 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 112 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 113 | fi 114 | 115 | if [ ! -f wp-tests-config.php ]; then 116 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 117 | # remove all forward slashes in the end 118 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 119 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 120 | sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 121 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 122 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 123 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 124 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 125 | fi 126 | 127 | } 128 | 129 | recreate_db() { 130 | shopt -s nocasematch 131 | if [[ $1 =~ ^(y|yes)$ ]] 132 | then 133 | mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA 134 | create_db 135 | echo "Recreated the database ($DB_NAME)." 136 | else 137 | echo "Leaving the existing database ($DB_NAME) in place." 138 | fi 139 | shopt -u nocasematch 140 | } 141 | 142 | create_db() { 143 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 144 | } 145 | 146 | install_db() { 147 | 148 | if [ ${SKIP_DB_CREATE} = "true" ]; then 149 | return 0 150 | fi 151 | 152 | # parse DB_HOST for port or socket references 153 | local PARTS=(${DB_HOST//\:/ }) 154 | local DB_HOSTNAME=${PARTS[0]}; 155 | local DB_SOCK_OR_PORT=${PARTS[1]}; 156 | local EXTRA="" 157 | 158 | if ! [ -z $DB_HOSTNAME ] ; then 159 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 160 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 161 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 162 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 163 | elif ! [ -z $DB_HOSTNAME ] ; then 164 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 165 | fi 166 | fi 167 | 168 | # create database 169 | if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] 170 | then 171 | echo "Reinstalling will delete the existing test database ($DB_NAME)" 172 | read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB 173 | recreate_db $DELETE_EXISTING_DB 174 | else 175 | create_db 176 | fi 177 | } 178 | 179 | install_wp 180 | install_test_suite 181 | install_db 182 | 183 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automattic/lazy-load", 3 | "type": "wordpress-plugin", 4 | "description": "Lazy load images to improve page load times and server bandwidth. Images are loaded only when visible to the user.", 5 | "homepage": "https://github.com/Automattic/lazy-load/", 6 | "license": "GPL-2.0-or-later", 7 | "authors": [ 8 | { 9 | "name": "Automattic", 10 | "homepage": "https://automattic.com/" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.6", 15 | "composer/installers": "~1.0" 16 | }, 17 | "require-dev": { 18 | "automattic/vipwpcs": "^2.2", 19 | "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7", 20 | "php-parallel-lint/php-parallel-lint": "^1.0", 21 | "phpcompatibility/phpcompatibility-wp": "^2.1", 22 | "phpunit/phpunit": "^4 || ^5 || ^6 || ^7", 23 | "squizlabs/php_codesniffer": "^3.5", 24 | "wp-coding-standards/wpcs": "^2.3.0", 25 | "yoast/phpunit-polyfills": "^0.2.0" 26 | }, 27 | "scripts": { 28 | "cbf": [ 29 | "@php ./vendor/bin/phpcbf" 30 | ], 31 | "coverage": [ 32 | "@php ./vendor/bin/phpunit --coverage-html ./build/coverage-html" 33 | ], 34 | "coverage-ci": [ 35 | "@php ./vendor/bin/phpunit" 36 | ], 37 | "cs": [ 38 | "@php ./vendor/bin/phpcs" 39 | ], 40 | "lint": [ 41 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" 42 | ], 43 | "lint-ci": [ 44 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git --checkstyle" 45 | ], 46 | "prepare-ci": [ 47 | "bash bin/install-wp-tests.sh wordpress_test root root localhost" 48 | ], 49 | "test": [ 50 | "@php ./vendor/bin/phpunit --testsuite WP_Tests" 51 | ], 52 | "test-ms": [ 53 | "@putenv WP_MULTISITE=1", 54 | "@composer test" 55 | ] 56 | }, 57 | "support": { 58 | "issues": "https://github.com/Automattic/lazy-load/issues", 59 | "source": "https://github.com/Automattic/lazy-load" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /images/1x1.trans.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/lazy-load/131e5428454945b7c5d1fd19fcb3feacc1cabe5b/images/1x1.trans.gif -------------------------------------------------------------------------------- /js/jquery.sonar.js: -------------------------------------------------------------------------------- 1 | /* 2 | An elem for determining if an elem is within a certain 3 | distance from the edge above or below the screen, and attaching 4 | a function to execute once the elem is in view. 5 | 6 | General Usage: 7 | 8 | * Place the library anywhere in your JavaScript code before you 9 | intend to call the function. 10 | 11 | * To initialize Sonar with a different default distance, modify 12 | the sonar = Sonar() line immediately following the Sonar 13 | library definition. Example: 14 | 15 | sonar=Sonar(100); // Initializes Sonar with a 100px default distance. 16 | 17 | Note: 18 | 19 | * The default distance is 0 pixels. 20 | 21 | 22 | sonar.detect() Usage 23 | 24 | * Use sonar.detect(elem, distance) to check if the 25 | elem is within screen boundaries. 26 | 27 | @elem - The elem you want to detect visibility. 28 | @distance - The distance from the screen edge that should 29 | count in the check. Uses default distance if not specified. 30 | 31 | * Note: sonar.detect() adds a property to 32 | ojbects called sonarElemTop. Test to ensure there 33 | aren't any conflicts with your code. If there 34 | are, rename sonarElemTop to something else in the code. 35 | 36 | * sonar.detect() returns: 37 | true if the elem is within the screen boundaries 38 | false if th elem is out of the screen boundaries 39 | 40 | Example: 41 | 42 | Here's how to check if an advertisment is visible on a 43 | page that has the id, "ad". 44 | 45 | if (sonar.detect(document.getElementById("ad"))) 46 | { 47 | alert('The ad is visible on screen!'); 48 | } 49 | else 50 | { 51 | alert ('The ad is not on screen!); 52 | } 53 | 54 | sonar.add() Usage 55 | 56 | * This method stores elems that are then polled 57 | on user scroll by the Sonar.detect() method. 58 | 59 | * Polling initializes once the sonar.add() method is passed 60 | an elem with the following properties: 61 | 62 | obj : A reference to the elem to observe until it is within 63 | the specified distance (px). 64 | 65 | id : An alternative to the obj parameter, an "id" can be used 66 | to grab the elem to observe. 67 | 68 | call: The function to call when the elem is within the 69 | specified distance (px). The @elem argument will 70 | include the elem that triggered the callback. 71 | 72 | px : The specified distance to include as being visible on 73 | screen. This property is optional (default is 0). 74 | 75 | Example: 76 | 77 | sonar.add( 78 | { 79 | obj: document.getElementById("0026-get-out-the-way"), 80 | call: function(elem) // elem will include the elem that triggered the function. 81 | { 82 | swfelem.embedSWF("../player.swf", "0026-get-out-the-way", "640", "500", "9.0.0", 83 | {}, {file: "0026-get-out-the-way.flv", fullscreen: true}, 84 | {allowfullscreen: true, allowscriptaccess: "always"}); 85 | }, 86 | px: 400 87 | }); 88 | 89 | You can also specify an id tag to be grabbed instead of the elem: 90 | 91 | sonar.add( 92 | { 93 | id: "0026-get-out-the-way", 94 | call: function(elem) // elem will include the elem that triggered the function. 95 | { 96 | swfelem.embedSWF("../player.swf", "0026-get-out-the-way", "640", "500", "9.0.0", 97 | {}, {file: "0026-get-out-the-way.flv", fullscreen: true}, 98 | {allowfullscreen: true, allowscriptaccess: "always"}); 99 | }, 100 | px: 400 101 | }); 102 | 103 | Notes: 104 | 105 | * Setting the body or html of your page to 100% will cause sonar to have 106 | an invalid height calculation in Firefox. It is recommended that you 107 | do not set this CSS property. 108 | 109 | Example: 110 | 111 | html, body { 112 | height:100%; // Do not do this. 113 | } 114 | 115 | * If you want to set the default distance to something other 116 | than 0, either update the property directly in the code or 117 | you can do this: 118 | 119 | sonar.blip.d = 100; // Where 100 = 100 pixels above and below the screen edge. 120 | 121 | * Sleep well at night knowing Sonar automatically cleans up the 122 | event listeners on the scroll event once all calls have executed. 123 | 124 | Code History: 125 | 126 | v3 :: 8/14/2009 - David Artz (david.artz@corp.aol.com) 127 | * Fixed a bug in the polling code where splicing caused our 128 | for loop to skip over the next iteration in the loop. This 129 | caused some images in the poll to be detected when they 130 | should have been. 131 | * Re-factored Sonar to use the "Module" JavaScript library 132 | pattern, making our private variables and functions more 133 | private and inaccessible from the public interface. 134 | * Updated the sonar.add() function to return true or false, 135 | useful for determining if Sonar added the elem to the 136 | poll or executed its callback immediately. 137 | 138 | v2 :: 3/24/2009 - David Artz (david.artz@corp.aol.com) 139 | * Added support for IE 8. 140 | * Updated the way scroll top and screen height are detected, now 141 | works in IE/FF/Safari quirks mode. 142 | * Added null check for IE, it was polling for an elem that had recently 143 | been spliced out of the array. Nasty. 144 | * Modified for loop to use standard syntax. for (i in x) is known to be 145 | buggy with JS frameworks that override arrays. 146 | * Added sonar.b property to cache the body element (improving lookup time). 147 | 148 | v1 :: 11/18/2008 - David Artz (david.artz@corp.aol.com) 149 | * Officially released code for general use. 150 | 151 | */ 152 | 153 | (function( $, win, doc, undefined ){ 154 | 155 | $.fn.sonar = function( distance, full ){ 156 | // No callbacks, return the results from Sonar for 157 | // the first element in the stack. 158 | if ( typeof distance === "boolean" ) { 159 | full = distance; 160 | distance = undefined; 161 | } 162 | 163 | return $.sonar( this[0], distance, full ); 164 | }; 165 | 166 | var body = doc.body, 167 | $win = $(win), 168 | 169 | onScreenEvent = "scrollin", 170 | offScreenEvent = "scrollout", 171 | 172 | detect = function( elem, distance, full ){ 173 | 174 | if ( elem ) { 175 | 176 | // Cache the body elem in our private global. 177 | body || ( body = doc.body ); 178 | 179 | var parentElem = elem, // Clone the elem for use in our loop. 180 | 181 | elemTop = 0, // The resets the calculated elem top to 0. 182 | 183 | // Used to recalculate elem.sonarElemTop if body height changes. 184 | bodyHeight = body.offsetHeight, 185 | 186 | // NCZ: I don't think you need innerHeight, I believe all major browsers support clientHeight. 187 | screenHeight = win.innerHeight || doc.documentElement.clientHeight || body.clientHeight || 0, // Height of the screen. 188 | 189 | // NCZ: I don't think you need pageYOffset, I believe all major browsers support scrollTop. 190 | scrollTop = doc.documentElement.scrollTop || win.pageYOffset || body.scrollTop || 0, // How far the user scrolled down. 191 | elemHeight = elem.offsetHeight || 0; // Height of the element. 192 | 193 | // If our custom "sonarTop" variable is undefined, or the document body 194 | // height has changed since the last time we ran sonar.detect()... 195 | if ( !elem.sonarElemTop || elem.sonarBodyHeight !== bodyHeight ) { 196 | 197 | // Loop through the offsetParents to calculate it. 198 | if ( parentElem.offsetParent ) { 199 | do { 200 | elemTop += parentElem.offsetTop; 201 | } 202 | while ( parentElem = parentElem.offsetParent ); 203 | } 204 | 205 | // Set the custom property (sonarTop) to avoid future attempts to calculate 206 | // the distance on this elem from the top of the page. 207 | elem.sonarElemTop = elemTop; 208 | 209 | // Along the same lines, store the body height when we calculated 210 | // the elem's top. 211 | elem.sonarBodyHeight = bodyHeight; 212 | } 213 | 214 | // If no distance was given, assume 0. 215 | distance = distance === undefined ? 0 : distance; 216 | 217 | // Dump all calculated variables. 218 | /* 219 | console.dir({ 220 | elem: elem, 221 | sonarElemTop: elem.sonarElemTop, 222 | elemHeight: elemHeight, 223 | scrollTop: scrollTop, 224 | screenHeight: screenHeight, 225 | distance: distance, 226 | full: full 227 | }); 228 | */ 229 | 230 | // If elem bottom is above the screen top and 231 | // the elem top is below the screen bottom, it's false. 232 | // If full is specified, it si subtracted or added 233 | // as needed from the element's height. 234 | return (!(elem.sonarElemTop + (full ? 0 : elemHeight) < scrollTop - distance) && 235 | !(elem.sonarElemTop + (full ? elemHeight : 0) > scrollTop + screenHeight + distance)); 236 | } 237 | }, 238 | 239 | // Container for elems needing to be polled. 240 | pollQueue = {}, 241 | 242 | // Indicates if scroll events are bound to the poll. 243 | pollActive = 0, 244 | 245 | // Used for debouncing. 246 | pollId, 247 | 248 | // Function that handles polling when the user scrolls. 249 | poll = function(){ 250 | 251 | // Debouncing speed optimization. Essentially prevents 252 | // poll requests from queue'ing up and overloading 253 | // the scroll event listener. 254 | pollId && clearTimeout( pollId ); 255 | pollId = setTimeout(function(){ 256 | 257 | var elem, 258 | elems, 259 | screenEvent, 260 | options, 261 | detected, 262 | i, l; 263 | 264 | for ( screenEvent in pollQueue ) { 265 | 266 | elems = pollQueue[ screenEvent ]; 267 | 268 | for (i = 0, l = elems.length; i < l; i++) { 269 | 270 | options = elems[i]; 271 | elem = options.elem; 272 | 273 | // console.log("Polling " + elem.id); 274 | 275 | detected = detect( elem, options.px, options.full ); 276 | 277 | // If the elem is not detected (offscreen) or detected (onscreen) 278 | // remove the elem from the queue and fire the callback. 279 | if ( screenEvent === offScreenEvent ? !detected : detected ) { 280 | // // console.log(screenEvent); 281 | if (!options.tr) { 282 | 283 | if ( elem[ screenEvent ] ) { 284 | // console.log("triggered:" + elem.id); 285 | // Trigger the onscreen or offscreen event depending 286 | // on the desired event. 287 | $(elem).trigger( screenEvent ); 288 | 289 | options.tr = 1; 290 | 291 | // removeSonar was called on this element, clean it up 292 | // instead of triggering the event. 293 | } else { 294 | // console.log("Deleting " + elem.id); 295 | 296 | // Remove this object from the elem poll container. 297 | elems.splice(i, 1); 298 | 299 | // Decrement the counter and length because we just removed 300 | // one from it. 301 | i--; 302 | l--; 303 | } 304 | } 305 | } else { 306 | options.tr = 0; 307 | } 308 | } 309 | } 310 | 311 | }, 0 ); // End setTimeout performance tweak. 312 | }, 313 | 314 | removeSonar = function( elem, screenEvent ){ 315 | // console.log("Removing " + elem.id); 316 | elem[ screenEvent ] = 0; 317 | }, 318 | 319 | addSonar = function( elem, options ) { 320 | // console.log("Really adding " + elem.id); 321 | // Prepare arguments. 322 | var distance = options.px, 323 | full = options.full, 324 | screenEvent = options.evt, 325 | parent = win, // Getting ready to accept parents: options.parent || win, 326 | detected = detect( elem, distance, full /*, parent */ ), 327 | triggered = 0; 328 | 329 | elem[ screenEvent ] = 1; 330 | 331 | // If the elem is not detected (offscreen) or detected (onscreen) 332 | // trigger the event and fire the callback immediately. 333 | if ( screenEvent === offScreenEvent ? !detected : detected ) { 334 | // console.log("Triggering " + elem.id + " " + screenEvent ); 335 | // Trigger the onscreen event at the next possible cycle. 336 | // Artz: Ask the jQuery team why I needed to do this. 337 | setTimeout(function(){ 338 | $(elem).trigger( screenEvent === offScreenEvent ? offScreenEvent : onScreenEvent ); 339 | }, 0); 340 | triggered = 1; 341 | // Otherwise, add it to the polling queue. 342 | } 343 | 344 | // console.log("Adding " + elem.id + " to queue."); 345 | // Push the element and its callback into the poll queue. 346 | pollQueue[ screenEvent ].push({ 347 | elem: elem, 348 | px: distance, 349 | full: full, 350 | tr: triggered/* , 351 | parent: parent */ 352 | }); 353 | 354 | // Activate the poll if not currently activated. 355 | if ( !pollActive ) { 356 | $win.bind( "scroll", poll ); 357 | pollActive = 1; 358 | } 359 | 360 | 361 | // Call the prepare function if there, used to 362 | // prepare the element if we detected it. 363 | // Artz: Not implemented yet...used to preprocess elements in same loop. 364 | /* 365 | if ( prepCallback ) { 366 | prepCallback.call( elem, elem, detected ); 367 | } 368 | */ 369 | }; 370 | 371 | // Open sonar function up to the public. 372 | $.sonar = detect; 373 | 374 | pollQueue[ onScreenEvent ] = []; 375 | $.event.special[ onScreenEvent ] = { 376 | 377 | add: function( handleObj ) { 378 | var data = handleObj.data || {}, 379 | elem = this; 380 | 381 | if (!elem[onScreenEvent]){ 382 | addSonar(this, { 383 | px: data.distance, 384 | full: data.full, 385 | evt: onScreenEvent /*, 386 | parent: data.parent */ 387 | }); 388 | } 389 | }, 390 | 391 | remove: function( handleObj ) { 392 | removeSonar( this, onScreenEvent ); 393 | } 394 | 395 | }; 396 | 397 | pollQueue[ offScreenEvent ] = []; 398 | $.event.special[ offScreenEvent ] = { 399 | 400 | add: function( handleObj ) { 401 | 402 | var data = handleObj.data || {}, 403 | elem = this; 404 | 405 | if (!elem[offScreenEvent]){ 406 | addSonar(elem, { 407 | px: data.distance, 408 | full: data.full, 409 | evt: offScreenEvent /*, 410 | parent: data.parent */ 411 | }); 412 | } 413 | }, 414 | 415 | remove: function( handleObj ) { 416 | removeSonar( this, offScreenEvent ); 417 | } 418 | }; 419 | 420 | // console.log(pollQueue); 421 | })( jQuery, window, document ); -------------------------------------------------------------------------------- /js/jquery.sonar.min.js: -------------------------------------------------------------------------------- 1 | (function(e,h,l,c){e.fn.sonar=function(o,n){if(typeof o==="boolean"){n=o;o=c}return e.sonar(this[0],o,n)};var f=l.body,a="scrollin",m="scrollout",b=function(r,n,t){if(r){f||(f=l.body);var s=r,u=0,v=f.offsetHeight,o=h.innerHeight||l.documentElement.clientHeight||f.clientHeight||0,q=l.documentElement.scrollTop||h.pageYOffset||f.scrollTop||0,p=r.offsetHeight||0;if(!r.sonarElemTop||r.sonarBodyHeight!==v){if(s.offsetParent){do{u+=s.offsetTop}while(s=s.offsetParent)}r.sonarElemTop=u;r.sonarBodyHeight=v}n=n===c?0:n;return(!(r.sonarElemTop+(t?0:p)q+o+n))}},d={},j=0,i=function(){setTimeout(function(){var s,o,t,q,p,r,n;for(t in d){o=d[t];for(r=0,n=o.length;r since it's mostly all metadata, e.g. OG tags 32 | } 33 | 34 | static function setup_filters() { 35 | add_filter( 'the_content', array( __CLASS__, 'add_image_placeholders' ), 99 ); // run this later, so other content filters have run, including image_add_wh on WP.com 36 | add_filter( 'post_thumbnail_html', array( __CLASS__, 'add_image_placeholders' ), 11 ); 37 | } 38 | 39 | static function add_scripts() { 40 | wp_enqueue_script( 'wpcom-lazy-load-images', self::get_url( 'js/lazy-load.js' ), array( 'jquery', 'jquery-sonar' ), self::version, true ); 41 | wp_enqueue_script( 'jquery-sonar', self::get_url( 'js/jquery.sonar.min.js' ), array( 'jquery' ), self::version, true ); 42 | } 43 | 44 | static function add_image_placeholders( $content ) { 45 | if ( ! self::is_enabled() ) 46 | return $content; 47 | 48 | // Don't lazyload for feeds, previews, mobile 49 | if( is_feed() || is_preview() ) 50 | return $content; 51 | 52 | // Don't lazyload for amp-wp content 53 | if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) { 54 | return $content; 55 | } 56 | 57 | // Don't lazy-load if the content has already been run through previously 58 | if ( false !== strpos( $content, 'data-lazy-src' ) ) 59 | return $content; 60 | 61 | // This is a pretty simple regex, but it works 62 | $content = preg_replace_callback( '#<(img)([^>]+?)(>(.*?)|[\/]?>)#si', array( __CLASS__, 'process_image' ), $content ); 63 | 64 | return $content; 65 | } 66 | 67 | static function process_image( $matches ) { 68 | $old_attributes_str = $matches[2]; 69 | $old_attributes_kses_hair = wp_kses_hair( $old_attributes_str, wp_allowed_protocols() ); 70 | 71 | if ( empty( $old_attributes_kses_hair['src'] ) ) { 72 | return $matches[0]; 73 | } 74 | 75 | $old_attributes = self::flatten_kses_hair_data( $old_attributes_kses_hair ); 76 | $new_attributes = $old_attributes; 77 | 78 | // Set placeholder and lazy-src 79 | $new_attributes['src'] = self::get_placeholder_image(); 80 | $new_attributes['data-lazy-src'] = $old_attributes['src']; 81 | 82 | // Handle `srcset` 83 | if ( ! empty( $new_attributes['srcset'] ) ) { 84 | $new_attributes['data-lazy-srcset'] = $old_attributes['srcset']; 85 | unset( $new_attributes['srcset'] ); 86 | } 87 | 88 | // Handle `sizes` 89 | if ( ! empty( $new_attributes['sizes'] ) ) { 90 | $new_attributes['data-lazy-sizes'] = $old_attributes['sizes']; 91 | unset( $new_attributes['sizes'] ); 92 | } 93 | 94 | $new_attributes_str = self::build_attributes_string( $new_attributes ); 95 | 96 | return sprintf( '', $new_attributes_str, $matches[0] ); 97 | } 98 | 99 | private static function get_placeholder_image() { 100 | return apply_filters( 'lazyload_images_placeholder_image', self::get_url( 'images/1x1.trans.gif' ) ); 101 | } 102 | 103 | private static function flatten_kses_hair_data( $attributes ) { 104 | $flattened_attributes = array(); 105 | foreach ( $attributes as $name => $attribute ) { 106 | $flattened_attributes[ $name ] = $attribute['value']; 107 | } 108 | return $flattened_attributes; 109 | } 110 | 111 | private static function build_attributes_string( $attributes ) { 112 | $string = array(); 113 | foreach ( $attributes as $name => $value ) { 114 | if ( '' === $value ) { 115 | $string[] = sprintf( '%s', $name ); 116 | } else { 117 | $string[] = sprintf( '%s="%s"', $name, esc_attr( $value ) ); 118 | } 119 | } 120 | return implode( ' ', $string ); 121 | } 122 | 123 | static function is_enabled() { 124 | return self::$enabled; 125 | } 126 | 127 | static function get_url( $path = '' ) { 128 | return plugins_url( ltrim( $path, '/' ), __FILE__ ); 129 | } 130 | } 131 | 132 | function lazyload_images_add_placeholders( $content ) { 133 | return LazyLoad_Images::add_image_placeholders( $content ); 134 | } 135 | 136 | add_action( 'init', array( 'LazyLoad_Images', 'init' ) ); 137 | 138 | endif; 139 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Lazy Load === 2 | Contributors: batmoo, automattic, jakemgold, 10up 3 | Tags: lazy load, images, front-end optimization 4 | Requires at least: 3.2 5 | Tested up to: 5.0 6 | Stable tag: 0.7 7 | 8 | Lazy load images to improve page load times and server bandwidth. Images are loaded only when visible to the user. 9 | 10 | == Description == 11 | 12 | Lazy load images to improve page load times. Uses jQuery.sonar to only load an image when it's visible in the viewport. 13 | 14 | This plugin is an amalgamation of code written by the WordPress.com VIP team at Automattic, the TechCrunch 2011 Redesign team, and Jake Goldman (10up LLC). 15 | 16 | Uses jQuery.sonar by Dave Artz (AOL). 17 | 18 | == Installation == 19 | 20 | 1. Upload the plugin to your plugins directory 21 | 1. Activate the plugin through the 'Plugins' menu in WordPress 22 | 1. Enjoy! 23 | 24 | == Screenshots == 25 | 26 | No applicable screenshots 27 | 28 | == Frequently Asked Questions == 29 | 30 | = How do I change the placeholder image = 31 | 32 | ` 33 | add_filter( 'lazyload_images_placeholder_image', 'my_custom_lazyload_placeholder_image' ); 34 | function my_custom_lazyload_placeholder_image( $image ) { 35 | return 'http://url/to/image'; 36 | } 37 | ` 38 | 39 | = How do I lazy load other images in my theme? = 40 | 41 | You can use the lazyload_images_add_placeholders helper function: 42 | 43 | ` 44 | if ( function_exists( 'lazyload_images_add_placeholders' ) ) 45 | $content = lazyload_images_add_placeholders( $content ); 46 | ` 47 | 48 | Or, you can add an attribute called "data-lazy-src" with the source of the image URL and set the actual image URL to a transparent 1x1 pixel. 49 | 50 | You can also use output buffering, though this isn't recommended: 51 | 52 | ` 53 | if ( function_exists( 'lazyload_images_add_placeholders' ) ) 54 | ob_start( 'lazyload_images_add_placeholders' ); 55 | ` 56 | 57 | This will lazy load all your images. 58 | 59 | == Changelog == 60 | 61 | = 0.7 = 62 | 63 | * srcset and sizes support (props deas and mariusveltan) 64 | * Disable for AMP content 65 | 66 | = 0.6.1 = 67 | 68 | * Security: XSS fix (reported by Jouko Pynnöne 69 | 70 | = 0.6 = 71 | 72 | * Filter to control when lazy loading is enabled 73 | 74 | = 0.5 = 75 | 76 | * Fix lazyload_images_add_placeholders by adding missing return, props Kevin Smith 77 | * Lazy load avatars, props i8ramin 78 | * Don't lazy load images in the Dashboard 79 | * Better compatibility with Jetpack Carousel 80 | 81 | = 0.4 = 82 | 83 | * New helper function to lazy load non-post content 84 | * Prevent circular lazy-loading 85 | 86 | = 0.3 = 87 | 88 | * Make LazyLoad a static class so that it's easier to change its hooks 89 | * Hook in at a higher priority for content filters 90 | 91 | = 0.2 = 92 | 93 | * Adds noscript tags to allow the image to show up in no-js contexts (including crawlers), props smub 94 | * Lazy Load post thumbnails, props ivancamilov 95 | 96 | = 0.1 = 97 | 98 | * Initial working version 99 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | _ll = $lazy_load; 17 | 18 | add_filter( 'lazyload_images_placeholder_image', array( $this, 'override_placeholder' ) ); 19 | } 20 | 21 | function override_placeholder() { 22 | return 'placeholder.jpg'; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /tests/test-process-image.php: -------------------------------------------------------------------------------- 1 | array( 16 | array( 17 | '', 18 | 'img', 19 | ' id="img"', 20 | ), 21 | '', 22 | ), 23 | 24 | 'img_simple' => array( 25 | array( 26 | '', 27 | 'img', 28 | ' src="image.jpg"', 29 | ), 30 | '', 31 | ), 32 | 33 | 'img_with_other_attributes' => array( 34 | array( 35 | 'Alt!', 36 | 'img', 37 | ' src="image.jpg" alt="Alt!"', 38 | ), 39 | 'Alt!', 40 | ), 41 | 42 | 'img_with_srcset' => array( 43 | array( 44 | '', 45 | 'img', 46 | ' src="image.jpg" srcset="medium.jpg 1000w, large.jpg 2000w"', 47 | 48 | ), 49 | '', 50 | ), 51 | 52 | 'img_with_sizes' => array( 53 | array( 54 | '', 55 | 'img', 56 | ' src="image.jpg" sizes="(min-width: 36em) 33.3vw, 100vw"', 57 | 58 | ), 59 | '', 60 | ), 61 | ); 62 | } 63 | 64 | /** 65 | * @dataProvider get_test_data 66 | */ 67 | function test_process_image( $image_parts, $expected_html ) { 68 | $actual_html = LazyLoad_Images::process_image( $image_parts ); 69 | 70 | $this->assertEquals( $expected_html, $actual_html ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /wpcom-helper.php: -------------------------------------------------------------------------------- 1 |