diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5e6c415 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [ master ] + pull_request: + branches: [ master ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + container: nimlang/nim + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + # Runs a single command using the runners shell + - name: Tests + run: | + nimble develop -y + nim c -r tests/tester.nim diff --git a/.gitignore b/.gitignore index 42b07a7..842e140 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ nimcache/ -/tests/tester \ No newline at end of file +/tests/tester +karax/tools/karun +*.code-workspace +*.exe +app.js +app.html diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0c60bd2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -sudo: false -language: c -os: linux -install: - - git clone https://github.com/nim-lang/nim - - cd nim - - git clone --depth 1 https://github.com/nim-lang/csources.git - - cd csources - - sh build.sh - - cd ../ - - bin/nim c koch - - ./koch boot -d:release - - cd .. -before_script: - - set -e - - export PATH=$(pwd)/nim/bin:$(pwd):$PATH -script: - - nim c -r tests/tester.nim diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ac49796 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Xored Software, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/docs.rst b/docs/docs.rst index 8298ba9..3eec1a4 100644 --- a/docs/docs.rst +++ b/docs/docs.rst @@ -10,17 +10,17 @@ and some extensions that allow for an efficient component system. These extensions are: ``VNodeKind.int`` - The node havs a single integer field. + The node has a single integer field. ``VNodeKind.bool`` The node has a single boolean field. -``VNodeKint.vthunk`` +``VNodeKind.vthunk`` The node is a `virtual thunk`:idx:. This means there is a function attached to it that produces the ``VNode`` structure on demand. -``VNodeKint.dthunk`` +``VNodeKind.dthunk`` The node is a `DOM thunk`:idx:. This means there is a function attached to it that produces the ``Node`` DOM structure on demand. @@ -53,6 +53,6 @@ Event system Karax does not abstract over the event system the DOM offers much: The same ``Event`` structure is used. Every callback has the -signature ``proc (ev: Event; n: VNode)``. +signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``. diff --git a/examples/button.nim b/examples/button.nim new file mode 100644 index 0000000..d86fa82 --- /dev/null +++ b/examples/button.nim @@ -0,0 +1,15 @@ +include karax / prelude + +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 diff --git a/examples/buttonlambda.nim b/examples/buttonlambda.nim new file mode 100644 index 0000000..2ddadff --- /dev/null +++ b/examples/buttonlambda.nim @@ -0,0 +1,14 @@ +include karax / prelude +from future 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 diff --git a/examples/carousel/carousel.html b/examples/carousel/carousel.html index 979dd38..500e73a 100644 --- a/examples/carousel/carousel.html +++ b/examples/carousel/carousel.html @@ -1,15 +1,15 @@ - - Carousel app - + + Carousel app + - + -
+
- + - + diff --git a/examples/carousel/carousel.nims b/examples/carousel/carousel.nims index 1451fc0..cef2dec 100644 --- a/examples/carousel/carousel.nims +++ b/examples/carousel/carousel.nims @@ -1 +1 @@ ---path: "../../src" +--path: "../../karax" diff --git a/examples/config.nims b/examples/config.nims new file mode 100644 index 0000000..76ed4d9 --- /dev/null +++ b/examples/config.nims @@ -0,0 +1 @@ +--path: ".." diff --git a/examples/hellostyle.nim b/examples/hellostyle.nim new file mode 100644 index 0000000..716c2f0 --- /dev/null +++ b/examples/hellostyle.nim @@ -0,0 +1,17 @@ +include karax / prelude +import karax / vstyles + +proc createDom(): VNode = + result = buildHtml(tdiv): + tdiv(style = style(StyleAttr.color, "red".cstring)): + text "red" + tdiv(style = style(StyleAttr.color, "blue")): + text "blue" + # explicit `kstring` required for varargs overload + tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): + text "italic orange" + # can use a string directly + tdiv(style = "font-style: oblique; color: pink".toCss): + text "oblique pink" + +setRenderer createDom diff --git a/examples/hellouniverse.nim b/examples/hellouniverse.nim new file mode 100644 index 0000000..8343d07 --- /dev/null +++ b/examples/hellouniverse.nim @@ -0,0 +1,12 @@ +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 diff --git a/examples/helloworld.nim b/examples/helloworld.nim new file mode 100644 index 0000000..76c43db --- /dev/null +++ b/examples/helloworld.nim @@ -0,0 +1,7 @@ +include karax / prelude + +proc createDom(): VNode = + result = buildHtml(tdiv): + text "Hello World!" + +setRenderer createDom diff --git a/examples/login.nim b/examples/login.nim new file mode 100644 index 0000000..0350e23 --- /dev/null +++ b/examples/login.nim @@ -0,0 +1,46 @@ +include karax / prelude +from sugar import `=>` +import karax / errors + +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)) + +# some consts in order to prevent typos: +const + username = kstring"username" + password = kstring"password" + +proc validateNotEmpty(field: kstring): proc () = + result = proc () = + let x = getVNodeById(field) + if x.text == "": + errors.setError(field, field & " must not be empty") + else: + errors.setError(field, "") + +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." + +setError username, username & " must not be empty" +setError password, password & " must not be empty" + +when not declared(toychat): + setRenderer loginDialog diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim index 7bc9c65..c84b68b 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -1,5 +1,5 @@ -import karax, karaxdsl, vdom, kdom, compact +import karax / [karax, karaxdsl, vdom, kdom, compact] const Play = 0 diff --git a/examples/mediaplayer/playerapp.html b/examples/mediaplayer/playerapp.html index 9e076e6..a45a2e8 100644 --- a/examples/mediaplayer/playerapp.html +++ b/examples/mediaplayer/playerapp.html @@ -1,15 +1,15 @@ - - Player app - + + Player app + - + -
+
- + - + diff --git a/examples/mediaplayer/playerapp.nim b/examples/mediaplayer/playerapp.nim index e611eec..acb2e68 100644 --- a/examples/mediaplayer/playerapp.nim +++ b/examples/mediaplayer/playerapp.nim @@ -1,6 +1,6 @@ ## Example app that shows how to write and embed a custom component. -include karaxprelude +include karax/prelude import mediaplayer const url = "https://www.w3schools.com/html/mov_bbb.mp4" diff --git a/examples/mediaplayer/playerapp.nims b/examples/mediaplayer/playerapp.nims index 9028ff1..cf2eb9e 100644 --- a/examples/mediaplayer/playerapp.nims +++ b/examples/mediaplayer/playerapp.nims @@ -1 +1 @@ ---path: "../../src" +--path: "../.." diff --git a/examples/scrollapp/scrollapp.html b/examples/scrollapp/scrollapp.html index 1ff8b1b..44d657e 100644 --- a/examples/scrollapp/scrollapp.html +++ b/examples/scrollapp/scrollapp.html @@ -1,15 +1,15 @@ - - Todo app - + + Todo app + - + -
+
- + - + diff --git a/examples/scrollapp/scrollapp.nim b/examples/scrollapp/scrollapp.nim index 8ead611..4490447 100644 --- a/examples/scrollapp/scrollapp.nim +++ b/examples/scrollapp/scrollapp.nim @@ -1,7 +1,7 @@ ## Example that shows how to accomplish an "infinitely scrolling" app. -include karaxprelude -import jstrutils, kdom, vstyles +include karax/prelude +import karax / [jstrutils, kdom, vstyles] var entries: seq[cstring] = @[] for i in 1..500: diff --git a/examples/scrollapp/scrollapp.nims b/examples/scrollapp/scrollapp.nims index 9028ff1..cf2eb9e 100644 --- a/examples/scrollapp/scrollapp.nims +++ b/examples/scrollapp/scrollapp.nims @@ -1 +1 @@ ---path: "../../src" +--path: "../.." diff --git a/examples/todoapp/todoapp.html b/examples/todoapp/todoapp.html index 488a3bf..ab3daa7 100644 --- a/examples/todoapp/todoapp.html +++ b/examples/todoapp/todoapp.html @@ -1,16 +1,16 @@ - - Todo app - - + + Todo app + + - + -
+
- + - + diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 47ddeae..e3720fd 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -1,5 +1,5 @@ -import vdom, karax, karaxdsl, jstrutils, compact, localstorage +import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage] type Filter = enum @@ -9,7 +9,7 @@ var selectedEntry = -1 filter: Filter entriesLen: int - + doneswitch = true const contentSuffix = cstring"content" completedSuffix = cstring"completed" @@ -41,8 +41,9 @@ proc updateEntry(pos: int, content: cstring, completed: bool) = markAsCompleted(pos, completed) proc onTodoEnter(ev: Event; n: VNode) = - addEntry(n.value, false) - n.value = "" + if n.value.strip() != "": + addEntry(n.value, false) + n.value = "" proc removeHandler(ev: Event; n: VNode) = updateEntry(n.index, cstring(nil), false) @@ -61,9 +62,9 @@ proc toggleEntry(ev: Event; n: VNode) = markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = - clear() - selectedEntry = -1 - + for i in 0.. - + diff --git a/experiments/config.nims b/experiments/config.nims new file mode 100644 index 0000000..86673aa --- /dev/null +++ b/experiments/config.nims @@ -0,0 +1,2 @@ +--path: "../karax" +--path: ".." diff --git a/experiments/echartstest.html b/experiments/echartstest.html new file mode 100644 index 0000000..8c9b225 --- /dev/null +++ b/experiments/echartstest.html @@ -0,0 +1,14 @@ + + + + + Karax ECharts Test + + + + +
+ + + + diff --git a/experiments/echartstest.nim b/experiments/echartstest.nim new file mode 100644 index 0000000..950c6ce --- /dev/null +++ b/experiments/echartstest.nim @@ -0,0 +1,58 @@ +import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson] + +type + Views = enum + Customers, Products + +var + currentView = Customers + +type + EChart* = ref object + +proc echartsInit(n: Element): EChart {.importc: "echarts.init".} +proc setOption(x: EChart; option: JsonNode) {.importcpp.} + +proc postRender(data: RouterData) = + if currentView == Products: + let myChart = echartsInit(kdom.getElementById("echartSection")) + # specify chart configuration item and data + let option = %*{ + "title": { + "text": "ECharts entry example" + }, + "tooltip": {}, + "legend": { + "data": ["Sales"] + }, + "xAxis": { + "data": ["shirt","cardign","chiffon shirt","pants","heels","socks"] + }, + "yAxis": {}, + "series": [{ + "name": "Sales", + "type": "bar", + "data": [5, 20, 36, 10, 10, 20] + }] + } + myChart.setOption(option) + +proc createDom(data: RouterData): VNode = + let hash = data.hashPart + if hash == cstring"#/Products": currentView = Products + else: currentView = Customers + + result = buildHtml(tdiv): + ul(class = "tabs"): + for v in low(Views)..high(Views): + li: + a(href = "#/" & $v): + text cstring($v) + + if currentView == Products: + tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px"))) + tdiv: + text "other section" + +setRenderer createDom, "ROOT", postRender +setForeignNodeId "echartSection" diff --git a/experiments/menus.nim b/experiments/menus.nim new file mode 100644 index 0000000..6ea0529 --- /dev/null +++ b/experiments/menus.nim @@ -0,0 +1,65 @@ +## Example program that shows how to create menus with Karax. + +include prelude +import jstrutils, kdom + +proc contentA(): VNode = + result = buildHtml(tdiv): + text "content A" + +proc contentB(): VNode = + result = buildHtml(tdiv): + text "content B" + +proc contentC(): VNode = + result = buildHtml(tdiv): + text "content C" + + + +type + MenuItemHandler = proc(): VNode + +var content: MenuItemHandler = contentA + +proc menuAction(x: MenuItemHandler): proc() = + result = proc() = content = x + +proc buildMenu(): VNode = + result = buildHtml(tdiv): + nav(class="navbar is-primary"): + tdiv(class="navbar-brand"): + a(class="navbar-item", onclick = menuAction(contentA)): + strong: + text "My awesome menu" + + tdiv(id="navMenuTransparentExample", class="navbar-menu"): + tdiv(class = "navbar-start"): + tdiv(class="navbar-item has-dropdown is-hoverable"): + a(class="navbar-link", onclick = menuAction(contentB)): + text "Masters" + tdiv(class="navbar-dropdown is-boxed"): + a(class="navbar-item", onclick = menuAction(contentC)): + text "Inventory" + a(class="navbar-item", onclick = menuAction(contentC)): + text "Product" + hr(class="navbar-divider"): + a(class="navbar-item", onclick = menuAction(contentC)): + text "Product" + + tdiv(class="navbar-item has-dropdown is-hoverable"): + a(class="navbar-link", onclick = menuAction(contentC)): + text "Transactions" + tdiv(class = "navbar-dropdown is-boxed", id="blogDropdown"): + a(class="navbar-item", onclick = menuAction(contentC)): + text "Purchase Bill" + a(class="navbar-item", onclick = menuAction(contentC)): + text "Purchase Return" + a(class="navbar-item", onclick = menuAction(contentC)): + text "Sale Bill" + hr(class="navbar-divider"): + a(class="navbar-item", onclick = menuAction(contentC)): + text "Stock Adjustment" + content() + +setRenderer buildMenu diff --git a/experiments/nativenodes.nim b/experiments/nativenodes.nim new file mode 100644 index 0000000..9d255ee --- /dev/null +++ b/experiments/nativenodes.nim @@ -0,0 +1,18 @@ + + +include karax / prelude +import karax / kdom + +proc myInput: VNode = + result = buildHtml: + input() + +var inp = vnodeToDom(myInput()) + +proc myAwesomeComponent(x: Node): VNode = + result = dthunk(x) + +proc main: VNode = + result = myAwesomeComponent(inp) + +setRenderer main diff --git a/experiments/refs/demo.html b/experiments/refs/demo.html index e1fe419..941333a 100644 --- a/experiments/refs/demo.html +++ b/experiments/refs/demo.html @@ -8,6 +8,6 @@
- + diff --git a/experiments/scrollapp.html b/experiments/scrollapp.html index ad3ce2f..8b909f1 100644 --- a/experiments/scrollapp.html +++ b/experiments/scrollapp.html @@ -10,7 +10,7 @@
- + diff --git a/experiments/scrollapp_simple.nim b/experiments/scrollapp_simple.nim new file mode 100644 index 0000000..cdd19cd --- /dev/null +++ b/experiments/scrollapp_simple.nim @@ -0,0 +1,25 @@ +## Example that shows how to accomplish an "infinitely scrolling" app. + +include karax / prelude +import karax / [kdom, vstyles] + +var entries: seq[cstring] = @[] +for i in 1..500: + entries.add(cstring("Entry ") & &i) + +proc scrollEvent(ev: Event; n: VNode) = + let d = n.dom + if d != nil and inViewport(d.lastChild): + # "load" more data: + for i in 1..50: + entries.add(cstring("Loaded Entry ") & &i) + +proc createDom(): VNode = + result = buildHtml(): + tdiv(onscroll=scrollEvent, style=style( + (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))): + for x in entries: + tdiv: + text x + +setRenderer createDom diff --git a/experiments/trello/knete.nim b/experiments/trello/knete.nim new file mode 100644 index 0000000..3a09ee3 --- /dev/null +++ b/experiments/trello/knete.nim @@ -0,0 +1,752 @@ +## Raw DOM manipulation. No DOM diff'ing, no cry. + +import macros, tables +from strutils import startsWith, toLowerAscii, toUpperAscii + +when defined(js): + type kstring* = cstring +else: + type kstring* = string + +type + Tag* {.pure.} = enum + text = "#text", + + html, head, title, base, link, meta, style, + script, noscript, + body, section, nav, article, aside, + h1, h2, h3, h4, h5, h6, + header, footer, address, main + + p, hr, pre, blockquote, ol, ul, li, + dl, dt, dd, + figure, figcaption, + + tdiv = "div", + + a, em, strong, small, + strikethrough = "s", cite, quote, + dfn, abbr, data, time, code, `var` = "var", samp, + kdb, sub, sup, italic = "i", bold = "b", underlined = "u", + mark, ruby, rt, rp, bdi, dbo, span, br, wbr, + ins, del, img, iframe, embed, `object` = "object", + param, video, audio, source, track, canvas, map, + area, svg, math, + + table, caption, colgroup, col, tbody, thead, + tfoot, tr, td, th, + + form, fieldset, legend, label, input, button, + select, datalist, optgroup, option, textarea, + keygen, output, progress, meter, + details, summary, command, menu + +type + EventKind* {.pure.} = enum ## The events supported by the virtual DOM. + onclick, ## An element is clicked. + oncontextmenu, ## An element is right-clicked. + ondblclick, ## An element is double clicked. + onkeyup, ## A key was released. + onkeydown, ## A key is pressed. + onkeypressed, # A key was pressed. + onfocus, ## An element got the focus. + onblur, ## An element lost the focus. + onchange, ## The selected value of an element was changed. + onscroll, ## The user scrolled within an element. + + onmousedown, ## A pointing device button (usually a mouse) is pressed + ## on an element. + onmouseenter, ## A pointing device is moved onto the element that + ## has the listener attached. + onmouseleave, ## A pointing device is moved off the element that + ## has the listener attached. + onmousemove, ## A pointing device is moved over an element. + onmouseout, ## A pointing device is moved off the element that + ## has the listener attached or off one of its children. + onmouseover, ## A pointing device is moved onto the element that has + ## the listener attached or onto one of its children. + onmouseup, ## A pointing device button is released over an element. + + ondrag, ## An element or text selection is being dragged (every 350ms). + ondragend, ## A drag operation is being ended (by releasing a mouse button + ## or hitting the escape key). + ondragenter, ## A dragged element or text selection enters a valid drop target. + ondragleave, ## A dragged element or text selection leaves a valid drop target. + ondragover, ## An element or text selection is being dragged over a valid + ## drop target (every 350ms). + ondragstart, ## The user starts dragging an element or text selection. + ondrop, ## An element is dropped on a valid drop target. + + onsubmit, ## A form is submitted + oninput, ## An input value changes + + onkeyupenter, ## vdom extension: an input field received the ENTER key press + onkeyuplater ## vdom extension: a key was pressed and some time + ## passed (useful for on-the-fly text completions) + + +macro buildLookupTables(): untyped = + var a = newTree(nnkBracket) + for i in low(Tag)..high(Tag): + let x = $i + let y = if x[0] == '#': x else: toUpperAscii(x) + a.add(newCall("kstring", newLit(y))) + var e = newTree(nnkBracket) + for i in low(EventKind)..high(EventKind): + e.add(newCall("kstring", newLit(substr($i, 2)))) + + template tmpl(a, e) {.dirty.} = + const + toTagName*: array[Tag, kstring] = a + toEventName*: array[EventKind, kstring] = e + + result = getAst tmpl(a, e) + +buildLookupTables() + +# ---------------- CSS ---------------------------------------------- + +type + StyleAttr* {.pure.} = enum + ## The style attributes supported by the virtual DOM. + ## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp + alignContent + alignItems + alignSelf + animation + animationDelay + animationDirection + animationDuration + animationFillMode + animationIterationCount + animationName + animationTimingFunction + animationPlayState + background + backgroundAttachment + backgroundColor + backgroundImage + backgroundPosition + backgroundRepeat + backgroundClip + backgroundOrigin + backgroundSize + backfaceVisibility + border + borderBottom + borderBottomColor + borderBottomLeftRadius + borderBottomRightRadius + borderBottomStyle + borderBottomWidth + borderCollapse + borderColor + borderImage + borderImageOutset + borderImageRepeat + borderImageSlice + borderImageSource + borderImageWidth + borderLeft + borderLeftColor + borderLeftStyle + borderLeftWidth + borderRadius + borderRight + borderRightColor + borderRightStyle + borderRightWidth + borderSpacing + borderStyle + borderTop + borderTopColor + borderTopLeftRadius + borderTopRightRadius + borderTopStyle + borderTopWidth + borderWidth + bottom + boxDecorationBreak + boxShadow + boxSizing + captionSide + clear + clip + color + columnCount + columnFill + columnGap + columnRule + columnRuleColor + columnRuleStyle + columnRuleWidth + columns + columnSpan + columnWidth + content + counterIncrement + counterReset + cursor + direction + display + emptyCells + filter + flex + flexBasis + flexDirection + flexFlow + flexGrow + flexShrink + flexWrap + cssFloat + font + fontFamily + fontSize + fontSizeAdjust + fontStretch + fontStyle + fontVariant + fontWeight + hangingPunctuation + height + hyphens + icon + imageOrientation + justifyContent + left + letterSpacing + lineHeight + listStyle + listStyleImage + listStylePosition + listStyleType + margin + marginBottom + marginLeft + marginRight + marginTop + maxHeight + maxWidth + minHeight + minWidth + navDown + navIndex + navLeft + navRight + navUp + opacity + order + orphans + outline + outlineColor + outlineOffset + outlineStyle + outlineWidth + overflow + overflowX + overflowY + padding + paddingBottom + paddingLeft + paddingRight + paddingTop + pageBreakAfter + pageBreakBefore + pageBreakInside + perspective + perspectiveOrigin + pointerEvents + position + quotes + resize + right + scrollbar3dLightColor # note: missing in w3schools reference + scrollbarArrowColor # note: missing in w3schools reference + scrollbarBaseColor # note: missing in w3schools reference + scrollbarDarkshadowColor # note: missing in w3schools reference + scrollbarFaceColor # note: missing in w3schools reference + scrollbarHighlightColor # note: missing in w3schools reference + scrollbarShadowColor # note: missing in w3schools reference + scrollbarTrackColor # note: missing in w3schools reference + tableLayout + tabSize + textAlign + textAlignLast + textDecoration + textDecorationColor + textDecorationLine + textDecorationStyle + textIndent + textJustify + textOverflow + textShadow + textTransform + top + transform + transformOrigin + transformStyle + transition + transitionDelay + transitionDuration + transitionProperty + transitionTimingFunction + unicodeBidi + userSelect + verticalAlign + visibility + whiteSpace + width + wordBreak + wordSpacing + wordWrap + widows + zIndex + +macro buildStyleLookupTable(): untyped = + var e = newTree(nnkBracket) + for i in low(StyleAttr)..high(StyleAttr): + e.add(newCall("kstring", newLit($i))) + template tmpl(e) {.dirty.} = + const + toStyleAttrName: array[StyleAttr, kstring] = e + result = getAst tmpl(e) + +buildStyleLookupTable() + +type + StyleObj {.importc.} = object + 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 + Style* = ref StyleObj + + ElementObj {.importc.} = object + id*, class*, value*: cstring + disabled*: bool + style*: Style + Element* = ref ElementObj + + DocumentObj {.importc.} = object + Document = ref DocumentObj + +proc setStyle(s: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.} + +proc applyStyles*(e: Element; pairs: openArray[(StyleAttr, kstring)]) = + for x in pairs: + e.style.setStyle(toStyleAttrName[x[0]], x[1]) + +# ------- Tree manipulation --------------------------- + +var document {.importc.}: Document + +proc parent*(x: Element): Element {.importcpp: "#.parentNode".} + +proc up*(x: Element; className: cstring): Element = + result = x + while result != nil and result.class != className: + result = result.parent + +proc len*(x: Element): int {.importcpp: "#.childNodes.length".} +proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".} +proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.} +proc add*(n, child: Element) {.importcpp: "appendChild".} + +proc removeChild(n, child: Element) {.importcpp.} +proc replaceChild(n, newNode, oldNode: Element) {.importcpp.} + +proc replace*(self, by: Element) = replaceChild(self.parent, by, self) +proc delete*(self: Element) = removeChild(self.parent, self) + +proc insertBefore(n, newNode, before: Element) {.importcpp.} +proc insert*(before, newNode: Element) = insertBefore(before.parent, newNode, before) + +proc createTextNode(d: Document, text: cstring): Element {.importcpp.} +proc text*(s: kstring): Element = createTextNode(document, s) + +iterator items*(n: Element): Element = + for i in 0.. 0: + result[L-1] = tcall2(result[L-1], tmpContext) + of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt, + nnkFinally: + # recurse for every child: + result = copyNimNode(n) + for x in n: + result.add tcall2(x, tmpContext) + of nnkCaseStmt: + # recurse for children, but don't add call for case ident + result = copyNimNode(n) + result.add n[0] + for i in 1 ..< n.len: + result.add tcall2(n[i], tmpContext) + of nnkProcDef: + let name = getName n[0] + if name.startsWith"on": + # turn it into an anon proc: + let anon = copyNimTree(n) + anon[0] = newEmptyNode() + if tmpContext == nil: + error "no Element to attach the event handler to" + else: + result = newCall(evHandler(), tmpContext, + newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi")) + else: + result = n + of nnkVarSection, nnkLetSection, nnkConstSection: + result = n + of nnkCallKinds - {nnkInfix}: + let op = getName(n[0]) + if isComponent(op) == ComponentKind.Tag: + let tmp = genSym(nskLet, "tmp") + let call = newCall(bindSym"tree", newDotExpr(bindSym"Tag", n[0])) + result = newTree( + if tmpContext == nil: nnkStmtListExpr else: nnkStmtList, + newLetStmt(tmp, call)) + for i in 1 ..< n.len: + # named parameters are transformed into attributes or events: + let x = n[i] + if x.kind == nnkExprEqExpr: + let key = getName x[0] + if key.startsWith("on"): + result.add newCall(evHandler(), + tmp, newDotExpr(bindSym"EventKind", x[0]), x[1]) + elif key in SpecialAttrs: + result.add newDotAsgn(tmp, key, x[1]) + elif eqIdent(key, "style"): + result.add newCall(bindSym"applyStyles", tmp, toKstring x[1]) + elif eqIdent(key, "setFocus"): + result.add newCall(bindSym"focus", tmp) + else: + result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) + elif eqIdent(x, "setFocus"): + result.add newCall(bindSym"focus", tmp) + else: + result.add tcall2(x, tmp) + if tmpContext == nil: + result.add tmp + else: + result.add newCall(bindSym"add", tmpContext, tmp) + elif tmpContext != nil and op notin StmtContext: + var hasEventHandlers = false + for i in 1..= 0.16.1" +requires "nim >= 0.18.0" +requires "ws" +requires "dotenv" +skipDirs = @["examples", "experiments", "tests"] -skipDirs = @["examples/todoapp"] \ No newline at end of file +bin = @["karax/tools/karun"] +installExt = @["nim"] diff --git a/karax/autocomplete.nim b/karax/autocomplete.nim new file mode 100644 index 0000000..210ed4a --- /dev/null +++ b/karax/autocomplete.nim @@ -0,0 +1,160 @@ + +import karax, karaxdsl, vdom, kdom, jstrutils + +type + AutocompleteState* = ref object + choices, candidates: seq[cstring] + selected, maxMatches: int + showCandidates, controlPressed: bool + +proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState = + ## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum + ## number of elements to show. + AutocompleteState(choices: choices, candidates: @[], + selected: -1, maxMatches: maxMatches, showCandidates: false, + controlPressed: false) + +proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode = + var inp: VNode + + proc commit(ev: Event) = + s.showCandidates = false + onselection(inp.dom.value) + when false: + if inp.dom != nil: + echo "setting to A ", inp.dom.value.isNil + result.text = inp.dom.value + else: + echo "setting to B ", inp.text.isNil + result.text = inp.text + for e in result.events: + if e[0] == EventKind.onchange: + e[1](ev, result) + + proc onkeyuplater(ev: Event; n: VNode) = + if not s.controlPressed: + let v = n.value + s.candidates.setLen 0 + for c in s.choices: + if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c) + + proc onkeydown(ev: Event; n: VNode) = + const + LEFT = 37 + UP = 38 + RIGHT = 39 + DOWN = 40 + TAB = 9 + ESC = 27 + ENTER = 13 + # UP: Move focus to the previous item. If on first item, move focus to the input. + # If on the input, move focus to last item. + # DOWN: Move focus to the next item. If on last item, move focus to the input. + # If on the input, move focus to the first item. + # ESCAPE: Close the menu. + # ENTER: Select the currently focused item and close the menu. + # TAB: Select the currently focused item, close the menu, and + # move focus to the next focusable element + s.controlPressed = false + case ev.keyCode + of UP: + s.controlPressed = true + s.showCandidates = true + if s.selected > 0: + dec s.selected + n.setInputText s.candidates[s.selected] + of DOWN: + s.controlPressed = true + s.showCandidates = true + if s.selected < s.candidates.len - 1: + inc s.selected + n.setInputText s.candidates[s.selected] + of ESC: + s.showCandidates = false + s.controlPressed = true + of ENTER: + s.controlPressed = true +# inp.setInputText s.choices[i] + commit(ev) + else: + discard + + proc window(s: AutocompleteState): (int, int) = + var first, last: int + if s.selected >= 0: + first = s.selected - (s.maxMatches div 2) + last = s.selected + (s.maxMatches div 2) + if first < 0: first = 0 + # too few because we had to trim first? + if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1 + else: + first = 0 + last = s.maxMatches - 1 + if last > high(s.candidates): last = high(s.candidates) + # still too few because we're at the end? + if (last - first + 1) < s.maxMatches: + first = last - s.maxMatches + 1 + if first < 0: first = 0 + + result = (first, last) + + inp = buildHtml: + input(onkeyuplater = onkeyuplater, + onkeydown = onkeydown, + # onblur = proc (ev: Event; n: VNode) = commit(ev), + onfocus = proc (ev: Event; n: VNode) = + onkeyuplater(ev, n) + s.showCandidates = true) + + proc select(i: int): proc(ev: Event; n: VNode) = + result = proc(ev: Event; n: VNode) = + s.selected = i + s.showCandidates = false + inp.setInputText s.choices[i] + commit(ev) + + result = buildHtml(table): + tr: + td: + inp + if s.showCandidates: + let (first, last) = window(s) + for i in first..last: + tr: + td(onclick = select(i), class = cstring"button " & + (if i == s.selected: cstring"is-primary" else: "")): + text s.candidates[i] + +when isMainModule: + const suggestions = @[cstring"ActionScript", + "AppleScript", + "Asp", + "BASIC", + "C", + "C++", + "Clojure", + "COBOL", + "Erlang", + "Fortran", + "Groovy", + "Haskell", + "Java", + "JavaScript", + "Lisp", + "Nim", + "Perl", + "PHP", + "Python", + "Ruby", + "Scala", + "Scheme"] + + var s = newAutocomplete(suggestions) + + proc main(): VNode = + result = buildHtml(tdiv): + autocomplete(s, proc (s: cstring) = echo "now ", s)#: + # proc onchange(ev: Event; n: VNode) = + # echo "now selected ", n.kind + + setRenderer(main) diff --git a/karax/compact.nim b/karax/compact.nim new file mode 100644 index 0000000..4e5dd6d --- /dev/null +++ b/karax/compact.nim @@ -0,0 +1,114 @@ +## Components in Karax are built by the ``.component`` macro annotation. + +when defined(js): + import jdict, kdom + +import macros, vdom, tables, strutils, kbase + +when defined(js): + var + vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]() +else: + var + vcomponents* = newTable[kstring, proc(args: seq[VNode]): VNode]() + +type + ComponentKind* {.pure.} = enum + None, + Tag, + VNode + +var + allcomponents {.compileTime.} = initTable[string, ComponentKind]() + +proc isComponent*(x: string): ComponentKind {.compileTime.} = + allcomponents.getOrDefault(x) + +proc addTags() {.compileTime.} = + let x = (bindSym"VNodeKind").getTypeImpl + expectKind(x, nnkEnumTy) + for i in ord(VNodeKind.html)..ord(VNodeKind.high): + # +1 because of empty node at the start of the enum AST: + let tag = $x[i+1] + allcomponents[tag] = ComponentKind.Tag + +static: + addTags() + +proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} = + #let t = symbolicType.getTypeImpl + let t = repr(symbolicType) + case t + of "cstring": + result = quote do: + args[`index`].text + of "int", "VKey": + result = quote do: + args[`index`].intValue + of "bool": + result = quote do: + args[`index`].intValue != 0 + elif t.endsWith"Kind": + result = quote do: + `symbolicType`(args[`index`].intValue) + else: + # just pass it along, maybe there is some conversion for it: + result = quote do: + args[`index`] + +proc newname*(n: NimNode): NimNode = + if n.kind == nnkPostfix: + n[1] = newname(n[1]) + result = n + elif n.kind == nnkSym: + result = ident(n.strVal) + else: + result = n + +when defined(js): + macro compact*(prc: untyped): untyped = + ## A 'compact' tree generation proc is one that only depends on its + ## inputs and should be stored as a compact virtual DOM tree and + ## only expanded on demand (when its inputs changed). + var n = prc.copyNimNode + for i in 0..6: n.add prc[i].copyNimTree + expectKind(n, nnkProcDef) + if n[0].kind == nnkEmpty: + error("please pass a non anonymous proc", n[0]) + let name = n[0] + let params = params(n) + let rettype = repr params[0] + var isvirtual = ComponentKind.None + if rettype == "VNode": + isvirtual = ComponentKind.VNode + else: + error "component must return VNode", params[0] + let realName = if name.kind == nnkPostfix: name[1] else: name + let nn = $realName + n[0] = ident("inner" & nn) + var unpackCall = newCall(n[0]) + var counter = 0 + for i in 1 ..< params.len: + let param = params[i] + let L = param.len + let typ = param[L-2] + for j in 0 .. L-3: + unpackCall.add unpack(typ, counter) + inc counter + + template vwrapper(pname, unpackCall) {.dirty.} = + proc pname(args: seq[VNode]): VNode = + unpackCall + + template vregister(key, val) = + bind jdict.`[]=` + `[]=`(vcomponents, kstring(key), val) + + result = newTree(nnkStmtList, n) + + if isvirtual == ComponentKind.VNode: + result.add getAst(vwrapper(newname name, unpackCall)) + result.add getAst(vregister(newLit(nn), realName)) + allcomponents[nn] = isvirtual + when defined(debugKaraxDsl): + echo repr result diff --git a/karax/errors.nim b/karax/errors.nim new file mode 100644 index 0000000..aedd103 --- /dev/null +++ b/karax/errors.nim @@ -0,0 +1,32 @@ +## Error handling logic for form input validation. + +import karax, jdict + +var + gerrorMsgs = newJDict[cstring, cstring]() + gerrorCounter = 0 + +proc hasErrors*(): bool = gerrorCounter != 0 + +proc hasError*(field: cstring): bool = gerrorMsgs.contains(field) and len(gerrorMsgs[field]) > 0 + +proc disableOnError*(): cstring = toDisabled(hasErrors()) + +proc getError*(field: cstring): cstring = + if not gerrorMsgs.contains(field): + result = "" + else: + result = gerrorMsgs[field] + +proc setError*(field, msg: cstring) = + let previous = getError(field) + if len(msg) == 0: + if len(previous) != 0: dec(gerrorCounter) + else: + if len(previous) == 0: inc(gerrorCounter) + gerrorMsgs[field] = msg + +proc clearErrors*() = + let m = gerrorMsgs + for k in m.keys: + setError(k, "") diff --git a/src/i18n.nim b/karax/i18n.nim similarity index 100% rename from src/i18n.nim rename to karax/i18n.nim diff --git a/src/jdict.nim b/karax/jdict.nim similarity index 86% rename from src/jdict.nim rename to karax/jdict.nim index 6e2aa9f..c9e2224 100644 --- a/src/jdict.nim +++ b/karax/jdict.nim @@ -11,6 +11,12 @@ proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty( proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".} +iterator keys*[K, V](d: JDict[K, V]): K = + var kkk: K + {.emit: ["for (", kkk, " in ", d, ") {"].} + yield kkk + {.emit: ["}"].} + type JSeq*[T] = ref object diff --git a/karax/jjson.nim b/karax/jjson.nim new file mode 100644 index 0000000..ad6106b --- /dev/null +++ b/karax/jjson.nim @@ -0,0 +1,67 @@ +## This module implements some small zero-overhead 'JsonNode' type +## and helper that maps directly to JavaScript objects. + +type + JsonNode* {.importc.} = ref object + +proc `[]`*(obj: JsonNode; fieldname: cstring): JsonNode {.importcpp: "#[#]".} +proc `[]`*(obj: JsonNode; index: int): JsonNode {.importcpp: "#[#]".} +proc `[]=`*[T](obj: JsonNode; fieldname: cstring; value: T) + {.importcpp: "#[#] = #".} +proc length(x: JsonNode): int {.importcpp: "#.length".} +proc len*(x: JsonNode): int = (if x.isNil: 0 else: x.length) + +proc newJsonNode*(fields: varargs[(cstring, JsonNode)]): JsonNode = + result = JsonNode() + for f in fields: + result[f[0]] = f[1] + +proc newJObject*(): JsonNode = + result = JsonNode() + +proc newJArray*(elements: varargs[JsonNode]): JsonNode {.importcpp: "#".} + +proc newJNull*(): JsonNode = nil + +template `%`*(x: typed): JsonNode = cast[JsonNode](x) +template `%`*(x: string): JsonNode = cast[JsonNode](cstring x) + +proc getNum*(x: JsonNode): int {.importcpp: "#".} + +proc getInt*(x: JsonNode): int {.importcpp: "#".} +proc getStr*(x: JsonNode): cstring {.importcpp: "#".} +proc getFNum*(x: JsonNode): cstring {.importcpp: "#".} + +iterator items*(x: JsonNode): JsonNode = + for i in 0..=0)", nodecl.} + +proc containsIgnoreCase*(a, b: cstring): bool {. + importcpp: """(#.search(new RegExp(#.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$$\|]/g, "\\$$&") , "i"))>=0)""", nodecl.} + proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.} proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.} diff --git a/karax/jwebsockets.nim b/karax/jwebsockets.nim new file mode 100644 index 0000000..7c94da7 --- /dev/null +++ b/karax/jwebsockets.nim @@ -0,0 +1,17 @@ +## Websockets support for the JSON backend. + +type + MessageEvent* {.importc.} = ref object + data*: cstring + + WebSocket* {.importc.} = ref object + onmessage*: proc (e: MessageEvent) + onopen*: proc (e: MessageEvent) + +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 new file mode 100644 index 0000000..4235b50 --- /dev/null +++ b/karax/kajax.nim @@ -0,0 +1,132 @@ +## Karax -- Single page applications for Nim. + +## This module implements support for `ajax`:idx: socket +## 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; + cont: proc (httpStatus: int; response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi, + useBinary: bool = false, + blob: Blob = nil) = + proc contWrapper(httpStatus: int; response: cstring) = + cont(httpStatus, response) + if doRedraw: redraw(kxi) + + var this {.importc: "this".}: ThisObj + + let ajax = newRequest() + ajax.open(meth, url, true) + for a, b in items(headers): + ajax.setRequestHeader(a, b) + ajax.statechange proc() = + if this.readyState == 4: + if this.status == 200: + contWrapper(this.status, this.responseText) + else: + contWrapper(this.status, this.responseText) + if useBinary: + ajax.send(blob) + else: + ajax.send(data) + +proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; + data: cstring; + cont: proc (httpStatus: int, response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi) = + ajax("POST", url, headers, data, cont, doRedraw, kxi) + +proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; + data: Blob; + cont: proc (httpStatus: int, response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi) = + ajax("POST", url, headers, "", cont, doRedraw, kxi, true, data) + +proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)]; + cont: proc (httpStatus: int, response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi) = + ajax("GET", url, headers, nil, cont, doRedraw, kxi) + +proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)]; + data: cstring; + cont: proc (httpStatus: int, response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi) = + ajax("PUT", url, headers, data, cont, doRedraw, kxi) + +proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)]; + cont: proc (httpStatus: int, response: cstring); + doRedraw: bool = true, + kxi: KaraxInstance = kxi) = + ajax("DELETE", url, headers, nil, cont, doRedraw, kxi) + + +proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".} +proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".} + diff --git a/src/karax.nim b/karax/karax.nim similarity index 59% rename from src/karax.nim rename to karax/karax.nim index aef2730..2246676 100644 --- a/src/karax.nim +++ b/karax/karax.nim @@ -2,18 +2,21 @@ import kdom, vdom, jstrutils, compact, jdict, vstyles -export kdom.Event +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. + ## The preferred way of debugging karax applications. type PatchKind = enum - pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach + pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame Patch = object k: PatchKind parent, current: Node - n: VNode + newNode, oldNode: VNode PatchV = object parent, newChild: VNode pos: int @@ -22,15 +25,19 @@ type parent, current: Node type + RouterData* = ref object ## information that is passed to the 'renderer' callback + hashPart*: cstring ## the hash part of the URL for routing. + KaraxInstance* = ref object ## underlying karax instance. Usually you don't have ## know about this. - rootId: cstring not nil - renderer: proc (): VNode {.closure.} + rootId: cstring + renderer: proc (data: RouterData): VNode {.closure.} currentTree: VNode - postRenderCallback: proc () + 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] @@ -38,8 +45,10 @@ type runCount: int components: seq[ComponentPair] surpressRedraws*: bool + byId: JDict[cstring, VNode] when defined(stats): recursion: int + orphans: JDict[cstring, bool] var @@ -69,6 +78,24 @@ template keyeventBody() = # Do not call redraw() here! That is already done # by ``karax.addEventHandler``. +proc karaxEvents(d: Node): JSeq[(cstring, NativeEventHandler)] {.importcpp: "#.karaxEvents".} +proc `karaxEvents=`(d: Node; value: JSeq[(cstring, NativeEventHandler)]) {.importcpp: "#.karaxEvents = #".} + +proc addEventShell(d: Node; name: cstring; h: NativeEventHandler) = + # The DOM is such a pathetic piece of junk that it doesn't + # offer 'removeAllEventHandlers()'. Hence we store the event + # handler twice in 'd' so that we can emulate this properly. + # This is required to fix bug #139. + d.addEventListener(name, h) + if d.karaxEvents == nil: + d.karaxEvents = newJSeq[(cstring, NativeEventHandler)]() + d.karaxEvents.add((name, h)) + +proc removeAllEventHandlers(d: Node) = + if d.karaxEvents != nil: + for i in 0..>" & depth) - elif n.kind == VNodeKind.text: - kout depth, n.text - for i in 0 ..< n.len: - printV(n[i], depth & " ") +when false: + proc printV(n: VNode; depth: cstring = "") = + kout depth, cstring($n.kind), cstring"key ", n.index + #for k, v in pairs(n.style): + # kout depth, "style: ", k, v + if n.kind == VNodeKind.component: + let nn = VComponent(n) + if nn.expanded != nil: printV(nn.expanded, ">>" & depth) + elif n.kind == VNodeKind.text: + kout depth, n.text + for i in 0 ..< n.len: + printV(n[i], depth & " ") proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node; - na: VNode) = + na, oldNode: VNode) = let L = kxi.patchLen if L >= kxi.patches.len: # allocate more space: - kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, n: na)) + kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, + newNode: na, oldNode: oldNode)) else: kxi.patches[L].k = ka kxi.patches[L].parent = parenta kxi.patches[L].current = currenta - kxi.patches[L].n = na + kxi.patches[L].newNode = na + kxi.patches[L].oldNode = oldNode inc kxi.patchLen proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = @@ -311,26 +382,41 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = kxi.patchesV[L].pos = pos inc kxi.patchLenV +proc moveDom(dest, src: VNode) = + dest.dom = src.dom + src.dom = nil + if dest.id != nil: + kxi.byId[dest.id] = dest + assert dest.len == src.len + for i in 0.. We cannot ever use the old VDOM, we have to use the new virtual +# DOM. For identical nodes we need to take over the .dom field from the old +# node since we don't recompute them. This must be done recursively. In +# vnodeToDom we have to check whether the 'dom' field was already set. If so, +# There is nothing to do. +# "Similar" nodes can have the opposite effect; consider +# +# AAAABAAAA +# AAAACDAAAA +# +# In this example B did change to C and 'D' is new. However, replacing B by +# CD is fine. +# + + +proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) = when defined(stats): if kxi.recursion > 100: - echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode) + echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false) if oldNode.kind == VNodeKind.text: echo oldNode.text - #return - #doAssert false, "overflow!" inc kxi.recursion - result = eq(newNode, oldNode) + let result = eq(newNode, oldNode, false) case result of componentsIdentical: kxi.components.add ComponentPair(oldNode: VComponent(oldNode), @@ -370,90 +478,54 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) - when false: - if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: - if oldNode.text != newNode.text: - oldNode.text = newNode.text - oldNode.dom.value = newNode.text let newLength = newNode.len let oldLength = oldNode.len - if newLength == 0 and oldLength == 0: return result + if newLength == 0 and oldLength == 0: return let minLength = min(newLength, oldLength) assert oldNode.kind == newNode.kind var commonPrefix = 0 - let isSpecial = oldNode.kind == VNodeKind.component or - oldNode.kind == VNodeKind.vthunk or - oldNode.kind == VNodeKind.dthunk - template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) = - let oldLen = kxi.patchLen - let oldLenV = kxi.patchLenV - assert i < a.len - assert j < b.len - let r = if isSpecial: - diff(a[i], b[j], parent, current, kxi) - else: - diff(a[i], b[j], current, current.childNodes[j], kxi) - case r - of identical, componentsIdentical, similar: - a[i] = b[j] - action - of usenewNode: - kxi.addPatchV(b, j, a[i]) - action - # unfortunately, we need to propagate the changes upwards: - result = useNewNode - of different: - # undo what 'diff' would have done: - kxi.patchLen = oldLen - kxi.patchLenV = oldLenV - if result != different: result = r - break # compute common prefix: while commonPrefix < minLength: - eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): + if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical: + kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix]) inc commonPrefix + else: + break # compute common suffix: var oldPos = oldLength - 1 var newPos = newLength - 1 while oldPos >= commonPrefix and newPos >= commonPrefix: - eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): + if eq(newNode[newPos], oldNode[oldPos], true) == identical: + kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos]) dec oldPos dec newPos + else: + break let pos = min(oldPos, newPos) + 1 # now the different children are in commonPrefix .. pos - 1: for i in commonPrefix..pos-1: - let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) - if r == usenewNode: - #oldNode[i] = newNode[i] - kxi.addPatchV(oldNode, i, newNode[i]) - elif r != different: - newNode[i] = oldNode[i] - #else: - # result = usenewNode + diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) if oldPos + 1 == oldLength: for i in pos..newPos: - kxi.addPatch(pkAppend, current, nil, newNode[i]) - result = usenewNode + kxi.addPatch(pkAppend, current, nil, newNode[i], nil) else: let before = current.childNodes[oldPos + 1] for i in pos..newPos: - kxi.addPatch(pkInsertBefore, current, before, newNode[i]) - result = usenewNode + kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil) # XXX call 'attach' here? for i in pos..oldPos: detach(oldNode[i]) #doAssert i < current.childNodes.len - kxi.addPatch(pkRemove, current, current.childNodes[i], nil) - result = usenewNode + kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil) of different: detach(oldNode) - kxi.addPatch(pkReplace, parent, current, newNode) + kxi.addPatch(pkReplace, parent, current, newNode, nil) of usenewNode: doAssert(false, "eq returned usenewNode") when defined(stats): dec kxi.recursion @@ -470,11 +542,7 @@ proc applyComponents(kxi: KaraxInstance) = let newNode = kxi.components[i].newNode when defined(karaxDebug): echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil - if x.realDomImpl != nil: - let current = kxi.components[i].current - let parent = kxi.components[i].parent - kxi.addPatch(pkReplace, parent, current, x) - elif x.changedImpl != nil and x.changedImpl(x, newNode): + if x.changedImpl != nil and x.changedImpl(x, newNode): when defined(karaxDebug): echo "Component ", newNode.text, " did change" let current = kxi.components[i].current @@ -487,24 +555,25 @@ proc applyComponents(kxi: KaraxInstance) = x.renderedVersion = x.version if oldExpanded == nil: detach(x) - kxi.addPatch(pkReplace, parent, current, x.expanded) + kxi.addPatch(pkReplace, parent, current, x.expanded, nil) when defined(karaxDebug): echo "Component ", newNode.text, ": old expansion didn't exist" else: - let res = diff(x.expanded, oldExpanded, parent, current, kxi) - if res == usenewNode: - when defined(karaxDebug): - echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)" - discard "diff created a patchset for us, so this is fine" - elif res != different: - when defined(karaxDebug): - echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever" - x.expanded = oldExpanded - assert oldExpanded.dom != nil, "old expanded.dom is nil" - else: - when defined(karaxDebug): - echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)" - assert x.expanded.dom != nil, "expanded.dom is nil" + diff(x.expanded, oldExpanded, parent, current, kxi) + when false: + if res == usenewNode: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)" + discard "diff created a patchset for us, so this is fine" + elif res != different: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever" + x.expanded = oldExpanded + assert oldExpanded.dom != nil, "old expanded.dom is nil" + else: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)" + assert x.expanded.dom != nil, "expanded.dom is nil" inc i setLen(kxi.components, 0) @@ -519,7 +588,7 @@ when defined(stats): proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) = detach(parent[position]) let current = parent.dom - kxi.addPatch(pkRemove, current, current.childNodes[position], nil) + kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil) parent.delete(position) applyPatch(kxi) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) @@ -527,11 +596,11 @@ proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) = proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) = let current = parent.dom if position >= parent.len: - kxi.addPatch(pkAppend, current, nil, kid) + kxi.addPatch(pkAppend, current, nil, kid, nil) parent.add(kid) else: let before = current.childNodes[position] - kxi.addPatch(pkInsertBefore, current, before, kid) + kxi.addPatch(pkInsertBefore, current, before, kid, nil) parent.insert(kid, position) applyPatch(kxi) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) @@ -539,7 +608,7 @@ proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) = proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = let olddom = oldNode.dom doAssert olddom != nil - discard diff(newNode, oldNode, nil, olddom, kxi) + diff(newNode, oldNode, nil, olddom, kxi) # this is a bit nasty: Since we cannot patch the 'parent' of # the current VNode (because we don't store it at all!), we # need to override the fields individually: @@ -550,20 +619,44 @@ proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = kxi.currentTree = newNode doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) +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 - let newtree = kxi.renderer() + kxi.renderId = 0 + + if kxi.rendering: + # there is a render already running, delay 1 frame + kxi.renderId = reqFrame(proc () = kxi.dodraw) + return + + kxi.rendering = true + + let rdata = RouterData(hashPart: hashPart) + let newtree = kxi.renderer(rdata) inc kxi.runCount newtree.id = kxi.rootId kxi.toFocus = nil if kxi.currentTree == nil: - let asdom = vnodeToDom(newtree, kxi) + let asdom = toDom(newtree, useAttachedNode = true, kxi) replaceById(kxi.rootId, asdom) else: doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) let olddom = document.getElementById(kxi.rootId) - discard diff(newtree, kxi.currentTree, nil, olddom, kxi) - #kout cstring"patch len ", patches.len + diff(newtree, kxi.currentTree, nil, olddom, kxi) when defined(profileKarax): echo "<<<<<<<<<<<<<<" echa reasons @@ -577,20 +670,17 @@ proc dodraw(kxi: KaraxInstance) = doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: - kxi.postRenderCallback() + kxi.postRenderCallback(rdata) # now that it's part of the DOM, give it the focus: if kxi.toFocus != nil: kxi.toFocus.focus() - kxi.renderId = 0 + kxi.rendering = false when defined(stats): kxi.recursion = 0 var total = 0 echo "depth ", depth(kxi.currentTree, total), " total ", total -proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".} -proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".} - proc redraw*(kxi: KaraxInstance = kxi) = # we buffer redraw requests: when false: @@ -608,26 +698,49 @@ proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi) proc init(ev: Event) = kxi.renderId = reqFrame(proc () = kxi.dodraw) -proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", - clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = +proc setRenderer*(renderer: proc (data: RouterData): VNode, + root: cstring = "ROOT", + clientPostRenderCallback: + proc (data: RouterData) = nil): KaraxInstance {. + discardable.} = ## Setup Karax. Usually the return value can be ignored. + if document.getElementById(root).isNil: + let msg = "Could not find a
with id=" & root & + ". Karax needs it as its rendering target." + raise newException(Exception, $msg) + result = KaraxInstance(rootId: root, renderer: renderer, postRenderCallback: clientPostRenderCallback, patches: newSeq[Patch](60), patchesV: newSeq[PatchV](30), - components: @[]) + components: @[], + surpressRedraws: false, + byId: newJDict[cstring, VNode](), + orphans: newJDict[cstring, bool]()) kxi = result window.onload = init + onhashChange = proc() = redraw() -proc setInitializer*(renderer: proc (): VNode, root: cstring = "ROOT", - clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = +proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", + clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = + ## Setup Karax. Usually the return value can be ignored. + proc wrapRenderer(data: RouterData): VNode = result = renderer() + proc wrapPostRender(data: RouterData) = + if clientPostRenderCallback != nil: clientPostRenderCallback() + setRenderer(wrapRenderer, root, wrapPostRender) + +proc setInitializer*(renderer: proc (data: RouterData): VNode, root: cstring = "ROOT", + clientPostRenderCallback: + proc (data: RouterData) = nil): KaraxInstance {.discardable.} = ## Setup Karax. Usually the return value can be ignored. result = KaraxInstance(rootId: root, renderer: renderer, postRenderCallback: clientPostRenderCallback, patches: newSeq[Patch](60), patchesV: newSeq[PatchV](30), components: @[], - surpressRedraws: true) + surpressRedraws: true, + byId: newJDict[cstring, VNode](), + orphans: newJDict[cstring, bool]()) kxi = result window.onload = init @@ -655,16 +768,21 @@ proc addEventHandler*(n: VNode; k: EventKind; action: proc(); if not kxi.surpressRedraws: redraw(kxi) addEventListener(n, k, wrapper) -proc setOnHashChange*(action: proc (hashPart: cstring)) = - var onhashChange {.importc: "window.onhashchange".}: proc() - var hashPart {.importc: "window.location.hash".}: cstring +proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} = + ## Now deprecated, instead pass a callback to ``setRenderer`` that receives + ## a ``data: RouterData`` parameter. proc wrapper() = action(hashPart) redraw() onhashchange = wrapper -{.push stackTrace:off.} +proc setForeignNodeId*(id: cstring; kxi: KaraxInstance = kxi) = + ## Declares a node ID as "foreign". Foreign nodes are not + ## under Karax's control in the sense that Karax does not attempt + ## to perform structural checks on them. + kxi.orphans[id] = true +{.push stackTrace:off.} proc setupErrorHandler*() = ## Installs an error handler that transforms native JS unhandled ## exceptions into Nim based stack traces. If `useAlert` is false, @@ -674,7 +792,7 @@ proc setupErrorHandler*() = var onerror {.importc: "window.onerror", used.} = proc (msg, url: cstring, line, col: int, error: cstring): bool = var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring() - kout(x) + echo(x) return true # suppressErrorAlert {.pop.} @@ -682,9 +800,35 @@ proc prepend(parent, kid: Element) = parent.insertBefore(kid, parent.firstChild) proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) = - let body = document.getElementById("body") let s = document.createElement("script") s.setAttr "type", "text/javascript" s.setAttr "src", jsfilename - body.prepend(s) + document.body.prepend(s) redraw(kxi) + +proc runLater*(action: proc(); later = 400): Timeout {.discardable.} = + proc wrapper() = + action() + redraw() + result = setTimeout(wrapper, later) + +proc setInputText*(n: VNode; s: cstring) = + ## Sets the text of input elements. + 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/src/karaxdsl.nim b/karax/karaxdsl.nim similarity index 83% rename from src/karaxdsl.nim rename to karax/karaxdsl.nim index dd480e1..451632c 100644 --- a/src/karaxdsl.nim +++ b/karax/karaxdsl.nim @@ -1,25 +1,46 @@ -import macros, karax, vdom, compact +import macros, vdom, compact, kbase from strutils import startsWith, toLowerAscii +when defined(js): + import karax + const StmtContext = ["kout", "inc", "echo", "dec", "!"] SpecialAttrs = ["id", "class", "value", "index", "style"] proc getName(n: NimNode): string = case n.kind - of nnkIdent: - result = $n.ident + of nnkIdent, nnkSym: + result = $n of nnkAccQuoted: result = "" for i in 0..