import type { CSSProperties, Component, Prop, VNodeChild, } from 'vue' import { defineComponent, h } from 'vue' import type { Axis, Block, DataRefer, Theme, View, ViewChildren, Widget, } from './types' import { provideBlockId } from './context' import { align, background, bordering, clip, gap, insets, radii, shadowing, size, unit, } from './utils' import DddView from './views/View.vue' import DddTextView from './views/TextView.vue' import DddImageView from './views/ImageView.vue' import DddAudioView from './views/AudioView.vue' /** * 相关框架组件渲染函数 */ export type WidgetRenderFunction = (widget: Widget, view: View, axis?: Axis) => VNodeChild /** * 注册的外部组件渲染函数 */ const widgets: Record = {} const views: Record = { text: DddTextView, image: DddImageView, audio: DddAudioView, video: DddAudioView, } export function registerWidget(some: Record): void { for (const [key, func] of Object.entries(some)) widgets[key] = func } function renderWidget(widget: Widget, view: View, axis?: Axis): VNodeChild { const func = widgets[widget.name] if (typeof func !== 'function') throw new Error(`unknown widget: ${widget.name}`) return func(widget, view, axis) } const isWidget = (s: any): s is Widget => s != null && 'name' in s function isRefer(s: any): s is DataRefer { return s != null && 'type' in s && 'key' in s && typeof s.type === 'string' && typeof s.key === 'string' } function renderRefer(refer: DataRefer, axis?: Axis): VNodeChild { const { key, type, ...attrs } = refer const component = views[type] if (component) { return h(component, { source: key, axis, ...attrs, }) } return h('b', { style: { color: 'red', fontSize: '36px', }, }, 'unimplemented') } export function isFlexible(theme: Theme) { return theme.axis != null || theme.mainAlign != null || theme.crossAlign != null || theme.wrap != null || theme.gap != null } export function render(view?: ViewChildren, axis?: Axis): VNodeChild { if (view == null) return null if (typeof view === 'string' || typeof view === 'number') return view if (Array.isArray(view)) return view.map(v => render(v, axis)) if (isRefer(view)) return renderRefer(view) if (isWidget(view.theme)) return renderWidget(view.theme, view, axis) const theme = view.theme const css: CSSProperties = { // boxed & spatial ...insets('padding', theme?.padding), ...insets('margin', theme?.margin), ...size(axis, theme), // decoration ...background(theme?.color, theme?.image, theme?.gradient), ...bordering(theme?.border), ...radii(theme?.radius), ...shadowing(theme?.shadow), // textual color: theme?.textColor, fontSize: unit(theme?.fontSize), textAlign: theme?.textAlign, lineHeight: theme?.lineHeight, // clip ...clip(theme?.clip), } // note: 在前面初始化 css 时使用了参数 axis, // 所以在这里复用它用于子视图构建 axis = theme?.axis if (theme?.flexible || (theme && theme.flexible == null && isFlexible(theme))) { Object.assign(css, { display: 'flex', flexDirection: axis === 'y' ? 'column' : undefined, justifyContent: align(theme?.mainAlign), alignItems: align(theme?.crossAlign), flexWrap: theme?.wrap ? 'wrap' : undefined, ...gap(theme?.gap), }) } return h(DddView, { vid: view.vid, class: 'ddd-view', style: css, }, () => render(view.children, axis)) } export const RenderBlock = defineComponent({ name: 'RenderWidget', props: { block: { type: Object, required: true, } as Prop, }, setup(props) { provideBlockId(props.block!.vid) return () => { if (props.block == null) return null return render(props.block) } }, })