enhance(frontend): 投稿フォームのヒントを追加 (#16712)
* wip * wip * Update MkSpot.vue * Update MkPostForm.vue * wip * wip * Update CHANGELOG.md
This commit is contained in:
parent
e8c78e12d5
commit
e312283ea0
|
|
@ -13,6 +13,7 @@
|
||||||
- Enhance: プロフィールへのリンクをユーザーポップアップのアバターに追加
|
- Enhance: プロフィールへのリンクをユーザーポップアップのアバターに追加
|
||||||
- Enhance: ユーザーのノート、フォロー、フォロワーページへのリンクをユーザーポップアップに追加
|
- Enhance: ユーザーのノート、フォロー、フォロワーページへのリンクをユーザーポップアップに追加
|
||||||
- Enhance: プッシュ通知を行うための権限確認をより確実に行うように
|
- Enhance: プッシュ通知を行うための権限確認をより確実に行うように
|
||||||
|
- Enhance: 投稿フォームのチュートリアルを追加
|
||||||
- Fix: 紙吹雪エフェクトがアニメーション設定を考慮せず常に表示される問題を修正
|
- Fix: 紙吹雪エフェクトがアニメーション設定を考慮せず常に表示される問題を修正
|
||||||
- Fix: ナビゲーションバーのリアルタイムモード切替ボタンの状態をよりわかりやすく表示するように
|
- Fix: ナビゲーションバーのリアルタイムモード切替ボタンの状態をよりわかりやすく表示するように
|
||||||
- Fix: ページのタイトルが長いとき、はみ出る問題を修正
|
- Fix: ページのタイトルが長いとき、はみ出る問題を修正
|
||||||
|
|
|
||||||
|
|
@ -10030,6 +10030,60 @@ export interface Locale extends ILocale {
|
||||||
* チャンネルに投稿...
|
* チャンネルに投稿...
|
||||||
*/
|
*/
|
||||||
"channelPlaceholder": string;
|
"channelPlaceholder": string;
|
||||||
|
/**
|
||||||
|
* フォームの説明を表示
|
||||||
|
*/
|
||||||
|
"showHowToUse": string;
|
||||||
|
"_howToUse": {
|
||||||
|
/**
|
||||||
|
* 本文
|
||||||
|
*/
|
||||||
|
"content_title": string;
|
||||||
|
/**
|
||||||
|
* 投稿する内容を入力します。
|
||||||
|
*/
|
||||||
|
"content_description": string;
|
||||||
|
/**
|
||||||
|
* ツールバー
|
||||||
|
*/
|
||||||
|
"toolbar_title": string;
|
||||||
|
/**
|
||||||
|
* ファイルやアンケートの添付、注釈やハッシュタグの設定、絵文字やメンションの挿入などが行えます。
|
||||||
|
*/
|
||||||
|
"toolbar_description": string;
|
||||||
|
/**
|
||||||
|
* アカウントメニュー
|
||||||
|
*/
|
||||||
|
"account_title": string;
|
||||||
|
/**
|
||||||
|
* 投稿するアカウントを切り替えたり、アカウントに保存した下書き・予約投稿を一覧できます。
|
||||||
|
*/
|
||||||
|
"account_description": string;
|
||||||
|
/**
|
||||||
|
* 公開範囲
|
||||||
|
*/
|
||||||
|
"visibility_title": string;
|
||||||
|
/**
|
||||||
|
* ノートを公開する範囲の設定が行えます。
|
||||||
|
*/
|
||||||
|
"visibility_description": string;
|
||||||
|
/**
|
||||||
|
* メニュー
|
||||||
|
*/
|
||||||
|
"menu_title": string;
|
||||||
|
/**
|
||||||
|
* 下書きへの保存、投稿の予約、リアクションの設定など、その他のアクションが行えます。
|
||||||
|
*/
|
||||||
|
"menu_description": string;
|
||||||
|
/**
|
||||||
|
* 投稿ボタン
|
||||||
|
*/
|
||||||
|
"submit_title": string;
|
||||||
|
/**
|
||||||
|
* ノートを投稿します。Ctrl + Enter / Cmd + Enter でも投稿できます。
|
||||||
|
*/
|
||||||
|
"submit_description": string;
|
||||||
|
};
|
||||||
"_placeholders": {
|
"_placeholders": {
|
||||||
/**
|
/**
|
||||||
* いまどうしてる?
|
* いまどうしてる?
|
||||||
|
|
|
||||||
|
|
@ -2641,6 +2641,20 @@ _postForm:
|
||||||
replyPlaceholder: "このノートに返信..."
|
replyPlaceholder: "このノートに返信..."
|
||||||
quotePlaceholder: "このノートを引用..."
|
quotePlaceholder: "このノートを引用..."
|
||||||
channelPlaceholder: "チャンネルに投稿..."
|
channelPlaceholder: "チャンネルに投稿..."
|
||||||
|
showHowToUse: "フォームの説明を表示"
|
||||||
|
_howToUse:
|
||||||
|
content_title: "本文"
|
||||||
|
content_description: "投稿する内容を入力します。"
|
||||||
|
toolbar_title: "ツールバー"
|
||||||
|
toolbar_description: "ファイルやアンケートの添付、注釈やハッシュタグの設定、絵文字やメンションの挿入などが行えます。"
|
||||||
|
account_title: "アカウントメニュー"
|
||||||
|
account_description: "投稿するアカウントを切り替えたり、アカウントに保存した下書き・予約投稿を一覧できます。"
|
||||||
|
visibility_title: "公開範囲"
|
||||||
|
visibility_description: "ノートを公開する範囲の設定が行えます。"
|
||||||
|
menu_title: "メニュー"
|
||||||
|
menu_description: "下書きへの保存、投稿の予約、リアクションの設定など、その他のアクションが行えます。"
|
||||||
|
submit_title: "投稿ボタン"
|
||||||
|
submit_description: "ノートを投稿します。Ctrl + Enter / Cmd + Enter でも投稿できます。"
|
||||||
_placeholders:
|
_placeholders:
|
||||||
a: "いまどうしてる?"
|
a: "いまどうしてる?"
|
||||||
b: "何かありましたか?"
|
b: "何かありましたか?"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
<div :class="$style.headerLeft">
|
<div :class="$style.headerLeft">
|
||||||
<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
|
<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
|
||||||
<button v-click-anime v-tooltip="i18n.ts.account" :class="$style.account" class="_button" @click="openAccountMenu">
|
<button ref="accountMenuEl" v-click-anime v-tooltip="i18n.ts.account" :class="$style.account" class="_button" @click="openAccountMenu">
|
||||||
<img :class="$style.avatar" :src="(postAccount ?? $i).avatarUrl" style="border-radius: 100%;"/>
|
<img :class="$style.avatar" :src="(postAccount ?? $i).avatarUrl" style="border-radius: 100%;"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else><i class="ti ti-rocket-off"></i></span>
|
<span v-else><i class="ti ti-rocket-off"></i></span>
|
||||||
</button>
|
</button>
|
||||||
<button ref="otherSettingsButton" v-tooltip="i18n.ts.other" class="_button" :class="$style.headerRightItem" @click="showOtherSettings"><i class="ti ti-dots"></i></button>
|
<button ref="otherSettingsButton" v-tooltip="i18n.ts.other" class="_button" :class="$style.headerRightItem" @click="showOtherSettings"><i class="ti ti-dots"></i></button>
|
||||||
<button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
<button ref="submitButtonEl" v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
|
||||||
<div :class="$style.submitInner">
|
<div :class="$style.submitInner">
|
||||||
<template v-if="posted"></template>
|
<template v-if="posted"></template>
|
||||||
<template v-else-if="posting"><MkEllipsis/></template>
|
<template v-else-if="posting"><MkEllipsis/></template>
|
||||||
|
|
@ -60,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
|
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<MkInfo v-if="!store.r.tips.value.postForm" :class="$style.showHowToUse"><button class="_textButton" @click="showTour">{{ i18n.ts._postForm.showHowToUse }}</button></MkInfo>
|
||||||
<MkInfo v-if="scheduledAt != null" :class="$style.scheduledAt">
|
<MkInfo v-if="scheduledAt != null" :class="$style.scheduledAt">
|
||||||
<I18n :src="i18n.ts.scheduleToPostOnX" tag="span">
|
<I18n :src="i18n.ts.scheduleToPostOnX" tag="span">
|
||||||
<template #x>
|
<template #x>
|
||||||
|
|
@ -89,7 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
||||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||||
</div>
|
</div>
|
||||||
<footer :class="$style.footer">
|
<footer ref="footerEl" :class="$style.footer">
|
||||||
<div :class="$style.footerLeft">
|
<div :class="$style.footerLeft">
|
||||||
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.upload + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
|
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.upload + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
|
||||||
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.fromDrive + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
|
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.fromDrive + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
|
||||||
|
|
@ -153,6 +154,8 @@ import { DI } from '@/di.js';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
|
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
|
||||||
import { useUploader } from '@/composables/use-uploader.js';
|
import { useUploader } from '@/composables/use-uploader.js';
|
||||||
|
import { startTour } from '@/utility/tour.js';
|
||||||
|
import { closeTip } from '@/tips.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
|
@ -186,6 +189,9 @@ const cwInputEl = useTemplateRef('cwInputEl');
|
||||||
const hashtagsInputEl = useTemplateRef('hashtagsInputEl');
|
const hashtagsInputEl = useTemplateRef('hashtagsInputEl');
|
||||||
const visibilityButton = useTemplateRef('visibilityButton');
|
const visibilityButton = useTemplateRef('visibilityButton');
|
||||||
const otherSettingsButton = useTemplateRef('otherSettingsButton');
|
const otherSettingsButton = useTemplateRef('otherSettingsButton');
|
||||||
|
const accountMenuEl = useTemplateRef('accountMenuEl');
|
||||||
|
const footerEl = useTemplateRef('footerEl');
|
||||||
|
const submitButtonEl = useTemplateRef('submitButtonEl');
|
||||||
|
|
||||||
const posting = ref(false);
|
const posting = ref(false);
|
||||||
const posted = ref(false);
|
const posted = ref(false);
|
||||||
|
|
@ -1285,6 +1291,45 @@ function cancelSchedule() {
|
||||||
scheduledAt.value = null;
|
scheduledAt.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showTour() {
|
||||||
|
if (textareaEl.value == null ||
|
||||||
|
footerEl.value == null ||
|
||||||
|
accountMenuEl.value == null ||
|
||||||
|
visibilityButton.value == null ||
|
||||||
|
otherSettingsButton.value == null ||
|
||||||
|
submitButtonEl.value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startTour([{
|
||||||
|
element: textareaEl.value,
|
||||||
|
title: i18n.ts._postForm._howToUse.content_title,
|
||||||
|
description: i18n.ts._postForm._howToUse.content_description,
|
||||||
|
}, {
|
||||||
|
element: footerEl.value,
|
||||||
|
title: i18n.ts._postForm._howToUse.toolbar_title,
|
||||||
|
description: i18n.ts._postForm._howToUse.toolbar_description,
|
||||||
|
}, {
|
||||||
|
element: accountMenuEl.value,
|
||||||
|
title: i18n.ts._postForm._howToUse.account_title,
|
||||||
|
description: i18n.ts._postForm._howToUse.account_description,
|
||||||
|
}, {
|
||||||
|
element: visibilityButton.value,
|
||||||
|
title: i18n.ts._postForm._howToUse.visibility_title,
|
||||||
|
description: i18n.ts._postForm._howToUse.visibility_description,
|
||||||
|
}, {
|
||||||
|
element: otherSettingsButton.value,
|
||||||
|
title: i18n.ts._postForm._howToUse.menu_title,
|
||||||
|
description: i18n.ts._postForm._howToUse.menu_description,
|
||||||
|
}, {
|
||||||
|
element: submitButtonEl.value,
|
||||||
|
title: i18n.ts._postForm._howToUse.submit_title,
|
||||||
|
description: i18n.ts._postForm._howToUse.submit_description,
|
||||||
|
}]).then(() => {
|
||||||
|
closeTip('postForm');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.autofocus) {
|
if (props.autofocus) {
|
||||||
focus();
|
focus();
|
||||||
|
|
@ -1414,6 +1459,7 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
|
display: block;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
@ -1575,6 +1621,10 @@ html[data-color-scheme=light] .preview {
|
||||||
margin: 0 20px 16px 20px;
|
margin: 0 20px 16px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.showHowToUse {
|
||||||
|
margin: 0 20px 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.cw,
|
.cw,
|
||||||
.hashtags,
|
.hashtags,
|
||||||
.text {
|
.text {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="rootEl" :class="$style.root" :style="{ zIndex }">
|
||||||
|
<div :class="[$style.bg]"></div>
|
||||||
|
<div ref="spotEl" :class="$style.spot"></div>
|
||||||
|
<div ref="bodyEl" :class="$style.body" class="_panel _shadow">
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<div><b>{{ title }}</b></div>
|
||||||
|
<div>{{ description }}</div>
|
||||||
|
<div class="_buttons">
|
||||||
|
<MkButton v-if="hasPrev" small @click="prev"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
|
||||||
|
<MkButton v-if="hasNext" small primary @click="next">{{ i18n.ts.next }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||||
|
<MkButton v-else small primary @click="next">{{ i18n.ts.done }} <i class="ti ti-check"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { nextTick, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
|
||||||
|
import { calcPopupPosition } from '@/utility/popup-position.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
anchorElement?: HTMLElement;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
direction?: 'top' | 'bottom' | 'right' | 'left';
|
||||||
|
hasPrev: boolean;
|
||||||
|
hasNext: boolean;
|
||||||
|
}>(), {
|
||||||
|
direction: 'top',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(prev: 'prev'): void;
|
||||||
|
(next: 'next'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function prev() {
|
||||||
|
emit('prev');
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
emit('next');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootEl = useTemplateRef('rootEl');
|
||||||
|
const bodyEl = useTemplateRef('bodyEl');
|
||||||
|
const spotEl = useTemplateRef('spotEl');
|
||||||
|
const zIndex = os.claimZIndex('high');
|
||||||
|
const spotX = ref(0);
|
||||||
|
const spotY = ref(0);
|
||||||
|
const spotWidth = ref(0);
|
||||||
|
const spotHeight = ref(0);
|
||||||
|
|
||||||
|
function setPosition() {
|
||||||
|
if (spotEl.value == null) return;
|
||||||
|
if (bodyEl.value == null) return;
|
||||||
|
if (props.anchorElement == null) return;
|
||||||
|
|
||||||
|
const rect = props.anchorElement.getBoundingClientRect();
|
||||||
|
spotX.value = rect.left;
|
||||||
|
spotY.value = rect.top;
|
||||||
|
spotWidth.value = rect.width;
|
||||||
|
spotHeight.value = rect.height;
|
||||||
|
|
||||||
|
const data = calcPopupPosition(bodyEl.value, {
|
||||||
|
anchorElement: props.anchorElement,
|
||||||
|
direction: props.direction,
|
||||||
|
align: 'center',
|
||||||
|
innerMargin: 16,
|
||||||
|
x: props.x,
|
||||||
|
y: props.y,
|
||||||
|
});
|
||||||
|
|
||||||
|
bodyEl.value.style.transformOrigin = data.transformOrigin;
|
||||||
|
bodyEl.value.style.left = data.left + 'px';
|
||||||
|
bodyEl.value.style.top = data.top + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
let loopHandler;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
setPosition();
|
||||||
|
|
||||||
|
const loop = () => {
|
||||||
|
setPosition();
|
||||||
|
loopHandler = window.requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
|
||||||
|
loop();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.cancelAnimationFrame(loopHandler);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spot {
|
||||||
|
--x: v-bind("spotX + 'px'");
|
||||||
|
--y: v-bind("spotY + 'px'");
|
||||||
|
--width: v-bind("spotWidth + 'px'");
|
||||||
|
--height: v-bind("spotHeight + 'px'");
|
||||||
|
--padding: 8px;
|
||||||
|
position: absolute;
|
||||||
|
left: calc(var(--x) - var(--padding));
|
||||||
|
top: calc(var(--y) - var(--padding));
|
||||||
|
width: calc(var(--width) + var(--padding) * 2);
|
||||||
|
height: calc(var(--height) + var(--padding) * 2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 0 9999px #000a;
|
||||||
|
transition: left 0.2s ease-out, top 0.2s ease-out, width 0.2s ease-out, height 0.2s ease-out;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
position: absolute;
|
||||||
|
padding: 16px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: max-content;
|
||||||
|
max-width: min(500px, 100vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% {
|
||||||
|
background: color(from var(--MI_THEME-accent) srgb r g b / 0.1);
|
||||||
|
border: 1px solid color(from var(--MI_THEME-accent) srgb r g b / 0.75);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -11,6 +11,7 @@ export const TIPS = [
|
||||||
'postFormUploader',
|
'postFormUploader',
|
||||||
'clips',
|
'clips',
|
||||||
'userLists',
|
'userLists',
|
||||||
|
'postForm',
|
||||||
'tl.home',
|
'tl.home',
|
||||||
'tl.local',
|
'tl.local',
|
||||||
'tl.social',
|
'tl.social',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, ref, shallowRef, watch } from 'vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
|
type TourStep = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
element: HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function startTour(steps: TourStep[]) {
|
||||||
|
return new Promise<void>(async (resolve) => {
|
||||||
|
const currentStepIndex = ref(0);
|
||||||
|
const titleRef = ref(steps[0].title);
|
||||||
|
const descriptionRef = ref(steps[0].description);
|
||||||
|
const anchorElementRef = shallowRef<HTMLElement>(steps[0].element);
|
||||||
|
|
||||||
|
watch(currentStepIndex, (newIndex) => {
|
||||||
|
const step = steps[newIndex];
|
||||||
|
titleRef.value = step.title;
|
||||||
|
descriptionRef.value = step.description;
|
||||||
|
anchorElementRef.value = step.element;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dispose } = os.popup(await import('@/components/MkSpot.vue').then(x => x.default), {
|
||||||
|
title: titleRef,
|
||||||
|
description: descriptionRef,
|
||||||
|
anchorElement: anchorElementRef,
|
||||||
|
hasPrev: computed(() => currentStepIndex.value > 0),
|
||||||
|
hasNext: computed(() => currentStepIndex.value < steps.length - 1),
|
||||||
|
}, {
|
||||||
|
next: () => {
|
||||||
|
if (currentStepIndex.value >= steps.length - 1) {
|
||||||
|
dispose();
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentStepIndex.value++;
|
||||||
|
},
|
||||||
|
prev: () => {
|
||||||
|
currentStepIndex.value--;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue