| /* |
| 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."); |
| })(); |