neft
Version:
Universal Platform
914 lines (742 loc) • 33.2 kB
text/coffeescript
# Class
'use strict'
assert = require 'src/assert'
utils = require 'src/utils'
signal = require 'src/signal'
log = require 'src/log'
List = require 'src/list'
TagQuery = require 'src/document/element/element/tag/query'
log = log.scope 'Rendering', 'Class'
{emitSignal} = signal.Emitter
module.exports = (Renderer, Impl, itemUtils) ->
class ChangesObject
constructor: ->
@_links = []
@_attributes = {}
@_functions = []
@_bindings = {}
setAttribute: (prop, val) ->
@_attributes[prop] = val
if val instanceof Renderer.Component.Link
@_links.push val
return
setFunction: (prop, val) ->
boundFunc = (arg1, arg2) ->
if @_component
arr = @_component.objectsOrderSignalArr
arr[arr.length - 2] = arg1
arr[arr.length - 1] = arg2
val.apply @_target, arr
else
val.call @_target, arg1, arg2
@_functions.push prop, boundFunc
return
setBinding: (prop, val) ->
@_attributes[prop] = val
@_bindings[prop] = true
return
class Class extends Renderer.Extension
@__name__ = 'Class'
## *Renderer.Class* Class.New([*Component* component, *Object* options])
@New = (component, opts) ->
item = new Class
itemUtils.Object.initialize item, component, opts
item
## *Class* Class::constructor() : *Renderer.Extension*
lastUid = 0
constructor: ->
super()
@_classUid = (lastUid++)+''
@_priority = 0
@_inheritsPriority = 0
@_nestingPriority = 0
@_name = ''
@_changes = null
@_document = null
@_children = null
## *String* Class::name
This property is used in the *Item*::classes list
to identify various classes.
## *Signal* Class::onNameChange(*String* oldValue)
itemUtils.defineProperty
constructor: @
name: 'name'
developmentSetter: (val) ->
assert.isString val
assert.notLengthOf val, 0
setter: (_super) -> (val) ->
{target, name} = @
if name is val
return
if target
if target.classes.has(name)
@disable()
target._classExtensions[name] = null
target._classExtensions[val] = @
_super.call @, val
if target
if val
target._classExtensions ?= {}
if target._classes?.has(val)
@enable()
return
## *Item* Class::target
Reference to the *Item* on which this class has effects.
If state is created inside the *Item*, this property is set automatically.
## *Signal* Class::onTargetChange(*Item* oldValue)
itemUtils.defineProperty
constructor: @
name: 'target'
developmentSetter: (val) ->
if val?
assert.instanceOf val, itemUtils.Object
setter: (_super) -> (val) ->
oldVal = @_target
{name} = @
if oldVal is val
return
@disable()
if oldVal
utils.remove oldVal._extensions, @
if @_running and not @_document?._query
unloadObjects @, @_component, oldVal
if name
if oldVal
oldVal._classExtensions[name] = null
if val
val._classExtensions ?= {}
val._classExtensions[name] = @
_super.call @, val
if val
val._extensions.push @
if val._classes?.has(name) or @_when or (@_priority isnt -1 and @_component.ready and !@_name and !@_bindings?.when and !@_document?._query)
@enable()
return
## *Object* Class::changes
This objects contains all properties to change on the target item.
It accepts bindings and listeners as well.
utils.defineProperty @::, 'changes', null, ->
@_changes ||= new ChangesObject
, (obj) ->
assert.isObject obj
{changes} = @
for prop, val of obj
if typeof val is 'function'
changes.setFunction prop, val
else if Array.isArray(val) and val.length is 2 and typeof val[0] is 'function' and Array.isArray(val[1])
changes.setBinding prop, val
else
changes.setAttribute prop, val
return
## *Integer* Class::priority = `0`
## *Signal* Class::onPriorityChange(*Integer* oldValue)
itemUtils.defineProperty
constructor: @
name: 'priority'
defaultValue: 0
developmentSetter: (val) ->
assert.isInteger val
setter: (_super) -> (val) ->
_super.call @, val
updatePriorities @
return
## *Boolean* Class::when
Indicates whether the class is active or not.
When it's `true`, this state is appended on the
end of the *Item*::classes list.
Mostly used with bindings.
```javascript
Grid {
columns: 2
// reduce to one column if the view width is lower than 500 pixels
Class {
when: view.width < 500
changes: {
columns: 1
}
}
}
```
## *Signal* Class::onWhenChange(*Boolean* oldValue)
enable: ->
docQuery = @_document?._query
if @_running or not @_target or docQuery
if docQuery
for classElem in @_document._classesInUse
classElem.enable()
return
super()
updateTargetClass saveAndEnableClass, @_target, @
unless @_document?._query
loadObjects @, @_component, @_target
return
disable: ->
if not @_running or not @_target
if @_document and @_document._query
for classElem in @_document._classesInUse
classElem.disable()
return
super()
unless @_document?._query
unloadObjects @, @_target
updateTargetClass saveAndDisableClass, @_target, @
return
## *Object* Class::children
utils.defineProperty @::, 'children', null, ->
@_children ||= new ChildrenObject(@)
, (val) ->
{children} = @
# clear
length = children.length
while length--
children.pop length
if val
assert.isArray val
for child in val
children.append child
return
class ChildrenObject
## *Integer* Class::children.length = `0`
constructor: (ref) ->
@_ref = ref
@length = 0
## *Object* Class::children.append(*Object* value)
append: (val) ->
assert.instanceOf val, itemUtils.Object
assert.isNot val, @_ref
unless @_ref._component.isClone
@_ref._component.disabledObjects[val.id] = true
if val instanceof Class
updateChildPriorities @_ref, val
@[@length++] = val
val
## *Object* Class::children.pop(*Integer* index)
pop: (i=@length-1) ->
assert.operator i, '>=', 0
assert.operator i, '<', @length
oldVal = @[i]
delete @[i]
--@length
oldVal
clone: (component) ->
clone = cloneClassWithNoDocument.call @, component
if doc = @_document
cloneDoc = clone.document
cloneDoc.query = doc.query
for name, arr of doc._signals
cloneDoc._signals[name] = utils.clone arr
clone
loadObjects = (classElem, component, item) ->
if children = classElem._children
for child in children
if child instanceof Renderer.Item
child.parent ?= item
else
if child instanceof Class
updateChildPriorities classElem, child
child.target ?= item
return
unloadObjects = (classElem, item) ->
if children = classElem._children
for child in children
if child instanceof Renderer.Item
if child.parent is item
child.parent = null
else
if child.target is item
child.target = null
return
updateChildPriorities = (parent, child) ->
child._inheritsPriority = parent._inheritsPriority + parent._priority
child._nestingPriority = parent._nestingPriority + 1 + (child._document?._priority or 0)
updatePriorities child
return
updatePriorities = (classElem) ->
# refresh if needed
if classElem._running and ifClassListWillChange(classElem)
target = classElem._target
updateTargetClass disableClass, target, classElem
updateClassList target
updateTargetClass enableClass, target, classElem
# children
if children = classElem._children
for child in children
if child instanceof Class
updateChildPriorities classElem, child
# document
if document = classElem._document
{_inheritsPriority, _nestingPriority} = classElem
for child in document._classesInUse
child._inheritsPriority = _inheritsPriority
child._nestingPriority = _nestingPriority
updatePriorities child
for child in document._classesPool
child._inheritsPriority = _inheritsPriority
child._nestingPriority = _nestingPriority
return
ifClassListWillChange = (classElem) ->
target = classElem._target
classList = target._classList
index = classList.indexOf classElem
if index > 0 and classListSortFunc(classElem, classList[index-1]) < 0
return true
if index < classList.length-1 and classListSortFunc(classElem, classList[index+1]) > 0
return true
false
classListSortFunc = (a, b) ->
(b._priority + b._inheritsPriority) - (a._priority + a._inheritsPriority) or
(b._nestingPriority) - (a._nestingPriority)
updateClassList = (item) ->
item._classList.sort classListSortFunc
cloneClassChild = (classElem, component, child) ->
clone = component.cloneRawObject child
cloneComp = clone._component.belongsToComponent or clone._component
cloneComp.onObjectChange ?= signal.create()
if clone.id
cloneComp.setObjectById clone, clone.id
cloneComp.initObjects()
if component.objects[clone.id]
component.setObjectById clone, clone.id
return clone
cloneClassWithNoDocument = (component) ->
clone = Class.New component
clone.id = @id
clone._classUid = @_classUid
clone._name = @_name
clone._priority = @_priority
clone._inheritsPriority = @_inheritsPriority
clone._nestingPriority = @_nestingPriority
clone._changes = @_changes
if @_bindings
for prop, val of @_bindings
clone.createBinding prop, val, component
# clone children
if children = @_children
for child, i in children
childClone = cloneClassChild clone, component, child
clone.children.append childClone
if component.isDeepClone
# clone links
if (changes = @_changes)
for link in changes._links
linkClone = cloneClassChild clone, component, link.getItem(@_component)
clone
{splitAttribute, getObjectByPath} = itemUtils
setAttribute = (item, attr, val) ->
path = splitAttribute attr
if object = getObjectByPath(item, path)
object[path[path.length - 1]] = val
return
saveAndEnableClass = (item, classElem) ->
item._classList.unshift classElem
if ifClassListWillChange(classElem)
updateClassList item
enableClass item, classElem
saveAndDisableClass = (item, classElem) ->
disableClass item, classElem
utils.remove item._classList, classElem
ATTRS_ALIAS_DEF = [
['x', 'anchors.left', 'anchors.right', 'anchors.horizontalCenter', 'anchors.centerIn', 'anchors.fill', 'anchors.fillWidth'],
['y', 'anchors.top', 'anchors.bottom', 'anchors.verticalCenter', 'anchors.centerIn', 'anchors.fill', 'anchors.fillHeight'],
['width', 'anchors.fill', 'anchors.fillWidth', 'layout.fillWidth'],
['height', 'anchors.fill', 'anchors.fillHeight', 'layout.fillHeight'],
['margin.horizontal', 'margin.left'],
['margin.horizontal', 'margin.right'],
['margin.vertical', 'margin.top'],
['margin.vertical', 'margin.bottom'],
['padding.horizontal', 'padding.left'],
['padding.horizontal', 'padding.right'],
['padding.vertical', 'padding.top']
['padding.vertical', 'padding.bottom']
]
ATTRS_ALIAS = Object.create null
ATTRS_ALIAS['margin'] = ['margin.left', 'margin.right', 'margin.horizontal', 'margin.top', 'margin.bottom', 'margin.vertical']
ATTRS_ALIAS['padding'] = ['padding.left', 'padding.right', 'padding.horizontal', 'padding.top', 'padding.bottom', 'padding.vertical']
ATTRS_ALIAS['alignment'] = ['alignment.horizontal', 'alignment.vertical']
do ->
for aliases in ATTRS_ALIAS_DEF
for prop in aliases
arr = ATTRS_ALIAS[prop] ?= []
for alias in aliases
if alias isnt prop
arr.push alias
return
getContainedAttributeOrAlias = (classElem, attr) ->
if changes = classElem._changes
attrs = changes._attributes
if attrs[attr] isnt undefined
return attr
else if aliases = ATTRS_ALIAS[attr]
for alias in aliases
if attrs[alias] isnt undefined
return alias
return ''
getPropertyDefaultValue = (obj, prop) ->
proto = Object.getPrototypeOf obj
innerProp = itemUtils.getInnerPropName(prop)
if innerProp of proto
proto[innerProp]
else
proto[prop]
enableClass = (item, classElem) ->
assert.instanceOf item, itemUtils.Object
assert.instanceOf classElem, Class
classList = item._classList
classListIndex = classList.indexOf classElem
classListLength = classList.length
if classListIndex is -1
return
unless changes = classElem._changes
return
attributes = changes._attributes
bindings = changes._bindings
functions = changes._functions
# functions
for attr, i in functions by 2
path = splitAttribute attr
object = getObjectByPath item, path
`//<development>`
if not object or typeof object?[path[path.length - 1]] isnt 'function'
log.error "Handler '#{attr}' doesn't exist in '#{item.toString()}', from '#{classElem.toString()}'"
`//</development>`
object?[path[path.length - 1]]? functions[i+1], classElem
# attributes
for attr, val of attributes
path = null
writeAttr = true
alias = ''
for i in [classListIndex-1..0] by -1
if getContainedAttributeOrAlias(classList[i], attr)
writeAttr = false
break
if writeAttr
# unset alias
for i in [classListIndex+1...classListLength] by 1
if (alias = getContainedAttributeOrAlias(classList[i], attr)) and alias isnt attr
path = splitAttribute alias
object = getObjectByPath item, path
lastPath = path[path.length - 1]
unless object
continue
defaultValue = getPropertyDefaultValue object, lastPath
defaultIsBinding = !!classList[i].changes._bindings[alias]
if defaultIsBinding
object.createBinding lastPath, null, classElem._component, item
object[lastPath] = defaultValue
break
# set new attribute
if attr isnt alias or not path
path = splitAttribute attr
lastPath = path[path.length - 1]
object = getObjectByPath item, path
# create property on demand
if object instanceof itemUtils.CustomObject and not (lastPath of object)
itemUtils.Object.createProperty object._ref, lastPath
else
`//<development>`
if not object or not (lastPath of object)
log.error "Attribute '#{attr}' doesn't exist in '#{item.toString()}', from '#{classElem.toString()}'"
continue
`//</development>`
unless object
continue
if bindings[attr]
object.createBinding lastPath, val, classElem._component, item
else
if object._bindings?[lastPath]
object.createBinding lastPath, null, classElem._component, item
if val instanceof Renderer.Component.Link
object[lastPath] = val.getItem classElem._component
else
object[lastPath] = val
return
disableClass = (item, classElem) ->
assert.instanceOf item, itemUtils.Object
assert.instanceOf classElem, Class
classList = item._classList
classListIndex = classList.indexOf classElem
classListLength = classList.length
if classListIndex is -1
return
unless changes = classElem._changes
return
attributes = changes._attributes
bindings = changes._bindings
functions = changes._functions
# functions
for attr, i in functions by 2
path = splitAttribute attr
object = getObjectByPath item, path
object?[path[path.length - 1]]?.disconnect functions[i+1], classElem
# attributes
for attr, val of attributes
path = null
restoreDefault = true
alias = ''
for i in [classListIndex-1..0] by -1
# BUG: undefined on QML (potential Array::sort bug)
unless classList[i]
continue
if getContainedAttributeOrAlias(classList[i], attr)
restoreDefault = false
break
if restoreDefault
# get default value
defaultValue = undefined
defaultIsBinding = false
for i in [classListIndex+1...classListLength] by 1
if alias = getContainedAttributeOrAlias(classList[i], attr)
defaultValue = classList[i].changes._attributes[alias]
defaultIsBinding = !!classList[i].changes._bindings[alias]
break
alias ||= attr
# restore attribute
if !!bindings[attr]
path = splitAttribute attr
object = getObjectByPath item, path
lastPath = path[path.length - 1]
unless object
continue
object.createBinding lastPath, null, classElem._component, item
# set default value
if attr isnt alias or not path
path = splitAttribute alias
object = getObjectByPath item, path
lastPath = path[path.length - 1]
unless object
continue
`//<development>`
unless lastPath of object
continue
`//</development>`
if defaultIsBinding
object.createBinding lastPath, defaultValue, classElem._component, item
else
if defaultValue is undefined
defaultValue = getPropertyDefaultValue object, lastPath
object[lastPath] = defaultValue
return
runQueue = (target) ->
classQueue = target._classQueue
[func, target, classElem] = classQueue
func target, classElem
classQueue.shift()
classQueue.shift()
classQueue.shift()
if classQueue.length > 0
runQueue target
return
updateTargetClass = (func, target, classElem) ->
classQueue = target._classQueue
classQueue.push func, target, classElem
if classQueue.length is 3
runQueue target
return
## *Object* Class::document
class ClassChildDocument
constructor: (parent) ->
@_ref = parent._ref
@_parent = parent
@_multiplicity = 0
Object.preventExtensions @
class ClassDocument extends itemUtils.DeepObject
@__name__ = 'ClassDocument'
## *Signal* Class::onDocumentChange(*Object* document)
itemUtils.defineProperty
constructor: Class
name: 'document'
valueConstructor: @
onTargetChange = (oldVal) ->
if oldVal
oldVal.document.onNodeChange.disconnect @reloadQuery, @
if val = @_ref._target
val.document.onNodeChange @reloadQuery, @
if oldVal isnt val
@reloadQuery()
return
constructor: (ref) ->
@_query = ''
@_classesInUse = []
@_classesPool = []
@_nodeWatcher = null
@_priority = 0
super ref
ref.onTargetChange onTargetChange, @
onTargetChange.call @, ref._target
## *Signal* Class::document.onNodeAdd(*Element* node)
signal.Emitter.createSignal @, 'onNodeAdd'
## *Signal* Class::document.onNodeRemove(*Element* node)
signal.Emitter.createSignal @, 'onNodeRemove'
## *String* Class::document.query
## *Signal* Class::document.onQueryChange(*String* oldValue)
itemUtils.defineProperty
constructor: @
name: 'query'
defaultValue: ''
namespace: 'document'
parentConstructor: ClassDocument
developmentSetter: (val) ->
assert.isString val
setter: (_super) -> (val) ->
assert.notOk @_parent
if @_query is val
return
unless @_query
unloadObjects @, @_target
_super.call @, val
@reloadQuery()
# update priority
if @_ref._priority < 1
# TODO
# while calculating selector priority we take only the first query
# as a priority for the whole selector;
# to fix this we can split selector with multiple queries ('a, b')
# into separated class instances
cmdLen = TagQuery.getSelectorPriority val, 0, 1
oldPriority = @_priority
@_priority = cmdLen
@_ref._nestingPriority += cmdLen - oldPriority
updatePriorities @_ref
unless val
loadObjects @, @_component, @_target
return
getChildClass = (style, parentClass) ->
for classElem in style._extensions
if classElem instanceof Class
if classElem._document?._parent is parentClass
return classElem
return
connectNodeStyle = (style) ->
# omit duplications
uid = @_ref._classUid
for classElem in style._extensions
if classElem instanceof Class
if classElem isnt @_ref and classElem._classUid is uid and classElem._document instanceof ClassChildDocument
classElem._document._multiplicity++
return
# get class
unless classElem = @_classesPool.pop()
newComp = @_ref._component.cloneComponentObject()
classElem = cloneClassWithNoDocument.call @_ref, newComp
classElem._document = new ClassChildDocument @
# save
@_classesInUse.push classElem
classElem.target = style
# run if needed
if not classElem._bindings?.when
classElem.enable()
return
disconnectNodeStyle = (style) ->
unless classElem = getChildClass(style, @)
return
if classElem._document._multiplicity > 0
classElem._document._multiplicity--
return
classElem.target = null
utils.remove @_classesInUse, classElem
@_classesPool.push classElem
return
onNodeStyleChange = (oldVal, val) ->
if oldVal
disconnectNodeStyle.call @, oldVal
if val
connectNodeStyle.call @, val
return
onNodeAdd = (node) ->
node.onStyleChange onNodeStyleChange, @
if style = node._style
connectNodeStyle.call @, style
emitSignal @, 'onNodeAdd', node
return
onNodeRemove = (node) ->
node.onStyleChange.disconnect onNodeStyleChange, @
if style = node._style
disconnectNodeStyle.call @, style
emitSignal @, 'onNodeRemove', node
return
reloadQuery: ->
# remove old
@_nodeWatcher?.disconnect()
@_nodeWatcher = null
while classElem = @_classesInUse.pop()
classElem.target = null
@_classesPool.push classElem
# add new ones
if (query = @_query) and (target = @_ref.target) and (node = target.document.node) and node.watch
watcher = @_nodeWatcher = node.watch query
watcher.onAdd onNodeAdd, @
watcher.onRemove onNodeRemove, @
return
## *List* Item::classes
Classes at the end of the list have the highest priority.
This property has a setter, which accepts a string and an array of strings.
## *Signal* Item::onClassesChange(*String* added, *String* removed)
normalizeClassesValue = (val) ->
if typeof val is 'string'
if val.indexOf(',') isnt -1
val = val.split ','
else
val = val.split ' '
else if val instanceof List
val = val.items()
val
class ClassesList extends List
constructor: ->
super()
utils.defineProperty @::, 'append', null, do (_super = @::append) ->
-> _super
, (val) ->
val = normalizeClassesValue val
if Array.isArray(val)
for name in val
if name = name.trim()
@append name
return
utils.defineProperty @::, 'remove', null, do (_super = @::remove) ->
-> _super
, (val) ->
val = normalizeClassesValue val
if Array.isArray(val)
for name in val
if name = name.trim()
@remove name
return
Renderer.onReady ->
itemUtils.defineProperty
constructor: Renderer.Item
name: 'classes'
defaultValue: null
getter: do ->
onChange = (oldVal, index) ->
val = @_classes.get(index)
onPop.call @, oldVal, index
onInsert.call @, val, index
@onClassesChange.emit val, oldVal
return
onInsert = (val, index) ->
@_classExtensions[val]?.enable()
@onClassesChange.emit val
return
onPop = (oldVal, index) ->
unless @_classes.has(oldVal)
@_classExtensions[oldVal]?.disable()
@onClassesChange.emit null, oldVal
return
(_super) -> ->
unless @_classes
@_classExtensions ?= {}
list = @_classes = new ClassesList
list.onChange onChange, @
list.onInsert onInsert, @
list.onPop onPop, @
_super.call @
setter: (_super) -> (val) ->
val = normalizeClassesValue val
{classes} = @
classes.clear()
if Array.isArray(val)
for name in val
if name = name.trim()
classes.append name
return
Class