Skip to content

Commit

Permalink
feat: emoji button and suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
gbicou committed Dec 13, 2023
1 parent 41bbc45 commit bbb84a6
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 3 deletions.
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
2 changes: 2 additions & 0 deletions 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 Down
7 changes: 6 additions & 1 deletion 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();
},
};
},
};
2 changes: 2 additions & 0 deletions src/extensions/emoji.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ExtensionMeta } from "./index";
import { suggestion } from "./emoji-suggestion";

const extension: ExtensionMeta = {
name: "emoji",
Expand Down Expand Up @@ -27,6 +28,7 @@ const extension: ExtensionMeta = {
const { Emoji } = await import("@tiptap-pro/extension-emoji");
return Emoji.configure({
enableEmoticons: props.emojiEnableEmoticons,
suggestion,
});
},
};
Expand Down
6 changes: 4 additions & 2 deletions src/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"table_toggle_header_row": "Toggle header row",
"table_toggle_header_column": "Toggle header column",
"table_toggle_header_cell": "Toggle header cell",
"invisible_characters": "Invisible characters"
"invisible_characters": "Invisible characters",
"emoji": "Insert emoji"
}
},
"fr": {
Expand All @@ -46,7 +47,8 @@
"table_toggle_header_row": "Basculer la ligne d'en-tête",
"table_toggle_header_column": "Basculer la colonne d'en-tête",
"table_toggle_header_cell": "Basculer la cellule d'en-tête",
"invisible_characters": "Caractères invisibles"
"invisible_characters": "Caractères invisibles",
"emoji": "Insérer un émoji"
}
}
}
25 changes: 25 additions & 0 deletions src/tiptap-editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,22 @@
<icons.Image />
</v-button>

<v-emoji-picker
v-if="editorExtensions.includes('emoji')"
v-tooltip="t('tiptap.emoji')"
:disabled="props.disabled"
:x-small="false"
:secondary="false"
small
icon
@emoji-selected="
async (emoji) => {
const shortcode = await resolveEmoji(emoji);
editor.chain().focus().setEmoji(shortcode).run();
}
"
/>

<v-menu v-if="editorExtensions.includes('table')" show-arrow placement="bottom-start">
<template #activator="{ toggle }">
<v-button
Expand Down Expand Up @@ -698,6 +714,10 @@
fill: var(--theme--form--field--input--foreground);
}
.v-icon {
color: var(--theme--form--field--input--foreground);
}
[disabled] svg,
.disabled svg {
fill: var(--theme--form--field--input--foreground-subdued);
Expand Down Expand Up @@ -1050,6 +1070,11 @@ watch(
(disabled) => editor.setEditable(!disabled),
);
const resolveEmoji = async (emoji) => {
const { emojiToShortcode } = await import("@tiptap-pro/extension-emoji");
return emojiToShortcode(emoji, editor.storage.emoji.emojis);
};
onBeforeUnmount(() => {
editor.destroy();
});
Expand Down

0 comments on commit bbb84a6

Please sign in to comment.