import * as flexui from '@openflexui/core'
import { StyleIsolation } from './types'
import { guid } from './utils'
import { TraitBehavior } from './behavior'
import { BehaviorBuilder, ComponentBuilder } from './builder'
import type { FrontendEnv } from '.'
import type {
ComponentStaticConfig,
ComponentDefinitionOptions,
BehaviorDefinition,
ComponentDefinition,
PageDefinition,
utils as typeUtils,
} from './types'
import type { DefaultBehaviorBuilder, DefaultComponentBuilder } from './builder'
import type { Behavior, ComponentType } from './behavior'
import type { Component } from './component'
export interface PageConstructor {
<TData extends DataList, TNewExtraFields extends { [k: PropertyKey]: any }>(
definition: PageDefinition<TData, TNewExtraFields> &
ThisType<
Component<
TData,
Empty,
TNewExtraFields,
never,
Empty
>
>,
): ComponentType<
TData,
Empty,
TNewExtraFields,
never,
Empty
>
}
export interface ComponentConstructor {
(): DefaultComponentBuilder
<
TData extends DataList,
TProperty extends PropertyList,
TMethod extends MethodList,
TComponentExport,
>(
definition: ComponentDefinition<TData, TProperty, TMethod, TComponentExport> &
ThisType<
Component<
TData,
TProperty,
TMethod,
TComponentExport,
Empty
>
>,
): ComponentType<
TData,
TProperty,
TMethod,
TComponentExport,
Empty
>
}
export interface BehaviorConstructor {
(): DefaultBehaviorBuilder
<
TData extends DataList,
TProperty extends PropertyList,
TMethod extends MethodList,
TComponentExport,
>(
definition: BehaviorDefinition<TData, TProperty, TMethod, TComponentExport> &
ThisType<
Component<
TData,
TProperty,
TMethod,
TComponentExport,
Empty
>
>,
): Behavior<
TData,
TProperty,
TMethod,
never,
TComponentExport,
Empty
>
trait<TIn extends { [key: string]: any }>(): TraitBehavior<TIn, TIn>
trait<TIn extends { [key: string]: any }, TOut extends { [key: string]: any }>(
trans: (impl: TIn) => TOut,
): TraitBehavior<TIn, TOut>
}
export interface ComponentEnv {
Page: PageConstructor
Component: ComponentConstructor
Behavior: BehaviorConstructor
}
type Empty = typeUtils.Empty
type DataList = typeUtils.DataList
type PropertyList = typeUtils.PropertyList
type MethodList = typeUtils.MethodList
* A component space with FlexUI code manager
*/
export class CodeSpace {
_$env: FrontendEnv
private _$isMainSpace: boolean
private space: flexui.ComponentSpace
private styleScopeManager: flexui.StyleScopeManager
private staticConfigMap: { [path: string]: ComponentStaticConfig }
private styleSheetMap: {
[path: string]: { url: string | undefined; styleScopeName: string | undefined }
}
private compiledTemplateMap: { [path: string]: flexui.template.ComponentTemplate }
private waitingAliasMap: { [is: string]: string[] }
_$sharedStyleScope: flexui.StyleScopeId
_$styleIsolationMap: { [path: string]: StyleIsolation }
_$overallBehavior: flexui.GeneralBehavior | null = null
constructor(
env: FrontendEnv,
isMainSpace: boolean,
publicComponents: { [alias: string]: string },
globalCodeSpace?: CodeSpace,
) {
this._$env = env
this._$isMainSpace = isMainSpace
this.space = new flexui.ComponentSpace(
'',
globalCodeSpace?.space,
globalCodeSpace?.space.styleScopeManager,
)
;(this.space as any).__hasCodeSpace = this
this.styleScopeManager = this.space.styleScopeManager
this.staticConfigMap = Object.create(null) as { [path: string]: ComponentStaticConfig }
this.styleSheetMap = Object.create(null) as {
[path: string]: { url: string; styleScopeName: string }
}
this.compiledTemplateMap = Object.create(null) as {
[path: string]: flexui.template.ComponentTemplate
}
this.waitingAliasMap = Object.create(null) as { [path: string]: string[] }
Object.keys(publicComponents).forEach((alias) => {
const is = publicComponents[alias]!
if (this.waitingAliasMap[is]) this.waitingAliasMap[is]!.push(alias)
else this.waitingAliasMap[is] = [alias]
})
this._$sharedStyleScope = this.styleScopeManager.register('')
this.space.setSharedStyleScope(this._$sharedStyleScope)
this._$styleIsolationMap = Object.create(null) as { [path: string]: StyleIsolation }
}
isMainSpace(): boolean {
return this._$isMainSpace
}
* Get the underlying component space
*/
getComponentSpace(): flexui.ComponentSpace {
return this.space
}
* Add a behavior for all components
*
* Must be called before any component registration so that the component will use this behavior.
* Set to `null` to disable this behavior.
*/
setOverallBehavior(behavior: flexui.GeneralBehavior) {
this._$overallBehavior = behavior
}
* Import another component space as a plugin (must be in the same environment)
*
* Components from the imported `codeSpace` can be used with URLs.
* If `domainAlias` is provided, the URL format is `plugin://DOMAIN_ALIAS/COMPONENT_SPACE_EXPORT_ALIAS` .
* If `privateImport` is enabled, another URL format is `plugin-private://DOMAIN/COMPONENT_IS` .
*/
importCodeSpace(id: string, domainAlias?: string, privateImport = false) {
const cs = this._$env.getCodeSpace(id)
if (!cs) {
throw new Error(`There is no space "${id}" in the environment`)
}
if (domainAlias) this.space.importSpace(`plugin://${domainAlias}`, cs.space, false)
if (privateImport) this.space.importSpace(`plugin-private://${id}`, cs.space, true)
}
* Add a style sheet URL with style scope name
*
* The URL should be recognized by the target backend.
* The `path` should not contain the `.hjss` suffix.
*/
addStyleSheet(path: string, styleSheetUrl?: string, styleScopeName?: string) {
this.styleSheetMap[path] = { url: styleSheetUrl, styleScopeName }
}
* Get a style sheet URL
*
* The URL should be recognized by the target backend.
* The `path` should not contain the `.hjss` suffix.
*/
getStyleSheet(path: string): string | undefined {
return this.styleSheetMap[path]?.url
}
getStyleScopeName(path: string): string | undefined {
return this.styleSheetMap[path]?.styleScopeName
}
* Add a compiled template
*
* The content is the execution result of the generated string of the template compiler.
* The `path` should not contain the `.hxml` suffix.
*/
addCompiledTemplate(path: string, content: flexui.template.ComponentTemplate) {
this.compiledTemplateMap[path] = content
}
* Get a compiled template
*
* The content is the execution result of the generated string of the template compiler.
* The `path` should not contain the `.hxml` suffix.
*/
getCompiledTemplate(path: string): flexui.template.ComponentTemplate | undefined {
return this.compiledTemplateMap[path]
}
* Add a static JSON config
*
* The `path` should not contain the `.json` suffix.
*/
addComponentStaticConfig(path: string, content: ComponentStaticConfig) {
this.staticConfigMap[path] = content
}
* Get a static JSON config
*
* The `path` should not contain the `.json` suffix.
*/
getComponentStaticConfig(path: string): ComponentStaticConfig | undefined {
return this.staticConfigMap[path]
}
prepareComponentOptions(
is: string,
options?: ComponentDefinitionOptions,
): [flexui.ComponentOptions, StyleIsolation] {
const staticConfig = this.staticConfigMap[is]
const pureDataPatternString = staticConfig?.pureDataPattern
const pureDataPattern = pureDataPatternString
? new RegExp(pureDataPatternString)
: options?.pureDataPattern
const addGlobalClass = staticConfig?.addGlobalClass
let styleIsolation = staticConfig?.styleIsolation
if (styleIsolation === undefined) {
if (staticConfig?.component) {
styleIsolation = addGlobalClass ? StyleIsolation.ApplyShared : StyleIsolation.Isolated
} else {
styleIsolation = StyleIsolation.Shared
}
}
let styleScope: flexui.StyleScopeId | undefined
let extraStyleScope: flexui.StyleScopeId | undefined
if (
styleIsolation === StyleIsolation.ApplyShared ||
styleIsolation === StyleIsolation.PageApplyShared
) {
styleScope = this.styleScopeManager.register(this.getStyleScopeName(is) || `__${guid()}`)
extraStyleScope = this._$sharedStyleScope
} else if (
(styleIsolation === StyleIsolation.Shared || styleIsolation === StyleIsolation.PageShared) &&
this._$isMainSpace
) {
styleScope = this._$sharedStyleScope
} else {
styleScope = this.styleScopeManager.register(this.getStyleScopeName(is) || `__${guid()}`)
}
const ret: flexui.ComponentOptions = {
idPrefixGenerator: guid,
multipleSlots: options?.multipleSlots,
dynamicSlots: options?.dynamicSlots,
pureDataPattern,
virtualHost: options?.virtualHost,
styleScope,
extraStyleScope,
dataDeepCopy: options?.dataDeepCopy,
propertyPassingDeepCopy: options?.propertyPassingDeepCopy,
propertyEarlyInit: options?.propertyEarlyInit,
propertyComparer: options?.propertyComparer,
}
return [ret, styleIsolation]
}
* Create a component definition environment
*
* During the sync execution of `cb` , `Component` and `Behavior` global vars will be available.
* `globalObject` should be the JavaScript global object, a.k.a. `window` in DOM.
* Some global variables, a.k.a `Component` `Behavior` `Page` ,
* will be written into `globalObject` .
* `path` should be the component path (without ".js" extension).
*/
globalComponentEnv<T>(globalObject: any, path: string, cb: () => T): T {
return this.componentEnv(path, ({ Page, Component, Behavior }) => {
const oldPage = globalObject.Page
const oldComponent = globalObject.Component
const oldBehavior = globalObject.Behavior
globalObject.Page = Page
globalObject.Component = Component
globalObject.Behavior = Behavior
const ret = cb()
globalObject.Page = oldPage
globalObject.Component = oldComponent
globalObject.Behavior = oldBehavior
return ret
})
}
* Create a component definition environment
*
* Like `globalComponentEnv` , but the global variables are given as callback arguments.
*/
componentEnv<T>(path: string, cb: (env: ComponentEnv) => T): T {
const self = this
function pageConstructor<
TData extends DataList,
TNewExtraFields extends { [k: PropertyKey]: any },
>(
definition: PageDefinition<TData, TNewExtraFields> &
ThisType<
Component<
TData,
Empty,
TNewExtraFields,
never,
Empty
>
>,
) {
return self.component(path).pageDefinition(definition).register()
}
function componentConstructor(): DefaultComponentBuilder
function componentConstructor<
TData extends DataList,
TProperty extends PropertyList,
TMethod extends MethodList,
TComponentExport,
>(
definition: ComponentDefinition<TData, TProperty, TMethod, TComponentExport>,
): ComponentType<
TData,
TProperty,
TMethod,
TComponentExport,
Empty
>
function componentConstructor<
TData extends DataList,
TProperty extends PropertyList,
TMethod extends MethodList,
TComponentExport,
>(definition?: ComponentDefinition<TData, TProperty, TMethod, TComponentExport>) {
if (definition !== undefined) {
return self.component(path).definition(definition).register()
}
return self.component(path)
}
function behaviorConstructor(): DefaultBehaviorBuilder
function behaviorConstructor<
TData extends DataList,
TProperty extends PropertyList,
TMethod extends MethodList,
TComponentExport,
>(
definition: BehaviorDefinition<TData, TProperty, TMethod, TComponentExport>,
): Behavior<
TData,
TProperty,
TMethod,
never,
TComponentExport,
Empty
>
function behaviorConstructor<
TData extends DataList,
TProperty extends PropertyList,
TMethod extends MethodList,
TComponentExport,
>(definition?: BehaviorDefinition<TData, TProperty, TMethod, TComponentExport>) {
if (definition !== undefined) {
return self.behavior().definition(definition).register()
}
return self.behavior()
}
behaviorConstructor.trait = self.traitBehavior.bind(self)
return cb({
Page: pageConstructor,
Component: componentConstructor,
Behavior: behaviorConstructor,
})
}
* Build a component outside of a definition environment
*
* The method do not need a definition environment, so the global object pollution is avoided.
* `path` should be the component path (without ".js" extension).
*/
component(path: string): DefaultComponentBuilder {
return ComponentBuilder.create(this, path, this.waitingAliasMap[path])
}
* Build a behavior outside of a definition environment
*
* The method do not need a definition environment, so the global object pollution is avoided.
*/
behavior(): DefaultBehaviorBuilder {
return BehaviorBuilder.create(this)
}
* Define a trait behavior
*/
traitBehavior<TIn extends { [key: string]: any }>(): TraitBehavior<TIn, TIn>
traitBehavior<TIn extends { [key: string]: any }, TOut extends { [key: string]: any }>(
trans: (impl: TIn) => TOut,
): TraitBehavior<TIn, TOut>
traitBehavior<TIn extends { [key: string]: any }, TOut extends { [key: string]: any }>(
trans?: (impl: TIn) => TOut,
): TraitBehavior<TIn, TOut> {
if (trans === undefined) {
return new TraitBehavior(this.space.defineTraitBehavior<TIn>())
}
return new TraitBehavior(this.space.defineTraitBehavior<TIn, TOut>(trans))
}
}