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()
$('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.close').on 'click', @handle_remove_model_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
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')
.append(type)
li_node = $("", class: 'column')
.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()
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()
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()
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('input[name=type]')
.val('string')
.end()
.find('input[name=name]')
.val('')
.focus()
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('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 = parent.data('model_name')
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)
$ ->
window.erd = new ERD('erd', $('#erd'), window.raw_edges)