karax/karax.nim
2017-03-31 14:00:23 +02:00

376 lines
11 KiB
Nim

# Simple lib to write JS UIs
import dom, vdom, jstrutils
export dom.Element, dom.Event, dom.cloneNode, dom
proc kout*[T](x: T) {.importc: "console.log", varargs.}
## the preferred way of debugging karax applications.
proc id*(e: Node): cstring {.importcpp: "#.id", nodecl.}
proc `id=`*(e: Node; x: cstring) {.importcpp: "#.id = #", nodecl.}
proc class*(e: Node): cstring {.importcpp: "#.className", nodecl.}
proc `class=`*(e: Node; v: cstring) {.importcpp: "#.className = #", nodecl.}
proc value*(e: Element): cstring {.importcpp: "#.value", nodecl.}
proc `value=`*(e: Element; v: cstring) {.importcpp: "#.value = #", nodecl.}
proc `disabled=`*(e: Element; v: bool) {.importcpp: "#.disabled = #", nodecl.}
proc getElementsByClass*(e: Element; name: cstring): seq[Element] {.
importcpp: "#.getElementsByClassName(#)", nodecl.}
type
Timeout* = ref object
var
document* {.importc.}: Document
toFocus: Element
proc vnodeToDom(n: VNode): Element =
if n.kind == VNodeKind.text:
result = cast[Element](document.createTextNode(n.text))
else:
result = document.createElement(toTag[n.kind])
for k in n:
appendChild(result, vnodeToDom(k))
# text is mapped to 'value':
if n.text != nil:
result.value = n.text
if n.id != nil:
result.id = n.id
if n.class != nil:
result.class = n.class
for k, v in attrs(n):
result.setAttribute(k, v)
let myn = n
for e, h in items(n.events):
proc wrapper(): proc (ev: Event) =
let hh = h
result = proc (ev: Event) =
assert myn != nil
hh(ev, myn)
result.addEventListener(toEventName[e], wrapper())
if n.kind == VNodeKind.input and toFocus.isNil:
toFocus = result
proc same(n: VNode, e: Element): bool =
if toTag[n.kind] == e.nodename:
result = true
if n.kind != VNodeKind.text:
if e.childNodes.len != n.len: return false
for i in 0 ..< n.len:
if not same(n[i], cast[Element](e.childNodes[i])): return false
var
dorender: proc (): VNode {.closure.}
drawTimeout: Timeout
currentTree: VNode
proc setRenderer*(renderer: proc (): VNode) =
dorender = renderer
proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
proc clearTimeout*(t: Timeout) {.importc, nodecl.}
#proc targetElem*(e: Event): Element = cast[Element](e.target)
#proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
#proc getElementsByClassName*(cls: cstring): seq[Element] {.importc:
# "document.getElementsByClassName", nodecl.}
proc textContent(e: Element): cstring {.
importcpp: "#.textContent", nodecl.}
proc replaceById(id: cstring; newTree: Node) =
let x = document.getElementById(id)
x.parentNode.replaceChild(newTree, x)
#newTree.id = id
proc equals(a, b: VNode): bool =
if a.kind != b.kind: return false
if a.id != b.id: return false
if a.kind == VNodeKind.text:
if a.text != b.text: return false
if not sameAttrs(a, b): return false
# XXX test event listeners here?
# --> maybe give nodes a hash?
when false:
# this needs to be done differently in a virtual DOM:
if a.class != b.class:
# style differences are updated in place and we pretend
# it's still the same node
a.class = b.class
return true
proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
if not equals(newNode, oldNode):
let n = vnodeToDom(newNode)
if parent == nil:
replaceById("ROOT", n)
else:
parent.replaceChild(n, current)
elif newNode.kind != VNodeKind.text:
let newLength = newNode.len
let oldLength = oldNode.len
assert oldNode.kind == newNode.kind
when false:
if current.nodeName != toTag[oldNode.kind]:
kout current.nodeName
kout toTag[oldNode.kind]
assert false
for i in 0..min(newLength, oldLength)-1:
updateElement(current, current.childNodes[i],
newNode[i],
oldNode[i])
if newLength > oldLength:
for i in oldLength..newLength-1:
current.appendChild(vnodeToDom(newNode[i]))
elif oldLength > newLength:
for i in countdown(oldLength-1, newLength):
current.removeChild(current.lastChild)
proc dodraw() =
let newtree = dorender()
newtree.id = "ROOT"
toFocus = nil
if currentTree == nil:
currentTree = newtree
let asdom = vnodeToDom currentTree
replaceById("ROOT", asdom)
else:
let olddom = document.getElementById("ROOT")
updateElement(nil, olddom, newtree, currentTree)
assert same(newtree, document.getElementById("ROOT"))
currentTree = newtree
# now that it's part of the DOM, give it the focus:
if toFocus != nil:
toFocus.focus()
proc redraw*() =
# we buffer redraw requests:
when true:
if drawTimeout != nil:
clearTimeout(drawTimeout)
drawTimeout = setTimeout(dodraw, 30)
else:
dodraw()
#proc prepend*(parent, kid: Element) =
# parent.insertBefore(kid, parent.firstChild)
# prependChild(parent, kid)
proc len(x: Element): int {.importcpp: "#.childNodes.length".}
proc `[]`(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".}
proc isInt*(s: cstring): bool {.asmNoStackFrame.} =
asm """
return s.match(/^[0-9]+$/);
"""
proc suffix*(s, prefix: cstring): cstring =
if s.startsWith(prefix):
result = s.substr(prefix.len)
else:
kout(cstring"bug! " & s & cstring" does not start with " & prefix)
proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
proc scrollTop*(e: Element): int {.importcpp: "#.scrollTop", nodecl.}
proc offsetHeight*(e: Element): int {.importcpp: "#.offsetHeight", nodecl.}
proc offsetTop*(e: Element): int {.importcpp: "#.offsetTop", nodecl.}
template onImpl(s) {.dirty.} =
proc wrapper(ev: Event; n: VNode) =
action(ev, n)
redraw()
addEventListener(e, s, wrapper)
proc setOnclick*(e: VNode; action: EventHandler) =
onImpl EventKind.onclick
proc setOnfocuslost*(e: VNode; action: EventHandler) =
onImpl EventKind.onblur
proc setOnchanged*(e: VNode; action: EventHandler) =
onImpl EventKind.onchange
proc setOnscroll*(e: VNode; action: EventHandler) =
onImpl EventKind.onscroll
var
linkCounter: int
proc link*(id: int): VNode =
result = newVNode(VNodeKind.anchor)
result.setAttr("href", "#")
inc linkCounter
result.setAttr("id", $linkCounter & ":" & $id)
proc link*(action: EventHandler): VNode =
result = newVNode(VNodeKind.anchor)
result.setAttr("href", "#")
result.setOnclick action
proc button*(caption: cstring; action: EventHandler; disabled=false): VNode =
result = newVNode(VNodeKind.button)
result.add text(caption)
if action != nil:
result.setOnClick action
if disabled:
result.setAttr("disabled", "true")
proc select*(choices: openarray[cstring]): VNode =
result = newVNode(VNodeKind.select)
var i = 0
for c in choices:
result.add tree(VNodeKind.option, [(cstring"value", toCstr(i))], text(c))
inc i
proc select*(choices: openarray[(int, cstring)]): VNode =
result = newVNode(VNodeKind.select)
for c in choices:
result.add tree(VNodeKind.option, [(cstring"value", toCstr(c[0]))], text(c[1]))
var radioCounter: int
proc radio*(choices: openarray[(int, cstring)]): VNode =
result = newVNode(VNodeKind.fieldset)
var i = 0
inc radioCounter
for c in choices:
let id = cstring"radio_" & c[1] & toCstr(i)
var kid = tree(VNodeKind.input, [(cstring"type", cstring"radio"),
(cstring"id", id), (cstring"name", cstring"radio" & toCStr(radioCounter)),
(cstring"value", toCStr(c[0]))])
if i == 0:
kid.setAttr(cstring"checked", cstring"checked")
var lab = tree(VNodeKind.label, [(cstring"for", id)], text(c[1]))
kid.add lab
result.add kid
inc i
proc tag*(kind: VNodeKind; id=cstring(nil), class=cstring(nil)): VNode =
result = newVNode(kind)
result.id = id
result.class = class
proc tdiv*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.tdiv, id, class)
proc span*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.span, id, class)
proc valueAsInt*(e: Element): int = parseInt(e.value)
proc th*(s: cstring): VNode =
result = newVNode(VNodeKind.th)
result.add text(s)
proc td*(s: string): VNode =
result = newVNode(VNodeKind.td)
result.add text(s)
proc td*(s: VNode): VNode =
result = newVNode(VNodeKind.td)
result.add s
proc td*(class: cstring; s: VNode): VNode =
result = newVNode(VNodeKind.td)
result.add s
result.class = class
proc table*(class=cstring(nil), kids: varargs[VNode]): VNode =
result = tag(VNodeKind.table, nil, class)
for k in kids: result.add k
proc tr*(kids: varargs[VNode]): VNode =
result = newVNode(VNodeKind.tr)
for k in kids:
if k.kind in {VNodeKind.td, VNodeKind.th}:
result.add k
else:
result.add td(k)
proc getAttr(e: Element; key: cstring): cstring {.
importcpp: "#.getAttribute(#)", nodecl.}
proc realtimeInput*(id, val: cstring; changed: proc(value: cstring)): VNode =
#let oldElem = getElementById(id)
#if oldElem != nil: return oldElem
#let newVal = if oldElem.isNil: val else: $oldElem.value
var timer: Timeout
proc wrapper() =
changed(document.getElementById(id).value)
redraw()
proc onkeyup(ev: Event; n: VNode) =
if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400)
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
result.id = id
result.value = val
result.addEventListener(EventKind.onkeyup, onkeyup)
proc enterInput*(id, val: cstring; onenter: proc(value: cstring)): VNode =
#let oldElem = getElementById(id)
#if oldElem != nil: return oldElem
#let newVal = if oldElem.isNil: val else: $oldElem.value
proc onkeyup(ev: Event; n: VNode) =
if ev.keyCode == 13:
onenter(document.getElementById(id).value)
redraw()
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
result.id = id
result.value = val
result.addEventListener(EventKind.onkeyup, onkeyup)
proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int; response: cstring)) =
proc setRequestHeader(a, b: cstring) {.importc: "ajax.setRequestHeader".}
{.emit: """
var ajax = new XMLHttpRequest();
ajax.open(`meth`,`url`,true);""".}
for a, b in items(headers):
setRequestHeader(a, b)
{.emit: """
ajax.onreadystatechange = function(){
if(this.readyState == 4){
if(this.status == 200){
`cont`(this.status, this.responseText);
} else {
`cont`(this.status, this.statusText);
}
}
}
ajax.send(`data`);
""".}
proc ajaxPut*(url: string; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring)) =
ajax("PUT", url, headers, data, cont)
proc ajaxGet*(url: string; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring)) =
ajax("GET", url, headers, nil, cont)
{.push stackTrace:off.}
proc setupErrorHandler*(useAlert=false) =
## Installs an error handler that transforms native JS unhandled
## exceptions into Nim based stack traces. If `useAlert` is false,
## the error message it put into the console, otherwise `alert`
## is called.
proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
{.emit: """
window.onerror = function(msg, url, line, col, error) {
var x = "Error: " + msg + "\n" + `stackTraceAsCstring`()
if (`useAlert`)
alert(x);
else
console.log(x);
var suppressErrorAlert = true;
return suppressErrorAlert;
};""".}
{.pop.}