Skip to content

Instantly share code, notes, and snippets.

@ewistrand
Created November 25, 2015 21:39
Show Gist options
  • Select an option

  • Save ewistrand/e82a5760aa6363cfeb2c to your computer and use it in GitHub Desktop.

Select an option

Save ewistrand/e82a5760aa6363cfeb2c to your computer and use it in GitHub Desktop.

Revisions

  1. ewistrand created this gist Nov 25, 2015.
    233 changes: 233 additions & 0 deletions live.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,233 @@
    /*
    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.");
    })();