neft
Version:
Universal Platform
665 lines (513 loc) • 21 kB
text/coffeescript
# Document
'use strict'
utils = require 'src/utils'
assert = require 'src/assert'
log = require 'src/log'
signal = require 'src/signal'
Dict = require 'src/dict'
List = require 'src/list'
assert = assert.scope 'View'
log = log.scope 'View'
{Emitter} = signal
{emitSignal} = Emitter
module.exports = class Document extends Emitter
files = @_files = {}
pool = Object.create null
getFromPool = (path) ->
pool[path]?.pop()
@__name__ = 'Document'
@__path__ = 'Document'
@JSON_CTORS = []
JSON_CTOR_ID = @JSON_CTOR_ID = @JSON_CTORS.push(Document) - 1
i = 1
JSON_PATH = i++
JSON_NODE = i++
JSON_TARGET_NODE = i++
JSON_PROPS_TO_PARSE = i++
JSON_COMPONENTS = i++
JSON_SCRIPTS = i++
JSON_PROP_CHANGES = i++
JSON_INPUTS = i++
JSON_CONDITIONS = i++
JSON_ITERATORS = i++
JSON_USES = i++
JSON_REFS = i++
JSON_PROPS_TO_SET = i++
JSON_LOGS = i++
JSON_STYLES = i++
JSON_ARGS_LENGTH = @JSON_ARGS_LENGTH = i
@FILES_PATH = ''
@SCRIPTS_PATH = ''
signal.create @, 'onCreate'
signal.create @, 'onError'
signal.create @, 'onBeforeParse'
signal.create @, 'onParse'
signal.create @, 'onStyle'
## *Signal* Document.onBeforeRender(*Document* file)
Corresponding node handler: *n-onBeforeRender=""*.
signal.create @, 'onBeforeRender'
## *Signal* Document.onRender(*Document* file)
Corresponding node handler: *n-onRender=""*.
signal.create @, 'onRender'
## *Signal* Document.onBeforeRevert(*Document* file)
Corresponding node handler: *n-onBeforeRevert=""*.
signal.create @, 'onBeforeRevert'
## *Signal* Document.onRevert(*Document* file)
Corresponding node handler: *n-onRevert=""*.
signal.create @, 'onRevert'
@Element = require('./element/index')
@PropChange = require('./propChange') @
@Use = require('./use') @
@Scripts = require('./scripts') @
@Input = require('./input') @
@Condition = require('./condition') @
@Iterator = require('./iterator') @
@Log = require('./log') @
@PropsToSet = require('./propsToSet') @
## *Document* Document.fromHTML(*String* path, *String* html)
@fromHTML = do ->
unless utils.isNode
return (path, html) ->
throw new Error 'Document.fromHTML is available only on the server'
clear = require('./file/clear') Document
(path, html) ->
assert.isString path
assert.notLengthOf path, 0
assert.notOk files[path]?
assert.isString html
if html is ''
html = '<html></html>'
# get node
node = Document.Element.fromHTML html
# clear
clear node
# create file
Document.fromElement path, node
## *Document* Document.fromElement(*String* path, *Element* element)
@fromElement = (path, node) ->
assert.isString path
assert.notLengthOf path, 0
assert.instanceOf node, Document.Element
assert.notOk files[path]?
# create
file = new Document path, node
## *Document* Document.fromJSON(*String*|*Object* json)
@fromJSON = (json) ->
# parse json
if typeof json is 'string'
json = utils.tryFunction JSON.parse, null, [json], json
assert.isArray json
if file = files[json[JSON_PATH]]
return file
file = Document.JSON_CTORS[json[0]]._fromJSON json
assert.notOk files[file.path]?
# save to storage
files[file.path] = file
file
@_fromJSON = do ->
parseObject = (file, obj, target) ->
for key, val of obj
target[key] = Document.JSON_CTORS[val[0]]._fromJSON file, val
return
parseArray = (file, arr, target) ->
for val in arr
target.push Document.JSON_CTORS[val[0]]._fromJSON file, val
return
(arr, obj) ->
unless obj
node = Document.Element.fromJSON arr[JSON_NODE]
obj = new Document arr[JSON_PATH], node
# targetNode
if arr[JSON_TARGET_NODE]
obj.targetNode = node.getChildByAccessPath arr[JSON_TARGET_NODE]
# propsToParse
{propsToParse} = obj
jsonPropsToParse = arr[JSON_PROPS_TO_PARSE]
for propNode, i in jsonPropsToParse by 2
propsToParse.push node.getChildByAccessPath(propNode)
propsToParse.push jsonPropsToParse[i+1]
utils.merge obj.components, arr[JSON_COMPONENTS]
if (scripts = arr[JSON_SCRIPTS])?
obj.scripts = Document.JSON_CTORS[scripts[0]]._fromJSON(obj, scripts)
parseArray obj, arr[JSON_PROP_CHANGES], obj.propChanges
parseArray obj, arr[JSON_INPUTS], obj.inputs
parseArray obj, arr[JSON_CONDITIONS], obj.conditions
parseArray obj, arr[JSON_ITERATORS], obj.iterators
parseArray obj, arr[JSON_USES], obj.uses
for ref, path of arr[JSON_REFS]
obj.refs[ref] = obj.node.getChildByAccessPath path
parseArray obj, arr[JSON_PROPS_TO_SET], obj.propsToSet
`//<development>`
parseArray obj, arr[JSON_LOGS], obj.logs
`//</development>`
parseArray obj, arr[JSON_STYLES], obj.styles
obj
## Document.parse(*Document* file)
@parse = do ->
unless utils.isNode
return (file) ->
throw new Error "Document.parse() is available only on the server"
components = require('./file/parse/components') Document
scripts = require('./file/parse/scripts') Document
styles = require('./file/parse/styles') Document
props = require('./file/parse/props') Document
propChanges = require('./file/parse/propChanges') Document
iterators = require('./file/parse/iterators') Document
target = require('./file/parse/target') Document
uses = require('./file/parse/uses') Document
storage = require('./file/parse/storage') Document
conditions = require('./file/parse/conditions') Document
refs = require('./file/parse/refs') Document
logs = require('./file/parse/logs') Document
propSetting = require('./file/parse/propSetting') Document
(file) ->
assert.instanceOf file, Document
assert.notOk files[file.path]?
files[file.path] = file
# trigger signal
Document.onBeforeParse.emit file
# parse
styles file
components file
scripts file
iterators file
props file
propChanges file
target file
uses file
storage file
conditions file
refs file
propSetting file
`//<development>`
logs file
`//</development>`
# trigger signal
Document.onParse.emit file
file
## *Document* Document.factory(*String* path)
@factory = (path) ->
unless files.hasOwnProperty path
# TODO: trigger here instance of `LoadError` class
Document.onError.emit path
assert.isString path, "path is not a string"
assert.ok files[path]?, "the given file path '#{path}' doesn't exist"
# from pool
if r = getFromPool(path)
return r
# clone original
file = files[path].clone()
Document.onCreate.emit file
file
## *Document* Document::constructor(*String* path, *Element* element)
@emitNodeSignal = emitNodeSignal = (file, propName, prop1, prop2) ->
if nodeSignal = file.node.props[propName]
nodeSignal?.call? file, prop1, prop2
return
constructor: (@path, @node) ->
assert.isString @path
assert.notLengthOf @path, 0
assert.instanceOf @node, Document.Element
super()
@isClone = false
@uid = utils.uid()
@isRendered = false
@targetNode = null
@parent = null
@scope = null
@scripts = null
@props = null
@context = null
@source = null
@parentUse = null
@propsToParse = []
@components = {}
@propChanges = []
@inputs = []
@conditions = []
@iterators = []
@uses = []
@refs = {}
@propsToSet = []
@logs = []
@styles = []
@inputRefs = new Dict
@inputProps = new Dict
@inputState = new Dict
@inputArgs = [@inputRefs, @inputProps, @inputState]
@node.onPropsChange @_updateInputPropsKey, @
@inputProps.extend @node.props
`//<development>`
if @constructor is Document
Object.preventExtensions @
`//</development>`
# *Document* Document::render([*Any* props, *Any* context, *Document* source])
render: (props, context, source, refs) ->
unless @isClone
@clone().render props, context, source, refs
else
@_render(props, context, source, refs)
_updateInputPropsKey: (key) ->
{inputProps, source, props} = @
viewProps = @node.props
if source
val = source.node.props[key]
if val is undefined and props
val = props[key]
if val is undefined
val = viewProps[key]
else
if props
val = props[key]
if val is undefined
val = viewProps[key]
if val is undefined
inputProps.pop key
else
inputProps.set key, val
return
_render: do ->
renderTarget = require('./file/render/parse/target') Document
(props = true, context = null, source, refs) ->
assert.notOk @isRendered
@props = props
@source = source
@context = context
{inputProps, inputRefs} = @
if props instanceof Dict
props.onChange @_updateInputPropsKey, @
if source?
# props
viewProps = @node.props
sourceProps = source.node.props
source.node.onPropsChange @_updateInputPropsKey, @
for prop, val of inputProps
if viewProps[prop] is props[prop] is sourceProps[prop] is undefined
inputProps.pop prop
for prop, val of viewProps
if props[prop] is sourceProps[prop] is undefined
if val isnt undefined
inputProps.set prop, val
for prop, val of props
if sourceProps[prop] is undefined
if val isnt undefined
inputProps.set prop, val
for prop, val of sourceProps
if val isnt undefined
inputProps.set prop, val
else
# props
viewProps = @node.props
for prop, val of inputProps
if viewProps[prop] is props[prop] is undefined
inputProps.pop prop
for prop, val of viewProps
if props[prop] is undefined
if val isnt undefined
inputProps.set prop, val
for prop, val of props
if val isnt undefined
inputProps.set prop, val
# refs
if refs
viewRefs = @refs
for prop, val of inputRefs
if viewRefs[prop] is undefined and refs[prop] is undefined
inputRefs.pop prop
for prop, val of refs
if viewRefs[prop] is undefined
inputRefs.set prop, val
for prop, val of viewRefs
inputRefs.set prop, val
Document.onBeforeRender.emit @
emitNodeSignal @, 'n-onBeforeRender'
if @scope?.node is @node
@scope.state = @inputState
emitSignal @scope, 'onBeforeRender'
# inputs
for input in @inputs
input.render()
# conditions
for condition in @conditions
condition.render()
# uses
for use in @uses
unless use.isRendered
use.render()
# iterators
for iterator in @iterators
iterator.render()
# source
renderTarget @, source
# logs
`//<development>`
for log in @logs
log.render()
`//</development>`
@isRendered = true
Document.onRender.emit @
emitNodeSignal @, 'n-onRender'
if @scope?.node is @node
emitSignal @scope, 'onRender'
for input in @inputs
input.onRender()
@
## *Document* Document::revert()
revert: do ->
target = require('./file/render/revert/target') Document
->
assert.ok @isRendered
@isRendered = false
Document.onBeforeRevert.emit @
emitNodeSignal @, 'n-onBeforeRevert'
if @scope?.node is @node
emitSignal @scope, 'onBeforeRevert'
if @props instanceof Dict
@props.onChange.disconnect @_updateInputPropsKey, @
# props
if @source
@source.node.onPropsChange.disconnect @_updateInputPropsKey, @
# parent use
@parentUse?.detachUsedComponent()
# inputs
if @inputs
for input, i in @inputs
input.revert()
# uses
if @uses
for use in @uses
use.revert()
# iterators
if @iterators
for iterator, i in @iterators
iterator.revert()
# target
target @, @source
@props = null
@source = null
@context = null
@inputState.clear()
Document.onRevert.emit @
emitNodeSignal @, 'n-onRevert'
if @scope?.node is @node
@scope.state = null
emitSignal @scope, 'onRevert'
@
## *Document* Document::use(*String* useName, [*Document* document])
use: (useName, view) ->
if @uses
for use in @uses
if use.name is useName
elem = use
break
if elem
elem.render view
else
log.warn "'#{@path}' view doesn't have '#{useName}' use"
@
## *Signal* Document::onReplaceByUse(*Document.Use* use)
Corresponding node handler: *n-onReplaceByUse=""*.
Emitter.createSignal @, 'onReplaceByUse'
## *Document* Document::clone()
clone: ->
# from pool
if r = getFromPool(@path)
r
else
if @isClone and (original = files[@path])
original._clone()
else
@_clone()
parseProp = do ->
cache = Object.create null
(val) ->
func = cache[val] ?= new Function 'Dict', 'List', "return #{val}"
func Dict, List
_clone: ->
clone = new Document @path, @node.cloneDeep()
clone.isClone = true
clone.components = @components
if @targetNode
clone.targetNode = @node.getCopiedElement @targetNode, clone.node
# propsToParse
{propsToParse} = @
for propNode, i in propsToParse by 2
propNode = @node.getCopiedElement propNode, clone.node
propName = propsToParse[i+1]
propNode.props.set propName, parseProp(propNode.props[propName])
# propChanges
for propChange in @propChanges
clone.propChanges.push propChange.clone @, clone
# refs
for ref, node of @refs
clone.refs[ref] = @node.getCopiedElement node, clone.node
clone.inputRefs.extend clone.refs
# inputs
for input in @inputs
clone.inputs.push input.clone @, clone
# conditions
for condition in @conditions
clone.conditions.push condition.clone @, clone
# iterators
for iterator in @iterators
clone.iterators.push iterator.clone @, clone
# uses
for use in @uses
clone.uses.push use.clone @, clone
# props to set
for propsToSet in @propsToSet
clone.propsToSet.push propsToSet.clone @, clone
# logs
for log in @logs
clone.logs.push log.clone @, clone
# scope
if @scripts
clone.scope = @scripts.createCloneScope clone
clone
## Document::destroy()
destroy: ->
if @isRendered
@revert()
pathPool = pool[@path] ?= []
assert.notOk utils.has(pathPool, @)
pathPool.push @
return
## *Object* Document::toJSON()
toJSON: do ->
callToJSON = (elem) ->
elem.toJSON()
(key, arr) ->
if @isClone and original = Document._files[@path]
return original.toJSON key, arr
unless arr
arr = new Array JSON_ARGS_LENGTH
arr[0] = JSON_CTOR_ID
arr[JSON_PATH] = @path
arr[JSON_NODE] = @node.toJSON()
# targetNode
if @targetNode
arr[JSON_TARGET_NODE] = @targetNode.getAccessPath @node
# propsToParse
propsToParse = arr[JSON_PROPS_TO_PARSE] = new Array @propsToParse.length
for propNode, i in @propsToParse by 2
propsToParse[i] = propNode.getAccessPath @node
propsToParse[i+1] = @propsToParse[i+1]
arr[JSON_COMPONENTS] = @components
arr[JSON_SCRIPTS] = @scripts
arr[JSON_PROP_CHANGES] = @propChanges.map callToJSON
arr[JSON_INPUTS] = @inputs.map callToJSON
arr[JSON_CONDITIONS] = @conditions.map callToJSON
arr[JSON_ITERATORS] = @iterators.map callToJSON
arr[JSON_USES] = @uses.map callToJSON
refs = arr[JSON_REFS] = {}
for ref, node of @refs
refs[ref] = node.getAccessPath @node
arr[JSON_PROPS_TO_SET] = @propsToSet
`//<development>`
arr[JSON_LOGS] = @logs.map callToJSON
`//</development>`
`//<production>`
arr[JSON_LOGS] = []
`//</production>`
arr[JSON_STYLES] = @styles.map callToJSON
arr