From 683c2298dcdbda96d44e23d5f4db3f3c959d1161 Mon Sep 17 00:00:00 2001 From: Yorhel Date: Wed, 30 Oct 2019 12:39:07 +0100 Subject: ulist: Inline assigning labels to VNs I'm really unhappy with the workarounds to deal with the global onClick subscription doing the right thing, but I wasn't able to find a good alternative. --- elm/Lib/Html.elm | 4 ++ elm/ULists/LabelEdit.elm | 117 ++++++++++++++++++++++++++++++++++++++++++++ elm/ULists/ManageLabels.elm | 2 +- elm/ULists/ManageLabels.js | 4 +- 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 elm/ULists/LabelEdit.elm (limited to 'elm') diff --git a/elm/Lib/Html.elm b/elm/Lib/Html.elm index d9c0594a..770e9142 100644 --- a/elm/Lib/Html.elm +++ b/elm/Lib/Html.elm @@ -12,6 +12,10 @@ import Lib.Api as Api onClickN : m -> Attribute m onClickN action = custom "click" (JD.succeed { message = action, stopPropagation = True, preventDefault = True}) +-- onClick with preventDefault +onClickD : m -> Attribute m +onClickD action = custom "click" (JD.succeed { message = action, stopPropagation = False, preventDefault = True}) + -- onInput that also tells us whether the input is valid onInputValidation : (String -> Bool -> msg) -> Attribute msg onInputValidation msg = custom "input" <| diff --git a/elm/ULists/LabelEdit.elm b/elm/ULists/LabelEdit.elm new file mode 100644 index 00000000..eb59c823 --- /dev/null +++ b/elm/ULists/LabelEdit.elm @@ -0,0 +1,117 @@ +module ULists.LabelEdit exposing (main) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Task +import Process +import Browser +import Browser.Events as E +import Json.Decode as JD +import Set exposing (Set) +import Dict exposing (Dict) +import Lib.Html exposing (..) +import Lib.Api as Api +import Gen.Api as GApi +import Gen.LabelEdit as GLE + + +main : Program GLE.Recv Model Msg +main = Browser.element + { init = \f -> (init f, Cmd.none) + , subscriptions = \model -> if model.opened then E.onClick (JD.succeed (Open False)) else Sub.none + , view = view + , update = update + } + +type alias Model = + { uid : Int + , vid : Int + , labels : List GLE.RecvLabels + , sel : Set Int -- Set of label IDs applied on the server + , tsel : Set Int -- Set of label IDs applied on the client + , state : Dict Int Api.State -- Only for labels that are being changed + , opened : Bool + } + +init : GLE.Recv -> Model +init f = + { uid = f.uid + , vid = f.vid + , labels = f.labels + , sel = Set.fromList f.selected + , tsel = Set.fromList f.selected + , state = Dict.empty + , opened = False + } + +type Msg + = Redo Msg + | Open Bool + | Toggle Int Bool + | Saved Int Bool GApi.Response + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + -- 'Redo' will process the same message again after a very short timeout, + -- this is used to overrule an 'Open False' triggered by the onClick + -- subscription. + Redo m -> (model, Cmd.batch [ Task.perform (\_ -> m) (Task.succeed ()), Task.perform (\_ -> m) (Process.sleep 0) ]) + Open b -> ({ model | opened = b }, Cmd.none) + + -- The 'opened = True' counters the onClick subscription that would have + -- closed the dropdown, this works because that subscription triggers + -- before the Toggle (I just hope this is well-defined, otherwise we need + -- to use Redo for this one as well). + Toggle l b -> + ( { model + | opened = True + , tsel = if b then Set.insert l model.tsel else Set.remove l model.tsel + , state = Dict.insert l Api.Loading model.state + } + , Api.post "/u/ulist/setlabel.json" (GLE.encode { uid = model.uid, vid = model.vid, label = l, applied = b }) (Saved l b) + ) + + Saved l b (GApi.Success) -> ({ model | sel = if b then Set.insert l model.sel else Set.remove l model.sel, state = Dict.remove l model.state }, Cmd.none) + Saved l b e -> ({ model | state = Dict.insert l (Api.Error e) model.state }, Cmd.none) + + +view : Model -> Html Msg +view model = + let + str = String.join ", " <| List.filterMap (\l -> if Set.member l.id model.sel then Just l.label else Nothing) model.labels + + item l = + let lid = "label_edit_" ++ String.fromInt model.vid ++ "_" ++ String.fromInt l.id + in + li [ class "linkradio" ] + [ inputCheck lid (Set.member l.id model.tsel) (Toggle l.id) + , label [ for lid ] + [ text l.label + , text " " + , span [ class "spinner", classList [("invisible", Dict.get l.id model.state /= Just Api.Loading)] ] [] + , case Dict.get l.id model.state of + Just (Api.Error _) -> b [ class "standout" ] [ text "error" ] -- Need something better + _ -> text "" + ] + ] + + loading = List.any (\s -> s == Api.Loading) <| Dict.values model.state + + in + div [ class "labeledit" ] + [ a [ href "#", onClickD (Redo (Open (not model.opened))) ] + [ text <| if str == "" then "-" else str + , span [] + [ if loading && not model.opened + then span [ class "spinner" ] [] + else i [] [ text "▾" ] + ] + ] + , div [] + [ ul [ classList [("hidden", not model.opened)] ] + <| List.map item model.labels + ] + ] diff --git a/elm/ULists/ManageLabels.elm b/elm/ULists/ManageLabels.elm index f6e1d5e5..1e953b0a 100644 --- a/elm/ULists/ManageLabels.elm +++ b/elm/ULists/ManageLabels.elm @@ -86,7 +86,7 @@ view model = ] ] in - Html.form [ onSubmit Submit, class "labeledit hidden" ] + Html.form [ onSubmit Submit, class "managelabels hidden" ] [ div [ ] [ b [] [ text "How to use labels" ] , ul [] diff --git a/elm/ULists/ManageLabels.js b/elm/ULists/ManageLabels.js index 85312d21..76ee960c 100644 --- a/elm/ULists/ManageLabels.js +++ b/elm/ULists/ManageLabels.js @@ -1,6 +1,6 @@ -document.querySelectorAll('#labeledit').forEach(function(b) { +document.querySelectorAll('#managelabels').forEach(function(b) { b.onclick = function() { - document.querySelectorAll('.labeledit').forEach(function(e) { e.classList.toggle('hidden') }) + document.querySelectorAll('.managelabels').forEach(function(e) { e.classList.toggle('hidden') }) }; return false; }) -- cgit v1.2.3