reworked event handling again; added scrollapp example

This commit is contained in:
Andreas Rumpf 2017-04-15 15:50:07 +02:00
commit 686ad23f2f
12 changed files with 273 additions and 225 deletions

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="nimcache/scrollapp.js"></script>
</body>
</html>

View file

@ -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

View file

@ -0,0 +1 @@
--path: "../../src"

View file

@ -59,8 +59,8 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
button(class = "destroy", key=id, onclick=removeHandler) button(class = "destroy", key=id, onclick=removeHandler)
else: else:
input(class = "edit", name = "title", key=id, input(class = "edit", name = "title", key=id,
onfocusLost = focusLost, onblur = focusLost,
onenter = editEntry, value = d, setFocus) onkeyupenter = editEntry, value = d, setFocus)
proc createDom(): VNode = proc createDom(): VNode =
result = buildHtml(tdiv(class="todomvc-wrapper")): result = buildHtml(tdiv(class="todomvc-wrapper")):
@ -69,7 +69,7 @@ proc createDom(): VNode =
h1: h1:
text "todos" text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo", input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onenter = onTodoEnter, setFocus) onkeyupenter = onTodoEnter, setFocus)
section(class = "main"): section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", name = "toggle") input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone): label(`for` = "toggle-all", onclick = onAllDone):
@ -94,13 +94,13 @@ proc createDom(): VNode =
text cstring" item" & &(if entriesCount != 1: "s left" else: " left") text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
ul(class = "filters"): ul(class = "filters"):
li: li:
anchor(class = selected(all), href = "#/"): a(class = selected(all), href = "#/"):
text "All" text "All"
li: li:
anchor(class = selected(active), href = "#/active"): a(class = selected(active), href = "#/active"):
text "Active" text "Active"
li: li:
anchor(class = selected(completed), href = "#/completed"): a(class = selected(completed), href = "#/completed"):
text "Completed" text "Completed"
button(class = "clear-completed", onclick = clearCompleted): button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")" text "Clear completed (" & &completedCount & ")"

View file

@ -100,6 +100,50 @@ proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix))
#proc ceil(f: float): int {.importc: "Math.ceil", nodecl.} #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: when false:
var plugins {.exportc.}: seq[(string, proc())] = @[] var plugins {.exportc.}: seq[(string, proc())] = @[]
@ -130,3 +174,30 @@ proc carousel*(): VNode =
button(onclick = prev): button(onclick = prev):
text "Previous" 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..<e.len:
visibleKeys(e[i], a, b, h, count)

View file

@ -33,6 +33,4 @@ Goals
Todo Todo
==== ====
- Expose a way to access the attached real DOM element so that scroll
positions etc can be used.
- Expose a nice API to do Ajax requests. - Expose a nice API to do Ajax requests.

View file

@ -46,17 +46,17 @@ type
closed*: bool closed*: bool
defaultStatus*: cstring defaultStatus*: cstring
innerHeight*, innerWidth*: int innerHeight*, innerWidth*: int
locationbar*: ref TLocationBar locationbar*: ref LocationBar
menubar*: ref TMenuBar menubar*: ref MenuBar
name*: cstring name*: cstring
outerHeight*, outerWidth*: int outerHeight*, outerWidth*: int
pageXOffset*, pageYOffset*: int pageXOffset*, pageYOffset*: int
personalbar*: ref TPersonalBar personalbar*: ref PersonalBar
scrollbars*: ref TScrollBars scrollbars*: ref ScrollBars
statusbar*: ref TStatusBar statusbar*: ref StatusBar
status*: cstring status*: cstring
toolbar*: ref TToolBar toolbar*: ref ToolBar
frames*: seq[TFrame] frames*: seq[Frame]
Frame* = ref FrameObj Frame* = ref FrameObj
FrameObj {.importc.} = object of WindowObj FrameObj {.importc.} = object of WindowObj
@ -360,26 +360,26 @@ type
language*: cstring language*: cstring
platform*: cstring platform*: cstring
userAgent*: cstring userAgent*: cstring
mimeTypes*: seq[ref TMimeType] mimeTypes*: seq[ref MimeType]
TPlugin* {.importc.} = object of RootObj Plugin* {.importc.} = object of RootObj
description*: cstring description*: cstring
filename*: cstring filename*: cstring
name*: cstring name*: cstring
TMimeType* {.importc.} = object of RootObj MimeType* {.importc.} = object of RootObj
description*: cstring description*: cstring
enabledPlugin*: ref TPlugin enabledPlugin*: ref Plugin
suffixes*: seq[cstring] suffixes*: seq[cstring]
`type`*: cstring `type`*: cstring
TLocationBar* {.importc.} = object of RootObj LocationBar* {.importc.} = object of RootObj
visible*: bool visible*: bool
TMenuBar* = TLocationBar MenuBar* = LocationBar
TPersonalBar* = TLocationBar PersonalBar* = LocationBar
TScrollBars* = TLocationBar ScrollBars* = LocationBar
TToolBar* = TLocationBar ToolBar* = LocationBar
TStatusBar* = TLocationBar StatusBar* = LocationBar
Screen = ref ScreenObj Screen = ref ScreenObj
ScreenObj {.importc.} = object of RootObj ScreenObj {.importc.} = object of RootObj
@ -390,8 +390,15 @@ type
pixelDepth*: int pixelDepth*: int
width*: int width*: int
TTimeOut* {.importc.} = object of RootObj TimeOut* {.importc.} = ref object of RootObj
TInterval* {.importc.} = object of RootObj Interval* {.importc.} = object of RootObj
proc len*(x: Node): int {.importcpp: "#.childNodes.length".}
proc `[]`*(x: Node; idx: int): Element {.importcpp: "#.childNodes[#]".}
proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
proc clearTimeout*(t: Timeout) {.importc, nodecl.}
proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
{.push importcpp.} {.push importcpp.}
@ -403,8 +410,8 @@ proc alert*(w: Window, msg: cstring)
proc back*(w: Window) proc back*(w: Window)
proc blur*(w: Window) proc blur*(w: Window)
proc captureEvents*(w: Window, eventMask: int) {.deprecated.} proc captureEvents*(w: Window, eventMask: int) {.deprecated.}
proc clearInterval*(w: Window, interval: ref TInterval) proc clearInterval*(w: Window, interval: ref Interval)
proc clearTimeout*(w: Window, timeout: ref TTimeOut) proc clearTimeout*(w: Window, timeout: TimeOut)
proc close*(w: Window) proc close*(w: Window)
proc confirm*(w: Window, msg: cstring): bool proc confirm*(w: Window, msg: cstring): bool
proc disableExternalCapture*(w: Window) proc disableExternalCapture*(w: Window)
@ -427,10 +434,10 @@ proc resizeTo*(w: Window, x, y: int)
proc routeEvent*(w: Window, event: Event) proc routeEvent*(w: Window, event: Event)
proc scrollBy*(w: Window, x, y: int) proc scrollBy*(w: Window, x, y: int)
proc scrollTo*(w: Window, x, y: int) proc scrollTo*(w: Window, x, y: int)
proc setInterval*(w: Window, code: cstring, pause: int): ref TInterval proc setInterval*(w: Window, code: cstring, pause: int): ref Interval
proc setInterval*(w: Window, function: proc (), pause: int): ref TInterval proc setInterval*(w: Window, function: proc (), pause: int): ref Interval
proc setTimeout*(w: Window, code: cstring, pause: int): ref TTimeOut proc setTimeout*(w: Window, code: cstring, pause: int): TimeOut
proc setTimeout*(w: Window, function: proc (), pause: int): ref TInterval proc setTimeout*(w: Window, function: proc (), pause: int): ref Interval
proc stop*(w: Window) proc stop*(w: Window)
proc requestAnimationFrame*(w: Window, function: proc (time: float)): int proc requestAnimationFrame*(w: Window, function: proc (time: float)): int
proc cancelAnimationFrame*(w: Window, id: int) proc cancelAnimationFrame*(w: Window, id: int)
@ -543,24 +550,37 @@ proc parseInt*(s: cstring): int {.importc, nodecl.}
proc parseInt*(s: cstring, radix: int):int {.importc, nodecl.} proc parseInt*(s: cstring, radix: int):int {.importc, nodecl.}
proc id*(n: Node): cstring {.importcpp: "#.id", nodecl.}
proc `id=`*(n: Node; x: cstring) {.importcpp: "#.id = #", nodecl.}
proc class*(n: Node): cstring {.importcpp: "#.className", nodecl.}
proc `class=`*(n: Node; v: cstring) {.importcpp: "#.className = #", nodecl.}
proc value*(n: Node): cstring {.importcpp: "#.value", nodecl.}
proc `value=`*(n: Node; v: cstring) {.importcpp: "#.value = #", nodecl.}
proc `disabled=`*(n: Node; v: bool) {.importcpp: "#.disabled = #", nodecl.}
proc getElementsByClass*(n: Node; name: cstring): seq[Node] {.
importcpp: "#.getElementsByClassName(#)", nodecl.}
type type
TEventHandlers* {.deprecated.} = EventTargetObj BoundingRect* {.importc.} = object
TWindow* {.deprecated.} = WindowObj top*, bottom*, left*, right*: int
TFrame* {.deprecated.} = FrameObj
TNode* {.deprecated.} = NodeObj proc getBoundingClientRect*(e: Node): BoundingRect {.
TDocument* {.deprecated.} = DocumentObj importcpp: "getBoundingClientRect", nodecl.}
TElement* {.deprecated.} = ElementObj proc clientHeight*(): int {.
TLink* {.deprecated.} = LinkObj importcpp: "(window.innerHeight || document.documentElement.clientHeight)@", nodecl}
TEmbed* {.deprecated.} = EmbedObj proc clientWidth*(): int {.
TAnchor* {.deprecated.} = AnchorObj importcpp: "(window.innerWidth || document.documentElement.clientWidth)@", nodecl}
TOption* {.deprecated.} = OptionObj
TForm* {.deprecated.} = FormObj proc inViewport*(el: Node): bool =
TImage* {.deprecated.} = ImageObj let rect = el.getBoundingClientRect()
TNodeType* {.deprecated.} = NodeType result = rect.top >= 0 and rect.left >= 0 and
TEvent* {.deprecated.} = EventObj rect.bottom <= clientHeight() and
TLocation* {.deprecated.} = LocationObj rect.right <= clientWidth()
THistory* {.deprecated.} = HistoryObj
TNavigator* {.deprecated.} = NavigatorObj proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.}
TStyle* {.deprecated.} = StyleObj proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.}
TScreen* {.deprecated.} = ScreenObj proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.}
TApplet* {.importc, deprecated.} = object of RootObj

View file

@ -1,28 +1,12 @@
# Simple lib to write JS UIs ## Karax -- Single page applications for Nim.
import dom, vdom, jstrutils, components, jdict import dom, vdom, jstrutils, components, jdict
export dom.Event, dom.cloneNode, dom export dom.Event #, dom.cloneNode, dom
proc len(x: Node): int {.importcpp: "#.childNodes.length".}
proc `[]`(x: Node; idx: int): Element {.importcpp: "#.childNodes[#]".}
proc kout*[T](x: T) {.importc: "console.log", varargs.} proc kout*[T](x: T) {.importc: "console.log", varargs.}
## the preferred way of debugging karax applications. ## 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 hasProp(e: Node; prop: cstring): bool {.importcpp: "(#.hasOwnProperty(#))".}
proc rawkey(e: Node): VKey {.importcpp: "#.karaxKey", nodecl.} proc rawkey(e: Node): VKey {.importcpp: "#.karaxKey", nodecl.}
proc key*(e: Node): VKey = proc key*(e: Node): VKey =
@ -30,26 +14,6 @@ proc key*(e: Node): VKey =
else: result = -1 else: result = -1
proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} 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 var
toFocus: Node toFocus: Node
toFocusV: VNode toFocusV: VNode
@ -57,24 +21,69 @@ var
proc setFocus*(n: VNode) = proc setFocus*(n: VNode) =
toFocusV = n toFocusV = n
proc isElementInViewport(el: Node; h: var int): bool = # ----------------- event wrapping ---------------------------------------
let rect = el.getBoundingClientRect()
h = rect.bottom - rect.top template nativeValue(ev): cstring = cast[Element](ev.target).value
result = rect.top >= 0 and rect.left >= 0 and template setNativeValue(ev, val) = cast[Element](ev.target).value = val
rect.bottom <= clientHeight() and
rect.right <= clientWidth() 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 = proc vnodeToDom(n: VNode): Node =
if n.kind == VNodeKind.text: if n.kind == VNodeKind.text:
result = document.createTextNode(n.text) result = document.createTextNode(n.text)
attach n
elif n.kind == VNodeKind.vthunk: elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n) let x = callThunk(vcomponents[n.text], n)
return vnodeToDom(x) result = vnodeToDom(x)
attach n
return result
elif n.kind == VNodeKind.dthunk: elif n.kind == VNodeKind.dthunk:
let x = callThunk(dcomponents[n.text], n) result = callThunk(dcomponents[n.text], n)
return x attach n
return result
else: else:
result = document.createElement(toTag[n.kind]) result = document.createElement(toTag[n.kind])
attach n
for k in n: for k in n:
appendChild(result, vnodeToDom(k)) appendChild(result, vnodeToDom(k))
# text is mapped to 'value': # text is mapped to 'value':
@ -89,14 +98,8 @@ proc vnodeToDom(n: VNode): Node =
for k, v in attrs(n): for k, v in attrs(n):
if v != nil: if v != nil:
result.setAttr(k, v) result.setAttr(k, v)
let myn = n
for e, h in items(n.events): for e, h in items(n.events):
proc wrapper(): proc (ev: Event) = wrapEvent(result, n, e, h)
let hh = h
result = proc (ev: Event) =
assert myn != nil
hh(ev, myn)
result.addEventListener(toEventName[e], wrapper())
if n == toFocusV and toFocus.isNil: if n == toFocusV and toFocus.isNil:
toFocus = result toFocus = result
@ -112,23 +115,12 @@ var
dorender: proc (): VNode {.closure.} dorender: proc (): VNode {.closure.}
currentTree: VNode 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) = proc replaceById(id: cstring; newTree: Node) =
let x = document.getElementById(id) let x = document.getElementById(id)
x.parentNode.replaceChild(newTree, x) x.parentNode.replaceChild(newTree, x)
#newTree.id = id #newTree.id = id
proc equals(a, b: VNode): bool = proc equalsShallow(a, b: VNode): bool =
if a.kind != b.kind: return false if a.kind != b.kind: return false
if a.id != b.id: return false if a.id != b.id: return false
if a.key != b.key: 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.text != b.text: return false
if a.len != b.len: return false if a.len != b.len: return false
for i in 0..<a.len: for i in 0..<a.len:
if not equals(a[i], b[i]): return false if not equalsShallow(a[i], b[i]): return false
if not sameAttrs(a, b): return false if not sameAttrs(a, b): return false
if a.class != b.class: return false if a.class != b.class: return false
# XXX test event listeners here? # XXX test event listeners here?
@ -156,7 +148,8 @@ proc equalsTree(a, b: VNode): bool =
return eq(a, b) return eq(a, b)
proc updateElement(parent, current: Node, newNode, oldNode: VNode) = proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
if not equals(newNode, oldNode): if not equalsShallow(newNode, oldNode):
detach(oldNode)
let n = vnodeToDom(newNode) let n = vnodeToDom(newNode)
if parent == nil: if parent == nil:
replaceById("ROOT", n) replaceById("ROOT", n)
@ -175,6 +168,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
current.appendChild(vnodeToDom(newNode[i])) current.appendChild(vnodeToDom(newNode[i]))
elif oldLength > newLength: elif oldLength > newLength:
for i in countdown(oldLength-1, newLength): for i in countdown(oldLength-1, newLength):
detach(oldNode[i])
current.removeChild(current.lastChild) current.removeChild(current.lastChild)
else: else:
var commonPrefix = 0 var commonPrefix = 0
@ -191,9 +185,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
var pos = min(oldPos, newPos) + 1 var pos = min(oldPos, newPos) + 1
for i in commonPrefix..pos-1: for i in commonPrefix..pos-1:
updateElement(current, current.childNodes[i], updateElement(current, current.childNodes[i], newNode[i], oldNode[i])
newNode[i],
oldNode[i])
var nextChildPos = oldPos + 1 var nextChildPos = oldPos + 1
while pos <= newPos: while pos <= newPos:
@ -206,7 +198,8 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
inc pos inc pos
inc nextChildPos inc nextChildPos
for i in 0..oldPos-pos: for i in pos..oldPos:
detach(oldNode[i])
current.removeChild(current.childNodes[pos]) current.removeChild(current.childNodes[pos])
when false: when false:
@ -229,20 +222,6 @@ proc dodraw() =
if toFocus != nil: if toFocus != nil:
toFocus.focus() 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..<e.len:
visibleKeys(e[i], a, b, h, count)
proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".} proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".}
proc redraw*() = proc redraw*() =
@ -263,50 +242,16 @@ proc setRenderer*(renderer: proc (): VNode) =
dorender = renderer dorender = renderer
window.onload = init window.onload = init
proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.} proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) =
proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.} ## Implements the foundation of Karax's event management.
proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.} ## Karax DSL transforms ``tag(onEvent = handler)`` to
## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
template onImpl(s) {.dirty.} = ## where ``wrapper`` calls the passed ``action`` and then triggers
## a ``redraw``.
proc wrapper(ev: Event; n: VNode) = proc wrapper(ev: Event; n: VNode) =
action(ev, n) action(ev, n)
redraw() redraw()
addEventListener(e, s, wrapper) addEventListener(n, k, wrapper)
proc setOnclick*(e: VNode; action: EventHandler) =
onImpl EventKind.onclick
proc setOnDblclick*(e: VNode; action: EventHandler) =
onImpl EventKind.ondblclick
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
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)
proc setOnHashChange*(action: proc (hashPart: cstring)) = proc setOnHashChange*(action: proc (hashPart: cstring)) =
var onhashChange {.importc: "window.onhashchange".}: proc() var onhashChange {.importc: "window.onhashchange".}: proc()
@ -316,40 +261,6 @@ proc setOnHashChange*(action: proc (hashPart: cstring)) =
redraw() redraw()
onhashchange = wrapper onhashchange = wrapper
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)
redraw()
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 ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring; data: cstring;
cont: proc (httpStatus: int; response: cstring)) = cont: proc (httpStatus: int; response: cstring)) =
@ -402,7 +313,7 @@ proc setupErrorHandler*(useAlert=false) =
{.pop.} {.pop.}
proc prepend*(parent, kid: Element) = proc prepend(parent, kid: Element) =
parent.insertBefore(kid, parent.firstChild) parent.insertBefore(kid, parent.firstChild)
proc loadScript*(jsfilename: cstring) = proc loadScript*(jsfilename: cstring) =

View file

@ -75,7 +75,8 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
if x.kind == nnkExprEqExpr: if x.kind == nnkExprEqExpr:
let key = getName x[0] let key = getName x[0]
if key.startsWith("on"): if key.startsWith("on"):
result.add newCall(!("set" & key), tmp, x[1]) result.add newCall(bindSym"addEventHandler",
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1])
elif key in SpecialAttrs: elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1]) result.add newDotAsgn(tmp, key, x[1])
else: else:

View file

@ -1,6 +1,6 @@
## Virtual DOM implementation for Karax. ## Virtual DOM implementation for Karax.
from dom import Event from dom import Event, Node
import shash, macros import shash, macros
from strutils import toUpperAscii from strutils import toUpperAscii
@ -21,7 +21,7 @@ type
tdiv = "div", tdiv = "div",
anchor, em, strong, small, a, em, strong, small,
strikethrough = "s", cite, quote, strikethrough = "s", cite, quote,
dfn, abbr, data, time, code, `var` = "var", samp, dfn, abbr, data, time, code, `var` = "var", samp,
kdb, sub, sup, italic = "i", bold = "b", underlined = "u", kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
@ -70,7 +70,11 @@ type
ondragover, ## An element or text selection is being dragged over a valid ondragover, ## An element or text selection is being dragged over a valid
## drop target (every 350ms). ## drop target (every 350ms).
ondragstart, ## The user starts dragging an element or text selection. ondragstart, ## The user starts dragging an element or text selection.
ondrop ## An element is dropped on a valid drop target. ondrop, ## An element is dropped on a valid drop target.
onkeyupenter, ## vdom extension: an input field received the ENTER key press
onkeyuplater ## vdom extension: a key was pressed and some time
## passed (useful for on-the-fly text completions)
macro buildLookupTables(): untyped = macro buildLookupTables(): untyped =
var a = newTree(nnkBracket) var a = newTree(nnkBracket)
@ -104,6 +108,8 @@ type
events*: seq[(EventKind, EventHandler)] events*: seq[(EventKind, EventHandler)]
hash*: Hash hash*: Hash
validHash*: bool validHash*: bool
dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node
## is not part of the virtual DOM anymore.
proc value*(n: VNode): cstring = n.text proc value*(n: VNode): cstring = n.text
proc `value=`*(n: VNode; v: cstring) = n.text = v proc `value=`*(n: VNode; v: cstring) = n.text = v

View file

@ -1,10 +1,10 @@
import vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils import dom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils
var var
entries: seq[cstring] entries: seq[cstring]
results: seq[cstring] results: seq[cstring]
timeout : Timeout timeout: Timeout
proc reset() = proc reset() =
results.add cstring"reset started" results.add cstring"reset started"
@ -139,12 +139,9 @@ proc createDom(): VNode =
tdiv: tdiv:
text r text r
setRenderer createDom proc onload() =
proc onload(session: cstring) {.exportc.} =
for i in 0..5: # 0_000: for i in 0..5: # 0_000:
entries.add(cstring($i)) entries.add(cstring($i))
init()
var dtReset = 100 var dtReset = 100
var dtTest = 500 var dtTest = 500
@ -181,3 +178,6 @@ proc onload(session: cstring) {.exportc.} =
t += dtReset t += dtReset
timeout = setTimeout(test7, t) timeout = setTimeout(test7, t)
onload()
setRenderer createDom

1
tests/diffDomTests.nims Normal file
View file

@ -0,0 +1 @@
--path: "../src"