diff --git a/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj b/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj
index 778e3e02..671ee9c2 100644
--- a/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj
+++ b/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj
@@ -84,6 +84,7 @@
+
diff --git a/src/Aardvark.UI.Primitives/Primitives/SimplePrimitives.fs b/src/Aardvark.UI.Primitives/Primitives/SimplePrimitives.fs
index fdfc4be9..3aa97014 100644
--- a/src/Aardvark.UI.Primitives/Primitives/SimplePrimitives.fs
+++ b/src/Aardvark.UI.Primitives/Primitives/SimplePrimitives.fs
@@ -619,6 +619,81 @@ module SimplePrimitives =
)
)
+ let private accordionImpl (toggle: Option int -> 'msg>) (active: Choice, aval>)
+ (attributes: AttributeMap<'msg>) (sections: alist>) =
+ let dependencies =
+ Html.semui @ [ { name = "accordion"; url = "resources/accordion.js"; kind = Script }]
+
+ let attributes =
+ let basic =
+ AttributeMap.ofList [
+ clazz "ui accordion"
+
+ if toggle.IsSome then
+ onEvent "onopen" [] (List.head >> int >> toggle.Value true)
+ onEvent "onclose" [] (List.head >> int >> toggle.Value false)
+ ]
+
+ AttributeMap.union attributes basic
+
+ let boot =
+ let exclusive =
+ match active with
+ | Choice2Of2 _ -> "true"
+ | _ -> "false"
+
+ String.concat "" [
+ "const $self = $('#__ID__');"
+ "aardvark.accordion($self, " + exclusive + ", channelActive);"
+ ]
+
+ let channels =
+ let channel =
+ match active with
+ | Choice1Of2 set -> ASet.channel set
+ | Choice2Of2 index -> AVal.channel index
+
+ [ "channelActive", channel ]
+
+ require dependencies (
+ onBoot' channels boot (
+ Incremental.div attributes <| alist {
+ for (title, node) in sections do
+ div [clazz "title"] [
+ i [clazz "dropdown icon"] []
+ text title
+ ]
+ div [clazz "content"] [
+ node
+ ]
+ }
+ )
+ )
+
+ /// Simple container dividing content into titled sections, which can be opened and closed.
+ /// The active set holds the indices of the open sections.
+ /// The toggle (index, isOpen) message is fired when a section is opened or closed.
+ let accordion (toggle: int * bool -> 'msg) (active: aset)
+ (attributes: AttributeMap<'msg>) (sections: alist>) =
+ sections |> accordionImpl (Some (fun s i -> toggle (i, s))) (Choice1Of2 active) attributes
+
+ /// Simple container dividing content into titled sections, which can be opened and closed (only one can be open at a time).
+ /// The active value holds the index of the open section, or -1 if there is no open section.
+ /// The setActive (index | -1) message is fired when a section is opened or closed.
+ let accordionExclusive (setActive: int -> 'msg) (active: aval)
+ (attributes: AttributeMap<'msg>) (sections: alist>) =
+ let map o i = if o then i else -1
+ sections |> accordionImpl (Some (fun s -> map s >> setActive)) (Choice2Of2 active) attributes
+
+ /// Simple container dividing content into titled sections, which can be opened and closed.
+ /// If exclusive is true, only one section can be open at a time.
+ let accordionSimple (exclusive: bool) (attributes: AttributeMap<'msg>) (sections: alist>) =
+ let active =
+ if exclusive then Choice2Of2 (AVal.constant -1)
+ else Choice1Of2 ASet.empty
+
+ sections |> accordionImpl None active attributes
+
[]
module ``Primtive Builders`` =
@@ -883,4 +958,26 @@ module SimplePrimitives =
let inline dropdownMultiSelect (attributes : AttributeMap<'msg>)
(compare : Option<'T -> 'T -> int>) (defaultText : string)
(values : amap<'T, DomNode<'msg>>) (selected : alist<'T>) (update : 'T list -> 'msg) =
- Incremental.dropdownMultiSelect attributes compare defaultText values selected update
\ No newline at end of file
+ Incremental.dropdownMultiSelect attributes compare defaultText values selected update
+
+ /// Simple container dividing content into titled sections, which can be opened and closed.
+ /// The active set holds the indices of the open sections.
+ /// The toggle message (index, isOpen) is fired when a section is opened or closed.
+ let inline accordion (toggle: int * bool -> 'msg) (active: aset)
+ (attributes: Attribute<'msg> list) (sections: list>) =
+ let attributes = AttributeMap.ofList attributes
+ sections |> AList.ofList |> Incremental.accordion toggle active attributes
+
+ /// Simple container dividing content into titled sections, which can be opened and closed (only one can be open at a time).
+ /// The active value holds the index of the open section, or -1 if there is no open section.
+ /// The setActive (index | -1) message is fired when a section is opened or closed.
+ let inline accordionExclusive (setActive: int -> 'msg) (active: aval)
+ (attributes: Attribute<'msg> list) (sections: list>) =
+ let attributes = AttributeMap.ofList attributes
+ sections |> AList.ofList |> Incremental.accordionExclusive setActive active attributes
+
+ /// Simple container dividing content into titled sections, which can be opened and closed.
+ /// If exclusive is true, only one section can be open at a time.
+ let inline accordionSimple (exclusive: bool) (attributes: Attribute<'msg> list) (sections: list>) =
+ let attributes = AttributeMap.ofList attributes
+ sections |> AList.ofList |> Incremental.accordionSimple exclusive attributes
\ No newline at end of file
diff --git a/src/Aardvark.UI.Primitives/resources/accordion.js b/src/Aardvark.UI.Primitives/resources/accordion.js
new file mode 100644
index 00000000..782a95cd
--- /dev/null
+++ b/src/Aardvark.UI.Primitives/resources/accordion.js
@@ -0,0 +1,65 @@
+if (!aardvark.accordion) {
+ /**
+ * @param {HTMLElement[]} $self
+ * @param {HTMLElement} item
+ * @param {string} event
+ */
+ const onToggle = function ($self, item, event) {
+ const index = $self.children('.content').index(item);
+ aardvark.processEvent($self[0].id, event, index);
+ };
+
+ /**
+ * @param {HTMLElement[]} $self
+ * @param {{cnt: int, value: int}} op
+ */
+ const processMessage = function ($self, op) {
+ if (op.value < 0 || op.value >= $self.children('.content').length) {
+ return;
+ }
+
+ if (op.cnt > 0) {
+ $self.accordion('open', op.value);
+ } else if (op.cnt < 0) {
+ $self.accordion('close', op.value);
+ }
+ };
+
+ /**
+ * @param {HTMLElement[]} $self
+ * @param {int} index
+ */
+ const processExclusiveMessage = function ($self, index) {
+ if (index >= 0) {
+ if (index < $self.children('.content').length) {
+ $self.accordion('open', index);
+ }
+ } else {
+ const $content = $self.children('.content');
+ const active = $content.index($content.filter('.active'));
+
+ if (active >= 0) {
+ $self.accordion('close', active);
+ }
+ }
+ };
+
+ /**
+ * @param {HTMLElement[]} $self
+ * @param {boolean} exclusive
+ * @param {{onmessage: function}} channel
+ */
+ aardvark.accordion = function ($self, exclusive, channel) {
+ $self.accordion({
+ exclusive: exclusive,
+ onOpen: function () { onToggle($self, this, 'onopen'); },
+ onClose: function () { onToggle($self, this, 'onclose'); }
+ });
+
+ if (exclusive) {
+ channel.onmessage = (index) => processExclusiveMessage($self, index);
+ } else {
+ channel.onmessage = (op) => processMessage($self, op);
+ }
+ };
+}
\ No newline at end of file
diff --git a/src/Examples (dotnetcore)/23 - Inputs/App.fs b/src/Examples (dotnetcore)/23 - Inputs/App.fs
index 514c87b8..ff42c62f 100644
--- a/src/Examples (dotnetcore)/23 - Inputs/App.fs
+++ b/src/Examples (dotnetcore)/23 - Inputs/App.fs
@@ -75,8 +75,7 @@ let view (model : AdaptiveModel) =
div [ style "margin-bottom: 10px" ] [ text str ]
body [style "background-color: lightslategrey"] [
- div [ clazz "ui vertical inverted menu"; style "min-width: 410px" ] [
-
+ div [ clazz "ui vertical inverted menu"; style "min-width: 420px" ] [
div [ clazz "item" ] [
button [clazz "ui inverted labeled icon button"; onClick (fun _ -> Reset)] [
i [clazz "red delete icon"] []
@@ -84,177 +83,164 @@ let view (model : AdaptiveModel) =
]
]
- // Checkboxes
- div [ clazz "header item" ] [
- h3 [] [ text "Checkboxes" ]
- ]
-
- div [ clazz "item" ] [
- simplecheckbox {
- attributes [clazz "ui inverted checkbox"]
- state model.active
- toggle ToggleActive
- content [ text "Is the thing active?"; i [clazz "icon rocket" ] [] ]
- }
- //checkbox [clazz "ui inverted checkbox"] model.active ToggleActive [ text "Is the thing active?"; i [clazz "icon rocket" ] [] ]
- ]
-
- div [ clazz "item" ] [
- checkbox [clazz "ui inverted toggle checkbox"] model.active ToggleActive "Is the thing active?"
- ]
-
- // Sliders
- div [ clazz "header item" ] [
- h3 [] [ text "Sliders" ]
- ]
-
- div [ clazz "item" ] [
- description "Float"
- slider { min = 1.0; max = 100.0; step = 0.1 } [clazz "ui inverted red slider"] model.value SetValue
- ]
-
- div [ clazz "item" ] [
- description "Integer"
- slider { min = 0; max = 20; step = 1 } [clazz "ui inverted small bottom aligned labeled ticked blue slider"] model.intValue SetInt
- ]
-
- // Input fields
- div [ clazz "header item" ] [
- h3 [] [ text "Input fields" ]
- ]
-
- div [ clazz "item" ] [
- description "Numeric (float)"
- simplenumeric {
- attributes [clazz "ui inverted input"]
- value model.value
- update SetValue
- step 0.1
- largeStep 1.0
- min 1.0
- max 100.0
- }
- //numeric { min = -1E15; max = 1E15; smallStep = 0.1; largeStep = 100.0 } [clazz "ui inverted input"] model.value SetValue
- ]
-
- div [ clazz "item" ] [
- description "Numeric (integer)"
- simplenumeric' {
- attributes [clazz "ui inverted input"]
- value model.intValue
- iconRight "inverted users"
- update SetInt
- step 1
- largeStep 5
- min -100000
- max 100000
- }
- //numeric { min = 0; max = 10000; smallStep = 1; largeStep = 10 } [clazz "ui inverted input"] model.intValue SetInt
- ]
-
- div [ clazz "item" ] [
- description "Numeric (decimal)"
- simplenumeric {
- attributes [clazz "ui inverted input"]
- value model.decValue
- update SetDecimal
- step 1m
- largeStep 5m
- min -100000m
- max 100000m
- }
- ]
-
- div [ clazz "item" ] [
- description "Numeric (unsigned integer)"
- simplenumeric' {
- attributes [clazz "ui inverted input"]
- labelLeft "$"
- labelRight "inverted basic" ".00"
- value model.uintValue
- update SetUInt
- step 1u
- largeStep 5u
- min 0u
- max 100000u
- }
- ]
-
- div [ clazz "item" ] [
- description "Text input with validation"
- textbox { regex = Some "^[a-zA-Z_]+$"; maxLength = Some 6 } [clazz "ui inverted input"] model.name SetName
- ]
+ accordionSimple false [ clazz "inverted item" ] [
+ // Checkboxes
+ "Checkboxes", div [ clazz "menu" ] [
+ div [ clazz "item" ] [
+ simplecheckbox {
+ attributes [clazz "ui inverted checkbox"]
+ state model.active
+ toggle ToggleActive
+ content [ text "Is the thing active?"; i [clazz "icon rocket" ] [] ]
+ }
+ //checkbox [clazz "ui inverted checkbox"] model.active ToggleActive [ text "Is the thing active?"; i [clazz "icon rocket" ] [] ]
+ ]
- // Dropdowns
- div [ clazz "header item" ] [
- h3 [] [ text "Dropdown menus" ]
- ]
+ div [ clazz "item" ] [
+ checkbox [clazz "ui inverted toggle checkbox"] model.active ToggleActive "Is the thing active?"
+ ]
+ ]
- div [ clazz "item" ] [
- description "Non-clearable"
- dropdownUnclearable [ clazz "inverted selection" ] enumValues model.enumValue SetEnumValue
- ]
+ // Sliders
+ "Sliders", div [ clazz "menu" ] [
+ div [ clazz "item" ] [
+ description "Float"
+ slider { min = 1.0; max = 100.0; step = 0.1 } [clazz "ui inverted red slider"] model.value SetValue
+ ]
- div [ clazz "item" ] [
- description "Clearable"
- dropdown { mode = DropdownMode.Text <| Some "blub"; onTrigger = TriggerDropdown.Hover } [ clazz "inverted selection" ] alternatives model.alt SetAlternative
- ]
+ div [ clazz "item" ] [
+ description "Integer"
+ slider { min = 0; max = 20; step = 1 } [clazz "ui inverted small bottom aligned labeled ticked blue slider"] model.intValue SetInt
+ ]
+ ]
- div [ clazz "item" ] [
- description "Icon mode"
- dropdown { mode = DropdownMode.Icon "sidebar"; onTrigger = TriggerDropdown.Hover } [ clazz "inverted icon top left pointing dropdown circular button" ] alternatives model.alt SetAlternative
- ]
+ // Input fields
+ "Input fields", div [ clazz "menu" ] [
+ div [ clazz "item" ] [
+ description "Numeric (float)"
+ simplenumeric {
+ attributes [clazz "ui inverted input"]
+ value model.value
+ update SetValue
+ step 0.1
+ largeStep 1.0
+ min 1.0
+ max 100.0
+ }
+ //numeric { min = -1E15; max = 1E15; smallStep = 0.1; largeStep = 100.0 } [clazz "ui inverted input"] model.value SetValue
+ ]
- div [ clazz "item" ] [
- description "Multi select"
- let atts = AttributeMap.ofList [clazz "inverted clearable search"]
- dropdownMultiSelect atts None "Search..." alternatives model.alts SetAlternatives
- ]
+ div [ clazz "item" ] [
+ description "Numeric (integer)"
+ simplenumeric' {
+ attributes [clazz "ui inverted input"]
+ value model.intValue
+ iconRight "inverted users"
+ update SetInt
+ step 1
+ largeStep 5
+ min -100000
+ max 100000
+ }
+ //numeric { min = 0; max = 10000; smallStep = 1; largeStep = 10 } [clazz "ui inverted input"] model.intValue SetInt
+ ]
- // Color picker
- div [ clazz "header item"; style "display: flex" ] [
- h3 [] [ text "Color picker" ]
+ div [ clazz "item" ] [
+ description "Numeric (decimal)"
+ simplenumeric {
+ attributes [clazz "ui inverted input"]
+ value model.decValue
+ update SetDecimal
+ step 1m
+ largeStep 5m
+ min -100000m
+ max 100000m
+ }
+ ]
- Incremental.div (AttributeMap.ofAMap <| amap {
- let! c = model.color
- yield style $"width: 16px; height: 16px; margin-left: 10px; margin-top: 5px; border: thin solid; background-color: #{c.ToHexString()}"
- }) AList.empty
- ]
+ div [ clazz "item" ] [
+ description "Numeric (unsigned integer)"
+ simplenumeric' {
+ attributes [clazz "ui inverted input"]
+ labelLeft "$"
+ labelRight "inverted basic" ".00"
+ value model.uintValue
+ update SetUInt
+ step 1u
+ largeStep 5u
+ min 0u
+ max 100000u
+ }
+ ]
- div [ clazz "item" ] [
- description "Dropdown variations"
+ div [ clazz "item" ] [
+ description "Text input with validation"
+ textbox { regex = Some "^[a-zA-Z_]+$"; maxLength = Some 6 } [clazz "ui inverted input"] model.name SetName
+ ]
+ ]
- div [style "display: flex"] [
- div [style "margin-right: 5px"] [
- let cfg = { ColorPicker.Config.Dark.Default with palette = Some ColorPicker.Palette.Basic }
- ColorPicker.view cfg SetColor model.color
+ // Dropdowns
+ "Dropdown menus", div [ clazz "menu" ] [
+ div [ clazz "item" ] [
+ description "Non-clearable"
+ dropdownUnclearable [ clazz "inverted selection" ] enumValues model.enumValue SetEnumValue
]
- div [style "margin-left: 5px; margin-right: 5px"] [
- let cfg = { ColorPicker.Config.Dark.PaletteOnly with palette = Some ColorPicker.Palette.Reduced }
- ColorPicker.view cfg SetColor model.color
+ div [ clazz "item" ] [
+ description "Clearable"
+ dropdown { mode = DropdownMode.Text <| Some "blub"; onTrigger = TriggerDropdown.Hover } [ clazz "inverted selection" ] alternatives model.alt SetAlternative
]
- div [style "margin-left: 5px; margin-right: 5px"] [
- let cfg = { ColorPicker.Config.Dark.PickerOnlyWithAlpha with preferredFormat = ColorPicker.Format.HSL }
- ColorPicker.view cfg SetColor model.color
+ div [ clazz "item" ] [
+ description "Icon mode"
+ dropdown { mode = DropdownMode.Icon "sidebar"; onTrigger = TriggerDropdown.Hover } [ clazz "inverted icon top left pointing dropdown circular button" ] alternatives model.alt SetAlternative
]
- div [style "margin-left: 5px"] [
- ColorPicker.view ColorPicker.Config.Dark.Disabled SetColor model.color
+ div [ clazz "item" ] [
+ description "Multi select"
+ let atts = AttributeMap.ofList [clazz "inverted clearable search"]
+ dropdownMultiSelect atts None "Search..." alternatives model.alts SetAlternatives
]
]
- ]
- div [ clazz "item" ] [
- description "Inline with persistent selection"
+ // Color picker
+ "Color picker", div [ clazz "menu" ] [
+ div [ clazz "item" ] [
+ description "Dropdown variations"
+
+ div [style "display: flex"] [
+ div [style "margin-right: 5px"] [
+ let cfg = { ColorPicker.Config.Dark.Default with palette = Some ColorPicker.Palette.Basic }
+ ColorPicker.view cfg SetColor model.color
+ ]
+
+ div [style "margin-left: 5px; margin-right: 5px"] [
+ let cfg = { ColorPicker.Config.Dark.PaletteOnly with palette = Some ColorPicker.Palette.Reduced }
+ ColorPicker.view cfg SetColor model.color
+ ]
+
+ div [style "margin-left: 5px; margin-right: 5px"] [
+ let cfg = { ColorPicker.Config.Dark.PickerOnlyWithAlpha with preferredFormat = ColorPicker.Format.HSL }
+ ColorPicker.view cfg SetColor model.color
+ ]
+
+ div [style "margin-left: 5px"] [
+ ColorPicker.view ColorPicker.Config.Dark.Disabled SetColor model.color
+ ]
+ ]
+ ]
- let cfg = { ColorPicker.Config.Dark.Default with
- localStorageKey = Some "aardvark.media.colorpicker.example"
- pickerStyle = Some { ColorPicker.PickerStyle.ToggleWithAlpha with showButtons = true; textInput = ColorPicker.TextInput.Enabled }
- displayMode = ColorPicker.DisplayMode.Inline }
+ div [ clazz "item" ] [
+ description "Inline with persistent selection palette"
- ColorPicker.view cfg SetColor model.color
+ let cfg = { ColorPicker.Config.Dark.Default with
+ localStorageKey = Some "aardvark.media.colorpicker.example"
+ pickerStyle = Some { ColorPicker.PickerStyle.ToggleWithAlpha with showButtons = true; textInput = ColorPicker.TextInput.Enabled }
+ displayMode = ColorPicker.DisplayMode.Inline }
+
+ ColorPicker.view cfg SetColor model.color
+ ]
+ ]
]
]
]