diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5e6c415 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [ master ] + pull_request: + branches: [ master ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + container: nimlang/nim + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + # Runs a single command using the runners shell + - name: Tests + run: | + nimble develop -y + nim c -r tests/tester.nim diff --git a/.gitignore b/.gitignore index acb4ec1..842e140 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ nimcache/ karax/tools/karun *.code-workspace *.exe +app.js +app.html diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6e03ea9..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -sudo: false -language: c -os: linux -install: - - git clone https://github.com/nim-lang/nim - - cd nim - - git clone --depth 1 https://github.com/nim-lang/csources.git - - cd csources - - sh build.sh - - cd ../ - - bin/nim c koch - - ./koch boot -d:release - - ./koch tools - - cd .. -before_script: - - set -e - - export PATH=$(pwd)/nim/bin:$(pwd):$PATH -script: - - nimble develop - - nim c -r tests/tester.nim diff --git a/docs/docs.rst b/docs/docs.rst index 8298ba9..3eec1a4 100644 --- a/docs/docs.rst +++ b/docs/docs.rst @@ -10,17 +10,17 @@ and some extensions that allow for an efficient component system. These extensions are: ``VNodeKind.int`` - The node havs a single integer field. + The node has a single integer field. ``VNodeKind.bool`` The node has a single boolean field. -``VNodeKint.vthunk`` +``VNodeKind.vthunk`` The node is a `virtual thunk`:idx:. This means there is a function attached to it that produces the ``VNode`` structure on demand. -``VNodeKint.dthunk`` +``VNodeKind.dthunk`` The node is a `DOM thunk`:idx:. This means there is a function attached to it that produces the ``Node`` DOM structure on demand. @@ -53,6 +53,6 @@ Event system Karax does not abstract over the event system the DOM offers much: The same ``Event`` structure is used. Every callback has the -signature ``proc (ev: Event; n: VNode)``. +signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``. diff --git a/examples/button.nim b/examples/button.nim index bd49dc3..d86fa82 100644 --- a/examples/button.nim +++ b/examples/button.nim @@ -1,4 +1,3 @@ - include karax / prelude var lines: seq[kstring] = @[] diff --git a/examples/carousel/carousel.html b/examples/carousel/carousel.html index 979dd38..500e73a 100644 --- a/examples/carousel/carousel.html +++ b/examples/carousel/carousel.html @@ -1,15 +1,15 @@ - - Carousel app - + + Carousel app + - + -
+
- + - + diff --git a/examples/hellostyle.nim b/examples/hellostyle.nim new file mode 100644 index 0000000..716c2f0 --- /dev/null +++ b/examples/hellostyle.nim @@ -0,0 +1,17 @@ +include karax / prelude +import karax / vstyles + +proc createDom(): VNode = + result = buildHtml(tdiv): + tdiv(style = style(StyleAttr.color, "red".cstring)): + text "red" + tdiv(style = style(StyleAttr.color, "blue")): + text "blue" + # explicit `kstring` required for varargs overload + tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): + text "italic orange" + # can use a string directly + tdiv(style = "font-style: oblique; color: pink".toCss): + text "oblique pink" + +setRenderer createDom diff --git a/examples/hellouniverse.nim b/examples/hellouniverse.nim index 9d6ebc6..8343d07 100644 --- a/examples/hellouniverse.nim +++ b/examples/hellouniverse.nim @@ -1,10 +1,9 @@ - include karax / prelude import random proc createDom(): VNode = result = buildHtml(tdiv): - if random(100) <= 50: + if rand(100) <= 50: text "Hello World!" else: text "Hello Universe" diff --git a/examples/login.nim b/examples/login.nim index 6cc38b1..0350e23 100644 --- a/examples/login.nim +++ b/examples/login.nim @@ -1,6 +1,5 @@ - include karax / prelude -from future import `=>` +from sugar import `=>` import karax / errors proc loginField(desc, field, class: kstring; @@ -18,7 +17,7 @@ const proc validateNotEmpty(field: kstring): proc () = result = proc () = let x = getVNodeById(field) - if x.text.isNil or x.text == "": + if x.text == "": errors.setError(field, field & " must not be empty") else: errors.setError(field, "") @@ -28,7 +27,7 @@ var loggedIn: bool proc loginDialog(): VNode = result = buildHtml(tdiv): if not loggedIn: - loginField("Name :", username, "input", validateNotEmpty) + loginField("Name: ", username, "input", validateNotEmpty) loginField("Password: ", password, "password", validateNotEmpty) button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()): text "Login" diff --git a/examples/mediaplayer/playerapp.html b/examples/mediaplayer/playerapp.html index 9e076e6..a45a2e8 100644 --- a/examples/mediaplayer/playerapp.html +++ b/examples/mediaplayer/playerapp.html @@ -1,15 +1,15 @@ - - Player app - + + Player app + - + -
+
- + - + diff --git a/examples/scrollapp/scrollapp.html b/examples/scrollapp/scrollapp.html index 1ff8b1b..44d657e 100644 --- a/examples/scrollapp/scrollapp.html +++ b/examples/scrollapp/scrollapp.html @@ -1,15 +1,15 @@ - - Todo app - + + Todo app + - + -
+
- + - + diff --git a/examples/todoapp/todoapp.html b/examples/todoapp/todoapp.html index 488a3bf..ab3daa7 100644 --- a/examples/todoapp/todoapp.html +++ b/examples/todoapp/todoapp.html @@ -1,16 +1,16 @@ - - Todo app - - + + Todo app + + - + -
+
- + - + diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index b944de5..e3720fd 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -9,7 +9,7 @@ var selectedEntry = -1 filter: Filter entriesLen: int - + doneswitch = true const contentSuffix = cstring"content" completedSuffix = cstring"completed" @@ -62,9 +62,9 @@ proc toggleEntry(ev: Event; n: VNode) = markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = - clear() - selectedEntry = -1 - + for i in 0.. - + diff --git a/experiments/echartstest.html b/experiments/echartstest.html index fd510bd..8c9b225 100644 --- a/experiments/echartstest.html +++ b/experiments/echartstest.html @@ -10,5 +10,5 @@
- + diff --git a/experiments/refs/demo.html b/experiments/refs/demo.html index e1fe419..941333a 100644 --- a/experiments/refs/demo.html +++ b/experiments/refs/demo.html @@ -8,6 +8,6 @@
- + diff --git a/experiments/scrollapp.html b/experiments/scrollapp.html index ad3ce2f..8b909f1 100644 --- a/experiments/scrollapp.html +++ b/experiments/scrollapp.html @@ -10,7 +10,7 @@
- + diff --git a/karax.nimble b/karax.nimble index 87ce20f..8712a6d 100644 --- a/karax.nimble +++ b/karax.nimble @@ -1,14 +1,15 @@ # Package -version = "1.0.0" +version = "1.2.1" author = "Andreas Rumpf" description = "Karax is a framework for developing single page applications in Nim." license = "MIT" # Dependencies -requires "nim >= 0.16.1" - +requires "nim >= 0.18.0" +requires "ws" +requires "dotenv" skipDirs = @["examples", "experiments", "tests"] bin = @["karax/tools/karun"] diff --git a/karax/compact.nim b/karax/compact.nim index 3363802..4e5dd6d 100644 --- a/karax/compact.nim +++ b/karax/compact.nim @@ -61,7 +61,7 @@ proc newname*(n: NimNode): NimNode = n[1] = newname(n[1]) result = n elif n.kind == nnkSym: - result = ident($n.symbol) + result = ident(n.strVal) else: result = n diff --git a/karax/jwebsockets.nim b/karax/jwebsockets.nim index 2b1412e..7c94da7 100644 --- a/karax/jwebsockets.nim +++ b/karax/jwebsockets.nim @@ -11,4 +11,7 @@ type proc newWebSocket*(url, key: cstring): WebSocket {.importcpp: "new WebSocket(@)".} +proc newWebSocket*(url: cstring): WebSocket + {.importcpp: "new WebSocket(@)".} + proc send*(w: WebSocket; data: cstring) {.importcpp.} diff --git a/karax/kajax.nim b/karax/kajax.nim index 114b29d..4235b50 100644 --- a/karax/kajax.nim +++ b/karax/kajax.nim @@ -4,6 +4,66 @@ ## handling. import karax +import jsffi except `&` +import jscore +import dom + +type + ProgressEvent* {.importc.}= object + loaded*: float + total*: float + + FormData* {.importc.} = JsObject + + HttpRequest* {.importc.} = ref object + ThisObj {.importc.} = ref object + readyState, status: int + responseText, statusText: cstring + + XMLHttpRequestUpload* {.importc.} = JsObject + +proc newFormData*(): FormData {.importcpp: "new FormData()".} +proc append*(f: FormData, key: cstring, value: Blob) {.importcpp:"#.append(@)".} +proc append*(f: FormData, key: cstring, value: cstring) {.importcpp:"#.append(@)".} + +proc setRequestHeader*(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".} +proc statechange*(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".} +proc send*(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".} +proc send*(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".} +proc open*(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".} +proc newRequest*(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".} + +when not declared(dom.File): + type + DomFile = ref FileObj + FileObj {.importc.} = object of Blob + lastModified: int + name: cstring +else: + type + DomFile = dom.File + +proc uploadFile*(url: cstring, file: Blob, onprogress :proc(data: ProgressEvent), + cont: proc (httpStatus: int; response: cstring); + headers: openarray[(cstring, cstring)] = []) = + proc contWrapper(httpStatus: int; response: cstring) = + cont(httpStatus, response) + + proc upload(r: HttpRequest):XMLHttpRequestUpload {.importcpp: "#.upload".} + var this {.importc: "this".}: ThisObj + + var formData = newFormData() + formData.append("upload_file",file) + formData.append("filename", DomFile(file).name) + let ajax = newRequest() + ajax.open("POST", url, true) + for a, b in items(headers): + ajax.setRequestHeader(a, b) + ajax.statechange proc() = + if this.readyState == 4: + contWrapper(this.status, this.responseText) + ajax.upload.onprogress = onprogress + ajax.send(formData.to(cstring)) proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)]; data: cstring; @@ -16,20 +76,8 @@ proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)]; cont(httpStatus, response) if doRedraw: redraw(kxi) - type - HttpRequest {.importc.} = ref object - ThisObj {.importc.} = ref object - readyState, status: int - responseText, statusText: cstring - - proc setRequestHeader(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".} - proc statechange(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".} - proc send(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".} - proc send(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".} - proc open(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".} - proc newRequest(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".} - var this {.importc: "this".}: ThisObj + let ajax = newRequest() ajax.open(meth, url, true) for a, b in items(headers): @@ -81,3 +129,4 @@ proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)]; proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".} proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".} + diff --git a/karax/karax.nim b/karax/karax.nim index 2fc8632..2246676 100644 --- a/karax/karax.nim +++ b/karax/karax.nim @@ -7,17 +7,16 @@ export kdom.Event, kdom.Blob when defined(nimNoNil): {.experimental: "notnil".} -proc kout*[T](x: T) {.importc: "console.log", varargs, deprecated.} - ## the preferred way of debugging karax applications. Now deprecated, - ## you can now use ``system.echo`` instead. +proc kout*[T](x: T) {.importc: "console.log", varargs.} + ## The preferred way of debugging karax applications. type PatchKind = enum - pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach + pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame Patch = object k: PatchKind parent, current: Node - n: VNode + newNode, oldNode: VNode PatchV = object parent, newChild: VNode pos: int @@ -31,13 +30,14 @@ type KaraxInstance* = ref object ## underlying karax instance. Usually you don't have ## know about this. - rootId: cstring not nil + rootId: cstring renderer: proc (data: RouterData): VNode {.closure.} currentTree: VNode postRenderCallback: proc (data: RouterData) toFocus: Node toFocusV: VNode renderId: int + rendering: bool patches: seq[Patch] # we reuse this to save allocations patchLen: int patchesV: seq[PatchV] @@ -78,6 +78,24 @@ template keyeventBody() = # Do not call redraw() here! That is already done # by ``karax.addEventHandler``. +proc karaxEvents(d: Node): JSeq[(cstring, NativeEventHandler)] {.importcpp: "#.karaxEvents".} +proc `karaxEvents=`(d: Node; value: JSeq[(cstring, NativeEventHandler)]) {.importcpp: "#.karaxEvents = #".} + +proc addEventShell(d: Node; name: cstring; h: NativeEventHandler) = + # The DOM is such a pathetic piece of junk that it doesn't + # offer 'removeAllEventHandlers()'. Hence we store the event + # handler twice in 'd' so that we can emulate this properly. + # This is required to fix bug #139. + d.addEventListener(name, h) + if d.karaxEvents == nil: + d.karaxEvents = newJSeq[(cstring, NativeEventHandler)]() + d.karaxEvents.add((name, h)) + +proc removeAllEventHandlers(d: Node) = + if d.karaxEvents != nil: + for i in 0..= kxi.patches.len: # allocate more space: - kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, n: na)) + kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, + newNode: na, oldNode: oldNode)) else: kxi.patches[L].k = ka kxi.patches[L].parent = parenta kxi.patches[L].current = currenta - kxi.patches[L].n = na + kxi.patches[L].newNode = na + kxi.patches[L].oldNode = oldNode inc kxi.patchLen proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = @@ -344,26 +382,40 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = kxi.patchesV[L].pos = pos inc kxi.patchLenV +proc moveDom(dest, src: VNode) = + dest.dom = src.dom + src.dom = nil + if dest.id != nil: + kxi.byId[dest.id] = dest + assert dest.len == src.len + for i in 0.. We cannot ever use the old VDOM, we have to use the new virtual +# DOM. For identical nodes we need to take over the .dom field from the old +# node since we don't recompute them. This must be done recursively. In +# vnodeToDom we have to check whether the 'dom' field was already set. If so, +# There is nothing to do. +# "Similar" nodes can have the opposite effect; consider +# +# AAAABAAAA +# AAAACDAAAA +# +# In this example B did change to C and 'D' is new. However, replacing B by +# CD is fine. +# + + +proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) = when defined(stats): if kxi.recursion > 100: - echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode) + echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false) if oldNode.kind == VNodeKind.text: echo oldNode.text - #return - #doAssert false, "overflow!" inc kxi.recursion - result = eq(newNode, oldNode) + let result = eq(newNode, oldNode, false) case result of componentsIdentical: kxi.components.add ComponentPair(oldNode: VComponent(oldNode), @@ -404,90 +478,54 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) - when false: - if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: - if oldNode.text != newNode.text: - oldNode.text = newNode.text - oldNode.dom.value = newNode.text let newLength = newNode.len let oldLength = oldNode.len - if newLength == 0 and oldLength == 0: return result + if newLength == 0 and oldLength == 0: return let minLength = min(newLength, oldLength) assert oldNode.kind == newNode.kind var commonPrefix = 0 - let isSpecial = oldNode.kind == VNodeKind.component or - oldNode.kind == VNodeKind.vthunk or - oldNode.kind == VNodeKind.dthunk - template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) = - let oldLen = kxi.patchLen - let oldLenV = kxi.patchLenV - assert i < a.len - assert j < b.len - let r = if isSpecial: - diff(a[i], b[j], parent, current, kxi) - else: - diff(a[i], b[j], current, current.childNodes[j], kxi) - case r - of identical, componentsIdentical, similar: - a[i] = b[j] - action - of usenewNode: - kxi.addPatchV(b, j, a[i]) - action - # unfortunately, we need to propagate the changes upwards: - result = useNewNode - of different: - # undo what 'diff' would have done: - kxi.patchLen = oldLen - kxi.patchLenV = oldLenV - if result != different: result = r - break # compute common prefix: while commonPrefix < minLength: - eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): + if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical: + kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix]) inc commonPrefix + else: + break # compute common suffix: var oldPos = oldLength - 1 var newPos = newLength - 1 while oldPos >= commonPrefix and newPos >= commonPrefix: - eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): + if eq(newNode[newPos], oldNode[oldPos], true) == identical: + kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos]) dec oldPos dec newPos + else: + break let pos = min(oldPos, newPos) + 1 # now the different children are in commonPrefix .. pos - 1: for i in commonPrefix..pos-1: - let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) - if r == usenewNode: - #oldNode[i] = newNode[i] - kxi.addPatchV(oldNode, i, newNode[i]) - elif r != different: - newNode[i] = oldNode[i] - #else: - # result = usenewNode + diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) if oldPos + 1 == oldLength: for i in pos..newPos: - kxi.addPatch(pkAppend, current, nil, newNode[i]) - result = usenewNode + kxi.addPatch(pkAppend, current, nil, newNode[i], nil) else: let before = current.childNodes[oldPos + 1] for i in pos..newPos: - kxi.addPatch(pkInsertBefore, current, before, newNode[i]) - result = usenewNode + kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil) # XXX call 'attach' here? for i in pos..oldPos: detach(oldNode[i]) #doAssert i < current.childNodes.len - kxi.addPatch(pkRemove, current, current.childNodes[i], nil) - result = usenewNode + kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil) of different: detach(oldNode) - kxi.addPatch(pkReplace, parent, current, newNode) + kxi.addPatch(pkReplace, parent, current, newNode, nil) of usenewNode: doAssert(false, "eq returned usenewNode") when defined(stats): dec kxi.recursion @@ -517,24 +555,25 @@ proc applyComponents(kxi: KaraxInstance) = x.renderedVersion = x.version if oldExpanded == nil: detach(x) - kxi.addPatch(pkReplace, parent, current, x.expanded) + kxi.addPatch(pkReplace, parent, current, x.expanded, nil) when defined(karaxDebug): echo "Component ", newNode.text, ": old expansion didn't exist" else: - let res = diff(x.expanded, oldExpanded, parent, current, kxi) - if res == usenewNode: - when defined(karaxDebug): - echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)" - discard "diff created a patchset for us, so this is fine" - elif res != different: - when defined(karaxDebug): - echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever" - x.expanded = oldExpanded - assert oldExpanded.dom != nil, "old expanded.dom is nil" - else: - when defined(karaxDebug): - echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)" - assert x.expanded.dom != nil, "expanded.dom is nil" + diff(x.expanded, oldExpanded, parent, current, kxi) + when false: + if res == usenewNode: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)" + discard "diff created a patchset for us, so this is fine" + elif res != different: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever" + x.expanded = oldExpanded + assert oldExpanded.dom != nil, "old expanded.dom is nil" + else: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)" + assert x.expanded.dom != nil, "expanded.dom is nil" inc i setLen(kxi.components, 0) @@ -549,7 +588,7 @@ when defined(stats): proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) = detach(parent[position]) let current = parent.dom - kxi.addPatch(pkRemove, current, current.childNodes[position], nil) + kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil) parent.delete(position) applyPatch(kxi) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) @@ -557,11 +596,11 @@ proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) = proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) = let current = parent.dom if position >= parent.len: - kxi.addPatch(pkAppend, current, nil, kid) + kxi.addPatch(pkAppend, current, nil, kid, nil) parent.add(kid) else: let before = current.childNodes[position] - kxi.addPatch(pkInsertBefore, current, before, kid) + kxi.addPatch(pkInsertBefore, current, before, kid, nil) parent.insert(kid, position) applyPatch(kxi) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) @@ -569,7 +608,7 @@ proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) = proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = let olddom = oldNode.dom doAssert olddom != nil - discard diff(newNode, oldNode, nil, olddom, kxi) + diff(newNode, oldNode, nil, olddom, kxi) # this is a bit nasty: Since we cannot patch the 'parent' of # the current VNode (because we don't store it at all!), we # need to override the fields individually: @@ -583,21 +622,41 @@ proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = var onhashChange {.importc: "window.onhashchange".}: proc() var hashPart {.importc: "window.location.hash".}: cstring +proc avoidDomDiffing*(kxi: KaraxInstance = kxi) = + ## enforce a full redraw for the next redraw operation. + ## This can be used as a temporary way to workaround DOM diffing + ## problems or to avoid the DOM diffing when you already know + ## it should use a completely new DOM. + ## This is an experimental API. + kxi.currentTree = nil + +proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".} +when false: + proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".} + proc dodraw(kxi: KaraxInstance) = if kxi.renderer.isNil: return + kxi.renderId = 0 + + if kxi.rendering: + # there is a render already running, delay 1 frame + kxi.renderId = reqFrame(proc () = kxi.dodraw) + return + + kxi.rendering = true + let rdata = RouterData(hashPart: hashPart) let newtree = kxi.renderer(rdata) inc kxi.runCount newtree.id = kxi.rootId kxi.toFocus = nil if kxi.currentTree == nil: - let asdom = vnodeToDom(newtree, kxi) + let asdom = toDom(newtree, useAttachedNode = true, kxi) replaceById(kxi.rootId, asdom) else: doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) let olddom = document.getElementById(kxi.rootId) - discard diff(newtree, kxi.currentTree, nil, olddom, kxi) - #kout cstring"patch len ", patches.len + diff(newtree, kxi.currentTree, nil, olddom, kxi) when defined(profileKarax): echo "<<<<<<<<<<<<<<" echa reasons @@ -616,16 +675,12 @@ proc dodraw(kxi: KaraxInstance) = # now that it's part of the DOM, give it the focus: if kxi.toFocus != nil: kxi.toFocus.focus() - kxi.renderId = 0 + kxi.rendering = false when defined(stats): kxi.recursion = 0 var total = 0 echo "depth ", depth(kxi.currentTree, total), " total ", total -proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".} -when false: - proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".} - proc redraw*(kxi: KaraxInstance = kxi) = # we buffer redraw requests: when false: @@ -713,7 +768,7 @@ proc addEventHandler*(n: VNode; k: EventKind; action: proc(); if not kxi.surpressRedraws: redraw(kxi) addEventListener(n, k, wrapper) -proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated.} = +proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} = ## Now deprecated, instead pass a callback to ``setRenderer`` that receives ## a ``data: RouterData`` parameter. proc wrapper() = @@ -745,11 +800,10 @@ proc prepend(parent, kid: Element) = parent.insertBefore(kid, parent.firstChild) proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) = - let body = document.getElementById("body") let s = document.createElement("script") s.setAttr "type", "text/javascript" s.setAttr "src", jsfilename - body.prepend(s) + document.body.prepend(s) redraw(kxi) proc runLater*(action: proc(); later = 400): Timeout {.discardable.} = @@ -763,8 +817,18 @@ proc setInputText*(n: VNode; s: cstring) = n.text = s if n.dom != nil: n.dom.value = s +proc getInputText*(n: VNode): cstring = + if n.dom != nil: + result = n.dom.value + proc toChecked*(checked: bool): cstring = (if checked: cstring"checked" else: cstring(nil)) proc toDisabled*(disabled: bool): cstring = (if disabled: cstring"disabled" else: cstring(nil)) + +proc toSelected*(selected: bool): cstring = + (if selected: cstring"selected" else: cstring(nil)) + +proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node = + result = toDom(n, useAttachedNode = false, kxi) diff --git a/karax/karaxdsl.nim b/karax/karaxdsl.nim index 81ba99d..451632c 100644 --- a/karax/karaxdsl.nim +++ b/karax/karaxdsl.nim @@ -11,8 +11,8 @@ const proc getName(n: NimNode): string = case n.kind - of nnkIdent: - result = $n.ident + of nnkIdent, nnkSym: + result = $n of nnkAccQuoted: result = "" for i in 0..`_. +from dom import nil -when not defined(js) and not defined(Nimdoc): - {.error: "This module only works on the JavaScript platform".} - -type - EventTarget* = ref EventTargetObj - EventTargetObj {.importc.} = object of RootObj - onabort*: proc (event: Event) {.nimcall.} - onblur*: proc (event: Event) {.nimcall.} - onchange*: proc (event: Event) {.nimcall.} - onclick*: proc (event: Event) {.nimcall.} - ondblclick*: proc (event: Event) {.nimcall.} - onerror*: proc (event: Event) {.nimcall.} - onfocus*: proc (event: Event) {.nimcall.} - onkeydown*: proc (event: Event) {.nimcall.} - onkeypress*: proc (event: Event) {.nimcall.} - onkeyup*: proc (event: Event) {.nimcall.} - onload*: proc (event: Event) {.nimcall.} - onmousedown*: proc (event: Event) {.nimcall.} - onmousemove*: proc (event: Event) {.nimcall.} - onmouseout*: proc (event: Event) {.nimcall.} - onmouseover*: proc (event: Event) {.nimcall.} - onmouseup*: proc (event: Event) {.nimcall.} - onreset*: proc (event: Event) {.nimcall.} - onselect*: proc (event: Event) {.nimcall.} - onsubmit*: proc (event: Event) {.nimcall.} - onunload*: proc (event: Event) {.nimcall.} - - # https://developer.mozilla.org/en-US/docs/Web/Events - DomEvent* {.pure.} = enum - Abort = "abort", - BeforeInput = "beforeinput", - Blur = "blur", - Click = "click", - CompositionEnd = "compositionend", - CompositionStart = "compositionstart", - CompositionUpdate = "compositionupdate", - DblClick = "dblclick", - Error = "error", - Focus = "focus", - FocusIn = "focusin", - FocusOut = "focusout", - Input = "input", - KeyDown = "keydown", - KeyPress = "keypress", - KeyUp = "keyup", - Load = "load", - MouseDown = "mousedown", - MouseEnter = "mouseenter", - MouseLeave = "mouseleave", - MouseMove = "mousemove", - MouseOut = "mouseout", - MouseOver = "mouseover", - MouseUp = "mouseup", - Resize = "resize", - Scroll = "scroll", - Select = "select", - Unload = "unload", - Wheel = "wheel" - - Window* = ref WindowObj - WindowObj {.importc.} = object of EventTargetObj - document*: Document - event*: Event - history*: History - location*: Location - closed*: bool - defaultStatus*: cstring - innerHeight*, innerWidth*: int - locationbar*: ref LocationBar - menubar*: ref MenuBar - name*: cstring - outerHeight*, outerWidth*: int - pageXOffset*, pageYOffset*: int - personalbar*: ref PersonalBar - scrollbars*: ref ScrollBars - statusbar*: ref StatusBar - status*: cstring - toolbar*: ref ToolBar - frames*: seq[Frame] - screen*: Screen - - Frame* = ref FrameObj - FrameObj {.importc.} = object of WindowObj - - ClassList* = ref ClassListObj - ClassListObj {.importc.} = object of RootObj - - NodeType* = enum - ElementNode = 1, - AttributeNode, - TextNode, - CDATANode, - EntityRefNode, - EntityNode, - ProcessingInstructionNode, - CommentNode, - DocumentNode, - DocumentTypeNode, - DocumentFragmentNode, - NotationNode - - Node* = ref NodeObj - NodeObj {.importc.} = object of EventTargetObj - attributes*: seq[Node] - childNodes*: seq[Node] - children*: seq[Node] - data*: cstring - firstChild*: Node - lastChild*: Node - nextSibling*: Node - nodeName*: cstring - nodeType*: NodeType - nodeValue*: cstring - parentNode*: Node - previousSibling*: Node - innerHTML*: cstring - style*: Style - when defined(nodejs): - id*: cstring - - Document* = ref DocumentObj - DocumentObj {.importc.} = object of NodeObj - alinkColor*: cstring - bgColor*: cstring - body*: Element - charset*: cstring - cookie*: cstring - defaultCharset*: cstring - fgColor*: cstring - head*: Element - lastModified*: cstring - linkColor*: cstring - referrer*: cstring - title*: cstring - URL*: cstring - vlinkColor*: cstring - anchors*: seq[AnchorElement] - forms*: seq[FormElement] - images*: seq[ImageElement] - applets*: seq[Element] - embeds*: seq[EmbedElement] - links*: seq[LinkElement] - - Element* = ref ElementObj - ElementObj {.importc.} = object of NodeObj - classList*: Classlist - checked*: bool - defaultChecked*: bool - defaultValue*: cstring - disabled*: bool - form*: FormElement - name*: cstring - readOnly*: bool - options*: seq[OptionElement] - selectedOptions*: seq[OptionElement] - clientWidth*, clientHeight*: int - contentEditable*: string - isContentEditable*: bool - dir*: string - offsetHeight*: int - offsetWidth*: int - offsetLeft*: int - offsetTop*: int - - # https://developer.mozilla.org/en-US/docs/Web/API/ValidityState - ValidityState* = ref ValidityStateObj - ValidityStateObj {.importc.} = object - badInput*: bool - customError*: bool - patternMismatch*: bool - rangeOverflow*: bool - rangeUnderflow*: bool - stepMismatch*: bool - tooLong*: bool - tooShort*: bool - typeMismatch*: bool - valid*: bool - valueMissing*: bool - - # https://developer.mozilla.org/en-US/docs/Web/API/Blob - Blob* = ref BlobObj - BlobObj {.importc.} = object of RootObj - size*: int - `type`*: cstring - - # https://developer.mozilla.org/en-US/docs/Web/API/File - File* = ref FileObj - FileObj {.importc.} = object of Blob - lastModified*: int - name*: cstring - - # https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement - InputElement* = ref InputElementObj - InputElementObj {.importc.} = object of Element - # Properties related to the parent form - formAction*: cstring - formEncType*: cstring - formMethod*: cstring - formNoValidate*: bool - formTarget*: cstring - - # Properties that apply to any type of input element that is not hidden - `type`*: cstring - autofocus*: bool - required*: bool - value*: cstring - validity*: ValidityState - validationMessage*: cstring - willValidate*: bool - - # Properties that apply only to elements of type "checkbox" or "radio" - indeterminate*: bool - - # Properties that apply only to elements of type "image" - alt*: cstring - height*: cstring - src*: cstring - width*: cstring - - # Properties that apply only to elements of type "file" - accept*: cstring - files*: seq[Blob] - - # Properties that apply only to text/number-containing or elements - autocomplete*: cstring - maxLength*: int - size*: int - pattern*: cstring - placeholder*: cstring - min*: cstring - max*: cstring - selectionStart*: int - selectionEnd*: int - selectionDirection*: cstring - - # Properties not yet categorized - dirName*: cstring - accessKey*: cstring - list*: Element - multiple*: bool - labels*: seq[Element] - step*: cstring - valueAsDate*: cstring - valueAsNumber*: float - - LinkElement* = ref LinkObj - LinkObj {.importc.} = object of ElementObj - target*: cstring - text*: cstring - x*: int - y*: int - - EmbedElement* = ref EmbedObj - EmbedObj {.importc.} = object of ElementObj - height*: int - hspace*: int - src*: cstring - width*: int - `type`*: cstring - vspace*: int - - AnchorElement* = ref AnchorObj - AnchorObj {.importc.} = object of ElementObj - text*: cstring - x*, y*: int - - OptionElement* = ref OptionObj - OptionObj {.importc.} = object of ElementObj - defaultSelected*: bool - selected*: bool - selectedIndex*: int - text*: cstring - value*: cstring - - # https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement - FormElement* = ref FormObj - FormObj {.importc.} = object of ElementObj - acceptCharset*: cstring - action*: cstring - autocomplete*: cstring - elements*: seq[Element] - encoding*: cstring - enctype*: cstring - length*: int - `method`*: cstring - noValidate*: bool - target*: cstring - - ImageElement* = ref ImageObj - ImageObj {.importc.} = object of ElementObj - border*: int - complete*: bool - height*: int - hspace*: int - lowsrc*: cstring - src*: cstring - vspace*: int - width*: int - - Style* = ref StyleObj - StyleObj {.importc.} = object of RootObj - background*: cstring - backgroundAttachment*: cstring - backgroundColor*: cstring - backgroundImage*: cstring - backgroundPosition*: cstring - backgroundRepeat*: cstring - border*: cstring - borderBottom*: cstring - borderBottomColor*: cstring - borderBottomStyle*: cstring - borderBottomWidth*: cstring - borderColor*: cstring - borderLeft*: cstring - borderLeftColor*: cstring - borderLeftStyle*: cstring - borderLeftWidth*: cstring - borderRight*: cstring - borderRightColor*: cstring - borderRightStyle*: cstring - borderRightWidth*: cstring - borderStyle*: cstring - borderTop*: cstring - borderTopColor*: cstring - borderTopStyle*: cstring - borderTopWidth*: cstring - borderWidth*: cstring - bottom*: cstring - captionSide*: cstring - clear*: cstring - clip*: cstring - color*: cstring - cursor*: cstring - direction*: cstring - display*: cstring - emptyCells*: cstring - cssFloat*: cstring - font*: cstring - fontFamily*: cstring - fontSize*: cstring - fontStretch*: cstring - fontStyle*: cstring - fontVariant*: cstring - fontWeight*: cstring - height*: cstring - left*: cstring - letterSpacing*: cstring - lineHeight*: cstring - listStyle*: cstring - listStyleImage*: cstring - listStylePosition*: cstring - listStyleType*: cstring - margin*: cstring - marginBottom*: cstring - marginLeft*: cstring - marginRight*: cstring - marginTop*: cstring - maxHeight*: cstring - maxWidth*: cstring - minHeight*: cstring - minWidth*: cstring - overflow*: cstring - padding*: cstring - paddingBottom*: cstring - paddingLeft*: cstring - paddingRight*: cstring - paddingTop*: cstring - pageBreakAfter*: cstring - pageBreakBefore*: cstring - pointerEvents*: cstring - position*: cstring - right*: cstring - scrollbar3dLightColor*: cstring - scrollbarArrowColor*: cstring - scrollbarBaseColor*: cstring - scrollbarDarkshadowColor*: cstring - scrollbarFaceColor*: cstring - scrollbarHighlightColor*: cstring - scrollbarShadowColor*: cstring - scrollbarTrackColor*: cstring - tableLayout*: cstring - textAlign*: cstring - textDecoration*: cstring - textIndent*: cstring - textTransform*: cstring - transform*: cstring - top*: cstring - verticalAlign*: cstring - visibility*: cstring - width*: cstring - wordSpacing*: cstring - zIndex*: int - - EventPhase* = enum - None = 0, - CapturingPhase, - AtTarget, - BubblingPhase - - # https://developer.mozilla.org/en-US/docs/Web/API/Event - Event* = ref EventObj - EventObj {.importc.} = object of RootObj - bubbles*: bool - cancelBubble*: bool - cancelable*: bool - composed*: bool - currentTarget*: Node - defaultPrevented*: bool - eventPhase*: int - target*: Node - `type`*: cstring - isTrusted*: bool - - # https://developer.mozilla.org/en-US/docs/Web/API/UIEvent - UIEvent* = ref UIEventObj - UIEventObj {.importc.} = object of Event - detail*: int64 - view*: Window - - # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent - KeyboardEvent* = ref KeyboardEventObj - KeyboardEventObj {.importc.} = object of UIEvent - altKey*, ctrlKey*, metaKey*, shiftKey*: bool - code*: cstring - isComposing*: bool - key*: cstring - keyCode*: int - location*: int - - # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values - KeyboardEventKey* {.pure.} = enum - # Modifier keys - Alt, - AltGraph, - CapsLock, - Control, - Fn, - FnLock, - Hyper, - Meta, - NumLock, - ScrollLock, - Shift, - Super, - Symbol, - SymbolLock, - - # Whitespace keys - ArrowDown, - ArrowLeft, - ArrowRight, - ArrowUp, - End, - Home, - PageDown, - PageUp, - - # Editing keys - Backspace, - Clear, - Copy, - CrSel, - Cut, - Delete, - EraseEof, - ExSel, - Insert, - Paste, - Redo, - Undo, - - # UI keys - Accept, - Again, - Attn, - Cancel, - ContextMenu, - Escape, - Execute, - Find, - Finish, - Help, - Pause, - Play, - Props, - Select, - ZoomIn, - ZoomOut, - - # Device keys - BrigtnessDown, - BrigtnessUp, - Eject, - LogOff, - Power, - PowerOff, - PrintScreen, - Hibernate, - Standby, - WakeUp, - - # Common IME keys - AllCandidates, - Alphanumeric, - CodeInput, - Compose, - Convert, - Dead, - FinalMode, - GroupFirst, - GroupLast, - GroupNext, - GroupPrevious, - ModeChange, - NextCandidate, - NonConvert, - PreviousCandidate, - Process, - SingleCandidate, - - # Korean keyboards only - HangulMode, - HanjaMode, - JunjaMode, - - # Japanese keyboards only - Eisu, - Hankaku, - Hiragana, - HiraganaKatakana, - KanaMode, - KanjiMode, - Katakana, - Romaji, - Zenkaku, - ZenkakuHanaku, - - # Function keys - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - Soft1, - Soft2, - Soft3, - Soft4, - - # Phone keys - AppSwitch, - Call, - Camera, - CameraFocus, - EndCall, - GoBack, - GoHome, - HeadsetHook, - LastNumberRedial, - Notification, - MannerMode, - VoiceDial, - - # Multimedia keys - ChannelDown, - ChannelUp, - MediaFastForward, - MediaPause, - MediaPlay, - MediaPlayPause, - MediaRecord, - MediaRewind, - MediaStop, - MediaTrackNext, - MediaTrackPrevious, - - # Audio control keys - AudioBalanceLeft, - AudioBalanceRight, - AudioBassDown, - AudioBassBoostDown, - AudioBassBoostToggle, - AudioBassBoostUp, - AudioBassUp, - AudioFaderFront, - AudioFaderRear, - AudioSurroundModeNext, - AudioTrebleDown, - AudioTrebleUp, - AudioVolumeDown, - AUdioVolumeMute, - AudioVolumeUp, - MicrophoneToggle, - MicrophoneVolumeDown, - MicrophoneVolumeMute, - MicrophoneVolumeUp, - - # TV control keys - TV, - TV3DMode, - TVAntennaCable, - TVAudioDescription, - TVAudioDescriptionMixDown, - TVAudioDescriptionMixUp, - TVContentsMenu, - TVDataService, - TVInput, - TVInputComponent1, - TVInputComponent2, - TVInputComposite1, - TVInputComposite2, - TVInputHDMI1, - TVInputHDMI2, - TVInputHDMI3, - TVInputHDMI4, - TVInputVGA1, - TVMediaContext, - TVNetwork, - TVNumberEntry, - TVPower, - TVRadioService, - TVSatellite, - TVSatelliteBS, - TVSatelliteCS, - TVSatelliteToggle, - TVTerrestrialAnalog, - TVTerrestrialDigital, - TVTimer, - - # Media controller keys - AVRInput, - AVRPower, - ColorF0Red, - ColorF1Green, - ColorF2Yellow, - ColorF3Blue, - ColorF4Grey, - ColorF5Brown, - ClosedCaptionToggle, - Dimmer, - DisplaySwap, - DVR, - Exit, - FavoriteClear0, - FavoriteClear1, - FavoriteClear2, - FavoriteClear3, - FavoriteRecall0, - FavoriteRecall1, - FavoriteRecall2, - FavoriteRecall3, - FavoriteStore0, - FavoriteStore1, - FavoriteStore2, - FavoriteStore3, - Guide, - GuideNextDay, - GuidePreviousDay, - Info, - InstantReplay, - Link, - ListProgram, - LiveContent, - Lock, - MediaApps, - MediaAudioTrack, - MediaLast, - MediaSkipBackward, - MediaSkipForward, - MediaStepBackward, - MediaStepForward, - MediaTopMenu, - NavigateIn, - NavigateNext, - NavigateOut, - NavigatePrevious, - NextFavoriteChannel, - NextUserProfile, - OnDemand, - Pairing, - PinPDown, - PinPMove, - PinPUp, - PlaySpeedDown, - PlaySpeedReset, - PlaySpeedUp, - RandomToggle, - RcLowBattery, - RecordSpeedNext, - RfBypass, - ScanChannelsToggle, - ScreenModeNext, - Settings, - SplitScreenToggle, - STBInput, - STBPower, - Subtitle, - Teletext, - VideoModeNext, - Wink, - ZoomToggle, - - # Speech recognition keys - SpeechCorrectionList, - SpeechInputToggle, - - # Document keys - Close, - New, - Open, - Print, - Save, - SpellCheck, - MailForward, - MailReply, - MailSend, - - # Application selector keys - LaunchCalculator, - LaunchCalendar, - LaunchContacts, - LaunchMail, - LaunchMediaPlayer, - LaunchMusicPlayer, - LaunchMyComputer, - LaunchPhone, - LaunchScreenSaver, - LaunchSpreadsheet, - LaunchWebBrowser, - LaunchWebCam, - LaunchWordProcessor, - LaunchApplication1, - LaunchApplication2, - LaunchApplication3, - LaunchApplication4, - LaunchApplication5, - LaunchApplication6, - LaunchApplication7, - LaunchApplication8, - LaunchApplication9, - LaunchApplication10, - LaunchApplication11, - LaunchApplication12, - LaunchApplication13, - LaunchApplication14, - LaunchApplication15, - LaunchApplication16, - - # Browser control keys - BrowserBack, - BrowserFavorites, - BrowserForward, - BrowserHome, - BrowserRefresh, - BrowserSearch, - BrowserStop, - - # Numeric keypad keys - Key11, - Key12, - Separator - - MouseButtons* = enum - NoButton = 0, - PrimaryButton = 1, - SecondaryButton = 2, - AuxilaryButton = 4, - FourthButton = 8, - FifthButton = 16 - - # https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent - MouseEvent* = ref MouseEventObj - MouseEventObj {.importc.} = object of UIEvent - altKey*, ctrlKey*, metaKey*, shiftKey*: bool - button*: int - buttons*: int - clientX*, clientY*: int - movementX*, movementY*: int - offsetX*, offsetY*: int - pageX*, pageY*: int - relatedTarget*: EventTarget - #region*: cstring - screenX*, screenY*: int - x*, y*: int - - DataTransferItemKind* {.pure.} = enum - File = "file", - String = "string" - - # https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem - DataTransferItem* = ref DataTransferItemObj - DataTransferItemObj {.importc.} = object of RootObj - kind*: cstring - `type`*: cstring - - # https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer - DataTransfer* = ref DataTransferObj - DataTransferObj {.importc.} = object of RootObj - dropEffect*: cstring - effectAllowed*: cstring - files*: seq[Element] - items*: seq[DataTransferItem] - types*: seq[cstring] - - DataTransferDropEffect* {.pure.} = enum - None = "none", - Copy = "copy", - Link = "link", - Move = "move" - - DataTransferEffectAllowed* {.pure.} = enum - None = "none", - Copy = "copy", - CopyLink = "copyLink", - CopyMove = "copyMove", - Link = "link", - LinkMove = "linkMove", - Move = "move", - All = "all", - Uninitialized = "uninitialized" - - DragEventTypes* = enum - Drag = "drag", - DragEnd = "dragend", - DragEnter = "dragenter", - DragExit = "dragexit", - DragLeave = "dragleave", - DragOver = "dragover", - DragStart = "dragstart", - Drop = "drop" - - # https://developer.mozilla.org/en-US/docs/Web/API/DragEvent - DragEvent* {.importc.} = object of MouseEvent - dataTransfer*: DataTransfer - - TouchList* {.importc.} = ref object of RootObj - length*: int - - Touch* = ref TouchObj - TouchObj {.importc.} = object of RootObj - identifier*: int - screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int - target*: Element - radiusX*, radiusY*: int - rotationAngle*: int - force*: float - - TouchEvent* = ref TouchEventObj - TouchEventObj {.importc.} = object of UIEvent - changedTouches*, targetTouches*, touches*: seq[Touch] - - Location* = ref LocationObj - LocationObj {.importc.} = object of RootObj - hash*: cstring - host*: cstring - hostname*: cstring - href*: cstring - pathname*: cstring - port*: cstring - protocol*: cstring - search*: cstring - - History* = ref HistoryObj - HistoryObj {.importc.} = object of RootObj - length*: int - - Navigator* = ref NavigatorObj - NavigatorObj {.importc.} = object of RootObj - appCodeName*: cstring - appName*: cstring - appVersion*: cstring - cookieEnabled*: bool - language*: cstring - platform*: cstring - userAgent*: cstring - mimeTypes*: seq[ref MimeType] - - Plugin* {.importc.} = object of RootObj - description*: cstring - filename*: cstring - name*: cstring - - MimeType* {.importc.} = object of RootObj - description*: cstring - enabledPlugin*: ref Plugin - suffixes*: seq[cstring] - `type`*: cstring - - LocationBar* {.importc.} = object of RootObj - visible*: bool - MenuBar* = LocationBar - PersonalBar* = LocationBar - ScrollBars* = LocationBar - ToolBar* = LocationBar - StatusBar* = LocationBar - - Screen = ref ScreenObj - ScreenObj {.importc.} = object of RootObj - availHeight*: int - availWidth*: int - colorDepth*: int - height*: int - pixelDepth*: int - width*: int - - TimeOut* {.importc.} = ref object of RootObj - Interval* {.importc.} = object of RootObj - - AddEventListenerOptions* = object - capture*: bool - once*: bool - passive*: bool - -when defined(nodejs): - # we provide a dummy DOM for nodejs for testing purposes - proc len*(x: Node): int = x.childNodes.len - proc `[]`*(x: Node; idx: int): Element = - assert idx >= 0 and idx < x.childNodes.len - result = cast[Element](x.childNodes[idx]) - - var document* = Document(nodeType: DocumentNode) - - proc getElem(x: Element; id: cstring): Element = - if x.id == id: return x - for i in 0..= 0 and rect.left >= 0 and - rect.bottom <= clientHeight().float and - rect.right <= clientWidth().float - -proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.} -proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.} -proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.} -proc offsetLeft*(e: Node): int {.importcpp: "#.offsetLeft", nodecl.} + import dom + export dom diff --git a/karax/kdom_impl.nim b/karax/kdom_impl.nim new file mode 100644 index 0000000..aaa26cb --- /dev/null +++ b/karax/kdom_impl.nim @@ -0,0 +1,1232 @@ +# +# +# Nim's Runtime Library +# (c) Copyright 2012 Andreas Rumpf +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## Declaration of the Document Object Model for the `JavaScript backend +## `_. + +when not defined(js) and not defined(Nimdoc): + {.error: "This module only works on the JavaScript platform".} + +type + EventTarget* = ref EventTargetObj + EventTargetObj {.importc.} = object of RootObj + onabort*: proc (event: Event) {.nimcall.} + onblur*: proc (event: Event) {.nimcall.} + onchange*: proc (event: Event) {.nimcall.} + onclick*: proc (event: Event) {.nimcall.} + ondblclick*: proc (event: Event) {.nimcall.} + onerror*: proc (event: Event) {.nimcall.} + onfocus*: proc (event: Event) {.nimcall.} + onkeydown*: proc (event: Event) {.nimcall.} + onkeypress*: proc (event: Event) {.nimcall.} + onkeyup*: proc (event: Event) {.nimcall.} + onload*: proc (event: Event) {.nimcall.} + onmousedown*: proc (event: Event) {.nimcall.} + onmousemove*: proc (event: Event) {.nimcall.} + onmouseout*: proc (event: Event) {.nimcall.} + onmouseover*: proc (event: Event) {.nimcall.} + onmouseup*: proc (event: Event) {.nimcall.} + onreset*: proc (event: Event) {.nimcall.} + onselect*: proc (event: Event) {.nimcall.} + onsubmit*: proc (event: Event) {.nimcall.} + onunload*: proc (event: Event) {.nimcall.} + + # https://developer.mozilla.org/en-US/docs/Web/Events + DomEvent* {.pure.} = enum + Abort = "abort", + BeforeInput = "beforeinput", + Blur = "blur", + Click = "click", + CompositionEnd = "compositionend", + CompositionStart = "compositionstart", + CompositionUpdate = "compositionupdate", + DblClick = "dblclick", + Error = "error", + Focus = "focus", + FocusIn = "focusin", + FocusOut = "focusout", + Input = "input", + KeyDown = "keydown", + KeyPress = "keypress", + KeyUp = "keyup", + Load = "load", + MouseDown = "mousedown", + MouseEnter = "mouseenter", + MouseLeave = "mouseleave", + MouseMove = "mousemove", + MouseOut = "mouseout", + MouseOver = "mouseover", + MouseUp = "mouseup", + Resize = "resize", + Scroll = "scroll", + Select = "select", + Unload = "unload", + Wheel = "wheel" + + Window* = ref WindowObj + WindowObj {.importc.} = object of EventTargetObj + document*: Document + event*: Event + history*: History + location*: Location + closed*: bool + defaultStatus*: cstring + innerHeight*, innerWidth*: int + locationbar*: ref LocationBar + menubar*: ref MenuBar + name*: cstring + outerHeight*, outerWidth*: int + pageXOffset*, pageYOffset*: int + personalbar*: ref PersonalBar + scrollbars*: ref ScrollBars + statusbar*: ref StatusBar + status*: cstring + toolbar*: ref ToolBar + frames*: seq[Frame] + screen*: Screen + + Frame* = ref FrameObj + FrameObj {.importc.} = object of WindowObj + + ClassList* = ref ClassListObj + ClassListObj {.importc.} = object of RootObj + + NodeType* = enum + ElementNode = 1, + AttributeNode, + TextNode, + CDATANode, + EntityRefNode, + EntityNode, + ProcessingInstructionNode, + CommentNode, + DocumentNode, + DocumentTypeNode, + DocumentFragmentNode, + NotationNode + + Node* = ref NodeObj + NodeObj {.importc.} = object of EventTargetObj + attributes*: seq[Node] + childNodes*: seq[Node] + children*: seq[Node] + data*: cstring + firstChild*: Node + lastChild*: Node + nextSibling*: Node + nodeName*: cstring + nodeType*: NodeType + nodeValue*: cstring + parentNode*: Node + previousSibling*: Node + innerHTML*: cstring + style*: Style + when defined(nodejs): + id*: cstring + + Document* = ref DocumentObj + DocumentObj {.importc.} = object of NodeObj + alinkColor*: cstring + bgColor*: cstring + body*: Element + charset*: cstring + cookie*: cstring + defaultCharset*: cstring + fgColor*: cstring + head*: Element + lastModified*: cstring + linkColor*: cstring + referrer*: cstring + title*: cstring + URL*: cstring + vlinkColor*: cstring + anchors*: seq[AnchorElement] + forms*: seq[FormElement] + images*: seq[ImageElement] + applets*: seq[Element] + embeds*: seq[EmbedElement] + links*: seq[LinkElement] + + Element* = ref ElementObj + ElementObj {.importc.} = object of NodeObj + classList*: Classlist + checked*: bool + defaultChecked*: bool + defaultValue*: cstring + disabled*: bool + form*: FormElement + name*: cstring + readOnly*: bool + options*: seq[OptionElement] + selectedOptions*: seq[OptionElement] + clientWidth*, clientHeight*: int + contentEditable*: string + isContentEditable*: bool + dir*: string + offsetHeight*: int + offsetWidth*: int + offsetLeft*: int + offsetTop*: int + + # https://developer.mozilla.org/en-US/docs/Web/API/ValidityState + ValidityState* = ref ValidityStateObj + ValidityStateObj {.importc.} = object + badInput*: bool + customError*: bool + patternMismatch*: bool + rangeOverflow*: bool + rangeUnderflow*: bool + stepMismatch*: bool + tooLong*: bool + tooShort*: bool + typeMismatch*: bool + valid*: bool + valueMissing*: bool + + # https://developer.mozilla.org/en-US/docs/Web/API/Blob + Blob* = ref BlobObj + BlobObj {.importc.} = object of RootObj + size*: int + `type`*: cstring + + # https://developer.mozilla.org/en-US/docs/Web/API/File + File* = ref FileObj + FileObj {.importc.} = object of Blob + lastModified*: int + name*: cstring + + # https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement + InputElement* = ref InputElementObj + InputElementObj {.importc.} = object of Element + # Properties related to the parent form + formAction*: cstring + formEncType*: cstring + formMethod*: cstring + formNoValidate*: bool + formTarget*: cstring + + # Properties that apply to any type of input element that is not hidden + `type`*: cstring + autofocus*: bool + required*: bool + value*: cstring + validity*: ValidityState + validationMessage*: cstring + willValidate*: bool + + # Properties that apply only to elements of type "checkbox" or "radio" + indeterminate*: bool + + # Properties that apply only to elements of type "image" + alt*: cstring + height*: cstring + src*: cstring + width*: cstring + + # Properties that apply only to elements of type "file" + accept*: cstring + files*: seq[Blob] + + # Properties that apply only to text/number-containing or elements + autocomplete*: cstring + maxLength*: int + size*: int + pattern*: cstring + placeholder*: cstring + min*: cstring + max*: cstring + selectionStart*: int + selectionEnd*: int + selectionDirection*: cstring + + # Properties not yet categorized + dirName*: cstring + accessKey*: cstring + list*: Element + multiple*: bool + labels*: seq[Element] + step*: cstring + valueAsDate*: cstring + valueAsNumber*: float + + LinkElement* = ref LinkObj + LinkObj {.importc.} = object of ElementObj + target*: cstring + text*: cstring + x*: int + y*: int + + EmbedElement* = ref EmbedObj + EmbedObj {.importc.} = object of ElementObj + height*: int + hspace*: int + src*: cstring + width*: int + `type`*: cstring + vspace*: int + + AnchorElement* = ref AnchorObj + AnchorObj {.importc.} = object of ElementObj + text*: cstring + x*, y*: int + + OptionElement* = ref OptionObj + OptionObj {.importc.} = object of ElementObj + defaultSelected*: bool + selected*: bool + selectedIndex*: int + text*: cstring + value*: cstring + + # https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement + FormElement* = ref FormObj + FormObj {.importc.} = object of ElementObj + acceptCharset*: cstring + action*: cstring + autocomplete*: cstring + elements*: seq[Element] + encoding*: cstring + enctype*: cstring + length*: int + `method`*: cstring + noValidate*: bool + target*: cstring + + ImageElement* = ref ImageObj + ImageObj {.importc.} = object of ElementObj + border*: int + complete*: bool + height*: int + hspace*: int + lowsrc*: cstring + src*: cstring + vspace*: int + width*: int + + Style* = ref StyleObj + StyleObj {.importc.} = object of RootObj + background*: cstring + backgroundAttachment*: cstring + backgroundColor*: cstring + backgroundImage*: cstring + backgroundPosition*: cstring + backgroundRepeat*: cstring + border*: cstring + borderBottom*: cstring + borderBottomColor*: cstring + borderBottomStyle*: cstring + borderBottomWidth*: cstring + borderColor*: cstring + borderLeft*: cstring + borderLeftColor*: cstring + borderLeftStyle*: cstring + borderLeftWidth*: cstring + borderRight*: cstring + borderRightColor*: cstring + borderRightStyle*: cstring + borderRightWidth*: cstring + borderStyle*: cstring + borderTop*: cstring + borderTopColor*: cstring + borderTopStyle*: cstring + borderTopWidth*: cstring + borderWidth*: cstring + bottom*: cstring + captionSide*: cstring + clear*: cstring + clip*: cstring + color*: cstring + cursor*: cstring + direction*: cstring + display*: cstring + emptyCells*: cstring + cssFloat*: cstring + font*: cstring + fontFamily*: cstring + fontSize*: cstring + fontStretch*: cstring + fontStyle*: cstring + fontVariant*: cstring + fontWeight*: cstring + height*: cstring + left*: cstring + letterSpacing*: cstring + lineHeight*: cstring + listStyle*: cstring + listStyleImage*: cstring + listStylePosition*: cstring + listStyleType*: cstring + margin*: cstring + marginBottom*: cstring + marginLeft*: cstring + marginRight*: cstring + marginTop*: cstring + maxHeight*: cstring + maxWidth*: cstring + minHeight*: cstring + minWidth*: cstring + overflow*: cstring + padding*: cstring + paddingBottom*: cstring + paddingLeft*: cstring + paddingRight*: cstring + paddingTop*: cstring + pageBreakAfter*: cstring + pageBreakBefore*: cstring + pointerEvents*: cstring + position*: cstring + right*: cstring + scrollbar3dLightColor*: cstring + scrollbarArrowColor*: cstring + scrollbarBaseColor*: cstring + scrollbarDarkshadowColor*: cstring + scrollbarFaceColor*: cstring + scrollbarHighlightColor*: cstring + scrollbarShadowColor*: cstring + scrollbarTrackColor*: cstring + tableLayout*: cstring + textAlign*: cstring + textDecoration*: cstring + textIndent*: cstring + textTransform*: cstring + transform*: cstring + top*: cstring + verticalAlign*: cstring + visibility*: cstring + width*: cstring + wordSpacing*: cstring + zIndex*: int + + EventPhase* = enum + None = 0, + CapturingPhase, + AtTarget, + BubblingPhase + + # https://developer.mozilla.org/en-US/docs/Web/API/Event + Event* = ref EventObj + EventObj {.importc.} = object of RootObj + bubbles*: bool + cancelBubble*: bool + cancelable*: bool + composed*: bool + currentTarget*: Node + defaultPrevented*: bool + eventPhase*: int + target*: Node + `type`*: cstring + isTrusted*: bool + + # https://developer.mozilla.org/en-US/docs/Web/API/UIEvent + UIEvent* = ref UIEventObj + UIEventObj {.importc.} = object of Event + detail*: int64 + view*: Window + + # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent + KeyboardEvent* = ref KeyboardEventObj + KeyboardEventObj {.importc.} = object of UIEvent + altKey*, ctrlKey*, metaKey*, shiftKey*: bool + code*: cstring + isComposing*: bool + key*: cstring + keyCode*: int + location*: int + + # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + KeyboardEventKey* {.pure.} = enum + # Modifier keys + Alt, + AltGraph, + CapsLock, + Control, + Fn, + FnLock, + Hyper, + Meta, + NumLock, + ScrollLock, + Shift, + Super, + Symbol, + SymbolLock, + + # Whitespace keys + ArrowDown, + ArrowLeft, + ArrowRight, + ArrowUp, + End, + Home, + PageDown, + PageUp, + + # Editing keys + Backspace, + Clear, + Copy, + CrSel, + Cut, + Delete, + EraseEof, + ExSel, + Insert, + Paste, + Redo, + Undo, + + # UI keys + Accept, + Again, + Attn, + Cancel, + ContextMenu, + Escape, + Execute, + Find, + Finish, + Help, + Pause, + Play, + Props, + Select, + ZoomIn, + ZoomOut, + + # Device keys + BrigtnessDown, + BrigtnessUp, + Eject, + LogOff, + Power, + PowerOff, + PrintScreen, + Hibernate, + Standby, + WakeUp, + + # Common IME keys + AllCandidates, + Alphanumeric, + CodeInput, + Compose, + Convert, + Dead, + FinalMode, + GroupFirst, + GroupLast, + GroupNext, + GroupPrevious, + ModeChange, + NextCandidate, + NonConvert, + PreviousCandidate, + Process, + SingleCandidate, + + # Korean keyboards only + HangulMode, + HanjaMode, + JunjaMode, + + # Japanese keyboards only + Eisu, + Hankaku, + Hiragana, + HiraganaKatakana, + KanaMode, + KanjiMode, + Katakana, + Romaji, + Zenkaku, + ZenkakuHanaku, + + # Function keys + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + Soft1, + Soft2, + Soft3, + Soft4, + + # Phone keys + AppSwitch, + Call, + Camera, + CameraFocus, + EndCall, + GoBack, + GoHome, + HeadsetHook, + LastNumberRedial, + Notification, + MannerMode, + VoiceDial, + + # Multimedia keys + ChannelDown, + ChannelUp, + MediaFastForward, + MediaPause, + MediaPlay, + MediaPlayPause, + MediaRecord, + MediaRewind, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + + # Audio control keys + AudioBalanceLeft, + AudioBalanceRight, + AudioBassDown, + AudioBassBoostDown, + AudioBassBoostToggle, + AudioBassBoostUp, + AudioBassUp, + AudioFaderFront, + AudioFaderRear, + AudioSurroundModeNext, + AudioTrebleDown, + AudioTrebleUp, + AudioVolumeDown, + AUdioVolumeMute, + AudioVolumeUp, + MicrophoneToggle, + MicrophoneVolumeDown, + MicrophoneVolumeMute, + MicrophoneVolumeUp, + + # TV control keys + TV, + TV3DMode, + TVAntennaCable, + TVAudioDescription, + TVAudioDescriptionMixDown, + TVAudioDescriptionMixUp, + TVContentsMenu, + TVDataService, + TVInput, + TVInputComponent1, + TVInputComponent2, + TVInputComposite1, + TVInputComposite2, + TVInputHDMI1, + TVInputHDMI2, + TVInputHDMI3, + TVInputHDMI4, + TVInputVGA1, + TVMediaContext, + TVNetwork, + TVNumberEntry, + TVPower, + TVRadioService, + TVSatellite, + TVSatelliteBS, + TVSatelliteCS, + TVSatelliteToggle, + TVTerrestrialAnalog, + TVTerrestrialDigital, + TVTimer, + + # Media controller keys + AVRInput, + AVRPower, + ColorF0Red, + ColorF1Green, + ColorF2Yellow, + ColorF3Blue, + ColorF4Grey, + ColorF5Brown, + ClosedCaptionToggle, + Dimmer, + DisplaySwap, + DVR, + Exit, + FavoriteClear0, + FavoriteClear1, + FavoriteClear2, + FavoriteClear3, + FavoriteRecall0, + FavoriteRecall1, + FavoriteRecall2, + FavoriteRecall3, + FavoriteStore0, + FavoriteStore1, + FavoriteStore2, + FavoriteStore3, + Guide, + GuideNextDay, + GuidePreviousDay, + Info, + InstantReplay, + Link, + ListProgram, + LiveContent, + Lock, + MediaApps, + MediaAudioTrack, + MediaLast, + MediaSkipBackward, + MediaSkipForward, + MediaStepBackward, + MediaStepForward, + MediaTopMenu, + NavigateIn, + NavigateNext, + NavigateOut, + NavigatePrevious, + NextFavoriteChannel, + NextUserProfile, + OnDemand, + Pairing, + PinPDown, + PinPMove, + PinPUp, + PlaySpeedDown, + PlaySpeedReset, + PlaySpeedUp, + RandomToggle, + RcLowBattery, + RecordSpeedNext, + RfBypass, + ScanChannelsToggle, + ScreenModeNext, + Settings, + SplitScreenToggle, + STBInput, + STBPower, + Subtitle, + Teletext, + VideoModeNext, + Wink, + ZoomToggle, + + # Speech recognition keys + SpeechCorrectionList, + SpeechInputToggle, + + # Document keys + Close, + New, + Open, + Print, + Save, + SpellCheck, + MailForward, + MailReply, + MailSend, + + # Application selector keys + LaunchCalculator, + LaunchCalendar, + LaunchContacts, + LaunchMail, + LaunchMediaPlayer, + LaunchMusicPlayer, + LaunchMyComputer, + LaunchPhone, + LaunchScreenSaver, + LaunchSpreadsheet, + LaunchWebBrowser, + LaunchWebCam, + LaunchWordProcessor, + LaunchApplication1, + LaunchApplication2, + LaunchApplication3, + LaunchApplication4, + LaunchApplication5, + LaunchApplication6, + LaunchApplication7, + LaunchApplication8, + LaunchApplication9, + LaunchApplication10, + LaunchApplication11, + LaunchApplication12, + LaunchApplication13, + LaunchApplication14, + LaunchApplication15, + LaunchApplication16, + + # Browser control keys + BrowserBack, + BrowserFavorites, + BrowserForward, + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + + # Numeric keypad keys + Key11, + Key12, + Separator + + MouseButtons* = enum + NoButton = 0, + PrimaryButton = 1, + SecondaryButton = 2, + AuxilaryButton = 4, + FourthButton = 8, + FifthButton = 16 + + # https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent + MouseEvent* = ref MouseEventObj + MouseEventObj {.importc.} = object of UIEvent + altKey*, ctrlKey*, metaKey*, shiftKey*: bool + button*: int + buttons*: int + clientX*, clientY*: int + movementX*, movementY*: int + offsetX*, offsetY*: int + pageX*, pageY*: int + relatedTarget*: EventTarget + #region*: cstring + screenX*, screenY*: int + x*, y*: int + + DataTransferItemKind* {.pure.} = enum + File = "file", + String = "string" + + # https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem + DataTransferItem* = ref DataTransferItemObj + DataTransferItemObj {.importc.} = object of RootObj + kind*: cstring + `type`*: cstring + + # https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer + DataTransfer* = ref DataTransferObj + DataTransferObj {.importc.} = object of RootObj + dropEffect*: cstring + effectAllowed*: cstring + files*: seq[Element] + items*: seq[DataTransferItem] + types*: seq[cstring] + + DataTransferDropEffect* {.pure.} = enum + None = "none", + Copy = "copy", + Link = "link", + Move = "move" + + DataTransferEffectAllowed* {.pure.} = enum + None = "none", + Copy = "copy", + CopyLink = "copyLink", + CopyMove = "copyMove", + Link = "link", + LinkMove = "linkMove", + Move = "move", + All = "all", + Uninitialized = "uninitialized" + + DragEventTypes* = enum + Drag = "drag", + DragEnd = "dragend", + DragEnter = "dragenter", + DragExit = "dragexit", + DragLeave = "dragleave", + DragOver = "dragover", + DragStart = "dragstart", + Drop = "drop" + + # https://developer.mozilla.org/en-US/docs/Web/API/DragEvent + DragEvent* {.importc.} = object of MouseEvent + dataTransfer*: DataTransfer + + TouchList* {.importc.} = ref object of RootObj + length*: int + + Touch* = ref TouchObj + TouchObj {.importc.} = object of RootObj + identifier*: int + screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int + target*: Element + radiusX*, radiusY*: int + rotationAngle*: int + force*: float + + TouchEvent* = ref TouchEventObj + TouchEventObj {.importc.} = object of UIEvent + changedTouches*, targetTouches*, touches*: seq[Touch] + + Location* = ref LocationObj + LocationObj {.importc.} = object of RootObj + hash*: cstring + host*: cstring + hostname*: cstring + href*: cstring + pathname*: cstring + port*: cstring + protocol*: cstring + search*: cstring + + History* = ref HistoryObj + HistoryObj {.importc.} = object of RootObj + length*: int + + Navigator* = ref NavigatorObj + NavigatorObj {.importc.} = object of RootObj + appCodeName*: cstring + appName*: cstring + appVersion*: cstring + cookieEnabled*: bool + language*: cstring + platform*: cstring + userAgent*: cstring + mimeTypes*: seq[ref MimeType] + + Plugin* {.importc.} = object of RootObj + description*: cstring + filename*: cstring + name*: cstring + + MimeType* {.importc.} = object of RootObj + description*: cstring + enabledPlugin*: ref Plugin + suffixes*: seq[cstring] + `type`*: cstring + + LocationBar* {.importc.} = object of RootObj + visible*: bool + MenuBar* = LocationBar + PersonalBar* = LocationBar + ScrollBars* = LocationBar + ToolBar* = LocationBar + StatusBar* = LocationBar + + Screen = ref ScreenObj + ScreenObj {.importc.} = object of RootObj + availHeight*: int + availWidth*: int + colorDepth*: int + height*: int + pixelDepth*: int + width*: int + + TimeOut* {.importc.} = ref object of RootObj + Interval* {.importc.} = object of RootObj + + AddEventListenerOptions* = object + capture*: bool + once*: bool + passive*: bool + +when defined(nodejs): + # we provide a dummy DOM for nodejs for testing purposes + proc len*(x: Node): int = x.childNodes.len + proc `[]`*(x: Node; idx: int): Element = + assert idx >= 0 and idx < x.childNodes.len + result = cast[Element](x.childNodes[idx]) + + var document* = Document(nodeType: DocumentNode) + + proc getElem(x: Element; id: cstring): Element = + if x.id == id: return x + for i in 0..= 0 and rect.left >= 0 and + rect.bottom <= clientHeight().float and + rect.right <= clientWidth().float + +proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.} +proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.} +proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.} +proc offsetLeft*(e: Node): int {.importcpp: "#.offsetLeft", nodecl.} diff --git a/karax/tools/karun.nim b/karax/tools/karun.nim index dc1ba44..adf1ecb 100644 --- a/karax/tools/karun.nim +++ b/karax/tools/karun.nim @@ -1,55 +1,98 @@ -## Simple tool to quickly run Karax applications. Generates the HTML -## required to run a Karax app and opens it in a browser. - -import os, - strutils, - parseopt, - browsers, - times, - tables - +import os, strutils, browsers,times, tables +import parseopt +import threadpool +import static_server const css = """ - + """ - html = """ + +const html = """ + $1 $2 - -
- + +
+ +$3 """ +const websocket = """ + +""" proc exec(cmd: string) = if os.execShellCmd(cmd) != 0: quit "External command failed: " & cmd -proc build(name: string, rest: string, selectedCss: string, run: bool) = +proc build(rest: string, selectedCss: string, run: bool, watch: bool) = echo("Building...") - createDir("nimcache") - exec("nim js --out:nimcache/" & name & ".js " & rest) - let dest = "nimcache" / name & ".html" - writeFile(dest, html % [name, selectedCss]) - if run: openDefaultBrowser(dest) + let cmd = "nim js --out:" & "app" & ".js " & rest + if watch: + discard os.execShellCmd(cmd) + else: + exec cmd + let dest = "app" & ".html" + let script = if watch: websocket else: "" + writeFile(dest, html % ["app", selectedCss, script]) + if run: openDefaultBrowser("http://localhost:8080") + +proc watchBuild(filePath: string, selectedCss: string, rest: string) {.thread.} = + var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable + while true: + sleep(300) + for path in walkDirRec("."): + if ".git" in path: + continue + var (_, _, ext) = splitFile(path) + if ext in [".scss",".sass",".less",".styl",".pcss",".postcss"]: + continue + if files.hasKey(path): + if files[path] != getLastModificationTime(path): + echo("File changed: " & path) + build(rest,selectedCss, false, true) + files[path] = getLastModificationTime(path) + else: + if absolutePath(path) in [absolutePath("app" & ".js"),absolutePath("app" & ".html")]: + continue + files[path] = getLastModificationTime(path) + +proc serve(){.thread.} = + serveStatic() proc main = var op = initOptParser() var rest = op.cmdLineRest var file = "" var run = false - var selectedCss = "" var watch = false - var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable - + var selectedCss = "" while true: op.next() case op.kind @@ -59,8 +102,11 @@ proc main = run = true rest = rest.replace("--run ") of "css": - selectedCss = css - rest = rest.replace("--css ") + if op.val != "": + selectedCss = readFile(op.val) + else: + selectedCss = css + rest = rest.substr(rest.find(" ")) else: discard of cmdShortOption: if op.key == "r": @@ -73,26 +119,11 @@ proc main = of cmdEnd: break if file.len == 0: quit "filename expected" - let name = file.splitFile.name - build(name, rest, selectedCss, run) - echo("after build") + if run: + spawn serve() if watch: - # TODO: launch http server - while true: - sleep(300) - for path in walkDirRec("."): - if ".git" in path: - continue - if files.hasKey(path): - if files[path] != getLastModificationTime(path): - echo("File changed: " & path) - build(name, rest, selectedCss, run) - files[path] = getLastModificationTime(path) - else: - files[path] = getLastModificationTime(path) + spawn watchBuild(file, selectedCss, rest) + build(rest,selectedCss, run, watch) + sync() main() - - - - diff --git a/karax/tools/karun.nims b/karax/tools/karun.nims new file mode 100644 index 0000000..8983496 --- /dev/null +++ b/karax/tools/karun.nims @@ -0,0 +1 @@ +switch("threads", "on") \ No newline at end of file diff --git a/karax/tools/static_server.nim b/karax/tools/static_server.nim new file mode 100644 index 0000000..eeddf4f --- /dev/null +++ b/karax/tools/static_server.nim @@ -0,0 +1,141 @@ +import + std/[net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5, + logging, httpcore, asyncfile, asynchttpserver, tables, times] +from cgi import decodeUrl +import ws, dotenv + +var logger = newConsoleLogger() +addHandler(logger) + +when defined(release): + setLogFilter(lvlError) + +type + RawHeaders* = seq[tuple[key, val: string]] + +proc toStr(headers: RawHeaders): string = + $newHttpHeaders(headers) + +proc send(request: Request, code: HttpCode, headers: RawHeaders, + body: string): Future[void] = + return request.respond(code, body, newHttpHeaders(headers)) + +proc statusContent(request: Request, status: HttpCode, content: string, + headers: RawHeaders): Future[void] = + try: + result = send(request, status, headers, content) + debug(" ", status, " ", toStr(headers)) + except: + error("Could not send response: ", osErrorMsg(osLastError())) + +proc sendStaticIfExists(req: Request, paths: seq[string]): Future[HttpCode] {.async.} = + result = Http200 + let mimes = newMimetypes() + for p in paths: + if fileExists(p): + if fpOthersRead notin getFilePermissions(p): + return Http403 + let fileSize = getFileSize(p) + let extPos = searchExtPos(p) + let mimetype = mimes.getMimetype( + if extPos >= 0: p.substr(extPos + 1) + else: "") + if fileSize < 10_000_000: # 10 mb + var file = readFile(p) + var hashed = getMD5(file) + # If the user has a cached version of this file and it matches our + # version, let them use it + if req.headers.getOrDefault("If-None-Match") == hashed: + await req.statusContent(Http304, "", default(RawHeaders)) + else: + await req.statusContent(Http200, file, @{ + "Content-Type": mimetype, + "ETag": hashed + }) + else: + let headers = @{ + "Content-Type": mimetype, + "Content-Length": $fileSize + } + await req.statusContent(Http200, "", headers) + var fileStream = newFutureStream[string]("sendStaticIfExists") + var file = openAsync(p, fmRead) + # Let `readToStream` write file data into fileStream in the + # background. + asyncCheck file.readToStream(fileStream) + # The `writeFromStream` proc will complete once all the data in the + # `bodyStream` has been written to the file. + while true: + let (hasValue, value) = await fileStream.read() + if hasValue: + await req.client.send(value) + else: + break + file.close() + return + # If we get to here then no match could be found. + return Http404 + +proc handleFileRequest(req: Request): Future[HttpCode] {.async.} = + # Find static file. + var reqPath = cgi.decodeUrl(req.url.path) + var staticDir = getEnv("staticDir") # it's assumed a relative dir + var status = Http400 + var path = staticDir / reqPath + normalizePathEnd(path, false) + if dirExists(path): + status = await sendStaticIfExists(req, @[path / "index.html", path / "index.htm"]) + else: + status = await sendStaticIfExists(req, @[path]) + return status + +proc handleWs(req: Request) {.async.} = + var ws = await newWebSocket(req) + await ws.send("Welcome to simple echo server") + + var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable + let watchedFiles = [absolutePath "app.js", absolutePath "app.html"] + for path in watchedFiles: + files[path] = getLastModificationTime(path) + + while ws.readyState == Open: + await sleepAsync(500) + var changed = false + for path in watchedFiles: + if files[path] != getLastModificationTime(path): + changed = true + files[path] = getLastModificationTime(path) + if changed: + await ws.send("refresh") + changed = false + +proc serveStatic*() = + if fileExists("static.env"): + var env: DotEnv + env = initDotEnv(getCurrentDir(), "static.env") + env.overload() + else: + putEnv("staticDir", "assets/") + + var server = newAsyncHttpServer() + proc cb(req: Request) {.gcsafe, async.} = + if req.url.path == "/ws": + await handleWs(req) + if req.url.path == "/": + await req.respond(Http200, readFile "app.html") + elif req.url.path == "/app.js": + let file = absolutePath("app" & ".js") + if not file.fileExists: + error(file, " does not exist!") + if fpUserRead notin os.getFilePermissions(file): + error("Could not read ", file, "!") + await req.respond(Http200, readFile(file)) + else: + let status = await handleFileRequest(req) + if status != Http200: + await req.respond(status, "") + + waitFor server.serve(Port(8080), cb) + +when isMainModule: + serveStatic() diff --git a/karax/vdom.nim b/karax/vdom.nim index 3dc61d9..8fd654e 100644 --- a/karax/vdom.nim +++ b/karax/vdom.nim @@ -8,7 +8,7 @@ else: Node* = ref object import macros, vstyles, kbase -from strutils import toUpperAscii +from strutils import toUpperAscii, toLowerAscii, tokenize type VNodeKind* {.pure.} = enum @@ -35,7 +35,19 @@ type mark, ruby, rt, rp, bdi, dbo, span, br, wbr, ins, del, img, iframe, embed, `object` = "object", param, video, audio, source, track, canvas, map, - area, svg, math, + area, math, + + # SVG elements, see: https://www.w3.org/TR/SVG2/eltindex.html + animate, animateMotion, animateTransform, circle, clipPath, defs, desc, + `discard` = "discard", ellipse, feBlend, feColorMatrix, feComponentTransfer, + feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap, + feDistantLight, feDropShadow, feFlood, feFuncA, feFuncB, feFuncG, feFuncR, + feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset, + fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence, + filter, foreignObject, g, image, line, linearGradient, marker, mask, + metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect, + `set` = "set", stop, svg, switch, symbol, txt = "text", textPath, tspan, + unknown, use, view, table, caption, colgroup, col, tbody, thead, tfoot, tr, td, th, @@ -45,6 +57,10 @@ type keygen, output, progress, meter, details, summary, command, menu +const selfClosing = {area, base, br, col, embed, hr, img, input, + link, meta, param, source, track, wbr} + + type EventKind* {.pure.} = enum ## The events supported by the virtual DOM. onclick, ## An element is clicked. @@ -89,9 +105,16 @@ type onanimationiteration, onkeyupenter, ## vdom extension: an input field received the ENTER key press - onkeyuplater ## vdom extension: a key was pressed and some time + onkeyuplater, ## vdom extension: a key was pressed and some time ## passed (useful for on-the-fly text completions) + onload, # img + ontransitioncancel, + ontransitionend, + ontransitionrun, + ontransitionstart, + + onwheel # fires when the user rotates a wheel button on a pointing device. macro buildLookupTables(): untyped = var a = newTree(nnkBracket) @@ -294,6 +317,12 @@ proc sameAttrs*(a, b: VNode): bool = proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) = n.events.add((event, handler, nil)) +when kstring is cstring: + proc len(a: kstring): int = + # xxx: maybe move where kstring is defined + # without this, `n.field.len` fails on js (non web) platform + if a == nil: 0 else: a.len + template toStringAttr(field) = if n.field.len > 0: result.add " " & astToStr(field) & " = " & $n.field @@ -379,6 +408,8 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) = if n.kind == VNodeKind.text: result.addEscaped(n.text) + elif n.kind == VNodeKind.verbatim: + result.add(n.text) else: let kind = $n.kind result.add('<') @@ -397,7 +428,17 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) = result.add("=\"") result.addEscapedAttr(v) result.add('"') - # XXX add style to string + if n.style != nil: + result.add " style=\"" + for k, v in pairs(n.style): + if v.len == 0: continue + for t in tokenize($k, seps={'A' .. 'Z'}): + if t.isSep: result.add '-' + result.add toLowerAscii(t.token) + result.add ": " + result.add v + result.add "; " + result.add('"') if n.len > 0: result.add('>') if n.len > 1: @@ -422,8 +463,13 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) = result.add("") - else: + elif n.kind in selfClosing: result.add(" />") + else: + result.add(">") + result.add("") proc `$`*(n: VNode): kstring = diff --git a/karax/vstyles.nim b/karax/vstyles.nim index e2fca7d..7097aee 100644 --- a/karax/vstyles.nim +++ b/karax/vstyles.nim @@ -1,5 +1,9 @@ +##[ +see examples/hellostyle.nim +]## -import macros, kbase +import std/[macros, strutils] +import kbase when defined(js): import kdom, jdict @@ -234,7 +238,10 @@ proc eq*(a, b: VStyle): bool = if a[i] != b[i]: return false return true -proc setAttr(s: VStyle; a, value: kstring) {.noSideEffect.} = +proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} = + ## inserts (a, value) in sorted order of key `a` + # worst case quadratic complexity (if given styles in reverse order), hopefully + # not a concern assuming small cardinal var i = 0 while i < s.len: if s[i] == a: @@ -243,8 +250,8 @@ proc setAttr(s: VStyle; a, value: kstring) {.noSideEffect.} = elif s[i] > a: s.add "" s.add "" - # insertion point here: - for j in countdown(s.len-1, i, 2): + # insertion point here, shift all remaining pairs by 2 indexes + for j in countdown(s.len-1, i+3, 2): s[j] = s[j-2] s[j-1] = s[j-3] s[i] = a @@ -289,6 +296,24 @@ proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} = result[] = @[] result.setAttr a, val +proc toCss*(a: string): VStyle = + ##[ + See example in hellostyle.nim + Allows passing a css string directly, eg: + tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): discard + tdiv(style = "font-style: oblique; color: pink".toCss): discard + ]## + when defined(js): + result = newJSeq[cstring]() + else: + new(result) + result[] = @[] + for ai in a.split(";"): + var ai = ai.strip + if ai.len == 0: continue + let aj = ai.strip.split(":", maxsplit=1) + result.setAttr(aj[0], aj[1]) + when defined(js): proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..d8a3814 --- /dev/null +++ b/readme.md @@ -0,0 +1,394 @@ +![karax](https://user-images.githubusercontent.com/22755228/117183486-482b2a00-ade0-11eb-88e6-d8eeb28951ca.png) + +![Github Actions](https://img.shields.io/github/workflow/status/karaxnim/karax/CI?style=for-the-badge) ![GitHub issues](https://img.shields.io/github/issues-raw/karaxnim/karax?style=for-the-badge) ![GitHub](https://img.shields.io/github/license/karaxnim/karax?style=for-the-badge) ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/karaxnim/karax?sort=semver&style=for-the-badge) ![https://nim-lang.org](https://img.shields.io/badge/nim-powered-ffc200?style=for-the-badge) + +# Karax +Karax is a framework for developing single page applications in Nim. + +## Install + +To use Karax you must have nim installed. You can follow the instructions [here](https://nim-lang.org/install.html). + +Then you can install karax through nimble: +``nimble install karax`` + +## Try Karax +To try it out, run: + +``cd ~/projects # Insert your favourite directory for projects`` + +``nimble develop karax # This will clone Karax and create a link to it in ~/.nimble`` + +``cd karax`` + +``cd examples/todoapp`` + +``nim js todoapp.nim`` + +``open todoapp.html`` + +``cd ../..`` + +``cd examples/mediaplayer`` + +``nim js playerapp.nim`` + +``open playerapp.html`` + +It uses a virtual DOM like React, but is much smaller than the existing +frameworks plus of course it's written in Nim for Nim. No external +dependencies! And thanks to Nim's whole program optimization only what +is used ends up in the generated JavaScript code. + + +## Goals + + +- Leverage Nim's macro system to produce a framework that allows + for the development of applications that are boilerplate free. +- Keep it small, keep it fast, keep it flexible. + + + +## Hello World + + +The simplest Karax program looks like this: + +```nim + +include karax / prelude + +proc createDom(): VNode = + result = buildHtml(tdiv): + text "Hello World!" + +setRenderer createDom +``` + +Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead +here. ``tdiv`` produces a ``
`` virtual DOM node. + +As you can see, karax comes with its own ``buildHtml`` DSL for convenient +construction of (virtual) DOM trees (of type ``VNode``). Karax provides +a tiny build tool called ``karun`` that generates the HTML boilerplate code that +embeds and invokes the generated JavaScript code:: + +``nim c karax/tools/karun`` +``karax/tools/karun -r helloworld.nim`` + +Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by +``buildHtml``: + +```nim + +let tmp1 = tree(VNodeKind.tdiv) +add(tmp1, text "Hello World!") +tmp1 +``` +(I shortened the IDs for better readability.) + +Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree +construction so that it composes with all of Nim's control flow constructs: + + +```nim + +include karax / prelude +import random + +proc createDom(): VNode = + result = buildHtml(tdiv): + if rand(100) <= 50: + text "Hello World!" + else: + text "Hello Universe" + +randomize() +setRenderer createDom + +``` +Produces: + +```nim + +let tmp1 = tree(VNodeKind.tdiv) +if rand(100) <= 50: + add(tmp1, text "Hello World!") +else: + add(tmp1, text "Hello Universe") +tmp1 +``` + +## Event model + +Karax does not change the DOM's event model much, here is a program +that writes "Hello simulated universe" on a button click: + +```nim + +include karax / prelude +# alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson] + +var lines: seq[kstring] = @[] + +proc createDom(): VNode = + result = buildHtml(tdiv): + button: + text "Say hello!" + proc onclick(ev: Event; n: VNode) = + lines.add "Hello simulated universe" + for x in lines: + tdiv: + text x + +setRenderer createDom +``` + +``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible +string"; for the JS target that is an immutable JavaScript string) which +is preferred for efficiency on the JS target. However, on the native targets +``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML +construction is also avaible for the native targets (!) and the ``kstring`` +abstraction helps to deal with these conflicting requirements. + +Karax's DSL is quite flexible when it comes to event handlers, so the +following syntax is also supported: + +```nim + +include karax / prelude +from sugar import `=>` + +var lines: seq[kstring] = @[] + +proc createDom(): VNode = + result = buildHtml(tdiv): + button(onclick = () => lines.add "Hello simulated universe"): + text "Say hello!" + for x in lines: + tdiv: + text x + +setRenderer createDom +``` + +The ``buildHtml`` macro produces this code for us: + +```nim + +let tmp2 = tree(VNodeKind.tdiv) +let tmp3 = tree(VNodeKind.button) +addEventHandler(tmp3, EventKind.onclick, + () => lines.add "Hello simulated universe", kxi) +add(tmp3, text "Say hello!") +add(tmp2, tmp3) +for x in lines: + let tmp4 = tree(VNodeKind.tdiv) + add(tmp4, text x) + add(tmp2, tmp4) +tmp2 +``` +As the examples grow larger it becomes more and more visible of what +a DSL that composes with the builtin Nim control flow constructs buys us. +Once you have tasted this power there is no going back and languages +without AST based macro system simply don't cut it anymore. + + +## Attaching data to an event handler + + +Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any +additional data that should be passed to the event handler needs to be +done via Nim's closures. In general this means a pattern like this: + +```nim + +proc menuAction(menuEntry: kstring): proc() = + result = proc() = + echo "clicked ", menuEntry + +proc buildMenu(menu: seq[kstring]): VNode = + result = buildHtml(tdiv): + for m in menu: + nav(class="navbar is-primary"): + tdiv(class="navbar-brand"): + a(class="navbar-item", onclick = menuAction(m)): +``` + +## DOM diffing + +Ok, so now we have seen DOM creation and event handlers. But how does +Karax actually keep the DOM up to date? The trick is that every event +handler is wrapped in a helper proc that triggers a *redraw* operation +that calls the *renderer* that you initially passed to ``setRenderer``. +So a new virtual DOM is created and compared against the previous +virtual DOM. This comparison produces a patch set that is then applied +to the real DOM the browser uses internally. This process is called +"virtual DOM diffing" and other frameworks, most notably Facebook's +*React*, do quite similar things. The virtual DOM is faster to create +and manipulate than the real DOM so this approach is quite efficient. + + +## Form validation +Most applications these days have some "login" +mechanism consisting of ``username`` and ``password`` and +a ``login`` button. The login button should only be clickable +if ``username`` and ``password`` are not empty. An error +message should be shown as long as one input field is empty. + +To create new UI elements we write a ``loginField`` proc that +returns a ``VNode``: + +```nim + +proc loginField(desc, field, class: kstring; + validator: proc (field: kstring): proc ()): VNode = + result = buildHtml(tdiv): + label(`for` = field): + text desc + input(class = class, id = field, onchange = validator(field)) +``` + +We use the ``karax / errors`` module to help with this error +logic. The ``errors`` module is mostly a mapping from strings to +strings but it turned out that the logic is tricky enough to warrant +a library solution. ``validateNotEmpty`` returns a closure that +captures the ``field`` parameter: + +```nim + +proc validateNotEmpty(field: kstring): proc () = + result = proc () = + let x = getVNodeById(field).getInputText + if x.isNil or x == "": + errors.setError(field, field & " must not be empty") + else: + errors.setError(field, "") +``` + +This indirection is required because +event handlers in Karax need to have the type ``proc ()`` +or ``proc (ev: Event; n: VNode)``. The errors module also +gives us a handy ``disableOnError`` helper. It returns +``"disabled"`` if there are errors. Now we have all the +pieces together to write our login dialog: + + +```nim + +# some consts in order to prevent typos: +const + username = kstring"username" + password = kstring"password" + +var loggedIn: bool + +proc loginDialog(): VNode = + result = buildHtml(tdiv): + if not loggedIn: + loginField("Name :", username, "input", validateNotEmpty) + loginField("Password: ", password, "password", validateNotEmpty) + button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()): + text "Login" + p: + text errors.getError(username) + p: + text errors.getError(password) + else: + p: + text "You are now logged in." + +setRenderer loginDialog +``` + +(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/login.nim).) + +This code still has a bug though, when you run it, the ``login`` button is not +disabled until some input fields are validated! This is easily fixed, +at initialization we have to do: + +```nim + +setError username, username & " must not be empty" +setError password, password & " must not be empty" +``` +There are likely more elegant solutions to this problem. + +## Routing + + +For routing ``setRenderer`` can be called with a callback that takes a parameter of +type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example: + +```nim + +proc createDom(data: RouterData): VNode = + if data.hashPart == "#/": filter = all + elif data.hashPart == "#/completed": filter = completed + elif data.hashPart == "#/active": filter = active + result = buildHtml(tdiv(class="todomvc-wrapper")): + section(class = "todoapp"): + ... + +setRenderer createDom +``` +(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/todoapp/todoapp.nim).) + +## Server Side HTML Rendering + +Karax can also be used to render HTML on the server. Only a subset of +modules can be used since there is no JS interpreter. + +```nim + +import karax / [karaxdsl, vdom] + +const places = @["boston", "cleveland", "los angeles", "new orleans"] + +proc render*(): string = + let vnode = buildHtml(tdiv(class = "mt-3")): + h1: text "My Web Page" + p: text "Hello world" + ul: + for place in places: + li: text place + dl: + dt: text "Can I use Karax for client side single page apps?" + dd: text "Yes" + + dt: text "Can I use Karax for server side HTML rendering?" + dd: text "Yes" + result = $vnode + +echo render() +``` +## Generate HTML with event handlers + +If you are writing a static site generator or do server-side HTML rendering +via ``nim c``, you may want to override ``addEventHandler`` when using event +handlers to avoid compiler complaints. + +Here's an example of auto submit a dropdown when a value is selected: + +```nim + +template kxi(): int = 0 +template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) = + n.setAttr($k, action) + +let + names = @["nim", "c", "python"] + selected_name = request.params.getOrDefault("name") + hello = buildHtml(html): + form(`method` = "get"): + select(name="name", onchange="this.form.submit()"): + for name in names: + if name == selected_name: + option(selected = ""): text name + else: + option: text name +``` + +## License +MIT License. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt). diff --git a/readme.rst b/readme.rst deleted file mode 100644 index 7881074..0000000 --- a/readme.rst +++ /dev/null @@ -1,327 +0,0 @@ -Karax – Single page applications in Nim |travis| -================================================ - -Karax is a framework for developing single page applications in Nim. -The API is reasonably stable and version 1 should arrive anytime soon -now. - -To try it out, run:: - - cd ~/projects # Insert your favourite directory for projects - - nimble develop karax # This will clone Karax and create a link to it in ~/.nimble - - cd karax - - cd examples/todoapp - nim js todoapp.nim - open todoapp.html - cd ../.. - - cd examples/mediaplayer - nim js playerapp.nim - open playerapp.html - -It uses a virtual DOM like React, but is much smaller than the existing -frameworks plus of course it's written in Nim for Nim. No external -dependencies! And thanks to Nim's whole program optimization only what -is used ends up in the generated JavaScript code. - - -Goals -===== - -- Leverage Nim's macro system to produce a framework that allows - for the development of applications that are boilerplate free. -- Keep it small, keep it fast, keep it flexible. - -.. |travis| image:: https://travis-ci.org/pragmagic/karax.svg?branch=master - :target: https://travis-ci.org/pragmagic/karax - - -Hello World -=========== - -The simplest Karax program looks like this: - -.. code-block:: nim - - include karax / prelude - - proc createDom(): VNode = - result = buildHtml(tdiv): - text "Hello World!" - - setRenderer createDom - - -Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead -here. ``tdiv`` produces a ``
`` virtual DOM node. - -As you can see, karax comes with its own ``buildHtml`` DSL for convenient -construction of (virtual) DOM trees (of type ``VNode``). Karax provides -a tiny build tool called ``karun`` that generates the HTML boilerplate code that -embeds and invokes the generated JavaScript code:: - - nim c karax/tools/karun - karax/tools/karun -r helloworld.nim - -Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by -``buildHtml``: - -.. code-block:: nim - - let tmp1 = tree(VNodeKind.tdiv) - add(tmp1, text "Hello World!") - tmp1 - -(I shortened the IDs for better readability.) - -Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree -construction so that it composes with all of Nim's control flow constructs: - - -.. code-block:: nim - - include karax / prelude - import random - - proc createDom(): VNode = - result = buildHtml(tdiv): - if random(100) <= 50: - text "Hello World!" - else: - text "Hello Universe" - - randomize() - setRenderer createDom - - -Produces: - -.. code-block:: nim - - let tmp1 = tree(VNodeKind.tdiv) - if random(100) <= 50: - add(tmp1, text "Hello World!") - else: - add(tmp1, text "Hello Universe") - tmp1 - - -Event model -=========== - -Karax does not change the DOM's event model much, here is a program -that writes "Hello simulated universe" on a button click: - -.. code-block:: nim - - include karax / prelude - # alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson] - - var lines: seq[kstring] = @[] - - proc createDom(): VNode = - result = buildHtml(tdiv): - button: - text "Say hello!" - proc onclick(ev: Event; n: VNode) = - lines.add "Hello simulated universe" - for x in lines: - tdiv: - text x - - setRenderer createDom - - -``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible -string"; for the JS target that is an immutable JavaScript string) which -is preferred for efficiency on the JS target. However, on the native targets -``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML -construction is also avaible for the native targets (!) and the ``kstring`` -abstraction helps to deal with these conflicting requirements. - -Karax's DSL is quite flexible when it comes to event handlers, so the -following syntax is also supported: - -.. code-block:: nim - - include karax / prelude - from sugar import `=>` - - var lines: seq[kstring] = @[] - - proc createDom(): VNode = - result = buildHtml(tdiv): - button(onclick = () => lines.add "Hello simulated universe"): - text "Say hello!" - for x in lines: - tdiv: - text x - - setRenderer createDom - - -The ``buildHtml`` macro produces this code for us: - -.. code-block:: nim - - let tmp2 = tree(VNodeKind.tdiv) - let tmp3 = tree(VNodeKind.button) - addEventHandler(tmp3, EventKind.onclick, - () => lines.add "Hello simulated universe", kxi) - add(tmp3, text "Say hello!") - add(tmp2, tmp3) - for x in lines: - let tmp4 = tree(VNodeKind.tdiv) - add(tmp4, text x) - add(tmp2, tmp4) - tmp2 - -As the examples grow larger it becomes more and more visible of what -a DSL that composes with the builtin Nim control flow constructs buys us. -Once you have tasted this power there is no going back and languages -without AST based macro system simply don't cut it anymore. - - -Attaching data to an event handler -================================== - -Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any -additional data that should be passed to the event handler needs to be -done via Nim's closures. In general this means a pattern like this: - -.. code-block:: nim - - proc menuAction(menuEntry: kstring): proc() = - result = proc() = - echo "clicked ", menuEntry - - proc buildMenu(menu: seq[kstring]): VNode = - result = buildHtml(tdiv): - for m in menu: - nav(class="navbar is-primary"): - tdiv(class="navbar-brand"): - a(class="navbar-item", onclick = menuAction(m)): - - -DOM diffing -=========== - -Ok, so now we have seen DOM creation and event handlers. But how does -Karax actually keep the DOM up to date? The trick is that every event -handler is wrapped in a helper proc that triggers a *redraw* operation -that calls the *renderer* that you initially passed to ``setRenderer``. -So a new virtual DOM is created and compared against the previous -virtual DOM. This comparison produces a patch set that is then applied -to the real DOM the browser uses internally. This process is called -"virtual DOM diffing" and other frameworks, most notably Facebook's -*React*, do quite similar things. The virtual DOM is faster to create -and manipulate than the real DOM so this approach is quite efficient. - - -Form validation -=============== - -Most applications these days have some "login" -mechanism consisting of ``username`` and ``password`` and -a ``login`` button. The login button should only be clickable -if ``username`` and ``password`` are not empty. An error -message should be shown as long as one input field is empty. - -To create new UI elements we write a ``loginField`` proc that -returns a ``VNode``: - -.. code-block:: nim - - proc loginField(desc, field, class: kstring; - validator: proc (field: kstring): proc ()): VNode = - result = buildHtml(tdiv): - label(`for` = field): - text desc - input(class = class, id = field, onchange = validator(field)) - -We use the ``karax / errors`` module to help with this error -logic. The ``errors`` module is mostly a mapping from strings to -strings but it turned out that the logic is tricky enough to warrant -a library solution. ``validateNotEmpty`` returns a closure that -captures the ``field`` parameter: - -.. code-block:: nim - - proc validateNotEmpty(field: kstring): proc () = - result = proc () = - let x = getVNodeById(field) - if x.text.isNil or x.text == "": - errors.setError(field, field & " must not be empty") - else: - errors.setError(field, "") - -This indirection is required because -event handlers in Karax need to have the type ``proc ()`` -or ``proc (ev: Event; n: VNode)``. The errors module also -gives us a handy ``disableOnError`` helper. It returns -``"disabled"`` if there are errors. Now we have all the -pieces together to write our login dialog: - - -.. code-block:: nim - - # some consts in order to prevent typos: - const - username = kstring"username" - password = kstring"password" - - var loggedIn: bool - - proc loginDialog(): VNode = - result = buildHtml(tdiv): - if not loggedIn: - loginField("Name :", username, "input", validateNotEmpty) - loginField("Password: ", password, "password", validateNotEmpty) - button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()): - text "Login" - p: - text errors.getError(username) - p: - text errors.getError(password) - else: - p: - text "You are now logged in." - - setRenderer loginDialog - -(Full example `here `_.) - -This code still has a bug though, when you run it, the ``login`` button is not -disabled until some input fields are validated! This is easily fixed, -at initialization we have to do: - -.. code-block:: nim - - setError username, username & " must not be empty" - setError password, password & " must not be empty" - -There are likely more elegant solutions to this problem. - - -Routing -======= - -For routing ``setRenderer`` can be called with a callback that takes a parameter of -type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example: - -.. code-block:: nim - - proc createDom(data: RouterData): VNode = - if data.hashPart == "#/": filter = all - elif data.hashPart == "#/completed": filter = completed - elif data.hashPart == "#/active": filter = active - result = buildHtml(tdiv(class="todomvc-wrapper")): - section(class = "todoapp"): - ... - - setRenderer createDom - -(Full example `here `_.) diff --git a/tests/diffDomTests.html b/tests/diffDomTests.html index cb6ab28..f8f0e18 100644 --- a/tests/diffDomTests.html +++ b/tests/diffDomTests.html @@ -9,7 +9,7 @@
- + diff --git a/tests/difftest.nim b/tests/difftest.nim index fca3391..aa2a3c0 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -22,15 +22,19 @@ proc shortRepr(n: VNode): string = var err = 0 proc doDiff(a, b: VNode; expected: varargs[string]) = - discard diff(b, a, nil, vnodeToDom(a, kxi), kxi) + diff(b, a, nil, vnodeToDom(a, kxi), kxi) + var j = 0 for i in 0..= expected.len: - echo "patches differ; expected nothing but got: ", p - inc err - elif p != expected[i]: - echo "patches differ; expected ", expected[i], " but got: ", p - inc err + if kxi.patches[i].k != pkSame: + let n = if kxi.patches[i].k == pkDetach: kxi.patches[i].oldNode else: kxi.patches[i].newNode + let p = $kxi.patches[i].k & " " & shortRepr(n) + if j >= expected.len: + echo "patches differ; expected nothing but got: ", p + inc err + elif p != expected[j]: + echo "patches differ; expected ", expected[i], " but got: ", p + inc err + inc j #hasDom(kxi.currentTree) kxi.patchLen = 0 diff --git a/tests/domEventTests.html b/tests/domEventTests.html index 26e161f..dbbff0d 100644 --- a/tests/domEventTests.html +++ b/tests/domEventTests.html @@ -8,6 +8,6 @@
- + diff --git a/tests/nativehtmlgen.nim b/tests/nativehtmlgen.nim index 5bb7186..b30ab6a 100644 --- a/tests/nativehtmlgen.nim +++ b/tests/nativehtmlgen.nim @@ -5,6 +5,10 @@ import "../karax" / [karaxdsl, vdom] when defined(js): {.error: "Use 'nim c' to compile this example".} +template kxi(): int = 0 +template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) = + n.setAttr($k, action) + let tab = buildHtml(table): tr: td: @@ -16,5 +20,8 @@ let tab = buildHtml(table): text "Cell C" td: text "Cell D" + td: + a(href = "#/", onclick = "javascript:myFunc()"): + text"haha" echo tab