From d101b53d3de2c80cfa54f89bcb3fff67d808f294 Mon Sep 17 00:00:00 2001 From: chfhtw Date: Fri, 25 Jul 2025 09:27:56 +0200 Subject: [PATCH] Calendar: ResizeObserver for compact day mode --- .../js/components/Calendar/Mode/Day/View.js | 15 +++- public/js/components/Cis/LvPlan/LvPlan.js | 3 +- .../js/components/Cis/Mylv/RoomInformation.js | 3 +- public/js/composables/Responsive.js | 85 +++++++++++++++++++ 4 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 public/js/composables/Responsive.js diff --git a/public/js/components/Calendar/Mode/Day/View.js b/public/js/components/Calendar/Mode/Day/View.js index 09738f1d0..cd953f33a 100644 --- a/public/js/components/Calendar/Mode/Day/View.js +++ b/public/js/components/Calendar/Mode/Day/View.js @@ -3,6 +3,8 @@ import LabelDay from '../../Base/Label/Day.js'; import LabelDow from '../../Base/Label/Dow.js'; import LabelTime from '../../Base/Label/Time.js'; +import { useResizeObserver } from '../../../../composables/Responsive.js'; + export default { name: "DayView", components: { @@ -22,8 +24,7 @@ export default { required: true }, emptyMessage: String, - emptyMessageDetails: String, - compact: Boolean + emptyMessageDetails: String }, emits: [ "requestModalOpen", @@ -104,11 +105,21 @@ export default { } } }, + setup() { + const container = Vue.ref(null); // use useTemplateRef when updating to Vue 3.5 + const { compact } = useResizeObserver(container, 750); + + return { + container, // must be exposed or it won't be set in Vue < 3.5 + compact + }; + }, mounted() { this.gridMainRef = this.$refs.grid.$refs.main; }, template: /* html */`
diff --git a/public/js/components/Cis/LvPlan/LvPlan.js b/public/js/components/Cis/LvPlan/LvPlan.js index cdb2c38c1..4fbb7c871 100644 --- a/public/js/components/Cis/LvPlan/LvPlan.js +++ b/public/js/components/Cis/LvPlan/LvPlan.js @@ -34,8 +34,7 @@ export default { modeOptions: { day: { emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')), - emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')), - compact: false + emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')) }, week: { collapseEmptyDays: false diff --git a/public/js/components/Cis/Mylv/RoomInformation.js b/public/js/components/Cis/Mylv/RoomInformation.js index ac02a9d51..0f4fa0603 100644 --- a/public/js/components/Cis/Mylv/RoomInformation.js +++ b/public/js/components/Cis/Mylv/RoomInformation.js @@ -32,8 +32,7 @@ export default { modeOptions: { day: { emptyMessage: Vue.computed(() => this.$p.t('rauminfo/keineRaumReservierung')), - emptyMessageDetails: Vue.computed(() => this.$p.t('rauminfo/keineRaumReservierung')), - compact: false + emptyMessageDetails: Vue.computed(() => this.$p.t('rauminfo/keineRaumReservierung')) }, week: { collapseEmptyDays: false diff --git a/public/js/composables/Responsive.js b/public/js/composables/Responsive.js new file mode 100644 index 000000000..f1761d870 --- /dev/null +++ b/public/js/composables/Responsive.js @@ -0,0 +1,85 @@ +/** + * size returns the key of smallest threshold array (integer converts to + * { compact: threshold }) entry that is bigger than the current width of + * the element or 'full' if none is found. + * compact is true if the smallest threshold array entry is bigger than the + * current width or false otherwise. + * + * @param DOMElement|VueTemplateRef element + * @param object|array|integer threshold + * @return object { compact:Boolean, size:String } + */ +export function useResizeObserver(element, threshold) { + /* Result Vars */ + const compact = Vue.ref(false); + const size = Vue.ref(false); + + /* Helper Vars */ + const mounted = Vue.ref(false); + const elementRef = Vue.computed(() => { + if (!Vue.isRef(element)) + return element; + + if (!element.value) + return element.value; + + if (element.value.$el) // Maybe there is a better test + return element.value.$el; + + return element.value; + }); + const compareArray = Vue.computed(() => { + const input = Vue.isRef(threshold) ? threshold.value : threshold; + if (Number.isInteger(input)) + return [['compact', input]]; + if (Array.isArray(input)) + return input.map((value, key) => [key, value]).sort((a, b) => a[1]-b[1]); + return Object.entries(input).sort((a, b) => a[1]-b[1]); + }); + + /* Helper Functions */ + function updateResultVars() { + const compare = threshold.value || threshold; + if (elementRef.value.offsetWidth === undefined) + return; + + const found = compareArray.value.find(compare => compare[1] > elementRef.value.offsetWidth); + + size.value = found ? found[0] : 'full'; + compact.value = (size.value == compareArray.value[0][0]); + } + + /* Observer */ + const observer = new ResizeObserver(() => { + if (elementRef.value) { + updateResultVars(); + } + }); + /* Observer Helper Functions */ + function addObserver() { + if (!elementRef.value) + return; + + updateResultVars(); + observer.observe(elementRef.value); + mounted.value = true; + } + function removeObserver() { + if (mounted.value) { + observer.disconnect() + } + } + + /* Main Logic */ + Vue.onMounted(addObserver); + Vue.onUnmounted(removeObserver); + + Vue.watchEffect(() => { + if (elementRef.value) { + removeObserver(); + addObserver(); + } + }); + + return { compact, size }; +} \ No newline at end of file