Skip to content

Commit e88eb61

Browse files
Initial commit of TextCursor
0 parents  commit e88eb61

File tree

5 files changed

+377
-0
lines changed

5 files changed

+377
-0
lines changed

bower.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "purescript-textcursor",
3+
"ignore": [
4+
"**/.*",
5+
"node_modules",
6+
"bower_components",
7+
"output"
8+
],
9+
"dependencies": {
10+
"purescript-prelude": "^3.0.0",
11+
"purescript-newtype": "^2.0.0",
12+
"purescript-profunctor-lenses": "^3.2.0",
13+
"purescript-dom": "^4.3.1",
14+
"purescript-symbols": "^3.0.0"
15+
},
16+
"devDependencies": {}
17+
}

src/DOM/Util/TextCursor.purs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
module DOM.Util.TextCursor
2+
( TextCursor(..)
3+
, _before, _selected, _after
4+
, selectAll, moveCursorToStart, moveCursorToEnd
5+
, content, empty, single
6+
, insert, mapAll
7+
) where
8+
9+
import Prelude
10+
import Data.Symbol (SProxy(..))
11+
import Data.Newtype (class Newtype)
12+
import Data.Lens (Lens', (.~))
13+
import Data.Lens.Record (prop)
14+
import Data.Lens.Iso.Newtype (_Newtype)
15+
16+
-- | The `TextCursor` type represents text selection within an input element.
17+
-- | It consists of three regions of text: the text before the cursor, the text
18+
-- | selected, and the text after the selection. This allows replacements to
19+
-- | occur while keeping intact the cursor position/selection.
20+
newtype TextCursor = TextCursor
21+
{ before :: String
22+
, selected :: String
23+
, after :: String
24+
}
25+
26+
derive instance textCursorNewtype :: Newtype TextCursor _
27+
28+
-- | Get the current text in the field. (Everything before, inside, and after
29+
-- | the selection.)
30+
content :: TextCursor -> String
31+
content (TextCursor { before, selected, after }) = before <> selected <> after
32+
33+
-- | An empty input field. No selection.
34+
empty :: TextCursor
35+
empty = TextCursor { before: "", selected: "", after: "" }
36+
37+
-- | Lens for the text before the selection. Empty if the cursor is at the
38+
-- | beginning or the selection starts from the beginning.
39+
_before :: Lens' TextCursor String
40+
_before = _Newtype <<< prop (SProxy :: SProxy "before")
41+
42+
-- | Lens for the text that is selected. Empty if nothing is selected.
43+
_selected :: Lens' TextCursor String
44+
_selected = _Newtype <<< prop (SProxy :: SProxy "selected")
45+
46+
-- | Lens for the text after the selection. Empty if the cursor or selection
47+
-- | reaches the end.
48+
_after :: Lens' TextCursor String
49+
_after = _Newtype <<< prop (SProxy :: SProxy "after")
50+
51+
-- | Apply a `Lens` setting a value to an empty `TextCursor`. When used with
52+
-- | `beforeL`, `selectedL`, or `afterL` this will provide a `TextCursor` with
53+
-- | only one non-empty field.
54+
single :: Lens' TextCursor String -> String -> TextCursor
55+
single l v = l .~ v $ empty
56+
57+
-- | Map all three fields of the `TextCursor` with an endomorphism, performing
58+
-- | a replacement or other transformation such as normalization.
59+
mapAll :: (String -> String) -> TextCursor -> TextCursor
60+
mapAll f (TextCursor { before, selected, after }) = TextCursor
61+
{ before: f before
62+
, selected: f selected
63+
, after: f after
64+
}
65+
66+
-- | Move the cursor to the start of a field, preserving the overall text
67+
-- | content.
68+
moveCursorToStart :: TextCursor -> TextCursor
69+
moveCursorToStart tc = TextCursor
70+
{ before: ""
71+
, selected: ""
72+
, after: content tc
73+
}
74+
75+
-- | Select all of the text in a field.
76+
-- |
77+
-- | Note: selection direction is not specified.
78+
selectAll :: TextCursor -> TextCursor
79+
selectAll tc = TextCursor
80+
{ before: ""
81+
, selected: content tc
82+
, after: ""
83+
}
84+
85+
-- | Move the cursor to the end of a field, preserving the overall text content.
86+
moveCursorToEnd :: TextCursor -> TextCursor
87+
moveCursorToEnd tc = TextCursor
88+
{ before: content tc
89+
, selected: ""
90+
, after: ""
91+
}
92+
93+
-- | Insert a string at the cursor position. If text is selected, the insertion
94+
-- | will be part of the selection. Otherwise it is inserted before the cursor.
95+
insert :: String -> TextCursor -> TextCursor
96+
insert insertion = case _ of
97+
TextCursor { before, selected: "", after } -> TextCursor
98+
{ before: before <> insertion
99+
, selected: ""
100+
, after: after
101+
}
102+
TextCursor { before, selected, after } -> TextCursor
103+
{ before: before
104+
, selected: selected <> insertion
105+
, after: after
106+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
module DOM.Util.TextCursor.Element
2+
( module DOM.Util.TextCursor.Element.Type
3+
, module DOM.Util.TextCursor.Element.HTML
4+
, textCursor, setTextCursor
5+
, modifyTextCursor, modifyTextCursorST
6+
, focusTextCursor, focusTextCursorById
7+
) where
8+
9+
import Prelude
10+
import Data.Maybe (Maybe(Just))
11+
import Data.Tuple (Tuple(Tuple))
12+
import Data.String (length, splitAt)
13+
import Data.Lens (Lens', (.~))
14+
import Control.Monad.Eff (Eff)
15+
import Control.Monad.Eff.Class (class MonadEff, liftEff)
16+
import Control.Monad.State.Class (class MonadState, modify)
17+
import DOM (DOM)
18+
import DOM.Node.Types (ElementId)
19+
import DOM.HTML.HTMLElement (focus)
20+
import DOM.Util.TextCursor (TextCursor(..), content)
21+
import DOM.Util.TextCursor.Element.Type
22+
( TextCursorElement(..)
23+
, htmlTextCursorElementToHTMLElement
24+
, read, readEventTarget
25+
, validate, validate'
26+
, lookupAndValidate
27+
, lookupValidateAndDo
28+
)
29+
import DOM.Util.TextCursor.Element.HTML
30+
( value, setValue
31+
, selectionStart, setSelectionStart
32+
, selectionEnd, setSelectionEnd
33+
)
34+
35+
-- | Helper to split a `String` at a specific position without worrying about
36+
-- | `Nothing`.
37+
splitAtTuple :: Int -> String -> Tuple String String
38+
splitAtTuple i s = case splitAt i s of
39+
Just {before, after} -> Tuple before after
40+
_ | i > 0 -> Tuple s ""
41+
| otherwise -> Tuple "" s
42+
43+
-- | Get the `TextCursor` from a `TextCursorElement`.
44+
textCursor :: forall eff. TextCursorElement -> Eff ( dom :: DOM | eff ) TextCursor
45+
textCursor element = do
46+
val <- value element
47+
start <- selectionStart element
48+
end <- selectionEnd element
49+
let (Tuple prior after) = splitAtTuple end val
50+
let (Tuple before selected) = splitAtTuple start prior
51+
pure $ TextCursor
52+
{ before
53+
, selected
54+
, after
55+
}
56+
57+
-- | Set the `TextCursor` on a `TextCursorElement`. Calls `setValue`,
58+
-- | `setSelectionStart`, and `setSelectionEnd`.
59+
setTextCursor :: forall eff. TextCursor -> TextCursorElement -> Eff ( dom :: DOM | eff ) Unit
60+
setTextCursor (tc@TextCursor { before, selected, after }) element = do
61+
setValue (content tc) element
62+
let start = length before
63+
let end = start + length selected
64+
setSelectionStart start element
65+
setSelectionEnd end element
66+
67+
-- | Modifies the `TextCursor` on an element through the given endomorphism.
68+
modifyTextCursor :: forall eff. (TextCursor -> TextCursor) -> TextCursorElement -> Eff ( dom :: DOM | eff ) Unit
69+
modifyTextCursor f element = do
70+
tc <- f <$> textCursor element
71+
setTextCursor tc element
72+
73+
-- | Modifies the `TextCursor` on an element as well as setting the result in a
74+
-- | State+Eff monad via a lens. Useful for components processing input events!
75+
modifyTextCursorST :: forall eff m s.
76+
MonadState s m =>
77+
MonadEff ( dom :: DOM | eff ) m =>
78+
Lens' s TextCursor ->
79+
(TextCursor -> TextCursor) ->
80+
TextCursorElement -> m Unit
81+
modifyTextCursorST l f element = do
82+
tc <- liftEff $ f <$> textCursor element
83+
liftEff $ setTextCursor tc element
84+
modify $ l .~ tc
85+
86+
-- | Focuses an element after setting the `TextCursor`.
87+
focusTextCursor :: forall eff. TextCursor -> TextCursorElement -> Eff ( dom :: DOM | eff ) Unit
88+
focusTextCursor tc element = do
89+
setTextCursor tc element
90+
focus (htmlTextCursorElementToHTMLElement element)
91+
92+
-- | Looks up an element by id to focus with a `TextCursor`.
93+
focusTextCursorById :: forall eff. ElementId -> TextCursor -> Eff ( dom :: DOM | eff ) Unit
94+
focusTextCursorById name tc = do
95+
lookupValidateAndDo name (focusTextCursor tc)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module DOM.Util.TextCursor.Element.HTML
2+
( value, setValue
3+
, selectionStart, setSelectionStart
4+
, selectionEnd, setSelectionEnd
5+
) where
6+
7+
import Prelude
8+
import DOM.Util.TextCursor.Element.Type (TextCursorElement(TextArea, Input))
9+
import Control.Monad.Eff (Eff)
10+
import DOM (DOM)
11+
import DOM.HTML.Types (HTMLInputElement, HTMLTextAreaElement)
12+
import DOM.HTML.HTMLInputElement as HInput
13+
import DOM.HTML.HTMLTextAreaElement as HTextArea
14+
15+
-- | Lift a pair of getters (Input/TextArea).
16+
getter
17+
:: forall a.
18+
(HTMLInputElement -> a) ->
19+
(HTMLTextAreaElement -> a) ->
20+
TextCursorElement -> a
21+
getter f _ (Input e) = f e
22+
getter _ g (TextArea e) = g e
23+
24+
-- | Lift a pair of setters (Input/TextArea).
25+
setter
26+
:: forall a b.
27+
(b -> HTMLInputElement -> a) ->
28+
(b -> HTMLTextAreaElement -> a) ->
29+
b -> TextCursorElement -> a
30+
setter f _ v (Input e) = f v e
31+
setter _ g v (TextArea e) = g v e
32+
33+
-- | Get the current text value of a `TextCursorElement`.
34+
value :: forall eff. TextCursorElement -> Eff ( dom :: DOM | eff ) String
35+
value = getter HInput.value HTextArea.value
36+
37+
-- | Set the text value of a `TextCursorElement` to the specified string.
38+
setValue :: forall eff. String -> TextCursorElement -> Eff ( dom :: DOM | eff ) Unit
39+
setValue = setter HInput.setValue HTextArea.setValue
40+
41+
-- | Get the index of the start of the selection.
42+
selectionStart :: forall eff. TextCursorElement -> Eff ( dom :: DOM | eff ) Int
43+
selectionStart = getter HInput.selectionStart HTextArea.selectionStart
44+
45+
-- | Set the index of the start of the selection.
46+
setSelectionStart :: forall eff. Int -> TextCursorElement -> Eff ( dom :: DOM | eff ) Unit
47+
setSelectionStart = setter HInput.setSelectionStart HTextArea.setSelectionStart
48+
49+
-- | Get the index of the end of the selection.
50+
selectionEnd :: forall eff. TextCursorElement -> Eff ( dom :: DOM | eff ) Int
51+
selectionEnd = getter HInput.selectionEnd HTextArea.selectionEnd
52+
53+
-- | Set the index of the end of the selection.
54+
setSelectionEnd :: forall eff. Int -> TextCursorElement -> Eff ( dom :: DOM | eff ) Unit
55+
setSelectionEnd = setter HInput.setSelectionEnd HTextArea.setSelectionEnd
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
module DOM.Util.TextCursor.Element.Type
2+
( TextCursorElement(..)
3+
, htmlTextCursorElementToHTMLElement
4+
, read, read', readEventTarget
5+
, validate, validate'
6+
, lookupAndValidate
7+
, lookupValidateAndDo
8+
) where
9+
10+
import Prelude (Unit, bind, map, pure, (<$>), (<#>), (<<<), (>>=))
11+
import Data.Maybe (Maybe(..), maybe)
12+
import Data.Either (Either(..))
13+
import Data.Array (elem)
14+
import Data.Traversable (traverse_)
15+
import Data.Foreign (F, Foreign, toForeign)
16+
import Control.Alternative ((<|>))
17+
import Control.Monad.Except (runExcept)
18+
import Control.Monad.Eff (Eff)
19+
import DOM (DOM)
20+
import DOM.Event.Event (Event, target)
21+
import DOM.HTML.Types
22+
( HTMLElement, HTMLInputElement, HTMLTextAreaElement
23+
, htmlDocumentToNonElementParentNode
24+
, htmlInputElementToHTMLElement
25+
, htmlTextAreaElementToHTMLElement
26+
, readHTMLInputElement
27+
, readHTMLTextAreaElement
28+
)
29+
import DOM.HTML (window)
30+
import DOM.HTML.Window (document)
31+
import DOM.HTML.HTMLInputElement (type_)
32+
import DOM.Node.Types (Element, ElementId)
33+
import DOM.Node.NonElementParentNode (getElementById)
34+
35+
-- | A container for the two usable `Element` types:
36+
-- | - `HTMLInputElement`
37+
-- | - `HTMLTextAreaElement`
38+
-- |
39+
-- | Note that not all `HTMLInputElement` nodes are valid, as they must contain
40+
-- | text content. See `validate` for specifics.
41+
-- |
42+
-- | Common operations are defined in `TextCursor.Element.HTML`.
43+
data TextCursorElement = Input HTMLInputElement | TextArea HTMLTextAreaElement
44+
45+
-- | Convert a `TextCursorElement` to a generic `HTMLElement`. Useful for
46+
-- | `focus`.
47+
htmlTextCursorElementToHTMLElement :: TextCursorElement -> HTMLElement
48+
htmlTextCursorElementToHTMLElement (Input e) = htmlInputElementToHTMLElement e
49+
htmlTextCursorElementToHTMLElement (TextArea e) = htmlTextAreaElementToHTMLElement e
50+
51+
-- | Read a `TextCursorElement` from a `Foreign` type.
52+
read' :: Foreign -> F TextCursorElement
53+
read' e = ta <|> i
54+
where
55+
-- prefer TextArea, which needs no validation
56+
ta = TextArea <$> readHTMLTextAreaElement e
57+
i = Input <$> readHTMLInputElement e
58+
59+
read :: Element -> F TextCursorElement
60+
read = read' <<< toForeign
61+
62+
-- | Read a `TextCursorElement` from the `target` field of an `Event`.
63+
readEventTarget :: Event -> F TextCursorElement
64+
readEventTarget = read' <<< toForeign <<< target
65+
66+
-- | Validate a `TextCursorElement`. Input fields need to have one of the
67+
-- | following types when this is called:
68+
-- | - text (default)
69+
-- | - email
70+
-- | - search
71+
-- | - url
72+
validate :: forall eff. TextCursorElement -> Eff ( dom :: DOM | eff ) (Maybe TextCursorElement)
73+
validate = case _ of
74+
tae@(TextArea e) -> pure (Just tae)
75+
Input e -> map Input <$> validateInput e
76+
where
77+
validateInput :: HTMLInputElement -> Eff ( dom :: DOM | eff ) (Maybe HTMLInputElement)
78+
validateInput e = do
79+
inputtype <- type_ e
80+
pure if elem inputtype whitelist
81+
then Just e
82+
else Nothing
83+
where
84+
whitelist = ["", "text", "email", "search", "url"]
85+
86+
-- | Convert from a `Foreign` error computation (type `F`) to a validated
87+
-- | `TextCursorElement`.
88+
validate' :: forall eff. F TextCursorElement -> Eff ( dom :: DOM | eff ) (Maybe TextCursorElement)
89+
validate' f =
90+
case runExcept f of
91+
Left _ -> pure Nothing
92+
Right e -> validate e
93+
94+
-- | Look up a `TextCursorElement` in the document by id.
95+
lookupAndValidate :: forall eff. ElementId -> Eff ( dom :: DOM | eff ) (Maybe TextCursorElement)
96+
lookupAndValidate name = do
97+
win <- window
98+
doc <- htmlDocumentToNonElementParentNode <$> document win
99+
getElementById name doc <#> map read >>= maybe (pure Nothing) validate'
100+
101+
-- | Look up a `TextCursorElement` by id and run an action if found.
102+
lookupValidateAndDo :: forall eff. ElementId -> (TextCursorElement -> Eff ( dom :: DOM | eff ) Unit) -> Eff ( dom :: DOM | eff ) Unit
103+
lookupValidateAndDo name action =
104+
lookupAndValidate name >>= traverse_ action

0 commit comments

Comments
 (0)