diff --git a/.gitignore b/.gitignore index 3c3629e..f06235c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +dist diff --git a/.jshintrc b/.jshintrc index 84b19fe..5df675b 100644 --- a/.jshintrc +++ b/.jshintrc @@ -12,5 +12,12 @@ "unused": true, "node": true, "loopfunc": true, - "predef": [ "window", "document", "define", "shoestring", "XMLHttpRequest", "ActiveXObject", "Window", "localStorage" ] + + "browser": true, + "qunit": true, + + "globals": { + "loadCSS": false, + "onloadCSS": false + } } diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +node_modules diff --git a/.repo-rt b/.repo-rt deleted file mode 100644 index 98ab428..0000000 --- a/.repo-rt +++ /dev/null @@ -1 +0,0 @@ -.jenkins diff --git a/.travis.yml b/.travis.yml index 0889d1c..143400b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - '0.11' + - 12.6.0 before_script: - npm install script: grunt -v diff --git a/Gruntfile.js b/Gruntfile.js index 4071e7b..55ba773 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,23 +1,47 @@ /* global module:false */ module.exports = function(grunt) { - // Project configuration. + require( 'matchdep' ).filterDev( ['grunt-*', '!grunt-cli'] ).forEach( grunt.loadNpmTasks ); + + // Project configuration. grunt.initConfig({ - jshint: { + jshint: { all: { options: { jshintrc: ".jshintrc" }, - src: ['Gruntfile.js', '*.js'] + src: [ + '*.js', + 'test/**/*.js', + 'src/**/*.js', + ] + } + }, + concat: { + dist: { + files: { + 'dist/loadCSS.js': ['src/loadCSS.js'], + 'dist/onloadCSS.js': ['src/onloadCSS.js'] + } + } + }, + uglify: { + options: { + preserveComments: /^\!/ + }, + dist: { + files: { + 'dist/loadCSS.min.js': ['src/loadCSS.js'], + 'dist/onloadCSS.min.js': ['src/onloadCSS.js'] + } } }, - qunit: { + qunit: { files: ['test/qunit/**/*.html'] } - }); + }); - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-qunit'); - grunt.registerTask('default', ['jshint', 'qunit']); + grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']); + grunt.registerTask('stage', ['default']); }; diff --git a/LICENSE b/LICENSE index 934365e..733395b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Filament Group +Copyright (c) @scottjehl, 2016 Filament Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ 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. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 8eb70a8..ab87c0b 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,42 @@ +:warning: This project is archived and the repository is no longer maintained. + # loadCSS -A function for loading CSS asynchronously -[c]2016 @scottjehl, [Filament Group, Inc.](https://www.filamentgroup.com/) +A pattern for loading CSS asynchronously +[c]2020 @scottjehl, @zachleat [Filament Group, Inc.](https://www.filamentgroup.com/) Licensed MIT -## Why loadCSS? +## Why an ansychronous CSS loader? -Referencing CSS files with `link[rel=stylesheet]` or `@import` will cause most browsers to delay page rendering while the stylesheet loads. This is desirable in many cases, but when loading stylesheets that are not critical to the initial rendering of a page, loadCSS (and upcoming web standards mentioned below) allows you to load stylesheets asynchronously, so they don’t block page rendering. +Referencing CSS stylesheets with `link[rel=stylesheet]` or `@import` causes browsers to delay page rendering while a stylesheet loads. When loading stylesheets that are not critical to the initial rendering of a page, this blocking behavior is undesirable. The pattern below allows us to fetch and apply CSS asynchronously. If necessary, this repo also offers a separate (and optional) JavaScript function for loading stylesheets dynamically. -* Latest release: https://github.com/filamentgroup/loadCSS/releases -* NPM: https://www.npmjs.com/package/fg-loadcss -## Basic Usage +## How to use -With the [`loadCSS` function](https://github.com/filamentgroup/loadCSS/blob/master/src/loadCSS.js) referenced in your page, simply call the `loadCSS` function and pass it a stylesheet URL: +As a primary pattern, we recommend loading asynchronous CSS like this from HTML: -```css -loadCSS( "path/to/mystylesheet.css" ); -``` +`` -The code above will insert a new CSS stylesheet `link` *after* the last stylesheet or script that loadCSS finds in the page, and the function will return a reference to that `link` element, should you want to use it later in your script. Multiple calls to loadCSS will reference CSS files in the order they are called, but keep in mind that they may finish loading in a different order than they were called depending on network conditions. +This article explains why this approach is best: https://www.filamentgroup.com/lab/load-css-simpler/ -## Recommended Usage Pattern +That is probably all you need! But if you want to load a CSS file from a JavaScript function, read on... -Browsers are beginning to support a standard markup pattern for loading CSS (and other file types) asynchronously: `` ([W3C Spec](https://www.w3.org/TR/2015/WD-preload-20150721/)). We recommend using this markup pattern to reference any CSS files that should be loaded asynchronously, and using `loadCSS` to polyfill support browsers that don't yet support this new feature. +## Dynamic CSS loading with the loadCSS function -The markup for referencing your CSS file looks like this: +The [loadCSS.js](https://github.com/filamentgroup/loadCSS/blob/master/src/loadCSS.js) file exposes a global `loadCSS` function that you can call to load CSS files programmatically, if needed. This is handy for cases where you need to dynamically load CSS from script. -```html - - +``` javascript +loadCSS( "path/to/mystylesheet.css" ); ``` -Since `rel=preload` does not apply the CSS on its own (it merely fetches it), there is an `onload` handler on the `link` that will do that for us as soon as the CSS finishes loading. Since this step requires JavaScript, you may want to include an ordinary reference to your CSS file as well, using a `noscript` element to ensure it only applies in non-JavaScript settings. - -With that markup in the `head` of your page, include the [loadCSS script](https://github.com/filamentgroup/loadCSS/blob/master/src/loadCSS.js), as well as the [loadCSS rel=preload polyfill script](https://github.com/filamentgroup/loadCSS/blob/master/src/cssrelpreload.js) in your page (inline to run right away, or in an external file if the CSS is low-priority). - -No further configuration is needed, as these scripts will automatically detect if the browsers supports `rel=preload`, and if it does not, they will find CSS files referenced in the DOM and preload them using loadCSS. In browsers that natively support `rel=preload`, these scripts will do nothing, allowing the browser to load and apply the asynchronous CSS (note the `onload` attribute above, which is there to set the `link`'s `rel` attribute to stylesheet once it finishes loading in browsers that support `rel=preload`). - -Note: regardless of whether the browser supports `rel=preload` or not, your CSS file will be referenced from the same spot in the source order as the original `link` element. Keep this in mind, as you may want to place the `link` in a particular location in your `head` element so that the CSS loads with an expected cascade order. - -You can view a demo of this `rel=preload` pattern here: http://filamentgroup.github.io/loadCSS/test/preload.html - +The code above will insert a new CSS stylesheet `link` *after* the last stylesheet or script that it finds in the page, and the function will return a reference to that `link` element, should you want to reference it later in your script. Multiple calls to loadCSS will reference CSS files in the order they are called, but keep in mind that they may finish loading in a different order than they were called. ## Function API -If you're calling loadCSS manually (without the `rel=preload` pattern, the function has 3 optional arguments. +The loadCSS function has 3 optional arguments. - `before`: By default, loadCSS attempts to inject the stylesheet link *after* all CSS and JS in the page. However, if you desire a more specific location in your document, such as before a particular stylesheet link, you can use the `before` argument to specify a particular element to use as an insertion point. Your stylesheet will be inserted *before* the element you specify. For example, here's how that can be done by simply applying an `id` attribute to your `script`. - ``` html +```html ... - - - -

Chrome reduced bug example

-

Toggling a dynamically-embedded stylesheet's media type can cause a double request to the CSS file.

-

The script in this page creates a stylesheet link with a non-matching media type (media="none"), injects it into the DOM, and then toggles its media type back to "all" after a timeout. On some page loads, but not all, the stylesheet will be requested twice. This appears to be true regardless of when the media attribute is toggled (it happens via an onload handler as well, for example).

-

This bug exists in loadCSS and we've so far been unable to work around it.

-

+ + Tests for "link" attributes + + + +

Tests for link attributes

+

Support for Subresource Integrity requires providing integrity and crossorigin attributes to the link Element.

+

By supplying an Object of attribue name/attribute value pairs as the fourth argument to loadCSS(), attributes can be set on the stylesheet's DOM Element.

+

+loadCSS( 
+	"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css",
+	null,
+	null,
+	{
+		"title": "Bootstrap Styles",
+		"type": "text/css",
+		"crossorigin": "anonymous",
+		"integrity": "sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
+		// etc.
+	}
+);
+		
+

The following tests aim to validate the behavior of the API using correct and incorrect configurations:

+ + + + + + + + + + + + + + + + + + + + + + +
TestResult
Correct integrity digestRunning test...
Incorrect integrity digestRunning test...
Incorrect crossorigin attributeRunning test...
+ + diff --git a/test/body.html b/test/body.html index d5ad750..99e14bb 100644 --- a/test/body.html +++ b/test/body.html @@ -6,7 +6,7 @@

Text before the link

- +

Text after the link

diff --git a/test/control.html b/test/control.html index 8bdf6c7..dbe416a 100644 --- a/test/control.html +++ b/test/control.html @@ -3,10 +3,9 @@ Blocking Test - +

This is a control test file to verify that an ordinary link to a slow-responding stylesheet will block render. If this text is not visible for 5 seconds or so, assumptions about CSS blocking render are valid in your browser.

- - + diff --git a/test/dom-append.html b/test/dom-append.html new file mode 100644 index 0000000..09a698b --- /dev/null +++ b/test/dom-append.html @@ -0,0 +1,18 @@ + + + + Blocking Test + + + +

This is a control test file to verify that a JavaScript appended link to a slow-responding stylesheet will block render. If this text is not visible for 5 seconds or so, assumptions about CSS blocking render are valid in your browser.

+ + + diff --git a/test/import-head.html b/test/import-head.html new file mode 100644 index 0000000..0091041 --- /dev/null +++ b/test/import-head.html @@ -0,0 +1,13 @@ + + + + Blocking Test + + + + +

This is a control test file to verify that an ordinary link to a slow-responding stylesheet will block render. If this text is not visible for 5 seconds or so, assumptions about CSS blocking render are valid in your browser.

+ + diff --git a/test/import.html b/test/import.html index 1716f26..cb80e9e 100644 --- a/test/import.html +++ b/test/import.html @@ -7,7 +7,7 @@

This is a control test file to verify that an ordinary link to a slow-responding stylesheet will block render. If this text is not visible for 5 seconds or so, assumptions about CSS blocking render are valid in your browser.

diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..7df2a30 --- /dev/null +++ b/test/index.html @@ -0,0 +1,46 @@ + + + + LoadCSS Diagnostic Test Files + + + + +

CSS Loading Diagnostic Tests

+

This directory contains a variety of files that employ various CSS loading techniques. + Most files reference a CSS file that may include a 5-second server delay to mimic latency, when tested on a node server. + The delay makes it easier to determine if content is rendered before the CSS loads.

+ +

Recommended Pattern

+ + +

Standard Loading Tests

+ + + + +

LoadCSS Assisted Loading

+ + + + + diff --git a/test/mediatoggle.html b/test/mediatoggle.html new file mode 100644 index 0000000..7d74e83 --- /dev/null +++ b/test/mediatoggle.html @@ -0,0 +1,36 @@ + + + + CSS link media toggle + + + + + + +

This is a test page that references a stylesheet using a standard link[rel=stylesheet] with a print media type, which is swapped to all media when loaded..

+ +

That markup looks like this:

+
<link rel="stylesheet" href="slow.css" media="print" onload="this.media='all'">
+ +

Note: When run locally, the CSS file has a 5 second delay built into its server response time. If it is loaded in a non-blocking manner as desired, you should be able to read this text before the page is styled as white text on green background.

+ + + diff --git a/test/new-high.html b/test/new-high.html new file mode 100644 index 0000000..f46849d --- /dev/null +++ b/test/new-high.html @@ -0,0 +1,24 @@ + + + + Recommended Pattern Test + + + + + +

LoadCSS Project

+ +

LoadCSS: High-Priority Asynchronous CSS Loading Demo

+ +

This test page demonstrates a new pattern for loading a stylesheet asynchronously at a low priority. The page will turn green when the CSS loads.

+

+<link rel="alternate stylesheet preload" href="slow.css" title="styles" as="style" onload="this.title='';this.rel='stylesheet'">
+
+

Instead of the usual print toggle, this page is loading CSS with rel="alternate stylesheet preload", which will load async at a high priority. + On load, the rel is set to stylesheet and title is turned off, causing it to apply. + What's nice about this pattern is you can lower the priority by removing the rel value of "preload" to decrease its priority from high to low. View low priority async demo

+ + + + diff --git a/test/new-low.html b/test/new-low.html new file mode 100644 index 0000000..ca1059b --- /dev/null +++ b/test/new-low.html @@ -0,0 +1,23 @@ + + + + Recommended Pattern Test + + + + + +

LoadCSS Project

+ +

LoadCSS: Low-Priority Asynchronous CSS Loading Demo

+ +

This test page demonstrates a new pattern for loading a stylesheet asynchronously at a low priority. The page will turn green when the CSS loads.

+

+<link rel="alternate stylesheet" href="slow.css" title="styles" as="style" onload="this.title='';this.rel='stylesheet'">
+
+

Instead of the usual print toggle, this page is loading CSS with rel="alternate stylesheet", which will load async at a low priority. Onload, the rel is set to stylesheet and title is turned off, causing it to apply. + What's nice about this pattern is you can add another rel value of "preload" to increase its priority from low to high. View high priority async demo

+ + + + diff --git a/test/preload.html b/test/preload.html deleted file mode 100644 index da5465b..0000000 --- a/test/preload.html +++ /dev/null @@ -1,166 +0,0 @@ - - - - CSS link[rel=preload] Polyfill - - - - - - - - - -

Async CSS w/ link[rel=preload]

-

This is a test page that references a slow-loading stylesheet using the new standard link[rel=preload].

- -

That markup looks like this:

-
<link rel="preload" href="http://scottjehl.com/css-temp/slow.php" as="style" onload="this.rel='stylesheet'">
- -

In supporting browsers (such as Chrome Canary at time of writing), this markup will cause the browser to fetch the CSS file in an asynchronous, non-render-blocking manner, and once loaded, its onload event handler will change its rel property to "stylesheet" causing it to apply visibly in the page (the CSS file will color the page background green once loaded).

- -

For browsers that do not yet support link[rel=preload], this page includes a small script, loadCSS.js, and a feature-test-based polyfill function (cssrelpreload.js) to fetch all link[rel=preload] stylesheets asynchronously and apply them to the page. The function polls the document for new links to preload until the window's onload event fires, and requires no configuration to work.

- -

Note: The CSS file has a 5 second delay built into its server response time. If it is loaded in a non-blocking manner as desired, you should be able to read this text before the page is styled as white text on green background.

- - - diff --git a/test/qunit/index.html b/test/qunit/index.html index db6986e..44ceae9 100644 --- a/test/qunit/index.html +++ b/test/qunit/index.html @@ -6,12 +6,10 @@ - - diff --git a/test/qunit/libs/qunit/qunit.js b/test/qunit/libs/qunit/qunit.js index 302545f..21b7e7c 100644 --- a/test/qunit/libs/qunit/qunit.js +++ b/test/qunit/libs/qunit/qunit.js @@ -7,6 +7,7 @@ * Released under the MIT license. * http://jquery.org/license */ +/* jshint ignore:start */ (function( window ) { diff --git a/test/qunit/tests.js b/test/qunit/tests.js index a756a0c..200ce98 100644 --- a/test/qunit/tests.js +++ b/test/qunit/tests.js @@ -1,4 +1,3 @@ -/*global window:true*/ (function(window) { /* ======== A Handy Little QUnit Reference ======== @@ -48,6 +47,21 @@ }); }); + asyncTest( 'loadCSS loads a CSS file with specific attributes', function(){ + expect(3); + var attributes = { + title: "Default Style", + type: "text/css" + }; + var ss = loadCSS("files/test.css", null, null, attributes); + onloadCSS( ss, function(){ + ok("stylesheet loaded successfully"); + equal(ss.title, attributes.title, "'title' attribute should be '" + attributes.title + "'"); + equal(ss.type, attributes.type, "'type' attribute should be '" + attributes.type + "'"); + start(); + }); + }); + asyncTest( 'loadCSS sets media type before and after the stylesheet is loaded', function(){ expect(2); var ss = loadCSS("files/test.css"); @@ -89,36 +103,5 @@ } ); }); - test( 'loadCSS preload polyfill methods ', function(){ - expect(5); - - ok( window.loadCSS.relpreload, "loadCSS.relpreload should exist" ); - ok( typeof window.loadCSS.relpreload === "object", "relpreload should be an object" ); - ok( typeof window.loadCSS.relpreload.support === "function", "relpreload.support should be a function" ); - ok( typeof window.loadCSS.relpreload.poly === "function", "relpreload.poly should be a function" ); - ok( typeof window.loadCSS.relpreload.support() === "boolean", "relpreload.support should be a bool" ); - }); - - asyncTest( 'rel=preload stylesheet loads via polyfill', function(){ - expect(1); - var preloadElem = document.getElementById("preloadtest"); - var preloadHref = preloadElem.getAttribute("href"); - function loaded(){ - return document.querySelector( 'link[href="'+ preloadHref +'"][rel="stylesheet"]' ) || document.querySelector( 'link[href="'+ preloadElem.href +'"][rel="stylesheet"]' ); - } - - window.setTimeout(function(){ - if( window.loadCSS.relpreload.support() ){ - ok( loaded(), "stylesheet is in dom and applied without a polyfill" ); - } - else { - ok( loaded(), "stylesheet is in dom and applied with a polyfill" ); - } - - start(); - },3000); - - }); - }(window)); diff --git a/test/recommended.html b/test/recommended.html new file mode 100644 index 0000000..974d25b --- /dev/null +++ b/test/recommended.html @@ -0,0 +1,11 @@ + + + + Recommended Pattern Test + + + + +

This is a test file to demonstrate the recommended pattern for loading a stylesheet asynchronously.

+ + diff --git a/test/slow.css b/test/slow.css new file mode 100644 index 0000000..19045c2 --- /dev/null +++ b/test/slow.css @@ -0,0 +1,3 @@ +/* This file was delivered after a purposeful 5 second delay to demonstrate latency. */ +body { background: green; color: #fff; } +a { color: #fff;} \ No newline at end of file diff --git a/test/test-onload.html b/test/test-onload.html index c91d5cf..5e13ac8 100644 --- a/test/test-onload.html +++ b/test/test-onload.html @@ -4,108 +4,12 @@ Blocking Test diff --git a/test/test.html b/test/test.html index 993163d..bbe3652 100644 --- a/test/test.html +++ b/test/test.html @@ -9,94 +9,8 @@ }