123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- # Simple lib to write JS UIs
- import dom
- 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 className*(e: Node): cstring {.importcpp: "#.className", nodecl.}
- proc `className=`*(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 getElementsByClass*(e: Element; name: cstring): seq[Element] {.importcpp: "#.getElementsByClassName(#)", nodecl.}
- proc toLower*(x: cstring): cstring {.
- importcpp: "#.toLowerCase()", nodecl.}
- proc replace*(x: cstring; search, by: cstring): cstring {.
- importcpp: "#.replace(#, #)", nodecl.}
- type
- EventHandler* = proc(ev: Event)
- EventHandlerId* = proc(ev: Event; id: int)
- Timeout* = ref object
- var document* {.importc.}: Document
- var
- dorender: proc (): Element {.closure.}
- drawTimeout: Timeout
- currentTree: Element
- proc setRenderer*(renderer: proc (): Element) =
- 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 = getElementById(id)
- x.parentNode.replaceChild(newTree, x)
- newTree.id = id
- proc equals(a, b: Node): bool =
- if a.nodeType != b.nodeType: return false
- if a.id != b.id: return false
- if a.nodeName != b.nodeName: return false
- if a.nodeType == TextNode:
- if a.data != b.data: return false
- elif a.childNodes.len != b.childNodes.len:
- return false
- if a.className != b.className:
- # style differences are updated in place and we pretend
- # it's still the same node
- a.className = b.className
- #return false
- return true
- proc diffTree(parent, a, b: Node) =
- if equals(a, b):
- if a.nodeType != TextNode:
- # we need to do this correctly in the presence of asyncronous
- # DOM updates:
- var i = 0
- while i < a.childNodes.len and a.childNodes.len == b.childNodes.len:
- diffTree(a, a.childNodes[i], b.childNodes[i])
- inc i
- elif parent == nil:
- replaceById("ROOT", b)
- else:
- parent.replaceChild(b, a)
- proc dodraw() =
- let newtree = dorender()
- newtree.id = "ROOT"
- if currentTree == nil:
- currentTree = newtree
- replaceById("ROOT", currentTree)
- else:
- diffTree(nil, currentTree, newtree)
- proc redraw*() =
- # we buffer redraw requests:
- if drawTimeout != nil:
- clearTimeout(drawTimeout)
- drawTimeout = setTimeout(dodraw, 30)
- proc tree*(tag: string; kids: varargs[Element]): Element =
- result = document.createElement tag
- for k in kids:
- result.appendChild k
- proc tree*(tag: string; attrs: openarray[(string, string)];
- kids: varargs[Element]): Element =
- result = tree(tag, kids)
- for a in attrs: result.setAttribute(a[0], a[1])
- proc text*(s: string): Element = cast[Element](document.createTextNode(s))
- proc text*(s: cstring): Element = cast[Element](document.createTextNode(s))
- proc add*(parent, kid: Element) =
- if parent.nodeName == cstring"TR" and (
- kid.nodeName == cstring"TD" or kid.nodeName == cstring"TH"):
- let k = document.createElement("TD")
- appendChild(k, kid)
- appendChild(parent, k)
- else:
- appendChild(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]+$/);
- """
- var
- linkCounter: int
- proc link*(id: int): Element =
- result = document.createElement("a")
- result.setAttribute("href", "#")
- inc linkCounter
- result.setAttribute("id", $linkCounter & ":" & $id)
- proc link*(action: EventHandler): Element =
- result = document.createElement("a")
- result.setAttribute("href", "#")
- addEventListener(result, "click", action)
- proc parseInt*(s: cstring): int {.importc, nodecl.}
- proc parseFloat*(s: cstring): float {.importc, nodecl.}
- proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.}
- proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.}
- proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.}
- proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
- proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}
- #proc len*(s: cstring): int {.importcpp: "#.length", nodecl.}
- proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.}
- proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
- 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 valueAsInt*(e: Element): int = parseInt(e.value)
- 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) =
- action(ev)
- redraw()
- addEventListener(e, s, wrapper)
- proc setOnclick*(e: Element; action: proc(ev: Event)) =
- onImpl "click"
- proc setOnclick*(e: Element; action: proc(ev: Event; id: int)) =
- proc wrapper(ev: Event) =
- let id = ev.target.id
- let a = id.split(":")
- if a.len == 2:
- action(ev, parseInt(a[1]))
- redraw()
- else:
- kout(cstring("cannot deal with id "), id)
- addEventListener(e, "click", wrapper)
- proc setOnfocuslost*(e: Element; action: EventHandler) =
- onImpl "blur"
- proc setOnchanged*(e: Element; action: EventHandler) =
- onImpl "change"
- proc setOnscroll*(e: Element; action: EventHandler) =
- onImpl "scroll"
- proc select*(choices: openarray[string]): Element =
- result = document.createElement("select")
- var i = 0
- for c in choices:
- result.add tree("option", [("value", $i)], text(c))
- inc i
- proc select*(choices: openarray[(int, string)]): Element =
- result = document.createElement("select")
- for c in choices:
- result.add tree("option", [("value", $c[0])], text(c[1]))
- var radioCounter: int
- proc radio*(choices: openarray[(int, string)]): Element =
- result = document.createElement("fieldset")
- var i = 0
- inc radioCounter
- for c in choices:
- let id = "radio_" & c[1] & $i
- var kid = tree("input", [("type", "radio"),
- ("id", id), ("name", "radio" & $radioCounter),
- ("value", $c[0])])
- if i == 0:
- kid.setAttribute("checked", "checked")
- var lab = tree("label", [("for", id)], text(c[1]))
- kid.add lab
- result.add kid
- inc i
- proc tag*(name: string; id="", class=""): Element =
- result = document.createElement(name)
- if id.len > 0:
- result.setAttribute("id", id)
- if class.len > 0:
- result.setAttribute("class", class)
- proc tdiv*(id="", class=""): Element = tag("div", id, class)
- proc span*(id="", class=""): Element = tag("span", id, class)
- proc th*(s: string): Element =
- result = tag("th")
- result.add text(s)
- proc td*(s: string): Element =
- result = tag("td")
- result.add text(s)
- proc td*(s: Element): Element =
- result = tag("td")
- result.add s
- proc td*(class: string; s: Element): Element =
- result = tag("td")
- result.add s
- result.setAttribute("class", class)
- proc table*(class="", kids: varargs[Element]): Element =
- result = tag("table", "", class)
- for k in kids: result.add k
- proc tr*(kids: varargs[Element]): Element =
- result = tag("tr")
- for k in kids:
- if k.nodeName == cstring"TD" or k.nodeName == cstring"TH":
- result.add k
- else:
- result.add td(k)
- proc setClass*(e: Element; value: string) =
- e.setAttribute("class", value)
- proc setAttr*(e: Element; key, value: cstring) =
- e.setAttribute(key, value)
- proc getAttr*(e: Element; key: cstring): cstring {.
- importcpp: "#.getAttribute(#)", nodecl.}
- proc realtimeInput*(id, val: string; changed: proc(value: cstring)): Element =
- let oldElem = getElementById(id)
- #if oldElem != nil: return oldElem
- let newVal = if oldElem.isNil: val else: $oldElem.value
- var timer: Timeout
- proc wrapper() =
- changed(getElementById(id).value)
- redraw()
- proc onkeyup(ev: Event) =
- if timer != nil: clearTimeout(timer)
- timer = setTimeout(wrapper, 400)
- result = tree("input", [("type", "text"),
- ("value", newVal),
- ("id", id)])
- result.addEventListener("keyup", onkeyup)
- proc ajax(meth, url: cstring; headers: openarray[(string, string)];
- 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[(string, string)];
- data: cstring;
- cont: proc (httpStatus: int, response: cstring)) =
- ajax("PUT", url, headers, data, cont)
- proc ajaxGet*(url: string; headers: openarray[(string, string)];
- 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.}
|