DEV Community 👩‍💻👨‍💻

Cover image for Making TinyMCE draggable with Stimulus
djchadderton
djchadderton

Posted on

Making TinyMCE draggable with Stimulus

My previous post Making TinyMCE work with Rails, Turbolinks and Stimulus looked at how to integrate the full-featured text editor TinyMCE, which can clash with Turbo/Turbolinks and cause issues, into a Rails app using Stimulus.

Another of TinyMCE's little quirks is that it doesn't like to be moved around the page. I had a form with several text editor fields which I wanted to be able to drag to reorder, but whenever I did so, TinyMCE would break.

The only way to make it behave seems to be to remove TinyMCE from every textarea on the page before the drag and reinitialise them all afterwards. A bit of a hassle, but not too difficult with a Stimulus controller.

I used a Stimulus controller I named drag_controller.js to take care of the drag and drop using Sortable (install with yarn add sortablejs), but other packages may work just as well. This is imported at the top of the controller.

import { Controller } from "stimulus"
import Sortable from "sortablejs"

export default class extends Controller {
}
Enter fullscreen mode Exit fullscreen mode

Sortable provides hooks for before it starts to drag and after it has finished called onStart and onEnd, which are perfect for this purpose (if you use another package, they may be named differently).

I therefore added a connect() method:

connect() {
  this.sortable = Sortable.create(this.element, {
    onStart: this.start.bind(this),
    onEnd: this.end.bind(this),
    // Any other settings
  })
}
Enter fullscreen mode Exit fullscreen mode

The this.start() method needs to check that tinyMCE exists on the page, then, if it does, loop through every TinyMCE instance twice: once to store its settings, then again to destroy it (if you try to do this in the same loop, you will be destroying elements in the array you are looping through, which confuses the loop counter, causing it to skip over some).

start() {
  this.settings = []
  if (tinyMCE) {
    tinyMCE.editors.forEach(editor => this.settings.push(editor.settings))
    tinyMCE.editors.forEach(editor => editor.remove())
  }
}
Enter fullscreen mode Exit fullscreen mode

These settings contain the ID for each HTML element that should have a TinyMCE instance attached to it, so there is no need to look for them in order to add them back in the correct place; just loop through the settings and initialise each one.

end() {
  // Some code to update position numbers of elements for sorting
  this.settings.forEach(setting => tinyMCE.init(setting))
}
Enter fullscreen mode Exit fullscreen mode

Just to tidy up, make sure the disconnect method destroys the Sortable (or whatever) instance.

disconnect() {
  this.sortable.destroy()
}
Enter fullscreen mode Exit fullscreen mode

To be honest, it feels a bit dirty to be manipulating TinyMCE elements in a controller that should just be handling drag and drop, but it works. If you know a better, cleaner way, please let me know in the comments.

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.