change h5 to web

This commit is contained in:
pengfei.zhou
2019-12-31 18:31:47 +08:00
committed by osborn
parent 1b0695d317
commit 08654fb1fe
38 changed files with 6 additions and 6 deletions

View File

@@ -0,0 +1,55 @@
import { jsObtainContext, jsCallEntityMethod } from 'doric/src/runtime/sandbox'
import { Panel } from 'doric'
import { DoricPlugin } from "./DoricPlugin"
import { createContext, destroyContext } from "./DoricDriver"
import { DoricStackNode } from './shader/DoricStackNode'
import { DoricViewNode } from './shader/DoricViewNode'
const doricContexts: Map<string, DoricContext> = new Map
let __contextId__ = 0
function getContextId() {
return `context_${__contextId__++}`
}
export function getDoricContext(contextId: string) {
return doricContexts.get(contextId)
}
export class DoricContext {
contextId = getContextId()
pluginInstances: Map<string, DoricPlugin> = new Map
rootNode: DoricStackNode
headNodes: Map<string, DoricViewNode> = new Map
constructor(content: string) {
createContext(this.contextId, content)
doricContexts.set(this.contextId, this)
this.rootNode = new DoricStackNode(this)
}
get panel() {
return jsObtainContext(this.contextId)?.entity as Panel
}
invokeEntityMethod(method: string, ...otherArgs: any) {
const argumentsList: any = [this.contextId]
for (let i = 0; i < arguments.length; i++) {
argumentsList.push(arguments[i])
}
return Reflect.apply(jsCallEntityMethod, this.panel, argumentsList)
}
init(frame: {
width: number,
height: number,
}, extra?: object) {
this.invokeEntityMethod("__init__", frame, extra ? JSON.stringify(extra) : undefined)
}
teardown() {
for (let plugin of this.pluginInstances.values()) {
plugin.onTearDown()
}
destroyContext(this.contextId)
}
}

View File

@@ -0,0 +1,147 @@
import { jsCallResolve, jsCallReject, jsCallbackTimer, jsCallEntityMethod, jsReleaseContext } from 'doric/src/runtime/sandbox'
import { acquireJSBundle, acquirePlugin } from './DoricRegistry'
import { getDoricContext } from './DoricContext'
import { DoricPlugin } from './DoricPlugin'
function getScriptId(contextId: string) {
return `__doric_script_${contextId}`
}
const originSetTimeout = window.setTimeout
const originClearTimeout = window.clearTimeout
const originSetInterval = window.setInterval
const originClearInterval = window.clearInterval
const timers: Map<number, { handleId: number, repeat: boolean }> = new Map
export function injectGlobalObject(name: string, value: any) {
Reflect.set(window, name, value, window)
}
export function loadJS(contextId: string, script: string) {
const scriptElement = document.createElement('script')
scriptElement.text = script
scriptElement.id = getScriptId(contextId)
document.body.appendChild(scriptElement)
}
function packageModuleScript(name: string, content: string) {
return `Reflect.apply(doric.jsRegisterModule,this,[${name},Reflect.apply(function(__module){(function(module,exports,require,setTimeout,setInterval,clearTimeout,clearInterval){
${content}
})(__module,__module.exports,doric.__require__,doricSetTimeout,doricSetInterval,doricClearTimeout,doricClearInterval);
return __module.exports;},this,[{exports:{}}])])`
}
function packageCreateContext(contextId: string, content: string) {
return `//@ sourceURL=contextId_${contextId}.js
Reflect.apply(function(doric,context,Entry,require,exports,setTimeout,setInterval,clearTimeout,clearInterval){
${content}
},doric.jsObtainContext("${contextId}"),[undefined,doric.jsObtainContext("${contextId}"),doric.jsObtainEntry("${contextId}"),doric.__require__,{},doricSetTimeout,doricSetInterval,doricClearTimeout,doricClearInterval])`
}
function initDoric() {
injectGlobalObject("Environment", {
platform: "h5"
})
injectGlobalObject("nativeEmpty", () => undefined)
injectGlobalObject('nativeLog', (type: 'd' | 'w' | 'e', message: string) => {
switch (type) {
case 'd':
console.log(message)
break
case 'w':
console.warn(message)
break
case 'e':
console.error(message)
break
}
})
injectGlobalObject('nativeRequire', (moduleName: string) => {
const bundle = acquireJSBundle(moduleName)
if (bundle === undefined || bundle.length === 0) {
console.log(`Cannot require JS Bundle :${moduleName}`)
return false
} else {
loadJS(moduleName, packageModuleScript(moduleName, packageModuleScript(name, bundle)))
return true
}
})
injectGlobalObject('nativeBridge', (contextId: string, namespace: string, method: string, callbackId: string, args?: any) => {
const pluginClass = acquirePlugin(namespace)
const doricContext = getDoricContext(contextId)
if (pluginClass === undefined) {
console.error(`Cannot find Plugin:${namespace}`)
return false
}
if (doricContext === undefined) {
console.error(`Cannot find Doric Context:${contextId}`)
return false
}
let plugin = doricContext.pluginInstances.get(namespace)
if (plugin === undefined) {
plugin = new pluginClass(doricContext) as DoricPlugin
doricContext.pluginInstances.set(namespace, plugin)
}
if (!Reflect.has(plugin, method)) {
console.error(`Cannot find Method:${method} in plugin ${namespace}`)
return false
}
const pluginMethod = Reflect.get(plugin, method, plugin)
if (typeof pluginMethod !== 'function') {
console.error(`Plugin ${namespace}'s property ${method}'s type is ${typeof pluginMethod} not function,`)
}
const ret = Reflect.apply(pluginMethod, plugin, [args])
if (ret instanceof Promise) {
ret.then(
e => {
jsCallResolve(contextId, callbackId, e)
},
e => {
jsCallReject(contextId, callbackId, e)
})
} else if (ret !== undefined) {
jsCallResolve(contextId, callbackId, ret)
}
return true
})
injectGlobalObject('nativeSetTimer', (timerId: number, time: number, repeat: boolean) => {
if (repeat) {
const handleId = originSetInterval(() => {
jsCallbackTimer(timerId)
}, time)
timers.set(timerId, { handleId, repeat })
} else {
const handleId = originSetTimeout(() => {
jsCallbackTimer(timerId)
}, time)
timers.set(timerId, { handleId, repeat })
}
})
injectGlobalObject('nativeClearTimer', (timerId: number) => {
const timerInfo = timers.get(timerId)
if (timerInfo) {
if (timerInfo.repeat) {
originClearInterval(timerInfo.handleId)
} else {
originClearTimeout(timerInfo.handleId)
}
}
})
}
export function createContext(contextId: string, content: string) {
loadJS(contextId, packageCreateContext(contextId, content))
}
export function destroyContext(contextId: string) {
jsReleaseContext(contextId)
const scriptElement = document.getElementById(getScriptId(contextId))
if (scriptElement) {
document.body.removeChild(scriptElement)
}
}
initDoric()

View File

@@ -0,0 +1,59 @@
import axios from 'axios'
import { DoricContext } from './DoricContext'
export class DoricElement extends HTMLElement {
context?: DoricContext
constructor() {
super()
}
get src() {
return this.getAttribute('src') as string
}
get alias() {
return this.getAttribute('alias') as string
}
set src(v: string) {
this.setAttribute('src', v)
}
set alias(v: string) {
this.setAttribute('alias', v)
}
connectedCallback() {
if (this.src && this.context === undefined) {
axios.get<string>(this.src).then(result => {
this.load(result.data)
})
}
}
disconnectedCallback() {
}
adoptedCallback() {
}
attributeChangedCallback() {
}
onDestroy() {
this.context?.teardown()
}
load(content: string) {
this.context = new DoricContext(content)
const divElement = document.createElement('div')
divElement.style.position = 'relative'
divElement.style.height = '100%'
this.append(divElement)
this.context.rootNode.view = divElement
this.context.init({
width: divElement.offsetWidth,
height: divElement.offsetHeight,
})
}
}

View File

@@ -0,0 +1,12 @@
import { DoricContext } from "./DoricContext"
export type DoricPluginClass = { new(...args: any[]): {} }
export class DoricPlugin {
context: DoricContext
constructor(context: DoricContext) {
this.context = context
}
onTearDown() {
}
}

View File

@@ -0,0 +1,63 @@
import { DoricPluginClass } from "./DoricPlugin"
import { ShaderPlugin } from "./plugins/ShaderPlugin"
import { DoricViewNodeClass } from "./shader/DoricViewNode"
import { DoricStackNode } from "./shader/DoricStackNode"
import { DoricVLayoutNode } from './shader/DoricVLayoutNode'
import { DoricHLayoutNode } from './shader/DoricHLayoutNode'
import { DoricTextNode } from "./shader/DoricTextNode"
import { DoricImageNode } from "./shader/DoricImageNode"
import { DoricScrollerNode } from "./shader/DoricScrollerNode"
import { ModalPlugin } from './plugins/ModalPlugin'
import { StoragePlugin } from "./plugins/StoragePlugin"
import { NavigatorPlugin } from "./navigate/NavigatorPlugin"
import { PopoverPlugin } from './plugins/PopoverPlugin'
import { DoricListItemNode } from "./shader/DoricListItemNode"
import { DoricListNode } from "./shader/DoricListNode"
const bundles: Map<string, string> = new Map
const plugins: Map<string, DoricPluginClass> = new Map
const nodes: Map<string, DoricViewNodeClass> = new Map
export function acquireJSBundle(name: string) {
return bundles.get(name)
}
export function registerJSBundle(name: string, bundle: string) {
bundles.set(name, bundle)
}
export function registerPlugin(name: string, plugin: DoricPluginClass) {
plugins.set(name, plugin)
}
export function acquirePlugin(name: string) {
return plugins.get(name)
}
export function registerViewNode(name: string, node: DoricViewNodeClass) {
nodes.set(name, node)
}
export function acquireViewNode(name: string) {
return nodes.get(name)
}
registerPlugin('shader', ShaderPlugin)
registerPlugin('modal', ModalPlugin)
registerPlugin('storage', StoragePlugin)
registerPlugin('navigator', NavigatorPlugin)
registerPlugin('popover', PopoverPlugin)
registerViewNode('Stack', DoricStackNode)
registerViewNode('VLayout', DoricVLayoutNode)
registerViewNode('HLayout', DoricHLayoutNode)
registerViewNode('Text', DoricTextNode)
registerViewNode('Image', DoricImageNode)
registerViewNode('Scroller', DoricScrollerNode)
registerViewNode('ListItem', DoricListItemNode)
registerViewNode('List', DoricListNode)

View File

@@ -0,0 +1,36 @@
import { DoricElement } from "../DoricElement"
export class NavigationElement extends HTMLElement {
elementStack: DoricElement[] = []
get currentNode() {
for (let i = 0; i < this.childNodes.length; i++) {
if (this.childNodes[i] instanceof DoricElement) {
return this.childNodes[i] as DoricElement
}
}
return undefined
}
push(element: DoricElement) {
const currentNode = this.currentNode
if (currentNode) {
this.elementStack.push(currentNode)
this.replaceChild(element, currentNode)
} else {
this.appendChild(element)
}
}
pop() {
const lastElement = this.elementStack.pop()
const currentNode = this.currentNode
if (lastElement && currentNode) {
this.replaceChild(lastElement, currentNode)
currentNode.onDestroy()
} else {
window.history.back()
}
}
}

View File

@@ -0,0 +1,34 @@
import { DoricPlugin } from "../DoricPlugin";
import { DoricElement } from "../DoricElement";
import { NavigationElement } from "./NavigationElement";
export class NavigatorPlugin extends DoricPlugin {
navigation: NavigationElement | undefined = document.getElementsByTagName('doric-navigation')[0] as (NavigationElement | undefined)
push(args: {
scheme: string,
config?: {
alias?: string,
extra?: string,
}
}) {
if (this.navigation) {
const div = new DoricElement
div.src = args.scheme
div.alias = args.config?.alias || args.scheme
this.navigation.push(div)
return Promise.resolve()
} else {
return Promise.reject('Not implemented')
}
}
pop() {
if (this.navigation) {
this.navigation.pop()
return Promise.resolve()
} else {
return Promise.reject('Not implemented')
}
}
}

View File

@@ -0,0 +1,72 @@
import { DoricPlugin } from '../DoricPlugin'
import { TOP, CENTER_Y, BOTTOM, toPixelString } from '../shader/DoricViewNode'
export class ModalPlugin extends DoricPlugin {
toast(args: {
msg?: string,
gravity?: number
}) {
const toastElement = document.createElement('div')
toastElement.style.position = "absolute"
toastElement.style.textAlign = "center"
toastElement.style.width = "100%"
const textElement = document.createElement('span')
textElement.innerText = args.msg || ""
textElement.style.backgroundColor = "#777777"
textElement.style.color = "white"
textElement.style.paddingLeft = '20px'
textElement.style.paddingRight = '20px'
textElement.style.paddingTop = '10px'
textElement.style.paddingBottom = '10px'
toastElement.appendChild(textElement)
document.body.appendChild(toastElement)
const gravity = args.gravity || BOTTOM
if ((gravity & TOP) == TOP) {
toastElement.style.top = toPixelString(30)
} else if ((gravity & BOTTOM) == BOTTOM) {
toastElement.style.bottom = toPixelString(30)
} else if ((gravity & CENTER_Y) == CENTER_Y) {
toastElement.style.top = toPixelString(document.body.offsetHeight / 2 - toastElement.offsetHeight / 2)
}
setTimeout(() => {
document.body.removeChild(toastElement)
}, 2000)
return Promise.resolve()
}
alert(args: {
title?: string,
msg?: string,
okLabel?: string,
}) {
window.alert(args.msg || "")
return Promise.resolve()
}
confirm(args: {
title?: string,
msg?: string,
okLabel?: string,
cancelLabel?: string,
}) {
if (window.confirm(args.msg || "")) {
return Promise.resolve()
} else {
return Promise.reject()
}
}
prompt(args: {
title?: string,
msg?: string,
okLabel?: string,
cancelLabel?: string,
defaultText?: string
text?: string
}) {
const result = window.prompt(args.msg || "", args.defaultText)
if (result) {
return Promise.resolve(result)
} else {
return Promise.reject(result)
}
}
}

View File

@@ -0,0 +1,59 @@
import { DoricPlugin } from '../DoricPlugin'
import { DVModel, DoricViewNode } from '../shader/DoricViewNode';
import { DoricContext } from '../DoricContext';
export class PopoverPlugin extends DoricPlugin {
fullScreen = document.createElement('div')
constructor(context: DoricContext) {
super(context)
this.fullScreen.id = `PopOver__${context.contextId}`
this.fullScreen.style.position = 'fixed'
this.fullScreen.style.top = '0px'
this.fullScreen.style.width = "100%"
this.fullScreen.style.height = "100%"
}
show(model: DVModel) {
const viewNode = DoricViewNode.create(this.context, model.type)
if (viewNode === undefined) {
return Promise.reject(`Cannot create ViewNode for ${model.type}`)
}
viewNode.viewId = model.id
viewNode.init()
viewNode.blend(model.props)
this.fullScreen.appendChild(viewNode.view)
this.context.headNodes.set(model.id, viewNode)
if (!document.body.contains(this.fullScreen)) {
document.body.appendChild(this.fullScreen)
}
return Promise.resolve()
}
dismiss(args?: { id: string }) {
if (args) {
const viewNode = this.context.headNodes.get(args.id)
if (viewNode) {
this.fullScreen.removeChild(viewNode.view)
}
if (this.context.headNodes.size === 0) {
document.body.removeChild(this.fullScreen)
}
} else {
this.dismissAll()
}
return Promise.resolve()
}
dismissAll() {
for (let node of this.context.headNodes.values()) {
this.context.headNodes.delete(node.viewId)
this.fullScreen.removeChild(node.view)
}
if (document.body.contains(this.fullScreen)) {
document.body.removeChild(this.fullScreen)
}
}
onTearDown() {
super.onTearDown()
this.dismissAll()
}
}

View File

@@ -0,0 +1,20 @@
import { DoricPlugin } from "../DoricPlugin";
import { DVModel } from "../shader/DoricViewNode";
export class ShaderPlugin extends DoricPlugin {
render(ret: DVModel) {
if (this.context.rootNode.viewId?.length > 0) {
if (this.context.rootNode.viewId === ret.id) {
this.context.rootNode.blend(ret.props)
} else {
const viewNode = this.context.headNodes.get(ret.id)
if (viewNode) {
viewNode.blend(ret.props)
}
}
} else {
this.context.rootNode.viewId = ret.id
this.context.rootNode.blend(ret.props)
}
}
}

View File

@@ -0,0 +1,41 @@
import { DoricPlugin } from "../DoricPlugin";
export class StoragePlugin extends DoricPlugin {
setItem(args: {
zone?: string,
key: string,
value: string
}) {
localStorage.setItem(`${args.zone}_${args.key}`, args.value)
return Promise.resolve()
}
getItem(args: {
zone?: string,
key: string,
}) {
return Promise.resolve(localStorage.getItem(`${args.zone}_${args.key}`))
}
remove(args: {
zone?: string,
key: string,
}) {
localStorage.removeItem(`${args.zone}_${args.key}`)
return Promise.resolve()
}
clear(args: {
zone: string,
}) {
let removingKeys = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key && key.startsWith(`${args.zone}_`)) {
removingKeys.push(key)
}
}
removingKeys.forEach(e => {
localStorage.removeItem(e)
})
return Promise.resolve()
}
}

View File

@@ -0,0 +1,60 @@
import { DoricGroupViewNode, LEFT, RIGHT, CENTER_X, CENTER_Y, TOP, BOTTOM, toPixelString } from "./DoricViewNode";
export class DoricHLayoutNode extends DoricGroupViewNode {
space = 0
gravity = 0
build() {
const ret = document.createElement('div')
ret.style.display = "flex"
ret.style.flexDirection = "row"
ret.style.flexWrap = "nowrap"
return ret
}
blendProps(v: HTMLElement, propName: string, prop: any) {
if (propName === 'space') {
this.space = prop
} else if (propName === 'gravity') {
this.gravity = prop
this.gravity = prop
if ((this.gravity & LEFT) === LEFT) {
this.view.style.justifyContent = "flex-start"
} else if ((this.gravity & RIGHT) === RIGHT) {
this.view.style.justifyContent = "flex-end"
} else if ((this.gravity & CENTER_X) === CENTER_X) {
this.view.style.justifyContent = "center"
}
if ((this.gravity & TOP) === TOP) {
this.view.style.alignItems = "flex-start"
} else if ((this.gravity & BOTTOM) === BOTTOM) {
this.view.style.alignItems = "flex-end"
} else if ((this.gravity & CENTER_Y) === CENTER_Y) {
this.view.style.alignItems = "center"
}
} else {
super.blendProps(v, propName, prop)
}
}
layout() {
super.layout()
this.childNodes.forEach((e, idx) => {
e.view.style.flexShrink = "0"
if (e.layoutConfig?.weight) {
e.view.style.flex = `${e.layoutConfig?.weight}`
}
e.view.style.marginLeft = toPixelString(e.layoutConfig?.margin?.left || 0)
e.view.style.marginRight = toPixelString(
(idx === this.childNodes.length - 1) ? 0 : this.space
+ (e.layoutConfig?.margin?.right || 0))
e.view.style.marginTop = toPixelString(e.layoutConfig?.margin?.top || 0)
e.view.style.marginBottom = toPixelString(e.layoutConfig?.margin?.bottom || 0)
if ((e.layoutConfig.alignment & TOP) === TOP) {
e.view.style.alignSelf = "flex-start"
} else if ((e.layoutConfig.alignment & BOTTOM) === BOTTOM) {
e.view.style.alignSelf = "flex-end"
} else if ((e.layoutConfig.alignment & CENTER_Y) === CENTER_Y) {
e.view.style.alignSelf = "center"
}
})
}
}

View File

@@ -0,0 +1,59 @@
import { DoricViewNode, LEFT, RIGHT, CENTER_X, CENTER_Y, TOP, BOTTOM, toPixelString, toRGBAString } from "./DoricViewNode";
enum ScaleType {
ScaleToFill = 0,
ScaleAspectFit,
ScaleAspectFill,
}
export class DoricImageNode extends DoricViewNode {
build(): HTMLElement {
const ret = document.createElement('img')
ret.style.objectFit = "fill"
return ret
}
blendProps(v: HTMLImageElement, propName: string, prop: any) {
switch (propName) {
case 'imageUrl':
v.setAttribute('src', prop)
break
case 'imageBase64':
v.setAttribute('src', prop)
break
case 'loadCallback':
v.onload = () => {
this.callJSResponse(prop, {
width: v.width,
height: v.height
})
}
break
case 'scaleType':
switch (prop) {
case ScaleType.ScaleToFill:
v.style.objectFit = "fill"
break
case ScaleType.ScaleAspectFit:
v.style.objectFit = "contain"
break
case ScaleType.ScaleAspectFill:
v.style.objectFit = "cover"
break
}
break
case 'isBlur':
if (prop) {
v.style.filter = 'blur(8px)'
} else {
v.style.filter = ''
}
break
default:
super.blendProps(v, propName, prop)
break
}
}
}

View File

@@ -0,0 +1,5 @@
import { DoricStackNode } from "./DoricStackNode";
export class DoricListItemNode extends DoricStackNode {
}

View File

@@ -0,0 +1,109 @@
import { DoricSuperNode, DVModel, DoricViewNode } from "./DoricViewNode";
import { DoricListItemNode } from "./DoricListItemNode";
export class DoricListNode extends DoricSuperNode {
itemCount = 0
renderItemFuncId?: string
onLoadMoreFuncId?: string
loadMoreViewId?: string
batchCount = 15
loadMore = false
childNodes: DoricListItemNode[] = []
loadMoreViewNode?: DoricListItemNode
blendProps(v: HTMLParagraphElement, propName: string, prop: any) {
switch (propName) {
case "itemCount":
this.itemCount = prop as number
break
case "renderItem":
this.reset()
this.renderItemFuncId = prop as string
break
case "onLoadMore":
this.onLoadMoreFuncId = prop as string
break
case "loadMoreView":
this.loadMoreViewId = prop as string
break
case "batchCount":
this.batchCount = prop as number
break
case "loadMore":
this.loadMore = prop as boolean
break
default:
super.blendProps(v, propName, prop)
break
}
}
reset() {
while (this.view.lastElementChild) {
this.view.removeChild(this.view.lastElementChild)
}
}
onBlended() {
super.onBlended()
if (this.childNodes.length !== this.itemCount) {
const ret = this.callJSResponse("renderBunchedItems", this.childNodes.length, this.itemCount) as DVModel[]
this.childNodes = this.childNodes.concat(ret.map(e => {
const viewNode = DoricViewNode.create(this.context, e.type) as DoricListItemNode
viewNode.viewId = e.id
viewNode.init(this)
viewNode.blend(e.props)
this.view.appendChild(viewNode.view)
return viewNode
}))
}
if (this.loadMoreViewNode && this.view.contains(this.loadMoreViewNode.view)) {
this.view.removeChild(this.loadMoreViewNode.view)
}
if (this.loadMore) {
if (!this.loadMoreViewNode) {
const loadMoreViewModel = this.getSubModel(this.loadMoreViewId || "")
if (loadMoreViewModel) {
this.loadMoreViewNode = DoricViewNode.create(this.context, loadMoreViewModel.type) as DoricListItemNode
this.loadMoreViewNode.viewId = loadMoreViewModel.id
this.loadMoreViewNode.init(this)
this.loadMoreViewNode.blend(loadMoreViewModel.props)
}
}
if (this.loadMoreViewNode) {
this.view.appendChild(this.loadMoreViewNode.view)
}
}
}
blendSubNode(model: DVModel) {
const viewNode = this.getSubNodeById(model.id)
if (viewNode) {
viewNode.blend(model.props)
}
}
getSubNodeById(viewId: string) {
if (viewId === this.loadMoreViewId) {
return this.loadMoreViewNode
}
return this.childNodes.filter(e => e.viewId === viewId)[0]
}
onScrollToEnd() {
if (this.loadMore && this.onLoadMoreFuncId) {
this.callJSResponse(this.onLoadMoreFuncId)
}
}
build() {
const ret = document.createElement('div')
ret.style.overflow = "scroll"
ret.addEventListener("scroll", () => {
if (this.loadMore) {
if (ret.scrollTop + ret.offsetHeight === ret.scrollHeight) {
this.onScrollToEnd()
}
}
})
return ret
}
}

View File

@@ -0,0 +1,18 @@
import { DoricSuperNode, DVModel } from "./DoricViewNode";
export class DoricRefreshableNode extends DoricSuperNode {
blendSubNode(model: DVModel) {
}
getSubNodeById(viewId: string) {
return undefined
}
build() {
const ret = document.createElement('div')
return ret
}
}

View File

@@ -0,0 +1,69 @@
import { LEFT, RIGHT, CENTER_X, CENTER_Y, TOP, BOTTOM, toPixelString, DoricSuperNode, DVModel, DoricViewNode } from "./DoricViewNode";
export class DoricScrollerNode extends DoricSuperNode {
childViewId: string = ""
childNode?: DoricViewNode
build() {
const ret = document.createElement('div')
ret.style.overflow = "scroll"
return ret
}
blendProps(v: HTMLElement, propName: string, prop: any) {
if (propName === 'content') {
this.childViewId = prop
} else {
super.blendProps(v, propName, prop)
}
}
blendSubNode(model: DVModel): void {
this.childNode?.blend(model.props)
}
getSubNodeById(viewId: string) {
return viewId === this.childViewId ? this.childNode : undefined
}
onBlended() {
super.onBlended()
const model = this.getSubModel(this.childViewId)
if (model === undefined) {
return
}
if (this.childNode) {
if (this.childNode.viewId === this.childViewId) {
///skip
} else {
if (this.reusable && this.childNode.viewType === model.type) {
this.childNode.viewId = model.id
this.childNode.blend(model.props)
} else {
this.view.removeChild(this.childNode.view)
const childNode = DoricViewNode.create(this.context, model.type)
if (childNode === undefined) {
return
}
childNode.viewId = model.id
childNode.init(this)
childNode.blend(model.props)
this.view.appendChild(childNode.view)
this.childNode = childNode
}
}
} else {
const childNode = DoricViewNode.create(this.context, model.type)
if (childNode === undefined) {
return
}
childNode.viewId = model.id
childNode.init(this)
childNode.blend(model.props)
this.view.appendChild(childNode.view)
this.childNode = childNode
}
}
layout() {
super.layout()
}
}

View File

@@ -0,0 +1,56 @@
import { DoricGroupViewNode, LayoutSpec, FrameSize, LEFT, RIGHT, CENTER_X, CENTER_Y, TOP, BOTTOM, toPixelString } from "./DoricViewNode";
export class DoricStackNode extends DoricGroupViewNode {
build() {
const ret = document.createElement('div')
ret.style.position = "relative"
return ret
}
layout() {
super.layout()
Promise.resolve().then(_ => {
this.configSize()
this.configOffset()
})
}
configSize() {
if (this.layoutConfig.widthSpec === LayoutSpec.WRAP_CONTENT) {
const width = this.childNodes.reduce((prev, current) => {
return Math.max(prev, current.view.offsetWidth)
}, 0)
this.view.style.width = toPixelString(width)
}
if (this.layoutConfig.heightSpec === LayoutSpec.WRAP_CONTENT) {
const height = this.childNodes.reduce((prev, current) => {
return Math.max(prev, current.view.offsetHeight)
}, 0)
this.view.style.height = toPixelString(height)
}
}
configOffset() {
this.childNodes.forEach(e => {
e.view.style.position = "absolute"
e.view.style.left = toPixelString(e.offsetX + this.paddingLeft)
e.view.style.top = toPixelString(e.offsetY + this.paddingTop)
const gravity = e.layoutConfig.alignment
if ((gravity & LEFT) === LEFT) {
e.view.style.left = toPixelString(0)
} else if ((gravity & RIGHT) === RIGHT) {
e.view.style.left = toPixelString(this.view.offsetWidth - e.view.offsetWidth)
} else if ((gravity & CENTER_X) === CENTER_X) {
e.view.style.left = toPixelString(this.view.offsetWidth / 2 - e.view.offsetWidth / 2)
}
if ((gravity & TOP) === TOP) {
e.view.style.top = toPixelString(0)
} else if ((gravity & BOTTOM) === BOTTOM) {
e.view.style.top = toPixelString(this.view.offsetHeight - e.view.offsetHeight)
} else if ((gravity & CENTER_Y) === CENTER_Y) {
e.view.style.top = toPixelString(this.view.offsetHeight / 2 - e.view.offsetHeight / 2)
}
})
}
}

View File

@@ -0,0 +1,49 @@
import { DoricViewNode, LEFT, RIGHT, CENTER_X, CENTER_Y, TOP, BOTTOM, toPixelString, toRGBAString } from "./DoricViewNode";
export class DoricTextNode extends DoricViewNode {
textElement!: HTMLElement
build(): HTMLElement {
const div = document.createElement('div')
div.style.display = "flex"
this.textElement = document.createElement('span')
div.appendChild(this.textElement)
div.style.justifyContent = "center"
div.style.alignItems = "center"
return div
}
blendProps(v: HTMLElement, propName: string, prop: any) {
switch (propName) {
case 'text':
this.textElement.innerText = prop
break
case 'textSize':
v.style.fontSize = toPixelString(prop)
break
case 'textColor':
v.style.color = toRGBAString(prop)
break
case 'textAlignment':
const gravity: number = prop
if ((gravity & LEFT) === LEFT) {
v.style.justifyContent = "flex-start"
} else if ((gravity & RIGHT) === RIGHT) {
v.style.justifyContent = "flex-end"
} else if ((gravity & CENTER_X) === CENTER_X) {
v.style.justifyContent = "center"
}
if ((gravity & TOP) === TOP) {
v.style.alignItems = "flex-start"
} else if ((gravity & BOTTOM) === BOTTOM) {
v.style.alignItems = "flex-end"
} else if ((gravity & CENTER_Y) === CENTER_Y) {
v.style.alignItems = "center"
}
break
default:
super.blendProps(v, propName, prop)
break
}
}
}

View File

@@ -0,0 +1,60 @@
import { DoricGroupViewNode, LEFT, RIGHT, CENTER_X, CENTER_Y, TOP, BOTTOM, toPixelString } from "./DoricViewNode";
export class DoricVLayoutNode extends DoricGroupViewNode {
space = 0
gravity = 0
build() {
const ret = document.createElement('div')
ret.style.display = "flex"
ret.style.flexDirection = "column"
ret.style.flexWrap = "nowrap"
return ret
}
blendProps(v: HTMLElement, propName: string, prop: any) {
if (propName === 'space') {
this.space = prop
} else if (propName === 'gravity') {
this.gravity = prop
if ((this.gravity & LEFT) === LEFT) {
this.view.style.alignItems = "flex-start"
} else if ((this.gravity & RIGHT) === RIGHT) {
this.view.style.alignItems = "flex-end"
} else if ((this.gravity & CENTER_X) === CENTER_X) {
this.view.style.alignItems = "center"
}
if ((this.gravity & TOP) === TOP) {
this.view.style.justifyContent = "flex-start"
} else if ((this.gravity & BOTTOM) === BOTTOM) {
this.view.style.justifyContent = "flex-end"
} else if ((this.gravity & CENTER_Y) === CENTER_Y) {
this.view.style.justifyContent = "center"
}
} else {
super.blendProps(v, propName, prop)
}
}
layout() {
super.layout()
this.childNodes.forEach((e, idx) => {
e.view.style.flexShrink = "0"
if (e.layoutConfig?.weight) {
e.view.style.flex = `${e.layoutConfig?.weight}`
}
e.view.style.marginTop = toPixelString(e.layoutConfig?.margin?.top || 0)
e.view.style.marginBottom = toPixelString(
(idx === this.childNodes.length - 1) ? 0 : this.space
+ (e.layoutConfig?.margin?.bottom || 0))
e.view.style.marginLeft = toPixelString(e.layoutConfig?.margin?.left || 0)
e.view.style.marginRight = toPixelString(e.layoutConfig?.margin?.right || 0)
if ((e.layoutConfig.alignment & LEFT) === LEFT) {
e.view.style.alignSelf = "flex-start"
} else if ((e.layoutConfig.alignment & RIGHT) === RIGHT) {
e.view.style.alignSelf = "flex-end"
} else if ((e.layoutConfig.alignment & CENTER_X) === CENTER_X) {
e.view.style.alignSelf = "center"
}
})
}
}

View File

@@ -0,0 +1,491 @@
import { DoricContext } from "../DoricContext";
import { acquireViewNode } from "../DoricRegistry";
export enum LayoutSpec {
EXACTLY = 0,
WRAP_CONTENT = 1,
AT_MOST = 2,
}
const SPECIFIED = 1
const START = 1 << 1
const END = 1 << 2
const SHIFT_X = 0
const SHIFT_Y = 4
export const LEFT = (START | SPECIFIED) << SHIFT_X
export const RIGHT = (END | SPECIFIED) << SHIFT_X
export const TOP = (START | SPECIFIED) << SHIFT_Y
export const BOTTOM = (END | SPECIFIED) << SHIFT_Y
export const CENTER_X = SPECIFIED << SHIFT_X
export const CENTER_Y = SPECIFIED << SHIFT_Y
export const CENTER = CENTER_X | CENTER_Y
export type FrameSize = {
width: number,
height: number,
}
export function toPixelString(v: number) {
return `${v}px`
}
export function toRGBAString(color: number) {
let strs = []
for (let i = 0; i < 32; i += 8) {
strs.push(((color >> i) & 0xff).toString(16))
}
strs = strs.map(e => {
if (e.length === 1) {
return '0' + e
}
return e
}).reverse()
/// RGBA
return `#${strs[1]}${strs[2]}${strs[3]}${strs[0]}`
}
export type DoricViewNodeClass = { new(...args: any[]): {} }
export interface DVModel {
id: string,
type: string,
props: {
[index: string]: any
},
}
export abstract class DoricViewNode {
viewId = ""
viewType = "View"
context: DoricContext
superNode?: DoricSuperNode
layoutConfig = {
widthSpec: LayoutSpec.EXACTLY,
heightSpec: LayoutSpec.EXACTLY,
alignment: 0,
weight: 0,
margin: {
left: 0,
right: 0,
top: 0,
bottom: 0
}
}
padding = {
left: 0,
right: 0,
top: 0,
bottom: 0,
}
border?: {
width: number,
color: number,
}
frameWidth = 0
frameHeight = 0
offsetX = 0
offsetY = 0
view!: HTMLElement
constructor(context: DoricContext) {
this.context = context
}
init(superNode?: DoricSuperNode) {
if (superNode) {
this.superNode = superNode
if (this instanceof DoricSuperNode) {
this.reusable = superNode.reusable
}
}
this.view = this.build()
}
abstract build(): HTMLElement
get paddingLeft() {
return this.padding.left || 0
}
get paddingRight() {
return this.padding.right || 0
}
get paddingTop() {
return this.padding.top || 0
}
get paddingBottom() {
return this.padding.bottom || 0
}
get borderWidth() {
return this.border?.width || 0
}
blend(props: { [index: string]: any }) {
this.view.id = `${this.viewId}`
for (let key in props) {
this.blendProps(this.view, key, props[key])
}
this.onBlended()
this.layout()
}
onBlended() {
}
configBorder() {
if (this.border) {
this.view.style.borderStyle = "solid"
this.view.style.borderWidth = toPixelString(this.border.width)
this.view.style.borderColor = toRGBAString(this.border.color)
}
}
configWidth() {
switch (this.layoutConfig.widthSpec) {
case LayoutSpec.WRAP_CONTENT:
this.view.style.width = "max-content"
break
case LayoutSpec.AT_MOST:
this.view.style.width = "100%"
break
case LayoutSpec.EXACTLY:
default:
this.view.style.width = toPixelString(this.frameWidth
- this.paddingLeft - this.paddingRight
- this.borderWidth * 2)
break
}
}
configHeight() {
switch (this.layoutConfig.heightSpec) {
case LayoutSpec.WRAP_CONTENT:
this.view.style.height = "max-content"
break
case LayoutSpec.AT_MOST:
this.view.style.height = "100%"
break
case LayoutSpec.EXACTLY:
default:
this.view.style.height = toPixelString(this.frameHeight
- this.paddingTop - this.paddingBottom
- this.borderWidth * 2)
break
}
}
configMargin() {
if (this.layoutConfig.margin) {
this.view.style.marginLeft = toPixelString(this.layoutConfig.margin.left || 0)
this.view.style.marginRight = toPixelString(this.layoutConfig.margin.right || 0)
this.view.style.marginTop = toPixelString(this.layoutConfig.margin.top || 0)
this.view.style.marginBottom = toPixelString(this.layoutConfig.margin.bottom || 0)
}
}
configPadding() {
if (this.padding) {
this.view.style.paddingLeft = toPixelString(this.paddingLeft)
this.view.style.paddingRight = toPixelString(this.paddingRight)
this.view.style.paddingTop = toPixelString(this.paddingTop)
this.view.style.paddingBottom = toPixelString(this.paddingBottom)
}
}
layout() {
this.configMargin()
this.configBorder()
this.configPadding()
this.configWidth()
this.configHeight()
}
blendProps(v: HTMLElement, propName: string, prop: any) {
switch (propName) {
case "border":
this.border = prop
break
case "padding":
this.padding = prop
break
case 'width':
this.frameWidth = prop as number
break
case 'height':
this.frameHeight = prop as number
break
case 'backgroundColor':
this.backgroundColor = prop as number
break
case 'layoutConfig':
const layoutConfig = prop
for (let key in layoutConfig) {
Reflect.set(this.layoutConfig, key, Reflect.get(layoutConfig, key, layoutConfig))
}
break
case 'x':
this.offsetX = prop as number
break
case 'y':
this.offsetY = prop as number
break
case 'onClick':
this.view.onclick = (event: Event) => {
this.callJSResponse(prop as string)
event.stopPropagation()
}
break
case 'corners':
if (typeof prop === 'object') {
this.view.style.borderTopLeftRadius = toPixelString(prop.leftTop)
this.view.style.borderTopRightRadius = toPixelString(prop.rightTop)
this.view.style.borderBottomRightRadius = toPixelString(prop.rightBottom)
this.view.style.borderBottomLeftRadius = toPixelString(prop.leftBottom)
} else {
this.view.style.borderRadius = toPixelString(prop)
}
break
case 'shadow':
const opacity = prop.opacity || 0
if (opacity > 0) {
const offsetX = prop.offsetX || 0
const offsetY = prop.offsetY || 0
const shadowColor = prop.color || 0xff000000
const shadowRadius = prop.radius
const alpha = opacity * 255
this.view.style.boxShadow = `${toPixelString(offsetX)} ${toPixelString(offsetY)} ${toPixelString(shadowRadius)} ${toRGBAString((shadowColor & 0xffffff) | ((alpha & 0xff) << 24))} `
} else {
this.view.style.boxShadow = ""
}
break
}
}
set backgroundColor(v: number) {
this.view.style.backgroundColor = toRGBAString(v)
}
static create(context: DoricContext, type: string) {
const viewNodeClass = acquireViewNode(type)
if (viewNodeClass === undefined) {
console.error(`Cannot find ViewNode for ${type}`)
return undefined
}
const ret = new viewNodeClass(context) as DoricViewNode
ret.viewType = type
return ret
}
getIdList() {
const ids: string[] = []
let viewNode: DoricViewNode | undefined = this
do {
ids.push(viewNode.viewId)
viewNode = viewNode.superNode
} while (viewNode)
return ids.reverse()
}
callJSResponse(funcId: string, ...args: any) {
const argumentsList: any = ['__response__', this.getIdList(), funcId]
for (let i = 1; i < arguments.length; i++) {
argumentsList.push(arguments[i])
}
return Reflect.apply(this.context.invokeEntityMethod, this.context, argumentsList)
}
}
export abstract class DoricSuperNode extends DoricViewNode {
reusable = false
subModels: Map<String, DVModel> = new Map
blendProps(v: HTMLElement, propName: string, prop: any) {
if (propName === 'subviews') {
if (prop instanceof Array) {
prop.forEach((e: DVModel) => {
this.mixinSubModel(e)
this.blendSubNode(e)
})
}
} else {
super.blendProps(v, propName, prop)
}
}
mixinSubModel(subNode: DVModel) {
const oldValue = this.getSubModel(subNode.id)
if (oldValue) {
this.mixin(subNode, oldValue)
} else {
this.subModels.set(subNode.id, subNode)
}
}
getSubModel(id: string) {
return this.subModels.get(id)
}
mixin(src: DVModel, target: DVModel) {
for (let key in src.props) {
if (key === "subviews") {
continue
}
Reflect.set(target.props, key, Reflect.get(src.props, key))
}
}
clearSubModels() {
this.subModels.clear()
}
removeSubModel(id: string) {
this.subModels.delete(id)
}
abstract blendSubNode(model: DVModel): void
abstract getSubNodeById(viewId: string): DoricViewNode | undefined
}
export abstract class DoricGroupViewNode extends DoricSuperNode {
childNodes: DoricViewNode[] = []
childViewIds: string[] = []
init(superNode?: DoricSuperNode) {
super.init(superNode)
this.view.style.overflow = "hidden"
}
blendProps(v: HTMLElement, propName: string, prop: any) {
if (propName === 'children') {
if (prop instanceof Array) {
this.childViewIds = prop
}
} else {
super.blendProps(v, propName, prop)
}
}
blend(props: { [index: string]: any }) {
super.blend(props)
}
onBlended() {
super.onBlended()
this.configChildNode()
}
configChildNode() {
this.childViewIds.forEach((childViewId, index) => {
const model = this.getSubModel(childViewId)
if (model === undefined) {
return
}
if (index < this.childNodes.length) {
const oldNode = this.childNodes[index]
if (oldNode.viewId === childViewId) {
//The same,skip
} else {
if (this.reusable) {
if (oldNode.viewType === model.type) {
//Same type,can be reused
oldNode.viewId = childViewId
oldNode.blend(model.props)
} else {
//Replace this view
this.view.removeChild(oldNode.view)
const newNode = DoricViewNode.create(this.context, model.type)
if (newNode === undefined) {
return
}
newNode.viewId = childViewId
newNode.init(this)
newNode.blend(model.props)
this.childNodes[index] = newNode
this.view.replaceChild(newNode.view, oldNode.view)
}
} else {
//Find in remain nodes
let position = -1
for (let start = index + 1; start < this.childNodes.length; start++) {
if (childViewId === this.childNodes[start].viewId) {
//Found
position = start
break
}
}
if (position >= 0) {
//Found swap idx,position
const reused = this.childNodes[position]
const abandoned = this.childNodes[index]
this.childNodes[index] = reused
this.childNodes[position] = abandoned
this.view.removeChild(reused.view)
this.view.insertBefore(reused.view, abandoned.view)
this.view.removeChild(abandoned.view)
if (position === this.view.childElementCount - 1) {
this.view.appendChild(abandoned.view)
} else {
this.view.insertBefore(abandoned.view, this.view.children[position])
}
} else {
//Not found,insert
const newNode = DoricViewNode.create(this.context, model.type)
if (newNode === undefined) {
return
}
newNode.viewId = childViewId
newNode.init(this)
newNode.blend(model.props)
this.childNodes[index] = newNode
this.view.insertBefore(newNode.view, this.view.children[index])
}
}
}
} else {
//Insert
const newNode = DoricViewNode.create(this.context, model.type)
if (newNode === undefined) {
return
}
newNode.viewId = childViewId
newNode.init(this)
newNode.blend(model.props)
this.childNodes.push(newNode)
this.view.appendChild(newNode.view)
}
})
let size = this.childNodes.length
for (let idx = this.childViewIds.length; idx < size; idx++) {
this.view.removeChild(this.childNodes[idx].view)
}
this.childNodes = this.childNodes.slice(0, this.childViewIds.length)
}
blendSubNode(model: DVModel) {
this.getSubNodeById(model.id)?.blend(model.props)
}
getSubNodeById(viewId: string) {
return this.childNodes.filter(e => e.viewId === viewId)[0]
}
}