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