├── .gitignore ├── README.md ├── app ├── event.js ├── hoverinspect.js ├── icon.png ├── icon128.png ├── icon48.png ├── icon_active.png ├── manifest.json ├── prism.js └── template.html ├── demo.gif └── release ├── hover-inspect.crx └── hover-inspect.crx.zip /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | *.sublime-project 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hover Element Inspector 2 | ============= 3 | 4 | Chrome extension for blazing fast layout inspection. View page elements code with associated attributes and dimensions by mouseover. Get visual overview of box model. 5 | 6 | It's useful for quick page overview, inspections and simple debugging.
Inspired by Firefox element inspector. 7 | 8 | It uses [Prism](http://prismjs.com/) for color highlighting. 9 | 10 | ## In action 11 | ![In action](https://github.com/NV0/hover-inspect/blob/master/demo.gif?raw=true) 12 | 13 | ## Installation 14 | Packed extension: 15 | [Packed version](https://github.com/NV0/hover-inspect/releases/download/v2.1/hover-inspect.crx.zip)
16 | You can also install it manually: [Manual installation instructions](http://lifehacker.com/5919997/how-to-install-extensions-that-arent-from-the-official-chrome-web-store) 17 | 18 | ## Tips 19 | 20 | For the best experience set a keyboard shortcut for this extension in the Extension Settings to quickly enable/disable inspection (Ctrl-Shift-Z for example). 21 | -------------------------------------------------------------------------------- /app/event.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Hover inspect extension for Chrome 3 | * https://github.com/NV0/hover-inspect 4 | */ 5 | 6 | (function() { 7 | 8 | var tabs = {}; 9 | 10 | 11 | var inspect = { 12 | activate: function(id) { 13 | 14 | this.id = id; 15 | 16 | chrome.tabs.executeScript(this.id, { 17 | file: 'prism.js' 18 | }); 19 | chrome.tabs.executeScript(this.id, { 20 | file: 'hoverinspect.js' 21 | }, function() { 22 | chrome.tabs.sendMessage(this.id, { 23 | action: 'activate' 24 | }); 25 | }.bind(this)); 26 | 27 | chrome.browserAction.setIcon({ 28 | tabId: this.id, 29 | path: { 30 | 19: "icon_active.png" 31 | } 32 | }); 33 | }, 34 | 35 | deactivate: function() { 36 | 37 | chrome.tabs.sendMessage(this.id, { 38 | action: 'deactivate' 39 | }); 40 | 41 | chrome.browserAction.setIcon({ 42 | tabId: this.id, 43 | path: { 44 | 19: "icon.png" 45 | } 46 | }); 47 | } 48 | 49 | }; 50 | 51 | function toggle(tab) { 52 | 53 | if (!tabs[tab.id]) { 54 | tabs[tab.id] = Object.create(inspect); 55 | tabs[tab.id].activate(tab.id); 56 | } else { 57 | tabs[tab.id].deactivate(); 58 | for (var tabId in tabs) { 59 | if (tabId == tab.id) delete tabs[tabId]; 60 | } 61 | } 62 | } 63 | 64 | chrome.browserAction.onClicked.addListener(toggle); 65 | 66 | })(); 67 | -------------------------------------------------------------------------------- /app/hoverinspect.js: -------------------------------------------------------------------------------- 1 | var injected = injected || (function() { 2 | 3 | // Inspector constructor 4 | 5 | var Inspector = function() { 6 | this.highlight = this.highlight.bind(this); 7 | this.log = this.log.bind(this); 8 | this.codeOutput = this.codeOutput.bind(this); 9 | this.layout = this.layout.bind(this); 10 | this.handleResize = this.handleResize.bind(this); 11 | 12 | this.$target = document.body; 13 | this.$cacheEl = document.body; 14 | this.$cacheElMain = document.body; 15 | 16 | this.serializer = new XMLSerializer(); 17 | this.forbidden = [this.$cacheEl, document.body, document.documentElement]; 18 | }; 19 | 20 | Inspector.prototype = { 21 | 22 | getNodes: function() { 23 | var path = chrome.extension.getURL("template.html"); 24 | 25 | var xmlhttp = new XMLHttpRequest(); 26 | 27 | xmlhttp.onreadystatechange = function() { 28 | if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { 29 | this.template = xmlhttp.responseText; 30 | this.createNodes(); 31 | this.registerEvents(); 32 | } 33 | }.bind(this); 34 | 35 | xmlhttp.open("GET", path, true); 36 | xmlhttp.send(); 37 | }, 38 | 39 | createNodes: function() { 40 | 41 | this.$host = document.createElement('div'); 42 | this.$host.className = 'tl-host'; 43 | this.$host.style.cssText = 'all: initial;'; 44 | 45 | 46 | var shadow = this.$host.createShadowRoot(); 47 | document.body.appendChild(this.$host); 48 | 49 | var templateMarkup = document.createElement("div"); 50 | templateMarkup.innerHTML = this.template; 51 | shadow.innerHTML = templateMarkup.querySelector('template').innerHTML; 52 | 53 | this.$wrap = shadow.querySelector('.tl-wrap'); 54 | this.$code = shadow.querySelector('.tl-code'); 55 | 56 | this.$canvas = shadow.querySelector('#tl-canvas'); 57 | this.c = this.$canvas.getContext('2d'); 58 | this.width = this.$canvas.width = window.innerWidth; 59 | this.height = this.$canvas.height = window.innerHeight; 60 | 61 | this.highlight(); 62 | }, 63 | 64 | registerEvents: function() { 65 | document.addEventListener('mousemove', this.log); 66 | document.addEventListener('scroll', this.layout); 67 | window.addEventListener('resize', function(){ 68 | this.handleResize(); 69 | this.layout(); 70 | }.bind(this)); 71 | }, 72 | 73 | log: function(e) { 74 | this.$target = e.target; 75 | 76 | // check if element cached 77 | if (this.forbidden.indexOf(this.$target) !== -1) return; 78 | 79 | this.stringified = this.serializer.serializeToString(this.$target); 80 | 81 | 82 | this.codeOutput(); 83 | 84 | this.$cacheEl = this.$target; 85 | this.layout(); 86 | 87 | }, 88 | 89 | codeOutput: function() { 90 | if (this.$cacheElMain === this.$target) return; 91 | this.$cacheElMain = this.$target; 92 | 93 | var fullCode = this.stringified 94 | .slice(0, this.stringified.indexOf('>') + 1) 95 | .replace(/ xmlns="[^"]*"/, ''); 96 | 97 | this.$code.innerText = fullCode; // set full element code 98 | this.highlight(); // highlight element 99 | 100 | 101 | }, 102 | 103 | // redraw overlay 104 | layout: function() { 105 | var box, computedStyle, rect; 106 | var c = this.c; 107 | 108 | rect = this.$target.getBoundingClientRect(); 109 | computedStyle = window.getComputedStyle(this.$target); 110 | box = { 111 | width: rect.width, 112 | height: rect.height, 113 | top: rect.top, 114 | left: rect.left, 115 | margin: { 116 | top: computedStyle.marginTop, 117 | right: computedStyle.marginRight, 118 | bottom: computedStyle.marginBottom, 119 | left: computedStyle.marginLeft 120 | }, 121 | padding: { 122 | top: computedStyle.paddingTop, 123 | right: computedStyle.paddingRight, 124 | bottom: computedStyle.paddingBottom, 125 | left: computedStyle.paddingLeft 126 | } 127 | }; 128 | 129 | // pluck negatives 130 | ['margin', 'padding'].forEach(function(property) { 131 | for (var el in box[property]) { 132 | var val = parseInt(box[property][el], 10); 133 | box[property][el] = Math.max(0, val); 134 | } 135 | }); 136 | 137 | c.clearRect(0, 0, this.width, this.height); 138 | 139 | box.left = Math.floor(box.left) + 1.5; 140 | box.width = Math.floor(box.width) - 1; 141 | 142 | var x, y, width, height; 143 | 144 | // margin 145 | x = box.left - box.margin.left; 146 | y = box.top - box.margin.top; 147 | width = box.width + box.margin.left + box.margin.right; 148 | height = box.height + box.margin.top + box.margin.bottom; 149 | 150 | c.fillStyle = 'rgba(255,165,0,0.5)'; 151 | c.fillRect(x, y, width, height); 152 | 153 | // padding 154 | x = box.left; 155 | y = box.top; 156 | width = box.width; 157 | height = box.height; 158 | 159 | c.fillStyle = 'rgba(158,113,221,0.5)'; 160 | c.clearRect(x, y, width, height); 161 | c.fillRect(x, y, width, height); 162 | 163 | // content 164 | x = box.left + box.padding.left; 165 | y = box.top + box.padding.top; 166 | width = box.width - box.padding.right - box.padding.left; 167 | height = box.height - box.padding.bottom - box.padding.top; 168 | 169 | c.fillStyle = 'rgba(73,187,231,0.25)'; 170 | c.clearRect(x, y, width, height); 171 | c.fillRect(x, y, width, height); 172 | 173 | // rulers (horizontal - =) 174 | x = -10; 175 | y = Math.floor(box.top) + 0.5; 176 | width = this.width + 10; 177 | height = box.height - 1; 178 | 179 | c.beginPath(); 180 | c.setLineDash([10,3]); 181 | c.fillStyle = 'rgba(0,0,0,0.02)'; 182 | c.strokeStyle = 'rgba(13, 139, 201, 0.45)'; 183 | c.lineWidth = 1; 184 | c.rect(x, y, width, height); 185 | c.stroke(); 186 | c.fill(); 187 | 188 | // rulers (vertical - ||) 189 | x = box.left; 190 | y = -10; 191 | width = box.width; 192 | height = this.height + 10; 193 | 194 | c.beginPath(); 195 | c.setLineDash([10,3]); 196 | c.fillStyle = 'rgba(0,0,0,0.02)'; 197 | c.strokeStyle = 'rgba(13, 139, 201, 0.45)'; 198 | c.lineWidth = 1; 199 | c.rect(x, y, width, height); 200 | c.stroke(); 201 | c.fill(); 202 | }, 203 | 204 | handleResize: function() { 205 | this.width = this.$canvas.width = window.innerWidth; 206 | this.height = this.$canvas.height = window.innerHeight; 207 | }, 208 | 209 | // code highlighting 210 | highlight: function() { 211 | Prism.highlightElement(this.$code); 212 | }, 213 | 214 | activate: function() { 215 | this.getNodes(); 216 | }, 217 | 218 | deactivate: function() { 219 | this.$wrap.classList.add('-out'); 220 | document.removeEventListener('mousemove', this.log); 221 | setTimeout(function() { 222 | document.body.removeChild(this.$host); 223 | }.bind(this), 600); 224 | } 225 | }; 226 | 227 | var hi = new Inspector(); 228 | 229 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { 230 | if (request.action === 'activate') { 231 | return hi.activate(); 232 | } else { 233 | return hi.deactivate(); 234 | } 235 | }); 236 | 237 | return true; 238 | })(); 239 | -------------------------------------------------------------------------------- /app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyashubin/hover-inspect/7350e067db43dce49cdad4f6437d9f42abc42f05/app/icon.png -------------------------------------------------------------------------------- /app/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyashubin/hover-inspect/7350e067db43dce49cdad4f6437d9f42abc42f05/app/icon128.png -------------------------------------------------------------------------------- /app/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyashubin/hover-inspect/7350e067db43dce49cdad4f6437d9f42abc42f05/app/icon48.png -------------------------------------------------------------------------------- /app/icon_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyashubin/hover-inspect/7350e067db43dce49cdad4f6437d9f42abc42f05/app/icon_active.png -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "Hover Inspect", 5 | "version": "2.1.0", 6 | "description": "Blazing fast page layout inspection", 7 | "author": "Shubin Ilya", 8 | 9 | "browser_action": { 10 | "default_icon": "icon.png" 11 | }, 12 | 13 | "icons": { 14 | "16": "icon.png", 15 | "48": "icon48.png", 16 | "128": "icon128.png" 17 | }, 18 | 19 | "background": { 20 | "scripts": ["event.js"] 21 | }, 22 | 23 | "permissions": ["activeTab"], 24 | "web_accessible_resources": ["template.html"] 25 | } 26 | -------------------------------------------------------------------------------- /app/prism.js: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css */ 2 | self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{};var Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{encode:function(e){return e instanceof n?new n(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(d instanceof a)){c.lastIndex=0;var m=c.exec(d);if(m){u&&(f=m[1].length);var y=m.index-1+f,m=m[0].slice(f),v=m.length,k=y+v,b=d.slice(0,y+1),w=d.slice(k+1),N=[p,1];b&&N.push(b);var O=new a(l,g?t.tokenize(m,g):m,h);N.push(O),w&&N.push(w),Array.prototype.splice.apply(r,N)}}}}}return r},hooks:{all:{},add:function(e,n){var a=t.hooks.all;a[e]=a[e]||[],a[e].push(n)},run:function(e,n){var a=t.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(n)}}},n=t.Token=function(e,t,n){this.type=e,this.content=t,this.alias=n};if(n.stringify=function(e,a,r){if("string"==typeof e)return e;if("[object Array]"==Object.prototype.toString.call(e))return e.map(function(t){return n.stringify(t,a,e)}).join("");var i={type:e.type,content:n.stringify(e.content,a,r),tag:"span",classes:["token",e.type],attributes:{},language:a,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}t.hooks.run("wrap",i);var o="";for(var s in i.attributes)o+=s+'="'+(i.attributes[s]||"")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'" '+o+">"+i.content+""},!self.document)return self.addEventListener?(self.addEventListener("message",function(e){var n=JSON.parse(e.data),a=n.language,r=n.code;self.postMessage(JSON.stringify(t.util.encode(t.tokenize(r,t.languages[a])))),self.close()},!1),self.Prism):self.Prism;var a=document.getElementsByTagName("script");return a=a[a.length-1],a&&(t.filename=a.src,document.addEventListener&&!a.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism);; 3 | Prism.languages.markup={comment://g,prolog:/<\?.+?\?>/,doctype://,cdata://i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|[^\s'">=]+))?\s*)*\/?>/gi,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,inside:{punctuation:/=|>|"/g}},punctuation:/\/?>/g,"attr-name":{pattern:/[\w:-]+/g,inside:{namespace:/^[\w-]+?:/}}}},entity:/\&#?[\da-z]{1,8};/gi},Prism.hooks.add("wrap",function(t){"entity"===t.type&&(t.attributes.title=t.content.replace(/&/,"&"))});; 4 | Prism.languages.css={comment:/\/\*[\w\W]*?\*\//g,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*{))/gi,inside:{punctuation:/[;:]/g}},url:/url\((["']?).*?\1\)/gi,selector:/[^\{\}\s][^\{\};]*(?=\s*\{)/g,property:/(\b|\B)[\w-]+(?=\s*:)/gi,string:/("|')(\\?.)*?\1/g,important:/\B!important\b/gi,punctuation:/[\{\};:]/g,"function":/[-a-z0-9]+(?=\()/gi},Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{style:{pattern:/[\w\W]*?<\/style>/gi,inside:{tag:{pattern:/|<\/style>/gi,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.css}}});; 5 | -------------------------------------------------------------------------------- /app/template.html: -------------------------------------------------------------------------------- 1 | 167 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyashubin/hover-inspect/7350e067db43dce49cdad4f6437d9f42abc42f05/demo.gif -------------------------------------------------------------------------------- /release/hover-inspect.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyashubin/hover-inspect/7350e067db43dce49cdad4f6437d9f42abc42f05/release/hover-inspect.crx -------------------------------------------------------------------------------- /release/hover-inspect.crx.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyashubin/hover-inspect/7350e067db43dce49cdad4f6437d9f42abc42f05/release/hover-inspect.crx.zip --------------------------------------------------------------------------------