diff --git a/diffDomTests.html b/diffDomTests.html new file mode 100644 index 0000000..5ecf130 --- /dev/null +++ b/diffDomTests.html @@ -0,0 +1,19 @@ + + + + + Diff dom tests + + + + + +
+ + + + + + diff --git a/diffDomTests.nim b/diffDomTests.nim new file mode 100644 index 0000000..fe808f6 --- /dev/null +++ b/diffDomTests.nim @@ -0,0 +1,178 @@ + +import vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils + +var + entries: seq[cstring] + timeout : Timeout + +proc reset() = + kout cstring"reset started" + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] + redraw() + kout cstring"reset finished" + +proc checkOrder(order : seq[int]): bool = + var ul = getElementById("ul") + if ul == nil or len(ul.children) != len(order): + return false + var pos = 0 + for child in ul.children: + if child.id != $order[pos]: + return false + inc pos + return true + +proc check1() = + let result = checkOrder(@[0, 1, 2, 3, 4, 7, 5]) + if result: + kout cstring"test1 - OK" + else: + kout cstring"test1 - FAIL" + +# result: 0 1 2 3 4 7 5 +proc test1() = + kout cstring"test1 started" + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] + entries.insert(cstring("7"), 5) + redraw() + timeout = setTimeout(check1, 20) + +proc check2() = + let result = checkOrder(@[8, 0, 1, 2, 3, 4, 7, 5]) + if result: + kout cstring"test1 - OK" + else: + kout cstring"test2 - FAIL" + +# result: 8 0 1 2 3 4 7 5 +proc test2() = + kout cstring"test2 started" + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] + entries.insert(cstring("7"), 5) + entries.insert(cstring("8"), 0) + redraw() + timeout = setTimeout(check2, 20) + +proc check3() = + let result = checkOrder(@[2, 3, 4, 1]) + if result: + kout cstring"test3 - OK" + else: + kout cstring"test3 - FAIL" + +# result: 2 3 4 1 +proc test3() = + kout cstring"test3 started" + entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1") ] + redraw() + timeout = setTimeout(check3, 20) + +proc check4() = + let result = checkOrder(@[5, 6, 7, 8]) + if result: + kout cstring"test4 - OK" + else: + kout cstring"test4 - FAIL" + +# result: 5 6 7 8 +proc test4() = + kout cstring"test4 started" + entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ] + redraw() + timeout = setTimeout(check4, 20) + +proc check5() = + let result = checkOrder(@[0, 1, 3, 5, 4, 5]) + if result: + kout cstring"test5 - OK" + else: + kout cstring"test5 - FAIL" + +# result: 0 1 3 5 4 5 +proc test5() = + kout cstring"test5 started" + entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5") ] + redraw() + timeout = setTimeout(check5, 20) + +proc check6() = + let result = checkOrder(@[]) + if result: + kout cstring"test6 - OK" + else: + kout cstring"test6 - FAIL" + +# result: empty +proc test6() = + kout cstring"test6 started" + entries = @[] + redraw() + timeout = setTimeout(check6, 20) + +proc check7() = + let result = checkOrder(@[2]) + if result: + kout cstring"test7 - OK" + else: + kout cstring"test7 - FAIL" + +# result: 2 +proc test7() = + kout cstring"test7 started" + entries = @[cstring("2")] + redraw() + timeout = setTimeout(check7, 20) + +proc createEntry(id: int): VNode = + result = buildHtml(): + button(id="" & $id): + text $id + +proc createDom(): VNode = + result = buildHtml(tdiv()): + ul(id="ul"): + for e in entries: + createEntry(parseInt(e)) + +setRenderer createDom + +proc onload(session: cstring) {.exportc.} = + for i in 0..5: # 0_000: + entries.add(cstring($i)) + init() + + var dtReset = 100 + var dtTest = 500 + + var t = dtReset + timeout = setTimeout(test1, t) + t += dtTest + timeout = setTimeout(reset, t) + t += dtReset + + timeout = setTimeout(test2, t) + t += dtTest + timeout = setTimeout(reset, t) + t += dtReset + + timeout = setTimeout(test3, t) + t += dtTest + timeout = setTimeout(reset, t) + t += dtReset + + timeout = setTimeout(test4, t) + t += dtTest + timeout = setTimeout(reset, t) + t += dtReset + + timeout = setTimeout(test5, t) + t += dtTest + timeout = setTimeout(reset, t) + t += dtReset + + timeout = setTimeout(test6, t) + t += dtTest + timeout = setTimeout(reset, t) + t += dtReset + + timeout = setTimeout(test7, t) diff --git a/karax.nim b/karax.nim index 5858f36..fb3003e 100644 --- a/karax.nim +++ b/karax.nim @@ -117,7 +117,7 @@ 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 getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.} #proc getElementsByClassName*(cls: cstring): seq[Element] {.importc: # "document.getElementsByClassName", nodecl.} @@ -147,6 +147,13 @@ proc equals(a, b: VNode): bool = # --> maybe give nodes a hash? return true +proc equalsTree(a, b: VNode): bool = + if not a.validHash: + a.calcHash() + if not b.validHash: + b.calcHash() + return a.hash == b.hash + proc updateElement(parent, current: Node, newNode, oldNode: VNode) = if not equals(newNode, oldNode): let n = vnodeToDom(newNode) @@ -156,21 +163,49 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = parent.replaceChild(n, current) elif newNode.kind != VNodeKind.text: let newLength = newNode.len - let oldLength = oldNode.len + var oldLength = oldNode.len + let minLength = min(newLength, oldLength) assert oldNode.kind == newNode.kind - when false: - if current.nodeName != toTag[oldNode.kind]: - kout current.nodeName - kout toTag[oldNode.kind] - assert false - for i in 0..min(newLength, oldLength)-1: - updateElement(current, current[i], newNode[i], oldNode[i]) - if newLength > oldLength: - for i in oldLength..newLength-1: - current.appendChild(vnodeToDom(newNode[i])) - elif oldLength > newLength: - for i in countdown(oldLength-1, newLength): - current.removeChild(current.lastChild) + when defined(simpleDiff): + for i in 0..min(newLength, oldLength)-1: + updateElement(current, current[i], newNode[i], oldNode[i]) + if newLength > oldLength: + for i in oldLength..newLength-1: + current.appendChild(vnodeToDom(newNode[i])) + elif oldLength > newLength: + for i in countdown(oldLength-1, newLength): + current.removeChild(current.lastChild) + else: + var commonPrefix = 0 + while commonPrefix < minLength and equalsTree(newNode[commonPrefix], oldNode[commonPrefix]): + inc commonPrefix + + var oldPos = oldLength - 1 + var newPos = newLength - 1 + while oldPos >= commonPrefix and newPos >= commonPrefix and equalsTree(newNode[newPos], oldNode[oldPos]): + dec oldPos + dec newPos + + var pos = min(oldPos, newPos) + 1 + for i in commonPrefix..pos-1: + updateElement(current, current.childNodes[i], + newNode[i], + oldNode[i]) + + var nextChildPos = oldPos + 1 + while pos <= newPos: + if nextChildPos == oldLength: + current.appendChild(vnodeToDom(newNode[pos])) + else: + current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos]) + # added new Node, so old state of VDOM have one more Node + inc oldLength + inc pos + inc nextChildPos + + for i in 0..oldPos-pos: + current.removeChild(current.childNodes[pos]) + proc dodraw() = let newtree = dorender() @@ -187,7 +222,7 @@ proc dodraw() = currentTree = newtree # now that it's part of the DOM, give it the focus: if toFocus != nil: - toFocus.focus() + discard "toFocus.focus()" proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) = # we only care about nodes that have a key: diff --git a/scrollapp.nim b/scrollapp.nim index f3b70b2..7b7ce8e 100644 --- a/scrollapp.nim +++ b/scrollapp.nim @@ -90,6 +90,9 @@ proc lazyEntry(args: seq[VNode]): VNode = proc createEntry(id: int; d: cstring; completed, selected: bool): VNode = result = thunk(lazyEntry, id, d, completed, selected) +proc myscroll(ev: Event; n: VNode) = + kout cstring"scrolling event here" + proc createDom(): VNode = result = buildHtml(tdiv(class="todomvc-wrapper")): section(class = "todoapp"): @@ -102,6 +105,11 @@ proc createDom(): VNode = input(class = "toggle-all", `type` = "checkbox", name = "toggle") label(`for` = "toggle-all", onclick = onAllDone): text "Mark all as complete" + tdiv(onscroll = myscroll, style = "overflow: visible; width: 0px; display: block; max-height: 1em"): + for i in 0..4: + tdiv: + text entries[i][0] + var entriesCount = 0 var completedCount = 0 ul(class = "todo-list"): diff --git a/todoapp.nim b/todoapp.nim index 63875a6..ea71c66 100644 --- a/todoapp.nim +++ b/todoapp.nim @@ -119,3 +119,6 @@ proc onload(session: cstring) {.exportc.} = for i in 0..1: # 0_000: entries.add((cstring"Entry " & &i, false)) init() + +for i in 0..10_000: + entries.add((cstring"Entry " & &i, false)) \ No newline at end of file diff --git a/vdom.nim b/vdom.nim index ba1202d..0ddfc3a 100644 --- a/vdom.nim +++ b/vdom.nim @@ -1,6 +1,7 @@ # Virtual DOM implementation from dom import Event +import shash type VNodeKind* {.pure.} = enum @@ -43,6 +44,8 @@ type attrs: seq[cstring] events*: seq[(EventKind, EventHandler)] thunk*: proc (args: seq[VNode]): VNode + hash*: Hash + validHash*: bool proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v @@ -146,6 +149,33 @@ proc toString*(n: VNode; result: var string; indent: int) = for i in 1..indent: result.add ' ' result.add "\L" +proc calcHash*(n: VNode) = + if n.validHash: return + n.validHash = true + var h: Hash = ord n.kind + if n.id != nil: + h &= "id" + h &= n.id + if n.class != nil: + h &= "class" + h &= n.class + if n.key >= 0: + h &= "k" + h &= n.key + for k, v in attrs(n): + h &= " " + h &= k + h &= "=" + h &= v + if n.kind == VNodeKind.text or n.text != nil: + h &= "t" + h &= n.text + else: + for child in items(n): + calcHash(child) + h &= child.hash + n.hash = h + proc `$`*(n: VNode): cstring = var res = "" toString(n, res, 0)