Skip to content

Commit

Permalink
Merge pull request #147 from SinglePoi/feat/cmd-msgbox
Browse files Browse the repository at this point in the history
feat: command for messagebox
  • Loading branch information
Nauxscript authored Mar 12, 2024
2 parents b5b5d88 + 45e7d4a commit 26901fe
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 9 deletions.
2 changes: 1 addition & 1 deletion apps/client/components/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import { useMessage } from "naive-ui";
import { navigateTo } from "nuxt/app";
import { useRoute } from "vue-router";
import { Theme, useDarkMode } from "~/composables/darkMode";
import MessageBox from "./main/MessageBox.vue";
import MessageBox from "./main/MessageBox/MessageBox.vue";
import { useUserStore } from "~/store/user";
import { cleanToken } from "~/utils/token";
import { computed } from "vue";
Expand Down
128 changes: 128 additions & 0 deletions apps/client/components/main/MessageBox/MessageBox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { createVNode, render, type ComponentPublicInstance } from "vue";
import MessageBoxConstructor from "./MessageBox.vue";
import type { IMessageBoxProps } from "~/composables/messageBox/modal";

interface MessageBoxOptions {
/** Text content of confirm button */
confirmBtnText: string;
/** Text content of cancel button */
cancelBtnText: string;
/** Custom element to append the message box to */
appendTo?: HTMLElement | string;
}

type Action = "confirm" | "cancel";

interface MessageBoxProps extends IMessageBoxProps {
container: HTMLElement;
}

const messageInstance = new Map<
ComponentPublicInstance<MessageBoxProps>,
{
options: any;
reject: (res: any) => void;
resolve: (reson?: any) => void;
}
>();

const genContainer = () => {
return document.createElement("div");
};

const getAppendToElement = (props: any): HTMLElement => {
let appendTo: HTMLElement | null = document.body;
if (props.appendTo) {
if (typeof props.appendTo === "string") {
appendTo = document.querySelector<HTMLElement>(props.appendTo);
}
if (props.appendTo instanceof Element) {
appendTo = props.appendTo;
}
if (!(appendTo instanceof Element)) {
appendTo = document.body;
}
}
return appendTo;
};

const teardown = (
vm: ComponentPublicInstance<MessageBoxProps>,
container: HTMLElement
) => {
render(null, container);
messageInstance.delete(vm);
};

const initInstance = (props: any, container: HTMLElement) => {
const vnode = createVNode(MessageBoxConstructor, props);
render(vnode, container);
getAppendToElement(props).appendChild(container.firstElementChild!);
return vnode.component;
};

const showMessage = (options: any) => {
const container = genContainer();

options.onConfirm = () => {
const currentMsg = messageInstance.get(vm)!;
currentMsg.resolve("confirm");

teardown(vm, container);
};

options.onCancel = () => {
const currentMsg = messageInstance.get(vm)!;
currentMsg.reject("cancel");

teardown(vm, container);
};

const instance = initInstance(options, container)!;

const vm = instance.proxy as ComponentPublicInstance<MessageBoxProps>;

vm.container = container;

for (const prop in options) {
if (Object.hasOwn(options, prop) && !Object.hasOwn(vm.$props, prop)) {
vm[prop as keyof ComponentPublicInstance] = options[prop];
}
}

return vm;
};

function MessageBox(
content: string = "Are you sure?",
title: string = "Tips",
options?: MessageBoxOptions
): Promise<Action> {
return new Promise((resolve, reject) => {
const vm = showMessage(
Object.assign(
{
content,
title,
isShowModal: true,
confirmBtnText: "Confirm",
cancelBtnText: "Cancel",
},
options
)
);
messageInstance.set(vm, {
options,
resolve,
reject,
});
});
}

MessageBox.close = () => {
messageInstance.forEach((_, vm) => {
teardown(vm, vm.container);
});
};

export default MessageBox;
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<p class="py-4">{{ content }}</p>
<div class="modal-action">
<form method="dialog">
<button class="btn" @click="isShow = false">{{ cancelBtnText }}</button>
<button class="btn cancel" @click="handleCancel">{{ cancelBtnText }}</button>
</form>
<button v-if="confirmBtnText" class="btn" @click="handleConfirm">{{ confirmBtnText }}</button>
<button v-if="confirmBtnText" class="btn confirm" @click="handleConfirm">{{ confirmBtnText }}</button>
</div>
</div>
</dialog>
Expand All @@ -27,6 +27,6 @@ const props = withDefaults(defineProps<IMessageBoxProps>(), {
const emits = defineEmits<EmitsType>();
const { dialogBoxRef, isShow, handleConfirm } = useMessageBoxModal(props, emits)
const { dialogBoxRef, isShow, handleConfirm, handleCancel } = useMessageBoxModal(props, emits)
</script>
65 changes: 65 additions & 0 deletions apps/client/components/main/MessageBox/tests/message-box.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { afterEach, describe, expect, test } from "vitest";
import MessageBox from "../MessageBox";
import { nextTick } from "vue";
import { flushPromises } from "@vue/test-utils";

const selector = ".modal";

describe("MessageBox", () => {
afterEach(async () => {
MessageBox.close();
document.body.innerHTML = "";
await flushPromises();
});

test("create messageBox", async () => {
MessageBox("这是一段内容", "消息");
let msgbox: any = document.querySelector(selector);

expect(msgbox).toBeDefined();
expect(msgbox.querySelector(".font-bold").textContent).toEqual("消息");
expect(msgbox.querySelector(".py-4").textContent).toEqual("这是一段内容");
});

test("close messageBox", async () => {
MessageBox("这是一段内容", "消息");

MessageBox.close();
let msgbox: any = document.querySelector(selector);
expect(msgbox).toBe(null);
});

describe("promise", () => {
test("confirm", async () => {
let msgAction = "";
MessageBox("此操作将永久删除该文件, 是否继续?", "提示").then((action) => {
msgAction = action;
});
await flushPromises();
const btn = document
.querySelector(selector)!
.querySelector(".confirm") as HTMLButtonElement;
btn.click();
await flushPromises();

expect(msgAction).toEqual("confirm");
});

test("cancel", async () => {
let msgAction = "";
MessageBox("此操作将永久删除该文件, 是否继续?", "提示").catch(
(action) => {
msgAction = action;
}
);
await flushPromises();
const btn = document
.querySelector(selector)!
.querySelector(".cancel") as HTMLButtonElement;
btn.click();
await flushPromises();

expect(msgAction).toEqual("cancel");
});
});
});
2 changes: 1 addition & 1 deletion apps/client/components/main/Tool.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { useCourseStore } from "~/store/course";
import { useGameMode } from "~/composables/main/game";
import { useRankModal } from "~/composables/rank/modal";
import ProgressRank from "~/components/rank/ProgressRank.vue";
import MessageBox from "~/components/main/MessageBox.vue";
import MessageBox from "~/components/main/MessageBox/MessageBox.vue";
import StudyVideoLink from "./StudyVideoLink.vue";
const rankModal = useRankModal();
Expand Down
8 changes: 5 additions & 3 deletions apps/client/composables/messageBox/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ export interface IMessageBoxProps {
export interface EmitsType {
(event: "update:isShowModal", isShow: boolean): void;
(event: "confirm"): void;
(event: "cancel"): void;
}

export function useMessageBoxModal(props: IMessageBoxProps, emits: EmitsType) {
let dialogBoxRef = ref<HTMLElement | null>(null);

function handleConfirm() {
handleClose()
emits("confirm");
handleCancel()
}

function handleClose() {
function handleCancel() {
isShow.value = false;
emits("cancel");
}

const isShow = computed({
Expand Down Expand Up @@ -52,6 +54,6 @@ export function useMessageBoxModal(props: IMessageBoxProps, emits: EmitsType) {
dialogBoxRef,
isShow,
handleConfirm,
handleClose
handleCancel,
};
}
2 changes: 1 addition & 1 deletion apps/client/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ import { onMounted, onUnmounted, ref } from "vue";
import { useRouter } from "vue-router";
import { registerShortcut, cancelShortcut } from "~/utils/keyboardShortcuts";
import { useGameStore } from "~/store/game";
import MessageBox from "~/components/main/MessageBox.vue";
import MessageBox from "~/components/main/MessageBox/MessageBox.vue";
const { handleKeydown, isLoading } = useShortcutToGame();
const gameStore = useGameStore();
Expand Down

0 comments on commit 26901fe

Please sign in to comment.