yjs/lib/Types/Xml.coffee
2015-02-26 21:28:06 +00:00

493 lines
14 KiB
CoffeeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class YXml
constructor: (tag_or_dom, attributes = {})->
if not tag_or_dom?
# nop
else if tag_or_dom.constructor is String
tagname = tag_or_dom
@_xml = {}
@_xml.children = []
#TODO: How to force the user to specify parameters?
#if not tagname?
# throw new Error "You must specify a tagname"
@_xml.tagname = tagname
if attributes.constructor isnt Object
throw new Error "The attributes must be specified as a Object"
for a_name, a of attributes
if a.constructor isnt String
throw new Error "The attributes must be of type String!"
@_xml.attributes = attributes
@_xml.classes = {}
_classes = @_xml.attributes.class
delete @_xml.attributes.class
if _classes?
for c_name, c in _classes.split(" ")
if c.length > 0
@_xml.classes[c_name] = c
undefined
else if tag_or_dom instanceof Element
@_dom = tag_or_dom
@_xml = {}
_name: "Xml"
_getModel: (Y, ops)->
if not @_model?
if @_dom?
@_xml.tagname = @_dom.tagName.toLowerCase()
@_xml.attributes = {}
@_xml.classes = {}
for attribute in @_dom.attributes
if attribute.name is "class"
for c in attribute.value.split(" ")
@_xml.classes[c] = true
else
@_xml.attributes[attribute.name] = attribute.value
@_xml.children = []
for child in @_dom.childNodes
if child.nodeType is child.TEXT_NODE
@_xml.children.push child.textContent
else
@_xml.children.push(new YXml(child))
@_model = new ops.MapManager(@).execute()
@_model.val("attributes", new Y.Object(@_xml.attributes))
@_model.val("classes", new Y.Object(@_xml.classes))
@_model.val("tagname", @_xml.tagname)
@_model.val("children", new Y.List(@_xml.children))
if @_xml.parent?
@_model.val("parent", @_xml.parent)
if @_dom?
@getDom() # two way bind dom to this xml type
@_setModel @_model
@_model
_setModel: (@_model)->
@_model.observe (events)->
for event in events
if event.name is "parent" and event.type isnt "add"
parent = event.oldValue
children = parent._model.val("children")?.val()
if children?
for c,i in children
if c is @
parent._model.val("children").delete i
break
delete @_xml
_setParent: (parent)->
if parent instanceof YXml
if @_model?
@remove()
@_model.val("parent", parent)
else
@_xml.parent = parent
else
throw new Error "parent must be of type Y.Xml!"
toString: ()->
xml = "<"+@_model.val("tagname")
for name, value of @attr()
xml += " "+name+'="'+value+'"'
xml += ">"
for child in @_model.val("children").val()
xml += child.toString()
xml += '</'+@_model.val("tagname")+'>'
xml
#
# Get/set the attribute(s) of this element.
# .attr()
# .attr(name)
# .attr(name, value)
#
attr: (name, value)->
if arguments.length > 1
if value.constructor isnt String
throw new Error "The attributes must be of type String!"
if name is "class"
classes = value.split(" ")
cs = {}
for c in classes
cs[c] = true
@_model.val("classes", new @_model.custom_types.Object(cs))
else
@_model.val("attributes").val(name, value)
@
else if arguments.length > 0
if name is "class"
Object.keys(@_model.val("classes").val()).join(" ")
else
@_model.val("attributes").val(name)
else
attrs = @_model.val("attributes").val()
classes = Object.keys(@_model.val("classes").val()).join(" ")
if classes.length > 0
attrs["class"] = classes
attrs
#
# Adds the specified class(es) to this element
#
addClass: (names)->
for name in names.split(" ")
@_model.val("classes").val(name, true)
@
#
# Insert content, specified by the parameter, after this element
# .after(content [, content])
#
after: ()->
parent = @_model.val("parent")
if not parent?
throw new Error "This Xml Element must not have siblings! (for it does not have a parent)"
# find the position of this element
for c,position in parent.getChildren()
if c is @
break
contents = []
for content in arguments
if content instanceof YXml
content._setParent(@_model.val("parent"))
else if content.constructor isnt String
throw new Error "Y.Xml.after expects instances of YXml or String as a parameter"
contents.push content
parent._model.val("children").insertContents(position+1, contents)
#
# Insert content, specified by the parameter, to the end of this element
# .append(content [, content])
#
append: ()->
for content in arguments
if content instanceof YXml
content._setParent(@)
else if content.constructor isnt String
throw new Error "Y.Xml.after expects instances of YXml or String as a parameter"
@_model.val("children").push(content)
@
#
# Insert content, specified by the parameter, after this element
# .after(content [, content])
#
before: ()->
parent = @_model.val("parent")
if not parent?
throw new Error "This Xml Element must not have siblings! (for it does not have a parent)"
# find the position of this element
for c,position in parent.getChildren()
if c is @
break
contents = []
for content in arguments
if content instanceof YXml
content._setParent(@_model.val("parent"))
else if content.constructor isnt String
throw new Error "Y.Xml.after expects instances of YXml or String as a parameter"
contents.push content
parent._model.val("children").insertContents(position, contents)
#
# Remove all child nodes of the set of matched elements from the DOM.
# .empty()
#
empty: ()->
# TODO: do it like this : @_model.val("children", new Y.List())
children = @_model.val("children")
for child in children.val()
if child.constructor is String
children.delete(0)
else
child.remove()
#
# Determine whether any of the matched elements are assigned the given class.
# .hasClass(className)
#
hasClass: (className)->
if @_model.val("classes").val(className)?
true
else
false
#
# Insert content, specified by the parameter, to the beginning of this element.
# .prepend(content [, content])
#
prepend: ()->
for content in arguments
if content instanceof YXml
content._setParent(@)
else if content.constructor isnt String
throw new Error "Y.Xml.after expects instances of YXml or String as a parameter"
@_model.val("children").insert(0, content)
@
#
# Remove this element from the DOM
# .remove()
#
remove: ()->
parent = @_model.delete("parent")
@
#
# Remove an attribute from this element
# .removeAttr(attrName)
#
removeAttr: (attrName)->
if attrName is "class"
@_model.val("classes", new @_model.custom_types.Object())
else
@_model.val("attributes").delete(attrName)
@
#
# Remove a single class, multiple classes, or all classes from this element
# .removeClass([className])
#
removeClass: ()->
if arguments.length is 0
@_model.val("classes", new @_model.custom_types.Object())
else
for className in arguments
@_model.val("classes").delete(className)
@
#
# Add or remove one or more classes from this element,
# depending on either the classs presence or the value of the state argument.
# .toggleClass([className])
#
toggleClass: ()->
for className in arguments
classes = @_model.val("classes")
if classes.val(className)?
classes.delete(className)
else
classes.val(className, true)
@
#
# Get the parent of this Element
# @Note: Every XML element can only have one parent
# .getParent()
#
getParent: ()->
@_model.val("parent")
#
# Get all the children of this XML Element as an Array
# @Note: The children are either of type Y.Xml or String
# .getChildren()
#
getChildren: ()->
@_model.val("children").val()
getPosition: ()->
parent = @_model.val("parent")
if parent?
for c,i in parent._model.val("children").val()
if c is @
return i
throw new Error "This is not a child of its parent (should not happen in Y.Xml!)"
else
null
getDom: ()->
if not @_dom?
@_dom = document.createElement(@_model.val("tagname"))
# set the attributes _and_ the classes (@see .attr())
for attr_name, attr_value of @attr()
@_dom.setAttribute attr_name, attr_value
for child,i in @getChildren()
if child.constructor is String
dom = document.createTextNode child
else
dom = child.getDom()
@_dom.insertBefore dom
that = @
if (not @_dom._y_xml?)
@_dom._y_xml = @
initialize_proxies.call @
@_model.val("children").observe (events)->
for event in events
if event.type is "insert"
if event.value.constructor is String
newNode = document.createTextNode(event.value)
else
newNode = event.value.getDom()
event.value._setParent that
children = that._dom.childNodes
if children.length is event.position
rightNode = null
else
rightNode = children[event.position]
dont_proxy ()->
that._dom.insertBefore newNode, rightNode
else if event.type is "delete"
deleted = event.oldValue.getDom()
dont_proxy ()->
that._dom.removeChild deleted
@_model.val("attributes").observe (events)->
for event in events
if event.type is "add" or event.type is "update"
newval = event.object.val(event.name)
dont_proxy ()->
that._dom.setAttribute event.name, newval
else if event.type is "delete"
dont_proxy ()->
that._dom.removeAttribute event.name
setClasses = ()->
that._model.val("classes").observe (events)->
for event in events
if event.type is "add" or event.type is "update"
dont_proxy ()->
that._dom.classList.add event.name # classes are stored as the keys
else if event.type is "delete"
dont_proxy ()->
that._dom.classList.remove event.name
setClasses()
@_model.observe (events)->
for event in events
if event.type is "add" or event.type is "update"
dont_proxy ()->
classes = that.attr("class")
if (not classes?) or classes is ""
that._dom.removeAttribute "class"
else
that._dom.setAttribute "class", that.attr("class")
setClasses()
@_dom
proxies_are_initialized = false
# some dom implementations may call another dom.method that simulates the behavior of another.
# For example xml.insertChild(dom) , wich inserts an element at the end, and xml.insertAfter(dom,null) wich does the same
# But Y's proxy may be called only once!
proxy_token = false
dont_proxy = (f)->
proxy_token = true
try
f()
catch e
proxy_token = false
throw new Error e
proxy_token = false
initialize_proxies = ()->
_proxy = (f_name, f, source = Element.prototype, y)->
old_f = source[f_name]
source[f_name] = ()->
if (not (y? or @_y_xml?)) or proxy_token
old_f.apply this, arguments
else if @_y_xml?
f.apply @_y_xml, arguments
else
f.apply y, arguments
that = this
f_add = (c)->
that.addClass c
_proxy "add", f_add, @_dom.classList, @
f_remove = (c)->
that.removeClass c
_proxy "remove", f_remove, @_dom.classList, @
@_dom.__defineSetter__ 'className', (val)->
that.attr('class', val)
@_dom.__defineGetter__ 'className', ()->
that.attr('class')
@_dom.__defineSetter__ 'textContent', (val)->
# remove all nodes
that.empty()
# insert word content
if val isnt ""
that.append val
if proxies_are_initialized
return
proxies_are_initialized = true
# the following methods are initialized on prototypes and therefore they need to be written only once!
insertBefore = (insertedNode_s, adjacentNode)->
if adjacentNode?
pos = adjacentNode._y_xml.getPosition()
else
pos = @getChildren().length
new_childs = []
if insertedNode_s.nodeType is insertedNode_s.DOCUMENT_FRAGMENT_NODE
child = insertedNode_s.firstChild
while child?
new_childs.push child
child = child.nextSibling
else
new_childs.push insertedNode_s
new_childs = new_childs.map (child)->
if child._y_xml?
child._y_xml
else if child.nodeType == child.TEXT_NODE
child.textContent
else
new YXml(child)
@_model.val("children").insertContents pos, new_childs
_proxy 'insertBefore', insertBefore
_proxy 'appendChild', insertBefore
_proxy 'removeAttribute', (name)->
@removeAttr name
_proxy 'setAttribute', (name, value)->
@attr name, value
removeChild = (node)->
node._y_xml.remove()
_proxy 'removeChild', removeChild
replaceChild = (insertedNode, replacedNode)-> # TODO: handle replace with replace behavior...
insertBefore.call this, insertedNode, replacedNode
removeChild.call this, replacedNode
_proxy 'replaceChild', replaceChild
if window?
if window.Y?
window.Y.Xml = YXml
else
throw new Error "You must first import Y!"
if module?
module.exports = YXml