///
namespace Leaf {
export class DataSource {
public events = new EventEmitter<{
[key: string]: any
change: Partial
}>()
public readonly fields: (keyof TAttribute)[]
// instance id
public id = Date.now().toString() + Math.random().toString()
protected _data: TAttribute = {} as TAttribute
public readonly data: TAttribute
[index: string]: any
constructor(public readonly _name: string, protected _fields: (keyof TAttribute)[] = []) {
for (let field of this._fields) {
this.define(field)
}
Object.defineProperty(this, "fields", {
get: () => {
return this._fields.slice(0)
}
})
Object.defineProperty(this, "data", {
get: () => {
return this as any as TAttribute
},
set: (values: TAttribute) => {
this.sets(values)
}
})
}
protected define(name: keyof TAttribute, defaultValue: any = null) {
Object.defineProperty(this, name, {
get: () => {
return this.get(name)
},
set: (value) => {
return this.set(name, value)
}
})
}
public hasKey(key: string) {
return this._fields.indexOf(key as keyof TAttribute) >= 0
}
public sets(obj: any): boolean {
if (!obj) {
return
}
if (obj instanceof DataSource) {
obj = obj.toJSON()
}
let changed = false
let diff = {}
for (let key in obj) {
if (!this.hasKey(key)) {
continue
}
let old = this._data[key]
let value = obj[key]
if (this.set(key as keyof TAttribute, value, true)) {
changed = true
this.events.emit(`change/${key}`, value, old)
diff[key as string] = old
}
}
if (changed) {
this.events.emit("change", diff as any)
}
}
public set(key: keyof TAttribute, value: any, silent?: boolean): boolean {
let old = this._data[key as string]
if (!this.hasKey(key as string)) return false;
if (Leaf.Util.deepEqual(old, value)) {
return false
}
this._data[key as string] = value
if (silent) return true
this.events.emit(`change/${key}`, value, old)
let diff: { [index: string]: any } = {}
diff[key as string] = old
this.events.emit("change", diff as any)
return true
}
public get(key: keyof TAttribute) {
return this._data[key as string] || null
}
public toJSON() {
return JSON.parse(JSON.stringify(this._data))
}
}
export class Model extends DataSource{ }
export class DataTree {
constructor(public readonly source: DataSource) {
}
getNode(path?: string) {
if (!path) return new DataNode(this)
path = path.trim()
let routes = path.split(".")
let parent = this.source.data as any
let valid = false
for (let route of routes) {
if (!this.valueCanBeObject(parent[route])) {
return null
}
if (parent[route]) {
parent = parent[route]
} else if (this.valueCanBeObject(parent[route])) {
parent = {}
continue
} else {
return null
}
}
// Point at something can possibly be a data node
return new DataNode(this, path)
}
valueCanBeObject(v: any) {
if (typeof v === "string") return false
if (typeof v === "boolean") return false
if (typeof v === "number") return false
if (typeof v === "undefined") return false
if (typeof v === "object") return true
return false
}
getValueByPath(path: string) {
if (!path) return this.source["_data"]
path = path.trim()
let routes = path.split(".")
let parent = this.source["_data"] as any
let valid = false
let last = routes.pop()
for (let route of routes) {
if (!this.valueCanBeObject(parent[route])) {
return undefined
}
if (parent[route]) {
parent = parent[route]
} else {
return undefined
}
}
// Point at something can possibly be a data node
let v = parent[last]
return v
}
setValueByPath(path: string, v: any): boolean {
if (!path) {
this.source.sets(v)
return true
}
path = path.trim()
let routes = path.split(".")
let root = routes[0]
let last = routes.pop()
let parent = this.source["_data"] as any
let valid = false
let passed = []
for (let route of routes) {
passed.push(route)
if (!this.valueCanBeObject(parent[route])) {
throw new Error(`Typeof ${typeof parent} at ${passed.join(".")} in path ${path} is not a nestable`)
}
if (parent[route]) {
parent = parent[route]
} else if (this.valueCanBeObject(parent[route])) {
parent[route] = {}
parent = parent[route]
}
}
// Point at something can possibly be a data node
let old = parent[last]
parent[last] = v
this.source.events.emit(`change/${root}`, v, old)
routes.push(last)
this.source.events.emit(`change`, {
[routes.join(".")]: old
} as any)
return true
}
}
export class DataNode {
constructor(public readonly tree: DataTree, public readonly path: string = "") {
Object.defineProperty(this, "v", {
get: () => this.get(),
set: (v) => this.set(v)
})
}
getProxyOrValue() {
let v = this.get()
if (this.tree.valueCanBeObject(v)) {
return new Proxy((v) as Object, {
get: (obj, k) => {
if (typeof k !== "string") return
let node = this.tree.getNode(this.path + `${this.path && "."}${k}`)
if (node) return node.getProxyOrValue()
return obj[k]
},
set: (obj, k, v) => {
if (typeof k !== "string") return
this.tree.setValueByPath(this.path + `${this.path && "."}${k}`, v)
return true
}
})
}
return v
}
set(v: T) {
return this.tree.setValueByPath(this.path, v)
}
get(): T {
return this.tree.getValueByPath(this.path)
}
toString() {
return ``
}
}
}