reworked event handling again; added scrollapp example
This commit is contained in:
parent
5bc7d4e336
commit
686ad23f2f
12 changed files with 273 additions and 225 deletions
15
examples/scrollapp/scrollapp.html
Normal file
15
examples/scrollapp/scrollapp.html
Normal 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>
|
||||||
24
examples/scrollapp/scrollapp.nim
Normal file
24
examples/scrollapp/scrollapp.nim
Normal 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
|
||||||
1
examples/scrollapp/scrollapp.nims
Normal file
1
examples/scrollapp/scrollapp.nims
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
--path: "../../src"
|
||||||
|
|
@ -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 & ")"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
110
src/dom.nim
110
src/dom.nim
|
|
@ -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
|
|
||||||
|
|
|
||||||
235
src/karax.nim
235
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
|
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) =
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
12
src/vdom.nim
12
src/vdom.nim
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
1
tests/diffDomTests.nims
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
--path: "../src"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue