diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 5e6c415..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,31 +0,0 @@ -# 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 842e140..acb4ec1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,3 @@ nimcache/ karax/tools/karun *.code-workspace *.exe -app.js -app.html diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6e03ea9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +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 3eec1a4..8298ba9 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 has a single integer field. + The node havs a single integer field. ``VNodeKind.bool`` The node has a single boolean field. -``VNodeKind.vthunk`` +``VNodeKint.vthunk`` The node is a `virtual thunk`:idx:. This means there is a function attached to it that produces the ``VNode`` structure on demand. -``VNodeKind.dthunk`` +``VNodeKint.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)`` or empty ``proc ()``. +signature ``proc (ev: Event; n: VNode)``. diff --git a/examples/button.nim b/examples/button.nim index d86fa82..bd49dc3 100644 --- a/examples/button.nim +++ b/examples/button.nim @@ -1,3 +1,4 @@ + include karax / prelude var lines: seq[kstring] = @[] diff --git a/examples/carousel/carousel.html b/examples/carousel/carousel.html index 500e73a..979dd38 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 deleted file mode 100644 index 716c2f0..0000000 --- a/examples/hellostyle.nim +++ /dev/null @@ -1,17 +0,0 @@ -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 8343d07..9d6ebc6 100644 --- a/examples/hellouniverse.nim +++ b/examples/hellouniverse.nim @@ -1,9 +1,10 @@ + include karax / prelude import random proc createDom(): VNode = result = buildHtml(tdiv): - if rand(100) <= 50: + if random(100) <= 50: text "Hello World!" else: text "Hello Universe" diff --git a/examples/login.nim b/examples/login.nim index 0350e23..6cc38b1 100644 --- a/examples/login.nim +++ b/examples/login.nim @@ -1,5 +1,6 @@ + include karax / prelude -from sugar import `=>` +from future import `=>` import karax / errors proc loginField(desc, field, class: kstring; @@ -17,7 +18,7 @@ const proc validateNotEmpty(field: kstring): proc () = result = proc () = let x = getVNodeById(field) - if x.text == "": + if x.text.isNil or x.text == "": errors.setError(field, field & " must not be empty") else: errors.setError(field, "") @@ -27,7 +28,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 a45a2e8..9e076e6 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 44d657e..1ff8b1b 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 ab3daa7..488a3bf 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 e3720fd..b944de5 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) = - for i in 0.. - + diff --git a/experiments/echartstest.html b/experiments/echartstest.html index 8c9b225..fd510bd 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 941333a..e1fe419 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 8b909f1..ad3ce2f 100644 --- a/experiments/scrollapp.html +++ b/experiments/scrollapp.html @@ -10,7 +10,7 @@
- + diff --git a/karax.nimble b/karax.nimble index 8712a6d..87ce20f 100644 --- a/karax.nimble +++ b/karax.nimble @@ -1,15 +1,14 @@ # Package -version = "1.2.1" +version = "1.0.0" author = "Andreas Rumpf" description = "Karax is a framework for developing single page applications in Nim." license = "MIT" # Dependencies -requires "nim >= 0.18.0" -requires "ws" -requires "dotenv" +requires "nim >= 0.16.1" + skipDirs = @["examples", "experiments", "tests"] bin = @["karax/tools/karun"] diff --git a/karax/compact.nim b/karax/compact.nim index 4e5dd6d..3363802 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.strVal) + result = ident($n.symbol) else: result = n diff --git a/karax/jwebsockets.nim b/karax/jwebsockets.nim index 7c94da7..2b1412e 100644 --- a/karax/jwebsockets.nim +++ b/karax/jwebsockets.nim @@ -11,7 +11,4 @@ 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 4235b50..114b29d 100644 --- a/karax/kajax.nim +++ b/karax/kajax.nim @@ -4,66 +4,6 @@ ## 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; @@ -76,8 +16,20 @@ proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)]; cont(httpStatus, response) if doRedraw: redraw(kxi) - var this {.importc: "this".}: ThisObj + 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): @@ -129,4 +81,3 @@ 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 2246676..2fc8632 100644 --- a/karax/karax.nim +++ b/karax/karax.nim @@ -7,16 +7,17 @@ export kdom.Event, kdom.Blob when defined(nimNoNil): {.experimental: "notnil".} -proc kout*[T](x: T) {.importc: "console.log", varargs.} - ## The preferred way of debugging karax applications. +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. type PatchKind = enum - pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame + pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach Patch = object k: PatchKind parent, current: Node - newNode, oldNode: VNode + n: VNode PatchV = object parent, newChild: VNode pos: int @@ -30,14 +31,13 @@ type KaraxInstance* = ref object ## underlying karax instance. Usually you don't have ## know about this. - rootId: cstring + rootId: cstring not nil 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,24 +78,6 @@ 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, - newNode: na, oldNode: oldNode)) + kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, n: na)) else: kxi.patches[L].k = ka kxi.patches[L].parent = parenta kxi.patches[L].current = currenta - kxi.patches[L].newNode = na - kxi.patches[L].oldNode = oldNode + kxi.patches[L].n = na inc kxi.patchLen proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = @@ -382,40 +344,26 @@ 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) = +proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult = when defined(stats): if kxi.recursion > 100: - echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false) + echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode) if oldNode.kind == VNodeKind.text: echo oldNode.text + #return + #doAssert false, "overflow!" inc kxi.recursion - let result = eq(newNode, oldNode, false) + result = eq(newNode, oldNode) case result of componentsIdentical: kxi.components.add ComponentPair(oldNode: VComponent(oldNode), @@ -478,54 +404,90 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) = 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 + if newLength == 0 and oldLength == 0: return result 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: - if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical: - kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix]) + eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): inc commonPrefix - else: - break # compute common suffix: var oldPos = oldLength - 1 var newPos = newLength - 1 while oldPos >= commonPrefix and newPos >= commonPrefix: - if eq(newNode[newPos], oldNode[oldPos], true) == identical: - kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos]) + eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): 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: - diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) + 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 if oldPos + 1 == oldLength: for i in pos..newPos: - kxi.addPatch(pkAppend, current, nil, newNode[i], nil) + kxi.addPatch(pkAppend, current, nil, newNode[i]) + result = usenewNode else: let before = current.childNodes[oldPos + 1] for i in pos..newPos: - kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil) + kxi.addPatch(pkInsertBefore, current, before, newNode[i]) + result = usenewNode # 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, nil) + kxi.addPatch(pkRemove, current, current.childNodes[i], nil) + result = usenewNode of different: detach(oldNode) - kxi.addPatch(pkReplace, parent, current, newNode, nil) + kxi.addPatch(pkReplace, parent, current, newNode) of usenewNode: doAssert(false, "eq returned usenewNode") when defined(stats): dec kxi.recursion @@ -555,25 +517,24 @@ proc applyComponents(kxi: KaraxInstance) = x.renderedVersion = x.version if oldExpanded == nil: detach(x) - kxi.addPatch(pkReplace, parent, current, x.expanded, nil) + kxi.addPatch(pkReplace, parent, current, x.expanded) when defined(karaxDebug): echo "Component ", newNode.text, ": old expansion didn't exist" else: - 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" + 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" inc i setLen(kxi.components, 0) @@ -588,7 +549,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, nil) + kxi.addPatch(pkRemove, current, current.childNodes[position], nil) parent.delete(position) applyPatch(kxi) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) @@ -596,11 +557,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, nil) + kxi.addPatch(pkAppend, current, nil, kid) parent.add(kid) else: let before = current.childNodes[position] - kxi.addPatch(pkInsertBefore, current, before, kid, nil) + kxi.addPatch(pkInsertBefore, current, before, kid) parent.insert(kid, position) applyPatch(kxi) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) @@ -608,7 +569,7 @@ proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) = proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = let olddom = oldNode.dom doAssert olddom != nil - diff(newNode, oldNode, nil, olddom, kxi) + discard 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: @@ -622,41 +583,21 @@ 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 = toDom(newtree, useAttachedNode = true, kxi) + let asdom = vnodeToDom(newtree, kxi) replaceById(kxi.rootId, asdom) else: doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) let olddom = document.getElementById(kxi.rootId) - diff(newtree, kxi.currentTree, nil, olddom, kxi) + discard diff(newtree, kxi.currentTree, nil, olddom, kxi) + #kout cstring"patch len ", patches.len when defined(profileKarax): echo "<<<<<<<<<<<<<<" echa reasons @@ -675,12 +616,16 @@ proc dodraw(kxi: KaraxInstance) = # now that it's part of the DOM, give it the focus: if kxi.toFocus != nil: kxi.toFocus.focus() - kxi.rendering = false + kxi.renderId = 0 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: @@ -768,7 +713,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: "use setRenderer instead".} = +proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated.} = ## Now deprecated, instead pass a callback to ``setRenderer`` that receives ## a ``data: RouterData`` parameter. proc wrapper() = @@ -800,10 +745,11 @@ 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 - document.body.prepend(s) + body.prepend(s) redraw(kxi) proc runLater*(action: proc(); later = 400): Timeout {.discardable.} = @@ -817,18 +763,8 @@ 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 451632c..81ba99d 100644 --- a/karax/karaxdsl.nim +++ b/karax/karaxdsl.nim @@ -11,8 +11,8 @@ const proc getName(n: NimNode): string = case n.kind - of nnkIdent, nnkSym: - result = $n + of nnkIdent: + result = $n.ident of nnkAccQuoted: result = "" for i in 0..`_. -when not declared(dom.DomApiVersion): - include kdom_impl +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/kdom_impl.nim b/karax/kdom_impl.nim deleted file mode 100644 index aaa26cb..0000000 --- a/karax/kdom_impl.nim +++ /dev/null @@ -1,1232 +0,0 @@ -# -# -# 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 adf1ecb..dc1ba44 100644 --- a/karax/tools/karun.nim +++ b/karax/tools/karun.nim @@ -1,98 +1,55 @@ +## 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 = """ - + """ - -const html = """ + html = """ - $1 $2 - -
- -$3 + +
+ """ -const websocket = """ - -""" proc exec(cmd: string) = if os.execShellCmd(cmd) != 0: quit "External command failed: " & cmd -proc build(rest: string, selectedCss: string, run: bool, watch: bool) = +proc build(name: string, rest: string, selectedCss: string, run: bool) = echo("Building...") - 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() + createDir("nimcache") + exec("nim js --out:nimcache/" & name & ".js " & rest) + let dest = "nimcache" / name & ".html" + writeFile(dest, html % [name, selectedCss]) + if run: openDefaultBrowser(dest) proc main = var op = initOptParser() var rest = op.cmdLineRest var file = "" var run = false - var watch = false var selectedCss = "" + var watch = false + var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable + while true: op.next() case op.kind @@ -102,11 +59,8 @@ proc main = run = true rest = rest.replace("--run ") of "css": - if op.val != "": - selectedCss = readFile(op.val) - else: - selectedCss = css - rest = rest.substr(rest.find(" ")) + selectedCss = css + rest = rest.replace("--css ") else: discard of cmdShortOption: if op.key == "r": @@ -119,11 +73,26 @@ proc main = of cmdEnd: break if file.len == 0: quit "filename expected" - if run: - spawn serve() + let name = file.splitFile.name + build(name, rest, selectedCss, run) + echo("after build") if watch: - spawn watchBuild(file, selectedCss, rest) - build(rest,selectedCss, run, watch) - sync() + # 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) main() + + + + diff --git a/karax/tools/karun.nims b/karax/tools/karun.nims deleted file mode 100644 index 8983496..0000000 --- a/karax/tools/karun.nims +++ /dev/null @@ -1 +0,0 @@ -switch("threads", "on") \ No newline at end of file diff --git a/karax/tools/static_server.nim b/karax/tools/static_server.nim deleted file mode 100644 index eeddf4f..0000000 --- a/karax/tools/static_server.nim +++ /dev/null @@ -1,141 +0,0 @@ -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 8fd654e..3dc61d9 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, toLowerAscii, tokenize +from strutils import toUpperAscii type VNodeKind* {.pure.} = enum @@ -35,19 +35,7 @@ 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, 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, + area, svg, math, table, caption, colgroup, col, tbody, thead, tfoot, tr, td, th, @@ -57,10 +45,6 @@ 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. @@ -105,16 +89,9 @@ 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) @@ -317,12 +294,6 @@ 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 @@ -408,8 +379,6 @@ 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('<') @@ -428,17 +397,7 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) = result.add("=\"") result.addEscapedAttr(v) result.add('"') - 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('"') + # XXX add style to string if n.len > 0: result.add('>') if n.len > 1: @@ -463,13 +422,8 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) = result.add("") - elif n.kind in selfClosing: - result.add(" />") else: - result.add(">") - result.add("") + result.add(" />") proc `$`*(n: VNode): kstring = diff --git a/karax/vstyles.nim b/karax/vstyles.nim index 7097aee..e2fca7d 100644 --- a/karax/vstyles.nim +++ b/karax/vstyles.nim @@ -1,9 +1,5 @@ -##[ -see examples/hellostyle.nim -]## -import std/[macros, strutils] -import kbase +import macros, kbase when defined(js): import kdom, jdict @@ -238,10 +234,7 @@ proc eq*(a, b: VStyle): bool = if a[i] != b[i]: return false return true -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 +proc setAttr(s: VStyle; a, value: kstring) {.noSideEffect.} = var i = 0 while i < s.len: if s[i] == a: @@ -250,8 +243,8 @@ proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} = elif s[i] > a: s.add "" s.add "" - # insertion point here, shift all remaining pairs by 2 indexes - for j in countdown(s.len-1, i+3, 2): + # insertion point here: + for j in countdown(s.len-1, i, 2): s[j] = s[j-2] s[j-1] = s[j-3] s[i] = a @@ -296,24 +289,6 @@ 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 deleted file mode 100644 index d8a3814..0000000 --- a/readme.md +++ /dev/null @@ -1,394 +0,0 @@ -![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 new file mode 100644 index 0000000..7881074 --- /dev/null +++ b/readme.rst @@ -0,0 +1,327 @@ +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 f8f0e18..cb6ab28 100644 --- a/tests/diffDomTests.html +++ b/tests/diffDomTests.html @@ -9,7 +9,7 @@
- + diff --git a/tests/difftest.nim b/tests/difftest.nim index aa2a3c0..fca3391 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -22,19 +22,15 @@ proc shortRepr(n: VNode): string = var err = 0 proc doDiff(a, b: VNode; expected: varargs[string]) = - diff(b, a, nil, vnodeToDom(a, kxi), kxi) - var j = 0 + discard diff(b, a, nil, vnodeToDom(a, kxi), kxi) for i in 0..= 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 + let p = $kxi.patches[i].k & " " & shortRepr(kxi.patches[i].n) + if i >= 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 #hasDom(kxi.currentTree) kxi.patchLen = 0 diff --git a/tests/domEventTests.html b/tests/domEventTests.html index dbbff0d..26e161f 100644 --- a/tests/domEventTests.html +++ b/tests/domEventTests.html @@ -8,6 +8,6 @@
- + diff --git a/tests/nativehtmlgen.nim b/tests/nativehtmlgen.nim index b30ab6a..5bb7186 100644 --- a/tests/nativehtmlgen.nim +++ b/tests/nativehtmlgen.nim @@ -5,10 +5,6 @@ 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: @@ -20,8 +16,5 @@ let tab = buildHtml(table): text "Cell C" td: text "Cell D" - td: - a(href = "#/", onclick = "javascript:myFunc()"): - text"haha" echo tab