Home page model cards rewrite (#562)

Refactored the experimental-grade code with some mixins,
better separated code.

Implements #555:
Allow the home page cards to be customized - reordered, grouped,
and edited, with config & slots and a dedicated editor.

Implements #556:
Add glance badges to the location cards (other types tbd.)
See #556 for the specifications on how to organize your model
to get the badges.

Styling fixes and cleanups.

Signed-off-by: Yannick Schaus <github@schaus.net>
pull/566/head
Yannick Schaus 2020-11-28 19:44:09 +01:00 committed by GitHub
parent 69eede2135
commit a8fd16f8c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1305 additions and 435 deletions

View File

@ -0,0 +1,77 @@
import { WidgetDefinition, pt, pi, pg, pb, pn, po } from '../helpers'
import { actionGroup, actionParams } from '../actions'
export const OhHomePageDefinition = () => new WidgetDefinition('oh-home-page', 'Home page')
.params([
pt('displayModelCardsTo', 'Display model cards to', 'Restrict who sees the Locations/Equipment/Properties tabs with the model cards')
.o([
{ value: 'role:administrator', label: 'Administrators' },
{ value: 'role:user', label: 'Users' }
]).m(),
pt('allowChatInputTo', 'Allow chat input box to', 'Restrict who can interact with the chatbot input box on the top of the Overview tab (when available)')
.o([
{ value: 'role:administrator', label: 'Administrators' },
{ value: 'role:user', label: 'Users' }
]).m(),
pt('hiddenModelTabs', 'Hidden Model Tabs', 'Hide individual model exploring tabs from view')
.o([
{ value: 'locations', label: 'Locations' },
{ value: 'equipment', label: 'Equipment' },
{ value: 'properties', label: 'Properties' }
]).m()
])
const ModelCardParameterGroup = () => pg('card', 'Model Card', 'General settings for this card')
const ModelCardParameters = () => [
pt('title', 'Title', 'Title of the card'),
pt('subtitle', 'Subtitle', 'Subtitle of the card'),
pt('backgroundColor', 'Background Color', 'Color of the card\'s background; if unset, choose automatically from built-in defaults for certain semantic classes')
.o([
{ value: 'red', label: 'Red' },
{ value: 'green', label: 'Green' },
{ value: 'blue', label: 'Blue' },
{ value: 'pink', label: 'Pink' },
{ value: 'yellow', label: 'Yellow' },
{ color: 'orange', label: 'Orange' },
{ value: 'purple', label: 'Purple' },
{ value: 'deeppurple', label: 'Deep Purple' },
{ value: 'lightblue', label: 'Light Blue' },
{ value: 'teal', label: 'Teal' },
{ value: 'lime', label: 'Lime' },
{ value: 'deeporange', label: 'Deep Orange' },
{ value: 'gray', label: 'Gray' },
{ value: 'black', label: 'Black' }
]),
pt('backgroundImage', 'Background Image', 'URL of an image to display in the background'),
pb('invertText', 'Invert Text', 'Display the text in black (for light backgrounds)')
]
export const OhLocationCardParameters = () => new WidgetDefinition('oh-location-card', 'Location Card', 'A card showing model items in a certain location')
.paramGroup(ModelCardParameterGroup(), ModelCardParameters())
.paramGroup(pg('glance', 'Card at-a-glance badges'), [
pb('disableBadges', 'Disable badges', 'Do not examine items to display badges - can help with performance if you don\'t need them.'),
pt('badges', 'Enabled badges', 'Select the badges you wish to show in the header of the card. Display all if none are selected.')
.o([
{ value: 'lights', label: 'Lights On' },
{ value: 'windows', label: 'Open Windows' },
{ value: 'doors', label: 'Open Doors' },
{ value: 'garagedoors', label: 'Open Garage Doors' },
{ value: 'blinds', label: 'Open Blinds' },
{ value: 'presence', label: 'Presence Detected' },
{ value: 'lock', label: 'Locks' },
{ value: 'climate', label: 'Climate Control Powered On' },
{ value: 'screens', label: 'Screens Powered On' },
{ value: 'projectors', label: 'Projectors Powered On' },
{ value: 'speakers', label: 'Speakers/AV Receivers Powered On' },
{ value: 'temperature', label: 'Average Temperature (+ Setpoint)' },
{ value: 'humidity', label: 'Average Humidity' },
{ value: 'luminance', label: 'Average Luminance' }
], true, true)
])
export const OhEquipmentCardParameters = () => new WidgetDefinition('oh-equipment-card', 'Equipment Class Card', 'A card showing model items belonging to a certain equipment class')
.paramGroup(ModelCardParameterGroup(), ModelCardParameters())
export const OhPropertyCardParameters = () => new WidgetDefinition('oh-property-card', 'Property Card', 'A card showing model items related to a certain property')
.paramGroup(ModelCardParameterGroup(), ModelCardParameters())

View File

@ -1,5 +1,8 @@
import mixin from '@/components/widgets/widget-mixin'
export default {
props: ['color', 'type', 'header', 'title', 'subtitle', 'items'],
mixins: [mixin],
props: ['type', 'element'],
data () {
return {
opened: false,
@ -12,9 +15,59 @@ export default {
beforeDestroy () {
window.removeEventListener('popstate', this.back)
},
computed: {
title () {
if (this.config.title) return this.config.title
return (this.element) ? this.element.defaultTitle : '?'
},
subtitle () {
if (this.config.subtitle) return this.config.subtitle
return (this.element) ? this.element.defaultSubtitle : '?'
},
color () {
if (this.config.backgroundColor) {
return this.config.backgroundColor
}
switch (this.type) {
case 'location':
const item = this.element.item
if (item.metadata.semantics.value.indexOf('LivingRoom') > 0) return 'orange'
if (item.metadata.semantics.value.indexOf('Kitchen') > 0) return 'deeporange'
if (item.metadata.semantics.value.indexOf('Bedroom') > 0) return 'pink'
if (item.metadata.semantics.value.indexOf('Bathroom') > 0) return 'blue'
if (item.metadata.semantics.value.indexOf('_Room') > 0) return 'lightblue'
if (item.metadata.semantics.value.indexOf('_Floor') > 0) return 'deeppurple'
if (item.metadata.semantics.value.indexOf('_Outdoor') > 0) return 'green'
return 'gray'
case 'equipment':
const equipmentType = this.element.key
if (equipmentType === 'HVAC') return 'red'
if (equipmentType === 'Lightbulb') return 'yellow'
if (equipmentType === 'Window') return 'blue'
if (equipmentType === 'Door') return 'green'
if (equipmentType === 'Camera') return 'pink'
if (equipmentType === 'Blinds') return 'teal'
if (equipmentType === 'SmokeDetector' || equipmentType === 'Siren') return 'deeppurple'
// etc. - use a map
return 'gray'
case 'property':
const property = this.element.key
if (property === 'Temperature') return 'red'
if (property === 'Light') return 'orange'
if (property === 'Humidity') return 'blue'
if (property === 'Presence') return 'teal'
if (property === 'Pressure') return 'deeppurple'
// etc. - use a map
return 'gray'
default:
return 'gray'
}
}
},
methods: {
cardOpening () {
history.pushState({ cardId: this.cardId }, null, window.location.href.split('#card=')[0] + '#' + this.$f7.utils.serializeObject({ card: this.cardId }))
this.cardId = this.title + '-' + this.$f7.utils.id()
history.pushState({ cardId: this.cardId }, null, window.location.href.split('#card=')[0] + '#' + this.$f7.utils.serializeObject({ card: this.element.key }))
setTimeout(() => { this.opened = true })
},
cardClosed () {
@ -25,7 +78,7 @@ export default {
setTimeout(() => { this.$f7.card.close(this.$refs.card.$el) }, 100)
},
back (evt) {
console.log(evt.state)
console.debug(evt.state)
if (!evt.state || evt.state.cardId === this.cardId) return
if (this.opened) this.closeCard()
}

View File

@ -0,0 +1,67 @@
<template>
<model-card type="equipment" :context="context" :element="element" header-height="150px">
<template v-slot:glance>
<div v-if="context && context.component.slots && context.component.slots.glance" class="display-flex flex-direction-column align-items-flex-start">
<generic-widget-component :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.glance" :key="'glance-' + idx" @command="onCommand" />
</div>
<!-- <div class="equipment-stats" v-else><small v-if="element.equipment">{{element.equipment.length}}</small></div> -->
</template>
<div class="card-content-padding">
<generic-widget-component :context="listContext" />
<p>
<f7-button fill round large card-close :color="color" class="margin-horizontal">Close</f7-button>
</p>
</div>
</model-card>
</template>
<style lang="stylus">
.equipment-card
height 150px
.equipment-stats
font-weight normal
</style>
<script>
import mixin from '@/components/widgets/widget-mixin'
import itemDefaultListComponent from '@/components/widgets/standard/list/default-list-item'
import CardMixin from './card-mixin'
import ModelCard from './model-card.vue'
export default {
mixins: [mixin, CardMixin],
components: {
ModelCard
},
computed: {
listContext () {
const standaloneEquipment = this.element.equipment.filter((i) => i.points.length === 0).map((i) => itemDefaultListComponent(i.item, true))
const equipmentWithPoints = this.element.equipment.filter((i) => i.points.length !== 0).map((i) => {
return [
{
component: 'oh-list-item',
config: {
title: i.item.label || i.item.name,
divider: true
}
},
...i.points.map((p) => itemDefaultListComponent(p))
]
})
return {
store: this.$store.getters.trackedItems,
component: {
component: 'oh-list',
config: {
mediaList: true
},
slots: {
default: [...standaloneEquipment, ...equipmentWithPoints].flat()
}
}
}
}
}
}
</script>

View File

@ -1,73 +0,0 @@
<template>
<f7-card expandable ref="card" class="equipments-card" :animate="$f7.data.themeOptions.expandableCardAnimation !== 'disabled'" card-tablet-fullscreen v-on:card:opened="cardOpening" v-on:card:closed="cardClosed">
<f7-card-content :padding="false">
<div :class="`bg-color-${color}`" :style="{height: '150px'}">
<f7-card-header text-color="white" class="display-block">
{{title || 'Something'}}
<div class="equipment-stats"><small v-if="subtitle">{{subtitle}}</small></div>
<br>
<!-- <h1>State</h1> -->
</f7-card-header>
<f7-link
card-close
color="white"
class="card-opened-fade-in"
:style="{position: 'absolute', right: '15px', top: '15px'}"
icon-f7="multiply_circle_fill"
></f7-link>
</div>
<div v-if="opened">
<generic-widget-component :context="listContext" />
<p>
<f7-button fill round large card-close :color="color" class="margin-horizontal">Close</f7-button>
</p>
</div>
</f7-card-content>
</f7-card>
</template>
<style lang="stylus">
.equipments-card
height 150px
.equipment-stats
font-weight normal
</style>
<script>
import itemDefaultListComponent from '@/components/widgets/standard/list/default-list-item'
import CardMixin from './card-mixin'
export default {
mixins: [CardMixin],
computed: {
listContext () {
const standaloneEquipments = this.items.filter((i) => i.points.length === 0).map((i) => itemDefaultListComponent(i.item, true))
const equipmentsWithPoints = this.items.filter((i) => i.points.length !== 0).map((i) => {
return [
{
component: 'oh-list-item',
config: {
title: i.item.label || i.item.name,
divider: true
}
},
...i.points.map((p) => itemDefaultListComponent(p))
]
})
return {
store: this.$store.getters.trackedItems,
component: {
component: 'oh-list',
config: {
mediaList: true
},
slots: {
default: [...standaloneEquipments, ...equipmentsWithPoints].flat()
}
}
}
}
}
}
</script>

View File

@ -0,0 +1,32 @@
/**
* Retrieves equipment based on their semantic class
* @param {Array} arr the array of equipment items & points to search
* @param {String} value the semantic class (value) to find
* @param {Boolean} partial match subclasses
*/
export function findEquipment (arr, value, partial) {
return arr.filter((e) => (partial) ? e.item.metadata.semantics.value.indexOf(value) === 0 : e.item.metadata.semantics.value === value)
}
/**
* Retrieve the flatten list of points from the provided equipment collection
* @param {Array} equipment the equipment collection
*/
export function allEquipmentPoints (equipment) {
return equipment.map((e) => e.points || []).flat()
}
/**
* Retrieves points based on their semantic class and eventual related property
* @param {Array} arr the array of source points
* @param {String} value the semantic class (value) to find
* @param {Boolean} partial match subclasses
* @param {String} property return only points also related to this property
*/
export function findPoints (arr, value, partial, property) {
console.debug(arr)
const points = arr.filter((p) => (partial) ? p.metadata.semantics.value.indexOf(value) === 0 : p.metadata.semantics.value === value)
if (!property) return points
console.debug('found1' + points)
return points.filter((p) => p.metadata.semantics.config.relatesTo === property)
}

View File

@ -0,0 +1,104 @@
<template>
<span class="padding-right location-status-badge" v-show="reduce" :class="{ invert: invertColor }">
<oh-icon v-if="config.icon.indexOf('oh:') === 0" :icon="config.icon.replace('oh:', '')" :state="config.state" class="oh-icon-badge" width="20" height="20" />
<f7-icon v-else-if="config.icon.indexOf('f7:') === 0" :f7="config.icon.replace('f7:', '')" :color="config.invertText ? 'black' : 'white'" class="f7-icon-badge" size="20" />
<!-- <oh-icon v-if="config.icon.indexOf('oh:') === 0 && config.stateOff" v-show="!reduce" icon="config.icon.replace('oh:', '')" :state="config.stateOff" class="oh-icon-badge" width="20" height="20" /> -->
<span class="glance-label" v-show="reduce">{{reduce}} {{config.unit}}</span>
<span class="glance-label" v-show="reduceAux"><small>({{reduceAux}} {{config.unit}})</small></span>
</span>
</template>
<style lang="stylus">
.location-status-badge
.oh-icon-badge
filter brightness(100)
&.invert .oh-icon-badge
filter brightness(0)
.f7-icon-badge
line-height 20px
margin-top -7px
.glance-label
line-height 20px
vertical-align top
</style>
<script>
import { allEquipmentPoints, findPoints } from '../glance-helpers'
export default {
props: ['element', 'type', 'customConfig', 'invertColor', 'store'],
data () {
return {
badgeConfigs: {
temperature: { icon: 'f7:thermometer', unit: '°' },
humidity: { icon: 'f7:drop', unit: '%' },
luminance: { icon: 'f7:sun_min', unit: 'lux' }
}
}
},
computed: {
config () {
return this.badgeConfigs[this.type]
},
query () {
let direct
switch (this.type) {
case 'temperature':
direct = findPoints(this.element.properties, 'Point_Measurement', true, 'Property_Temperature')
if (direct.length) return direct
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Measurement', true, 'Property_Temperature')
case 'humidity':
direct = findPoints(this.element.properties, 'Point_Measurement', true, 'Property_Humidity')
if (direct.length) return direct
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Measurement', true, 'Property_Humidity')
case 'luminance':
direct = findPoints(this.element.properties, 'Point_Measurement', true, 'Property_Light')
if (direct.length) return direct
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Measurement', true, 'Property_Light')
default:
return []
}
},
queryAux () {
let direct
switch (this.type) {
case 'temperature':
direct = findPoints(this.element.properties, 'Point_Setpoint', true, 'Property_Temperature')
if (direct.length) return direct
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Setpoint', true, 'Property_Temperature')
default:
return []
}
},
map () {
return this.query.map((item) => this.store[item.name].state)
},
mapAux () {
return this.queryAux.map((item) => this.store[item.name].state)
},
reduce () {
const ret = this.map.reduce((avg, state, arr, { length }) => {
const value = Number.parseFloat(state.split(' ')[0])
if (Number.isFinite(value)) {
return avg + value / length
}
return avg
}, 0)
return (this.type === 'temperature') ? Math.round(ret * 10) / 10 : Math.round(ret)
},
reduceAux () {
if (this.type !== 'temperature') return undefined
const ret = this.mapAux.reduce((avg, state, arr, { length }) => {
const value = Number.parseFloat(state.split(' ')[0])
if (Number.isFinite(value)) {
return avg + value / length
}
return avg
}, 0)
return Math.round(ret * 10) / 10
}
}
}
</script>

View File

@ -0,0 +1,165 @@
<template>
<span class="padding-right location-status-badge" v-show="reduce" :class="{ invert: invertColor }">
<oh-icon v-if="config.icon.indexOf('oh:') === 0" :icon="config.icon.replace('oh:', '')" :state="config.state" class="oh-icon-badge" width="20" height="20" />
<f7-icon v-else-if="config.icon.indexOf('f7:') === 0" :f7="config.icon.replace('f7:', '')" :color="config.invertText ? 'black' : 'white'" class="f7-icon-badge" size="20" />
<!-- <oh-icon v-if="config.icon.indexOf('oh:') === 0 && config.stateOff" v-show="!reduce" icon="config.icon.replace('oh:', '')" :state="config.stateOff" class="oh-icon-badge" width="20" height="20" /> -->
<span class="glance-label" v-show="reduce > 1">{{reduce}}</span>
</span>
</template>
<style lang="stylus">
.location-status-badge
.oh-icon-badge
filter brightness(100)
&.invert .oh-icon-badge
filter brightness(0)
.f7-icon-badge
line-height 20px
margin-top -7px
.glance-label
line-height 20px
vertical-align top
</style>
<script>
import { findEquipment, allEquipmentPoints, findPoints } from '../glance-helpers'
export default {
props: ['element', 'type', 'customConfig', 'invertColor', 'store'],
data () {
return {
badgeConfigs: {
lights: { icon: 'oh:lightbulb' },
windows: { icon: 'oh:window', state: 'open' },
doors: { icon: 'oh:door', state: 'open' },
garagedoors: { icon: 'oh:garagedoor', state: 'open' },
blinds: { icon: 'oh:cinemascreen', state: '100' },
presence: { icon: 'oh:motion', state: 'on' },
lock: { icon: 'oh:lock', state: 'open', stateOff: 'closed' },
climate: { icon: 'oh:climate', state: 'on' },
screens: { icon: 'f7:tv' },
projectors: { icon: 'f7:videocam_fill' },
speakers: { icon: 'f7:speaker_2_fill' }
}
}
},
computed: {
config () {
return this.badgeConfigs[this.type]
},
query () {
let direct, equipment, allPoints, points
switch (this.type) {
case 'lights':
direct = findPoints(this.element.properties, 'Point_Control', true, 'Property_Light')
if (direct.length) return direct
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Control', true, 'Property_Light')
case 'windows':
equipment = findEquipment(this.element.equipment, 'Equipment_Window', false)
if (!equipment.length) return []
allPoints = allEquipmentPoints(equipment)
points = findPoints(allPoints, 'Point_Status_OpenState', false)
if (points.length) return points
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
case 'doors':
equipment = [
...findEquipment(this.element.equipment, 'Equipment_Door', false),
...findEquipment(this.element.equipment, 'Equipment_Door_FrontDoor', false)
]
if (!equipment.length) return []
allPoints = allEquipmentPoints(equipment)
points = findPoints(allPoints, 'Point_Status_OpenState', false)
if (points.length) return points
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
case 'garagedoors':
equipment = findEquipment(this.element.equipment, 'Equipment_Door_GarageDoor', false)
if (!equipment.length) return []
allPoints = allEquipmentPoints(equipment)
points = findPoints(allPoints, 'Point_Status_OpenState', false)
if (points.length) return points
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
case 'blinds':
equipment = findEquipment(this.element.equipment, 'Equipment_Blinds', false)
if (!equipment.length) return []
allPoints = allEquipmentPoints(equipment)
points = findPoints(allPoints, 'Point_Status_OpenState', false)
if (points.length) return points
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
case 'presence':
direct = findPoints(this.element.properties, 'Point_Status', false, 'Property_Presence')
if (direct.length) return direct
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Status', true, 'Property_Presence')
case 'lock':
equipment = findEquipment(this.element.equipment, 'Equipment_Lock', false)
if (!equipment.length) return []
allPoints = allEquipmentPoints(equipment)
points = [
...findPoints(allPoints, 'Point_Status_OpenState', false),
...findPoints(allPoints, 'Point_Status', false),
...findPoints(allPoints, 'Point_Control', true)
]
if (points.length) return points
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
case 'climate':
equipment = findEquipment(this.element.equipment, 'Equipment_HVAC', true)
if (!equipment.length) return []
allPoints = allEquipmentPoints(equipment)
points = [
...findPoints(allPoints, 'Point_Status', false),
...findPoints(allPoints, 'Point_Control', true)
]
if (points.length) return points
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
case 'screens':
equipment = findEquipment(this.element.equipment, 'Equipment_Screen', true)
if (!equipment.length) return []
allPoints = allEquipmentPoints(equipment)
points = [
...findPoints(allPoints, 'Point_Status', false, 'Property_Power'),
...findPoints(allPoints, 'Point_Control', true, 'Property_Power')
]
if (points.length) return points
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
case 'speakers':
equipment = [
...findEquipment(this.element.equipment, 'Equipment_Receiver', false),
...findEquipment(this.element.equipment, 'Equipment_Speaker', false)
]
if (!equipment.length) return []
allPoints = allEquipmentPoints(equipment)
points = [
...findPoints(allPoints, 'Point_Status', false, 'Property_Power'),
...findPoints(allPoints, 'Point_Control', true, 'Property_Power')
]
if (points.length) return points
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
case 'projectors':
equipment = findEquipment(this.element.equipment, 'Equipment_Projector', false)
if (!equipment.length) return []
allPoints = allEquipmentPoints(equipment)
points = [
...findPoints(allPoints, 'Point_Status', false, 'Property_Power'),
...findPoints(allPoints, 'Point_Control', true, 'Property_Power')
]
if (points.length) return points
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
default:
return []
}
},
map () {
return this.query.map((item) => this.store[item.name].state)
},
reduce () {
switch (this.type) {
case 'lights':
return this.map.filter((state) => state === 'ON' || (state.split(',').length === 3 && state.split(',')[2] !== '0') || (state.indexOf(',') < 0 && Number.parseInt(state) > 0)).length
case 'blinds':
return this.map.filter((state) => state === 'OPEN' || Number.parseInt(state) > 0).length
default:
return this.map.filter((state) => state === 'ON' || state === 'OPEN').length
}
}
}
}
</script>

View File

@ -1,36 +1,35 @@
<template>
<f7-card expandable ref="card" class="location-card" :animate="$f7.data.themeOptions.expandableCardAnimation !== 'disabled'" card-tablet-fullscreen v-on:card:opened="cardOpening" v-on:card:closed="cardClosed">
<f7-card-content :padding="false">
<div :class="`bg-color-${color}`" :style="{height: '200px'}">
<f7-card-header text-color="white" class="display-block">
{{title || 'Something'}}
<div><small>{{subtitle || '&nbsp;'}}</small></div>
<div class="location-stats" v-if="items.equipments.length > 0"><small><f7-icon ios="f7:cube_box" aurora="f7:cube_box" md="material:payments" />&nbsp;{{items.equipments.length}}</small></div>
<div class="location-stats" v-if="items.properties.length > 0"><small><f7-icon ios="f7:bolt" aurora="f7:bolt" md="material:flash_on" />&nbsp;{{items.properties.length}}</small></div>
</f7-card-header>
<f7-link
card-close
color="white"
class="card-opened-fade-in"
:style="{position: 'absolute', right: '15px', top: '15px'}"
icon-f7="multiply_circle_fill"
></f7-link>
<model-card type="location" :context="context" :element="element" header-height="200px">
<template v-slot:glance>
<div v-if="!subtitle && parentLocation" class="subtitle"><small>{{parentLocation}}</small></div>
<div v-if="context && context.component.slots && context.component.slots.glance" class="display-flex flex-direction-column align-items-flex-start">
<generic-widget-component :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.glance" :key="'glance-' + idx" @command="onCommand" />
</div>
<div class="card-content-padding" v-if="opened && items.equipments.length > 0 && items.properties.length > 0">
<f7-segmented round tag="p">
<f7-button round outline :active="activeTab === 'equipments'" :color="color" @click="activeTab = 'equipments'">Equipment</f7-button>
<f7-button round outline :active="activeTab === 'properties'" :color="color" @click="activeTab = 'properties'">Properties</f7-button>
</f7-segmented>
<div class="location-stats margin-top" :class="config.invertText ? 'invert-text' : ''" v-if="!config.disableBadges">
<span v-for="badgeType in ['lights', 'windows', 'doors', 'garagedoors', 'blinds', 'presence', 'lock', 'climate', 'screens', 'projectors', 'speakers']" :key="badgeType">
<status-badge v-if="!config.badges || !config.badges.length || config.badges.indexOf(badgeType) >= 0"
:store="context.store" :element="element" :type="badgeType" :invert-color="config.invertText" />
</span>
</div>
<div v-if="opened">
<generic-widget-component v-if="activeTab === 'equipments'" :context="equipmentsListContext" />
<generic-widget-component v-if="activeTab === 'properties'" :context="propertiesListContext" />
<p>
<f7-button fill round large card-close :color="color" class="margin-horizontal">Close</f7-button>
</p>
<div class="location-stats margin-top-half" v-if="!config.disableBadges">
<span v-for="badgeType in ['temperature', 'humidity', 'luminance']" :key="badgeType">
<measurement-badge v-if="!config.badges || !config.badges.length || config.badges.indexOf(badgeType) >= 0"
:store="context.store" :element="element" :type="badgeType" :invert-color="config.invertText" />
</span>
</div>
</f7-card-content>
</f7-card>
</template>
<div class="card-content-padding">
<f7-segmented round tag="p" v-if="element.equipment.length > 0 && element.properties.length > 0">
<f7-button round outline :active="activeTab === 'equipment'" :color="color" @click="activeTab = 'equipment'">Equipment</f7-button>
<f7-button round outline :active="activeTab === 'properties'" :color="color" @click="activeTab = 'properties'">Properties</f7-button>
</f7-segmented>
<generic-widget-component v-if="activeTab === 'equipment'" :context="equipmentListContext" />
<generic-widget-component v-if="activeTab === 'properties'" :context="propertiesListContext" />
<p>
<f7-button fill round large card-close :color="color" class="margin-horizontal">Close</f7-button>
</p>
</div>
</model-card>
</template>
<style lang="stylus">
@ -38,18 +37,31 @@
height 200px
.location-stats
font-weight normal
font-size 16px
max-width calc(340px - 2*var(--f7-card-header-padding-horizontal))
display flex
flex-wrap wrap
</style>
<script>
import mixin from '@/components/widgets/widget-mixin'
import itemDefaultListComponent from '@/components/widgets/standard/list/default-list-item'
import CardMixin from './card-mixin'
import ModelCard from './model-card.vue'
import StatusBadge from './glance/location/status-badge.vue'
import MeasurementBadge from './glance/location/measurement-badge.vue'
export default {
mixins: [CardMixin],
props: ['color', 'type', 'header', 'title', 'subtitle', 'items'],
mixins: [mixin, CardMixin],
props: ['parentLocation'],
components: {
ModelCard,
StatusBadge,
MeasurementBadge
},
data () {
return {
activeTab: (this.items.equipments.length === 0 && this.items.properties.length > 0) ? 'properties' : 'equipments'
activeTab: (this.element.equipment.length === 0 && this.element.properties.length > 0) ? 'properties' : 'equipment'
}
},
methods: {
@ -64,14 +76,14 @@ export default {
mediaList: true
},
slots: {
default: this.items.properties.map((i) => itemDefaultListComponent(i))
default: this.element.properties.map((i) => itemDefaultListComponent(i))
}
}
}
},
equipmentsListContext () {
const standaloneEquipments = this.items.equipments.filter((i) => i.points.length === 0).map((i) => itemDefaultListComponent(i.item))
const equipmentsWithPoints = this.items.equipments.filter((i) => i.points.length !== 0).map((i) => {
equipmentListContext () {
const standaloneEquipment = this.element.equipment.filter((i) => i.points.length === 0).map((i) => itemDefaultListComponent(i.item))
const equipmentWithPoints = this.element.equipment.filter((i) => i.points.length !== 0).map((i) => {
return [
{
component: 'oh-list-item',
@ -92,7 +104,7 @@ export default {
mediaList: true
},
slots: {
default: [...standaloneEquipments, ...equipmentsWithPoints].flat()
default: [...standaloneEquipment, ...equipmentWithPoints].flat()
}
}
}

View File

@ -0,0 +1,91 @@
<template>
<f7-card expandable ref="card" class="model-card" :class="type + '-card'" :animate="$f7.data.themeOptions.expandableCardAnimation !== 'disabled'" card-tablet-fullscreen v-on:card:opened="cardOpening" v-on:card:closed="cardClosed">
<f7-card-content :padding="false">
<div :class="(!config.backgroundImage) ? `bg-color-${color}` : undefined" :style="{ height: `calc(var(--f7-safe-area-top) + ${headerHeight})` }">
<f7-card-header :text-color="config.invertText ? 'black' : 'white'" class="display-block card-header" :style="{ height: `calc(var(--f7-safe-area-top) + ${headerHeight})` }">
<img v-if="config.backgroundImage" class="card-background" :src="config.backgroundImage" :style="config.backgroundImageStyle" />
<slot name="header">
<div v-if="context && context.component.slots && context.component.slots.header">
<generic-widget-component :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.header" :key="'header-' + idx" @command="onCommand" />
</div>
<div v-else>
{{title}}
<div v-if="subtitle" class="subtitle"><small>{{subtitle || '&nbsp;'}}</small></div>
</div>
</slot>
<slot name="glance">
</slot>
</f7-card-header>
<f7-link
card-close
color="white"
class="card-opened-fade-in card-close-button"
icon-f7="multiply_circle_fill"
></f7-link>
</div>
<div v-if="opened">
<slot>
</slot>
</div>
</f7-card-content>
</f7-card>
</template>
<style lang="stylus">
.model-card
--card-offset calc(40px + var(--f7-safe-area-left) + var(--f7-safe-area-right))
--card-width calc(100% - var(--f7-safe-area-left) - var(--f7-safe-area-right) - 40px)
.card-close-button
position absolute
top calc(16px + var(--f7-safe-area-top))
right calc(var(--f7-card-content-padding-horizontal) + var(--f7-safe-area-right))
&.invert-text
.card-background
position absolute
left 0
top 0
width 100%
height 100%
object-fit cover
object-position center
z-index -1
background-color #ccc
transform translateX(calc(-1*var(--card-offset)/2))
&.card-opening, &.card-opened
.card-header
padding-top calc(var(--f7-card-header-padding-vertical) + var(--f7-safe-area-top))
&.card-opening, &.card-closing, &.card-transitioning
transition-duration 300ms
&.card-opening
.card-content, .card-header
transition-duration 350ms
&.card-closing
.card-content, .card-header
transition-duration 400ms
&.card-opening, &.card-opened, &.card-closing
.card-background
transition-duration 400ms
&.card-opening, &.card-opened
.card-background
transform translateX(0)
.subtitle
font-weight normal
font-size normal
line-height 0.7
@media (min-width: 768px)
.model-card
--card-offset calc(675px - 200px + var(--f7-safe-area-left) + var(--f7-safe-area-right))
</style>
<script>
import CardMixin from './card-mixin'
export default {
mixins: [CardMixin],
props: ['headerHeight'],
methods: {
}
}
</script>

View File

@ -1,32 +1,21 @@
<template>
<f7-card expandable ref="card" class="property-card" :animate="$f7.data.themeOptions.expandableCardAnimation !== 'disabled'" card-tablet-fullscreen v-on:card:opened="cardOpening" v-on:card:closed="cardClosed">
<f7-card-content :padding="false">
<div :class="`bg-color-${color}`" :style="{height: '150px'}">
<f7-card-header text-color="white" class="display-block">
{{title || 'Something'}}
<div class="property-stats"><small v-if="subtitle">{{subtitle}}</small></div>
<br>
<!-- <h1>State</h1> -->
</f7-card-header>
<f7-link
card-close
color="white"
class="card-opened-fade-in"
:style="{position: 'absolute', right: '15px', top: '15px'}"
icon-f7="multiply_circle_fill"
></f7-link>
<model-card type="property" :context="context" :element="element" header-height="150px">
<template v-slot:glance>
<div v-if="context && context.component.slots && context.component.slots.glance" class="display-flex flex-direction-column align-items-flex-start">
<generic-widget-component :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.glance" :key="'glance-' + idx" @command="onCommand" />
</div>
<div class="card-content-padding" v-if="opened">
<generic-widget-component :context="listContext" />
<p class="padding-top margin-horizontal">
<f7-button outline round :color="color" :href="`/analyzer/?items=${items.map((m) => m.name).join(',')}`">Analyze{{items.length > 1 ? ' all' : ''}}</f7-button>
</p>
<p class="margin-horizontal">
<f7-button fill round large card-close :color="color">Close</f7-button>
</p>
</div>
</f7-card-content>
</f7-card>
<!-- <div class="property-stats" v-else><small v-if="element.points">{{element.points.length}}</small></div> -->
</template>
<div class="card-content-padding">
<generic-widget-component :context="listContext" />
<p class="padding-top margin-horizontal">
<f7-button outline round :color="color" :href="`/analyzer/?items=${element.points.map((m) => m.name).join(',')}`">Analyze{{element.points.length > 1 ? ' all' : ''}}</f7-button>
</p>
<p class="margin-horizontal">
<f7-button fill round large card-close :color="color">Close</f7-button>
</p>
</div>
</model-card>
</template>
<style lang="stylus">
@ -37,11 +26,16 @@
</style>
<script>
import mixin from '@/components/widgets/widget-mixin'
import itemDefaultListComponent from '@/components/widgets/standard/list/default-list-item'
import CardMixin from './card-mixin'
import ModelCard from './model-card.vue'
export default {
mixins: [CardMixin],
mixins: [mixin, CardMixin],
components: {
ModelCard
},
computed: {
listContext () {
let pointsByType = []
@ -73,7 +67,7 @@ export default {
},
itemsByPointType () {
const points = {}
this.items.forEach((item) => {
this.element.points.forEach((item) => {
const pointType = item.metadata.semantics.value.replace('Point_', '')
if (!points[pointType]) points[pointType] = []
points[pointType].push(item)

View File

@ -13,6 +13,7 @@ import * as PlanWidgets from '@/components/widgets/plan'
import * as MapWidgets from '@/components/widgets/map'
import { OhChartPageDefinition } from '@/assets/definitions/widgets/chart/page'
import ChartWidgetsDefinitions from '@/assets/definitions/widgets/chart/index'
import { OhLocationCardParameters, OhEquipmentCardParameters, OhPropertyCardParameters } from '@/assets/definitions/widgets/home'
function getWidgetDefinitions (cm) {
const mode = cm.state.originalMode
@ -34,6 +35,7 @@ function getWidgetDefinitions (cm) {
.filter((w) => w.widget && typeof w.widget === 'function')
const f7Components = Object.values(f7vue).filter((m) => m.name && m.name.indexOf('f7-') === 0)
return [
...(componentType === 'home') ? [OhLocationCardParameters(), OhEquipmentCardParameters(), OhPropertyCardParameters()] : [],
...ohComponents.map((c) => c.widget()).sort((c1, c2) => c1.name.localeCompare(c2.name)),
...f7Components.sort((c1, c2) => c1.name.localeCompare(c2.name))
]

View File

@ -268,6 +268,7 @@ export default {
pageTypes: [
{ type: 'sitemap', label: 'Sitemap', componentType: 'Sitemap', icon: 'menu' },
{ type: 'layout', label: 'Layout', componentType: 'oh-layout-page', icon: 'rectangle_grid_2x2' },
{ type: 'home', label: 'Home', componentType: 'oh-home-page', icon: 'house' },
{ type: 'tabs', label: 'Tabbed', componentType: 'oh-tabs-page', icon: 'squares_below_rectangle' },
{ type: 'map', label: 'Map', componentType: 'oh-map-page', icon: 'map' },
{ type: 'plan', label: 'Floor plan', componentType: 'oh-plan-page', icon: 'square_stack_3d_up' },

View File

@ -74,6 +74,7 @@ export default {
pageTypes: [
{ type: 'sitemap', label: 'Sitemap', componentType: 'Sitemap', icon: 'menu' },
{ type: 'layout', label: 'Layout', componentType: 'oh-layout-page', icon: 'rectangle_grid_2x2' },
{ type: 'home', label: 'Home', componentType: 'oh-home-page', icon: 'house' },
{ type: 'tabs', label: 'Tabbed', componentType: 'oh-tabs-page', icon: 'squares_below_rectangle' },
{ type: 'map', label: 'Map', componentType: 'oh-map-page', icon: 'map' },
{ type: 'plan', label: 'Floor plan', componentType: 'oh-plan-page', icon: 'square_stack_3d_up' },

View File

@ -222,61 +222,6 @@ html
.aurora .after-big-title
margin-top 5rem !important
.demo-expandable-cards
justify-content center
.card-header
padding-top calc(var(--f7-safe-area-top) + var(--f7-card-header-padding-vertical))
padding-left calc(var(--f7-safe-area-left) + var(--f7-card-header-padding-horizontal))
/* Demo Expandable Cards */
@media (min-width 768px)
.demo-expandable-cards
display flex
flex-wrap wrap
.demo-expandable-cards .card
flex-shrink 10
min-width 0
@media (min-width 768px) and (max-width 1023px)
.demo-expandable-cards .card
width 340px
// .demo-expandable-cards .card
// width calc((100% - var(--f7-card-expandable-margin-horizontal) * 3) / 2)
//
.demo-expandable-cards .card:nth-child(n),
.demo-expandable-cards .card:nth-child(n + 1)
margin-left 0
.demo-expandable-cards .card:nth-child(n + 3)
margin-top 0
@media (min-width 1024px)
.demo-expandable-cards
margin-top 2rem
.card
width 340px
margin-top 0
.demo-expandable-cards .card:nth-child(n),
.demo-expandable-cards .card:nth-child(n + 1)
margin-left 0
.demo-expandable-cards .card:nth-child(n + 3)
margin-top 0
// hide scrollbar
.card-expandable.card-opened .card-content
overflow-y scroll
scrollbar-width none /* Firefox */
-ms-overflow-style none /* IE 10+ */
.card-expandable.card-opened .card-content::-webkit-scrollbar /* WebKit */
width 0
height 0
// Remove the borders and background for admin links in sidebar
.list.admin-links
ul

View File

@ -12,29 +12,29 @@
{{title}}
</f7-nav-title>
<f7-nav-right>
<f7-link v-if="this.$store.getters.isAdmin" icon-ios="f7:pencil" icon-aurora="f7:pencil" icon-md="material:edit" :href="(homePageComponent) ? '/settings/pages/home/home' : '/settings/pages/home/add'"></f7-link>
<f7-link icon-ios="f7:sidebar_right" icon-aurora="f7:sidebar_right" icon-md="material:exit_to_app" panel-open="right"></f7-link>
</f7-nav-right>
</f7-navbar>
<f7-toolbar tabbar labels bottom>
<f7-toolbar tabbar labels bottom v-if="tabsVisible">
<f7-link tab-link @click="currentTab = 'overview'" :tab-link-active="currentTab === 'overview'" icon-ios="f7:house_fill" icon-aurora="f7:house_fill" icon-md="material:home" text="Overview"></f7-link>
<f7-link tab-link @click="currentTab = 'locations'" :tab-link-active="currentTab === 'locations'" icon-ios="f7:placemark_fill" icon-aurora="f7:placemark_fill" icon-md="material:place" text="Locations"></f7-link>
<f7-link tab-link @click="currentTab = 'equipments'" :tab-link-active="currentTab === 'equipments'" icon-ios="f7:cube_box_fill" icon-aurora="f7:cube_box_fill" icon-md="material:payments" text="Equipment"></f7-link>
<f7-link tab-link @click="currentTab = 'properties'" :tab-link-active="currentTab === 'properties'" icon-ios="f7:bolt_fill" icon-aurora="f7:bolt_fill" icon-md="material:flash_on" text="Properties"></f7-link>
<f7-link tab-link v-if="tabVisible('locations')" @click="currentTab = 'locations'" :tab-link-active="currentTab === 'locations'" icon-ios="f7:placemark_fill" icon-aurora="f7:placemark_fill" icon-md="material:place" text="Locations"></f7-link>
<f7-link tab-link v-if="tabVisible('equipment')" @click="currentTab = 'equipment'" :tab-link-active="currentTab === 'equipment'" icon-ios="f7:cube_box_fill" icon-aurora="f7:cube_box_fill" icon-md="material:payments" text="Equipment"></f7-link>
<f7-link tab-link v-if="tabVisible('properties')" @click="currentTab = 'properties'" :tab-link-active="currentTab === 'properties'" icon-ios="f7:bolt_fill" icon-aurora="f7:bolt_fill" icon-md="material:flash_on" text="Properties"></f7-link>
</f7-toolbar>
<f7-tabs v-if="items">
<f7-tabs>
<f7-tab id="tab-overview" :tab-active="currentTab === 'overview'" @tab:show="() => this.currentTab = 'overview'">
<overview-tab v-if="currentTab === 'overview'" :context="context" :items="items" />
<overview-tab v-if="currentTab === 'overview'" :context="context" :allow-chat="allowChat" />
</f7-tab>
<f7-tab id="tab-locations" :tab-active="currentTab === 'locations'" @tab:show="() => this.currentTab = 'locations'">
<locations-tab v-if="currentTab === 'locations'" :context="context" :semantic-items="semanticItems" />
<model-tab v-if="currentTab === 'locations'" :context="context" type="locations" :model="model" :page="homePageComponent" />
</f7-tab>
<f7-tab id="tab-equipments" :tab-active="currentTab === 'equipments'" @tab:show="() => this.currentTab = 'equipments'">
<equipments-tab v-if="currentTab === 'equipments'" :context="context" :semantic-items="semanticItems" />
<f7-tab id="tab-equipment" :tab-active="currentTab === 'equipment'" @tab:show="() => this.currentTab = 'equipment'">
<model-tab v-if="currentTab === 'equipment'" :context="context" type="equipment" :model="model" :page="homePageComponent" />
</f7-tab>
<f7-tab id="tab-properties" :tab-active="currentTab === 'properties'" @tab:show="() => this.currentTab = 'properties'">
<properties-tab v-if="currentTab === 'properties'" :context="context" :semantic-items="semanticItems" />
<model-tab v-if="currentTab === 'properties'" :context="context" type="properties" :model="model" :page="homePageComponent" />
</f7-tab>
</f7-tabs>
</f7-page>
@ -59,26 +59,23 @@
top -6px
letter-spacing 1px
color var(--f7-list-item-footer-text-color)
.edit-home-button
float right
display absolute
z-index 9000
</style>
<script>
import OverviewTab from './home/overview-tab.vue'
import LocationsTab from './home/locations-tab.vue'
import EquipmentsTab from './home/equipments-tab.vue'
import PropertiesTab from './home/properties-tab.vue'
import ModelTab from './home/model-tab.vue'
import { compareItems } from '@/components/widgets/widget-order'
function compareObjects (o1, o2) {
return compareItems(o1.item || o1, o2.item || o2)
}
import HomeCards from './home/homecards-mixin'
export default {
mixins: [HomeCards],
components: {
OverviewTab,
LocationsTab,
EquipmentsTab,
PropertiesTab
ModelTab
},
data () {
return {
@ -86,8 +83,7 @@ export default {
showTasks: true,
showCards: false,
currentTab: 'overview',
items: [],
semanticItems: {}
items: []
}
},
computed: {
@ -96,13 +92,39 @@ export default {
store: this.$store.getters.trackedItems
}
},
homePageComponent () {
const page = this.$store.getters.page('home')
if (!page) return null
if (page.component !== 'oh-home-page') return null
return page
},
tabsVisible () {
if (!this.homePageComponent) return true
const visibleTo = this.homePageComponent.config.displayModelCardsTo
if (visibleTo === undefined || !visibleTo.length) return true
const user = this.$store.getters.user
if (!user) return false
if (user.roles && user.roles.some(r => visibleTo.indexOf('role:' + r) >= 0)) return true
if (visibleTo.indexOf('user:' + user.name) >= 0) return true
return false
},
allowChat () {
if (!this.homePageComponent) return true
const visibleTo = this.homePageComponent.config.allowChatInputTo
if (visibleTo === undefined || !visibleTo.length) return true
const user = this.$store.getters.user
if (!user) return false
if (user.roles && user.roles.some(r => visibleTo.indexOf('role:' + r) >= 0)) return true
if (visibleTo.indexOf('user:' + user.name) >= 0) return true
return false
},
title () {
switch (this.currentTab) {
case 'overview':
return 'Home'
case 'locations':
return 'Locations'
case 'equipments':
case 'equipment':
return 'Equipment'
case 'properties':
return 'Properties'
@ -114,7 +136,7 @@ export default {
methods: {
onPageAfterIn () {
this.$store.dispatch('startTrackingStates')
this.load()
this.loadModel()
},
onPageBeforeOut () {
this.$store.dispatch('stopTrackingStates')
@ -122,72 +144,12 @@ export default {
onPageInit () {
this.$f7.panel.get('left').enableVisibleBreakpoint()
},
load () {
this.$oh.api.get('/rest/items?metadata=semantics,listWidget,widgetOrder').then((data) => {
this.items = data
// get the location items
this.semanticItems.locations = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics.value.indexOf('Location') === 0
}).sort(compareObjects).map((l) => {
return {
item: l,
properties: data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.config.relatesTo &&
item.metadata.semantics.config.hasLocation === l.name
}).sort(compareObjects),
equipments: data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.value.indexOf('Equipment') === 0 &&
item.metadata.semantics.config.hasLocation === l.name
}).sort(compareObjects).map((item) => {
return {
item: item,
points: data.filter((item2, index, items) => {
return item2.metadata && item2.metadata.semantics &&
item2.metadata.semantics && item2.metadata.semantics.config &&
item2.metadata.semantics.config.isPointOf === item.name
}).sort(compareObjects)
}
})
}
})
// get the equipment items
this.semanticItems.equipments = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics &&
item.metadata.semantics.value.indexOf('Equipment') === 0
}).sort(compareObjects).reduce((prev, item, i, properties) => {
const equipmentType = item.metadata.semantics.value.split('_')[1] || 'Equipment'
if (!prev[equipmentType]) prev[equipmentType] = []
const equipmentWithPoints = {
item: item,
points: data.filter((item2, index, items) => {
return item2.metadata && item2.metadata.semantics &&
item2.metadata.semantics && item2.metadata.semantics.config &&
item2.metadata.semantics.config.isPointOf === item.name
}).sort(compareObjects)
}
prev[equipmentType].push(equipmentWithPoints)
return prev
}, {})
// get the property items
this.semanticItems.properties = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.config.relatesTo
}).sort(compareObjects).reduce((prev, item, i, properties) => {
const property = item.metadata.semantics.config.relatesTo.split('_')[1]
if (!prev[property]) prev[property] = []
prev[property].push(item)
return prev
}, {})
})
tabVisible (tab) {
if (!this.tabsVisible) return false
if (!this.homePageComponent) return true
const hiddenTabs = this.homePageComponent.config.hiddenModelTabs
if (hiddenTabs === undefined || !hiddenTabs.length) return true
return hiddenTabs.indexOf(tab) < 0
}
}
}

View File

@ -1,47 +0,0 @@
<template>
<div v-if="showCards">
<div class="demo-expandable-cards">
<equipments-card v-for="equipmentType in Object.keys(semanticItems.equipments).sort()" :key="equipmentType"
:title="equipmentType"
:subtitle="`${semanticItems.equipments[equipmentType].length} item${semanticItems.equipments[equipmentType].length > 1 ? 's' : ''}`"
:color="color(equipmentType)"
:items="semanticItems.equipments[equipmentType]" />
</div>
</div>
</template>
<script>
import EquipmentsCard from '../../components/cards/equipments-card.vue'
export default {
props: ['semanticItems'],
components: {
EquipmentsCard
},
data () {
return {
showCards: false,
properties: []
}
},
created () {
this.showCards = true
},
methods: {
hideCards () {
this.showCards = false
},
color (equipmentType) {
if (equipmentType === 'HVAC') return 'red'
if (equipmentType === 'Lightbulb') return 'yellow'
if (equipmentType === 'Window') return 'blue'
if (equipmentType === 'Door') return 'green'
if (equipmentType === 'Camera') return 'pink'
if (equipmentType === 'Blinds') return 'teal'
if (equipmentType === 'SmokeDetector' || equipmentType === 'Siren') return 'deeppurple'
// etc. - use a map
return 'gray'
}
}
}
</script>

View File

@ -0,0 +1,27 @@
export default (model, type, page) => {
const cardOrder = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].config && page.slots[type][0].config.cardOrder) ? page.slots[type][0].config.cardOrder : []
const elements = [...model[type]].map((e) => {
if (e.separator) return e
const card = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].slots && page.slots[type][0].slots[e.key]) ? page.slots[type][0].slots[e.key][0] : null
if (card) e.card = card
return e
})
let groups = []
let currentGroup = []
for (const orderKey of cardOrder) {
if (orderKey.separator) {
if (currentGroup.length) groups.push(currentGroup)
currentGroup = []
currentGroup.push(orderKey)
} else {
const idx = elements.findIndex((c) => c.key === orderKey)
if (idx >= 0) {
currentGroup.push(elements[idx])
elements.splice(idx, 1)
}
}
}
if (currentGroup.length) groups.push(currentGroup)
groups.push([...elements])
return groups
}

View File

@ -0,0 +1,121 @@
import cardGroups from './homecards-grouping'
import { compareItems } from '@/components/widgets/widget-order'
export default {
data () {
return {
model: {},
modelReady: false
}
},
computed: {
},
methods: {
cardGroups (type, page) {
return cardGroups(this.model, type, page)
},
compareObjects (o1, o2) {
return compareItems(o1.item || o1, o2.item || o2)
},
buildModelCard (type, source, key, page) {
const card = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].slots && page.slots[type][0].slots[key]) ? page.slots[type][0].slots[key][0] : null
switch (type) {
case 'location':
let defaultLocationTitle = source.item.label || source.item.name
return Object.assign(source, {
key,
card,
defaultTitle: defaultLocationTitle
})
case 'equipment':
let defaultEquipmentTitle = key
return {
key,
card,
defaultTitle: defaultEquipmentTitle,
equipment: source
}
case 'property':
let defaultPropertyTitle = key
return {
key,
card,
defaultTitle: defaultPropertyTitle,
points: source
}
}
},
loadModel (page) {
this.$oh.api.get('/rest/items?metadata=semantics,listWidget,widgetOrder').then((data) => {
this.items = data
// get the location items
const locations = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics.value.indexOf('Location') === 0
}).sort(this.compareObjects).map((l) => {
return {
item: l,
properties: data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.config.relatesTo &&
item.metadata.semantics.config.hasLocation === l.name
}).sort(this.compareObjects),
equipment: data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.value.indexOf('Equipment') === 0 &&
item.metadata.semantics.config.hasLocation === l.name
}).sort(this.compareObjects).map((item) => {
return {
item: item,
points: data.filter((item2, index, items) => {
return item2.metadata && item2.metadata.semantics &&
item2.metadata.semantics && item2.metadata.semantics.config &&
item2.metadata.semantics.config.isPointOf === item.name
}).sort(this.compareObjects)
}
})
}
})
// get the equipment items
const equipment = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics &&
item.metadata.semantics.value.indexOf('Equipment') === 0
}).sort(this.compareObjects).reduce((prev, item, i, properties) => {
const equipmentType = item.metadata.semantics.value.split('_')[1] || 'Equipment'
if (!prev[equipmentType]) prev[equipmentType] = []
const equipmentWithPoints = {
item: item,
points: data.filter((item2, index, items) => {
return item2.metadata && item2.metadata.semantics &&
item2.metadata.semantics && item2.metadata.semantics.config &&
item2.metadata.semantics.config.isPointOf === item.name
}).sort(this.compareObjects)
}
prev[equipmentType].push(equipmentWithPoints)
return prev
}, {})
// get the property items
const properties = data.filter((item, index, items) => {
return item.metadata && item.metadata.semantics &&
item.metadata.semantics && item.metadata.semantics.config &&
item.metadata.semantics.config.relatesTo
}).sort(this.compareObjects).reduce((prev, item, i, properties) => {
const property = item.metadata.semantics.config.relatesTo.split('_')[1]
if (!prev[property]) prev[property] = []
prev[property].push(item)
return prev
}, {})
this.model.locations = locations.map(l => this.buildModelCard('location', l, l.item.name, page))
this.model.equipment = Object.keys(equipment).sort().map(k => this.buildModelCard('equipment', equipment[k], k, page))
this.model.properties = Object.keys(properties).sort().map(k => this.buildModelCard('property', properties[k], k, page))
this.modelReady = true
})
}
}
}

View File

@ -1,50 +0,0 @@
<template>
<div v-if="showCards">
<div class="demo-expandable-cards">
<location-card v-for="location in semanticItems.locations.filter((l) => l.equipments.length > 0 || l.properties.length > 0)" :key="location.item.name"
:title="location.item.label" :items="location" :subtitle="parentLocationName(location.item)" :color="color(location.item)" />
</div>
</div>
</template>
<script>
import LocationCard from '../../components/cards/location-card.vue'
export default {
props: ['semanticItems'],
components: {
LocationCard
},
data () {
return {
showCards: false,
locations: []
}
},
created () {
this.showCards = true
},
methods: {
hideCards () {
this.showCards = false
},
color (item) {
if (item.metadata.semantics.value.indexOf('LivingRoom') > 0) return 'orange'
if (item.metadata.semantics.value.indexOf('Kitchen') > 0) return 'deeporange'
if (item.metadata.semantics.value.indexOf('Bedroom') > 0) return 'pink'
if (item.metadata.semantics.value.indexOf('Bathroom') > 0) return 'blue'
if (item.metadata.semantics.value.indexOf('_Room') > 0) return 'lightblue'
if (item.metadata.semantics.value.indexOf('_Floor') > 0) return 'deeppurple'
if (item.metadata.semantics.value.indexOf('_Outdoor') > 0) return 'green'
return 'gray'
},
parentLocationName (item) {
if (item.metadata.semantics.config && item.metadata.semantics.config.isPartOf) {
const parent = (this.semanticItems.locations.find((i) => i.item.name === item.metadata.semantics.config.isPartOf))
return parent.item.label || parent.item.name
}
return ''
}
}
}
</script>

View File

@ -0,0 +1,131 @@
<template>
<div v-if="showCards && groups">
<div v-for="(elements, idx) in groups" :key="idx">
<f7-block-title medium v-if="elements.length > 0 && elements[0].separator">{{elements[0].separator}}</f7-block-title>
<div class="model-cards-section" v-if="elements.length > 0">
<div v-for="(element, idx) in elements.filter((e) => !isCardExcluded(e))" :key="idx">
<location-card v-if="type === 'locations' && !element.separator && (element.equipment.length > 0 || element.properties.length > 0)" :key="element.key"
type="location" :element="element" :context="cardContext(element)" :parent-location="parentLocationName(element.item)" />
<equipment-card v-if="type === 'equipment' && !element.separator" :key="element.key"
type="equipment" :element="element" :context="cardContext(element)" />
<property-card v-if="type === 'properties' && !element.separator" :key="element.key"
type="property" :element="element" :context="cardContext(element)" />
</div>
</div>
</div>
</div>
</template>
<style lang="stylus">
.model-cards-section
justify-content center
@media (min-width 768px)
.model-cards-section
display flex
flex-wrap wrap
.model-cards-section .card
flex-shrink 10
min-width 0
@media (min-width 768px) and (max-width 1023px)
.model-cards-section .card
width 340px
margin-top 0
// .model-cards-section .card
// width calc((100% - var(--f7-card-expandable-margin-horizontal) * 3) / 2)
//
.model-cards-section .card:nth-child(n),
.model-cards-section .card:nth-child(n + 1)
margin-left 0
.model-cards-section .card:nth-child(n + 3)
margin-top 0
@media (min-width 1024px)
.model-cards-section
margin-top 2rem
.card
width 340px
margin-top 0
.model-cards-section .card:nth-child(n),
.model-cards-section .card:nth-child(n + 1)
margin-left 0
.model-cards-section .card:nth-child(n + 3)
margin-top 0
// hide scrollbar
.card-expandable.card-opened .card-content
overflow-y scroll
scrollbar-width none /* Firefox */
-ms-overflow-style none /* IE 10+ */
.card-expandable.card-opened .card-content::-webkit-scrollbar /* WebKit */
width 0
height 0
</style>
<script>
import cardGroups from './homecards-grouping'
import LocationCard from '../../components/cards/location-card.vue'
import EquipmentCard from '../../components/cards/equipment-card.vue'
import PropertyCard from '../../components/cards/property-card.vue'
export default {
props: ['type', 'model', 'page'],
components: {
LocationCard,
EquipmentCard,
PropertyCard
},
data () {
return {
showCards: false
}
},
created () {
this.showCards = true
},
computed: {
groups () {
return cardGroups(this.model, this.type, this.page)
}
},
methods: {
hideCards () {
this.showCards = false
},
isCardExcluded (card) {
if (!card.key) return
const page = this.page
const type = this.type
const excludedCards = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].config && page.slots[type][0].config.excludedCards) ? page.slots[type][0].config.excludedCards : []
const excludedIdx = excludedCards.indexOf(card.key)
return excludedIdx >= 0
},
cardContext (element) {
return {
component: element.card || {
component: (this.type === 'locations') ? 'oh-location-card' : (this.type === 'equipment') ? 'oh-equipment-card' : 'oh-property-card',
config: {}
},
store: this.$store.getters.trackedItems
}
},
parentLocationName (item) {
if (item.metadata.semantics.config && item.metadata.semantics.config.isPartOf) {
const parent = (this.model.locations.find((i) => i.item.name === item.metadata.semantics.config.isPartOf))
return parent.item.label || parent.item.name
}
return ''
}
}
}
</script>

View File

@ -52,7 +52,7 @@ import OhLayoutPage from '@/components/widgets/layout/oh-layout-page.vue'
import Habot from '../../components/home/habot.vue'
export default {
props: ['context', 'items'],
props: ['context', 'allowChat'],
components: {
OhLayoutPage,
Habot
@ -69,7 +69,7 @@ export default {
},
computed: {
showHABot () {
return this.$store.getters.apiEndpoint('habot') && localStorage.getItem('openhab.ui:theme.home.hidechatinput') !== 'true'
return this.$store.getters.apiEndpoint('habot') && this.allowChat && localStorage.getItem('openhab.ui:theme.home.hidechatinput') !== 'true'
},
overviewPage () {
const page = this.$store.getters.page('overview')

View File

@ -1,45 +0,0 @@
<template>
<div v-if="showCards">
<div class="demo-expandable-cards">
<property-card v-for="property in Object.keys(semanticItems.properties).sort()" :key="property"
:title="property"
:subtitle="`${semanticItems.properties[property].length} item${semanticItems.properties[property].length > 1 ? 's' : ''}`"
:color="color(property)"
:items="semanticItems.properties[property]" />
</div>
</div>
</template>
<script>
import PropertyCard from '../../components/cards/property-card.vue'
export default {
props: ['semanticItems'],
components: {
PropertyCard
},
data () {
return {
showCards: false,
properties: []
}
},
created () {
this.showCards = true
},
methods: {
hideCards () {
this.showCards = false
},
color (property) {
if (property === 'Temperature') return 'red'
if (property === 'Light') return 'orange'
if (property === 'Humidity') return 'blue'
if (property === 'Presence') return 'teal'
if (property === 'Pressure') return 'deeppurple'
// etc. - use a map
return 'gray'
}
}
}
</script>

View File

@ -39,7 +39,7 @@
</f7-tab>
<f7-tab id="code" @tab:show="() => { this.currentTab = 'code' }" :tab-active="currentTab === 'code'">
<editor v-if="currentTab === 'code'" :style="{ opacity: previewMode ? '0' : '' }" class="page-code-editor" mode="application/vnd.openhab.uicomponent;type=chart" :value="pageYaml" @input="(value) => pageYaml = value" />
<editor v-if="currentTab === 'code'" :style="{ opacity: previewMode ? '0' : '' }" class="page-code-editor" mode="application/vnd.openhab.uicomponent+yaml;type=chart" :value="pageYaml" @input="(value) => pageYaml = value" />
<!-- <pre v-show="!previewMode" class="yaml-message padding-horizontal" :class="[yamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{yamlError}}</pre> -->
<oh-chart-page class="chart-page" v-if="ready && previewMode" :context="context" :key="pageKey" />

View File

@ -0,0 +1,298 @@
<template>
<f7-page @page:afterin="onPageAfterIn" @page:beforeout="onPageBeforeOut" class="home-editor">
<f7-navbar title="Edit Home Page" back-link="Back" no-hairline>
<f7-nav-right>
<f7-link @click="save()" v-if="$theme.md" icon-md="material:save" icon-only></f7-link>
<f7-link @click="save()" v-if="!$theme.md">Save<span v-if="$device.desktop">&nbsp;(Ctrl-S)</span></f7-link>
</f7-nav-right>
</f7-navbar>
<f7-toolbar tabbar position="top" v-if="!previewMode">
<f7-link @click="currentTab = 'design'; fromYaml()" :tab-link-active="currentTab === 'design'" class="tab-link">Design</f7-link>
<f7-link @click="currentTab = 'code'; toYaml()" :tab-link-active="currentTab === 'code'" class="tab-link">Code</f7-link>
</f7-toolbar>
<f7-toolbar v-else tabbar position="top">
<f7-link v-for="tab in modelTabs" :key="tab.value" @click="showCardControls = false; currentModelTab = tab.value" :tab-link-active="currentModelTab === tab.value" class="tab-link">{{tab.label}}</f7-link>
</f7-toolbar>
<f7-toolbar bottom class="toolbar-details">
<div style="margin-left: auto">
<f7-toggle :checked="previewMode" @toggle:change="(value) => togglePreviewMode(value)"></f7-toggle> Run mode<span v-if="$device.desktop">&nbsp;(Ctrl-R)</span>
</div>
</f7-toolbar>
<f7-tabs class="tabs-editor-tabs">
<f7-tab id="design" class="tabs-editor-design-tab" @tab:show="() => this.currentTab = 'design'" :tab-active="currentTab === 'design'">
<f7-block v-if="!ready" class="text-align-center">
<f7-preloader></f7-preloader>
<div>Loading...</div>
</f7-block>
<!-- <f7-block class="block-narrow" v-if="ready && !previewMode">
<page-settings :page="page" :createMode="createMode" />
</f7-block> -->
<f7-block class="block-narrow" style="padding-bottom: 8rem" v-if="ready && !previewMode">
<f7-col>
<f7-block-title>Page Configuration</f7-block-title>
<config-sheet
:parameterGroups="pageWidgetDefinition.props.parameterGroups || []"
:parameters="pageWidgetDefinition.props.parameters || []"
:configuration="page.config"
@updated="dirty = true"
/>
</f7-col>
<f7-col v-if="modelReady && !previewMode">
<f7-block-title>Cards</f7-block-title>
<f7-segmented strong tag="p">
<f7-button v-for="tab in modelTabs" :key="tab.value" @click="showCardControls = false; currentModelTab = tab.value" :active="currentModelTab === tab.value" :text="tab.label"></f7-button>
</f7-segmented>
<div class="display-block padding">
<div class="no-padding float-right">
<f7-button @click="showCardControls = !showCardControls" small outline :fill="showCardControls" sortable-toggle=".sortable" style="margin-top: -3px; margin-right: 5px"
color="gray" icon-size="12" icon-ios="material:wrap_text" icon-md="material:wrap_text" icon-aurora="material:wrap_text">&nbsp;Reorder</f7-button>
</div>
</div>
<f7-list media-list class="homecards-list" sortable :key="'cards-' + currentModelTab + cardListId" @sortable:sort="reorderCard">
<f7-list-item media-item :link="(showCardControls) ? undefined : ''"
@click.native="(ev) => cardClicked(ev, card, idx)"
v-for="(card, idx) in cardGroups(currentModelTab, page).flat()" :key="idx"
:title="card.separator || card.defaultTitle" :footer="(card.separator) ? '(separator)' : card.key">
<f7-menu slot="content-start" class="configure-layout-menu">
<f7-menu-item icon-f7="list_bullet" dropdown>
<f7-menu-dropdown>
<f7-menu-dropdown-item v-if="!card.separator" @click="configureCard(card)" href="#" text="Configure Card"></f7-menu-dropdown-item>
<f7-menu-dropdown-item v-if="!card.separator" @click="editCardCode(card)" href="#" text="Edit YAML"></f7-menu-dropdown-item>
<f7-menu-dropdown-item v-if="card.separator" @click="renameCardSeparator(idx)" href="#" text="Rename"></f7-menu-dropdown-item>
<f7-menu-dropdown-item divider></f7-menu-dropdown-item>
<f7-menu-dropdown-item v-if="!card.separator" @click="addCardSeparator(idx)" href="#" text="Add Separator Before"></f7-menu-dropdown-item>
<f7-menu-dropdown-item v-if="card.separator" @click="removeCardSeparator(idx)" href="#" text="Remove Separator"></f7-menu-dropdown-item>
</f7-menu-dropdown>
</f7-menu-item>
</f7-menu>
<f7-checkbox :checked="!isCardExcluded(card)" :disabled="card.separator !== undefined" slot="content-start" class="margin-right" />
</f7-list-item>
</f7-list>
</f7-col>
</f7-block>
<div v-else-if="ready && previewMode && currentTab === 'design'" :context="context" :key="pageKey">
<model-tab style="margin-bottom: 4rem" :context="context" :type="currentModelTab" :model="model" :page="page" />
</div>
</f7-tab>
<f7-tab id="code" @tab:show="() => { this.currentTab = 'code' }" :tab-active="currentTab === 'code'">
<editor v-if="currentTab === 'code'" :style="{ opacity: previewMode ? '0' : '' }" class="page-code-editor" mode="application/vnd.openhab.uicomponent+yaml;type=home" :value="pageYaml" @input="(value) => pageYaml = value" />
<!-- <pre class="yaml-message padding-horizontal" :class="[yamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{yamlError}}</pre> -->
<div v-if="ready && previewMode" :context="context" :key="pageKey">
<model-tab style="margin-bottom: 4rem" :context="context" :type="currentModelTab" :model="model" :page="page" />
</div>
</f7-tab>
</f7-tabs>
</f7-page>
</template>
<style lang="stylus">
.page-code-editor.vue-codemirror
display block
top calc(var(--f7-navbar-height) + var(--f7-tabbar-height))
height calc(100% - 3*var(--f7-navbar-height))
width 100%
.yaml-message
display block
position absolute
top 80%
white-space pre-wrap
.homecards-list
.item-link
overflow inherit
z-index inherit !important
</style>
<script>
import PageDesigner from '../pagedesigner-mixin'
import HomeCards from '../../../home/homecards-mixin'
import YAML from 'yaml'
import { OhHomePageDefinition, OhLocationCardParameters, OhEquipmentCardParameters, OhPropertyCardParameters } from '@/assets/definitions/widgets/home'
import ConfigSheet from '@/components/config/config-sheet.vue'
import ModelTab from '@/pages/home/model-tab.vue'
const ConfigurableWidgets = {
'oh-location-card': OhLocationCardParameters,
'oh-equipment-card': OhEquipmentCardParameters,
'oh-property-card': OhPropertyCardParameters
}
export default {
mixins: [PageDesigner, HomeCards],
components: {
'editor': () => import('@/components/config/controls/script-editor.vue'),
ConfigSheet,
ModelTab
},
props: ['createMode', 'uid'],
data () {
return {
pageWidgetDefinition: OhHomePageDefinition(),
modelTabs: [
{ value: 'locations', label: 'Locations' },
{ value: 'equipment', label: 'Equipment' },
{ value: 'properties', label: 'Properties' }
],
currentModelTab: 'locations',
showCardControls: false,
cardListId: this.$f7.utils.id(),
page: {
uid: 'home',
component: 'oh-home-page',
config: {
label: 'Home Page'
},
slots: {
locations: [ { component: 'oh-locations-tab', config: {}, slots: {} } ],
equipment: [ { component: 'oh-equipment-tab', config: {}, slots: {} } ],
properties: [ { component: 'oh-properties-tab', config: {}, slots: {} } ]
}
}
}
},
watch: {
pageReady (val) {
if (val) {
this.loadModel(this.page)
}
}
},
methods: {
addWidget (component, widgetType, parentContext, slot) {
if (!slot) slot = 'default'
if (!component.slots) component.slots = {}
if (!component.slots[slot]) component.slots[slot] = []
if (widgetType) {
component.slots[slot].push({
component: widgetType,
config: {
title: 'New Tab',
icon: 'f7:squares_below_rectangle'
},
slots: { default: [] }
})
this.forceUpdate()
}
},
getWidgetDefinition (componentType) {
return ConfigurableWidgets[componentType] ? ConfigurableWidgets[componentType]() : null
},
ensureCardComponentExists (card) {
if (!this.page.slots[this.currentModelTab][0].slots[card.key]) {
this.page.slots[this.currentModelTab][0].slots[card.key] = [
{
component: (this.currentModelTab) === 'locations' ? 'oh-location-card' : (this.currentModelTab === 'equipment') ? 'oh-equipment-card' : 'oh-property-card',
config: {}
}
]
}
},
configureCard (card) {
if (!card.key) return
if (!this.page.slots[this.currentModelTab] || !this.page.slots[this.currentModelTab][0] || !this.page.slots[this.currentModelTab][0].slots) return
this.ensureCardComponentExists(card)
return this.configureWidget(this.page.slots[this.currentModelTab][0].slots[card.key][0])
},
editCardCode (card) {
if (!card.key) return
if (!this.page.slots[this.currentModelTab] || !this.page.slots[this.currentModelTab][0] || !this.page.slots[this.currentModelTab][0].slots) return
this.ensureCardComponentExists(card)
return this.editWidgetCode(this.page.slots[this.currentModelTab][0].slots[card.key][0])
},
addCardSeparator (idx) {
const orderedCards = this.cardGroups(this.currentModelTab, this.page).flat().map((e) => (e.separator) ? e : e.key)
orderedCards.splice(idx, 0, { separator: 'New Section' })
this.$set(this.page.slots[this.currentModelTab][0].config, 'cardOrder', orderedCards)
this.renameCardSeparator(idx)
},
renameCardSeparator (idx) {
const orderedCards = this.cardGroups(this.currentModelTab, this.page).flat().map((e) => (e.separator) ? e : e.key)
if (orderedCards[idx].separator) {
this.$f7.dialog.prompt('Enter the title of the separator:', null,
(title) => {
orderedCards[idx].separator = title
this.$set(this.page.slots[this.currentModelTab][0].config, 'cardOrder', orderedCards)
},
null,
orderedCards[idx].separator)
}
},
removeCardSeparator (idx) {
const orderedCards = this.cardGroups(this.currentModelTab, this.page).flat().map((e) => (e.separator) ? e : e.key)
orderedCards.splice(idx, 1)
this.$set(this.page.slots[this.currentModelTab][0].config, 'cardOrder', orderedCards)
},
cardClicked (ev, card, idx) {
ev.cancelBubble = true
let el = ev.target
if (el.classList.contains('icon-checkbox')) {
this.toggleCardDisplay(card)
return
}
while (!el.classList.contains('media-item')) {
if (el && el.classList.contains('menu')) return
el = el.parentElement
}
if (card.separator) {
this.renameCardSeparator(idx)
}
this.configureCard(card)
},
reorderCard (ev) {
const orderedCards = this.cardGroups(this.currentModelTab, this.page).flat().map((e) => (e.separator) ? e : e.key)
const newOrder = [...orderedCards]
newOrder.splice(ev.to, 0, newOrder.splice(ev.from, 1)[0])
this.$set(this.page.slots[this.currentModelTab][0].config, 'cardOrder', newOrder)
this.cardListId = null
this.showCardControls = false
this.$nextTick(() => {
this.cardListId = this.$f7.utils.id()
})
},
isCardExcluded (card) {
if (!card.key) return
const page = this.page
const type = this.currentModelTab
const excludedCards = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].config && page.slots[type][0].config.excludedCards) ? page.slots[type][0].config.excludedCards : []
const excludedIdx = excludedCards.indexOf(card.key)
return excludedIdx >= 0
},
toggleCardDisplay (card) {
if (!card.key) return
const page = this.page
const type = this.currentModelTab
const excludedCards = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].config && page.slots[type][0].config.excludedCards) ? page.slots[type][0].config.excludedCards : []
const excludedIdx = excludedCards.indexOf(card.key)
if (excludedIdx < 0) {
this.$set(this.page.slots[type][0].config, 'excludedCards', [...excludedCards, card.key])
} else {
this.page.slots[type][0].config.excludedCards.splice(excludedIdx, 1)
}
},
toYaml () {
this.pageYaml = YAML.stringify(Object.assign({ config: this.page.config }, this.page.slots))
},
fromYaml () {
try {
const updatedTabs = YAML.parse(this.pageYaml)
this.$set(this.page, 'slots', updatedTabs)
this.$set(this.page, 'config', this.page.slots.config)
delete this.page.slots.config
this.forceUpdate()
return true
} catch (e) {
this.$f7.dialog.alert(e).open()
return false
}
}
}
}
</script>

View File

@ -199,7 +199,8 @@ export default {
this.currentComponent = component
const popup = {
component: WidgetConfigPopup
component: WidgetConfigPopup,
componentType: this.type
}
this.$f7router.navigate({

View File

@ -139,6 +139,7 @@ export default {
pageTypes: [
{ type: 'sitemap', label: 'Sitemap', componentType: 'Sitemap', icon: 'menu' },
{ type: 'layout', label: 'Layout', componentType: 'oh-layout-page', icon: 'rectangle_grid_2x2' },
{ type: 'home', label: 'Home', componentType: 'oh-home-page', icon: 'house' },
{ type: 'tabs', label: 'Tabbed', componentType: 'oh-tabs-page', icon: 'squares_below_rectangle' },
{ type: 'map', label: 'Map', componentType: 'oh-map-page', icon: 'map' },
{ type: 'plan', label: 'Floor plan', componentType: 'oh-plan-page', icon: 'square_stack_3d_up' },