Sitemap editor: Various fixes (#3194)

Relates to: https://github.com/openhab/openhab-webui/pull/3184#issuecomment-2886630684

- Fixes scroll through the three on iOS, immediately moves elements.
- Fixes dirty warning when selecting an Item.
- Fixes editor gets dirty when collapsing the tree.
- Drag and drop also struggled when there were mutiple equal trees. When
  dragging one of these, it could not find the proper new position. This
  has been fixed as well.

---------

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
pull/3195/head
Mark Herwege 2025-05-19 19:33:30 +02:00 committed by GitHub
parent 68d47c5d22
commit 5d01ce3efc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 68 additions and 56 deletions

View File

@ -78,7 +78,7 @@ export default {
}
},
methods: {
allowedWidgetTypes (parentWidget, excludeWidgetIndex) {
allowedWidgetTypes (parentWidget) {
let types = this.WIDGET_TYPES.filter(w => w.type !== 'Sitemap')
// Button only allowed inside Buttongrid
if (parentWidget.component === 'Buttongrid') return types.filter(t => t.type === 'Button')
@ -88,8 +88,7 @@ export default {
// Linkable widget types only contain frames or none at all
if (this.LINKABLE_WIDGET_TYPES.includes(parentWidget.component)) {
if (parentWidget.slots?.widgets?.length > 0) {
const widgetList = parentWidget.slots.widgets.filter((widget, index) => index !== excludeWidgetIndex)
if (widgetList.find(w => w.component === 'Frame')) {
if (parentWidget.slots.widgets.find(w => w.component === 'Frame')) {
return types.filter(t => t.type === 'Frame')
} else {
return types.filter(t => t.type !== 'Frame')
@ -98,6 +97,14 @@ export default {
}
return types
},
canAddChildren (widget) {
if (!widget) return false
if (widget.component === 'Buttongrid') {
const buttons = widget.config.buttons
if (Array.isArray(buttons) && buttons.length) return false
}
return this.LINKABLE_WIDGET_TYPES.includes(widget.component)
},
widgetTypeDef (component) {
const componentType = component ?? this.widget.component
return this.WIDGET_TYPES.find(w => w.type === componentType)

View File

@ -8,8 +8,8 @@
@treeview:open="setWidgetClosed(false)"
@treeview:close="setWidgetClosed(true)"
@click="select">
<draggable :list="children" group="sitemap-treeview" animation="150" fallbackOnBody="true" swapThreshold="0.6"
@start="onStart" @change="onChange" @end="onEnd">
<draggable :disabled="!dropAllowed(widget)" :list="children" group="sitemap-treeview" animation="150" fallbackOnBody="true" swapThreshold="0.6" scrollSensitivity="200" delay="400" delayOnTouchOnly="true" touchStartThreshold="10"
@start="onStart" @end="onEnd" :move="onMove">
<sitemap-treeview-item class="sitemap-treeview-item" v-for="(childwidget, idx) in children"
:key="idx"
:includeItemName="includeItemName"
@ -39,7 +39,6 @@
<script>
import SitemapMixin from '@/components/pagedesigner/sitemap/sitemap-mixin'
import Draggable from 'vuedraggable'
import fastDeepEqual from 'fast-deep-equal/es6'
export default {
name: 'sitemap-treeview-item',
@ -69,51 +68,32 @@ export default {
console.debug('Drag start event:', event)
this.$set(this.localMoveState, 'moving', true)
this.$set(this.localMoveState, 'widget', this.widget.slots.widgets[event.oldIndex])
this.$set(this.localMoveState, 'oldList', this.widget.slots.widgets)
this.$set(this.localMoveState, 'oldIndex', event.oldIndex)
this.$set(this.localMoveState, 'newParent', this.parentWidget)
},
onChange (event) {
console.debug('Drag change event:', event)
if (event.added) {
this.$set(this.localMoveState, 'moving', false)
this.validateMove(event.added.newIndex)
onMove (event) {
console.debug('Drag move event:', event)
const newParent = event.relatedContext?.element?.parent
if (newParent) {
this.$set(this.localMoveState, 'newParent', newParent)
}
},
onEnd (event) {
console.debug('Drag end event:', event)
this.$set(this.localMoveState, 'moving', false)
this.validateMove(event.newIndex)
},
validateMove (newIndex) {
const widget = this.localMoveState.widget
const parentWidget = this.findParent(widget, this.localSitemap)
const newList = parentWidget.slots.widgets
console.debug('New parent:', parentWidget)
if (!this.allowedWidgetTypes(parentWidget, newIndex).map(wt => wt.type).includes(widget.component)) {
this.$f7.toast.create({
text: 'Move invalid',
destroyOnClose: true,
closeTimeout: 2000
}).open()
console.debug('Move invalid, restore in original position:', this.localMoveState)
newList.splice(newIndex, 1)
this.localMoveState.oldList.splice(this.localMoveState.oldIndex, 0, widget)
} else {
console.debug('Move valid')
const parentWidget = this.localMoveState.newParent
if (widget && parentWidget) {
this.$set(widget, 'parent', parentWidget)
}
this.$set(this.localMoveState, 'moving', false)
this.$set(this.localMoveState, 'widget', null)
this.$set(this.localMoveState, 'newParent', null)
},
findParent (widget, parentWidget) {
if (parentWidget.slots?.widgets) {
for (const w of parentWidget.slots.widgets) {
if (fastDeepEqual(widget, w)) {
return parentWidget
} else {
const parent = this.findParent(widget, w)
if (parent != null) return parent
}
}
dropAllowed (widget) {
if (!this.canAddChildren(widget)) return false
if (!this.localMoveState.widget || this.allowedWidgetTypes(widget).map(wt => wt.type).includes(this.localMoveState.widget.component)) {
return true
}
return null
return false
},
setWidgetClosed (closed) {
this.$set(this.widget, 'closed', closed)

View File

@ -86,7 +86,7 @@
<div><f7-block-title>Icon Color</f7-block-title></div>
<attribute-details :widget="selectedWidget" attribute="iconcolor" placeholder="item_name operator value = color" />
</f7-block>
<f7-block v-if="selectedWidget && canAddChildren && selectedWidget.component !== 'Buttongrid'">
<f7-block v-if="selectedWidget && canAddChildren(selectedWidget) && selectedWidget.component !== 'Buttongrid'">
<div><f7-block-title>Add Child Widget</f7-block-title></div>
<f7-card>
<f7-card-content>
@ -96,7 +96,7 @@
</f7-card-content>
</f7-card>
</f7-block>
<f7-block v-if="selectedWidget && canAddChildren && selectedWidget.component === 'Buttongrid'">
<f7-block v-if="selectedWidget && canAddChildren(selectedWidget) && selectedWidget.component === 'Buttongrid'">
<div><f7-block-title>Add Button Widget</f7-block-title></div>
<f7-card>
<f7-card-content>
@ -126,11 +126,11 @@
</f7-tab>
</f7-tabs>
<f7-fab class="add-to-sitemap-fab" v-if="canAddChildren && selectedWidget.component !== 'Buttongrid'" position="right-center" slot="fixed" color="blue" @click="$refs.widgetTypeSelection.open()">
<f7-fab class="add-to-sitemap-fab" v-if="canAddChildren(selectedWidget) && selectedWidget.component !== 'Buttongrid'" position="right-center" slot="fixed" color="blue" @click="$refs.widgetTypeSelection.open()">
<f7-icon ios="f7:plus" md="material:add" aurora="f7:plus" />
<f7-icon ios="f7:multiply" md="material:close" aurora="f7:multiply" />
</f7-fab>
<f7-fab class="add-to-sitemap-fab" v-if="canAddChildren && selectedWidget.component === 'Buttongrid'" position="right-center" slot="fixed" color="blue" @click="addWidget('Button')">
<f7-fab class="add-to-sitemap-fab" v-if="canAddChildren(selectedWidget) && selectedWidget.component === 'Buttongrid'" position="right-center" slot="fixed" color="blue" @click="addWidget('Button')">
<f7-icon ios="f7:plus" md="material:add" aurora="f7:plus" />
<f7-icon ios="f7:multiply" md="material:close" aurora="f7:multiply" />
</f7-fab>
@ -290,6 +290,7 @@ import AttributeDetails from '@/components/pagedesigner/sitemap/attribute-detail
import SitemapTreeviewItem from '@/components/pagedesigner/sitemap/treeview-item.vue'
import SitemapMixin from '@/components/pagedesigner/sitemap/sitemap-mixin'
import DirtyMixin from '@/pages/settings/dirty-mixin'
import fastDeepEqual from 'fast-deep-equal/es6'
export default {
mixins: [DirtyMixin, SitemapMixin],
@ -315,6 +316,7 @@ export default {
tags: [],
slots: { widgets: [] }
},
lastCleanSitemap: null,
selectedWidget: null,
selectedWidgetParent: null,
previousSelection: null,
@ -329,14 +331,6 @@ export default {
if (!this.selectedWidget) return false
return (Array.isArray(this.selectedWidget.slots?.widgets) && this.selectedWidget.slots.widgets.length)
},
canAddChildren () {
if (!this.selectedWidget) return false
if (this.selectedWidget.component === 'Buttongrid') {
const buttons = this.selectedWidget.config.buttons
if (Array.isArray(buttons) && buttons.length) return false
}
return this.LINKABLE_WIDGET_TYPES.includes(this.selectedWidget.component)
},
canShowValue () {
if (!this.selectedWidget) return false
return this.WIDGET_TYPES_SHOWING_VALUE.includes(this.selectedWidget.component)
@ -348,9 +342,12 @@ export default {
},
watch: {
sitemap: {
handler: function () {
if (!this.loading) {
handler (newVal) {
if (this.loading) return
if (!fastDeepEqual(this.stripClosed(newVal), this.lastCleanSitemap)) {
this.dirty = true
} else {
this.dirty = false
}
},
deep: true
@ -381,6 +378,29 @@ export default {
ev.preventDefault()
}
},
stripClosed (obj) {
// Remove the closed field as it is only used for expanding the tree, and should not impact the dirty state
if (Array.isArray(obj)) {
return obj.map(this.stripClosed)
} else if (obj !== null && typeof obj === 'object') {
// also exclude parent to avoid recursive copies
const { parent, closed, ...rest } = obj
const result = {}
for (const key in rest) {
result[key] = this.stripClosed(rest[key])
}
return result
} else {
return obj
}
},
setParents (widget) {
// keep parents with widget for drag and drop
widget.slots?.widgets?.forEach((w) => {
this.$set(w, 'parent', widget)
this.setParents(w)
})
},
load () {
if (this.loading) return
this.loading = true
@ -388,6 +408,7 @@ export default {
if (this.ready && this.dirty) this.save(true, true)
if (this.createMode) {
this.$set(this, 'lastCleanSitemap', this.stripClosed(this.sitemap))
this.loading = false
this.ready = true
} else {
@ -395,6 +416,8 @@ export default {
const sitemap = this.preProcessSitemapLoad(data)
this.$set(this, 'sitemap', sitemap)
this.$nextTick(() => {
this.$set(this, 'lastCleanSitemap', this.stripClosed(this.sitemap))
this.setParents(this.sitemap)
this.ready = true
this.loading = false
})
@ -444,6 +467,8 @@ export default {
destroyOnClose: true,
closeTimeout: 2000
}).open()
this.$set(this, 'lastCleanSitemap', this.stripClosed(this.sitemap))
this.setParents(sitemap)
}
this.$f7.emit('sidebarRefresh', null)
// if (!stay) this.$f7router.back()