diff --git a/bundles/org.openhab.ui/doc/components/index.md b/bundles/org.openhab.ui/doc/components/index.md
index 97ea803cac..418a367e07 100644
--- a/bundles/org.openhab.ui/doc/components/index.md
+++ b/bundles/org.openhab.ui/doc/components/index.md
@@ -87,6 +87,8 @@ source: https://github.com/openhab/openhab-webui/edit/main/bundles/org.openhab.u
| Component | Name | Description |
|--------|------|-------------|
| [`oh-block`](./oh-block.html) | [Layout Grid Block](./oh-block.html) | A block in a grid layout |
+| [`oh-canvas-item`](./oh-canvas-item.html) | [Canvas Item](./oh-canvas-item.html) | Specific attributes to display widgets on a canvas. |
+| [`oh-canvas-layout`](./oh-canvas-layout.html) | [Canvas Layout](./oh-canvas-layout.html) | Position widgets on a canvas layout with arbitrary position and size down to pixel resolution |
| [`oh-grid-col`](./oh-grid-col.html) | [Layout Grid Column](./oh-grid-col.html) | A column in a grid layout |
| [`oh-grid-layout`](./oh-grid-layout.html) | [Fixed Grid Layout](./oh-grid-layout.html) | Arranges widgets on a grid of squares with user-defined sizes |
| [`oh-grid-row`](./oh-grid-row.html) | [Layout Grid Row](./oh-grid-row.html) | A row in a grid layout |
diff --git a/bundles/org.openhab.ui/doc/components/oh-canvas-item.md b/bundles/org.openhab.ui/doc/components/oh-canvas-item.md
new file mode 100644
index 0000000000..29595330c2
--- /dev/null
+++ b/bundles/org.openhab.ui/doc/components/oh-canvas-item.md
@@ -0,0 +1,96 @@
+---
+title: oh-canvas-item - Canvas Item
+component: oh-canvas-item
+label: Canvas Item
+description: Specific attributes to display widgets on a canvas.
+source: https://github.com/openhab/openhab-webui/edit/main/bundles/org.openhab.ui/doc/components/oh-canvas-item.md
+prev: /docs/ui/components/
+---
+
+# oh-canvas-item - Canvas Item
+
+
+
+[[toc]]
+
+
+
+
+Specific attributes to display widgets on a canvas.
+
+
+## Configuration
+
+
+
+### Layout Settings
+
+
+
+
+ Preserve classic appearance of widgets as in standard layout pages.
+
+
+
+
+ Do not shadow inner elements of standard widgets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.ui/doc/components/oh-canvas-layout.md b/bundles/org.openhab.ui/doc/components/oh-canvas-layout.md
new file mode 100644
index 0000000000..4b78d9b295
--- /dev/null
+++ b/bundles/org.openhab.ui/doc/components/oh-canvas-layout.md
@@ -0,0 +1,143 @@
+---
+title: oh-canvas-layout - Canvas Layout
+component: oh-canvas-layout
+label: Canvas Layout
+description: Position widgets on a canvas layout with arbitrary position and size down to pixel resolution
+source: https://github.com/openhab/openhab-webui/edit/main/bundles/org.openhab.ui/doc/components/oh-canvas-layout.md
+prev: /docs/ui/components/
+---
+
+# oh-canvas-layout - Canvas Layout
+
+
+
+[[toc]]
+
+
+
+
+Position widgets on a canvas layout with arbitrary position and size down to pixel resolution
+
+
+## Configuration
+
+
+
+### Layout Settings
+
+
+
+
+ Grid size in pixels used to snap content (default 20)
+
+
+
+
+
+### Screen Settings
+
+
+
+
+ Screen width in pixels (default 1280)
+
+
+
+
+ Screen width in pixels (default 720)
+
+
+
+
+ Scale content to screen width (can lead to unexpected styling issues) (default false)
+
+
+
+
+ The URL of the image to display as background
+
+
+
+
+ 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"
+
+
+
+
+
+### Canvas items shadow
+
+
+
+
+ Shadow applied to box elements (box-shadow CSS syntax).
+
+
+
+
+ Shadow applied to text elements or font icons (text-shadow CSS syntax)
+
+
+
+
+ Shadow applied to raster or SVG image elements (filter: drop-shadow() CSS syntax)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.ui/web/package-lock.json b/bundles/org.openhab.ui/web/package-lock.json
index 25dbecd5d3..2bd784a00e 100644
--- a/bundles/org.openhab.ui/web/package-lock.json
+++ b/bundles/org.openhab.ui/web/package-lock.json
@@ -36,6 +36,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",
@@ -21404,6 +21405,15 @@
"npm": ">= 3.0.0"
}
},
+ "node_modules/vue-draggable-resizable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/vue-draggable-resizable/-/vue-draggable-resizable-2.3.0.tgz",
+ "integrity": "sha512-77CLRj1TPwB30pwsjOf3pkd1UzYanCdKXbqhILJ0Oo5QQl50lvBfyQCXxMFzwWwTc3sbBbQH3FfWSV+BkoSElA==",
+ "engines": {
+ "node": ">= 4.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
"node_modules/vue-echarts": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-4.1.0.tgz",
@@ -41382,6 +41392,11 @@
"diff-match-patch": "^1.0.0"
}
},
+ "vue-draggable-resizable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/vue-draggable-resizable/-/vue-draggable-resizable-2.3.0.tgz",
+ "integrity": "sha512-77CLRj1TPwB30pwsjOf3pkd1UzYanCdKXbqhILJ0Oo5QQl50lvBfyQCXxMFzwWwTc3sbBbQH3FfWSV+BkoSElA=="
+ },
"vue-echarts": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-4.1.0.tgz",
diff --git a/bundles/org.openhab.ui/web/package.json b/bundles/org.openhab.ui/web/package.json
index cbff3656a8..93e4b20fd4 100644
--- a/bundles/org.openhab.ui/web/package.json
+++ b/bundles/org.openhab.ui/web/package.json
@@ -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",
diff --git a/bundles/org.openhab.ui/web/src/assets/definitions/widgets/layout/index.js b/bundles/org.openhab.ui/web/src/assets/definitions/widgets/layout/index.js
index 992c16d3d0..aab1d82e11 100644
--- a/bundles/org.openhab.ui/web/src/assets/definitions/widgets/layout/index.js
+++ b/bundles/org.openhab.ui/web/src/assets/definitions/widgets/layout/index.js
@@ -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 OhCanvasItemDefinition () {
+ return new WidgetDefinition('oh-canvas-item', 'Canvas Item', 'Specific attributes to display widgets on a canvas.')
+ .paramGroup(pg('appearance', 'Layout Settings'), [
+ pb('notStyled', 'Preserve classic style', 'Preserve classic appearance of widgets as in standard layout pages.'),
+ pb('noCanvasShadow', 'No elements shadow', 'Do not shadow inner elements of standard widgets')
+ .v((value, configuration, configDescription, parameters) => { return configuration.notStyled !== true })
+ ])
+}
+
+export function OhCanvasLayoutDefinition () {
+ return new WidgetDefinition('oh-canvas-layout', 'Canvas Layout', 'Position widgets on a canvas 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 20)')
+ ])
+ .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', 'Canvas 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()
+ ])
+}
diff --git a/bundles/org.openhab.ui/web/src/components/widgets/layout/index.js b/bundles/org.openhab.ui/web/src/components/widgets/layout/index.js
index 2bb41c8df0..36579a55f9 100644
--- a/bundles/org.openhab.ui/web/src/components/widgets/layout/index.js
+++ b/bundles/org.openhab.ui/web/src/components/widgets/layout/index.js
@@ -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 OhCanvasLayout } from './oh-canvas-layout.vue'
+export { default as OhCanvasItem } from './oh-canvas-item.vue'
diff --git a/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-canvas-item.vue b/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-canvas-item.vue
new file mode 100644
index 0000000000..133b2e5bf0
--- /dev/null
+++ b/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-canvas-item.vue
@@ -0,0 +1,237 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-canvas-layout.vue b/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-canvas-layout.vue
new file mode 100644
index 0000000000..fc956e6ca2
--- /dev/null
+++ b/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-canvas-layout.vue
@@ -0,0 +1,255 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getCurrentScreenResolution() }}
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-layout-page.vue b/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-layout-page.vue
index b6e8e52072..a7d13eb959 100644
--- a/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-layout-page.vue
+++ b/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-layout-page.vue
@@ -1,6 +1,6 @@
-
+
-
+
+
+
+
-
+
diff --git a/bundles/org.openhab.ui/web/src/pages/settings/pages/layout/layout-edit.vue b/bundles/org.openhab.ui/web/src/pages/settings/pages/layout/layout-edit.vue
index 74c5949f15..472f0c333a 100644
--- a/bundles/org.openhab.ui/web/src/pages/settings/pages/layout/layout-edit.vue
+++ b/bundles/org.openhab.ui/web/src/pages/settings/pages/layout/layout-edit.vue
@@ -36,41 +36,55 @@
!(context.component.slots.default && context.component.slots.default.length) &&
!(context.component.slots.masonry && context.component.slots.masonry.length) &&
!(context.component.slots.grid && context.component.slots.grid.length) &&
+ !(context.component.slots.canvas && context.component.slots.canvas.length) &&
+ page.uid !== 'overview' &&
!['responsive', 'fixed'].includes(page.config.layoutType)"
- class="block-narrow margin-bottom" inset>
-
- Choose a layout style
-
-
-
-
-
-
- Responsive
-
-
- Create a page that automatically adjusts to the size of the screen. Suitable for use with any device.
-
-
-
-
-
-
-
- Fixed Grid
-
-
- Create a panel-like page for a specific screen size. Suitable for e.g. wall mounted tablets.
-
-
-
-
+ class="block-narrow no-padding">
+
+
+
+ Layout Type
+
+
+
+
+ Switch to a fixed layout type, suitable for e.g. wall mounted tablets:
+
+
+
+
+
+
+ Fixed Grid
+
+
+ Position and resize widgets on a grid with fixed dimensions.
+
+
+
+
+
+
+
+ Fixed Canvas
+
+
+ Position and resize widgets freely over a fixed background.
+
+
+
+
+
+
+
+
-
+ @add-grid-item="addGridItem"
+ @add-canvas-item="addCanvasItem" />
{ this.currentTab = 'code' }" :tab-active="currentTab === 'code'">
@@ -148,7 +162,8 @@ export default {
config: {},
slots: {
default: [],
- grid: []
+ grid: [],
+ canvas: []
}
},
addFromModelContext: {},
@@ -273,12 +288,15 @@ export default {
this.addFromModelContext = {}
this.forceUpdate()
},
- setLayoutType (layoutType) {
+ setLayoutType (layoutType, fixedType) {
this.page.config.layoutType = layoutType
+ this.page.config.fixedType = fixedType
if (layoutType === 'responsive') {
this.page.slots.default = []
- } else {
+ } else if (layoutType === 'fixed' && fixedType === 'grid') {
this.page.slots.grid = []
+ } else if (layoutType === 'fixed' && fixedType === 'canvas') {
+ this.page.slots.canvas = []
}
this.forceUpdate()
},
@@ -300,7 +318,13 @@ export default {
addGridItem (component) {
component.slots['grid'].push({
component: 'oh-grid-item',
- config: { x: 5, y: 3, h: 2, w: 2 },
+ config: { x: 5, y: 3, h: 2, w: 2 }
+ })
+ },
+ addCanvasItem (component) {
+ component.slots['canvas'].push({
+ component: 'oh-canvas-item',
+ config: { x: 10, y: 10, h: 50, w: 50 },
slots: { default: [] }
})
this.forceUpdate()
@@ -316,21 +340,24 @@ export default {
config: this.page.config,
blocks: this.page.slots.default,
masonry: this.page.slots.masonry,
- grid: this.page.slots.grid
+ grid: this.page.slots.grid,
+ canvas: this.page.slots.canvas
})
},
fromYaml () {
try {
const updatedPage = YAML.parse(this.pageYaml)
- if (updatedPage.config && updatedPage.config.layoutType && updatedPage.config.layoutType === 'fixed' &&
+ if (updatedPage.config && updatedPage.config.layoutType &&
+ updatedPage.config.layoutType === 'fixed' &&
((updatedPage.blocks && updatedPage.blocks.length) || (updatedPage.masonry && updatedPage.masonry.length))) {
- throw new Error('Using blocks and masonry in fixed-size layouts is not possible')
+ throw new Error('Using blocks and masonry in fixed layouts is not possible')
}
this.$set(this.page, 'config', updatedPage.config)
this.$set(this.page.slots, 'default', updatedPage.blocks)
this.$set(this.page.slots, 'masonry', updatedPage.masonry)
this.$set(this.page.slots, 'grid', updatedPage.grid)
+ this.$set(this.page.slots, 'canvas', updatedPage.canvas)
this.forceUpdate()
return true
} catch (e) {
diff --git a/bundles/org.openhab.ui/web/src/pages/settings/pages/pagedesigner-mixin.js b/bundles/org.openhab.ui/web/src/pages/settings/pages/pagedesigner-mixin.js
index e935277f08..96b43e54cb 100644
--- a/bundles/org.openhab.ui/web/src/pages/settings/pages/pagedesigner-mixin.js
+++ b/bundles/org.openhab.ui/web/src/pages/settings/pages/pagedesigner-mixin.js
@@ -43,6 +43,8 @@ export default {
pasteWidget: this.pasteWidget,
moveWidgetUp: this.moveWidgetUp,
moveWidgetDown: this.moveWidgetDown,
+ sendWidgetToBack: this.sendWidgetToBack,
+ bringWidgetToFront: this.bringWidgetToFront,
removeWidget: this.removeWidget
} : null,
clipboardtype: this.clipboardType
@@ -285,18 +287,25 @@ export default {
},
moveWidgetUp (component, parentContext, slot = 'default') {
let siblings = parentContext.component.slots[slot]
- let pos = siblings.indexOf(component)
- if (pos <= 0) return
- siblings.splice(pos, 1)
- siblings.splice(pos - 1, 0, component)
- this.forceUpdate()
+ this.moveWidget(component, parentContext, slot, siblings.indexOf(component) - 1)
},
moveWidgetDown (component, parentContext, slot = 'default') {
+ let siblings = parentContext.component.slots[slot]
+ this.moveWidget(component, parentContext, slot, siblings.indexOf(component) + 1)
+ },
+ bringWidgetToFront (component, parentContext, slot = 'default') {
+ this.moveWidget(component, parentContext, slot, parentContext.component.slots[slot].length)
+ },
+ sendWidgetToBack (component, parentContext, slot = 'default') {
+ this.moveWidget(component, parentContext, slot, 0)
+ },
+ moveWidget (component, parentContext, slot = 'default', newPos) {
let siblings = parentContext.component.slots[slot]
let pos = siblings.indexOf(component)
- if (pos >= siblings.length - 1) return
+ newPos = Math.max(0, Math.min(siblings.length, newPos))
+ if (pos === newPos) return
siblings.splice(pos, 1)
- siblings.splice(pos + 1, 0, component)
+ siblings.splice(newPos, 0, component)
this.forceUpdate()
},
removeWidget (component, parentContext, slot = 'default') {