summaryrefslogtreecommitdiff
path: root/packages/frontend/src/os.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/os.ts')
-rw-r--r--packages/frontend/src/os.ts203
1 files changed, 122 insertions, 81 deletions
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 6adf2e590b..f6f4d62d50 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -5,12 +5,13 @@
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
-import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
+import { Component, markRaw, Ref, ref, defineAsyncComponent, nextTick } from 'vue';
import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/scripts/form.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
+import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
@@ -22,8 +23,11 @@ import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue';
import { MenuItem } from '@/types/menu.js';
-import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
+import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
+import { focusParent } from '@/scripts/focus.js';
export const openingWindowsCount = ref(0);
@@ -123,11 +127,13 @@ export function promiseDialog<T extends Promise<any>>(
});
// NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない)
- popup(MkWaitingDialog, {
+ const { dispose } = popup(MkWaitingDialog, {
success: success,
showing: showing,
text: text,
- }, {}, 'closed');
+ }, {
+ closed: () => dispose(),
+ });
return promise;
}
@@ -173,28 +179,24 @@ type EmitsExtractor<T> = {
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
};
-export async function popup<T extends Component>(
+export function popup<T extends Component>(
component: T,
props: ComponentProps<T>,
events: ComponentEmit<T> = {} as ComponentEmit<T>,
- disposeEvent?: keyof ComponentEmit<T>,
-): Promise<{ dispose: () => void }> {
+): { dispose: () => void } {
markRaw(component);
const id = ++popupIdCount;
const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
window.setTimeout(() => {
- popups.value = popups.value.filter(popup => popup.id !== id);
+ popups.value = popups.value.filter(p => p.id !== id);
}, 0);
};
const state = {
component,
props,
- events: disposeEvent ? {
- ...events,
- [disposeEvent]: dispose,
- } : events,
+ events,
id,
};
@@ -206,16 +208,20 @@ export async function popup<T extends Component>(
}
export function pageWindow(path: string) {
- popup(MkPageWindow, {
+ const { dispose } = popup(MkPageWindow, {
initialPath: path,
- }, {}, 'closed');
+ }, {
+ closed: () => dispose(),
+ });
}
export function toast(message: string, renderMfm = false) {
- popup(MkToast, {
+ const { dispose } = popup(MkToast, {
message,
renderMfm,
- }, {}, 'closed');
+ }, {
+ closed: () => dispose(),
+ });
}
export function alert(props: {
@@ -224,11 +230,12 @@ export function alert(props: {
text?: string;
}): Promise<void> {
return new Promise(resolve => {
- popup(MkDialog, props, {
+ const { dispose } = popup(MkDialog, props, {
done: () => {
resolve();
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
@@ -240,14 +247,15 @@ export function confirm(props: {
cancelText?: string;
}): Promise<{ canceled: boolean }> {
return new Promise(resolve => {
- popup(MkDialog, {
+ const { dispose } = popup(MkDialog, {
...props,
showCancelButton: true,
}, {
done: result => {
resolve(result ? result : { canceled: true });
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
@@ -269,7 +277,7 @@ export function actions<T extends {
canceled: false; result: T[number]['value'];
}> {
return new Promise(resolve => {
- popup(MkDialog, {
+ const { dispose } = popup(MkDialog, {
...props,
actions: props.actions.map(a => ({
text: a.text,
@@ -283,7 +291,8 @@ export function actions<T extends {
done: result => {
resolve(result ? result : { canceled: true });
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
@@ -331,7 +340,7 @@ export function inputText(props: {
canceled: false; result: string | null;
}> {
return new Promise(resolve => {
- popup(MkDialog, {
+ const { dispose } = popup(MkDialog, {
title: props.title,
text: props.text,
input: {
@@ -346,7 +355,8 @@ export function inputText(props: {
done: result => {
resolve(result ? result : { canceled: true });
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
@@ -385,7 +395,7 @@ export function inputNumber(props: {
canceled: false; result: number | null;
}> {
return new Promise(resolve => {
- popup(MkDialog, {
+ const { dispose } = popup(MkDialog, {
title: props.title,
text: props.text,
input: {
@@ -398,7 +408,8 @@ export function inputNumber(props: {
done: result => {
resolve(result ? result : { canceled: true });
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
@@ -413,7 +424,7 @@ export function inputDate(props: {
canceled: false; result: Date;
}> {
return new Promise(resolve => {
- popup(MkDialog, {
+ const { dispose } = popup(MkDialog, {
title: props.title,
text: props.text,
input: {
@@ -425,7 +436,8 @@ export function inputDate(props: {
done: result => {
resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
@@ -435,23 +447,29 @@ export function authenticateDialog(): Promise<{
canceled: false; result: { password: string; token: string | null; };
}> {
return new Promise(resolve => {
- popup(MkPasswordDialog, {}, {
+ const { dispose } = popup(MkPasswordDialog, {}, {
done: result => {
resolve(result ? { canceled: false, result } : { canceled: true, result: undefined });
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
+type SelectItem<C> = {
+ value: C;
+ text: string;
+};
+
// default が指定されていたら result は null になり得ないことを保証する overload function
export function select<C = any>(props: {
title?: string;
text?: string;
default: string;
- items: {
- value: C;
- text: string;
- }[];
+ items: (SelectItem<C> | {
+ sectionTitle: string;
+ items: SelectItem<C>[];
+ } | undefined)[];
}): Promise<{
canceled: true; result: undefined;
} | {
@@ -461,10 +479,10 @@ export function select<C = any>(props: {
title?: string;
text?: string;
default?: string | null;
- items: {
- value: C;
- text: string;
- }[];
+ items: (SelectItem<C> | {
+ sectionTitle: string;
+ items: SelectItem<C>[];
+ } | undefined)[];
}): Promise<{
canceled: true; result: undefined;
} | {
@@ -474,28 +492,29 @@ export function select<C = any>(props: {
title?: string;
text?: string;
default?: string | null;
- items: {
- value: C;
- text: string;
- }[];
+ items: (SelectItem<C> | {
+ sectionTitle: string;
+ items: SelectItem<C>[];
+ } | undefined)[];
}): Promise<{
canceled: true; result: undefined;
} | {
canceled: false; result: C | null;
}> {
return new Promise(resolve => {
- popup(MkDialog, {
+ const { dispose } = popup(MkDialog, {
title: props.title,
text: props.text,
select: {
- items: props.items,
+ items: props.items.filter(x => x !== undefined),
default: props.default ?? null,
},
}, {
done: result => {
resolve(result ? result : { canceled: true });
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
@@ -505,53 +524,57 @@ export function success(): Promise<void> {
window.setTimeout(() => {
showing.value = false;
}, 1000);
- popup(MkWaitingDialog, {
+ const { dispose } = popup(MkWaitingDialog, {
success: true,
showing: showing,
}, {
done: () => resolve(),
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
export function waiting(): Promise<void> {
return new Promise(resolve => {
const showing = ref(true);
- popup(MkWaitingDialog, {
+ const { dispose } = popup(MkWaitingDialog, {
success: false,
showing: showing,
}, {
done: () => resolve(),
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> {
return new Promise(resolve => {
- popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
done: result => {
resolve(result);
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> {
return new Promise(resolve => {
- popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
includeSelf: opts.includeSelf,
localOnly: opts.localOnly,
}, {
ok: user => {
resolve(user);
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
return new Promise(resolve => {
- popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
type: 'file',
multiple,
}, {
@@ -560,13 +583,14 @@ export async function selectDriveFile(multiple: boolean): Promise<Misskey.entiti
resolve(files);
}
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
export async function selectDriveFolder(multiple: boolean): Promise<Misskey.entities.DriveFolder[]> {
return new Promise(resolve => {
- popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
type: 'folder',
multiple,
}, {
@@ -575,20 +599,22 @@ export async function selectDriveFolder(multiple: boolean): Promise<Misskey.enti
resolve(folders);
}
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog>): Promise<string> {
return new Promise(resolve => {
- popup(MkEmojiPickerDialog, {
+ const { dispose } = popup(MkEmojiPickerDialog, {
src,
...opts,
}, {
done: emoji => {
resolve(emoji);
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
@@ -597,7 +623,7 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
uploadFolder?: string | null;
}): Promise<Misskey.entities.DriveFile> {
return new Promise(resolve => {
- popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
file: image,
aspectRatio: options.aspectRatio,
uploadFolder: options.uploadFolder,
@@ -605,73 +631,88 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
ok: x => {
resolve(x);
},
- }, 'closed');
+ closed: () => dispose(),
+ });
});
}
export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: {
align?: string;
width?: number;
- viaKeyboard?: boolean;
onClosing?: () => void;
}): Promise<void> {
- return new Promise(resolve => {
- let dispose;
- popup(MkPopupMenu, {
+ let returnFocusTo = getHTMLElementOrNull(src) ?? getHTMLElementOrNull(document.activeElement);
+ return new Promise(resolve => nextTick(() => {
+ const { dispose } = popup(MkPopupMenu, {
items,
src,
width: options?.width,
align: options?.align,
- viaKeyboard: options?.viaKeyboard,
+ returnFocusTo,
}, {
closed: () => {
resolve();
dispose();
+ returnFocusTo = null;
},
closing: () => {
- if (options?.onClosing) options.onClosing();
+ options?.onClosing?.();
},
- }).then(res => {
- dispose = res.dispose;
});
- });
+ }));
}
export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
+ if (
+ defaultStore.state.contextMenu === 'native' ||
+ (defaultStore.state.contextMenu === 'appWithShift' && !ev.shiftKey)
+ ) {
+ return Promise.resolve();
+ }
+
+ let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(document.activeElement);
ev.preventDefault();
- return new Promise(resolve => {
- let dispose;
- popup(MkContextMenu, {
+ return new Promise(resolve => nextTick(() => {
+ const { dispose } = popup(MkContextMenu, {
items,
ev,
}, {
closed: () => {
resolve();
dispose();
+
+ // MkModalを通していないのでここでフォーカスを戻す処理を行う
+ if (returnFocusTo != null) {
+ focusParent(returnFocusTo, true, false);
+ returnFocusTo = null;
+ }
},
- }).then(res => {
- dispose = res.dispose;
});
- });
+ }));
}
export function post(props: Record<string, any> = {}): Promise<void> {
- showMovedDialog();
+ pleaseLogin(undefined, (props.initialText || props.initialNote ? {
+ type: 'share',
+ params: {
+ text: props.initialText ?? props.initialNote.text,
+ visibility: props.initialVisibility ?? props.initialNote?.visibility,
+ localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
+ },
+ } : undefined));
+ showMovedDialog();
return new Promise(resolve => {
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
// Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
// 複数のpost formを開いたときに場合によってはエラーになる
// もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが
- let dispose;
- popup(MkPostFormDialog, props, {
+ const { dispose } = popup(MkPostFormDialog, props, {
closed: () => {
resolve();
dispose();
},
- }).then(res => {
- dispose = res.dispose;
});
});
}