Halogen + contenteditable

Has anyone had any success using contenteditable on arbitrary elements in a nice way with Halogen? I think I got most of the way there, but I was forced into a bit of an FFI hack for the last part.

To make an HTML div that I can respond to input events on, I did this:

import DOM.HTML.Indexed (Interactive)
import Web.Event.Event (Event)
import Halogen.HTML (AttrName, ElemName(..), Node, element, text)
import Halogen.HTML.Properties (attr)

type HTMLeditDiv = Interactive (onScroll :: Event, onInput :: Event)

editDiv :: forall w i. Node HTMLeditDiv w i
editDiv = element (ElemName "div")

which I can then use as

editDiv [attr (AttrName "contenteditable") "true",
         onInput $ Just <<< HandleInput] [text "Some editable text"]

That works fine: the div is editable, and it generates the right events as you edit the text.

The problem comes when you come to process those events, because I need to extract the HTML text from the event target, and the only way I can see to do it is something like this:

foreign import editDivContent :: Event -> String

handleAction :: forall o m. MonadEffect m => Action -> HalogenM State Action () o m Unit
handleAction = case _ of
  HandleInput event -> do
    liftEffect $ log $ "EVENT: " <> editDivContent event

where editDivContent is this FFI function:

exports.editDivContent = function(event) {
  return event.target.innerHTML;
}

Is there a better way to do this? I fear I’ve just not read the docs for the event types properly!

3 Likes

I know this is years old, but this is what came up when I Googled, so this is probably the place to go…

It seems like using onInput for contenteditable is a mess, because the cursor position seems to get reset on every edit. So instead I’ve taken to using an onBlur handler to process the new content at the end.

Either way, there’s still the task of extracting the content. You can get the event target thus:

import Data.Maybe as Maybe
import Web.DOM.Element as Element
import Web.Event.Event as Event

eventTargetElement :: Event.Event -> Maybe.Maybe Element.Element
eventTargetElement event = Element.fromEventTarget =<< Event.target event

For me, it was sufficient to simulate innerText using this monster:

import Data.Foldable as Fold
import Data.Traversable as Trav
import Web.DOM.HTMLCollection as HTMLs
import Web.DOM.ParentNode as Parent

innerText :: Element.Element -> Effect String
innerText elt = do
  myText <- Node.textContent (Element.toNode elt)
  children <- HTMLs.toArray =<< Parent.children (Element.toParentNode elt)
  childrenText <- Trav.traverse innerText children
  pure $ myText <> Fold.fold childrenText

but if you want innerHTML maybe there’s no better option than the FFI.