Skip to content

Commit 85520ff

Browse files
committed
add TomSelect based selector
1 parent de7de06 commit 85520ff

File tree

5 files changed

+332
-47
lines changed

5 files changed

+332
-47
lines changed

NetworkDynamicsInspector/src/NetworkDynamicsInspector.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module NetworkDynamicsInspector
22

33
using Bonito: Bonito, @js_str, Asset, CSS, Styles,
4-
Grid, Card, DOM, Session
4+
Grid, Card, DOM, Session, ES6Module
55
using NetworkDynamics: NetworkDynamics, SII, EIndex, VIndex, Network,
66
get_metadata, has_metadata, get_position, has_position,
77
obssym, psym, sym, extract_nw
@@ -39,6 +39,8 @@ const APP_CSS = Asset(joinpath(ASSETS, "app.css"))
3939
# node fence see https://github.com/electron/electron/issues/254
4040
const NODE_FENCE = Asset(joinpath(ASSETS, "node_fence.js"))
4141
const NODE_UNFENCE = Asset(joinpath(ASSETS, "node_unfence.js"))
42+
const TOMSELECT_ESS = ES6Module(joinpath(ASSETS, "tomselect.js"))
43+
const TOMSELECT_CSS = Asset(joinpath(ASSETS, "tomselect.css"))
4244

4345
include("widgets.jl")
4446
include("graphplot.jl")

NetworkDynamicsInspector/src/utils.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,11 @@ end
129129

130130
function download_assets()
131131
assets = Dict(
132-
"jquery.js" => "https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js",
133-
"select2.css" => "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css",
134-
"select2.js" => "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js",
132+
"jquery.js" => "https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js",
133+
"select2.css" => "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css",
134+
"select2.js" => "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js",
135+
"tomselect.css" => "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/tom-select.css",
136+
"tomselect.js" => "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/tom-select.complete.min.js",
135137
)
136138
to_download = filter(x -> !isfile(joinpath(ASSETS, x.first)), assets)
137139

NetworkDynamicsInspector/src/widgets.jl

Lines changed: 204 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,6 @@ function Bonito.jsrender(session::Session, multiselect::MultiSelect)
358358

359359
onany(jsselection) do _jssel
360360
sel = jsselection_to_selection(multiselect.options[], _jssel)
361-
# @info "New jsselection triggers new selection:" _jssel sel
362361
if sel != multiselect.selection[]
363362
multiselect.selection[] = sel
364363
end
@@ -578,6 +577,210 @@ function _selection_to_jsselection(options, selection)
578577
end
579578
end
580579

580+
struct TomSelect{T}
581+
options::Observable{Vector{Union{T, OptionGroup{T}}}}
582+
selection::Observable{Vector{T}}
583+
placeholder::String
584+
multi::Bool
585+
option_to_string::Any
586+
class::String
587+
id::String
588+
function TomSelect(_options, _selection=nothing; T=Any, placeholder="", multi=true, option_to_string=repr, class="", id="")
589+
options = _options isa Observable ? _options : Observable{Vector{T}}(_options)
590+
selection = if isnothing(_selection)
591+
Observable(T[])
592+
elseif _selection isa Observable
593+
@assert _selection isa Observable{Vector{T}}
594+
_selection
595+
else
596+
Observable{Vector{T}}(_selection)
597+
end
598+
_class = multi ? "bonito-tomselect-multi" : "bonito-tomselect-single"
599+
if class != ""
600+
_class *= " " * class
601+
end
602+
if id == ""
603+
id = gendomid("tomselect")
604+
end
605+
new{T}(options, selection, placeholder, multi, option_to_string, _class, id)
606+
end
607+
end
608+
609+
function Bonito.jsrender(session::Session, tomselect::TomSelect{T}) where {T}
610+
# generate internal observables of js representation of options and selection
611+
tsoptions = Observable{Any}()
612+
tsselection = Observable{Vector{String}}(String[])
613+
614+
onany(tsselection) do _tssel
615+
sel = tsselection_to_selection(tomselect, _tssel)
616+
if sel != tomselect.selection[]
617+
@debug "MS \"$(tomselect.placeholder)\": New tsselection triggers new selection:" _tssel sel
618+
tomselect.selection[] = sel
619+
end
620+
nothing
621+
end
622+
623+
on(tomselect.selection; update=true) do _sel
624+
# check validity of selection
625+
if !tomselect.multi && length(_sel) > 1
626+
deleteat!(_sel, firstindex(_sel)+1:lastindex(_sel))
627+
end
628+
_tssel = selection_to_tsselection(tomselect, _sel)
629+
_validsel = tsselection_to_selection(tomselect, _tssel)
630+
tomselect.selection.val = _validsel
631+
_validtssel = selection_to_tsselection(tomselect, _validsel)
632+
633+
@debug "MS \"$(tomselect.placeholder)\": New selection trigger tsselection" _sel _validsel _validtssel
634+
# musst update tsselection because options may have changed
635+
tsselection[] = _validtssel
636+
637+
nothing
638+
end
639+
640+
on(tomselect.options, update=true) do _opts
641+
# update options on js side
642+
@debug "MS \"$(tomselect.placeholder)\": New options" _opts
643+
tsoptions[] = options_to_tsoptions(tomselect, _opts)
644+
notify(tomselect.selection)
645+
nothing
646+
end
647+
648+
649+
# Create a multi-select element
650+
style = Styles(
651+
"width" => "100%",
652+
)
653+
654+
domtype = tomselect.multi ? DOM.input : DOM.select
655+
tom_dom = domtype(;
656+
class=tomselect.class,
657+
style,
658+
id=tomselect.id,
659+
)
660+
661+
# node fence see https://github.com/electron/electron/issues/254
662+
container = DOM.div(
663+
TOMSELECT_ESS,
664+
TOMSELECT_CSS,
665+
tom_dom
666+
)
667+
668+
js_init = js"""
669+
($TOMSELECT_ESS).then((ts) => {
670+
const tom_dom = document.getElementById($(tomselect.id));
671+
if (!tom_dom) {
672+
console.error("TomSelect not found for id $(tomselect.id)");
673+
}
674+
675+
const settings = {
676+
options: $(tsoptions).value.opts,
677+
optgroups: $(tsoptions).value.groups,
678+
hideSelected: false,
679+
items: Array.from($(tsselection).value),
680+
optgroupField: 'class',
681+
placeholder: $(tomselect.placeholder),
682+
plugins: {},
683+
};
684+
// push remove button to plugins when multi
685+
if ($(tomselect.multi)) {
686+
settings.plugins.remove_button = {
687+
title: 'Remove this item'
688+
}
689+
}
690+
const tomselect = new TomSelect(tom_dom, settings);
691+
692+
function selEvent(){
693+
const newsel = Array.from(tomselect.items);
694+
console.log("Change event, update tsselection", newsel);
695+
$(tsselection).notify(newsel);
696+
}
697+
//tomselect.on('item_add', selEvent);
698+
//tomselect.on('item_remove', selEvent);
699+
tomselect.on('change', selEvent);
700+
// tomselect.on('item_select', selEvent); // click on tag?
701+
702+
$(tsoptions).on(val => {
703+
console.log("Got new options", val);
704+
tomselect.clear(true); // false -> silent
705+
tomselect.clearOptions();
706+
tomselect.clearOptionGroups();
707+
tomselect.addOptions(val.opts);
708+
val.groups.forEach(group => {
709+
tomselect.addOptionGroup(group.value, {label: group.label});
710+
});
711+
tomselect.refreshOptions(false); // false -> dont open
712+
console.log("Finished update of options")
713+
// seting value will update the tsselection
714+
// which triggers check in julia
715+
//console.log("Setting value in option update", $(tsselection).value);
716+
//tomselect.setValue($(tsselection).value);
717+
//tomselect.refreshItems();
718+
});
719+
720+
function array_equal(a1, a2) {
721+
if (a1 === a2) return true; // If both are the same reference
722+
if (a1 == null || a2 == null) return false; // If one is null or undefined
723+
if (a1.length !== a2.length) return false; // If lengths are different
724+
725+
return a1.every(function(val, i){return val === a2[i];})
726+
}
727+
728+
$(tsselection).on(val => {
729+
const current_items = Array.from(tomselect.items);
730+
if (!array_equal(val, current_items)) {
731+
console.log("Update displayed value of tomselect", val, current_items);
732+
tomselect.setValue(val, true); // do not notify
733+
//tomselect.refreshItems();
734+
}
735+
});
736+
});
737+
"""
738+
Bonito.evaljs(session, js_init)
739+
740+
return Bonito.jsrender(session, container)
741+
end
742+
743+
function options_to_tsoptions(tomselect, options)
744+
to_string = tomselect.option_to_string
745+
tsoptions = []
746+
jsgroups = []
747+
for option in options
748+
if option isa OptionGroup
749+
class = option.label
750+
push!(jsgroups, (;value=option.label, label=option.label))
751+
for suboption in option.options
752+
text = to_string(suboption)
753+
push!(tsoptions, (;class, value=text, text=text))
754+
end
755+
else
756+
text = to_string(option)
757+
push!(tsoptions, (;value=text, text=text))
758+
end
759+
end
760+
(; opts=tsoptions, groups=jsgroups)
761+
end
762+
763+
function tsselection_to_selection(tomselect::TomSelect{T}, jsselection) where {T}
764+
isempty(jsselection) && return T[]
765+
newsel = _tsselection_to_selection.(Ref(tomselect.options[]), jsselection, tomselect.option_to_string)
766+
filter(!isnothing, newsel)
767+
end
768+
function _tsselection_to_selection(options, tsselection::String, tostring)
769+
for option in options
770+
if option isa OptionGroup
771+
for suboption in option.options
772+
tostring(suboption) == tsselection && return suboption
773+
end
774+
else
775+
tostring(option) == tsselection && return option
776+
end
777+
end
778+
end
779+
function selection_to_tsselection(tomselect, selection)
780+
tomselect.option_to_string.(selection)
781+
end
782+
783+
581784
@kwdef struct ToggleSwitch
582785
value::Observable{Bool} = Observable{Bool}()
583786
height::Int = 24

0 commit comments

Comments
 (0)