diff --git a/packages/frontend/src/components/MkEmojiPickerWindow.vue b/packages/frontend/src/components/MkEmojiPickerWindow.vue new file mode 100644 index 0000000000..0bcc7066c9 --- /dev/null +++ b/packages/frontend/src/components/MkEmojiPickerWindow.vue @@ -0,0 +1,49 @@ + + + + + + + \ No newline at end of file diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 189730330a..7a4c6605bb 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -7,6 +7,7 @@ import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue'; import { EventEmitter } from 'eventemitter3'; +import insertTextAtCursor from 'insert-text-at-cursor'; import * as Misskey from 'cherrypick-js'; import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/scripts/form.js'; @@ -20,6 +21,7 @@ import MkWelcomeToast from '@/components/MkWelcomeToast.vue'; import MkDialog from '@/components/MkDialog.vue'; import MkPasswordDialog from '@/components/MkPasswordDialog.vue'; import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue'; +import MkEmojiPickerWindow from '@/components/MkEmojiPickerWindow.vue'; import MkPopupMenu from '@/components/MkPopupMenu.vue'; import MkContextMenu from '@/components/MkContextMenu.vue'; import { MenuItem } from '@/types/menu.js'; @@ -610,6 +612,60 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: { }); } +type AwaitType = + T extends Promise ? U : + T extends (...args: any[]) => Promise ? V : + T; +let openingEmojiPicker: AwaitType> | null = null; +let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null; +export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) { + if (openingEmojiPicker) return; + + activeTextarea = initialTextarea; + + const textareas = document.querySelectorAll('textarea, input'); + for (const textarea of Array.from(textareas)) { + textarea.addEventListener('focus', () => { + activeTextarea = textarea; + }); + } + + const observer = new MutationObserver(records => { + for (const record of records) { + for (const node of Array.from(record.addedNodes).filter(node => node instanceof HTMLElement) as HTMLElement[]) { + const textareas = node.querySelectorAll('textarea, input') as NodeListOf>; + for (const textarea of Array.from(textareas).filter(textarea => textarea.dataset.preventEmojiInsert == null)) { + if (document.activeElement === textarea) activeTextarea = textarea; + textarea.addEventListener('focus', () => { + activeTextarea = textarea; + }); + } + } + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: false, + characterData: false, + }); + + openingEmojiPicker = await popup(MkEmojiPickerWindow, { + src, + ...opts, + }, { + chosen: emoji => { + insertTextAtCursor(activeTextarea, emoji); + }, + closed: () => { + openingEmojiPicker!.dispose(); + openingEmojiPicker = null; + observer.disconnect(); + }, + }); +} + export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: { align?: string; width?: number;