Skip to content

Commit

Permalink
Added a new widget type - Interactive Table.
Browse files Browse the repository at this point in the history
	modified:   js_dependencies/table.css
	modified:   src/widgets.jl
  • Loading branch information
abcdvvvv committed Jan 19, 2025
1 parent c36f481 commit b8494d2
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 9 deletions.
115 changes: 106 additions & 9 deletions js_dependencies/table.css
Original file line number Diff line number Diff line change
@@ -1,26 +1,123 @@
/* ============================== */
/* Default-theme */
/* ============================== */
table {
font-family: Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
font-family: Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
color: #000000;
}

table td,
table th {
padding: 1px 5px;
resize: horizontal;
/* Drag-and-drop column widths */
overflow: auto;
min-width: 100px;
/* Give an initial column width */
border: 1px solid #dddddd;
/* add a border */
}

table tr:nth-child(even) {
background-color: #f2f2f2;
}

td, th, tr {
table tr:hover {
background-color: #dddddd;
}

table th {
padding-top: 10px;
padding-bottom: 10px;
text-align: left;
background-color: #e2e2e2;
color: #000000;
/* font color */
}


/* ============================== */
/* Light-theme */
/* ============================== */
.light-theme table {
font-family: Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
color: #000000;
}

.light-theme table td,
.light-theme table th {
padding: 1px 5px;
resize: horizontal; /* Drag-and-drop column widths */
overflow: auto;
min-width: 100px; /* Give an initial column width */
border: 1px solid #dddddd; /* add a border */
}

tr:nth-child(even) {
.light-theme table tr:nth-child(even) {
background-color: #f2f2f2;
}

tr:hover {
background-color: #ddd;
.light-theme table tr:hover {
background-color: #dddddd;
}

th {
.light-theme table th {
padding-top: 10px;
padding-bottom: 10px;
text-align: left;
background-color: #e2e2e2;
color: white;
color: #000000; /* font color */
}

.light-theme table input {
background-color: rgba(255, 255, 255, 0.5);
color: #000; /* font color */
font-size: 14px;
}


/* ============================== */
/* Dark-theme */
/* ============================== */
.dark-theme table {
font-family: Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
color: #ffffff;
background-color: #2c2c2c;
}

.dark-theme table td,
.dark-theme table th {
padding: 1px 5px;
resize: horizontal;
overflow: auto;
min-width: 100px;
border: 1px solid #555555;
}

.dark-theme table tr:nth-child(even) {
background-color: #3a3a3a;
}

.dark-theme table tr:hover {
background-color: #444444;
}

.dark-theme table th {
padding-top: 10px;
padding-bottom: 10px;
text-align: left;
background-color: #555555;
color: #ffffff;
}

.dark-theme table input {
background-color: rgba(255, 255, 255, 0);
color: #fff; /* font color */
font-size: 14px;
}
93 changes: 93 additions & 0 deletions src/widgets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,57 @@ function Table(table; class="", row_renderer=render_row_value)
return Table(table, class, row_renderer)
end

struct InteractTable
table
cell_obs::Vector{Vector{Observable}}
colnames::Tuple
coltypes::Tuple

function InteractTable(tbl)
# construct cell_obs
colnames = Tables.schema(tbl).names
coltypes = Tables.schema(tbl).types

coltable = Tables.columntable(tbl)
ncols = length(colnames)
nrows = length(getproperty(coltable, colnames[1]))

# creat nrows×ncols Observable
cell_obs = Vector{Vector{Observable}}(undef, ncols)
for (j, colname) in enumerate(colnames)
colvec = getproperty(coltable, colname)
cell_obs[j] = [Observable(colvec[i]) for i in 1:nrows]
end

autowrite!(tbl, cell_obs, colnames, ncols, nrows)
return new(tbl, cell_obs, colnames, coltypes)
end
end

"""
Automatic write-back function for editable tables. It is used to synchronize the output from the frontend to the backend.
"""
function autowrite!(tbl, cell_obs, colnames, ncols, nrows)
# Now the function can only be used for `tbl isa Dict && tbl[colname] isa Vector`
if tbl isa Dict
# Names in Tables.schema() are Symbols, but we want to use strings for the Dict keys.
for j in 1:ncols
colkey = string(colnames[j])
if haskey(tbl, colkey)
colvec = tbl[colkey]
@assert colvec isa AbstractVector "tbl[colname] is not a vector."
for i in 1:nrows
obs = cell_obs[j][i]
on(obs) do newval
colvec[i] = newval # tbl[colkey][i] is okay for Dict
end
end
end
end
end
# else: This is left blank to extend other types (e.g. NamedTuple, DataFrame, CustomTable).
end

function jsrender(session::Session, table::Table)
names = string.(Tables.schema(table.table).names)
header = DOM.thead(DOM.tr(DOM.th.(names)...))
Expand All @@ -422,6 +473,48 @@ function jsrender(session::Session, table::Table)
)
end

function jsrender(session::Session, table::InteractTable)
(; cell_obs, colnames, coltypes) = table
header = DOM.thead(DOM.tr(DOM.th.(string.(colnames))...))
ncols, nrows = length(colnames), length(cell_obs[1])

# Construct an editable <tbody>
tr_nodes = Vector{Hyperscript.Node{Hyperscript.HTMLSVG}}(undef, nrows)
for i in 1:nrows
td_nodes = Vector{Hyperscript.Node{Hyperscript.HTMLSVG}}(undef, ncols)
for j in 1:ncols
obs = cell_obs[j][i]
coltype = coltypes[j]
if coltype <: Number
input_type = "number"
parse_js = js""" e => {
const val = parseFloat(e.target.value);
if (!isNaN(val)) {
$(obs).notify(val);
}}
"""
else
input_type = "text"
parse_js = js"e => {$(obs).notify(e.target.value);}"
end
# <input "text/number">
input_el = DOM.input(; type=input_type, value=obs, onchange=parse_js)
# Use onjs to listen to cell_obs (backend), synchronizing the frontend input_el.value when it changes.
onjs(session, obs, js"v => event.srcElement.value = String(v)")

td_nodes[j] = DOM.td(input_el)
end
tr_nodes[i] = DOM.tr(td_nodes...)
end

body = DOM.tbody(tr_nodes...)

return DOM.div(
jsrender(session, Asset(Bonito.dependency_path("table.css"))),
DOM.table(header, body),
)
end

struct CodeEditor
theme::String
language::String
Expand Down

0 comments on commit b8494d2

Please sign in to comment.