summaryrefslogtreecommitdiff
path: root/assets/js/live.js
blob: a015d05f73be7e93fa907ad4dfbd6e91430b55b2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/*
  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.");
})();