github.com/simpleiot/simpleiot@v0.18.3/frontend/src/UI/NodeInputs.elm (about) 1 module UI.NodeInputs exposing 2 ( NodeInputOptions 3 , nodeButtonActionText 4 , nodeCheckboxInput 5 , nodeCounterWithReset 6 , nodeKeyValueInput 7 , nodeListInput 8 , nodeNumberInput 9 , nodeOnOffInput 10 , nodeOptionInput 11 , nodePasteButton 12 , nodeTextInput 13 , nodeTimeDateInput 14 ) 15 16 import Api.Node exposing (Node) 17 import Api.Point as Point exposing (Point) 18 import Color 19 import Element exposing (..) 20 import Element.Border as Border 21 import Element.Font as Font 22 import Element.Input as Input 23 import List.Extra 24 import Round 25 import Svg as S 26 import Svg.Attributes as Sa 27 import Time 28 import Time.Extra 29 import UI.Button 30 import UI.Form as Form 31 import UI.Sanitize as Sanitize 32 import UI.Style as Style 33 import UI.ViewIf exposing (viewIf) 34 import Utils.Time exposing (scheduleToLocal, scheduleToUTC) 35 36 37 type alias NodeInputOptions msg = 38 { onEditNodePoint : List Point -> msg 39 , onEditScratch : String -> msg 40 , node : Node 41 , now : Time.Posix 42 , zone : Time.Zone 43 , labelWidth : Int 44 , scratch : String 45 } 46 47 48 nodeTextInput : 49 NodeInputOptions msg 50 -> String 51 -> String 52 -> String 53 -> String 54 -> Element msg 55 nodeTextInput o key typ lbl placeholder = 56 let 57 textRaw = 58 Point.getText o.node.points typ key 59 in 60 Input.text 61 [] 62 { onChange = 63 \d -> 64 o.onEditNodePoint [ Point typ key o.now 0 d 0 ] 65 , text = 66 if textRaw == "123BLANK123" then 67 "" 68 69 else 70 let 71 v = 72 Point.getValue o.node.points typ key 73 in 74 if v /= 0 then 75 "" 76 77 else 78 textRaw 79 , placeholder = Just <| Input.placeholder [] <| text placeholder 80 , label = 81 if lbl == "" then 82 Input.labelHidden "" 83 84 else 85 Input.labelLeft [ width (px o.labelWidth) ] <| el [ alignRight ] <| text <| lbl ++ ":" 86 } 87 88 89 nodeTimeDateInput : NodeInputOptions msg -> Int -> Element msg 90 nodeTimeDateInput o labelWidth = 91 let 92 zoneOffset = 93 Time.Extra.toOffset o.zone o.now 94 95 sModel = 96 pointsToSchedule o.node.points 97 98 sLocal = 99 checkScheduleToLocal zoneOffset sModel 100 101 sendTime updateSchedule tm = 102 let 103 tmClean = 104 Sanitize.time tm 105 in 106 updateSchedule sLocal tmClean 107 |> checkScheduleToUTC zoneOffset 108 |> scheduleToPoints o.now 109 |> o.onEditNodePoint 110 111 updateDate index dUpdate = 112 let 113 dClean = 114 Sanitize.date dUpdate 115 116 updatedDates = 117 List.Extra.setAt index dClean sLocal.dates 118 119 sUpdate = 120 { sLocal | dates = updatedDates } 121 in 122 sUpdate 123 |> checkScheduleToUTC zoneOffset 124 |> scheduleToPoints o.now 125 |> o.onEditNodePoint 126 127 deleteDate index = 128 let 129 updatedDates = 130 List.Extra.removeAt index sLocal.dates 131 132 sUpdate = 133 { sLocal | dates = updatedDates } 134 in 135 sUpdate 136 |> checkScheduleToUTC zoneOffset 137 |> scheduleToPoints o.now 138 |> o.onEditNodePoint 139 140 weekdaysChecked = 141 List.foldl 142 (\w ret -> 143 (w /= 0) || ret 144 ) 145 False 146 sLocal.weekdays 147 148 weekdayCheckboxInput index label = 149 Input.checkbox [] 150 { onChange = 151 \d -> 152 updateScheduleWkday sLocal index d 153 |> checkScheduleToUTC zoneOffset 154 |> scheduleToPoints o.now 155 |> o.onEditNodePoint 156 , checked = List.member index sLocal.weekdays 157 , icon = Input.defaultCheckbox 158 , label = Input.labelAbove [] <| text label 159 } 160 161 dateCount = 162 List.length sLocal.dates 163 in 164 column [ spacing 5 ] 165 [ if dateCount <= 0 then 166 wrappedRow 167 [ spacing 20 168 , paddingEach { top = 0, right = 0, bottom = 5, left = 0 } 169 ] 170 -- here, number matches Go Weekday definitions 171 -- https://pkg.go.dev/time#Weekday 172 [ el [ width <| px (o.labelWidth - 120) ] none 173 , text "Weekdays:" 174 , weekdayCheckboxInput 0 "S" 175 , weekdayCheckboxInput 1 "M" 176 , weekdayCheckboxInput 2 "T" 177 , weekdayCheckboxInput 3 "W" 178 , weekdayCheckboxInput 4 "T" 179 , weekdayCheckboxInput 5 "F" 180 , weekdayCheckboxInput 6 "S" 181 ] 182 183 else 184 none 185 , Input.text 186 [] 187 { label = Input.labelLeft [ width (px labelWidth) ] <| el [ alignRight ] <| text <| "Start time:" 188 , onChange = sendTime (\sched tm -> { sched | startTime = tm }) 189 , text = sLocal.startTime 190 , placeholder = Nothing 191 } 192 , Input.text 193 [] 194 { label = Input.labelLeft [ width (px labelWidth) ] <| el [ alignRight ] <| text <| "End time:" 195 , onChange = sendTime (\sched tm -> { sched | endTime = tm }) 196 , text = sLocal.endTime 197 , placeholder = Nothing 198 } 199 , if not weekdaysChecked then 200 let 201 dateCountS = 202 String.fromInt dateCount 203 in 204 column [] 205 [ el [ Element.paddingEach { top = 0, bottom = 0, right = 0, left = labelWidth - 59 } ] <| text "Dates:" 206 , column [ spacing 5 ] <| 207 List.indexedMap 208 (\i date -> 209 row [ spacing 10 ] 210 [ Input.text [] 211 { label = Input.labelLeft [ width (px labelWidth) ] <| text "" 212 , onChange = updateDate i 213 , text = date 214 , placeholder = Nothing 215 } 216 , UI.Button.x <| deleteDate i 217 ] 218 ) 219 sLocal.dates 220 , el [ Element.paddingEach { top = 6, bottom = 0, right = 0, left = labelWidth - 59 } ] <| 221 Form.button 222 { label = "Add Date" 223 , color = Style.colors.blue 224 , onPress = 225 o.onEditNodePoint 226 [ { typ = Point.typeDate 227 , key = dateCountS 228 , text = "" 229 , time = o.now 230 , tombstone = 0 231 , value = 0 232 } 233 ] 234 } 235 ] 236 237 else 238 none 239 ] 240 241 242 pointsToSchedule : List Point -> Utils.Time.Schedule 243 pointsToSchedule points = 244 let 245 start = 246 Point.getText points Point.typeStart "" 247 248 end = 249 Point.getText points Point.typeEnd "" 250 251 weekdays = 252 List.filter 253 (\d -> 254 let 255 dString = 256 String.fromInt d 257 258 p = 259 Point.getValue points Point.typeWeekday dString 260 in 261 p == 1 262 ) 263 [ 0, 1, 2, 3, 4, 5, 6 ] 264 265 datePoints = 266 List.filter 267 (\p -> 268 p.typ == Point.typeDate && p.tombstone == 0 269 ) 270 points 271 |> List.sortWith Point.sort 272 273 dates = 274 List.map (\p -> p.text) datePoints 275 in 276 { startTime = start 277 , endTime = end 278 , weekdays = weekdays 279 , dates = dates 280 , dateCount = List.length datePoints 281 } 282 283 284 scheduleToPoints : Time.Posix -> Utils.Time.Schedule -> List Point 285 scheduleToPoints now sched = 286 [ Point Point.typeStart "0" now 0 sched.startTime 0 287 , Point Point.typeEnd "0" now 0 sched.endTime 0 288 ] 289 ++ List.map 290 (\wday -> 291 if List.member wday sched.weekdays then 292 Point Point.typeWeekday (String.fromInt wday) now 1 "" 0 293 294 else 295 Point Point.typeWeekday (String.fromInt wday) now 0 "" 0 296 ) 297 [ 0, 1, 2, 3, 4, 5, 6 ] 298 ++ List.indexedMap 299 (\i d -> Point Point.typeDate (String.fromInt i) now 0 d 0) 300 sched.dates 301 ++ (if List.length sched.dates < sched.dateCount then 302 -- some dates have been deleted, so send some tombstone points to fill out array length 303 List.map 304 (\i -> 305 Point Point.typeDate (String.fromInt i) now 0 "" 1 306 ) 307 (List.range (List.length sched.dates) (sched.dateCount - 1)) 308 309 else 310 [] 311 ) 312 313 314 315 -- only convert to utc if both times and all dates are valid 316 317 318 checkScheduleToUTC : Int -> Utils.Time.Schedule -> Utils.Time.Schedule 319 checkScheduleToUTC offset sched = 320 if validHM sched.startTime && validHM sched.endTime && validDates sched.dates then 321 scheduleToUTC offset sched 322 323 else 324 sched 325 326 327 updateScheduleWkday : Utils.Time.Schedule -> Int -> Bool -> Utils.Time.Schedule 328 updateScheduleWkday sched index checked = 329 let 330 weekdays = 331 if checked then 332 if List.member index sched.weekdays then 333 sched.weekdays 334 335 else 336 index :: sched.weekdays 337 338 else 339 List.Extra.remove index sched.weekdays 340 in 341 { sched | weekdays = List.sort weekdays } 342 343 344 345 -- only convert to local if both times are valid 346 347 348 checkScheduleToLocal : Int -> Utils.Time.Schedule -> Utils.Time.Schedule 349 checkScheduleToLocal offset sched = 350 if validHM sched.startTime && validHM sched.endTime && validDates sched.dates then 351 scheduleToLocal offset sched 352 353 else 354 sched 355 356 357 validHM : String -> Bool 358 validHM t = 359 case Sanitize.parseHM t of 360 Just _ -> 361 True 362 363 Nothing -> 364 False 365 366 367 validDate : String -> Bool 368 validDate d = 369 case Sanitize.parseDate d of 370 Just _ -> 371 True 372 373 Nothing -> 374 False 375 376 377 validDates : List String -> Bool 378 validDates dates = 379 List.foldl 380 (\d ret -> 381 if not ret then 382 ret 383 384 else if d == "" then 385 True 386 387 else 388 validDate d 389 ) 390 True 391 dates 392 393 394 nodeButtonActionText : 395 NodeInputOptions msg 396 -> String 397 -> String 398 -> String 399 -> String 400 -> Color 401 -> Element msg 402 nodeButtonActionText o key typ value lbl color = 403 let 404 sendText = 405 o.onEditNodePoint 406 [ { typ = typ 407 , key = key 408 , text = value 409 , time = o.now 410 , tombstone = 0 411 , value = 0 412 } 413 ] 414 in 415 Form.button 416 { label = lbl 417 , color = color 418 , onPress = sendText 419 } 420 421 422 nodeCheckboxInput : 423 NodeInputOptions msg 424 -> String 425 -> String 426 -> String 427 -> Element msg 428 nodeCheckboxInput o key typ lbl = 429 Input.checkbox 430 [] 431 { onChange = 432 \d -> 433 let 434 v = 435 if d then 436 1.0 437 438 else 439 0.0 440 in 441 o.onEditNodePoint 442 [ Point typ key o.now v "" 0 ] 443 , checked = 444 Point.getValue o.node.points typ key == 1 445 , icon = Input.defaultCheckbox 446 , label = 447 if lbl /= "" then 448 Input.labelLeft [ width (px o.labelWidth) ] <| 449 el [ alignRight ] <| 450 text <| 451 lbl 452 ++ ":" 453 454 else 455 Input.labelHidden "" 456 } 457 458 459 nodeNumberInput : 460 NodeInputOptions msg 461 -> String 462 -> String 463 -> String 464 -> Element msg 465 nodeNumberInput o key typ lbl = 466 let 467 pMaybe = 468 Point.get o.node.points typ key 469 470 currentValue = 471 case pMaybe of 472 Just p -> 473 if p.text /= "" then 474 if p.text == Point.blankMajicValue || p.text == "blank" then 475 "" 476 477 else if p.text == "-" then 478 "-" 479 480 else 481 Sanitize.float p.text 482 483 else 484 String.fromFloat (Round.roundNum 6 p.value) 485 486 Nothing -> 487 "" 488 489 currentValueF = 490 case pMaybe of 491 Just p -> 492 p.value 493 494 Nothing -> 495 0 496 in 497 Input.text 498 [] 499 { onChange = 500 \d -> 501 let 502 dCheck = 503 if d == "" then 504 Point.blankMajicValue 505 506 else if d == "-" then 507 "-" 508 509 else 510 case String.toFloat d of 511 Just _ -> 512 d 513 514 Nothing -> 515 currentValue 516 517 v = 518 if dCheck == Point.blankMajicValue || dCheck == "-" then 519 0 520 521 else 522 Maybe.withDefault currentValueF <| String.toFloat dCheck 523 in 524 o.onEditNodePoint 525 [ Point typ key o.now v dCheck 0 ] 526 , text = currentValue 527 , placeholder = Nothing 528 , label = 529 if lbl == "" then 530 Input.labelHidden "" 531 532 else 533 Input.labelLeft [ width (px o.labelWidth) ] <| el [ alignRight ] <| text <| lbl ++ ":" 534 } 535 536 537 nodeOptionInput : 538 NodeInputOptions msg 539 -> String 540 -> String 541 -> String 542 -> List ( String, String ) 543 -> Element msg 544 nodeOptionInput o key typ lbl options = 545 Input.radio 546 [ spacing 6 ] 547 { onChange = 548 \sel -> 549 o.onEditNodePoint 550 [ Point typ key o.now 0 sel 0 ] 551 , label = 552 Input.labelLeft [ padding 12, width (px o.labelWidth) ] <| 553 el [ alignRight ] <| 554 text <| 555 lbl 556 ++ ":" 557 , selected = Just <| Point.getText o.node.points typ key 558 , options = 559 List.map 560 (\opt -> 561 Input.option (Tuple.first opt) (text (Tuple.second opt)) 562 ) 563 options 564 } 565 566 567 nodeCounterWithReset : 568 NodeInputOptions msg 569 -> String 570 -> String 571 -> String 572 -> String 573 -> Element msg 574 nodeCounterWithReset o key typ pointResetName lbl = 575 let 576 currentValue = 577 Point.getValue o.node.points typ key 578 579 currentResetValue = 580 Point.getValue o.node.points pointResetName key /= 0 581 in 582 row [ spacing 20 ] 583 [ el [ width (px o.labelWidth) ] <| 584 el [ alignRight ] <| 585 text <| 586 lbl 587 ++ ": " 588 ++ String.fromFloat currentValue 589 , Input.checkbox [] 590 { onChange = 591 \v -> 592 let 593 vFloat = 594 if v then 595 1.0 596 597 else 598 0 599 in 600 o.onEditNodePoint [ Point pointResetName key o.now vFloat "" 0 ] 601 , icon = Input.defaultCheckbox 602 , checked = currentResetValue 603 , label = 604 Input.labelLeft [] (text "reset") 605 } 606 ] 607 608 609 nodeOnOffInput : 610 NodeInputOptions msg 611 -> String 612 -> String 613 -> String 614 -> String 615 -> Element msg 616 nodeOnOffInput o key typ pointSetName lbl = 617 let 618 currentValue = 619 Point.getValue o.node.points typ key 620 621 currentSetValue = 622 Point.getValue o.node.points pointSetName key 623 624 fill = 625 if currentSetValue == 0 then 626 Color.rgb 0.5 0.5 0.5 627 628 else 629 Color.rgb255 50 100 150 630 631 fillS = 632 Color.toCssString fill 633 634 offset = 635 if currentSetValue == 0 then 636 3 637 638 else 639 3 + 24 640 641 newValue = 642 if currentSetValue == 0 then 643 1 644 645 else 646 0 647 in 648 row [ spacing 10 ] 649 [ el [ width (px o.labelWidth) ] <| el [ alignRight ] <| text <| lbl ++ ":" 650 , Input.button 651 [] 652 { onPress = Just <| o.onEditNodePoint [ Point pointSetName key o.now newValue "" 0 ] 653 , label = 654 el [ width (px 100) ] <| 655 html <| 656 S.svg [ Sa.viewBox "0 0 48 24" ] 657 [ S.rect 658 [ Sa.x "0" 659 , Sa.y "0" 660 , Sa.width "48" 661 , Sa.height "24" 662 , Sa.ry "3" 663 , Sa.rx "3" 664 , Sa.fill fillS 665 ] 666 <| 667 if currentValue /= currentSetValue then 668 let 669 fillFade = 670 if currentSetValue == 0 then 671 Color.rgb 0.9 0.9 0.9 672 673 else 674 Color.rgb255 150 200 255 675 676 fillFadeS = 677 Color.toCssString fillFade 678 in 679 [ S.animate 680 [ Sa.attributeName "fill" 681 , Sa.dur "2s" 682 , Sa.repeatCount "indefinite" 683 , Sa.values <| 684 fillFadeS 685 ++ ";" 686 ++ fillS 687 ++ ";" 688 ++ fillFadeS 689 ] 690 [] 691 ] 692 693 else 694 [] 695 , S.rect 696 [ Sa.x (String.fromFloat offset) 697 , Sa.y "3" 698 , Sa.width "18" 699 , Sa.height "18" 700 , Sa.ry "3" 701 , Sa.rx "3" 702 , Sa.fill (Color.toCssString Color.white) 703 ] 704 [] 705 ] 706 } 707 ] 708 709 710 nodePasteButton : 711 NodeInputOptions msg 712 -> Element msg 713 -> String 714 -> String 715 -> Element msg 716 nodePasteButton o label typ value = 717 row [ spacing 10, paddingEach { top = 0, bottom = 0, right = 0, left = 75 } ] 718 [ UI.Button.clipboard <| o.onEditNodePoint [ Point typ "0" o.now 0 value 0 ] 719 , label 720 ] 721 722 723 nodeListInput : NodeInputOptions msg -> String -> String -> String -> Element msg 724 nodeListInput o typ label buttonLabel = 725 let 726 entries = 727 Point.getTextArray o.node.points typ 728 729 entriesArrayCount = 730 List.Extra.count 731 (\p -> 732 p.typ == typ 733 ) 734 o.node.points 735 736 entriesArrayCountS = 737 String.fromInt entriesArrayCount 738 739 entriesToPoints es = 740 List.indexedMap 741 (\i s -> 742 Point typ (String.fromInt i) o.now 0 s 0 743 ) 744 es 745 ++ List.map 746 (\i -> 747 Point typ (String.fromInt i) o.now 0 "" 1 748 ) 749 (List.range (List.length es) (entriesArrayCount - 1)) 750 751 updateEntry i update = 752 List.Extra.setAt i update entries |> entriesToPoints |> o.onEditNodePoint 753 754 deleteEntry i = 755 List.Extra.removeAt i entries |> entriesToPoints |> o.onEditNodePoint 756 in 757 column [ centerX, spacing 5 ] <| 758 (el [ Font.bold, centerX, Element.paddingXY 0 6 ] <| 759 Element.text label 760 ) 761 :: List.indexedMap 762 (\i s -> 763 row [ spacing 10 ] 764 [ Input.text [] 765 { label = Input.labelHidden "entry name" 766 , onChange = updateEntry i 767 , text = s 768 , placeholder = Nothing 769 } 770 , UI.Button.x <| deleteEntry i 771 ] 772 ) 773 entries 774 ++ [ el [ Element.paddingXY 0 6, centerX ] <| 775 Form.button 776 { label = buttonLabel 777 , color = Style.colors.blue 778 , onPress = 779 o.onEditNodePoint 780 [ { typ = typ 781 , key = entriesArrayCountS 782 , text = "" 783 , time = o.now 784 , tombstone = 0 785 , value = 0 786 } 787 ] 788 } 789 ] 790 791 792 type alias Edges = 793 { top : Int 794 , right : Int 795 , bottom : Int 796 , left : Int 797 } 798 799 800 edges : Edges 801 edges = 802 { top = 0 803 , right = 0 804 , bottom = 0 805 , left = 0 806 } 807 808 809 nodeKeyValueInput : NodeInputOptions msg -> String -> String -> String -> Element msg 810 nodeKeyValueInput o typ label buttonLabel = 811 let 812 points = 813 Point.getAll o.node.points typ |> Point.filterDeleted |> List.sortWith Point.sort 814 815 newKV = 816 o.onEditNodePoint 817 [ { typ = typ 818 , key = o.scratch 819 , text = "" 820 , time = o.now 821 , tombstone = 0 822 , value = 0 823 } 824 ] 825 in 826 column [ centerX, spacing 5 ] 827 [ viewIf (List.length points > 0) <| keyValues o typ label points 828 , row [ spacing 10 ] 829 [ el [ Element.paddingEach { edges | left = 40 }, centerX ] <| 830 text "Add: " 831 , Input.text [ Form.onEnter newKV ] 832 { label = Input.labelHidden "key" 833 , onChange = o.onEditScratch 834 , text = o.scratch 835 , placeholder = Just <| Input.placeholder [] <| text "new key" 836 } 837 , Form.button 838 { label = buttonLabel 839 , color = Style.colors.blue 840 , onPress = newKV 841 } 842 ] 843 ] 844 845 846 keyValues : NodeInputOptions msg -> String -> String -> List Point -> Element msg 847 keyValues o typ label points = 848 let 849 deleteEntry key = 850 o.onEditNodePoint 851 [ { typ = typ 852 , key = key 853 , text = "" 854 , time = o.now 855 , tombstone = 1 856 , value = 0 857 } 858 ] 859 in 860 column [ centerX, spacing 5 ] 861 [ el [ Font.bold, centerX, Element.paddingXY 0 6 ] <| 862 Element.text label 863 , table [ padding 7 ] 864 { data = points 865 , columns = 866 let 867 cell = 868 el [ paddingXY 15 5, Border.width 0, centerY ] 869 in 870 [ { header = cell <| el [ Font.bold, centerX ] <| text "Key" 871 , width = shrink 872 , view = \p -> cell <| text <| p.key ++ ":" 873 } 874 , { header = cell <| el [ Font.bold, centerX ] <| text "Value" 875 , width = fill 876 , view = \p -> cell <| nodeTextInput o p.key typ "" "value" 877 } 878 , { header = cell <| el [ Font.bold, centerX ] <| text "Delete" 879 , width = shrink 880 , view = \p -> cell <| el [ centerX ] <| UI.Button.x <| deleteEntry p.key 881 } 882 ] 883 } 884 ]