-
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
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-- Copyright 2026 Shota FUJI
--
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
--
-- SPDX-License-Identifier: MPL-2.0
module Parameters.Form exposing (Model, Msg(..), init, update, view)
import Dict exposing (Dict)
import Html exposing (div, input, label, node, p, span, text)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Length exposing (Length, toMM)
import Parameters exposing (Parameters)
import Parameters.Constraints exposing (NumberConstraints, constraints)
import Parameters.Key as Key exposing (Key(..))
import Parameters.Parser exposing (Error(..), parseInt, parseLength)
import Task
-- MODEL
type alias Fields =
Dict String String
type alias Errors =
Dict String Error
type alias Model =
{ fields : Fields
, errors : Errors
}
init : Parameters -> Model
init params =
let
fields =
Dict.fromList
[ ( Key.toString LugWidth, String.fromFloat (toMM params.lugWidth) )
, ( Key.toString BuckleHoleCount, String.fromInt params.longPiece.buckleHole.count )
, ( Key.toString BuckleHoleDiameter, String.fromFloat (toMM params.longPiece.buckleHole.diameter) )
, ( Key.toString CanvasMargin, String.fromFloat (toMM params.canvas.margin) )
]
in
{ fields = fields
, errors =
case parse fields of
Ok _ ->
Dict.empty
Err errors ->
errors
}
-- UPDATE
type InternalMsg
= FieldChanged Key String
type Msg
= Internal InternalMsg
| ParametersChangeRequested Parameters
parseField : Key -> (String -> Result Error a) -> Fields -> Result Error a
parseField key f fields =
Dict.get (Key.toString key) fields
|> Maybe.withDefault ""
|> f
mkErrors : List ( Key, Maybe Error ) -> Errors
mkErrors list =
list
|> List.filterMap (\( key, error ) -> Maybe.map (\e -> ( Key.toString key, e )) error)
|> Dict.fromList
getError : Result a b -> Maybe a
getError r =
case r of
Err e ->
Just e
_ ->
Nothing
parseBuckleHole : Fields -> Result Errors Parameters.BuckleHole
parseBuckleHole fields =
case
( parseField BuckleHoleCount (parseInt constraints.longPiece.buckleHole.count) fields
, parseField BuckleHoleDiameter (parseLength constraints.longPiece.buckleHole.diameter) fields
)
of
( Ok count, Ok diameter ) ->
let
base =
Parameters.default.longPiece.buckleHole
in
Ok { base | count = count, diameter = diameter }
( count, diameter ) ->
Err
(mkErrors
[ ( BuckleHoleCount, getError count )
, ( BuckleHoleDiameter, getError diameter )
]
)
parseLongPiece : Fields -> Result Errors Parameters.LongPiece
parseLongPiece fields =
case parseBuckleHole fields of
Ok buckleHole ->
let
base =
Parameters.default.longPiece
in
Ok { base | buckleHole = buckleHole }
Err errors ->
Err errors
parseCanvas : Fields -> Result Errors Parameters.Canvas
parseCanvas fields =
case parseField CanvasMargin (parseLength constraints.canvas.margin) fields of
Ok margin ->
let
base =
Parameters.default.canvas
in
Ok { base | margin = margin }
Err err ->
Err (mkErrors [ ( CanvasMargin, Just err ) ])
parse : Fields -> Result Errors Parameters
parse fields =
case
( parseField LugWidth (parseLength constraints.lugWidth) fields
, parseLongPiece fields
, parseCanvas fields
)
of
( Ok lugWidth, Ok longPiece, Ok canvas ) ->
let
base =
Parameters.default
in
Ok { base | lugWidth = lugWidth, longPiece = longPiece, canvas = canvas }
( lugWidth, longPiece, canvas ) ->
[ mkErrors [ ( LugWidth, getError lugWidth ) ]
, getError longPiece |> Maybe.withDefault Dict.empty
, getError canvas |> Maybe.withDefault Dict.empty
]
|> List.map Dict.toList
|> List.concat
|> Dict.fromList
|> Err
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Internal (FieldChanged key value) ->
let
fields =
Dict.insert (Key.toString key) value model.fields
in
case parse fields of
Ok newParams ->
( { model | fields = fields, errors = Dict.empty }
, Task.perform identity (Task.succeed (ParametersChangeRequested newParams))
)
Err errors ->
( { model | fields = fields, errors = errors }, Cmd.none )
ParametersChangeRequested _ ->
( model, Cmd.none )
-- VIEW
lengthFieldAttrs : NumberConstraints Length -> List (Html.Attribute msg)
lengthFieldAttrs { min, max } =
[ case min of
Just x ->
toMM x
|> String.fromFloat
|> Html.Attributes.min
Nothing ->
class ""
, case max of
Just x ->
toMM x
|> String.fromFloat
|> Html.Attributes.max
Nothing ->
class ""
, attribute "inputmode" "numeric"
, required True
]
intFieldAttrs : NumberConstraints Int -> List (Html.Attribute msg)
intFieldAttrs { min, max } =
[ case min of
Just x ->
Html.Attributes.min (String.fromInt x)
Nothing ->
class ""
, case max of
Just x ->
Html.Attributes.max (String.fromInt x)
Nothing ->
class ""
, attribute "inputmode" "numeric"
, required True
]
ariaInvalid : Bool -> Html.Attribute msg
ariaInvalid invalid =
attribute "aria-invalid"
(if invalid then
"true"
else
"false"
)
ariaDescribedBy : List String -> Html.Attribute msg
ariaDescribedBy ids =
case ids of
[] ->
class ""
_ ->
ids
|> String.join " "
|> attribute "aria-describedby"
errorId : Key -> String
errorId key =
Key.toString key ++ "_error"
errorText : Error -> List (Html.Html msg)
errorText e =
case e of
MissingValue ->
[ text "This field is required." ]
BelowMin n ->
[ text ("Value must be greater than or equals to " ++ String.fromFloat n ++ ".") ]
AboveMax n ->
[ text ("Value must be less than or equals to " ++ String.fromFloat n ++ ".") ]
NotALength ->
[ text "Invalid length value. Set an integer or a floating point number." ]
NotAnInt ->
[ text "Invalid integer value. This field only accepts integer value." ]
descriptionId : Key -> String
descriptionId key =
Key.toString key ++ "_description"
view : Model -> Parameters -> List (Html.Attribute InternalMsg) -> Html.Html Msg
view model params attrs =
let
value : Key -> Html.Attribute InternalMsg
value key =
model.fields
|> Dict.get (Key.toString key)
|> Maybe.withDefault ""
|> Html.Attributes.value
in
div attrs
[ node "x-field"
[]
[ label [ for (Key.toString LugWidth), attribute "slot" "title" ] [ text "Lug width" ]
, p
[ id (descriptionId LugWidth), attribute "slot" "description" ]
[ text "This will be the final width of your strap. "
, text "You can use a size smaller than your lug width to create a play."
]
, node "x-number-input"
[]
[ input
(id (Key.toString LugWidth)
:: step "1.0"
:: value LugWidth
:: onInput (FieldChanged LugWidth)
:: ariaInvalid (not (Dict.get (Key.toString LugWidth) model.errors == Nothing))
:: ariaDescribedBy [ errorId LugWidth, descriptionId LugWidth ]
:: lengthFieldAttrs constraints.lugWidth
)
[]
, span [ attribute "slot" "unit" ] [ text "mm" ]
]
, p
[ id (errorId LugWidth), attribute "slot" "error" ]
(Dict.get (Key.toString LugWidth) model.errors
|> Maybe.map errorText
|> Maybe.withDefault []
)
]
, node "x-field"
[]
[ label [ for (Key.toString BuckleHoleCount), attribute "slot" "title" ] [ text "Hole Count" ]
, p
[ id (descriptionId BuckleHoleCount), attribute "slot" "description" ]
[ text "Set 0 to disable buckle holes generation." ]
, node "x-number-input"
[]
[ input
(id (Key.toString BuckleHoleCount)
:: step "1"
:: value BuckleHoleCount
:: onInput (FieldChanged BuckleHoleCount)
:: ariaInvalid (not (Dict.get (Key.toString BuckleHoleCount) model.errors == Nothing))
:: ariaDescribedBy [ errorId BuckleHoleCount, descriptionId BuckleHoleCount ]
:: intFieldAttrs constraints.longPiece.buckleHole.count
)
[]
]
, p
[ id (errorId BuckleHoleCount), attribute "slot" "error" ]
(Dict.get (Key.toString BuckleHoleCount) model.errors
|> Maybe.map errorText
|> Maybe.withDefault []
)
]
, node "x-field"
[]
[ label [ for (Key.toString BuckleHoleDiameter), attribute "slot" "title" ] [ text "Hole Diameter" ]
, p
[ id (descriptionId BuckleHoleDiameter), attribute "slot" "description" ]
[ text "Diameter of buckle holes. You can leave the default value if you're going to use the center mark." ]
, node "x-number-input"
[]
[ input
(id (Key.toString BuckleHoleDiameter)
:: step "1.0"
:: value BuckleHoleDiameter
:: onInput (FieldChanged BuckleHoleDiameter)
:: disabled (params.longPiece.buckleHole.count == 0)
:: ariaInvalid (not (Dict.get (Key.toString BuckleHoleDiameter) model.errors == Nothing))
:: ariaDescribedBy [ errorId BuckleHoleDiameter, descriptionId BuckleHoleDiameter ]
:: lengthFieldAttrs constraints.longPiece.buckleHole.diameter
)
[]
, span [ attribute "slot" "unit" ] [ text "mm" ]
]
, p
[ id (errorId BuckleHoleDiameter), attribute "slot" "error" ]
(Dict.get (Key.toString BuckleHoleDiameter) model.errors
|> Maybe.map errorText
|> Maybe.withDefault []
)
]
, node "x-field"
[]
[ label [ for (Key.toString CanvasMargin), attribute "slot" "title" ] [ text "Print Margin" ]
, p
[ id (descriptionId CanvasMargin), attribute "slot" "description" ]
[ text "Lower values can cause printing problems depending on your printer." ]
, node "x-number-input"
[]
[ input
(id (Key.toString CanvasMargin)
:: step "1.0"
:: value CanvasMargin
:: onInput (FieldChanged CanvasMargin)
:: ariaInvalid (not (Dict.get (Key.toString CanvasMargin) model.errors == Nothing))
:: ariaDescribedBy [ errorId CanvasMargin, descriptionId CanvasMargin ]
:: lengthFieldAttrs constraints.canvas.margin
)
[]
, span [ attribute "slot" "unit" ] [ text "mm" ]
]
, p
[ id (errorId CanvasMargin), attribute "slot" "error" ]
(Dict.get (Key.toString CanvasMargin) model.errors
|> Maybe.map errorText
|> Maybe.withDefault []
)
]
]
|> Html.map Internal