blob: 11b6bccb3cd461c482f85cbd0bdf5dac249de99c [file] [log] [blame]
-- Copyright 2020 The Fuchsia Authors. All rights reserved.
-- Use of this source code is governed by a BSD-style license that can be
-- found in the LICENSE file.
module Editors exposing
( Editor(..)
, Model
, Msg
, decode
, encode
, init
, initCmd
, update
, view
)
{-| This module implements the input/output editors.
Input and output editors are not independent. Anything to do with an output mode
-- its active/nonactive state, its content, its options -- is always relative to
an input mode. For example, FIDL Text -> Bytes and Bytes -> Bytes have distinct
output state even though both are Bytes.
This module does not keep track of the Ace editor state. That is done in
JavaScript-land because it's easier there.
-}
import Form exposing (Form)
import Html exposing (Html)
import Html.Attributes as Attributes
import Html.Events as Events
import Json.Decode as Decode
import Json.Encode as Encode
import Mode exposing (Mode)
import Ports
------------- MODEL ------------------------------------------------------------
type alias Model =
{ activeInput : Mode
, inputMap : Mode.Map InputState
}
type alias InputState =
{ activeOutput : Mode
, outputMap : Mode.Map OutputState
}
type alias OutputState =
{ options : Options
}
type alias Options =
Form.Model
type alias Active =
{ input : Mode
, output : Mode
, options : Options
}
init : Model
init =
let
input =
Mode.defaultInput
inputState =
defaultInputState input
in
{ activeInput = input
, inputMap = Mode.singleton input inputState
}
initCmd : Model -> Cmd msg
initCmd model =
updateEditors (getActive model)
defaultInputState : Mode -> InputState
defaultInputState input =
let
output =
Mode.defaultOutput input
outputState =
defaultOutputState input output
in
{ activeOutput = output
, outputMap = Mode.singleton output outputState
}
defaultOutputState : Mode -> Mode -> OutputState
defaultOutputState input output =
{ options = Form.empty }
getInputState : Mode -> Model -> InputState
getInputState input model =
Maybe.withDefault
(defaultInputState input)
(Mode.get input model.inputMap)
getOutputState : Mode -> Mode -> InputState -> OutputState
getOutputState input output inputState =
Maybe.withDefault
(defaultOutputState input output)
(Mode.get output inputState.outputMap)
getActive : Model -> Active
getActive model =
let
input =
model.activeInput
inputState =
getInputState input model
output =
inputState.activeOutput
outputState =
getOutputState input output inputState
options =
outputState.options
in
Active input output options
------------- UPDATE -----------------------------------------------------------
type Msg
= NoOp
| -- Switch to this input mode. This also implies switching to a new output
-- mode (the one that was most recently used for that input).
ActivateInput Mode
-- Switch to this output mode, keeping the same input mode.
| ActivateOutput Mode
-- Update options for the current input/output mode combination.
| UpdateOptions Form.Msg
update : Msg -> Model -> ( Model, Cmd msg )
update msg model =
let
newModel =
case msg of
NoOp ->
model
ActivateInput input ->
activateInput input model
ActivateOutput output ->
activateOutput output model
UpdateOptions formMsg ->
updateOptions (Form.update formMsg) model
cmd =
case msg of
NoOp ->
Cmd.none
_ ->
updateEditors (getActive newModel)
in
( newModel, cmd )
activateInput : Mode -> Model -> Model
activateInput input model =
let
inputState =
getInputState input model
newInputMap =
model.inputMap |> Mode.insert input inputState
in
{ model | activeInput = input, inputMap = newInputMap }
activateOutput : Mode -> Model -> Model
activateOutput output model =
let
input =
model.activeInput
inputState =
getInputState input model
outputState =
getOutputState input output inputState
newOutputMap =
inputState.outputMap |> Mode.insert output outputState
newInputState =
{ inputState | activeOutput = output, outputMap = newOutputMap }
newInputMap =
model.inputMap |> Mode.insert input newInputState
in
{ model | inputMap = newInputMap }
updateOptions : (Options -> Options) -> Model -> Model
updateOptions alter model =
let
input =
model.activeInput
inputState =
getInputState input model
output =
inputState.activeOutput
outputState =
getOutputState input output inputState
newOutputState =
{ options = alter outputState.options }
newOutputMap =
inputState.outputMap |> Mode.insert output newOutputState
newInputState =
{ inputState | outputMap = newOutputMap }
newInputMap =
model.inputMap |> Mode.insert input newInputState
in
{ model | inputMap = newInputMap }
updateEditors : Active -> Cmd msg
updateEditors active =
Ports.updateEditors
(Mode.encode active.input)
(Mode.encode active.output)
(Form.encodeEffective
(Mode.options active.input active.output)
active.options
)
------------- VIEW -------------------------------------------------------------
type Editor
= Input
| Output
view : Editor -> (Msg -> msg) -> String -> Model -> Html msg
view editor toMsg themeClass model =
let
active =
getActive model
{ modes, activeMode, msg, id, optionsPart } =
case editor of
Input ->
{ modes = Mode.inputs
, activeMode = active.input
, msg = ActivateInput
, id = "InputEditor"
, optionsPart = []
}
Output ->
let
form =
Mode.options active.input active.output
in
{ modes = Mode.outputs active.input
, activeMode = active.output
, msg = ActivateOutput
, id = "OutputEditor"
, optionsPart =
if List.isEmpty form.inputs then
[]
else
[ optionsView form active.options toMsg ]
}
navbarPart =
navbarView modes activeMode (toMsg << msg) themeClass
editorPart =
Html.div
[ Attributes.id id
, Attributes.class "editor"
]
[]
in
Html.div
[ Attributes.class "editor-wrapper"
, Events.on "keydown" (keyDownEvent editor active toMsg)
]
(navbarPart :: editorPart :: optionsPart)
navbarView : List Mode -> Mode -> (Mode -> msg) -> String -> Html msg
navbarView modes activeMode msg themeClass =
let
navItem mode =
-- We provide keyboard shortcuts on the parent div to navigate
-- between modes. This replaces tab navigation, hence tabindex -1.
-- We need to use button tags so that they can be focused and the
-- parent div will receive keyboard events.
Html.button
[ Attributes.type_ "button"
, Attributes.tabindex -1
, Attributes.class "navbar-item"
, if mode == activeMode then
Attributes.class ("navbar-item--active " ++ themeClass)
else
Events.onClick (msg mode)
]
[ Html.text (Mode.title mode) ]
in
Html.div
[ Attributes.class "navbar" ]
(List.map navItem modes)
optionsView : Form -> Form.Model -> (Msg -> msg) -> Html msg
optionsView form options toMsg =
Html.div
[ Attributes.class "editor-options" ]
[ Form.view
form
[]
(toMsg << UpdateOptions)
options
]
keyDownEvent : Editor -> Active -> (Msg -> msg) -> Decode.Decoder msg
keyDownEvent editor active toMsg =
let
( activate, previous, next ) =
case editor of
Input ->
( ActivateInput
, Mode.previousInput active.input
, Mode.nextInput active.input
)
Output ->
( ActivateOutput
, Mode.previousOutput active.input active.output
, Mode.nextOutput active.input active.output
)
chooseMsg ctrl key =
if ctrl && key == "[" then
activate previous
else if ctrl && key == "]" then
activate next
else
NoOp
in
Decode.map toMsg <|
Decode.map2 chooseMsg
(Decode.field "ctrlKey" Decode.bool)
(Decode.field "key" Decode.string)
------------- ENCODE / DECODE --------------------------------------------------
encode : Model -> Encode.Value
encode model =
Encode.object
[ ( "activeInput", Mode.encode model.activeInput )
, ( "inputMap", Mode.encodeMap encodeInputState model.inputMap )
]
encodeInputState : InputState -> Encode.Value
encodeInputState inputState =
Encode.object
[ ( "activeOutput", Mode.encode inputState.activeOutput )
, ( "outputMap", Mode.encodeMap encodeOutputState inputState.outputMap )
]
encodeOutputState : OutputState -> Encode.Value
encodeOutputState outputState =
Encode.object
[ ( "options", Form.encode outputState.options ) ]
decode : Decode.Decoder Model
decode =
decodeMap2Dependent Model
(Decode.field "activeInput" Mode.decode)
(\activeInput ->
Decode.field "inputMap"
(Mode.decodeMap (decodeInputState activeInput))
)
decodeInputState : Mode -> Decode.Decoder InputState
decodeInputState activeInput =
decodeMap2Dependent InputState
(Decode.field "activeOutput" Mode.decode)
(\activeOutput ->
Decode.field "outputMap"
(Mode.decodeMap (decodeOutputState activeInput activeOutput))
)
decodeOutputState : Mode -> Mode -> Decode.Decoder OutputState
decodeOutputState activeInput activeOutput =
let
form =
Mode.options activeInput activeOutput
in
Decode.map OutputState
(Decode.field "options" (Form.decode form))
decodeMap2Dependent :
(a -> b -> value)
-> Decode.Decoder a
-> (a -> Decode.Decoder b)
-> Decode.Decoder value
decodeMap2Dependent f decodeA decodeB =
decodeA
|> Decode.andThen
(\a -> Decode.map (f a) (decodeB a))