Hi Justin,
I’m just tinkering with a widget where I want to synchronize two f7-swipers. I’ve been inspired by the SemanticHome Menu and have borrowed the approach they use to fetch data from the semantic model. In this case, it’s a room overview page displaying rooms from the model, similar to the Location tab but with a different design.
The idea is to generate a swiper slide for each floor, outbuilding, or outdoor area based on how things are organized in the semantic model. The top swiper generates data points with names of floors, buildings, etc. The bottom swiper generates a swiper slide with room cards for the rooms on the selected floor, building, or outdoor area.
My goal is that when selecting, for example, the ground floor in the top swiper, the bottom swiper will show the room cards for that floor. If you instead swipe through the bottom swiper, the selection in the top swiper should follow the current floor.
I’ve created two versions: one where I use two swipers, which I haven’t been able to get working, and another where I use only one swiper at the top. In the second version, selecting a floor or area displays room cards for the chosen selection.
Two-swiper version:
uid: RoomsWidget_with_Floor_and_Room_Swipers_v4
tags:
- MainUI
props:
parameters:
- description: Navn for rum i dit sprog
label: Title
name: title
required: false
type: TEXT
parameterGroups: []
timestamp: Dec 9, 2024, 7:39:39 PM
component: f7-card
config:
style:
--f7-card-bg-color: transparent
--f7-card-header-border-color: transparent
box-shadow: none
font-family: sans-serif
font-weight: bold
height: auto
margin-left: =(screen.viewAreaWidth >= 800) ? ("35px") :("5px")
margin-right: =(screen.viewAreaWidth >= 800) ? ("35px") :("5px")
margin-top: 0px
stylesheet: |
.button-style {
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.5);
margin: 0;
padding: 0;
background-color: =(themeOptions.dark=="light") ? ("#FFFFFF"):("#252525");
border-radius: var(--f7-card-expandable-border-radius);
height: 155px;
box-sizing: border-box;
}
.button-style:active {
box-shadow: inset 0px 10px 20px 2px rgba(0, 0, 0, 0.75);
}
.selected_menu_item {
border: 1px solid red;
border-radius: var(--f7-card-expandable-border-radius);
}
.responsive-swiper {
width: 70%;
margin: 0 auto;
}
.responsive-font {
font-size: 18px;
}
.f7-col {
flex: 1 1 100%;
max-width: calc(100% - 8px);
margin: 4px;
box-sizing: border-box;
}
/* Media queries */
@media (max-width: 1200px) {
.responsive-swiper {
width: 90%;
}
.responsive-font {
font-size: 16px;
}
}
@media (max-width: 800px) {
.responsive-swiper {
width: 90%;
}
}
@media (max-width: 400px) {
.responsive-swiper {
width: calc(100% - 20px);
margin: 0 10px;
}
.responsive-font {
font-size: 14px;
}
}
@media (min-width: 600px) {
.f7-col {
flex: 1 1 30%; /* Tre kort per række */
max-width: calc(100% - 10px);
box-sizing: border-box;
margin-left: 5px;
margin-right: 5px;
padding: 0px;
}
}
.responsive-row {
margin-left: 0px;
margin-right: 0px;
}
@media (min-width: 800px) {
.responsive-row {
margin-left: 50px;
margin-right: 50px;
}
}
slots:
default:
- component: f7-row
config:
style:
align-items: flex-start
display: flex
justify-content: space-between
margin-bottom: 25px
margin-top: 0
padding-top: 14px
width: 100%
slots:
default:
- component: oh-clock
config:
format: HH:mm
style:
align-items: center
background: =(themeOptions.dark == "light") ? "white" :"black"
border-radius: var(--f7-card-expandable-border-radius)
display: flex
height: 25px
justify-content: flex-start
margin-left: 14px
width: 52px
- component: Label
config:
style:
color: =(themeOptions.dark == "light") ? "black" :"white"
flex-grow: 1
font-size: "=screen.viewAreaWidth <= 400 ? '25px' : (screen.viewAreaWidth <= 600
? '30px' : (screen.viewAreaWidth <= 800 ? '35px' :
(screen.viewAreaWidth <= 1000 ? '40px' : '45px')))"
margin: 0
text-align: center
text: "=props.title ? props.title : 'Rooms'"
- component: f7-block
config:
style:
width: 52px
- component: f7-col
config:
style:
flex-grow: 1
slots:
default:
- component: f7-swiper
config:
class:
- responsive-swiper
params:
onSlideChange:
- action: update
variable: activeFloor
value: =swiper.activeIndex
breakpoints:
"0":
slidesPerView: 2.2
spaceBetween: 0
"240":
slidesPerView: 3.2
spaceBetween: 0
"320":
slidesPerView: 4.2
spaceBetween: 0
"480":
slidesPerView: 5.2
spaceBetween: 0
"640":
slidesPerView: 6.2
spaceBetween: 0
"940":
slidesPerView: 7.2
spaceBetween: 0
"1400":
slidesPerView: 8.2
spaceBetween: 0
"1800":
slidesPerView: 9.2
spaceBetween: 0
centerInsufficientSlides: true
grabCursor: true
keyboard: true
mousewheel: true
navigation: false
observeSlideChildren: true
observer: true
runCallbacksOnInit: true
updateOnWindowResize: true
watchOverflow: true
slots:
default:
- component: f7-swiper-slide
config:
expandable: true
style:
border-radius: 5px
slots:
default:
- component: oh-button
config:
action: variable
actionVariable: objVar
actionVariableValue:
floor: ALL
selectSection: SECTION1
class:
- '=vars.objVar ? "" : "unselected_menu_item"'
- '=(vars.objVar.floor == loop.floorArraySrc.name) ?
"selected_menu_item" : "unselected_menu_item"'
- responsive-font
style:
color: =(themeOptions.dark=="light") ? "black" :"white"
text: ALL
- component: f7-swiper-slide
config:
expandable: true
style:
border-radius: 5px
slots:
default:
- component: oh-button
config:
actionVariable: objVar
actionVariableValue:
floor: FAVORITES
selectSection: SECTION1
class:
- '=vars.objVar ? "" : "unselected_menu_item"'
- '=(vars.objVar.floor == loop.floorArraySrc.name) ?
"selected_menu_item" : "unselected_menu_item"'
- responsive-font
style:
color: =(themeOptions.dark=="light") ? "black" :"white"
text: FAVORITES
- component: oh-repeater
config:
cacheSource: true
fetchMetadata: metadata,semantics,widgetOrder
filter: (loop.floorArray.metadata) &&
((loop.floorArray.metadata.semantics.value).includes("Floor")
||
(loop.floorArray.metadata.semantics.value).includes("Outdoor")
||
(loop.floorArray.metadata.semantics.value).includes("Building"))
for: floorArray
fragment: true
itemTags: ","
sortBy: loop.floorArray.metadata.sortKey
sortOrder: ascending
sourceType: itemsWithTags
slots:
default:
- component: oh-repeater
config:
cacheSource: true
fetchMetadata: metadata
for: floorArraySrc
fragment: true
in: =loop.floorArray_source
sourceType: array
visible: =loop.floorArray_idx == "0"
slots:
default:
- component: f7-swiper-slide
config:
expandable: true
style:
border-radius: 5px
slots:
default:
- component: oh-button
config:
action: variable
actionVariable: objVar
actionVariableValue:
floor: =loop.floorArraySrc.name
selectSection: SECTION2
class:
- '=vars.objVar ? "" :
"unselected_menu_item"'
- '=(vars.objVar.floor ==
loop.floorArraySrc.name) ?
"selected_menu_item" :
"unselected_menu_item"'
- responsive-font
style:
color: =(themeOptions.dark=="light") ? "black" :"white"
text: =loop.floorArraySrc.label
- component: f7-swiper
config:
class:
- custom-swiper
params:
onSlideChange:
- action: update
variable: activeFloor
value: =swiper.activeIndex
spaceBetween: 10
slidesPerView: 1
centerInsufficientSlides: true
grabCursor: true
keyboard: true
mousewheel: true
navigation: false
observeSlideChildren: true
observer: true
runCallbacksOnInit: true
updateOnWindowResize: true
watchOverflow: true
slots:
default:
- component: oh-repeater
config:
cacheSource: true
fetchMetadata: metadata, semantics, widgetOrder
filter: (loop.floorArray.metadata) &&
((loop.floorArray.metadata.semantics.value).includes("Floor") ||
(loop.floorArray.metadata.semantics.value).includes("Outdoor")
||
(loop.floorArray.metadata.semantics.value).includes("Building"))
for: floorArray
fragment: true
itemTags: ","
sortBy: loop.floorArray.metadata.sortKey
sortOrder: ascending
sourceType: itemsWithTags
slots:
default:
- component: f7-swiper-slide
config:
expandable: true
style:
border-radius: 5px
slots:
default:
- component: f7-row
config:
class:
- responsive-row
style:
display: flex
flex-wrap: wrap
gap: 3px
margin-top: 30px
slots:
default:
- component: oh-repeater
config:
cacheSource: true
fetchMetadata: metadata, semantics, widgetOrder
filter: (loop.roomArray.metadata) &&
((loop.roomArray.metadata.semantics.value).includes("Room")
||
(loop.roomArray.metadata.semantics.value).includes("Corridor"))
&&
((loop.roomArray.metadata.semantics.config.isPartOf).includes(loop.floorArray.name))
for: roomArray
fragment: true
itemTags: ","
sourceType: itemsWithTags
slots:
default:
- component: f7-col
config:
class:
- f7-col
slots:
default:
- component: oh-button
config:
action: navigate
actionPage: ='page:' + props.link_1
actionPageTransition: f7-dive
class:
- button-style
slots:
default:
- component: oh-icon
config:
icon: iconify:system-uicons:lightbulb-on
style:
color: yellow
font-size: "=screen.viewAreaWidth <= 400 ? '26px' : (screen.viewAreaWidth <= 600
? '28px' :
(screen.viewAreaWidth <=
800 ? '30px'
:(screen.viewAreaWidth <=
1100 ? '32px' : '34px')))"
margin-bottom: 5px
- component: Label
config:
style:
color: =(themeOptions.dark=="light") ? "black" :"white"
font-size: "=screen.viewAreaWidth <= 400 ? '12px' : (screen.viewAreaWidth <= 600
? '16px' :
(screen.viewAreaWidth <=
800 ? '14px'
:(screen.viewAreaWidth <=
1100 ? '16px' : '18px')))"
font-weight: bold
margin-top: auto
text: =loop.roomArray.label
One-swiper version:
uid: RoomsWidget_with_Floor_and_Room_Swipers_v3
tags:
- MainUI
props:
parameters:
- description: Navn for rum i dit sprog
label: Title
name: title
required: false
type: TEXT
parameterGroups: []
timestamp: Dec 1, 2024, 7:34:59 PM
component: f7-card
config:
style:
--f7-card-bg-color: transparent
--f7-card-header-border-color: transparent
box-shadow: none
font-family: sans-serif
font-weight: bold
height: auto
margin-left: =(screen.viewAreaWidth >= 800) ? ("35px") :("5px")
margin-right: =(screen.viewAreaWidth >= 800) ? ("35px") :("5px")
margin-top: 0px
stylesheet: |
.button-style {
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.5);
margin: 0;
padding: 0;
background-color: =(themeOptions.dark=="light") ? ("#FFFFFF"):("#252525");
border-radius: var(--f7-card-expandable-border-radius);
height: 155px;
box-sizing: border-box;
}
.button-style:active {
box-shadow: inset 0px 10px 20px 2px rgba(0, 0, 0, 0.75);
}
.selected_menu_item {
border: 1px solid red;
border-radius: var(--f7-card-expandable-border-radius);
}
.responsive-swiper {
width: 70%;
margin: 0 auto;
}
.responsive-font {
font-size: 18px;
}
.f7-col {
flex: 1 1 100%;
max-width: calc(100% - 8px);
margin: 4px;
box-sizing: border-box;
}
/* Media queries */
@media (max-width: 1200px) {
.responsive-swiper {
width: 90%;
}
.responsive-font {
font-size: 16px;
}
}
@media (max-width: 800px) {
.responsive-swiper {
width: 90%;
}
}
@media (max-width: 400px) {
.responsive-swiper {
width: calc(100% - 20px);
margin: 0 10px;
}
.responsive-font {
font-size: 14px;
}
}
@media (min-width: 600px) {
.f7-col {
flex: 1 1 30%; /* Tre kort per række */
max-width: calc(100% - 10px);
box-sizing: border-box;
margin-left: 5px;
margin-right: 5px;
padding: 0px;
}
}
.responsive-row {
margin-left: 0px;
margin-right: 0px;
}
@media (min-width: 800px) {
.responsive-row {
margin-left: 50px;
margin-right: 50px;
}
}
slots:
default:
- component: f7-row
config:
style:
align-items: flex-start
display: flex
justify-content: space-between
margin-bottom: 25px
margin-top: 0
padding-top: 14px
width: 100%
slots:
default:
- component: oh-clock
config:
format: HH:mm
style:
align-items: center
background: =(themeOptions.dark == "light") ? "white" :"black"
border-radius: var(--f7-card-expandable-border-radius)
display: flex
height: 25px
justify-content: flex-start
margin-left: 14px
width: 52px
- component: Label
config:
style:
color: =(themeOptions.dark == "light") ? "black" :"white"
flex-grow: 1
font-size: "=screen.viewAreaWidth <= 400 ? '25px' : (screen.viewAreaWidth <= 600
? '30px' : (screen.viewAreaWidth <= 800 ? '35px' :
(screen.viewAreaWidth <= 1000 ? '40px' : '45px')))"
margin: 0
text-align: center
text: "=props.title ? props.title : 'Rooms'"
- component: f7-block
config:
style:
width: 52px
- component: f7-col
config:
style:
flex-grow: 1
slots:
default:
- component: f7-swiper
config:
class:
- responsive-swiper
params:
breakpoints:
"0":
slidesPerView: 2.2
spaceBetween: 0
"240":
slidesPerView: 3.2
spaceBetween: 0
"320":
slidesPerView: 4.2
spaceBetween: 0
"480":
slidesPerView: 5.2
spaceBetween: 0
"640":
slidesPerView: 6.2
spaceBetween: 0
"940":
slidesPerView: 7.2
spaceBetween: 0
"1400":
slidesPerView: 8.2
spaceBetween: 0
"1800":
slidesPerView: 9.2
spaceBetween: 0
centerInsufficientSlides: true
grabCursor: true
keyboard: true
mousewheel: true
navigation: false
observeSlideChildren: true
observer: true
runCallbacksOnInit: true
updateOnWindowResize: true
watchOverflow: true
slots:
default:
- component: f7-swiper-slide
config:
expandable: true
style:
border-radius: 5px
slots:
default:
- component: oh-button
config:
action: variable
actionVariable: objVar
actionVariableValue:
floor: ALL
selectSection: SECTION1
class:
- '=vars.objVar ? "" : "unselected_menu_item"'
- '=(vars.objVar.floor == loop.floorArraySrc.name) ?
"selected_menu_item" : "unselected_menu_item"'
- responsive-font
style:
color: =(themeOptions.dark=="light") ? "black" :"white"
text: ALL
- component: f7-swiper-slide
config:
expandable: true
style:
border-radius: 5px
slots:
default:
- component: oh-button
config:
actionVariable: objVar
actionVariableValue:
floor: FAVORITES
selectSection: SECTION1
class:
- '=vars.objVar ? "" : "unselected_menu_item"'
- '=(vars.objVar.floor == loop.floorArraySrc.name) ?
"selected_menu_item" : "unselected_menu_item"'
- responsive-font
style:
color: =(themeOptions.dark=="light") ? "black" :"white"
text: FAVORITES
- component: oh-repeater
config:
cacheSource: true
fetchMetadata: metadata,semantics,widgetOrder
filter: (loop.floorArray.metadata) &&
((loop.floorArray.metadata.semantics.value).includes("Floor")
||
(loop.floorArray.metadata.semantics.value).includes("Outdoor")
||
(loop.floorArray.metadata.semantics.value).includes("Building"))
for: floorArray
fragment: true
itemTags: ","
sortBy: loop.floorArray.metadata.sortKey
sortOrder: ascending
sourceType: itemsWithTags
slots:
default:
- component: oh-repeater
config:
cacheSource: true
fetchMetadata: metadata
for: floorArraySrc
fragment: true
in: =loop.floorArray_source
sourceType: array
visible: =loop.floorArray_idx == "0"
slots:
default:
- component: f7-swiper-slide
config:
expandable: true
style:
border-radius: 5px
slots:
default:
- component: oh-button
config:
action: variable
actionVariable: objVar
actionVariableValue:
floor: =loop.floorArraySrc.name
selectSection: SECTION2
class:
- '=vars.objVar ? "" :
"unselected_menu_item"'
- '=(vars.objVar.floor ==
loop.floorArraySrc.name) ?
"selected_menu_item" :
"unselected_menu_item"'
- responsive-font
style:
color: =(themeOptions.dark=="light") ? "black" :"white"
text: =loop.floorArraySrc.label
- component: f7-row
config:
class:
- responsive-row
style:
display: flex
flex-wrap: wrap
gap: 3px
margin-top: 30px
slots:
default:
- component: oh-repeater
config:
cacheSource: true
fetchMetadata: metadata, semantics, widgetOrder
filter: (loop.roomArray.metadata) &&
((loop.roomArray.metadata.semantics.value).includes("Room") ||
(loop.roomArray.metadata.semantics.value).includes("Corridor"))
&&
((loop.roomArray.metadata.semantics.config.isPartOf).includes(vars.objVar.floor))
for: roomArray
fragment: true
itemTags: ","
sourceType: itemsWithTags
slots:
default:
- component: f7-col
config:
class:
- f7-col
slots:
default:
- component: oh-button
config:
action: navigate
actionPage: ='page:' + props.link_1
actionPageTransition: f7-dive
class:
- button-style
slots:
default:
- component: oh-icon
config:
icon: iconify:system-uicons:lightbulb-on
style:
color: yellow
font-size: "=screen.viewAreaWidth <= 400 ? '26px' : (screen.viewAreaWidth <= 600
? '28px' : (screen.viewAreaWidth <= 800 ?
'30px' :(screen.viewAreaWidth <= 1100 ?
'32px' : '34px')))"
margin-bottom: 5px
- component: Label
config:
style:
color: =(themeOptions.dark=="light") ? "black" :"white"
font-size: "=screen.viewAreaWidth <= 400 ? '12px' : (screen.viewAreaWidth <= 600
? '16px' : (screen.viewAreaWidth <= 800 ?
'14px' :(screen.viewAreaWidth <= 1100 ?
'16px' : '18px')))"
font-weight: bold
margin-top: auto
text: =loop.roomArray.label
I’m working on some UI design ideas, aiming to create something easy to navigate and visually clear.
Of course, based on my preferences, I think the approach they’ve taken with the SemanticHomeMenu is the way forward and a really good initiative.
screenshot:
Best regards,
Anders