evcc-io/assets/js/components/Sessions/LegendList.vue

209 lines
4.7 KiB
Vue

<template>
<ul
class="root p-0 d-flex flex-wrap column-gap-4 row-gap-2"
:class="{
'root--small-equal-widths': smallEqualWidths,
'root--grid': grid,
}"
>
<li
v-for="legend in legends"
:key="legend.label"
class="legend-item d-flex align-items-baseline gap-2 no-wrap"
:class="{
'legend-item--focusable': legend.focusable,
'legend-item--dim': legend.dim,
}"
:role="legend.focusable ? 'button' : undefined"
:tabindex="legend.focusable ? 0 : undefined"
@click="legend.focusable && onFocus(legend)"
@keydown.enter.prevent="legend.focusable && onFocus(legend)"
@keydown.space.prevent="legend.focusable && onFocus(legend)"
>
<button
v-if="legend.color && isPickable(legend)"
type="button"
class="legend-color-btn align-self-center me-1 bg-transparent border-0 p-0 d-inline-flex"
:style="{ '--badge-color': legend.color }"
:aria-label="legend.label"
@click.stop="onPick(legend, $event)"
>
<span
class="legend-color"
:class="colorClass(legend)"
:style="{
backgroundColor: legend.color,
borderColor: legend.color,
}"
></span>
</button>
<span
v-else-if="legend.color"
class="legend-color align-self-center me-1"
:class="colorClass(legend)"
:style="{
backgroundColor: legend.color,
borderColor: legend.color,
}"
></span>
<div class="legend-label text-nowrap">{{ legend.label }}</div>
<div
v-for="value in valueList(legend.value)"
:key="value"
class="text-muted text-nowrap legend-value text-end"
>
{{ value }}
</div>
</li>
<ColorPickerPopover
v-model="pickerOpen"
:anchor-el="pickerAnchor"
:color="pickerColor"
:title="pickerTitle"
@update:color="onColorChange"
/>
</ul>
</template>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import type { Legend } from "./types";
import type { DeviceColors } from "@/types/evcc";
import ColorPickerPopover from "../Helper/ColorPickerPopover.vue";
import api from "../../api";
export default defineComponent({
name: "LegendList",
components: { ColorPickerPopover },
props: {
legends: Array as PropType<Legend[]>,
grid: Boolean,
smallEqualWidths: Boolean,
deviceColors: { type: Object as PropType<DeviceColors>, default: () => ({}) },
},
emits: ["focus"],
data() {
return {
pickerOpen: false,
pickerAnchor: null as HTMLElement | null,
pickerId: "",
};
},
computed: {
pickerTitle(): string {
const found = (this.legends || []).find((l) => l.id === this.pickerId);
return found?.label || this.pickerId;
},
pickerColor(): string {
return this.deviceColors[this.pickerId] || "";
},
},
methods: {
valueList(value: Legend["value"]) {
if (!value) return [];
return Array.isArray(value) ? value : [value];
},
colorClass(legend: Legend) {
return legend.type === "line" ? "legend-color--line" : "legend-color--area";
},
isPickable(legend: Legend) {
return !!legend.id;
},
onPick(legend: Legend, e: MouseEvent) {
if (!legend.id) return;
if (this.pickerOpen && this.pickerId === legend.id) {
this.pickerOpen = false;
return;
}
this.pickerAnchor = (e.currentTarget as HTMLElement) || null;
this.pickerId = legend.id;
this.pickerOpen = true;
},
onFocus(legend: Legend) {
this.$emit("focus", legend);
},
async onColorChange(color: string) {
if (!this.pickerId) return;
try {
await api.put("devicecolors", { title: this.pickerId, color });
} catch (e) {
console.error("set device color failed", e);
}
},
},
});
</script>
<style scoped>
.root {
justify-content: flex-start;
}
.legend-color {
width: 1rem;
height: 1rem;
flex-shrink: 0;
display: inline-block;
}
.legend-item--focusable {
cursor: pointer;
user-select: none;
transition: opacity 150ms;
}
.legend-item--dim {
opacity: 0.35;
}
.legend-color--area {
border-radius: 50%;
}
.legend-color--line {
height: 2px;
border-radius: 1px;
align-self: center;
}
.legend-color-btn {
cursor: pointer;
border-radius: 50%;
transition: box-shadow 120ms ease-out;
}
.legend-color-btn:hover {
box-shadow: 0 0 0 5px color-mix(in srgb, var(--badge-color) 25%, transparent);
}
.legend-color-btn:focus-visible {
outline: 2px solid var(--evcc-default-text);
outline-offset: 2px;
}
.legend-label {
flex-shrink: 0;
flex-grow: 0;
}
.root--grid .legend-label {
flex-grow: 1;
flex-shrink: 1;
text-overflow: ellipsis;
overflow: hidden;
}
.root--grid .legend-item {
flex-grow: 1;
flex-basis: 100%;
}
.root--grid .legend-value:last-child {
flex-basis: 3.5rem;
}
.root--small-equal-widths {
display: flex;
justify-content: space-evenly;
}
.root--small-equal-widths .legend-item {
flex-basis: 8rem;
}
.root--small-equal-widths .legend-label {
flex-grow: 1;
flex-shrink: 1;
}
</style>