Skip to content
Merged

Koala #2662

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 37 additions & 106 deletions packages/modules/web_themes/koala/source/src/components/BaseCarousel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
class="row no-wrap justify-center carousel-slide"
>
<div
v-for="item in group"
v-for="(item, idx) in group"
:key="item"
class="item-container"
:style="`min-height: ${maxCardHeight}px`"
:ref="idx === 0 && index === 0 ? 'itemRef' : undefined"
>
<slot name="item" :item="item"></slot>
</div>
Expand All @@ -34,15 +34,7 @@
</template>

<script setup lang="ts">
import {
ref,
computed,
watch,
nextTick,
onMounted,
onBeforeUnmount,
provide,
} from 'vue';
import { ref, computed, watch, nextTick, onMounted } from 'vue';
import { useQuasar } from 'quasar';

const props = defineProps<{
Expand All @@ -53,81 +45,34 @@ const $q = useQuasar();
const currentSlide = ref<number>(0);
const animated = ref<boolean>(true);
const carouselRef = ref<{ $el: HTMLElement } | null>(null);
const carouselWidth = ref(0);
let resizeObserver: ResizeObserver | null = null;
const maxCardHeight = ref<number>(0);

// Calculates and sets the maximum card height for all cards in the active slide
const updateMaxCardHeight = () => {
// Quasar adds CSS class .q-carousel__slide--active when the slide is active - calculation then made only on active slide/s
const cards = document.querySelectorAll(
'.q-carousel__slide--active .item-container',
);
const heights = Array.from(cards).map(
(card) => (card as HTMLElement).offsetHeight,
);
maxCardHeight.value = Math.max(...heights);
};

// Sets up a MutationObserver to watch for changes in the cards of the active slide
const observeCardChanges = () => {
const observer = new MutationObserver(() => {
updateMaxCardHeight();
});
const cards = document.querySelectorAll(
'.q-carousel__slide--active .item-container',
);
cards.forEach((card) => {
observer.observe(card, {
childList: true,
subtree: true,
attributes: true,
});
});
};

onMounted(() => {
nextTick(() => {
if (carouselRef.value && carouselRef.value.$el) {
carouselWidth.value = carouselRef.value.$el.offsetWidth;
// Set up ResizeObserver to update width and card height on resize
resizeObserver = new ResizeObserver(() => {
if (carouselRef.value && carouselRef.value.$el) {
carouselWidth.value = carouselRef.value.$el.offsetWidth;
updateMaxCardHeight();
}
});
resizeObserver.observe(carouselRef.value.$el);
}
// Calculate initial card height and set up MutationObserver
updateMaxCardHeight();
observeCardChanges();
});
});

onBeforeUnmount(() => {
if (resizeObserver && carouselRef.value && carouselRef.value.$el) {
resizeObserver.unobserve(carouselRef.value.$el);
const itemRef = ref<(HTMLElement | null)[]>([]);
const groupSize = ref<number>(2);

const updateGroupSize = () => {
if (!itemRef.value[0]) {
groupSize.value = 1; // Fallback to 1 if no items are available
setTimeout(updateGroupSize, 50);
return;
}
});

const effectiveCardWidth = ref<number | undefined>(undefined);

// Function provided to child components
const setCardWidth = (width: number | undefined) => {
effectiveCardWidth.value = width ? width + 72 : undefined; // Add 72px to account for padding / margins / navigation buttons in carousel
const carouselSlideWidth =
carouselRef.value?.$el.querySelector('.q-carousel__slide')?.clientWidth ??
0;
const itemWidth = itemRef.value[0]?.clientWidth ?? 300; // Fallback
// Get computed padding from the carousel slide element
let padding = 0;
const slideEl = carouselRef.value?.$el.querySelector('.q-carousel__slide');
if (slideEl) {
const style = window.getComputedStyle(slideEl);
padding =
parseFloat(style.paddingLeft || '0') +
parseFloat(style.paddingRight || '0');
}
groupSize.value = Math.max(
1,
Math.floor((carouselSlideWidth - padding) / itemWidth),
);
};

provide('setCardWidth', setCardWidth);

// Computes how many cards can fit in the carousel based on carousel width and the card width
const groupSize = computed(() => {
return effectiveCardWidth.value
? Math.max(1, Math.floor(carouselWidth.value / effectiveCardWidth.value))
: 380;
});

// Groups the items into arrays for each slide, based on the computed group size
const groupedItems = computed(() => {
return props.items.reduce((resultArray, item, index) => {
const chunkIndex = Math.floor(index / groupSize.value);
Expand All @@ -139,35 +84,21 @@ const groupedItems = computed(() => {
}, [] as number[][]);
});

// Updates the current slide and recalculates card heights when the grouped items change
onMounted(async () => {
await nextTick(() => {
updateGroupSize();
window.addEventListener('resize', updateGroupSize);
});
});

watch(
() => groupedItems.value,
async (newValue, oldValue) => {
const findSlide = (itemId: number) => {
return newValue.findIndex((group) => group.includes(itemId));
};
if (!oldValue || oldValue.length === 0) {
currentSlide.value = 0;
return;
}
animated.value = false;
currentSlide.value = Math.max(
findSlide(oldValue[currentSlide.value][0]),
0,
);
await nextTick();
animated.value = true;
updateMaxCardHeight();
observeCardChanges();
},
() => props.items,
() => updateGroupSize(),
);

// Called when the slide changes; recalculates card heights and scrolls to the previous position
const handleSlideChange = () => {
const currentScroll = window.scrollY;
nextTick(() => {
updateMaxCardHeight();
observeCardChanges();
window.scrollTo(0, currentScroll);
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,19 @@
class="full-height card-width"
:class="{ 'battery-sum': props.batteryId === -1 }"
>
<q-card-section
class="text-h6 items-center text-bold justify-between ellipsis"
:title="cardTitle"
>
{{ cardTitle }}
</q-card-section>
<q-separator inset />
<q-card-section class="row flex justify-end">
<q-card-section class="row items-center justify-between">
<div class="text-h6 text-bold ellipsis" :title="cardTitle">
{{ cardTitle }}
</div>
<q-icon
class="cursor-pointer q-mt-sm"
class="cursor-pointer q-ml-auto"
v-if="showSettings"
name="settings"
size="sm"
@click="dialog?.open()"
/>
</q-card-section>
<q-separator class="q-mt-sm" />
<q-card-section
class="row q-mt-sm text-subtitle2 justify-between full-width"
>
Expand All @@ -28,23 +25,27 @@
{{ power }}
</div>
</q-card-section>
<q-separator v-if="showSettings" inset class="q-mt-sm" />
<q-card-section
v-if="showSettings"
class="row q-mt-md justify-between text-subtitle2"
>
<div>Laden mit Überschuss:</div>
<div class="q-ml-sm row items-center">
<q-icon
:name="batteryMode.icon"
size="sm"
class="q-mr-sm"
color="primary"
/>
{{ batteryMode.label }}
</div>
</q-card-section>
<q-separator inset class="q-mt-sm" />
<div v-if="showSettings">
<q-separator inset class="q-mt-sm" />
<q-card-section
v-if="showSettings"
class="row q-mt-md justify-between text-subtitle2"
>
<div>Laden mit Überschuss:</div>
<div class="q-ml-sm row items-center">
<q-icon
:name="batteryMode.icon"
size="sm"
class="q-mr-sm"
color="primary"
/>
<div>
{{ batteryMode.label }}
</div>
</div>
</q-card-section>
</div>
<q-separator inset />
<q-card-section>
<div class="text-subtitle1 text-weight-bold q-mt-sm">Heute:</div>
<div class="row q-mt-sm text-subtitle2 justify-between full-width">
Expand All @@ -62,29 +63,20 @@
</q-card-section>
<q-separator inset class="q-mt-sm" />
<q-card-section>
<SliderDouble
class="q-mt-sm"
:current-value="soc"
:readonly="true"
limit-mode="none"
/>
<SliderDouble :current-value="soc" :readonly="true" limit-mode="none" />
</q-card-section>
</q-card>
<BatterySettingsDialog :battery-id="props.batteryId" ref="dialog" />
</template>

<script setup lang="ts">
import { computed, ref, onMounted, inject } from 'vue';
import { computed, ref } from 'vue';
import { useMqttStore } from 'src/stores/mqtt-store';
import BatterySettingsDialog from './BatterySettingsDialog.vue';
import { useBatteryModes } from 'src/composables/useBatteryModes.ts';
import SliderDouble from './SliderDouble.vue';

const cardRef = ref<{ $el: HTMLElement } | null>(null);
const setCardWidth = inject<((width: number | undefined) => void) | undefined>(
'setCardWidth',
undefined,
);

const props = defineProps<{
batteryId: number;
Expand Down Expand Up @@ -153,11 +145,6 @@ const dailyExportedEnergy = computed(() => {
'---'
);
});

onMounted(() => {
const cardWidth = cardRef.value?.$el.clientWidth;
setCardWidth?.(cardWidth);
});
</script>

<style scoped>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
:maximized="$q.screen.width < 385"
:backdrop-filter="$q.screen.width < 385 ? '' : 'blur(4px)'"
>
<q-card style="min-width: 24em">
<q-card>
<q-card-section>
<div class="text-h6">{{ cardTitle }}</div>
<div class="text-subtitle2 q-mt-sm">Laden mit Überschuss Modus:</div>
<div class="row">
<div class="text-h6 q-pr-sm">Einstellungen:</div>
<div class="text-h6 ellipsis" :title="name">{{ name }}</div>
</div>
</q-card-section>
<q-separator />
<q-card-section>
<div class="text-subtitle2">Laden mit Überschuss Modus:</div>
<BatteryModeButtons />
</q-card-section>
<q-card-actions align="right">
Expand All @@ -32,11 +38,11 @@ const props = defineProps<{

const mqttStore = useMqttStore();

const cardTitle = computed(() => {
if (props.batteryId === undefined) {
return 'Übergreifende Einstellungen';
const name = computed(() => {
if (props.batteryId === undefined || props.batteryId === -1) {
return 'Übergreifend';
}
return `Einstellungen ${mqttStore.batteryName(props.batteryId)}`;
return mqttStore.batteryName(props.batteryId);
});

defineExpose({
Expand Down
Loading