140 lines
3.3 KiB
Vue
140 lines
3.3 KiB
Vue
<template>
|
|
<div class="word-counter-varchar d-flex flex-row justify-content-around align-items-center w-100">
|
|
<span>
|
|
{{ __("vue.WordCounterComponent.words") }} : <b>{{ inTwords }}</b>
|
|
</span>
|
|
<span>
|
|
{{ __("vue.WordCounterComponent.characters") }} : <b>{{ inTchars }}</b>
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {
|
|
nextTick,
|
|
onBeforeUnmount,
|
|
onMounted,
|
|
ref,
|
|
useAttrs,
|
|
watch
|
|
} from "vue";
|
|
|
|
import { __ } from "@modules/trans";
|
|
import z from "@modules/zod";
|
|
|
|
defineOptions({ name: "WordCounterComponent" });
|
|
|
|
const props = defineProps({
|
|
for: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
});
|
|
|
|
const attrs = useAttrs();
|
|
|
|
const inTid = ref<string>("");
|
|
const inTwords = ref<number>(0);
|
|
const inTchars = ref<number>(0);
|
|
const inTinput = ref<HTMLInputElement | HTMLTextAreaElement | null>(null);
|
|
|
|
const calculateValues = (string: string): [number, number] => {
|
|
const arr = string.split(" ");
|
|
return [arr.filter((word) => word !== "").length, string.length];
|
|
};
|
|
|
|
const updateValues = (string: string) => {
|
|
const [words, chars] = calculateValues(string);
|
|
inTwords.value = words;
|
|
inTchars.value = chars;
|
|
};
|
|
|
|
const handleInput = (e: Event) => {
|
|
const el = e.target as HTMLInputElement | HTMLTextAreaElement | null;
|
|
if (!el) {
|
|
throw new Error(`${inTid.value} is not an input or textarea`);
|
|
}
|
|
updateValues(el.value);
|
|
};
|
|
|
|
onMounted(() => {
|
|
const json = String(attrs.json ?? "{}");
|
|
let data;
|
|
try {
|
|
data = z.object({
|
|
for: z.string()
|
|
.nonempty()
|
|
.refine((val) => globalThis.document
|
|
.getElementById(val), {message: "The 'for' field must not be empty and exist in the DOM" })
|
|
}).parse(JSON.parse(json));
|
|
} catch (e) {
|
|
console.error(`Invalid JSON data for WordCounterComponent: ${e}`);
|
|
data = {};
|
|
}
|
|
|
|
inTid.value = String(props.for ?? "");
|
|
if (data.for) {
|
|
inTid.value = data.for;
|
|
}
|
|
});
|
|
|
|
watch(
|
|
() => props.for,
|
|
(newVal) => {
|
|
if (newVal) {
|
|
inTid.value = newVal;
|
|
}
|
|
}
|
|
);
|
|
|
|
watch(inTid, async (newVal) => {
|
|
if (!newVal) return;
|
|
|
|
await nextTick();
|
|
const input = document.getElementById(newVal);
|
|
|
|
if (!input || !(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) {
|
|
throw new Error(`${newVal} is not an input or textarea`);
|
|
}
|
|
|
|
inTinput.value = input;
|
|
});
|
|
|
|
watch(inTinput, (newInput, oldInput) => {
|
|
if (oldInput) {
|
|
oldInput.removeEventListener("input", handleInput);
|
|
}
|
|
|
|
if (!newInput) return;
|
|
|
|
newInput.addEventListener("input", handleInput);
|
|
updateValues(newInput.value);
|
|
});
|
|
|
|
// Éviter les fuites de mémoire lors de la destruction du composant
|
|
onBeforeUnmount(() => {
|
|
if (inTinput.value) {
|
|
inTinput.value.removeEventListener("input", handleInput);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
#word-counter+input[type="text"],
|
|
.word-counter+input[type="text"],
|
|
#word-counter+textarea,
|
|
.word-counter+textarea,
|
|
#word-counter-varchar+textarea,
|
|
.word-counter-varchar+textarea {
|
|
border-radius: 0 0 var(--bs-border-radius-xl) var(--bs-border-radius-xl);
|
|
}
|
|
|
|
.word-counter-varchar {
|
|
color: var(--bs-body-color);
|
|
border: var(--bs-border-width) solid var(--bs-border-color);
|
|
border-bottom: 0;
|
|
background-color: rgb(var(--bs-secondary-bg-rgb));
|
|
border-top-left-radius: var(--bs-border-radius-xl);
|
|
border-top-right-radius: var(--bs-border-radius-xl);
|
|
}
|
|
</style>
|