Skip to content

Instantly share code, notes, and snippets.

@dodgydre
Last active November 23, 2024 09:26
Show Gist options
  • Save dodgydre/d712f9cf596b281614a727ec5bd53eb0 to your computer and use it in GitHub Desktop.
Save dodgydre/d712f9cf596b281614a727ec5bd53eb0 to your computer and use it in GitHub Desktop.
Vue 3 Quill 2

Quill 2.0.0 component for vue 3

Using - "quill": "^2.0.0-rc.2"

Adjust app.js depending on your use case - I am using it in a Laravel/Inertia project so I've got the Editor component registered in the CreateInertiaApp.

The other files I've got in a quill folder (quill_options.js is quill/options.js, etc.)

<script setup>
import { ref } from "vue"
const editor = ref(null)
const body = ref("")
</script>
<template>
<div>
<Editor
ref="editor"
:options="{ placeholder: '' }"
v-model:content="body"
contentType="html"
>
</Editor>
</div>
</template>
import "./bootstrap";
import "../css/app.css";
import { createApp, h } from "vue";
import { createInertiaApp } from "@inertiajs/vue3";
import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers";
import { ZiggyVue } from "../../vendor/tightenco/ziggy/dist/vue.m";
import Editor from "./quill/" // Vue 3 Quill 2.0.0 Editor Component
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) =>
resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob("./Pages/**/*.vue"),
),
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue, Ziggy)
.component("Editor", Editor) // Register the component globally Editor Component
.mount(el);
},
progress: {
color: "#4B5563",
},
});
import {
TextChangeHandler,
SelectionChangeHandler,
EditorChangeHandler,
QuillOptionsStatic,
RangeStatic,
Sources,
Module,
} from 'quill'
import { PropType, nextTick, defineComponent, onBeforeUnmount, onMounted, ref, watch, h } from "vue";
import Quill from "quill";
import Delta from "quill-delta";
import { toolbarOptions, ToolbarOptions } from "./options";
type ContentPropType = string | Delta | undefined | null
export const Editor = defineComponent({
name: 'Editor',
inheritAttrs: false,
props: {
content: {
type: [String, Object] as PropType<ContentPropType>,
default: null,
},
contentType: {
type: String as PropType<'delta' | 'html' | 'text'>,
default: "delta",
validator: (value: string) => {
return ["delta", "html", "text"].includes(value);
},
},
enable: {
type: Boolean,
default: true,
},
readOnly: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
required: false,
},
theme: {
type: String as PropType<'snow' | 'bubble' | ''>,
default: "snow",
validator: (value: string) => {
return ["snow", "bubble", ""].includes(value);
},
},
toolbar: {
type: [String, Array, Object],
required: false,
validator: (value: string | unknown) => {
if (typeof value === "string" && value !== "") {
return value.charAt(0) === "#"
? true
: Object.keys(toolbarOptions).indexOf(value) !== -1;
}
return true
},
},
modules: {
type: Object as PropType<Module | Module[]>,
required: false,
},
options: {
type: Object as PropType<QuillOptionsStatic>,
required: false,
},
globalOptions: {
type: Object as PropType<QuillOptionsStatic>,
required: false,
},
},
emits: [
"textChange",
"selectionChange",
"editorChange",
"update:content",
"blur",
"focus",
"ready",
],
setup: (props, ctx) => {
onMounted(() => {
initialize();
});
onBeforeUnmount(() => {
quill = null;
});
let quill: Quill | null;
let options: QuillOptionsStatic;
const editor = ref<HTMLElement>();
const registerModule = (moduleName: string, module: any) => {
if (Quill?.imports && moduleName in Quill.imports) {
return
}
Quill.register(moduleName, module)
}
const initialize = () => {
if (!editor.value) return
options = composeOptions()
// Register modules
if (props.modules) {
if (Array.isArray(props.modules)) {
for (const module of props.modules) {
registerModule(`modules/${module.name}`, module.module)
}
} else {
registerModule(`modules/${props.modules.name}`, props.modules.module)
}
}
// Create new Quill instance
quill = new Quill(editor.value, options)
// Set editor content
setContents(props.content)
// Set event handlers
quill.on('text-change', handleTextChange)
quill.on('selection-change', handleSelectionChange)
quill.on('editor-change', handleEditorChange)
// Remove editor class when theme changes
if (props.theme !== 'bubble') editor.value.classList.remove('ql-bubble')
if (props.theme !== 'snow') editor.value.classList.remove('ql-snow')
// Fix clicking the quill toolbar is detected as blur event
quill
.getModule('toolbar')?.container.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault()
})
// Emit ready event
ctx.emit('ready', quill)
}
const composeOptions = (): QuillOptionsStatic => {
const clientOptions: QuillOptionsStatic = {};
if (props.theme !== "") clientOptions.theme = props.theme;
if (props.readOnly) clientOptions.readOnly = props.readOnly;
if (props.placeholder) clientOptions.placeholder = props.placeholder;
if (props.toolbar && props.toolbar !== "") {
clientOptions.modules = {
toolbar: (() => {
if (typeof props.toolbar === "object") {
return props.toolbar;
} else if (typeof props.toolbar === "string") {
const str = props.toolbar;
return str.charAt(0) === "#"
? props.toolbar
: toolbarOptions[props.toolbar as keyof ToolbarOptions];
}
return;
})(),
};
}
if (props.modules) {
const modules = (() => {
const modulesOption = {};
if (Array.isArray(props.modules)) {
for (const module of props.modules) {
modulesOption[module.name] = module.options ?? {};
}
} else {
modulesOption[props.modules.name] = props.modules.options ?? {};
}
return modulesOption;
})();
clientOptions.modules = Object.assign({}, clientOptions.modules, modules);
}
return Object.assign({}, props.globalOptions, props.options, clientOptions);
};
const maybeClone = (delta: ContentPropType) => {
return typeof delta === "object" && delta ? delta.slice() : delta;
};
const deltaHasValuesOtherThanRetain = (delta: Delta) => {
return Object.values(delta.ops).some(
(v) => !v.retain || Object.keys(v).length !== 1
);
};
let internalModel: typeof props.content;
const internalModelEquals = (against: ContentPropType) => {
if (typeof internalModel === typeof against) {
if (against === internalModel) {
return true;
}
// Ref/Proxy does not support instanceof, so do a loose check
if (
typeof against === "object" &&
against &&
typeof internalModel === "object" &&
internalModel
) {
return !deltaHasValuesOtherThanRetain(internalModel.diff(against as Delta));
}
}
return false;
};
const handleTextChange = (delta: Delta, oldContents: Delta, source: Sources) => {
internalModel = maybeClone(getContents() as string | Delta);
// Update v-model:content when text changes
if (!internalModelEquals(props.content)) {
ctx.emit("update:content", internalModel);
}
ctx.emit("textChange", { delta, oldContents, source });
};
const isEditorFocus = ref<Boolean>();
const handleSelectionChange = (range, oldRange, source) => {
// Set isEditorFocus if quill.hasFocus()
isEditorFocus.value = !!quill?.hasFocus();
ctx.emit("selectionChange", { range, oldRange, source });
};
watch(isEditorFocus, (focus) => {
if (focus) ctx.emit("focus", editor);
else ctx.emit("blur", editor);
});
const handleEditorChange = (...args:
| [
name: 'text-change',
delta: Delta,
oldContents: Delta,
source: Sources
]
| [
name: 'selection-change',
range: RangeStatic,
oldRange: RangeStatic,
source: Sources
]) => {
if (args[0] === "text-change")
ctx.emit("editorChange", {
name: args[0],
delta: args[1],
oldContents: args[2],
source: args[3],
});
if (args[0] === "selection-change")
ctx.emit("editorChange", {
name: args[0],
range: args[1],
oldRange: args[2],
source: args[3],
});
};
const getEditor = (): HTMLElement => {
return editor.value as HTMLElement;
};
const getToolbar = (): HTMLElement => {
return quill?.getModule("toolbar")?.container;
};
const getQuill = (): Quill => {
if (quill) return quill;
else
throw `The quill editor hasn't been instantiated yet,
make sure to call this method when the editor ready
or use v-on:ready="onReady(quill)" event instead.`;
};
const getContents = (index?: number, length?: number) => {
if (props.contentType === "html") {
return getHTML();
} else if (props.contentType === "text") {
return getText(index, length);
}
return quill?.getContents(index, length);
};
const setContents = (content: ContentPropType, source: Sources = 'api') => {
const normalizedContent = !content
? props.contentType === "delta"
? new Delta()
: ""
: content;
if (props.contentType === "html") {
setHTML(normalizedContent as string);
} else if (props.contentType === "text") {
setText(normalizedContent as string, source);
} else {
quill?.setContents(normalizedContent as Delta, source);
}
internalModel = maybeClone(normalizedContent);
};
const getText = (index?: number, length?: number): string => {
return quill?.getText(index, length) ?? "";
};
const setText = (text: string, source: Sources = "api") => {
quill?.setText(text, source);
};
const getHTML = (): string => {
return quill?.root.innerHTML ?? "";
};
const setHTML = (html: string) => {
if (quill) quill.root.innerHTML = html;
};
const pasteHTML = (html: string, source: Sources = "api") => {
const delta = quill?.clipboard.convert(html as {});
if (delta) quill?.setContents(delta, source);
};
const focus = () => {
quill?.focus();
};
const reinit = () => {
nextTick(() => {
if (!ctx.slots.toolbar && quill)
quill.getModule("toolbar")?.container.remove()
initialize();
});
};
watch(
() => props.content,
(newContent) => {
if (!quill || !newContent || internalModelEquals(newContent)) return;
// Restore the selection and cursor position after updating the content
const selection = quill.getSelection();
if (selection) {
nextTick(() => quill?.setSelection(selection));
}
setContents(newContent);
},
{ deep: true }
);
watch(
() => props.enable,
(newValue) => {
if (quill) quill.enable(newValue);
}
);
return {
editor,
getEditor,
getToolbar,
getQuill,
getContents,
setContents,
getHTML,
setHTML,
pasteHTML,
focus,
getText,
setText,
reinit,
};
},
render() {
return [
this.$slots.toolbar?.(),
h('div', { ref: 'editor', ...this.$attrs})
]
},
})
import { Editor } from "./Editor.ts";
const globalOptions = {
debug: "warn",
modules: {
toolbar: {
container: [
"bold",
"italic",
"underline",
{ list: "ordered" },
{ list: "bullet" },
{ indent: "-1" },
{ indent: "+1" },
{ size: ["small", false, "large", "huge"] },
{ color: [] },
{ background: [] },
"clean",
"image",
"video",
],
handlers: {
},
},
},
placeholder: "Something to add...",
theme: "snow",
};
Editor.props.globalOptions.default = () => globalOptions;
export default Editor;
// Quill toolbar options
export type ToolbarOptions = typeof toolbarOptions
export const toolbarOptions = {
essential: [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
["bold", "italic", "underline"],
[{ list: "ordered" }, { list: "bullet" }, { align: [] }],
["blockquote", "code-block", "link"],
[{ color: [] }, "clean"],
],
minimal: [
[{ header: 1 }, { header: 2 }],
["bold", "italic", "underline"],
[{ list: "ordered" }, { list: "bullet" }, { align: [] }],
],
full: [
["bold", "italic", "underline", "strike"], // toggled buttons
["blockquote", "code-block"],
[{ header: 1 }, { header: 2 }], // custom button values
[{ list: "ordered" }, { list: "bullet" }],
[{ script: "sub" }, { script: "super" }], // superscript/subscript
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
[{ direction: "rtl" }], // text direction
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ font: [] }],
[{ align: [] }],
["link", "video", "image"],
["clean"], // remove formatting button
],
};
@VIXI0
Copy link

VIXI0 commented Apr 5, 2024

thanks Buddy!! you have made my day!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment