class ERD
constructor: (@name, @elem, @edges) ->
@paper = Raphael(@name, @elem.data('svg_width'), @elem.data('svg_height'))
@setup_handlers()
models = @elem.find('.model')
@models = {}
for model in models
@models[$(model).data('model_name')] = model
@connect_arrows(@edges)
upsert_change: (action, model, column, from, to) ->
rows = ($(tr).find('td') for tr in $('#changes > tbody > tr'))
existing = null
$(rows).each (i, row) ->
existing = row if (action == $(row[0]).html()) && (model == $(row[1]).html()) && (column == $(row[2]).html())
if existing == null
$('#changes > tbody').append("""
#{action} |
#{model} |
#{column} |
#{from} |
#{to} |
""")
else
$(existing[3]).text(from)
$(existing[4]).text(to)
$('#changes').show()
positions: (div) ->
[left, width, top, height] = [parseFloat(div.css('left')), parseFloat(div.css('width')), parseFloat(div.css('top')), parseFloat(div.css('height'))]
{left: left, right: left + width, top: top, bottom: top + height, center: {x: (left + left + width) / 2, y: (top + top + height) / 2}, vertex: {}}
connect_arrows: (edges) =>
$.each edges, (i, edge) =>
@connect_arrow edge, $(@models[edge.from]), $(@models[edge.to])
connect_arrow: (edge, from_elem, to_elem) ->
#TODO handle self referential associations
return if from_elem.attr('id') == to_elem.attr('id')
edge.path.remove() if edge.path?
from = @positions(from_elem)
to = @positions(to_elem)
#FIXME terrible code
a = (to.center.y - from.center.y) / (to.center.x - from.center.x)
b = from.center.y - from.center.x * a
x2y = (x) -> ( a * x + b )
y2x = (y) -> ( (y - b) / a )
if from.center.x > to.center.x
[from.vertex.x, from.vertex.y] = [from.left, x2y(from.left)]
[to.vertex.x, to.vertex.y] = [to.right, x2y(to.right)]
else
[from.vertex.x, from.vertex.y] = [from.right, x2y(from.right)]
[to.vertex.x, to.vertex.y] = [to.left, x2y(to.left)]
for rect in [from, to]
if rect.vertex.y < rect.top
[rect.vertex.x, rect.vertex.y, rect.vertex.direction] = [y2x(rect.top), rect.top, 'v']
else if rect.vertex.y > rect.bottom
[rect.vertex.x, rect.vertex.y, rect.vertex.direction] = [y2x(rect.bottom), rect.bottom, 'v']
else
from.vertex.direction = 'h'
if from.vertex.direction == 'h'
path = "M#{Math.floor(from.vertex.x)} #{Math.floor(from.vertex.y)}H#{Math.floor((from.vertex.x + to.vertex.x) / 2)} V#{Math.floor(to.vertex.y)} H#{Math.floor(to.vertex.x)}"
else
path = "M#{Math.floor(from.vertex.x)} #{Math.floor(from.vertex.y)}V#{Math.floor((from.vertex.y + to.vertex.y) / 2)} H#{Math.floor(to.vertex.x)} V#{Math.floor(to.vertex.y)}"
edge.path = @paper.path(path).attr({'stroke-width': 2, opacity: 0.5, 'arrow-end': 'classic-wide-long'})
setup_handlers: ->
@setup_click_handlers()
@setup_submit_handlers()
@setup_migration_event_handlers()
$('div.model').draggable(drag: @handle_drag)
handle_drag: (ev, ui) =>
target = $(ev.target)
target.addClass('noclick')
model_name = target.data('model_name')
from = target.data('original_position')
to = [target.css('left').replace(/px$/, ''), target.css('top').replace(/px$/, '')].join()
@upsert_change 'move', model_name, '', '', to
@connect_arrows(@edges.filter((e)-> e.from == model_name || e.to == model_name))
setup_click_handlers: ->
$('div.model_name_text, span.column_name_text, span.column_type_text').on 'click', @handle_text_elem_click
$('div.model a.add_column').on 'click', @handle_add_column_click
$('div.model a.cancel').on 'click', @handle_cancel_click
$('div.model a.close').on 'click', @handle_remove_model_click
$('#new_model_add_column').on 'click', @handle_new_model_add_column_click
$('div.model a.cancel').on 'click', @handle_cancel_click
$('div#open_migration').on 'click', @handle_open_migration_click
$('div#close_migration').on 'click', @handle_close_migration_click
setup_submit_handlers: ->
$('form.rename_model_form').on 'submit', @handle_rename_model
$('form.rename_column_form').on 'submit', @handle_rename_column
$('form.alter_column_form').on 'submit', @handle_change_column_type
$('form.add_column_form').on 'submit', @handle_add_column
$('#changes_form').on 'submit', @handle_save
setup_migration_event_handlers: ->
$('#migration_status tr input').on 'click', ->
$(this).parents('tr').toggleClass('active')
$('#migration_status thead td button').on 'click', (ev) ->
ev.preventDefault()
$('#migration_status').toggleClass('show_all_migrations')
handle_save: (ev) =>
changes = $('#changes > tbody > tr').map(->
change = {}
$(this).find('td').each ->
name = $(this).data('name')
value = $(this).html()
change[name] = value
change
).toArray()
$('#changes_form').find('input[name=changes]').val(JSON.stringify(changes))
handle_add_column: (ev) =>
ev.preventDefault()
target = $(ev.target)
name = target.find('input[name=name]').val()
return if name == ''
model = target.find('input[name=model]').val()
type = target.find('input[name=type]').val()
@upsert_change 'add_column', model, "#{name}(#{type})", '', ''
name_span = $("", class: 'column_name_text')
.append(name)
type_span = $("", class: 'column_type_text unsaved')
.append(type)
li_node = $("", class: 'column unsaved').append(name_span).append(" ").append(type_span)
target.hide()
.parent()
.siblings('.columns')
.find('ul').append(li_node).end()
.end()
.find('a.add_column').show()
handle_change_column_type: (ev) =>
ev.preventDefault()
target = $(ev.target)
to = target.find('input[name=to]').val()
return if to == ''
model = target.find('input[name=model]').val()
column = target.find('input[name=column]').val()
type = target.find('input[name=type]').val()
if to != type
@upsert_change 'alter_column', model, column, type, to
target.hide()
.siblings('.column_type_text').text(to).show().addClass('unsaved')
.parents('.column').addClass('unsaved')
handle_rename_column: (ev) =>
ev.preventDefault()
target = $(ev.target)
to = target.find('input[name=to]').val()
return if to == ''
model = target.find('input[name=model]').val()
column = target.find('input[name=column]').val()
if to != column
@upsert_change 'rename_column', model, column, column, to
target.hide()
.siblings('.column_name_text').text(to).show()
.parents('.column').addClass('unsaved')
handle_rename_model: (ev) =>
ev.preventDefault()
target = $(ev.target)
to = target.find('input[name=to]').val()
return if to == ''
model = target.find('input[name=model]').val()
if to != model
@upsert_change 'rename_model', model, '', model, to
target.hide()
.siblings('.model_name_text').text(to).show().addClass('unsaved')
handle_add_column_click: (ev) =>
ev.preventDefault()
target = $(ev.currentTarget)
m = target.parents('div.model')
if m.hasClass('noclick')
m.removeClass('noclick')
return false
target.hide()
.next('form').show()
.find('a.cancel').show().end()
.find('input[name=type]').val('string').end()
.find('input[name=name]').val('').focus()
handle_cancel_click: (ev) =>
ev.preventDefault()
target = $(ev.currentTarget)
m = target.parents('div.model')
if m.hasClass('noclick')
m.removeClass('noclick')
return false
target.hide()
.parent('form').hide()
.prev('a.add_column, span, div').show()
handle_text_elem_click: (ev) =>
target = $(ev.currentTarget)
text = target.text()
m = target.parents('div.model')
if m.hasClass('noclick')
m.removeClass('noclick')
return false
target.hide()
.next('form').show()
.find('a.cancel').show().end()
.find('input[name=to]').val(text).focus()
handle_remove_model_click: (ev) =>
ev.preventDefault()
target = $(ev.target)
parent = target.parent()
m = target.parents('div.model')
if m.hasClass('noclick')
m.removeClass('noclick')
return false
return unless confirm('remove this table?')
model_name = m.data('model_name')
window.erd.upsert_change 'remove_model', model_name, '', '', ''
parent.hide()
$.each @edges, (i, edge) =>
@edges.splice i, 1 if (edge.from == model_name) || (edge.to == model_name)
@paper.clear()
@connect_arrows(@edges)
handle_new_model_add_column_click: (ev) =>
ev.preventDefault()
target = $(ev.currentTarget)
target.parent().siblings('table').append(' | : | |
').find('tr:last > td > input:first').focus()
handle_open_migration_click: (ev) =>
ev.preventDefault()
target = $(ev.currentTarget)
text = target.text()
m = target.parents('div.model')
if m.hasClass('noclick')
m.removeClass('noclick')
return false
target.hide()
.next('div').show()
.find('#close_migration').show()
handle_close_migration_click: (ev) =>
ev.preventDefault()
target = $(ev.currentTarget)
text = target.text()
m = target.parents('div.model')
if m.hasClass('noclick')
m.removeClass('noclick')
return false
target.hide()
.parent().hide()
.prev('div').show()
$ ->
window.erd = new ERD('erd', $('#erd'), window.raw_edges)
$('#erd').css('height', window.innerHeight)
$(window).on 'resize', ->
$('#erd').css('height', window.innerHeight)
$("#open_migration").click ->
$('#close_migration, #open_create_model_dialog').css('right', $('#migration').width() + ($(this).width() / 2) - 5)
$("#close_migration").click ->
$('#open_create_model_dialog').css('right', 15)
$('#open_up').click ->
$('#migration_status .up').addClass('open')
$('#migration_status .down').removeClass('open')
$('#open_down').click ->
$('#migration_status .down').addClass('open')
$('#migration_status .up').removeClass('open')
$('#close_all').click ->
$('#migration_status tr').removeClass('open')
$('#create_model_form').dialog
autoOpen: false,
height: 450,
width: 450,
modal: true,
buttons:
'Create Model': ->
model = $('#new_model_name').val()
columns = ''
$('#create_model_table > tbody > tr').each (i, row) ->
[name, type] = ($(v).val() for v in $(row).find('input'))
columns += "#{name}#{if type then ":#{type}" else ''} " if name
window.erd.upsert_change 'create_model', model, columns, '', ''
$(this).find('table > tbody > tr').each (i, row) ->
row.remove() if i >= 1
$(this).find('input').val('')
$(this).find('input[name=new_model_column_type_1]').val('string')
$(this).dialog('close')
Cancel: ->
$(this).dialog('close')
$('#open_create_model_dialog').click (ev) ->
ev.preventDefault()
$('#create_model_form').dialog('open')