diff --git a/bundles/org.openhab.ui/web/package.json b/bundles/org.openhab.ui/web/package.json index 99cca105d3..480d47c32b 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..0b21a03c1f 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 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() + ]) +} 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..e5fd5ff0b3 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 OhPlanLayout } from './oh-plan-layout.vue' +export { default as OhPlanItem } from './oh-plan-item.vue' 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..802cd9168c 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/components/widgets/layout/oh-plan-item.vue b/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-plan-item.vue new file mode 100644 index 0000000000..7d8118166f --- /dev/null +++ b/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-plan-item.vue @@ -0,0 +1,223 @@ + + + + + + + + + + Auto Size + + + + Shadow + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-plan-layout.vue b/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-plan-layout.vue new file mode 100644 index 0000000000..fde95f55f3 --- /dev/null +++ b/bundles/org.openhab.ui/web/src/components/widgets/layout/oh-plan-layout.vue @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + {{ getCurrentScreenResolution() }} + + + + + + + + + + diff --git a/bundles/org.openhab.ui/web/src/js/app.js b/bundles/org.openhab.ui/web/src/js/app.js index 8d2306d17b..67e59f5a67 100644 --- a/bundles/org.openhab.ui/web/src/js/app.js +++ b/bundles/org.openhab.ui/web/src/js/app.js @@ -58,6 +58,11 @@ Vue.use(Trend) // Import Fulscreen Plugin import fullscreen from 'vue-fullscreen' Vue.use(fullscreen) +// Import Vue Draggable Resizable +import VueDraggableResizable from 'vue-draggable-resizable' +// optionally import default styles +import 'vue-draggable-resizable/dist/VueDraggableResizable.css' +Vue.component('vue-draggable-resizable', VueDraggableResizable) // Extend prototype with the openHAB API interface Vue.prototype.$oh = openhab 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..cef0278f7b 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,13 +36,14 @@ !(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) && - !['responsive', 'fixed'].includes(page.config.layoutType)" + !(context.component.slots.plan && context.component.slots.plan.length) && + !['responsive', 'fixed', 'plan'].includes(page.config.layoutType)" class="block-narrow margin-bottom" inset> Choose a layout style - + @@ -53,7 +54,7 @@ - + @@ -64,13 +65,25 @@ + + + + + Floorplan + + + Create a plan-like page for a specific screen size, with pixel positionning of widgets. Suitable for e.g. wall mounted tablets. + + + + @add-grid-item="addGridItem" + @add-plan-item="addPlanItem" /> { this.currentTab = 'code' }" :tab-active="currentTab === 'code'"> @@ -148,7 +161,8 @@ export default { config: {}, slots: { default: [], - grid: [] + grid: [], + plan: [] } }, addFromModelContext: {}, @@ -277,8 +291,10 @@ export default { this.page.config.layoutType = layoutType if (layoutType === 'responsive') { this.page.slots.default = [] - } else { + } else if (layoutType === 'fixed') { this.page.slots.grid = [] + } else { + this.page.slots.plan = [] } this.forceUpdate() }, @@ -300,7 +316,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 } + }) + }, + addPlanItem (component) { + component.slots['plan'].push({ + component: 'oh-plan-item', + config: { x: 10, y: 10, h: 50, w: 50 }, slots: { default: [] } }) this.forceUpdate() @@ -316,21 +338,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, + plan: this.page.slots.plan }) }, 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.config.layoutType === 'plan') && ((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-size or plan 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, 'plan', updatedPage.plan) 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') {