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;