From e72972e515b7ed05062a48c9dbd6301806bbc59b Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 18 Jul 2024 17:56:17 +0200 Subject: [PATCH] [Primitives] Simplifiy accordion and move to Accordion module --- .../Aardvark.UI.Primitives.fsproj | 1 + .../Primitives/Accordion.fs | 142 ++++++++++++++ .../Primitives/SimplePrimitives.fs | 179 +----------------- src/Examples (dotnetcore)/23 - Inputs/App.fs | 2 +- 4 files changed, 145 insertions(+), 179 deletions(-) create mode 100644 src/Aardvark.UI.Primitives/Primitives/Accordion.fs diff --git a/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj b/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj index 82330678..7b0f3f12 100644 --- a/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj +++ b/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj @@ -16,6 +16,7 @@ + diff --git a/src/Aardvark.UI.Primitives/Primitives/Accordion.fs b/src/Aardvark.UI.Primitives/Primitives/Accordion.fs new file mode 100644 index 00000000..b3c35889 --- /dev/null +++ b/src/Aardvark.UI.Primitives/Primitives/Accordion.fs @@ -0,0 +1,142 @@ +namespace Aardvark.UI.Primitives + +open Aardvark.UI +open Aardvark.UI.Generic +open FSharp.Data.Adaptive + +module Accordion = + + [] + type AccordionInput<'msg> = + | Multi of active: aset * callback: (bool -> int -> 'msg) + | Single of active: aval * callback: (bool -> int -> 'msg) + | Empty of exclusive: bool + + member inline x.Callback = + match x with + | Multi (_, cb) | Single (_, cb) -> Some cb + | _ -> None + + member inline x.IsExclusive = + match x with + | Single _ | Empty true -> true + | _ -> false + + module AccordionInternals = + + [] + type HeaderConverter() = + static member inline ToHeader (header: DomNode<'T>) = header + static member inline ToHeader (header: aval) = Incremental.text header + static member inline ToHeader (header: string) = text header + + let inline private toHeaderAux (_: ^Converter) (header: ^Header) = + ((^Converter or ^Header) : (static member ToHeader : ^Header -> DomNode<'T>) (header)) + + let inline toHeader (header: ^Header) : DomNode<'T> = + toHeaderAux Unchecked.defaultof header + + let inline toSections (sections: seq< ^Header * DomNode<'msg>>) = + sections |> Seq.map (fun (h, c) -> toHeader h, c) + + let accordionImpl (input: AccordionInput<'msg>) (attributes: AttributeMap<'msg>) (sections: seq * DomNode<'msg>>) = + let dependencies = + Html.semui @ [ { name = "accordion"; url = "resources/accordion.js"; kind = Script }] + + let attributes = + let basic = + AttributeMap.ofList [ + clazz "ui accordion" + + match input.Callback with + | Some cb -> + onEvent "onopen" [] (List.head >> int >> cb true) + onEvent "onclose" [] (List.head >> int >> cb false) + + | _ -> () + ] + + AttributeMap.union attributes basic + + let channel = + match input with + | AccordionInput.Multi (set, _) -> + Some (ASet.channel set) + + | AccordionInput.Single (index, _) -> + index |> AVal.map (fun i -> + if i < 0 then SetOperation.Rem -1 // Handle in JS, we don't know the actual index here + else SetOperation.Add i + ) + |> AVal.channel + |> Some + + | _ -> None + + let boot = + let exclusive = if input.IsExclusive then "true" else "false" + let channel = if channel.IsSome then "channelActive" else "null" + + String.concat "" [ + "const $self = $('#__ID__');" + "aardvark.accordion($self, " + exclusive + ", " + channel + ");" + ] + + let channels = + match channel with + | Some ch -> [ "channelActive", ch ] + | _ -> [] + + let isActive = + let set = + match input with + | AccordionInput.Multi (set, _) -> set |> ASet.toAVal |> AVal.force + | AccordionInput.Single (index, _) -> index |> AVal.force |> HashSet.single + | _ -> HashSet.empty + + fun i -> set |> HashSet.contains i + + require dependencies ( + onBoot' channels boot ( + Incremental.div attributes <| AList.ofList [ + let sections = Array.ofSeq sections + + for index = 0 to sections.Length - 1 do + let title, node = sections.[index] + let active = isActive index + + div [clazz "title"; if active then clazz "active"] [ + i [clazz "dropdown icon"] [] + title + ] + div [clazz "content"; if active then clazz "active"] [ + 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. + /// The section headers can be provided as DomNode, aval, or string. + /// The attributes can be provided as AttributeMap, amap, alist, or sequence of (conditional) attributes. + let inline accordion (toggle: int * bool -> 'msg) (active: aset) attributes (sections: seq< ^Header * DomNode<'msg>>) = + let cb s i = toggle (i, s) + AccordionInternals.toSections sections |> AccordionInternals.accordionImpl (AccordionInput.Multi (active, cb)) (att 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. + /// The section headers can be provided as DomNode, aval, or string. + /// The attributes can be provided as AttributeMap, amap, alist, or sequence of (conditional) attributes. + let inline accordionExclusive (setActive: int -> 'msg) (active: aval) attributes (sections: seq< ^Header * DomNode<'msg>>) = + let cb s i = (if s then i else -1) |> setActive + AccordionInternals.toSections sections |> AccordionInternals.accordionImpl (AccordionInput.Single (active, cb)) (att 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. + /// The section headers can be provided as DomNode, aval, or string. + /// The attributes can be provided as AttributeMap, amap, alist, or sequence of (conditional) attributes. + let inline accordionSimple (exclusive: bool) attributes (sections: seq< ^Header * DomNode<'msg>>) = + AccordionInternals.toSections sections |> AccordionInternals.accordionImpl (AccordionInput.Empty exclusive) (att attributes) \ No newline at end of file diff --git a/src/Aardvark.UI.Primitives/Primitives/SimplePrimitives.fs b/src/Aardvark.UI.Primitives/Primitives/SimplePrimitives.fs index 6ab9c87e..4d1822f5 100644 --- a/src/Aardvark.UI.Primitives/Primitives/SimplePrimitives.fs +++ b/src/Aardvark.UI.Primitives/Primitives/SimplePrimitives.fs @@ -343,139 +343,6 @@ module SimplePrimitives = ) ) - [] - type private AccordionInput<'msg> = - | Multi of active: aset * callback: (bool -> int -> 'msg) - | Single of active: aval * callback: (bool -> int -> 'msg) - | Empty of exclusive: bool - - member inline x.Callback = - match x with - | Multi (_, cb) | Single (_, cb) -> Some cb - | _ -> None - - member inline x.IsExclusive = - match x with - | Single _ | Empty true -> true - | _ -> false - - let private accordionImpl (input: AccordionInput<'msg>) (attributes: AttributeMap<'msg>) (sections: list * DomNode<'msg>>) = - let dependencies = - Html.semui @ [ { name = "accordion"; url = "resources/accordion.js"; kind = Script }] - - let attributes = - let basic = - AttributeMap.ofList [ - clazz "ui accordion" - - match input.Callback with - | Some cb -> - onEvent "onopen" [] (List.head >> int >> cb true) - onEvent "onclose" [] (List.head >> int >> cb false) - - | _ -> () - ] - - AttributeMap.union attributes basic - - let channel = - match input with - | AccordionInput.Multi (set, _) -> - Some (ASet.channel set) - - | AccordionInput.Single (index, _) -> - index |> AVal.map (fun i -> - if i < 0 then SetOperation.Rem -1 // Handle in JS, we don't know the actual index here - else SetOperation.Add i - ) - |> AVal.channel - |> Some - - | _ -> None - - let boot = - let exclusive = if input.IsExclusive then "true" else "false" - let channel = if channel.IsSome then "channelActive" else "null" - - String.concat "" [ - "const $self = $('#__ID__');" - "aardvark.accordion($self, " + exclusive + ", " + channel + ");" - ] - - let channels = - match channel with - | Some ch -> [ "channelActive", ch ] - | _ -> [] - - let isActive = - let set = - match input with - | AccordionInput.Multi (set, _) -> set |> ASet.toAVal |> AVal.force - | AccordionInput.Single (index, _) -> index |> AVal.force |> HashSet.single - | _ -> HashSet.empty - - fun i -> set |> HashSet.contains i - - require dependencies ( - onBoot' channels boot ( - Incremental.div attributes <| AList.ofList [ - let sections = Array.ofList sections - - for index = 0 to sections.Length - 1 do - let title, node = sections.[index] - let active = isActive index - - div [clazz "title"; if active then clazz "active"] [ - i [clazz "dropdown icon"] [] - title - ] - div [clazz "content"; if active then clazz "active"] [ - 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: list * DomNode<'msg>>) = - let cb s i = toggle (i, s) - sections |> accordionImpl (AccordionInput.Multi (active, cb)) 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: list * DomNode<'msg>>) = - let cb s i = (if s then i else -1) |> setActive - sections |> accordionImpl (AccordionInput.Single (active, cb)) 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: list * DomNode<'msg>>) = - sections |> accordionImpl (AccordionInput.Empty exclusive) attributes - - /// 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: list * DomNode<'msg>>) = - sections |> List.map (fun (t, c) -> Incremental.text t, c) |> 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 accordionExclusive (setActive: int -> 'msg) (active: aval) - (attributes: AttributeMap<'msg>) (sections: list * DomNode<'msg>>) = - sections |> List.map (fun (t, c) -> Incremental.text t, c) |> 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 accordionSimple (exclusive: bool) (attributes: AttributeMap<'msg>) (sections: list * DomNode<'msg>>) = - sections |> List.map (fun (t, c) -> Incremental.text t, c) |> accordionSimple' exclusive attributes - [] module ``Primtive Builders`` = @@ -666,48 +533,4 @@ module SimplePrimitives = Incremental.textbox cfg (att atts) state update let inline textarea (cfg : TextAreaConfig) atts (state : aval) (update : string -> 'msg) = - Incremental.textarea cfg (att atts) state 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 * DomNode<'msg>>) = - let attributes = AttributeMap.ofList attributes - sections |> 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 * DomNode<'msg>>) = - let attributes = AttributeMap.ofList attributes - sections |> 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 * DomNode<'msg>>) = - let attributes = AttributeMap.ofList attributes - sections |> Incremental.accordionSimple' exclusive attributes - - /// 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 |> List.map (fun (t, n) -> AVal.constant t, n) |> 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 |> List.map (fun (t, n) -> AVal.constant t, n) |> 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 |> List.map (fun (t, n) -> AVal.constant t, n) |> Incremental.accordionSimple exclusive attributes \ No newline at end of file + Incremental.textarea cfg (att atts) state update \ 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 a93d0bea..c45133f8 100644 --- a/src/Examples (dotnetcore)/23 - Inputs/App.fs +++ b/src/Examples (dotnetcore)/23 - Inputs/App.fs @@ -76,7 +76,7 @@ let view (model : AdaptiveModel) = ] ] - accordionSimple' true [ clazz "inverted item" ] [ + Accordion.accordionSimple true [ clazz "inverted item" ] [ // Checkboxes text "Checkboxes", div [ clazz "menu" ] [ div [ clazz "item" ] [