app/javascript/js/controllers/fields/tiptap_field_controller.js

Summary

Maintainability
A
0 mins
Test Coverage
import { Controller } from '@hotwired/stimulus'
import { Editor } from '@tiptap/core'

import Bold from '@tiptap/extension-bold'
import BulletList from '@tiptap/extension-bullet-list'
import Document from '@tiptap/extension-document'
import HardBreak from '@tiptap/extension-hard-break'
import Italic from '@tiptap/extension-italic'
import Link from '@tiptap/extension-link'
import ListItem from '@tiptap/extension-list-item'
import OrderedList from '@tiptap/extension-ordered-list'
import Paragraph from '@tiptap/extension-paragraph'
import Strike from '@tiptap/extension-strike'
import Text from '@tiptap/extension-text'
import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder'

export default class extends Controller {
  static targets = ['editor', 'controller', 'input']

  connect() {
    this.initEditor()
    this.initToolbar()
  }

  initEditor = () => {
    this.editor = new Editor({
      element: this.editorTarget,
      extensions: [
        Bold,
        BulletList,
        Document,
        HardBreak,
        Italic,
        Link.configure({
          openOnClick: false,
        }),
        ListItem,
        OrderedList,
        Paragraph,
        Placeholder.configure({
          placeholder: this.inputTarget.placeholder
        }),
        Strike,
        Text,
        Underline,
      ],
      type: 'HTML',
      content: this.inputTarget.value,
      onUpdate: this.onUpdate,
      onSelectionUpdate: this.onSelectionUpdate,
    })
  }

  initToolbar = () => {
    this.buttons = {
      bold: this.element.querySelector(".tiptap__button--bold"),
      italic: this.element.querySelector(".tiptap__button--italic"),
      underline: this.element.querySelector(".tiptap__button--underline"),
      strike: this.element.querySelector(".tiptap__button--strike"),
      bulletList: this.element.querySelector(".tiptap__button--ul"),
      orderedList: this.element.querySelector(".tiptap__button--ol"),
      link: this.element.querySelector(".tiptap__button--link")
    };

    this.linkArea = this.element.querySelector(".tiptap__link-area")
    this.linkInput = this.element.querySelector(".tiptap__link-field")
    this.unsetButton = this.element.querySelector(".tiptap__link-button--unset")
  }

  onUpdate = () => {
    this.inputTarget.value = this.editor.getHTML()
  }

  onSelectionUpdate = () => {
    Object.keys(this.buttons).forEach(action => {
      const isActive = this.editor.isActive(action)
      this.buttons[action].classList.toggle("tiptap__button--selected", isActive)

      if (action === "link") {
        this.buttons[action].disabled = this.editor.view.state.selection.empty && !isActive
      }
    })

    this.updateLinkArea()
    this.updateLinkButtonState()
  }

  handleButtonClick(event) {
    const action = event.target.dataset.action;
    if (action && this[action]) {
      this[action](event)
    }
  }

  updateButtonState(action) {
    this.buttons[action].classList.toggle("tiptap__button--selected", this.editor.isActive(action))
  }

  updateLinkButtonState() {
    const isLinkActive = this.editor.isActive('link')
    const isSelectionEmpty = this.editor.view.state.selection.empty;
    this.buttons.link.disabled = isSelectionEmpty && !isLinkActive;
  }

  updateLinkArea() {
    const isLinkActive = this.editor.isActive('link')
    this.linkArea.classList.toggle("hidden", !isLinkActive)
    this.unsetButton.classList.toggle("hidden", !isLinkActive)

    if (isLinkActive) {
      const previousUrl = this.editor.getAttributes('link').href;
      this.linkInput.value = previousUrl || "";
    } else {
      this.linkInput.value = "";
    }
  }

  bold() {
    this.editor.chain().focus().toggleBold().run()
    this.updateButtonState("bold")
  }

  italic() {
    this.editor.chain().focus().toggleItalic().run()
    this.updateButtonState("italic")
  }

  underline() {
    this.editor.chain().focus().toggleUnderline().run()
    this.updateButtonState("underline")
  }

  strike() {
    this.editor.chain().focus().toggleStrike().run()
    this.updateButtonState("strike")
  }

  unorderedList() {
    this.editor.chain().focus().toggleBulletList().run()
    this.updateButtonState("bulletList")
  }

  orderedList() {
    this.editor.chain().focus().toggleOrderedList().run()
    this.updateButtonState("orderedList")
  }

  toggleLinkArea(event) {
    const button = event.target.closest(".tiptap__button")
    const linkArea = this.element.querySelector(".tiptap__link-area")
    const previousUrl = this.editor.getAttributes('link').href
    const linkInput = this.element.querySelector(".tiptap__link-field")

    if (previousUrl) {
      linkInput.value = previousUrl
    }

    if (button.classList.contains("tiptap__button--selected")) {
      linkArea.classList.toggle("hidden", true)
      button.classList.toggle("tiptap__button--selected", false)
    } else {
      if (!this.editor.view.state.selection.empty) {
        linkArea.classList.toggle("hidden", false)
        button.classList.toggle("tiptap__button--selected", true)
      }
    }
  }

  setLink() {
    const linkInput = this.element.querySelector(".tiptap__link-field").value
    const unsetButton = this.element.querySelector(".tiptap__link-button--unset")

    if (linkInput) {
      this.editor.chain().focus().extendMarkRange('link').setLink({ href: linkInput }).run()
      unsetButton.classList.toggle("hidden", false)
    } else {
      this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
      unsetButton.classList.toggle("hidden", true)
    }
  }

  unsetLink() {
    const unsetButton = this.element.querySelector(".tiptap__link-button--unset")

    this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
    unsetButton.classList.toggle("hidden", true)
  }

  preventEnter(event) {
    if (event.key === 'Enter' || event.keyCode === 13) {
      event.preventDefault()
    }
  }

  disconnect() {
    this.editor.destroy()
  }
}