1 | 1 |
new file mode 100755 |
... | ... |
@@ -0,0 +1,233 @@ |
1 |
+/* |
|
2 |
+ Live.js - One script closer to Designing in the Browser |
|
3 |
+ Written for Handcraft.com by Martin Kool (@mrtnkl). |
|
4 |
+ |
|
5 |
+ Version 4. |
|
6 |
+ Recent change: Made stylesheet and mimetype checks case insensitive. |
|
7 |
+ |
|
8 |
+ http://livejs.com |
|
9 |
+ http://livejs.com/license (MIT) |
|
10 |
+ @livejs |
|
11 |
+ |
|
12 |
+ Include live.js#css to monitor css changes only. |
|
13 |
+ Include live.js#js to monitor js changes only. |
|
14 |
+ Include live.js#html to monitor html changes only. |
|
15 |
+ Mix and match to monitor a preferred combination such as live.js#html,css |
|
16 |
+ |
|
17 |
+ By default, just include live.js to monitor all css, js and html changes. |
|
18 |
+ |
|
19 |
+ Live.js can also be loaded as a bookmarklet. It is best to only use it for CSS then, |
|
20 |
+ as a page reload due to a change in html or css would not re-include the bookmarklet. |
|
21 |
+ To monitor CSS and be notified that it has loaded, include it as: live.js#css,notify |
|
22 |
+*/ |
|
23 |
+(function () { |
|
24 |
+ |
|
25 |
+ var headers = { "Etag": 1, "Last-Modified": 1, "Content-Length": 1, "Content-Type": 1 }, |
|
26 |
+ resources = {}, |
|
27 |
+ pendingRequests = {}, |
|
28 |
+ currentLinkElements = {}, |
|
29 |
+ oldLinkElements = {}, |
|
30 |
+ interval = 1000, |
|
31 |
+ loaded = false, |
|
32 |
+ active = { "html": 1, "css": 1, "js": 1 }; |
|
33 |
+ |
|
34 |
+ var Live = { |
|
35 |
+ |
|
36 |
+ // performs a cycle per interval |
|
37 |
+ heartbeat: function () { |
|
38 |
+ if (document.body) { |
|
39 |
+ // make sure all resources are loaded on first activation |
|
40 |
+ if (!loaded) Live.loadresources(); |
|
41 |
+ Live.checkForChanges(); |
|
42 |
+ } |
|
43 |
+ setTimeout(Live.heartbeat, interval); |
|
44 |
+ }, |
|
45 |
+ |
|
46 |
+ // loads all local css and js resources upon first activation |
|
47 |
+ loadresources: function () { |
|
48 |
+ |
|
49 |
+ // helper method to assert if a given url is local |
|
50 |
+ function isLocal(url) { |
|
51 |
+ var loc = document.location, |
|
52 |
+ reg = new RegExp("^\\.|^\/(?!\/)|^[\\w]((?!://).)*$|" + loc.protocol + "//" + loc.host); |
|
53 |
+ return url.match(reg); |
|
54 |
+ } |
|
55 |
+ |
|
56 |
+ // gather all resources |
|
57 |
+ var scripts = document.getElementsByTagName("script"), |
|
58 |
+ links = document.getElementsByTagName("link"), |
|
59 |
+ uris = []; |
|
60 |
+ |
|
61 |
+ // track local js urls |
|
62 |
+ for (var i = 0; i < scripts.length; i++) { |
|
63 |
+ var script = scripts[i], src = script.getAttribute("src"); |
|
64 |
+ if (src && isLocal(src)) |
|
65 |
+ uris.push(src); |
|
66 |
+ if (src && src.match(/\blive.js#/)) { |
|
67 |
+ for (var type in active) |
|
68 |
+ active[type] = src.match("[#,|]" + type) != null |
|
69 |
+ if (src.match("notify")) |
|
70 |
+ alert("Live.js is loaded."); |
|
71 |
+ } |
|
72 |
+ } |
|
73 |
+ if (!active.js) uris = []; |
|
74 |
+ if (active.html) uris.push(document.location.href); |
|
75 |
+ |
|
76 |
+ // track local css urls |
|
77 |
+ for (var i = 0; i < links.length && active.css; i++) { |
|
78 |
+ var link = links[i], rel = link.getAttribute("rel"), href = link.getAttribute("href", 2); |
|
79 |
+ if (href && rel && rel.match(new RegExp("stylesheet", "i")) && isLocal(href)) { |
|
80 |
+ uris.push(href); |
|
81 |
+ currentLinkElements[href] = link; |
|
82 |
+ } |
|
83 |
+ } |
|
84 |
+ |
|
85 |
+ // initialize the resources info |
|
86 |
+ for (var i = 0; i < uris.length; i++) { |
|
87 |
+ var url = uris[i]; |
|
88 |
+ Live.getHead(url, function (url, info) { |
|
89 |
+ resources[url] = info; |
|
90 |
+ }); |
|
91 |
+ } |
|
92 |
+ |
|
93 |
+ // add rule for morphing between old and new css files |
|
94 |
+ var head = document.getElementsByTagName("head")[0], |
|
95 |
+ style = document.createElement("style"), |
|
96 |
+ rule = "transition: all .3s ease-out;" |
|
97 |
+ css = [".livejs-loading * { ", rule, " -webkit-", rule, "-moz-", rule, "-o-", rule, "}"].join(''); |
|
98 |
+ style.setAttribute("type", "text/css"); |
|
99 |
+ head.appendChild(style); |
|
100 |
+ style.styleSheet ? style.styleSheet.cssText = css : style.appendChild(document.createTextNode(css)); |
|
101 |
+ |
|
102 |
+ // yep |
|
103 |
+ loaded = true; |
|
104 |
+ }, |
|
105 |
+ |
|
106 |
+ // check all tracking resources for changes |
|
107 |
+ checkForChanges: function () { |
|
108 |
+ for (var url in resources) { |
|
109 |
+ if (pendingRequests[url]) |
|
110 |
+ continue; |
|
111 |
+ |
|
112 |
+ Live.getHead(url, function (url, newInfo) { |
|
113 |
+ var oldInfo = resources[url], |
|
114 |
+ hasChanged = false; |
|
115 |
+ resources[url] = newInfo; |
|
116 |
+ for (var header in oldInfo) { |
|
117 |
+ // do verification based on the header type |
|
118 |
+ var oldValue = oldInfo[header], |
|
119 |
+ newValue = newInfo[header], |
|
120 |
+ contentType = newInfo["Content-Type"]; |
|
121 |
+ switch (header.toLowerCase()) { |
|
122 |
+ case "etag": |
|
123 |
+ if (!newValue) break; |
|
124 |
+ // fall through to default |
|
125 |
+ default: |
|
126 |
+ hasChanged = oldValue != newValue; |
|
127 |
+ break; |
|
128 |
+ } |
|
129 |
+ // if changed, act |
|
130 |
+ if (hasChanged) { |
|
131 |
+ Live.refreshResource(url, contentType); |
|
132 |
+ break; |
|
133 |
+ } |
|
134 |
+ } |
|
135 |
+ }); |
|
136 |
+ } |
|
137 |
+ }, |
|
138 |
+ |
|
139 |
+ // act upon a changed url of certain content type |
|
140 |
+ refreshResource: function (url, type) { |
|
141 |
+ switch (type.toLowerCase()) { |
|
142 |
+ // css files can be reloaded dynamically by replacing the link element |
|
143 |
+ case "text/css": |
|
144 |
+ var link = currentLinkElements[url], |
|
145 |
+ html = document.body.parentNode, |
|
146 |
+ head = link.parentNode, |
|
147 |
+ next = link.nextSibling, |
|
148 |
+ newLink = document.createElement("link"); |
|
149 |
+ |
|
150 |
+ html.className = html.className.replace(/\s*livejs\-loading/gi, '') + ' livejs-loading'; |
|
151 |
+ newLink.setAttribute("type", "text/css"); |
|
152 |
+ newLink.setAttribute("rel", "stylesheet"); |
|
153 |
+ newLink.setAttribute("href", url + "?now=" + new Date() * 1); |
|
154 |
+ next ? head.insertBefore(newLink, next) : head.appendChild(newLink); |
|
155 |
+ currentLinkElements[url] = newLink; |
|
156 |
+ oldLinkElements[url] = link; |
|
157 |
+ |
|
158 |
+ // schedule removal of the old link |
|
159 |
+ Live.removeoldLinkElements(); |
|
160 |
+ break; |
|
161 |
+ |
|
162 |
+ // check if an html resource is our current url, then reload |
|
163 |
+ case "text/html": |
|
164 |
+ if (url != document.location.href) |
|
165 |
+ return; |
|
166 |
+ |
|
167 |
+ // local javascript changes cause a reload as well |
|
168 |
+ case "text/javascript": |
|
169 |
+ case "application/javascript": |
|
170 |
+ case "application/x-javascript": |
|
171 |
+ document.location.reload(); |
|
172 |
+ } |
|
173 |
+ }, |
|
174 |
+ |
|
175 |
+ // removes the old stylesheet rules only once the new one has finished loading |
|
176 |
+ removeoldLinkElements: function () { |
|
177 |
+ var pending = 0; |
|
178 |
+ for (var url in oldLinkElements) { |
|
179 |
+ // if this sheet has any cssRules, delete the old link |
|
180 |
+ try { |
|
181 |
+ var link = currentLinkElements[url], |
|
182 |
+ oldLink = oldLinkElements[url], |
|
183 |
+ html = document.body.parentNode, |
|
184 |
+ sheet = link.sheet || link.styleSheet, |
|
185 |
+ rules = sheet.rules || sheet.cssRules; |
|
186 |
+ if (rules.length >= 0) { |
|
187 |
+ oldLink.parentNode.removeChild(oldLink); |
|
188 |
+ delete oldLinkElements[url]; |
|
189 |
+ setTimeout(function () { |
|
190 |
+ html.className = html.className.replace(/\s*livejs\-loading/gi, ''); |
|
191 |
+ }, 100); |
|
192 |
+ } |
|
193 |
+ } catch (e) { |
|
194 |
+ pending++; |
|
195 |
+ } |
|
196 |
+ if (pending) setTimeout(Live.removeoldLinkElements, 50); |
|
197 |
+ } |
|
198 |
+ }, |
|
199 |
+ |
|
200 |
+ // performs a HEAD request and passes the header info to the given callback |
|
201 |
+ getHead: function (url, callback) { |
|
202 |
+ pendingRequests[url] = true; |
|
203 |
+ var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XmlHttp"); |
|
204 |
+ xhr.open("HEAD", url, true); |
|
205 |
+ xhr.onreadystatechange = function () { |
|
206 |
+ delete pendingRequests[url]; |
|
207 |
+ if (xhr.readyState == 4 && xhr.status != 304) { |
|
208 |
+ xhr.getAllResponseHeaders(); |
|
209 |
+ var info = {}; |
|
210 |
+ for (var h in headers) { |
|
211 |
+ var value = xhr.getResponseHeader(h); |
|
212 |
+ // adjust the simple Etag variant to match on its significant part |
|
213 |
+ if (h.toLowerCase() == "etag" && value) value = value.replace(/^W\//, ''); |
|
214 |
+ if (h.toLowerCase() == "content-type" && value) value = value.replace(/^(.*?);.*?$/i, "$1"); |
|
215 |
+ info[h] = value; |
|
216 |
+ } |
|
217 |
+ callback(url, info); |
|
218 |
+ } |
|
219 |
+ } |
|
220 |
+ xhr.send(); |
|
221 |
+ } |
|
222 |
+ }; |
|
223 |
+ |
|
224 |
+ // start listening |
|
225 |
+ if (document.location.protocol != "file:") { |
|
226 |
+ if (!window.liveJsLoaded) |
|
227 |
+ Live.heartbeat(); |
|
228 |
+ |
|
229 |
+ window.liveJsLoaded = true; |
|
230 |
+ } |
|
231 |
+ else if (window.console) |
|
232 |
+ console.log("Live.js doesn't support the file protocol. It needs http."); |
|
233 |
+})(); |
|
0 | 234 |
\ No newline at end of file |