1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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
]
]
|