UNPKG

neft

Version:

Universal Platform

914 lines (742 loc) 33.2 kB
# 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