Tabbed pages: Show config in code tab & Enable expressions for title and icon (#2591)

Makes the tabbed page config editable though the code tab: Resolves #1276. Resolves #2103.
Enables expression evaluation for tab title and icon: Resolves #2571.

---------

Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
pull/2592/head
Florian Hotze 2024-05-27 20:18:00 +02:00 committed by GitHub
parent df8ef84ff0
commit 4c34b611ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 143 additions and 109 deletions

View File

@ -0,0 +1,119 @@
import expr from 'jse-eval'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import calendar from 'dayjs/plugin/calendar'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import isoWeek from 'dayjs/plugin/isoWeek'
import isToday from 'dayjs/plugin/isToday'
import isYesterday from 'dayjs/plugin/isYesterday'
import isTomorrow from 'dayjs/plugin/isTomorrow'
import store from '@/js/store'
import jsepRegex from '@jsep-plugin/regex'
import jsepArrow from '@jsep-plugin/arrow'
import jsepObject from '@jsep-plugin/object'
import jsepTemplate from '@jsep-plugin/template'
expr.jsep.plugins.register(jsepRegex, jsepArrow, jsepObject, jsepTemplate)
expr.addUnaryOp('@', (itemName) => {
if (itemName === undefined) return undefined
const item = store.getters.trackedItems[itemName]
return (item.displayState !== undefined) ? item.displayState : item.state
})
expr.addUnaryOp('@@', (itemName) => {
if (itemName === undefined) return undefined
return store.getters.trackedItems[itemName].state
})
expr.addUnaryOp('#', (itemName) => {
if (itemName === undefined) return undefined
return store.getters.trackedItems[itemName].numericState
})
dayjs.extend(relativeTime)
dayjs.extend(calendar)
dayjs.extend(localizedFormat)
dayjs.extend(isoWeek)
dayjs.extend(isToday)
dayjs.extend(isYesterday)
dayjs.extend(isTomorrow)
export default {
data () {
return {
exprAst: {}
}
},
methods: {
/**
* Evaluates a widget expression.
* May read <code>this.context</code> and <code>this.props</code> (see below).
*
* @param {string} key the key of the expression (used for abstract syntax tree caching)
* @param {string} value the expression to evaluate
* @param {object} [context] the context to evaluate the expression in (defaults to <code>this.context</code>)
* @param {object} [props] the props to make available to the expression (defaults to <code>this.props</code>)
* @returns {*} the result of the expression evaluation
*/
evaluateExpression (key, value, context, props) {
if (value === null) return null
const ctx = context || this.context
if (typeof value === 'string' && value.startsWith('=')) {
try {
// we cache the parsed abstract tree to prevent it from being parsed again at runtime
// in we're edit mode according to the context do not cache because the expression is subject to change
if (!this.exprAst[key] || ctx.editmode) {
this.exprAst[key] = expr.parse(value.substring(1))
}
return expr.evaluate(this.exprAst[key], {
items: ctx.store,
props: props || this.props,
config: ctx.component.config,
vars: ctx.vars,
loop: ctx.loop,
Math: Math,
Number: Number,
theme: this.$theme,
themeOptions: this.$f7.data.themeOptions,
device: this.$device,
screen: this.getScreenInfo(),
JSON: JSON,
dayjs: dayjs,
user: this.$store.getters.user
})
} catch (e) {
return e
}
} else if (typeof value === 'object' && !Array.isArray(value)) {
const evalObj = {}
for (const objKey in value) {
this.$set(evalObj, objKey, this.evaluateExpression(key + '.' + objKey, value[objKey]))
}
return evalObj
} else if (typeof value === 'object' && Array.isArray(value)) {
const evalArr = []
for (let i = 0; i < value.length; i++) {
this.$set(evalArr, i, this.evaluateExpression(key + '.' + i, value[i]))
}
return evalArr
} else {
return value
}
},
getScreenInfo () {
const pageCurrent = document.getElementsByClassName('page-current').item(0)
const pageContent = pageCurrent.getElementsByClassName('page-content').item(0)
const pageContentStyle = window.getComputedStyle(pageContent)
return {
width: window.screen.width,
height: window.screen.height,
availWidth: window.screen.availWidth,
availHeight: window.screen.availHeight,
colorDepth: window.screen.colorDepth,
pixelDepth: window.screen.pixelDepth,
viewAreaWidth: pageContent.clientWidth - parseFloat(pageContentStyle.paddingLeft) - parseFloat(pageContentStyle.paddingRight),
viewAreaHeight: pageContent.clientHeight - parseFloat(pageContentStyle.paddingTop) - parseFloat(pageContentStyle.paddingBottom)
}
}
}
}

View File

@ -1,50 +1,13 @@
// Import into widget components as a mixin!
import expr from 'jse-eval'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import calendar from 'dayjs/plugin/calendar'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import isoWeek from 'dayjs/plugin/isoWeek'
import isToday from 'dayjs/plugin/isToday'
import isYesterday from 'dayjs/plugin/isYesterday'
import isTomorrow from 'dayjs/plugin/isTomorrow'
import scope from 'scope-css'
import store from '@/js/store'
import jsepRegex from '@jsep-plugin/regex'
import jsepArrow from '@jsep-plugin/arrow'
import jsepObject from '@jsep-plugin/object'
import jsepTemplate from '@jsep-plugin/template'
expr.jsep.plugins.register(jsepRegex, jsepArrow, jsepObject, jsepTemplate)
expr.addUnaryOp('@', (itemName) => {
if (itemName === undefined) return undefined
const item = store.getters.trackedItems[itemName]
return (item.displayState !== undefined) ? item.displayState : item.state
})
expr.addUnaryOp('@@', (itemName) => {
if (itemName === undefined) return undefined
return store.getters.trackedItems[itemName].state
})
expr.addUnaryOp('#', (itemName) => {
if (itemName === undefined) return undefined
return store.getters.trackedItems[itemName].numericState
})
dayjs.extend(relativeTime)
dayjs.extend(calendar)
dayjs.extend(localizedFormat)
dayjs.extend(isoWeek)
dayjs.extend(isToday)
dayjs.extend(isYesterday)
dayjs.extend(isTomorrow)
import WidgetExpressionMixin from '@/components/widgets/widget-expression-mixin'
export default {
mixins: [WidgetExpressionMixin],
props: ['context'],
data () {
return {
exprAst: {},
vars: (this.context) ? this.context.vars : {},
widgetVars: {}
}
@ -124,67 +87,6 @@ export default {
}
},
methods: {
evaluateExpression (key, value, context) {
if (value === null) return null
const ctx = context || this.context
if (typeof value === 'string' && value.startsWith('=')) {
try {
// we cache the parsed abstract tree to prevent it from being parsed again at runtime
// in we're edit mode according to the context do not cache because the expression is subject to change
if (!this.exprAst[key] || ctx.editmode) {
this.exprAst[key] = expr.parse(value.substring(1))
}
return expr.evaluate(this.exprAst[key], {
items: ctx.store,
props: this.props,
config: ctx.component.config,
vars: ctx.vars,
loop: ctx.loop,
Math: Math,
Number: Number,
theme: this.$theme,
themeOptions: this.$f7.data.themeOptions,
device: this.$device,
screen: this.getScreenInfo(),
JSON: JSON,
dayjs: dayjs,
user: this.$store.getters.user
})
} catch (e) {
return e
}
} else if (typeof value === 'object' && !Array.isArray(value)) {
const evalObj = {}
for (const objKey in value) {
this.$set(evalObj, objKey, this.evaluateExpression(key + '.' + objKey, value[objKey]))
}
return evalObj
} else if (typeof value === 'object' && Array.isArray(value)) {
const evalArr = []
for (let i = 0; i < value.length; i++) {
this.$set(evalArr, i, this.evaluateExpression(key + '.' + i, value[i]))
}
return evalArr
} else {
return value
}
},
getScreenInfo () {
const pageCurrent = document.getElementsByClassName('page-current').item(0)
const pageContent = pageCurrent.getElementsByClassName('page-content').item(0)
const pageContentStyle = window.getComputedStyle(pageContent)
return {
width: window.screen.width,
height: window.screen.height,
availWidth: window.screen.availWidth,
availHeight: window.screen.availHeight,
colorDepth: window.screen.colorDepth,
pixelDepth: window.screen.pixelDepth,
viewAreaWidth: pageContent.clientWidth - parseFloat(pageContentStyle.paddingLeft) - parseFloat(pageContentStyle.paddingRight),
viewAreaHeight: pageContent.clientHeight - parseFloat(pageContentStyle.paddingTop) - parseFloat(pageContentStyle.paddingBottom)
}
},
childContext (component) {
return {
component: component,

View File

@ -16,10 +16,11 @@
<f7-link v-if="!page.config.hideSidebarIcon" class="sidebar-icon" icon-ios="f7:menu" icon-aurora="f7:menu" icon-md="material:menu" panel-open="left" />
<f7-link v-if="fullscreenIcon" class="fullscreen-icon" :icon-f7="fullscreenIcon" @click="toggleFullscreen" />
</template>
<f7-toolbar tabbar labels bottom v-if="page && pageType === 'tabs' && visibleToCurrentUser">
<f7-link v-for="(tab, idx) in page.slots.default" :key="idx" tab-link @click="onTabChange(idx)" :tab-link-active="currentTab === idx" :icon-ios="tab.config.icon" :icon-md="tab.config.icon" :icon-aurora="tab.config.icon" :text="tab.config.title" />
</f7-toolbar>
<!-- Tabbed Pages -->
<f7-toolbar tabbar labels bottom v-if="page && pageType === 'tabs' && visibleToCurrentUser">
<f7-link v-for="(tab, idx) in page.slots.default" :key="idx" tab-link @click="onTabChange(idx)" :tab-link-active="currentTab === idx" :icon-ios="tabEvaluateExpression(tab, idx, 'icon')" :icon-md="tabEvaluateExpression(tab, idx, 'icon')" :icon-aurora="tabEvaluateExpression(tab, idx, 'icon')" :text="tabEvaluateExpression(tab, idx, 'title')" />
</f7-toolbar>
<f7-tabs v-if="page && pageType === 'tabs' && visibleToCurrentUser">
<f7-tab v-for="(tab, idx) in page.slots.default" :key="idx" :tab-active="currentTab === idx">
<component v-if="currentTab === idx" :is="tabComponent(tab)" :context="tabContext(tab)" @command="onCommand" />
@ -47,8 +48,10 @@
<script>
import OhLayoutPage from '@/components/widgets/layout/oh-layout-page.vue'
import WidgetExpressionMixin from '@/components/widgets/widget-expression-mixin'
export default {
mixins: [WidgetExpressionMixin],
components: {
'oh-layout-page': OhLayoutPage,
'empty-state-placeholder': () => import('@/components/empty-state-placeholder.vue'),
@ -146,6 +149,10 @@ export default {
const page = this.$store.getters.page(tab.config.page.replace('page:', ''))
return page.component
},
tabEvaluateExpression (tab, idx, key) {
const ctx = this.tabContext(tab)
return this.evaluateExpression('tab-' + idx + '-' + key, tab.config[key], ctx, ctx.props)
},
toggleFullscreen () {
this.$fullscreen.toggle(document.body, {
wrap: false,

View File

@ -39,9 +39,9 @@
<f7-list media-list class="tabs-list">
<f7-list-item media-item v-for="(tab, idx) in page.slots.default" :key="idx"
:title="tab.config.title" :subtitle="tab.config.page"
:title="tabEvaluateExpression(tab, idx, 'title')" :subtitle="tab.config.page"
link="#" @click.native="(ev) => configureTab(ev, tab, context)">
<f7-icon slot="media" :ios="tab.config.icon" :md="tab.config.icon" :aurora="tab.config.icon" color="gray" :size="32" />
<f7-icon slot="media" :ios="tabEvaluateExpression(tab, idx, 'icon')" :md="tabEvaluateExpression(tab, idx, 'icon')" :aurora="tabEvaluateExpression(tab, idx, 'icon')" color="gray" :size="32" />
<f7-menu slot="content-start" class="configure-layout-menu">
<f7-menu-item icon-f7="list_bullet" dropdown>
<f7-menu-dropdown>
@ -91,7 +91,8 @@
</style>
<script>
import PageDesigner from '../pagedesigner-mixin'
import PageDesignerMixin from '@/pages/settings/pages/pagedesigner-mixin'
import WidgetExpressionMixin from '@/components/widgets/widget-expression-mixin'
import YAML from 'yaml'
@ -102,7 +103,7 @@ import PageSettings from '@/components/pagedesigner/page-settings.vue'
const ConfigurableWidgets = { OhTabDefinition }
export default {
mixins: [PageDesigner],
mixins: [PageDesignerMixin, WidgetExpressionMixin],
components: {
'editor': () => import(/* webpackChunkName: "script-editor" */ '@/components/config/controls/script-editor.vue'),
PageSettings
@ -150,15 +151,20 @@ export default {
}
this.context.editmode.configureWidget(tab, context)
},
tabEvaluateExpression (tab, idx, key) {
return this.evaluateExpression('tab-' + idx + '-' + key, tab.config[key], this.context, tab.config.pageConfig)
},
toYaml () {
this.pageYaml = YAML.stringify({
config: this.page.config,
tabs: this.page.slots.default
})
},
fromYaml () {
try {
const updatedTabs = YAML.parse(this.pageYaml)
this.$set(this.page.slots, 'default', updatedTabs.tabs)
const updatedPage = YAML.parse(this.pageYaml)
this.$set(this.page, 'config', updatedPage.config)
this.$set(this.page.slots, 'default', updatedPage.tabs)
this.forceUpdate()
return true
} catch (e) {