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
parent
df8ef84ff0
commit
4c34b611ed
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue