Create a temporary visual effect when insertNode on on react-slate

230 Views Asked by At

I allow our users to select snippets of text to insert into the Slate editor from a list of text items. To do so I use Transforms.insertNodes(editor, texts) which works perfectly.

The insertion can be abrupt though, from a UX perspective. Especially when there is already a lot of text in the editor. This makes its hard to see where the text you inserted ended up relative to the other text around it.

I am hoping to add a class (or something like it) to all newly inserted texts so that I can run a flash-esque background on the new text. However, I only want it there for a few second (using CSS animation) but I don’t want that class or style to persist to the saved content. Any ideas on how to accomplish this? Many thanks!

1

There are 1 best solutions below

0
On

Here was the best solution I could come up with. Feels pretty hacky and adds a small bit of latency. But, works well enough to create the desired effect reliably without issues.

As I create the Slate content based on the new text I am pasting, I inject a propert "inserted" as true. On the Slate render I pull properties and make them a class. There is a timeout that executes shortly after the insertion (based on the CSS transition that then quickly removes it. Finally, before I send the data to the DB, I make sure nothing with the "inserted" or "flash" properties are sent up by removing them from all words if found.

export const insertText = ({editor, text, inserted}) => {
  const textArr = text.split('\n').filter(text => !!text.trim())
  const texts = textArr.map(txt => ({inserted, type: 'paragraph', children: [{text: txt}]}))

  Transforms.insertNodes(editor, texts)
  setTimeout(() => {
    const el = document.querySelector('.inserted')
    if (el) {
      const rect = el.getBoundingClientRect()
      const isInViewport = rect.top >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight)
      if (!isInViewport) {
        el.style.scrollMargin = '100px'
        document.querySelector('.inserted').scrollIntoView(true)
      }
      setTimeout(() => {
        document.querySelectorAll('.inserted').forEach(el => el.classList.add('flash'))
        setTimeout(() => {
          document.querySelectorAll('.inserted').forEach(el => el.classList.remove('flash', 'inserted'))
        },1500)
      },300)
    }
  },300)

}

export const Element = props => {
  // console.log('ELEMENT', {props})
  const {attributes, children} = props
    return <div className={`${attributes.inserted ? 'inserted' : ''}`}>{children}</div>
}

Elsewhere in the save to DB.

...
...
handleSaveContent(value ? value.map(({flash, inserted,...rest}) => rest) : value)
...
...

In the CSS

.flash {
  animation: wordFlash 2s;
  animation-timing-function: ease-in-out;
}