karax.nim 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. # Simple lib to write JS UIs
  2. import dom
  3. export dom.Element, dom.Event, dom.cloneNode, dom
  4. proc kout*[T](x: T) {.importc: "console.log", varargs.}
  5. ## the preferred way of debugging karax applications.
  6. proc id*(e: Node): cstring {.importcpp: "#.id", nodecl.}
  7. proc `id=`*(e: Node; x: cstring) {.importcpp: "#.id = #", nodecl.}
  8. proc className*(e: Node): cstring {.importcpp: "#.className", nodecl.}
  9. proc `className=`*(e: Node; v: cstring) {.importcpp: "#.className = #", nodecl.}
  10. proc value*(e: Element): cstring {.importcpp: "#.value", nodecl.}
  11. proc `value=`*(e: Element; v: cstring) {.importcpp: "#.value = #", nodecl.}
  12. proc getElementsByClass*(e: Element; name: cstring): seq[Element] {.importcpp: "#.getElementsByClassName(#)", nodecl.}
  13. proc toLower*(x: cstring): cstring {.
  14. importcpp: "#.toLowerCase()", nodecl.}
  15. proc replace*(x: cstring; search, by: cstring): cstring {.
  16. importcpp: "#.replace(#, #)", nodecl.}
  17. type
  18. EventHandler* = proc(ev: Event)
  19. EventHandlerId* = proc(ev: Event; id: int)
  20. Timeout* = ref object
  21. var document* {.importc.}: Document
  22. var
  23. dorender: proc (): Element {.closure.}
  24. drawTimeout: Timeout
  25. currentTree: Element
  26. proc setRenderer*(renderer: proc (): Element) =
  27. dorender = renderer
  28. proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
  29. proc clearTimeout*(t: Timeout) {.importc, nodecl.}
  30. proc targetElem*(e: Event): Element = cast[Element](e.target)
  31. proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
  32. proc getElementsByClassName*(cls: cstring): seq[Element] {.importc:
  33. "document.getElementsByClassName", nodecl.}
  34. proc textContent*(e: Element): cstring {.
  35. importcpp: "#.textContent", nodecl.}
  36. proc replaceById*(id: cstring; newTree: Node) =
  37. let x = getElementById(id)
  38. x.parentNode.replaceChild(newTree, x)
  39. newTree.id = id
  40. proc equals(a, b: Node): bool =
  41. if a.nodeType != b.nodeType: return false
  42. if a.id != b.id: return false
  43. if a.nodeName != b.nodeName: return false
  44. if a.nodeType == TextNode:
  45. if a.data != b.data: return false
  46. elif a.childNodes.len != b.childNodes.len:
  47. return false
  48. if a.className != b.className:
  49. # style differences are updated in place and we pretend
  50. # it's still the same node
  51. a.className = b.className
  52. #return false
  53. return true
  54. proc diffTree(parent, a, b: Node) =
  55. if equals(a, b):
  56. if a.nodeType != TextNode:
  57. # we need to do this correctly in the presence of asyncronous
  58. # DOM updates:
  59. var i = 0
  60. while i < a.childNodes.len and a.childNodes.len == b.childNodes.len:
  61. diffTree(a, a.childNodes[i], b.childNodes[i])
  62. inc i
  63. elif parent == nil:
  64. replaceById("ROOT", b)
  65. else:
  66. parent.replaceChild(b, a)
  67. proc dodraw() =
  68. let newtree = dorender()
  69. newtree.id = "ROOT"
  70. if currentTree == nil:
  71. currentTree = newtree
  72. replaceById("ROOT", currentTree)
  73. else:
  74. diffTree(nil, currentTree, newtree)
  75. proc redraw*() =
  76. # we buffer redraw requests:
  77. if drawTimeout != nil:
  78. clearTimeout(drawTimeout)
  79. drawTimeout = setTimeout(dodraw, 30)
  80. proc tree*(tag: string; kids: varargs[Element]): Element =
  81. result = document.createElement tag
  82. for k in kids:
  83. result.appendChild k
  84. proc tree*(tag: string; attrs: openarray[(string, string)];
  85. kids: varargs[Element]): Element =
  86. result = tree(tag, kids)
  87. for a in attrs: result.setAttribute(a[0], a[1])
  88. proc text*(s: string): Element = cast[Element](document.createTextNode(s))
  89. proc text*(s: cstring): Element = cast[Element](document.createTextNode(s))
  90. proc add*(parent, kid: Element) =
  91. if parent.nodeName == cstring"TR" and (
  92. kid.nodeName == cstring"TD" or kid.nodeName == cstring"TH"):
  93. let k = document.createElement("TD")
  94. appendChild(k, kid)
  95. appendChild(parent, k)
  96. else:
  97. appendChild(parent, kid)
  98. proc len*(x: Element): int {.importcpp: "#.childNodes.length".}
  99. proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".}
  100. proc isInt*(s: cstring): bool {.asmNoStackFrame.} =
  101. asm """
  102. return s.match(/^[0-9]+$/);
  103. """
  104. var
  105. linkCounter: int
  106. proc link*(id: int): Element =
  107. result = document.createElement("a")
  108. result.setAttribute("href", "#")
  109. inc linkCounter
  110. result.setAttribute("id", $linkCounter & ":" & $id)
  111. proc link*(action: EventHandler): Element =
  112. result = document.createElement("a")
  113. result.setAttribute("href", "#")
  114. addEventListener(result, "click", action)
  115. proc parseInt*(s: cstring): int {.importc, nodecl.}
  116. proc parseFloat*(s: cstring): float {.importc, nodecl.}
  117. proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.}
  118. proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.}
  119. proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.}
  120. proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
  121. proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}
  122. #proc len*(s: cstring): int {.importcpp: "#.length", nodecl.}
  123. proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.}
  124. proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
  125. proc suffix*(s, prefix: cstring): cstring =
  126. if s.startsWith(prefix):
  127. result = s.substr(prefix.len)
  128. else:
  129. kout(cstring"bug! " & s & cstring" does not start with " & prefix)
  130. proc valueAsInt*(e: Element): int = parseInt(e.value)
  131. proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
  132. proc scrollTop*(e: Element): int {.importcpp: "#.scrollTop", nodecl.}
  133. proc offsetHeight*(e: Element): int {.importcpp: "#.offsetHeight", nodecl.}
  134. proc offsetTop*(e: Element): int {.importcpp: "#.offsetTop", nodecl.}
  135. template onImpl(s) {.dirty} =
  136. proc wrapper(ev: Event) =
  137. action(ev)
  138. redraw()
  139. addEventListener(e, s, wrapper)
  140. proc setOnclick*(e: Element; action: proc(ev: Event)) =
  141. onImpl "click"
  142. proc setOnclick*(e: Element; action: proc(ev: Event; id: int)) =
  143. proc wrapper(ev: Event) =
  144. let id = ev.target.id
  145. let a = id.split(":")
  146. if a.len == 2:
  147. action(ev, parseInt(a[1]))
  148. redraw()
  149. else:
  150. kout(cstring("cannot deal with id "), id)
  151. addEventListener(e, "click", wrapper)
  152. proc setOnfocuslost*(e: Element; action: EventHandler) =
  153. onImpl "blur"
  154. proc setOnchanged*(e: Element; action: EventHandler) =
  155. onImpl "change"
  156. proc setOnscroll*(e: Element; action: EventHandler) =
  157. onImpl "scroll"
  158. proc select*(choices: openarray[string]): Element =
  159. result = document.createElement("select")
  160. var i = 0
  161. for c in choices:
  162. result.add tree("option", [("value", $i)], text(c))
  163. inc i
  164. proc select*(choices: openarray[(int, string)]): Element =
  165. result = document.createElement("select")
  166. for c in choices:
  167. result.add tree("option", [("value", $c[0])], text(c[1]))
  168. var radioCounter: int
  169. proc radio*(choices: openarray[(int, string)]): Element =
  170. result = document.createElement("fieldset")
  171. var i = 0
  172. inc radioCounter
  173. for c in choices:
  174. let id = "radio_" & c[1] & $i
  175. var kid = tree("input", [("type", "radio"),
  176. ("id", id), ("name", "radio" & $radioCounter),
  177. ("value", $c[0])])
  178. if i == 0:
  179. kid.setAttribute("checked", "checked")
  180. var lab = tree("label", [("for", id)], text(c[1]))
  181. kid.add lab
  182. result.add kid
  183. inc i
  184. proc tag*(name: string; id="", class=""): Element =
  185. result = document.createElement(name)
  186. if id.len > 0:
  187. result.setAttribute("id", id)
  188. if class.len > 0:
  189. result.setAttribute("class", class)
  190. proc tdiv*(id="", class=""): Element = tag("div", id, class)
  191. proc span*(id="", class=""): Element = tag("span", id, class)
  192. proc th*(s: string): Element =
  193. result = tag("th")
  194. result.add text(s)
  195. proc td*(s: string): Element =
  196. result = tag("td")
  197. result.add text(s)
  198. proc td*(s: Element): Element =
  199. result = tag("td")
  200. result.add s
  201. proc td*(class: string; s: Element): Element =
  202. result = tag("td")
  203. result.add s
  204. result.setAttribute("class", class)
  205. proc table*(class="", kids: varargs[Element]): Element =
  206. result = tag("table", "", class)
  207. for k in kids: result.add k
  208. proc tr*(kids: varargs[Element]): Element =
  209. result = tag("tr")
  210. for k in kids:
  211. if k.nodeName == cstring"TD" or k.nodeName == cstring"TH":
  212. result.add k
  213. else:
  214. result.add td(k)
  215. proc setClass*(e: Element; value: string) =
  216. e.setAttribute("class", value)
  217. proc setAttr*(e: Element; key, value: cstring) =
  218. e.setAttribute(key, value)
  219. proc getAttr*(e: Element; key: cstring): cstring {.
  220. importcpp: "#.getAttribute(#)", nodecl.}
  221. proc realtimeInput*(id, val: string; changed: proc(value: cstring)): Element =
  222. let oldElem = getElementById(id)
  223. #if oldElem != nil: return oldElem
  224. let newVal = if oldElem.isNil: val else: $oldElem.value
  225. var timer: Timeout
  226. proc wrapper() =
  227. changed(getElementById(id).value)
  228. redraw()
  229. proc onkeyup(ev: Event) =
  230. if timer != nil: clearTimeout(timer)
  231. timer = setTimeout(wrapper, 400)
  232. result = tree("input", [("type", "text"),
  233. ("value", newVal),
  234. ("id", id)])
  235. result.addEventListener("keyup", onkeyup)
  236. proc ajax(meth, url: cstring; headers: openarray[(string, string)];
  237. data: cstring;
  238. cont: proc (httpStatus: int; response: cstring)) =
  239. proc setRequestHeader(a, b: cstring) {.importc: "ajax.setRequestHeader".}
  240. {.emit: """
  241. var ajax = new XMLHttpRequest();
  242. ajax.open(`meth`,`url`,true);""".}
  243. for a, b in items(headers):
  244. setRequestHeader(a, b)
  245. {.emit: """
  246. ajax.onreadystatechange = function(){
  247. if(this.readyState == 4){
  248. if(this.status == 200){
  249. `cont`(this.status, this.responseText);
  250. } else {
  251. `cont`(this.status, this.statusText);
  252. }
  253. }
  254. }
  255. ajax.send(`data`);
  256. """.}
  257. proc ajaxPut*(url: string; headers: openarray[(string, string)];
  258. data: cstring;
  259. cont: proc (httpStatus: int, response: cstring)) =
  260. ajax("PUT", url, headers, data, cont)
  261. proc ajaxGet*(url: string; headers: openarray[(string, string)];
  262. cont: proc (httpStatus: int, response: cstring)) =
  263. ajax("GET", url, headers, nil, cont)
  264. {.push stackTrace:off.}
  265. proc setupErrorHandler*(useAlert=false) =
  266. ## Installs an error handler that transforms native JS unhandled
  267. ## exceptions into Nim based stack traces. If `useAlert` is false,
  268. ## the error message it put into the console, otherwise `alert`
  269. ## is called.
  270. proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
  271. {.emit: """
  272. window.onerror = function(msg, url, line, col, error) {
  273. var x = "Error: " + msg + "\n" + `stackTraceAsCstring`()
  274. if (`useAlert`)
  275. alert(x);
  276. else
  277. console.log(x);
  278. var suppressErrorAlert = true;
  279. return suppressErrorAlert;
  280. };""".}
  281. {.pop.}