live.js 8.9 KB

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