evcc-io/assets/js/components/Issue/AdditionalItem.vue

165 lines
4 KiB
Vue

<template>
<div class="mb-5" :data-testid="`${id}-additional-item`">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-baseline gap-2">
<strong>{{ title }}</strong>
<button
type="button"
class="btn btn-link btn-sm p-0 text-muted content"
:class="{ 'content--dimmed': !included }"
@click="openModal"
>
{{ $t("issue.additional.showDetails") }}
</button>
</div>
<div class="form-check form-switch mb-0">
<input
:id="id"
v-model="isIncluded"
class="form-check-input"
:class="switchClass"
type="checkbox"
role="switch"
/>
<label class="form-check-label" :for="id">
{{ $t("issue.additional.include") }}
</label>
</div>
</div>
<div class="content" :class="{ 'content--dimmed': !included }">
<slot name="description" :openModal="openModal"></slot>
</div>
<!-- Modal using GenericModal -->
<GenericModal
:id="modalId"
:title="title"
size="lg"
:autofocus="false"
:data-testid="`${id}-modal`"
@closed="modalClosed"
>
<!-- Custom controls slot inside modal -->
<slot name="controls"></slot>
<textarea
ref="textarea"
:value="localContent"
class="form-control font-monospace textarea--tiny"
:rows="textareaRows"
style="white-space: pre; overflow-wrap: normal; resize: vertical"
@input="handleLocalInput"
></textarea>
<div class="d-flex justify-content-end mt-3">
<button
v-if="hasChanges"
type="button"
class="btn btn-primary"
@click="applyAndClose"
>
{{ $t("config.general.applyAndClose") }}
</button>
<button v-else type="button" class="btn btn-secondary" @click="closeModal">
{{ $t("config.general.close") }}
</button>
</div>
</GenericModal>
</div>
</template>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import Modal from "bootstrap/js/dist/modal";
import GenericModal from "../Helper/GenericModal.vue";
import type { HelpType } from "./types";
export default defineComponent({
name: "IssueAdditionalItem",
components: {
GenericModal,
},
props: {
title: { type: String, required: true },
id: { type: String, required: true },
included: { type: Boolean, required: true },
content: { type: String, default: "" },
description: { type: String, default: "" },
helpType: { type: String as PropType<HelpType> },
},
emits: ["update:included", "update:content"],
data() {
return {
localContent: "",
};
},
computed: {
isIncluded: {
get(): boolean {
return this.included;
},
set(value: boolean) {
this.$emit("update:included", value);
},
},
modalId(): string {
return `${this.id}Modal`;
},
textareaRows(): number {
if (!this.localContent) return 26;
const lines = this.localContent.split("\n").length;
return Math.max(26, lines + 1);
},
hasChanges(): boolean {
return this.localContent !== this.content;
},
switchClass(): string {
return this.included && this.helpType === "issue" ? "bg-danger border-danger" : "";
},
},
watch: {
content(newContent: string) {
this.localContent = newContent;
},
},
methods: {
openModal() {
this.localContent = this.content;
const modalElement = document.getElementById(this.modalId) as HTMLElement;
if (modalElement) {
Modal.getOrCreateInstance(modalElement).show();
}
},
modalClosed() {
// Reset local content if there are unsaved changes
this.localContent = this.content;
},
handleLocalInput(event: Event) {
const target = event.target as HTMLTextAreaElement;
this.localContent = target.value;
},
applyAndClose() {
this.$emit("update:content", this.localContent);
this.closeModal();
},
closeModal() {
const modalElement = document.getElementById(this.modalId) as HTMLElement;
if (modalElement) {
Modal.getOrCreateInstance(modalElement).hide();
}
},
},
});
</script>
<style scoped>
.content {
opacity: 1;
transition: opacity 0.3s ease;
}
.content--dimmed {
opacity: 0.5;
}
</style>