├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.txt ├── docs ├── benchmark.png └── docs.rst ├── examples ├── ajax │ ├── ajax.nim │ └── styles.css ├── button.nim ├── buttonlambda.nim ├── carousel │ ├── carousel.html │ ├── carousel.nim │ └── carousel.nims ├── config.nims ├── hellostyle.nim ├── hellouniverse.nim ├── helloworld.nim ├── karaxtmulti │ ├── README.md │ ├── app1.nim │ ├── app2.nim │ └── index.html ├── login.nim ├── mediaplayer │ ├── mediaplayer.nim │ ├── playerapp.html │ ├── playerapp.nim │ └── playerapp.nims ├── scrollapp │ ├── scrollapp.html │ ├── scrollapp.nim │ └── scrollapp.nims ├── slider.nim ├── todoapp │ ├── style.css │ ├── todoapp.html │ ├── todoapp.nim │ └── todoapp.nims └── toychat.nim ├── experiments ├── ajaxtest.html ├── ajaxtest.nim ├── config.nims ├── dyn.nim ├── echartstest.html ├── echartstest.nim ├── menus.nim ├── nativenodes.nim ├── nextgen.nim ├── oldwidgets.nim ├── refs │ ├── demo.html │ ├── demo.nim │ └── demo.nims ├── scrollapp.html ├── scrollapp.nim ├── scrollapp_simple.nim ├── style.css └── trello │ ├── knete.nim │ ├── trello.nim │ └── widgets.nim ├── guide ├── Installation.md └── Introduction.md ├── karax.nimble ├── karax ├── autocomplete.nim ├── compact.nim ├── errors.nim ├── i18n.nim ├── jdict.nim ├── jjson.nim ├── jstrutils.nim ├── jwebsockets.nim ├── kajax.nim ├── karax.nim ├── karaxdsl.nim ├── kbase.nim ├── kdom.nim ├── kdom_impl.nim ├── languages.nim ├── localstorage.nim ├── lookuptables.nim ├── prelude.nim ├── reactive.nim ├── shash.nim ├── tools │ ├── karun.nim │ ├── karun.nims │ └── static_server.nim ├── vdom.nim ├── vstyles.nim └── xdom.nim ├── readme.md └── tests ├── blur.nim ├── compiler_tests.nim ├── components.nim ├── diffDomTests.html ├── diffDomTests.nim ├── difftest.nim ├── domEventTests.css ├── domEventTests.html ├── domEventTests.nim ├── jquery-3.2.1.js ├── lists.nim ├── nativehtmlgen.nim ├── nim.cfg ├── scope.nim ├── tester.nim └── xmlNodeConversionTests.nim /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | container: 23 | image: nimlang/nim 24 | options: --user 1001 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@v4 29 | # Runs a single command using the runners shell 30 | - name: Tests 31 | run: | 32 | nimble develop -y 33 | nim c -r tests/tester.nim 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | /tests/tester 3 | karax/tools/karun 4 | *.code-workspace 5 | *.exe 6 | app.js 7 | app.html 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Xored Software, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karaxnim/karax/a1eaaa87af756c21db4524c38cad4a004218f740/docs/benchmark.png -------------------------------------------------------------------------------- /docs/docs.rst: -------------------------------------------------------------------------------- 1 | 2 | Virtual DOM 3 | =========== 4 | 5 | The virtual dom is in the ``vdom`` module. 6 | 7 | The ``VNodeKind`` enum describes every tag that the HTML 5 spec includes 8 | and some extensions that allow for an efficient component system. 9 | 10 | These extensions are: 11 | 12 | ``VNodeKind.int`` 13 | The node has a single integer field. 14 | 15 | ``VNodeKind.bool`` 16 | The node has a single boolean field. 17 | 18 | ``VNodeKind.vthunk`` 19 | The node is a `virtual thunk`:idx:. This means there is a 20 | function attached to it that produces the ``VNode`` structure 21 | on demand. 22 | 23 | ``VNodeKind.dthunk`` 24 | The node is a `DOM thunk`:idx:. This means there is a 25 | function attached to it that produces the ``Node`` DOM structure 26 | on demand. 27 | 28 | For convenience of the resulting Nim DSL these tags have enum names 29 | that differ from their HTML equivalent: 30 | 31 | ================= ======================================================= 32 | Enum value HTML 33 | ================= ======================================================= 34 | ``tdiv`` ``div`` (changed because ``div`` is a keyword in Nim) 35 | ``italic`` ``i`` 36 | ``bold`` ``b`` 37 | ``strikethrough`` ``s`` 38 | ``underlined`` ``u`` 39 | ================= ======================================================= 40 | 41 | 42 | A virtual dom node also has a special field set called ``key``, an integer 43 | that can be used as a data model specific key/id. It can be accessed in event 44 | handlers to change the data model. See the todoapp for an example of how to 45 | use it. 46 | 47 | **Note**: This is not a hint for the DOM diff algorithm, multipe nodes can 48 | all have the same key value. 49 | 50 | 51 | Event system 52 | ============ 53 | 54 | Karax does not abstract over the event system the DOM offers much: The same 55 | ``Event`` structure is used. Every callback has the 56 | signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``. 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/ajax/ajax.nim: -------------------------------------------------------------------------------- 1 | 2 | ##[ 3 | An example that uses ajax to load the nim package list and display a simple searchable index 4 | ]## 5 | 6 | import karax/[karax, karaxdsl, vdom, jstrutils, kajax, jjson] 7 | 8 | type 9 | Package = object 10 | name, description, url: cstring 11 | 12 | Progress = enum 13 | Loading, 14 | Loaded 15 | 16 | AppState = object 17 | progress: Progress 18 | packages: seq[Package] 19 | searchText: cstring 20 | 21 | var state = AppState() 22 | 23 | proc init = 24 | # start a request to get the package list from github 25 | ajaxGet("https://raw.githubusercontent.com/nim-lang/packages/master/packages.json", @[], proc (status: int, response: cstring) = 26 | for json in parse(response): 27 | # only add entries that aren't aliases 28 | if not json.hasField("alias"): 29 | state.packages.add(Package( 30 | name: json["name"].getStr(), 31 | description: json["description"].getStr(), 32 | url: json["url"].getStr() 33 | )) 34 | state.progress = Loaded 35 | ) 36 | 37 | proc drawPackage(package: Package): VNode = 38 | buildHtml(tdiv(class="result")): 39 | h2: 40 | a(href=package.url): 41 | text package.name 42 | p: text package.description 43 | 44 | proc loadingView: VNode = 45 | buildHtml(tdiv): 46 | h1: text "Loading..." 47 | 48 | proc searchView: VNode = 49 | buildHtml(tdiv): 50 | h1: text "Search" 51 | input: 52 | proc onkeyup(e: Event, n: VNode) = 53 | state.searchText = n.value 54 | 55 | # we show up to 50 packages that match the search text. If the input is 56 | # empty, we just show the first 50 packages in the list 57 | var found = 0 58 | 59 | for package in state.packages: 60 | if state.searchText.len == 0 or package.name.contains(state.searchText) or package.description.contains(state.searchText): 61 | drawPackage(package) 62 | inc found 63 | if found > 50: 64 | break 65 | 66 | proc main: VNode = 67 | if state.progress == Loading: 68 | loadingView() 69 | else: 70 | searchView() 71 | 72 | setRenderer(main) 73 | init() 74 | -------------------------------------------------------------------------------- /examples/ajax/styles.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | max-width: 60em; 4 | margin: 0 auto; 5 | font-family: sans-serif; 6 | } 7 | 8 | input { 9 | font-size: 1.5em; 10 | padding: .4em .6em; 11 | } 12 | 13 | .result h2 { 14 | margin-top: 2em; 15 | } 16 | -------------------------------------------------------------------------------- /examples/button.nim: -------------------------------------------------------------------------------- 1 | include karax / prelude 2 | 3 | var lines: seq[kstring] = @[] 4 | 5 | proc createDom(): VNode = 6 | result = buildHtml(tdiv): 7 | button: 8 | text "Say hello!" 9 | proc onclick(ev: Event; n: VNode) = 10 | lines.add "Hello simulated universe" 11 | for x in lines: 12 | tdiv: 13 | text x 14 | 15 | setRenderer createDom 16 | -------------------------------------------------------------------------------- /examples/buttonlambda.nim: -------------------------------------------------------------------------------- 1 | include karax / prelude 2 | from sugar import `=>` 3 | 4 | var lines: seq[kstring] = @[] 5 | 6 | proc createDom(): VNode = 7 | result = buildHtml(tdiv): 8 | button(onclick = () => lines.add "Hello simulated universe"): 9 | text "Say hello!" 10 | for x in lines: 11 | tdiv: 12 | text x 13 | 14 | setRenderer createDom 15 | -------------------------------------------------------------------------------- /examples/carousel/carousel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Carousel app 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/carousel/carousel.nim: -------------------------------------------------------------------------------- 1 | ## This demo shows how you can develop your own stateful components with Karax. 2 | 3 | import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom 4 | 5 | type 6 | Carousel = ref object of VComponent 7 | counter: int 8 | cntdown: int 9 | timer: TimeOut 10 | list: seq[cstring] 11 | 12 | const ticksUntilChange = 5 13 | 14 | var 15 | images: seq[cstring] = @[cstring"a", "b", "c", "d"] 16 | 17 | var 18 | refA:Carousel 19 | refB:Carousel 20 | refC:Carousel 21 | refD:Carousel 22 | proc render(x: VComponent): VNode = 23 | let self = Carousel(x) 24 | 25 | proc docount() = 26 | dec self.cntdown 27 | if self.cntdown == 0: 28 | self.counter = (self.counter + 1) mod self.list.len 29 | self.cntdown = ticksUntilChange 30 | else: 31 | self.timer = setTimeout(docount, 30) 32 | markDirty(self) 33 | redraw() 34 | 35 | proc onclick(ev: Event; n: VNode) = 36 | if self.timer != nil: 37 | clearTimeout(self.timer) 38 | self.timer = setTimeout(docount, 30) 39 | 40 | let col = 41 | case self.counter 42 | of 0: cstring"#4d4d4d" 43 | of 1: cstring"#ff00ff" 44 | of 2: cstring"#00ffff" 45 | of 3: cstring"#ffff00" 46 | else: cstring"red" 47 | result = buildHtml(tdiv()): 48 | text self.list[self.counter] 49 | button(onclick = onclick): 50 | text "Next" 51 | tdiv(style = style(StyleAttr.color, col)): 52 | text "This changes its color." 53 | if self.cntdown != ticksUntilChange: 54 | text &self.cntdown 55 | 56 | proc carousel(nref:var Carousel): Carousel = 57 | if nref == nil: 58 | nref = newComponent(Carousel, render) 59 | nref.list = images 60 | nref.cntdown = ticksUntilChange 61 | return nref 62 | else: 63 | return nref 64 | 65 | proc createDom(): VNode = 66 | result = buildHtml(table): 67 | tr: 68 | td: 69 | carousel(nref=refA) 70 | td: 71 | carousel(nref=refB) 72 | tr: 73 | td: 74 | carousel(nref=refC) 75 | td: 76 | carousel(nref=refD) 77 | 78 | setRenderer createDom 79 | -------------------------------------------------------------------------------- /examples/carousel/carousel.nims: -------------------------------------------------------------------------------- 1 | --path: "../../karax" 2 | -------------------------------------------------------------------------------- /examples/config.nims: -------------------------------------------------------------------------------- 1 | --path: ".." 2 | -------------------------------------------------------------------------------- /examples/hellostyle.nim: -------------------------------------------------------------------------------- 1 | include karax / prelude 2 | import karax / vstyles 3 | 4 | proc createDom(): VNode = 5 | result = buildHtml(tdiv): 6 | tdiv(style = style(StyleAttr.color, "red".cstring)): 7 | text "red" 8 | tdiv(style = style(StyleAttr.color, "blue")): 9 | text "blue" 10 | # explicit `kstring` required for varargs overload 11 | tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): 12 | text "italic orange" 13 | # can use a string directly 14 | tdiv(style = "font-style: oblique; color: pink".toCss): 15 | text "oblique pink" 16 | 17 | setRenderer createDom 18 | -------------------------------------------------------------------------------- /examples/hellouniverse.nim: -------------------------------------------------------------------------------- 1 | include karax / prelude 2 | import random 3 | 4 | proc createDom(): VNode = 5 | result = buildHtml(tdiv): 6 | if rand(100) <= 50: 7 | text "Hello World!" 8 | else: 9 | text "Hello Universe" 10 | 11 | randomize() 12 | setRenderer createDom 13 | -------------------------------------------------------------------------------- /examples/helloworld.nim: -------------------------------------------------------------------------------- 1 | include karax / prelude 2 | 3 | proc createDom(): VNode = 4 | result = buildHtml(tdiv): 5 | text "Hello World!" 6 | 7 | setRenderer createDom 8 | -------------------------------------------------------------------------------- /examples/karaxtmulti/README.md: -------------------------------------------------------------------------------- 1 | Example to use multiple karax instances on the same page. 2 | 3 | build example: 4 | 5 | ``` 6 | nim js -d:kxiname="app1" .\app1.nim 7 | nim js -d:kxiname="app2" .\app2.nim 8 | ``` 9 | 10 | then start `index.html` in the browser. -------------------------------------------------------------------------------- /examples/karaxtmulti/app1.nim: -------------------------------------------------------------------------------- 1 | include karax / prelude 2 | 3 | var lines: seq[kstring] = @[] 4 | 5 | proc createDom(): VNode = 6 | result = buildHtml(tdiv): 7 | button: 8 | text "Say hello!" 9 | proc onclick(ev: Event; n: VNode) = 10 | lines.add "Hello simulated universe" 11 | for x in lines: 12 | tdiv: 13 | text x 14 | 15 | var a1 = setRenderer(createDom, "ROOT1") -------------------------------------------------------------------------------- /examples/karaxtmulti/app2.nim: -------------------------------------------------------------------------------- 1 | include karax / prelude 2 | import karax / kdom 3 | 4 | let id = "foo1" 5 | let id2 = "foo2" 6 | let valueInitial = "150" 7 | var value = valueInitial 8 | proc updateText() = 9 | value = $document.getElementById($id).value 10 | proc createDom(): VNode = 11 | result = buildHtml(tdiv): 12 | input(`type`="range", min = "10", max = "170", value = valueInitial, id = id): 13 | proc oninput(ev: Event; target: VNode) = updateText() 14 | tdiv(id=id2): 15 | text value 16 | 17 | 18 | var a2 = setRenderer(createDom, "ROOT2") -------------------------------------------------------------------------------- /examples/karaxtmulti/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Multiple Karax apps 6 | 7 | 8 | 9 | 10 | 15 | 16 |

Use multiple karax apps.

17 | 18 |
19 | 20 | 21 |
22 | 23 | 24 |
25 |

Some example html

26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
CompanyContactCountry
Alfreds FutterkisteMaria AndersGermany
Centro comercial MoctezumaFrancisco ChangMexico
43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/login.nim: -------------------------------------------------------------------------------- 1 | include karax / prelude 2 | from sugar import `=>` 3 | import karax / errors 4 | 5 | proc loginField(desc, field, class: kstring; 6 | validator: proc (field: kstring): proc ()): VNode = 7 | result = buildHtml(tdiv): 8 | label(`for` = field): 9 | text desc 10 | input(class = class, id = field, onkeyup = validator(field)) 11 | 12 | # some consts in order to prevent typos: 13 | const 14 | username = kstring"username" 15 | password = kstring"password" 16 | 17 | proc validateNotEmpty(field: kstring): proc () = 18 | result = proc () = 19 | let x = getVNodeById(field) 20 | if x.getInputText == "": 21 | errors.setError(field, field & " must not be empty") 22 | else: 23 | errors.setError(field, "") 24 | 25 | var loggedIn: bool 26 | 27 | proc loginDialog(): VNode = 28 | result = buildHtml(tdiv): 29 | if not loggedIn: 30 | loginField("Name: ", username, "input", validateNotEmpty) 31 | loginField("Password: ", password, "password", validateNotEmpty) 32 | button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()): 33 | text "Login" 34 | p: 35 | text errors.getError(username) 36 | p: 37 | text errors.getError(password) 38 | else: 39 | p: 40 | text "You are now logged in." 41 | 42 | setError username, username & " must not be empty" 43 | setError password, password & " must not be empty" 44 | 45 | when not declared(toychat): 46 | setRenderer loginDialog 47 | -------------------------------------------------------------------------------- /examples/mediaplayer/mediaplayer.nim: -------------------------------------------------------------------------------- 1 | 2 | import karax / [karax, karaxdsl, vdom, kdom, compact] 3 | 4 | const 5 | Play = 0 6 | Big = 1 7 | Small = 2 8 | Normal = 3 9 | 10 | proc paused(n: Node): bool {.importcpp: "#.paused".} 11 | proc play(n: Node) {.importcpp.} 12 | proc pause(n: Node) {.importcpp.} 13 | proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".} 14 | 15 | proc mplayer*(id, resource: cstring): VNode {.compact.} = 16 | proc handler(ev: Event; n: VNode) = 17 | let myVideo = document.getElementById(id) 18 | case n.index 19 | of Play: 20 | if myVideo.paused: 21 | myVideo.play() 22 | else: 23 | myVideo.pause() 24 | of Big: myVideo.width = 560 25 | of Small: myVideo.width = 320 26 | of Normal: myVideo.width = 420 27 | else: discard 28 | 29 | result = buildHtml(tdiv): 30 | button(onclick=handler, index=Play): 31 | text "Play/Pause" 32 | button(onclick=handler, index=Big): 33 | text "Big" 34 | button(onclick=handler, index=Small): 35 | text "Small" 36 | button(onclick=handler, index=Normal): 37 | text "Normal" 38 | br() 39 | br() 40 | video(id=id, width="420"): 41 | source(src=resource, `type`="video/mp4"): 42 | text "Your browser does not support HTML5 video." 43 | -------------------------------------------------------------------------------- /examples/mediaplayer/playerapp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Player app 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/mediaplayer/playerapp.nim: -------------------------------------------------------------------------------- 1 | ## Example app that shows how to write and embed a custom component. 2 | 3 | include karax/prelude 4 | import mediaplayer 5 | 6 | const url = "https://www.w3schools.com/html/mov_bbb.mp4" 7 | 8 | proc createDom(): VNode = 9 | result = buildHtml(table): 10 | tr: 11 | td: 12 | mplayer("vid1", url) 13 | td: 14 | mplayer("vid2", url) 15 | tr: 16 | td: 17 | mplayer("vid3", url) 18 | td: 19 | mplayer("vid4", url) 20 | 21 | setRenderer createDom 22 | -------------------------------------------------------------------------------- /examples/mediaplayer/playerapp.nims: -------------------------------------------------------------------------------- 1 | --path: "../.." 2 | -------------------------------------------------------------------------------- /examples/scrollapp/scrollapp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo app 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/scrollapp/scrollapp.nim: -------------------------------------------------------------------------------- 1 | ## Example that shows how to accomplish an "infinitely scrolling" app. 2 | 3 | include karax/prelude 4 | import karax / [jstrutils, kdom, vstyles] 5 | 6 | var entries: seq[cstring] = @[] 7 | for i in 1..500: 8 | entries.add(cstring("Entry ") & &i) 9 | 10 | proc scrollEvent(ev: Event; n: VNode) = 11 | let d = n.dom 12 | if d != nil and inViewport(d.lastChild): 13 | # "load" more data: 14 | for i in 1..50: 15 | entries.add(cstring("Loaded Entry ") & &i) 16 | 17 | proc createDom(): VNode = 18 | result = buildHtml(): 19 | tdiv(onscroll=scrollEvent, style=style( 20 | (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))): 21 | for x in entries: 22 | tdiv: 23 | text x 24 | 25 | setRenderer createDom 26 | -------------------------------------------------------------------------------- /examples/scrollapp/scrollapp.nims: -------------------------------------------------------------------------------- 1 | --path: "../.." 2 | -------------------------------------------------------------------------------- /examples/slider.nim: -------------------------------------------------------------------------------- 1 | include karax / prelude 2 | import karax / kdom 3 | 4 | let id = "foo1" 5 | let id2 = "foo2" 6 | let valueInitial = "150" 7 | var value = valueInitial 8 | proc updateText() = 9 | value = $document.getElementById($id).value 10 | proc createDom(): VNode = 11 | result = buildHtml(tdiv): 12 | input(`type`="range", min = "10", max = "170", value = valueInitial, id = id): 13 | proc oninput(ev: Event; target: VNode) = updateText() 14 | tdiv(id=id2): 15 | text value 16 | 17 | 18 | setRenderer createDom, "ROOT" -------------------------------------------------------------------------------- /examples/todoapp/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .todomvc-wrapper { 8 | visibility: visible !important; 9 | } 10 | 11 | button { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | background: none; 16 | font-size: 100%; 17 | vertical-align: baseline; 18 | font-family: inherit; 19 | font-weight: inherit; 20 | color: inherit; 21 | -webkit-appearance: none; 22 | appearance: none; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-font-smoothing: antialiased; 25 | font-smoothing: antialiased; 26 | } 27 | 28 | body { 29 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 30 | line-height: 1.4em; 31 | background: #f5f5f5; 32 | color: #4d4d4d; 33 | min-width: 230px; 34 | max-width: 550px; 35 | margin: 0 auto; 36 | -webkit-font-smoothing: antialiased; 37 | -moz-font-smoothing: antialiased; 38 | font-smoothing: antialiased; 39 | font-weight: 300; 40 | } 41 | 42 | button, 43 | input[type="checkbox"] { 44 | outline: none; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | } 50 | 51 | .todoapp { 52 | background: #fff; 53 | margin: 130px 0 40px 0; 54 | position: relative; 55 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 56 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 57 | } 58 | 59 | .todoapp input::-webkit-input-placeholder { 60 | font-style: italic; 61 | font-weight: 300; 62 | color: #e6e6e6; 63 | } 64 | 65 | .todoapp input::-moz-placeholder { 66 | font-style: italic; 67 | font-weight: 300; 68 | color: #e6e6e6; 69 | } 70 | 71 | .todoapp input::input-placeholder { 72 | font-style: italic; 73 | font-weight: 300; 74 | color: #e6e6e6; 75 | } 76 | 77 | .todoapp h1 { 78 | position: absolute; 79 | top: -155px; 80 | width: 100%; 81 | font-size: 100px; 82 | font-weight: 100; 83 | text-align: center; 84 | color: rgba(175, 47, 47, 0.15); 85 | -webkit-text-rendering: optimizeLegibility; 86 | -moz-text-rendering: optimizeLegibility; 87 | text-rendering: optimizeLegibility; 88 | } 89 | 90 | .new-todo, 91 | .edit { 92 | position: relative; 93 | margin: 0; 94 | width: 100%; 95 | font-size: 24px; 96 | font-family: inherit; 97 | font-weight: inherit; 98 | line-height: 1.4em; 99 | border: 0; 100 | outline: none; 101 | color: inherit; 102 | padding: 6px; 103 | border: 1px solid #999; 104 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 105 | box-sizing: border-box; 106 | -webkit-font-smoothing: antialiased; 107 | -moz-font-smoothing: antialiased; 108 | font-smoothing: antialiased; 109 | } 110 | 111 | .new-todo { 112 | padding: 16px 16px 16px 60px; 113 | border: none; 114 | background: rgba(0, 0, 0, 0.003); 115 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 116 | } 117 | 118 | .main { 119 | position: relative; 120 | z-index: 2; 121 | border-top: 1px solid #e6e6e6; 122 | } 123 | 124 | label[for='toggle-all'] { 125 | display: none; 126 | } 127 | 128 | .toggle-all { 129 | position: absolute; 130 | top: -55px; 131 | left: -12px; 132 | width: 60px; 133 | height: 34px; 134 | text-align: center; 135 | border: none; /* Mobile Safari */ 136 | } 137 | 138 | .toggle-all:before { 139 | content: '>'; 140 | font-size: 22px; 141 | color: #e6e6e6; 142 | padding: 10px 27px 10px 27px; 143 | } 144 | 145 | .toggle-all:checked:before { 146 | color: #737373; 147 | } 148 | 149 | .todo-list { 150 | margin: 0; 151 | padding: 0; 152 | list-style: none; 153 | } 154 | 155 | .todo-list li { 156 | position: relative; 157 | font-size: 24px; 158 | border-bottom: 1px solid #ededed; 159 | } 160 | 161 | .todo-list li:last-child { 162 | border-bottom: none; 163 | } 164 | 165 | .todo-list li.editing { 166 | border-bottom: none; 167 | padding: 0; 168 | } 169 | 170 | .todo-list li.editing .edit { 171 | display: block; 172 | width: 506px; 173 | padding: 13px 17px 12px 17px; 174 | margin: 0 0 0 43px; 175 | } 176 | 177 | .todo-list li.editing .view { 178 | display: none; 179 | } 180 | 181 | .todo-list li .toggle { 182 | text-align: center; 183 | width: 40px; 184 | /* auto, since non-WebKit browsers doesn't support input styling */ 185 | height: auto; 186 | position: absolute; 187 | top: 0; 188 | bottom: 0; 189 | margin: auto 0; 190 | border: none; /* Mobile Safari */ 191 | -webkit-appearance: none; 192 | appearance: none; 193 | } 194 | 195 | .todo-list li .toggle:after { 196 | content: url('data:image/svg+xml,'); 197 | } 198 | 199 | .todo-list li .toggle:checked:after { 200 | content: url('data:image/svg+xml,'); 201 | } 202 | 203 | .todo-list li label { 204 | white-space: pre-line; 205 | word-break: break-all; 206 | padding: 15px 60px 15px 15px; 207 | margin-left: 45px; 208 | display: block; 209 | line-height: 1.2; 210 | transition: color 0.4s; 211 | } 212 | 213 | .todo-list li.completed label { 214 | color: #d9d9d9; 215 | text-decoration: line-through; 216 | } 217 | 218 | .todo-list li .destroy { 219 | display: none; 220 | position: absolute; 221 | top: 0; 222 | right: 10px; 223 | bottom: 0; 224 | width: 40px; 225 | height: 40px; 226 | margin: auto 0; 227 | font-size: 30px; 228 | color: #cc9a9a; 229 | margin-bottom: 11px; 230 | transition: color 0.2s ease-out; 231 | } 232 | 233 | .todo-list li .destroy:hover { 234 | color: #af5b5e; 235 | } 236 | 237 | .todo-list li .destroy:after { 238 | content: 'x'; 239 | } 240 | 241 | .todo-list li:hover .destroy { 242 | display: block; 243 | } 244 | 245 | .todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | .footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | .todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | .todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | .filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | .filters li { 291 | display: inline; 292 | } 293 | 294 | .filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | .filters li a.selected, 304 | .filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | .filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | .clear-completed, 313 | html .clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | .clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | .info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | /* font-size: 10px; */ 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | .info p { 335 | line-height: 1; 336 | } 337 | 338 | .info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | .info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | .toggle-all, 354 | .todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | .todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | .toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } -------------------------------------------------------------------------------- /examples/todoapp/todoapp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo app 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/todoapp/todoapp.nim: -------------------------------------------------------------------------------- 1 | 2 | import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage] 3 | 4 | type 5 | Filter = enum 6 | all, active, completed 7 | 8 | var 9 | selectedEntry = -1 10 | filter: Filter 11 | entriesLen: int 12 | doneswitch = true 13 | const 14 | contentSuffix = cstring"content" 15 | completedSuffix = cstring"completed" 16 | lenSuffix = cstring"entriesLen" 17 | 18 | proc getEntryContent(pos: int): cstring = 19 | result = getItem(&pos & contentSuffix) 20 | if result == cstring"null": 21 | result = nil 22 | 23 | proc isCompleted(pos: int): bool = 24 | var value = getItem(&pos & completedSuffix) 25 | result = value == cstring"true" 26 | 27 | proc setEntryContent(pos: int, content: cstring) = 28 | setItem(&pos & contentSuffix, content) 29 | 30 | proc markAsCompleted(pos: int, completed: bool) = 31 | setItem(&pos & completedSuffix, &completed) 32 | 33 | proc addEntry(content: cstring, completed: bool) = 34 | setEntryContent(entriesLen, content) 35 | markAsCompleted(entriesLen, completed) 36 | inc entriesLen 37 | setItem(lenSuffix, &entriesLen) 38 | 39 | proc updateEntry(pos: int, content: cstring, completed: bool) = 40 | setEntryContent(pos, content) 41 | markAsCompleted(pos, completed) 42 | 43 | proc onTodoEnter(ev: Event; n: VNode) = 44 | if n.value.strip() != "": 45 | addEntry(n.value, false) 46 | n.value = "" 47 | 48 | proc removeHandler(ev: Event; n: VNode) = 49 | updateEntry(n.index, cstring(nil), false) 50 | 51 | proc editHandler(ev: Event; n: VNode) = 52 | selectedEntry = n.index 53 | 54 | proc focusLost(ev: Event; n: VNode) = selectedEntry = -1 55 | 56 | proc editEntry(ev: Event; n: VNode) = 57 | setEntryContent(n.index, n.value) 58 | selectedEntry = -1 59 | 60 | proc toggleEntry(ev: Event; n: VNode) = 61 | let id = n.index 62 | markAsCompleted(id, not isCompleted(id)) 63 | 64 | proc onAllDone(ev: Event; n: VNode) = 65 | for i in 0.. 2 | 3 | 4 | 5 | Karax AJAX Test 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /experiments/ajaxtest.nim: -------------------------------------------------------------------------------- 1 | import kdom, kajax 2 | 3 | proc cb(httpStatus: int, response: cstring) = 4 | echo "Worked!" 5 | 6 | ajaxGet("https://httpbin.org/get", @[], cb) 7 | -------------------------------------------------------------------------------- /experiments/config.nims: -------------------------------------------------------------------------------- 1 | --path: "../karax" 2 | --path: ".." 3 | -------------------------------------------------------------------------------- /experiments/dyn.nim: -------------------------------------------------------------------------------- 1 | 2 | 3 | type 4 | EntryPoint = proc() 5 | 6 | 7 | proc dynmain = 8 | echo "dynamically loaded" 9 | 10 | 11 | var plugins {.importc.}: seq[(string, EntryPoint)] 12 | 13 | plugins.add(("dyn", EntryPoint dynmain)) 14 | -------------------------------------------------------------------------------- /experiments/echartstest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Karax ECharts Test 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /experiments/echartstest.nim: -------------------------------------------------------------------------------- 1 | import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson] 2 | 3 | type 4 | Views = enum 5 | Customers, Products 6 | 7 | var 8 | currentView = Customers 9 | 10 | type 11 | EChart* = ref object 12 | 13 | proc echartsInit(n: Element): EChart {.importc: "echarts.init".} 14 | proc setOption(x: EChart; option: JsonNode) {.importcpp.} 15 | 16 | proc postRender(data: RouterData) = 17 | if currentView == Products: 18 | let myChart = echartsInit(kdom.getElementById("echartSection")) 19 | # specify chart configuration item and data 20 | let option = %*{ 21 | "title": { 22 | "text": "ECharts entry example" 23 | }, 24 | "tooltip": {}, 25 | "legend": { 26 | "data": ["Sales"] 27 | }, 28 | "xAxis": { 29 | "data": ["shirt","cardign","chiffon shirt","pants","heels","socks"] 30 | }, 31 | "yAxis": {}, 32 | "series": [{ 33 | "name": "Sales", 34 | "type": "bar", 35 | "data": [5, 20, 36, 10, 10, 20] 36 | }] 37 | } 38 | myChart.setOption(option) 39 | 40 | proc createDom(data: RouterData): VNode = 41 | let hash = data.hashPart 42 | if hash == cstring"#/Products": currentView = Products 43 | else: currentView = Customers 44 | 45 | result = buildHtml(tdiv): 46 | ul(class = "tabs"): 47 | for v in low(Views)..high(Views): 48 | li: 49 | a(href = "#/" & $v): 50 | text cstring($v) 51 | 52 | if currentView == Products: 53 | tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px"))) 54 | tdiv: 55 | text "other section" 56 | 57 | setRenderer createDom, "ROOT", postRender 58 | setForeignNodeId "echartSection" 59 | -------------------------------------------------------------------------------- /experiments/menus.nim: -------------------------------------------------------------------------------- 1 | ## Example program that shows how to create menus with Karax. 2 | 3 | include prelude 4 | import jstrutils, kdom 5 | 6 | proc contentA(): VNode = 7 | result = buildHtml(tdiv): 8 | text "content A" 9 | 10 | proc contentB(): VNode = 11 | result = buildHtml(tdiv): 12 | text "content B" 13 | 14 | proc contentC(): VNode = 15 | result = buildHtml(tdiv): 16 | text "content C" 17 | 18 | 19 | 20 | type 21 | MenuItemHandler = proc(): VNode 22 | 23 | var content: MenuItemHandler = contentA 24 | 25 | proc menuAction(x: MenuItemHandler): proc() = 26 | result = proc() = content = x 27 | 28 | proc buildMenu(): VNode = 29 | result = buildHtml(tdiv): 30 | nav(class="navbar is-primary"): 31 | tdiv(class="navbar-brand"): 32 | a(class="navbar-item", onclick = menuAction(contentA)): 33 | strong: 34 | text "My awesome menu" 35 | 36 | tdiv(id="navMenuTransparentExample", class="navbar-menu"): 37 | tdiv(class = "navbar-start"): 38 | tdiv(class="navbar-item has-dropdown is-hoverable"): 39 | a(class="navbar-link", onclick = menuAction(contentB)): 40 | text "Masters" 41 | tdiv(class="navbar-dropdown is-boxed"): 42 | a(class="navbar-item", onclick = menuAction(contentC)): 43 | text "Inventory" 44 | a(class="navbar-item", onclick = menuAction(contentC)): 45 | text "Product" 46 | hr(class="navbar-divider"): 47 | a(class="navbar-item", onclick = menuAction(contentC)): 48 | text "Product" 49 | 50 | tdiv(class="navbar-item has-dropdown is-hoverable"): 51 | a(class="navbar-link", onclick = menuAction(contentC)): 52 | text "Transactions" 53 | tdiv(class = "navbar-dropdown is-boxed", id="blogDropdown"): 54 | a(class="navbar-item", onclick = menuAction(contentC)): 55 | text "Purchase Bill" 56 | a(class="navbar-item", onclick = menuAction(contentC)): 57 | text "Purchase Return" 58 | a(class="navbar-item", onclick = menuAction(contentC)): 59 | text "Sale Bill" 60 | hr(class="navbar-divider"): 61 | a(class="navbar-item", onclick = menuAction(contentC)): 62 | text "Stock Adjustment" 63 | content() 64 | 65 | setRenderer buildMenu 66 | -------------------------------------------------------------------------------- /experiments/nativenodes.nim: -------------------------------------------------------------------------------- 1 | 2 | 3 | include karax / prelude 4 | import karax / kdom 5 | 6 | proc myInput: VNode = 7 | result = buildHtml: 8 | input() 9 | 10 | var inp = vnodeToDom(myInput()) 11 | 12 | proc myAwesomeComponent(x: Node): VNode = 13 | result = dthunk(x) 14 | 15 | proc main: VNode = 16 | result = myAwesomeComponent(inp) 17 | 18 | setRenderer main 19 | -------------------------------------------------------------------------------- /experiments/nextgen.nim: -------------------------------------------------------------------------------- 1 | 2 | import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive 3 | 4 | proc textInput*(text: RString; focus: RBool): VNode {.track.} = 5 | proc onFlip(ev: Event; target: VNode) = 6 | focus <- not focus.value 7 | 8 | proc onKeyupEnter(ev: Event; target: VNode) = 9 | text <- target.value 10 | 11 | proc onkeyup(ev: Event; n: VNode) = 12 | # keep displayValue up to date, but do not tell the client yet! 13 | text.value = n.value 14 | 15 | result = buildHtml(input(`type`="text", 16 | value=text.value, onblur=onFlip, onfocus=onFlip, 17 | onkeyupenter=onkeyupenter, onkeyup=onkeyup, setFocus=focus.value)) 18 | 19 | var 20 | errmsg = rstr("") 21 | 22 | makeReactive: 23 | type 24 | User = ref object 25 | firstname, lastname: cstring 26 | selected: bool 27 | 28 | var gu = newRSeq(@[ (User(rawFirstname: "Some", rawLastname: "Body")), 29 | (User(rawFirstname: "Some", rawLastname: "One")), 30 | (User(rawFirstname: "Some", rawLastname: "Two"))]) 31 | var prevSelected: User = nil #newReactive[User](nil) 32 | 33 | proc unselect() = 34 | if prevSelected != nil: 35 | prevSelected.selected = false 36 | prevSelected = nil 37 | 38 | proc select(u: User) = 39 | unselect() 40 | u.selected = true 41 | prevSelected = u 42 | 43 | proc toUI*(isFirstname: bool): RString = 44 | result = RString() 45 | result.subscribe proc (v: cstring) = 46 | if v.len > 0: 47 | let p = prevSelected #selected.value 48 | if p != nil: 49 | if isFirstName: 50 | p.firstname = v 51 | else: 52 | p.lastname = v 53 | unselect() 54 | errmsg <- "" 55 | else: 56 | errmsg <- "name must not be empty" 57 | 58 | var inpFirstname = toUI(true) 59 | var inpLastname = toUI(false) 60 | 61 | proc adaptFocus(def = false): RBool = 62 | result = RBool() 63 | result.value = def 64 | when false: 65 | result.subscribe proc (hasFocus: bool) = 66 | if not hasFocus: 67 | unselect() 68 | 69 | var focusA = adaptFocus() 70 | var focusB = adaptFocus() 71 | 72 | proc styler(): VStyle = 73 | result = style( 74 | (StyleAttr.position, cstring"relative"), 75 | (StyleAttr.paddingLeft, cstring"10px"), 76 | (StyleAttr.paddingRight, cstring"5px"), 77 | (StyleAttr.height, cstring"30px"), 78 | (StyleAttr.lineHeight, cstring"30px"), 79 | (StyleAttr.border, cstring"solid 8px " & (if focusA.value: cstring"red" else: cstring"black")), 80 | (StyleAttr.fontSize, cstring"12px"), 81 | (StyleAttr.fontWeight, cstring"600") 82 | ) 83 | 84 | var clicks = 0 85 | 86 | proc renderUser(u: User): VNode {.track.} = 87 | result = buildHtml(tdiv): 88 | if u.selected: 89 | inpFirstname <- u.firstname 90 | tdiv: 91 | textInput inpFirstname, focusA 92 | inpLastname <- u.lastname 93 | tdiv: 94 | textInput inpLastname, focusB 95 | else: 96 | button: 97 | text "..." 98 | proc onclick(ev: Event; n: VNode) = 99 | select(u) 100 | text u.firstname & " " & u.lastname 101 | button: 102 | text "(x)" 103 | proc onclick(ev: Event; n: VNode) = 104 | gu.deleteElem(u) 105 | 106 | proc main(gu: RSeq[User]): VNode = 107 | result = buildHtml(tdiv): 108 | tdiv: 109 | button: 110 | text "Add User" 111 | proc onclick(ev: Event; n: VNode) = 112 | inc clicks 113 | gu.add User(rawFirstname: "Added", rawLastname: &clicks) 114 | tdiv: 115 | text errmsg 116 | vmapIt(gu, tdiv, renderUser(it)) 117 | 118 | proc init(): VNode = main(gu) 119 | 120 | setInitializer(init) 121 | -------------------------------------------------------------------------------- /experiments/oldwidgets.nim: -------------------------------------------------------------------------------- 1 | var 2 | linkCounter: int 3 | 4 | proc link*(id: int): VNode = 5 | result = newVNode(VNodeKind.anchor) 6 | result.setAttr("href", "#") 7 | inc linkCounter 8 | result.setAttr("id", $linkCounter & ":" & $id) 9 | 10 | proc link*(action: EventHandler): VNode = 11 | result = newVNode(VNodeKind.anchor) 12 | result.setAttr("href", "#") 13 | result.setOnclick action 14 | 15 | when false: 16 | proc button*(caption: cstring; action: EventHandler; disabled=false): VNode = 17 | result = newVNode(VNodeKind.button) 18 | result.add text(caption) 19 | if action != nil: 20 | result.setOnClick action 21 | if disabled: 22 | result.setAttr("disabled", "true") 23 | 24 | proc select*(choices: openarray[cstring]): VNode = 25 | result = newVNode(VNodeKind.select) 26 | var i = 0 27 | for c in choices: 28 | result.add tree(VNodeKind.option, [(cstring"value", toCstr(i))], text(c)) 29 | inc i 30 | 31 | proc select*(choices: openarray[(int, cstring)]): VNode = 32 | result = newVNode(VNodeKind.select) 33 | for c in choices: 34 | result.add tree(VNodeKind.option, [(cstring"value", toCstr(c[0]))], text(c[1])) 35 | 36 | var radioCounter: int 37 | 38 | proc radio*(choices: openarray[(int, cstring)]): VNode = 39 | result = newVNode(VNodeKind.fieldset) 40 | var i = 0 41 | inc radioCounter 42 | for c in choices: 43 | let id = cstring"radio_" & c[1] & toCstr(i) 44 | var kid = tree(VNodeKind.input, [(cstring"type", cstring"radio"), 45 | (cstring"id", id), (cstring"name", cstring"radio" & toCStr(radioCounter)), 46 | (cstring"value", toCStr(c[0]))]) 47 | if i == 0: 48 | kid.setAttr(cstring"checked", cstring"checked") 49 | var lab = tree(VNodeKind.label, [(cstring"for", id)], text(c[1])) 50 | kid.add lab 51 | result.add kid 52 | inc i 53 | 54 | proc tag*(kind: VNodeKind; id=cstring(nil), class=cstring(nil)): VNode = 55 | result = newVNode(kind) 56 | result.id = id 57 | result.class = class 58 | 59 | proc tdiv*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.tdiv, id, class) 60 | proc span*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.span, id, class) 61 | 62 | proc valueAsInt*(e: Node): int = parseInt(e.value) 63 | 64 | proc th*(s: cstring): VNode = 65 | result = newVNode(VNodeKind.th) 66 | result.add text(s) 67 | 68 | proc td*(s: string): VNode = 69 | result = newVNode(VNodeKind.td) 70 | result.add text(s) 71 | 72 | proc td*(s: VNode): VNode = 73 | result = newVNode(VNodeKind.td) 74 | result.add s 75 | 76 | proc td*(class: cstring; s: VNode): VNode = 77 | result = newVNode(VNodeKind.td) 78 | result.add s 79 | result.class = class 80 | 81 | proc table*(class=cstring(nil), kids: varargs[VNode]): VNode = 82 | result = tag(VNodeKind.table, nil, class) 83 | for k in kids: result.add k 84 | 85 | proc tr*(kids: varargs[VNode]): VNode = 86 | result = newVNode(VNodeKind.tr) 87 | for k in kids: 88 | if k.kind in {VNodeKind.td, VNodeKind.th}: 89 | result.add k 90 | else: 91 | result.add td(k) 92 | 93 | proc suffix*(s, prefix: cstring): cstring = 94 | if s.startsWith(prefix): 95 | result = s.substr(prefix.len) 96 | else: 97 | kout(cstring"bug! " & s & cstring" does not start with " & prefix) 98 | 99 | proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix)) 100 | 101 | #proc ceil(f: float): int {.importc: "Math.ceil", nodecl.} 102 | 103 | proc realtimeInput*(val: cstring; action: EventHandler): VNode = 104 | var timer: Timeout 105 | proc onkeyup(ev: Event; n: VNode) = 106 | proc wrapper() = keyeventBody() 107 | 108 | if timer != nil: clearTimeout(timer) 109 | timer = setTimeout(wrapper, 400) 110 | result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) 111 | result.value = val 112 | result.addEventListener(EventKind.onkeyup, onkeyup) 113 | 114 | proc enterInput*(id, val: cstring; action: EventHandler): VNode = 115 | proc onkeyup(ev: Event; n: VNode) = 116 | if ev.keyCode == 13: keyeventBody() 117 | 118 | result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) 119 | result.id = id 120 | result.value = val 121 | result.addEventListener(EventKind.onkeyup, onkeyup) 122 | 123 | proc setOnEnter*(n: VNode; action: EventHandler) = 124 | proc onkeyup(ev: Event; n: VNode) = 125 | if ev.keyCode == 13: keyeventBody() 126 | n.addEventListener(EventKind.onkeyup, onkeyup) 127 | 128 | proc setOnscroll*(action: proc(min, max: VKey; diff: int)) = 129 | var oldY = window.pageYOffset 130 | 131 | proc wrapper(ev: Event) = 132 | let dir = window.pageYOffset - oldY 133 | if dir == 0: return 134 | 135 | var a = VKey high(int) 136 | var b = VKey 0 137 | var h, count: int 138 | document.visibleKeys(a, b, h, count) 139 | let avgh = h / count 140 | let diff = toInt(dir.float / avgh) 141 | if diff != 0: 142 | oldY = window.pageYOffset 143 | action(a, b, diff) 144 | redraw() 145 | 146 | document.addEventListener("scroll", wrapper) 147 | 148 | when false: 149 | var plugins {.exportc.}: seq[(string, proc())] = @[] 150 | 151 | proc onInput(val: cstring) = 152 | kout val 153 | if val == "dyn": 154 | kout(plugins.len) 155 | if plugins.len > 0: 156 | plugins[0][1]() 157 | 158 | var 159 | images: seq[cstring] = @[cstring"a", "b", "c", "d"] 160 | 161 | proc carousel*(): VNode = 162 | var currentIndex = 0 163 | 164 | proc next(ev: Event; n: VNode) = 165 | currentIndex = (currentIndex + 1) mod images.len 166 | 167 | proc prev(ev: Event; n: VNode) = 168 | currentIndex = (currentIndex - 1) mod images.len 169 | 170 | result = buildHtml(tdiv): 171 | text images[currentIndex] 172 | button(onclick = next): 173 | text "Next" 174 | button(onclick = prev): 175 | text "Previous" 176 | 177 | #proc targetElem*(e: Event): Element = cast[Element](e.target) 178 | 179 | #proc getElementsByClassName*(cls: cstring): seq[Element] {.importc: 180 | # "document.getElementsByClassName", nodecl.} 181 | #proc textContent(e: Node): cstring {. 182 | # importcpp: "#.textContent", nodecl.} 183 | 184 | proc isElementInViewport(el: Node; h: var int): bool = 185 | let rect = el.getBoundingClientRect() 186 | h = rect.bottom - rect.top 187 | result = rect.top >= 0 and rect.left >= 0 and 188 | rect.bottom <= clientHeight() and 189 | rect.right <= clientWidth() 190 | 191 | proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) = 192 | # we only care about nodes that have a key: 193 | var hh = 0 194 | # do not recurse if there is a 'key' field already: 195 | if e.key >= 0: 196 | if isElementInViewport(e, hh): 197 | inc count 198 | inc h, hh 199 | a = min(a, e.key) 200 | b = max(b, e.key) 201 | else: 202 | for i in 0.. 2 | 3 | 4 | 5 | 6 | Refs 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /experiments/refs/demo.nim: -------------------------------------------------------------------------------- 1 | include karax/prelude 2 | import sugar, sequtils 3 | 4 | type 5 | CustomRef = ref object of VComponent 6 | 7 | method onAttach(r: CustomRef) = 8 | kout(cstring"custom ref attached") 9 | 10 | method onDetach(r: CustomRef) = 11 | kout(cstring"custom ref detached") 12 | 13 | var 14 | modelData = @[5, 2, 4] 15 | 16 | refA = VComponent() 17 | refB = VComponent() 18 | refC = CustomRef() 19 | refSeq = newSeq[VComponent]() 20 | 21 | proc onClick(ev: Event, n: VNode) = 22 | modelData.add(0) 23 | 24 | proc showRefs() = 25 | kout(refA) 26 | kout(refB) 27 | kout(refC) 28 | for i in 0 ..< refSeq.len: 29 | kout(refSeq[i].vnode) 30 | #kout(refSeq.map(nref => nref.vnode)) 31 | 32 | proc secureRefSlot(i: int): VComponent = 33 | while refSeq.len <= i: 34 | refSeq.add(VComponent()) 35 | result = refSeq[i] 36 | 37 | proc view(): VNode = 38 | result = buildHtml(): 39 | tdiv: 40 | button(onclick=onClick): 41 | text "click me" 42 | # storing refs to single elements is straightforward now 43 | if modelData.len mod 2 == 0: 44 | tdiv(nref=refA): 45 | text "A" 46 | tdiv(nref=refB): 47 | text "B" 48 | tdiv(nref=refC): 49 | text "C" 50 | # It's a bit more tricky when containers are involved: 51 | for i, x in modelData.pairs: 52 | tdiv(nref=secureRefSlot(i)): 53 | text "List item: " & $x 54 | 55 | proc renderer(): VNode = 56 | view() 57 | 58 | setRenderer(renderer, "ROOT", showRefs) 59 | -------------------------------------------------------------------------------- /experiments/refs/demo.nims: -------------------------------------------------------------------------------- 1 | --path: "../../src" 2 | -------------------------------------------------------------------------------- /experiments/scrollapp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo app 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /experiments/scrollapp.nim: -------------------------------------------------------------------------------- 1 | 2 | import vdom, karax, karaxdsl, jdict, jstrutils, compact 3 | 4 | type 5 | Filter = enum 6 | all, active, completed 7 | 8 | var 9 | entries: seq[(cstring, bool)] 10 | visibleA = 0 11 | visibleB = 10 12 | selectedEntry = -1 13 | filter: Filter 14 | 15 | proc onTodoEnter(ev: Event; n: VNode) = 16 | entries.add((n.value, false)) 17 | n.value = "" 18 | 19 | proc removeHandler(ev: Event; n: VNode) = 20 | entries[n.key] = (cstring(nil), false) 21 | 22 | proc editHandler(ev: Event; n: VNode) = 23 | selectedEntry = n.key 24 | 25 | proc focusLost(ev: Event; n: VNode) = selectedEntry = -1 26 | 27 | proc editEntry(ev: Event; n: VNode) = 28 | entries[n.key][0] = n.value 29 | selectedEntry = -1 30 | 31 | proc toggleEntry(ev: Event; n: VNode) = 32 | let id = n.key 33 | entries[id][1] = not entries[id][1] 34 | 35 | proc onAllDone(ev: Event; n: VNode) = 36 | entries = @[] 37 | selectedEntry = -1 38 | 39 | proc clearCompleted(ev: Event, n: VNode) = 40 | for i in 0.. 0: 123 | # scroll downwards: 124 | visibleA = max(a-overshoot2, 0) 125 | visibleB = min(b+diff+overshoot1, entries.len-1) 126 | elif diff < 0: 127 | # scroll upwards: 128 | visibleA = max(a+diff-overshoot1, 0) 129 | visibleB = min(b+overshoot2, entries.len-1) 130 | ) 131 | 132 | setOnHashChange(proc(hash: cstring) = 133 | if hash == cstring"#/": filter = all 134 | elif hash == cstring"#/completed": filter = completed 135 | elif hash == cstring"#/active": filter = active 136 | ) 137 | setRenderer createDom 138 | 139 | proc onload(session: cstring) {.exportc.} = 140 | for i in 0..visibleB: 141 | entries.add((cstring"Entry " & &i, false)) 142 | init() 143 | -------------------------------------------------------------------------------- /experiments/scrollapp_simple.nim: -------------------------------------------------------------------------------- 1 | ## Example that shows how to accomplish an "infinitely scrolling" app. 2 | 3 | include karax / prelude 4 | import karax / [kdom, vstyles] 5 | 6 | var entries: seq[cstring] = @[] 7 | for i in 1..500: 8 | entries.add(cstring("Entry ") & &i) 9 | 10 | proc scrollEvent(ev: Event; n: VNode) = 11 | let d = n.dom 12 | if d != nil and inViewport(d.lastChild): 13 | # "load" more data: 14 | for i in 1..50: 15 | entries.add(cstring("Loaded Entry ") & &i) 16 | 17 | proc createDom(): VNode = 18 | result = buildHtml(): 19 | tdiv(onscroll=scrollEvent, style=style( 20 | (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))): 21 | for x in entries: 22 | tdiv: 23 | text x 24 | 25 | setRenderer createDom 26 | -------------------------------------------------------------------------------- /experiments/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .todomvc-wrapper { 8 | visibility: visible !important; 9 | } 10 | 11 | button { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | background: none; 16 | font-size: 100%; 17 | vertical-align: baseline; 18 | font-family: inherit; 19 | font-weight: inherit; 20 | color: inherit; 21 | -webkit-appearance: none; 22 | appearance: none; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-font-smoothing: antialiased; 25 | font-smoothing: antialiased; 26 | } 27 | 28 | body { 29 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 30 | line-height: 1.4em; 31 | background: #f5f5f5; 32 | color: #4d4d4d; 33 | min-width: 230px; 34 | max-width: 550px; 35 | margin: 0 auto; 36 | -webkit-font-smoothing: antialiased; 37 | -moz-font-smoothing: antialiased; 38 | font-smoothing: antialiased; 39 | font-weight: 300; 40 | } 41 | 42 | button, 43 | input[type="checkbox"] { 44 | outline: none; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | } 50 | 51 | .todoapp { 52 | background: #fff; 53 | margin: 130px 0 40px 0; 54 | position: relative; 55 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 56 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 57 | } 58 | 59 | .todoapp input::-webkit-input-placeholder { 60 | font-style: italic; 61 | font-weight: 300; 62 | color: #e6e6e6; 63 | } 64 | 65 | .todoapp input::-moz-placeholder { 66 | font-style: italic; 67 | font-weight: 300; 68 | color: #e6e6e6; 69 | } 70 | 71 | .todoapp input::input-placeholder { 72 | font-style: italic; 73 | font-weight: 300; 74 | color: #e6e6e6; 75 | } 76 | 77 | .todoapp h1 { 78 | position: absolute; 79 | top: -155px; 80 | width: 100%; 81 | font-size: 100px; 82 | font-weight: 100; 83 | text-align: center; 84 | color: rgba(175, 47, 47, 0.15); 85 | -webkit-text-rendering: optimizeLegibility; 86 | -moz-text-rendering: optimizeLegibility; 87 | text-rendering: optimizeLegibility; 88 | } 89 | 90 | .new-todo, 91 | .edit { 92 | position: relative; 93 | margin: 0; 94 | width: 100%; 95 | font-size: 24px; 96 | font-family: inherit; 97 | font-weight: inherit; 98 | line-height: 1.4em; 99 | border: 0; 100 | outline: none; 101 | color: inherit; 102 | padding: 6px; 103 | border: 1px solid #999; 104 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 105 | box-sizing: border-box; 106 | -webkit-font-smoothing: antialiased; 107 | -moz-font-smoothing: antialiased; 108 | font-smoothing: antialiased; 109 | } 110 | 111 | .new-todo { 112 | padding: 16px 16px 16px 60px; 113 | border: none; 114 | background: rgba(0, 0, 0, 0.003); 115 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 116 | } 117 | 118 | .main { 119 | position: relative; 120 | z-index: 2; 121 | border-top: 1px solid #e6e6e6; 122 | } 123 | 124 | label[for='toggle-all'] { 125 | display: none; 126 | } 127 | 128 | .toggle-all { 129 | position: absolute; 130 | top: -55px; 131 | left: -12px; 132 | width: 60px; 133 | height: 34px; 134 | text-align: center; 135 | border: none; /* Mobile Safari */ 136 | } 137 | 138 | .toggle-all:before { 139 | content: '>'; 140 | font-size: 22px; 141 | color: #e6e6e6; 142 | padding: 10px 27px 10px 27px; 143 | } 144 | 145 | .toggle-all:checked:before { 146 | color: #737373; 147 | } 148 | 149 | .todo-list { 150 | margin: 0; 151 | padding: 0; 152 | list-style: none; 153 | } 154 | 155 | .todo-list li { 156 | position: relative; 157 | font-size: 24px; 158 | border-bottom: 1px solid #ededed; 159 | } 160 | 161 | .todo-list li:last-child { 162 | border-bottom: none; 163 | } 164 | 165 | .todo-list li.editing { 166 | border-bottom: none; 167 | padding: 0; 168 | } 169 | 170 | .todo-list li.editing .edit { 171 | display: block; 172 | width: 506px; 173 | padding: 13px 17px 12px 17px; 174 | margin: 0 0 0 43px; 175 | } 176 | 177 | .todo-list li.editing .view { 178 | display: none; 179 | } 180 | 181 | .todo-list li .toggle { 182 | text-align: center; 183 | width: 40px; 184 | /* auto, since non-WebKit browsers doesn't support input styling */ 185 | height: auto; 186 | position: absolute; 187 | top: 0; 188 | bottom: 0; 189 | margin: auto 0; 190 | border: none; /* Mobile Safari */ 191 | -webkit-appearance: none; 192 | appearance: none; 193 | } 194 | 195 | .todo-list li .toggle:after { 196 | content: url('data:image/svg+xml,'); 197 | } 198 | 199 | .todo-list li .toggle:checked:after { 200 | content: url('data:image/svg+xml,'); 201 | } 202 | 203 | .todo-list li label { 204 | white-space: pre-line; 205 | word-break: break-all; 206 | padding: 15px 60px 15px 15px; 207 | margin-left: 45px; 208 | display: block; 209 | line-height: 1.2; 210 | transition: color 0.4s; 211 | } 212 | 213 | .todo-list li.completed label { 214 | color: #d9d9d9; 215 | text-decoration: line-through; 216 | } 217 | 218 | .todo-list li .destroy { 219 | display: none; 220 | position: absolute; 221 | top: 0; 222 | right: 10px; 223 | bottom: 0; 224 | width: 40px; 225 | height: 40px; 226 | margin: auto 0; 227 | font-size: 30px; 228 | color: #cc9a9a; 229 | margin-bottom: 11px; 230 | transition: color 0.2s ease-out; 231 | } 232 | 233 | .todo-list li .destroy:hover { 234 | color: #af5b5e; 235 | } 236 | 237 | .todo-list li .destroy:after { 238 | content: 'x'; 239 | } 240 | 241 | .todo-list li:hover .destroy { 242 | display: block; 243 | } 244 | 245 | .todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | .footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | .todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | .todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | .filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | .filters li { 291 | display: inline; 292 | } 293 | 294 | .filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | .filters li a.selected, 304 | .filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | .filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | .clear-completed, 313 | html .clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | .clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | .info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | /* font-size: 10px; */ 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | .info p { 335 | line-height: 1; 336 | } 337 | 338 | .info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | .info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | .toggle-all, 354 | .todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | .todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | .toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } -------------------------------------------------------------------------------- /experiments/trello/trello.nim: -------------------------------------------------------------------------------- 1 | 2 | import knete, widgets 3 | import karax / jstrutils 4 | import std / dom 5 | 6 | type 7 | Attachable* = ref object of RootObj ## an element that is attachable to 8 | ## DOM elements. 9 | attachedTo*: seq[Element] 10 | 11 | TaskId = int 12 | Task = ref object 13 | id: TaskId 14 | name, desc: kstring 15 | 16 | ColumnId = distinct int 17 | Column = ref object of Attachable 18 | id: ColumnId 19 | header: kstring 20 | tasks: seq[Task] 21 | 22 | Board = ref object 23 | columns: seq[Column] 24 | 25 | var b = Board(columns: @[Column(id: ColumnId 0, header: "To Do"), 26 | Column(id: ColumnId 1, header: "Doing"), 27 | Column(id: ColumnId 2, header: "Done")]) 28 | 29 | proc removeTask(t: TaskId) = 30 | for c in b.columns: 31 | for i in 0 ..< c.tasks.len: 32 | if c.tasks[i].id == t: 33 | delete(c.tasks, i) 34 | break 35 | 36 | proc moveTask(t: TaskId; dest: Column) = 37 | for c in b.columns: 38 | for i in 0 ..< c.tasks.len: 39 | let task = c.tasks[i] 40 | if task.id == t: 41 | delete(c.tasks, i) 42 | dest.tasks.add task 43 | return 44 | 45 | var nextTaskId = 0 ## negative because they are not yet backed 46 | ## up by the database 47 | 48 | proc createTask(c: Column; name, desc: kstring): Task = 49 | dec nextTaskId 50 | result = Task(id: TaskId nextTaskId, name: name, desc: desc) 51 | c.tasks.add result 52 | 53 | # --------------------- UI ----------------------------------------------- 54 | 55 | proc renderTask(t: Task): Element = 56 | proc dodelete = 57 | removeTask t.id 58 | delete result 59 | 60 | proc dragstart(ev: Event) = 61 | ev.prepareDragData("taskid", ev.target.id) 62 | 63 | result = buildHtml(): 64 | tdiv(draggable="true", ondragstart=dragstart, id = &t.id): 65 | bindField t.name 66 | br() 67 | bindField t.desc 68 | button(onclick = dodelete): 69 | text cross 70 | 71 | type 72 | NewTaskPanel = object 73 | c: Column 74 | e: Element 75 | 76 | proc open(p: var NewTaskPanel; c: Column) = 77 | p.e.style.display = "block" 78 | p.c = c 79 | 80 | proc newTaskDialog(): NewTaskPanel = 81 | var nameInp = buildHtml(): 82 | input(setFocus = true) 83 | var descInp = buildHtml(): 84 | input() 85 | 86 | proc close = 87 | result.e.style.display = "none" 88 | 89 | proc submit = 90 | let t = createTask(result.c, nameInp.value, descInp.value) 91 | close() 92 | result.c.attachedTo[0].add renderTask(t) 93 | 94 | result.e = buildHtml(): 95 | tdiv(style={display: "none", position: "fixed", 96 | left: "0", top: "0", width: "100%", height: "100%", 97 | overflow: "auto", 98 | backgroundColor: "rgb(0,0,0)", 99 | backgroundColor: "rgba(0,0,0,0.4)", zIndex: "1"}): 100 | tdiv(style={backgroundColor: "#fefefe", 101 | margin: "15% auto", padding: "20px", border: "1px solid #888", 102 | width: "80%"}): 103 | span(onclick = close, style={color: "#aaa", 104 | cssFloat: "right", 105 | fontSize: "28px", 106 | fontWeight: "bold"}): 107 | text cross 108 | p: 109 | text "Task name" 110 | nameInp 111 | p: 112 | text "Task description" 113 | descInp 114 | span(onclick = submit, style={color: "#0f0", 115 | cssFloat: "left", 116 | fontWeight: "bold"}): 117 | text "Submit" 118 | 119 | var dialog = newTaskDialog() 120 | 121 | proc renderColumn(c: Column): Element = 122 | proc doadd(c: Column): proc () = 123 | result = proc() = dialog.open(c) 124 | 125 | proc allowDrop(ev: Event) = ev.preventDefault() 126 | proc drop(ev: Event) = 127 | ev.preventDefault() 128 | let data = ev.recvDragData("taskid") 129 | moveTask(parseInt data, c) 130 | Element(ev.target).up("mycolumn").add(getElementById(data)) 131 | 132 | result = buildHtml(): 133 | tdiv(class = "mycolumn", style = {cssFloat: "left", width: "20%"}, 134 | ondrop = drop, ondragover = allowDrop): 135 | tdiv(class = "myheader"): 136 | bindField c.header 137 | span(onclick = doadd(c)): 138 | text plus 139 | for t in c.tasks: 140 | renderTask(t) 141 | c.attachedTo.add result 142 | 143 | proc renderBoard(b: Board): Element = 144 | result = buildHtml(tdiv): 145 | dialog.e 146 | for c in b.columns: 147 | renderColumn(c) 148 | 149 | proc main(hashPart: kstring): Element = 150 | result = renderBoard(b) 151 | 152 | setInitializer main 153 | -------------------------------------------------------------------------------- /experiments/trello/widgets.nim: -------------------------------------------------------------------------------- 1 | ## Common widget implemenations 2 | 3 | import knete 4 | import std / dom 5 | 6 | const 7 | cross* = kstring"x" 8 | plus* = kstring"+" 9 | 10 | const 11 | LEFT* = 37 12 | UP* = 38 13 | RIGHT* = 39 14 | DOWN* = 40 15 | TAB* = 9 16 | ESC* = 27 17 | ENTER* = 13 18 | 19 | proc editable*(x: kstring; onchanged: proc (value: kstring); 20 | isEdited = false): Element = 21 | proc onenter(ev: Event) = Element(ev.target).blur() 22 | proc submit(ev: Event) = 23 | onchanged(ev.target.value) 24 | # close the loop: This is a common pattern in Knete. 25 | replace(result, editable(ev.target.value, onchanged, false)) 26 | 27 | proc makeEditable() = 28 | # close the loop: This is a common pattern in Knete. 29 | replace(result, editable(x, onchanged, true)) 30 | 31 | result = buildHtml(): 32 | if isEdited: 33 | input(class = "edit", 34 | onblur = submit, 35 | onkeyupenter = onenter, 36 | value = x, 37 | setFocus = true) 38 | else: 39 | span(onclick = makeEditable): 40 | text x 41 | 42 | template bindField*(field): Element = 43 | editable field, proc (value: kstring) = 44 | field = value 45 | -------------------------------------------------------------------------------- /guide/Installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | First, make sure that you have Nim installed. 4 | Then, run `nimble install karax` to install Karax. 5 | 6 | Afterwards, run `karun` to make sure that you also have Karax's build tool installed. 7 | Karax uses Nim's JS backend to compile, so most modern browsers should be supported. 8 | Ready to learn more? Head over to the Introduction. 9 | -------------------------------------------------------------------------------- /guide/Introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## What is Karax? 4 | Karax is a framework for developing SPAs (single page applications) using Nim. 5 | It is designed to be easy to use and fast, by using a virtual DOM model similar to React. 6 | 7 | ## Getting Started 8 | > We assume that the reader has knowledge of basic HTML, CSS, and Nim. 9 | > Knowledge of Javascript (specifically events) is reccomended, but not required. 10 | 11 | We'll be using `karun` for most of this guide, although it is possible to compile Karax applications using `nim js` 12 | 13 | Here's a simple example: 14 | ```nim 15 | include karax/prelude # imports many of the basic Karax modules 16 | 17 | proc createDom(): VNode = # define a function to return our HTML nodes 18 | buildHtml(p): # create a paragraph element 19 | text "Welcome to Karax!" # set the text inside of the paragraph element 20 | 21 | setRenderer createDom # tell Karax to use function to render 22 | ``` 23 | 24 | Save this file as `ìndex.nim`. Then, run 25 | ``` 26 | karun -r index.nim 27 | ``` 28 | This should compile the file using the `js` backend from Nim and open the file in your default browser! 29 | Note that you can also pass in the `-w` flag to make it so that whenever you save the `ìndex.nim` file, it will automatically rebuild and refresh the page. 30 | 31 | The syntax here shouldn't be too confusing. 32 | Karax comes with a built in DSL (domain specific language) to aid in generating HTML nodes. 33 | 34 | If you want to bind to an HTML attribute, you can do the following: 35 | ```nim 36 | include karax/prelude 37 | 38 | proc createDom(): VNode = 39 | buildHtml(p(title = "Here is some help text!")): 40 | text "Hover over me to see the help text!" 41 | 42 | setRenderer createDom 43 | ``` 44 | Pretty simple, right? You can specify any HTML attribute that you want here. 45 | 46 | ### Conditionals and Loops 47 | It's simple to toggle whether an element exists as well. 48 | 49 | ```nim 50 | include karax/prelude 51 | var show = true 52 | proc createDom(): VNode = 53 | buildHtml(tdiv): 54 | if show: 55 | p: 56 | text "Now you see me!" 57 | else: 58 | p: 59 | text "Now you dont!" 60 | 61 | setRenderer createDom 62 | ``` 63 | Go ahead and try running this. Change `show` to `false`, and you should see the text change as well! 64 | Standard Nim if statements work just fine and are handled by the DSL. 65 | 66 | There is one thing that is unfamiliar here though: What is the `tdiv` tag? 67 | Since Nim has `div` as a reserved keyword, Karax defines tdiv to refer to the `div` element. 68 | You can see a list of all of the tags along with their mapping at the top of this file: [vdom.nim](https://github.com/karaxnim/karax/blob/master/karax/vdom.nim) 69 | 70 | How do we display a list of elements in Karax? As you might expect, using a Nim for loop. 71 | 72 | ```nim 73 | include karax/prelude 74 | var list = @[kstring"Apples", "Oranges", "Bananas"] 75 | proc createDom(): VNode = 76 | buildHtml(tdiv): 77 | for fruit in list: 78 | p: 79 | text fruit 80 | text " is a fruit" 81 | 82 | setRenderer createDom 83 | ``` 84 | It just works! Note that we used multiple text functions to add more text together. 85 | It is also possible to do this using string concatenation or interpolation, but this is simpler. 86 | 87 | #### `kstring` vs `string` vs `cstring` 88 | As you may have noticed above, we made `list` into a seq[kstring] rather than a seq[string]. 89 | If you've ever worked with Nim's `js` backend, you would know that Javascript strings are different from Nim strings. 90 | Nim uses the `cstring` type to denote a "compatible string". 91 | In our case, this corresponds to the native Javascript string type. 92 | In fact, if you try using Nim's string type on the Javascript backend, you'll get something like: 93 | ``` 94 | [45, 67, 85, 34, ...] 95 | ``` 96 | This is how Nim strings are handle internally - a sequence of numbers. 97 | We use `cstring` to avoid taking a performance penalty when working with strings, as the native string type is faster than a list of numbers. 98 | 99 | What is `kstring` then? `kstring` corresponds to a `cstring` when compiled using the `js` backend, and a `string` otherwise. 100 | This makes it much easier to write code that can be used on multiple platforms. 101 | 102 | ### Handling User Input 103 | Karax allows you to simply utilize existing DOM events to handle user input. 104 | 105 | ```nim 106 | include karax/prelude 107 | import algorithm 108 | var message = "Karax is fun!" 109 | proc createDom(): VNode = 110 | buildHtml(tdiv): 111 | p: 112 | text message 113 | button: 114 | text "Click me to reverse!" 115 | proc onclick = 116 | message.reverse() 117 | 118 | setRenderer createDom 119 | ``` 120 | Clicking that button causes the onclick event to fire, which reverses our string. 121 | Note that we treat `message` as a string - that way we can reverse the string using Nim's `algorithm` module. 122 | 123 | Karax can work with text inputs as well! 124 | 125 | ```nim 126 | include karax/prelude 127 | var message = kstring"Karax is fun!" 128 | proc createDom(): VNode = 129 | buildHtml(tdiv): 130 | p: 131 | text message 132 | input(value = message): 133 | proc oninput(e: Event, n: VNode) = 134 | message = n.value 135 | 136 | setRenderer createDom 137 | ``` 138 | No manual DOM manipulation required! 139 | Just change the variable and everything magically updates. 140 | 141 | You may notice that this time our `proc` has two arguments. 142 | You can see the Karax documentation for information on VNode, but the `Event` type is from the `dom` module in the stdlib. 143 | It is defined similarly to a DOM event in Javascript. 144 | 145 | ### Composing and Reusability 146 | Unlike other web frameworks, Karax doesn't have explicit components. 147 | Instead, it gives you the freedom to organize your code how you want. 148 | 149 | So, to mimic what components do we can just use functions. 150 | 151 | ```nim 152 | include karax/prelude 153 | type Task = ref object 154 | id: int 155 | text: kstring 156 | var tasks = @[ 157 | Task(id: 0, text: "Buy milk"), 158 | Task(id: 1, text: "Clean table"), 159 | Task(id: 2, text: "Call mom") 160 | ] 161 | proc render(t: Task): VNode = 162 | buildHtml(li): 163 | text t.text 164 | 165 | proc createDom(): VNode = 166 | buildHtml(tdiv): 167 | ol: 168 | for task in tasks: 169 | task.render() 170 | 171 | setRenderer createDom 172 | ``` 173 | With this, we can easily divide up parts of a more complex application. 174 | Function arguments can take the place of "props", and we're not constrained by "components" in any way. 175 | 176 | We've briefly covered many of the features that Karax offers. 177 | Interested in more? Continue reading for more advanced topics! 178 | -------------------------------------------------------------------------------- /karax.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "1.5.0" 4 | author = "Andreas Rumpf" 5 | description = "Karax is a framework for developing single page applications in Nim." 6 | license = "MIT" 7 | 8 | # Dependencies 9 | 10 | requires "nim >= 0.18.0" 11 | requires "ws" 12 | requires "dotenv >= 2.0.0" 13 | skipDirs = @["examples", "experiments", "tests"] 14 | 15 | bin = @["karax/tools/karun"] 16 | installExt = @["nim"] 17 | -------------------------------------------------------------------------------- /karax/autocomplete.nim: -------------------------------------------------------------------------------- 1 | 2 | import karax, karaxdsl, vdom, kdom, jstrutils 3 | 4 | type 5 | AutocompleteState* = ref object 6 | choices, candidates: seq[cstring] 7 | selected, maxMatches: int 8 | showCandidates, controlPressed: bool 9 | 10 | proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState = 11 | ## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum 12 | ## number of elements to show. 13 | AutocompleteState(choices: choices, candidates: @[], 14 | selected: -1, maxMatches: maxMatches, showCandidates: false, 15 | controlPressed: false) 16 | 17 | proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode = 18 | var inp: VNode 19 | 20 | proc commit(ev: Event) = 21 | s.showCandidates = false 22 | onselection(inp.dom.value) 23 | when false: 24 | if inp.dom != nil: 25 | echo "setting to A ", inp.dom.value.isNil 26 | result.text = inp.dom.value 27 | else: 28 | echo "setting to B ", inp.text.isNil 29 | result.text = inp.text 30 | for e in result.events: 31 | if e[0] == EventKind.onchange: 32 | e[1](ev, result) 33 | 34 | proc onkeyuplater(ev: Event; n: VNode) = 35 | if not s.controlPressed: 36 | let v = n.value 37 | s.candidates.setLen 0 38 | for c in s.choices: 39 | if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c) 40 | 41 | proc onkeydown(ev: Event; n: VNode) = 42 | const 43 | LEFT = 37 44 | UP = 38 45 | RIGHT = 39 46 | DOWN = 40 47 | TAB = 9 48 | ESC = 27 49 | ENTER = 13 50 | # UP: Move focus to the previous item. If on first item, move focus to the input. 51 | # If on the input, move focus to last item. 52 | # DOWN: Move focus to the next item. If on last item, move focus to the input. 53 | # If on the input, move focus to the first item. 54 | # ESCAPE: Close the menu. 55 | # ENTER: Select the currently focused item and close the menu. 56 | # TAB: Select the currently focused item, close the menu, and 57 | # move focus to the next focusable element 58 | s.controlPressed = false 59 | case ev.keyCode 60 | of UP: 61 | s.controlPressed = true 62 | s.showCandidates = true 63 | if s.selected > 0: 64 | dec s.selected 65 | n.setInputText s.candidates[s.selected] 66 | of DOWN: 67 | s.controlPressed = true 68 | s.showCandidates = true 69 | if s.selected < s.candidates.len - 1: 70 | inc s.selected 71 | n.setInputText s.candidates[s.selected] 72 | of ESC: 73 | s.showCandidates = false 74 | s.controlPressed = true 75 | of ENTER: 76 | s.controlPressed = true 77 | # inp.setInputText s.choices[i] 78 | commit(ev) 79 | else: 80 | discard 81 | 82 | proc window(s: AutocompleteState): (int, int) = 83 | var first, last: int 84 | if s.selected >= 0: 85 | first = s.selected - (s.maxMatches div 2) 86 | last = s.selected + (s.maxMatches div 2) 87 | if first < 0: first = 0 88 | # too few because we had to trim first? 89 | if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1 90 | else: 91 | first = 0 92 | last = s.maxMatches - 1 93 | if last > high(s.candidates): last = high(s.candidates) 94 | # still too few because we're at the end? 95 | if (last - first + 1) < s.maxMatches: 96 | first = last - s.maxMatches + 1 97 | if first < 0: first = 0 98 | 99 | result = (first, last) 100 | 101 | inp = buildHtml: 102 | input(onkeyuplater = onkeyuplater, 103 | onkeydown = onkeydown, 104 | # onblur = proc (ev: Event; n: VNode) = commit(ev), 105 | onfocus = proc (ev: Event; n: VNode) = 106 | onkeyuplater(ev, n) 107 | s.showCandidates = true) 108 | 109 | proc select(i: int): proc(ev: Event; n: VNode) = 110 | result = proc(ev: Event; n: VNode) = 111 | s.selected = i 112 | s.showCandidates = false 113 | inp.setInputText s.choices[i] 114 | commit(ev) 115 | 116 | result = buildHtml(table): 117 | tr: 118 | td: 119 | inp 120 | if s.showCandidates: 121 | let (first, last) = window(s) 122 | for i in first..last: 123 | tr: 124 | td(onclick = select(i), class = cstring"button " & 125 | (if i == s.selected: cstring"is-primary" else: "")): 126 | text s.candidates[i] 127 | 128 | when isMainModule: 129 | const suggestions = @[cstring"ActionScript", 130 | "AppleScript", 131 | "Asp", 132 | "BASIC", 133 | "C", 134 | "C++", 135 | "Clojure", 136 | "COBOL", 137 | "Erlang", 138 | "Fortran", 139 | "Groovy", 140 | "Haskell", 141 | "Java", 142 | "JavaScript", 143 | "Lisp", 144 | "Nim", 145 | "Perl", 146 | "PHP", 147 | "Python", 148 | "Ruby", 149 | "Scala", 150 | "Scheme"] 151 | 152 | var s = newAutocomplete(suggestions) 153 | 154 | proc main(): VNode = 155 | result = buildHtml(tdiv): 156 | autocomplete(s, proc (s: cstring) = echo "now ", s)#: 157 | # proc onchange(ev: Event; n: VNode) = 158 | # echo "now selected ", n.kind 159 | 160 | setRenderer(main) 161 | -------------------------------------------------------------------------------- /karax/compact.nim: -------------------------------------------------------------------------------- 1 | ## Components in Karax are built by the ``.component`` macro annotation. 2 | 3 | when defined(js): 4 | import jdict, kdom 5 | 6 | import macros, vdom, tables, strutils, kbase 7 | 8 | when defined(js): 9 | var 10 | vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]() 11 | else: 12 | var 13 | vcomponents* = newTable[kstring, proc(args: seq[VNode]): VNode]() 14 | 15 | type 16 | ComponentKind* {.pure.} = enum 17 | None, 18 | Tag, 19 | VNode 20 | 21 | var 22 | allcomponents {.compileTime.} = initTable[string, ComponentKind]() 23 | 24 | proc isComponent*(x: string): ComponentKind {.compileTime.} = 25 | allcomponents.getOrDefault(x) 26 | 27 | proc addTags() {.compileTime.} = 28 | let x = (bindSym"VNodeKind").getTypeImpl 29 | expectKind(x, nnkEnumTy) 30 | for i in ord(VNodeKind.html)..ord(VNodeKind.high): 31 | # +1 because of empty node at the start of the enum AST: 32 | let tag = $x[i+1] 33 | allcomponents[tag] = ComponentKind.Tag 34 | 35 | static: 36 | addTags() 37 | 38 | proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} = 39 | #let t = symbolicType.getTypeImpl 40 | let t = repr(symbolicType) 41 | case t 42 | of "cstring": 43 | result = quote do: 44 | args[`index`].text 45 | of "int", "VKey": 46 | result = quote do: 47 | args[`index`].intValue 48 | of "bool": 49 | result = quote do: 50 | args[`index`].intValue != 0 51 | elif t.endsWith"Kind": 52 | result = quote do: 53 | `symbolicType`(args[`index`].intValue) 54 | else: 55 | # just pass it along, maybe there is some conversion for it: 56 | result = quote do: 57 | args[`index`] 58 | 59 | proc newname*(n: NimNode): NimNode = 60 | if n.kind == nnkPostfix: 61 | n[1] = newname(n[1]) 62 | result = n 63 | elif n.kind == nnkSym: 64 | result = ident(n.strVal) 65 | else: 66 | result = n 67 | 68 | when defined(js): 69 | macro compact*(prc: untyped): untyped = 70 | ## A 'compact' tree generation proc is one that only depends on its 71 | ## inputs and should be stored as a compact virtual DOM tree and 72 | ## only expanded on demand (when its inputs changed). 73 | var n = prc.copyNimNode 74 | for i in 0..6: n.add prc[i].copyNimTree 75 | expectKind(n, nnkProcDef) 76 | if n[0].kind == nnkEmpty: 77 | error("please pass a non anonymous proc", n[0]) 78 | let name = n[0] 79 | let params = params(n) 80 | let rettype = repr params[0] 81 | var isvirtual = ComponentKind.None 82 | if rettype == "VNode": 83 | isvirtual = ComponentKind.VNode 84 | else: 85 | error "component must return VNode", params[0] 86 | let realName = if name.kind == nnkPostfix: name[1] else: name 87 | let nn = $realName 88 | n[0] = ident("inner" & nn) 89 | var unpackCall = newCall(n[0]) 90 | var counter = 0 91 | for i in 1 ..< params.len: 92 | let param = params[i] 93 | let L = param.len 94 | let typ = param[L-2] 95 | for j in 0 .. L-3: 96 | unpackCall.add unpack(typ, counter) 97 | inc counter 98 | 99 | template vwrapper(pname, unpackCall) {.dirty.} = 100 | proc pname(args: seq[VNode]): VNode = 101 | unpackCall 102 | 103 | template vregister(key, val) = 104 | bind jdict.`[]=` 105 | `[]=`(vcomponents, kstring(key), val) 106 | 107 | result = newTree(nnkStmtList, n) 108 | 109 | if isvirtual == ComponentKind.VNode: 110 | result.add getAst(vwrapper(newname name, unpackCall)) 111 | result.add getAst(vregister(newLit(nn), realName)) 112 | allcomponents[nn] = isvirtual 113 | when defined(debugKaraxDsl): 114 | echo repr result 115 | -------------------------------------------------------------------------------- /karax/errors.nim: -------------------------------------------------------------------------------- 1 | ## Error handling logic for form input validation. 2 | 3 | import karax, jdict 4 | 5 | var 6 | gerrorMsgs = newJDict[cstring, cstring]() 7 | gerrorCounter = 0 8 | 9 | proc hasErrors*(): bool = gerrorCounter != 0 10 | 11 | proc hasError*(field: cstring): bool = gerrorMsgs.contains(field) and len(gerrorMsgs[field]) > 0 12 | 13 | proc disableOnError*(): cstring = toDisabled(hasErrors()) 14 | 15 | proc getError*(field: cstring): cstring = 16 | if not gerrorMsgs.contains(field): 17 | result = "" 18 | else: 19 | result = gerrorMsgs[field] 20 | 21 | proc setError*(field, msg: cstring) = 22 | let previous = getError(field) 23 | if len(msg) == 0: 24 | if len(previous) != 0: dec(gerrorCounter) 25 | else: 26 | if len(previous) == 0: inc(gerrorCounter) 27 | gerrorMsgs[field] = msg 28 | 29 | proc clearErrors*() = 30 | let m = gerrorMsgs 31 | for k in m.keys: 32 | setError(k, "") 33 | -------------------------------------------------------------------------------- /karax/i18n.nim: -------------------------------------------------------------------------------- 1 | ## Karax -- Single page applications for Nim. 2 | 3 | ## i18n support for Karax applications. We distinguish between: 4 | ## 5 | ## - translate: Lookup text translations 6 | ## - localize: Localize Date and Time objects to local formats 7 | ## 8 | ## Localization has not yet been implemented. 9 | 10 | import languages, jdict, jstrutils 11 | 12 | var currentLanguage = Language.enUS 13 | 14 | proc getLanguage(): cstring = 15 | {.emit: "`result` = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;".} 16 | 17 | proc detectLanguage*(): Language = 18 | let x = getLanguage() 19 | for i in low(Language)..high(Language): 20 | if languageToCode[i] == x: return i 21 | return Language.enUS 22 | 23 | proc setCurrentLanguage*(newLanguage = detectLanguage()) = 24 | currentLanguage = newLanguage 25 | 26 | proc getCurrentLanguage*(): Language = currentLanguage 27 | 28 | type Translation* = JDict[cstring, cstring] 29 | 30 | var 31 | translations: array[Language, Translation] 32 | 33 | proc registerTranslation*(lang: Language; t: Translation) = translations[lang] = t 34 | 35 | proc addTranslation*(lang: Language; key, val: cstring) = 36 | if translations[lang].isNil: translations[lang] = newJDict[cstring, cstring]() 37 | translations[lang][key] = val 38 | 39 | proc translate(x: cstring): cstring = 40 | let y = translations[currentLanguage] 41 | if y != nil and y.contains(x): 42 | result = y[x] 43 | else: 44 | result = x 45 | 46 | type TranslatedString* = distinct cstring 47 | 48 | template i18n*(x: string): TranslatedString = TranslatedString(translate(cstring x)) 49 | 50 | proc raiseInvalidFormat(errmsg: string) = 51 | raise newException(ValueError, errmsg) 52 | 53 | discard """ 54 | $[1:item|items]1$ selected. 55 | """ 56 | 57 | proc `{}`(x: cstring; i: int): char {.importcpp: "#.charCodeAt(#)".} 58 | proc sadd(s: JSeq[char]; c: char) {.importcpp: "#.push(String.fromCharCode(#))".} 59 | 60 | proc parseChoice(f: cstring; i, choice: int, 61 | r: JSeq[char]) = 62 | var i = i 63 | while i < f.len: 64 | var n = 0 65 | let oldI = i 66 | var toAdd = false 67 | while i < f.len and f{i} >= '0' and f{i} <= '9': 68 | n = n * 10 + ord(f{i}) - ord('0') 69 | inc i 70 | if oldI != i: 71 | if f{i} == ':': 72 | inc i 73 | else: 74 | raiseInvalidFormat"':' after number expected" 75 | toAdd = choice == n 76 | else: 77 | # an else section does not start with a number: 78 | toAdd = true 79 | while i < f.len and f{i} != ']' and f{i} != '|': 80 | if toAdd: r.sadd f{i} 81 | inc i 82 | if toAdd: break 83 | inc i 84 | 85 | proc join(x: JSeq[char]): cstring {.importcpp: "#.join(\"\")".} 86 | proc add(x: JSeq[char]; y: cstring) = 87 | for i in 0..= '0' and f{i} <= '9': 107 | j = j * 10 + ord(f{i}) - ord('0') 108 | inc i 109 | let idx = if not negative: j-1 else: args.len-j 110 | r.add args[idx] 111 | of '$': 112 | inc(i) 113 | r.add '$' 114 | of '[': 115 | let start = i+1 116 | while i < f.len and f{i} != ']': inc i 117 | inc i 118 | if i >= f.len: raiseInvalidFormat"']' expected" 119 | case f{i} 120 | of '#': 121 | parseChoice(f, start, parseInt args[num], r) 122 | inc i 123 | inc num 124 | of '1'..'9', '-': 125 | var j = 0 126 | var negative = f{i} == '-' 127 | if negative: inc i 128 | while f{i} >= '0' and f{i} <= '9': 129 | j = j * 10 + ord(f{i}) - ord('0') 130 | inc i 131 | let idx = if not negative: j-1 else: args.len-j 132 | parseChoice(f, start, parseInt args[idx], r) 133 | else: raiseInvalidFormat"argument index expected after ']'" 134 | else: 135 | raiseInvalidFormat("'#', '$', or number expected") 136 | if i < f.len and f{i} == '$': inc i 137 | else: 138 | r.sadd f{i} 139 | inc i 140 | result = join(r) 141 | 142 | 143 | when isMainModule: 144 | addTranslation(Language.deDE, "$#$ -> $#$", "$2 macht $1") 145 | setCurrentLanguage(Language.deDE) 146 | 147 | echo(i18n"$#$ -> $#$" % [cstring"1", "2"]) 148 | echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"1", "2"]) 149 | echo(i18n"$[1:item |items ]1 -> $1" % [cstring"0", "2"]) 150 | echo(i18n"$[1:item |items ]1 -> $1" % [cstring"2", "2"]) 151 | echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"3", "2"]) 152 | echo(i18n"$1 $[1:item |4:huha |items ]1$ -> $1" % [cstring"4", "2"]) 153 | -------------------------------------------------------------------------------- /karax/jdict.nim: -------------------------------------------------------------------------------- 1 | 2 | type 3 | JDict*[K, V] = ref object 4 | 5 | proc `[]`*[K, V](d: JDict[K, V], k: K): V {.importcpp: "#[#]".} 6 | proc `[]=`*[K, V](d: JDict[K, V], k: K, v: V) {.importcpp: "#[#] = #".} 7 | 8 | proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".} 9 | 10 | proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".} 11 | 12 | proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".} 13 | 14 | iterator keys*[K, V](d: JDict[K, V]): K = 15 | var kkk: K 16 | {.emit: ["for (", kkk, " in ", d, ") {"].} 17 | yield kkk 18 | {.emit: ["}"].} 19 | 20 | type 21 | JSeq*[T] = ref object 22 | 23 | proc `[]`*[T](s: JSeq[T], i: int): T {.importcpp: "#[#]", noSideEffect.} 24 | proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.} 25 | 26 | proc newJSeq*[T](len: int = 0): JSeq[T] {.importcpp: "new Array(#)".} 27 | proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.} 28 | proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.} 29 | 30 | proc shrink*[T](s: JSeq[T]; shorterLen: int) {.importcpp: "#.length = #", noSideEffect.} 31 | -------------------------------------------------------------------------------- /karax/jjson.nim: -------------------------------------------------------------------------------- 1 | ## This module implements some small zero-overhead 'JsonNode' type 2 | ## and helper that maps directly to JavaScript objects. 3 | 4 | type 5 | JsonNode* {.importc.} = ref object 6 | 7 | proc `[]`*(obj: JsonNode; fieldname: cstring): JsonNode {.importcpp: "#[#]".} 8 | proc `[]`*(obj: JsonNode; index: int): JsonNode {.importcpp: "#[#]".} 9 | proc `[]=`*[T](obj: JsonNode; fieldname: cstring; value: T) 10 | {.importcpp: "#[#] = #".} 11 | proc length(x: JsonNode): int {.importcpp: "#.length".} 12 | proc len*(x: JsonNode): int = (if x.isNil: 0 else: x.length) 13 | 14 | proc parse*(input: cstring): JsonNode {.importcpp: "JSON.parse(#)".} 15 | proc hasField*(obj: JsonNode; fieldname: cstring): bool {.importcpp: "#[#] !== undefined".} 16 | 17 | proc newJsonNode*(fields: varargs[(cstring, JsonNode)]): JsonNode = 18 | result = JsonNode() 19 | for f in fields: 20 | result[f[0]] = f[1] 21 | 22 | proc newJObject*(): JsonNode = 23 | result = JsonNode() 24 | 25 | proc newJArray*(elements: varargs[JsonNode]): JsonNode {.importcpp: "#".} 26 | 27 | proc newJNull*(): JsonNode = nil 28 | 29 | template `%`*(x: typed): JsonNode = cast[JsonNode](x) 30 | template `%`*(x: string): JsonNode = cast[JsonNode](cstring x) 31 | 32 | proc getNum*(x: JsonNode): int {.importcpp: "#".} 33 | 34 | proc getInt*(x: JsonNode): int {.importcpp: "#".} 35 | proc getStr*(x: JsonNode): cstring {.importcpp: "#".} 36 | proc getFNum*(x: JsonNode): cstring {.importcpp: "#".} 37 | proc getBool*(x: JsonNode): bool {.importcpp: "#".} 38 | 39 | iterator items*(x: JsonNode): JsonNode = 40 | for i in 0..=0)", nodecl.} 11 | 12 | proc containsIgnoreCase*(a, b: cstring): bool {. 13 | importcpp: """(#.search(new RegExp(#.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$$\|]/g, "\\$$&") , "i"))>=0)""", nodecl.} 14 | 15 | proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.} 16 | proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.} 17 | 18 | #proc len*(s: cstring): int {.importcpp: "#.length", nodecl.} 19 | proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.} 20 | proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} 21 | proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} 22 | proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.} 23 | proc `&`*(s: float): cstring {.importcpp: "((#)+'')", nodecl.} 24 | 25 | proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.} 26 | 27 | proc isInt*(s: cstring): bool {.asmNoStackFrame.} = 28 | asm """ 29 | return `s`.match(/^[0-9]+$/); 30 | """ 31 | 32 | proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.} 33 | proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.} 34 | 35 | proc join*(a: openArray[cstring], sep = cstring""): cstring {.importcpp: "(#.join(#))", nodecl.} 36 | -------------------------------------------------------------------------------- /karax/jwebsockets.nim: -------------------------------------------------------------------------------- 1 | ## Websockets support for the JSON backend. 2 | 3 | type 4 | MessageEvent* {.importc.} = ref object 5 | data*: cstring 6 | 7 | WebSocket* {.importc.} = ref object 8 | onmessage*: proc (e: MessageEvent) 9 | onopen*: proc (e: MessageEvent) 10 | 11 | proc newWebSocket*(url, key: cstring): WebSocket 12 | {.importcpp: "new WebSocket(@)".} 13 | 14 | proc newWebSocket*(url: cstring): WebSocket 15 | {.importcpp: "new WebSocket(@)".} 16 | 17 | proc send*(w: WebSocket; data: cstring) {.importcpp.} 18 | -------------------------------------------------------------------------------- /karax/kajax.nim: -------------------------------------------------------------------------------- 1 | ## Karax -- Single page applications for Nim. 2 | 3 | ## This module implements support for `ajax`:idx: socket 4 | ## handling. 5 | 6 | import karax 7 | import jsffi except `&` 8 | import jscore 9 | import dom 10 | 11 | type 12 | ProgressEvent* {.importc.}= object 13 | loaded*: float 14 | total*: float 15 | 16 | FormData* {.importc.} = JsObject 17 | 18 | HttpRequest* {.importc.} = ref object 19 | readyState, status: int 20 | responseText, statusText: cstring 21 | 22 | XMLHttpRequestUpload* {.importc.} = JsObject 23 | 24 | proc newFormData*(): FormData {.importcpp: "new FormData()".} 25 | proc append*(f: FormData, key: cstring, value: Blob) {.importcpp:"#.append(@)".} 26 | proc append*(f: FormData, key: cstring, value: cstring) {.importcpp:"#.append(@)".} 27 | 28 | proc setRequestHeader*(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".} 29 | proc statechange*(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".} 30 | proc send*(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".} 31 | proc send*(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".} 32 | proc open*(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".} 33 | proc newRequest*(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".} 34 | 35 | when not declared(dom.File): 36 | type 37 | DomFile = ref FileObj 38 | FileObj {.importc.} = object of Blob 39 | lastModified: int 40 | name: cstring 41 | else: 42 | type 43 | DomFile = dom.File 44 | 45 | proc uploadFile*(url: cstring, file: Blob, onprogress :proc(data: ProgressEvent), 46 | cont: proc (httpStatus: int; response: cstring); 47 | headers: openarray[(cstring, cstring)] = []) = 48 | proc contWrapper(httpStatus: int; response: cstring) = 49 | cont(httpStatus, response) 50 | 51 | proc upload(r: HttpRequest):XMLHttpRequestUpload {.importcpp: "#.upload".} 52 | 53 | var formData = newFormData() 54 | formData.append("upload_file",file) 55 | formData.append("filename", DomFile(file).name) 56 | let ajax = newRequest() 57 | ajax.open("POST", url, true) 58 | for a, b in items(headers): 59 | ajax.setRequestHeader(a, b) 60 | ajax.statechange proc() = 61 | if ajax.readyState == 4: 62 | contWrapper(ajax.status, ajax.responseText) 63 | ajax.upload.onprogress = onprogress 64 | ajax.send(formData.to(cstring)) 65 | 66 | proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)]; 67 | data: cstring; 68 | cont: proc (httpStatus: int; response: cstring); 69 | doRedraw: bool = true, 70 | kxi: KaraxInstance = kxi, 71 | useBinary: bool = false, 72 | blob: Blob = nil) = 73 | proc contWrapper(httpStatus: int; response: cstring) = 74 | cont(httpStatus, response) 75 | if doRedraw: redraw(kxi) 76 | 77 | 78 | let ajax = newRequest() 79 | ajax.open(meth, url, true) 80 | for a, b in items(headers): 81 | ajax.setRequestHeader(a, b) 82 | ajax.statechange proc() = 83 | if ajax.readyState == 4: 84 | if ajax.status == 200: 85 | contWrapper(ajax.status, ajax.responseText) 86 | else: 87 | contWrapper(ajax.status, ajax.responseText) 88 | if useBinary: 89 | ajax.send(blob) 90 | else: 91 | ajax.send(data) 92 | 93 | proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; 94 | data: cstring; 95 | cont: proc (httpStatus: int, response: cstring); 96 | doRedraw: bool = true, 97 | kxi: KaraxInstance = kxi) = 98 | ajax("POST", url, headers, data, cont, doRedraw, kxi) 99 | 100 | proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; 101 | data: Blob; 102 | cont: proc (httpStatus: int, response: cstring); 103 | doRedraw: bool = true, 104 | kxi: KaraxInstance = kxi) = 105 | ajax("POST", url, headers, "", cont, doRedraw, kxi, true, data) 106 | 107 | proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)]; 108 | cont: proc (httpStatus: int, response: cstring); 109 | doRedraw: bool = true, 110 | kxi: KaraxInstance = kxi) = 111 | ajax("GET", url, headers, nil, cont, doRedraw, kxi) 112 | 113 | proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)]; 114 | data: cstring; 115 | cont: proc (httpStatus: int, response: cstring); 116 | doRedraw: bool = true, 117 | kxi: KaraxInstance = kxi) = 118 | ajax("PUT", url, headers, data, cont, doRedraw, kxi) 119 | 120 | proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)]; 121 | cont: proc (httpStatus: int, response: cstring); 122 | doRedraw: bool = true, 123 | kxi: KaraxInstance = kxi) = 124 | ajax("DELETE", url, headers, nil, cont, doRedraw, kxi) 125 | 126 | 127 | proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".} 128 | proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".} 129 | 130 | -------------------------------------------------------------------------------- /karax/karaxdsl.nim: -------------------------------------------------------------------------------- 1 | 2 | import macros, vdom, compact, kbase 3 | from strutils import startsWith, toLowerAscii, cmpIgnoreStyle 4 | 5 | when defined(js): 6 | import karax 7 | 8 | const 9 | StmtContext = ["kout", "inc", "echo", "dec", "!"] 10 | SpecialAttrs = ["id", "class", "value", "index", "style"] 11 | 12 | proc getName(n: NimNode): string = 13 | case n.kind 14 | of nnkIdent, nnkSym: 15 | result = $n 16 | of nnkAccQuoted: 17 | result = "" 18 | for i in 0.. 0: 50 | var hasNoRedrawPragma = false 51 | for i in 0 ..< len(anon.pragma): 52 | # using anon because anon needs to get rid of the pragma 53 | if anon.pragma[i].kind == nnkIdent and cmpIgnoreStyle(anon.pragma[i].strVal, "noredraw") == 0: 54 | hasNoRedrawPragma = true 55 | anon.pragma.del(i) 56 | break 57 | if hasNoRedrawPragma: 58 | return newCall(ident"addEventHandlerNoRedraw", tmpContext, 59 | newDotExpr(bindSym"EventKind", name), anon) 60 | result = call 61 | 62 | proc tcall2(n, tmpContext: NimNode): NimNode = 63 | # we need to distinguish statement and expression contexts: 64 | # every call statement 's' needs to be transformed to 'dest.add s'. 65 | # If expressions need to be distinguished from if statements. Since 66 | # we know we start in a statement context, it's pretty simple to 67 | # figure out expression contexts: In calls everything is an expression 68 | # (except for the last child of the macros we consider here), 69 | # lets, consts, types can be considered as expressions 70 | # case is complex, calls are assumed to produce a value. 71 | when defined(js): 72 | template evHandler(): untyped = bindSym"addEventHandler" 73 | else: 74 | template evHandler(): untyped = ident"addEventHandler" 75 | 76 | case n.kind 77 | of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr: 78 | if tmpContext != nil: 79 | result = newCall(bindSym"add", tmpContext, n) 80 | else: 81 | result = n 82 | of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr, 83 | nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse, 84 | nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple: 85 | # recurse for the last son: 86 | result = copyNimTree(n) 87 | let L = n.len 88 | assert n.len == result.len 89 | if L > 0: 90 | result[L-1] = tcall2(result[L-1], tmpContext) 91 | of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt, 92 | nnkFinally, nnkBlockStmt, nnkBlockExpr: 93 | # recurse for every child: 94 | result = copyNimNode(n) 95 | for x in n: 96 | result.add tcall2(x, tmpContext) 97 | of nnkCaseStmt: 98 | # recurse for children, but don't add call for case ident 99 | result = copyNimNode(n) 100 | result.add n[0] 101 | for i in 1 ..< n.len: 102 | result.add tcall2(n[i], tmpContext) 103 | of nnkProcDef: 104 | let name = getName n[0] 105 | if name.startsWith"on": 106 | # turn it into an anon proc: 107 | let anon = copyNimTree(n) 108 | anon[0] = newEmptyNode() 109 | if tmpContext == nil: 110 | error "no VNode to attach the event handler to" 111 | else: 112 | let call = newCall(evHandler(), tmpContext, 113 | newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi")) 114 | result = handleNoRedrawPragma(call, tmpContext, n[0], anon) 115 | else: 116 | result = n 117 | of nnkVarSection, nnkLetSection, nnkConstSection: 118 | result = n 119 | of nnkCallKinds - {nnkInfix}: 120 | let op = getName(n[0]) 121 | let ck = isComponent(op) 122 | if ck != ComponentKind.None: 123 | let tmp = genSym(nskLet, "tmp") 124 | let call = if ck == ComponentKind.Tag: 125 | newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0])) 126 | elif ck == ComponentKind.VNode: 127 | newCall(bindSym"vthunk", newLit(op)) 128 | else: 129 | newCall(bindSym"dthunk", newLit(op)) 130 | result = newTree( 131 | if tmpContext == nil: nnkStmtListExpr else: nnkStmtList, 132 | newLetStmt(tmp, call)) 133 | for i in 1 ..< n.len: 134 | # named parameters are transformed into attributes or events: 135 | let x = n[i] 136 | if x.kind == nnkExprEqExpr: 137 | let key = getName x[0] 138 | if key.startsWith("on"): 139 | result.add newCall(evHandler(), 140 | tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi")) 141 | elif eqIdent(key, "style") and x[1].kind == nnkTableConstr: 142 | result.add newDotAsgn(tmp, key, newCall("style", toKstring x[1])) 143 | elif key in SpecialAttrs: 144 | result.add newDotAsgn(tmp, key, x[1]) 145 | if key == "value": 146 | result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) 147 | elif eqIdent(key, "setFocus"): 148 | result.add newCall(key, tmp, x[1], ident"kxi") 149 | elif eqIdent(key, "events"): 150 | result.add newCall(bindSym"mergeEvents", tmp, x[1]) 151 | else: 152 | result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) 153 | elif ck != ComponentKind.Tag: 154 | call.add x 155 | elif eqIdent(x, "setFocus"): 156 | result.add newCall(x, tmp, bindSym"true", ident"kxi") 157 | else: 158 | result.add tcall2(x, tmp) 159 | if tmpContext == nil: 160 | result.add tmp 161 | else: 162 | result.add newCall(bindSym"add", tmpContext, tmp) 163 | elif tmpContext != nil and op notin StmtContext: 164 | var hasEventHandlers = false 165 | for i in 1..= 0: 205 | x.s.delete(position) 206 | x.broadcast(Mark) 207 | x.broadcast(Deleted, position) 208 | 209 | when false: 210 | proc `:=`[T](x: Reactive[T], f: proc(): T) = 211 | toTrack = wrapObserver(f()) 212 | x.value = f() 213 | toTrack = nil 214 | 215 | proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = 216 | let xl = x.L.value 217 | let res = newRSeq[U](xl) 218 | for i in 0..