coffeescript-ui
Version:
Coffeescript User Interface System
1,741 lines (1,340 loc) • 44.3 kB
text/coffeescript
###
* coffeescript-ui - Coffeescript User Interface System (CUI)
* Copyright (c) 2013 - 2016 Programmfabrik GmbH
* MIT Licence
* https://github.com/programmfabrik/coffeescript-ui, http://www.coffeescript-ui.org
###
class CUI.dom
@data: (node, key, data) ->
if not node
return undefined
if node.hasOwnProperty('DOM')
node = node.DOM
CUI.util.assert(node instanceof HTMLElement, "dom.data","node needs to be instance of HTMLElement", node: node)
if key == undefined
return node.__dom_data or {}
if CUI.util.isPlainObject(key)
for k, v of key
CUI.dom.data(node, k, v)
return node
if data == undefined
return node.__dom_data?[key]
if not node.__dom_data
node.__dom_data = {}
node.__dom_data[key] = data
return node
@removeData: (node, key) ->
if not node
return undefined
if node.__dom_data
delete(node.__dom_data[key])
if CUI.util.isEmptyObject(node.__dom_data)
delete(node.__dom_data)
node
# find an element starting from node, but never going
# up
@findElement: (node, selector, nodeFilter, forward=true, siblingOnly=false) ->
els = @findElements(node, selector, nodeFilter, 1, forward, siblingOnly)
if els.length == 0
return null
return els[0]
# find an element starting from node, with going up
@findNextElement: (node, selector, nodeFilter=false, forward=true, siblingOnly=true) ->
el = @findElement(node, selector, nodeFilter, forward, siblingOnly)
# console.debug "find next element", node, el
if el
return el
# find next parent node which has a sibling
while true
node = node.parentNode
# console.debug "checking next node", node
if not node
return null
if forward
sibling = node.nextElementSibling
else
sibling = node.previousElementSibling
# console.debug "sibling", forward, sibling
if sibling
break
@findNextElement(sibling, selector, nodeFilter, forward, false)
@findPreviousElement: (node, selector, nodeFilter=false) ->
@findNextElement(node, selector, nodeFilter, false)
@findNextVisibleElement: (node, selector, forward=true) ->
@findNextElement(node, selector, ((node) =>
# console.debug "node visible?", DOM.isVisible(node), node
CUI.dom.isVisible(node)), forward)
@findPreviousVisibleElement: (node, selector) ->
@findNextVisibleElement(node, selector, false)
@findNextSiblings: (node, selector, nodeFilter=false) ->
@findElements(node, selector, nodeFilter, null, true, true)
@findPreviousSiblings: (node, selector, nodeFilter=false) ->
@findElements(node, selector, nodeFilter, null, false, true)
# find the next node starting from node start
# which matches the selector
@findElements: (node=document.documentElement, selector=null, nodeFilter=false, maxEls=null, forward=true, siblingOnly=false, elements) ->
if not elements
CUI.util.assert(node instanceof HTMLElement, "DOM.findElement", "node needs to be instanceof HTMLElement.", node: node, selector: selector)
elements = []
check_node = not siblingOnly
else
check_node = true
accept_node = not nodeFilter or nodeFilter(node)
# console.debug "findElement", maxEls, node, accept_node, nodeFilter
if check_node and accept_node
# console.debug "checking node", node
if selector == null or CUI.dom.matches(node, selector)
# console.debug "node ok..."
elements.push(node)
if elements.length == maxEls
# console.debug "enough!", elements.length, maxEls
return elements
else
; # console.debug "node not ok..."
if forward
child = node.firstElementChild
sibling = node.nextElementSibling
else
child = node.lastElementChild
sibling = node.previousElementSibling
# console.debug "child/sibling", child, sibling, siblingOnly
if child and not siblingOnly and accept_node
# console.debug "dive to", node
@findElements(child, selector, nodeFilter, maxEls, forward, siblingOnly, elements)
if elements.length == maxEls
return elements
if sibling
# console.debug "sibling to", sibling
@findElements(sibling, selector, nodeFilter, maxEls, forward, siblingOnly, elements)
if elements.length == maxEls
return elements
# console.debug "nothing found, returning with", elements.length
return elements
@children: (node, filter) ->
children = []
for child, idx in node.children
if not filter or @is(child, filter)
children.push(child)
children
# finds the first element child which is not
# filtered by the optional node filter
@firstElementChild: (node, nodeFilter) ->
child = node.firstElementChild
while true
if not child
return null
if not nodeFilter or @is(child, nodeFilter)
return child
child = child.nextElementSibling
@lastElementChild: (node, nodeFilter) ->
child = node.lastElementChild
while true
if not child
return null
if not nodeFilter or @is(child, nodeFilter)
return child
child = child.previousElementSibling
@nextElementSibling: (node, nodeFilter) ->
sibling = node
while true
sibling = sibling.nextElementSibling
if not sibling
return null
if not nodeFilter or @is(sibling, nodeFilter)
return sibling
@previousElementSibling: (node, nodeFilter) ->
sibling = node
while true
sibling = sibling.previousElementSibling
if not sibling
return null
if not nodeFilter or @is(sibling, nodeFilter)
return sibling
@removeAttribute: (node, key) ->
if not node
return null
node.removeAttribute(key)
node
@setAttribute: (_node, key, value) ->
if not _node
return null
node = _node.DOM or _node
if CUI.util.isNull(value) or value == false
return @removeAttribute(node, key)
if value == true
node.setAttribute(key, key)
else
node.setAttribute(key, value)
node
@hasAttribute: (node, key) ->
if not node
return false
if node.hasOwnProperty('DOM')
node = node.DOM
node.hasAttribute(key)
@setAttributeMap: (_node, map) ->
if not _node
return null
node = _node.DOM or _node
if not map
return node
for key, value of map
CUI.dom.setAttribute(node, key, value)
node
@width: (docElem, value) ->
if docElem == document or docElem == window
if value != undefined
CUI.util.assert(false, "CUI.dom.width", "Unable to set width on a non HTMLElement", docElem: docElem)
return window.innerWidth
if value == undefined
@getDimension(docElem, "contentBoxWidth")
else
@setDimension(docElem, "contentBoxWidth", value)
@height: (docElem, value) ->
if docElem == document or docElem == window
if value != undefined
CUI.util.assert(false, "CUI.dom.height", "Unable to set width on a non HTMLElement", docElem: docElem)
return window.innerHeight
if value == undefined
@getDimension(docElem, "contentBoxHeight")
else
@setDimension(docElem, "contentBoxHeight", value)
@__append: (node, content, append=true) ->
if CUI.util.isNull(content)
return node
if CUI.util.isArray(content) or content instanceof HTMLCollection or content instanceof NodeList
idx = 0
len = content.length
while idx < len
@__append(node, content[idx], append)
if len > content.length
# leave idx == 0, list is live
else
idx++
len = content.length
return node
switch typeof(content)
when "number", "boolean"
append_node = document.createTextNode(content + "")
when "string"
append_node = document.createTextNode(content)
else
if content.hasOwnProperty('DOM')
append_node = content.DOM
if CUI.util.isNull(append_node)
return
else
append_node = content
if append
CUI.util.assert(append_node instanceof Node, "CUI.dom.append", "Content needs to be instanceof Node, string, boolean, or number.", node: append_node)
node.appendChild(append_node)
else
CUI.util.assert(append_node instanceof Node, "CUI.dom.prepend", "Content needs to be instanceof Node, string, boolean, or number.", node: append_node)
node.insertBefore(append_node, node.firstChild)
return node
@replace: (node, content) ->
if node.hasOwnProperty('DOM')
node = node.DOM
@empty(node)
@append(node, content)
@prepend: (node, content) ->
@__append(node, content, false)
@append: (node, content) ->
@__append(node, content)
# @getById: (uniqueId) ->
# dom_el = document.getElementById("cui-dom-element-"+uniqueId)
# if not dom_el
# return null
# DOM.data(dom_el, "element")
@getAttribute: (node, key) ->
if node.hasOwnProperty('DOM')
node = node.DOM
node.getAttribute(key)
@remove: (_node) ->
node = (_node?.DOM or _node)
if not node
return null
node.parentNode?.removeChild(node)
node
@empty: (node) ->
if not node
return null
if node.hasOwnProperty('DOM')
node = node.DOM
CUI.util.assert(CUI.util.isElement(node), "CUI.dom.empty", "top needs to be Element", node: node)
while last = node.lastChild
node.removeChild(last)
node
# checks if any of the classes are set
@hasClass: (element, cls) ->
if not element or not cls
return null
if element.DOM
element = element.DOM
for _cls in cls.trim().split(/\s+/)
if _cls == ""
continue
if element.classList.contains(_cls)
return true
return false
@toggleClass: (element, cls) ->
@setClass(element, cls, not @hasClass(element, cls))
@setClass: (element, cls, on_off) ->
if on_off
@addClass(element, cls)
else
@removeClass(element, cls)
return on_off
@setAria: (element, attr, value) ->
if value == true
@setAttribute(element, "aria-"+attr, "true")
else if value == false
@setAttribute(element, "aria-"+attr, "false")
else
@setAttribute(element, "aria-"+attr, value)
@addClass: (element, cls) ->
if not cls or not element
return element
for _cls in cls.trim().split(/\s+/)
if _cls == ""
continue
(element.DOM or element).classList.add(_cls)
element
@removeClass: (element, cls) ->
if not cls or not element
return element
for _cls in cls.trim().split(/\s+/)
if _cls == ""
continue
(element.DOM or element).classList.remove(_cls)
element
# returns the relative position of either
# the next scrollable parent or positioned parent
@getRelativeOffset: (node, untilElem = null, ignore_margin = false) ->
CUI.util.assert(CUI.util.isElement(node), "CUI.dom.getRelativePosition", "Node needs to HTMLElement.", node: node)
dim_node = CUI.dom.getDimensions(node)
parent = node.parentNode
if ignore_margin
margin_key_top = "viewportTop"
margin_key_left = "viewportLeft"
else
margin_key_top = "viewportTopMargin"
margin_key_left = "viewportLeftMargin"
while true
dim = CUI.dom.getDimensions(parent)
if parent == document.body or
parent == document.documentElement or
parent == document
offset =
parent: parent
top: dim_node[margin_key_top] + document.body.scrollTop
left: dim_node[margin_key_left] + document.body.scrollLeft
break
if dim.canHaveScrollbar or
parent == node.offsetParent or
parent == untilElem
offset =
parent: parent
top: dim_node[margin_key_top] - (dim.viewportTop + dim.borderTop) + dim.scrollTop
left: dim_node[margin_key_left] - (dim.viewportLeft + dim.borderTop) + dim.scrollLeft
break
parent = parent.parentNode
if not parent
break
# console.debug parent, node, offset.top, offset.left
return offset
@hasAnimatedClone: (node) ->
!!node.__clone
# if selector is set, watch matched nodes
#
@initAnimatedClone: (node, selector) ->
@removeAnimatedClone(node)
clone = node.cloneNode(true)
node.__clone = clone
if selector
watched_nodes = CUI.dom.matchSelector(node, selector)
clone.__watched_nodes = CUI.dom.matchSelector(clone, selector)
else
watched_nodes = CUI.dom.children(node)
clone.__watched_nodes = CUI.dom.children(clone)
offset = CUI.dom.getRelativeOffset(node)
if not CUI.dom.isPositioned(offset.parent)
node.__parent_saved_position = offset.parent.style.position
offset.parent.style.position = "relative"
CUI.dom.setStyle clone,
position: "absolute"
"pointer-events": "none"
top: offset.top
left: offset.left
# left: "300px"
node.style.opacity = "0"
dim = CUI.dom.getDimensions(node)
CUI.dom.addClass(clone, "cui-dom-animated-clone")
# We need this micro DIV to push the scroll height / left
div = CUI.dom.element("div", style: "position: absolute; opacity: 0; width: 1px; height: 1px;")
clone.appendChild(div)
CUI.dom.insertAfter(node, clone)
CUI.dom.setDimension(clone, "marginBoxWidth", dim.marginBoxWidth)
CUI.dom.setDimension(clone, "marginBoxHeight", dim.marginBoxHeight)
for clone_child, idx in clone.__watched_nodes
clone_child.__watched_node = watched_nodes[idx]
CUI.dom.setStyle clone_child,
position: "absolute"
margin: 0
@syncAnimatedClone(node)
node.__clone.__syncScroll = =>
div.style.top = (node.scrollHeight-1)+"px";
div.style.left = (node.scrollWidth-1)+"px";
clone.scrollTop = node.scrollTop
clone.scrollLeft = node.scrollLeft
CUI.Events.listen
type: "scroll"
instance: clone
node: node
call: =>
node.__clone.__syncScroll()
node.__clone.__syncScroll()
node
@syncAnimatedClone: (node) ->
clone = node.__clone
if not clone
return
for clone_child, idx in clone.__watched_nodes
child = clone_child.__watched_node
# We don't check if the child is still in DOM, for
# now this case is being ignored.
offset_new = @getRelativeOffset(child, node, true)
CUI.dom.setStyle clone_child,
top: offset_new.top
left: offset_new.left
node
@removeAnimatedClone: (node) ->
if node.hasOwnProperty("__parent_saved_position")
node.style.position = node.__parent_saved_position or ""
delete(node.__parent_saved_position)
if not node.__clone
return
CUI.Events.ignore(instance: node.__clone)
node.style.opacity = ""
CUI.dom.remove(node.__clone)
delete(node.__clone)
node
# sets the absolute position of an element
@setAbsolutePosition: (element, offset) ->
CUI.util.assert(CUI.util.isElement(element), "CUI.dom.setAbsolutePosition", "element needs to be an instance of HTMLElement", element: element, offset: offset)
CUI.util.assert(CUI.util.isNumber(offset?.left) and CUI.util.isNumber(offset?.top), "CUI.dom.setAbsolutePosition", "offset.left and offset.top must be >= 0", element: element, offset: offset)
# the offset needs to be corrected by the parent offset
# of our DOM element
offsetParent = element.offsetParent
if offsetParent == document.documentElement
layer_parent_offset =
top: 0
left: 0
correct_offset =
top: document.body.scrollTop
left: document.body.scrollLeft
else
dim = CUI.dom.getDimensions(offsetParent)
layer_parent_offset =
top: dim.top
left: dim.left
# position: relative/absolute anchor
# is the point between padding and border,
# we need to adjust this to the border
layer_parent_offset.top += dim.borderTopWidth
layer_parent_offset.left += dim.borderLeftWidth
correct_offset =
top: dim.scrollTop
left: dim.scrollLeft
CUI.dom.setStyle(element,
top: offset.top - layer_parent_offset.top + correct_offset.top
left: offset.left - layer_parent_offset.left + correct_offset.left
)
@__failedDOMInserts = 0
@waitForDOMRemove: (_opts) ->
opts = CUI.Element.readOpts _opts, "CUI.dom.waitForDOMRemove",
node:
mandatory: true
check: (v) ->
CUI.dom.isNode(v)
ms:
default: 200
check: (v) ->
v > 0
node = CUI.dom.getNode(opts.node)
dfr = new CUI.Deferred()
check_in_dom = =>
if not CUI.dom.isInDOM(node)
dfr.resolve()
return
CUI.setTimeout
call: check_in_dom
ms: opts.ms
track: false
check_in_dom()
dfr.promise()
@waitForDOMInsert: (_opts) ->
opts = CUI.Element.readOpts _opts, "CUI.dom.waitForDOMInsert",
node:
mandatory: true
check: (v) ->
CUI.dom.isNode(v)
node = CUI.dom.getNode(opts.node)
if CUI.dom.isInDOM(node)
return CUI.resolvedPromise(true)
dfr = new CUI.Deferred()
# If we use MutationObserver, and a not gets not inserted
# we never free memory on these nodes we wait to be inserted.
# mo = new MutationObserver =>
# console.debug "waiting for dom insert", node
# if DOM.isInDOM(node)
# if dfr.state() == "pending"
# # console.warn "inserted by mutation", node
# dfr.resolve()
# dfr.always =>
# mo.disconnect()
# mo.observe(document.documentElement, childList: true, subtree: true)
# return dfr.promise()
#add animation style
for prefix in ["-webkit-", "-moz-", "-ms-", "-o-", ""]
# nodeInserted needs to be defined in CSS!
CUI.dom.setStyleOne(node, "#{prefix}animation-duration", "0.001s")
CUI.dom.setStyleOne(node, "#{prefix}animation-name", "nodeInserted")
timeout = null
CUI.Events.wait
node: node
type: "animationstart"
maxWait: -1
.done =>
if CUI.dom.isInDOM(node)
dfr.resolve()
return
c = @__failedDOMInserts++
console.warn("[##{c}] Element received animationstart event but is not in DOM yet. We poll with timeout 0.")
tries = 0
check_for_node = ->
if CUI.dom.isInDOM(node)
console.warn("[##{c}] Poll done, element is in DOM now.")
dfr.resolve()
else if tries < 10
console.warn("[##{c}] Checking for node failed, try: ", tries)
tries = tries + 1
CUI.setTimeout(check_for_node, 0)
else
console.error("[##{c}] Checking for node failed. Giving up.", node)
CUI.setTimeout(check_for_node, 0)
.fail(dfr.reject)
dfr.promise()
@getNode: (node) ->
if node.DOM and node != window
node.DOM
else
node
# small experiment, testing...
@printElement: (_opts) ->
opts = CUI.Element.readOpts _opts, "CUI.dom.printElement",
docElem:
check: (v) ->
v instanceof HTMLElement
title:
default: "CUI-Print-Window"
check: String
windowName:
default: "_blank"
check: String
windowFeatures:
default: "width=400,height=800"
check: String
bodyClasses:
default: []
check: Array
if opts.docElem == document.documentElement
docElem = document.body
else
docElem = opts.docElem
win = window.open("", opts.windowName, opts.windowFeatures)
if not CUI.util.isEmpty(opts.title)
win.document.title = opts.title
for style_node in CUI.dom.matchSelector(document.head, "link[rel='stylesheet']")
new_node = style_node.cloneNode(true)
href = ez5.getAbsoluteURL(new_node.getAttribute("href"))
new_node.setAttribute("href", href)
console.debug "cloning css node for href", href
win.document.head.appendChild(new_node)
win.document.body.innerHTML = docElem.outerHTML
win.document.body.classList.add("cui-dom-print-element")
for cls in opts.bodyClasses
win.document.body.classList.add(cls)
win.print()
@isNode: (node, level=0) ->
if not node
return false
if node == document.documentElement or
node == window or
node == document or
node.nodeType or
(@isNode(node.DOM, level+1) and level == 0)
true
else
false
# Inserts the node like array "slice"
@insertChildAtPosition: (node, node_insert, pos) ->
CUI.util.assert(CUI.util.isInteger(pos) and pos >= 0 and pos <= node.children.length, "CUI.dom.insertAtPosition", "Unable to insert node at position ##{pos}.", node: node, node_insert: node_insert, pos: pos)
if pos == node.children.length
node.appendChild(node_insert)
else if node.children[pos] != node_insert
@insertBefore(node.children[pos], node_insert)
@insertBefore: (_node, node_before) ->
node = (_node?.DOM or _node)
if not node
return null
if node_before
node.parentNode.insertBefore(node_before, node)
node
@insertAfter: (_node, node_after) ->
node = (_node?.DOM or _node)
if not node
return null
if node_after
node.parentNode.insertBefore(node_after, node.nextSibling)
node
@is: (node, selector) ->
if not node
return null
if selector instanceof HTMLElement
return node == selector
if CUI.util.isFunction(selector)
return !!selector(node)
if node not instanceof HTMLElement
return null
@matches(node, selector)
@matches: (node, selector) ->
if not node
return null
node[CUI.dom.matchFunc](selector)
@matchFunc: (->
d = document.createElement("div")
for k in ["matches", "webkitMatchesSelector", "mozMatchesSelector", "oMatchesSelector", "msMatchesSelector"]
if d[k]
return k
CUI.util.assert(false, "Could not determine match function on docElem")
)()
@find: (sel) ->
@matchSelector(document.documentElement, sel)
@matchSelector: (docElem, sel, trySelf=false) ->
if docElem.hasOwnProperty('DOM')
docElem = docElem.DOM
CUI.util.assert(docElem instanceof HTMLElement or docElem == document, "CUI.dom.matchSelector", "docElem needs to be instanceof HTMLElement or document.", docElem: docElem)
# console.error "matchSelector", docElem, sel, trySelf
list = docElem.querySelectorAll(sel)
# console.debug "DONE"
if trySelf and list.length == 0
if docElem[CUI.dom.matchFunc](sel)
list = [docElem]
else
list = []
return list
# returns the element matching first the selector
# upwards, ends at untilDocElem
# selector & untilDocElem: collect everything until selector matches, but
# not further than untilDocElem
# selector: collection eveverything until selector matches, null if no match
# untilDocElem: stop collecting at docElem
@elementsUntil: (docElem, selector, untilDocElem) ->
CUI.util.assert(docElem instanceof Node or docElem == window, "CUI.dom.elementsUntil", "docElem needs to be instanceof Node or window.", docElem: docElem, selector: selector, untilDocElem: untilDocElem)
testDocElem = docElem
path = [testDocElem]
while true
if selector and @is(testDocElem, selector)
return path
if testDocElem == untilDocElem
if selector
# this means we have not found any
# elements which match the selector, so we
return []
else
return path
testDocElem = CUI.dom.parent(testDocElem)
if testDocElem == null
if selector
return []
else
return path
path.push(testDocElem)
# this should unreachable
return []
@parent: (docElem) ->
if docElem == window
null
else if docElem == document
window
else
if docElem.hasOwnProperty('DOM')
docElem = docElem.DOM
docElem.parentNode
@closest: (docElem, selector) ->
@closestUntil(docElem, selector)
@closestUntil: (docElem, selector, untilDocElem) ->
if not selector
return null
path = @elementsUntil(docElem, selector, untilDocElem)
if path.length == 0
return null
path[path.length-1]
# selector is a stopper (like untiDocElem)
@parentsUntil: (docElem, selector, untilDocElem=document.documentElement) ->
parentElem = CUI.dom.parent(docElem)
if not parentElem
return []
@elementsUntil(parentElem, selector, untilDocElem)
# selector is a filter
@parents: (docElem, selector, untilDocElem=document.documentElement) ->
CUI.util.assert(docElem instanceof Element or docElem == document or docElem == window, "CUI.dom.parents", "element needs to be instanceof HTMLElement, document, or window.", element: docElem)
path = @parentsUntil(docElem, null, untilDocElem)
if not selector
return path
# filter parents
parents = []
for parent in path
if @is(parent, selector)
parents.push(parent)
parents
@isInDOM: (docElem) ->
if not docElem
return null
if docElem.hasOwnProperty('DOM')
docElem = docElem.DOM
CUI.util.assert(docElem instanceof Node, "CUI.dom.isInDOM", "docElem needs to be instanceof Node.", docElem: docElem)
document.documentElement.contains(docElem)
# new nodes can be node or Array of nodes
@replaceWith: (node, new_node) ->
if node.hasOwnProperty('DOM')
node = node.DOM
if new_node.hasOwnProperty('DOM')
new_node = new_node.DOM
CUI.util.assert(node instanceof Node and (new_node instanceof Node or new_node instanceof NodeList), "CUI.dom.replaceWidth", "nodes need to be instanceof Node.", node: node, newNode: new_node)
CUI.util.assert(node.parentNode instanceof Node, "CUI.dom.replaceWith", "parentNode of node needs to be an instance of Node", node: node, parentNode: node.parentNode)
if new_node instanceof NodeList
first_node = new_node[0]
node.parentNode.replaceChild(first_node, node)
while (new_node.length > 0)
@insertAfter(first_node, new_node[new_node.length-1])
return first_node
else
return node.parentNode.replaceChild(new_node, node)
@getRect: (docElem) ->
if docElem.hasOwnProperty('DOM')
docElem = docElem.DOM
docElem.getBoundingClientRect()
@getComputedStyle: (docElem) ->
if docElem.hasOwnProperty('DOM')
docElem = docElem.DOM
window.getComputedStyle(docElem)
@setStyle: (docElem, style, append="px") ->
if docElem.hasOwnProperty('DOM')
docElem = docElem.DOM
CUI.util.assert(docElem instanceof HTMLElement, "CUI.dom.setStyle", "docElem needs to be instanceof HTMLElement.", docElem: docElem)
for k, v of style
if v == undefined
continue
switch v
when "", null
set = ""
else
if isNaN(Number(v))
set = v
else if v == 0 or v == "0"
set = 0
else
set = v + append
if k.startsWith("--")
docElem.style.setProperty(k, set)
else
docElem.style[k] = set
docElem
@getStyle: (element) ->
if element.hasOwnProperty('DOM')
element = element.DOM
styles = {}
for styleKey, styleValue of element.style
if not CUI.util.isNull(styleValue)
styles[styleKey] = styleValue
styles
@setStyleOne: (docElem, key, value) ->
map = {}
map[key] = value
@setStyle(docElem, map)
@setStylePx: (docElem, style) ->
console.error("CUI.dom.setStylePx is deprectaed, use CUI.dom.setStyle.")
@setStyle(docElem, style)
@getRelativePosition: (docElem) ->
CUI.util.assert(docElem instanceof HTMLElement, "CUI.dom.getRelativePosition", "docElem needs to be instanceof HTMLElement.", docElem: docElem)
dim = CUI.dom.getDimensions(docElem)
top: dim.offsetTopScrolled
left: dim.offsetLeftScrolled
@getDimensions: (docElem) ->
if CUI.util.isNull(docElem)
return null
if docElem == window or docElem == document
return {
width: window.innerWidth
height: window.innerHeight
}
if docElem.hasOwnProperty('DOM')
docElem = docElem.DOM
cs = @getComputedStyle(docElem)
rect = @getRect(docElem)
dim =
computedStyle: cs
clientBoundingRect: rect
for k1 in ["margin", "padding", "border"]
for k2 in ["Top", "Right", "Bottom", "Left"]
dim_key = k1+k2
switch k1
when "border"
dim[dim_key] = @getCSSFloatValue(cs["border#{k2}Width"])
else
dim[dim_key] = @getCSSFloatValue(cs[dim_key])
dim[k1+"Vertical"] = dim[k1+"Top"] + dim[k1+"Bottom"]
dim[k1+"Horizontal"] = dim[k1+"Left"] + dim[k1+"Right"]
dim.contentBoxWidth = Math.max(0, rect.width - dim.borderHorizontal - dim.paddingHorizontal)
dim.contentBoxHeight = Math.max(0, rect.height - dim.borderVertical - dim.paddingVertical)
dim.innerBoxWidth = Math.max(0, rect.width - dim.borderHorizontal)
dim.innerBoxHeight = Math.max(0, rect.height - dim.borderVertical)
dim.borderBoxWidth = rect.width
dim.borderBoxHeight = rect.height
if cs.boxSizing == "content-box"
dim.contentWidthAdjust = dim.borderBoxWidth - dim.contentBoxWidth
dim.contentHeightAdjust = dim.borderBoxHeight - dim.contentBoxHeight
else
dim.contentWidthAdjust = 0
dim.contentHeightAdjust = 0
dim.marginBoxWidth = Math.max(0, rect.width + dim.marginHorizontal)
dim.marginBoxHeight = Math.max(0, rect.height + dim.marginVertical)
dim.viewportTop = rect.top
dim.viewportTopMargin = rect.top - dim.marginTop
dim.viewportLeft = rect.left
dim.viewportLeftMargin = rect.left - dim.marginLeft
dim.viewportBottom = rect.bottom
dim.viewportBottomMargin = rect.bottom + dim.marginBottom
dim.viewportRight = rect.right
dim.viewportRightMargin = rect.right + dim.marginRight
dim.viewportCenterTop = rect.top + ((rect.bottom - rect.top) / 2)
dim.viewportCenterLeft = rect.left + ((rect.right - rect.left) / 2)
# passthru keys
for k in [
"left"
"top"
"minWidth"
"minHeight"
"maxWidth"
"maxHeight"
"marginRight"
"marginLeft"
"marginTop"
"marginBottom"
"borderTopWidth"
"borderLeftWidth"
"borderBottomWidth"
"borderRightWidth"
]
dim[k] = @getCSSFloatValue(cs[k])
for k in [
"offsetWidth"
"offsetHeight"
"offsetTop"
"offsetLeft"
"clientWidth"
"clientHeight"
"scrollWidth"
"scrollHeight"
"scrollLeft"
"scrollTop"
]
dim[k] = docElem[k]
dim.scaleX = dim.borderBoxWidth / dim.offsetWidth or 1
dim.scaleY = dim.borderBoxHeight / dim.offsetHeight or 1
if docElem.offsetParent
dim.offsetTopScrolled = dim.offsetTop + docElem.offsetParent.scrollTop
dim.offsetLeftScrolled = dim.offsetLeft + docElem.offsetParent.scrollLeft
else
dim.offsetTopScrolled = dim.offsetTop + document.body.scrollTop
dim.offsetLeftScrolled = dim.offsetLeft + document.body.scrollLeft
for k in [
"offsetWidth"
"offsetLeft"
"clientWidth"
"scrollWidth"
"scrollLeft"
]
dim[k+"Scaled"] = dim[k] * dim.scaleX
for k in [
"offsetHeight"
"offsetTop"
"clientHeight"
"scrollHeight"
"scrollTop"
]
dim[k+"Scaled"] = dim[k] * dim.scaleY
dim.verticalScrollbarWidth = dim.offsetWidth - dim.borderHorizontal - dim.clientWidth
dim.horizontalScrollbarHeight = dim.offsetHeight - dim.borderVertical - dim.clientHeight
dim.canHaveScrollbar = cs.overflowX in ["auto", "scroll"] or cs.overflowY in ["auto", "scroll"]
dim.horizontalScrollbarAtStart = dim.scrollLeft == 0
dim.horizontalScrollbarAtEnd = dim.scrollWidth - dim.scrollLeft - dim.clientWidth - dim.verticalScrollbarWidth < 1
dim.verticalScrollbarAtStart = dim.scrollTop == 0
dim.verticalScrollbarAtEnd = dim.scrollHeight - dim.scrollTop - dim.clientHeight - dim.horizontalScrollbarHeight < 1
dim.viewportTopContent = rect.top + dim.borderTop + dim.paddingTop
dim.viewportLeftContent = rect.left + dim.borderLeft + dim.paddingLeft
dim.viewportBottomContent = rect.bottom - dim.borderBottom - Math.max(dim.paddingBottom, dim.horizontalScrollbarHeight)
dim.viewportRightContent = rect.right - dim.borderRight- Math.max(dim.paddingRight, dim.verticalScrollbarWidth)
dim.viewportTopInner = rect.top + dim.borderTop
dim.viewportLeftInner = rect.left + dim.borderLeft
dim.viewportBottomInner = rect.bottom - dim.borderBottom - dim.horizontalScrollbarHeight
dim.viewportRightInner = rect.right - dim.borderRight- dim.verticalScrollbarWidth
dim
# returns the scrollable parents
@parentsScrollable: (node) ->
parents = []
for parent, idx in CUI.dom.parents(node)
dim = CUI.dom.getDimensions(parent)
if dim.canHaveScrollbar
parents.push(parent)
parents
@setDimension: (docElem, key, value) ->
set = {}
set[key] = value
@setDimensions(docElem, set)
@getDimension: (docElem, key) ->
@getDimensions(docElem)[key]
@prepareSetDimensions: (docElem) ->
if docElem.__prep_dim
return
docElem.__prep_dim =
borderBox: @isBorderBox(docElem)
dim: @getDimensions(docElem)
@
@setDimensions: (docElem, _dim) ->
@prepareSetDimensions(docElem)
css = {}
borderBox = docElem.__prep_dim.borderBox
dim = docElem.__prep_dim.dim
delete(docElem.__prep_dim)
set_dim = CUI.util.copyObject(_dim)
cssFloat = {}
set = (key, value) =>
if CUI.util.isNull(value) or isNaN(value)
return
if not cssFloat.hasOwnProperty(key)
if key in ["width", "height"] and value < 0
value = 0
cssFloat[key] = value
return
CUI.util.assert(cssFloat[key] == value, "CUI.dom.setDimensions", "Unable to set contradicting values for #{key}.", docElem: docElem, dim: set_dim)
return
# passthru keys
for k in ["width", "height", "left", "top"]
if set_dim.hasOwnProperty(k)
set(k, set_dim[k])
delete(set_dim[k])
if set_dim.hasOwnProperty("contentBoxWidth")
if borderBox
set("width", set_dim.contentBoxWidth + dim.paddingHorizontal + dim.borderHorizontal)
else
set("width", set_dim.contentBoxWidth)
delete(set_dim.contentBoxWidth)
if set_dim.hasOwnProperty("contentBoxHeight")
if borderBox
set("height",set_dim.contentBoxHeight + dim.paddingVertical + dim.borderVertical)
else
set("height",set_dim.contentBoxHeight)
delete(set_dim.contentBoxHeight)
if set_dim.hasOwnProperty("borderBoxWidth")
if borderBox
set("width", set_dim.borderBoxWidth)
else
set("width", set_dim.borderBoxWidth - dim.paddingHorizontal - dim.borderHorizontal)
delete(set_dim.borderBoxWidth)
if set_dim.hasOwnProperty("borderBoxHeight")
if borderBox
set("height",set_dim.borderBoxHeight)
else
set("height",set_dim.borderBoxHeight - dim.paddingVertical - dim.borderVertical)
delete(set_dim.borderBoxHeight)
if set_dim.hasOwnProperty("marginBoxWidth")
if borderBox
set("width", set_dim.marginBoxWidth - dim.marginHorizontal)
else
set("width", set_dim.marginBoxWidth - dim.marginHorizontal - dim.paddingHorizontal - dim.borderHorizontal)
delete(set_dim.marginBoxWidth)
if set_dim.hasOwnProperty("marginBoxHeight")
if borderBox
set("height", set_dim.marginBoxHeight - dim.marginVertical)
else
set("height", set_dim.marginBoxHeight - dim.marginVertical - dim.paddingVertical - dim.borderHorizontal)
delete(set_dim.marginBoxHeight)
left_over_keys = Object.keys(set_dim)
CUI.util.assert(left_over_keys.length == 0, "CUI.dom.setDimensions", "Unknown keys in dimension: \""+left_over_keys.join("\", \"")+"\".", docElem: docElem, dim: _dim)
@setStyle(docElem, cssFloat)
cssFloat
@htmlToNodes: (html) ->
if CUI.util.isNull(html)
return
d = @element("DIV")
d.innerHTML = html
d.childNodes
# runs callback on each textnode
@findTextInNodes: (nodes, callback, texts = []) ->
for node in nodes
child_nodes = []
for child in node.childNodes
switch child.nodeType
when 3 # Text
textContent = child.textContent.trim()
if textContent.length > 0
callback?(child, textContent)
texts.push(textContent)
when 1 # Element
child_nodes.push(child)
@findTextInNodes(child_nodes, callback, texts)
return texts
# turns 14.813px into a Number
@getCSSFloatValue: (v) ->
if v.indexOf("px") == -1
return 0
fl = parseFloat(v.substr(0, v.length-2))
fl
@isPositioned: (docElem) ->
CUI.util.assert(docElem instanceof HTMLElement, "CUI.dom.isPositioned", "docElem needs to be instance of HTMLElement.", docElem: docElem)
@getComputedStyle(docElem).position in ["relative", "absolute", "fixed"]
@isVisible: (docElem) ->
style = @getComputedStyle(docElem)
if style.visibility == "hidden" or style.display == "none"
false
else
true
# @hasOverflow: (docElem) ->
# style = @getComputedStyle(docElem)
# if style.overflowX == "visible" and style.overflowY == "visible"
# true
# else
# false
@getBoxSizing: (docElem) ->
@getComputedStyle(docElem).boxSizing
@isBorderBox: (docElem) ->
@getBoxSizing(docElem) == "border-box"
@isContentBox: (docElem) ->
@getBoxSizing(docElem) == "content-box"
@hideElement: (docElem) ->
if not docElem
return
if docElem.hasOwnProperty('DOM')
docElem = docElem.DOM
if docElem.style.display != "none"
docElem.__saved_display = docElem.style.display
docElem.style.display = "none"
docElem
@focus: (element) ->
if not element
return
if element.DOM
element = element.DOM
element.focus()
@blur: (element) ->
if not element
return
if element.DOM
element = element.DOM
element.blur()
# remove all children from a DOM node (detach)
@removeChildren: (docElem, filter) ->
CUI.util.assert(docElem instanceof HTMLElement, "CUI.dom.removeChildren", "element needs to be instance of HTMLElement", element: docElem)
for child in @children(docElem, filter)
docElem.removeChild(child)
return docElem
@showElement: (docElem) ->
if not docElem
return
if docElem.hasOwnProperty('DOM')
docElem = docElem.DOM
docElem.style.display = docElem.__saved_display or ""
delete(docElem.__saved_display)
docElem
@space: (style = null) ->
switch style
when "small"
@element("DIV", class: "cui-small-space")
when "large"
@element("DIV", class: "cui-large-space")
when "flexible"
@element("DIV", class: "cui-flexible-space")
when null
@element("DIV", class: "cui-space")
else
CUI.util.assert(false, "CUI.dom.space", "Unknown style: "+style)
@element: (tagName, attrs) ->
CUI.dom.setAttributeMap(document.createElement(tagName), attrs)
@debugRect: ->
@remove(@find("#cui-debug-rect")[0])
if arguments.length == 0
return
if arguments.length == 2 or not CUI.util.isArray(arguments[0])
dim = arguments[0]
pattern = arguments[1]
arr = []
for k in ["Top", "Left", "Bottom", "Right"]
if CUI.util.isEmpty(pattern) or pattern == "*"
k = k.toLowerCase()
value = dim[k]
else
value = dim[pattern.replace("*", k)]
arr.push(value)
else if CUI.util.isArray(arguments[0])
arr = arguments[0]
else
console.error("CUI.dom.debugRect: Argument Error.")
return
[top, left, bottom, right] = arr
width = right - left
height = bottom - top
d = @element("DIV", id: "cui-debug-rect")
@setStyle d,
position: "absolute"
border: "2px solid red"
boxSizing: "border-box"
top: top
left: left
width: width
height: height
document.body.appendChild(d)
console.debug "CUI.dom.debugRect:", [top, left, bottom, right]
d
@scrollIntoView: (docElem) ->
if not docElem
return null
if docElem.nodeType == 3 # textnode
docElem = docElem.parentNode
if docElem.hasOwnProperty('DOM')
docElem = docElem.DOM
parents = CUI.dom.parentsUntil(docElem)
dim = null
measure = =>
dim = @getDimensions(docElem)
measure()
for p, idx in parents
dim_p = @getDimensions(p)
if dim_p.computedStyle.overflowY != "visible"
off_bottom = dim.viewportBottomMargin - dim_p.viewportBottomContent
if off_bottom > 0
p.scrollTop = p.scrollTop + off_bottom
measure()
off_top = dim.viewportTopMargin - dim_p.viewportTopContent
if off_top < 0
p.scrollTop = p.scrollTop + off_top
measure()
if dim_p.computedStyle.overflowX != "visible"
off_right = dim.viewportRightMargin - dim_p.viewportRightContent
if off_right > 0
p.scrollLeft = p.scrollLeft + off_right
measure()
off_left = dim.viewportLeftMargin - dim_p.viewportLeftContent
if off_left < 0
p.scrollLeft = p.scrollLeft + off_left
measure()
return docElem
@setClassOnMousemove: (_opts={}) ->
opts = CUI.Element.readOpts _opts, "CUI.dom.setClassOnMousemove",
delayRemove:
check: Function
class:
mandatory: true
check: String
ms:
default: 3000
mandatory: true
check: (v) ->
v > 0
element:
mandatory: true
check: (v) ->
v instanceof HTMLElement
instance: {}
add_class = ->
CUI.dom.addClass(opts.element, opts.class)
schedule_remove_mousemoved_class()
remove_mousemoved_class = ->
if opts.delayRemove?() or CUI.globalDrag
schedule_remove_mousemoved_class()
return
CUI.dom.removeClass(opts.element, opts.class)
schedule_remove_mousemoved_class = ->
CUI.scheduleCallback
ms: opts.ms
call: remove_mousemoved_class
CUI.Events.listen
node: opts.element
type: ["mousemove", "wheel"]
instance: opts.instance
call: (ev) ->
add_class()
return
CUI.Events.listen
node: opts.element
type: "mouseleave"
instance: opts.instance
call: ->
remove_mousemoved_class()
@requestFullscreen: (elem) ->
if elem.hasOwnProperty('DOM')
elem = elem.DOM
CUI.util.assert(elem instanceof HTMLElement, "startFullscreen", "element needs to be instance of HTMLElement", element: elem)
if elem.requestFullscreen
elem.requestFullscreen()
else if elem.webkitRequestFullscreen
elem.webkitRequestFullscreen()
else if elem.mozRequestFullScreen
elem.mozRequestFullScreen()
else if elem.msRequestFullscreen
elem.msRequestFullscreen()
dfr = new CUI.Deferred()
# send notifiy on open and done on exit
fsc_ev = CUI.Events.listen
type: "fullscreenchange"
node: window
call: (ev) =>
if CUI.dom.isFullscreen()
dfr.notify()
else
CUI.Events.ignore(fsc_ev)
dfr.resolve()
return
dfr.promise()
@exitFullscreen: ->
if not CUI.dom.isFullscreen()
return CUI.resolvedPromise()
dfr = new CUI.Deferred()
if document.exitFullscreen
document.exitFullscreen()
else if document.msExitFullscreen
document.msExitFullscreen()
else if (document.mozCancelFullScreen)
document.mozCancelFullScreen()
else if (document.webkitExitFullscreen)
document.webkitExitFullscreen()
CUI.Events.listen
type: "fullscreenchange"
node: window
only_once: true
call: =>
dfr.resolve()
return
dfr.promise()
@fullscreenElement: ->
document.fullscreenElement or
document.webkitFullscreenElement or
document.mozFullScreenElement or
document.msFullscreenElement or
undefined
@fullscreenEnabled: ->
document.fullscreenEnabled or
document.webkitFullscreenEnabled or
document.mozFullScreenEnabled or
document.msFullscreenEnabled or
false
@isFullscreen: ->
document.fullscreen or
document.webkitIsFullScreen or
document.mozFullScreen or
!!document.msFullscreenElement or
false
@$element: (tagName, cls, attrs={}, no_tables=false) ->
if not CUI.util.isEmpty(cls)
attrs.class = cls
if no_tables
if CUI.util.isEmpty(cls)
attrs.class = "cui-"+tagName
else
attrs.class = "cui-"+tagName+" "+cls
tagName = "div"
node = CUI.dom.element(tagName, attrs)
node
@div: (cls, attrs) ->
CUI.dom.$element("div", cls, attrs)
@video: (cls, attrs) ->
CUI.dom.$element("video", cls, attrs)
@audio: (cls, attrs) ->
CUI.dom.$element("audio", cls, attrs)
@source: (cls, attrs) ->
CUI.dom.$element("source", cls, attrs)
@span: (cls, attrs) ->
CUI.dom.$element("span", cls, attrs)
@table: (cls, attrs) ->
CUI.dom.$element("table", cls, attrs, true)
@img: (cls, attrs) ->
CUI.dom.$element("img", cls, attrs)
@tr: (cls, attrs) ->
CUI.dom.$element("tr", cls, attrs, true)
@th: (cls, attrs) ->
CUI.dom.$element("th", cls, attrs, true)
@td: (cls, attrs) ->
CUI.dom.$element("td", cls, attrs, true)
@i: (cls, attrs) ->
CUI.dom.$element("i", cls, attrs)
@p: (cls, attrs) ->
CUI.dom.$element("p", cls, attrs)
@pre: (cls, attrs) ->
CUI.dom.$element("pre", cls, attrs)
@ul: (cls, attrs) ->
CUI.dom.$element("ul", cls, attrs)
@a: (cls, attrs) ->
CUI.dom.$element("a", cls, attrs)
@b: (cls, attrs) ->
CUI.dom.$element("b", cls, attrs)
@li: (cls, attrs) ->
CUI.dom.$element("li", cls, attrs)
@label: (cls, attrs) ->
CUI.dom.$element("label", cls, attrs)
@h1: (cls, attrs) ->
CUI.dom.$element("h1", cls, attrs)
@h2: (cls, attrs) ->
CUI.dom.$element("h2", cls, attrs)
@h3: (cls, attrs) ->
CUI.dom.$element("h3", cls, attrs)
@h4: (cls, attrs) ->
CUI.dom.$element("h4", cls, attrs)
@h5: (cls, attrs) ->
CUI.dom.$element("h5", cls, attrs)
@h6: (cls, attrs) ->
CUI.dom.$element("h6", cls, attrs)
@text: (text, cls, attrs) ->
s = CUI.dom.span(cls, attrs)
s.textContent = text
s
@textEmpty: (text) ->
s = CUI.dom.span("italic")
s.textContent = text
s
@table_one_row: ->
CUI.dom.append(CUI.dom.table(), CUI.dom.tr_one_row.apply(@, arguments))
@tr_one_row: ->
tr = CUI.dom.tr()
append = (__a) ->
td = CUI.dom.td()
CUI.dom.append(tr, td)
add_content = (___a) =>
if CUI.util.isArray(___a)
for a in ___a
add_content(a)
else if ___a?.DOM
CUI.dom.append(td, ___a.DOM)
else if not CUI.util.isNull(___a)
CUI.dom.append(td, ___a)
return
add_content(__a)
return
for a in arguments
if CUI.util.isArray(a)
for _a in a
append(_a)
else
append(a)
tr