Skip to content

Commit

Permalink
Merge pull request #73 from gbicou/emoji
Browse files Browse the repository at this point in the history
Emoji support
  • Loading branch information
gbicou authored Dec 13, 2023
2 parents 236fd5f + bbb84a6 commit 5637c16
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-deers-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bicou/directus-extension-tiptap": minor
---

Tiptap Emoji extension
5 changes: 5 additions & 0 deletions .changeset/thin-fans-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bicou/directus-extension-tiptap": minor
---

emoji button and suggestions
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"@tiptap/extension-typography": "^2.1.13",
"@tiptap/extension-underline": "^2.1.13",
"@tiptap/pm": "^2.1.13",
"@tiptap/suggestion": "^2.1.13",
"@tiptap/starter-kit": "^2.1.13"
},
"devDependencies": {
Expand All @@ -87,6 +88,7 @@
"rollup": "^4.8.0",
"rollup-plugin-node-externals": "^6.1.2",
"sass": "^1.69.5",
"tippy.js": "^6.3.7",
"typescript": "~5.2.2",
"vue": "^3.3.11",
"vue-i18n": "^9.8.0"
Expand All @@ -101,6 +103,7 @@
},
"packageManager": "[email protected]",
"optionalDependencies": {
"@tiptap-pro/extension-invisible-characters": "^2.5.1"
"@tiptap-pro/extension-invisible-characters": "^2.5.1",
"@tiptap-pro/extension-emoji": "^2.5.1"
}
}
65 changes: 65 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

113 changes: 113 additions & 0 deletions src/components/emoji-list.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<template>
<div class="emoji-list">
<v-list dense>
<v-list-item
v-for="(item, index) in items"
:key="index"
clickable
:active="index === selectedIndex"
@click="selectItem(index)"
>
<v-list-item-icon>
{{ item.emoji }}
</v-list-item-icon>
<v-list-item-content> :{{ item.name }}: </v-list-item-content>
</v-list-item>
</v-list>
</div>
</template>

<script>
export default {
props: {
items: {
type: Array,
required: true,
},
command: {
type: Function,
required: true,
},
editor: {
type: Object,
required: true,
},
},
data() {
return {
selectedIndex: 0,
};
},
watch: {
items() {
this.selectedIndex = 0;
},
},
methods: {
onKeyDown({ event }) {
if (event.key === "ArrowUp") {
this.upHandler();
return true;
}
if (event.key === "ArrowDown") {
this.downHandler();
return true;
}
if (event.key === "Enter") {
this.enterHandler();
return true;
}
return false;
},
upHandler() {
this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length;
},
downHandler() {
this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
},
enterHandler() {
this.selectItem(this.selectedIndex);
},
selectItem(index) {
const item = this.items[index];
if (item) {
this.command({ name: item.name });
}
},
},
};
</script>

<style lang="scss">
.emoji-list {
max-height: 30vh;
padding: 0 4px;
overflow-x: hidden;
overflow-y: auto;
background-color: var(--theme--popover--menu--background);
border: none;
border-radius: var(--theme--popover--menu--border-radius);
box-shadow: var(--theme--popover--menu--box-shadow);
transition-timing-function: var(--transition-out);
transition-duration: var(--fast);
transition-property: opacity, transform;
contain: content;
.v-list {
--v-list-background-color: transparent;
}
}
</style>
66 changes: 66 additions & 0 deletions src/extensions/emoji-suggestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { VueRenderer } from "@tiptap/vue-3";
import EmojiList from "../components/emoji-list.vue";
import type { SuggestionOptions } from "@tiptap/suggestion";
import type { EmojiItem } from "@tiptap-pro/extension-emoji";
import tippy, { type Instance as TippyInstance } from "tippy.js";

export const suggestion: Omit<SuggestionOptions, "editor"> = {
items: ({ editor, query }) => {
return editor.storage.emoji.emojis
.filter(({ shortcodes, tags }: EmojiItem) => {
return (
shortcodes.find((shortcode) => shortcode.startsWith(query.toLowerCase())) ||
tags.find((tag) => tag.startsWith(query.toLowerCase()))
);
})
.slice(0, 5);
},

render: () => {
let component: VueRenderer | null = null;
let popup: TippyInstance | null = null;

return {
onStart: (props) => {
component = new VueRenderer(EmojiList, {
props,
editor: props.editor,
});

popup = tippy(document.body, {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
},

onUpdate(props) {
component?.updateProps(props);

popup?.setProps({
getReferenceClientRect: props.clientRect,
});
},

onKeyDown(props) {
if (props.event.key === "Escape") {
popup?.hide();
component?.destroy();

return true;
}

return component?.ref?.onKeyDown(props);
},

onExit() {
popup?.destroy();
component?.destroy();
},
};
},
};
36 changes: 36 additions & 0 deletions src/extensions/emoji.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ExtensionMeta } from "./index";
import { suggestion } from "./emoji-suggestion";

const extension: ExtensionMeta = {
name: "emoji",
title: "Emoji",
package: "@tiptap-pro/extension-emoji",
group: "node",
defaults: {
enableEmoticons: false,
},
options: [
{
field: "emojiEnableEmoticons",
name: "Emoji emoticons",
type: "boolean",
meta: {
interface: "boolean",
width: "half",
note: "Specifies whether text should be converted to emoticons (e.g. <3 to ❤️)",
},
schema: {
default_value: false,
},
},
],
load: async (props) => {
const { Emoji } = await import("@tiptap-pro/extension-emoji");
return Emoji.configure({
enableEmoticons: props.emojiEnableEmoticons,
suggestion,
});
},
};

export default extension;
3 changes: 3 additions & 0 deletions src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import task from "./task";
import table from "./table";
import image from "./image";
import invisibleCharacters from "./invisible-characters";
import emoji from "./emoji";

type ExtensionGroup = "mark" | "node" | "editor";

Expand All @@ -40,6 +41,7 @@ interface ExtensionsProps {
focusMode: FocusOptions["mode"];
taskItemNested: TaskItemOptions["nested"];
tableResizable: TableOptions["resizable"];
emojiEnableEmoticons: boolean;
}

export interface ExtensionMeta<E extends AnyExtension = AnyExtension> {
Expand Down Expand Up @@ -71,6 +73,7 @@ export const extensionsMeta: ExtensionMeta[] = [
characterCount,
// pro
invisibleCharacters,
emoji,
];

export async function loadExtensions(props: ExtensionsProps): Promise<Extensions> {
Expand Down
Loading

0 comments on commit 5637c16

Please sign in to comment.