Skip to content

Commit d51d306

Browse files
authored
Refactor widget-expression-mixin to TS composable (#3462)
Signed-off-by: Florian Hotze <[email protected]>
1 parent 8dc7696 commit d51d306

File tree

16 files changed

+404
-202
lines changed

16 files changed

+404
-202
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
diff --git a/node_modules/jse-eval/package.json b/node_modules/jse-eval/package.json
2+
index 5149f84..e5c5193 100644
3+
--- a/node_modules/jse-eval/package.json
4+
+++ b/node_modules/jse-eval/package.json
5+
@@ -7,7 +7,10 @@
6+
"main": "dist/jse-eval.cjs",
7+
"module": "dist/jse-eval.module.js",
8+
"exports": {
9+
- "import": "./dist/jse-eval.modern.js",
10+
+ "import": {
11+
+ "default": "./dist/jse-eval.modern.js",
12+
+ "types": "./dist/index.d.ts"
13+
+ },
14+
"require": "./dist/jse-eval.cjs"
15+
},
16+
"unpkg": "dist/jse-eval.umd.js",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { TrackedItems } from '@/js/stores/useStatesStore'
2+
3+
type WidgetConfig = any
4+
type WidgetProps = Record<string, any>
5+
type WidgetPropDefinitions = any
6+
7+
interface WidgetComponent {
8+
component?: string,
9+
config?: WidgetConfig,
10+
props?: WidgetPropDefinitions,
11+
slots?: Record<string, Array<any>>
12+
}
13+
14+
interface WidgetContext {
15+
component: WidgetComponent,
16+
config?: WidgetConfig,
17+
/**
18+
* oh-context constants
19+
*/
20+
const?: Record<string, any>,
21+
ctxVars?: any,
22+
editmode?: boolean,
23+
/**
24+
* oh-context functions
25+
*/
26+
fn?: Record<string, function>,
27+
/**
28+
* oh-repeater loop variables
29+
*/
30+
loop?: Record<string, any>,
31+
props: WidgetProps,
32+
store: TrackedItems,
33+
/**
34+
* variable.ts variable scope
35+
*/
36+
varScope?: string,
37+
vars?: any
38+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { computed, getCurrentInstance, inject, type Ref } from 'vue'
2+
import { theme } from 'framework7-vue'
3+
4+
import expr, { addUnaryOp, evaluate, parse } from 'jse-eval'
5+
import dayjs from 'dayjs'
6+
import relativeTime from 'dayjs/plugin/relativeTime'
7+
import calendar from 'dayjs/plugin/calendar'
8+
import localizedFormat from 'dayjs/plugin/localizedFormat'
9+
import isoWeek from 'dayjs/plugin/isoWeek'
10+
import isToday from 'dayjs/plugin/isToday'
11+
import isYesterday from 'dayjs/plugin/isYesterday'
12+
import isTomorrow from 'dayjs/plugin/isTomorrow'
13+
14+
import jsepRegex from '@jsep-plugin/regex'
15+
import jsepArrow from '@jsep-plugin/arrow'
16+
import jsepObject from '@jsep-plugin/object'
17+
import jsepTemplate from '@jsep-plugin/template'
18+
19+
import { useUIOptionsStore } from '@/js/stores/useUIOptionsStore'
20+
import { useStatesStore } from '@/js/stores/useStatesStore'
21+
import { useUserStore } from '@/js/stores/useUserStore'
22+
import { i18n } from '@/js/i18n.ts'
23+
24+
import type { WidgetContext, WidgetProps } from './types'
25+
26+
expr.jsep.plugins.register(jsepRegex, jsepArrow, jsepObject, jsepTemplate)
27+
28+
addUnaryOp('@', (itemName: string | undefined): string => {
29+
if (itemName === undefined) return '-'
30+
const item = useStatesStore().trackedItems[itemName]
31+
return (item.displayState !== undefined ? item.displayState : item.state) ?? '-'
32+
})
33+
addUnaryOp('@@', (itemName: string | undefined): string => {
34+
if (itemName === undefined) return '-'
35+
return useStatesStore().trackedItems[itemName]?.state ?? '-'
36+
})
37+
addUnaryOp('#', (itemName: string | undefined): number | undefined => {
38+
if (itemName === undefined) return undefined
39+
return useStatesStore().trackedItems[itemName]?.numericState
40+
})
41+
42+
dayjs.extend(relativeTime)
43+
dayjs.extend(calendar)
44+
dayjs.extend(localizedFormat)
45+
dayjs.extend(isoWeek)
46+
dayjs.extend(isToday)
47+
dayjs.extend(isYesterday)
48+
dayjs.extend(isTomorrow)
49+
50+
interface ExpressionAstCache {
51+
[key: string]: any
52+
}
53+
54+
interface ScreenInfo {
55+
width: number
56+
height: number
57+
availWidth: number
58+
availHeight: number
59+
colorDepth: number
60+
pixelDepth: number
61+
viewAreaWidth: number | null
62+
viewAreaHeight: number | null
63+
appWidth: number
64+
appHeight: number
65+
}
66+
67+
/**
68+
* Composable providing the functionality to evaluate widget expressions.
69+
*
70+
* The `screen.viewAreaWidth` and `screen.viewAreaHeight` properties are only available to expressions
71+
* if `viewAreaWidth` and `viewAreaHeight` refs are provided via Vue's dependency injection mechanism.
72+
*
73+
* Widget expression evaluations need access to the current widget context and props.
74+
* If they are available at composable instantiation, they can be passed as properties to the composable.
75+
* If they, however, aren't available at instantiation (e.g. because they are computed), they can be passed as function parameters later.
76+
*
77+
* @param properties
78+
*/
79+
export function useWidgetExpression (properties: { context?: WidgetContext, props?: WidgetProps } = {}) {
80+
// imports
81+
const userStore = useUserStore()
82+
const uiOptionsStore = useUIOptionsStore()
83+
84+
const instance = getCurrentInstance()
85+
const global = instance?.appContext.config.globalProperties
86+
87+
// data
88+
const exprAst: ExpressionAstCache = {}
89+
const viewAreaWidth = inject('viewAreaWidth', null) as Ref<number> | null
90+
const viewAreaHeight = inject('viewAreaHeight', null) as Ref<number> | null
91+
92+
// computed
93+
const appWidth = computed(() => global?.$f7dim.width ?? 0)
94+
const appHeight = computed(() => global?.$f7dim.height ?? 0)
95+
96+
const screenInfo = computed<ScreenInfo>(() => {
97+
return {
98+
width: window.screen.width,
99+
height: window.screen.height,
100+
availWidth: window.screen.availWidth,
101+
availHeight: window.screen.availHeight,
102+
colorDepth: window.screen.colorDepth,
103+
pixelDepth: window.screen.pixelDepth,
104+
viewAreaWidth: viewAreaWidth != null ? viewAreaWidth.value : null,
105+
viewAreaHeight: viewAreaHeight != null ? viewAreaHeight.value : null,
106+
appWidth: appWidth.value,
107+
appHeight: appHeight.value
108+
}
109+
})
110+
111+
// methods
112+
function getAllVars (context: WidgetContext): Record<string, any> {
113+
const vars: Record<string, any> = {}
114+
if (context.vars) {
115+
for (const varKey in context.vars) {
116+
vars[varKey] = context.vars[varKey]
117+
}
118+
}
119+
if (context.varScope) {
120+
const scopeIDs = context.varScope.split('-')
121+
for (let scope_idx = 1; scope_idx < scopeIDs.length; scope_idx++) {
122+
const scopeKey = scopeIDs.slice(0, scope_idx + 1).join('-')
123+
if (context.ctxVars?.[scopeKey]) {
124+
for (const varKey in context.ctxVars[scopeKey]) {
125+
vars[varKey] = context.ctxVars[scopeKey][varKey]
126+
}
127+
}
128+
}
129+
}
130+
return vars
131+
}
132+
133+
/**
134+
* Evaluates a widget expression.
135+
* If widget context and props were not passed to the composable at instantiation, they have to be passed to the function.
136+
*
137+
* @param key the key of the expression (used for abstract syntax tree caching)
138+
* @param value the expression to evaluate
139+
* @param context the context to evaluate the expression in (not required if already provided as composable property)
140+
* @param props the props to make available to the expression (not required if already provided as composable property)
141+
* @returns the result of the expression evaluation
142+
*/
143+
function evaluateExpression (key: string, value: any, context?: WidgetContext, props?: WidgetProps): any {
144+
if (value === null) return null
145+
const ctx = context || properties.context
146+
if (!ctx) return null
147+
if (typeof value === 'string' && value.startsWith('=')) {
148+
try {
149+
// we cache the parsed abstract tree to prevent it from being parsed again at runtime
150+
// if we are in edit-mode according to the context, do not cache because the expression is subject to change
151+
if (!exprAst[key] || ctx.editmode) {
152+
exprAst[key] = parse(value.substring(1))
153+
}
154+
return evaluate(exprAst[key], {
155+
items: ctx.store,
156+
props: props || properties?.props,
157+
config: ctx.component?.config,
158+
fn: ctx.fn,
159+
const: ctx.const,
160+
vars: getAllVars(ctx),
161+
loop: ctx.loop,
162+
Math,
163+
Number,
164+
theme,
165+
themeOptions: uiOptionsStore.themeOptions(),
166+
device: global?.$device,
167+
screen: screenInfo.value,
168+
JSON,
169+
dayjs,
170+
user: userStore.user,
171+
translation: i18n.global.t,
172+
t: i18n.global.t
173+
})
174+
} catch (e) {
175+
return e
176+
}
177+
} else if (typeof value === 'object' && !Array.isArray(value)) {
178+
const evalObj: Record<string, any> = {}
179+
for (const objKey in value) {
180+
evalObj[objKey] = evaluateExpression(key + '.' + objKey, value[objKey], ctx, props || properties?.props)
181+
}
182+
return evalObj
183+
} else if (typeof value === 'object' && Array.isArray(value)) {
184+
const evalArr: any[] = []
185+
for (let i = 0; i < value.length; i++) {
186+
evalArr[i] = evaluateExpression(key + '.' + i, value[i], ctx, props || properties?.props)
187+
}
188+
return evalArr
189+
} else {
190+
return value
191+
}
192+
}
193+
194+
return {
195+
evaluateExpression
196+
}
197+
}

0 commit comments

Comments
 (0)