UNPKG

neft

Version:

Universal Platform

665 lines (513 loc) 21 kB
# 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