├── README.markdown └── livecss.js /README.markdown: -------------------------------------------------------------------------------- 1 | Live CSS - Making the browser dance to your CSS 2 | =============================================== 3 | Live CSS will monitor <link> tags on the page and poll the server for changes to the CSS. When there are changes, the page's styles get updated. 4 | 5 | It's a simple tool, and it's immensely powerful because it enables the One True Workflow we've all lusted after: to have your browser and your editor side by side on screen and watch the page update in real time as you type in your CSS files. It looks like magic. 6 | 7 | Usage 8 | ----- 9 | Just include livecss.js in your page and then call this function: 10 | 11 | livecss.watchAll() - starts polling all tags in the current page for changes. 12 | 13 | If you want more fine grained control over which CSS is being autoreloaded: 14 | 15 | livecss.watch(linkElement) - start watching a single element for changes. 16 | livecss.unwatchAll() 17 | livecss.unwatch(linkElement) 18 | 19 | For convenience, livecss will call watchAll() right away if the page has "startlivecss=true" in the URL's query string. 20 | 21 | Tips 22 | ---- 23 | Make sure your server is setting a valid **last-modified** header for its CSS responses. Livecss detects new CSS by frequently making a HEAD request to the URLs referenced in the <link> tags on the page, and it reloads the files which have a recent last-modified header. If your last-modified header is blank or always set to "now", the CSS will continuously reload, once per second. This will put extra load on the browser. 24 | 25 | Consider adding a development querystring parameter to your page and initiate livecss only when that querystring parameter is present. Otherwise, the polling from livecss can generate a lot of server log noise, which is annoying when you're hacking on backend code. 26 | 27 | Bookmarklet 28 | ----------- 29 | You can paste this code snippet into your URL bar (or create a bookmark out of it) to start livecss on any page you're viewing. 30 | 31 | javascript:(function(){ 32 | var s=document.createElement('script'); 33 | s.type='text/javascript'; 34 | s.src='https://github.com/ooyala/livecss/raw/master/livecss.js'; 35 | s.addEventListener('load', function() { livecss.watchAll(); }, false); 36 | document.getElementsByTagName('head')[0].appendChild(s); 37 | })() 38 | 39 | Contributing 40 | ------------ 41 | Feel free create tickets for enhancement ideas, or just fork and submit a pull request. 42 | 43 | License 44 | ------- 45 | Licensed under the [MIT license](http://www.opensource.org/licenses/mit-license.php). 46 | 47 | Credits 48 | ------- 49 | Phil Crosby (twitter @philcrosby) -------------------------------------------------------------------------------- /livecss.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Live CSS will monitor tags on the page and poll the server for changes to the CSS. This enables you 3 | * to refresh styles without disrupting the state of the view, and the page updates itself without you 4 | * having to switch from your editor to the browser and hit refresh. 5 | * 6 | * Usage: 7 | * livecss.watchAll() - starts polling all tags in the current page for changes. 8 | * 9 | * If you want more fine grained control over which CSS is being autoreloaded: 10 | * livecss.watch(linkElement) - start watching a single element for changes. 11 | * livecss.unwatchAll() 12 | * livecss.unwatch(linkElement) 13 | * 14 | * For convenience, livecss will call watchAll() right away if the page has "startlivecss=true" in the URL's 15 | * query string. 16 | */ 17 | var livecss = { 18 | // How often to poll for changes to the CSS. 19 | pollFrequency: 1000, 20 | outstandingRequests: {}, // stylesheet url => boolean 21 | filesLastModified: {}, // stylesheet url => last modified timestamp 22 | watchTimers: {}, // stylesheet url => timer ID 23 | 24 | /* 25 | * Begins polling all link elements on the current page for changes. 26 | */ 27 | watchAll: function() { 28 | this.unwatchAll(); 29 | var timerId = setInterval(this.proxy(function() { 30 | var linkElements = document.getElementsByTagName("link"); 31 | var validMediaTypes = ["screen", "handheld", "all", ""]; 32 | for (var i = 0; i < linkElements.length; i++) { 33 | var media = (linkElements[i].getAttribute("media") || "").toLowerCase(); 34 | if (linkElements[i].getAttribute("rel") == "stylesheet" 35 | && livecss.indexOf(validMediaTypes, media) >= 0 36 | && this.isLocalLink(linkElements[i])) { 37 | this.refreshLinkElement(linkElements[i]); 38 | } 39 | } 40 | }), this.pollFrequency); 41 | this.watchTimers["all"] = timerId; 42 | }, 43 | 44 | watch: function(linkElement) { 45 | var url = linkElement.getAttribute("href"); 46 | this.unwatch(url); 47 | this.watchTimers[url] = setInterval(this.proxy(function() { 48 | var linkElement = this.linkElementWithHref(url); 49 | this.refreshLinkElement(linkElement); 50 | }), this.pollFrequency); 51 | }, 52 | 53 | unwatchAll: function() { 54 | for (var url in this.watchTimers) 55 | this.unwatch(url); 56 | }, 57 | 58 | unwatch: function(url) { 59 | if (this.watchTimers[url] != null) { 60 | clearInterval(this.watchTimers[url]); 61 | delete this.watchTimers[url]; 62 | delete this.outstandingRequests[url]; 63 | } 64 | }, 65 | 66 | linkElementWithHref: function(url) { 67 | var linkElements = document.getElementsByTagName("link"); 68 | for (var i = 0; i < linkElements.length; i++) 69 | if (linkElements[i].href == url) 70 | return linkElements[i] 71 | }, 72 | 73 | /* 74 | * Replaces a link element with a new one for the given URL. This has to wait for the new to fully 75 | * load, because simply changing the href on an existing causes the page to flicker. 76 | */ 77 | replaceLinkElement: function(linkElement, stylesheetUrl) { 78 | var parent = linkElement.parentNode; 79 | var sibling = linkElement.nextSibling; 80 | var url = this.addCacheBust(linkElement.href); 81 | 82 | var newLinkElement = document.createElement("link"); 83 | newLinkElement.href = url; 84 | newLinkElement.setAttribute("rel", "stylesheet"); 85 | 86 | if (sibling) 87 | parent.insertBefore(newLinkElement, sibling); 88 | else 89 | parent.appendChild(newLinkElement); 90 | 91 | // We're polling to check whether the CSS is loaded, because firefox doesn't support an onload event 92 | // for elements. 93 | var loadingTimer = setInterval(this.proxy(function() { 94 | if (!this.isCssElementLoaded(newLinkElement)) return; 95 | if (typeof(console) != "undefined") 96 | console.log("CSS refreshed:", this.removeCacheBust(url)); 97 | clearInterval(loadingTimer); 98 | delete this.outstandingRequests[this.removeCacheBust(url)]; 99 | parent.removeChild(linkElement); 100 | }), 100); 101 | }, 102 | 103 | /* 104 | * Refreshes the provided linkElement if it's changed. We issue a HEAD request for the CSS. If its 105 | * last-modified header is changed, we remove and re-add the element to the DOM which trigger a 106 | * re-render from the browser. This uses a cache-bust querystring parameter to ensure we always bust through 107 | * the browser's cache. 108 | */ 109 | refreshLinkElement: function(linkElement) { 110 | var url = this.removeCacheBust(linkElement.getAttribute("href")); 111 | if (this.outstandingRequests[url]) return; 112 | var request = new XMLHttpRequest(); 113 | this.outstandingRequests[url] = request; 114 | var cacheBustUrl = this.addCacheBust(url); 115 | 116 | request.onreadystatechange = this.proxy(function(event) { 117 | if (request.readyState != 4) return; 118 | delete this.outstandingRequests[url]; 119 | if (request.status != 200 && request.status != 304) return; 120 | var lastModified = Date.parse(request.getResponseHeader("Last-Modified")); 121 | if (!this.filesLastModified[url] || this.filesLastModified[url] < lastModified) { 122 | this.filesLastModified[url] = lastModified; 123 | this.replaceLinkElement(linkElement, cacheBustUrl); 124 | } 125 | }); 126 | request.open("HEAD", cacheBustUrl); 127 | request.send(null); 128 | }, 129 | 130 | isCssElementLoaded: function(cssElement) { 131 | // cssElement.sheet.cssRules will throw an error in firefox when the css file is not yet loaded. 132 | try { return (cssElement.sheet && cssElement.sheet.cssRules.length > 0); } catch(error) { } 133 | return false; 134 | }, 135 | 136 | /* returns true for local urls such as: '/screen.css', 'http://mydomain.com/screen.css', 'css/screen.css' 137 | */ 138 | isLocalLink: function(linkElement) { 139 | //On all tested browsers, this javascript property returns a normalized URL 140 | var url = linkElement.href; 141 | var regexp = new RegExp("^\/|^" + 142 | document.location.protocol + "//" + document.location.host); 143 | return (url.search(regexp) == 0); 144 | }, 145 | 146 | /* 147 | * Adds and removes a "cache_bust" querystring parameter to the given URLs. This is so we always bust 148 | * through the browser's cache when checking for updated CSS. 149 | */ 150 | addCacheBust: function(url) { return this.removeCacheBust(url) + "?cache_bust=" + (new Date()).getTime(); }, 151 | removeCacheBust: function(url) { return url.replace(/\?cache_bust=[^&]+/, ""); }, 152 | 153 | /* A utility method to bind the value of "this". Equivalent to jQuery's proxy() function. */ 154 | proxy: function(fn) { 155 | var self = this; 156 | return function() { return fn.apply(self, []); }; 157 | }, 158 | 159 | /* Unfortunately IE7 doesn't have this built-in. */ 160 | indexOf: function(array, item) { 161 | for (var i = 0; i < array.length; i++) { if (array[i] == item) return i; } 162 | return -1; 163 | }, 164 | 165 | /* A utility function for abstracting the difference between event listening in IE and other browsers. */ 166 | addEventListener: function(object, event, fn) { 167 | object.attachEvent ? object.attachEvent("on" + event, fn) : object.addEventListener(event, fn, false); 168 | } 169 | }; 170 | 171 | if (window.location.search.toString().indexOf("startlivecss=true") >= 0) 172 | livecss.addEventListener(window, "load", function() { livecss.watchAll(); }); --------------------------------------------------------------------------------