window.Xray = {}
return unless $ = window.jQuery
# Max CSS z-index. The overlay and xray bar use this.
MAX_ZINDEX = 2147483647
# Initialize Xray. Called immediately, but some setup is deferred until DOM ready.
Xray.init = do ->
return if Xray.initialized
Xray.initialized = true
is_mac = navigator.platform.toUpperCase().indexOf('MAC') isnt -1
# Register keyboard shortcuts
$(document).keydown (e) ->
# cmd+shift+x on Mac, ctrl+shift+x on other platforms
if (is_mac and e.metaKey or !is_mac and e.ctrlKey) and e.shiftKey and e.keyCode is 88
if Xray.isShowing then Xray.hide() else Xray.show()
if Xray.isShowing and e.keyCode is 27 # esc
Xray.hide()
$ ->
# Instantiate the overlay singleton.
new Xray.Overlay
# Go ahead and do a pass on the DOM to find templates.
Xray.findTemplates()
# Ready to rock.
console?.log "Ready to Xray. Press #{if is_mac then 'cmd+shift+x' else 'ctrl+shift+x'} to scan your UI."
# Returns all currently created Xray.Specimen objects.
Xray.specimens = ->
Xray.ViewSpecimen.all.concat Xray.TemplateSpecimen.all
# Looks up the stored constructor info
Xray.constructorInfo = (constructor) ->
if window.XrayPaths
for own info, func of window.XrayPaths
return JSON.parse(info) if func == constructor
null
# Scans the document for templates, creating Xray.TemplateSpecimens for them.
Xray.findTemplates = -> util.bm 'findTemplates', ->
# Find all comments
comments = $('*:not(iframe,script)').contents().filter ->
this.nodeType == 8 and this.data[0..9] == "XRAY START"
# Find the comment for each. Everything between the
# start and end comment becomes the contents of an Xray.TemplateSpecimen.
for comment in comments
[_, id, path, url] = comment.data.match(/^XRAY START (\d+) (\S*) (\S*)/)
$templateContents = new jQuery
el = comment.nextSibling
until !el or (el.nodeType == 8 and el.data == "XRAY END #{id}")
if el.nodeType == 1 and el.tagName != 'SCRIPT'
$templateContents.push el
el = el.nextSibling
# Remove XRAY template comments from the DOM.
el.parentNode.removeChild(el) if el?.nodeType == 8
comment.parentNode.removeChild(comment)
# Add the template specimen
Xray.TemplateSpecimen.add $templateContents,
name: path.split('/').slice(-1)[0]
path: path
url: url
# Open the given filesystem path by calling out to Xray's server.
Xray.open = (url) ->
window.open(url, null, 'width=700, height=500')
# Show the Xray overlay
Xray.show = (type = null) ->
Xray.Overlay.instance().show(type)
# Hide the Xray overlay
Xray.hide = ->
Xray.Overlay.instance().hide()
Xray.toggleSettings = ->
Xray.Overlay.instance().settings.toggle()
# Wraps a DOM element that Xray is tracking. This is subclassed by
# Xray.TemplateSpecimen and Xray.ViewSpecimen.
class Xray.Specimen
@add: (el, info = {}) ->
@all.push new this(el, info)
@remove: (el) ->
@find(el)?.remove()
@find: (el) ->
el = el[0] if el instanceof jQuery
for specimen in @all
return specimen if specimen.el == el
null
@reset: ->
@all = []
constructor: (contents, info = {}) ->
@el = if contents instanceof jQuery then contents[0] else contents
@$contents = $(contents)
@name = info.name
@path = info.path
@url = info.url
remove: ->
idx = @constructor.all.indexOf(this)
@constructor.all.splice(idx, 1) unless idx == -1
isVisible: ->
@$contents.length and @$contents.is(':visible')
makeBox: ->
@bounds = util.computeBoundingBox(@$contents)
@$box = $("
").css(@bounds)
# If the element is fixed, override the computed position with the fixed one.
if @$contents.css('position') == 'fixed'
@$box.css
position : 'fixed'
top : @$contents.css('top')
left : @$contents.css('left')
@$box.click => Xray.open @url + '/blurbs/' + @path + '/edit'
@$box.append @makeLabel
makeLabel: =>
$("
").append(@name)
# Wraps elements that constitute a Javascript "view" object, e.g.
# Backbone.View.
class Xray.ViewSpecimen extends Xray.Specimen
@all = []
# Wraps elements that were rendered by a template, e.g. a Rails partial or
# a client-side rendered JS template.
class Xray.TemplateSpecimen extends Xray.Specimen
@all = []
# Singleton class for the Xray "overlay" invoked by the keyboard shortcut
class Xray.Overlay
@instance: ->
@singletonInstance ||= new this
constructor: ->
Xray.Overlay.singletonInstance = this
@bar = new Xray.Bar('#xray-bar')
@settings = new Xray.Settings('#xray-settings')
@shownBoxes = []
@$overlay = $('
')
@$overlay.click => @hide()
show: (type = null) ->
@reset()
Xray.isShowing = true
util.bm 'show', =>
@bar.$el.find('#xray-bar-togglers .xray-bar-btn').removeClass('active')
unless @$overlay.is(':visible')
$('body').append @$overlay
@bar.show()
switch type
when 'templates'
Xray.findTemplates()
specimens = Xray.TemplateSpecimen.all
@bar.$el.find('.xray-bar-templates-toggler').addClass('active')
when 'views'
specimens = Xray.ViewSpecimen.all
@bar.$el.find('.xray-bar-views-toggler').addClass('active')
else
Xray.findTemplates()
specimens = Xray.specimens()
@bar.$el.find('.xray-bar-all-toggler').addClass('active')
for element in specimens
continue unless element.isVisible()
element.makeBox()
# A cheap way to "order" the boxes, where boxes positioned closer to the
# bottom right of the document have a higher z-index.
element.$box.css
zIndex: Math.ceil(MAX_ZINDEX*0.9 + element.bounds.top + element.bounds.left)
@shownBoxes.push element.$box
$('body').append element.$box
reset: ->
$box.remove() for $box in @shownBoxes
@shownBoxes = []
hide: ->
Xray.isShowing = false
@$overlay.detach()
@reset()
@bar.hide()
# The Xray bar shows controller, action, and view information, and has
# toggle buttons for showing the different types of specimens in the overlay.
class Xray.Bar
constructor: (el) ->
@$el = $(el)
@$el.css(zIndex: MAX_ZINDEX)
@$el.find('#xray-bar-controller-path .xray-bar-btn').click ->
Xray.open($(this).attr('data-path'))
@$el.find('.xray-bar-all-toggler').click -> Xray.show()
@$el.find('.xray-bar-templates-toggler').click -> Xray.show('templates')
@$el.find('.xray-bar-views-toggler').click -> Xray.show('views')
@$el.find('.xray-bar-settings-btn').click -> Xray.toggleSettings()
show: ->
@$el.show()
@originalPadding = parseInt $('html').css('padding-bottom')
if @originalPadding < 40
$('html').css paddingBottom: 40
hide: ->
@$el.hide()
$('html').css paddingBottom: @originalPadding
class Xray.Settings
constructor: (el) ->
@$el = $(el)
@$el.find('form').submit @save
toggle: =>
@$el.toggle()
save: (e) =>
e.preventDefault()
editor = @$el.find('#xray-editor-input').val()
$.ajax
url: '/_xray/config'
type: 'POST'
data: {editor: editor}
success: => @displayUpdateMsg(true)
error: => @displayUpdateMsg(false)
displayUpdateMsg: (success) =>
if success
$msg = $("Success!")
else
$msg = $("Uh oh, something went wrong!")
@$el.append($msg)
$msg.delay(2000).fadeOut(500, => $msg.remove(); @toggle())
# Utility methods.
util =
# Benchmark a piece of code
bm: (name, fn) ->
time = new Date
result = fn()
# console.log "#{name} : #{new Date() - time}ms"
result
# Computes the bounding box of a jQuery set, which may be many sibling
# elements with no parent in the set.
computeBoundingBox: ($contents) ->
# Edge case: the container may not physically wrap its children, for
# example if they are floated and no clearfix is present.
if $contents.length == 1 and $contents.height() <= 0
return util.computeBoundingBox($contents.children())
boxFrame =
top : Number.POSITIVE_INFINITY
left : Number.POSITIVE_INFINITY
right : Number.NEGATIVE_INFINITY
bottom : Number.NEGATIVE_INFINITY
for el in $contents
$el = $(el)
continue unless $el.is(':visible')
frame = $el.offset()
frame.right = frame.left + $el.outerWidth()
frame.bottom = frame.top + $el.outerHeight()
boxFrame.top = frame.top if frame.top < boxFrame.top
boxFrame.left = frame.left if frame.left < boxFrame.left
boxFrame.right = frame.right if frame.right > boxFrame.right
boxFrame.bottom = frame.bottom if frame.bottom > boxFrame.bottom
return {
left : boxFrame.left
top : boxFrame.top
width : boxFrame.right - boxFrame.left
height : boxFrame.bottom - boxFrame.top
}