neft
Version:
Universal Platform
556 lines (451 loc) • 17.1 kB
text/coffeescript
'use strict'
assert = require 'src/assert'
utils = require 'src/utils'
signal = require 'src/signal'
log = require 'src/log'
Renderer = require 'src/renderer'
log = log.scope 'Styles'
ATTRS_CLASS_PRIORITY = 9999
PROPS_CLASS_PRIORITY = -2
module.exports = (File, data) -> class Style
{windowStyle, styles, queries} = data
{Element} = File
{Tag, Text} = Element
@__name__ = 'Style'
@__path__ = 'File.Style'
@JSON_CTOR_ID = File.Style?.JSON_CTOR_ID
@JSON_CTOR_ID ?= File.JSON_CTORS.push(Style) - 1
{JSON_CTOR_ID} = @
i = 1
JSON_NODE = i++
JSON_PROPS = i++
JSON_CHILDREN = i++
JSON_ARGS_LENGTH = @JSON_ARGS_LENGTH = i
@applyStyleQueriesInDocument = (file, localQueries = queries) ->
assert.instanceOf file, File
for elem in localQueries
nodes = file.node.queryAll elem.query
for node in nodes
unless node instanceof Tag
log.warn 'document.query can be attached only to tags; ' +
"query '#{elem.query}' has been omitted for this node"
continue
node.props.set 'n-style', elem.style
file
@createStylesInDocument = do ->
getStyleAttrs = (node) ->
props = null
for prop of node.props when node.props.hasOwnProperty(prop)
if prop.slice(0, 8) is 'n-style:'
props ?= {}
props[prop] = true
props
forNode = (file, node, parentStyle) ->
isText = node instanceof Text
if isText or node.props['n-style']
style = new Style
style.file = file
style.node = node
style.parent = parentStyle
style.props = not isText and getStyleAttrs node
if parentStyle
parentStyle.children.push style
else
file.styles.push style
parentStyle = style
unless isText
for child in node.children
forNode file, child, parentStyle
return
(file) ->
assert.instanceOf file, File
forNode file, file.node, null
file
@extendDocumentByStyles = (file) ->
assert.instanceOf file, File
Style.applyStyleQueriesInDocument file
Style.createStylesInDocument file
file
@_fromJSON = (file, arr, obj) ->
unless obj
obj = new Style
obj.file = file
obj.node = file.node.getChildByAccessPath arr[JSON_NODE]
obj.props = arr[JSON_PROPS]
for child in arr[JSON_CHILDREN]
cloneChild = Style._fromJSON file, child
cloneChild.parent = obj
obj.children.push cloneChild
obj
constructor: ->
@file = null
@node = null
@props = null
@parent = null
@children = []
@isAutoParent = true
@item = null
@scope = null
@textObject = null
@propsClass = null
@propsClass = null
Object.seal @
createClassWithPriority: (priority) ->
assert.ok @item
r = Renderer.Class.New()
r.target = @item
if priority?
r.priority = priority
r
getTextObject: ->
{item} = @
assert.isDefined item
assert.isNotDefined @textObject
if @node instanceof Text
item
else if ($ = item._$) and 'text' of $
$
else if 'text' of item
item
updateText: ->
{textObject, node} = @
assert.isDefined textObject
isText = node instanceof Text
if node instanceof Tag
text = node.stringifyChildren()
else
text = node.text
textObject.text = text
return
setProp: do ->
PREFIX_LENGTH = 'n-style:'.length
getSplitProp = do ->
cache = Object.create null
(prop) ->
cache[prop] ||= prop.slice(PREFIX_LENGTH).split ':'
getPropertyPath = do ->
cache = Object.create null
(prop) ->
cache[prop] ||= prop.slice(PREFIX_LENGTH).replace /:/g, '.'
getInternalProperty = do ->
cache = Object.create null
(prop) ->
cache[prop] ||= "_#{prop}"
(prop, val, oldVal) ->
assert.instanceOf @, Style
assert.isDefined @propsClass
{propsClass} = @
parts = getSplitProp prop
# get object
obj = @item
for i in [0...parts.length - 1] by 1
unless obj = obj[parts[i]]
log.warn "Attribute '#{prop}' doesn't exist in item '#{@item}'"
return false
# break if property doesn't exist
lastPart = utils.last parts
unless lastPart of obj
log.warn "Attribute '#{prop}' doesn't exist in item '#{@item}'"
return false
# set value
internalProp = getInternalProperty lastPart
# connect a function to the signal
if obj[internalProp] is undefined and typeof obj[lastPart] is 'function' and obj[lastPart].connect
if typeof oldVal is 'function'
obj[lastPart].disconnect oldVal
if typeof val is 'function'
obj[lastPart] val
# omit 'null' values for primitive properties;
# all props from string interpolation may be equal 'null' by default
else if val isnt null or typeof internalProp is 'object'
isEnabled = propsClass.running
if isEnabled
propsClass.disable()
propsClass.changes.setAttribute getPropertyPath(prop), val
if isEnabled
propsClass.enable()
return true
###
Updates item classes comparing changes between given values.
Classes order is preserved.
###
syncClassProp: (val, oldVal) ->
{item} = @
{classes} = item
if typeof val is 'string' and val isnt ''
newClasses = val.split(' ')
# check removed values
if typeof oldVal is 'string' and oldVal isnt ''
oldClasses = oldVal.split ' '
for name in oldClasses when name isnt ''
if not newClasses or not utils.has(newClasses, name)
classes.remove name
# add new classes
if newClasses
prevIndex = -1
for name, i in newClasses when name isnt ''
index = classes.index name
if prevIndex is -1 and index is -1
index = classes.length
classes.append name
else if index isnt prevIndex + 1
if index isnt -1
classes.pop index
if prevIndex > index
prevIndex--
index = prevIndex + 1
classes.insert index, name
prevIndex = index
return
findAndSetLinkUri: ->
assert.isDefined @item
{node} = @
tmp = node
while tmp
if tmp._documentStyle and tmp isnt node
break
if tmp.name is 'a' and tmp.props.has('href')
@setLinkUri tmp.props.href
break
tmp = tmp.parent
return
setLinkUri: (val) ->
if @item
@item.linkUri = val + ''
return
findAndSetVisibility: ->
assert.isDefined @item
{node} = @
tmp = node
while tmp
if tmp._documentStyle and tmp isnt node
break
unless tmp.visible
@setVisibility false
break
tmp = tmp.parent
return
###
Sets the item visibility.
###
setVisibility: (val) ->
assert.isBoolean val
if @item
@propsClass ?= @createClassWithPriority PROPS_CLASS_PRIORITY
@propsClass.disable()
@propsClass.changes._attributes.visible = val
@propsClass.enable()
return
###
Creates and initializes renderer item based on the node 'n-style' attribute.
The style node 'n-style' attribute may be:
- a 'Renderer.Item' instance - item will be used as is,
- a string in format:
- 'renderer:Type' where the 'Type' is a Renderer class;
a new item will be created,
- 'styles:File:Style:SubId' where the 'File' is a property
from 'styles' passed to initialize this file,
'Style' is a main item id in File,
the 'SubId' is a main item children id;
an item from the first parent with style 'styles:File:Style' will be used,
- 'styles:File:Style' where 'SubId' is unknown and a main item
from the Style will be used; matched items will be cloned;
- 'styles:File' where 'Style' is a '_main' by default;
matched items will be cloned.
The newly created or found item is initialized.
###
createItem: ->
assert.isNotDefined @item, "Can't create a style item, because it already exists"
assert.isNotDefined @node.style, "Can't create a style item, because the node already has a style"
unless windowStyle
return
{node} = @
if node instanceof Tag
id = node.props['n-style']
assert.isDefined id, "Tag must specify 'n-style' prop to create an item for it"
else if node instanceof Text
id = Renderer.Text.New()
# use an item from attribute
if id instanceof Renderer.Item
@item = id
@isAutoParent = not id.parent
# create an item from styles
else if /^styles\:/.test(id)
[_, file, style, subid] = id.split(':')
style ?= '_main'
if subid
parentId = "styles:#{file}:#{style}"
parent = @parent
loop
if parent and parent.node.props['n-style'] is parentId
scope = parent.scope
@item = scope.objects[subid]
else if not parent?.scope and file in ['view', '__view__']
@item = windowStyle.objects[subid]
if @item or not parent
break
parent = parent.parent
unless @item
log.warn "Can't find `#{id}` style item"
return
else
@scope = styles[file]?[style]?.getComponent()
if @scope
@item = @scope.item
else
log.warn "Style file `#{id}` can't be find"
# create an item from renderer
else if /^renderer\:/.test(id)
[_, type] = id.split(':')
assert.isDefined Renderer[type], "'#{id}' is not defined in Renderer"
@item = Renderer[type].New()
else
throw new Error "Unexpected n-style; '#{id}' given"
if @item
@isAutoParent = not @item.parent
# set visibility
@findAndSetVisibility()
# set text
if @textObject = @getTextObject()
@updateText()
# set linkUri
@findAndSetLinkUri()
if node instanceof Tag
# set props
if @props
@propsClass = @createClassWithPriority ATTRS_CLASS_PRIORITY
for key of @props
@setProp key, node.props[key], null
@propsClass.enable()
# set class prop
if classAttr = node.props['class']
@syncClassProp classAttr, ''
# find parent if necessary or only update index for fixed parents
if @isAutoParent
@findItemParent()
else
@findItemIndex()
# set node style
node.style = @item
# set style node
@item.document.node = node
return
###
Create an item for this style and for children recursively.
Item may not be created if it won't be used, that is:
- parent is a text style.
###
createItemDeeply: ->
@createItem()
# optimization - don't create styles inside the text style
unless @textObject
for child in @children
child.createItemDeeply()
return
findItemParent: ->
if not @isAutoParent
return false
{node} = @
tmpNode = node.parent
while tmpNode
if style = tmpNode._documentStyle
if item = style.item
@item.parent = item
break
tmpNode = tmpNode.parent
unless item
@item.parent = null
return false
return true
setItemParent: (val) ->
if @isAutoParent and @item
@item.parent = val
@findItemIndex()
return
findItemWithParent = (item, parent) ->
tmp = item
while tmp and (tmpParent = tmp._parent)
if tmpParent is parent
return tmp
tmp = tmpParent
return
findItemIndex: ->
{node, item} = @
unless parent = item.parent
return false
tmpIndexNode = node
parent = parent._children?._target or parent
tmpSiblingNode = tmpIndexNode
# by parents
while tmpIndexNode
# by previous sibling
while tmpSiblingNode
if tmpSiblingNode isnt node
# get sibling item
tmpSiblingDocStyle = tmpSiblingNode._documentStyle
if tmpSiblingDocStyle and tmpSiblingDocStyle.isAutoParent
if tmpSiblingItem = tmpSiblingDocStyle.item
if tmpSiblingTargetItem = findItemWithParent(tmpSiblingItem, parent)
if item isnt tmpSiblingTargetItem
item.previousSibling = tmpSiblingTargetItem
return true
# check children of special tags
else unless tmpSiblingDocStyle
tmpIndexNode = tmpSiblingNode
tmpSiblingNode = utils.last tmpIndexNode.children
continue
# check previous sibling
tmpSiblingNode = tmpSiblingNode._previousSibling
# no sibling found, but parent is styled
if tmpIndexNode isnt node and tmpIndexNode.style
return true
# check parent
if tmpSiblingNode = tmpIndexNode._previousSibling
tmpIndexNode = tmpSiblingNode
else if tmpIndexNode = tmpIndexNode._parent
# out of scope
if tmpIndexNode._documentStyle?.item is parent
# no styled previous siblings found;
# add item as the first node defined element
targetChild = null
child = parent.children.firstChild
while child
if child isnt item and child.document.node
targetChild = child
break
child = child.nextSibling
item.nextSibling = targetChild
return true
return false
clone: (originalFile, file) ->
clone = new Style
clone.file = file
node = clone.node = originalFile.node.getCopiedElement @node, file.node
node._documentStyle = clone
if node instanceof Tag
styleAttr = node.props['n-style']
clone.isAutoParent = not /^styles:(.+?)\:(.+?)\:(.+?)$/.test(styleAttr)
# set props
if @props
clone.props = @props
# clone children
for child in @children
child = child.clone originalFile, file
child.parent = clone
clone.children.push child
# create item recursively
if not @parent
clone.createItemDeeply()
clone
toJSON: do ->
callToJSON = (elem) ->
elem.toJSON()
(key, arr) ->
unless arr
arr = new Array JSON_ARGS_LENGTH
arr[0] = JSON_CTOR_ID
arr[JSON_NODE] = @node.getAccessPath @file.node
arr[JSON_PROPS] = @props
arr[JSON_CHILDREN] = @children.map callToJSON
arr
Style