From 686ad23f2f0f7311c2e4220339eb6f10afe25f6f Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 15 Apr 2017 15:50:07 +0200 Subject: [PATCH] reworked event handling again; added scrollapp example --- examples/scrollapp/scrollapp.html | 15 ++ examples/scrollapp/scrollapp.nim | 24 +++ examples/scrollapp/scrollapp.nims | 1 + examples/todoapp/todoapp.nim | 12 +- experiments/oldwidgets.nim | 71 +++++++++ readme.rst | 2 - src/dom.nim | 110 ++++++++------ src/karax.nim | 235 ++++++++++-------------------- src/karaxdsl.nim | 3 +- src/vdom.nim | 12 +- tests/diffDomTests.nim | 12 +- tests/diffDomTests.nims | 1 + 12 files changed, 273 insertions(+), 225 deletions(-) create mode 100644 examples/scrollapp/scrollapp.html create mode 100644 examples/scrollapp/scrollapp.nim create mode 100644 examples/scrollapp/scrollapp.nims create mode 100644 tests/diffDomTests.nims 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..