Skip to content

Commit

Permalink
Add new UI element: MultiCheckBox (#79)
Browse files Browse the repository at this point in the history
Co-authored-by: Fons van der Plas <[email protected]>
  • Loading branch information
LoganWalls and fonsp authored Mar 29, 2021
1 parent 5487059 commit eea7bbd
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ version = "0.7.4"
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Expand All @@ -17,6 +18,7 @@ Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
Reexport = "^1"
Suppressor = "^0.2.0"
julia = "^1"
JSON = "^0.21.1"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
24 changes: 24 additions & 0 deletions assets/multicheckbox.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.multicheckbox-container {
display: flex;
flex-wrap: wrap;
max-height: 8em;
}

.multicheckbox {
display: flex;
}

div.multicheckbox {
margin: 0.1em 0.3em;
align-items: center;
}

label.multicheckbox,
input.multicheckbox {
cursor: pointer;
}

.select-all {
font-style: italic;
color: hsl(0, 0%, 25%, 0.7);
}
119 changes: 119 additions & 0 deletions assets/multicheckbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
const container = (currentScript ? currentScript : this.currentScript)
.previousElementSibling;

// Add checkboxes
const inputEls = [];
for (let i = 0; i < labels.length; i++) {
const boxId = `box-${i}`;

const item = document.createElement("div");
item.classList.add("multicheckbox");

const checkbox = document.createElement("input");
checkbox.classList.add("multicheckbox");
checkbox.type = "checkbox";
checkbox.id = boxId;
checkbox.name = labels[i];
checkbox.value = values[i];
checkbox.checked = checked[i];
inputEls.push(checkbox);
item.appendChild(checkbox);

const label = document.createElement("label");
label.classList.add("multicheckbox");
label.htmlFor = boxId;
label.innerText = labels[i];
item.appendChild(label);

container.appendChild(item);
}

// Add listeners
function sendEvent() {
container.value = inputEls.filter((o) => o.checked).map((o) => o.value);
container.dispatchEvent(new CustomEvent("input"));
}

function updateSelectAll() {}

if (includeSelectAll) {
// Add select-all checkbox.
const selectAllItem = document.createElement("div");
selectAllItem.classList.add("multicheckbox");
selectAllItem.classList.add("select-all");

const selectAllInput = document.createElement("input");
selectAllInput.classList.add("multicheckbox");
selectAllInput.type = "checkbox";
selectAllInput.id = "select-all";
selectAllItem.appendChild(selectAllInput);

const selectAllLabel = document.createElement("label");
selectAllLabel.classList.add("multicheckbox");
selectAllLabel.htmlFor = "select-all";
selectAllLabel.innerText = "Select All";
selectAllItem.appendChild(selectAllLabel);

container.prepend(selectAllItem);

function onSelectAllClick(event) {
event.stopPropagation();
inputEls.forEach((o) => (o.checked = this.checked));
sendEvent();
}
selectAllInput.addEventListener("click", onSelectAllClick);

/// Taken from: https://stackoverflow.com/questions/10099158/how-to-deal-with-browser-differences-with-indeterminate-checkbox
/// Determine the checked state to give to a checkbox
/// with indeterminate state, so that it becomes checked
/// on click on IE, Chrome and Firefox 5+
function getCheckedStateForIndeterminate() {
// Create a unchecked checkbox with indeterminate state
const test = document.createElement("input");
test.type = "checkbox";
test.checked = false;
test.indeterminate = true;

// Try to click the checkbox
const body = document.body;
body.appendChild(test); // Required to work on FF
test.click();
body.removeChild(test); // Required to work on FF

// Check if the checkbox is now checked and cache the result
if (test.checked) {
getCheckedStateForIndeterminate = function () {
return false;
};
return false;
} else {
getCheckedStateForIndeterminate = function () {
return true;
};
return true;
}
}

updateSelectAll = function () {
const checked = inputEls.map((o) => o.checked);
if (checked.every((x) => x)) {
selectAllInput.checked = true;
selectAllInput.indeterminate = false;
} else if (checked.some((x) => x)) {
selectAllInput.checked = getCheckedStateForIndeterminate();
selectAllInput.indeterminate = true;
} else {
selectAllInput.checked = false;
selectAllInput.indeterminate = false;
}
};
// Call once at the beginning to initialize.
updateSelectAll();
}

function onItemClick(event) {
event.stopPropagation();
updateSelectAll();
sendEvent();
}
inputEls.forEach((el) => el.addEventListener("click", onItemClick));
131 changes: 131 additions & 0 deletions src/MultiCheckBox.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
### A Pluto.jl notebook ###
# v0.12.21

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
quote
local el = $(esc(element))
global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
el
end
end

# ╔═╡ 34012b14-d597-4b9d-b23d-66b638e4c282
using JSON: escape_string

# ╔═╡ a8c1e0d2-3604-4e1d-a87c-c8f5b86b79ed
md"""
# MultiCheckBox
"""

# ╔═╡ c8350f43-0d30-45d0-873b-ff56c5801ac1
md"""
## Definition
"""

# ╔═╡ 144bff17-30eb-458a-8e94-33e1f11edbeb
"Convert a Julia array to a JS array in string form."
function jsarray_string(a::AbstractVector{T}) where {T <: AbstractString}
string("[\"", join(map(escape_string, a), "\",\""), "\"]")
end

# ╔═╡ 91a08b98-52b5-4a2a-8180-7cba6d7232cd
function jsarray_string(a::AbstractVector{T}) where {T}
string("[", join(a, ","), "]")
end

# ╔═╡ 430e2c1a-832f-11eb-024a-13e3989fd7c2
begin
export MultiCheckBox

"""A group of checkboxes (`<input type="checkbox">`) - the user can choose enable or disable of the `options`, an array of `Strings`.
The value returned via `@bind` is a list containing the currently checked items.
See also: [`MultiSelect`](@ref).
`options` can also be an array of pairs `key::String => value::Any`. The `key` is returned via `@bind`; the `value` is shown.
`defaults` specifies which options should be checked initally.
`orientation` specifies whether the options should be arranged in `:row`'s `:column`'s.
`select_all` specifies whether or not to include a "Select All" checkbox.
# Examples
`@bind snacks MultiCheckBox(["🥕", "🐟", "🍌"]))`
`@bind snacks MultiCheckBox(["🥕" => "🐰", "🐟" => "🐱", "🍌" => "🐵"]; default=["🥕", "🍌"])`
`@bind animals MultiCheckBox(["🐰", "🐱" , "🐵", "🐘", "🦝", "🐿️" , "🐝", "🐪"]; orientation=:column, select_all=true)`"""
struct MultiCheckBox
options::Array{Pair{<:AbstractString,<:Any},1}
default::Union{Missing,AbstractVector{AbstractString}}
orientation::Symbol
select_all::Bool
end

MultiCheckBox(options::Array{<:AbstractString,1}; default=String[], orientation=:row, select_all=false) = MultiCheckBox([o => o for o in options], default, orientation, select_all)
MultiCheckBox(options::Array{<:Pair{<:AbstractString,<:Any},1}; default=String[], orientation=:row, select_all=false) = MultiCheckBox(options, default, orientation, select_all)

function Base.show(io::IO, ::MIME"text/html", multicheckbox::MultiCheckBox)
if multicheckbox.orientation == :column
flex_direction = "column"
elseif multicheckbox.orientation == :row
flex_direction = "row"
else
error("Invalid orientation $orientation. Orientation should be :row or :column")
end

js = read(joinpath(@__DIR__, "..", "assets", "multicheckbox.js"), String)
css = read(joinpath(@__DIR__, "..", "assets", "multicheckbox.css"), String)

labels = String[]
vals = String[]
default_checked = Bool[]
for (k, v) in multicheckbox.options
push!(labels, v)
push!(vals, k)
push!(default_checked, k in multicheckbox.default)
end

print(io, """
<multi-checkbox class="multicheckbox-container" style="flex-direction:$(flex_direction);"></multi-checkbox>
<script type="text/javascript">
const labels = $(jsarray_string(labels));
const values = $(jsarray_string(vals));
const checked = $(jsarray_string(default_checked));
const defaults = $(jsarray_string(multicheckbox.default));
const includeSelectAll = $(multicheckbox.select_all);
$(js)
</script>
<style type="text/css">
$(css)
</style>
""")
end

Base.get(multicheckbox::MultiCheckBox) = multicheckbox.default
end

# ╔═╡ 8bfaf4c8-557d-433e-a228-aac493746efc
@bind animals MultiCheckBox(["🐰", "🐱" , "🐵", "🐘", "🦝", "🐿️" , "🐝", "🐪"]; orientation=:column, select_all=true)

# ╔═╡ 8e9f3962-d86c-4e07-b5d3-f31ee5361ca2
animals

# ╔═╡ 60183ad1-4919-4402-83fb-d53b86dda0a6
MultiCheckBox(["🐰 &&\\a \$\$", "🐱" , "🐵", "🐘", "🦝", "🐿️" , "🐝", "🐪"])

# ╔═╡ Cell order:
# ╟─a8c1e0d2-3604-4e1d-a87c-c8f5b86b79ed
# ╠═8bfaf4c8-557d-433e-a228-aac493746efc
# ╠═8e9f3962-d86c-4e07-b5d3-f31ee5361ca2
# ╠═60183ad1-4919-4402-83fb-d53b86dda0a6
# ╟─c8350f43-0d30-45d0-873b-ff56c5801ac1
# ╠═430e2c1a-832f-11eb-024a-13e3989fd7c2
# ╠═34012b14-d597-4b9d-b23d-66b638e4c282
# ╠═144bff17-30eb-458a-8e94-33e1f11edbeb
# ╠═91a08b98-52b5-4a2a-8180-7cba6d7232cd
3 changes: 3 additions & 0 deletions src/PlutoUI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ include("./Terminal.jl")
include("./RangeSlider.jl")
include("./DisplayTricks.jl")

@reexport module MultiCheckBoxNotebook
include("./MultiCheckBox.jl")
end
@reexport module TableOfContentsNotebook
include("./TableOfContents.jl")
end
Expand Down

0 comments on commit eea7bbd

Please sign in to comment.