").append(@name)
127 |
128 |
129 | # Wraps elements that constitute a Javascript "view" object, e.g.
130 | # Backbone.View.
131 | class Xray.ViewSpecimen extends Xray.Specimen
132 | @all = []
133 |
134 |
135 | # Wraps elements that were rendered by a template, e.g. a Rails partial or
136 | # a client-side rendered JS template.
137 | class Xray.TemplateSpecimen extends Xray.Specimen
138 | @all = []
139 |
140 |
141 | # Singleton class for the Xray "overlay" invoked by the keyboard shortcut
142 | class Xray.Overlay
143 | @instance: ->
144 | @singletonInstance ||= new this
145 |
146 | constructor: ->
147 | Xray.Overlay.singletonInstance = this
148 | @bar = new Xray.Bar('#xray-bar')
149 | @settings = new Xray.Settings('#xray-settings')
150 | @shownBoxes = []
151 | @$overlay = $('
')
152 | @$overlay.click => @hide()
153 |
154 | show: (type = null) ->
155 | @reset()
156 | Xray.isShowing = true
157 | util.bm 'show', =>
158 | @bar.$el().find('#xray-bar-togglers .xray-bar-btn').removeClass('active')
159 | unless @$overlay.is(':visible')
160 | $('body').append @$overlay
161 | @bar.show()
162 | switch type
163 | when 'templates'
164 | Xray.findTemplates()
165 | specimens = Xray.TemplateSpecimen.all
166 | @bar.$el().find('.xray-bar-templates-toggler').addClass('active')
167 | when 'views'
168 | specimens = Xray.ViewSpecimen.all
169 | @bar.$el().find('.xray-bar-views-toggler').addClass('active')
170 | else
171 | Xray.findTemplates()
172 | specimens = Xray.specimens()
173 | @bar.$el().find('.xray-bar-all-toggler').addClass('active')
174 | for element in specimens
175 | continue unless element.isVisible()
176 | element.makeBox()
177 | # A cheap way to "order" the boxes, where boxes positioned closer to the
178 | # bottom right of the document have a higher z-index.
179 | element.$box.css
180 | zIndex: Math.ceil(MAX_ZINDEX*0.9 + element.bounds.top + element.bounds.left)
181 | @shownBoxes.push element.$box
182 | $('body').append element.$box
183 |
184 | reset: ->
185 | $box.remove() for $box in @shownBoxes
186 | @shownBoxes = []
187 |
188 | hide: ->
189 | Xray.isShowing = false
190 | @$overlay.detach()
191 | @reset()
192 | @bar.hide()
193 |
194 |
195 | # The Xray bar shows controller, action, and view information, and has
196 | # toggle buttons for showing the different types of specimens in the overlay.
197 | class Xray.Bar
198 | constructor: (el) ->
199 | @el = el
200 |
201 | # Defer wiring up jQuery event handlers until needed and then memoize the
202 | # result. If the Bar element no longer exists in the DOM, re-wire it.
203 | # This allows the Bar to keep working even if e.g. Turbolinks replaces the
204 | # DOM out from under us.
205 | $el: ->
206 | return @$el_memo if @$el_memo? && $.contains(window.document, @$el_memo[0])
207 | @$el_memo = $(@el)
208 | @$el_memo.css(zIndex: MAX_ZINDEX)
209 | @$el_memo.find('#xray-bar-controller-path .xray-bar-btn').click ->
210 | Xray.open($(this).attr('data-path'))
211 | @$el_memo.find('.xray-bar-all-toggler').click -> Xray.show()
212 | @$el_memo.find('.xray-bar-templates-toggler').click -> Xray.show('templates')
213 | @$el_memo.find('.xray-bar-views-toggler').click -> Xray.show('views')
214 | @$el_memo.find('.xray-bar-settings-btn').click -> Xray.toggleSettings()
215 | @$el_memo
216 |
217 | show: ->
218 | @$el().show()
219 | @originalPadding = parseInt $('html').css('padding-bottom')
220 | if @originalPadding < 40
221 | $('html').css paddingBottom: 40
222 |
223 | hide: ->
224 | @$el().hide()
225 | $('html').css paddingBottom: @originalPadding
226 |
227 |
228 | class Xray.Settings
229 | constructor: (el) ->
230 | @el = el
231 |
232 | $el: ->
233 | return @$el_memo if @$el_memo? && $.contains(window.document, @$el_memo[0])
234 | @$el_memo = $(@el)
235 | @$el_memo.find('form').submit @save
236 | @$el_memo
237 |
238 | toggle: =>
239 | @$el().toggle()
240 |
241 | save: (e) =>
242 | e.preventDefault()
243 | editor = @$el().find('#xray-editor-input').val()
244 | $.ajax
245 | url: '/_xray/config'
246 | type: 'POST'
247 | data: {editor: editor}
248 | success: => @displayUpdateMsg(true)
249 | error: => @displayUpdateMsg(false)
250 |
251 | displayUpdateMsg: (success) =>
252 | if success
253 | $msg = $("
Success!")
254 | else
255 | $msg = $("
Uh oh, something went wrong!")
256 | @$el().append($msg)
257 | $msg.delay(2000).fadeOut(500, => $msg.remove(); @toggle())
258 |
259 |
260 | # Utility methods.
261 | util =
262 | # Benchmark a piece of code
263 | bm: (name, fn) ->
264 | time = new Date
265 | result = fn()
266 | # console.log "#{name} : #{new Date() - time}ms"
267 | result
268 |
269 | # Computes the bounding box of a jQuery set, which may be many sibling
270 | # elements with no parent in the set.
271 | computeBoundingBox: ($contents) ->
272 | # Edge case: the container may not physically wrap its children, for
273 | # example if they are floated and no clearfix is present.
274 | if $contents.length == 1 and $contents.height() <= 0
275 | return util.computeBoundingBox($contents.children())
276 |
277 | boxFrame =
278 | top : Number.POSITIVE_INFINITY
279 | left : Number.POSITIVE_INFINITY
280 | right : Number.NEGATIVE_INFINITY
281 | bottom : Number.NEGATIVE_INFINITY
282 |
283 | for el in $contents
284 | $el = $(el)
285 | continue unless $el.is(':visible')
286 | frame = $el.offset()
287 | frame.right = frame.left + $el.outerWidth()
288 | frame.bottom = frame.top + $el.outerHeight()
289 | boxFrame.top = frame.top if frame.top < boxFrame.top
290 | boxFrame.left = frame.left if frame.left < boxFrame.left
291 | boxFrame.right = frame.right if frame.right > boxFrame.right
292 | boxFrame.bottom = frame.bottom if frame.bottom > boxFrame.bottom
293 |
294 | return {
295 | left : boxFrame.left
296 | top : boxFrame.top
297 | width : boxFrame.right - boxFrame.left
298 | height : boxFrame.bottom - boxFrame.top
299 | }
300 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/xray.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | /* selector for element and children */
4 | #xray-overlay, #xray-overlay *, #xray-overlay a:hover, #xray-overlay a:visited, #xray-overlay a:active,
5 | #xray-bar, #xray-bar *, #xray-bar a:hover, #xray-bar a:visited, #xray-bar a:active {
6 | background:none;
7 | border:none;
8 | bottom:auto;
9 | clear:none;
10 | cursor:default;
11 | float:none;
12 | font-family:Arial, Helvetica, sans-serif;
13 | font-size:medium;
14 | font-style:normal;
15 | font-weight:normal;
16 | height:auto;
17 | left:auto;
18 | letter-spacing:normal;
19 | line-height:normal;
20 | max-height:none;
21 | max-width:none;
22 | min-height:0;
23 | min-width:0;
24 | overflow:visible;
25 | position:static;
26 | right:auto;
27 | text-align:left;
28 | text-decoration:none;
29 | text-indent:0;
30 | text-transform:none;
31 | top:auto;
32 | visibility:visible;
33 | white-space:normal;
34 | width:auto;
35 | z-index:auto;
36 | }
37 |
38 | #xray-overlay {
39 | position: fixed; left: 0; top: 0; bottom: 0; right: 0;
40 | background: rgba(0,0,0,0.7);
41 | background: -webkit-radial-gradient(center, ellipse cover, rgba(0,0,0,0.4) 10%, rgba(0,0,0,0.8) 100%);
42 | z-index: 9000;
43 | }
44 |
45 | .xray-specimen {
46 | position: absolute;
47 | background: rgba(255,255,255,0.15);
48 | outline: 1px solid rgba(255,255,255,0.8);
49 | outline-offset: -1px;
50 | color: #666;
51 | font-family: "Helvetica Neue", sans-serif;
52 | font-size: 13px;
53 | box-shadow: 0 1px 3px rgba(0,0,0,0.7);
54 | }
55 |
56 | .xray-specimen:hover {
57 | cursor: pointer;
58 | background: rgba(255,255,255,0.4);
59 | }
60 |
61 | .xray-specimen.TemplateSpecimen {
62 | outline: 1px solid rgba(255,50,50,0.8);
63 | background: rgba(255,50,50,0.1);
64 | }
65 |
66 | .xray-specimen.TemplateSpecimen:hover {
67 | background: rgba(255,50,50,0.4);
68 | }
69 |
70 | .xray-specimen-handle {
71 | float:left;
72 | background: #fff;
73 | padding: 0 3px;
74 | color: #333;
75 | font-size: 10px;
76 | }
77 |
78 | .xray-specimen-handle.TemplateSpecimen {
79 | background: rgba(255,50,50,0.8);
80 | color: #fff;
81 | }
82 |
83 | #xray-bar {
84 | position: fixed;
85 | left: 0;
86 | right: 0;
87 | bottom: 0;
88 | height: 40px;
89 | padding: 0 8px;
90 | background: #222;
91 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
92 | font-weight: 200;
93 | color: #fff;
94 | z-index: 10000;
95 | box-shadow: 0 -1px 0 rgba(255,255,255,0.1), inset 0 2px 6px rgba(0,0,0,0.8);
96 | background-image: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.3)),
97 | url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDRkNBMTUwNzdGRTIxMUUyQjBGQ0NBRTc5RDQ3MEJFNSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDRkNBMTUwODdGRTIxMUUyQjBGQ0NBRTc5RDQ3MEJFNSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkNGQ0ExNTA1N0ZFMjExRTJCMEZDQ0FFNzlENDcwQkU1IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkNGQ0ExNTA2N0ZFMjExRTJCMEZDQ0FFNzlENDcwQkU1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+aIv7XwAAAEVJREFUeNpiFBMTY2BgEBISYsAGWICAATdgkZeXB1Lv37/HLo1LAgKYGPACdOl3YIAwHNOpKFw0aTSXokujuZSA0wACDABh2BIyJ1wQkwAAAABJRU5ErkJggg==);
98 | }
99 |
100 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dppx) {
101 | #xray-bar {
102 | background-image: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.3)),
103 | url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0Q0ZGQkRGRTdGRTMxMUUyQjBGQ0NBRTc5RDQ3MEJFNSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0Q0ZGQkRGRjdGRTMxMUUyQjBGQ0NBRTc5RDQ3MEJFNSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkNGQ0ExNTA5N0ZFMjExRTJCMEZDQ0FFNzlENDcwQkU1IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkNGQ0ExNTBBN0ZFMjExRTJCMEZDQ0FFNzlENDcwQkU1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+T6y4zwAAAIZJREFUeNrkkjEKwCAMRWMbERw8gE5O3v9q6lIEbbDQMZJ2Kv0gBOSR8HkqxphzBgDnnDEGJNkRsfc+xmitWWtF8KaUuqZ7EMDee1qutQ4hSGGkl1Kis2utYviYgUfZ4EU+CiP/TV0y/i02l1L6DA3is3n/FjDvHy5bYfxbF8b490fDTgEGAJveOCvuYEabAAAAAElFTkSuQmCC);
104 | background-size: auto, 10px 10px;
105 | }
106 | }
107 |
108 | #xray-bar .xray-bar-btn {
109 | position: relative;
110 | color: #fff;
111 | margin: 8px 1px;
112 | height: 24px;
113 | line-height: 24px;
114 | padding: 0 8px;
115 | float: left;
116 | font-size: 14px;
117 | cursor: pointer;
118 | vertical-align: middle;
119 | background-color: #444;
120 | background-image: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.2));
121 | border-radius: 2px;
122 | box-shadow: 1px 1px 1px rgba(0,0,0,0.5),
123 | inset 0 1px 0 rgba(255, 255, 255, 0.2),
124 | inset 0 0 2px rgba(255, 255, 255, 0.2);
125 | text-shadow: 0 -1px 0 rgba(0,0,0,0.4);
126 | transition: background-color 0.1s;
127 | }
128 |
129 | #xray-bar .xray-bar-btn b {
130 | position: absolute;
131 | display: block;
132 | right: -19px;
133 | top: 0;
134 | width: 20px;
135 | height: 24px;
136 | z-index: 10;
137 | overflow: hidden;
138 | font-size: 44px;
139 | line-height: 19px;
140 | text-indent: -7px;
141 | }
142 |
143 | #xray-bar .xray-bar-btn b:before {
144 | content: "";
145 | width: 18px;
146 | height: 18px;
147 | display: block;
148 | position: absolute;
149 | left: -9px;
150 | top: 3px;
151 | border-radius: 2px;
152 | box-shadow: 1px -1px 1px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 0 2px rgba(255, 255, 255, 0.2);
153 | background-image: linear-gradient(135deg, rgba(0,0,0,0), rgba(0,0,0,0.2));
154 | -webkit-transform: rotate(45deg);
155 | transform: rotate(45deg);
156 | transition: background-color 0.1s;
157 | }
158 |
159 | #xray-bar .xray-bar-btn:hover {
160 | background-color: #555;
161 | }
162 |
163 | #xray-bar #xray-bar-controller-path {
164 | margin-right: 20px;
165 | }
166 |
167 | #xray-bar #xray-bar-controller-path .xray-bar-btn {
168 | border-radius: 2px 0 0 2px;
169 | padding: 0 6px 0 16px;
170 | margin: 8px 1px 8px 0;
171 | }
172 |
173 | #xray-bar #xray-bar-controller-path .xray-bar-btn:first-child {
174 | padding-left: 8px;
175 | }
176 |
177 | #xray-bar #xray-bar-controller-path .xray-bar-btn:last-child {
178 | border-radius: 2px;
179 | padding-right: 10px;
180 | }
181 |
182 | #xray-bar-controller-path .xray-bar-controller { padding-left: 6px; }
183 | #xray-bar-controller-path .xray-bar-controller,
184 | #xray-bar-controller-path .xray-bar-controller b:before { background-color: #444; }
185 | #xray-bar-controller-path .xray-bar-controller:hover,
186 | #xray-bar-controller-path .xray-bar-controller:hover b:before { background-color: #555; }
187 | #xray-bar-controller-path .xray-bar-controller-action { color: #ddd; }
188 | #xray-bar-controller-path .xray-bar-layout,
189 | #xray-bar-controller-path .xray-bar-layout b:before { background-color: #c12e27; }
190 | #xray-bar-controller-path .xray-bar-layout:hover,
191 | #xray-bar-controller-path .xray-bar-layout:hover b:before { background-color: #de362d; }
192 | #xray-bar-controller-path .xray-bar-view,
193 | #xray-bar-controller-path .xray-bar-view b:before { background-color: #ff2c1e; }
194 | #xray-bar-controller-path .xray-bar-view:hover,
195 | #xray-bar-controller-path .xray-bar-view:hover b:before { background-color: #ff4c36; }
196 |
197 | #xray-bar #xray-bar-togglers {
198 | float: left;
199 | margin-left: 20px;
200 | }
201 |
202 | #xray-bar #xray-bar-togglers .xray-bar-btn {
203 | border-radius: 0;
204 | margin-right: 0;
205 | color: #999;
206 | }
207 |
208 | #xray-bar #xray-bar-togglers .xray-bar-btn:first-child {
209 | border-radius: 2px 0 0 2px;
210 | }
211 |
212 | #xray-bar #xray-bar-togglers .xray-bar-btn:last-child {
213 | border-radius: 0 2px 2px 0;
214 | }
215 |
216 | #xray-bar #xray-bar-togglers .xray-bar-btn:before {
217 | font-size: 9px;
218 | vertical-align: middle;
219 | margin-bottom: 1px;
220 | margin-right: 5px;
221 | background: rgba(255,255,255,0.2);
222 | color: #eee;
223 | padding: 2px 4px;
224 | text-shadow: none;
225 | }
226 |
227 | #xray-bar #xray-bar-togglers .xray-bar-btn.active {
228 | background: #555;
229 | color: #fff;
230 | }
231 |
232 | #xray-bar #xray-bar-togglers .xray-bar-templates-toggler:before { content: 'HTML'; }
233 | #xray-bar #xray-bar-togglers .xray-bar-templates-toggler.active:before { background: red; }
234 | #xray-bar #xray-bar-togglers .xray-bar-views-toggler:before { content: 'JS'; }
235 | #xray-bar #xray-bar-togglers .xray-bar-views-toggler.active:before { background: #fff; color: #333; }
236 | #xray-bar #xray-bar-togglers .xray-bar-styles-toggler:before { content: 'CSS'; }
237 |
238 | #xray-bar #xray-bar-togglers .xray-icon-search:before {
239 | font-size: 16px;
240 | background: none;
241 | padding: 0;
242 | margin: 0;
243 | }
244 |
245 | #xray-bar .xray-bar-settings-btn {
246 | position: absolute;
247 | right: 10px;
248 | top: 10px;
249 | color: #666;
250 | cursor: pointer;
251 | text-shadow: 0 1px 0 #000;
252 | font-size: 16px;
253 | -webkit-touch-callout: none;
254 | -webkit-user-select: none;
255 | -khtml-user-select: none;
256 | -moz-user-select: none;
257 | -ms-user-select: none;
258 | user-select: none;
259 | }
260 |
261 | #xray-bar .xray-bar-settings-btn:hover {
262 | color: #fff;
263 | }
264 |
265 | #xray-settings {
266 | position: absolute;
267 | right: 0;
268 | bottom: 40px;
269 | width: 300px;
270 | height: 100px;
271 | background: rgba(0,0,0,0.9);
272 | padding: 10px;
273 | font-size: 14px
274 | }
275 |
276 | #xray-settings label {
277 | display: inline;
278 | margin: 2px 10px;
279 | padding: 0;
280 | }
281 |
282 | #xray-settings input {
283 | padding: 5px;
284 | display: inline;
285 | background: #333;
286 | border: 1px solid #666;
287 | color: #fff;
288 | width: 200px;
289 | margin: 0;
290 | font-size: 13px;
291 | line-height: 13px;
292 | border-radius: 3px;
293 | vertical-align: middle;
294 | }
295 |
296 | #xray-settings button {
297 | position: absolute;
298 | right: 18px;
299 | left: 18px;
300 | bottom: 10px;
301 | padding: 7px;
302 | color: #fff;
303 | background: #04be00;
304 | text-align: center;
305 | cursor: pointer;
306 | }
307 |
308 | #xray-settings button:hover {
309 | background: #049d00;
310 | }
311 |
312 | #xray-settings p {
313 | font-size: 12px;
314 | color: #666;
315 | text-align: center;
316 | margin: 10px 0 0 0;
317 | }
318 |
319 | #xray-settings .xray-settings-update-msg {
320 | margin-left: 12px;
321 | }
322 |
323 |
324 | @font-face {
325 | font-family: 'xray-icons';
326 | src: url("data:application/octet-stream;base64,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") format('woff'), url("data:application/octet-stream;base64,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JWBkI7ADJWBkUFghGyMhWSMgILAEJiNGYThZLbAwLLAAFiAgILAFJiAuRyNHI2EjPDgtsDEssAAWILAJI0IgICBGI0ewACsjYTgtsDIssAAWsAMlsAIlRyNHI2GwAFRYLiA8IyEbsAIlsAIlRyNHI2EgsAUlsAQlRyNHI2GwBiWwBSVJsAIlYbABRWMjIFhiGyFZY7ABRWJgIy4jICA8ijgjIVktsDMssAAWILAJQyAuRyNHI2EgYLAgYGawgGIjICA8ijgtsDQsIyAuRrACJUZSWCA8WS6xJAEUKy2wNSwjIC5GsAIlRlBYIDxZLrEkARQrLbA2LCMgLkawAiVGUlggPFkjIC5GsAIlRlBYIDxZLrEkARQrLbA3LLAuKyMgLkawAiVGUlggPFkusSQBFCstsDgssC8riiAgPLAEI0KKOCMgLkawAiVGUlggPFkusSQBFCuwBEMusCQrLbA5LLAAFrAEJbAEJiAuRyNHI2GwBkUrIyA8IC4jOLEkARQrLbA6LLEJBCVCsAAWsAQlsAQlIC5HI0cjYSCwBCNCsAZFKyCwYFBYILBAUVizAiADIBuzAiYDGllCQiMgR7AEQ7CAYmAgsAArIIqKYSCwAkNgZCOwA0NhZFBYsAJDYRuwA0NgWbADJbCAYmGwAiVGYTgjIDwjOBshICBGI0ewACsjYTghWbEkARQrLbA7LLAuKy6xJAEUKy2wPCywLyshIyAgPLAEI0IjOLEkARQrsARDLrAkKy2wPSywABUgR7AAI0KyAAEBFRQTLrAqKi2wPiywABUgR7AAI0KyAAEBFRQTLrAqKi2wPyyxAAEUE7ArKi2wQCywLSotsEEssAAWRSMgLiBGiiNhOLEkARQrLbBCLLAJI0KwQSstsEMssgAAOistsEQssgABOistsEUssgEAOistsEYssgEBOistsEcssgAAOystsEgssgABOystsEkssgEAOystsEossgEBOystsEsssgAANystsEwssgABNystsE0ssgEANystsE4ssgEBNystsE8ssgAAOSstsFAssgABOSstsFEssgEAOSstsFIssgEBOSstsFMssgAAPCstsFQssgABPCstsFUssgEAPCstsFYssgEBPCstsFcssgAAOCstsFgssgABOCstsFkssgEAOCstsFossgEBOCstsFsssDArLrEkARQrLbBcLLAwK7A0Ky2wXSywMCuwNSstsF4ssAAWsDArsDYrLbBfLLAxKy6xJAEUKy2wYCywMSuwNCstsGEssDErsDUrLbBiLLAxK7A2Ky2wYyywMisusSQBFCstsGQssDIrsDQrLbBlLLAyK7A1Ky2wZiywMiuwNistsGcssDMrLrEkARQrLbBoLLAzK7A0Ky2waSywMyuwNSstsGossDMrsDYrLbBrLCuwCGWwAyRQeLABFTAtAABLuADIUlixAQGOWbkIAAgAYyCwASNEILADI3CyBCgJRVJEswoLBgQrsQYBRLEkAYhRWLBAiFixBgNEsSYBiFFYuAQAiFixBgFEWVlZWbgB/4WwBI2xBQBE") format('truetype');
327 | }
328 |
329 | [class^="xray-icon-"]:before,
330 | [class*=" xray-icon-"]:before {
331 | font-family: 'xray-icons';
332 | font-style: normal;
333 | font-weight: normal;
334 | speak: none;
335 | display: inline-block;
336 | text-decoration: inherit;
337 | width: 1em;
338 | margin-right: 0.1em;
339 | text-align: center;
340 | line-height: 1em;
341 | }
342 |
343 | .xray-icon-cog:before { content: '\e800'; } /* '' */
344 | .xray-icon-flash:before { content: '\26a1'; } /* '⚡' */
345 | .xray-icon-cancel:before { content: '\2715'; } /* '✕' */
346 | .xray-icon-github:before { content: '\f056'; } /* '' */
347 | .xray-icon-columns:before { content: '\f0db'; } /* '' */
348 | .xray-icon-doc:before { content: '📄'; } /* '\1f4c4' */
349 | .xray-icon-search:before { content: '🔍'; } /* '\1f50d' */
350 |
--------------------------------------------------------------------------------
/app/views/_xray_bar.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if Xray.request_info.present? %>
3 |
4 | <% if Xray.request_info[:controller] %>
5 |
6 |
7 | <%= Xray.request_info[:controller][:name] %>#<%= Xray.request_info[:controller][:action] %>
8 |
9 | <% end %>
10 | <% if Xray.request_info[:view] && Xray.request_info.fetch(:controller, {})[:name] != "Rails::InfoController" %>
11 | <% layout_path = lookup_context.find(Xray.request_info[:view][:layout]).identifier %>
12 |
13 |
14 | <%= layout_path.split('/').last %>
15 |
16 |
17 | <%= Xray.request_info[:view][:path].split('/').last %>
18 |
19 | <% end %>
20 |
21 | <% end %>
22 |
23 |
24 |
25 | templates
26 |
27 |
28 | views
29 |
30 |
31 |
32 |
40 | <%= stylesheet_link_tag :xray %>
41 |
42 |
--------------------------------------------------------------------------------
/example/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brentd/xray-rails/3d5fe94abee742fff387c393fc5c42db20649e09/example/screenshot.png
--------------------------------------------------------------------------------
/lib/xray-rails.rb:
--------------------------------------------------------------------------------
1 | require "json"
2 | require "active_support/all"
3 | require_relative "xray/version"
4 | require_relative "xray/aliasing"
5 | require_relative "xray/config"
6 | require_relative "xray/middleware"
7 |
8 | if defined?(Rails) && Rails.env.development?
9 | require "xray/engine"
10 | end
11 |
12 | module Xray
13 | FILE_PLACEHOLDER = '$file'
14 |
15 | # Used to collect request information during each request cycle for use in
16 | # the Xray bar.
17 | def self.request_info
18 | Thread.current[:request_info] ||= {}
19 | end
20 |
21 | # Returns augmented HTML where the source is simply wrapped in an HTML
22 | # comment with filepath info. Xray.js uses these comments to associate
23 | # elements with the templates that rendered them.
24 | #
25 | # This:
26 | #
27 | # ...
28 | #
29 | #
30 | # Becomes:
31 | #
32 | #
33 | # ...
34 | #
35 | #
36 | def self.augment_template(source, path)
37 | id = next_id
38 | if source.include?('\n#{source}\n"
46 | end
47 | ActiveSupport::SafeBuffer === source ? ActiveSupport::SafeBuffer.new(augmented) : augmented
48 | end
49 |
50 | def self.next_id
51 | @id = (@id ||= 0) + 1
52 | end
53 |
54 | def self.open_file(file)
55 | editor = Xray.config.editor
56 | cmd = if editor.include?('$file')
57 | editor.gsub '$file', file
58 | else
59 | "#{editor} \"#{file}\""
60 | end
61 | Open3.capture3(cmd)
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/xray/aliasing.rb:
--------------------------------------------------------------------------------
1 | module Xray
2 | # This module implements the old ActiveSupport alias_method_chain feature
3 | # with a new name, and without the deprecation warnings. In ActiveSupport 5+,
4 | # this style of patching was deprecated in favor of Module.prepend. But
5 | # Module.prepend is not present in Ruby 1.9, which we would still like to
6 | # support. So we continue to use of alias_method_chain, albeit with a
7 | # different name to avoid collisions.
8 | #
9 | # TODO: remove this and drop support for Ruby 1.9.
10 | #
11 | module Aliasing
12 | # This code is copied and pasted from ActiveSupport, but with :xray
13 | # hardcoded as the feature name, and with the deprecation warning removed.
14 | def xray_method_alias(target)
15 | feature = :xray
16 |
17 | # Strip out punctuation on predicates, bang or writer methods since
18 | # e.g. target?_without_feature is not a valid method name.
19 | aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
20 | yield(aliased_target, punctuation) if block_given?
21 |
22 | with_method = "#{aliased_target}_with_#{feature}#{punctuation}"
23 | without_method = "#{aliased_target}_without_#{feature}#{punctuation}"
24 |
25 | alias_method without_method, target
26 | alias_method target, with_method
27 |
28 | case
29 | when public_method_defined?(without_method)
30 | public target
31 | when protected_method_defined?(without_method)
32 | protected target
33 | when private_method_defined?(without_method)
34 | private target
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/xray/config.rb:
--------------------------------------------------------------------------------
1 | module Xray
2 |
3 | def self.config
4 | @@config ||= Config.new
5 | end
6 |
7 | class Config
8 | CONFIG_FILE = ".xrayconfig"
9 |
10 | def default_editor
11 | ENV['GEM_EDITOR'] ||
12 | ENV['VISUAL'] ||
13 | ENV['EDITOR'] ||
14 | '/usr/local/bin/subl'
15 | end
16 |
17 | def editor
18 | load_config[:editor]
19 | end
20 |
21 | def editor=(new_editor)
22 | if new_editor && new_editor != editor
23 | write_config(editor: new_editor)
24 | true
25 | else
26 | false
27 | end
28 | end
29 |
30 | def to_yaml
31 | {editor: editor}.to_yaml
32 | end
33 |
34 | def config_file
35 | if File.exist?("#{Dir.pwd}/#{CONFIG_FILE}")
36 | "#{Dir.pwd}/#{CONFIG_FILE}"
37 | else
38 | "#{Dir.home}/#{CONFIG_FILE}"
39 | end
40 | end
41 |
42 | private
43 |
44 | def write_config(new_config)
45 | config = load_config.merge(new_config)
46 | File.open(config_file, 'w') { |f| f.write(config.to_yaml) }
47 | end
48 |
49 | def load_config
50 | default_config.merge(local_config)
51 | end
52 |
53 | def local_config
54 | YAML.load_file(config_file)
55 | rescue
56 | {}
57 | end
58 |
59 | def default_config
60 | { editor: default_editor }
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/xray/engine.rb:
--------------------------------------------------------------------------------
1 | module Xray
2 |
3 | # This is the main point of integration with Rails. This engine hooks into
4 | # Sprockets and monkey patches ActionView in order to augment the app's JS
5 | # and HTML templates with filepath information that can be used by xray.js
6 | # in the browser. It also hooks in a middleware responsible for injecting
7 | # xray.js and the xray bar into the app's response bodies.
8 | class Engine < ::Rails::Engine
9 | initializer "xray.initialize" do |app|
10 | app.middleware.use Xray::Middleware
11 |
12 | # Required by Rails 4.1
13 | app.config.assets.precompile += %w(xray.js xray.css)
14 | end
15 |
16 | config.after_initialize do |app|
17 | ensure_asset_pipeline_enabled! app
18 |
19 | # Monkey patch ActionView::Template to augment server-side templates
20 | # with filepath information. See `Xray.augment_template` for details.
21 | ActionView::Template.class_eval do
22 | extend Xray::Aliasing
23 |
24 | def render_with_xray(*args, **kwargs, &block)
25 | path = identifier
26 | view = args.first
27 | source = render_without_xray(*args, **kwargs, &block)
28 |
29 | suitable_template = !(view.respond_to?(:mailer) && view.mailer) &&
30 | !path.include?('_xray_bar') &&
31 | path =~ /\.(html|slim|haml|hamlc)(\.|$)/ &&
32 | path !~ /\.(js|json|css)(\.|$)/
33 |
34 | options = args.last.kind_of?(Hash) ? args.last : {}
35 |
36 | if source && suitable_template && !(options.has_key?(:xray) && (options[:xray] == false))
37 | Xray.augment_template(source, path)
38 | else
39 | source
40 | end
41 | end
42 | xray_method_alias :render
43 | end
44 |
45 | # Sprockets preprocessor interface which supports all versions of Sprockets.
46 | # See: https://github.com/rails/sprockets/blob/master/guides/extending_sprockets.md#supporting-all-versions-of-sprockets-in-processors
47 | class JavascriptPreprocessor
48 | def initialize(filename, &block)
49 | @filename = filename
50 | @source = block.call
51 | end
52 |
53 | def render(context, empty_hash_wtf)
54 | self.class.run(@filename, @source, context)
55 | end
56 |
57 | def self.run(filename, source, context)
58 | path = Pathname.new(context.filename).to_s
59 | if path =~ /^#{Rails.root}.+\.(jst)(\.|$)/
60 | Xray.augment_template(source, path)
61 | else
62 | source
63 | end
64 | end
65 |
66 | def self.call(input)
67 | filename = input[:filename]
68 | source = input[:data]
69 | context = input[:environment].context_class.new(input)
70 |
71 | result = run(filename, source, context)
72 | context.metadata.merge(data: result)
73 | end
74 | end
75 |
76 | # Augment JS templates
77 | app.assets.register_preprocessor 'application/javascript', JavascriptPreprocessor
78 |
79 | # This event is called near the beginning of a request cycle. We use it to
80 | # collect information about the controller and action that is responding, for
81 | # display in the Xray bar.
82 | ActiveSupport::Notifications.subscribe('start_processing.action_controller') do |*args|
83 | event = ActiveSupport::Notifications::Event.new(*args)
84 | controller_name = event.payload[:controller]
85 | action_name = event.payload[:action]
86 | path = ActiveSupport::Dependencies.search_for_file(controller_name.underscore)
87 |
88 | Xray.request_info.clear
89 |
90 | Xray.request_info[:controller] = {
91 | :path => path,
92 | :name => controller_name,
93 | :action => action_name
94 | }
95 | end
96 |
97 | # This event is called each time during the request cycle that
98 | # ActionView renders a template. The first time it's called will most
99 | # likely be the view the controller is rendering, which is what we're
100 | # interested in.
101 | ActiveSupport::Notifications.subscribe('render_template.action_view') do |*args|
102 | event = ActiveSupport::Notifications::Event.new(*args)
103 | layout = event.payload[:layout]
104 | path = event.payload[:identifier]
105 |
106 | # We are only interested in the first notification that has a layout.
107 | if layout
108 | Xray.request_info[:view] ||= {
109 | :path => path,
110 | :layout => layout
111 | }
112 | end
113 | end
114 | end
115 |
116 | def ensure_asset_pipeline_enabled!(app)
117 | unless app.assets
118 | raise "xray-rails requires the Rails asset pipeline.
119 | The asset pipeline is currently disabled in this application.
120 | Either convert your application to use the asset pipeline, or remove xray-rails from your Gemfile."
121 | end
122 | end
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/lib/xray/middleware.rb:
--------------------------------------------------------------------------------
1 | require "open3"
2 |
3 | module Xray
4 | OPEN_PATH = '/_xray/open'
5 | UPDATE_CONFIG_PATH = '/_xray/config'
6 |
7 | # This middleware is responsible for injecting xray.js and the Xray bar into
8 | # the app's pages. It also listens for requests to open files with the user's
9 | # editor.
10 | class Middleware
11 | def initialize(app)
12 | @app = app
13 | end
14 |
15 | def call(env)
16 | # Request for opening a file path.
17 | if env['PATH_INFO'] == OPEN_PATH
18 | req, res = Rack::Request.new(env), Rack::Response.new
19 | out, _err, status = Xray.open_file(req.GET['path'])
20 | if status.success?
21 | res.status = 200
22 | else
23 | res.write out
24 | res.status = 500
25 | end
26 | res.finish
27 | elsif env['PATH_INFO'] == UPDATE_CONFIG_PATH
28 | req, res = Rack::Request.new(env), Rack::Response.new
29 | if req.post? && Xray.config.editor = req.POST['editor']
30 | res.status = 200
31 | else
32 | res.status = 400
33 | end
34 | res.finish
35 |
36 | # Inject xray.js and friends if this is a successful HTML response
37 | else
38 | status, headers, response = @app.call(env)
39 |
40 | if html_headers?(status, headers) && body = response_body(response)
41 | if body =~ script_matcher('xray')
42 | # Inject the xray bar if xray.js is already on the page
43 | inject_xray_bar!(body)
44 | elsif Rails.application.config.assets.debug
45 | # Otherwise try to inject xray.js if assets are unbundled
46 | if append_js!(body, 'jquery', 'xray')
47 | inject_xray_bar!(body)
48 | end
49 | end
50 |
51 | content_length = body.bytesize.to_s
52 |
53 | # For rails v4.2.0+ compatibility
54 | if defined?(ActionDispatch::Response::RackBody) && ActionDispatch::Response::RackBody === response
55 | response = response.instance_variable_get(:@response)
56 | end
57 |
58 | # Modifying the original response obj maintains compatibility with other middlewares
59 | if ActionDispatch::Response === response
60 | response.body = [body]
61 | response.header['Content-Length'] = content_length unless committed?(response)
62 | response.to_a
63 | else
64 | headers['Content-Length'] = content_length
65 | [status, headers, [body]]
66 | end
67 | else
68 | [status, headers, response]
69 | end
70 | end
71 | end
72 |
73 | private
74 |
75 | def committed?(response)
76 | response.respond_to?(:committed?) && response.committed?
77 | end
78 |
79 | def inject_xray_bar!(html)
80 | html.sub!(/]*>/) { "#{$~}\n#{render_xray_bar}" }
81 | end
82 |
83 | def render_xray_bar
84 | if ApplicationController.respond_to?(:render)
85 | # Rails 5
86 | ApplicationController.render(:partial => "/xray_bar").html_safe
87 | else
88 | # Rails <= 4.2
89 | ac = ActionController::Base.new
90 | ac.render_to_string(:partial => '/xray_bar').html_safe
91 | end
92 | end
93 |
94 | # Matches:
95 | #
96 | #
97 | #
98 | #
99 | def script_matcher(script_name)
100 | /
101 | ' }
11 | let(:html_identifier) { 'template.html' }
12 | let(:txt_identifier) { 'template.txt' }
13 |
14 | it 'should render and augment valid HTML like files by default' do
15 | subject.should_receive(:render_without_xray).with(*xray_enabled_render_args).and_return(render_result)
16 | subject.should_receive(:identifier).and_return(html_identifier)
17 | Xray.should_receive(:augment_template).with(render_result, html_identifier).and_return(augmented_render_result)
18 | expect(subject.render(*xray_enabled_render_args)).to eql(augmented_render_result)
19 | end
20 |
21 | it 'should render and augment when template source is an empty string' do
22 | subject.should_receive(:render_without_xray).with(*xray_enabled_render_args).and_return('')
23 | subject.should_receive(:identifier).and_return(html_identifier)
24 | Xray.should_receive(:augment_template).with('', html_identifier).and_return(augmented_render_result)
25 | expect(subject.render(*xray_enabled_render_args)).to eql(augmented_render_result)
26 | end
27 |
28 | it 'should render but not augment HTML if :xray => false passed as an option' do
29 | subject.should_receive(:render_without_xray).with(*xray_enabled_render_args).and_return(render_result)
30 | subject.should_receive(:identifier).and_return(html_identifier)
31 | Xray.should_receive(:augment_template).with(render_result, html_identifier).and_return(augmented_render_result)
32 | expect(subject.render(*xray_enabled_render_args)).to eql(augmented_render_result)
33 | end
34 |
35 | it 'should render but not augment non HTML files' do
36 | subject.should_receive(:render_without_xray).with(*xray_disabled_render_args).and_return(plain_text_result)
37 | subject.should_receive(:identifier).and_return(txt_identifier)
38 | Xray.should_not_receive(:augment_template)
39 | expect(subject.render(*xray_disabled_render_args)).to eql(plain_text_result)
40 | end
41 |
42 | it 'should render but not augment when template source is nil' do
43 | subject.should_receive(:render_without_xray).with(*xray_enabled_render_args).and_return(nil)
44 | subject.should_receive(:identifier).and_return(html_identifier)
45 | Xray.should_not_receive(:augment_template)
46 | expect(subject.render(*xray_enabled_render_args)).to eql(nil)
47 | end
48 | end
49 | end
50 |
51 |
--------------------------------------------------------------------------------
/spec/xray/middleware_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Xray::Middleware, "in a middleware stack" do
4 | def mock_response(status, content_type, body)
5 | body = body.unindent
6 | app = Rack::Builder.new do
7 | use Xray::Middleware
8 | run lambda { |env| [status, {'Content-Type' => content_type}, [body]] }
9 | end
10 | Rack::MockRequest.new(app).get('/')
11 | end
12 |
13 | context "when the response is html and contains " do
14 | it "injects the xray bar and xray.js" do
15 | response = mock_response 200, 'text/html', <<-HTML
16 |
17 |
18 |
19 |
20 |
21 |
22 | HTML
23 | expect(response.body).to have_selector('#xray-bar')
24 | expect(response.body).to have_selector('script[src^="/assets/xray"]')
25 | end
26 |
27 | it "does not inject xray.js or the xray bar if jquery is not found" do
28 | response = mock_response 200, 'text/html', <<-HTML
29 |
30 |
31 |
32 |
33 | HTML
34 | expect(response.body).to_not have_selector('#xray-bar')
35 | expect(response.body).to_not have_selector('script[src^="/assets/xray"]')
36 | end
37 |
38 | it "does inject xray.js or the xray bar if jquery2 is found" do
39 | response = mock_response 200, 'text/html', <<-HTML
40 |
41 |
42 |
43 |
44 |
45 |
46 | HTML
47 | expect(response.body).to have_selector('#xray-bar')
48 | expect(response.body).to have_selector('script[src^="/assets/xray"]')
49 | end
50 |
51 | it "does inject xray.js or the xray bar if jquery3 is found" do
52 | response = mock_response 200, 'text/html', <<-HTML
53 |
54 |
55 |
56 |
57 |
58 |
59 | HTML
60 | expect(response.body).to have_selector('#xray-bar')
61 | expect(response.body).to have_selector('script[src^="/assets/xray"]')
62 | end
63 | end
64 |
65 | context "when the response does not contain " do
66 | it "does not inject xray bar or xray.js" do
67 | response = mock_response 200, 'text/html', <<-HTML
68 |
just some html
69 | HTML
70 | expect(response.body).to_not have_selector('#xray-bar')
71 | expect(response.body).to_not have_selector('script[src^="/assets/xray"]')
72 | end
73 | end
74 |
75 | context "when the response is blank" do
76 | it "does not inject xray" do
77 | response = mock_response 200, 'text/html', ''
78 | expect(response.body).to_not have_selector('#xray-bar')
79 | expect(response.body).to_not have_selector('script[src^="/assets/xray"]')
80 | end
81 | end
82 |
83 | context "when the response is unsuccessful" do
84 | it "does not inject xray" do
85 | response = mock_response 500, 'text/html', ''
86 | expect(response.body).to_not have_selector('#xray-bar')
87 | expect(response.body).to_not have_selector('script[src^="/assets/xray"]')
88 | end
89 | end
90 | end
91 |
92 | describe Xray::Middleware, "in a Rails app" do
93 | it "injects xray.js into the response" do
94 | visit '/'
95 | expect(page).to have_selector('script[src^="/assets/xray"]')
96 | end
97 |
98 | it "injects the xray bar into the response" do
99 | visit '/'
100 | expect(page).to have_selector('#xray-bar')
101 | end
102 |
103 | it "doesn't mess with non-html requests" do
104 | visit '/non_html'
105 | expect(page.html).not_to include('xray')
106 | expect(page).not_to have_selector('#xray-bar')
107 | end
108 |
109 | context "edge cases" do
110 | it "does not add html comments to json.haml pages" do
111 | visit '/made_with_haml.json'
112 | expect(page.html).not_to include('