diff --git a/examples/scrollapp/scrollapp.html b/examples/scrollapp/scrollapp.html
new file mode 100644
index 0000000..1ff8b1b
--- /dev/null
+++ b/examples/scrollapp/scrollapp.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Todo app
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/scrollapp/scrollapp.nim b/examples/scrollapp/scrollapp.nim
new file mode 100644
index 0000000..e5ef9c1
--- /dev/null
+++ b/examples/scrollapp/scrollapp.nim
@@ -0,0 +1,24 @@
+## Example that shows how to accomplish an "infinitely scrolling" app.
+
+include karaxprelude
+import jstrutils, dom
+
+var entries: seq[cstring] = @[]
+for i in 1..500:
+ entries.add(cstring("Entry ") & &i)
+
+proc scrollEvent(ev: Event; n: VNode) =
+ let d = n.dom
+ if d != nil and inViewport(d.lastChild):
+ # "load" more data:
+ for i in 1..50:
+ entries.add(cstring("Loaded Entry ") & &i)
+
+proc createDom(): VNode =
+ result = buildHtml():
+ tdiv(onscroll=scrollEvent, style="height: 400px; overflow: scroll"):
+ for x in entries:
+ tdiv:
+ text x
+
+setRenderer createDom
diff --git a/examples/scrollapp/scrollapp.nims b/examples/scrollapp/scrollapp.nims
new file mode 100644
index 0000000..9028ff1
--- /dev/null
+++ b/examples/scrollapp/scrollapp.nims
@@ -0,0 +1 @@
+--path: "../../src"
diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim
index f2f0083..9027603 100644
--- a/examples/todoapp/todoapp.nim
+++ b/examples/todoapp/todoapp.nim
@@ -59,8 +59,8 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
button(class = "destroy", key=id, onclick=removeHandler)
else:
input(class = "edit", name = "title", key=id,
- onfocusLost = focusLost,
- onenter = editEntry, value = d, setFocus)
+ onblur = focusLost,
+ onkeyupenter = editEntry, value = d, setFocus)
proc createDom(): VNode =
result = buildHtml(tdiv(class="todomvc-wrapper")):
@@ -69,7 +69,7 @@ proc createDom(): VNode =
h1:
text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
- onenter = onTodoEnter, setFocus)
+ onkeyupenter = onTodoEnter, setFocus)
section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
@@ -94,13 +94,13 @@ proc createDom(): VNode =
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
ul(class = "filters"):
li:
- anchor(class = selected(all), href = "#/"):
+ a(class = selected(all), href = "#/"):
text "All"
li:
- anchor(class = selected(active), href = "#/active"):
+ a(class = selected(active), href = "#/active"):
text "Active"
li:
- anchor(class = selected(completed), href = "#/completed"):
+ a(class = selected(completed), href = "#/completed"):
text "Completed"
button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")"
diff --git a/experiments/oldwidgets.nim b/experiments/oldwidgets.nim
index fce9908..023dc98 100644
--- a/experiments/oldwidgets.nim
+++ b/experiments/oldwidgets.nim
@@ -100,6 +100,50 @@ proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
#proc ceil(f: float): int {.importc: "Math.ceil", nodecl.}
+proc realtimeInput*(val: cstring; action: EventHandler): VNode =
+ var timer: Timeout
+ proc onkeyup(ev: Event; n: VNode) =
+ proc wrapper() = keyeventBody()
+
+ if timer != nil: clearTimeout(timer)
+ timer = setTimeout(wrapper, 400)
+ result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
+ result.value = val
+ result.addEventListener(EventKind.onkeyup, onkeyup)
+
+proc enterInput*(id, val: cstring; action: EventHandler): VNode =
+ proc onkeyup(ev: Event; n: VNode) =
+ if ev.keyCode == 13: keyeventBody()
+
+ result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
+ result.id = id
+ result.value = val
+ result.addEventListener(EventKind.onkeyup, onkeyup)
+
+proc setOnEnter*(n: VNode; action: EventHandler) =
+ proc onkeyup(ev: Event; n: VNode) =
+ if ev.keyCode == 13: keyeventBody()
+ n.addEventListener(EventKind.onkeyup, onkeyup)
+
+proc setOnscroll*(action: proc(min, max: VKey; diff: int)) =
+ var oldY = window.pageYOffset
+
+ proc wrapper(ev: Event) =
+ let dir = window.pageYOffset - oldY
+ if dir == 0: return
+
+ var a = VKey high(int)
+ var b = VKey 0
+ var h, count: int
+ document.visibleKeys(a, b, h, count)
+ let avgh = h / count
+ let diff = toInt(dir.float / avgh)
+ if diff != 0:
+ oldY = window.pageYOffset
+ action(a, b, diff)
+ redraw()
+
+ document.addEventListener("scroll", wrapper)
when false:
var plugins {.exportc.}: seq[(string, proc())] = @[]
@@ -130,3 +174,30 @@ proc carousel*(): VNode =
button(onclick = prev):
text "Previous"
+#proc targetElem*(e: Event): Element = cast[Element](e.target)
+
+#proc getElementsByClassName*(cls: cstring): seq[Element] {.importc:
+# "document.getElementsByClassName", nodecl.}
+#proc textContent(e: Node): cstring {.
+# importcpp: "#.textContent", nodecl.}
+
+proc isElementInViewport(el: Node; h: var int): bool =
+ let rect = el.getBoundingClientRect()
+ h = rect.bottom - rect.top
+ result = rect.top >= 0 and rect.left >= 0 and
+ rect.bottom <= clientHeight() and
+ rect.right <= clientWidth()
+
+proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) =
+ # we only care about nodes that have a key:
+ var hh = 0
+ # do not recurse if there is a 'key' field already:
+ if e.key >= 0:
+ if isElementInViewport(e, hh):
+ inc count
+ inc h, hh
+ a = min(a, e.key)
+ b = max(b, e.key)
+ else:
+ for i in 0..= 0 and rect.left >= 0 and
+ rect.bottom <= clientHeight() and
+ rect.right <= clientWidth()
+
+proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.}
+proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.}
+proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.}
diff --git a/src/karax.nim b/src/karax.nim
index 00efc5e..a535ae7 100644
--- a/src/karax.nim
+++ b/src/karax.nim
@@ -1,28 +1,12 @@
-# Simple lib to write JS UIs
+## Karax -- Single page applications for Nim.
import dom, vdom, jstrutils, components, jdict
-export dom.Event, dom.cloneNode, dom
-
-proc len(x: Node): int {.importcpp: "#.childNodes.length".}
-proc `[]`(x: Node; idx: int): Element {.importcpp: "#.childNodes[#]".}
+export 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: Node): cstring {.importcpp: "#.value", nodecl.}
-proc `value=`*(e: Node; v: cstring) {.importcpp: "#.value = #", nodecl.}
-
-proc `disabled=`*(e: Node; v: bool) {.importcpp: "#.disabled = #", nodecl.}
-
-proc getElementsByClass*(e: Node; name: cstring): seq[Node] {.
- importcpp: "#.getElementsByClassName(#)", nodecl.}
-
proc hasProp(e: Node; prop: cstring): bool {.importcpp: "(#.hasOwnProperty(#))".}
proc rawkey(e: Node): VKey {.importcpp: "#.karaxKey", nodecl.}
proc key*(e: Node): VKey =
@@ -30,26 +14,6 @@ proc key*(e: Node): VKey =
else: result = -1
proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.}
-type
- BoundingRect {.importc.} = object
- top, bottom, left, right: int
-
-proc getBoundingClientRect(e: Node): BoundingRect {.
- importcpp: "getBoundingClientRect", nodecl.}
-proc clientHeight(): int {.
- importcpp: "(window.innerHeight || document.documentElement.clientHeight)@", nodecl}
-proc clientWidth(): int {.
- importcpp: "(window.innerWidth || document.documentElement.clientWidth)@", nodecl}
-
-when false:
- proc pageYOffset(): int {.
- importcpp: "(window.pageYOffset)@", nodecl}
- proc pageXOffset(): int {.
- importcpp: "(window.pageXOffset)@", nodecl}
-
-type
- Timeout* = ref object
-
var
toFocus: Node
toFocusV: VNode
@@ -57,24 +21,69 @@ var
proc setFocus*(n: VNode) =
toFocusV = n
-proc isElementInViewport(el: Node; h: var int): bool =
- let rect = el.getBoundingClientRect()
- h = rect.bottom - rect.top
- result = rect.top >= 0 and rect.left >= 0 and
- rect.bottom <= clientHeight() and
- rect.right <= clientWidth()
+# ----------------- event wrapping ---------------------------------------
+
+template nativeValue(ev): cstring = cast[Element](ev.target).value
+template setNativeValue(ev, val) = cast[Element](ev.target).value = val
+
+template keyeventBody() =
+ n.value = nativeValue(ev)
+ action(ev, n)
+ setNativeValue(ev, n.value)
+ # Do not call redraw() here! That is already done
+ # by ``karax.addEventHandler``.
+
+proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) =
+ proc stdWrapper(): (proc (ev: Event)) =
+ let action = action
+ let n = n
+ result = proc (ev: Event) =
+ action(ev, n)
+
+ proc enterWrapper(): (proc (ev: Event)) =
+ let action = action
+ let n = n
+ result = proc (ev: Event) =
+ if ev.keyCode == 13: keyeventBody()
+
+ proc laterWrapper(): (proc (ev: Event)) =
+ let action = action
+ let n = n
+ var timer: Timeout
+ result = proc (ev: Event) =
+ proc wrapper() = keyeventBody()
+ if timer != nil: clearTimeout(timer)
+ timer = setTimeout(wrapper, 400)
+
+ case k
+ of EventKind.onkeyuplater:
+ d.addEventListener("keyup", laterWrapper())
+ of EventKind.onkeyupenter:
+ d.addEventListener("keyup", enterWrapper())
+ else:
+ d.addEventListener(toEventName[k], stdWrapper())
+
+# --------------------- DOM diff -----------------------------------------
+
+template detach(n: VNode) = n.dom = nil
+template attach(n: Vnode) = n.dom = result
proc vnodeToDom(n: VNode): Node =
if n.kind == VNodeKind.text:
result = document.createTextNode(n.text)
+ attach n
elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n)
- return vnodeToDom(x)
+ result = vnodeToDom(x)
+ attach n
+ return result
elif n.kind == VNodeKind.dthunk:
- let x = callThunk(dcomponents[n.text], n)
- return x
+ result = callThunk(dcomponents[n.text], n)
+ attach n
+ return result
else:
result = document.createElement(toTag[n.kind])
+ attach n
for k in n:
appendChild(result, vnodeToDom(k))
# text is mapped to 'value':
@@ -89,14 +98,8 @@ proc vnodeToDom(n: VNode): Node =
for k, v in attrs(n):
if v != nil:
result.setAttr(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())
+ wrapEvent(result, n, e, h)
if n == toFocusV and toFocus.isNil:
toFocus = result
@@ -112,23 +115,12 @@ var
dorender: proc (): VNode {.closure.}
currentTree: VNode
-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: Node): 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 =
+proc equalsShallow(a, b: VNode): bool =
if a.kind != b.kind: return false
if a.id != b.id: return false
if a.key != b.key: return false
@@ -138,7 +130,7 @@ proc equals(a, b: VNode): bool =
if a.text != b.text: return false
if a.len != b.len: return false
for i in 0.. newLength:
for i in countdown(oldLength-1, newLength):
+ detach(oldNode[i])
current.removeChild(current.lastChild)
else:
var commonPrefix = 0
@@ -191,9 +185,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
var pos = min(oldPos, newPos) + 1
for i in commonPrefix..pos-1:
- updateElement(current, current.childNodes[i],
- newNode[i],
- oldNode[i])
+ updateElement(current, current.childNodes[i], newNode[i], oldNode[i])
var nextChildPos = oldPos + 1
while pos <= newPos:
@@ -206,7 +198,8 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
inc pos
inc nextChildPos
- for i in 0..oldPos-pos:
+ for i in pos..oldPos:
+ detach(oldNode[i])
current.removeChild(current.childNodes[pos])
when false:
@@ -229,20 +222,6 @@ proc dodraw() =
if toFocus != nil:
toFocus.focus()
-proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) =
- # we only care about nodes that have a key:
- var hh = 0
- # do not recurse if there is a 'key' field already:
- if e.key >= 0:
- if isElementInViewport(e, hh):
- inc count
- inc h, hh
- a = min(a, e.key)
- b = max(b, e.key)
- else:
- for i in 0..