Switch to a marginally less hack-y editing solution for calls.

This commit is contained in:
Gareth Latty
2020-05-22 21:47:03 +01:00
parent 6c2a4118a2
commit 373312ccba
14 changed files with 461 additions and 305 deletions
+33 -29
View File
@@ -1,33 +1,37 @@
{
"type": "application",
"source-directories": ["./src/elm", "./elm-material/src/elm"],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
"elm/browser": "1.0.2",
"elm/bytes": "1.0.8",
"elm/core": "1.0.5",
"elm/file": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm-community/list-extra": "8.2.4",
"lattyware/elm-fontawesome": "4.0.0",
"lattyware/elm-json-diff": "1.0.0",
"norpan/elm-html5-drag-drop": "3.1.4",
"norpan/elm-json-patch": "1.0.1"
"type": "application",
"source-directories": [
"./src/elm",
"./elm-material/src/elm"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
"elm/browser": "1.0.2",
"elm/bytes": "1.0.8",
"elm/core": "1.0.5",
"elm/file": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm-community/list-extra": "8.2.4",
"jinjor/elm-diff": "1.0.6",
"lattyware/elm-fontawesome": "4.0.0",
"lattyware/elm-json-diff": "1.0.0",
"norpan/elm-html5-drag-drop": "3.1.4",
"norpan/elm-json-patch": "1.0.1"
},
"indirect": {
"elm/regex": "1.0.0",
"elm/svg": "1.0.1",
"elm/virtual-dom": "1.0.2"
}
},
"indirect": {
"elm/regex": "1.0.0",
"elm/svg": "1.0.1",
"elm/virtual-dom": "1.0.2"
"test-dependencies": {
"direct": {},
"indirect": {}
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
Binary file not shown.
+8 -11
View File
@@ -110,12 +110,9 @@ encode (Call call) =
editor : Call -> Editor.Model
editor (Call lines) =
{ atoms = lines |> List.map (List.concatMap partToAtoms) |> List.intersperse [ NewLine ] |> List.concat
, selection = Nothing
, selection = { start = 0, end = 0 }
, selecting = Nothing
, moving = Nothing
, hover = Nothing
, cursor = 0
, styled = []
, control = False
}
@@ -124,7 +121,7 @@ partToAtoms : Part -> List Editor.Atom
partToAtoms part =
case part of
Part.Text text style ->
text |> String.toList |> List.map Editor.Letter
text |> String.toList |> List.map (\c -> Editor.Letter c style)
Part.Slot transform style ->
[ Editor.Slot transform style ]
@@ -151,10 +148,10 @@ atomsToParts ( f, r ) =
group a b =
case a of
Letter _ ->
Letter _ styleA ->
case b of
Letter _ ->
True
Letter _ styleB ->
styleA == styleB
_ ->
False
@@ -164,7 +161,7 @@ atomsToParts ( f, r ) =
toChar atom =
case atom of
Letter char ->
Letter char _ ->
Just char
_ ->
@@ -172,11 +169,11 @@ atomsToParts ( f, r ) =
toPart ( first, rest ) =
case first of
Letter _ ->
Letter _ style ->
(first :: rest)
|> List.filterMap toChar
|> String.fromList
|> (\t -> Part.Text t Style.None |> Just)
|> (\t -> Part.Text t style |> Just)
Slot transform style ->
Part.Slot transform style |> Just
+3 -3
View File
@@ -93,7 +93,7 @@ subscriptions model =
onUrlRequest : Browser.UrlRequest -> Msg
onUrlRequest urlRequest =
onUrlRequest _ =
NoOp
@@ -114,10 +114,10 @@ update msg model =
SetError error ->
case error of
Error.Transient Error.AuthFailure ->
( model, Route.redirectTo Login model.navKey )
( { model | auth = Nothing }, Route.redirectTo Login model.navKey )
Error.User Error.NotAuthenticated ->
( model, Route.redirectTo Login model.navKey )
( { model | auth = Nothing }, Route.redirectTo Login model.navKey )
_ ->
( { model | error = Just error }, Cmd.none )
+3 -1
View File
@@ -87,7 +87,9 @@ update msg model =
newDecks =
model.decks |> Maybe.map (List.filter (\d -> d.code /= code))
in
( { model | decks = newDecks, edit = Nothing }, GlobalRoute.redirectTo (Route.Decks Route.List) model.navKey )
( { model | decks = newDecks, edit = Nothing }
, GlobalRoute.redirectTo (Route.Decks Route.List) model.navKey
)
Save code patch ->
case model.auth of
+12 -25
View File
@@ -75,15 +75,19 @@ update msg model =
UpdateCall callEditorMsg ->
let
editing =
( editing, cmd ) =
case model.editing of
Just (CallEditor index old m) ->
m |> CallEditor.update callEditorMsg |> CallEditor index old |> Just
let
( e, eCmd ) =
m |> CallEditor.update callEditorMsg
in
( e |> CallEditor index old |> Just, eCmd )
other ->
other
( other, Cmd.none )
in
( { model | editing = editing }, Cmd.none )
( { model | editing = editing }, cmd )
StartEditing cardEditor ->
let
@@ -217,18 +221,13 @@ view code model =
d =
IconButton.view (Icon.trash |> Icon.viewIcon) "Delete" (Delete |> wrap |> Just)
( problems, addSlot ) =
problems =
case callEditor of
Just editorModel ->
( CallEditor.problems editorModel
, IconButton.view (Icon.plusCircle |> Icon.viewIcon)
"Add Slot"
(CallEditor.AddSlot |> UpdateCall |> Edit |> wrap |> Just)
|> Just
)
CallEditor.problems editorModel
_ ->
( [], Nothing )
[]
noProblems =
List.isEmpty problems
@@ -242,24 +241,12 @@ view code model =
s =
IconButton.view (Icon.save |> Icon.viewIcon) "Save" sAction
controlsContents =
[ Just d, addSlot, Just s ] |> List.filterMap identity
problemsView =
if noProblems then
Html.text ""
else
Html.ul [ HtmlA.class "problems" ]
(problems |> List.map (\p -> Html.li [] [ Html.text p ]))
in
[ Html.div [ HtmlA.class "overlay" ]
[ Html.div [ HtmlA.class "background", EndEditing |> wrap |> HtmlE.onClick ] []
, Card.view []
[ Html.div [ HtmlA.class "editing" ] c
, problemsView
, Html.div [ HtmlA.class "editing-controls" ] controlsContents
, Html.div [ HtmlA.class "editing-controls" ] [ d, s ]
]
]
]
@@ -5,17 +5,22 @@ module ManyDecks.Pages.Decks.Edit.CallEditor exposing
, view
)
import Browser.Events as Browser
import Cards.Call as Call
import Cards.Call.Style as Style
import Cards.Call.Transform as Transform
import Cards.Call.Style as Style exposing (Style)
import Cards.Call.Transform as Transform exposing (Transform)
import Cards.Card as Card
import Diff
import FontAwesome.Icon as Icon
import FontAwesome.Layering as Icon
import FontAwesome.Solid as Icon
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import Json.Decode as Json
import List.Extra as List
import ManyDecks.Pages.Decks.Edit.CallEditor.Model exposing (..)
import ManyDecks.Ports as Ports
import Material.IconButton as IconButton
deleteSpan : Span -> Model -> Model
@@ -27,7 +32,7 @@ deleteSpan { start, end } model =
right =
model.atoms |> List.drop end
in
{ model | atoms = List.concat [ left, right ], selection = Nothing, cursor = start }
{ model | atoms = List.concat [ left, right ], selection = span start start }
insertAt : List Atom -> Position -> Model -> Model
@@ -38,160 +43,19 @@ insertAt new position model =
atoms =
List.concat [ left, new, right ]
endPos =
position + List.length new
in
{ model | atoms = atoms, cursor = position + List.length new }
{ model | atoms = atoms, selection = span endPos endPos }
moveCursor : (Position -> Position) -> Model -> Model
moveCursor move model =
let
afterLast =
List.length model.atoms
cursor =
min afterLast (max (model.cursor |> move) 0)
selection =
case model.selecting of
Just start ->
selectionOf start cursor
Nothing ->
Nothing
in
{ model | cursor = cursor, selection = selection }
moveRow : Int -> Model -> Model
moveRow diff model =
let
applyNTimes n f value =
if n > 0 then
value |> f |> applyNTimes (n - 1) f
else
value
atoms =
model.atoms
in
if diff > 0 then
model |> applyNTimes diff (moveCursor (\p -> p + toEndOfLine atoms p + 1))
else
model |> applyNTimes -diff (moveCursor (\p -> p - toStartOfLine atoms p - 1))
toEndOfLine : List Atom -> Position -> Int
toEndOfLine atoms p =
atoms
|> List.drop p
|> List.findIndex ((==) NewLine)
|> Maybe.withDefault (List.length atoms)
toStartOfLine : List Atom -> Position -> Int
toStartOfLine atoms p =
let
pRev =
List.length atoms - p
in
atoms
|> List.reverse
|> List.drop pRev
|> List.findIndex ((==) NewLine)
|> Maybe.withDefault p
update : Msg -> Model -> Model
update : Msg -> Model -> ( Model, Cmd msg )
update msg model =
case msg of
KeyUp key ->
case key of
Control "Shift" ->
{ model | selecting = Nothing }
Control "Control" ->
{ model | control = False }
_ ->
model
KeyDown key ->
let
cursor =
model.cursor
op =
case key of
Control "Delete" ->
case model.selection of
Nothing ->
deleteSpan (span cursor (cursor + 1))
Just selection ->
deleteSpan selection
Control "Backspace" ->
case model.selection of
Nothing ->
deleteSpan (span (cursor - 1) cursor)
Just selection ->
deleteSpan selection
Control "Enter" ->
case model.selection of
Nothing ->
insertAt [ NewLine ] cursor
Just selection ->
deleteSpan selection >> (\m -> insertAt [ NewLine ] m.cursor m)
Control "ArrowLeft" ->
moveCursor (\c -> c - 1)
Control "ArrowRight" ->
moveCursor (\c -> c + 1)
Control "ArrowUp" ->
moveRow -1
Control "ArrowDown" ->
moveRow 1
Control "End" ->
moveCursor (\p -> p + toEndOfLine model.atoms p)
Control "Home" ->
moveCursor (\p -> p - toStartOfLine model.atoms p)
Control "Shift" ->
\m -> { m | selecting = Just m.cursor }
Control "Control" ->
\m -> { m | control = True }
Character char ->
if model.control then
identity
else
case model.selection of
Nothing ->
insertAt [ Letter char ] cursor
Just selection ->
deleteSpan selection >> insertAt [ Letter char ] selection.start
_ ->
identity
in
op model
Enter position ->
let
( selection, cursor ) =
selection =
case model.selecting of
Just start ->
let
@@ -202,45 +66,35 @@ update msg model =
else
position + 1
in
( selectionOf start end, end )
span start end
Nothing ->
let
c =
if model.moving /= Nothing then
position
if model.moving /= Nothing then
span position position
else
model.cursor
in
( model.selection, c )
else
model.selection
in
{ model | hover = Just position, selection = selection, cursor = cursor }
( { model | selection = selection }, Ports.setCallInputGhostSelection selection )
Leave position ->
let
hover =
if model.hover == Just position then
Nothing
else
model.hover
in
{ model | hover = hover }
( model, Cmd.none )
StartSelection position ->
{ model
| selection = Nothing
, selecting = Just position
, cursor = position
}
let
selection =
span position position
in
( { model | selection = selection, selecting = Just position }
, Ports.setCallInputGhostSelection selection
)
StartMoving position ->
{ model | moving = Just position }
( { model | moving = Just position }, Cmd.none )
EndSelection position ->
let
( s, cursor ) =
s =
case model.selecting of
Just start ->
let
@@ -251,19 +105,19 @@ update msg model =
else
position + 1
in
( selectionOf start end, end )
span start end
Nothing ->
( Nothing, position )
span position position
m =
{ model | selection = s, selecting = Nothing, cursor = cursor }
{ model | selection = s, selecting = Nothing }
newModel =
case m.moving of
Just from ->
if from == position then
{ m | selection = span from (from + 1) |> Just }
{ m | selection = span from (from + 1) }
else
let
@@ -287,18 +141,98 @@ update msg model =
Nothing ->
m
in
{ newModel | moving = Nothing }
( { newModel | moving = Nothing }, Ports.setCallInputGhostSelection newModel.selection )
AddSlot ->
model |> insertAt [ Slot Transform.None Style.None ] model.cursor
let
newModel =
model
|> deleteSpan model.selection
|> insertAt [ Slot Transform.None Style.None ] model.selection.start
in
( newModel, Ports.setCallInputGhostSelection newModel.selection )
SetStyle style ->
( applyToSelection (setStyle style) model, Cmd.none )
SetTransform transform ->
let
setTransform atom =
case atom of
Slot _ s ->
Slot transform s
_ ->
atom
in
( applyToSelection setTransform model, Cmd.none )
UpdateFromGhost str ->
let
old =
model.atoms |> List.map atomToChar
new =
str |> String.toList
diff =
Diff.diff old new
folder change ( input, output ) =
case change of
Diff.NoChange _ ->
case input of
first :: rest ->
( rest, Just first :: output )
_ ->
( input, output )
Diff.Added char ->
let
previousStyle =
case output of
first :: _ ->
first |> Maybe.andThen getStyle
_ ->
Nothing
setStyleIfPrevious =
previousStyle |> Maybe.map (\s -> setStyle s) |> Maybe.withDefault identity
in
( input, (char |> charToAtom |> setStyleIfPrevious |> Just) :: output )
Diff.Removed _ ->
case input of
_ :: rest ->
( rest, output )
_ ->
( input, output )
( _, updated ) =
List.foldl folder ( model.atoms, [] ) diff
in
( { model | atoms = updated |> List.filterMap identity |> List.reverse }, Cmd.none )
GhostSelectionChanged selection ->
( { model | selection = selection }, Cmd.none )
applyToSelection : (Atom -> Atom) -> Model -> Model
applyToSelection f model =
{ model | atoms = model.atoms |> applyToSpan model.selection f }
applyToSpan : Span -> (Atom -> Atom) -> List Atom -> List Atom
applyToSpan s f =
List.updateIfIndex (\i -> inSpan i s) f
subscriptions : (Msg -> msg) -> Sub msg
subscriptions wrap =
Sub.batch
[ Browser.onKeyDown (keyDecoder |> Json.map (KeyDown >> wrap))
, Browser.onKeyUp (keyDecoder |> Json.map (KeyUp >> wrap))
]
Ports.getCallInputGhostSelection (GhostSelectionChanged >> wrap)
lines : List Atom -> List (List ( Int, Atom ))
@@ -311,10 +245,150 @@ lines =
view : (Msg -> msg) -> Model -> Html msg
view wrap model =
let
stringVersion =
model.atoms
|> List.map atomToChar
|> String.fromList
callInputGhost =
Html.textarea
[ HtmlA.id "call-input-ghost"
, HtmlA.value stringVersion
, HtmlE.onInput (UpdateFromGhost >> wrap)
, onSelect (GhostSelectionChanged >> wrap)
]
[]
content =
model.atoms ++ [ NewLine ] |> lines |> List.map (viewLine wrap model)
callInputGhost :: (model.atoms ++ [ NewLine ] |> lines |> List.map (viewLine wrap model))
selection =
model.selection
( ( selectedStyle, styleEnabled ), ( selectedTransform, transformEnabled ) ) =
let
selected =
model.atoms |> List.drop selection.start |> List.take (selection.end - selection.start)
allSame parts =
case parts |> List.filterMap identity of
first :: rest ->
if rest |> List.all ((==) first) then
Just first
else
Nothing
[] ->
Nothing
in
( ( selected |> List.map getStyle |> allSame, True )
, ( selected |> List.map getTransform |> allSame, selected |> List.any isSlot )
)
buttonFor icon value flipIf disableValue description act enabled =
let
( action, fullIcon ) =
if flipIf == Just value then
( act disableValue, [ icon, Icon.slash |> Icon.viewIcon ] )
else
( act value, [ icon ] )
finalAction =
if enabled then
Just action
else
Nothing
in
IconButton.view (Icon.layers [] fullIcon) description finalAction
controls =
Html.div [ HtmlA.class "call-controls" ]
[ IconButton.view (Icon.plusCircle |> Icon.viewIcon)
"Add Slot"
(AddSlot |> wrap |> Just)
, Html.div []
[ buttonFor (Icon.text [] "Aa")
Transform.Capitalize
selectedTransform
Transform.None
"Capitalize"
(SetTransform >> wrap)
transformEnabled
, buttonFor (Icon.text [] "AA")
Transform.UpperCase
selectedTransform
Transform.None
"Uppercase"
(SetTransform >> wrap)
transformEnabled
]
, Html.div []
[ buttonFor (Icon.italic |> Icon.viewIcon)
Style.Em
selectedStyle
Style.None
"Emphasize"
(SetStyle >> wrap)
styleEnabled
]
]
ps =
problems model
problemsView =
if ps |> List.isEmpty then
Html.text ""
else
Html.ul [ HtmlA.class "problems" ]
(ps |> List.map (\p -> Html.li [] [ Html.text p ]))
in
Html.div [] [ Card.view Call.type_ Card.Immutable content Card.Face ]
Html.div []
[ controls
, Card.view Call.type_ Card.Immutable content Card.Face
, problemsView
]
onSelect : (Span -> msg) -> Html.Attribute msg
onSelect wrap =
let
decoder =
Json.map2 (\s e -> span s e |> wrap)
(Json.at [ "target", "selectionStart" ] Json.int)
(Json.at [ "target", "selectionEnd" ] Json.int)
in
HtmlE.on "select" decoder
atomToChar : Atom -> Char
atomToChar atom =
case atom of
Letter char _ ->
char
Slot _ _ ->
'_'
NewLine ->
'\n'
charToAtom : Char -> Atom
charToAtom char =
case char of
'_' ->
Slot Transform.None Style.None
'\n' ->
NewLine
_ ->
Letter char Style.None
problems : Model -> List String
@@ -326,18 +400,42 @@ problems model =
[ "Calls must contain at least one slot." ]
selectionOf : Position -> Position -> Maybe Span
selectionOf start end =
if start /= end then
span start end |> Just
else
Nothing
viewLine : (Msg -> msg) -> Model -> List ( Int, Atom ) -> Html msg
viewLine wrap model line =
Html.p [] (line |> List.map (viewAtom wrap model))
Html.p [] (line |> clusterAtoms |> List.map (viewClusters (viewAtom wrap model)))
clusterAtoms : List ( a, Atom ) -> List ( ( a, Atom ), List ( a, Atom ) )
clusterAtoms line =
let
isCluster ( _, a ) ( _, b ) =
case a of
Letter c _ ->
case b of
Letter _ _ ->
if c == ' ' then
False
else
True
_ ->
False
_ ->
False
in
line |> List.groupWhile isCluster
viewClusters : (( Int, Atom ) -> Html msg) -> ( ( Int, Atom ), List ( Int, Atom ) ) -> Html msg
viewClusters v ( single, rest ) =
case rest of
[] ->
v single
_ ->
Html.span [] (single :: rest |> List.map v)
viewAtom : (Msg -> msg) -> Model -> ( Int, Atom ) -> Html msg
@@ -348,28 +446,66 @@ viewAtom wrap model ( position, atom ) =
, position |> Leave |> wrap |> HtmlE.onMouseLeave
, position |> EndSelection |> wrap |> HtmlE.onMouseUp
, HtmlA.classList
[ ( "cursor", model.cursor == position )
, ( "selected", model.selection |> Maybe.map (inSpan position) |> Maybe.withDefault False )
[ ( "cursor", model.selection == Span position position )
, ( "selected", model.selection |> inSpan position )
]
]
in
case atom of
Letter char ->
Html.span
Letter char style ->
Style.toNode style
([ position |> StartSelection |> wrap |> HtmlE.onMouseDown ] ++ attrs)
[ char |> String.fromChar |> Html.text ]
Slot _ _ ->
Slot transform style ->
let
slotAttrs =
[ HtmlA.class "slot empty", position |> StartMoving |> wrap |> HtmlE.onMouseDown ]
HtmlA.class "slot empty"
:: (position |> StartMoving |> wrap |> HtmlE.onMouseDown)
:: Transform.toAttributes transform
in
Html.span (slotAttrs ++ attrs) []
Style.toNode style (slotAttrs ++ attrs) []
NewLine ->
Html.span ([ HtmlA.class "spacer" ] ++ attrs) []
getStyle : Atom -> Maybe Style
getStyle atom =
case atom of
Letter _ s ->
Just s
Slot _ s ->
Just s
_ ->
Nothing
setStyle : Style -> Atom -> Atom
setStyle style atom =
case atom of
Letter c _ ->
Letter c style
Slot t _ ->
Slot t style
_ ->
atom
getTransform : Atom -> Maybe Transform
getTransform atom =
case atom of
Slot t _ ->
Just t
_ ->
Nothing
span : Position -> Position -> Span
span a b =
if a < b then
@@ -384,21 +520,6 @@ inSpan position { start, end } =
position >= start && position < end
keyDecoder : Json.Decoder Key
keyDecoder =
Json.map toKey (Json.field "key" Json.string)
toKey : String -> Key
toKey string =
case String.uncons string of
Just ( char, "" ) ->
Character char
_ ->
Control string
isSlot : Atom -> Bool
isSlot atom =
case atom of
@@ -6,12 +6,9 @@ import Cards.Call.Transform exposing (Transform)
type alias Model =
{ atoms : List Atom
, selection : Maybe Span
, selection : Span
, selecting : Maybe Position
, moving : Maybe Position
, hover : Maybe Position
, cursor : Position
, styled : List ( Span, Style )
, control : Bool
}
@@ -27,7 +24,7 @@ type alias Span =
type Atom
= Letter Char
= Letter Char Style
| Slot Transform Style
| NewLine
@@ -38,11 +35,8 @@ type Msg
| StartSelection Position
| EndSelection Position
| StartMoving Position
| KeyDown Key
| KeyUp Key
| AddSlot
type Key
= Character Char
| Control String
| SetStyle Style
| SetTransform Transform
| UpdateFromGhost String
| GhostSelectionChanged Span
+7
View File
@@ -2,6 +2,7 @@ port module ManyDecks.Ports exposing (..)
import Json.Decode as Json
import ManyDecks.Auth as Auth
import ManyDecks.Pages.Decks.Edit.CallEditor.Model exposing (Span)
port tryGoogleAuth : String -> Cmd msg
@@ -20,3 +21,9 @@ port storeAuth : Maybe Auth.Auth -> Cmd msg
port copy : String -> Cmd msg
port setCallInputGhostSelection : Span -> Cmd msg
port getCallInputGhostSelection : (Span -> msg) -> Sub msg
+7
View File
@@ -9,6 +9,11 @@ interface Flags {
auth?: Auth;
}
interface Span {
start: number;
end: number;
}
type GoogleAuthResult = { code: string } | { error: string };
export interface InboundPort<T> {
@@ -29,6 +34,8 @@ export namespace Elm {
json5Decoded: OutboundPort<object>;
storeAuth: InboundPort<Auth | undefined>;
copy: InboundPort<string>;
setCallInputGhostSelection: InboundPort<Span>;
getCallInputGhostSelection: OutboundPort<Span>;
};
}
export function init(options: {
+17
View File
@@ -123,6 +123,23 @@
flex-grow: 1;
height: 1.2em;
}
#call-input-ghost {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: -100;
opacity: 0;
pointer-events: none;
}
}
.call-controls {
display: flex;
justify-content: space-between;
}
.editing-controls {
+2 -1
View File
@@ -49,7 +49,8 @@
justify-content: flex-start;
margin: 0;
span {
span,
em {
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-all;
+19
View File
@@ -69,6 +69,25 @@ const main = async () => {
}
}
});
app.ports.setCallInputGhostSelection.subscribe(({ start, end }) => {
const element = document.getElementById("call-input-ghost");
if (element !== null) {
const textarea = element as HTMLTextAreaElement;
textarea.focus();
textarea.setSelectionRange(start, end);
if (!element.oninput) {
const updateSelection = () => {
app.ports.getCallInputGhostSelection.send({
start: textarea.selectionStart,
end: textarea.selectionEnd,
});
};
element.oninput = updateSelection;
element.onkeydown = updateSelection;
}
}
});
};
main().catch(console.error);
+2 -2
View File
@@ -99,7 +99,7 @@ const main = async (): Promise<void> => {
app.delete("/api/users", async (req, res) => {
const claims = auth.validate(req.body.token);
await store.deleteUser(claims.sub);
res.status(HttpStatus.OK);
res.status(HttpStatus.OK).json({});
});
app.get("/api/decks/:deckCode", async (req, res) => {
@@ -156,7 +156,7 @@ const main = async (): Promise<void> => {
const id = Code.decode(req.params.deckCode);
const claims = auth.validate(req.body.token);
await store.deleteDeck(id, claims.sub);
res.status(HttpStatus.OK);
res.status(HttpStatus.OK).json({});
});
app.use(