├── .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)([^>]+?)(>(.*?)\\1>|[\/]?>)#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 | '
',
36 | 'img',
37 | ' src="image.jpg" alt="Alt!"',
38 | ),
39 | '
',
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 |