UNPKG

coffeescript-ui

Version:
1,741 lines (1,340 loc) 44.3 kB
### * 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