Details | Last modification | View Log | RSS feed
| Rev | Author | Line No. | Line |
|---|---|---|---|
| 1 | espaco | 1 | /* |
| 2 | * Live CSS will monitor <link> 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 <link> 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 <link> 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 <link> to fully |
||
| 75 | * load, because simply changing the href on an existing <link> 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 <link> 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 <link> 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(); }); |