behavior DataTableKeyboardNav(input)
def _moveFocus(rowModifier, colModifier)
set row to (@data-row of input) as Int
set col to (@data-col of input) as Int
set row to row + rowModifier
set col to col + colModifier
set dataTable to the closest parent
get the first in the dataTable
if it exists
call it.focus()
remove .editing from .editing
end
end
on keydown from input
if event.key is 'ArrowLeft' then _moveFocus(0, -1)
else if event.key is 'ArrowRight' then _moveFocus(0, 1)
else if event.key is 'ArrowUp' then _moveFocus(-1, 0)
else if event.key is 'ArrowDown' then _moveFocus(1, 0)
end
end
end
behavior DataTableModalInput(input)
def _moveFocus(rowModifier, colModifier)
get the closest parent <[data-row] />
set row to its dataset.row as Int
get the closest parent <[data-col] />
set col to its dataset.col as Int
set row to row + rowModifier
set col to col + colModifier
set dataTable to the closest parent <.data-table />
set headerRow to the first <.header.row /> in dataTable
set cols to <[data-col] /> in headerRow
set rows to <[data-row] /> in dataTable
get the first <[data-row='${row}']>[data-col='${col}']>input[type='text'] /> in the dataTable
if it exists
call it.focus()
remove .editing from .editing
end
end
on moveLeft from input
_moveFocus(0, -1)
end
on moveRight from input
_moveFocus(0, 1)
end
on moveUp from input
_moveFocus(-1, 0)
end
on moveDown from input
_moveFocus(1, 0)
end
on keydown from input
if event.key is 'Tab'
event.preventDefault()
if event.shiftKey == true
_moveFocus(0, -1)
else
_moveFocus(0, 1)
end
exit
end
if event.key is 'Enter'
if event.shiftKey == true
_moveFocus(-1, 0)
else
_moveFocus(1, 0)
end
exit
end
if input matches .editing
if event.key is 'Escape'
remove .editing from input
make a CustomEvent from 'update', { detail: { source: input } } called updateEvent
get the closest parent .data-table
call it.dispatchEvent(updateEvent)
end
exit
else
if event.key is 'ArrowLeft' then _moveFocus(0, -1)
else if event.key is 'ArrowRight' then _moveFocus(0, 1)
else if event.key is 'ArrowUp' then _moveFocus(-1, 0)
else if event.key is 'ArrowDown' then _moveFocus(1, 0)
else if event.key is 'Backspace'
event.preventDefault()
_moveFocus(0, -1)
else if event.key is 'Delete'
event.preventDefault()
_moveFocus(1, 0)
get the closest parent <.row />
send deleteRow to it
end
exit
end
end
on input from input
if input does not match .editing
if event.data is not null
set the value of input to event.data
end
remove .editing from .editing
add .editing to input
end
get the closest parent <.row />
if it matches .placeholder
send newRow to it
end
end
on click from input
if input does not match .editing
remove .editing from .editing
end
end
on dblclick from input
if input does not match .editing
_beginEditing()
end
end
on focus from input
set dataTable to the closest parent <.data-table />
send cellFocus to the dataTable
end
on blur from input
set dataTable to the closest parent <.data-table />
send cellBlur to the dataTable
if input matches .editing
remove .editing from input
make a CustomEvent from 'update', { detail: { source: input } } called updateEvent
get the closest parent .data-table
call it.dispatchEvent(updateEvent)
end
end
set :isMouseOver to false
on mouseenter
set :isMouseOver to true
end
on mouseleave
set :isMouseOver to false
end
on pointerdown
if input matches .editing
exit
end
set meter to 0
repeat until event pointerup
if :isMouseOver is false
_resetBorderImage()
break
end
if meter > 20
set input.style.borderImage to `linear-gradient(to right, #fff $meter%, transparent $meter%) 100% 1`
end
if meter < 100
set meter to meter + 2
wait 10ms
continue
end
_beginEditing()
break
end
end
on pointerup
_resetBorderImage()
end
def _beginEditing()
_resetBorderImage()
remove .editing from .editing
add .editing to input
-- set caret position to end
set val to input.value
set input.value to ''
set input.value to val
end
def _resetBorderImage()
set input.style.borderImage to `linear-gradient(to right, #fff 0%, transparent 0%) 100% 1`
end
end
behavior DataTableFocusRectangle(dataTable)
init
call document.createElement('div')
add .focused-cell-box to it
put it at the start of dataTable
_updateFocusedCellBox()
end
on cellFocus
_updateFocusedCellBox()
end
on cellBlur
_updateFocusedCellBox()
end
on columnResize
_updateFocusedCellBox()
end
on resize from window
_updateFocusedCellBox()
end
def _updateFocusedCellBox()
set rect to dataTable.getBoundingClientRect()
set cellHasFocus to false
set el to document.activeElement
if dataTable contains el and el.tagName matches 'INPUT' and el.type is 'text'
set rect to el.getBoundingClientRect()
set cellHasFocus to true
end
get the first <.focused-cell-box /> in dataTable
set focusedCellBox to it
set focusedCellBoxStyles to window.getComputedStyle(focusedCellBox)
set focusedCellBoxBorderWidth to ((focusedCellBoxStyles.getPropertyValue('border-width') as Float) or 0)
set its style.width to (rect.width - (focusedCellBoxBorderWidth * 2)) + 'px'
set its style.height to (rect.height - (focusedCellBoxBorderWidth * 2)) + 'px'
set its style.left to (rect.left + window.scrollX) + 'px'
set its style.top to (rect.top + window.scrollY) + 'px'
set its style.opacity to 1
if cellHasFocus is false
set its style.opacity to 0
end
end
end
behavior DataTableColumnSizing(row)
init
set dataTable to the closest parent <.data-table />
set headerRow to the first <.header.row /> in dataTable
set cols to <[data-col] /> in headerRow
for col in cols
get the first <[data-col="${col.dataset.col}"] /> in row
set its style.width to col.dataset.colwidth
get the first in it
if it is not null
set its style.width to col.dataset.colwidth
end
end
send columnResize to dataTable
end
end
behavior DataTableRowMod(row)
on newRow
set dataTable to the closest parent <.data-table />
if row matches .placeholder
remove .placeholder from row
set inputs to in row
for input in inputs
remove @form from input
end
set rowNumberCell to the first <.row-number.cell /> in row
set dataTableRows to <.row:not(.header) /> in dataTable
set rowNumberCell.innerHTML to dataTableRows.length
set templateRow to the first <.placeholder-row-template /> in dataTable
call templateRow.cloneNode(true)
set newPlaceholderRow to it
get newPlaceholderRow@hx-delete
if it exists
make a String from it called hxDeleteEndpoint
set rowID to crypto.randomUUID()
set hxDeleteEndpoint to hxDeleteEndpoint.replace('', rowID)
set newPlaceholderRow@hx-delete to hxDeleteEndpoint
end
set cols to <[data-col] /> in newPlaceholderRow
for col in cols
get the first in col
if it exists
set input to it
make a String from input@hx-post called hxPostEndpoint
set hxPostEndpoint to hxPostEndpoint.replace('', rowID)
set input@hx-post to hxPostEndpoint
end
end
remove .placeholder-row-template from newPlaceholderRow
add .placeholder to newPlaceholderRow
add .row to newPlaceholderRow
set inputRowNumber to row.dataset.row
increment inputRowNumber
set newPlaceholderRow.dataset.row to inputRowNumber
put newPlaceholderRow at the end of dataTable
_updateRowNumbers()
call window.htmx.process(document.body)
end
send update to the dataTable
end
on deleteRow
if row matches .placeholder
exit
end
set rowNumber to row.dataset.row as Int
set dataTable to the closest parent <.data-table />
set nextRow to <[data-row="${rowNumber+1}" />
remove .row from row
_updateRowNumbers()
get row@hx-delete
if it exists
send deleted to row
hide row
else
remove row
end
get document.activeElement
get the closest parent <[data-row] /> to it
if it matches .placeholder
send moveUp to document.activeElement
end
send cellFocus to the dataTable
send update to the dataTable
end
def _updateRowNumbers()
set dataTable to the closest parent <.data-table />
set dataTableRows to <.row:not(.header) /> in dataTable
for r in dataTableRows index i
if r does not match .placeholder
get the first <.row-number /> in r
set its innerHTML to i+1
set dataCols to <[data-prop]/> in r
for c in dataCols
get the first in c
set its name to dataTable.dataset.prop + "[" + i + "][" + c.dataset.prop + "]"
end
end
set r.dataset.row to i
end
end
end