Skip to content

Instantly share code, notes, and snippets.

@philipithomas
Last active March 23, 2023 01:42
Show Gist options
  • Save philipithomas/9801ec9e443b3264d6a1bc0989d153e3 to your computer and use it in GitHub Desktop.
Save philipithomas/9801ec9e443b3264d6a1bc0989d153e3 to your computer and use it in GitHub Desktop.
Tiptap controller for Stimulus.js (WIP)
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