/* Live.js - One script closer to Designing in the Browser Written for Handcraft.com by Martin Kool (@mrtnkl). Version 4. Recent change: Made stylesheet and mimetype checks case insensitive. http://livejs.com http://livejs.com/license (MIT) @livejs Include live.js#css to monitor css changes only. Include live.js#js to monitor js changes only. Include live.js#html to monitor html changes only. Mix and match to monitor a preferred combination such as live.js#html,css By default, just include live.js to monitor all css, js and html changes. Live.js can also be loaded as a bookmarklet. It is best to only use it for CSS then, as a page reload due to a change in html or css would not re-include the bookmarklet. To monitor CSS and be notified that it has loaded, include it as: live.js#css,notify */ (function () { var headers = { //"Etag": 1, "Last-Modified": 1, "Content-Length": 1, "Content-Type": 1, }, resources = {}, pendingRequests = {}, currentLinkElements = {}, oldLinkElements = {}, interval = 1000, loaded = false, active = { html: 1, css: 1, js: 1, }; var Live = { // performs a cycle per interval heartbeat: function () { if (document.body) { // make sure all resources are loaded on first activation if (!loaded) Live.loadresources(); Live.checkForChanges(); } setTimeout(Live.heartbeat, interval); }, // loads all local css and js resources upon first activation loadresources: function () { // helper method to assert if a given url is local function isLocal(url) { var loc = document.location, reg = new RegExp( "^\\.|^/(?!/)|^[\\w]((?!://).)*$|" + loc.protocol + "//" + loc.host, ); return url.match(reg); } // gather all resources var scripts = document.getElementsByTagName("script"), links = document.getElementsByTagName("link"), uris = []; // track local js urls for (var i = 0; i < scripts.length; i++) { var script = scripts[i], src = script.getAttribute("src"); if (src && isLocal(src)) uris.push(src); if (src && src.match(/\blive.js#/)) { for (var type in active) active[type] = src.match("[#,|]" + type) != null; if (src.match("notify")) alert("Live.js is loaded."); } } if (!active.js) uris = []; if (active.html) uris.push(document.location.href); // track local css urls for (var i = 0; i < links.length && active.css; i++) { var link = links[i], rel = link.getAttribute("rel"), href = link.getAttribute("href", 2); if ( href && rel && rel.match(new RegExp("stylesheet", "i")) && isLocal(href) ) { uris.push(href); currentLinkElements[href] = link; } } // initialize the resources info for (var i = 0; i < uris.length; i++) { var url = uris[i]; Live.getHead(url, function (url, info) { resources[url] = info; }); } // add rule for morphing between old and new css files var head = document.getElementsByTagName("head")[0], style = document.createElement("style"), rule = "transition: all .3s ease-out;"; css = [ ".livejs-loading * { ", rule, " -webkit-", rule, "-moz-", rule, "-o-", rule, "}", ].join(""); style.setAttribute("type", "text/css"); head.appendChild(style); style.styleSheet ? (style.styleSheet.cssText = css) : style.appendChild(document.createTextNode(css)); // yep loaded = true; }, // check all tracking resources for changes checkForChanges: function () { for (var url in resources) { if (pendingRequests[url]) continue; Live.getHead(url, function (url, newInfo) { var oldInfo = resources[url], hasChanged = false; resources[url] = newInfo; for (var header in oldInfo) { // do verification based on the header type var oldValue = oldInfo[header], newValue = newInfo[header], contentType = newInfo["Content-Type"]; switch (header.toLowerCase()) { case "etag": if (!newValue) break; // fall through to default default: hasChanged = oldValue != newValue; break; } // if changed, act if (hasChanged) { Live.refreshResource(url, contentType); break; } } }); } }, // act upon a changed url of certain content type refreshResource: function (url, type) { switch (type.toLowerCase()) { // css files can be reloaded dynamically // by replacing the link element case "text/css": var link = currentLinkElements[url], html = document.body.parentNode, head = link.parentNode, next = link.nextSibling, newLink = document.createElement("link"); html.className = html.className.replace(/\s*livejs\-loading/gi, "") + " livejs-loading"; newLink.setAttribute("type", "text/css"); newLink.setAttribute("rel", "stylesheet"); newLink.setAttribute("href", url + "?now=" + new Date() * 1); next ? head.insertBefore(newLink, next) : head.appendChild(newLink); currentLinkElements[url] = newLink; oldLinkElements[url] = link; // schedule removal of the old link Live.removeoldLinkElements(); break; // check if an html resource is our current url, then reload case "text/html": if (url != document.location.href) return; // local javascript changes cause a reload as well case "text/javascript": case "application/javascript": case "application/x-javascript": document.location.reload(); } }, // removes the old stylesheet rules only once the new one has finished // loading removeoldLinkElements: function () { var pending = 0; for (var url in oldLinkElements) { // if this sheet has any cssRules, delete the old link try { var link = currentLinkElements[url], oldLink = oldLinkElements[url], html = document.body.parentNode, sheet = link.sheet || link.styleSheet, rules = sheet.rules || sheet.cssRules; if (rules.length >= 0) { oldLink.parentNode.removeChild(oldLink); delete oldLinkElements[url]; setTimeout(function () { html.className = html.className.replace( /\s*livejs\-loading/gi, "", ); }, 100); } } catch (e) { pending++; } if (pending) setTimeout(Live.removeoldLinkElements, 50); } }, // performs a HEAD request and passes the header info to the given // callback getHead: function (url, callback) { pendingRequests[url] = true; var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XmlHttp"); xhr.open("HEAD", url, true); xhr.onreadystatechange = function () { delete pendingRequests[url]; if (xhr.readyState == 4 && xhr.status != 304) { xhr.getAllResponseHeaders(); var info = {}; for (var h in headers) { var value = xhr.getResponseHeader(h); // adjust the simple Etag variant to match on its significant part if (h.toLowerCase() == "etag" && value) value = value.replace(/^W\//, ""); if (h.toLowerCase() == "content-type" && value) value = value.replace(/^(.*?);.*?$/i, "$1"); info[h] = value; } callback(url, info); } }; xhr.send(); }, }; // start listening if (document.location.protocol != "file:") { if (!window.liveJsLoaded) Live.heartbeat(); window.liveJsLoaded = true; } else if (window.console) console.log( "Live.js doesn't support the file protocol. \ It needs http.", ); })();