calckey/packages/client/src/components/MkCaptcha.vue

137 lines
2.7 KiB
Vue

<template>
<div>
<span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis /></span>
<div ref="captchaEl"></div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import { defaultStore } from "@/store";
import { i18n } from "@/i18n";
type Captcha = {
render(
container: string | Node,
options: {
readonly [_ in
| "sitekey"
| "theme"
| "type"
| "size"
| "tabindex"
| "callback"
| "expired"
| "expired-callback"
| "error-callback"
| "endpoint"]?: unknown;
}
): string;
remove(id: string): void;
execute(id: string): void;
reset(id?: string): void;
getResponse(id: string): string;
};
type CaptchaProvider = "hcaptcha" | "recaptcha";
type CaptchaContainer = {
readonly [_ in CaptchaProvider]?: Captcha;
};
declare global {
interface Window extends CaptchaContainer {}
}
const props = defineProps<{
provider: CaptchaProvider;
sitekey: string;
modelValue?: string | null;
}>();
const emit = defineEmits<{
(ev: "update:modelValue", v: string | null): void;
}>();
const available = ref(false);
const captchaEl = ref<HTMLDivElement | undefined>();
const variable = computed(() => {
switch (props.provider) {
case "hcaptcha":
return "hcaptcha";
case "recaptcha":
return "grecaptcha";
}
});
const loaded = !!window[variable.value];
const src = computed(() => {
switch (props.provider) {
case "hcaptcha":
return "https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off";
case "recaptcha":
return "https://www.recaptcha.net/recaptcha/api.js?render=explicit";
}
});
const captcha = computed<Captcha>(
() => window[variable.value] || ({} as unknown as Captcha)
);
if (loaded) {
available.value = true;
} else {
(
document.getElementById(props.provider) ||
document.head.appendChild(
Object.assign(document.createElement("script"), {
async: true,
id: props.provider,
src: src.value,
})
)
).addEventListener("load", () => (available.value = true));
}
function reset() {
if (captcha.value.reset) captcha.value.reset();
}
function requestRender() {
if (captcha.value.render && captchaEl.value instanceof Element) {
captcha.value.render(captchaEl.value, {
sitekey: props.sitekey,
theme: defaultStore.state.darkMode ? "dark" : "light",
callback: callback,
"expired-callback": callback,
"error-callback": callback,
});
} else {
window.setTimeout(requestRender, 1);
}
}
function callback(response?: string) {
emit("update:modelValue", typeof response === "string" ? response : null);
}
onMounted(() => {
if (available.value) {
requestRender();
} else {
watch(available, requestRender);
}
});
onBeforeUnmount(() => {
reset();
});
defineExpose({
reset,
});
</script>