Skip to content

Commit

Permalink
Addition of a plan layout for floorplan-like page, in addition to res…
Browse files Browse the repository at this point in the history
…ponsive and grid layouts. Supports system and user-defined widgets.

Signed-off-by: Gautier Taravella <tarag@mailbox.org>
  • Loading branch information
tarag committed Aug 3, 2021
1 parent 36658f5 commit 82c087a
Show file tree
Hide file tree
Showing 9 changed files with 485 additions and 22 deletions.
1 change: 1 addition & 0 deletions bundles/org.openhab.ui/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"vue": "^2.6.12",
"vue-async-computed": "^3.9.0",
"vue-codemirror": "^4.0.6",
"vue-draggable-resizable": "^2.3.0",
"vue-echarts": "^4.1.0",
"vue-fragment": "^1.5.1",
"vue-fullscreen": "^2.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,31 @@ export function OhGridLayoutDefinition () {
pb('showFullscreenIcon', 'Show Fullscreen Icon', 'Show a fullscreen icon on the top right corner (default false)')
])
}

export function OhPlanItemDefinition () {
return new WidgetDefinition('oh-plan-item', 'Plan Item', 'Specific attributes to display widgets on a plan.')
.paramGroup(pg('appearance', 'Layout Settings'), [
pb('notStyled', 'Preserve classic style', 'Preserve classic appearance of widgets as in standard layout pages.'),
pb('noPlanShadow', 'No elements shadow', 'Do not shadow inner elements of standard widgets')
.v((value, configuration, configDescription, parameters) => { return configuration.notStyled !== true })
])
}

export function OhPlanLayoutDefinition () {
return new WidgetDefinition('oh-plan-layout', 'Plan Layout', 'Position widgets on a plan layout with arbitrary position and size down to pixel resolution')
.paramGroup(pg('layout', 'Layout Settings'), [
pn('grid', 'Grid size', 'Grid size in pixels used to snap content (default 5)')
])
.paramGroup(pg('screenSettings', 'Screen Settings'), [
pn('screenWidth', 'Screen Width', 'Screen width in pixels (default 1280)'),
pn('screenHeight', 'Screen Height', 'Screen width in pixels (default 720)'),
pb('scale', 'Scaling', 'Scale content to screen width (can lead to unexpected styling issues) (default false)'),
pt('imageUrl', 'Image URL', 'The URL of the image to display as background').c('url'),
pt('imageSrcSet', 'Image Source Set', 'The src-set attribute of background image element to take into account mulitple device resolutions. For example: "/static/floorplans/floor-0.jpg, /static/floorplans/floor-0@2x.jpg 2x"')
])
.paramGroup(pg('shadow', 'Plan items shadow'), [
pt('boxShadow', 'Box shadow', 'Shadow applied to box elements (box-shadow CSS syntax).').a(),
pt('textShadow', 'Text shadow', 'Shadow applied to text elements or font icons (text-shadow CSS syntax)').a(),
pt('filterShadow', 'Fitler Shadow', 'Shadow applied to raster or SVG image elements (filter: drop-shadow() CSS syntax)').a()
])
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export { default as OhGridCol } from './oh-grid-col.vue'
export { default as OhGridCells } from './oh-grid-cells.vue'
export { default as OhMasonry } from './oh-masonry.vue'
export { default as OhGridLayout } from './oh-grid-layout.vue'
export { default as OhPlanLayout } from './oh-plan-layout.vue'
export { default as OhPlanItem } from './oh-plan-item.vue'
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div>
<template v-if="config.layoutType !== 'fixed'">
<template v-if="config.layoutType === 'responsive'">
<oh-block v-for="(component, idx) in context.component.slots.default"
:key="idx"
:context="childContext(component)"
Expand Down Expand Up @@ -30,27 +30,31 @@
</f7-block>
</template>
</template>
<template v-else>
<template v-else-if="config.layoutType === 'fixed'">
<oh-grid-layout :context="context" />
</template>
<template v-else-if="config.layoutType === 'plan'">
<oh-plan-layout :context="context" />
</template>
</div>
</template>

<style lang="stylus">
</style>
<style lang="stylus"></style>

<script>
import mixin from '../widget-mixin'
import OhBlock from './oh-block.vue'
import OhMasonry from './oh-masonry.vue'
import OhGridLayout from './oh-grid-layout.vue'
import OhPlanLayout from './oh-plan-layout.vue'
export default {
mixins: [mixin],
components: {
OhBlock,
OhMasonry,
OhGridLayout
OhGridLayout,
OhPlanLayout
}
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
<template>
<vue-draggable-resizable
:id="'oh-plan-item-vdr-' +id"
:key="reloadKey"
:x="x"
:y="y"
:w="w"
:h="h"
:parent="true"
:draggable="!!context.editmode"
:resizable="!!context.editmode && !autosize"
:class-name="!!context.editmode ? 'oh-plan-item-editmode' : 'oh-plan-item-runmode'"
:grid="gridEnable ? [gridPitch,gridPitch] : undefined"
v-if="visible" class="oh-plan-item no-margin"
@dragging="onDrag"
@resizing="onResize"
:on-drag-start="onDragStartCallback"
:on-resize-start="onResizeStartCallback">
<f7-menu v-if="context.editmode" class="configure-plan-menu">
<f7-menu-item icon-f7="menu" dropdown icon-only>
<f7-menu-dropdown right>
<f7-menu-dropdown-item @click="context.editmode.configureWidget(context.component, context.parent)" href="#" text="Configure container" />
<f7-menu-dropdown-item v-if="context.component.slots.default.length > 0" @click="context.editmode.configureWidget(context.component.slots.default[0], context)" href="#" text="Configure Widget" />
<f7-menu-dropdown-item @click="context.editmode.editWidgetCode(context.component, context.parent)" href="#" text="Edit YAML" />
<f7-menu-dropdown-item v-if="context.component.slots.default.length > 0" @click="toggleAutoSize()" href="#">
<span>Auto Size</span>
<f7-icon class="margin-left" :f7="autosize ? 'checkmark_square' : 'square'" />
</f7-menu-dropdown-item>
<f7-menu-dropdown-item v-if="context.component.slots.default.length > 0" @click="toggleShadow()" href="#">
<span>Shadow</span>
<f7-icon class="margin-left" :f7="shadow ? 'checkmark_square' : 'square'" />
</f7-menu-dropdown-item>
<f7-menu-dropdown-item v-if="context.component.slots.default.length > 0" divider />
<f7-menu-dropdown-item @click="context.editmode.copyWidget(context.component, context.parent)" href="#" text="Copy" />
<f7-menu-dropdown-item divider />
<f7-menu-dropdown-item @click="context.editmode.bringWidgetToFront(context.component, context.parent, 'plan')" href="#" text="Bring to Front" />
<f7-menu-dropdown-item @click="context.editmode.moveWidgetDown(context.component, context.parent, 'plan')" href="#" text="Move Up" />
<f7-menu-dropdown-item @click="context.editmode.moveWidgetUp(context.component, context.parent, 'plan')" href="#" text="Move Down" />
<f7-menu-dropdown-item @click="context.editmode.sendWidgetToBack(context.component, context.parent, 'plan')" href="#" text="Send to Back" />
<f7-menu-dropdown-item divider />
<f7-menu-dropdown-item @click="context.editmode.removeWidget(context.component, context.parent, 'plan')" href="#" text="Remove Item" />
</f7-menu-dropdown>
</f7-menu-item>
</f7-menu>

<oh-placeholder-widget v-if="context.editmode && !context.component.slots.default.length" @click="context.editmode.addWidget(context.component, null, context.parent)" class="oh-plan-item-content" />
<generic-widget-component v-else-if="context.component.slots.default.length" :context="childContext(context.component.slots.default[0])" @command="onCommand" class="oh-plan-item-content"
:class="{
'oh-plan-item-styled' : styled,
'oh-plan-item-shadow' : styled && shadow
}" />

<f7-icon v-if="context.editmode" class="drag-handle" f7="move" size="15" color="gray" />
</vue-draggable-resizable>
</template>

<style lang="stylus">
.oh-plan-item-editmode
outline 1px dashed #F00
.oh-plan-item
position: absolute
.oh-plan-item-content
width 100%
height 100%
margin 0
.oh-plan-item-styled // override background obscuring styles from system widgets
&.card // apply to card items
box-shadow: none
background: none
.card-content, .card-footer
.segmented, .stepper, .toggle
background: var(--f7-card-bg-color);
.oh-plan-item-shadow // shadow tuned to various card widgets
&.card // apply to card items
.card-content, .card-footer
.segmented, .stepper, .toggle
box-shadow: var(--oh-plan-item-box-shadow)
.oh-slider
.range-bar, .range-knob, .range-knob-label
box-shadow: var(--oh-plan-item-box-shadow)
img, svg
filter: var(--oh-plan-item-svg-shadow)
.label-card-content
text-shadow: var(--oh-plan-item-text-shadow)
.placeholder-widget a
height: 100%
padding: 0
display: flex
.drag-handle // show drag handle on upper left corner
position: absolute !important
top: 0px
left: 0px
padding: 2px
z-index: 1000
.configure-plan-menu // show menu icon on upper right corner
position: absolute
top: 2px
right: 2px
.menu-inner
padding: 0px
.menu-inner:after
width: 0px
</style>

<script>
import mixin from '../widget-mixin'
import VueDraggableResizable from 'vue-draggable-resizable'
import OhPlaceholderWidget from '../layout/oh-placeholder-widget.vue'
import { OhPlanItemDefinition } from '@/assets/definitions/widgets/layout'
export default {
mixins: [mixin],
widget: OhPlanItemDefinition,
components: {
VueDraggableResizable,
OhPlaceholderWidget
},
props: {
gridPitch: Number,
gridEnable: Boolean,
id: String
},
data () {
return {
x: 0,
y: 0,
w: 0,
h: 0,
reloadKey: 0,
shadow: true,
styled: true
}
},
created () {
this.x = this.config.x || 20
this.y = this.config.y || 20
this.w = this.config.w || 100
this.h = this.config.h || 100
this.shadow = !this.config.noPlanShadow
this.styled = !this.config.notStyled
},
computed: {
autosize () {
return this.w === 'auto'
}
},
methods: {
toggleAutoSize () {
if (this.w === 'auto') {
const elem = document.getElementById('oh-plan-item-vdr-' + this.id)
this.w = this.context.component.config.w = elem.clientWidth
this.h = this.context.component.config.h = elem.clientHeight
} else {
this.w = this.context.component.config.w = 'auto'
this.h = this.context.component.config.h = 'auto'
this.reloadKey += 1
}
},
toggleShadow () {
this.shadow = !this.shadow
this.context.component.config.noPlanShadow = !this.shadow
},
onResize: function (x, y, width, height) {
this.x = this.context.component.config.x = x
this.y = this.context.component.config.y = y
this.w = this.context.component.config.w = width
this.h = this.context.component.config.h = height
},
onDrag: function (x, y) {
this.x = this.context.component.config.x = x
this.y = this.context.component.config.y = y
},
onResizeStartCallback (ev) {
if (this.w === 'auto' || this.h === 'auto') {
return false
}
const posOK = this.onDragStartCallback(ev)
if (this.gridEnable) {
const snapW = Math.round(this.w / this.gridPitch) * this.gridPitch
const snapH = Math.round(this.h / this.gridPitch) * this.gridPitch
if (this.w === snapW && this.h === snapH) {
// Widget already on grid, can continue to resize
return true && posOK
} else {
// Widget was not on grid, snap to grid upon first action
this.w = this.context.component.config.w = snapW
this.h = this.context.component.config.h = snapH
return false
}
} else {
return true
}
},
onDragStartCallback (ev) {
if (this.gridEnable) {
const snapX = Math.round(this.x / this.gridPitch) * this.gridPitch
const snapY = Math.round(this.y / this.gridPitch) * this.gridPitch
if (this.x === snapX && this.y === snapY) {
// Origin on grid, continue dragging action
return true
} else {
// First snap to grid component and stop action
this.x = this.context.component.config.x = snapX
this.y = this.context.component.config.y = snapY
return false
}
} else {
return true
}
}
}
}
</script>
Loading

0 comments on commit 82c087a

Please sign in to comment.