Last active
March 23, 2023 01:42
-
-
Save philipithomas/9801ec9e443b3264d6a1bc0989d153e3 to your computer and use it in GitHub Desktop.
Tiptap controller for Stimulus.js (WIP)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Controller } from "@hotwired/stimulus"; | |
import { Editor } from "@tiptap/core"; | |
import StarterKit from "@tiptap/starter-kit"; | |
import Link from "@tiptap/extension-link"; | |
import Image from "@tiptap/extension-image"; | |
import debounce from "lodash.debounce"; | |
import Placeholder from "@tiptap/extension-placeholder"; | |
import BubbleMenu from "@tiptap/extension-bubble-menu"; | |
import { Plugin, PluginKey } from "@tiptap/pm/state"; | |
import { Extension } from "@tiptap/core"; | |
export default class extends Controller { | |
static targets = [ | |
"editor", | |
"input", | |
"bubbleMenu", | |
"linkModal", | |
"linkModalInner", | |
]; | |
static values = { | |
toggleClass: { type: String, default: "hidden" }, | |
}; | |
connect() { | |
const self = this; | |
this.editor = new Editor({ | |
element: this.editorTarget, | |
editorProps: { | |
attributes: { | |
class: "prose", | |
}, | |
}, | |
extensions: [ | |
StarterKit, | |
Link.configure({ | |
openOnClick: false, | |
HTMLAttributes: { | |
class: "cursor-pointer", | |
}, | |
protocols: ["mailto"], | |
}), | |
Image, | |
Placeholder.configure({ | |
emptyEditorClass: "is-editor-empty", | |
placeholder: "Your post . . .", | |
}), | |
BubbleMenu.configure({ | |
element: this.bubbleMenuTarget, | |
tippyOptions: { | |
duration: 100, | |
}, | |
}), | |
Extension.create({ | |
name: "linkModal", | |
addProseMirrorPlugins() { | |
return [ | |
new Plugin({ | |
key: new PluginKey("linkModalHandlers"), | |
props: { | |
// Here is the full list: https://prosemirror.net/docs/ref/#view.EditorProps | |
handleKeyDown: (view, event) => { | |
if (event.key === "k" && event.metaKey) { | |
if (!view.state.selection.empty) { | |
event.preventDefault(); | |
self.openLinkModal(); | |
} | |
} | |
}, | |
}, | |
}), | |
]; | |
}, | |
}), | |
], | |
injectCSS: false, | |
content: this.inputTarget.value, | |
onUpdate: (e) => { | |
this.inputTarget.value = e.editor.getHTML(); | |
this.debouncedSave(e); | |
}, | |
}); | |
} | |
disconnect() { | |
this.editor.destroy(); | |
this.closeLinkModal(); | |
this.combokeys.unbind("meta+k"); | |
} | |
bold() { | |
this.editor.chain().focus().toggleBold().run(); | |
} | |
italic() { | |
this.editor.chain().focus().toggleItalic().run(); | |
} | |
debouncedSave = debounce((event) => { | |
this.inputTarget.form.requestSubmit(); | |
}, 1500); | |
openLinkModal(e) { | |
if (e && e.target.blur) { | |
e.target.blur(); | |
} | |
// Lock the scroll and save current scroll position | |
this.lockScroll(); | |
// Unhide the modal | |
this.linkModalTarget.classList.remove(this.toggleClassValue); | |
} | |
// Modal stuff | |
closeLinkModal(e) { | |
// Unlock the scroll and restore previous scroll position | |
this.unlockScroll(); | |
// Hide the modal | |
this.linkModalTarget.classList.add(this.toggleClassValue); | |
} | |
closeBackground(e) { | |
if ( | |
e.target === this.linkModalTarget || | |
e.target === this.linkModalInnerTarget | |
) { | |
this.closeLinkModal(e); | |
} | |
} | |
closeWithKeyboard(e) { | |
if ( | |
e.keyCode === 27 && | |
!this.linkModalTarget.classList.contains(this.toggleClassValue) | |
) { | |
this.closeLinkModal(e); | |
} | |
} | |
lockScroll() { | |
// Add right padding to the body so the page doesn't shift | |
// when we disable scrolling | |
const scrollbarWidth = | |
window.innerWidth - document.documentElement.clientWidth; | |
document.body.style.paddingRight = `${scrollbarWidth}px`; | |
// Add classes to body to fix its position | |
document.body.classList.add("fixed", "inset-x-0", "overflow-hidden"); | |
if (this.restoreScrollValue) { | |
// Save the scroll position | |
this.saveScrollPosition(); | |
// Add negative top position in order for body to stay in place | |
document.body.style.top = `-${this.scrollPosition}px`; | |
} | |
} | |
unlockScroll() { | |
// Remove tweaks for scrollbar | |
document.body.style.paddingRight = null; | |
// Remove classes from body to unfix position | |
document.body.classList.remove("fixed", "inset-x-0", "overflow-hidden"); | |
// Restore the scroll position of the body before it got locked | |
if (this.restoreScrollValue) { | |
this.restoreScrollPosition(); | |
// Remove the negative top inline style from body | |
document.body.style.top = null; | |
} | |
} | |
saveScrollPosition() { | |
this.scrollPosition = window.pageYOffset || document.body.scrollTop; | |
} | |
restoreScrollPosition() { | |
if (this.scrollPosition === undefined) return; | |
document.documentElement.scrollTop = this.scrollPosition; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment