├── .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 | 
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+""+i.tag+">"},!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:/
158 |
159 |
<html>
164 |