├── README.md ├── package.js ├── .versions ├── lib.html ├── lib.less └── lib.js /README.md: -------------------------------------------------------------------------------- 1 | ```diff 2 | - NOTE: This package is not maintained anymore. 3 | - If you want to help, please reach out to gwendall.esnault@gmail.com 4 | ``` 5 | 6 | Meteor Template Inspector 7 | ======================= 8 | 9 | Inspector for Blaze templates. See in a snap data, instance variables, helpers and events for your templates. 10 | [Demo](https://template-inspector.meteor.com) 11 | 12 | Installation 13 | ------------ 14 | 15 | ``` sh 16 | meteor add gwendall:template-inspector 17 | ``` 18 | 19 | To do 20 | ------------ 21 | - Show proper file-tree for templates (showing nested tpls on click) 22 | - Show helpers values (right now, not possible since helpers relying on Template.instance() can't get called from outside the template itself) 23 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "gwendall:template-inspector", 3 | summary: "Inspector for Blaze templates", 4 | git: "https://github.com/gwendall/meteor-template-inspector.git", 5 | version: "0.1.9" 6 | }); 7 | 8 | Package.onUse(function (api, where) { 9 | 10 | api.use([ 11 | "less@1.0.13", 12 | "random@1.0.2", 13 | "mizzao:jquery-ui@1.11.2", 14 | "mongo@1.0.11", 15 | "templating@1.0.11", 16 | "underscore@1.0.2", 17 | "momentjs:moment@2.9.0", 18 | "aldeed:template-extension@3.4.3", 19 | "dburles:collection-helpers@1.0.2", 20 | "gwendall:body-events@0.1.6", 21 | "gwendall:template-states@0.1.0" 22 | ], "client"); 23 | 24 | api.addFiles([ 25 | "lib.html", 26 | "lib.less", 27 | "lib.js", 28 | ], "client"); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | aldeed:template-extension@3.4.3 2 | base64@1.0.3 3 | binary-heap@1.0.3 4 | blaze@2.1.0 5 | blaze-tools@1.0.3 6 | callback-hook@1.0.3 7 | check@1.0.5 8 | dburles:collection-helpers@1.0.2 9 | ddp@1.1.0 10 | deps@1.0.7 11 | ejson@1.0.6 12 | geojson-utils@1.0.3 13 | gwendall:body-events@0.1.6 14 | gwendall:template-inspector@0.1.8 15 | gwendall:template-states@0.1.0 16 | html-tools@1.0.4 17 | htmljs@1.0.4 18 | id-map@1.0.3 19 | jquery@1.11.3_2 20 | json@1.0.3 21 | less@1.0.13 22 | logging@1.0.7 23 | meteor@1.1.5 24 | minifiers@1.1.4 25 | minimongo@1.0.7 26 | mizzao:build-fetcher@0.2.0 27 | mizzao:jquery-ui@1.11.2 28 | momentjs:moment@2.9.0 29 | mongo@1.1.0 30 | observe-sequence@1.0.5 31 | ordered-dict@1.0.3 32 | random@1.0.3 33 | reactive-var@1.0.5 34 | retry@1.0.3 35 | spacebars-compiler@1.0.5 36 | templating@1.1.0 37 | tracker@1.0.6 38 | underscore@1.0.3 39 | -------------------------------------------------------------------------------- /lib.html: -------------------------------------------------------------------------------- 1 | 35 | 36 | 110 | 111 | 125 | 126 | 134 | 135 | 144 | 145 | 160 | -------------------------------------------------------------------------------- /lib.less: -------------------------------------------------------------------------------- 1 | body.template-inspector-on { 2 | [data-template] { 3 | border: 2px solid rgba(68, 108, 179, 1) !important; 4 | -webkit-transition: all 0.30s ease-in-out; 5 | -moz-transition: all 0.30s ease-in-out; 6 | -ms-transition: all 0.30s ease-in-out; 7 | -o-transition: all 0.30s ease-in-out; 8 | outline: none; 9 | } 10 | 11 | [data-template].ti-active { 12 | z-index: 200; 13 | border: 2px solid rgba(239, 72, 54, 1) !important; 14 | box-shadow: 0 0 10px rgba(239, 72, 54, 1) !important; 15 | } 16 | } 17 | 18 | .template-inspector, 19 | .template-inspector * { 20 | box-sizing: border-box; 21 | } 22 | 23 | .flex-display { 24 | display: -webkit-box; 25 | display: -moz-box; 26 | display: -ms-flexbox; 27 | display: -webkit-flex; 28 | display: flex; 29 | } 30 | 31 | .flex-direction(@direction) { 32 | -webkit-flex-direction: @direction; 33 | flex-direction: @direction; 34 | } 35 | 36 | .flex-size(@size) { 37 | -webkit-box-flex: @size; 38 | -moz-box-flex: @size; 39 | -webkit-flex: @size; 40 | -ms-flex: @size; 41 | flex: @size; 42 | } 43 | 44 | .template-inspector { 45 | position: fixed; 46 | left: 10px; 47 | top: 10px; 48 | width: 450px; 49 | height: 600px; 50 | z-index: 100000000000000000000000000; 51 | border: rgba(0, 0, 0, .9) solid 5px; 52 | border-radius: 5px; 53 | background: rgba(245, 245, 245, .95); 54 | color: rgba(0, 0, 0, .7); 55 | font-family: "jaf-bernino-sans","Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Verdana,sans-serif; 56 | font-weight: normal; 57 | font-style: normal; 58 | font-size: 13px; 59 | line-height: 1.7; 60 | letter-spacing: -0.02em; 61 | overflow: hidden; 62 | text-rendering: optimizeLegibility; 63 | -webkit-font-smoothing: antialiased; 64 | min-width: 230px; 65 | min-height: 30px; 66 | opacity: 0; 67 | .flex-display; 68 | .flex-direction(column); 69 | &.reduced { 70 | height: 38px !important; 71 | width: 100px !important; 72 | top: auto !important; 73 | left: auto !important; 74 | right: 10px !important; 75 | bottom: 0 !important; 76 | border-radius: 5px 5px 0 0; 77 | .ti-content { 78 | display: none; 79 | } 80 | } 81 | .ti-header { 82 | background: rgba(0, 0, 0, .9); 83 | border-bottom: rgba(0, 0, 0, .9) solid 5px; 84 | color: white; 85 | cursor: move; 86 | z-index: 10; 87 | line-height: 20px; 88 | .flex-size(0 0 34px); 89 | div { 90 | display: table; 91 | padding: 5px 15px; 92 | float: left; 93 | } 94 | .action { 95 | float: right; 96 | &:hover { 97 | background: rgba(0, 0, 0, .9); 98 | cursor: pointer; 99 | } 100 | } 101 | } 102 | .ti-nav { 103 | background: rgba(0, 0, 0, .6); 104 | border-bottom: rgba(0, 0, 0, .9) solid thin; 105 | color: white; 106 | z-index: 10; 107 | .flex-size(0 0 34px); 108 | .ti-nav-item { 109 | float: left; 110 | width: calc(100%/3); 111 | padding: 5px 10px; 112 | text-align: center; 113 | cursor: pointer; 114 | &:hover { 115 | background: rgba(0, 0, 0, .1); 116 | } 117 | &.active { 118 | background: rgba(0, 0, 0, .2); 119 | } 120 | } 121 | } 122 | .ti-content { 123 | z-index: 9; 124 | overflow: hidden; 125 | .flex-size(1); 126 | .flex-display; 127 | .flex-direction(row); 128 | .ti-content-side { 129 | .flex-size(0 0 200px); 130 | border-right: rgba(0, 0, 0, .2) solid thin; 131 | .ti-section-item { 132 | cursor: pointer; 133 | } 134 | } 135 | .ti-content-main { 136 | .flex-size(1); 137 | background: white; 138 | overflow-x: hidden; 139 | overflow-y: scroll; 140 | } 141 | .ti-content-block { 142 | overflow: hidden; 143 | .flex-display; 144 | .flex-direction(column); 145 | .ti-content-header { 146 | .flex-size(0 0 28px); 147 | } 148 | .ti-content-body { 149 | .flex-size(1); 150 | overflow-x: hidden; 151 | overflow-y: scroll; 152 | } 153 | } 154 | } 155 | .ti-section { 156 | padding: 5px 10px; 157 | display: table; 158 | width: 100%; 159 | .template-active { 160 | font-weight: bold; 161 | position: relative; 162 | top: 3px; 163 | font-size: 14px; 164 | } 165 | } 166 | .ti-section-title { 167 | font-weight: bold; 168 | padding: 2px 10px; 169 | background: rgba(0, 0, 0, .06); 170 | border-top: rgba(0, 0, 0, .05) solid thin; 171 | border-bottom: rgba(0, 0, 0, .05) solid thin; 172 | } 173 | .ti-section-item { 174 | padding: 2px 10px; 175 | border-bottom: rgba(0, 0, 0, .08) solid thin; 176 | display: table; 177 | width: 100%; 178 | outline: 0; 179 | &:hover { 180 | background: rgba(0, 0, 0, .03); 181 | } 182 | &.active { 183 | background: rgba(68, 108, 179, 1); 184 | color: white; 185 | } 186 | } 187 | .ti-button { 188 | border: rgba(0, 0, 0, .1) solid 3px; 189 | border-radius: 3px; 190 | background: transparent; 191 | box-shadow: none; 192 | color: rgba(0, 0, 0, .5); 193 | padding: 0 7px; 194 | line-height: 24px; 195 | height: 32px; 196 | outline: 0; 197 | cursor: pointer; 198 | font-size: 12px; 199 | margin: 10px 5px; 200 | &:active { 201 | border: rgba(0, 0, 0, .2) solid 3px; 202 | } 203 | } 204 | .pull-right { 205 | float: right; 206 | } 207 | .text-center { 208 | text-align: center; 209 | padding: 2px 10px; 210 | } 211 | .text-main { 212 | margin-top: 10px; 213 | } 214 | .ellipsised { 215 | white-space: normal; 216 | overflow: hidden; 217 | text-overflow: ellipsis !important; 218 | -o-text-overflow: ellipsis !important; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /lib.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////// 2 | // Insert debug window in DOM // 3 | //////////////////////////////// 4 | 5 | Meteor.startup(function() { 6 | Session.set("activeTpl", null); 7 | Blaze.render(Template.templateInspector, document.body); 8 | }); 9 | 10 | ////////////////////////// 11 | // Functions and params // 12 | ////////////////////////// 13 | 14 | var getTplName = function(tpl) { 15 | var name = (tpl && Meteor._get(tpl, "view", "name")) || ""; 16 | return name.slice(9); // Removes "Template." 17 | } 18 | 19 | var setTplActive = function(tplId) { 20 | Session.set("activeTpl", tplId); 21 | $("[data-template]").removeClass("ti-active"); 22 | $("[data-template='" + tplId + "']").addClass("ti-active"); 23 | } 24 | 25 | var getTplInstance = function(tplId) { 26 | var view = Blaze.getView($("[data-template='" + tplId + "']").get(0)); 27 | var tpl = view && view.templateInstance && view.templateInstance(); 28 | return tpl || {}; 29 | } 30 | 31 | var getTplActive = function() { 32 | var tplId = Session.get("activeTpl"); 33 | return getTplInstance(tplId); 34 | } 35 | 36 | var tplOmit = ["", "body", "__dynamic", "__dynamicWithDataContext", "__IronDefaultLayout__", "templateInspector", "TI_active", "TI_history", "TI_list", "TI_listItem", "TI_listItemChildren"]; 37 | 38 | ///////////////////////////////////////////// 39 | // Attach attr and events to all templates // 40 | ///////////////////////////////////////////// 41 | 42 | Template.onRendered(function() { 43 | 44 | var tpl = this; 45 | var tplName = getTplName(tpl); 46 | if (!this.firstNode || _.contains(tplOmit, tplName)) return; 47 | 48 | // Add a random ID to each rendered template 49 | var el = $(tpl.firstNode); 50 | var tplId = Random.id(); 51 | el.attr("data-template", tplId); 52 | tpl.state("tplId", tplId); 53 | 54 | // Store the template ref w its parent in local DB for filetree 55 | /* 56 | var parentId = null; 57 | for (var i = 0; i < 10; i++) { 58 | var parent = tpl.parent(i); 59 | parentId = parent && parent.state && parent.state("tplId"); 60 | if (parentId && (parentId != tplId)) break; 61 | } 62 | */ 63 | var parentId = el.parents("[data-template]").first().attr("data-template"); 64 | TplList.insert({ 65 | tplId: tplId, 66 | tplName: tplName, 67 | parentId: parentId, 68 | renderedAt: Date.now() 69 | }); 70 | 71 | // Handle cases when the child gets rendered before its parent 72 | var children = el.find("[data-template]"); 73 | children.each(function() { 74 | var childrenId = $(this).attr("data-template"); 75 | var selector = { tplId: childrenId }; 76 | var modifier = { $set: { parentId: tplId }}; 77 | TplList.update(selector, modifier); 78 | }); 79 | 80 | }); 81 | 82 | // Remove destroyed templates from DB 83 | Template.onDestroyed(function() { 84 | var tpl = this; 85 | var tplName = getTplName(tpl); 86 | if (_.contains(tplOmit, tplName)) return; 87 | var tplId = tpl.state("tplId"); // Take state instead of DOM attr because sometime the tpl.firstNode is not avail in destroyed tpls 88 | TplList.remove({ 89 | tplId: tplId 90 | }); 91 | }); 92 | 93 | TplList = new Mongo.Collection(null); 94 | TplList.helpers({ 95 | tplChildren: function() { 96 | var selector = { parentId: this.tplId }; 97 | var options = {}; 98 | return TplList.find(selector, options); 99 | }, 100 | tplParentName: function() { 101 | var tpl = getTplInstance(this.parentId); 102 | return getTplName(tpl); 103 | } 104 | }); 105 | 106 | var handle = null; 107 | Template.body.events({ 108 | "mouseenter [data-template]": function(e, data, tpl) { 109 | if (handle) Meteor.clearTimeout(handle); 110 | handle = Meteor.setTimeout(function() { 111 | var tplId = tpl.state("tplId"); 112 | setTplActive(tplId); 113 | }, 500); 114 | }, 115 | "mouseleave [data-template]": function() { 116 | if (handle) Meteor.clearTimeout(handle); 117 | } 118 | }); 119 | 120 | ////////////////////// 121 | // Template history // 122 | ////////////////////// 123 | 124 | /* 125 | TplHistory = new Mongo.Collection(null); 126 | 127 | var saveTplHistory = function(ev, tpl) { 128 | Meteor.setTimeout(function() { 129 | try { 130 | var tplName = getTplName(tpl); 131 | if (_.contains(tplOmit, tplName)) return; 132 | TplHistory.insert({ ev: ev, name: tplName, doneAt: Date.now() }); 133 | } catch(err) {} 134 | }, 0); 135 | } 136 | 137 | Template.onCreated(function() { 138 | saveTplHistory("created", this); 139 | }); 140 | 141 | Template.onRendered(function() { 142 | saveTplHistory("rendered", this); 143 | }); 144 | 145 | Template.onDestroyed(function() { 146 | saveTplHistory("destroyed", this); 147 | }); 148 | 149 | Template.TI_history.rendered = function() { 150 | var content = $(".template-inspector .ti-content"); 151 | content.scrollTop(content.get(0).scrollHeight); 152 | } 153 | 154 | Template.TI_history.helpers({ 155 | items: function() { 156 | var selector = {}; 157 | var options = { limit: 10 }; 158 | var options = {}; 159 | return TplHistory.find(selector, options); 160 | } 161 | }); 162 | */ 163 | 164 | //////////////////////////////// 165 | // Debug window data & events // 166 | //////////////////////////////// 167 | 168 | Template.templateInspector.hooks({ 169 | created: function() { 170 | var tpl = this; 171 | tpl.state("section", "TI_active"); 172 | tpl.state("reduced", (localStorage.getItem("TIreduced") == "true") || false); 173 | tpl.autorun(function() { 174 | var reduced = tpl.state("reduced"); 175 | if (reduced) return $("body").removeClass("template-inspector-on"); 176 | return $("body").addClass("template-inspector-on"); 177 | }); 178 | 179 | tpl.state("activeTpl", {}); 180 | tpl.state("activeTplName", "Active"); 181 | tpl.autorun(function() { 182 | var tt = Session.get("activeTpl"); 183 | var activeTpl = getTplActive(); 184 | tpl.state("activeTpl", activeTpl); 185 | tpl.state("activeTplName", getTplName(activeTpl) || "Active"); 186 | }); 187 | }, 188 | rendered: function() { 189 | var el = this.$(".template-inspector"); 190 | el.resizable({ 191 | stop: function(ev, ui) { 192 | localStorage.setItem("TIwidth", el.outerWidth()); 193 | localStorage.setItem("TIheight", el.outerHeight()); 194 | } 195 | }); 196 | el.draggable({ 197 | handle: ".ti-header", 198 | stop: function(ev, ui) { 199 | localStorage.setItem("TItop", el.position().top); 200 | localStorage.setItem("TIleft", el.position().left); 201 | } 202 | }); 203 | var css = { opacity: 1 }; 204 | if (localStorage.getItem("TIwidth")) css.width = localStorage.getItem("TIwidth") + "px"; 205 | if (localStorage.getItem("TIheight")) css.height = localStorage.getItem("TIheight") + "px"; 206 | if (localStorage.getItem("TItop")) css.top = localStorage.getItem("TItop") + "px"; 207 | css.left = localStorage.getItem("TIleft") ? localStorage.getItem("TIleft") + "px" : $(window).width() - el.width() + "px"; 208 | el.css(css); 209 | } 210 | }); 211 | 212 | Template.templateInspector.events({ 213 | "click [data-section]": function(e, tpl) { 214 | tpl.state("section", $(e.currentTarget).data("section")); 215 | }, 216 | "click [data-toggle-visibility]": function(e, tpl) { 217 | tpl.state("reduced", !tpl.state("reduced")); 218 | localStorage.setItem("TIreduced", tpl.state("reduced")); 219 | }, 220 | "click [data-log-instance]": function(e, tpl) { 221 | var tpl = tpl.state("activeTpl"); 222 | console.log("Tpl instance for '" + getTplName(tpl) + "':", tpl); 223 | } 224 | }); 225 | 226 | ///////////////////// 227 | // Template active // 228 | ///////////////////// 229 | 230 | Template.TI_active.rendered = function() { 231 | var content = $(".template-inspector .ti-content"); 232 | content.scrollTop(0); 233 | }; 234 | 235 | Template.TI_active.helpers({ 236 | has: function() { 237 | return !!Session.get("activeTpl"); 238 | }, 239 | name: function() { 240 | var tpl = getTplActive(); 241 | return getTplName(tpl); 242 | }, 243 | events: function() { 244 | var tpl = getTplActive(); 245 | var eventMaps = Meteor._get(tpl, "view", "template", "__eventMaps") || []; 246 | var events = []; 247 | _.each(eventMaps, function(map) { 248 | events = events.concat(_.keys(map)); 249 | }); 250 | return events; 251 | }, 252 | helprs: function() { 253 | // return []; 254 | var tpl = getTplActive(); 255 | var helpers = Meteor._get(tpl, "view", "template", "__helpers") || {}; 256 | var result = []; 257 | _.each(helpers, function(value, key) { 258 | result.push({ 259 | key: key.trim(), 260 | value: "" 261 | // value: value 262 | }); 263 | }); 264 | return result; 265 | }, 266 | data: function() { 267 | var tpl = getTplActive(); 268 | var data = Meteor._get(tpl, "data") || {}; 269 | var result = []; 270 | _.each(data, function(value, key) { 271 | result.push({ 272 | key: key, 273 | value: value 274 | }); 275 | }); 276 | return result; 277 | }, 278 | instanceVars: function() { 279 | var tpl = getTplActive(); 280 | var keys = _.difference(_.keys(tpl), ["data", "firstNode", "lastNode", "view", "tplId"]); 281 | var vars = _.map(keys, function(key) { 282 | return { 283 | key: key, 284 | value: tpl[key] 285 | }; 286 | }); 287 | return vars; 288 | } 289 | }); 290 | 291 | /////////////////// 292 | // Template list // 293 | /////////////////// 294 | 295 | Template.TI_list.helpers({ 296 | items: function() { 297 | /* 298 | var selector = { parentId: null }; 299 | var options = { limit: 10 }; 300 | */ 301 | var selector = {}; 302 | var options = { sort: { tplName: -1 }}; 303 | return TplList.find(selector, options); 304 | } 305 | }); 306 | 307 | Template.TI_list.events({ 308 | "click [data-template-activate]": function(e, tpl) { 309 | var tplId = $(e.currentTarget).attr("data-template-activate"); 310 | setTplActive(tplId); 311 | }, 312 | "mouseenter [data-template-activate]": function(e, tpl) { 313 | var tplId = $(e.currentTarget).attr("data-template-activate"); 314 | $("[data-template]").removeClass("ti-active"); 315 | $("[data-template='" + tplId + "']").addClass("ti-active"); 316 | }, 317 | "mouseleave [data-template-activate]": function(e, tpl) { 318 | var tplId = Session.get("activeTpl"); 319 | setTplActive(tplId); 320 | } 321 | }); 322 | 323 | /* 324 | Template.TI_listItem.created = function() { 325 | var tpl = this; 326 | tpl.state("tplId", this.data.tplId); 327 | } 328 | 329 | Template.TI_listItem.helpers({ 330 | active: function() { 331 | var tpl = Template.instance(); 332 | return (tpl.state("tplId") === Session.get("activeTpl")); 333 | } 334 | }); 335 | */ 336 | 337 | Template.TI_listItem.helpers({ 338 | active: function() { 339 | return (Session.get("activeTpl") === this.tplId); 340 | } 341 | }); 342 | 343 | //////////////// 344 | // UI helpers // 345 | //////////////// 346 | 347 | UI.registerHelper("equals", function(v1, v2) { 348 | return (v1 === v2); 349 | }); 350 | 351 | UI.registerHelper("moment", function(date, format) { 352 | var m = moment(date); 353 | if (_.isString(format)) { 354 | m = m.format(format); 355 | } else { 356 | m = m.fromNow(); 357 | } 358 | return m; 359 | }); 360 | --------------------------------------------------------------------------------