@Mercury.tableEditor = (table, cell, cellContent) ->
Mercury.tableEditor.load(table, cell, cellContent)
return Mercury.tableEditor
jQuery.extend Mercury.tableEditor,
load: (@table, @cell, @cellContent = '') ->
@row = @cell.parent('tr')
@columnCount = @getColumnCount()
@rowCount = @getRowCount()
addColumn: (position = 'after') ->
sig = @cellSignatureFor(@cell)
for row, i in @table.find('tr')
rowSpan = 1
matchOptions = if position == 'after' then {right: sig.right} else {left: sig.left}
if matching = @findCellByOptionsFor(row, matchOptions)
newCell = jQuery("<#{matching.cell.get(0).tagName}>").html(@cellContent)
@setRowspanFor(newCell, matching.height)
if position == 'before' then matching.cell.before(newCell) else matching.cell.after(newCell)
i += matching.height - 1
else if intersecting = @findCellByIntersectionFor(row, sig)
@setColspanFor(intersecting.cell, intersecting.width + 1)
removeColumn: ->
sig = @cellSignatureFor(@cell)
return if sig.width > 1
removing = []
adjusting = []
for row, i in @table.find('tr')
if matching = @findCellByOptionsFor(row, {left: sig.left, width: sig.width})
removing.push(matching.cell)
i += matching.height - 1
else if intersecting = @findCellByIntersectionFor(row, sig)
adjusting.push(intersecting.cell)
jQuery(cell).remove() for cell in removing
@setColspanFor(cell, @colspanFor(cell) - 1) for cell in adjusting
addRow: (position = 'after') ->
newRow = jQuery('
')
if (rowspan = @rowspanFor(@cell)) > 1 && position == 'after'
@row = jQuery(@row.nextAll('tr')[rowspan - 2])
cellCount = 0
for cell in @row.find('th, td')
colspan = @colspanFor(cell)
newCell = jQuery("<#{cell.tagName}>").html(@cellContent)
@setColspanFor(newCell, colspan)
cellCount += colspan
if (rowspan = @rowspanFor(cell)) > 1 && position == 'after'
@setRowspanFor(cell, rowspan + 1)
continue
newRow.append(newCell)
if cellCount < @columnCount
rowCount = 0
for previousRow in @row.prevAll('tr')
rowCount += 1
for cell in jQuery(previousRow).find('td[rowspan], th[rowspan]')
rowspan = @rowspanFor(cell)
if rowspan - 1 >= rowCount && position == 'before'
@setRowspanFor(cell, rowspan + 1)
else if rowspan - 1 >= rowCount && position == 'after'
if rowspan - 1 == rowCount
newCell = jQuery("<#{cell.tagName}>").html(@cellContent)
@setColspanFor(newCell, @colspanFor(cell))
newRow.append(newCell)
else
@setRowspanFor(cell, rowspan + 1)
if position == 'before' then @row.before(newRow) else @row.after(newRow)
removeRow: ->
# check to see that all cells have the same rowspan, and figure out the minimum rowspan
rowspansMatch = true
prevRowspan = 0
minRowspan = 0
for cell in @row.find('td, th')
rowspan = @rowspanFor(cell)
rowspansMatch = false if prevRowspan && rowspan != prevRowspan
minRowspan = rowspan if rowspan < minRowspan || !minRowspan
prevRowspan = rowspan
return if !rowspansMatch && @rowspanFor(@cell) > minRowspan
# remove any emtpy rows below
if minRowspan > 1
jQuery(@row.nextAll('tr')[i]).remove() for i in [0..minRowspan - 2]
# find and move down any cells that have a larger rowspan
for cell in @row.find('td[rowspan], th[rowspan]')
sig = @cellSignatureFor(cell)
continue if sig.height == minRowspan
if match = @findCellByOptionsFor(@row.nextAll('tr')[minRowspan - 1], {left: sig.left, forceAdjacent: true})
@setRowspanFor(cell, @rowspanFor(cell) - @rowspanFor(@cell))
if match.direction == 'before' then match.cell.before(jQuery(cell).clone()) else match.cell.after(jQuery(cell).clone())
if @columnsFor(@row.find('td, th')) < @columnCount
# move up rows looking for cells with rowspans that might intersect
rowsAbove = 0
for aboveRow in @row.prevAll('tr')
rowsAbove += 1
for cell in jQuery(aboveRow).find('td[rowspan], th[rowspan]')
# if the cell intersects with the row we're trying to calculate on, and it's index is less than where we've
# gotten so far, add it
rowspan = @rowspanFor(cell)
@setRowspanFor(cell, rowspan - @rowspanFor(@cell)) if rowspan > rowsAbove
@row.remove()
increaseColspan: ->
cell = @cell.next('td, th')
return unless cell.length
return if @rowspanFor(cell) != @rowspanFor(@cell)
return if @cellIndexFor(cell) > @cellIndexFor(@cell) + @colspanFor(@cell)
@setColspanFor(@cell, @colspanFor(@cell) + @colspanFor(cell))
cell.remove()
decreaseColspan: ->
return if @colspanFor(@cell) == 1
@setColspanFor(@cell, @colspanFor(@cell) - 1)
newCell = jQuery("<#{@cell.get(0).tagName}>").html(@cellContent)
@setRowspanFor(newCell, @rowspanFor(@cell))
@cell.after(newCell)
increaseRowspan: ->
sig = @cellSignatureFor(@cell)
nextRow = @row.nextAll('tr')[sig.height - 1]
if nextRow && match = @findCellByOptionsFor(nextRow, {left: sig.left, width: sig.width})
@setRowspanFor(@cell, sig.height + match.height)
match.cell.remove()
decreaseRowspan: ->
sig = @cellSignatureFor(@cell)
return if sig.height == 1
nextRow = @row.nextAll('tr')[sig.height - 2]
if match = @findCellByOptionsFor(nextRow, {left: sig.left, forceAdjacent: true})
newCell = jQuery("<#{@cell.get(0).tagName}>").html(@cellContent)
@setColspanFor(newCell, @colspanFor(@cell))
@setRowspanFor(@cell, sig.height - 1)
if match.direction == 'before' then match.cell.before(newCell) else match.cell.after(newCell)
# Counts the columns of the first row (alpha row) in the table. We can safely rely on the first row always being
# comprised of a full set of cells or cells with colspans.
getColumnCount: ->
return @columnsFor(@table.find('thead tr:first-child, tbody tr:first-child, tfoot tr:first-child').first().find('td, th'))
# Counts the rows of the table.
getRowCount: ->
return @table.find('tr').length
# Gets the index for a given cell, taking into account that rows above it can have cells that have rowspans.
cellIndexFor: (cell) ->
cell = jQuery(cell)
# get the row for the cell and calculate all the columns in it
row = cell.parent('tr')
columns = @columnsFor(row.find('td, th'))
index = @columnsFor(cell.prevAll('td, th'))
# if the columns is less than expected, we need to look above for rowspans
if columns < @columnCount
# move up rows looking for cells with rowspans that might intersect
rowsAbove = 0
for aboveRow in row.prevAll('tr')
rowsAbove += 1
for aboveCell in jQuery(aboveRow).find('td[rowspan], th[rowspan]')
# if the cell intersects with the row we're trying to calculate on, and it's index is less than where we've
# gotten so far, add it
if @rowspanFor(aboveCell) > rowsAbove && @cellIndexFor(aboveCell) <= index
index += @colspanFor(aboveCell)
return index
# Creates a signature for a given cell, which is made up if it's size, and itself.
cellSignatureFor: (cell) ->
sig = {cell: jQuery(cell)}
sig.left = @cellIndexFor(cell)
sig.width = @colspanFor(cell)
sig.height = @rowspanFor(cell)
sig.right = sig.left + sig.width
return sig
# Find a cell based on options. Options can be:
# right
# or
# left, [width], [forceAdjacent]
# eg. findCellByOptionsFor(@row, {left: 1, width: 2, forceAdjacent: true})
findCellByOptionsFor: (row, options) ->
for cell in jQuery(row).find('td, th')
sig = @cellSignatureFor(cell)
if typeof(options.right) != 'undefined'
return sig if sig.right == options.right
if typeof(options.left) != 'undefined'
if options.width
return sig if sig.left == options.left && sig.width == options.width
else if !options.forceAdjacent
return sig if sig.left == options.left
else if options.forceAdjacent
if sig.left > options.left
prev = jQuery(cell).prev('td, th')
if prev.length
sig = @cellSignatureFor(prev)
sig.direction = 'after'
else
sig.direction = 'before'
return sig
if options.forceAdjacent
sig.direction = 'after'
return sig
return null
# Finds a cell that intersects with the current signature
findCellByIntersectionFor: (row, signature) ->
for cell in jQuery(row).find('td, th')
sig = @cellSignatureFor(cell)
return sig if sig.right - signature.left >= 0 && sig.right > signature.left
return null
# Counts all the columns in a given array of columns, taking colspans into
# account.
columnsFor: (cells) ->
count = 0
count += @colspanFor(cell) for cell in cells
return count
# Tries to get the colspan of a cell, falling back to 1 if there's none
# specified.
colspanFor: (cell) ->
return parseInt(jQuery(cell).attr('colspan')) || 1
# Tries to get the rowspan of a cell, falling back to 1 if there's none
# specified.
rowspanFor: (cell) ->
return parseInt(jQuery(cell).attr('rowspan')) || 1
# Sets the colspan of a cell, removing it if it's 1.
setColspanFor: (cell, value) ->
jQuery(cell).attr('colspan', if value > 1 then value else null)
# Sets the rowspan of a cell, removing it if it's 1
setRowspanFor: (cell, value) ->
jQuery(cell).attr('rowspan', if value > 1 then value else null)